okstra 0.38.1 → 0.40.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 (80) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/kr/architecture.md +18 -2
  4. package/docs/kr/cli.md +1 -1
  5. package/docs/project-structure-overview.md +2 -3
  6. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +326 -0
  7. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +337 -0
  8. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +410 -0
  9. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +728 -0
  10. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +113 -0
  11. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +173 -0
  12. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +154 -0
  13. package/docs/task-process/requirements-discovery.md +1 -1
  14. package/package.json +3 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/{python → bin}/lib/okstra/usage.sh +3 -2
  17. package/runtime/bin/okstra-codex-exec.sh +3 -3
  18. package/runtime/bin/okstra-trace-cleanup.sh +64 -26
  19. package/runtime/prompts/profiles/_common-contract.md +9 -5
  20. package/runtime/prompts/profiles/final-verification.md +18 -16
  21. package/runtime/prompts/profiles/implementation-planning.md +1 -0
  22. package/runtime/prompts/profiles/requirements-discovery.md +18 -1
  23. package/runtime/prompts/wizard/prompts.ko.json +11 -0
  24. package/runtime/python/okstra_ctl/consumers.py +1 -1
  25. package/runtime/python/okstra_ctl/fanout.py +35 -0
  26. package/runtime/python/okstra_ctl/migrate.py +21 -42
  27. package/runtime/python/okstra_ctl/reconcile.py +2 -2
  28. package/runtime/python/okstra_ctl/render_final_report.py +0 -1
  29. package/runtime/python/okstra_ctl/run.py +0 -29
  30. package/runtime/python/okstra_ctl/run_context.py +9 -12
  31. package/runtime/python/okstra_ctl/seeding.py +0 -192
  32. package/runtime/python/okstra_ctl/wizard.py +70 -5
  33. package/runtime/python/okstra_ctl/work_categories.py +21 -0
  34. package/runtime/python/okstra_ctl/worktree.py +74 -77
  35. package/runtime/python/okstra_project/__init__.py +0 -6
  36. package/runtime/python/okstra_project/dirs.py +0 -8
  37. package/runtime/schemas/final-report-v1.0.schema.json +34 -27
  38. package/runtime/skills/okstra-context-loader/SKILL.md +1 -1
  39. package/runtime/skills/okstra-convergence/SKILL.md +1 -1
  40. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  41. package/runtime/skills/okstra-run/SKILL.md +2 -0
  42. package/runtime/templates/prd/brief.template.md +1 -1
  43. package/runtime/templates/reports/fan-out-unit.template.md +25 -0
  44. package/runtime/templates/reports/final-report.template.md +24 -13
  45. package/runtime/templates/reports/final-verification-input.template.md +16 -5
  46. package/runtime/templates/reports/i18n/en.json +6 -3
  47. package/runtime/templates/reports/i18n/ko.json +6 -3
  48. package/runtime/templates/worker-prompt-preamble.md +7 -0
  49. package/runtime/validators/lib/fixtures.sh +2 -2
  50. package/runtime/validators/lib/validate-assets.sh +9 -0
  51. package/runtime/validators/validate-implementation-plan-stages.py +19 -11
  52. package/runtime/validators/validate-run.py +114 -0
  53. package/runtime/validators/validate-schedule.py +4 -4
  54. package/runtime/validators/validate_fanout.py +99 -0
  55. package/src/_proc.mjs +31 -0
  56. package/src/check-project.mjs +1 -25
  57. package/src/config.mjs +7 -31
  58. package/src/doctor.mjs +10 -29
  59. package/src/install.mjs +8 -36
  60. package/src/migrate.mjs +1 -18
  61. package/src/okstra-dirs.mjs +0 -11
  62. package/src/setup.mjs +1 -154
  63. package/src/uninstall.mjs +6 -13
  64. package/runtime/templates/okstra.CLAUDE.md +0 -104
  65. /package/runtime/{python → bin}/lib/okstra/cli.sh +0 -0
  66. /package/runtime/{python → bin}/lib/okstra/globals.sh +0 -0
  67. /package/runtime/{python → bin}/lib/okstra/interactive.sh +0 -0
  68. /package/runtime/{python → bin}/lib/okstra/project-resolver.sh +0 -0
  69. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-batch.sh +0 -0
  70. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-list.sh +0 -0
  71. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-open.sh +0 -0
  72. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-projects.sh +0 -0
  73. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reconcile.sh +0 -0
  74. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-reindex.sh +0 -0
  75. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-rerun.sh +0 -0
  76. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-show.sh +0 -0
  77. /package/runtime/{python → bin}/lib/okstra-ctl/cmd-tail.sh +0 -0
  78. /package/runtime/{python → bin}/lib/okstra-ctl/main.sh +0 -0
  79. /package/runtime/{python → bin}/lib/okstra-ctl/prepare.sh +0 -0
  80. /package/runtime/{python → bin}/lib/okstra-ctl/usage.sh +0 -0
@@ -29,12 +29,16 @@ profile document.
29
29
  - This rule does NOT relax any phase-specific Forbidden actions list; safety rules in the per-profile document remain in force regardless of the user's authority.
30
30
  - Anti-escalation rule (shared):
31
31
  - treating "다음 단계 진행해" or equivalent user phrases as authorisation to start a *different* lifecycle phase is forbidden. The next phase begins only in a separate okstra run launched with the new `--task-type`. Per-profile documents may further restrict this within their own scope.
32
- - Phase wrap-up — worker trace pane disposition (shared, MUST be the *last* step before returning control to the user):
33
- - Codex / Gemini worker wrappers spawn `tail -F` trace panes in the lead's tmux session, titled `<cli>-<role>-<pid>-trace` (e.g. `codex-worker-93421-trace`, `gemini-executor-93422-trace`) the matching caller (worker) pane is titled `<cli>-<role>-<pid>` so worker ↔ trace pairs can be matched by the shared `<pid>` suffix. They survive every worker invocation by design so the operator can scroll back through the final output, but accumulate across phases and clutter the screen.
34
- - When `$TMUX_PANE` is set, after the final-report file has been written and the routing recommendation has been issued, the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list` exactly once. The output is a tab-separated `<pane_id>\t<pane_title>` list of every trace pane registered for this Claude session.
35
- - If the list is empty, skip the question there is nothing to ask about.
32
+ - Phase-start pane reset (shared runs BEFORE dispatching each new worker batch):
33
+ - okstra creates two kinds of tmux pane per run: (a) **worker-agent panes** the harness gives to dispatched subagents (titled `claude-worker` / `codex-worker` / `gemini-worker` / `report-writer-worker`), and (b) **trace panes** the codex/gemini wrappers spawn (`<cli>-<role>-<pid>-trace`). Both accumulate across internal phases because each new phase dispatches a fresh worker batch and the prior panes are never reclaimed.
34
+ - When `$TMUX_PANE` is set, the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh` (no args) **immediately before** dispatching the next phase's workers — i.e. just before emitting each `PROGRESS: phase-5.5-convergence round=<N>` marker and just before `PROGRESS: phase-6-synthesis dispatching report-writer-worker`. This closes every prior-phase okstra pane (worker-agent + trace) for the lead session, while NEVER killing the lead's own pane.
35
+ - This is **automatic and silent** — NO user prompt. Report it in one short line (e.g. `이전 phase okstra pane 3개 정리`) and proceed. It is silent-skipped when `$TMUX_PANE` is unset; the lead MUST NOT fabricate a synthetic pane list in that case.
36
+ - Phase wrap-up — okstra pane disposition (shared, MUST be the *last* step before returning control to the user):
37
+ - At run end the only residual okstra panes are the LAST phase's (e.g. the `report-writer-worker` agent pane and any codex/gemini trace pane). `okstra-trace-cleanup.sh --list` returns one tab-separated `<pane_id>\t<pane_title>` line per residual okstra pane (worker-agent + trace) for this lead session.
38
+ - When `$TMUX_PANE` is set, after the final-report file has been written and the routing recommendation has been issued, the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list` exactly once. The output lists every residual okstra pane (worker-agent + trace) for this Claude session, never the lead's own pane.
39
+ - If the list is empty, skip the question — there is nothing to ask about (the phase-start resets above usually already cleared prior phases).
36
40
  - Otherwise the lead MUST present the user with a strict binary choice **before** declaring the phase complete. Use one prompt of this shape (Korean preferred, English acceptable if the rest of the run is in English):
37
- > 현재 phase 종료 시점입니다. 다음 worker trace pane 이 열려 있습니다 — 닫을까요?
41
+ > 현재 phase 종료 시점입니다. 다음 okstra pane 이 열려 있습니다 — 닫을까요?
38
42
  > <인용된 `--list` 출력>
39
43
  > (예) 모두 닫기 / (아니오) 그대로 두기
40
44
  - On `예` / `y` / `close` → run `$HOME/.okstra/bin/okstra-trace-cleanup.sh` (no args) and report the kill count back in one sentence.
@@ -8,31 +8,33 @@
8
8
  - Optional workers (opt-in via `--workers`):
9
9
  - gemini — when added to the roster it joins the analyser set; omitted by default
10
10
  {{INCLUDE:_common-contract.md}}
11
- - Primary focus areas:
12
- - requirement coverage
13
- - whether delivered config files and deployment manifests satisfy the recorded expected values
14
- - unresolved edge cases
15
- - regression risk
16
- - documentation or rollout gaps
17
- - reasons the change may still fail in production
18
- - Expected output emphasis:
19
- - acceptance blockers
20
- - residual risk
21
- - final release recommendations
11
+ - Primary focus areas (each maps to a deliverable section below):
12
+ - Acceptance-gating — a failure here pushes the verdict toward `blocked` / `conditional-accept`:
13
+ - requirement & acceptance coverage every must-pass point in the brief's `## Acceptance Criteria` (and the approved plan's requirements) is covered with a cited artifact or raised as an Acceptance Blocker; no silent omissions
14
+ - delivered artifacts match recorded expected values in `reference-expectations` (config files, deployment manifests, other recorded expected states); when reference-expectations are absent, record it as missing information rather than assuming a match
15
+ - test & validation suite pass status — independently re-run the read-only two-tier command set (Tier 1 = brief/approved-plan `validation`, Tier 2 = `project.json` `qaCommands`) and confirm each passes on the verified head, citing exact command + exit code
16
+ - test correctness — delivered tests actually assert the intended behaviour: no gutted/weakened assertions, no tautological or always-passing tests, no tests exercising only mocks; new behaviour has matching coverage
17
+ - no new defects introduced — the diff does not break previously-working behaviour and adds no new bug (logic/off-by-one, null/empty handling, resource leaks, broken error paths)
18
+ - scope conformance — the delivered diff stays within the approved plan's scope; flag out-of-scope edits, unrelated file changes, leftover debug/commented-out code, and unintended deletions
19
+ - Residual-tracked — note as Residual Risk unless severe enough to block:
20
+ - unresolved edge cases
21
+ - regression risk in adjacent code paths not directly changed
22
+ - documentation or rollout gaps
23
+ - production-specific failure modes not caught by tests (env/config drift across stages, secrets & permission/auth changes, migration ordering & rollback executability, observability gaps)
22
24
  - Pre-verification entry gate (mandatory — refuse to start if any item fails):
23
- - the task brief MUST cite the originating `implementation` final-report path under `## Source Implementation Report`. The lead opens that file and confirms it includes `Plan link & approval evidence`, `Commit list`, `Diff summary`, `Validation evidence`, and `Routing recommendation for final-verification`.
25
+ - the task brief MUST cite the originating `implementation` final-report path under `## Source Implementation Report`. The lead opens that file and confirms it includes the approved-plan reference (heading `Approved Plan Reference` or `Plan link & approval evidence`), `Commit list`, `Diff summary`, `Validation evidence`, and `Routing recommendation for final-verification`.
24
26
  - the task brief MUST identify the worktree / checkout under verification and the implementation base ref. If the implementation report names a task worktree, final-verification MUST inspect that same worktree rather than the caller's original checkout.
25
27
  - the lead MUST capture `git rev-parse HEAD`, `git status --short`, and `git diff --stat <implementation-base>..HEAD` from the verification worktree before dispatching workers. These values are the verification target and must be cited in the final report.
26
28
  - if the cited implementation report is missing, lacks commits for delivered code changes, or the current checkout does not match the implementation report's commit list / diff summary, the run MUST end with status `blocked` and route back to `implementation` or `implementation-planning` rather than verifying an ambiguous target.
27
29
  - Required deliverable shape (final report, in addition to the standard sections):
28
- - **Source Implementation Report**: relative path of the originating `implementation` final-report file, the quoted commit list / diff summary used as the verification target, the worktree path inspected, and the base/head SHAs captured at run start.
29
- - **Verdict vocabulary**: Section 2 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed.
30
+ - **Source Implementation Report**: relative path of the originating `implementation` final-report file, the quoted commit list / diff summary used as the verification target, the worktree path inspected, and the base/head SHAs captured at run start. The lead injects this same target snapshot into every analyser prompt (`**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` rather than verify an ambiguous target.
31
+ - **Verdict vocabulary**: Section 2 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is `accepted`.
30
32
  - **Acceptance Blockers block** (under section 4): one row per blocker with `id`, `severity` (`critical` / `major` / `minor`), evidence (file path, log excerpt, or test output), and the recommended follow-up phase (`error-analysis` or `implementation-planning`). Empty block is acceptable and preferred — render the single line `- No acceptance blockers found.`
31
33
  - **Residual Risk block** (under section 4): risks that are not blockers but should be tracked, each with mitigation owner and a trigger that would escalate them to a blocker.
32
34
  - **Validation Evidence**: for every requirement in the originating plan or task brief, cite the artifact (commit SHA, test output, log line, MCP SELECT result) that demonstrates coverage. Paraphrased "verified" claims without an artifact are rejected.
33
35
  - **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.
34
36
  - **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>`.
35
- - **Routing recommendation**: brief note on the next safe phase (`done`, `error-analysis`, `implementation-planning`) tied to the verdict and blocker list.
37
+ - **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`.
36
38
  - Clarification request policy (phase-specific addendum — shared policy is in `_common-contract.md`):
37
39
  - populate `## 5. 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
38
40
  - Self-review pass before finalising the report (`Claude lead` runs this; do not delegate to a generic subagent):
@@ -40,7 +42,7 @@
40
42
  2. **Blocker traceability** — every blocker cites a concrete artifact (file:line, log excerpt, test exit code, MCP SELECT). Blockers without evidence are demoted to residual risk or removed.
41
43
  3. **Coverage check** — every requirement in the originating plan/task brief is either marked covered (with artifact) or listed as a blocker. No silent omissions.
42
44
  4. **Verifier dissent preserved** — if workers reach different verdicts, the disagreement is visible in section 1.2; synthesis hides nothing.
43
- 5. **No-mutation audit** — scan the run's session transcripts for any Edit / Write / mutating Bash command. Any occurrence means the run has crossed into implementation and MUST be re-routed; do NOT silently strip the evidence.
45
+ 5. **No source-mutation audit** — scan the run's session transcripts for Edit / Write or state-mutating Bash commands that touch paths OUTSIDE `<PROJECT_ROOT>/.okstra/**` and outside the assigned run-artifact paths. Writes to worker prompts, audit sidecars, team-state, the final-report `data.json`, and rendered reports under the run directory are allowed okstra artifacts. Any source/schema/deployment mutation means the run has crossed into implementation and MUST be re-routed; do NOT silently strip the evidence.
44
46
  - Non-goals:
45
47
  - proposing unrelated refactors beyond the delivered scope
46
48
  - **source code edits, follow-up bug fixes, or scope expansion** — this run renders a verdict only; defects detected here become inputs to a new `error-analysis` or `implementation-planning` run
@@ -51,6 +51,7 @@
51
51
  - The final report MUST include section headings containing each of the following exact strings: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stage Exit Contract`, `Stage Validation`, `Dependency`, `Validation Checklist`, `Rollback`. (Approval is no longer a body section — it is the YAML frontmatter `approved` field.)
52
52
  - Korean translations are allowed in parentheses (e.g. `### Recommended Option (권장 옵션)`), but the English keyword must be present verbatim in the heading line.
53
53
  - The shape and ordering follow `final-report-template.md` section 4.5 (`Implementation Plan Deliverables`). Do NOT translate the heading keywords — `validators/validate-run.py` does substring matching on the raw report text and 7-of-8 missing strings is a real, repeatedly observed failure mode (root cause: writer translated the headings to Korean).
54
+ - Beyond substring matching, when the Plan Body Verification gate result is `passed` / `passed-with-dissent`, `validators/validate-run.py` runs the **structural** Stage Map validator (`validators/validate-implementation-plan-stages.py`) at the planning boundary — the exact `## 4.5 Stage Map` heading, each `## 4.5.<i> Stage <i>:` section with its four required subsections, the per-stage effective step count (≤6), and the `depends-on` DAG are all enforced here, not deferred to the `implementation` entry gate.
54
55
  - Required deliverable shape (final report, in addition to the standard sections):
55
56
  - at least two implementation options. **Each option must include**:
56
57
  - **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`.
@@ -14,12 +14,27 @@
14
14
  - `intent-inference` augmentations whose paired `intent-check:` row carries `[CONFIRMED …]` are treated as **confirmed**; trust the confirmation text in `## Reporter Confirmations` over the original inference if they differ. Unconfirmed `intent-inference` rows under `reporter-confirmations: skipped` follow the precondition's `skipped` branch above.
15
15
  - `conversion-block:` rows are explicit "translation failed" signals — never attempt to resolve them by inference here; the precondition above already handled them.
16
16
  - Primary focus areas:
17
- - classify the work as bugfix, feature, improvement, refactor, or ops-change
17
+ - classify the work as bugfix, feature, improvement, refactor, or ops
18
18
  - determine whether `error-analysis` or `implementation-planning` is the next safe step; direct `implementation` handoff is not a valid routing target because implementation requires an approved `implementation-planning` report
19
19
  - identify missing materials that block reliable routing
20
20
  - define task continuity expectations for long-running work under the same task key
21
21
  - capture approval or confirmation points before the next phase starts
22
22
  - **domain alignment check**: read `<PROJECT_ROOT>/.okstra/glossary.md` and `<PROJECT_ROOT>/.okstra/decisions/` titles if present. Absent okstra memory files are normal — do not error. Validate that every `terminology:*` entry under the brief's `Open Questions` has a canonical resolution before routing. Fuzzy or overloaded terms in the brief MUST be resolved to a single canonical term in this phase.
23
+ - Fan-out (multi-item / multi-domain requests only):
24
+ - 단일 항목/단일 도메인 요청은 fan-out 하지 않는다(현행 단일 라우팅 보존).
25
+ - 요청이 2개 이상 도메인에 걸치거나 독립 착수 가능한 작업 항목이 2개 이상이면,
26
+ 각 항목을 `runs/requirements-discovery/fan-out/unit-<NNN>.md` packet 으로 발행한다.
27
+ packet 은 `templates/reports/fan-out-unit.template.md` 형식을 따르며 frontmatter
28
+ `domain`(work-category 5-enum: bugfix / feature / refactor / ops / improvement),
29
+ `depends-on`(같은 fan-out 내 unit-id 의 inline 리스트 `[unit-001]`, 없으면 `[]`),
30
+ `recommended-next-phase`(error-analysis | implementation-planning)를 채운다.
31
+ - `runs/requirements-discovery/fan-out/index.md` 에 packet 을 depends-on 위상순서로
32
+ 번호목록(`1. unit-001`)으로 나열한다(생성 뷰; 직접 편집 금지 명시). depends-on 그래프는
33
+ DAG 여야 한다 — `validate_fanout` 가 순환을 검증 실패로 거부하므로, 순환이 생기면
34
+ packet 을 finalize 하기 전에 문제 의존을 끊어 DAG 로 만든다.
35
+ - 최종 리포트에는 분해 결과를 중복하지 말고 "fan-out: N packets → fan-out/index.md" 한 줄
36
+ 포인터만 둔다. packet 실행은 별도다: 사용자가 `okstra-run --task-brief <packet 경로>` 로
37
+ 각 단위를 새 task-key 로 시작한다(이 phase 는 다운스트림 run 을 직접 시작하지 않는다).
23
38
  - Decision-tree walk (bounded):
24
39
  - When the brief's `Desired Outcome`, classification, or routing target depends on a chain of decisions, walk that chain one branch at a time. Each branch is one `Clarification Items` row, not a free-form interview.
25
40
  - For every clarification row, put the single best answer and one-line rationale in `Expected form` as `Recommended: ...`. Put other options and one-sentence consequences in the same cell as `Alternatives: ...`.
@@ -40,3 +55,5 @@
40
55
  - full implementation design unless it is required to decide the next phase
41
56
  - **source code edits, plan authoring, builds, or deployments** — this run only classifies the work and routes it; deeper analysis and planning belong to subsequent phases
42
57
  - **writes outside `<PROJECT_ROOT>/.okstra/`** — this phase only uses okstra's artifact root. Glossary additions land in `<PROJECT_ROOT>/.okstra/glossary.md` (via `okstra-brief` Step 4.5); decision drafts land in `<PROJECT_ROOT>/.okstra/decisions/` (via `implementation-planning`).
58
+ - 작업 단위 분해(fan-out)는 이 phase 의 in-scope 다 — 단, 각 단위의 *해법 설계*·소스
59
+ 편집·plan 작성은 여전히 non-goal 이며 다운스트림 phase 가 담당한다
@@ -118,6 +118,17 @@
118
118
  "__free_input__": "직접 입력"
119
119
  }
120
120
  },
121
+ "branch_confirm": {
122
+ "label": "{summary}",
123
+ "labels": {
124
+ "new": "새 브랜치 `{branch}` 를 base-ref `{base_ref}` 에서 새 worktree(`{path}`)에 생성합니다 — 진행할까요?",
125
+ "reuse": "현재 worktree(`{path}`, 브랜치 `{branch}`)를 재사용합니다 — 진행할까요?",
126
+ "in_worktree": "현재 worktree(`{path}`)에서 그대로 진행합니다(이미 non-main worktree) — 진행할까요?",
127
+ "not_git": "git 저장소가 아니므로 `{path}` 에서 직접 진행합니다 — 진행할까요?"
128
+ },
129
+ "options": { "proceed": "진행", "edit": "base-ref 다시 고르기" },
130
+ "echo_template": "branch-confirm: {value}"
131
+ },
121
132
  "base_ref_text": {
122
133
  "label": "base ref 를 입력해주세요 (branch, tag, 또는 short/full SHA)",
123
134
  "echo_template": "base-ref: {value}"
@@ -36,7 +36,7 @@ def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
36
36
  status: str, **fields: Any) -> None:
37
37
  if status not in ("started", "done"):
38
38
  raise ValueError(f"status must be 'started' or 'done', got: {status!r}")
39
- with consumers_mutex(plan_run_root.name):
39
+ with consumers_mutex(plan_run_root):
40
40
  existing = read_consumers(plan_run_root)
41
41
  for row in existing:
42
42
  if (row.get("impl_task_key") == impl_task_key
@@ -0,0 +1,35 @@
1
+ """fan-out unit 의존 그래프: 위상정렬 + 순환검출 (Kahn, id 사전순 결정적)."""
2
+ from __future__ import annotations
3
+
4
+
5
+ class CycleError(ValueError):
6
+ """depends-on 그래프에 순환이 있을 때."""
7
+
8
+
9
+ def topological_order(units: dict[str, list[str]]) -> list[str]:
10
+ """unit-id 를 착수 가능한 순서(의존 먼저)로 반환.
11
+
12
+ units: unit-id -> 그 unit 이 의존하는 unit-id 목록.
13
+ 순환이면 CycleError, 알 수 없는 의존이면 ValueError.
14
+ """
15
+ indeg = {u: 0 for u in units}
16
+ for u, deps in units.items():
17
+ for d in deps:
18
+ if d not in units:
19
+ raise ValueError(f"unit {u!r} depends on unknown unit {d!r}")
20
+ indeg[u] += 1
21
+ queue = sorted(u for u, n in indeg.items() if n == 0)
22
+ order: list[str] = []
23
+ while queue:
24
+ u = queue.pop(0)
25
+ order.append(u)
26
+ for v, deps in units.items():
27
+ if u in deps:
28
+ indeg[v] -= 1
29
+ if indeg[v] == 0:
30
+ queue.append(v)
31
+ queue.sort()
32
+ if len(order) != len(units):
33
+ remaining = sorted(set(units) - set(order))
34
+ raise CycleError(f"dependency cycle among units: {remaining}")
35
+ return order
@@ -6,13 +6,11 @@
6
6
  - `prepare_migration_plan(project_root)` 는 부수효과 없이 변경 항목만 수집.
7
7
  - `apply_migration_plan(plan, *, dry_run=True)` 가 실제 실행. dry-run 이 default.
8
8
  - 가드: `.okstra/` 이미 존재 / `.project-docs/okstra/` 없음 / git 미사용 fallback.
9
- - 다음 다섯 가지 변경만 수행 (그 외 파일은 절대 건드리지 않음):
9
+ - 다음 가지 변경만 수행 (그 외 파일은 절대 건드리지 않음):
10
10
  1. `git mv .project-docs/okstra .okstra` (git 없으면 일반 `mv`).
11
11
  2. `.project-docs/` 가 비면 `rmdir .project-docs`.
12
- 3. `<PROJECT_ROOT>/CLAUDE.md` 의 `@.project-docs/okstra/CLAUDE.md` import
13
- 라인을 `@.okstra/CLAUDE.md` 교체.
14
- 4. `.gitignore` 의 `.project-docs/okstra/` 항목을 `.okstra/` 로 교체.
15
- 5. `~/.okstra/{recent,active}.jsonl` 와 `~/.okstra/worktrees/registry.json`
12
+ 3. `.gitignore` 의 `.project-docs/okstra/` 항목을 `.okstra/` 로 교체.
13
+ 4. `~/.okstra/{recent,active}.jsonl` `~/.okstra/worktrees/registry.json`
16
14
  의 해당 프로젝트 path 항목 갱신 (project_root 가 정확히 일치하는 row 만).
17
15
  """
18
16
  from __future__ import annotations
@@ -26,8 +24,6 @@ from pathlib import Path
26
24
  from typing import Optional
27
25
 
28
26
  from okstra_project.dirs import (
29
- CLAUDE_MD_IMPORT_LINE,
30
- LEGACY_CLAUDE_MD_IMPORT_LINE,
31
27
  LEGACY_OKSTRA_DIR_NAME,
32
28
  LEGACY_OKSTRA_RELATIVE,
33
29
  OKSTRA_DIR_NAME,
@@ -51,7 +47,6 @@ class MigrationPlan:
51
47
  target_dir: Path # <project>/.okstra
52
48
  use_git: bool
53
49
  remove_empty_parent: bool # True if .project-docs would be empty after move
54
- claude_md_path: Optional[Path] # <project>/CLAUDE.md if import-line update needed
55
50
  gitignore_path: Optional[Path] # <project>/.gitignore if entry update needed
56
51
  registry_updates: list[dict] = field(default_factory=list)
57
52
  # Each registry entry: {"file": "<abs path>", "row_count": N, "scope": "..."}
@@ -63,7 +58,6 @@ class MigrationPlan:
63
58
  "targetDir": str(self.target_dir),
64
59
  "useGit": self.use_git,
65
60
  "removeEmptyParent": self.remove_empty_parent,
66
- "claudeMdPath": str(self.claude_md_path) if self.claude_md_path else None,
67
61
  "gitignorePath": str(self.gitignore_path) if self.gitignore_path else None,
68
62
  "registryUpdates": self.registry_updates,
69
63
  }
@@ -78,7 +72,6 @@ class MigrationResult:
78
72
  dry_run: bool
79
73
  moved: bool
80
74
  parent_removed: bool
81
- claude_md_updated: bool
82
75
  gitignore_updated: bool
83
76
  registry_rows_updated: int
84
77
 
@@ -87,7 +80,6 @@ class MigrationResult:
87
80
  "dryRun": self.dry_run,
88
81
  "moved": self.moved,
89
82
  "parentRemoved": self.parent_removed,
90
- "claudeMdUpdated": self.claude_md_updated,
91
83
  "gitignoreUpdated": self.gitignore_updated,
92
84
  "registryRowsUpdated": self.registry_rows_updated,
93
85
  }
@@ -124,16 +116,6 @@ def prepare_migration_plan(
124
116
  parent = source.parent # <project>/.project-docs
125
117
  remove_empty_parent = _would_be_empty_after_remove(parent, source)
126
118
 
127
- claude_md = project_root / "CLAUDE.md"
128
- claude_md_path: Optional[Path] = None
129
- if claude_md.is_file():
130
- try:
131
- text = claude_md.read_text(encoding="utf-8")
132
- except OSError:
133
- text = ""
134
- if LEGACY_CLAUDE_MD_IMPORT_LINE in text:
135
- claude_md_path = claude_md
136
-
137
119
  gitignore = project_root / ".gitignore"
138
120
  gitignore_path: Optional[Path] = None
139
121
  if gitignore.is_file():
@@ -154,7 +136,6 @@ def prepare_migration_plan(
154
136
  target_dir=target,
155
137
  use_git=use_git,
156
138
  remove_empty_parent=remove_empty_parent,
157
- claude_md_path=claude_md_path,
158
139
  gitignore_path=gitignore_path,
159
140
  registry_updates=registry_updates,
160
141
  )
@@ -179,14 +160,12 @@ def apply_migration_plan(
179
160
  dry_run=True,
180
161
  moved=False,
181
162
  parent_removed=False,
182
- claude_md_updated=False,
183
163
  gitignore_updated=False,
184
164
  registry_rows_updated=0,
185
165
  )
186
166
 
187
167
  _do_move(plan)
188
168
  parent_removed = _maybe_remove_empty_parent(plan)
189
- claude_md_updated = _update_claude_md(plan)
190
169
  gitignore_updated = _update_gitignore(plan)
191
170
  registry_rows_updated = _apply_registry_updates(
192
171
  plan, okstra_home=_resolve_okstra_home(okstra_home)
@@ -196,7 +175,6 @@ def apply_migration_plan(
196
175
  dry_run=False,
197
176
  moved=True,
198
177
  parent_removed=parent_removed,
199
- claude_md_updated=claude_md_updated,
200
178
  gitignore_updated=gitignore_updated,
201
179
  registry_rows_updated=registry_rows_updated,
202
180
  )
@@ -272,17 +250,6 @@ def _maybe_remove_empty_parent(plan: MigrationPlan) -> bool:
272
250
  return False
273
251
 
274
252
 
275
- def _update_claude_md(plan: MigrationPlan) -> bool:
276
- if plan.claude_md_path is None:
277
- return False
278
- text = plan.claude_md_path.read_text(encoding="utf-8")
279
- new_text = text.replace(LEGACY_CLAUDE_MD_IMPORT_LINE, CLAUDE_MD_IMPORT_LINE)
280
- if new_text == text:
281
- return False
282
- plan.claude_md_path.write_text(new_text, encoding="utf-8")
283
- return True
284
-
285
-
286
253
  def _update_gitignore(plan: MigrationPlan) -> bool:
287
254
  if plan.gitignore_path is None:
288
255
  return False
@@ -307,6 +274,16 @@ def _update_gitignore(plan: MigrationPlan) -> bool:
307
274
  return True
308
275
 
309
276
 
277
+ def _mentions_project_path(haystack: str, project_str: str) -> bool:
278
+ """haystack 이 project_root 를 '경로로서' 언급하는지.
279
+
280
+ `project_str in haystack` 단순 substring 은 `/x/app` 가 `/x/app-v2/...` 에도
281
+ 매칭되어 형제 prefix 프로젝트의 행을 잘못 잡는다. 경로 구분자(`/`)나 JSON
282
+ 닫는 따옴표(`"`) 같은 경계가 뒤따를 때만 매칭해 prefix 오인을 막는다.
283
+ """
284
+ return f"{project_str}/" in haystack or f'{project_str}"' in haystack
285
+
286
+
310
287
  def _scan_registries(project_root: Path, *, okstra_home: Path) -> list[dict]:
311
288
  """Find okstra-home registry files that reference this project's old path."""
312
289
  project_str = str(project_root)
@@ -321,7 +298,9 @@ def _scan_registries(project_root: Path, *, okstra_home: Path) -> list[dict]:
321
298
  except OSError:
322
299
  continue
323
300
  count = sum(
324
- 1 for ln in lines if legacy_marker in ln or project_str in ln and ".project-docs/okstra" in ln
301
+ 1 for ln in lines
302
+ if legacy_marker in ln
303
+ or (_mentions_project_path(ln, project_str) and LEGACY_OKSTRA_DIR_NAME in ln)
325
304
  )
326
305
  if count > 0:
327
306
  updates.append({"file": str(path), "rowCount": count, "scope": name})
@@ -331,8 +310,8 @@ def _scan_registries(project_root: Path, *, okstra_home: Path) -> list[dict]:
331
310
  text = registry.read_text(encoding="utf-8")
332
311
  except OSError:
333
312
  text = ""
334
- if ".project-docs/okstra" in text and project_str in text:
335
- updates.append({"file": str(registry), "rowCount": text.count(".project-docs/okstra"), "scope": "worktrees-registry"})
313
+ if LEGACY_OKSTRA_DIR_NAME in text and _mentions_project_path(text, project_str):
314
+ updates.append({"file": str(registry), "rowCount": text.count(LEGACY_OKSTRA_DIR_NAME), "scope": "worktrees-registry"})
336
315
  return updates
337
316
 
338
317
 
@@ -354,7 +333,7 @@ def _apply_registry_updates(plan: MigrationPlan, *, okstra_home: Path) -> int:
354
333
  new_lines: list[str] = []
355
334
  changed = 0
356
335
  for line in text.splitlines(keepends=True):
357
- if project_str in line and LEGACY_OKSTRA_DIR_NAME in line:
336
+ if _mentions_project_path(line, project_str) and LEGACY_OKSTRA_DIR_NAME in line:
358
337
  new_lines.append(line.replace(LEGACY_OKSTRA_DIR_NAME, OKSTRA_DIR_NAME))
359
338
  changed += 1
360
339
  else:
@@ -372,7 +351,7 @@ def _apply_registry_updates(plan: MigrationPlan, *, okstra_home: Path) -> int:
372
351
  # otherwise leave it alone (covers the case where registry.json
373
352
  # contains an unrelated `.project-docs/okstra` literal in someone
374
353
  # else's row).
375
- if project_str in text and LEGACY_OKSTRA_DIR_NAME in text:
354
+ if _mentions_project_path(text, project_str) and LEGACY_OKSTRA_DIR_NAME in text:
376
355
  new_text = _replace_in_project_rows(text, project_str)
377
356
  if new_text != text:
378
357
  registry.write_text(new_text, encoding="utf-8")
@@ -397,7 +376,7 @@ def _replace_in_project_rows(text: str, project_str: str) -> str:
397
376
  changed = False
398
377
  for key, row in data.items():
399
378
  row_text = json.dumps(row)
400
- if project_str not in row_text or LEGACY_OKSTRA_DIR_NAME not in row_text:
379
+ if not _mentions_project_path(row_text, project_str) or LEGACY_OKSTRA_DIR_NAME not in row_text:
401
380
  continue
402
381
  new_row_text = row_text.replace(LEGACY_OKSTRA_DIR_NAME, OKSTRA_DIR_NAME)
403
382
  if new_row_text != row_text:
@@ -93,7 +93,7 @@ def _reconcile_active_locked(home: Path, *,
93
93
  abort_after_seconds: int,
94
94
  project: Optional[str] = None) -> dict:
95
95
  active = home / "active.jsonl"
96
- summary = {"completed": 0, "aborted": 0, "running": 0}
96
+ summary = {"completed": 0, "failed": 0, "aborted": 0, "running": 0}
97
97
  rows = read_jsonl(active)
98
98
  if not rows:
99
99
  return summary
@@ -128,7 +128,7 @@ def _reconcile_active_locked(home: Path, *,
128
128
  if row["status"] == "completed":
129
129
  summary["completed"] += 1
130
130
  else:
131
- summary["completed"] += 0 # 실패도 종결로 분류하지만 카운터는 분리
131
+ summary["failed"] += 1
132
132
  continue
133
133
  started = _parse_iso(row.get("startedAt", ""))
134
134
  if started and (now - started).total_seconds() > abort_after_seconds:
@@ -33,7 +33,6 @@ from typing import Any
33
33
  # downstream ``from jinja2 import ...`` resolves to the vendored copy.
34
34
  import okstra_vendor # noqa: F401 — side effect: sys.modules aliases
35
35
  from jinja2 import ChainableUndefined, Environment, FileSystemLoader
36
- from jinja2 import select_autoescape # noqa: F401 — kept for future HTML use
37
36
 
38
37
  from okstra_ctl.i18n import I18nError, SUPPORTED_LANGS, load_dictionary, make_jinja_global
39
38
 
@@ -54,12 +54,8 @@ from .render import (
54
54
  )
55
55
  from .run_context import compute_and_write_run_context, write_run_inputs
56
56
  from .seeding import (
57
- AgentsMdLinkError,
58
- ClaudeMdLinkError,
59
57
  SettingsLinkError,
60
58
  cleanup_obsolete_generated_docs,
61
- ensure_project_agents_md,
62
- ensure_project_claude_md,
63
59
  ensure_project_settings_symlink,
64
60
  installed_version,
65
61
  verify_installation,
@@ -1046,31 +1042,6 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
1046
1042
  file=__import__("sys").stderr,
1047
1043
  )
1048
1044
 
1049
- try:
1050
- claude_md_link = ensure_project_claude_md(project_root=Path(inp.project_root))
1051
- except ClaudeMdLinkError as exc:
1052
- print(
1053
- f"okstra-claude-md: failed to provision project CLAUDE.md import — "
1054
- f"Claude Code sessions in this project will not auto-load okstra guidance. ({exc})",
1055
- file=__import__("sys").stderr,
1056
- )
1057
- else:
1058
- if claude_md_link is None:
1059
- print(
1060
- "okstra-claude-md: ~/.okstra/templates/okstra.CLAUDE.md missing — "
1061
- "re-run 'npx okstra@latest install' to provision the symlink target.",
1062
- file=__import__("sys").stderr,
1063
- )
1064
-
1065
- try:
1066
- ensure_project_agents_md(project_root=Path(inp.project_root))
1067
- except AgentsMdLinkError as exc:
1068
- print(
1069
- f"okstra-agents-md: failed to provision <PROJECT>/AGENTS.md symlink — "
1070
- f"codex / aider sessions in this project will not auto-load okstra guidance. ({exc})",
1071
- file=__import__("sys").stderr,
1072
- )
1073
-
1074
1045
  return PrepareOutputs(
1075
1046
  ctx=ctx,
1076
1047
  prompt_text=prompt_text,
@@ -50,15 +50,6 @@ def _task_lock_path(task_key: str) -> Path:
50
50
  return locks / f"{safe}.lock"
51
51
 
52
52
 
53
- def _consumers_lock_path(plan_task_key: str) -> Path:
54
- """plan-task-key 별 consumers.jsonl append mutex."""
55
- home = _okstra_home()
56
- locks = home / ".locks"
57
- locks.mkdir(parents=True, exist_ok=True)
58
- safe = plan_task_key.replace("/", "_").replace(":", "_")
59
- return locks / f"{safe}.consumers.lock"
60
-
61
-
62
53
  @contextmanager
63
54
  def task_mutex(task_key: str) -> Iterator[None]:
64
55
  """task-key per-process mutex. 동시 호출은 락 안에서 직렬화된다."""
@@ -73,9 +64,15 @@ def task_mutex(task_key: str) -> Iterator[None]:
73
64
 
74
65
 
75
66
  @contextmanager
76
- def consumers_mutex(plan_task_key: str) -> Iterator[None]:
77
- """plan-task-key 별 consumers.jsonl append mutex."""
78
- path = _consumers_lock_path(plan_task_key)
67
+ def consumers_mutex(plan_run_root: Path) -> Iterator[None]:
68
+ """plan run-root 별 consumers.jsonl append mutex.
69
+
70
+ lock 은 consumers.jsonl 과 같은 디렉토리에 두어 run-root 마다 1:1 로
71
+ 격리한다. 마지막 경로 세그먼트(예: seq ``001``)만 키로 쓰면 서로 다른
72
+ task/project 의 동일 seq run 이 같은 lock 을 공유하므로 금지.
73
+ """
74
+ plan_run_root.mkdir(parents=True, exist_ok=True)
75
+ path = plan_run_root / ".consumers.lock"
79
76
  path.touch(exist_ok=True)
80
77
  with path.open("r+") as f:
81
78
  fcntl.flock(f.fileno(), fcntl.LOCK_EX)