diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r01/ACTUAL_INCOMPLETE_INDEX_REJECTION.json b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r01/ACTUAL_INCOMPLETE_INDEX_REJECTION.json new file mode 100644 index 0000000..c2af899 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r01/ACTUAL_INCOMPLETE_INDEX_REJECTION.json @@ -0,0 +1,20 @@ +{ + "target_commit": "e59206b9b2578ddb4a1ec023f836d413a7a8fbfb", + "actual_native_index": "artifacts/original-completion-2026-10-02/p08-02-native-mixed-r03/index.json", + "checker_rejected_actual_incomplete_evidence": true, + "deficits": [ + "OS proof: unsafe artifact path", + "collection failure", + "condition mapping actual_actor_positions", + "condition mapping combined_rescue_logistics_ev", + "condition mapping mixed_construction", + "condition mapping no_overlap_orphan_loss", + "condition mapping occupancy_change", + "condition mapping unsolvable_preservation", + "final regression: unsafe artifact path", + "missing/duplicate ON/OFF positive/refusal case" + ], + "elapsed_seconds": 0.594841375015676, + "evidence_source_modified": false, + "unit_accepted": false +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/CURRENT_CONTINUATION-r04.json b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/CURRENT_CONTINUATION-r04.json new file mode 100644 index 0000000..aea95ec --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/CURRENT_CONTINUATION-r04.json @@ -0,0 +1,58 @@ +{ + "task_id": "P08-02", + "state": "implementing", + "submitted": "e59206b9b2578ddb4a1ec023f836d413a7a8fbfb", + "repairs_retained": 3, + "individual_cap": null, + "actual_implementer_run": "c515ac662e484e8e8c90024b3c73ff53", + "actual_pid": 65613, + "commander_direction": "../unlimited-individual-limits-r01/OPUS_P08_RUN_DIRECTION.json", + "original_unit_accepted": false, + "original_denominator": { + "packages": 30, + "units": 88, + "partial_credit": false + }, + "all_conditions": [ + "mixed_construction", + "occupancy_change", + "actual_actor_positions", + "combined_rescue_logistics_ev", + "no_overlap_orphan_loss", + "unsolvable_preservation" + ], + "reuse_evidence": [ + { + "sha": "e59206b9b2578ddb4a1ec023f836d413a7a8fbfb", + "import_pid": 52821, + "parse_pid": 52931, + "passed": true, + "application": "diagnoses existing source; final changed fixture requires final native parse" + }, + { + "sha": "e59206b9b2578ddb4a1ec023f836d413a7a8fbfb", + "writer_pid": 52976, + "failed": true, + "application": "resource setup failure; no mixed migration acceptance" + }, + { + "diagnostic_pid": 55276, + "application": "missing materials10, diagnostic logging only; no acceptance" + }, + { + "application": "raw oldhistory fullSHA roundtrip Git2versions and verified gzip; reusable provenance" + } + ], + "missing": [ + "same-final-sha full ON/OFF success/refusal native writer/reader proof", + "actual normal OS input evidence", + "final regression", + "independent gameplay review", + "integration and ledger closure" + ], + "dependencies": [ + "registered catalog definition costs reused; no production economic values invented", + "private native profile/save isolation", + "normal menu interaction setup" + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/collect_with_storage_guard.py b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/collect_with_storage_guard.py new file mode 100644 index 0000000..71cd902 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/collect_with_storage_guard.py @@ -0,0 +1,37 @@ +"""Execute unchanged submitted P08 collector with storage-only closed-phase hook.""" +from pathlib import Path +import importlib.util,argparse,json,sys,hashlib +sys.dont_write_bytecode=True +parser=argparse.ArgumentParser();parser.add_argument('--commit',required=True);parser.add_argument('--output',type=Path,required=True);args=parser.parse_args() +root=Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-implement') +coordinator=Path(__file__).resolve().parent + +def load(name,path): + s=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);return m + +driver=load('submitted_p08_collector',root/'tools/run_p08_mixed_migration.py') +storage=load('closed_phase_storage',coordinator/'native_storage_guard.py') +original_module=driver.module +traces=[] + +def module(name,path): + m=original_module(name,path) + if name=='p08_standard_runner': + original=m.execute_phase + def execute(*values,**kwargs): + row=original(*values,**kwargs) + print(json.dumps({'phase':values[3],'passed':row['passed'],'actual_pid':row.get('actual_pid'),'exit':row.get('exit_code'),'error':row.get('validation_error'),'guard_error':row.get('guard_error')}),flush=True) + if row['passed']: + changes=storage.compress_closed_phase(args.output,row) + traces.append({'phase':values[3],'ended_native_pid':row.get('actual_pid'),'storage_only_changes':changes}) + print(json.dumps({'phase':values[3],'closed_proofs_preserved_and_compressed':len(changes)}),flush=True) + return row + m.execute_phase=execute + return m + +driver.module=module +native=argparse.Namespace(project_root=root,output=args.output.resolve(),commit=args.commit,godot='/Applications/Godot.app/Contents/MacOS/Godot',collect=True) +code=driver.collect(native) +record={'target_commit':args.commit,'native_exit':code,'storage_helper_sha256':hashlib.sha256((coordinator/'native_storage_guard.py').read_bytes()).hexdigest(),'collector_sha256':hashlib.sha256((root/'tools/run_p08_mixed_migration.py').read_bytes()).hexdigest(),'snapshots_or_timepoints_omitted':0,'worker_source_changed':False,'phase_traces':traces,'original_unit_accepted':False} +(coordinator/'NATIVE_STORAGE_COLLECTION_TRACE.json').write_text(json.dumps(record,ensure_ascii=False,indent=2)+'\n') +raise SystemExit(code) diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/native_storage_guard.py b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/native_storage_guard.py new file mode 100644 index 0000000..d7ae8b7 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p08-02-audit-r04/native_storage_guard.py @@ -0,0 +1,36 @@ +"""Coordinator storage only: transparently compress closed, owned phase proof files. + +No snapshot/timepoint filtering. No changes to worker code or native outputs. +Call only after execute_phase returned and actual engine_ended is true. +""" +from pathlib import Path +import hashlib,json,os,subprocess + +def compress_closed_phase(out, phase_row): + if phase_row.get('engine_ended') is not True: + raise RuntimeError('Owned native child must actually have ended') + out=Path(out).resolve() + rows=[] + candidates=[p for p in sorted((out/'pairs').rglob('*')) if p.is_file() and not p.is_symlink() and p.suffix in ('.var','.json') and p.stat().st_size>=65536 and not p.stat().st_flags&32] if (out/'pairs').exists() else [] + for offset in range(0,len(candidates),128): + probe=subprocess.run(['/usr/sbin/lsof','-t','--',*[str(p) for p in candidates[offset:offset+128]]],capture_output=True,text=True) + if probe.stdout.strip(): raise RuntimeError('Owned proof has open handles') + for p in candidates: + if not p.is_file() or p.is_symlink() or p.suffix not in ('.var','.json'): + continue + old=p.stat() + if old.st_flags & 32 or old.st_size < 65536: continue + data_sha=hashlib.sha256(p.read_bytes()).hexdigest() + temp=p.with_name(p.name+'.owned-hfs-temporary') + if temp.exists(): raise RuntimeError('Storage temporary collision') + subprocess.run(['/usr/bin/ditto','--hfsCompression','--noclone',str(p),str(temp)],check=True) + now=temp.stat() + if hashlib.sha256(temp.read_bytes()).hexdigest()!=data_sha or (now.st_size,now.st_mode,now.st_uid,now.st_gid,now.st_mtime_ns)!=(old.st_size,old.st_mode,old.st_uid,old.st_gid,old.st_mtime_ns): + raise RuntimeError('Content/metadata preservation failed') + if hashlib.sha256(p.read_bytes()).hexdigest()!=data_sha: + raise RuntimeError('Original proof changed before replacement') + os.replace(temp,p) + if hashlib.sha256(p.read_bytes()).hexdigest()!=data_sha: + raise RuntimeError('Post-replacement content mismatch') + rows.append({'path':str(p.relative_to(out)),'bytes':old.st_size,'sha256':data_sha,'physical_before':old.st_blocks*512,'physical_after':p.stat().st_blocks*512,'whole_bytes_and_mode_owner_mtime_preserved':True}) + return rows diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/ACTUAL_MODELS.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/ACTUAL_MODELS.json new file mode 100644 index 0000000..893c532 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/ACTUAL_MODELS.json @@ -0,0 +1,27 @@ +{ + "requested_models": { + "commander": "claude-opus-5-5", + "reviewer": "claude-opus-5-5", + "implementer": "gpt-6.1-sol" + }, + "commands": [ + { + "run_id": "1ae2b3b017ba4554ba5a5c5d4430eea4", + "actual_model": "claude-opus-5-5", + "record_sha256": "0461ab062ee532779755cf88478edf56b214389430d1d0dcc5b1c30dfb96ed08", + "record_path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p08-02/runs/1ae2b3b017ba4554ba5a5c5d4430eea4/command.json" + }, + { + "run_id": "1e1b149ca4c44ba5ad2bafd534e49ce1", + "actual_model": "claude-opus-5-5", + "record_sha256": "766a0d6b34de623e51e2977a5d0c912d9fde8094ba61d9d38e13d2f3ba105a12", + "record_path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p08-02/runs/1e1b149ca4c44ba5ad2bafd534e49ce1/command.json" + }, + { + "run_id": "c515ac662e484e8e8c90024b3c73ff53", + "actual_model": "gpt-6.1-sol", + "record_sha256": "dc94188ab040365f0849478568056acccf60b533958fbedb94babfe0d341aeea", + "record_path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p08-02/runs/c515ac662e484e8e8c90024b3c73ff53/command.json" + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/APPLIED.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/APPLIED.json new file mode 100644 index 0000000..42735b9 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/APPLIED.json @@ -0,0 +1,282 @@ +{ + "authorization": "direct human latest instruction", + "interpretation": "All The Tree individual repair caps and accumulated LLM invocation caps removed. Watchdog timeout and concurrent resource limits retained.", + "controls": [ + { + "control": "/private/tmp/the-tree-p24-02-control-r02", + "before_sha256": "34d0f62ec5b17b070a2d1ad4f58d06f3be7e9b6fb649ec86b366bc03b07c1329", + "after_sha256": "7229e4ade6b27abf2c67f979fc0fadeafe9c0e43aeb42736b5d8cfa1eaf30363" + }, + { + "control": "/Users/korizatou114/Antigravity/環境構築/agent-control", + "before_sha256": "34d0f62ec5b17b070a2d1ad4f58d06f3be7e9b6fb649ec86b366bc03b07c1329", + "after_sha256": "7229e4ade6b27abf2c67f979fc0fadeafe9c0e43aeb42736b5d8cfa1eaf30363" + } + ], + "tasks": [ + { + "project": "the-tree-mana-proposal", + "task": "MP-01", + "previous_cap": 2, + "cap": null, + "repairs": 0, + "state": "blocked" + }, + { + "project": "the-tree-mana-proposal-r02", + "task": "MP-01-r02", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "completed" + }, + { + "project": "the-tree-p04-01", + "task": "P04-01", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "repair_pending" + }, + { + "project": "the-tree-p06-01", + "task": "P06-01", + "previous_cap": 2, + "cap": null, + "repairs": 0, + "state": "blocked" + }, + { + "project": "the-tree-p08-02", + "task": "P08-02", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "submitted" + }, + { + "project": "the-tree-p13-03", + "task": "P13-03", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "blocked" + }, + { + "project": "the-tree-p18-01-model", + "task": "P18-01-CODE", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "repair_pending" + }, + { + "project": "the-tree-p22-02-contract", + "task": "P22-02", + "previous_cap": 3, + "cap": null, + "repairs": 3, + "state": "blocked" + }, + { + "project": "the-tree-p24-02-operations", + "task": "P24-02", + "previous_cap": 3, + "cap": null, + "repairs": 3, + "state": "submitted" + }, + { + "project": "the-tree-p24-03", + "task": "P24-03", + "previous_cap": 2, + "cap": null, + "repairs": 0, + "state": "blocked" + }, + { + "project": "the-tree-p24-03-code", + "task": "P24-03", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "completed" + }, + { + "project": "the-tree-runner-agy-execution", + "task": "RP24-02", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "repair_pending" + }, + { + "project": "the-tree-runner-p13-recovery", + "task": "RP13-01", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "repair_pending" + }, + { + "project": "the-tree-runner-p24-review-packet", + "task": "RP24-01", + "previous_cap": 2, + "cap": null, + "repairs": 2, + "state": "completed" + } + ], + "project_config_verification": [ + { + "project": "the-tree-mana-proposal", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-mana-proposal-r02", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p04-01", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p06-01", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p08-02", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p13-03", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p18-01-model", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p22-02-contract", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p24-02-operations", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p24-03", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-p24-03-code", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-runner-agy-execution", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-runner-p13-recovery", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + }, + { + "project": "the-tree-runner-p24-review-packet", + "max_repair_attempts": null, + "max_llm_runs": null, + "config_integrity_valid": true, + "timeout_seconds": 900, + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1 + } + ], + "verification": { + "local_policy_tests": 3, + "legacy_guard_tests": 3, + "runner_suite_including_new_default_test": 47, + "status": "PASS", + "actual_independent_review": "INDEPENDENT_OPUS_POLICY_REVIEW.json", + "review_scope": "operational policy only, no game unit acceptance", + "followup_completeness": "All14 stored config policy fields verified null and config integrity verified. Both control runner/schema field usages searched; only null-aware guards and defaults. New registration override tested. Counts/states test performed before P08 continued normally." + }, + "historical_controls": "Older /private/tmp/the-tree-rp* control snapshots retain historical bytes and are not execution entrypoints for this continuation; current OwnerCLI uses updated p24-02-control-r02 and plugin uses updated production Runner." +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/INDEPENDENT_OPUS_POLICY_REVIEW.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/INDEPENDENT_OPUS_POLICY_REVIEW.json new file mode 100644 index 0000000..74e54e5 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/INDEPENDENT_OPUS_POLICY_REVIEW.json @@ -0,0 +1,15 @@ +{ + "result": { + "status": "pass", + "summary": "I reviewed only the diff and the APPLIED record. I had no file or search tools in this session, so I couldn't read the rest of runner/core.py, the schemas, or the control DBs. Within the diff, the change is correct. Individual repair caps and cumulative LLM run caps are removed only when the value is null. The repairs counter still increments. The state gate (only repair_pending/blocked can be repaired), the running-run guard, LLM and implementer concurrency limits, and the schema check all stay in place. Finite caps still work: a finite contract cap with a finite policy cap is still bounded, a null contract cap with a finite policy cap is refused, bools are rejected by `type(cap) is int`, and negative caps are refused. New the-tree-* registrations get null policy caps, and their contracts are forced to null. I found no confirmed material defect. However, the record doesn't show that 'all caps removed' actually holds outside the three diffed call sites (findings 1–2), so I'm passing on code correctness, not on completeness.", + "findings": [ + "1 (completeness, unverified): The APPLIED record shows each task's contract cap set to null, but not each existing project's stored config.policy. The LLM run check reads self.config['policy']['max_llm_runs'] from each project's own config, not from the control code. If the 14 existing projects still store finite max_llm_runs / max_repair_attempts in their config, the cumulative LLM cap still applies to them. Also, tasks added later to those projects would not be forced to null, because add() only overrides when the policy repair cap is already None. Please confirm each project's config.policy shows max_llm_runs=null and max_repair_attempts=null, and that this config edit passes the integrity check that refused the policy-tamper test.", + "2 (other uses of the fields, unverified): The diff patches 3 places that read max_repair_attempts / max_llm_runs. Any other place that does arithmetic or comparisons on these fields would now hit None and raise TypeError. Examples: status/report output like f'{repairs}/{max}' or 'remaining' math, automatic moves to blocked when repairs reach the cap, review-packet generation, watchdog/recover paths, and any policy schema that types these as integer. Please run grep -n 'max_repair_attempts\\|max_llm_runs' over runner/ and control/schemas/ (both control copies) and confirm every match handles None. The six tests only cover add, repair, and run start.", + "3 (by design, note only): For the-tree-* projects, init() overwrites caps even when the caller passes an explicit finite policy, and add() rewrites the contract to null without saying so. So a The Tree project can no longer opt into a finite cap; finite caps only work for other projects. If approvals or review packets bind a hash or copy of the submitted contract, the stored contract (cap=null) will differ from what was reviewed. Check that no step compares contract digests.", + "4 (residual risk, accepted by owner): With both caps null, nothing limits the total number of repair cycles or LLM calls. Only concurrency limits and the watchdog timeout remain. If any automation calls repair() without a human in the loop, it can loop forever. The repairs counter and runs table still allow monitoring after the fact; consider an alert threshold rather than a cap.", + "5 (minor): The project match is a case-sensitive string prefix 'the-tree-', not 'TheTree' as written in the brief. Any project registered with that name gets the cap-free policy automatically. Also, check_schema's anyOf ignores any sibling keywords in the same schema node and drops the inner error detail. Neither matters for the current task.json nullable field." + ] + }, + "artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p08-02/runs/1e1b149ca4c44ba5ad2bafd534e49ce1", + "scope": "operational cap change only; NOT P08 acceptance" +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/OPUS_CONCRETE_DIRECTION.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/OPUS_CONCRETE_DIRECTION.json new file mode 100644 index 0000000..3af421b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/OPUS_CONCRETE_DIRECTION.json @@ -0,0 +1,10 @@ +{ + "decision": { + "action": "validate", + "task_id": "P08-02", + "reason": "【判断】P08-02は提出済み(e59206b9b2578ddb4a1ec023f836d413a7a8fbfb、repairs=2)なので、次の1手はこの提出SHAに対する正式なvalidate(p08-evidence)です。確定事実から、writer52976とQA55276はguest_elevatorで材料が10不足して失敗する見込みです。ただし、修正理由はRunnerの正式な検証失敗記録に紐づける必要があり、記録なしにrepairへ進むことはしません。全面的な再調査や再実装は行いません。\n\n【全体の順序】\n(1) 今:e59206bに対してvalidateを実行し、失敗の詳細(不足額・フェーズ・PID)を記録する。\n(2) Aをオーナー/Root側の運用変更として適用する。P08-02の作業者の許可範囲外であり、私(コマンダー)も編集しません。\n(3) Aが反映されたのを確かめてから、request-repairで同じ作業者・同じworktree(TheTree-managed-worktrees/p08-02/P08-02-implement)にB+Cを指示する。\n(4) Root側で収集する:実ネイティブのwriter/reader別PID、Operations ON/OFFの全ケース、後段で解消できないbatchの拒否、正式なセーブ/バックアップ、本番メニューでの実際のOS入力と画面、最終の全回帰テスト。\n(5) checker --verify-only を実行する。\n(6) 別セッションのOpusが独立してレビューする。\n(7) 統合準備を進める。統合とledger反映はオーナー判断です。\n\n【A Runner運用変更(オーナーの明示指示。Root/オーナーが実施し、作業者と私は編集しない)】\n・max_repair_attemptsにnullを許可し、null=上限なしとする。整数は従来どおり有限の上限として扱う。\n・repairsの使用回数はそのまま保持し、リセットや偽造はしない。\n・登録schemaのvalidator、policy、repair guardの3か所で、null時は回数チェックを飛ばす分岐にする。上限超過だけを理由にblockedや再承認待ちにしない。\n・TheTreeに登録済みの全タスクの値をnullに移行し、新規登録時の既定値もnullにする。\n・timeout、同時実行数、resource_locks、品質門(validate、独立レビュー、受入分母、統合承認)は一切緩めない。成功状態の書き込みや手動でのPASS化は禁止する。\n・変更差分と、全登録タスクに適用した結果の一覧を記録として残す。\n\n【B(request-repair時に作業者へ渡す指示。原製品の値は変更しない)】\n・P08 fixtureの計画建設をすべて対象に、実際のcatalogから費用を読み取り、合計を求めるコードをfixture内に置く。対象は次のとおり:warehouse、kitchen、infirmary、staff_door、reception×2、guest_elevator、staff_dormitory、premium_room×2、service_stairs×3、guest_stairs×3、staff_ginaのsigning_cost、rawfood8×実際の単価。定数のハードコードは禁止。\n・初期資源との差額だけを、既存の領域メソッドchange_resourcesで加算する。これは診断用の入力であることをmetadataに明記し、計算の内訳と合計を保存する。\n・catalog、価格、経済係数、本体コードは一切変更しない。\n・writerの事前チェックで、資源が不足していれば具体的な不足額を出して非0で終了させる。checker testに「資源不足の負例」を1件追加する。\n\n【C(同じ修正内で実施)】\n・旧1MBのhistoryファイルを、816BのPROPOSED_HISTORY_REFERENCE.json形式の参照に置き換える。参照にはGitの2版のblob SHAと、検証済みgzipのrawSHA/サイズを記載する。\n・metadata内のhistory linkのSHAを、PROPOSED_CURRENT_HISTORY_LINK.jsonに合わせて同期する。\n・checkerは参照を解決し、gzipを展開した内容のSHAが一致することを検証する。一致しない場合や欠損している場合は非0にする。\n・source17を含むpacketが500000未満(案では433604)であることを、Root側で実測する。過去の失敗ログはすべて保持する。\n・変更はallowed_paths(tests/fixtures/p08-02/**、execution/p08-02/**、checker/test/driver)の範囲に限る。p08-02-audit-r01の資料は読み取り専用の参照として使う。\n・作業者が実行するのは、python -B、ast.parse、checkerの局所テストまで。native実行と全回帰はRoot側の担当。\n\n【未完了の受入条件】OS入力、ON/OFFの全ケース、reader別PIDでの継続、全回帰、独立レビュー。いずれも未収集であり、合格扱いにはしません。", + "codex_model": "gpt-6.1-sol", + "codex_model_reason": "今回は検証の手番で、続く修正も範囲が限られています。fixture内での費用の合計計算とchange_resources、history参照ファイルの置き換えとSHA同期、checkerの負例1件で、原因と修正案はすでに確定しています。高度な推論は不要なので、品質・時間・使用量のバランスから既定のSolが妥当です。" + }, + "artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p08-02/runs/8f9b4eaf22ae48daba84c2f5993bd8a4" +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/OPUS_P08_RUN_DIRECTION.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/OPUS_P08_RUN_DIRECTION.json new file mode 100644 index 0000000..61f646e --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/OPUS_P08_RUN_DIRECTION.json @@ -0,0 +1,10 @@ +{ + "decision": { + "action": "run", + "task_id": "P08-02", + "reason": "P08-02は状態がrepair_ready(repairs=3、上限null=unlimited、保持済み)です。既存の同一worktree(base e196578、submitted e59206b)で通常のrepair runを1回だけ行います。全面的な再実装と2つ目のworkerの起動は禁止です。\n\n【worker作業指示】\n0) 最初に docs/plans/original-completion-2026-10-02/execution/p08-02/ROOT_REPAIR_INPUT-r04.json だけを読む。これはRootが格納した実提案4件の全文(約12KB)で、作業の主な入力。巨大なold historyやRoot外部の成果物パスは読まない・開かない・実行しない。次の手順に入る前に、変更範囲(allowed_pathsの範囲内)と検証方針を短く宣言する。\n\n1) 診断B(資源差額): 登録済みの建設費(construction cost)、雇用時の契約金(hire signing)、food 8件の価格を既存のドメイン定義から読み、合計する。その合計と、fixtureが記録する既存の change_resources 差額を照合する診断を準備する。経済値の新規作成、係数の発明、価格変更はしない。fixtureの値とseedは診断入力として明示する。一致しなければ、受入を狭めずに差分を報告する。\n\n2) fixture C(三段階の全建設): tests/fixtures/p08-02 のwriterとreaderを最小限変更し、建設の三段階(未着手または支払済みの開始段階、進行途中、完成施設)をすべて同一batchで網羅する。各段階のジョブについて、進捗・支払・予約・完成IDの保全が観測できるようにする。既存の6観点(客の経路、職員、救護、物流、EV、占有)は減らさない。\n\n3) old historyの重複削減: 約1MBの重複コピーを、実際に保全済みのGit上の2版と、gzip原文のSHA参照に置き換える。current metadataのリンク(implementation.json、CONDITION_MATRIX.mdなど)を同期する。old history参照は履歴保全のためのメタデータで、ゲーム受入の入力ではない。そのため、外部の絶対パスにあるgzipを実行時の必須依存としてcheckerに追加しない。原文SHAの照合は、Rootが実測済みの証拠を値として再利用する。削除する前に、参照先のSHAが一致することを確認する。履歴上の失敗ログは保持する。\n\n4) checkerとテストの変更は、受入に必要な最小限に限る。上記のB/C/参照変更をcheck()が検証できる範囲だけ追加する。正の例は到達可能にし、負の例は使い捨てfixtureの中だけで作る。テストを増やしすぎない。実装を写しただけの自明な試験も作らない。\n\n5) 次は不変: 保持対象のdriverとcheckerを含む全17 sources、.uid、full状態の収集全件、current_source_pins、completion_marker_contractsの既存エントリ、run_all_tests.py、本番のruntime/UIコード。900sのtimeoutは保持する。\n\n6) worker内での検証は、python -B による ast.parse と、checkerの意味のある局所試験(tools/test_p08_migration_evidence.py)だけ。Godot、native、full regression、OS収集は実行しない。\n\n7) 結果は status=submitted で返す。変更ファイル、SHA、実行した試験の実際の結果を書く。未達の原条件として、Rootによる実nativeの全ON/OFF writer/reader、OS操作、最終回帰、独立Opusレビューを列挙する。product側の欠陥を見つけた場合は、修正パッチ案と失敗の証拠を提案として添える。完了を自分で宣言しない。git書き込み、上限の変更、方針の変更はしない。", + "codex_model": "gpt-6.1-sol", + "codex_model_reason": "作業範囲が限られています。入力は約12KBのROOT_REPAIR_INPUT-r04.jsonに集約済みで、old historyは読みません。作業は資源差額の診断、fixtureの三段階網羅、old history重複のSHA参照化、checkerの最小限修正で、変更先はallowed_pathsの範囲内です。native、OS、回帰の検証はRootが担います。指示が具体的で設計判断の余地が小さいため、品質面で上位モデルを使う理由は弱く、時間と使用量の面では既定のsolが適切です。" + }, + "artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p08-02/runs/1ae2b3b017ba4554ba5a5c5d4430eea4" +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/REPORT.md b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/REPORT.md new file mode 100644 index 0000000..4b4e521 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/REPORT.md @@ -0,0 +1,5 @@ +個別上限撤廃はユーザーの直接指示に基づき適用した。The Tree登録14プロジェクト・14タスクの修正上限と累積LLM起動回数上限をnull(上限なし)へ移行した。使用済み回数、原受入分母、仕様、提出版、作業状態、検証、独立レビュー、実行同時数、watchdog timeoutを保持する。新規The Tree登録も上限なしを既定とする。 + +Opus 5.5の実判断と別セッションの運用変更レビューを保存した。レビューの留保(全保存policyと他使用箇所の確認)は、全14設定のnull値と整合ハッシュ、現行2controlの検索と局所試験で照合した。47試験(Runner既存46+新規登録の既定1)はPASS。他にnullable/state/count局所3と既存guard3もPASS。ゲームの受入を意味しない。 + +P08-02は同じ保持成果と同じSol担当を継続。個別上限による承認待ちは撤廃した。Opus具体指示はOPUS_P08_RUN_DIRECTION.json。過去native失敗と実保存履歴を再利用し、正確な新提出SHAに必要なnative・通常OS・最終回帰・独立レビューを実施してから元成果単位の完了を判定する。30/88の分母と部分点なしは維持。 diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/VALIDATION_STABLE_COW.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/VALIDATION_STABLE_COW.json new file mode 100644 index 0000000..27f2b07 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/VALIDATION_STABLE_COW.json @@ -0,0 +1,125 @@ +{ + "method": "same-byte APFS clone; mode uid gid flags mtime xattrs and fullSHA checked", + "rows": [ + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/audio/bgm/tree_theme_back_candidate.wav", + "sha256": "efe4cc1aa588661d73376720e4278427f7220ce93a5d446f198c5af607c141ca", + "bytes": 7526444, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/audio/bgm/tree_theme_front_candidate.wav", + "sha256": "6df0a7af307327f703c9325f5ca2ac456888102d50ef1fa76bf4cdf19d2b86cb", + "bytes": 7526444, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/kenney/tiny_town/Sample.png", + "sha256": "cb909e46bf51deb2906eeeb14b323aa42e861a738a734f449dc460ad682e4ccc", + "bytes": 112122, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-main-gameplay-visual-mock-v1.png", + "sha256": "58877bb1afbf0e72af6531cadab01eca82f9bd8cefb0cdb45f81d6d2dbb83717", + "bytes": 2722232, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-main-gameplay-visual-mock-v2-japanese-fantasy.png", + "sha256": "1c079ebb8b2b9610df0e79a2750cc5a0db53f26d137b6df511b12cb6884532d0", + "bytes": 3049138, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-main-gameplay-visual-mock-v3-tower-style.png", + "sha256": "f802d2b4f275f136fcacc963f7d06228f5d71dbc2399a19f7e04c6bc7531321b", + "bytes": 3076090, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v3-assets-front.png", + "sha256": "e54ff252bae66010dc885846acdcdb0117b930e0e3cfc8f5459acc2841eabf0d", + "bytes": 1461992, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v3-first-pass.png", + "sha256": "cc7f23ea258d49a8f2bd23735c63196b94ece380eaed934ee6e7f1b6d881cb44", + "bytes": 434318, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v4-icons.png", + "sha256": "b0826ffce116d96dbe5b901174713f84b55a0e72931f29821e0947b844b9c1b8", + "bytes": 1317405, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v5-map-icons.png", + "sha256": "aca56c954b209b0a03342c0fde576cef15bfaa2ce2a1f9feedd51744fcfdb42c", + "bytes": 1321334, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v6-actors-4k.png", + "sha256": "9b8a1d9e2e4408b7ee99505d1978a44a489eb8b6945d669d1c52e7e220937e79", + "bytes": 6569282, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v6-actors.png", + "sha256": "8f9ea1b8bbc779e3f401d4cfdc8dcb0d10b95b7f87f37d3737af9bc85aa9dddd", + "bytes": 1322150, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v7-skins-4k.png", + "sha256": "a32d61772e06b944cf7aeb7a2dd0bd1ccfac7cbb3d8e293a6d7075f780f48904", + "bytes": 8849219, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/mockups/the-tree-runtime-ui-v7-skins.png", + "sha256": "f0dec82c2da661df52124cd3c2f59c3ac94db9cb5d384846d42f1901ada370cf", + "bytes": 1778605, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/themes/japanese_fantasy/actor-markers-v2.png", + "sha256": "b742c207613d773e47f21c917975a2ba0eec8a2d9485ff655fef37e3ebf4bde4", + "bytes": 2165570, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/themes/japanese_fantasy/facility-icons-v2.png", + "sha256": "6fb830fbe3961125fe0f3e640efe63ecba4de5ea6fb758eaf0da2738940fa1b6", + "bytes": 1848036, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/themes/japanese_fantasy/system-icons-v2.png", + "sha256": "1f1d4f5e20ba4de61e3fb967352b68db435fa36ec5e4bacab2802f132e3b8d40", + "bytes": 1786659, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/themes/japanese_fantasy/ui-skin-v1.png", + "sha256": "4b4906cc9c6ed3fdb5330bfc11177376e5fec10afa3f2dac9318dbaf71833ff2", + "bytes": 2492477, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/themes/japanese_fantasy/world-back-v1.png", + "sha256": "2b0dbf07c17e677ed023c56b1e90f013c40362f46c427950033868b042508c7c", + "bytes": 2616183, + "all_content_and_attrs_preserved": true + }, + { + "path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f/assets/themes/japanese_fantasy/world-front-v1.png", + "sha256": "02b52ea05bdb4bbe78b8c905374052505bb73411eb7f2b0578e9b1a82994cde5", + "bytes": 2774704, + "all_content_and_attrs_preserved": true + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/applied_migration.py b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/applied_migration.py new file mode 100644 index 0000000..6e6e8ea --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/applied_migration.py @@ -0,0 +1,40 @@ +from pathlib import Path +import json,hashlib,sqlite3,time,shutil +root=Path('/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01') +controls=[Path('/private/tmp/the-tree-p24-02-control-r02'),Path('/Users/korizatou114/Antigravity/環境構築/agent-control')] +rows=[] +for i,c in enumerate(controls): + p=c/'runner/core.py'; old=p.read_text(); (root/f'core-before-{i}.py').write_text(old) + new=old.replace(' typ = schema.get("type")\n',' if "anyOf" in schema:\n for choice in schema["anyOf"]:\n try:\n check_schema(value, choice)\n return\n except GateError:\n pass\n raise GateError("結果schemaのanyOf違反")\n typ = schema.get("type")\n if typ == "null":\n require(value is None, "結果schemaの型違反: null")\n return\n',1) + new=new.replace(' require(0 <= contract["max_repair_attempts"] <= self.config["policy"]["max_repair_attempts"], "修正上限超過")',' cap = contract["max_repair_attempts"]; policy_cap = self.config["policy"]["max_repair_attempts"]\n require(cap is None and policy_cap is None or type(cap) is int and cap >= 0 and (policy_cap is None or cap <= policy_cap), "修正上限超過")',1) + new=new.replace('require(task["repairs"] < c["max_repair_attempts"], "repair_limit")','require(c["max_repair_attempts"] is None or task["repairs"] < c["max_repair_attempts"], "repair_limit")',1) + new=new.replace('require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限")','require(policy["max_llm_runs"] is None or self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限")',1) + assert new != old + p.write_text(new) + schema=c/'schemas/task.json'; s=json.loads(schema.read_text()); (root/f'task-schema-before-{i}.json').write_text(schema.read_text()); s['properties']['max_repair_attempts']={'anyOf':[{'type':'integer','minimum':0},{'type':'null'}],'default':None};schema.write_text(json.dumps(s,ensure_ascii=False,indent=2)+'\n') + rows.append({'control':str(c),'before_sha256':hashlib.sha256(old.encode()).hexdigest(),'after_sha256':hashlib.sha256(new.encode()).hexdigest()}) +# Future The Tree registrations inherit null; other projects retain defaults. +for c in controls: + p=c/'runner/core.py'; txt=p.read_text(); needle=' config = {"project": project, "repo": str(repo), "worktree_root": str(wt), "policy": p, "validations": validations}' + txt=txt.replace(needle,' if project.startswith("the-tree-"):\n p = dict(p, max_repair_attempts=None, max_llm_runs=None)\n'+needle,1) + p.write_text(txt) +# Atomic per project; no active inference/config migration races permitted. +records=[] +for cfg in sorted(Path('/Users/korizatou114/Library/Application Support/agent-control').glob('the-tree-*/config.json')): + config=json.loads(cfg.read_text()); db=sqlite3.connect(cfg.parent/'state.sqlite'); db.row_factory=sqlite3.Row + assert not db.execute("SELECT 1 FROM runs WHERE status='running'").fetchone(),str(cfg) + before=json.loads(json.dumps(config)); contracts=[dict(r) for r in db.execute('SELECT * FROM tasks')] + (root/(config['project']+'-before.json')).write_text(json.dumps({'config':before,'tasks':contracts},ensure_ascii=False,indent=2)+'\n') + config['policy']['max_repair_attempts']=None;config['policy']['max_llm_runs']=None + db.execute('BEGIN IMMEDIATE') + for row in contracts: + contract=json.loads(row['contract']);old_cap=contract['max_repair_attempts'];contract['max_repair_attempts']=None + db.execute('UPDATE tasks SET contract=? WHERE id=?',(json.dumps(contract,ensure_ascii=False),row['id'])) + data={'owner_instruction':'個別上限を全て撤廃して、勝手に作業停止することを避けて','previous_cap':old_cap,'new_cap':None,'unlimited':True,'repairs_retained':row['repairs'],'state_retained':row['state'],'llm_count_retained':True,'acceptance_and_scope_unchanged':True} + db.execute('INSERT INTO events(task_id,event,data,created) VALUES(?,?,?,?)',(row['id'],'owner_unlimited_individual_attempts',json.dumps(data,ensure_ascii=False),time.time())) + records.append({'project':config['project'],'task':row['id'],'previous_cap':old_cap,'cap':None,'repairs':row['repairs'],'state':row['state']}) + cfg.write_text(json.dumps(config,ensure_ascii=False,indent=2)+'\n') + digest=hashlib.sha256(json.dumps(config,sort_keys=True,ensure_ascii=False).encode()).hexdigest() + db.execute('UPDATE meta SET value=? WHERE key=?',(digest,'config_digest')); db.commit();db.close() +(root/'APPLIED.json').write_text(json.dumps({'authorization':'direct human latest instruction','interpretation':'All The Tree individual repair caps and accumulated LLM invocation caps removed. Watchdog timeout and concurrent resource limits retained.','controls':rows,'tasks':records},ensure_ascii=False,indent=2)+'\n') +print(json.dumps({'migrated_tasks':len(records),'projects':len(set(r['project'] for r in records)),'caps':None,'states_and_counts_retained':True})) diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/clone_stable_validation.py b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/clone_stable_validation.py new file mode 100644 index 0000000..22f5a0f --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/clone_stable_validation.py @@ -0,0 +1,29 @@ +from pathlib import Path +import subprocess,hashlib,os,json +src=Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-implement') +dst=Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-validation-8c6fdd1f') +rows=[] +def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest() +def attrs(p): + names=subprocess.check_output(['/usr/bin/xattr',str(p)],text=True).splitlines() + return {n:subprocess.check_output(['/usr/bin/xattr','-px',n,str(p)]).decode() for n in names} +for p in sorted((dst/'assets').rglob('*')): + if not p.is_file() or p.is_symlink() or p.stat().st_size<65536:continue + s=src/p.relative_to(dst) + if not s.is_file() or s.stat().st_size!=p.stat().st_size:continue + old=p.stat();h=sha(p) + if sha(s)!=h:continue + a=attrs(p);temp=p.with_name(p.name+'.owner-clone-temp') + assert not temp.exists() + subprocess.run(['/bin/cp','-pc',str(s),str(temp)],check=True) + os.utime(temp,ns=(old.st_atime_ns,old.st_mtime_ns));os.chmod(temp,old.st_mode);os.chflags(temp,old.st_flags) + for n in attrs(temp): + if n not in a:subprocess.run(['/usr/bin/xattr','-d',n,str(temp)],check=True) + for n,v in a.items():subprocess.run(['/usr/bin/xattr','-wx',n,''.join(v.split()),str(temp)],check=True) + t=temp.stat();assert sha(temp)==h and attrs(temp)==a and (t.st_size,t.st_mode,t.st_uid,t.st_gid,t.st_flags,t.st_mtime_ns)==(old.st_size,old.st_mode,old.st_uid,old.st_gid,old.st_flags,old.st_mtime_ns) + assert sha(p)==h + os.replace(temp,p);assert sha(p)==h + rows.append({'path':str(p),'sha256':h,'bytes':old.st_size,'all_content_and_attrs_preserved':True}) +output=Path('/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/VALIDATION_STABLE_COW.json') +output.write_text(json.dumps({'method':'same-byte APFS clone; mode uid gid flags mtime xattrs and fullSHA checked','rows':rows},indent=2)+'\n') +print('verified files',len(rows)) diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/core-before-0.py b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/core-before-0.py new file mode 100644 index 0000000..fdea4dd --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/core-before-0.py @@ -0,0 +1,1133 @@ +"""Deterministic local orchestration. No provider SDK or paid API fallback.""" +from __future__ import annotations + +import contextlib +import base64 +from datetime import datetime, timezone, timedelta +import fcntl +import threading +import fnmatch +import hashlib +import json +import math +import os +from pathlib import Path +import re +import shutil +import signal +import socket +import sqlite3 +import subprocess +import sys +import time +import uuid + +CONTROL = Path(__file__).resolve().parents[1] +APP_SUPPORT = Path.home() / "Library/Application Support/agent-control" +ID = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,63}$") + + +class GateError(RuntimeError): + pass + + +def require(value, message): + if not value: + raise GateError(message) + + +def read_json(path): + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def write_json(path, obj): + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(path.name + ".tmp") + temporary.write_text(json.dumps(obj, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + temporary.replace(path) + + +def digest(obj): + return hashlib.sha256(json.dumps(obj, sort_keys=True, ensure_ascii=False).encode()).hexdigest() + + +def git(repo, *args): + p = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, + env={**os.environ, "GIT_TERMINAL_PROMPT": "0"}, timeout=60) + require(p.returncode == 0, f"git {args[0]}: {p.stderr.strip()}") + return p.stdout.strip() + + + +SELF_TEST_SPEC_SHA = "5841d6ac4673336b2ece64efb1b9af3dbf67838bf54b8fa1021f2280e5105854" +SELF_TEST_BASE = "5811e01a17c423b156c5a45c49e2c6e81a4e2153" +SELF_TEST_ARGV = ["{python}", "-B", "-m", "unittest", "discover", "-s", "tests", "-p", "test_runner.py", "-v"] + + +def raw_blob(repo, commit, path): + oid = git(repo, "rev-parse", f"{commit}:{path}") + result = subprocess.run(["git", "-C", str(repo), "cat-file", "blob", oid], capture_output=True, timeout=60) + require(result.returncode == 0, "snapshot unavailable") + raw = result.stdout + algorithm = hashlib.sha1 if len(oid) == 40 else hashlib.sha256 + require(algorithm(b"blob " + str(len(raw)).encode() + b"\0" + raw).hexdigest() == oid, "snapshot inconsistent") + return oid, raw + + +def marker_rows(raw): + def pairs(items): + value = {} + for key, item in items: + require(key not in value, "duplicate JSON key") + value[key] = item + return value + def constant(value): + raise GateError("nonfinite JSON number") + def number(value): + parsed = float(value) + require(math.isfinite(parsed), "nonfinite JSON number") + return parsed + try: + text = raw.decode("utf-8") + decoder = json.JSONDecoder(object_pairs_hook=pairs, parse_constant=constant, parse_float=number) + obj = decoder.decode(text) + require(isinstance(obj, dict) and type(obj.get("schema_version")) is int + and obj["schema_version"] == 1 and isinstance(obj.get("rows"), list), "unsupported marker schema") + def space(i): + while i < len(text) and text[i] in " \t\r\n": i += 1 + return i + pos = space(space(0) + 1); span = None + while text[pos] != "}": + key, end = decoder.raw_decode(text, pos) + start = space(space(end) + 1) + _, end = decoder.raw_decode(text, start) + if key == "rows": span = start + pos = space(end) + if text[pos] == ",": pos = space(pos + 1) + require(span is not None, "missing rows") + pos = space(span + 1); rows = {} + while text[pos] != "]": + start = pos; row, end = decoder.raw_decode(text, pos) + require(isinstance(row, dict) and isinstance(row.get("test"), str) + and row["test"] and row["test"] not in rows, "invalid/duplicate row ID") + rows[row["test"]] = (row, text[start:end]) + pos = space(end) + if text[pos] == ",": pos = space(pos + 1) + require(len(rows) == len(obj["rows"]), "row cardinality mismatch") + return obj, rows + except (ValueError, UnicodeError, IndexError) as exc: + raise GateError("invalid marker JSON") from exc + + +def marker_context(repo, base, candidate, path, focus): + bo, braw = raw_blob(repo, base, path); co, craw = raw_blob(repo, candidate, path) + b, br = marker_rows(braw); c, cr = marker_rows(craw) + require(set(focus) <= cr.keys(), "focused row absent") + sha = lambda raw: hashlib.sha256(raw if isinstance(raw, bytes) else raw.encode("utf-8")).hexdigest() + unchanged = {k for k in br.keys() & cr.keys() if br[k][1] == cr[k][1]} + changed = (br.keys() | cr.keys()) - unchanged + def entry(k): + return {"kind": "modified" if k in br and k in cr else "added" if k in cr else "removed", + "before": br[k][0] if k in br else None, "after": cr[k][0] if k in cr else None, + "before_raw": br[k][1] if k in br else None, "after_raw": cr[k][1] if k in cr else None, + "before_raw_sha256": sha(br[k][1]) if k in br else None, + "after_raw_sha256": sha(cr[k][1]) if k in cr else None} + return {"review_context_format": "authenticated-json-rows-v1", + "base_commit": base, "candidate_commit": candidate, + "base_git_blob_oid": bo, "candidate_git_blob_oid": co, + "base_raw_sha256": sha(braw), "candidate_raw_sha256": sha(craw), + "metadata_before": {k:v for k,v in b.items() if k != "rows"}, + "metadata_current": {k:v for k,v in c.items() if k != "rows"}, + "base_rows": len(br), "candidate_rows": len(cr), + "changed_rows": {k:entry(k) for k in sorted(changed)}, + "focused_current_rows": {k:cr[k][0] for k in sorted(focus)}, + "focused_current_raw": {k:cr[k][1] for k in sorted(focus)}, + "unchanged_raw_row_count": len(unchanged), + "unchanged_raw_row_proof": {k:sha(cr[k][1]) for k in sorted(unchanged)}, + "omitted_current_rows": sorted(cr.keys() - changed - set(focus)), + "omission_rule": "Only byte-identical rows are omitted; full diff and validation sources remain."} + + +def clean(repo): + require(not git(repo, "status", "--porcelain", "--untracked-files=all"), "checkoutに未提出の変更があります") + + +def safe_relative(value): + p = Path(value) + return bool(value) and not p.is_absolute() and ".." not in p.parts and not value.startswith(".") + + +def allowed(path, patterns): + # Match literal files or recursive directory trees, not ambiguous shell globs. + return any(path == p or (p.endswith("/**") and path.startswith(p[:-3] + "/")) for p in patterns) + + +def secret_path(path): + return any(part in (".ssh", ".env", "credentials.json") or part.startswith(".env.") + or part.endswith((".pem", ".key", ".p12")) for part in Path(path).parts) + + +def path_scope(pattern): + return pattern[:-3] if pattern.endswith("/**") else pattern + + +def overlap(a, b): + a, b = path_scope(a), path_scope(b) + return a == b or a.startswith(b + "/") or b.startswith(a + "/") + + +def limited_environment(): + # Never inherit API keys, SSH agent, custom model providers or cloud credentials. + names = ("HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TERM", "TMPDIR", "SSL_CERT_FILE") + env = {k: os.environ[k] for k in names if k in os.environ} + env.update(PATH=f"{Path.home()}/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", + GIT_TERMINAL_PROMPT="0", PYTHONDONTWRITEBYTECODE="1", + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1", AGY_CLI_DISABLE_AUTO_UPDATE="true") + return env + + +def executable(service): + choices = [Path.home() / ".local/bin" / service] + if service == "codex": + choices += [Path("/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex")] + found = shutil.which(service) + if found: + choices.insert(0, Path(found)) + for path in choices: + if path.exists(): + return str(path) + raise GateError(f"environment_missing: {service} CLIがありません") + + +def subscription_auth(service): + """Refuse API-key auth and missing sign-in before any model request.""" + cli=executable(service) + if service=="codex": + p=subprocess.run([cli,"login","status"],capture_output=True,text=True,timeout=15,env=limited_environment()) + require(p.returncode==0 and "ChatGPT" in p.stdout+p.stderr,"authentication_missing: CodexのChatGPTログインが必要。APIキー経路は無効") + elif service=="claude": + p=subprocess.run([cli,"auth","status"],capture_output=True,text=True,timeout=15,env=limited_environment()) + data=json.loads(p.stdout or "{}") + require(data.get("loggedIn") is True,"authentication_missing: claude auth loginを実行してください") + require(data.get("authMethod") == "claude.ai","api_auth_disabled: Claude.aiサブスクリプションのログインが必要") + elif service=="agy": + config=Path.home()/".gemini/antigravity-cli/settings.json" + if config.exists(): + require(read_json(config).get("modelProvider") in (None,"","antigravity"),"api_auth_disabled: Antigravityのアカウント認証が必要") + + +def claude_session_auth(): + """Read only Claude's own subscription item in the trusted Runner process. + + Give the tool-free Claude process the access token in memory, never the + refresh token or access to the user's Keychain. Never log credential output. + CLI login owns renewal; an expired access token fails closed. + """ + p = subprocess.run(["/usr/bin/security", "find-generic-password", "-s", + "Claude Code-credentials", "-w"], capture_output=True, + text=True, timeout=15, env=limited_environment()) + require(p.returncode == 0, "authentication_missing: Claude専用のキーチェーン項目を取得できません") + try: + oauth = json.loads(p.stdout)["claudeAiOauth"] + token = oauth["accessToken"] + valid = isinstance(token, str) and bool(token) and oauth["expiresAt"] > time.time() * 1000 + except (KeyError, TypeError, ValueError): + raise GateError("authentication_missing: Claude認証データの形式が不正です") from None + require(valid, "authentication_expired: claude auth loginで更新してください") + return {"CLAUDE_CODE_OAUTH_TOKEN": token} + + +@contextlib.contextmanager +def agy_session_auth(runtime, provider_root=None): + """Version-pinned native file fallback; never open personal Keychains. + + Only the Antigravity consumer access token is copied to a private task + file. Native fallback paths are temporary links, serialized across all + projects. Existing user files are never overwritten. No refresh token is + shared with the worker, and all temporary auth files are removed on exit. + """ + version = subprocess.run([executable("agy"), "--version"], capture_output=True, + text=True, timeout=15, env=limited_environment()) + require(version.stdout.strip() == "1.3.2", "agy_auth_adapter_version: CLI更新後の互換性検証が必要です") + broker = APP_SUPPORT / "auth-broker" + broker.mkdir(parents=True, exist_ok=True, mode=0o700) + with (broker / "agy.lock").open("a") as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise GateError("resource_busy: Antigravity認証アダプター使用中") from None + root = Path(provider_root) if provider_root else Path.home() / ".gemini/antigravity-cli" + links = [root / "antigravity-oauth-token", root / "cache/antigravity-keyring-unavailable", root / "bin/agentapi"] + record = broker / "agy-session.json" + if record.exists(): + previous = read_json(record) + for pid in (previous["owner_pid"], previous.get("child_pid")): + if pid: + try: + os.kill(pid, 0) + except ProcessLookupError: + pass + else: + raise GateError("resource_busy: 以前のAntigravityプロセスが残っています。stopで停止してください") + require(previous["links"] == [str(p) for p in links], "agy_auth_conflict: 認証リンクの場所が変わっています") + for link, value in zip(links, previous["files"]): + source = Path(value) + require((source.parent.name == "provider-auth" and source.name in ("access.json", "keyring-unavailable")) or (source.name == "agentapi" and source.parent == Path(previous["files"][0]).parent.parent), "agy_auth_conflict: 回復記録が不正です") + if link.is_symlink() and link.readlink() == source: + link.unlink() + else: + require(not link.exists() and not link.is_symlink(), "agy_auth_conflict: 認証リンクの変更を検出しました") + source.unlink(missing_ok=True) + record.unlink() + require(not any(p.exists() or p.is_symlink() for p in links), "agy_auth_conflict: 既存のファイル認証設定を保護して停止しました") + def load(): + p = subprocess.run(["/usr/bin/security", "find-generic-password", "-s", "gemini", + "-a", "antigravity", "-w"], capture_output=True, text=True, + timeout=15, env=limited_environment()) + require(p.returncode == 0, "authentication_missing: Antigravity専用の認証項目がありません") + raw = p.stdout.strip() + try: + if raw.startswith("go-keyring-base64:"): + raw = base64.b64decode(raw.split(":", 1)[1]).decode() + elif raw.startswith("go-keyring-encoded:"): + raw = bytes.fromhex(raw.split(":", 1)[1]).decode() + data = json.loads(raw) + token = data["token"] + expiry = datetime.fromisoformat(token["expiry"].replace("Z", "+00:00")) + valid = isinstance(token["access_token"], str) and bool(token["access_token"]) + except (KeyError, ValueError, TypeError): + raise GateError("authentication_missing: Antigravity認証データの形式が不正です") from None + require(data.get("auth_method") == "consumer" and valid, "api_auth_disabled: 個人Googleアカウントの認証が必要です") + return token, expiry + token, expiry = load() + # Antigravity refreshes several minutes before the nominal expiry. + # Renew through the native CLI before stripping the refresh token. + if expiry <= datetime.now(timezone.utc) + timedelta(minutes=6): + # /model is a built-in command, not an inference prompt. Native CLI + # renews its own Keychain item in this trusted, tool-free probe. + p = subprocess.run([executable("agy"), "-p", "/model", "--output-format", "json", + "--print-timeout", "20s", "--log-file", str(Path(runtime)/"auth-renewal.log")], + capture_output=True, text=True, timeout=30, env=limited_environment()) + require(p.returncode == 0, "authentication_expired: Antigravityログインの更新が必要です") + token, expiry = load() + require(expiry > datetime.now(timezone.utc), "authentication_expired: Antigravityログインの更新が必要です") + private = Path(runtime) / "provider-auth" + private.mkdir(mode=0o700) + files = [private / "access.json", private / "keyring-unavailable", Path(runtime) / "agentapi"] + try: + write_json(record, {"owner_pid": os.getpid(), "links": [str(p) for p in links], "files": [str(p) for p in files]}) + # Select fields explicitly: refresh_token never reaches task storage. + fd = os.open(files[0], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, "w") as out: + json.dump({"auth_method": "consumer", "token": {k: token[k] for k in ("access_token", "token_type", "expiry")}}, out) + files[1].write_text("true"); files[1].chmod(0o600) + files[2].touch(mode=0o700, exist_ok=False) + for link, source in zip(links, files): + link.parent.mkdir(parents=True, exist_ok=True) + link.symlink_to(source) + yield record + finally: + for link, source in zip(links, files): + if link.is_symlink() and link.readlink() == source: + link.unlink() + source.unlink(missing_ok=True) + private.rmdir() + record.unlink(missing_ok=True) + + +def check_schema(value, schema): + """Small strict validator for the schemas we ship (also used locally after CLI output).""" + typ = schema.get("type") + types = {"object": dict, "array": list, "string": str, "integer": int, "boolean": bool} + require(typ not in types or isinstance(value, types[typ]), f"結果schemaの型違反: {typ}") + require("enum" not in schema or value in schema["enum"], "結果schemaのenum違反") + if isinstance(value, dict) and typ == "object": + require(set(schema.get("required", [])).issubset(value), "結果schemaの必須項目不足") + properties = schema.get("properties", {}) + require(schema.get("additionalProperties", True) or set(value).issubset(properties), "結果schemaの未定義項目") + for key, item in value.items(): + if key in properties: + check_schema(item, properties[key]) + if isinstance(value, list) and typ == "array": + for item in value: + check_schema(item, schema["items"]) + + +def seatbelt(worktree, runtime, writable, service=None): + """Outer macOS policy enforces actual paths and protects runner/approval/auth data. + + Codex provider auth is available to its CLI runtime. Claude gets only a + process-local access token; personal Keychains stay denied for all roles. + Use only trusted repositories, as documented in README.md. + """ + require(Path("/usr/bin/sandbox-exec").exists(), "isolation_missing: macOS sandbox-execが必要です") + quote = lambda p: json.dumps(str(Path(p).resolve()), ensure_ascii=False) + lines = ["(version 1)", "(allow default)", "(deny file-write*)"] + # CLI binary and provider state are the only home reads outside this task. + readable = [worktree, runtime, Path.home()/".local/bin", Path.home()/".local/share/claude"] + if service == "codex": + readable += [Path.home()/".codex/auth.json",Path.home()/".codex/tmp",Path.home()/".codex/installation_id"] + lines.append(f"(allow file-write* (subpath {quote(Path.home()/'.codex/tmp')}))") + lines.append(f"(allow file-write* (literal {quote(Path.home()/'.codex/installation_id')}))") + if service == "claude": + readable += [Path.home()/".claude.json"] + if service == "agy": + readable += [Path.home()/".gemini/antigravity-cli",Path.home()/".gemini/antigravity/antigravity_state.pbtxt", + Path.home()/".gemini/antigravity/installation_id",Path.home()/".gemini/google_accounts.json", + Path.home()/".gemini/antigravity-cli/settings.json",Path.home()/".gemini/config/projects/default-cli-project.json"] + lines.append(f"(allow file-write* (literal {quote(Path.home()/'.gemini/antigravity-cli/cache/default_project_id.txt')}))") + exceptions = set() + for p in readable: + kind = "literal" if p.is_file() else "subpath" + exceptions.add(f"({kind} {quote(p)})") + # getcwd and create_dir_all inspect ancestor directories. Permit directory + # traversal/listing, without reopening any sibling file contents. + for parent in Path(p).resolve().parents: + if parent.is_relative_to(Path.home()): + exceptions.add(f"(literal {quote(parent)})") + # Worktree .git points into shared metadata: permit read, never write. + common = Path(git(worktree, "rev-parse", "--git-common-dir")) + if not common.is_absolute(): + common = (worktree / common).resolve() + exceptions.add(f"(subpath {quote(common)})") + lines.append(f"(deny file-read-data (require-all (subpath {quote(Path.home())}) " + + " ".join(f"(require-not {x})" for x in sorted(exceptions)) + "))") + for p, recursive in writable: + lines.append(f"(allow file-write* ({'subpath' if recursive else 'literal'} {quote(p)}))") + lines.append(f"(allow file-write* (subpath {quote(runtime)}))") + if service == "agy": + lines.append('(deny file-write* (regex "/provider-auth(/|$)"))') + lines.append('(allow file-read* file-write* (literal "/dev/ptmx") (regex #"^/dev/ttys[0-9]+$"))') + lines.append('(allow file-write* (literal "/dev/null") (literal "/dev/tty") (literal "/dev/stdout") (literal "/dev/stderr"))') + # Higher-priority explicit denies cannot be reopened by a workspace symlink. + for root in [CONTROL, APP_SUPPORT, Path.home()/".ssh", Path.home()/".aws", Path.home()/".azure", + Path.home()/".config/gcloud", Path.home()/"Library/Keychains"]: + if root == APP_SUPPORT: + # runtime is outside state root; nothing in state is visible to agents. + pass + lines.append(f"(deny file-read* file-write* (subpath {quote(root)}))") + lines.append(f"(deny file-write* (subpath {quote(common)}))") + for p in worktree.rglob("*"): + if secret_path(str(p.relative_to(worktree))): + lines.append(f"(deny file-read* file-write* (literal {quote(p)}))") + return "\n".join(lines) + + +class Runner: + def __init__(self, project, state_root=APP_SUPPORT): + require(ID.fullmatch(project), "project IDが不正です") + self.root = Path(state_root).resolve() / project + require((self.root / "config.json").exists(), f"project未登録: {project}") + self.config = read_json(self.root / "config.json") + self.repo = Path(self.config["repo"]) + self.worktrees = Path(self.config["worktree_root"]) + self.db = sqlite3.connect(self.root / "state.sqlite", timeout=10, isolation_level=None) + self.db.row_factory = sqlite3.Row + self.db.execute("PRAGMA foreign_keys=ON") + self.db.execute("CREATE TABLE IF NOT EXISTS allocations(task_id TEXT PRIMARY KEY,port INTEGER UNIQUE,value TEXT NOT NULL)") + require(self.meta("config_digest") == digest(self.config), "登録後のpolicy変更を検出。別projectとして再登録してください") + self.baseline = read_json(self.root / "acceptance_baseline.json") + require(self.meta("baseline_digest") == digest(self.baseline), "受入分母の無断変更を検出しました") + + @staticmethod + def init(project, repo, baseline, validations, state_root=APP_SUPPORT, worktree_root=None, policy=None): + require(ID.fullmatch(project), "project IDが不正です") + repo = Path(repo).resolve() + root = Path(state_root).resolve() / project + require(not root.exists(), "同じprojectは既に登録されています") + require(Path(git(repo, "rev-parse", "--show-toplevel")).resolve() == repo, "repo rootを指定してください") + require(git(repo, "rev-parse", "--is-shallow-repository") == "false", "shallow cloneは未対応です") + git(repo, "rev-parse", "main^{commit}") + require(isinstance(baseline, list) and baseline, "受入項目を登録してください") + ids = [a["id"] for a in baseline] + require(len(set(ids)) == len(ids), "受入ID重複") + for a in baseline: + require(ID.fullmatch(a["id"]) and a.get("requirement_id") and a.get("text") and a.get("validation_ids"), "受入項目不足") + require(set(a["validation_ids"]).issubset(validations), "受入項目に未知の試験ID") + for key, val in validations.items(): + require(ID.fullmatch(key) and val.get("argv") and all(isinstance(x, str) for x in val["argv"]), "試験コマンドはargv配列が必要") + wt = Path(worktree_root or (repo.parent / (repo.name + "-worktrees"))).resolve() + require(not wt.is_relative_to(repo), "worktree rootはrepo外にしてください") + p = policy or read_json(CONTROL / "policies/default.json") + require(p["api_fallback"] is False and p["owner_approval_required"] is True, "初期運用のAPI/承認policy違反") + config = {"project": project, "repo": str(repo), "worktree_root": str(wt), "policy": p, "validations": validations} + root.mkdir(parents=True, mode=0o700) + os.chmod(root, 0o700) + write_json(root / "config.json", config) + write_json(root / "acceptance_baseline.json", baseline) + db = sqlite3.connect(root / "state.sqlite") + db.executescript(""" + CREATE TABLE meta(key TEXT PRIMARY KEY,value TEXT NOT NULL); + CREATE TABLE tasks(id TEXT PRIMARY KEY,contract TEXT NOT NULL,state TEXT NOT NULL, + worktree TEXT,submitted TEXT,candidate TEXT,main_base TEXT,reason TEXT,repairs INTEGER NOT NULL DEFAULT 0); + CREATE TABLE locks(resource TEXT PRIMARY KEY,task_id TEXT NOT NULL); + CREATE TABLE runs(id TEXT PRIMARY KEY,task_id TEXT,role TEXT,service TEXT,status TEXT, + pid INTEGER,started REAL,finished REAL,exit_code INTEGER,artifact TEXT,api_cost_usd REAL, + estimated_cost_usd REAL,subscription_usage REAL,usage_json TEXT); + CREATE TABLE evidence(id INTEGER PRIMARY KEY,task_id TEXT,kind TEXT,commit_sha TEXT,artifact TEXT,passed INTEGER); + CREATE TABLE approvals(task_id TEXT PRIMARY KEY,candidate TEXT,main_base TEXT,owner TEXT,created REAL); + CREATE TABLE events(id INTEGER PRIMARY KEY,task_id TEXT,event TEXT,data TEXT,created REAL); + """) + db.executemany("INSERT INTO meta VALUES(?,?)", [("config_digest",digest(config)),("baseline_digest",digest(baseline))]) + db.commit(); db.close() + return Runner(project, state_root) + + def meta(self, key): + return self.db.execute("SELECT value FROM meta WHERE key=?", (key,)).fetchone()[0] + + def task(self, task_id): + row = self.db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone() + require(row is not None, f"未知のタスク: {task_id}") + return dict(row) + + def event(self, task_id, name, data): + self.db.execute("INSERT INTO events(task_id,event,data,created) VALUES(?,?,?,?)", + (task_id, name, json.dumps(data, ensure_ascii=False), time.time())) + + def state(self, task_id, state, reason=None): + self.db.execute("UPDATE tasks SET state=?,reason=? WHERE id=?", (state, reason, task_id)) + self.event(task_id, state, {"reason": reason}) + + def add(self, contract): + check_schema(contract,read_json(CONTROL/"schemas/task.json")) + required = {"id", "title", "requirement_ids", "acceptance_ids", "spec_ref", "base_commit", "worker", + "allowed_paths", "depends_on", "resource_locks", "validation_ids", "max_repair_attempts"} + require(required.issubset(contract), f"タスク必須項目不足: {sorted(required-set(contract))}") + require(ID.fullmatch(contract["id"]), "task ID不正") + require(contract["worker"] in self.config["policy"]["allowed_services"] and contract["worker"] in ("codex","agy"), "未許可の実装サービス") + require(contract["allowed_paths"] and contract["acceptance_ids"] and contract["requirement_ids"], "変更範囲・受入・要件が必要") + require(len(set(contract["acceptance_ids"])) == len(contract["acceptance_ids"]), "受入項目重複") + require(all(ID.fullmatch(x) for x in contract["resource_locks"]), "resource lock名は英数字・ハイフン・アンダースコアのみ") + for p in contract["allowed_paths"]: + require(safe_relative(path_scope(p)) and not secret_path(p) and not any(x in path_scope(p) for x in "*?[]"), "pathは相対の具体的ファイルまたはdir/**のみ") + require(path_scope(p) not in ("AGENTS.md","CLAUDE.md","docs/spec") and not path_scope(p).startswith("docs/spec/"), "仕様・共通規約の変更は禁止") + require(set(contract["validation_ids"]).issubset(self.config["validations"]), "未知の検証ID") + for a in contract["acceptance_ids"]: + item = next((x for x in self.baseline if x["id"] == a), None) + require(item and item["requirement_id"] in contract["requirement_ids"], "受入と要件の対応不一致") + require(set(item["validation_ids"]).issubset(contract["validation_ids"]), "受入に必要な検証が不足") + spec = contract["spec_ref"].split("#")[0] + require(safe_relative(spec) and not secret_path(spec), "仕様path不正") + base = git(self.repo, "rev-parse", contract["base_commit"] + "^{commit}") + require(base == contract["base_commit"], "base_commitは完全なcommit SHAが必要") + git(self.repo, "merge-base", "--is-ancestor", base, "main") + spec_text = git(self.repo, "show", f"{base}:{spec}") + for rid in contract["requirement_ids"]: + require(rid in spec_text, f"仕様に要件IDがありません: {rid}") + require(0 <= contract["max_repair_attempts"] <= self.config["policy"]["max_repair_attempts"], "修正上限超過") + for dep in contract["depends_on"]: + self.task(dep) + require(contract["id"] not in contract["depends_on"], "自己依存") + existing = [json.loads(r[0]) for r in self.db.execute("SELECT contract FROM tasks")] + require(not any(set(c["acceptance_ids"]) & set(contract["acceptance_ids"]) for c in existing), "同一受入項目の重複タスクは禁止") + c = dict(contract, spec_sha256=hashlib.sha256(spec_text.encode()).hexdigest()) + self.db.execute("INSERT INTO tasks(id,contract,state) VALUES(?,?,?)", (c["id"],json.dumps(c,ensure_ascii=False),"ready")) + self.event(c["id"], "registered", c) + + @contextlib.contextmanager + def lease(self, resource, owner): + try: + self.db.execute("INSERT INTO locks VALUES(?,?)", (resource,owner)) + except sqlite3.IntegrityError: + raise GateError(f"resource_busy: {resource}") + try: + yield + finally: + self.db.execute("DELETE FROM locks WHERE resource=? AND task_id=?", (resource,owner)) + + def prepare(self, task_id): + task = self.task(task_id); c = json.loads(task["contract"]) + require(task["state"] in ("ready", "repair_ready"), "ready/repair_readyからだけ開始できます。修正はrepairで回数を記録してください") + for dep in c["depends_on"]: + require(self.task(dep)["state"] == "completed", f"dependency_pending: {dep}") + self.db.execute("BEGIN IMMEDIATE") + try: + for other in self.db.execute("SELECT t.contract FROM tasks t WHERE t.id IN (SELECT task_id FROM locks) AND t.id!=?",(task_id,)): + other_c = json.loads(other[0]) + require(not any(overlap(a,b) for a in c["allowed_paths"] for b in other_c["allowed_paths"]), "file_scope_busy") + for resource in c["resource_locks"] + ["task:" + task_id]: + row = self.db.execute("SELECT task_id FROM locks WHERE resource=?",(resource,)).fetchone() + require(not row or row[0] == task_id, f"resource_busy: {resource}") + self.db.execute("INSERT OR IGNORE INTO locks VALUES(?,?)", (resource,task_id)) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK"); raise + if not task["worktree"]: + wt = self.worktrees / (task_id + "-implement") + self.worktrees.mkdir(parents=True, exist_ok=True) + try: + git(self.repo,"worktree","add","--detach",str(wt),c["base_commit"]) + self.db.execute("UPDATE tasks SET worktree=? WHERE id=?",(str(wt),task_id)) + except BaseException: + self.db.execute("DELETE FROM locks WHERE task_id=?",(task_id,)); raise + else: + wt = Path(task["worktree"]) + runtime = self.runtime(task_id) + for p in c["allowed_paths"]: + actual = wt / path_scope(p) + require(actual.resolve().is_relative_to(wt.resolve()), "symlinkが範囲外を参照しています") + if p.endswith("/**"): + actual.mkdir(parents=True, exist_ok=True) + write_json(runtime/"allocation.json", self.allocation(task_id)) + return wt + + def runtime(self, task_id): + path = self.worktrees / ".runtime" / task_id + path.mkdir(parents=True, exist_ok=True, mode=0o700) + return path + + def allocation(self, task_id): + r = self.runtime(task_id) + existing = r / "allocation.json" + if existing.exists(): + value=read_json(existing) + self.db.execute("INSERT OR IGNORE INTO allocations VALUES(?,?,?)",(task_id,value["port"],json.dumps(value))) + return value + self.db.execute("BEGIN IMMEDIATE") + try: + registered=self.db.execute("SELECT value FROM allocations WHERE task_id=?",(task_id,)).fetchone() + if registered: + value=json.loads(registered[0]) + else: + used={row[0] for row in self.db.execute("SELECT port FROM allocations")} + used.update(read_json(p)["port"] for p in (self.worktrees/".runtime").glob("*/allocation.json")) + port=4200 + while True: + if port not in used: + with socket.socket() as s: + try: s.bind(("127.0.0.1",port)); break + except OSError: pass + port+=1 + value={"port":port,"url":f"http://127.0.0.1:{port}","test_db":str(r/"test.sqlite"), + "build_dir":str(r/"build"),"browser_profile":str(r/"chrome-profile"),"tmp":str(r/"tmp")} + self.db.execute("INSERT INTO allocations VALUES(?,?,?)",(task_id,port,json.dumps(value))) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK");raise + write_json(existing,value) + return value + + def packet(self, task_id, role, commit=None, worktree=None): + task = self.task(task_id); c=json.loads(task["contract"]) + revision = commit or c["base_commit"] + spec = c["spec_ref"].split("#")[0] + spec_text = git(self.repo,"show",f"{revision}:{spec}") + require(hashlib.sha256(spec_text.encode()).hexdigest() == c["spec_sha256"], "仕様が登録時から変わっています") + body = {"contract": c, "commit": revision, "spec": spec_text, + "acceptance": [a for a in self.baseline if a["id"] in c["acceptance_ids"]], + "rules": (CONTROL/"AGENTS.md").read_text(), "allocation":self.allocation(task_id)} + if role == "reviewer": + body["diff"] = git(self.repo,"diff","--no-ext-diff",c["base_commit"],revision) + names = git(self.repo,"ls-tree","-r","--name-only",revision).splitlines() + body["sources"] = {p: git(self.repo,"show",f"{revision}:{p}") for p in names + if allowed(p,c["allowed_paths"]) and not secret_path(p)} + if (self.config["project"] == "the-tree-p24-03-code" and task_id == "P24-03" + and c["base_commit"] == "286730f88ce430a1cbe1501d40361188f832d235" + and revision == "4cefcff849d4dc1211caa5066e24bfae666cc2f4"): + path = "docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json" + focus = ["tests/ui/" + name + ".gd" for name in + ("test_actor_selection", "test_selection_lifecycle", "test_selection_autosave", "test_selection_departure")] + require(path in body["sources"], "marker source absent") + body["sources"][path] = marker_context(self.repo, c["base_commit"], revision, path, focus) + # Include committed test scripts/directories named in registered + # validation argv, even when workers cannot edit those tests. + test_paths = {arg for vid in c["validation_ids"] for arg in self.config["validations"][vid]["argv"] + if safe_relative(arg) and not secret_path(arg)} + body["validation_sources"] = {p: git(self.repo,"show",f"{revision}:{p}") for p in names + if not secret_path(p) and any(p == x or p.startswith(x.rstrip("/") + "/") for x in test_paths)} + body["tests"] = [read_json(r[0]) for r in self.db.execute("SELECT artifact FROM evidence WHERE task_id=? AND kind='validation' AND commit_sha=? AND passed=1",(task_id,revision))] + require(len(json.dumps(body).encode()) < 500_000, "packetが大き過ぎます。タスクを小さく分割してください") + prompt = (CONTROL/"prompts"/(role+".txt")).read_text() + "\nDATA:\n" + json.dumps(body,ensure_ascii=False) + return prompt + + def provider_argv(self, service, role, runtime, schema): + cli = executable(service) + schema_file = runtime / "result.schema.json"; write_json(schema_file,schema) + model = self.config["policy"]["models"].get(service) + if service == "codex": + # macOS cannot install a second Seatbelt profile inside our process. + # The outer profile is mandatory and restricts writes more narrowly + # than workspace-write. No unsandboxed process path exists here. + args=[cli,"exec","--json","--ignore-user-config","--ephemeral","--sandbox","danger-full-access", + "-c","approval_policy=\"never\"","-c",f"sqlite_home={json.dumps(str(runtime/'sqlite'))}", + "-c",f"log_dir={json.dumps(str(runtime/'logs'))}","--output-schema",str(schema_file),"--output-last-message",str(runtime/"answer.json"),"-"] + elif service == "claude": + # No tools for commander and reviewer; all required data is in the packet. + args=[cli,"-p","--safe-mode","--restricted","--output-format","json","--tools","","--strict-mcp-config","--mcp-config",'{"mcpServers":{}}', + "--setting-sources","","--no-session-persistence","--permission-mode","plan","--json-schema",json.dumps(schema)] + budget=self.config["policy"].get("claude_max_budget_usd") + if budget is not None: args += ["--max-budget-usd",str(budget)] + elif service == "agy": + args=[cli,"--input-format","stream-json","--output-format","stream-json","--json-schema",str(schema_file), + "--log-file",str(runtime/"agy.log"),"--print-timeout",f'{self.config["policy"]["timeout_seconds"]}s',"--sandbox","--mode","accept-edits","--disable-slash-commands"] + else: + raise GateError("未許可のサービス") + if model: args += ["--model",model] + return args + + def trusted_self_test(self, task_id, role, service, argv, cwd, input_text, writable, timeout, validation_id, verification_kind=None, verification_sha=None): + require(self.config["project"] == "the-tree-runner-p24-review-packet" and task_id == "RP24-01" + and role == "validation" and service is None and input_text is None and not writable + and validation_id == "runner-unit" and type(timeout) is int and timeout == 180, + "trusted self-test context rejected") + require(digest(self.config) == self.meta("config_digest") + and read_json(self.root / "config.json") == self.config, "trusted config changed") + task = self.task(task_id); c = json.loads(task["contract"]) + v = self.config["validations"][validation_id] + require(v == {"execution": "trusted_runner_self_test", "argv": SELF_TEST_ARGV, "timeout_seconds": 180}, + "trusted validation config rejected") + expected = [a.replace("{python}", str(Path(sys._base_executable).resolve())) for a in SELF_TEST_ARGV] + require(argv == expected and c["id"] == task_id and c["validation_ids"] == [validation_id] + and c["spec_ref"] == "docs/spec/RP24-01.json" and c["spec_sha256"] == SELF_TEST_SPEC_SHA + and c["base_commit"] == SELF_TEST_BASE + and c["requirement_ids"] == [task_id] and c["acceptance_ids"] == [task_id] + and sorted(c["allowed_paths"]) == ["runner/core.py", "tests/test_runner.py"], "trusted task rejected") + submitted = task["submitted"] + phases = {"validation": ("validating",), "integration": ("reviewed", "blocked"), "final": ("approved",)} + require(submitted and verification_kind in phases and task["state"] in phases[verification_kind] + and verification_sha, "trusted verification phase rejected") + sha = verification_sha + if verification_kind == "validation": + require(sha == submitted, "trusted submission SHA rejected") + else: + if verification_kind == "final": + require(sha == task["candidate"], "trusted approved SHA rejected") + approval = self.db.execute("SELECT * FROM approvals WHERE task_id=?", (task_id,)).fetchone() + require(approval and approval["candidate"] == sha and approval["main_base"] == task["main_base"], + "trusted approval rejected") + self.has_evidence(task_id, "validation", submitted) + self.has_evidence(task_id, "review", submitted) + require(git(self.repo, "merge-base", submitted, sha) == submitted, "trusted submitted ancestry rejected") + require(git(self.repo, "rev-parse", sha + "^{tree}") == git(self.repo, "rev-parse", submitted + "^{tree}"), + "trusted submitted content rejected") + clean(cwd) + require(git(cwd, "rev-parse", "HEAD") == sha and not git(cwd, "branch", "--show-current"), "trusted checkout rejected") + require(Path(cwd).resolve().parent == self.worktrees.resolve() + and git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir") + == git(self.repo, "rev-parse", "--path-format=absolute", "--git-common-dir"), "trusted worktree rejected") + self.check_scope(task_id, Path(cwd), sha) + spec = git(cwd, "show", sha + ":" + c["spec_ref"]) + require(hashlib.sha256(spec.encode()).hexdigest() == SELF_TEST_SPEC_SHA, "trusted spec changed") + return {"execution": "trusted_runner_self_test", "target_commit": sha, "spec_sha256": c["spec_sha256"], + "contract_digest": digest(c), "config_digest": digest(self.config), "validation_id": validation_id, + "verification_kind": verification_kind, "submitted_commit": submitted} + + def process(self, task_id, role, service, argv, cwd, input_text=None, writable=(), timeout=None, execution=None, validation_id=None, verification_kind=None, verification_sha=None): + require(execution in (None, "trusted_runner_self_test"), "unknown execution mode") + provenance = self.trusted_self_test(task_id, role, service, argv, cwd, input_text, writable, timeout, validation_id, verification_kind, verification_sha) if execution else {"execution": "sandbox"} + run_id=uuid.uuid4().hex; artifact=self.root/"runs"/run_id; artifact.mkdir(parents=True) + runtime=self.runtime(task_id)/run_id; runtime.mkdir(parents=True) + env=limited_environment(); alloc=self.allocation(task_id) + if service == "claude": + require(role in ("commander", "reviewer"), "Claudeの資格情報は参照専用の役割に限定") + env.update(claude_session_auth()) + Path(alloc["tmp"]).mkdir(parents=True,exist_ok=True) + env.update(MAC_CHROMIUM_TMPDIR=alloc["tmp"],PORT=str(alloc["port"]),TEST_DATABASE_PATH=alloc["test_db"],BUILD_OUTPUT_DIR=alloc["build_dir"], + BROWSER_PROFILE_DIR=alloc["browser_profile"],TMPDIR=alloc["tmp"],TMP=alloc["tmp"],TEMP=alloc["tmp"]) + sb=seatbelt(Path(cwd),self.runtime(task_id),writable,service) if not execution else "" + (artifact/"sandbox.sb").write_text(sb) + write_json(artifact/"command.json",{"argv":argv,"cwd":str(cwd),"env_names":sorted(env), **provenance}) + if input_text is not None: + (artifact/"packet.txt").write_text(input_text) + self.db.execute("BEGIN IMMEDIATE") + try: + policy=self.config["policy"] + if service: + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL AND status='running'").fetchone()[0] < policy["max_llm"], "llm_concurrency_limit") + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限") + if role in ("implementer","ui"): + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE role IN ('implementer','ui') AND status='running'").fetchone()[0] < policy["max_implementers"], "implementer_concurrency_limit") + self.db.execute("INSERT INTO runs(id,task_id,role,service,status,started,artifact) VALUES(?,?,?,?,?,?,?)", + (run_id,task_id,role,service,"running",time.time(),str(artifact))) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK"); raise + proc=None; code=None + try: + auth = agy_session_auth(runtime) if service == "agy" else contextlib.nullcontext() + with auth as auth_record, (artifact/"stdout.log").open("w") as out, (artifact/"stderr.log").open("w") as err: + proc=subprocess.Popen(argv if execution else ["/usr/bin/sandbox-exec","-p",sb,*argv],cwd=cwd,env=env, + stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,start_new_session=True) + # Node/Chrome inspect their output descriptors at startup. A + # descriptor to protected state logs is denied by Seatbelt. + # Drain pipes in the trusted Runner, keeping logs inaccessible. + def drain(source, target): + try: + for chunk in iter(lambda: source.read(8192), ""): + target.write(chunk); target.flush() + finally: + source.close() + readers = [threading.Thread(target=drain,args=(proc.stdout,out)), + threading.Thread(target=drain,args=(proc.stderr,err))] + proc.stdout = proc.stderr = None + for reader in readers: reader.start() + self.db.execute("UPDATE runs SET pid=? WHERE id=?",(proc.pid,run_id)) + if auth_record: + info = read_json(auth_record); info["child_pid"] = proc.pid; write_json(auth_record, info) + try: + proc.communicate(input_text,timeout=timeout or policy["timeout_seconds"]) + code=proc.returncode + except (subprocess.TimeoutExpired,KeyboardInterrupt): + os.killpg(proc.pid,signal.SIGTERM) + try: proc.wait(timeout=3) + except subprocess.TimeoutExpired: os.killpg(proc.pid,signal.SIGKILL); proc.wait() + code=124 + # Terminate any background descendants that outlived the foreground CLI. + try: os.killpg(proc.pid,signal.SIGTERM) + except ProcessLookupError: pass + try: os.killpg(proc.pid,signal.SIGKILL) + except ProcessLookupError: pass + for reader in readers: reader.join(timeout=5) + self.db.execute("UPDATE runs SET status=?,finished=?,exit_code=? WHERE id=?", + ("finished" if code == 0 else "failed",time.time(),code,run_id)) + except BaseException: + if proc and proc.poll() is None: + os.killpg(proc.pid,signal.SIGKILL); proc.wait() + self.db.execute("UPDATE runs SET status='failed',finished=? WHERE id=?",(time.time(),run_id)) + raise + return artifact, code + + def llm(self, task_id, role, service, prompt, cwd, schema): + require(service in self.config["policy"]["allowed_services"], "サービス送信未許可") + subscription_auth(service) + runtime=self.runtime(task_id)/("provider-"+uuid.uuid4().hex); runtime.mkdir() + args=self.provider_argv(service,role,runtime,schema) + c=json.loads(self.task(task_id)["contract"]) + writable=[] + if role in ("implementer","ui"): + writable=[(Path(cwd)/path_scope(p),p.endswith("/**")) for p in c["allowed_paths"]] + input_text=prompt + if service == "agy": + input_text=json.dumps({"event":"user","message":{"content":prompt}},ensure_ascii=False)+"\n" + artifact,code=self.process(task_id,role,service,args,cwd,input_text,writable) + err=(artifact/"stderr.log").read_text() + require(code == 0, f"provider_failed: exit={code}; log={artifact}") + raw=(artifact/"stdout.log").read_text() + usage=None; estimate=None + if service == "codex": + events=[json.loads(line) for line in raw.splitlines() if line.strip().startswith("{")] + require(not any(e.get("type") in ("error","turn.failed") for e in events), "provider_error_event") + usage=next((e.get("usage") for e in reversed(events) if e.get("type")=="turn.completed"),None) + require((runtime/"answer.json").exists(), "必要な構造化結果がありません") + value=read_json(runtime/"answer.json") + elif service == "claude": + envelope=json.loads(raw); require(not envelope.get("is_error"), "Claude error envelope") + value=envelope.get("structured_output") + if value is None: value=json.loads(envelope.get("result","")) + usage=envelope.get("usage"); estimate=envelope.get("total_cost_usd") + else: + events=[json.loads(line) for line in raw.splitlines() if line.strip().startswith("{")] + terminal=[e["result"] for e in events if e.get("event")=="result"] + require(terminal and terminal[-1].get("status")=="SUCCESS", "Antigravity terminal result不足") + require(not re.search(r"soft.denied|permission.*denied|approval.*required",err,re.I), "permission_denied: 必要な操作が拒否されました") + require(not any(e.get("step_update",{}).get("tool_info",{}).get("error") for e in events), "Antigravity tool error") + value=terminal[-1].get("structured_output") + if value is None: value=json.loads(terminal[-1].get("response","")) + usage=terminal[-1].get("usage") + check_schema(value,schema) + write_json(artifact/"result.json",value) + self.db.execute("UPDATE runs SET usage_json=?,estimated_cost_usd=? WHERE artifact=?",(json.dumps(usage),estimate,str(artifact))) + return value,artifact + + def run(self, task_id): + wt=self.prepare(task_id); c=json.loads(self.task(task_id)["contract"]) + self.state(task_id,"implementing") + try: + role="ui" if c["worker"]=="agy" else "implementer" + schema=read_json(CONTROL/"schemas/result.json") + schema["properties"]["task_id"]["enum"]=[task_id] + schema["properties"]["commit"]["enum"]=[c["base_commit"]] + value,artifact=self.llm(task_id,role,c["worker"],self.packet(task_id,role),wt,schema) + require(value["status"]=="submitted", f"worker_blocked: {value['summary']}") + self.submit(task_id) + self.event(task_id,"worker_result",{"artifact":str(artifact)}) + except (GateError,ValueError,OSError) as exc: + self.state(task_id,"blocked",str(exc)); raise + + def check_scope(self, task_id, wt, commit=None): + c=json.loads(self.task(task_id)["contract"]) + if commit: + names=git(self.repo,"diff","--name-only","--no-renames",c["base_commit"],commit).splitlines() + else: + names=git(wt,"diff","--name-only","--no-renames",c["base_commit"]).splitlines() + names += git(wt,"ls-files","--others","--exclude-standard").splitlines() + require(names, "変更がありません") + for p in names: + require(allowed(p,c["allowed_paths"]) and not secret_path(p), f"scope_violation: {p}") + require(not (wt/p).is_symlink(), f"scope_violation: symlink {p}") + return sorted(set(names)) + + def submit(self, task_id): + task=self.task(task_id); require(task["state"] in ("implementing","ready","repair_ready"), "提出可能な状態ではありません") + wt=Path(task["worktree"] or self.prepare(task_id)) + c=json.loads(task["contract"]) + require(git(wt,"rev-parse","HEAD") in (c["base_commit"],task["submitted"]), "workerによるgit commitを検出") + names=self.check_scope(task_id,wt) + git(wt,"add","--",*names) + git(wt,"-c","user.name=Agent Control Runner","-c","user.email=agent-control@localhost","-c","core.hooksPath=/dev/null", + "-c","commit.gpgsign=false","commit","-m",f"{task_id}: {c['title']}") + sha=git(wt,"rev-parse","HEAD"); clean(wt) + self.db.execute("UPDATE tasks SET submitted=?,candidate=NULL,main_base=NULL WHERE id=?",(sha,task_id)) + self.state(task_id,"submitted") + + def evidence(self, task_id, kind, sha, artifact, passed): + self.db.execute("INSERT INTO evidence(task_id,kind,commit_sha,artifact,passed) VALUES(?,?,?,?,?)",(task_id,kind,sha,str(artifact),int(passed))) + + def verify(self, task_id, wt, sha, kind): + c=json.loads(self.task(task_id)["contract"]) + clean(wt); require(git(wt,"rev-parse","HEAD")==sha, "試験対象SHA不一致") + records=[] + # Heavy build/E2E and shared settings are serialized by one deterministic lease. + with self.lease("heavy-tests","verify:"+task_id): + for vid in c["validation_ids"]: + v=self.config["validations"][vid] + runtime=self.runtime(task_id) + # Use the installed Runner's base interpreter, not the caller's + # shell PATH (Apple's python launcher may fail under Seatbelt). + python = str(Path(sys._base_executable).resolve()) + args=[a.replace("{python}",python) for a in v["argv"]] + extra = {"execution": v["execution"], "validation_id": vid, "verification_kind": kind, "verification_sha": sha} if "execution" in v else {} + artifact,code=self.process(task_id,"validation",None,args,wt,writable=(),timeout=v.get("timeout_seconds",self.config["policy"]["test_timeout_seconds"]), **extra) + records.append({"id":vid,"exit_code":code,"artifact":str(artifact),"passed":code==0}) + # Successful tests must leave the exact source tree untouched. + clean(wt); require(git(wt,"rev-parse","HEAD")==sha,"試験がcommitを変更しました") + passed=all(r["passed"] for r in records) and len(records)==len(c["validation_ids"]) + report=self.root/"runs"/(uuid.uuid4().hex+"-"+kind+".json") + write_json(report,{"task_id":task_id,"commit":sha,"kind":kind,"checks":records,"passed":passed}) + self.evidence(task_id,kind,sha,report,passed) + require(passed,f"test_failed: {report}") + return report + + def detached(self, name, sha): + path=self.worktrees/name + require(not path.exists(), f"checkout既存: {path}") + git(self.repo,"worktree","add","--detach",str(path),sha) + return path + + def validate(self, task_id): + task=self.task(task_id); require(task["state"]=="submitted", "提出済みからだけ検証できます") + self.state(task_id,"validating") + wt=self.detached(task_id+"-validation-"+uuid.uuid4().hex[:8],task["submitted"]) + try: + self.check_scope(task_id,wt,task["submitted"]) + self.verify(task_id,wt,task["submitted"],"validation") + self.state(task_id,"review_pending") + except (GateError,OSError) as exc: + self.state(task_id,"repair_pending",str(exc)); raise + + def review(self, task_id): + task=self.task(task_id); require(task["state"] in ("review_pending","blocked","reviewed","approval_pending"), "機械検証合格後、オーナー承認前にレビューできます") + require(task["submitted"], "提出コミットがありません") + self.has_evidence(task_id,"validation",task["submitted"]) + self.db.execute("UPDATE tasks SET candidate=NULL,main_base=NULL WHERE id=?", (task_id,)) + self.db.execute("DELETE FROM approvals WHERE task_id=?", (task_id,)) + self.state(task_id,"review_pending") + wt=self.detached(task_id+"-review-"+uuid.uuid4().hex[:8],task["submitted"]) + schema=read_json(CONTROL/"schemas/result.json") + schema["properties"]["task_id"]["enum"]=[task_id] + schema["properties"]["commit"]["enum"]=[task["submitted"]] + try: + value,artifact=self.llm(task_id,"reviewer","claude",self.packet(task_id,"reviewer",task["submitted"]),wt,schema) + c=json.loads(task["contract"]) + passed=value["status"]=="pass" and set(value["acceptance_ids"])==set(c["acceptance_ids"]) and not any(f["severity"] in ("critical","high","medium") for f in value["findings"]) + self.evidence(task_id,"review",task["submitted"],artifact/"result.json",passed) + self.state(task_id,"reviewed" if passed else "repair_pending",None if passed else value["summary"]) + require(passed,"review_failed") + except (GateError,ValueError,OSError) as exc: + if self.task(task_id)["state"]=="review_pending": self.state(task_id,"blocked",str(exc)) + raise + + def has_evidence(self, task_id, kind, sha): + rows=self.db.execute("SELECT artifact FROM evidence WHERE task_id=? AND kind=? AND commit_sha=? AND passed=1",(task_id,kind,sha)).fetchall() + require(rows, f"必要な証跡がありません: {kind} {sha}") + require(all(Path(r[0]).exists() for r in rows), "証跡ファイルがありません") + + def prepare_integration(self, task_id): + task=self.task(task_id); require(task["state"] in ("reviewed", "blocked"), "レビュー合格後に統合候補を作成できます") + self.has_evidence(task_id,"review",task["submitted"]) + self.has_evidence(task_id,"validation",task["submitted"]) + with self.lease("integration","candidate:"+task_id): + main=git(self.repo,"rev-parse","main") + c=json.loads(task["contract"]) + current_spec=git(self.repo,"show",f"{main}:{c['spec_ref'].split('#')[0]}") + require(hashlib.sha256(current_spec.encode()).hexdigest()==c["spec_sha256"], "mainの仕様変更: タスク再計画が必要") + wt=self.detached(task_id+"-integration-"+uuid.uuid4().hex[:8],main) + try: + git(wt,"-c","user.name=Agent Control Runner","-c","user.email=agent-control@localhost","-c","core.hooksPath=/dev/null","-c","commit.gpgsign=false", + "merge","--no-ff","--no-edit",task["submitted"]) + candidate=git(wt,"rev-parse","HEAD") + self.verify(task_id,wt,candidate,"integration") + self.db.execute("UPDATE tasks SET candidate=?,main_base=? WHERE id=?",(candidate,main,task_id)) + self.state(task_id,"approval_pending") + except (GateError,OSError) as exc: + self.state(task_id,"blocked",str(exc)); raise + + def approve(self, task_id, candidate, main_base, owner): + task=self.task(task_id) + require(task["state"]=="approval_pending", "承認待ちではありません") + require(candidate==task["candidate"] and main_base==task["main_base"], "承認対象SHA不一致") + require(git(self.repo,"rev-parse","main")==main_base,"main更新: 統合候補の再検証が必要") + require(owner.strip(), "オーナー名が必要") + self.has_evidence(task_id,"integration",candidate) + self.db.execute("INSERT OR REPLACE INTO approvals VALUES(?,?,?,?,?)",(task_id,candidate,main_base,owner,time.time())) + self.state(task_id,"approved") + + def integrate(self, task_id): + task=self.task(task_id); require(task["state"]=="approved", "owner_approval_required") + approval=self.db.execute("SELECT * FROM approvals WHERE task_id=?",(task_id,)).fetchone() + require(approval and approval["candidate"]==task["candidate"] and approval["main_base"]==task["main_base"],"承認不一致") + with self.lease("integration","integrate:"+task_id): + clean(self.repo) + require(git(self.repo,"branch","--show-current")=="main","正本checkoutをmainにしてください") + if git(self.repo,"rev-parse","main")!=task["main_base"]: + self.db.execute("DELETE FROM approvals WHERE task_id=?",(task_id,)) + self.state(task_id,"reviewed","main_changed: 承認無効、統合候補を作り直してください") + raise GateError("main_changed") + for kind,sha in [("validation",task["submitted"]),("review",task["submitted"]),("integration",task["candidate"])]: + self.has_evidence(task_id,kind,sha) + wt=self.detached(task_id+"-final-"+uuid.uuid4().hex[:8],task["candidate"]) + self.verify(task_id,wt,task["candidate"],"final") + require(git(self.repo,"rev-parse","main")==task["main_base"],"main_changed during tests") + git(self.repo,"-c","core.hooksPath=/dev/null","merge","--ff-only",task["candidate"]) + require(git(self.repo,"rev-parse","main")==task["candidate"],"統合SHA不一致") + self.state(task_id,"completed") + self.db.execute("DELETE FROM locks WHERE task_id=?",(task_id,)) + write_json(self.root/"traceability"/(task_id+".json"),{"contract":json.loads(task["contract"]),"implementation_commit":task["submitted"], + "integration_commit":task["candidate"],"owner":approval["owner"],"evidence":[dict(r) for r in self.db.execute("SELECT * FROM evidence WHERE task_id=?",(task_id,))]}) + + def repair(self, task_id): + task=self.task(task_id); c=json.loads(task["contract"]) + require(task["state"] in ("repair_pending","blocked"),"修正対象の状態ではありません") + require(not self.db.execute("SELECT 1 FROM runs WHERE task_id=? AND status='running'",(task_id,)).fetchone(),"実行中です") + require(task["repairs"] < c["max_repair_attempts"], "repair_limit") + self.db.execute("UPDATE tasks SET repairs=repairs+1,candidate=NULL,main_base=NULL WHERE id=?",(task_id,)) + self.db.execute("DELETE FROM approvals WHERE task_id=?",(task_id,)) + self.state(task_id,"repair_ready") + + def stop(self, task_id): + rows=self.db.execute("SELECT * FROM runs WHERE task_id=? AND status='running'",(task_id,)).fetchall() + require(rows,"実行中プロセスがありません") + for r in rows: + require(r["pid"],"プロセス起動中。少し待ってください") + try: os.killpg(r["pid"],signal.SIGTERM) + except ProcessLookupError: pass + deadline=time.monotonic()+3 + while time.monotonic() time.time() * 1000 + except (KeyError, TypeError, ValueError): + raise GateError("authentication_missing: Claude認証データの形式が不正です") from None + require(valid, "authentication_expired: claude auth loginで更新してください") + return {"CLAUDE_CODE_OAUTH_TOKEN": token} + + +@contextlib.contextmanager +def agy_session_auth(runtime, provider_root=None): + """Version-pinned native file fallback; never open personal Keychains. + + Only the Antigravity consumer access token is copied to a private task + file. Native fallback paths are temporary links, serialized across all + projects. Existing user files are never overwritten. No refresh token is + shared with the worker, and all temporary auth files are removed on exit. + """ + version = subprocess.run([executable("agy"), "--version"], capture_output=True, + text=True, timeout=15, env=limited_environment()) + require(version.stdout.strip() == "1.3.2", "agy_auth_adapter_version: CLI更新後の互換性検証が必要です") + broker = APP_SUPPORT / "auth-broker" + broker.mkdir(parents=True, exist_ok=True, mode=0o700) + with (broker / "agy.lock").open("a") as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise GateError("resource_busy: Antigravity認証アダプター使用中") from None + root = Path(provider_root) if provider_root else Path.home() / ".gemini/antigravity-cli" + links = [root / "antigravity-oauth-token", root / "cache/antigravity-keyring-unavailable", root / "bin/agentapi"] + record = broker / "agy-session.json" + if record.exists(): + previous = read_json(record) + for pid in (previous["owner_pid"], previous.get("child_pid")): + if pid: + try: + os.kill(pid, 0) + except ProcessLookupError: + pass + else: + raise GateError("resource_busy: 以前のAntigravityプロセスが残っています。stopで停止してください") + require(previous["links"] == [str(p) for p in links], "agy_auth_conflict: 認証リンクの場所が変わっています") + for link, value in zip(links, previous["files"]): + source = Path(value) + require((source.parent.name == "provider-auth" and source.name in ("access.json", "keyring-unavailable")) or (source.name == "agentapi" and source.parent == Path(previous["files"][0]).parent.parent), "agy_auth_conflict: 回復記録が不正です") + if link.is_symlink() and link.readlink() == source: + link.unlink() + else: + require(not link.exists() and not link.is_symlink(), "agy_auth_conflict: 認証リンクの変更を検出しました") + source.unlink(missing_ok=True) + record.unlink() + require(not any(p.exists() or p.is_symlink() for p in links), "agy_auth_conflict: 既存のファイル認証設定を保護して停止しました") + def load(): + p = subprocess.run(["/usr/bin/security", "find-generic-password", "-s", "gemini", + "-a", "antigravity", "-w"], capture_output=True, text=True, + timeout=15, env=limited_environment()) + require(p.returncode == 0, "authentication_missing: Antigravity専用の認証項目がありません") + raw = p.stdout.strip() + try: + if raw.startswith("go-keyring-base64:"): + raw = base64.b64decode(raw.split(":", 1)[1]).decode() + elif raw.startswith("go-keyring-encoded:"): + raw = bytes.fromhex(raw.split(":", 1)[1]).decode() + data = json.loads(raw) + token = data["token"] + expiry = datetime.fromisoformat(token["expiry"].replace("Z", "+00:00")) + valid = isinstance(token["access_token"], str) and bool(token["access_token"]) + except (KeyError, ValueError, TypeError): + raise GateError("authentication_missing: Antigravity認証データの形式が不正です") from None + require(data.get("auth_method") == "consumer" and valid, "api_auth_disabled: 個人Googleアカウントの認証が必要です") + return token, expiry + token, expiry = load() + # Antigravity refreshes several minutes before the nominal expiry. + # Renew through the native CLI before stripping the refresh token. + if expiry <= datetime.now(timezone.utc) + timedelta(minutes=6): + # /model is a built-in command, not an inference prompt. Native CLI + # renews its own Keychain item in this trusted, tool-free probe. + p = subprocess.run([executable("agy"), "-p", "/model", "--output-format", "json", + "--print-timeout", "20s", "--log-file", str(Path(runtime)/"auth-renewal.log")], + capture_output=True, text=True, timeout=30, env=limited_environment()) + require(p.returncode == 0, "authentication_expired: Antigravityログインの更新が必要です") + token, expiry = load() + require(expiry > datetime.now(timezone.utc), "authentication_expired: Antigravityログインの更新が必要です") + private = Path(runtime) / "provider-auth" + private.mkdir(mode=0o700) + files = [private / "access.json", private / "keyring-unavailable", Path(runtime) / "agentapi"] + try: + write_json(record, {"owner_pid": os.getpid(), "links": [str(p) for p in links], "files": [str(p) for p in files]}) + # Select fields explicitly: refresh_token never reaches task storage. + fd = os.open(files[0], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, "w") as out: + json.dump({"auth_method": "consumer", "token": {k: token[k] for k in ("access_token", "token_type", "expiry")}}, out) + files[1].write_text("true"); files[1].chmod(0o600) + files[2].touch(mode=0o700, exist_ok=False) + for link, source in zip(links, files): + link.parent.mkdir(parents=True, exist_ok=True) + link.symlink_to(source) + yield record + finally: + for link, source in zip(links, files): + if link.is_symlink() and link.readlink() == source: + link.unlink() + source.unlink(missing_ok=True) + private.rmdir() + record.unlink(missing_ok=True) + + +def check_schema(value, schema): + """Small strict validator for the schemas we ship (also used locally after CLI output).""" + typ = schema.get("type") + types = {"object": dict, "array": list, "string": str, "integer": int, "boolean": bool} + require(typ not in types or isinstance(value, types[typ]), f"結果schemaの型違反: {typ}") + require("enum" not in schema or value in schema["enum"], "結果schemaのenum違反") + if isinstance(value, dict) and typ == "object": + require(set(schema.get("required", [])).issubset(value), "結果schemaの必須項目不足") + properties = schema.get("properties", {}) + require(schema.get("additionalProperties", True) or set(value).issubset(properties), "結果schemaの未定義項目") + for key, item in value.items(): + if key in properties: + check_schema(item, properties[key]) + if isinstance(value, list) and typ == "array": + for item in value: + check_schema(item, schema["items"]) + + +def seatbelt(worktree, runtime, writable, service=None): + """Outer macOS policy enforces actual paths and protects runner/approval/auth data. + + Codex provider auth is available to its CLI runtime. Claude gets only a + process-local access token; personal Keychains stay denied for all roles. + Use only trusted repositories, as documented in README.md. + """ + require(Path("/usr/bin/sandbox-exec").exists(), "isolation_missing: macOS sandbox-execが必要です") + quote = lambda p: json.dumps(str(Path(p).resolve()), ensure_ascii=False) + lines = ["(version 1)", "(allow default)", "(deny file-write*)"] + # CLI binary and provider state are the only home reads outside this task. + readable = [worktree, runtime, Path.home()/".local/bin", Path.home()/".local/share/claude"] + if service == "codex": + readable += [Path.home()/".codex/auth.json",Path.home()/".codex/tmp",Path.home()/".codex/installation_id"] + lines.append(f"(allow file-write* (subpath {quote(Path.home()/'.codex/tmp')}))") + lines.append(f"(allow file-write* (literal {quote(Path.home()/'.codex/installation_id')}))") + if service == "claude": + readable += [Path.home()/".claude.json"] + if service == "agy": + readable += [Path.home()/".gemini/antigravity-cli",Path.home()/".gemini/antigravity/antigravity_state.pbtxt", + Path.home()/".gemini/antigravity/installation_id",Path.home()/".gemini/google_accounts.json", + Path.home()/".gemini/antigravity-cli/settings.json",Path.home()/".gemini/config/projects/default-cli-project.json"] + lines.append(f"(allow file-write* (literal {quote(Path.home()/'.gemini/antigravity-cli/cache/default_project_id.txt')}))") + exceptions = set() + for p in readable: + kind = "literal" if p.is_file() else "subpath" + exceptions.add(f"({kind} {quote(p)})") + # getcwd and create_dir_all inspect ancestor directories. Permit directory + # traversal/listing, without reopening any sibling file contents. + for parent in Path(p).resolve().parents: + if parent.is_relative_to(Path.home()): + exceptions.add(f"(literal {quote(parent)})") + # Worktree .git points into shared metadata: permit read, never write. + common = Path(git(worktree, "rev-parse", "--git-common-dir")) + if not common.is_absolute(): + common = (worktree / common).resolve() + exceptions.add(f"(subpath {quote(common)})") + lines.append(f"(deny file-read-data (require-all (subpath {quote(Path.home())}) " + + " ".join(f"(require-not {x})" for x in sorted(exceptions)) + "))") + for p, recursive in writable: + lines.append(f"(allow file-write* ({'subpath' if recursive else 'literal'} {quote(p)}))") + lines.append(f"(allow file-write* (subpath {quote(runtime)}))") + if service == "agy": + lines.append('(deny file-write* (regex "/provider-auth(/|$)"))') + lines.append('(allow file-read* file-write* (literal "/dev/ptmx") (regex #"^/dev/ttys[0-9]+$"))') + lines.append('(allow file-write* (literal "/dev/null") (literal "/dev/tty") (literal "/dev/stdout") (literal "/dev/stderr"))') + # Higher-priority explicit denies cannot be reopened by a workspace symlink. + for root in [CONTROL, APP_SUPPORT, Path.home()/".ssh", Path.home()/".aws", Path.home()/".azure", + Path.home()/".config/gcloud", Path.home()/"Library/Keychains"]: + if root == APP_SUPPORT: + # runtime is outside state root; nothing in state is visible to agents. + pass + lines.append(f"(deny file-read* file-write* (subpath {quote(root)}))") + lines.append(f"(deny file-write* (subpath {quote(common)}))") + for p in worktree.rglob("*"): + if secret_path(str(p.relative_to(worktree))): + lines.append(f"(deny file-read* file-write* (literal {quote(p)}))") + return "\n".join(lines) + + +class Runner: + def __init__(self, project, state_root=APP_SUPPORT): + require(ID.fullmatch(project), "project IDが不正です") + self.root = Path(state_root).resolve() / project + require((self.root / "config.json").exists(), f"project未登録: {project}") + self.config = read_json(self.root / "config.json") + self.repo = Path(self.config["repo"]) + self.worktrees = Path(self.config["worktree_root"]) + self.db = sqlite3.connect(self.root / "state.sqlite", timeout=10, isolation_level=None) + self.db.row_factory = sqlite3.Row + self.db.execute("PRAGMA foreign_keys=ON") + self.db.execute("CREATE TABLE IF NOT EXISTS allocations(task_id TEXT PRIMARY KEY,port INTEGER UNIQUE,value TEXT NOT NULL)") + require(self.meta("config_digest") == digest(self.config), "登録後のpolicy変更を検出。別projectとして再登録してください") + self.baseline = read_json(self.root / "acceptance_baseline.json") + require(self.meta("baseline_digest") == digest(self.baseline), "受入分母の無断変更を検出しました") + + @staticmethod + def init(project, repo, baseline, validations, state_root=APP_SUPPORT, worktree_root=None, policy=None): + require(ID.fullmatch(project), "project IDが不正です") + repo = Path(repo).resolve() + root = Path(state_root).resolve() / project + require(not root.exists(), "同じprojectは既に登録されています") + require(Path(git(repo, "rev-parse", "--show-toplevel")).resolve() == repo, "repo rootを指定してください") + require(git(repo, "rev-parse", "--is-shallow-repository") == "false", "shallow cloneは未対応です") + git(repo, "rev-parse", "main^{commit}") + require(isinstance(baseline, list) and baseline, "受入項目を登録してください") + ids = [a["id"] for a in baseline] + require(len(set(ids)) == len(ids), "受入ID重複") + for a in baseline: + require(ID.fullmatch(a["id"]) and a.get("requirement_id") and a.get("text") and a.get("validation_ids"), "受入項目不足") + require(set(a["validation_ids"]).issubset(validations), "受入項目に未知の試験ID") + for key, val in validations.items(): + require(ID.fullmatch(key) and val.get("argv") and all(isinstance(x, str) for x in val["argv"]), "試験コマンドはargv配列が必要") + wt = Path(worktree_root or (repo.parent / (repo.name + "-worktrees"))).resolve() + require(not wt.is_relative_to(repo), "worktree rootはrepo外にしてください") + p = policy or read_json(CONTROL / "policies/default.json") + require(p["api_fallback"] is False and p["owner_approval_required"] is True, "初期運用のAPI/承認policy違反") + config = {"project": project, "repo": str(repo), "worktree_root": str(wt), "policy": p, "validations": validations} + root.mkdir(parents=True, mode=0o700) + os.chmod(root, 0o700) + write_json(root / "config.json", config) + write_json(root / "acceptance_baseline.json", baseline) + db = sqlite3.connect(root / "state.sqlite") + db.executescript(""" + CREATE TABLE meta(key TEXT PRIMARY KEY,value TEXT NOT NULL); + CREATE TABLE tasks(id TEXT PRIMARY KEY,contract TEXT NOT NULL,state TEXT NOT NULL, + worktree TEXT,submitted TEXT,candidate TEXT,main_base TEXT,reason TEXT,repairs INTEGER NOT NULL DEFAULT 0); + CREATE TABLE locks(resource TEXT PRIMARY KEY,task_id TEXT NOT NULL); + CREATE TABLE runs(id TEXT PRIMARY KEY,task_id TEXT,role TEXT,service TEXT,status TEXT, + pid INTEGER,started REAL,finished REAL,exit_code INTEGER,artifact TEXT,api_cost_usd REAL, + estimated_cost_usd REAL,subscription_usage REAL,usage_json TEXT); + CREATE TABLE evidence(id INTEGER PRIMARY KEY,task_id TEXT,kind TEXT,commit_sha TEXT,artifact TEXT,passed INTEGER); + CREATE TABLE approvals(task_id TEXT PRIMARY KEY,candidate TEXT,main_base TEXT,owner TEXT,created REAL); + CREATE TABLE events(id INTEGER PRIMARY KEY,task_id TEXT,event TEXT,data TEXT,created REAL); + """) + db.executemany("INSERT INTO meta VALUES(?,?)", [("config_digest",digest(config)),("baseline_digest",digest(baseline))]) + db.commit(); db.close() + return Runner(project, state_root) + + def meta(self, key): + return self.db.execute("SELECT value FROM meta WHERE key=?", (key,)).fetchone()[0] + + def task(self, task_id): + row = self.db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone() + require(row is not None, f"未知のタスク: {task_id}") + return dict(row) + + def event(self, task_id, name, data): + self.db.execute("INSERT INTO events(task_id,event,data,created) VALUES(?,?,?,?)", + (task_id, name, json.dumps(data, ensure_ascii=False), time.time())) + + def state(self, task_id, state, reason=None): + self.db.execute("UPDATE tasks SET state=?,reason=? WHERE id=?", (state, reason, task_id)) + self.event(task_id, state, {"reason": reason}) + + def add(self, contract): + check_schema(contract,read_json(CONTROL/"schemas/task.json")) + required = {"id", "title", "requirement_ids", "acceptance_ids", "spec_ref", "base_commit", "worker", + "allowed_paths", "depends_on", "resource_locks", "validation_ids", "max_repair_attempts"} + require(required.issubset(contract), f"タスク必須項目不足: {sorted(required-set(contract))}") + require(ID.fullmatch(contract["id"]), "task ID不正") + require(contract["worker"] in self.config["policy"]["allowed_services"] and contract["worker"] in ("codex","agy"), "未許可の実装サービス") + require(contract["allowed_paths"] and contract["acceptance_ids"] and contract["requirement_ids"], "変更範囲・受入・要件が必要") + require(len(set(contract["acceptance_ids"])) == len(contract["acceptance_ids"]), "受入項目重複") + require(all(ID.fullmatch(x) for x in contract["resource_locks"]), "resource lock名は英数字・ハイフン・アンダースコアのみ") + for p in contract["allowed_paths"]: + require(safe_relative(path_scope(p)) and not secret_path(p) and not any(x in path_scope(p) for x in "*?[]"), "pathは相対の具体的ファイルまたはdir/**のみ") + require(path_scope(p) not in ("AGENTS.md","CLAUDE.md","docs/spec") and not path_scope(p).startswith("docs/spec/"), "仕様・共通規約の変更は禁止") + require(set(contract["validation_ids"]).issubset(self.config["validations"]), "未知の検証ID") + for a in contract["acceptance_ids"]: + item = next((x for x in self.baseline if x["id"] == a), None) + require(item and item["requirement_id"] in contract["requirement_ids"], "受入と要件の対応不一致") + require(set(item["validation_ids"]).issubset(contract["validation_ids"]), "受入に必要な検証が不足") + spec = contract["spec_ref"].split("#")[0] + require(safe_relative(spec) and not secret_path(spec), "仕様path不正") + base = git(self.repo, "rev-parse", contract["base_commit"] + "^{commit}") + require(base == contract["base_commit"], "base_commitは完全なcommit SHAが必要") + git(self.repo, "merge-base", "--is-ancestor", base, "main") + spec_text = git(self.repo, "show", f"{base}:{spec}") + for rid in contract["requirement_ids"]: + require(rid in spec_text, f"仕様に要件IDがありません: {rid}") + require(0 <= contract["max_repair_attempts"] <= self.config["policy"]["max_repair_attempts"], "修正上限超過") + for dep in contract["depends_on"]: + self.task(dep) + require(contract["id"] not in contract["depends_on"], "自己依存") + existing = [json.loads(r[0]) for r in self.db.execute("SELECT contract FROM tasks")] + require(not any(set(c["acceptance_ids"]) & set(contract["acceptance_ids"]) for c in existing), "同一受入項目の重複タスクは禁止") + c = dict(contract, spec_sha256=hashlib.sha256(spec_text.encode()).hexdigest()) + self.db.execute("INSERT INTO tasks(id,contract,state) VALUES(?,?,?)", (c["id"],json.dumps(c,ensure_ascii=False),"ready")) + self.event(c["id"], "registered", c) + + @contextlib.contextmanager + def lease(self, resource, owner): + try: + self.db.execute("INSERT INTO locks VALUES(?,?)", (resource,owner)) + except sqlite3.IntegrityError: + raise GateError(f"resource_busy: {resource}") + try: + yield + finally: + self.db.execute("DELETE FROM locks WHERE resource=? AND task_id=?", (resource,owner)) + + def prepare(self, task_id): + task = self.task(task_id); c = json.loads(task["contract"]) + require(task["state"] in ("ready", "repair_ready"), "ready/repair_readyからだけ開始できます。修正はrepairで回数を記録してください") + for dep in c["depends_on"]: + require(self.task(dep)["state"] == "completed", f"dependency_pending: {dep}") + self.db.execute("BEGIN IMMEDIATE") + try: + for other in self.db.execute("SELECT t.contract FROM tasks t WHERE t.id IN (SELECT task_id FROM locks) AND t.id!=?",(task_id,)): + other_c = json.loads(other[0]) + require(not any(overlap(a,b) for a in c["allowed_paths"] for b in other_c["allowed_paths"]), "file_scope_busy") + for resource in c["resource_locks"] + ["task:" + task_id]: + row = self.db.execute("SELECT task_id FROM locks WHERE resource=?",(resource,)).fetchone() + require(not row or row[0] == task_id, f"resource_busy: {resource}") + self.db.execute("INSERT OR IGNORE INTO locks VALUES(?,?)", (resource,task_id)) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK"); raise + if not task["worktree"]: + wt = self.worktrees / (task_id + "-implement") + self.worktrees.mkdir(parents=True, exist_ok=True) + try: + git(self.repo,"worktree","add","--detach",str(wt),c["base_commit"]) + self.db.execute("UPDATE tasks SET worktree=? WHERE id=?",(str(wt),task_id)) + except BaseException: + self.db.execute("DELETE FROM locks WHERE task_id=?",(task_id,)); raise + else: + wt = Path(task["worktree"]) + runtime = self.runtime(task_id) + for p in c["allowed_paths"]: + actual = wt / path_scope(p) + require(actual.resolve().is_relative_to(wt.resolve()), "symlinkが範囲外を参照しています") + if p.endswith("/**"): + actual.mkdir(parents=True, exist_ok=True) + write_json(runtime/"allocation.json", self.allocation(task_id)) + return wt + + def runtime(self, task_id): + path = self.worktrees / ".runtime" / task_id + path.mkdir(parents=True, exist_ok=True, mode=0o700) + return path + + def allocation(self, task_id): + r = self.runtime(task_id) + existing = r / "allocation.json" + if existing.exists(): + value=read_json(existing) + self.db.execute("INSERT OR IGNORE INTO allocations VALUES(?,?,?)",(task_id,value["port"],json.dumps(value))) + return value + self.db.execute("BEGIN IMMEDIATE") + try: + registered=self.db.execute("SELECT value FROM allocations WHERE task_id=?",(task_id,)).fetchone() + if registered: + value=json.loads(registered[0]) + else: + used={row[0] for row in self.db.execute("SELECT port FROM allocations")} + used.update(read_json(p)["port"] for p in (self.worktrees/".runtime").glob("*/allocation.json")) + port=4200 + while True: + if port not in used: + with socket.socket() as s: + try: s.bind(("127.0.0.1",port)); break + except OSError: pass + port+=1 + value={"port":port,"url":f"http://127.0.0.1:{port}","test_db":str(r/"test.sqlite"), + "build_dir":str(r/"build"),"browser_profile":str(r/"chrome-profile"),"tmp":str(r/"tmp")} + self.db.execute("INSERT INTO allocations VALUES(?,?,?)",(task_id,port,json.dumps(value))) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK");raise + write_json(existing,value) + return value + + def packet(self, task_id, role, commit=None, worktree=None): + task = self.task(task_id); c=json.loads(task["contract"]) + revision = commit or c["base_commit"] + spec = c["spec_ref"].split("#")[0] + spec_text = git(self.repo,"show",f"{revision}:{spec}") + require(hashlib.sha256(spec_text.encode()).hexdigest() == c["spec_sha256"], "仕様が登録時から変わっています") + body = {"contract": c, "commit": revision, "spec": spec_text, + "acceptance": [a for a in self.baseline if a["id"] in c["acceptance_ids"]], + "rules": (CONTROL/"AGENTS.md").read_text(), "allocation":self.allocation(task_id)} + if role == "reviewer": + body["diff"] = git(self.repo,"diff","--no-ext-diff",c["base_commit"],revision) + names = git(self.repo,"ls-tree","-r","--name-only",revision).splitlines() + body["sources"] = {p: git(self.repo,"show",f"{revision}:{p}") for p in names + if allowed(p,c["allowed_paths"]) and not secret_path(p)} + if (self.config["project"] == "the-tree-p24-03-code" and task_id == "P24-03" + and c["base_commit"] == "286730f88ce430a1cbe1501d40361188f832d235" + and revision == "4cefcff849d4dc1211caa5066e24bfae666cc2f4"): + path = "docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json" + focus = ["tests/ui/" + name + ".gd" for name in + ("test_actor_selection", "test_selection_lifecycle", "test_selection_autosave", "test_selection_departure")] + require(path in body["sources"], "marker source absent") + body["sources"][path] = marker_context(self.repo, c["base_commit"], revision, path, focus) + # Include committed test scripts/directories named in registered + # validation argv, even when workers cannot edit those tests. + test_paths = {arg for vid in c["validation_ids"] for arg in self.config["validations"][vid]["argv"] + if safe_relative(arg) and not secret_path(arg)} + body["validation_sources"] = {p: git(self.repo,"show",f"{revision}:{p}") for p in names + if not secret_path(p) and any(p == x or p.startswith(x.rstrip("/") + "/") for x in test_paths)} + body["tests"] = [read_json(r[0]) for r in self.db.execute("SELECT artifact FROM evidence WHERE task_id=? AND kind='validation' AND commit_sha=? AND passed=1",(task_id,revision))] + require(len(json.dumps(body).encode()) < 500_000, "packetが大き過ぎます。タスクを小さく分割してください") + prompt = (CONTROL/"prompts"/(role+".txt")).read_text() + "\nDATA:\n" + json.dumps(body,ensure_ascii=False) + return prompt + + def provider_argv(self, service, role, runtime, schema): + cli = executable(service) + schema_file = runtime / "result.schema.json"; write_json(schema_file,schema) + model = self.config["policy"]["models"].get(service) + if service == "codex": + # macOS cannot install a second Seatbelt profile inside our process. + # The outer profile is mandatory and restricts writes more narrowly + # than workspace-write. No unsandboxed process path exists here. + args=[cli,"exec","--json","--ignore-user-config","--ephemeral","--sandbox","danger-full-access", + "-c","approval_policy=\"never\"","-c",f"sqlite_home={json.dumps(str(runtime/'sqlite'))}", + "-c",f"log_dir={json.dumps(str(runtime/'logs'))}","--output-schema",str(schema_file),"--output-last-message",str(runtime/"answer.json"),"-"] + elif service == "claude": + # No tools for commander and reviewer; all required data is in the packet. + args=[cli,"-p","--safe-mode","--restricted","--output-format","json","--tools","","--strict-mcp-config","--mcp-config",'{"mcpServers":{}}', + "--setting-sources","","--no-session-persistence","--permission-mode","plan","--json-schema",json.dumps(schema)] + budget=self.config["policy"].get("claude_max_budget_usd") + if budget is not None: args += ["--max-budget-usd",str(budget)] + elif service == "agy": + args=[cli,"--input-format","stream-json","--output-format","stream-json","--json-schema",str(schema_file), + "--log-file",str(runtime/"agy.log"),"--print-timeout",f'{self.config["policy"]["timeout_seconds"]}s',"--sandbox","--mode","accept-edits","--disable-slash-commands"] + else: + raise GateError("未許可のサービス") + if model: args += ["--model",model] + return args + + def trusted_self_test(self, task_id, role, service, argv, cwd, input_text, writable, timeout, validation_id, verification_kind=None, verification_sha=None): + require(self.config["project"] == "the-tree-runner-p24-review-packet" and task_id == "RP24-01" + and role == "validation" and service is None and input_text is None and not writable + and validation_id == "runner-unit" and type(timeout) is int and timeout == 180, + "trusted self-test context rejected") + require(digest(self.config) == self.meta("config_digest") + and read_json(self.root / "config.json") == self.config, "trusted config changed") + task = self.task(task_id); c = json.loads(task["contract"]) + v = self.config["validations"][validation_id] + require(v == {"execution": "trusted_runner_self_test", "argv": SELF_TEST_ARGV, "timeout_seconds": 180}, + "trusted validation config rejected") + expected = [a.replace("{python}", str(Path(sys._base_executable).resolve())) for a in SELF_TEST_ARGV] + require(argv == expected and c["id"] == task_id and c["validation_ids"] == [validation_id] + and c["spec_ref"] == "docs/spec/RP24-01.json" and c["spec_sha256"] == SELF_TEST_SPEC_SHA + and c["base_commit"] == SELF_TEST_BASE + and c["requirement_ids"] == [task_id] and c["acceptance_ids"] == [task_id] + and sorted(c["allowed_paths"]) == ["runner/core.py", "tests/test_runner.py"], "trusted task rejected") + submitted = task["submitted"] + phases = {"validation": ("validating",), "integration": ("reviewed", "blocked"), "final": ("approved",)} + require(submitted and verification_kind in phases and task["state"] in phases[verification_kind] + and verification_sha, "trusted verification phase rejected") + sha = verification_sha + if verification_kind == "validation": + require(sha == submitted, "trusted submission SHA rejected") + else: + if verification_kind == "final": + require(sha == task["candidate"], "trusted approved SHA rejected") + approval = self.db.execute("SELECT * FROM approvals WHERE task_id=?", (task_id,)).fetchone() + require(approval and approval["candidate"] == sha and approval["main_base"] == task["main_base"], + "trusted approval rejected") + self.has_evidence(task_id, "validation", submitted) + self.has_evidence(task_id, "review", submitted) + require(git(self.repo, "merge-base", submitted, sha) == submitted, "trusted submitted ancestry rejected") + require(git(self.repo, "rev-parse", sha + "^{tree}") == git(self.repo, "rev-parse", submitted + "^{tree}"), + "trusted submitted content rejected") + clean(cwd) + require(git(cwd, "rev-parse", "HEAD") == sha and not git(cwd, "branch", "--show-current"), "trusted checkout rejected") + require(Path(cwd).resolve().parent == self.worktrees.resolve() + and git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir") + == git(self.repo, "rev-parse", "--path-format=absolute", "--git-common-dir"), "trusted worktree rejected") + self.check_scope(task_id, Path(cwd), sha) + spec = git(cwd, "show", sha + ":" + c["spec_ref"]) + require(hashlib.sha256(spec.encode()).hexdigest() == SELF_TEST_SPEC_SHA, "trusted spec changed") + return {"execution": "trusted_runner_self_test", "target_commit": sha, "spec_sha256": c["spec_sha256"], + "contract_digest": digest(c), "config_digest": digest(self.config), "validation_id": validation_id, + "verification_kind": verification_kind, "submitted_commit": submitted} + + def process(self, task_id, role, service, argv, cwd, input_text=None, writable=(), timeout=None, execution=None, validation_id=None, verification_kind=None, verification_sha=None): + require(execution in (None, "trusted_runner_self_test"), "unknown execution mode") + provenance = self.trusted_self_test(task_id, role, service, argv, cwd, input_text, writable, timeout, validation_id, verification_kind, verification_sha) if execution else {"execution": "sandbox"} + run_id=uuid.uuid4().hex; artifact=self.root/"runs"/run_id; artifact.mkdir(parents=True) + runtime=self.runtime(task_id)/run_id; runtime.mkdir(parents=True) + env=limited_environment(); alloc=self.allocation(task_id) + if service == "claude": + require(role in ("commander", "reviewer"), "Claudeの資格情報は参照専用の役割に限定") + env.update(claude_session_auth()) + Path(alloc["tmp"]).mkdir(parents=True,exist_ok=True) + env.update(MAC_CHROMIUM_TMPDIR=alloc["tmp"],PORT=str(alloc["port"]),TEST_DATABASE_PATH=alloc["test_db"],BUILD_OUTPUT_DIR=alloc["build_dir"], + BROWSER_PROFILE_DIR=alloc["browser_profile"],TMPDIR=alloc["tmp"],TMP=alloc["tmp"],TEMP=alloc["tmp"]) + sb=seatbelt(Path(cwd),self.runtime(task_id),writable,service) if not execution else "" + (artifact/"sandbox.sb").write_text(sb) + write_json(artifact/"command.json",{"argv":argv,"cwd":str(cwd),"env_names":sorted(env), **provenance}) + if input_text is not None: + (artifact/"packet.txt").write_text(input_text) + self.db.execute("BEGIN IMMEDIATE") + try: + policy=self.config["policy"] + if service: + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL AND status='running'").fetchone()[0] < policy["max_llm"], "llm_concurrency_limit") + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限") + if role in ("implementer","ui"): + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE role IN ('implementer','ui') AND status='running'").fetchone()[0] < policy["max_implementers"], "implementer_concurrency_limit") + self.db.execute("INSERT INTO runs(id,task_id,role,service,status,started,artifact) VALUES(?,?,?,?,?,?,?)", + (run_id,task_id,role,service,"running",time.time(),str(artifact))) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK"); raise + proc=None; code=None + try: + auth = agy_session_auth(runtime) if service == "agy" else contextlib.nullcontext() + with auth as auth_record, (artifact/"stdout.log").open("w") as out, (artifact/"stderr.log").open("w") as err: + proc=subprocess.Popen(argv if execution else ["/usr/bin/sandbox-exec","-p",sb,*argv],cwd=cwd,env=env, + stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,start_new_session=True) + # Node/Chrome inspect their output descriptors at startup. A + # descriptor to protected state logs is denied by Seatbelt. + # Drain pipes in the trusted Runner, keeping logs inaccessible. + def drain(source, target): + try: + for chunk in iter(lambda: source.read(8192), ""): + target.write(chunk); target.flush() + finally: + source.close() + readers = [threading.Thread(target=drain,args=(proc.stdout,out)), + threading.Thread(target=drain,args=(proc.stderr,err))] + proc.stdout = proc.stderr = None + for reader in readers: reader.start() + self.db.execute("UPDATE runs SET pid=? WHERE id=?",(proc.pid,run_id)) + if auth_record: + info = read_json(auth_record); info["child_pid"] = proc.pid; write_json(auth_record, info) + try: + proc.communicate(input_text,timeout=timeout or policy["timeout_seconds"]) + code=proc.returncode + except (subprocess.TimeoutExpired,KeyboardInterrupt): + os.killpg(proc.pid,signal.SIGTERM) + try: proc.wait(timeout=3) + except subprocess.TimeoutExpired: os.killpg(proc.pid,signal.SIGKILL); proc.wait() + code=124 + # Terminate any background descendants that outlived the foreground CLI. + try: os.killpg(proc.pid,signal.SIGTERM) + except ProcessLookupError: pass + try: os.killpg(proc.pid,signal.SIGKILL) + except ProcessLookupError: pass + for reader in readers: reader.join(timeout=5) + self.db.execute("UPDATE runs SET status=?,finished=?,exit_code=? WHERE id=?", + ("finished" if code == 0 else "failed",time.time(),code,run_id)) + except BaseException: + if proc and proc.poll() is None: + os.killpg(proc.pid,signal.SIGKILL); proc.wait() + self.db.execute("UPDATE runs SET status='failed',finished=? WHERE id=?",(time.time(),run_id)) + raise + return artifact, code + + def llm(self, task_id, role, service, prompt, cwd, schema): + require(service in self.config["policy"]["allowed_services"], "サービス送信未許可") + subscription_auth(service) + runtime=self.runtime(task_id)/("provider-"+uuid.uuid4().hex); runtime.mkdir() + args=self.provider_argv(service,role,runtime,schema) + c=json.loads(self.task(task_id)["contract"]) + writable=[] + if role in ("implementer","ui"): + writable=[(Path(cwd)/path_scope(p),p.endswith("/**")) for p in c["allowed_paths"]] + input_text=prompt + if service == "agy": + input_text=json.dumps({"event":"user","message":{"content":prompt}},ensure_ascii=False)+"\n" + artifact,code=self.process(task_id,role,service,args,cwd,input_text,writable) + err=(artifact/"stderr.log").read_text() + require(code == 0, f"provider_failed: exit={code}; log={artifact}") + raw=(artifact/"stdout.log").read_text() + usage=None; estimate=None + if service == "codex": + events=[json.loads(line) for line in raw.splitlines() if line.strip().startswith("{")] + require(not any(e.get("type") in ("error","turn.failed") for e in events), "provider_error_event") + usage=next((e.get("usage") for e in reversed(events) if e.get("type")=="turn.completed"),None) + require((runtime/"answer.json").exists(), "必要な構造化結果がありません") + value=read_json(runtime/"answer.json") + elif service == "claude": + envelope=json.loads(raw); require(not envelope.get("is_error"), "Claude error envelope") + value=envelope.get("structured_output") + if value is None: value=json.loads(envelope.get("result","")) + usage=envelope.get("usage"); estimate=envelope.get("total_cost_usd") + else: + events=[json.loads(line) for line in raw.splitlines() if line.strip().startswith("{")] + terminal=[e["result"] for e in events if e.get("event")=="result"] + require(terminal and terminal[-1].get("status")=="SUCCESS", "Antigravity terminal result不足") + require(not re.search(r"soft.denied|permission.*denied|approval.*required",err,re.I), "permission_denied: 必要な操作が拒否されました") + require(not any(e.get("step_update",{}).get("tool_info",{}).get("error") for e in events), "Antigravity tool error") + value=terminal[-1].get("structured_output") + if value is None: value=json.loads(terminal[-1].get("response","")) + usage=terminal[-1].get("usage") + check_schema(value,schema) + write_json(artifact/"result.json",value) + self.db.execute("UPDATE runs SET usage_json=?,estimated_cost_usd=? WHERE artifact=?",(json.dumps(usage),estimate,str(artifact))) + return value,artifact + + def run(self, task_id): + wt=self.prepare(task_id); c=json.loads(self.task(task_id)["contract"]) + self.state(task_id,"implementing") + try: + role="ui" if c["worker"]=="agy" else "implementer" + schema=read_json(CONTROL/"schemas/result.json") + schema["properties"]["task_id"]["enum"]=[task_id] + schema["properties"]["commit"]["enum"]=[c["base_commit"]] + value,artifact=self.llm(task_id,role,c["worker"],self.packet(task_id,role),wt,schema) + require(value["status"]=="submitted", f"worker_blocked: {value['summary']}") + self.submit(task_id) + self.event(task_id,"worker_result",{"artifact":str(artifact)}) + except (GateError,ValueError,OSError) as exc: + self.state(task_id,"blocked",str(exc)); raise + + def check_scope(self, task_id, wt, commit=None): + c=json.loads(self.task(task_id)["contract"]) + if commit: + names=git(self.repo,"diff","--name-only","--no-renames",c["base_commit"],commit).splitlines() + else: + names=git(wt,"diff","--name-only","--no-renames",c["base_commit"]).splitlines() + names += git(wt,"ls-files","--others","--exclude-standard").splitlines() + require(names, "変更がありません") + for p in names: + require(allowed(p,c["allowed_paths"]) and not secret_path(p), f"scope_violation: {p}") + require(not (wt/p).is_symlink(), f"scope_violation: symlink {p}") + return sorted(set(names)) + + def submit(self, task_id): + task=self.task(task_id); require(task["state"] in ("implementing","ready","repair_ready"), "提出可能な状態ではありません") + wt=Path(task["worktree"] or self.prepare(task_id)) + c=json.loads(task["contract"]) + require(git(wt,"rev-parse","HEAD") in (c["base_commit"],task["submitted"]), "workerによるgit commitを検出") + names=self.check_scope(task_id,wt) + git(wt,"add","--",*names) + git(wt,"-c","user.name=Agent Control Runner","-c","user.email=agent-control@localhost","-c","core.hooksPath=/dev/null", + "-c","commit.gpgsign=false","commit","-m",f"{task_id}: {c['title']}") + sha=git(wt,"rev-parse","HEAD"); clean(wt) + self.db.execute("UPDATE tasks SET submitted=?,candidate=NULL,main_base=NULL WHERE id=?",(sha,task_id)) + self.state(task_id,"submitted") + + def evidence(self, task_id, kind, sha, artifact, passed): + self.db.execute("INSERT INTO evidence(task_id,kind,commit_sha,artifact,passed) VALUES(?,?,?,?,?)",(task_id,kind,sha,str(artifact),int(passed))) + + def verify(self, task_id, wt, sha, kind): + c=json.loads(self.task(task_id)["contract"]) + clean(wt); require(git(wt,"rev-parse","HEAD")==sha, "試験対象SHA不一致") + records=[] + # Heavy build/E2E and shared settings are serialized by one deterministic lease. + with self.lease("heavy-tests","verify:"+task_id): + for vid in c["validation_ids"]: + v=self.config["validations"][vid] + runtime=self.runtime(task_id) + # Use the installed Runner's base interpreter, not the caller's + # shell PATH (Apple's python launcher may fail under Seatbelt). + python = str(Path(sys._base_executable).resolve()) + args=[a.replace("{python}",python) for a in v["argv"]] + extra = {"execution": v["execution"], "validation_id": vid, "verification_kind": kind, "verification_sha": sha} if "execution" in v else {} + artifact,code=self.process(task_id,"validation",None,args,wt,writable=(),timeout=v.get("timeout_seconds",self.config["policy"]["test_timeout_seconds"]), **extra) + records.append({"id":vid,"exit_code":code,"artifact":str(artifact),"passed":code==0}) + # Successful tests must leave the exact source tree untouched. + clean(wt); require(git(wt,"rev-parse","HEAD")==sha,"試験がcommitを変更しました") + passed=all(r["passed"] for r in records) and len(records)==len(c["validation_ids"]) + report=self.root/"runs"/(uuid.uuid4().hex+"-"+kind+".json") + write_json(report,{"task_id":task_id,"commit":sha,"kind":kind,"checks":records,"passed":passed}) + self.evidence(task_id,kind,sha,report,passed) + require(passed,f"test_failed: {report}") + return report + + def detached(self, name, sha): + path=self.worktrees/name + require(not path.exists(), f"checkout既存: {path}") + git(self.repo,"worktree","add","--detach",str(path),sha) + return path + + def validate(self, task_id): + task=self.task(task_id); require(task["state"]=="submitted", "提出済みからだけ検証できます") + self.state(task_id,"validating") + wt=self.detached(task_id+"-validation-"+uuid.uuid4().hex[:8],task["submitted"]) + try: + self.check_scope(task_id,wt,task["submitted"]) + self.verify(task_id,wt,task["submitted"],"validation") + self.state(task_id,"review_pending") + except (GateError,OSError) as exc: + self.state(task_id,"repair_pending",str(exc)); raise + + def review(self, task_id): + task=self.task(task_id); require(task["state"] in ("review_pending","blocked","reviewed","approval_pending"), "機械検証合格後、オーナー承認前にレビューできます") + require(task["submitted"], "提出コミットがありません") + self.has_evidence(task_id,"validation",task["submitted"]) + self.db.execute("UPDATE tasks SET candidate=NULL,main_base=NULL WHERE id=?", (task_id,)) + self.db.execute("DELETE FROM approvals WHERE task_id=?", (task_id,)) + self.state(task_id,"review_pending") + wt=self.detached(task_id+"-review-"+uuid.uuid4().hex[:8],task["submitted"]) + schema=read_json(CONTROL/"schemas/result.json") + schema["properties"]["task_id"]["enum"]=[task_id] + schema["properties"]["commit"]["enum"]=[task["submitted"]] + try: + value,artifact=self.llm(task_id,"reviewer","claude",self.packet(task_id,"reviewer",task["submitted"]),wt,schema) + c=json.loads(task["contract"]) + passed=value["status"]=="pass" and set(value["acceptance_ids"])==set(c["acceptance_ids"]) and not any(f["severity"] in ("critical","high","medium") for f in value["findings"]) + self.evidence(task_id,"review",task["submitted"],artifact/"result.json",passed) + self.state(task_id,"reviewed" if passed else "repair_pending",None if passed else value["summary"]) + require(passed,"review_failed") + except (GateError,ValueError,OSError) as exc: + if self.task(task_id)["state"]=="review_pending": self.state(task_id,"blocked",str(exc)) + raise + + def has_evidence(self, task_id, kind, sha): + rows=self.db.execute("SELECT artifact FROM evidence WHERE task_id=? AND kind=? AND commit_sha=? AND passed=1",(task_id,kind,sha)).fetchall() + require(rows, f"必要な証跡がありません: {kind} {sha}") + require(all(Path(r[0]).exists() for r in rows), "証跡ファイルがありません") + + def prepare_integration(self, task_id): + task=self.task(task_id); require(task["state"] in ("reviewed", "blocked"), "レビュー合格後に統合候補を作成できます") + self.has_evidence(task_id,"review",task["submitted"]) + self.has_evidence(task_id,"validation",task["submitted"]) + with self.lease("integration","candidate:"+task_id): + main=git(self.repo,"rev-parse","main") + c=json.loads(task["contract"]) + current_spec=git(self.repo,"show",f"{main}:{c['spec_ref'].split('#')[0]}") + require(hashlib.sha256(current_spec.encode()).hexdigest()==c["spec_sha256"], "mainの仕様変更: タスク再計画が必要") + wt=self.detached(task_id+"-integration-"+uuid.uuid4().hex[:8],main) + try: + git(wt,"-c","user.name=Agent Control Runner","-c","user.email=agent-control@localhost","-c","core.hooksPath=/dev/null","-c","commit.gpgsign=false", + "merge","--no-ff","--no-edit",task["submitted"]) + candidate=git(wt,"rev-parse","HEAD") + self.verify(task_id,wt,candidate,"integration") + self.db.execute("UPDATE tasks SET candidate=?,main_base=? WHERE id=?",(candidate,main,task_id)) + self.state(task_id,"approval_pending") + except (GateError,OSError) as exc: + self.state(task_id,"blocked",str(exc)); raise + + def approve(self, task_id, candidate, main_base, owner): + task=self.task(task_id) + require(task["state"]=="approval_pending", "承認待ちではありません") + require(candidate==task["candidate"] and main_base==task["main_base"], "承認対象SHA不一致") + require(git(self.repo,"rev-parse","main")==main_base,"main更新: 統合候補の再検証が必要") + require(owner.strip(), "オーナー名が必要") + self.has_evidence(task_id,"integration",candidate) + self.db.execute("INSERT OR REPLACE INTO approvals VALUES(?,?,?,?,?)",(task_id,candidate,main_base,owner,time.time())) + self.state(task_id,"approved") + + def integrate(self, task_id): + task=self.task(task_id); require(task["state"]=="approved", "owner_approval_required") + approval=self.db.execute("SELECT * FROM approvals WHERE task_id=?",(task_id,)).fetchone() + require(approval and approval["candidate"]==task["candidate"] and approval["main_base"]==task["main_base"],"承認不一致") + with self.lease("integration","integrate:"+task_id): + clean(self.repo) + require(git(self.repo,"branch","--show-current")=="main","正本checkoutをmainにしてください") + if git(self.repo,"rev-parse","main")!=task["main_base"]: + self.db.execute("DELETE FROM approvals WHERE task_id=?",(task_id,)) + self.state(task_id,"reviewed","main_changed: 承認無効、統合候補を作り直してください") + raise GateError("main_changed") + for kind,sha in [("validation",task["submitted"]),("review",task["submitted"]),("integration",task["candidate"])]: + self.has_evidence(task_id,kind,sha) + wt=self.detached(task_id+"-final-"+uuid.uuid4().hex[:8],task["candidate"]) + self.verify(task_id,wt,task["candidate"],"final") + require(git(self.repo,"rev-parse","main")==task["main_base"],"main_changed during tests") + git(self.repo,"-c","core.hooksPath=/dev/null","merge","--ff-only",task["candidate"]) + require(git(self.repo,"rev-parse","main")==task["candidate"],"統合SHA不一致") + self.state(task_id,"completed") + self.db.execute("DELETE FROM locks WHERE task_id=?",(task_id,)) + write_json(self.root/"traceability"/(task_id+".json"),{"contract":json.loads(task["contract"]),"implementation_commit":task["submitted"], + "integration_commit":task["candidate"],"owner":approval["owner"],"evidence":[dict(r) for r in self.db.execute("SELECT * FROM evidence WHERE task_id=?",(task_id,))]}) + + def repair(self, task_id): + task=self.task(task_id); c=json.loads(task["contract"]) + require(task["state"] in ("repair_pending","blocked"),"修正対象の状態ではありません") + require(not self.db.execute("SELECT 1 FROM runs WHERE task_id=? AND status='running'",(task_id,)).fetchone(),"実行中です") + require(task["repairs"] < c["max_repair_attempts"], "repair_limit") + self.db.execute("UPDATE tasks SET repairs=repairs+1,candidate=NULL,main_base=NULL WHERE id=?",(task_id,)) + self.db.execute("DELETE FROM approvals WHERE task_id=?",(task_id,)) + self.state(task_id,"repair_ready") + + def stop(self, task_id): + rows=self.db.execute("SELECT * FROM runs WHERE task_id=? AND status='running'",(task_id,)).fetchall() + require(rows,"実行中プロセスがありません") + for r in rows: + require(r["pid"],"プロセス起動中。少し待ってください") + try: os.killpg(r["pid"],signal.SIGTERM) + except ProcessLookupError: pass + deadline=time.monotonic()+3 + while time.monotonic()= 0 and (policy_cap is None or cap <= policy_cap), "修正上限超過") + for dep in contract["depends_on"]: + self.task(dep) + require(contract["id"] not in contract["depends_on"], "自己依存") +@@ -735,7 +751,7 @@ + policy=self.config["policy"] + if service: + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL AND status='running'").fetchone()[0] < policy["max_llm"], "llm_concurrency_limit") +- require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限") ++ require(policy["max_llm_runs"] is None or self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限") + if role in ("implementer","ui"): + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE role IN ('implementer','ui') AND status='running'").fetchone()[0] < policy["max_implementers"], "implementer_concurrency_limit") + self.db.execute("INSERT INTO runs(id,task_id,role,service,status,started,artifact) VALUES(?,?,?,?,?,?,?)", +@@ -1000,7 +1016,7 @@ + task=self.task(task_id); c=json.loads(task["contract"]) + require(task["state"] in ("repair_pending","blocked"),"修正対象の状態ではありません") + require(not self.db.execute("SELECT 1 FROM runs WHERE task_id=? AND status='running'",(task_id,)).fetchone(),"実行中です") +- require(task["repairs"] < c["max_repair_attempts"], "repair_limit") ++ require(c["max_repair_attempts"] is None or task["repairs"] < c["max_repair_attempts"], "repair_limit") + self.db.execute("UPDATE tasks SET repairs=repairs+1,candidate=NULL,main_base=NULL WHERE id=?",(task_id,)) + self.db.execute("DELETE FROM approvals WHERE task_id=?",(task_id,)) + self.state(task_id,"repair_ready") diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/core-change-1.patch b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/core-change-1.patch new file mode 100644 index 0000000..207e915 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/core-change-1.patch @@ -0,0 +1,67 @@ +--- runner/core.py.before ++++ runner/core.py.after +@@ -338,7 +338,18 @@ + + def check_schema(value, schema): + """Small strict validator for the schemas we ship (also used locally after CLI output).""" ++ if "anyOf" in schema: ++ for choice in schema["anyOf"]: ++ try: ++ check_schema(value, choice) ++ return ++ except GateError: ++ pass ++ raise GateError("結果schemaのanyOf違反") + typ = schema.get("type") ++ if typ == "null": ++ require(value is None, "結果schemaの型違反: null") ++ return + types = {"object": dict, "array": list, "string": str, "integer": int, "boolean": bool} + require(typ not in types or isinstance(value, types[typ]), f"結果schemaの型違反: {typ}") + require("enum" not in schema or value in schema["enum"], "結果schemaのenum違反") +@@ -451,6 +462,8 @@ + require(not wt.is_relative_to(repo), "worktree rootはrepo外にしてください") + p = policy or read_json(CONTROL / "policies/default.json") + require(p["api_fallback"] is False and p["owner_approval_required"] is True, "初期運用のAPI/承認policy違反") ++ if project.startswith("the-tree-"): ++ p = dict(p, max_repair_attempts=None, max_llm_runs=None) + config = {"project": project, "repo": str(repo), "worktree_root": str(wt), "policy": p, "validations": validations} + root.mkdir(parents=True, mode=0o700) + os.chmod(root, 0o700) +@@ -490,6 +503,8 @@ + self.event(task_id, state, {"reason": reason}) + + def add(self, contract): ++ if self.config["project"].startswith("the-tree-") and self.config["policy"]["max_repair_attempts"] is None: ++ contract = dict(contract, max_repair_attempts=None) + check_schema(contract,read_json(CONTROL/"schemas/task.json")) + required = {"id", "title", "requirement_ids", "acceptance_ids", "spec_ref", "base_commit", "worker", + "allowed_paths", "depends_on", "resource_locks", "validation_ids", "max_repair_attempts"} +@@ -515,7 +530,8 @@ + spec_text = git(self.repo, "show", f"{base}:{spec}") + for rid in contract["requirement_ids"]: + require(rid in spec_text, f"仕様に要件IDがありません: {rid}") +- require(0 <= contract["max_repair_attempts"] <= self.config["policy"]["max_repair_attempts"], "修正上限超過") ++ cap = contract["max_repair_attempts"]; policy_cap = self.config["policy"]["max_repair_attempts"] ++ require(cap is None and policy_cap is None or type(cap) is int and cap >= 0 and (policy_cap is None or cap <= policy_cap), "修正上限超過") + for dep in contract["depends_on"]: + self.task(dep) + require(contract["id"] not in contract["depends_on"], "自己依存") +@@ -735,7 +751,7 @@ + policy=self.config["policy"] + if service: + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL AND status='running'").fetchone()[0] < policy["max_llm"], "llm_concurrency_limit") +- require(self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限") ++ require(policy["max_llm_runs"] is None or self.db.execute("SELECT COUNT(*) FROM runs WHERE service IS NOT NULL").fetchone()[0] < policy["max_llm_runs"], "budget_limit: LLM起動数上限") + if role in ("implementer","ui"): + require(self.db.execute("SELECT COUNT(*) FROM runs WHERE role IN ('implementer','ui') AND status='running'").fetchone()[0] < policy["max_implementers"], "implementer_concurrency_limit") + self.db.execute("INSERT INTO runs(id,task_id,role,service,status,started,artifact) VALUES(?,?,?,?,?,?,?)", +@@ -1000,7 +1016,7 @@ + task=self.task(task_id); c=json.loads(task["contract"]) + require(task["state"] in ("repair_pending","blocked"),"修正対象の状態ではありません") + require(not self.db.execute("SELECT 1 FROM runs WHERE task_id=? AND status='running'",(task_id,)).fetchone(),"実行中です") +- require(task["repairs"] < c["max_repair_attempts"], "repair_limit") ++ require(c["max_repair_attempts"] is None or task["repairs"] < c["max_repair_attempts"], "repair_limit") + self.db.execute("UPDATE tasks SET repairs=repairs+1,candidate=NULL,main_base=NULL WHERE id=?",(task_id,)) + self.db.execute("DELETE FROM approvals WHERE task_id=?",(task_id,)) + self.state(task_id,"repair_ready") diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/task-schema-before-0.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/task-schema-before-0.json new file mode 100644 index 0000000..82b5be2 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/task-schema-before-0.json @@ -0,0 +1,21 @@ +{ + "$schema":"https://json-schema.org/draft/2020-12/schema", + "title":"Runner task contract (not a provider configuration)", + "type":"object", + "required":["id","title","requirement_ids","acceptance_ids","spec_ref","base_commit","worker","allowed_paths","depends_on","resource_locks","validation_ids","max_repair_attempts"], + "properties":{ + "id":{"type":"string","pattern":"^[A-Za-z][A-Za-z0-9_-]{0,63}$"}, + "title":{"type":"string","minLength":1}, + "requirement_ids":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}, + "acceptance_ids":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}, + "spec_ref":{"type":"string"},"base_commit":{"type":"string","pattern":"^[0-9a-f]{40,64}$"}, + "worker":{"type":"string","enum":["codex","agy"]}, + "role":{"type":"string","enum":["implementer","ui"]}, + "allowed_paths":{"type":"array","items":{"type":"string"},"minItems":1}, + "depends_on":{"type":"array","items":{"type":"string"}}, + "resource_locks":{"type":"array","items":{"type":"string"},"uniqueItems":true}, + "validation_ids":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}, + "max_repair_attempts":{"type":"integer","minimum":0,"maximum":2} + }, + "additionalProperties":false +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/task-schema-before-1.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/task-schema-before-1.json new file mode 100644 index 0000000..82b5be2 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/task-schema-before-1.json @@ -0,0 +1,21 @@ +{ + "$schema":"https://json-schema.org/draft/2020-12/schema", + "title":"Runner task contract (not a provider configuration)", + "type":"object", + "required":["id","title","requirement_ids","acceptance_ids","spec_ref","base_commit","worker","allowed_paths","depends_on","resource_locks","validation_ids","max_repair_attempts"], + "properties":{ + "id":{"type":"string","pattern":"^[A-Za-z][A-Za-z0-9_-]{0,63}$"}, + "title":{"type":"string","minLength":1}, + "requirement_ids":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}, + "acceptance_ids":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}, + "spec_ref":{"type":"string"},"base_commit":{"type":"string","pattern":"^[0-9a-f]{40,64}$"}, + "worker":{"type":"string","enum":["codex","agy"]}, + "role":{"type":"string","enum":["implementer","ui"]}, + "allowed_paths":{"type":"array","items":{"type":"string"},"minItems":1}, + "depends_on":{"type":"array","items":{"type":"string"}}, + "resource_locks":{"type":"array","items":{"type":"string"},"uniqueItems":true}, + "validation_ids":{"type":"array","items":{"type":"string"},"minItems":1,"uniqueItems":true}, + "max_repair_attempts":{"type":"integer","minimum":0,"maximum":2} + }, + "additionalProperties":false +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/test_new_default.py b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/test_new_default.py new file mode 100644 index 0000000..d5302ba --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/test_new_default.py @@ -0,0 +1,13 @@ +import sys,json,unittest +sys.path.insert(0,'/private/tmp/the-tree-p24-02-control-r02') +from tests.test_runner import RunnerTests +class Defaults(unittest.TestCase): + def test_new_tree_registration_unlimited(self): + case=RunnerTests('test_repair_limit');case.setUp() + try: + case.r.config['project']='the-tree-default-test';case.r.config['policy']['max_repair_attempts']=None + c=dict(case.contract,id='T002',acceptance_ids=['AC-002'],requirement_ids=['REQ-002'],allowed_paths=['other/**'],resource_locks=[]) + case.r.add(c);self.assertIsNone(json.loads(case.r.task('T002')['contract'])['max_repair_attempts']) + self.assertEqual(c['max_repair_attempts'],2) + finally:case.tearDown() +if __name__=='__main__':unittest.main() diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/test_unlimited_policy.py b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/test_unlimited_policy.py new file mode 100644 index 0000000..6a410c7 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/test_unlimited_policy.py @@ -0,0 +1,24 @@ +import sys,json,sqlite3,unittest,importlib +from pathlib import Path +sys.path.insert(0,'/private/tmp/the-tree-p24-02-control-r02') +from runner.core import Runner,check_schema,GateError,read_json,CONTROL +class PolicyTests(unittest.TestCase): + def test_nullable_schema(self): + schema=read_json(CONTROL/'schemas/task.json')['properties']['max_repair_attempts'] + for v in (None,0,2,3,900):check_schema(v,schema) + for v in ('unlimited',{},[]): + with self.assertRaises(GateError):check_schema(v,schema) + def test_all_tree_state_and_counts_preserved(self): + audit=read_json('/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/APPLIED.json') + for row in audit['tasks']: + r=Runner(row['project']); t=r.task(row['task']);self.assertEqual(t['state'],row['state']);self.assertEqual(t['repairs'],row['repairs']);self.assertIsNone(json.loads(t['contract'])['max_repair_attempts']);self.assertIsNone(r.config['policy']['max_llm_runs']);r.db.close() + def test_unlimited_repair_and_live_guard(self): + from tests.test_runner import RunnerTests + case=RunnerTests('test_repair_limit');case.setUp() + try: + r=case.r;c=json.loads(r.task('T001')['contract']);c['max_repair_attempts']=None;r.db.execute('UPDATE tasks SET contract=? WHERE id=?',(json.dumps(c),'T001')) + r.db.execute('UPDATE tasks SET repairs=999 WHERE id=?',('T001',)) + r.state('T001','repair_pending');r.repair('T001');self.assertEqual(r.task('T001')['repairs'],1000);self.assertEqual(r.task('T001')['state'],'repair_ready') + with self.assertRaises(GateError):r.repair('T001') + finally:case.tearDown() +if __name__=='__main__':unittest.main() diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-mana-proposal-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-mana-proposal-before.json new file mode 100644 index 0000000..f6e9d29 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-mana-proposal-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-mana-proposal", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/mana-proposal", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "proposal-consistency": { + "argv": [ + "{python}", + "-B", + "-c", + "import json,hashlib,re\nfrom pathlib import Path\np=Path(\"docs/plans/original-completion-2026-10-02/execution/mana-proposal\")\nj=json.loads((p/\"proposal.json\").read_text());assert j[\"status\"]==\"proposed_unapproved\" and j[\"applied_to_product\"] is False\nfor k in [\"recommended\",\"alternatives\",\"demand_catalog\",\"ordering\",\"legacy_zero\",\"open_decisions\",\"source_pins\",\"comparisons\"]: assert j[k],k\nr=j[\"recommended\"]\nfor k in [\"initial_amount\",\"storage_capacity\",\"world_tree_generation_per_interval\",\"interval_minutes\"]: assert isinstance(r[k],(int,float)) and not isinstance(r[k],bool) and r[k]>0,k\nassert r[\"initial_amount\"]<=r[\"storage_capacity\"]\nfor f,h in j[\"source_pins\"].items():\n q=Path(f);assert not q.is_absolute() and \"..\" not in q.parts;assert hashlib.sha256(q.read_bytes()).hexdigest()==h,f\nrows=j[\"demand_catalog\"];assert len({x[\"definition_id\"] for x in rows})==len(rows)\nfor x in rows:\n q=Path(x[\"path\"]);t=q.read_text();v=re.search(r\"^maintenance_mana\\s*=\\s*([\\d.]+)\",t,re.M);assert v and float(v.group(1))==x[\"observed_maintenance_mana\"],str(q)\nassert (p/\"PROPOSAL.md\").stat().st_size>1000\nprint(\"proposal structure, current-source pins and observed demand validated; no product acceptance implied\")\n" + ], + "timeout_seconds": 120 + } + } + }, + "tasks": [ + { + "id": "MP-01", + "contract": "{\"id\": \"MP-01\", \"title\": \"承認用マナ契約候補の作成(原88成果の完了ではない)\", \"requirement_ids\": [\"P06-01\"], \"acceptance_ids\": [\"MP-01\"], \"spec_ref\": \"docs/spec/acceptance/P06-mana-proposal.json\", \"base_commit\": \"0bdc4886cf631879792e0c0a873ecf97b98a64ea\", \"worker\": \"codex\", \"allowed_paths\": [\"docs/plans/original-completion-2026-10-02/execution/mana-proposal/**\"], \"depends_on\": [], \"resource_locks\": [\"mana-proposal-draft\"], \"validation_ids\": [\"proposal-consistency\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"f3dfc050ee3c343dfd67d84b10b814479c4f652a51666006e69f4d50d99587e4\"}", + "state": "blocked", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/mana-proposal/MP-01-implement", + "submitted": null, + "candidate": null, + "main_base": null, + "reason": "worker_blocked: 許可範囲内にproposal.jsonとPROPOSAL.mdを作成。参照SHA・計算の静的整合性を確認しましたが、現仕様ファイルのSHA256が契約指定値と不一致です。製品未反映。正式試験・git書込み・Runner操作は未実施です。", + "repairs": 0 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-mana-proposal-r02-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-mana-proposal-r02-before.json new file mode 100644 index 0000000..e9bc947 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-mana-proposal-r02-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-mana-proposal-r02", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/mana-proposal-r02", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "proposal-consistency": { + "argv": [ + "{python}", + "-B", + "-c", + "import json,hashlib,re\nfrom pathlib import Path\np=Path(\"docs/plans/original-completion-2026-10-02/execution/mana-proposal\")\nj=json.loads((p/\"proposal.json\").read_text());assert j[\"status\"]==\"proposed_unapproved\" and j[\"applied_to_product\"] is False\nfor k in [\"recommended\",\"alternatives\",\"demand_catalog\",\"ordering\",\"legacy_zero\",\"open_decisions\",\"source_pins\",\"comparisons\"]: assert j[k],k\nr=j[\"recommended\"]\nfor k in [\"initial_amount\",\"storage_capacity\",\"world_tree_generation_per_interval\",\"interval_minutes\"]: assert isinstance(r[k],(int,float)) and not isinstance(r[k],bool) and r[k]>0,k\nassert r[\"initial_amount\"]<=r[\"storage_capacity\"]\nfor f,h in j[\"source_pins\"].items():\n q=Path(f);assert not q.is_absolute() and \"..\" not in q.parts;assert hashlib.sha256(q.read_bytes()).hexdigest()==h,f\nrows=j[\"demand_catalog\"];assert len({x[\"definition_id\"] for x in rows})==len(rows)\nfor x in rows:\n q=Path(x[\"path\"]);t=q.read_text();v=re.search(r\"^maintenance_mana\\s*=\\s*([\\d.]+)\",t,re.M);assert (float(v.group(1)) if v else 0)==x[\"observed_maintenance_mana\"],str(q)\nassert (p/\"PROPOSAL.md\").stat().st_size>1000\nprint(\"proposal structure, current-source pins and observed demand validated; no product acceptance implied\")\n" + ], + "timeout_seconds": 120 + } + } + }, + "tasks": [ + { + "id": "MP-01-r02", + "contract": "{\"id\": \"MP-01-r02\", \"title\": \"承認用マナ契約候補の作成(原88成果の完了ではない)\", \"requirement_ids\": [\"P06-01\"], \"acceptance_ids\": [\"MP-01-r02\"], \"spec_ref\": \"docs/spec/acceptance/P06-mana-proposal-r02.json\", \"base_commit\": \"5536bc041188d53229d18f33f08b17d4d83b7590\", \"worker\": \"codex\", \"allowed_paths\": [\"docs/plans/original-completion-2026-10-02/execution/mana-proposal/**\"], \"depends_on\": [], \"resource_locks\": [\"mana-proposal-draft\"], \"validation_ids\": [\"proposal-consistency\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"f8cf1753e7ab400130c761570828640c03e0d51073a03f887a9085cbe6be77c6\"}", + "state": "completed", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/mana-proposal-r02/MP-01-r02-implement", + "submitted": "6b78f8fd71131e61ba695939f4629fec9f51d7a9", + "candidate": "65a4fe2bb8e99812a8e8ad7f7a7addf489fe5067", + "main_base": "15168ffd47628c5e28f9df20748bfdebe60e228f", + "reason": null, + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p04-01-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p04-01-before.json new file mode 100644 index 0000000..78a48e8 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p04-01-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-p04-01", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p04-01", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "p04-acceptance": { + "argv": [ + "{python}", + "-B", + "tools/check_p04_order_evidence.py", + "--verify-only" + ], + "timeout_seconds": 180 + } + } + }, + "tasks": [ + { + "id": "P04-01", + "contract": "{\"id\": \"P04-01\", \"title\": \"同時発生の更新規約\", \"requirement_ids\": [\"P04-01\"], \"acceptance_ids\": [\"P04-01\"], \"spec_ref\": \"docs/spec/acceptance/P04-01.json\", \"base_commit\": \"deb1bcb2374eb378b840ca345db952bd8572cb6e\", \"worker\": \"codex\", \"allowed_paths\": [\"tools/check_p04_order_evidence.py\", \"docs/plans/original-completion-2026-10-02/execution/p04-01/**\"], \"depends_on\": [], \"resource_locks\": [\"p04-order-evidence-audit\"], \"validation_ids\": [\"p04-acceptance\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"9ec722e7dfb0bee178d291624a026420c08412574e238bba1fde012c7a6f7881\"}", + "state": "repair_pending", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p04-01/P04-01-implement", + "submitted": "a19737b1606ba360a18c8fe0914594f038034924", + "candidate": null, + "main_base": null, + "reason": "test_failed: /Users/korizatou114/Library/Application Support/agent-control/the-tree-p04-01/runs/f2ad55a6709b4c42901027d842358a74-validation.json", + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p06-01-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p06-01-before.json new file mode 100644 index 0000000..6769681 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p06-01-before.json @@ -0,0 +1,57 @@ +{ + "config": { + "project": "the-tree-p06-01", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p06-01", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "p06-contract-evidence": { + "argv": [ + "{python}", + "-B", + "docs/plans/original-completion-2026-10-02/execution/p06-01/validate_contract.py", + "--repo-root", + ".", + "--unit", + "P06-01" + ], + "timeout_seconds": 300 + } + } + }, + "tasks": [ + { + "id": "P06-01", + "contract": "{\"id\": \"P06-01\", \"title\": \"供給と蓄積の契約\", \"requirement_ids\": [\"P06-01\", \"B-2.6\", \"B-9.1\", \"B-10.6\", \"B-10.7\"], \"acceptance_ids\": [\"P06-01\"], \"spec_ref\": \"docs/spec/acceptance/P06-01.json\", \"base_commit\": \"dff514c774b2e0ef6df2dfdce3a2fd974c843706\", \"worker\": \"codex\", \"allowed_paths\": [\"docs/plans/original-completion-2026-10-02/execution/p06-01/**\"], \"depends_on\": [], \"resource_locks\": [\"p06-supply-contract\", \"the-tree-acceptance-ledger\"], \"validation_ids\": [\"p06-contract-evidence\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"ca81f9b65948808b33a0eaeb7406affda6237b5fb54549f90a3962f7dcdd71b8\"}", + "state": "blocked", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p06-01/P06-01-implement", + "submitted": null, + "candidate": null, + "main_base": null, + "reason": "worker_blocked: 指定範囲にAUDIT.md、matrix.json、validate_contract.pyを作成。正式試験・git書込み・Runner操作は未実施。未達を維持する検証器を準備した。", + "repairs": 0 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p08-02-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p08-02-before.json new file mode 100644 index 0000000..565a989 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p08-02-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-p08-02", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": "gpt-6.1-sol", + "claude": "claude-opus-5-5", + "agy": null + } + }, + "validations": { + "p08-evidence": { + "argv": [ + "{python}", + "-B", + "tools/check_p08_migration_evidence.py", + "--verify-only" + ], + "timeout_seconds": 180 + } + } + }, + "tasks": [ + { + "id": "P08-02", + "contract": "{\"id\": \"P08-02\", \"title\": \"旧配置の安全な変更\", \"requirement_ids\": [\"P08-02\"], \"acceptance_ids\": [\"P08-02\"], \"spec_ref\": \"docs/spec/acceptance/P08-02.json\", \"base_commit\": \"e196578c1a4ad2fdf84642565bd102e524be386f\", \"worker\": \"codex\", \"allowed_paths\": [\"docs/plans/original-completion-2026-10-02/execution/p08-02/**\", \"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json\", \"tests/s1/test_p08_combined_migration.gd\", \"tests/s1/test_p08_combined_migration.gd.uid\", \"tests/fixtures/p08-02/**\", \"tools/run_p08_mixed_migration.py\", \"tools/check_p08_migration_evidence.py\", \"tools/test_p08_migration_evidence.py\"], \"depends_on\": [], \"resource_locks\": [\"p08-combined-migration\", \"save-placement-final-proof\"], \"validation_ids\": [\"p08-evidence\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"19f02bb7912a4d7354e565186f02366fc1d4dd698468d26142e8204fd14ebfb0\"}", + "state": "submitted", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p08-02/P08-02-implement", + "submitted": "e59206b9b2578ddb4a1ec023f836d413a7a8fbfb", + "candidate": null, + "main_base": null, + "reason": null, + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p13-03-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p13-03-before.json new file mode 100644 index 0000000..f0f37a4 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p13-03-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-p13-03", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p13-03", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "p13-difference": { + "argv": [ + "{python}", + "-B", + "tools/check_p13_content_difference.py", + "--verify-only" + ], + "timeout_seconds": 120 + } + } + }, + "tasks": [ + { + "id": "P13-03", + "contract": "{\"id\": \"P13-03\", \"title\": \"内容差異の記録\", \"requirement_ids\": [\"P13-03\"], \"acceptance_ids\": [\"P13-03\"], \"spec_ref\": \"docs/spec/acceptance/P13-03.json\", \"base_commit\": \"e1a1651e15bbc9a0526ef31d0b2786feb20d8cd1\", \"worker\": \"codex\", \"allowed_paths\": [\"tools/check_p13_content_difference.py\", \"docs/plans/original-completion-2026-10-02/execution/p13-03/**\"], \"depends_on\": [], \"resource_locks\": [\"p13-content-difference-record\"], \"validation_ids\": [\"p13-difference\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"e1255835c1227e02bac42eb99b026b0fcbe69c272f9ed0b36199a2daf8d55c53\"}", + "state": "blocked", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p13-03/P13-03-implement", + "submitted": "7645c55149d3bebf3ed602cf3039b15b28fa376f", + "candidate": null, + "main_base": null, + "reason": "scope_violation: tools/__pycache__/check_p13_content_difference.cpython-314.pyc", + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p18-01-model-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p18-01-model-before.json new file mode 100644 index 0000000..6107faf --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p18-01-model-before.json @@ -0,0 +1,55 @@ +{ + "config": { + "project": "the-tree-p18-01-model", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p18-01-model", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": "gpt-6.1-sol", + "claude": "claude-opus-5-5", + "agy": null + } + }, + "validations": { + "p18-model": { + "argv": [ + "{python}", + "-B", + "tools/run_p18_relationship_tests.py", + "--output", + "{build_dir}/p18-relationship-evidence" + ], + "timeout_seconds": 1200 + } + } + }, + "tasks": [ + { + "id": "P18-01-CODE", + "contract": "{\"id\": \"P18-01-CODE\", \"title\": \"原本関係モデル・旧値保全・Runtime保存と読取経路\", \"requirement_ids\": [\"P18-01-CODE\"], \"acceptance_ids\": [\"P18-C-1\", \"P18-C-2\", \"P18-C-3\", \"P18-C-4\"], \"spec_ref\": \"docs/spec/acceptance/P18-01-code-r01.json\", \"base_commit\": \"d2a1f80a613e7c77b02b941026bc49c2d85db863\", \"worker\": \"codex\", \"allowed_paths\": [\"scripts/relationship/original_relationship_state.gd\", \"scripts/relationship/original_relationship_state.gd.uid\", \"scripts/relationship/relationship_service.gd\", \"autoload/simulation_runtime.gd\", \"tests/s4/test_original_relationship_model.gd\", \"tests/s4/test_original_relationship_model.gd.uid\", \"tests/s4/test_original_relationship_runtime.gd\", \"tests/s4/test_original_relationship_runtime.gd.uid\", \"tools/run_p18_relationship_tests.py\", \"tools/regression/suite-policy.json\", \"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json\", \"docs/plans/original-completion-2026-10-02/execution/p18-01-code/**\"], \"depends_on\": [], \"resource_locks\": [\"relationship-model-save\", \"simulation-runtime\", \"native-godot-owner\"], \"validation_ids\": [\"p18-model\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"04e9ae701b6ee1e399542564add695ca7fce54499e52dd2e907b1dbdc23a366d\"}", + "state": "repair_pending", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p18-01-model/P18-01-CODE-implement", + "submitted": "0084760ee05a97adfcbd783a71161491f3629918", + "candidate": null, + "main_base": null, + "reason": "I compared all four acceptance criteria against the spec, the full diff, the target sources, the identity catalog, the Runner p18-model result (exit 0) and the coordinator's native evidence (7 suites, 843 checks, 308 parse, 9 distinct PIDs, no guard or cleanup errors). I did not edit or run anything. Every change is inside allowed_paths. No existing test was deleted, disabled or weakened. Two new marker rows and two new suite-policy rows were added, pinned to the source hashes.\n\nP18-C-1 is met. The original relationship state is separate from the legacy one, with explicit directional subject/protagonist/rival identities. It requires all fields, rejects non-finite values, and enforces shield_max == affection. An unset record reads as configured:false with null values, distinct from an explicit zero. No production values or coefficients are introduced.\n\nP18-C-2 and P18-C-3 are mostly implemented: v2 nests the full v1 record, restore is atomic, the Runtime command and restore/rebind are real, and the report projection is read-only. But three things are unproven or change legacy behavior:\n(1) The merge-back of legacy aliases is not tested for changes to the aliased axes (affection/dependence).\n(2) Restore now rejects legacy saves it used to accept, through stricter type checks and a new actor-reference check.\n(3) Original records have no lifecycle once the subject leaves staff, which could make saves unloadable.\n\nP18-C-4: native local and affected evidence exists. Integration and the final affected validation have not happened yet, and the backend task correctly does not claim the P18-01 unit complete. Because of the verification gaps above, I am not passing it.", + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p22-02-contract-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p22-02-contract-before.json new file mode 100644 index 0000000..c56d14b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p22-02-contract-before.json @@ -0,0 +1,57 @@ +{ + "config": { + "project": "the-tree-p22-02-contract", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p22-02-contract", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": "gpt-6.1-sol", + "claude": "claude-opus-5-5", + "agy": null + } + }, + "validations": { + "p22-contract": { + "argv": [ + "{python}", + "-B", + "tools/check_p22_asset_contracts.py", + "--verify-only", + "--accepted-input", + "docs/spec/inputs/P22-02-accepted-usage.json", + "--require-ready" + ], + "timeout_seconds": 120 + } + } + }, + "tasks": [ + { + "id": "P22-02", + "contract": "{\"id\": \"P22-02\", \"title\": \"素材契約と権利(原条件全体、記録だけの完了代用なし)\", \"requirement_ids\": [\"P22-02\"], \"acceptance_ids\": [\"P22-02\"], \"spec_ref\": \"docs/spec/acceptance/P22-02.json\", \"base_commit\": \"10c62e1fa7b71576598583f434824b59816d4705\", \"worker\": \"codex\", \"allowed_paths\": [\"docs/plans/original-completion-2026-10-02/execution/p22-02/**\", \"tools/check_p22_asset_contracts.py\"], \"depends_on\": [], \"resource_locks\": [\"p22-asset-contracts-and-rights\"], \"validation_ids\": [\"p22-contract\"], \"max_repair_attempts\": 3, \"spec_sha256\": \"f252de3c5cff7ebfa62322ae2f2df09fb80a44bc59eb286b76d4acb4c12ca875\"}", + "state": "blocked", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p22-02-contract/P22-02-implement", + "submitted": "c6ccc5a6ab4b1670f1ccf813937edf339ca19139", + "candidate": null, + "main_base": null, + "reason": "worker_blocked: ASSET_CONTRACTS.mdとvalidation.jsonのみ同期。保持済みコードは変更なし。python3 -Bで13テスト合格、AST確認成功、ordinary exit 0、require-ready exit 2。出力フィールドとspec両hashを確認。指定patch・計測証拠がworktreeに存在せず参照先もアクセス拒否のため、完全最終パケットの500000未満を確認できない。487283は実測として報告していない。", + "repairs": 3 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-02-operations-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-02-operations-before.json new file mode 100644 index 0000000..944ac72 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-02-operations-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-p24-02-operations", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "p24-operations": { + "argv": [ + "{python}", + "-B", + "tools/check_p24_operations_evidence.py", + "--verify-only" + ], + "timeout_seconds": 180 + } + } + }, + "tasks": [ + { + "id": "P24-02", + "contract": "{\"id\": \"P24-02\", \"title\": \"操作と通知の全受入\", \"requirement_ids\": [\"P24-02\"], \"acceptance_ids\": [\"P24-02\"], \"spec_ref\": \"docs/spec/acceptance/P24-02-code-r01.json\", \"base_commit\": \"1a5abc65163097d4b36a060d68f58c92093eb4e9\", \"worker\": \"agy\", \"allowed_paths\": [\"scenes/main/main.gd\", \"scenes/main/world_preview.gd\", \"scripts/ui/event_log_dialog.gd\", \"scripts/ui/manual_save_slots_dialog.gd\", \"scripts/ui/operation_feedback.gd\", \"scripts/ui/operation_feedback.gd.uid\", \"scripts/ui/operations_camera_controls.gd\", \"scripts/ui/operations_camera_controls.gd.uid\", \"tests/ui/test_operations_notification.gd\", \"tests/ui/test_operations_notification.gd.uid\", \"tests/ui/fixtures/operations_os_observer.gd\", \"tests/ui/fixtures/operations_os_observer.gd.uid\", \"tools/check_p24_operations_evidence.py\", \"tools/run_all_tests.py\", \"tools/regression/suite-policy.json\", \"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json\", \"docs/plans/original-completion-2026-10-02/execution/p24-02/**\"], \"depends_on\": [], \"resource_locks\": [\"main-operations-notifications\", \"world-preview-click-controls\", \"save-label-presentation\", \"regression-roster-policy\"], \"validation_ids\": [\"p24-operations\"], \"max_repair_attempts\": 3, \"spec_sha256\": \"738268b406e381ccab415b426d4a547b02e7cb2a481cefce65579a1e346834c7\"}", + "state": "submitted", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations/P24-02-implement", + "submitted": "9641ed52def2ff8e5b0222449a4a83a1191e1d37", + "candidate": null, + "main_base": null, + "reason": null, + "repairs": 3 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-03-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-03-before.json new file mode 100644 index 0000000..e1f7681 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-03-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-p24-03", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-03", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "p24-acceptance": { + "argv": [ + "{python}", + "-B", + "tools/check_p24_selection_evidence.py", + "--verify-only" + ], + "timeout_seconds": 180 + } + } + }, + "tasks": [ + { + "id": "P24-03", + "contract": "{\"id\": \"P24-03\", \"title\": \"選択と保存の受入\", \"requirement_ids\": [\"P24-03\"], \"acceptance_ids\": [\"P24-03\"], \"spec_ref\": \"docs/spec/acceptance/P24-03.json\", \"base_commit\": \"9b2ee480a88054eb863eb5cdff855b379201a7c1\", \"worker\": \"agy\", \"allowed_paths\": [\"scenes/main/main.gd\", \"scripts/ui/guest_observation_panel.gd\", \"scripts/ui/staff_management_panel.gd\", \"tests/ui/test_selection_departure.gd\", \"tests/ui/test_selection_departure.gd.uid\", \"tests/ui/fixtures/selection_cross_process.gd\", \"tests/ui/fixtures/selection_cross_process.gd.uid\", \"tools/check_p24_selection_evidence.py\", \"tools/run_all_tests.py\", \"tools/regression/suite-policy.json\", \"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json\", \"docs/plans/original-completion-2026-10-02/execution/p24-03/**\"], \"depends_on\": [], \"resource_locks\": [\"main-selection-lifecycle\", \"selection-acceptance-profile\", \"regression-roster-policy\"], \"validation_ids\": [\"p24-acceptance\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"4d353ff816d8edbb39671159e4f9fd87c20224cf0c44ea7b8fadc03bf25ae244\"}", + "state": "blocked", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-03/P24-03-implement", + "submitted": null, + "candidate": null, + "main_base": null, + "reason": "runner_interrupted: dead process", + "repairs": 0 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-03-code-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-03-code-before.json new file mode 100644 index 0000000..1bf6b88 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-p24-03-code-before.json @@ -0,0 +1,54 @@ +{ + "config": { + "project": "the-tree-p24-03-code", + "repo": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-03-code", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "p24-acceptance": { + "argv": [ + "{python}", + "-B", + "tools/check_p24_selection_evidence.py", + "--verify-only" + ], + "timeout_seconds": 180 + } + } + }, + "tasks": [ + { + "id": "P24-03", + "contract": "{\"id\": \"P24-03\", \"title\": \"選択と保存の受入\", \"requirement_ids\": [\"P24-03\"], \"acceptance_ids\": [\"P24-03\"], \"spec_ref\": \"docs/spec/acceptance/P24-03-code-r02.json\", \"base_commit\": \"286730f88ce430a1cbe1501d40361188f832d235\", \"worker\": \"codex\", \"allowed_paths\": [\"scenes/main/main.gd\", \"scripts/ui/guest_observation_panel.gd\", \"scripts/ui/staff_management_panel.gd\", \"tests/ui/test_selection_departure.gd\", \"tests/ui/test_selection_departure.gd.uid\", \"tests/ui/fixtures/selection_cross_process.gd\", \"tests/ui/fixtures/selection_cross_process.gd.uid\", \"tools/check_p24_selection_evidence.py\", \"tools/run_all_tests.py\", \"tools/regression/suite-policy.json\", \"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json\", \"docs/plans/original-completion-2026-10-02/execution/p24-03/**\"], \"depends_on\": [], \"resource_locks\": [\"main-selection-lifecycle\", \"selection-acceptance-profile\", \"regression-roster-policy\"], \"validation_ids\": [\"p24-acceptance\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"dbb885b47a3ee4af3d4180bb97fe746e26a0e08d75af2cd3b5b727ed337bd37c\"}", + "state": "completed", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-03-code/P24-03-implement", + "submitted": "4cefcff849d4dc1211caa5066e24bfae666cc2f4", + "candidate": "0a759f13aed03c05fd4a25ab4a2c26eefec94e3c", + "main_base": "41a5acbd434990f3253f4990a6479b730d2312ee", + "reason": null, + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-agy-execution-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-agy-execution-before.json new file mode 100644 index 0000000..a0ce39b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-agy-execution-before.json @@ -0,0 +1,61 @@ +{ + "config": { + "project": "the-tree-runner-agy-execution", + "repo": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-p13-recovery-source", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-agy-execution", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "runner-unit": { + "execution": "trusted_runner_self_test", + "argv": [ + "{python}", + "-B", + "-m", + "unittest", + "discover", + "-s", + "tests", + "-p", + "test_runner.py", + "-v" + ], + "timeout_seconds": 180 + } + } + }, + "tasks": [ + { + "id": "RP24-02", + "contract": "{\"id\": \"RP24-02\", \"title\": \"Antigravityの二重sandboxと回復済み読取エラー判定を修正\", \"requirement_ids\": [\"RP24-02\"], \"acceptance_ids\": [\"RP24-02\"], \"spec_ref\": \"docs/spec/RP24-02.json\", \"base_commit\": \"831364de7eb9abacfafb0266776c949cfce23cd8\", \"worker\": \"codex\", \"allowed_paths\": [\"runner/core.py\", \"tests/test_runner.py\"], \"depends_on\": [], \"resource_locks\": [\"runner-agy-core-isolated\"], \"validation_ids\": [\"runner-unit\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"a636d68b9607efb57772839ebd947250b9faa63562729a59612c8405894a5ed1\"}", + "state": "repair_pending", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-agy-execution/RP24-02-implement", + "submitted": "09262f37c14397118d51b6b43c35ddbee14fcfa2", + "candidate": null, + "main_base": null, + "reason": "受入項目RP24-02を、仕様・差分・ソース・Runner試験結果と照合しました。結論は差し戻しです。\n二重sandboxの除去は満たしています。外側Seatbeltは維持されており、`--sandbox`だけを外し、`--dangerously-skip-permissions`も追加していません。self-test pinの拡張も満たしています。対象はproject/task/base/spec SHAの完全一致に限られ、RP24-01の互換性と既存の拒否経路も残っています。Runner試験runner-unitは成功(57件)しています。\n一方、中核要件である「実際に観測された回復済みview_file ENOENTを、正当な結果として分類する」が実データで成立していません。coordinatorが実際のstdout(2be30fb9…, sha256 f7e5ad9f…)を再生したところ、提出コードは step_index 0 の `user_input`/DONE を `Antigravity unknown step` として拒否しました。このstdoutは、本タスクの起点になった実際の失敗ログです。\n新規試験は、この`user_input`ステップを含まない合成fixtureで書かれていたため、この欠陥を検出できませんでした。読み取り専用の修正案(許可するstep_typeに`user_input`を追加し、正例と負例の試験を追加)は再生に成功しています。ただし採用も正式検証もされていません。\nこのため、現在の提出コミットでは受入を満たしたと判断できません。", + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-p13-recovery-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-p13-recovery-before.json new file mode 100644 index 0000000..dcdb9d7 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-p13-recovery-before.json @@ -0,0 +1,60 @@ +{ + "config": { + "project": "the-tree-runner-p13-recovery", + "repo": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-p13-recovery-source", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-p13-recovery", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "runner-unit": { + "argv": [ + "{python}", + "-B", + "-m", + "unittest", + "discover", + "-s", + "tests", + "-p", + "test_runner.py", + "-v" + ], + "timeout_seconds": 180 + } + } + }, + "tasks": [ + { + "id": "RP13-01", + "contract": "{\"id\": \"RP13-01\", \"title\": \"P13限定の提出復旧と新規文書資料削減\", \"requirement_ids\": [\"RP13-01\"], \"acceptance_ids\": [\"RP13-01\"], \"spec_ref\": \"docs/spec/RP13-01.json\", \"base_commit\": \"c88e2afbea3e1091a4cb048f49bd9891a6bdb869\", \"worker\": \"codex\", \"allowed_paths\": [\"runner/core.py\", \"tests/test_runner.py\"], \"depends_on\": [], \"resource_locks\": [\"runner-p13-code-isolated\"], \"validation_ids\": [\"runner-unit\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"c7ce1e08eefe0bc48afb17dcfec014221102f88072f8d9a3ef9f95b4a2c6664a\"}", + "state": "repair_pending", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-p13-recovery/RP13-01-implement", + "submitted": "1398aa2de9f4ca0fd57f7db9d640e829b425553b", + "candidate": null, + "main_base": null, + "reason": "test_failed: /Users/korizatou114/Library/Application Support/agent-control/the-tree-runner-p13-recovery/runs/6fc5dc653f934a01ad5d455c54a6143f-validation.json", + "repairs": 2 + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-p24-review-packet-before.json b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-p24-review-packet-before.json new file mode 100644 index 0000000..795f131 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/unlimited-individual-limits-r01/the-tree-runner-p24-review-packet-before.json @@ -0,0 +1,61 @@ +{ + "config": { + "project": "the-tree-runner-p24-review-packet", + "repo": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-p13-recovery-source", + "worktree_root": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-p24-review-packet", + "policy": { + "max_implementers": 1, + "max_llm": 2, + "max_heavy_tests": 1, + "max_llm_runs": 20, + "timeout_seconds": 900, + "test_timeout_seconds": 300, + "max_repair_attempts": 2, + "api_fallback": false, + "owner_approval_required": true, + "allowed_services": [ + "codex", + "claude", + "agy" + ], + "ui_mode": "semi-automatic", + "claude_max_budget_usd": null, + "models": { + "codex": null, + "claude": null, + "agy": null + } + }, + "validations": { + "runner-unit": { + "argv": [ + "{python}", + "-B", + "-m", + "unittest", + "discover", + "-s", + "tests", + "-p", + "test_runner.py", + "-v" + ], + "timeout_seconds": 180, + "execution": "trusted_runner_self_test" + } + } + }, + "tasks": [ + { + "id": "RP24-01", + "contract": "{\"id\": \"RP24-01\", \"title\": \"P24レビュー全文保持とRunner自身の正式試験実行を修正\", \"requirement_ids\": [\"RP24-01\"], \"acceptance_ids\": [\"RP24-01\"], \"spec_ref\": \"docs/spec/RP24-01.json\", \"base_commit\": \"5811e01a17c423b156c5a45c49e2c6e81a4e2153\", \"worker\": \"codex\", \"allowed_paths\": [\"runner/core.py\", \"tests/test_runner.py\"], \"depends_on\": [], \"resource_locks\": [\"runner-p24-code-isolated\"], \"validation_ids\": [\"runner-unit\"], \"max_repair_attempts\": 2, \"spec_sha256\": \"5841d6ac4673336b2ece64efb1b9af3dbf67838bf54b8fa1021f2280e5105854\"}", + "state": "completed", + "worktree": "/Users/korizatou114/Projects/TheTree-managed-worktrees/runner-p24-review-packet/RP24-01-implement", + "submitted": "053f391bf35482c0996292de263f2c39e88d96f0", + "candidate": "ba238177d33310fe8b4095998a178464488ed5e4", + "main_base": "5811e01a17c423b156c5a45c49e2c6e81a4e2153", + "reason": null, + "repairs": 2 + } + ] +}