okstra 0.137.0 → 0.139.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.
Files changed (46) hide show
  1. package/README.md +1 -0
  2. package/docs/architecture.md +7 -6
  3. package/docs/cli.md +4 -3
  4. package/docs/contributor-change-matrix.md +1 -1
  5. package/docs/for-ai/README.md +2 -0
  6. package/docs/for-ai/skills/okstra-code-review.md +57 -0
  7. package/docs/project-structure-overview.md +29 -2
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/workers/antigravity-worker.md +1 -1
  11. package/runtime/agents/workers/codex-worker.md +1 -1
  12. package/runtime/prompts/coding-preflight/architectures/hexagonal.md +34 -0
  13. package/runtime/prompts/coding-preflight/clean-code.md +54 -0
  14. package/runtime/prompts/coding-preflight/languages/javascript-typescript.md +11 -0
  15. package/runtime/prompts/lead/adapters/claude-code.md +1 -0
  16. package/runtime/prompts/lead/adapters/codex.md +1 -0
  17. package/runtime/prompts/lead/adapters/external.md +1 -0
  18. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  19. package/runtime/prompts/lead/report-writer.md +11 -10
  20. package/runtime/prompts/lead/team-contract.md +4 -2
  21. package/runtime/prompts/profiles/_implementation-diff-review.md +9 -4
  22. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  23. package/runtime/prompts/profiles/_implementation-self-check.md +3 -2
  24. package/runtime/prompts/profiles/_implementation-verifier.md +12 -3
  25. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  26. package/runtime/prompts/profiles/release-handoff.md +2 -1
  27. package/runtime/python/okstra_ctl/code_review_paths.py +64 -0
  28. package/runtime/python/okstra_ctl/code_review_target.py +152 -0
  29. package/runtime/python/okstra_ctl/codex_dispatch.py +158 -365
  30. package/runtime/python/okstra_ctl/dispatch_core.py +118 -138
  31. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +1053 -0
  32. package/runtime/python/okstra_ctl/render.py +9 -4
  33. package/runtime/python/okstra_ctl/run.py +40 -89
  34. package/runtime/python/okstra_ctl/stage_targets.py +232 -46
  35. package/runtime/python/okstra_ctl/wizard.py +2 -2
  36. package/runtime/python/okstra_ctl/worker_prompt_contract.py +73 -0
  37. package/runtime/python/okstra_ctl/worktree.py +40 -31
  38. package/runtime/python/okstra_project/__init__.py +2 -0
  39. package/runtime/python/okstra_project/state.py +142 -0
  40. package/runtime/skills/okstra-code-review/SKILL.md +180 -0
  41. package/runtime/skills/okstra-code-review/references/census-rules.md +100 -0
  42. package/runtime/skills/okstra-code-review/references/review-calibration.md +86 -0
  43. package/runtime/validators/lib/fixtures.sh +2 -0
  44. package/src/cli-registry.mjs +7 -0
  45. package/src/commands/inspect/code-review.mjs +35 -0
  46. package/src/lib/skill-catalog.mjs +1 -0
@@ -28,7 +28,7 @@ Two `frontmatter` approval fields are always emitted with their unset default
28
28
  4. Call `dispatch_worker(report_writer_assignment, prompt)` through the selected adapter.
29
29
  5. Call `await_workers([handle])` and verify both the data.json Result Path and worker-results audit path.
30
30
 
31
- The assignment's `modelExecutionValue` feeds both adapter dispatch and the prompt header in item 7 below, so the execution model and recorded `**Model:**` header always agree. Missing or unsupported model resolution is a pre-dispatch contract failure; the common contract does not choose a runtime fallback.
31
+ The assignment's `modelExecutionValue` feeds both adapter dispatch and the prompt header in item 9 below, so the execution model and recorded `**Model:**` header always agree. Missing or unsupported model resolution is a pre-dispatch contract failure; the common contract does not choose a runtime fallback.
32
32
 
33
33
  The prompt MUST include, in this order at the top:
34
34
 
@@ -43,19 +43,20 @@ The prompt MUST include, in this order at the top:
43
43
  - `**Worker Error Contract Path:** <absolute-path>` — selects `templates/worker-error-contract.md`.
44
44
  - `**Errors log path:** <absolute-path>` — run-level errors JSONL (`logs/errors-<task-type>-<seq>.jsonl`).
45
45
  - `**Errors sidecar path:** <absolute-path>` — this worker's per-run sidecar JSON (`worker-results/report-writer-worker-errors-<task-type>-<seq>.json`).
46
- 8. `**Model:** Report writer worker, <modelExecutionValue>` (resolved per Phase 5.5 anchor-header rules)
47
- 9. The full `[Required reading]` clause (see [team-contract](./team-contract.md)) — for Phase 6 it adds two **per-task-type, instruction-set-local** read-only files, both scoped to this run's task-type by `okstra-ctl` at prep time:
46
+ 8. `**Prompt Delivery Mode:** <mode>` the selected adapter's declared `initialPromptDeliveryMode`.
47
+ 9. `**Model:** Report writer worker, <modelExecutionValue>` (resolved per Phase 5.5 anchor-header rules)
48
+ 10. The full `[Required reading]` clause (see [team-contract](./team-contract.md)) — for Phase 6 it adds two **per-task-type, instruction-set-local** read-only files, both scoped to this run's task-type by `okstra-ctl` at prep time:
48
49
  - `<instruction-set>/final-report-schema.json` — a task-type excerpt of the data.json schema (the other task-types' deliverable blocks and their unreachable `$defs` are stripped; ~38% of the full schema is `$defs` alone). This is your authoring aid for the data.json shape — the installed schema, not the excerpt, is what the run is judged against. Do **NOT** pull the full `schemas/final-report-v1.0.schema.json` — it carries all task-types and its `schemas/...` path is not part of the task bundle. (Validation still runs against the full schema post-hoc via the renderer, so the excerpt never relaxes the contract.)
49
50
  - `<instruction-set>/final-report-template.md` — the **phase-stripped** template (every other task-type's §5.x deliverable block removed by `render.py`'s `_strip_phase_blocks`, leaving only your run's §5.x). Do **NOT** also pull the full `templates/reports/final-report.template.md` source (it re-adds ~330 lines of other phases' deliverables and is not in the task bundle).
50
- 10. A one-line MCP pointer instead of the verbatim block (redundant — the brief is already in the report-writer's Required reading, item 9): `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).`
51
- 11. The convergence classifications (Full/Partial/Contested/Worker-Unique), the round history data (`roundHistory[]`), the `round2SkippedReason` value, and pointers to all worker result files under `worker-results/`. The report-writer worker populates `crossVerification.roundHistory` in the data.json so Section 6 can show which rounds executed, queue sizes, and why Round 2 was (or was not) skipped. The renderer prints the full per-round table only when more than one round ran; single-round or zero-round histories are auto-collapsed to a one-line summary.
52
- 12. `**Report Language:** <en|ko>` — must be either `en` or `ko`; `auto`
51
+ 11. A one-line MCP pointer instead of the verbatim block (redundant — the brief is already in the report-writer's Required reading, item 10): `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).`
52
+ 12. The convergence classifications (Full/Partial/Contested/Worker-Unique), the round history data (`roundHistory[]`), the `round2SkippedReason` value, and pointers to all worker result files under `worker-results/`. The report-writer worker populates `crossVerification.roundHistory` in the data.json so Section 6 can show which rounds executed, queue sizes, and why Round 2 was (or was not) skipped. The renderer prints the full per-round table only when more than one round ran; single-round or zero-round histories are auto-collapsed to a one-line summary.
53
+ 13. `**Report Language:** <en|ko>` — must be either `en` or `ko`; `auto`
53
54
  has been resolved by the lead from project.json / global config
54
55
  before the dispatch is constructed. The worker copies this verbatim
55
56
  into `data.json.meta.reportLanguage`.
56
- 13. For implementation-planning runs: a literal block listing the 12 required English section headings — `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Plan Body Verification`, `Cross-Project Dependencies`, `Decision Drafts`. This list is `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`; that tuple is the SSOT and this block must match it exactly. The writer uses these exact substrings as section headings (Korean translation in parentheses is allowed), and the `Plan Body Verification` section carries its required `Gate result:` line.
57
- 14. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Audit sidecar path>. After writing the data.json, invoke "okstra render-final-report <Result Path>" through the available execution interface so the markdown sibling is rendered before you return. Do not return the report inline. The validator fails the run when (a)'s schema validation fails, when the rendered markdown is absent, or when (b) is missing.`
58
- 15. The prose budget (dedup contract): `verdictCard.finalConclusion` is the conclusion SSOT — at most 3 sentences. `rationale.*` fields stay within 2 sentences each and reference the verdict card / row IDs instead of restating their prose; `readerSummary` fields are one line each; `summary` stays at 3-5 rows unless the run covers multiple tickets. The schema field descriptions carry the same budgets (`tests/contract/test_report_prose_budget.py` guards both surfaces). Generation time scales with output volume, so exceeding the budget is a cost bug, not extra diligence.
57
+ 14. For implementation-planning runs: a literal block listing the 12 required English section headings — `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Plan Body Verification`, `Cross-Project Dependencies`, `Decision Drafts`. This list is `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`; that tuple is the SSOT and this block must match it exactly. The writer uses these exact substrings as section headings (Korean translation in parentheses is allowed), and the `Plan Body Verification` section carries its required `Gate result:` line.
58
+ 15. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Audit sidecar path>. After writing the data.json, invoke "okstra render-final-report <Result Path>" through the available execution interface so the markdown sibling is rendered before you return. Do not return the report inline. The validator fails the run when (a)'s schema validation fails, when the rendered markdown is absent, or when (b) is missing.`
59
+ 16. The prose budget (dedup contract): `verdictCard.finalConclusion` is the conclusion SSOT — at most 3 sentences. `rationale.*` fields stay within 2 sentences each and reference the verdict card / row IDs instead of restating their prose; `readerSummary` fields are one line each; `summary` stays at 3-5 rows unless the run covers multiple tickets. The schema field descriptions carry the same budgets (`tests/contract/test_report_prose_budget.py` guards both surfaces). Generation time scales with output volume, so exceeding the budget is a cost bug, not extra diligence.
59
60
 
60
61
  **Fix-run incremental authoring (applies when the run's profile carries a "Fix-Run Carry" block).** Do not author the data.json from scratch. Start by copying the previous run's data.json (the `Previous report` path in the Fix-Run Carry block) to this run's Result Path, then update ONLY the blocks the fix run changed: `meta`/`header` (run seq, dates), `executionStatus`, `implementation.verifierResults`, `implementation.validationEvidence`, `implementation.commitList` / `diffSummary`, `crossVerification`, `verdictCard`, `finalVerdict`, and any `evidence` rows the fix touched. Deliverable prose for unchanged sections is carried forward verbatim — do not re-generate it. Then invoke the renderer exactly as in a full run. The schema validation and renderer contract are unchanged, so an incrementally-authored data.json passes the same post-hoc gates. The lead's dispatch prompt MUST include the previous data.json path when the carry block is present.
61
62
 
@@ -286,7 +287,7 @@ Skipping this file because "the real report is in `reports/`" is wrong. Both fil
286
287
 
287
288
  ### Main Body Section
288
289
 
289
- Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the per-task-type instruction-set copy defined in Phase 6 dispatch item 8); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
290
+ Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the per-task-type instruction-set copy defined in Phase 6 dispatch item 10); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
290
291
 
291
292
  **Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` / `Direction` / `Next Step` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` and the first item of `## 3. Recommended Next Steps`. Divergence is `contract-violated`.
292
293
 
@@ -63,7 +63,9 @@ Only workers selected from `recommendedWorkers` in `task-manifest.json` and `res
63
63
 
64
64
  ## Worker Prompt Composition
65
65
 
66
- Every worker prompt MUST start with the anchor headers rendered by `okstra_ctl.worker_prompt_headers.worker_prompt_headers()` (the generating SSOT never hand-author or reorder them). The generated absolute `**Audit sidecar path:**` is derived by `audit_sidecar_rel()` from the canonical worker result path; workers write to that header and never synthesize a `runs/<task-type>/...` destination. Their meaning and extraction rules for workers are documented in the worker preamble §"Anchor headers". Phase-specific extra headers (implementation worktree, final-verification target snapshot, improvement-discovery grilling log) are listed there too.
66
+ `okstra_ctl.initial_prompt_materialization` is the canonical owner of roster-derived initial prompt rendering, validation, and immutable publication. Code-backed `dispatch_worker` mappings leave missing roster prompts to `materialize_initial_prompts()` and pass the selected adapter's declared `initialPromptDeliveryMode`; they do not compose or overwrite those prompts themselves. Native in-process dispatch uses the same headers and the adapter's declared `lazy-path-reference` mode, persists the prompt before dispatch, and remains outside reverify and critic handling.
67
+
68
+ Every worker prompt MUST start with the anchor headers rendered by `okstra_ctl.worker_prompt_headers.worker_prompt_headers()` (the generating SSOT — never hand-author or reorder them). Every persisted initial prompt also carries exactly one non-empty `**Prompt Delivery Mode:** <mode>` header whose value is `eager-include` or `lazy-path-reference`. Redispatch reuses that persisted initial prompt byte-for-byte; it never regenerates or overwrites it. The generated absolute `**Audit sidecar path:**` is derived by `audit_sidecar_rel()` from the canonical worker result path; workers write to that header and never synthesize a `runs/<task-type>/...` destination. Their meaning and extraction rules for workers are documented in the worker preamble §"Anchor headers". Phase-specific extra headers (implementation worktree, final-verification target snapshot, improvement-discovery grilling log) are listed there too.
67
69
 
68
70
  The `**Coding preflight pack:**` anchor is emitted only for the `implementation-executor` and `implementation-verifier` audiences. An implementation report-writer never receives it. A pane display role named `verifier` during `final-verification` is still an initial analysis worker; it neither receives implementation conventions nor becomes a Phase 5.5 reverify dispatch. Reverify is identified by the `-reverify-r<N>-` prompt/result path contract in [convergence](./convergence.md).
69
71
 
@@ -73,7 +75,7 @@ The body must include: role name, task type, task key, assigned model, output co
73
75
 
74
76
  A `final-verification` prompt may contain exactly one optional `## Run-specific directive` section for a true run delta. It is limited to 40 nonblank lines; the nonblank prompt body excluding common/target anchors is limited to 96 lines. Larger shared material belongs in the instruction set and analysis packet. Before initial dispatch, the selected adapter removes worker-specific header lines (including optional `**Pane role:**`) and requires the remaining normalized analysis bodies to be byte-identical; the semantic `Primary analysis packet` input remains in that comparison.
75
77
 
76
- The Phase 7 run validator enforces the same cross-task rule against the persisted prompt paths recorded by the run manifest, dispatch records, and team-state. Both dispatch adapters and `validators/validate-run.py` call `okstra_ctl.worker_prompt_contract.validate_initial_prompt_records()`. The validator resolves each record through `PromptPlan`, applies audience-specific anchors, compares every equality group with at least two prompts, validates report-writer common anchors without adding it to an analysis group, and ignores `-reverify-r<N>-` prompts because they belong to the lightweight convergence protocol. A dispatch-time bypass therefore remains a run-level contract violation.
78
+ The Phase 7 run validator enforces the same cross-task rule against the persisted prompt paths recorded by the run manifest, dispatch records, and team-state. `initial_prompt_materialization` validates roster-derived prompts before and after publication, while `validators/validate-run.py` revalidates them through `okstra_ctl.worker_prompt_contract.validate_initial_prompt_records()`. The validator resolves each record through `PromptPlan`, requires the delivery-mode header on every non-reverify initial record, applies audience-specific anchors, compares every equality group with at least two prompts, validates report-writer common anchors without adding it to an analysis group, and ignores `-reverify-r<N>-` prompts because they belong to the lightweight convergence protocol. A dispatch-time bypass therefore remains a run-level contract violation.
77
79
 
78
80
  When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.
79
81
 
@@ -11,7 +11,7 @@ Same delivery paths as the other executor gates (see _implementation-executor.md
11
11
  - Claude executor reads this file directly after its last Edit / Write.
12
12
  - codex / antigravity executor cannot read this path (outside the CLI sandbox),
13
13
  so the lead appends this file's body into the persisted executor prompt at
14
- dispatch time (see codex_dispatch.py `_implementation_executor_tail`).
14
+ dispatch time (see `okstra_ctl.initial_prompt_materialization.materialize_initial_prompts()`).
15
15
  The `Pre-commit diff review sweep` heading below is the literal string the CLI
16
16
  wrapper's "Executor post-write gate forwarding check" greps for in the persisted
17
17
  prompt (agents/workers/_cli-wrapper-template.md → Prompt Composition).
@@ -28,15 +28,20 @@ Do not scan holistically and stop when it "looks fine". Work the matrix exhausti
28
28
  1. **List every changed file:** `git diff --name-only <stage-base>..HEAD`. That list is your worklist — cover it to the end; no sampling.
29
29
  2. **Classify each file and select its rule set** from the conventions the preflight already loaded (do NOT re-derive the rules here — read them from the routed pack):
30
30
  - any source file → `clean-code.md`: truthful + standalone names, plain-English summary test, single-purpose ≤50-line functions, DRY (incl. scattered domain literals + documented forks), no magic numbers, shallow nesting, comments explain why.
31
- - test file (`*.spec.*` / `*.test.*` / `test_*.py` / `*_test.go` …) → `clean-code.md` "Testing discipline": no self-mocking of the SUT, behavioral (outcome) assertions not interaction-only, no tautological delegation assertion.
32
- - port / adapter / domain file, when the hexagonal overlay is loaded → `architectures/hexagonal.md`: no business logic in a port body, adapter methods are I/O only (no post-fetch filtering on domain state, no `findValid*`/`findActive*` names hiding a rule), domain objects declared under the domain boundary.
31
+ - a file that decides, mutates, or persists state → `clean-code.md` "Mutation and state boundaries": decide on the direct identifier rather than a status/flag proxy, capture before-state in one snapshot ahead of the mutating boundary, update only this work's owned fields on an existing row, re-read state before calling a zero-affected-rows write success or failure, put priority-between-inputs in a named domain function, and keep error messages to what was actually observed. Also check that no state union/enum was re-declared beside an authoritative one the domain or a dependency exports.
32
+ - test file (`*.spec.*` / `*.test.*` / `test_*.py` / `*_test.go` …) → `clean-code.md` "Testing discipline": no self-mocking of the SUT, behavioral (outcome) assertions not interaction-only, no tautological delegation assertion, no effect claimed under its own mock, shared-fixture defaults left on the ordinary path, setup values that actually separate the scenarios, every new test helper/mock used by a test in this same diff, and no positional mock-argument access (`rg 'mock\.calls'`).
33
+ - port / adapter / domain file, when the hexagonal overlay is loaded → `architectures/hexagonal.md`: no business logic in a port body, adapter methods are I/O only (no post-fetch filtering on domain state, no `findValid*`/`findActive*` names hiding a rule), domain objects declared under the domain boundary, no changed domain file importing an ORM / framework / adapter / service (read the import list — mechanical), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory: record it with the port sketch; the codebase already injecting concrete classes is the debt this pays down, not a reason to skip it).
33
34
  A file can hold several roles — apply every rule set that fits it.
34
35
  3. **Decide clean-or-finding for each cell.** Read the full file when a rule needs context (never judge a port/adapter/domain or a naming rule from the hunk alone).
35
36
  4. **Fix each finding in place** before the commit. When a readability finding is real, the fix is a named helper or named intermediate value — sketch the cleaner shape (a few lines) in your audit note, then apply it. When the fix is genuinely out of this stage's scope, record it as an `Out-of-plan` note instead of silently leaving it.
36
37
 
37
38
  ## Coverage footer (BLOCKING deliverable)
38
39
 
39
- End your audit-sidecar entry for this sweep with a one-line `Coverage:` footer naming each file you examined and the rule set(s) you applied to it — e.g. `Coverage: users.service.ts [names, plain-english], users.repository.adapter.ts [hexagonal, names], users.service.spec.ts [testing-discipline]`. The footer is how the completion self-check and any reviewer confirm nothing in the diff was skipped. A sweep with no footer is an unfinished sweep.
40
+ End your audit-sidecar entry for this sweep with a one-line `Coverage:` footer naming each file you examined, the rule set(s) you applied to it, **and the outcome of each** `clean` when you looked and found nothing, or `fixed <n>` when you fixed findings in place:
41
+
42
+ `Coverage: users.service.ts [names: clean, plain-english: fixed 1], users.repository.adapter.ts [hexagonal: fixed 1, names: clean], users.service.spec.ts [testing-discipline: clean]`
43
+
44
+ `clean` is a result you assert, not a blank you leave. Naming the rule set without its outcome reads identically whether you checked it or skipped it, which is exactly what this footer exists to distinguish — the completion self-check and any reviewer read it to confirm nothing in the diff was skipped. A sweep with no footer, or a footer whose entries carry no outcome, is an unfinished sweep.
40
45
 
41
46
  ## Graceful degradation
42
47
 
@@ -24,10 +24,10 @@ until Phase 5 ends, then drop from active context for Phase 6/7.
24
24
  - **CLI executor (BLOCKING when the executor provider is `codex` or `antigravity`):** the executor CLI process does NOT share the lead's context, and it cannot Read this sidecar's directory — that path sits outside the CLI sandbox and the CLI only sees its stdin prompt, so a file reference never reaches it. The lead MUST physically append the **body** of `_coding-conventions-preflight.md` into the persisted executor prompt at dispatch time: Read the file from the same absolute directory you read this sidecar from, then `Write`/`cat` its body into the persisted prompt. Never hand-retype it. 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).
25
25
  - **Pre-commit diff review sweep (BLOCKING — runs AFTER the last `Edit` / `Write`, BEFORE the final commit).** The gate body is a single source at `prompts/profiles/_implementation-diff-review.md` (sibling of this sidecar): an exhaustive file×rule sweep of the actual diff against the conventions the preflight loaded, with fix-in-place and a `Coverage:` footer. Do NOT re-type it from memory — deliver it by file so it cannot drift.
26
26
  - **Claude executor:** Read `_implementation-diff-review.md` end-to-end after the stage's last edit, run the sweep over `git diff <stage-base>..HEAD`, fix findings in place, and write its `Coverage:` footer to your audit sidecar before committing.
27
- - **CLI executor (codex / antigravity):** the CLI process cannot Read this path (outside its sandbox). The lead appends this file's body into the persisted executor prompt at dispatch time, between the preflight body and the self-check body (see `codex_dispatch.py` `_implementation_executor_tail`). The head-less executor honours all three gates from the single prompt.
27
+ - **CLI executor (codex / antigravity):** the CLI process cannot Read this path (outside its sandbox). The lead appends this file's body into the persisted executor prompt at dispatch time, between the preflight body and the self-check body (see `okstra_ctl.initial_prompt_materialization.materialize_initial_prompts()`). The head-less executor honours all three gates from the single prompt.
28
28
  - **Completion self-check (BLOCKING — runs BEFORE you claim the stage done).** The gate body is a single source at `prompts/profiles/_implementation-self-check.md` (sibling of this sidecar): the completion gate (diff-review Coverage footer present, functions ≤50 lines, conventions applied, truthful names & why-comments, real build/test run, cleanup). Do NOT re-type it from memory — deliver it by file so it cannot drift.
29
29
  - **Claude executor:** Read `_implementation-self-check.md` end-to-end before appending the `status:"done"` row, then write the confirming evidence per item to your audit sidecar.
30
- - **CLI executor (codex / antigravity):** the CLI process cannot Read this path (outside its sandbox). The lead appends this file's body into the persisted executor prompt at dispatch time, immediately after the diff-review body (see `codex_dispatch.py` `_implementation_executor_tail`). The head-less executor honours all three gates from the single prompt.
30
+ - **CLI executor (codex / antigravity):** the CLI process cannot Read this path (outside its sandbox). The lead appends this file's body into the persisted executor prompt at dispatch time, immediately after the diff-review body (see `okstra_ctl.initial_prompt_materialization.materialize_initial_prompts()`). The head-less executor honours all three gates from the single prompt.
31
31
  - **Stage discipline transcription (when a preceding stage is `done`):** the lead MUST transcribe the `Stage discipline` rule (from this run's rendered profile — the INCLUDEd `_stage-discipline.md` body) verbatim into the dispatched CLI executor prompt, so a codex/antigravity executor honors the prior-stage behavior-freeze. Declaration-level — no wrapper sentinel.
32
32
  - **Non-interactive auto-execution (BLOCKING when the executor provider is `codex` or `antigravity`).** A CLI executor runs head-less (`codex exec` / antigravity 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-cycle commit → `### 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`.
33
33
  - **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.
@@ -4,7 +4,7 @@ gate runs BEFORE the first Edit/Write; this gate runs BEFORE the executor claims
4
4
  done. Same delivery paths: Claude executor reads this file directly; codex /
5
5
  antigravity executor cannot read it (outside the CLI sandbox), so the lead
6
6
  appends this file's body into the persisted executor prompt at dispatch time
7
- (see codex_dispatch.py `_implementation_executor_tail`).
7
+ (see `okstra_ctl.initial_prompt_materialization.materialize_initial_prompts()`).
8
8
  -->
9
9
 
10
10
  # Implementation self-check (BLOCKING — before you claim done)
@@ -15,12 +15,13 @@ changed files (`git diff --name-only <stage-base>..HEAD`) and, for the per-file
15
15
  items, write the confirming evidence per file, not one global "✓". If any item
16
16
  fails, fix it or surface the violation — do not claim done on a failing item.
17
17
 
18
- - diff-review done: the `Pre-commit diff review sweep` ran over the full diff and its `Coverage:` footer is in your audit sidecar (every changed file named with the rule set applied). No footer → the sweep is unfinished; do not claim done.
18
+ - diff-review done: the `Pre-commit diff review sweep` ran over the full diff and its `Coverage:` footer is in your audit sidecar every changed file named, with each applied rule set carrying its outcome (`clean` or `fixed <n>`). No footer, or entries with no outcome → the sweep is unfinished; do not claim done.
19
19
  - functions: every new/edited function ≤50 effective lines, single purpose — cite the longest function's actual line count, not "all under 50"
20
20
  - conventions: applied the routed pack + project patterns (name which ones)
21
21
  - names & comments: names say what, comments say why; no obvious-restatement comments; no truthful-name violations
22
22
  - verification: ran the actual build/test — paste the exact command line and its real exit code / output tail, never a paraphrased "tests pass"
23
23
  - acceptance: each implemented plan step's `Acceptance:` condition is observably met — cite the RED→GREEN test (or the command output) proving it per step, not one global "acceptance met"; and each `Test case (success|boundary|failure)` the plan declared for this stage has a matching test in the diff (name the test), so declared edge/failure cases are not silently uncovered
24
+ - mutation check: for at least one test this diff added or changed, break the implementation it covers on purpose (return the wrong value, skip the write), confirm that test FAILS, then restore — cite the test name and the observed failure. A test that still passes against a broken implementation is not coverage, and "verified" claimed without this check plus the real command output above is not a verified claim
24
25
  - cleanup: no dead or commented-out code left behind
25
26
 
26
27
  Close with a `Self-check coverage:` line naming the files you verified the
@@ -12,7 +12,7 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
12
12
  - **Verifier dispatch labelling.** The core functional role label is `<provider>-verifier` (here, and identically in `final-verification`). Provider, role, and model identity are owned by `prompts/lead/okstra-lead-contract.md` "Model assignments"; the selected runtime adapter owns provider-native dispatch-label mapping (including any `name` / `**Pane role:**` fields) and token-attribution wiring under its "Semantic operation mapping".
13
13
  - The verifier slots are `Claude verifier` and `Codex verifier`, plus `Antigravity verifier` **only when `antigravity` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
14
14
  - Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
15
- - Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.6-sol`, `Antigravity verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.6-sol`), antigravity→`--antigravity-model` (default `auto`).
15
+ - Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.6-sol`, `Antigravity verifier`=`gemini-3.1-pro` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.6-sol`), antigravity→`--antigravity-model` (default `gemini-3.1-pro`).
16
16
  - Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.
17
17
 
18
18
  ## Verifier QA duties (independent re-run mandate)
@@ -117,10 +117,19 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
117
117
  - **Interaction-only assertion:** a test whose only/primary assertion is `toHaveBeenCalled*` / `toHaveBeenCalledTimes` on an internal helper or a non-side-effecting collaborator, with no assertion on the returned value / resulting state / persisted row / emitted event.
118
118
  - **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.
119
119
  - **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*`).
120
- - **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.
120
+ - **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; a changed file under the domain folder importing an ORM / DB layer, a framework or its DI decorators, or anything under adapters / infrastructure / services (read the import list — this verdict is mechanical, not a judgment).
121
121
  - **gitignored file committed to the branch:** any path in the `git diff --name-only <base>...HEAD` enumeration that `.gitignore` excludes — enumerate them by piping that list through `git check-ignore --stdin --no-index`. A committed ignored file means the executor bulk-added (`git add .`/`-A`) or force-staged (`git add -f`) it, leaking build output, scratch files, or verification artifacts into the eventual PR. This explicitly includes `.okstra/` paths (and `.project-docs/` when the legacy symlink is present): `.okstra/**` is gitignored, so a committed okstra file (qa scripts, conformance results) is always this defect. Cite each 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.
122
122
  - **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/scripts/` 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/scripts/` (or declaring it as a Tier 3 conformance script). Mock-only unit tests in source are NOT a hit.
123
- - **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`.
123
+ - **Proxy-based identity decision:** a move / ownership / re-parenting decision taken from a status field or flag while the source and destination identifiers were available and never compared. Cite the condition and the identifiers it should have compared, and show the opposite case the condition also reads true for.
124
+ - **Before-state read after a mutating boundary:** a value recorded as the "before" state (audit column, diagnostic field) that is read off the original object *after* a strategy call, repository write, reload, or intervening `await`, or reconstructed from the post-state. Cite the boundary and the field. Before-values feeding one diagnostic must come from a single pre-boundary snapshot.
125
+ - **Existing-row update outside its owned fields:** a reuse path that writes provenance, ownership, or externally-managed columns with no explicit requirement authorising it, or that loses the create-vs-reuse distinction before the write. The update field set must be an allowlist.
126
+ - **Zero affected rows treated as a verdict:** a conditional write whose zero-row result is reported as success, or as one undifferentiated failure, without re-reading state to separate already-done / conflicting / deleted.
127
+ - **Authoritative type re-declared:** a hand-maintained string union or state-literal list duplicating an enum / type the domain or a dependency already exports. Grep the state literals and the new type name; cite the existing declaration.
128
+ - **Shared fixture default flipped for an exception:** a common fixture's default inverted to exercise one exceptional path, silently changing the meaning of every existing no-argument caller. The exceptional condition belongs in the test that needs it.
129
+ - **Positional mock-argument access:** `mock.calls[<n>][<m>]` (or the framework equivalent) used to inspect arguments instead of an intent-revealing `toHaveBeenCalledWith` / explicit-absence assertion. Detect with `rg 'mock\.calls'` over the changed test files.
130
+ - **Non-separating test data:** two scenarios whose setup values and assertions are identical, so a wrong implementation passes both; or new test tooling added in this diff (mock, state setter, repository branch) that no test calls.
131
+ - **Effect claimed under its own mock:** a test presented as covering an effect whose producing path is replaced by a mock inside that same test. The mock's presence in the harness is not evidence the branch behind it works.
132
+ - **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, weak-but-not-misleading names, priority-between-inputs policy inlined in a service condition instead of a named domain function, an error message asserting a cause the code never observed, a memory / concurrency / batching change with no test pinning the bound it claims, or (hexagonal overlay only) a service dependency this diff adds or modifies that injects a concrete adapter instead of a port — record it with the port sketch; an existing convention of concrete injections does not convert this one to `clean`, it is the debt the rule pays down. These land in the verifier result as `should-fix` / `nit` recommendations, not as a `FAIL`.
124
133
  - **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.
125
134
 
126
135
  ### Fix-run incremental scope (applies when the profile carries a "Fix-Run Carry" block)
@@ -104,6 +104,7 @@
104
104
  - `Test case (boundary): <edge input → expected, the command/test name>` — an edge/boundary input (empty, min/max, missing, first/last, off-by-one). When the slice genuinely has no boundary, write `N/A — <reason>` so the value is non-empty and the omission is a conscious, reviewable decision — never leave it blank.
105
105
  - `Test case (failure): <invalid input → error/rejection, the command/test name>` — a negative/failure path (invalid input, unauthorized, error propagation).
106
106
  Validator S10d rejects a missing/empty line in any of the three categories (skipped only when a `TDD exemption:` line is present). The `RED:` step below must encode these cases, not a single assertion.
107
+ **Pick the cases by distinct outcome, not by line coverage.** When the stage writes or reconciles state, its meaningfully different outcomes are usually more than three — normal success, target already in the desired state (resume), existing data reused rather than created, a conflicting concurrent state, target absent, and mid-way failure with rollback. Enumerate the ones this stage actually implements and route them across the three lines (the `boundary` line is where resume / already-done / reuse belongs; `failure` carries conflict, absence, and rollback), naming each in the cell rather than collapsing them into "edge input". An implemented outcome with no declared case is a coverage gap the executor will not backfill.
107
108
  - **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
108
109
  - `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
109
110
  - `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | expected`. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change (no lower time bound; it may span several files that change together); for code steps include actual code or diff sketch. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`expected` = FAIL) — the RED step encodes the case set, not a single happy-path assertion; at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`expected` = PASS); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section (mirrors the executor's per-step exemption in `_implementation-executor.md`). Validator S10c enforces RED-first + GREEN **and** that the `RED:` step's `expected` reads FAIL / the `GREEN:` step's reads PASS; S10e rejects a `TDD exemption:` whose reason is not one of doc-only / config-only / pure-rename (both in `validators/validate-implementation-plan-stages.py`).
@@ -44,7 +44,7 @@
44
44
  - `change base branch` — return to Q2 and re-ask the base selection.
45
45
  - `cancel` — end the run without push or PR; record the cancellation in the final report.
46
46
  - The probe is read-only. It MUST NOT run `git merge`, `git rebase`, `git pull`, or any command that mutates the working tree or local refs.
47
- 3. **PR title + PR body confirmation** — show the lead's inline draft verbatim and capture one of:
47
+ 3. **PR title + PR body confirmation** — show the lead's inline draft verbatim, together with the filled-body self-check result (either `body self-check: clean` or the list of hits with their lines, per the drafting rules below), and capture one of:
48
48
  - `use as-is` — proceed with the drafted text.
49
49
  - `edit then proceed` — accept inline edits from the user, then proceed with the edited text.
50
50
  - `cancel` — end the run without executing push or PR commands; record the cancellation in the final report.
@@ -54,6 +54,7 @@
54
54
  - produce **two artifacts** before showing them to the user:
55
55
  1. **PR title** — by default the subject of the most recent implementation commit, or a concise Conventional Commits-style summary of the committed range.
56
56
  2. **PR body** — markdown filled from `PR_TEMPLATE_PATH`. The user-confirmation step's diff (Q3 `edit then proceed`) is computed against the filled template, not against the raw template file.
57
+ - **Filled-body self-check (runs on the drafted body, before Q3 shows it).** "Fill in the placeholders" is only observable if someone checks that they were filled, so scan the drafted body for these and carry the result into Q3: a residual HTML comment or template marker (`<!-- … -->`, `_Describe your changes…_`, a bare `TODO` / `FIXME` the diff did not introduce), an unchecked `- [ ]` box with no inline N/A justification, a section heading whose body is empty, and a whole body short enough to carry no substance. Each hit is reported to the user with its line — never silently left in, and never auto-filled with invented content. The user remains free to accept it; the point is that they see it before choosing `use as-is`.
57
58
  - Allowed actions during the run (Claude lead only):
58
59
  - 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>`.
59
60
  - 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.
@@ -0,0 +1,64 @@
1
+ """Filesystem layout for code-review result files.
2
+
3
+ The two modes deliberately live under different roots: an okstra session
4
+ review belongs to its task bundle, while a session-independent branch
5
+ review belongs to the authoring tree. Centralising the assembly here keeps
6
+ skill markdown from re-deriving literal paths.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+
13
+ from . import paths
14
+
15
+ REVIEW_DIRNAME = "code-reviews"
16
+ BRANCH_REVIEW_ROOT = ".project-docs"
17
+
18
+ _STAGE_FILE_RE = re.compile(r"^stage-(\d+)(?:-r(\d+))?\.md$")
19
+ _BRANCH_FILE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-(\d+)\.md$")
20
+
21
+
22
+ def stage_review_dir(project_root: Path, task_group: str, task_id: str) -> Path:
23
+ """Review directory inside the task bundle."""
24
+ return paths.task_dir(project_root, task_group, task_id) / REVIEW_DIRNAME
25
+
26
+
27
+ def branch_review_dir(project_root: Path, branch: str) -> Path:
28
+ """Review directory for a branch. Slashes stay as nested directories."""
29
+ return Path(project_root) / BRANCH_REVIEW_ROOT / REVIEW_DIRNAME / branch
30
+
31
+
32
+ def _existing_names(review_dir: Path) -> list[str]:
33
+ if not Path(review_dir).is_dir():
34
+ return []
35
+ return [entry.name for entry in Path(review_dir).iterdir() if entry.is_file()]
36
+
37
+
38
+ def next_stage_review(review_dir: Path, stage: int) -> tuple[Path, int]:
39
+ """Next round's file path and round number for one stage.
40
+
41
+ Round 1 carries no suffix; later rounds are `-r<N>`.
42
+ """
43
+ highest = 0
44
+ for name in _existing_names(review_dir):
45
+ matched = _STAGE_FILE_RE.match(name)
46
+ if not matched or int(matched.group(1)) != stage:
47
+ continue
48
+ highest = max(highest, int(matched.group(2) or 1))
49
+ round_no = highest + 1
50
+ stem = f"stage-{stage:02d}"
51
+ suffix = "" if round_no == 1 else f"-r{round_no}"
52
+ return Path(review_dir) / f"{stem}{suffix}.md", round_no
53
+
54
+
55
+ def next_branch_review(review_dir: Path, date: str) -> tuple[Path, int]:
56
+ """Next file path and sequence number for a branch review on `date`."""
57
+ highest = 0
58
+ for name in _existing_names(review_dir):
59
+ matched = _BRANCH_FILE_RE.match(name)
60
+ if not matched or matched.group(1) != date:
61
+ continue
62
+ highest = max(highest, int(matched.group(2)))
63
+ seq = highest + 1
64
+ return Path(review_dir) / f"{date}-{seq:02d}.md", seq
@@ -0,0 +1,152 @@
1
+ """코드 리뷰가 무엇을 읽고 결과 파일을 어디에 쓰는지 해소한다.
2
+
3
+ 두 모드를 한 진입점에 모아 스킬이 base 커밋도, 리뷰 파일 경로도, 회차도 다시
4
+ 유도하지 않게 한다. stage 모드는 read side 에 그대로 위임하고, branch 모드만
5
+ 여기서 해소한다 — caller 가 base 를 지정하지 않으면 기본 브랜치와의
6
+ merge-base 를 쓴다. 이 모듈은 인자 검증과 JSON 정형화만 하고, 경로·회차 조립은
7
+ code_review_paths 가, stage 규칙은 code_review_target_snapshot 이 소유한다.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ from datetime import date
17
+ from pathlib import Path
18
+
19
+ from okstra_project import (
20
+ ResolverError,
21
+ StateError,
22
+ code_review_target_snapshot,
23
+ resolve_project_root,
24
+ )
25
+
26
+ from .code_review_paths import branch_review_dir, next_branch_review
27
+
28
+ DEFAULT_BRANCH_CANDIDATES = ("main", "master")
29
+
30
+ _DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
31
+
32
+
33
+ def _git_out(repo: Path, *args: str) -> str:
34
+ result = subprocess.run(
35
+ ["git", "-C", str(repo), *args],
36
+ capture_output=True,
37
+ text=True,
38
+ check=False,
39
+ )
40
+ return result.stdout.strip() if result.returncode == 0 else ""
41
+
42
+
43
+ def _default_branch_ref(repo: Path) -> str:
44
+ """merge-base 를 잴 기준 ref — origin/HEAD 가 있으면 그것, 없으면 관례 이름."""
45
+ remote_head = _git_out(repo, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
46
+ if remote_head:
47
+ return remote_head
48
+ for candidate in DEFAULT_BRANCH_CANDIDATES:
49
+ if _git_out(repo, "rev-parse", "--verify", "--quiet", candidate):
50
+ return candidate
51
+ return ""
52
+
53
+
54
+ def _merge_base(repo: Path, branch: str) -> str:
55
+ default_ref = _default_branch_ref(repo)
56
+ merge_base = _git_out(repo, "merge-base", default_ref, branch) if default_ref else ""
57
+ if not merge_base:
58
+ raise StateError(
59
+ f"no merge-base for {branch!r} against the default branch — "
60
+ "pass --base explicitly",
61
+ stage="base_unresolved",
62
+ )
63
+ return merge_base
64
+
65
+
66
+ def _review_date(raw: str) -> str:
67
+ """branch 결과 파일명에 쓸 날짜.
68
+
69
+ next_branch_review 는 넘겨받은 문자열을 그대로 파일명에 쓰지만 되읽을 때는
70
+ `YYYY-MM-DD` 만 인식한다. 어긋난 날짜를 통과시키면 회차가 늘 1 로 돌아가
71
+ 직전 리뷰 파일을 덮어쓴다.
72
+ """
73
+ if not raw:
74
+ return date.today().isoformat()
75
+ if not _DATE_RE.match(raw):
76
+ raise StateError(f"--date must be YYYY-MM-DD, got {raw!r}", stage="args")
77
+ return raw
78
+
79
+
80
+ def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> dict:
81
+ head = _git_out(repo, "rev-parse", branch)
82
+ if not head:
83
+ raise StateError(f"branch {branch!r} not found", stage="branch_missing")
84
+ base_commit = base or _merge_base(repo, branch)
85
+ review_path, seq = next_branch_review(branch_review_dir(repo, branch), review_date)
86
+ return {
87
+ "mode": "branch",
88
+ "worktreePath": str(repo),
89
+ "branch": branch,
90
+ "baseCommit": base_commit,
91
+ "headCommit": head,
92
+ "reviewPath": str(review_path),
93
+ "round": seq,
94
+ }
95
+
96
+
97
+ def _build_parser() -> argparse.ArgumentParser:
98
+ parser = argparse.ArgumentParser(
99
+ description="Resolve a code review's diff range and result file path."
100
+ )
101
+ parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
102
+ parser.add_argument("--stage", type=int, default=0, help="stage number to review")
103
+ parser.add_argument("--branch", default="", help="branch to review")
104
+ parser.add_argument("--base", default="", help="explicit base ref for branch mode")
105
+ parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
106
+ parser.add_argument("--project-root", default="", help="explicit project root")
107
+ parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
108
+ parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
109
+ return parser
110
+
111
+
112
+ def _resolve(args: argparse.Namespace, project_root: Path) -> dict:
113
+ if args.task_key:
114
+ if args.stage < 1:
115
+ raise StateError("--task-key needs --stage <N>, N >= 1", stage="args")
116
+ return code_review_target_snapshot(project_root, args.task_key, args.stage)
117
+ if args.branch:
118
+ return _branch_snapshot(
119
+ project_root, args.branch, args.base, _review_date(args.date)
120
+ )
121
+ raise StateError("pass either --task-key --stage or --branch", stage="args")
122
+
123
+
124
+ def main(argv: list[str] | None = None) -> int:
125
+ args = _build_parser().parse_args(argv)
126
+ try:
127
+ project_root = resolve_project_root(
128
+ explicit_root=args.project_root, cwd=args.cwd
129
+ )
130
+ except ResolverError as exc:
131
+ print(json.dumps({"ok": False, "stage": "resolve", "reason": str(exc)}))
132
+ return 2
133
+
134
+ try:
135
+ snapshot = _resolve(args, Path(project_root))
136
+ except StateError as exc:
137
+ print(json.dumps({
138
+ "ok": False,
139
+ "stage": exc.stage or "target",
140
+ "reason": str(exc),
141
+ }))
142
+ return 1
143
+
144
+ print(json.dumps(
145
+ {"ok": True, "projectRoot": str(project_root), **snapshot},
146
+ ensure_ascii=False, indent=2,
147
+ ))
148
+ return 0
149
+
150
+
151
+ if __name__ == "__main__":
152
+ raise SystemExit(main(sys.argv[1:]))