okstra 0.67.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/okstra +25 -0
- package/docs/kr/architecture.md +17 -1
- package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +456 -0
- package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +1408 -0
- package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +1572 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +1 -1
- package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +107 -0
- package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +105 -0
- package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +156 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +8 -7
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/gemini-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +2 -2
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/profiles/_common-contract.md +6 -6
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
- package/runtime/prompts/profiles/_implementation-executor.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
- package/runtime/prompts/profiles/final-verification.md +3 -2
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +12 -5
- package/runtime/prompts/wizard/prompts.ko.json +5 -5
- package/runtime/python/okstra_ctl/conformance.py +17 -0
- package/runtime/python/okstra_ctl/consumers.py +72 -5
- package/runtime/python/okstra_ctl/git_reconcile.py +322 -0
- package/runtime/python/okstra_ctl/handoff.py +348 -0
- package/runtime/python/okstra_ctl/render.py +44 -2
- package/runtime/python/okstra_ctl/run.py +175 -44
- package/runtime/python/okstra_ctl/wizard.py +89 -22
- package/runtime/python/okstra_ctl/worktree.py +28 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +40 -9
- package/runtime/python/okstra_token_usage/collect.py +27 -0
- package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
- package/runtime/skills/okstra-convergence/SKILL.md +3 -3
- package/runtime/skills/okstra-report-writer/SKILL.md +8 -8
- package/runtime/skills/okstra-run/SKILL.md +43 -3
- package/runtime/skills/okstra-team-contract/SKILL.md +7 -7
- package/runtime/validators/validate-run.py +51 -11
- package/src/_python-helper.mjs +52 -0
- package/src/error-log.mjs +19 -0
- package/src/git-reconcile.mjs +31 -0
- package/src/handoff.mjs +30 -0
- package/src/inject-report-index.mjs +22 -0
- package/src/render-final-report.mjs +22 -0
- package/src/render-views.mjs +9 -48
- package/src/spawn-followups.mjs +23 -0
- package/src/token-usage.mjs +3 -34
|
@@ -49,16 +49,16 @@ profile document.
|
|
|
49
49
|
- The question MUST be a clean yes/no — do NOT offer "close some / keep some" partial answers, do NOT propose alternatives like "close only codex panes". The whole-set decision keeps the wrap-up predictable.
|
|
50
50
|
- This step is mandatory for every phase (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`). It is silent-skipped when `<RUN_DIR>/state/lead-pane.id` is empty/absent (lead running outside tmux); the lead MUST NOT fabricate a synthetic pane list in that case.
|
|
51
51
|
- Run-end teammate teardown (shared — MUST run AFTER the pane disposition question and BEFORE `PROGRESS: complete`):
|
|
52
|
-
- The lead created the worker team in Phase 3 (`TeamCreate
|
|
53
|
-
- This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallback there is no team to delete, so silent-skip.
|
|
52
|
+
- The lead created the worker team in Phase 3 (`TeamCreate` with the name recorded as team-state `teamName` — `okstra-<task-key>`, implementation stage runs `okstra-<task-key>-s<N>`). Worker teammates are NOT reclaimed on their own — without an explicit teardown they linger in the FleetView roster across this and later runs in the session. A lingering team also makes the next run of the same task-key collide on `TeamCreate` ("team already exists"), forcing the stale-team recovery path.
|
|
53
|
+
- This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallback — whether `teamCreate.status` is `"error"` or `"skipped"` (`reason: "concurrent-run"`) — there is no team to delete, so silent-skip.
|
|
54
54
|
- After the pane disposition step above, the lead MUST ask the user whether to disband the worker teammates. This is a strict binary choice:
|
|
55
|
-
> worker teammate 팀
|
|
55
|
+
> worker teammate 팀 `<teamName>` 을 해제할까요?
|
|
56
56
|
> (예) 팀원 정리 / (아니오) 그대로 두기
|
|
57
|
-
- On `아니오` / `n` / `keep` → do not call `TeamDelete()`. Tell the user that the team will remain visible in FleetView/Teams and can be removed later from the Teams UI, or by asking the lead in this thread to clean up
|
|
57
|
+
- On `아니오` / `n` / `keep` → do not call `TeamDelete()`. Tell the user that the team will remain visible in FleetView/Teams and can be removed later from the Teams UI, or by asking the lead in this thread to clean up `<teamName>` — and that a later run of the same task-key/stage will hit a "team already exists" collision until it is removed.
|
|
58
58
|
- On `예` / `y` / `cleanup` → emit `PROGRESS: phase-7-teardown disbanding team`, then run the sequence below. Token-usage collection MUST already be complete — `TeamDelete` removes `~/.claude/teams/<team>/` + `~/.claude/tasks/<team>/` but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown.
|
|
59
|
-
1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "
|
|
59
|
+
1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "<teamName>"` (the team-state `teamName`) exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
|
|
60
60
|
2. Call `TeamDelete()` — the single synchronization point for teammate teardown. If it errors with an active-members message, send each named active member a structured `SendMessage(to: <name>, message: { type: "shutdown_request" })` — the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field — wait briefly, run `okstra-team-reconcile.sh` one more time, retry `TeamDelete()` once, and proceed regardless of the result. NEVER loop and never use `TaskStop` (teammates are not background tasks — `TaskStop` 404s on a member address).
|
|
61
|
-
- If `TeamDelete()` is unavailable in the current host, or teardown still fails after the single retry, do not pretend cleanup succeeded. Report the exact residual action instead: `Teams/FleetView 에서 team
|
|
61
|
+
- If `TeamDelete()` is unavailable in the current host, or teardown still fails after the single retry, do not pretend cleanup succeeded. Report the exact residual action instead: `Teams/FleetView 에서 team <teamName> 삭제`, and if tmux panes were kept earlier also show the pane command `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`.
|
|
62
62
|
- Report successful teardown in one short line (e.g. `worker teammate 팀 해제`, or `stale 멤버 1명 reconcile 후 팀 해제`) and proceed to the final `PROGRESS: complete ...` line.
|
|
63
63
|
- Brief handoff contract (shared — applies whenever the run consumes a task brief produced by `okstra-brief`):
|
|
64
64
|
- the brief is a **pre-discovery artifact**: it converts a domain-reporter's words (non-expert *or* developer) into expert-consumable form so this and later phases can run with zero fill-in questions to the operator. The brief is **not** authoritative on solution decisions; it is authoritative on the reporter's intent.
|
|
@@ -44,6 +44,7 @@ are collected and convergence finished. Phase 1-5 do not need it.
|
|
|
44
44
|
git diff <base>..HEAD | grep -E '^\+[^+].*\b(TBD|TODO|FIXME|XXX|implement later|handle edge cases|similar to|placeholder)\b' || echo 'clean'
|
|
45
45
|
```
|
|
46
46
|
Only newly-added lines (those starting with `+` and not part of the `+++` header) are inspected. If output is anything other than `clean`, the run MUST either remove the placeholders before finalising or record an explicit justification per occurrence in the final report.
|
|
47
|
+
7. **Stage-foreign literal scrub** — when the report-writer modelled this stage's `data.json` on another stage's report (a common shortcut for structural consistency), stage-specific literals get copied verbatim and silently misattribute this run. Confirm every branch name, commit SHA, stageKey, and stage number in the report resolves to **this** run's stage `<N>` — its worktree branch is `<prefix>-<task-id>-s<N>`, its stageKey `<task-id>-stage-<N>`. Sweep the report for any `-s<M>` / `stage-<M>` / `Stage <M>` where `M ≠ N` and for SHAs not in this run's `Commit list`; each hit is a copy-from-other-stage defect to correct before finalising.
|
|
47
48
|
|
|
48
49
|
## Lead post-stage persistence (BLOCKING — runs after the Executor emits `### Stage Carry Evidence`)
|
|
49
50
|
|
|
@@ -24,12 +24,13 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
24
24
|
- **Language-agnostic principles that ALWAYS bind (the TDD loop below MUST satisfy them):** (1) no self-mocking of the SUT — stub/spy only injected collaborators, never the subject's own methods; (2) behavioral assertions on outcomes (return value, state, persisted rows, events, boundary calls) — never `toHaveBeenCalled*` on an internal helper as the only/primary assertion; (3) truthful names — a `get*` / `find*` that writes/inserts, or a name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*`), is a defect; (4) single-purpose functions ≤50 effective lines, plain-English readability.
|
|
25
25
|
- **Graceful degradation (codex / gemini executor runtimes, or any runtime where the `~/.claude/skills/okstra-coding-preflight/` files are absent or unreadable):** do NOT skip the gate — apply the agnostic principles above plus the project's own `CLAUDE.md` / `CONTRIBUTING` / formatter+lint config, and record `coding-conventions: skill-unavailable → applied <project rules + agnostic principles>` in the final report. Never claim a skill read that did not happen.
|
|
26
26
|
- **CLI executor transcription (BLOCKING when the executor provider is `codex` or `gemini`):** the executor CLI process does NOT share the lead's context — a gate that stays in lead memory never reaches it. The lead MUST copy this entire "Coding-conventions preflight" bullet tree (file-read instructions, project review rule packs, agnostic principles, graceful degradation) verbatim into the dispatched executor prompt body. Enforcement: the CLI wrapper agents refuse an implementation-Executor dispatch whose persisted prompt lacks the literal heading `Coding-conventions preflight`, returning `<SENTINEL_PREFIX>_PREFLIGHT_MISSING` (see `agents/workers/_cli-wrapper-template.md` → Prompt Composition).
|
|
27
|
+
- **Non-interactive auto-execution (BLOCKING when the executor provider is `codex` or `gemini`).** A CLI executor runs head-less (`codex exec` / gemini equivalent) — there is no human at the keyboard. Skills loaded during the run (tdd, coding-preflight, and others) contain "get user approval", "state your plan to the user and wait", or "ask before proceeding" gates written for interactive sessions; in this run those gates are **already satisfied** by the upstream `implementation-planning` approval (the plan this stage executes was human-approved). The executor MUST NOT stop to request approval, MUST NOT end its turn after only producing a plan, and MUST carry the stage through end-to-end — RED → GREEN → refactor → per-step commits → `### Stage Carry Evidence`. The ONLY skill step to skip is the interactive user-approval prompt itself; every other skill rule (TDD discipline, conventions, real-IO isolation) still binds. The lead MUST transcribe this bullet verbatim into the dispatched CLI executor prompt (same reason as the preflight transcription rule above — the CLI process does not share lead context). Stopping early for approval in a head-less run is the observed empty-exit failure (exit 0, no diff): treat it as `contract-violated`.
|
|
27
28
|
- **Mandatory TDD loop**: BEFORE the first `Edit` or `Write` call, the executor MUST apply a red-green-refactor loop for every code change in this run. This is required; skipping it is a `contract-violated` outcome. This governs HOW each step is executed (failing test first → minimal implementation → refactor); it does not override the approved plan's WHAT/file scope.
|
|
28
29
|
- Order of operations per plan step: (1) write/extend the test that captures the step's acceptance criterion and confirm it fails for the right reason, (2) commit the failing test (`test(<scope>): ...`), (3) implement the minimum change to make it pass, (4) commit the implementation (`feat|fix(<scope>): ...`), (5) refactor without changing behaviour and commit separately if any cleanup is made (`refactor(<scope>): ...`). The failing-then-passing transition between steps (2) and (4) is the `TDD evidence` required by the final report.
|
|
29
30
|
- Doc-only / config-only / pure-rename steps that have no observable runtime behaviour are exempt from the failing-test requirement, but the executor MUST cite the exemption per step in the final report (`TDD exemption: <reason>`).
|
|
30
31
|
- When the touched area has no existing test harness, the executor MUST stand up the minimum harness needed to host one regression test for this run rather than skipping TDD entirely. Record the harness-bootstrap step as an `Out-of-plan edit` if it is not in the plan.
|
|
31
32
|
- **DB / IO / SQL changes require real execution — mock-only is NOT validation evidence:** when this run's diff touches DB/IO/SQL (ORM / query-builder code — sequelize / typeorm / prisma / knex / raw SQL — `*.repository.*`, model/entity files, `migrations/**`, `*.sql`, or any changed query string), a mocked unit test cannot observe the SQL the query builder actually emits — a mocked suite once passed while `count({ col: 'FontFamily.fontFamily' })` threw `Unknown column` on the real DB. The executor MUST run the change against a real (or faithful-replica) datastore — the `db-test` validation step (plan `validation` db step, else `project.json.qaCommands.db-test`), targeting a **local / replica** DB — and cite its exact command + exit code in the final report's `Validation evidence`. If no real DB / `db-test` command is reachable, do NOT claim the change verified: label the DB portion `정적 분석상 …, 미검증(실행 안 함)` in the report, surface it in the routing recommendation, and never downplay the real run as "too heavy". `git push` stays forbidden (universal list); the unverified DB state is carried forward so `final-verification` cannot accept it and `release-handoff` cannot push.
|
|
32
|
-
- **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa directory `<task_root>/qa/` (the `TASK_QA_PATH` token; same directory that holds the Tier 3 conformance manifest). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite.
|
|
33
|
+
- **Real-IO test isolation (BLOCKING).** A test that exercises a **real** datastore, HTTP endpoint, external service, message queue, or filesystem — a live DB connection / DSN, a real `fetch` / `axios` / `http` request, an actual S3 / queue client, anything the project's normal CI test suite cannot run because that backend is absent — MUST be written under the task's qa directory `<task_root>/qa/` (the `TASK_QA_PATH` token; same directory that holds the Tier 3 conformance manifest). It MUST NOT be written into the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*`, or anywhere the project's lint/test globs collect. Two reasons: (a) the project's CI / normal suite has no real DB or network, so a real-IO test placed in source silently breaks the pipeline; (b) it is an okstra verification artifact, and the artifact-home rule confines okstra outputs to `.okstra/`. **The dividing line is the IO, not the intent:** a unit test that stubs/spies only *injected collaborators* (mock — no real socket, no real DB handle) is a TDD red-green artifact and stays in source; the moment a test opens a real connection or makes a real network call it belongs in qa. A stage's real-IO requirement check is a Tier 3 conformance script under `<task_root>/qa/` (declared via the implementation-planning conformance entry) — never smuggle real IO into a `*.spec.*` in source to make it run "as a unit test". The `db-test` real-execution gate above is satisfied by the conformance/db-test path against the replica, NOT by adding a live-DB `*.spec.*` to the project suite. **These qa artifacts stay untracked — never commit them.** `.okstra/**` is gitignored (the artifact-home rule); conformance scripts and their results are *executed* and recorded in the carry sidecar / verifier result, never written into git history. A committed `.okstra/qa` file is a stage-branch defect that leaks okstra internals into the eventual PR (see the `git add` rules below).
|
|
33
34
|
- re-read the approved plan end-to-end and parse the `## 5.5 Stage Map`. Read the **Stage** injected in the launch prompt (`Stage for this implementation run`): the single stage number this run owns. The runtime already selected and reserved this stage (one run = one stage) — do NOT recompute the start stage from `consumers.jsonl`.
|
|
34
35
|
- load every `runs/<plan-key>/carry/stage-<i>.json` for `i ∈ depends-on(this stage)` and inject them into the executor's working context as "runtime carry-in". For a `depends-on (none)` stage, no sidecar load — task-brief only.
|
|
35
36
|
- this stage's `depends-on` are all already `status:done`. Its file list, step order, Stage Validation commands, Stage Exit Contract, and rollback path are the authoritative scope.
|
|
@@ -58,6 +59,7 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
|
|
|
58
59
|
- read-only inspection commands: `git status`, `git diff`, `git log`, `grep`, `rg`, `find`, `cat`, `ls`, file Read tools
|
|
59
60
|
- build, lint, type-check, and test commands (`npm test`, `pytest`, `go build`, `cargo test`, `bash -n`, etc.)
|
|
60
61
|
- **local git operations only**: `git add`, `git commit`. Prefer small commits keyed to plan steps.
|
|
62
|
+
- **No okstra artifacts in commits (BLOCKING).** Never use `git add -f`. Before every `git commit`, run `git diff --cached --name-only` and confirm it contains zero `.okstra/` paths (and zero `.project-docs/` paths when the legacy symlink is present). `.okstra/**` is gitignored; force-staging it onto the stage branch is the one way these verification artifacts reach the upstream PR. Conformance/qa evidence belongs in the carry sidecar and verifier result — committing it is never correct, even when a step's instructions seem to ask for it.
|
|
61
63
|
- **Commit message format (mandatory)**: every commit message MUST follow Conventional Commits — `<type>(<scope>): <subject>` for the first line, optional body separated by a blank line, optional footer. Constraints:
|
|
62
64
|
- `<type>` MUST be one of: `feat` / `fix` / `perf` / `revert` / `deps` / `docs` / `refactor` / `build` / `ci` / `chore` / `test`. When the repo is `release-please`-managed, this aligns the commit with a configured changelog section.
|
|
63
65
|
- `<scope>` SHOULD be the plan step identifier or the primary module touched (e.g. `feat(report-writer): ...`). Omit the parentheses only when no meaningful scope applies.
|
|
@@ -96,6 +96,7 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
|
|
|
96
96
|
- **Tautological delegation assertion:** a test asserts the SUT result equals a direct call to the same pure helper/collaborator that the SUT delegates to, instead of asserting an independent literal value or observable state.
|
|
97
97
|
- **Untruthful name:** a read-named function (`get*` / `find*` / `load*`) that writes/inserts/mutates; an adapter or repository name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*` / `findActive*`).
|
|
98
98
|
- **Hexagonal (only when the overlay is loaded):** business logic inside a port body; an adapter method that is not pure I/O (post-fetch JS filtering on domain state, domain-rule evaluation); a domain object declared outside the `domain/` boundary.
|
|
99
|
+
- **okstra artifact committed to the branch:** any path in the `git diff --name-only <base>...HEAD` enumeration that lives under `.okstra/` (or `.project-docs/` when the legacy symlink is present). `.okstra/**` is gitignored, so a committed okstra file means the executor force-staged it (`git add -f`) — leaking verification artifacts (qa scripts, conformance results) into the eventual PR. Cite the path; recommend `git rm --cached <path>` to untrack it while keeping the file on disk. Conformance/qa evidence belongs in the carry sidecar / verifier result, never in git history.
|
|
99
100
|
- **Real-IO test in source tree:** a changed/added test under the project source test tree — `src/**`, `test/**`, `tests/**`, `**/__test__/**`, `**/__tests__/**`, `*.spec.*`, `*.test.*` — that opens a **real** DB connection / DSN, makes a real `fetch` / `axios` / `http` request, or otherwise hits real external IO without mocking the injected collaborator (a live handle, not a stub/spy). Real-IO tests MUST live under `<task_root>/qa/` per the executor's *Real-IO test isolation* rule — a live-IO test in source silently breaks the project's CI suite and violates the artifact-home rule. Cite the test file + the real-IO line; recommend moving it to `<task_root>/qa/` (or declaring it as a Tier 3 conformance script). Mock-only unit tests in source are NOT a hit.
|
|
100
101
|
- **Advisory findings (recorded as recommendations; verdict MAY still PASS):** function >50 effective lines, a single body mixing read+write stages, weak readability, a missing-but-non-critical outcome assertion, newly orphaned private/public code that is safe to remove but not on a critical path, or weak-but-not-misleading names. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
|
|
101
102
|
- **Output.** Every finding — blocking or advisory — is a structured item in the verifier's worker result (`path:line`, rule, severity, suggested fix) so it carries into Phase 5.5 convergence and the final report. A blocking hit sets the verifier verdict to `FAIL` with the rule cited, using the same verdict machinery as the Discrepancy rule above. `Claude lead` MUST NOT silently downgrade a cited blocking finding to advisory during synthesis; an override requires a concrete cited reason, exactly as for the Discrepancy rule.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- Pre-verification entry gate (resolved & enforced by `okstra render-bundle` prep — the lead does NOT recompute it):
|
|
27
27
|
- the verification target (scope / worktree / base / head / stages / source reports / diff stat) is injected as the `VERIFICATION_TARGET` block. The lead MUST treat it as authoritative and MUST NOT re-pick a target from the brief.
|
|
28
28
|
- **whole-task scope** (`--stage auto`, default): prep has already verified every Stage Map stage is `status:done` in `consumers.jsonl`, every done stage's `head_commit` is an ancestor of the task worktree HEAD (all stage branches merged), and the worktree is clean outside `.okstra/`. If any check failed the run never started (PrepareError); a started whole-task run is therefore a fully-merged, clean target.
|
|
29
|
-
- **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification
|
|
29
|
+
- **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification: it MUST NOT recommend plain `release-handoff`, but MAY recommend `release-handoff(stage-group)` when the verdict is `accepted` — the stage becomes PR-eligible for a stage-group handoff.
|
|
30
30
|
- the lead still captures `git rev-parse HEAD` / `git status --short` from the injected worktree to confirm the analysis ran against the injected head; a mismatch is a `tool-failure`, not a silent proceed.
|
|
31
31
|
- Required deliverable shape (final report, in addition to the standard sections):
|
|
32
32
|
- **Source Implementation Report(s)**: the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base/head SHAs, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). The lead injects this same snapshot into every analyser prompt (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification head SHA:** / **Verification diff stat:**`); a worker that cannot confirm its analysis ran against that exact head MUST record a `tool-failure`.
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
- **Read-only command log**: any pre-existing test/validation command executed during this run MUST be listed with its exact command line and exit code. No mutating commands may appear here.
|
|
38
38
|
- **Two-tier command lookup (shared with `implementation`):** when this phase performs its own independent re-validation, the command source is exactly the same two tiers `implementation` verifiers use — Tier 1 is the originating task brief / approved plan's `validation` set, Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` under `qaCommands`. Auto-detecting tools from manifest files is forbidden; missing tiers are recorded as `qa-command not configured: <category>` and do NOT trigger a guess. The `cmd` deny-list (`--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, `npm install` without `ci`, etc.) is enforced identically. NOTE: runtime fail-fast validation (`okstra_ctl.qa_commands.validate_qa_commands`) only fires at `--task-type implementation` run-prep, so this phase MUST self-check each `qaCommands` entry against the deny-list before executing it — if a denied token is present, skip the command and record it as a `Read-only command log` line `qa-command rejected (denied token: <token>): <label>`.
|
|
39
39
|
- **Tier 3 — stage conformance scripts (whole-task union):** because this phase verifies the **integrated, merged** state, it re-runs conformance against that state rather than per-stage. Read the task-level manifest `<task_root>/qa/conformance-manifest.json` (the directory is the `TASK_QA_PATH` token) and, in **whole-task scope**, run the `runCommand` of **every** `entries[]` item against the merged worktree, refreshing each `<task_root>/qa/result-<stageKey>.json` (`{ "stageKey", "overall": "PASS"|"FAIL"|"MISSING", "ranAt", "requirements" }`). In **single-stage scope**, run only the entry whose `stageKey` matches the verified stage. An entry carrying an `exemption` or user `waiver` is NOT executed — record the skip and reason; a `waiver` becomes a `conditional-accept` condition surfaced in the section 7 Verdict (conformance left unverified by user acknowledgement). Each `runCommand` runs in the worktree cwd with `qaEnv` env (replica DB DSN / app base URL / env file) — **replica / test environment only**, never shared / staging / prod, and the same source/lockfile mutation deny-list applies (a conformance script MAY mutate only its `qaEnv` replica datastore). Interpret each result from the exit code + stdout `QA-RESULT: PASS|FAIL` (last wins) and `REQ <id>: PASS|FAIL: <reason>` lines; no `QA-RESULT` marker → `MISSING`. Any entry whose result is not `PASS` (including `MISSING` or a never-run/missing sidecar) is an **Acceptance Blocker** (`major`+) — exactly like the DB real-execution gate above, since `accepted` requires zero blockers the verdict becomes `conditional-accept` / `blocked`. This is the same gate the `validate-run.py` Tier 3 check enforces on the result sidecars.
|
|
40
|
-
- **Routing recommendation**: the next safe phase — one of `release-handoff`, `done`, `error-analysis`, `implementation-planning` — tied to the verdict and blocker list. `release-handoff` is allowed ONLY when the Verdict Token is `accepted`. `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` run
|
|
40
|
+
- **Routing recommendation**: the next safe phase — one of `release-handoff`, `done`, `error-analysis`, `implementation-planning` — tied to the verdict and blocker list. `release-handoff` is allowed ONLY when the Verdict Token is `accepted`. `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` accepted run routes to `release-handoff(stage-group)` (or `implementation` / `done`); plain `release-handoff` remains whole-task-only. Enforcement: `validators/validate-run.py` rejects a `single-stage` report whose routing cites plain `release-handoff`.
|
|
41
|
+
- **Verified-row recording** (single-stage scope only): when the Verdict Token is `accepted`, the lead MUST run `okstra handoff record-verified --plan-run-root <plan-run-root> --stage <N> --report-path <final-report.md path> --data-json <final-report data.json path>` and quote the command + exit code in the report. The helper re-validates taskType/scope/verdict from data.json, so a non-accepted or whole-task report is rejected at the tool layer.
|
|
41
42
|
- Clarification request policy (phase-specific addendum — shared policy is in `_common-contract.md`):
|
|
42
43
|
- populate `## 1. Clarification Items` only when a blocker hinges on information only the user can supply (deployment intent, intended target environment, business-rule interpretation); use `Blocks=next-phase` for items that gate continuing to release-handoff
|
|
43
44
|
- Self-review pass before finalising the report (`Claude lead` runs this; do not delegate to a generic subagent):
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
- `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
|
|
34
34
|
- `## 7. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}; Direction `routing`; Next Step "사용자에게 후보 K개 선택 의뢰 (## 5.9 표 참조)"
|
|
35
35
|
- `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
|
|
36
|
-
- this report is authored free-form (improvement-discovery is not in the data.json schema enum); after the markdown is written, the report-writer runs `
|
|
36
|
+
- this report is authored free-form (improvement-discovery is not in the data.json schema enum); after the markdown is written, the report-writer runs `okstra inject-report-index <report.md> --report-language <en|ko>` to add the top-of-report Index + `I-NNN`/`C-NNN` scroll anchors. The run validator fails the report when the Index anchor is missing.
|
|
37
37
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
38
38
|
- if scan-scope or priority-lenses cannot be made concrete during Phase 1.5, end the run with Verdict Token `blocked`, populate `## 1. Clarification Items` with `Blocks=next-phase` rows, and do not run worker dispatch
|
|
39
39
|
- every clarification row carries a recommended answer + one-line rationale inside the `Expected form` cell
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release Handoff Profile
|
|
2
2
|
|
|
3
|
-
- Purpose: take an `accepted` final-verification verdict for an already-committed implementation branch and turn it into a delivered push and/or pull request, with explicit user selection at every mutating step
|
|
3
|
+
- Purpose: take an `accepted` final-verification verdict for an already-committed implementation branch and turn it into a delivered push and/or pull request, with explicit user selection at every mutating step. Two modes: **whole-task** (default — the verified task branch becomes one PR) and **stage-group** (a user-selected subset of verified stages is merged into a collector branch and becomes one PR).
|
|
4
4
|
- **Execution model: single-lead, no worker dispatch.** This phase is a thin orchestrator over `git` / `gh`; it does NOT run team-mode, does NOT call `TeamCreate`, does NOT dispatch analysis or drafter sub-agents, and does NOT run convergence. The Claude lead performs every step inline (drafting PR text, asking the user, running git / gh, writing the final report) — see "Lead-only contract" below.
|
|
5
5
|
- Worker roster: none — this profile intentionally has no `- Required workers:` block; the run is executed entirely by the Claude lead.
|
|
6
6
|
- Lead-only contract (replaces the shared team contract for this phase):
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
- The shared "authority & permissions assumption" rule from the common contract still applies: assume the user holds every permission needed; do not block on hypothetical approvals.
|
|
12
12
|
- The shared "MCP read-only" rule still applies if the brief lists MCP servers, though most release-handoff runs do not use MCP.
|
|
13
13
|
- Pre-handoff entry gate (mandatory — refuse to start if any item fails):
|
|
14
|
-
- the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report
|
|
14
|
+
- **whole-task mode**: the task brief MUST cite the originating `final-verification` final-report path under `## Source Verification Report`; the lead confirms its `Verdict Token` is exactly `accepted` and its `verificationScope` is `whole-task`.
|
|
15
|
+
- **stage-group mode**: the brief cites N single-stage `final-verification` reports (one per candidate stage); the lead confirms each `Verdict Token` is `accepted`. Eligibility is re-enforced by `okstra handoff` — the lead never hand-computes it.
|
|
15
16
|
- if the verdict is `conditional-accept`, `blocked`, or any other token (including ambiguous phrasing like "looks good"), the run MUST end immediately with status `blocked` and a routing recommendation back to `error-analysis` or `implementation-planning`. Do NOT prompt the user; Do NOT run any git command.
|
|
16
17
|
- the lead MUST capture `git status --short` and confirm the working tree is clean. Dirty state aborts the run; release-handoff packages the commits produced by `implementation`, it does not stage or commit changes.
|
|
17
18
|
- the lead MUST capture `git rev-parse --abbrev-ref HEAD` and record it as the **feature branch**. If the current branch is itself `main`, `master`, `prod`, `preprod`, `staging`, or `dev`, the run MUST end immediately — release-handoff never operates on a base branch.
|
|
@@ -22,6 +23,9 @@
|
|
|
22
23
|
- `push + PR` — push the feature branch, then open or reuse a pull request.
|
|
23
24
|
- `skip` — record the verified state and end the run without any git command.
|
|
24
25
|
If the user picks `skip`, route directly to the final-report self-review pass.
|
|
26
|
+
- **stage-group mode order**: G1 base branch first (same options as Q2 — the dependency-closure check needs `origin/<base>`), then G2 stage selection, then assemble, then Q2b/Q3 as usual with the collector branch as the PR head.
|
|
27
|
+
1g. **G2 — stage selection**: run `okstra handoff eligible --plan-run-root <plan-run-root> --approved-plan <approved plan path>` and present the returned stages (eligible ones selectable, blocked ones listed with their `reasons`) as a multi-select. At least one stage must be selected.
|
|
28
|
+
2g. **assemble**: run `okstra handoff assemble --plan-run-root <...> --approved-plan <...> --project-root <project root> --project-id <id> --task-group <g> --task-id <t> --work-category <c> --stages <csv> --base <chosen-base>`. Exit 2 means a stage-vs-stage merge conflict: show the `conflicts` paths and stop (route: reshape the group or resolve manually). Exit 1 means an eligibility/closure violation: show the error verbatim and re-ask G2. On success the returned `branch` is the PR head branch for every subsequent step.
|
|
25
29
|
2. **PR base branch** (only when the user picked `push + PR`) — present four options and capture exactly one:
|
|
26
30
|
- `staging`
|
|
27
31
|
- `preprod`
|
|
@@ -42,7 +46,7 @@
|
|
|
42
46
|
- `edit then proceed` — accept inline edits from the user, then proceed with the edited text.
|
|
43
47
|
- `cancel` — end the run without executing push or PR commands; record the cancellation in the final report.
|
|
44
48
|
- Inline drafting rules (Claude lead):
|
|
45
|
-
- read the run brief, the cited final-verification report, `git log --oneline <base>..HEAD`, and `git diff <base>..HEAD --stat` to ground the drafted text in actual committed changes.
|
|
49
|
+
- read the run brief, the cited final-verification report, `git log --oneline <base>..HEAD`, and `git diff <base>..HEAD --stat` to ground the drafted text in actual committed changes. In stage-group mode the draft is grounded on `git log <implementation_base_commit>..<collector HEAD>` / `git diff <implementation_base_commit>..<collector HEAD> --stat`, with source material = each selected stage's implementation report + its single-stage verification report.
|
|
46
50
|
- **PR body template** — the run context exposes `PR_TEMPLATE_PATH` and `PR_TEMPLATE_SOURCE`. The path MUST be an okstra-owned project artifact under `<PROJECT_ROOT>/.okstra/**` or a file already materialised into this run's artifact directory by the prepare step. The lead MUST `Read` this file verbatim, strip HTML comments, then fill in the placeholders. Do NOT hard-code a section list — the template is the source of truth for the structure. If the resolved file is missing or outside the okstra resource boundary at draft time, abort the run with a clear error rather than inventing a structure.
|
|
47
51
|
- produce **two artifacts** before showing them to the user:
|
|
48
52
|
1. **PR title** — by default the subject of the most recent implementation commit, or a concise Conventional Commits-style summary of the committed range.
|
|
@@ -50,11 +54,13 @@
|
|
|
50
54
|
- Allowed actions during the run (Claude lead only):
|
|
51
55
|
- read-only inspection: `git status`, `git status --short`, `git diff`, `git log`, `git rev-parse`, `git ls-remote --heads origin <name>`, `gh pr list --head <branch>`, `gh pr view <url>`.
|
|
52
56
|
- merge-conflict probe (only when the user picked `push + PR`): `git fetch origin <chosen-base>` and `git merge-tree --write-tree --merge-base origin/<chosen-base> HEAD origin/<chosen-base>`. Both are non-mutating with respect to the working tree.
|
|
53
|
-
- feature-branch push (only when the user picked `push + PR`): `git push -u origin <current-branch>`. The pushed ref MUST be the feature branch — never the chosen base branch.
|
|
57
|
+
- feature-branch push (only when the user picked `push + PR`): `git push -u origin <current-branch>`. The pushed ref MUST be the feature branch — never the chosen base branch. (stage-group mode: the collector branch returned by assemble)
|
|
54
58
|
- PR creation (only when the user picked `push + PR` AND no PR with the same head already exists on origin): `gh pr create --base <chosen-base> --head <current-branch> --title "<title>" --body "<body>"`. The title and body are the user-confirmed PR draft.
|
|
55
59
|
- PR reuse: if `gh pr list --head <branch> --state open --json url --jq '.[0].url'` returns a URL, treat that PR as already existing — record the URL in the final report and SKIP `gh pr create`.
|
|
60
|
+
- after `gh pr create` succeeds (or an existing PR is reused), the lead MUST run `okstra handoff record-pr --plan-run-root <...> --stages <csv> --branch <head branch> --url <pr url>` and quote the command + exit code in the final report. This applies to BOTH modes — whole-task runs record `--stages` as the full Stage Map list — so duplicate-PR prevention converges on one consumers record.
|
|
61
|
+
- stage-group helpers: `okstra handoff eligible`, `okstra handoff assemble`, `okstra handoff record-pr`. The assemble step is the ONLY path that may create commits (merge commits on the collector branch) in this phase.
|
|
56
62
|
- Forbidden actions (any occurrence → terminal status `contract-violated`):
|
|
57
|
-
- local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`).
|
|
63
|
+
- local commit commands of any kind (`git add`, `git commit`, `git restore --staged`, `git stash`), and any direct `git merge` / `git rebase` run by the lead. The single exception is the merge commits `okstra handoff assemble` itself creates on the collector branch — the lead never merges by hand.
|
|
58
64
|
- any of the following git push variants, regardless of intent or whether the user said "force it":
|
|
59
65
|
- `git push --force`
|
|
60
66
|
- `git push --force-with-lease`
|
|
@@ -88,6 +94,7 @@
|
|
|
88
94
|
- `- PR created: <url>` with title and base branch
|
|
89
95
|
- `- PR reused: <url>` when an existing PR was found via `gh pr list`
|
|
90
96
|
- `- PR creation skipped: <reason>` for any user-driven cancellation
|
|
97
|
+
- **Stage Group** (stage-group mode only): selected stages, each stage's single-stage verification report path + quoted `Verdict Token` row, collector branch name, merge commit SHAs from assemble, and the dependency-closure verdict (from the assemble output / error).
|
|
91
98
|
- **Routing recommendation**: explicit `done` token, since release-handoff is the terminal lifecycle phase. If the run ended in `skip` or `cancel`, the recommendation MUST also state whether re-entry into release-handoff is appropriate.
|
|
92
99
|
- Self-review pass before finalising the report (`Claude lead` runs this):
|
|
93
100
|
1. **Entry-gate audit** — section 2 cites the originating final-verification report path and the literal `Verdict Token` row with value `accepted`. If either is missing, the run is invalid and MUST be re-routed to `final-verification`.
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"in_worktree": "현재 worktree(`{path}`)에서 그대로 진행합니다(이미 non-main worktree) — 진행할까요?",
|
|
140
140
|
"not_git": "git 저장소가 아니므로 `{path}` 에서 직접 진행합니다 — 진행할까요?"
|
|
141
141
|
},
|
|
142
|
-
"options": { "proceed": "진행", "edit": "base-ref 다시 고르기" },
|
|
142
|
+
"options": { "proceed": "진행", "edit": "base-ref 다시 고르기", "abort": "중단" },
|
|
143
143
|
"echo_template": "branch-confirm: {value}"
|
|
144
144
|
},
|
|
145
145
|
"base_ref_text": {
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"echo_template": "approved-plan: {value}"
|
|
169
169
|
},
|
|
170
170
|
"approve_plan_confirm": {
|
|
171
|
-
"label": "이 플랜으로
|
|
171
|
+
"label": "이 플랜으로 진행할까요?\n {path}\n· 예 — 진행합니다. 플랜이 아직 승인 전이면 지금 data.json(정본) + 리포트를 함께 approved 로 처리한 뒤 진행합니다. (markdown 만 손으로 고치면 일관성 검증에서 거부되므로 이 경로로 승인하세요.)\n· 아니오 — 진행하지 않습니다.",
|
|
172
172
|
"echo_template": "approve-plan: {value}",
|
|
173
173
|
"options": {
|
|
174
174
|
"yes": "예 — 승인하고 진행",
|
|
@@ -179,16 +179,16 @@
|
|
|
179
179
|
"approved": "approved-plan: {path} (승인·진행 확인됨)"
|
|
180
180
|
},
|
|
181
181
|
"errors": {
|
|
182
|
-
"declined": "진행을 선택하지 않으면
|
|
182
|
+
"declined": "진행을 선택하지 않으면 다음 단계로 넘어갈 수 없습니다. 진행(예)하거나 위저드를 종료하세요.",
|
|
183
183
|
"still_unapproved": "approve-plan: 승인 처리 후에도 승인 상태가 아닙니다 (data.json/markdown 불일치): {path}"
|
|
184
184
|
}
|
|
185
185
|
},
|
|
186
186
|
"stage_pick": {
|
|
187
187
|
"label": "stage 범위를 선택하세요. auto 는 전체 task(모든 stage)를, 특정 번호는 해당 stage 만 대상으로 합니다.",
|
|
188
|
+
"label_final_verification": "검증할 implementation stage 를 선택하세요.",
|
|
188
189
|
"echo_template": "stage: {value}",
|
|
189
190
|
"options": {
|
|
190
|
-
"auto": "auto (다음 미완료 stage)"
|
|
191
|
-
"auto_final_verification": "auto (전체 task — 모든 stage 머지 후 한 번)"
|
|
191
|
+
"auto": "auto (다음 미완료 stage)"
|
|
192
192
|
}
|
|
193
193
|
},
|
|
194
194
|
"directive_pick": {
|
|
@@ -258,6 +258,23 @@ def apply_qa_waiver(manifest: object, stage_key: str, reason: str, *, at: str,
|
|
|
258
258
|
return False
|
|
259
259
|
|
|
260
260
|
|
|
261
|
+
def clear_qa_waiver(manifest: object, stage_key: str) -> bool:
|
|
262
|
+
"""stage_key entry 의 `waiver` 를 제거한다(in place). 제거했으면 True.
|
|
263
|
+
|
|
264
|
+
한 stage 의 새 run 이 시작될 때, 그 stage entry 에 남아 있던 이전 run 의
|
|
265
|
+
waiver(예: all-gate run 이 미래 stage 를 미리 waive 한 것)는 stale 다 —
|
|
266
|
+
그대로 두면 verifier 가 conformance 를 skip 해 마스킹된다. 이 run 이 실제로
|
|
267
|
+
검증하도록 제거한다. 사용자가 이번 run 에 같은 stage 를 명시 waive 한
|
|
268
|
+
경우(--qa-waiver)는 호출 측에서 걸러 보존한다."""
|
|
269
|
+
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
270
|
+
if not isinstance(entries, list):
|
|
271
|
+
return False
|
|
272
|
+
for entry in entries:
|
|
273
|
+
if isinstance(entry, dict) and entry.get("stageKey") == stage_key:
|
|
274
|
+
return entry.pop("waiver", None) is not None
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
|
|
261
278
|
def manifest_required_surfaces(manifest: object) -> set[str]:
|
|
262
279
|
"""매니페스트 전 entry 의 `requires` 합집합 — 선언된 surface 집합."""
|
|
263
280
|
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A row's identity for idempotency is the tuple
|
|
4
4
|
(impl_task_key, stage, status)
|
|
5
|
-
so the same (started / done) record is never duplicated.
|
|
5
|
+
so the same (started / done) record is never duplicated.
|
|
6
|
+
force_reappend=True 인 보정 append 만 같은 tuple 을 다른 head_commit 으로 재기록할 수 있다."""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
@@ -33,8 +34,19 @@ def read_consumers(plan_run_root: Path) -> List[Dict[str, Any]]:
|
|
|
33
34
|
return out
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
def latest_done_by_stage(rows: List[Dict[str, Any]]) -> Dict[int, Dict[str, Any]]:
|
|
38
|
+
"""stage → 마지막 done row. 보정(reconciled) row 가 같은 stage 에
|
|
39
|
+
재-append 되므로 done 읽기의 유일한 의미는 last-wins 다."""
|
|
40
|
+
out: Dict[int, Dict[str, Any]] = {}
|
|
41
|
+
for r in rows:
|
|
42
|
+
if r.get("status") == "done" and isinstance(r.get("stage"), int):
|
|
43
|
+
out[r["stage"]] = r
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
36
47
|
def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
37
|
-
status: str,
|
|
48
|
+
status: str, force_reappend: bool = False,
|
|
49
|
+
**fields: Any) -> None:
|
|
38
50
|
if status not in ("started", "done"):
|
|
39
51
|
raise ValueError(f"status must be 'started' or 'done', got: {status!r}")
|
|
40
52
|
with consumers_mutex(plan_run_root):
|
|
@@ -43,15 +55,70 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
|
43
55
|
if (row.get("impl_task_key") == impl_task_key
|
|
44
56
|
and row.get("stage") == stage
|
|
45
57
|
and row.get("status") == status):
|
|
46
|
-
|
|
58
|
+
if not force_reappend:
|
|
59
|
+
return # idempotent
|
|
60
|
+
if row.get("head_commit") == fields.get("head_commit"):
|
|
61
|
+
return # 동일 보정의 중복 재-append 방지
|
|
47
62
|
record: Dict[str, Any] = {
|
|
48
63
|
"impl_task_key": impl_task_key,
|
|
49
64
|
"stage": stage,
|
|
50
65
|
"status": status,
|
|
51
66
|
**fields,
|
|
52
67
|
}
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
_append_row(plan_run_root, record)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _append_row(plan_run_root: Path, record: Dict[str, Any]) -> None:
|
|
72
|
+
with _path(plan_run_root).open("a", encoding="utf-8") as f:
|
|
73
|
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def append_verified(plan_run_root: Path, *, impl_task_key: str, stage: int,
|
|
77
|
+
verdict: str, report_path: str) -> None:
|
|
78
|
+
"""단독-stage final-verification 결과 기록. 같은 report_path 재기록은 멱등,
|
|
79
|
+
다른 report_path 는 재검증으로 append 한다 (읽기는 last-wins)."""
|
|
80
|
+
with consumers_mutex(plan_run_root):
|
|
81
|
+
for row in read_consumers(plan_run_root):
|
|
82
|
+
if (row.get("impl_task_key") == impl_task_key
|
|
83
|
+
and row.get("stage") == stage
|
|
84
|
+
and row.get("status") == "verified"
|
|
85
|
+
and row.get("report_path") == report_path):
|
|
86
|
+
return
|
|
87
|
+
_append_row(plan_run_root, {
|
|
88
|
+
"impl_task_key": impl_task_key, "stage": stage,
|
|
89
|
+
"status": "verified", "verdict": verdict,
|
|
90
|
+
"report_path": report_path,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def append_pr(plan_run_root: Path, *, impl_task_key: str, stages: List[int],
|
|
95
|
+
branch: str, url: str) -> None:
|
|
96
|
+
"""handoff 의 PR 생성/재사용 기록. 같은 branch 의 pr 행이 이미 있으면 멱등."""
|
|
97
|
+
with consumers_mutex(plan_run_root):
|
|
98
|
+
for row in read_consumers(plan_run_root):
|
|
99
|
+
if row.get("status") == "pr" and row.get("branch") == branch:
|
|
100
|
+
return
|
|
101
|
+
_append_row(plan_run_root, {
|
|
102
|
+
"impl_task_key": impl_task_key, "stages": sorted(stages),
|
|
103
|
+
"status": "pr", "branch": branch, "url": url,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def verified_accepted_stages(rows: List[Dict[str, Any]]) -> set:
|
|
108
|
+
"""stage → 마지막 verified 행의 verdict 가 accepted 인 stage 집합 (last-wins)."""
|
|
109
|
+
last: Dict[int, str] = {}
|
|
110
|
+
for r in rows:
|
|
111
|
+
if r.get("status") == "verified" and isinstance(r.get("stage"), int):
|
|
112
|
+
last[r["stage"]] = (r.get("verdict") or "").strip().lower()
|
|
113
|
+
return {n for n, v in last.items() if v == "accepted"}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def pr_covered_stages(rows: List[Dict[str, Any]]) -> set:
|
|
117
|
+
out: set = set()
|
|
118
|
+
for r in rows:
|
|
119
|
+
if r.get("status") == "pr":
|
|
120
|
+
out.update(n for n in (r.get("stages") or []) if isinstance(n, int))
|
|
121
|
+
return out
|
|
55
122
|
|
|
56
123
|
|
|
57
124
|
# --- carry-as-SSOT done recovery ---------------------------------------------
|