-
New target projects never got .claude/agents/*.md deployed, so their first Planner call always failed immediately (claude --agent planner finds nothing in cwd) -- self-target usage never noticed since those files are already committed as part of this repo. create_project() now deploys and commits them alongside the existing .graphlemon/ init commit. Also: unroutable execution failures were recorded as a bare blocked_reason='execution_error' with no way to see what actually broke. scheduler._block_task()/task_execution._mark_blocked() now persist the real exception message into execution_event.payload (blocked_reason itself is unchanged -- budget_guardrail matches its exact value elsewhere). Found via a real console Start-button failure on a freshly created project; see docs/implementation-gap-analysis.md for the full writeup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Users had no way to actually start a queued goal from the console -- task.status just sits at 'queued' forever. Traced the root cause through 03-C: c_task_create's own contract ("若 max_concurrent_goals 已达上限, 任务落 queued 但不触发调度") implies task creation auto-triggers scheduling otherwise, and 03-C/04-P repeatedly assume an always-on Org Graph scheduler that watches the queue -- src/scheduler.py builds the mechanism but nothing ever spins it up as a running process, so no "start" button was ever designed because none was supposed to be needed. Design (confirmed with user, both recommended options chosen): - Pause/Stop are a soft stop, not a real subprocess kill -- the current in-flight `claude` call finishes naturally, only the next phase is withheld. Hard-killing would need Popen + PID tracking, out of scope. - No new task.status enum value -- reuses blocked_pending_human with blocked_reason='user_paused'/'user_stopped', riding the existing manual_resume mechanism instead of a schema change. New src/task_execution.py: run_or_resume_task_lifecycle() resumes from whichever phase actually hasn't run yet (queued/planning_brd_prd_sdd/ coding), start_task_execution_in_background() runs it to completion on its own daemon thread + sqlite connection, request_pause() blocks an in-flight task. Extended /route's manual_resume handling so resuming from a user-initiated pause/stop actually re-continues execution in the background, not just flips the status field back. Real gap found while building this: backend_server.create_server() never stored db_path on the server object -- needed for the new background thread to open its own connection to the same db. 22 new tests (14 src/test_task_execution.py + 8 TaskExecutionRouteTest in test_backend_server.py, HTTP wiring mocked around the already-unit-tested background thread mechanics). Verified against a real running server via curl: /execute 409s a non-queued task, /pause 409s a queued task and a double-pause, succeeds on a coding task. src/ suite: 496 tests, all green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
Two new console pages requested directly by the user, beyond 02-V's original declared non-goal ("不做完整多项目管理 UI(项目列表/切换)"): - v_project_list: full project list + rename + archive/reactivate. "Import an existing directory with real code" needs no new code -- project_scaffold.create_project() already only ever adds .graphlemon/ inside the target folder. New: src/project_query.py (list/get/update), GET/PUT/DELETE /api/projects[/:id]. DELETE archives (status='archived'), never a physical row delete -- project_id is FK-referenced from task/ artifact/bug_record/etc., and 01-M's own active/archived enum already models this as soft-delete. - v_goal_list: extends the existing v_task_board/v_task_detail rather than duplicating them. Added sort/order to task_query.list_tasks() (column whitelist, no injection surface) and edit/delete on v_task_detail, both restricted to status='queued' (task_query.TaskNotEditableError, HTTP 409) -- editing or deleting a goal after real work has referenced it would leave that history pointing at something that no longer matches reality. Implemented directly rather than through graphlemon's own self-hosting loop -- confirmed with the user first that implement_code() always uses a hardcoded single-node manifest (no plan_work_graph() call for Coder), so a multi-page/multi-endpoint feature like this would've been one unreviewable Coder session with no way to decompose it. Real bug found on first verification pass (not caught by the existing _insert_task test helper, which never writes execution_event): a real task_create.create_task() always writes an execution_event row in the same transaction (01-M's FK, no ON DELETE CASCADE) -- even a never-touched queued task already has one, so DELETE FROM task raised sqlite3.IntegrityError. Fixed by clearing the task's own execution_event rows first; new regression test goes through the real create_task() path. 474 tests green. Verified end-to-end against a real running server (every new endpoint exercised via curl) since the Chrome extension is still unavailable this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
Attempt 13b was the first real run to get all the way through a real cutover -- task lifecycle, classification, staging, health check (fixed last commit), and a genuine blue-green cutover all succeeded for real. It correctly landed in 'monitor' and then correctly couldn't finish: the real production exit condition (deploy_monitor.MONITORING_WINDOW_HOURS=24h OR MONITORING_MIN_TASK_COUNT=10 real tasks in the monitored project, NOT the disposable per-cycle canary project) isn't practical to wait out in an interactive session. This isn't a bug -- those are sensible real defaults -- it's a testability gap: run_self_hosting_closed_loop already exposes monitor_timeout_cycles as an override knob for exactly this kind of situation, but never threaded the two constants that actually gate "monitoring_window_clean" through to run_monitor_cycle. Added monitor_window_hours/monitor_min_task_count params, defaulting to deploy_monitor's real constants (production behavior unchanged), threaded through to the actual run_monitor_cycle call. New assertion locks in that overrides actually reach it. Also documents the full run of 9 real bugs found and fixed across 13 attempts in development-plan.md's U-042 note. 564 tests green (441 src + 123 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Attempts 7 and 11 both failed at exactly the same spot: process_alive read 'ok' within milliseconds of Popen, then http_health hit ECONNREFUSED in the same second and gave up. Real cause: process_alive only confirms the process EXISTS, not that backend_server.py has finished importing modules and actually bound its listen socket -- a real, non-zero startup gap. The old check_http_health did one urlopen(timeout=X) call; that timeout only bounds how long to wait for a response once connected, not how long to wait for the port to start accepting connections at all -- ECONNREFUSED fires essentially instantly when nothing's listening yet, regardless of timeout. This touches an explicit 03-C design statement ("不重试——重试属于给坏版本 第二次机会"), so confirmed with the user before changing it rather than just reinterpreting the policy unilaterally. Resolution: tolerating normal startup latency within ONE health-check pass isn't "retrying a failed check" -- check_process_alive already does exactly this (polls up to 30s for the pid to exist). http_health now does the same, polling within its own timeout for connection-refused-class errors specifically. A genuine non-2xx HTTP response is still a definitive answer from a live server and still fails immediately, no polling -- that boundary is unchanged. Two new real regression tests: a real server that starts listening 0.3s late is correctly waited for and passes; a real 500 response still fails immediately without being mistaken for "not up yet". 564 tests green (441 src + 123 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
…be04-main-checkout-drift
-
Attempt 10 crashed immediately in the fix committed last: git add -A -- . ':!.graphlemon-worktrees' throws "paths are ignored by one of your .gitignore files" (exit 1, not just advisory text -- confirmed even with advice.addIgnoredFile=false suppressed) the moment that path is ALSO gitignored, which it now always is. Reproduced locally. Plain `git add -A` (no explicit pathspec) already skips gitignored paths correctly on its own -- the explicit exclude was redundant now that .gitignore covers it, and actively conflicting. Simplified ensure_worktree_committed() back down. Also added the same .gitignore entry to test_work_node_run.py's _init_git_repo() fixture so real-worktree tests match production config instead of coincidentally not hitting this. 562 tests green (441 src + 121 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Attempts 8 and 9 both hit "local changes would be overwritten by merge" again, despite the earlier _check_prd_consistency worktree-isolation fix. Root cause is deeper: git worktrees deliberately SHARE project identity with their main checkout (same .git, just another checkout) -- a real `claude -p` call's memory:project write doesn't reliably respect the cwd it was invoked with, and can land directly on the main checkout regardless of which specific call triggered it. Worktree-wrapping one call (as done for _check_prd_consistency) only moves around which call happens to trigger this; it doesn't fix the underlying issue. ensure_worktree_committed() now also runs against repo_path itself (not just the worktree) right before every merge_worktree() call, in both work_node_run.run_work_node() and e2e_pipeline._check_prd_consistency(). Same already-tested mechanism, now applied generically regardless of which real call caused the drift or why. Fixing this surfaced a second real bug in the fix itself: running `git add -A` against repo_path while a node's own .graphlemon-worktrees/<id>/ checkout is still present on disk picks it up as a gitlink-like entry, which then shows as "deleted" the instant remove_worktree() runs right after. Fixed by excluding .graphlemon-worktrees/ from both the dirty-check and the add via pathspec (not relying on the caller repo's own .gitignore), plus added it to graphlemon's own .gitignore for correctness generally. New regression test: test_drift_on_main_checkout_is_auto_committed_before_merge reproduces a run_fn writing directly to repo_path instead of its worktree, and confirms a second node's own merge isn't affected by it. 562 tests green (441 src + 121 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
The 7th real self-hosting attempt was the first to actually reach a real health check against a real candidate process -- it correctly failed and triggered rollback_path_a for real: candidate terminated, git revert landed cleanly. But the candidate's `git worktree add --detach` checkout (candidate_instance_dir, tracked since U-037 specifically to know where this lives) was never removed -- left permanently on disk and still registered in `git worktree list`. A confirmed-bad candidate is never needed again after path A, unlike a successful cutover's candidate (which correctly becomes previous_instance_dir, kept until the monitoring window closes) -- these are different lifecycles that shouldn't share the same "leave it" behavior. rollback_path_a gains an optional candidate_instance_dir parameter (default None, existing tests unaffected) and removes the worktree before git revert when provided. self_hosting_e2e.py now threads through the value stage_deployment() already returns. New regression test uses a real `git worktree add --detach` checkout and confirms both the directory and its git worktree registration are gone after rollback. Also cleaned up 3 orphaned processes left by earlier test_deploy_rollback.py runs (backing temp dirs already gone) and the real leftover candidate worktree from this run. 561 tests green (440 src + 121 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
The 6th real self-hosting attempt (first to reach the full 4-node doc-writing graph after the previous five bugs) hit a genuinely new failure: this real graph_manifest had no edges at all, not just wrong field names -- worse than the case _normalize_graph_manifest already handled. With zero dependency info to normalize from, topological_order() scheduled 'benchmark' before 'sdd' (alphabetically first among zero-in-degree nodes), and build_prompt() crashed with KeyError: 'sdd_content' trying to format a template needing sdd's not-yet-produced output. _PROMPT_TEMPLATE always asks for exactly the same fixed 4-node linear chain -- that's a constant architectural fact, not something that should be re-inferred from the LLM's output every call. _normalize_graph_manifest now deterministically enforces brd->prd->sdd->benchmark whenever all four canonical artifact_types are present, regardless of what depends_on/edges the LLM did or didn't provide. Same "机制用代码" principle as ensure_worktree_committed and the earlier normalize logic. Three new regression tests: zero dependency info still gets the chain enforced, node_id != artifact_type still resolves to the real node_id (not a guessed string), and a partial set (missing one of the four types) is correctly left alone rather than guessed at. 560 tests green (440 src + 120 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
The 4th real self-hosting attempt (first to reach the actual doc-writing Work Graph after the previous three bugs) hit a real git merge failure: "local changes would be overwritten by merge", exit code 2. Root cause: _check_prd_consistency() calls execute_work_graph() directly against target_folder_path, completely bypassing work_node_run.run_work_node's worktree isolation. It uses agentType='planner' with memory:project, which writes a real update to .claude/agent-memory/planner/MEMORY.md in whatever cwd it runs with. Every prior real run of this check only ever targeted a throwaway clone, so this never mattered -- until self-targeting, where cwd IS the main checkout: the memory write landed as an uncommitted local change on master, which then blocked the SDD node's own (correctly isolated) worktree merge right after it. Fix: apply 05 §5.8's same unconditional self-target worktree rule here, reusing work_node_run's existing create_worktree/ensure_worktree_committed/ merge_worktree/remove_worktree rather than inventing a new mechanism. Non-self-target calls are unaffected (verified by both regression tests). Also cleaned up the real, now-recovered state from the failed 4th attempt: merged a real SDD-node worktree branch that got stranded uncommitted (conflict was just two near-identical memory file wordings, resolved by hand), and removed three orphaned worktrees left by earlier test runs. 557 tests green (437 src + 120 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
The third real self-hosting attempt (first to actually reach stage_deployment itself, past the earlier bootstrap/commit bugs) hit an uncaught FileNotFoundError: _copy_prod_db() assumed prod_db_path always exists and shutil.copy2()'d it unconditionally. graphlemon has never actually run as a persistent service, so ~/.graphlemon/graphlemon.db never existed. stage_deployment() already handles this exact class of "no prior state" bootstrap case for previous_commit_hash/previous_instance_dir -- prod_db_path never got the same treatment, and every existing test seeds a prod db in setUp so this was never exercised. Fix: when prod_db_path doesn't exist, create a fresh schema'd db instead of copying -- semantically correct, since a genuinely-first deployment has no production data to inherit. New regression test (BootstrapProdDbTest) covers it. 555 tests green (435 src + 120 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
A real self-hosting run (U-042, first real exercise of needs_worktree=True since U-035 forces it unconditionally for self-target) exposed a real bug RealGitWorktreeIsolationTest never caught: its own run_fn always committed its edits, but a real Coder isn't instructed anywhere to git commit its own changes. The Coder genuinely edited src/org_loop_route.py and verified it in its own test_report -- but never committed, so merge_worktree()'s `git merge` was a true no-op ("Already up to date"), and remove_worktree() then silently discarded the dirty worktree along with the real work. code_diff came out empty; the change never reached the branch at all. Adds ensure_worktree_committed(): checks `git status --porcelain` in the worktree before merging and auto-commits any dirty state found. A no-op when the worktree is legitimately clean (e.g. a pure investigation task). Same "判断用 Agent,机制用代码" principle already used elsewhere in this codebase -- don't rely on persona prompt-engineering for a mechanical correctness guarantee that the system itself should hold. Two new regression tests: one reproducing the exact "edited but never committed" scenario, one confirming a legitimately-clean worktree still merges fine. 554 tests green (435 src + 119 supervisor). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds src/deployment_query.py (list_deployments/get_current_deployment/ get_deployment -- no project_id filter needed, deployment has no project_id column since there's exactly one graphlemon self-target) + three new backend_server.py routes (GET /api/deployments, /current, /:id, the last with recent deploy_health_signal history) + a new v_deployment_status page in frontend/app.js polling every 5s (deployment.status changes don't go through c_ws_subscribe -- Deploy Supervisor writes deployment/ deploy_health_signal directly, no execution_event of its own). No 02-V spec exists for this view (PH5 was added after 02-V was written) -- DOM/class conventions follow the existing pages' own patterns. Verified against a real running server via urllib (browser extension unavailable this session), plus 9 deployment_query unit tests and 5 real HTTP route tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds supervisor/self_hosting_e2e.py: run_self_hosting_closed_loop() chains every PH5 module built this session (U-034~U-041) with the Graph Engine's own scheduler (U-032) into one real, callable path matching self-development- feasibility.md's 9-step scenario -- self-target Goal -> Planner/Coder/ Validator -> core_engine classification -> stage -> health check -> cutover (or rollback path A if health check fails) -> monitor until either monitoring_window_clean (finalized) or anomaly_detected (rollback path B). Both real outcome paths are branches of the same call, not two separate scripted scenarios. Explicitly documented as this acceptance scenario's own orchestration glue, not a claim about Deploy Supervisor's eventual production poll loop (U-036's discovery-loop hook is still unfilled; building that is future work). Mocked wiring tests (6, run in normal regression) stub each already independently-tested module boundary and only verify the branching logic. The real, literal acceptance test is built and ready (RealSelfHostingClosedLoopTest, reproduces the document's own "loop_round > 3 should be >= 3" example) but gated behind RUN_REAL_SELF_HOSTING_E2E=1 -- deliberately a separate, distinctly-named env var from this project's usual RUN_REAL_AGENT_E2E, since this is the highest-blast-radius action in the project (can replace the running graphlemon process and git-revert commits on this real repo). Per this Unit's own explicit build instruction, not executed as part of implementing it -- doing so requires checking with the user first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds supervisor/deploy_rollback.py: rollback_path_a (pre-cutover, health check failed -- terminate candidate, delete its one-time DB copy, git revert, record a bug_record(core_engine) audit trail) and rollback_path_b (post-cutover, anomaly detected -- stop the bad Live, revive previous_instance_dir, re-run the same three health-check signals directly against it, git revert only once re-verification passes). Design choices follow established conventions rather than inventing new ones: no unified rollback() entry point (deployment has no PID columns, same reason deploy_stage/deploy_cutover already require callers to pass pids explicitly); ROLLBACK_FAILED is the raised error name while the persisted deployment.status is 'blocked_pending_human' (same pattern CheckoutFailedError/CutoverFailedError already use). git revert conflicts fold into the same rollback-itself-failed fallback both paths share, per design doc §10 open question 3's own recommendation, with `git revert --abort` run afterward so a failed attempt doesn't leave the repo mid-conflict. This is PH5's highest-rework-risk Unit per development-plan.md's own flag, so its own "rollback itself fails" fallback is tested for real, not assumed: a genuinely broken previous_instance_dir (a real empty directory with no Python package in it) that really fails to bind its port, and a genuinely conflicting git history (a later commit touching the same line the self-modification changed) that really fails `git revert` -- both correctly land on blocked_pending_human without retrying forever or silently succeeding, and the conflict case leaves the repo clean afterward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds supervisor/deploy_monitor.py: run_monitor_cycle() is one sampling pass, called repeatedly by Deploy Supervisor's own poll loop (03-C's own text: not a one-shot call). process_alive/http_health/canary_task/ validator_approval_rate sampled in order, stopping at the first anomaly. validator_approval_rate's "approve rate" has no literal definition anywhere in 01-M/03-C/04-P (no such execution_event type exists) -- derived instead from a verified structural fact in scheduler.finish_task_lifecycle: PASS closes straight to done with no bug_record ever created, FAIL/INCONCLUSIVE always creates one via judge_root_cause. A task counts as approved iff no bug_record(source='validator') exists for it. Baseline is computed by the same function looking back MONITORING_WINDOW_HOURS from cutover_at rather than persisted as a new column; baseline=None (cold start) never flags an anomaly rather than defaulting to 0. Crash-count/consecutive-http-fail windows are approximated by sample count (N = CRASH_WINDOW / MONITOR_SAMPLE_INTERVAL) rather than a true rolling time window -- documented simplification, correct as long as sampling stays roughly interval-paced. Caught my own design misreading via the tests: process_alive's monitoring- phase anomaly condition is MAX_CRASH_COUNT crashes within CRASH_WINDOW (a crash-loop detector), not "currently not alive" -- unlike U-038's health check where any single failure fails closed. A single dead reading during monitoring is recorded but must not trigger an immediate rollback by itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds supervisor/deploy_cutover.py: stops the old Live process, retires the health-check-mode candidate, and relaunches the same already-verified candidate checkout bound to LIVE_PORT/PROD_DB_PATH -- http.server has no live socket-rebind operation, so "switch to production port/db" is implemented as stop-then-relaunch-from-the-same-dir rather than an in-place rebind (functionally equivalent, the only mechanism stdlib actually offers). Adds LIVE_PORT=8420 to 05 §5.9's MVP-fixed parameter table -- the one parameter that table was missing (frontend/app.js never needed one, same- origin relative fetches don't care, but successive deployments need to keep landing on the same port for a human bookmark to keep working). "在途 work_node_execution 重启后可恢复" is satisfied by relying on the existing REQ-06 recovery machinery unchanged -- cutover doesn't add any special-case logic, a cutover-triggered restart is just another interruption REQ-06 already survives. A real (not mocked) test caught a real bug: deploy_supervisor.is_alive()'s os.kill(pid, 0) check can't distinguish "truly running" from "exited but never reaped by its own parent" -- a zombie's PID slot answers kill(pid, 0) successfully until waited on. U-036's own subprocess test never caught this because that skeleton deletes its own PID file before exiting, sidestepping is_alive() entirely; deploy_cutover's test is the first one to actually poll for a stopped process to disappear, and it hung until fixed. Added reap_if_exited() (real os.waitpid for processes we spawned ourselves, falls back to is_alive() for pids from a prior session where ChildProcessError means we're not the real parent) and switched deploy_cutover._stop_process, deploy_health_check.check_process_alive, and deploy_supervisor.stop() to use it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds supervisor/deploy_health_check.py: run_health_check() runs the three pre-cutover checks in order, stopping at the first failure (03-C: "任一检查项 fail 立即整体判定 health_check_failed,不等待其余检查项跑完") and persisting a deploy_health_signal row for every check that actually ran. - process_alive: polls deploy_supervisor.is_alive() up to HEALTH_CHECK_STARTUP_TIMEOUT. - http_health: hits the candidate's /api/health -- added to backend_server.py (U-037 deliberately left this for U-038). Not just "process exists" -- runs a real SELECT 1 to catch a wedged sqlite connection a live-but-stuck process would otherwise pass. - canary_task: a real (not mocked) project+task creation round-trip against the candidate over HTTP -- no real Org Graph/claude calls, since c_task_create's synchronous 'queued' landing is already a deterministic, always-should-pass result, which is what a canary needs. Timeouts are the MVP-fixed drafts from 05 §5.9, explicitly flagged there as needing real calibration later, not hardcoded as authoritative. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds supervisor/deploy_stage.py: stage_deployment() creates a deployment row, git-worktree-checks-out the target commit into a fresh directory (--detach, never in-place git pull), copies the production DB to a one-time candidate copy, and Popens a real candidate backend process on a free port. Return value follows 04-P §4.6's state-machine table (status after the call is 'candidate_starting') rather than 03-C's one-line 'staged' summary -- 04-P's own trigger text ("c_deploy_supervisor_stage 完成" moves the row to candidate_starting) is the more detailed/authoritative source when the two disagree, same precedent used throughout this project. Found and closed two real gaps along the way: - deployment's own field list never included where a candidate's checkout directory lives -- added candidate_instance_dir to schema.py (also what the NEXT staging call's previous_instance_dir bootstraps from once a deployment reaches 'finalized'). - backend_server.py had no CLI entry point at all since U-007 -- create_server() was only ever instantiated in-process by tests. Added main() (argparse + serve_forever + SIGTERM->shutdown() from a separate thread, per http.server's own same-thread-deadlock warning). RealCandidateLaunchTest spawns and HTTP-probes a genuine `python3 -m src.backend_server` subprocess against a real git-worktree checkout of this repo (not mocked) -- caught a real ordering issue doing so: git worktree only sees committed content, so testing before this Unit's own commit landed would check out backend_server.py without main() at all. Fixed by using `git stash create` (builds a commit object from the current working tree without touching HEAD/index) as the commit-to-test-against, falling back to plain HEAD once the tree is clean post-commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
Ports graphloop's test_isolated_coding_workspace.py/test_workspace_security.py/ test_change_manifest.py/test_selective_apply.py into supervisor/isolation/ (only the import path changed -- the graphlemon modules were already adapted verbatim from graphloop, confirmed API-identical) plus a few extra edge cases the originals didn't cover. 52 tests green, replacing what had been a single manual smoke test since these modules were staged. Adds supervisor/deploy_supervisor.py: process lifecycle only (PID file, atomic status file, start/stop/is_running), no deploy decision logic yet -- that's U-037+'s job, injected via the poll_fn hook run_foreground() calls on a schedule. PID/status conventions adapted (not imported) from graphloop's hermes_watchdog.py atomic-write pattern. Fail-closed: any exit path (normal, SIGTERM, or an exception from poll_fn) unconditionally writes status='stopped' and removes the PID file, so is_running() never lies. RealSubprocessLifecycleTest actually spawns and stops a real `python3 -m supervisor.deploy_supervisor` process rather than mocking it, per this Unit's own "能独立启停" Done criterion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds paths.repo_root()/is_self_target() and forces needs_worktree=True in work_graph_orchestrate.run_graph() whenever target_folder_path is graphlemon's own checkout, regardless of caller intent -- 05 §5.8's unconditional rule, since a self-target in-place edit risks corrupting the currently-running process's own source out from under it. Adds src/core_engine_classify.py (path-based rule: .claude/agents/*.md, src/*, supervisor/* = core_engine; frontend/, docs/ = not) and bug_record.record_core_engine_change() (a PASS-path audit record, distinct from create_bug_record's FAIL-path root-cause attribution -- no route decision, status='resolved' from creation). scheduler.finish_task_lifecycle() records this audit trail as an additive side effect after close_task_done() on self-target PASS closures. Found and resolved a real SDD-internal conflict (confirmed with the user via AskUserQuestion) before wiring this up: supervisor-auto-rollback-design.md §1 triggers Deploy Supervisor on self-target+Validator-PASS alone, but 03-C §3.11/04-P §4.6 literally gate it on an existing core_engine bug_record -- and task_create.py confirms goal-type self-improvement tasks (the exact self-development-feasibility.md walkthrough scenario) never produce a bug_record on their happy path, which would silently defeat that whole task class under the literal reading. Resolved per user decision: trigger on self-target+PASS alone; the bug_record is a byproduct, not a gate. p_deploy_supervisor_blue_green (04-P §4.6) never touches task.status, so close_task_done() still runs unconditionally -- Deploy Supervisor is a fully decoupled state machine, matching §2's "no shared failure domain" principle. org_loop_route.py is intentionally untouched -- fast_lane/full_replan is FAIL-path retry routing, orthogonal to this PASS-path trigger; deployment row creation stays U-037's job per its own Done criterion. 417 tests green; RealGitWorktreeIsolationTest (U-013/REQ-03's real git worktree regression) re-verified passing per this Unit's own Done criterion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Adds the two Deploy Supervisor entities from 01-M §1.15/§1.16 (17 tables total): deployment (full 15-value blue-green state machine, per 01-M's own text rather than supervisor-auto-rollback-design.md §3's abbreviated summary table) and deploy_health_signal (append-only health/monitoring samples). ux_deployment_active enforces 01-M's "only one non-terminal deployment at a time" constraint via an expression-based unique index (CREATE UNIQUE INDEX ... ON deployment((1)) WHERE status NOT IN (...)) -- the same partial-unique-index idiom work_graph_run already uses, just with a constant instead of a real grouping column since there's exactly one graphlemon self-target. bug_record.root_cause_category's schema-level CHECK now accepts core_engine, but bug_record.py's own application-layer whitelist deliberately does not follow yet -- classifying a bug as core_engine is a file-path-based rule (05 §5.8), not something the Validator's existing 3-perspective judge_root_cause can produce, and wiring that classification + c_org_loop_route's routing is U-035's job. 392 tests green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
CLAUDE.md is explicit that PH5 (self-modification/auto-rollback) must not start until PH4 has genuinely closed -- not just "all unit tests green," but 06-验收与测试锚点.md's REQ-01~14 happy/boundary/failure cases actually covered. A REQ-by-REQ audit (the same discipline that caught U-031's integration gap) found 5 real gaps: - REQ-03 failure: work_node_execution.status jumped straight running->retrying, skipping 'reflecting' (declared in the schema enum, never once written by any code). - REQ-05 failure / REQ-09 boundary: the "scheduler keeps serving other queued tasks past one blocked_pending_human" scenario was never tested against a real scheduler -- the old test literally said "no scheduler exists yet to test against directly" (stale since U-032). - REQ-06 / REQ-07 / REQ-14 failure (the biggest gap, shared root cause): CorruptPersistedStateError, ClassifierBlockedError, and UsageLimitWaitTimeoutError were each correctly raised and each correctly marked their own work_node_execution 'blocked', but nothing ever routed that to task.status='blocked_pending_human' with the blocked_reason each REQ's acceptance text names. e2e_pipeline.run_end_to_end is split into run_task_lifecycle() so scheduler.py's run_goal_to_completion can create the task itself and catch these three specific exceptions, following the same UPDATE+execution_event pattern budget_guardrail.check_budget() already established for REQ-10. - REQ-02 boundary: PRD internal-contradiction check (verifying->reflecting) had no implementation anywhere -- artifact_write always marked validation_status='passed' unconditionally. Added an optional verify_fn hook to work_graph_orchestrate.run_graph() (one regeneration attempt, then ConsistencyCheckFailedError) and a real consistency-check agent() call scoped to the PRD node only. The first real run of this new call caught a genuine bug: the model returned "consistent": "false" as a JSON string, not a boolean, which `if ok:` silently treated as truthy -- fixed with explicit _coerce_bool(). - REQ-13 failure: minor test-assertion weakness (WS 404 rejection test didn't check the JSON error body). 384 tests green. REQ-01~14 now have real, exercised coverage rather than a "tests are green" assumption -- PH4 can be treated as genuinely closed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Reviews every real invocation of the migrated Planner/Coder/Validator personas since U-004's initial 2026-07-31 verdict (U-011's "SOUL 原则 #10" reference, U-014's full real snake-game closed loop, U-031's agentType spike + full plan_documents run) and finds no new persona-content deviation beyond the three already fixed in U-004 (Feishu Base token, lark-cli coupling, --dangerously-skip-permissions instruction). The disallowedTools bug found along the way (05 §5.4.1.2) is a tool-permission-layer Claude Code behavior, not persona content, so it doesn't extend A-2's deviation scope. Final verdict: stays "部分成立" (not upgraded to confirmed) -- the Coder rewrite already happened and later clean runs don't retroactively undo that -- but the deviation scope is now confirmed closed rather than open-ended, which is what U-033 exists to establish before PH5 planning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
04-P's blocked_pending_human branch, 01-M's task-table indexing rationale, and REQ-09's boundary case all presume an autonomous dispatcher scans queued tasks and keeps serving the queue past a blocked one -- but no such component existed. org_node_lifecycle._check_concurrency (U-010) only gates a single trigger call; e2e_pipeline.run_end_to_end (U-014) drives exactly one task, synchronously, only ever called from tests. Adds src/scheduler.py: run_scheduler_once() dispatches multiple Goals via ThreadPoolExecutor bounded by org_concurrency_config.max_concurrent_goals, each worker opening its own sqlite connection (with a busy_timeout) rather than sharing one across threads -- U-027's SIGSEGV already proved that needs a hand-held lock, which would just serialize everything back to front. Also closes a gap run_end_to_end's own docstring already flagged: it never called close_task_done (PASS) or org_level_loop.run_fix_loop (FAIL), leaving every task stuck at 'validating'. finish_task_lifecycle() applies that step without changing run_end_to_end's own tested behavior or scope. Real-write concurrency (multiple threads, separate connections, same db file) is stress-tested for real in test_scheduler.py without spending API budget -- the actually-new risk surface here is the threading/connection design, not the already-validated (U-031/U-017) work itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
U-031: closes a structural integration gap found during the REQ-01~14 regression audit -- plan_work_graph/execute_work_graph/run_work_node were each individually tested but never actually called together in production code (e2e_pipeline.py's only validated closed loop bypassed all three via flat org_node_trigger calls). Adds work_graph_orchestrate.py (topological sort over graph_manifest + one one-node dynamic-workflow agent() call per Work Node, needed for REQ-06's per-node breakpoint recovery), rewires e2e_pipeline.py's plan_documents/ implement_code onto the real chain, and fixes two bugs a real API run surfaced: graph_manifest's id/node_id/depends_on normalization (work_graph_plan._normalize_graph_manifest) and a missing repo_path in run_work_node's cwd resolution. work_node_run.py also now marks a node 'blocked' instead of leaving it stuck 'running' on any unretryable run_fn exception. U-024.5: the regression audit also found REQ-12's reconnect boundary case (refetch a snapshot after a dropped WS reconnects) was never implemented -- only the dashboard reacted to onStatusChange, and even it never refetched. Fixes mountDashboard/mountTaskBoard/mountTaskDetail (which embeds v_graph_visualizer) to refetch their current snapshot on reconnect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
三个都是 config_id='default' 单例配置,首次使用即创建、之后原地更新 (upsert),不需要单独的初始化流程: - c_concurrency_config_update:新增 src/settings_config.py( org_concurrency_config 之前完全没人碰过),校验 max_concurrent_goals 落在 1-50。 - c_budget_config_update:budget_guardrail.py 新增 upsert_budget_config() ——跟 U-019 已有的 update_budget_limit_and_resume()(只更新已存在的 行)不是一回事,这个是真正完整的设置页控制器,支持首次创建,沿用 同一套"额度调高后自动恢复 budget_exceeded 阻塞任务"副作用(提取成 _resume_budget_blocked_tasks() 共享)。 - c_log_retention_config_update:同样在 settings_config.py 里,校验 retention_days > 0。 三处校验都在业务层做完整检查,HTTP 层额外兜了 except (InvalidParamError, TypeError)——payload 数值字段缺失时 None<=0 这类比较会抛 TypeError 而 不是业务异常,两个都要接住才能保证返回干净的 400 而不是裸 500。 backend_server.py 第一次加了 do_PUT。 前端三个页面共享一个 renderSettingsSubnav() 小组件模拟 02-V 原型里的 设置下拉菜单。真实服务器端到端验证过三个 PUT/GET 的完整读写循环。 PH3(人机交互控制台)至此全部完成:U-022~U-030。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
c_skill_diff 和 U-028 的 c_artifact_get 一样,只存在于机器可读的 sdd-mvcp-contract.json,03-C 正文从未写出来,已订正补全。 c_skill_list/c_skill_get 两份文档都没定义,是 v_skill_library/ v_skill_audit_log 自己的展示需求倒逼出来的缺口。 新增 src/skill_query.py:list_skills()、get_skill_audit_log()、 get_diff()(git diff <before> <after>——skill_update.py 的 _write_and_commit 把每次迭代做成原子单文件 commit,整仓库范围的 diff 天然只涉及那一个 Skill 文件)。 关键安全考量:Skill 库活在 graphlemon 自己的仓库里,c_skill_rollback 会真的在这个仓库里跑 git commit——如果 HTTP 层写死指向真实仓库根目 录,任何针对这条路由的测试都会在正在跑的这个 graphlemon 仓库里留下 真实提交。参照 db_path 已有的做法,给 create_server() 加了 skill_repo_path 参数(默认 graphlemon 真实仓库根,测试传临时仓库), 不通过 HTTP 暴露这个参数,避免变成任意写入的口子。记录在 05 §5.4.1.4。 真实服务器 + 临时仓库端到端验证过完整的 update→diff→rollback 闭环, 确认过跑完之后 graphlemon 自己仓库的 git log 完全没变化。 v_skill_library/v_skill_audit_log 不检查"当前项目"——Skill 是平台级 资产,跟项目无关。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
c_artifact_get 在机器可读的 sdd-mvcp-contract.json 里其实定义过 (GET /api/artifacts/:artifact_id),只是 03-C 正文一直没写出来,已订正 补全。c_artifact_list 连 MVCP JSON 里都没有 operation 定义,纯粹是 v_artifact_browser 自己的过滤字段倒逼出来的缺口,跟 U-025 补 c_task_list 是同一类问题。 新增 src/artifact_query.py:list_artifacts() 返回裸数组(跟已有的 list_tasks() 保持同款风格);get_artifact() 把血缘链(按 depends_on_artifact_ids 往回走,只走第一个依赖——这条流水线的实际产出 只有线性 BRD→PRD→SDD 链)和文件内容(artifact 表本身没有 content 列, 走 project.target_folder_path + artifact.file_path 现读磁盘)一起返回, 预览面板一次请求就够。 前端预览统一用 <pre> 纯文本展示,不做 Markdown 渲染/代码高亮——02-V 原文想要那个效果,但需要引入第一个前端第三方库,跟这个控制台一贯的纯 vanilla JS、无构建步骤做法冲突,是有意简化。顺手给 index.html 常驻导航 栏加上了"产物"链接。 真实服务器验证过三级血缘链(BRD→PRD→SDD)返回顺序正确。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
execution_log_file 的行生命周期 U-013 早就管好了,但从来没有代码真的往 日志文件里写过内容——src/execution_log.py 新增 append_log() 补上这条 缺失的写路径,设计成 run_fn 执行过程中随时可调用,不关心节点在做什么。 get_log() 按 offset/limit 分页读,download=true 时走原始文件流。 c_ws_log_tail(WS /api/ws/work-nodes/:node_exec_id/log)复用 U-024 手写 的 RFC 6455 机制,轮询 execution_event(log_appended);节点一旦转终态 (done/blocked/failed)服务端主动发关闭帧断开,不让客户端一直轮询。 p_log_retention_cleanup 实现成纯函数 run_log_retention_cleanup():过期 扫描 + max_total_size_mb 超限按最旧优先追加候选 + 单个文件删除失败只 记录不阻塞其余项,没有配调度器(不在本 Unit 覆盖范围),留给未来的 调度进程周期调用。 过程中意外抓到一个真的并发 bug:_Server.server_close() 关闭共享 sqlite 连接时原来没拿 conn_lock,如果这时候正好有个 WS 轮询线程在 另一边持锁执行 SQL,两个线程无同步地并发操作同一个 C 级 sqlite3 连接 对象,实测直接把解释器崩了(SIGSEGV)——U-024 就已经有这个隐患,这次 高频 WS 测试把时序窗口撞开了。修好后连续跑几十次全部稳定通过,记录 在 05 §5.4.1.3。 frontend/app.js 的 v_log_viewer 挂在 v_task_detail 每个 to-do 节点的 展开详情里,首次展开才建连接(懒加载),运行中态走 c_ws_log_tail 实时 滚动、终态走 c_execution_log_get 静态拉取,活跃连接在任务级刷新/离开 页面时统一关闭。真实服务器 + Node 原生 WebSocket/fetch 端到端验证过 实时追加与终态自动断开的完整链路。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
纯前端 Unit,数据已经在 U-025 的 c_task_get 里,只是 task_query.py 的 _work_nodes_for_run 补上了 hover tooltip 需要但当时没取的字段 (input_schema_ref/output_schema_ref/quota_limit_type/ quota_wait_started_at)。 frontend/app.js 新增 computeGraphLayers():按 graph_manifest 节点的 depends_on 边做拓扑分层,渲染成按执行层级排布的行,fan-out/fan-in 都能正确分层——用一个 n1→[n2,n3]→n4 的真实 graph_manifest 验证过, 直接从 app.js 里取出 computeGraphLayers 本体(不是重新实现一份逻辑) 跑真实服务器返回的 JSON,分层结果完全正确。节点按 03-C 3.6 节六态 配色,waiting_for_quota 态 hover 展示 quota_limit_type + 已等待时长。 02-V §2.7 原描述还包含一份 Org Graph 层,但 v_task_detail 页面顶部 已有等价的 org-strip 面板,这里判断重复渲染没有意义,只做了 Work Graph DAG 层,Org Graph 复用页面已有的那个。 嵌入位置:v_task_detail 里原来"随 U-026 补上"的占位提示换成了真实 的 DAG 面板。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
三个页面需要的读接口此前都不存在,本 Unit 一并建了: - c_task_list/c_task_get(03-C §3.2,此前无 Unit 认领):新增 src/task_query.py + GET /api/tasks、GET /api/tasks/:task_id。 c_task_get 的返回比 03-C 字面契约更完整——额外带上 work_graph_run(含解析后的 graph_manifest)+ 每个 run 的 work_node_execution 列表,因为 v_task_detail 的 to-do 树需要这份数据, 03-C 已订正补全。 - c_org_loop_route(U-016 已实现 route_org_loop(),从未接 HTTP)第一次 接上 POST /api/tasks/:task_id/route,v_task_detail 的人工干预 (manual_resume)按钮需要它。 - 预算护栏此前没有任何读接口(03-C 只写了 PUT),新增 budget_guardrail.list_budget_configs() + GET /api/settings/budget 给仪表盘预算条用。 frontend/app.js 补上真实的 #/(org graph 三节点卡片、当前任务概要、 预算条、事件时间线,全部通过 c_ws_subscribe 实时刷新)、#/tasks (关键字/状态/org_node 过滤)、#/tasks/:task_id(task 字段、to-do 树、 关联 artifact/bug_record、blocked_pending_human 告警条 + manual_resume 按钮)。路由改成对象形式支持挂载态清理,切页面时关掉上一个页面的 WebSocket 连接。v_graph_visualizer/v_log_viewer 子模块明确标成占位 提示(随 U-026/U-027 补上),不是假造内容。 顺带把 U-023 里因 c_task_list 当时不存在而简化的 related_task_id 换回真下拉,goal_composer/bug_feedback 提交成功后也改回跳转真实的 v_task_detail。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Python stdlib 没有 WebSocket 支持,src/ws_subscribe.py 手写 RFC 6455 握手(hashlib+base64 算 Sec-WebSocket-Accept)+ 帧编解码(服务端帧不 加掩码,客户端帧强制解掩码),不引入 websockets 之类第三方库 (05 §5.7)。 推送用轮询而非写路径 hook:execution_event 的写入分散在十几个调用点 (org_node_trigger/work_graph_execute/work_node_run/task_create/ bug_record/...),没有单一写入路径可以插桩,backend_server.py 的 _handle_ws_subscribe 劫持 ThreadingHTTPServer 连接后每 0.5 秒按 rowid 增量查一次 execution_event(project_id/可选 task_id 过滤), 轻松满足 REQ-12 的 ≤2 秒目标。断线重连按 03-C §3.14 走 REST 快照,不 在 WS 层做消息重放,新连接只看连接那一刻起的新事件。 daemon_threads = True 避免长连接 WS 线程阻塞进程退出。 验证:test_backend_server.py 用原始 socket 走真实握手+推送延迟断言; 另外用 Node 原生 WebSocket 客户端做跨实现互操作性验证,连真实运行的 服务器,从建连到收到 c_task_create 触发的真实推送全程走通。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
c_task_create(U-009 已实现的 task_create.create_task())第一次接上 POST /api/tasks。c_bug_report_create 此前完全没有实现,新增 bug_record.create_bug_report() + POST /api/bug-reports。 发现并订正 03-C 一处输入契约遗漏:c_bug_report_create 的 input_contract 没写 project_id,但 execution_event.project_id 是 NOT NULL、bug_record 表自己没有 project_id 列可以在 related_task_id 留空时兜底推导,字面上无法满足约束——按 c_task_create 同款处理方式把 project_id 变成必填,03-C 已订正。 frontend/app.js 补上 #/goals/new、#/bugs/new 真实交互;控制台还没有 "当前项目"状态的正式载体(v_dashboard 是 U-025 才做),project_id 暂存 localStorage,v_project_setup 创建成功时写入。related_task_id 因为 c_task_list 未被任何 Unit 认领,简化成文本输入而非下拉,等 U-025 补上 任务列表后再换。提交成功后原型写的是跳转 v_task_detail,该页面也是 U-025 才建,暂时改成原页面内展示确认信息。 端到端冒烟时额外发现一个真 bug:create_bug_report() 原本没校验 project_id/related_task_id 是否存在,坏 project_id 会在 execution_event 插入时触发未捕获的 IntegrityError,请求直接 500——补了存在性校验, 改为干净的 INVALID_PARAM/400。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
c_pick_folder 补上 Windows(PowerShell FolderBrowserDialog)和 Linux (zenity 优先、回退 kdialog)实现,三平台都覆盖到。c_project_create (U-002 已实现的 project_scaffold.create_project())第一次接上 HTTP 路由 POST /api/projects——backend_server.py 因此第一次需要持有 sqlite 连接,schema.connect() 加 check_same_thread=False 参数,配合 server.conn_lock 序列化 ThreadingHTTPServer 各请求线程的访问。 frontend/app.js 补上 #/setup 真实交互(选文件夹→校验 Git 仓库→非 Git 仓库二次确认→创建→跳转仪表盘),DOM class 命名对齐 docs/wireframes/v_project_setup.html。 REQ-11 完整 happy 用例(含创建 task)还需要 U-023 的 v_goal_composer/c_task_create,本 Unit 只完成 v_project_setup/ c_pick_folder/c_project_create 部分,development-plan.md 已记录。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
now 默认值从 time.monotonic 改成 time.time(墙钟),使 quota 等待的 已耗时计算能在进程重启后正确续算;run_work_node 对已 done 的节点直接 返回存量结果不重跑,对 retrying/waiting_for_quota 节点从持久化的 retry_count/quota_wait_started_at 续算而非清零;needs_worktree=True 时复用已存在的 worktree 而不是重新 create。新增 resume_work_graph_run() 返回一次 run 里尚未完成的 node_exec_id 列表, 并对内部不一致的持久化状态统一抛 CorruptPersistedStateError。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Re-checking REQ-14's boundary case against the letter of its text found a real functional gap, not just a testing gap: U-013's opus handling only did "skip backoff" -- it never actually retried on a downgraded model, just failed immediately. REQ-14 boundary says "不走退避轮询,直接降级到非 Opus 模型重试" (skip backoff, retry immediately on a non-opus model) -- the retry was the part that was missing. Fixed: run_fn's signature is now run_fn(cwd, model=None); an opus quota signal retries immediately (no backoff) with opus_downgrade_model (default "claude-sonnet-5"). If the downgraded model somehow also reports an opus signal (shouldn't happen -- it's not using Opus), this doesn't loop forever chasing it, raises UsageLimitWaitTimeoutError instead. work_graph_execute.py's own opus handling (U-012) deliberately NOT changed to match: REQ-14's coverage only names c_work_node_run/c_org_node_trigger, and that mechanism (a multi-agent dynamic workflow, no single top-level --agent identity) has no one "the model" to downgrade -- a deeper A-1 question, documented as out of scope rather than silently left inconsistent without explanation. Also filled in a real test gap: quota_limit_type=weekly (detection + recovery, and the 9-day timeout) was never tested despite the mechanism already supporting it since U-013. 11 new/changed stdlib unittest cases across work_node_run.py's test suite (all existing run_fn lambdas updated for the new signature). 178 total across U-001-020, all passing. This closes PH2-S3 alongside U-019. REQ-14 is now genuinely complete, not just "mostly right." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Found running U-014's full real closed loop: the Validator gave an INCONCLUSIVE verdict and explicitly said it had no Bash tool, despite its frontmatter granting one. Isolated with a minimal test subagent: any "Bash(...)" scoped pattern (e.g. "Bash(git commit:*)") in a subagent's disallowedTools frontmatter field disables Bash *entirely*, not just that subcommand -- confirmed by comparing a disallowedTools=[Edit,Write] agent (Bash works) against one adding a single Bash(git commit:*) entry (Bash breaks completely). The CLI's own --disallowedTools flag documents this exact scoped syntax as supported; subagent frontmatter just doesn't honor it the same way, and nothing in the docs says so. Fixed agent_profile.py's validator FRONTMATTER to disallowedTools= [Edit, Write] only -- dropping the four Bash(git *) entries that were silently breaking test execution. The "don't commit/push/reset/clean" constraint now relies on the prose instruction already in the migrated AGENTS.md instead of a tool-level block that never actually worked; fixed that file's own (now-false) claim that it was enforced at the tool level, and documented the finding centrally in 05-实施约束与依赖.md §5.4.1.2 since it'll matter for any future agent definition, not just this one. Verified for real, redeployed: Bash now works for the actual Validator identity, Edit/Write remain correctly blocked. 1 test fixed (was asserting the now-removed scoped pattern) + 1 new test (disallowedTools frontmatter must never contain "Bash(" again). 174 total across U-001-019, all passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
check_budget(): accumulates current_usage on success (04-P monitoring-> monitoring), and on reaching/exceeding limit_value transitions the task to blocked_pending_human with blocked_reason='budget_exceeded' (never 'usage_limit_wait_timeout' -- 04-P is explicit these two guardrails, human- configured budget vs Claude Code's own plan quota, must not share a blocked_reason) and raises BudgetExceededError rather than returning allowed=false silently, matching 03-C §3.12's own errors list. update_budget_limit_and_resume(): REQ-10's boundary case needs "raise the limit -> paused tasks resume" to be real; built the minimal version this Unit needs rather than the full settings-page controller (c_budget_config_update, U-030/PH3's job) -- reuses U-016's manual_resume (org_loop_route.route_org_loop) for the actual status restoration instead of reimplementing it. 6 new stdlib unittest cases, all real (sqlite, no mocking needed). 173 total across U-001-019, all passing. Also: the U-014 full real closed-loop run that's been going in a detached background process since earlier this session finished -- 957s (~16min). All 4 real BRD/PRD/SDD/Benchmark docs, a real JS snake-game implementation (engine.js/engine.test.js/index.html/app.js) from the Coder, and a genuinely careful INCONCLUSIVE verdict from the Validator: it explicitly refused to accept the Coder's own claimed "10/10 passed, verified with headless Chrome" without being able to reproduce it, correctly citing its own migrated SOUL.md rule ("工具错误和权限不足属于INCONCLUSIVE,不能洗成PASS"). It also caught a real test-fidelity gap (T3/T7 not matching the Benchmark's literal wording) via careful static tracing. Result preserved at /private/tmp/.../scratchpad/snake-game-result for inspection. The Validator also reported no Bash tool available despite its frontmatter granting one -- investigating that as a follow-up, separate from this commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>