okstra 0.88.1 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.88.1",
3
+ "version": "0.89.0",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.88.1",
3
- "builtAt": "2026-06-17T05:43:28.195Z",
2
+ "package": "0.89.0",
3
+ "builtAt": "2026-06-17T10:43:53.695Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -184,7 +184,43 @@ fabricate a `timeout` status against a missing artifact.
184
184
 
185
185
  ---
186
186
 
187
+ ## 항목 X1 — cross-repo carry packet 소비 자동화 (okstra-brief) [미착수, 2026-06-17]
188
+
189
+ ### 배경
190
+
191
+ cross-repo 작업은 구조적으로 repo 별 별도 okstra run 이 필수다(worktree 단일 repo 바인딩 · run-index/registry 단일 project 키 · 편집 allowlist 가 해당 트리로 제한). **생산자 계약은 이미 구현됨** — implementation-planning 계획서가 `## Cross-Repo Carry — <repo>` 부록을 세 하위 섹션(Preconditions / Work for B / How to start)으로 방출한다([prompts/profiles/implementation-planning.md](../prompts/profiles/implementation-planning.md), 커밋 `cfd7c2c`).
192
+
193
+ **소비자 쪽은 현재 수동**이다: B repo 에서 `okstra-brief` → variant `Reporter input` → source `File` 로 A 계획서 절대경로를 붙여넣으면 **파일 전체가 verbatim 으로** Source Material 에 들어가([skills/okstra-brief/SKILL.md:147](../skills/okstra-brief/SKILL.md), [SKILL.md:162-164](../skills/okstra-brief/SKILL.md)), B 의 planning 이 A 자신의 stage·decision 노이즈에서 "B 가 할 일 / precondition" 을 직접 골라내야 한다.
194
+
195
+ ### 목표
196
+
197
+ B 의 `okstra-brief` 가 A 계획서에서 **`## Cross-Repo Carry — <repo>` 섹션만 추출**해 구조화 시드: `Work for B` → B 의 requirements(R-ID), `Preconditions` → B 의 외부 의존 + 관측 확인 신호(구현 대상 아님), recognition caveat 동반.
198
+
199
+ ### 변경 대상 파일
200
+
201
+ - [skills/okstra-brief/SKILL.md](../skills/okstra-brief/SKILL.md) — 신규 brief variant(또는 `File` 소스가 `## Cross-Repo Carry` 섹션을 감지하면 자동 분기) + 섹션 추출 단계.
202
+ - (선택) `## Cross-Repo Carry — <repo>` 파싱 헬퍼 — 순수 마크다운 파싱. 스킬·CLI 가 같이 쓰면 단일 참조점으로 `scripts/okstra_ctl/` Python 모듈에 둔다.
203
+
204
+ ### 결정해야 할 것
205
+
206
+ 1. 신규 brief variant 로 노출할지 vs `File` 소스 자동 감지로 처리할지.
207
+ 2. A 계획서를 수동 경로 입력 vs 최근 run 자동 추천(`okstra task-list`)으로 찾을지.
208
+ 3. `Preconditions` 를 B brief 의 어느 섹션(Open Questions / 외부 의존 전용 섹션)으로 매핑할지.
209
+
210
+ ### 불변 제약 (자동화해도 반드시 유지)
211
+
212
+ - B 는 자기 권한·worktree 로 setup/brief/run 을 실행 — 자동화가 "A 가 B 에 쓰기" 를 허용하면 안 됨.
213
+ - 경계를 넘는 읽기는 B 가 A 경로를 **명시 인용**하는 형태만(artifact-home, [SKILL.md:90-92](../skills/okstra-brief/SKILL.md)).
214
+ - A 의 done-state 는 B 게이트가 자동 인정하지 않음 — 자동화는 carry 를 구조화할 뿐 B 의 독립성은 그대로.
215
+
216
+ ### 결정
217
+
218
+ 2026-06-17 — 사용자가 당분간 수동 흐름 유지를 선택. 자동화는 본 항목으로 보류. 진행 전 brainstorming 으로 위 3개 결정 사항을 확정한다.
219
+
220
+ ---
221
+
187
222
  ## 변경 이력
188
223
 
189
224
  - 2026-05-03 — 작성. 수정 A(Claude worker Stop Condition) 동시 머지에 따른 후속 항목 기록.
190
225
  - 2026-06-10 — 수정 B(Leader-side 워커 soft timeout) 및 수정 C 후보(Phase 4 background polling / worker-results 파일 폴링 루프)를 self-scheduled polling 작업으로 해소 표기. 정본: okstra-team-contract "Worker-completion detection (self-scheduled polling)" 섹션, 설계: docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md.
226
+ - 2026-06-17 — 항목 X1(cross-repo carry packet 소비 자동화) 기록. 생산자 계약은 커밋 `cfd7c2c` 로 구현됨, 소비자(okstra-brief 자동 추출)는 사용자 선택으로 보류.
@@ -208,7 +208,7 @@ These phases are governed by [team-contract](./team-contract.md). It is the cano
208
208
 
209
209
  0. **Surface the Teams tools first (deferred-tool environments) — BLOCKING.** `TeamCreate` / `TeamDelete` / `SendMessage` / `TaskList` are commonly **deferred tools**: present by name but not callable until their schemas are loaded via ToolSearch. Before step 1, load them with the explicit selector form `ToolSearch("select:TeamCreate,TeamDelete,SendMessage,TaskList")` — NOT a keyword query. Keyword-search ranking is non-deterministic while MCP servers are still (re)connecting (this is why the failure surfaces on `implementation` entry, after a context compaction re-injects the deferred-tool list mid-reconnect, far more than on earlier phases), so a keyword miss is NOT evidence the environment lacks Agent Teams. If the `select:` result comes back without `TeamCreate`, retry the same `select:` call ONCE. A non-empty result means the tool exists — proceed to step 1 and let the actual `TeamCreate` return value decide ok/error. Only when the explicit `select:` for `TeamCreate` is empty on BOTH attempts may you treat the environment as lacking Agent Teams support and go to step 5.
210
210
  1. Call `TeamCreate(team_name: ..., description: "Lead-plus-worker okstra run for <task-key>")`. The team name comes verbatim from the launch prompt's Team Creation Gate block — `okstra-<task-key>`, and implementation stage runs append `-s<N>` (stage isolation: a leftover team from another stage of the same task must not collide).
211
- 2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` AND the exact name as `teamName` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback, and `teamName` is the SSOT every later consumer (teardown, reconcile, token collector) reads.
211
+ 2. Record the `TeamCreate` outcome in team-state under `teamCreate: { attempted: true, status: "ok"|"error", error?: <message> }` AND the exact name as `teamName` before any dispatch. This is the audit trail that justifies a later no-`team_name` fallback. `teamName` is only an audit/display label — the harness never creates a `~/.claude/teams/<teamName>/` directory (the on-disk team is `session-<leadSessionId-prefix>`), so run-end teardown and reconcile resolve the real team from disk via `lead.sessionId`, NOT from `teamName` (see `_common-contract.md` "Run-end teammate teardown").
212
212
  2-1. If `TeamCreate` fails with "team already exists" (stale leftover from an earlier attempt): call `TeamList`; if the team is listed in this session, `TeamDelete` it and retry step 1 once. If it is NOT listed, do NOT remove `~/.claude/teams/...` / `~/.claude/tasks/...` with shell commands on your own initiative — that is destructive harness-internal state and `rm -rf` is commonly denied by user permission rules. Ask the user via AskUserQuestion (recommended option: quarantine); on approval, move both dirs into `~/.okstra/trash/<UTC-timestamp>/` with `mv` (reversible), then retry step 1 once.
213
213
  3. Verify `team-state.lead.sessionId` is populated. The `okstra.sh` exec path fills it automatically (`generate_claude_session_id` → `claude --session-id ...`). The render-only / in-session takeover path (`okstra-run` skill) auto-detects the live session's jsonl via `resolve_inproc_lead_session_id`, but the detector is best-effort and may return empty if `~/.claude/projects/<encoded-cwd>/` is unreadable or has no jsonl yet. If `lead.sessionId` is empty at this point, write the running session's id into team-state before proceeding — Phase 7 token-usage collection depends on it and will fail with `lead jsonl not found (sessionId=)` otherwise.
214
214
  4. If `TeamCreate` succeeds, proceed to Phase 4 (dispatch with `team_name`).
@@ -32,11 +32,14 @@ profile document.
32
32
  - Run-start pane recording (shared — runs ONCE at run start, before the FIRST worker dispatch):
33
33
  - The codex/antigravity wrappers now self-anchor their trace pane by walking their own ancestor PIDs against tmux `pane_pid`s (see `lib/okstra/tmux-pane.sh`), so they no longer depend on this file. The lead still records its own pane id here for the cleanup steps below (which-pane-to-never-kill) and as the "am I in tmux" gate. A bare `tmux display-message -p '#{pane_id}'` is NOT reliable for this — Claude Code's Bash tool strips `$TMUX`/`$TMUX_PANE`, so that command returns the most-recently-active *client's* pane (often a different session, or a foreign pane when the lead is launched outside tmux entirely). The lead therefore records via the same ancestry resolver.
34
34
  - The lead MUST run once, at run start: `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true` (substitute the run's absolute `RUN_DIR`). When the lead is not inside a tmux pane (e.g. Claude launched from the GUI app) no ancestor matches a pane, the file is empty, and every pane step below silently no-ops — that empty/absent file is the single signal that the lead is not in tmux.
35
- - This recording is **silent internal setup**: the lead MUST emit NOTHING to the user about it — no pane id, no `%NNN`, no `lead-pane.id` path, no announcement that the phase-start-reset / wrap-up-disposition steps "activate". Just record the file and proceed.
36
- - Phase-start pane reset (shared — runs BEFORE dispatching each new worker batch):
37
- - okstra creates two kinds of tmux pane per run: (a) **worker-agent panes** the harness gives to dispatched subagents (titled `claude-worker` / `codex-worker` / `antigravity-worker` / `report-writer-worker`), and (b) **trace panes** the codex/antigravity wrappers spawn (`<cli>-<role>-<pid>-tail`). Both accumulate across internal phases because each new phase dispatches a fresh worker batch and the prior panes are never reclaimed.
38
- - When `<RUN_DIR>/state/lead-pane.id` is non-empty (the lead is in tmux), the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` **immediately before** dispatching the next phase's workersi.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 this run, while NEVER killing the lead's own pane.
39
- - This is **automatic and silent** NO user prompt. Report it in one short, plain-language line (e.g. `이전 phase okstra pane 3개 정리`) and proceed never expose internal identifiers (`%NNN`, `lead-pane.id`, step names like "phase-start pane reset"). It is silent-skipped when the lead is not in tmux; the lead MUST NOT fabricate a synthetic pane list in that case.
35
+ - This recording is **silent internal setup**: the lead MUST emit NOTHING to the user about it — no pane id, no `%NNN`, no `lead-pane.id` path, no announcement that the phase-start-cleanup / wrap-up-disposition steps "activate". Just record the file and proceed.
36
+ - Phase-start cleanup prior-batch panes + completed teammates (shared — runs BEFORE dispatching each new worker batch, at the SAME boundaries: just before each `PROGRESS: phase-5.5-convergence round=<N>` marker and just before `PROGRESS: phase-6-synthesis dispatching report-writer-worker`):
37
+ - **(a) tmux panes.** okstra creates two kinds of tmux pane per run: **worker-agent panes** the harness gives to dispatched subagents (titled `claude-worker` / `codex-worker` / `antigravity-worker` / `report-writer-worker`), and **trace panes** the codex/antigravity wrappers spawn (`<cli>-<role>-<pid>-tail`). Both accumulate because each new batch spawns fresh panes and the prior ones are never reclaimed. When `<RUN_DIR>/state/lead-pane.id` is non-empty (the lead is in tmux), the lead MUST run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` immediately before dispatching the next batch. This closes every prior-batch okstra pane (worker-agent + trace) for this run, while NEVER killing the lead's own pane. Silent-skipped when the lead is not in tmux; the lead MUST NOT fabricate a synthetic pane list in that case.
38
+ - **(b) completed teammates (Teams mode only).** Each convergence round / critic / gap-verification / report-writer dispatch spawns teammates under fresh Agent `name`s (`<role>-reverify-r<N>`, `<provider>-worker-critic`, `report-writer-worker`, …); completed teammates do NOT leave the FleetView roster on their own — they linger as idle pills until run-end teardown, so a long run's roster grows unreadable. Before dispatching the next batch, the lead MUST send `SendMessage(to: <name>, message: { type: "shutdown_request" })` the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field to every teammate it spawned earlier in THIS run **whose completion is already confirmed** (its Result Path exists / it is in the self-scheduled-polling done set). This is the same graceful-shutdown primitive used at `Run-end teammate teardown`, applied per batch instead of once at the end.
39
+ - **Only confirmed-complete teammates, never the lead.** NEVER send a shutdown_request to a teammate whose result file has not yet appeared. In particular the critic runs CONCURRENTLY with the first reverify round (see `convergence` "Coverage critic pass"), so it MUST NOT be shut down until its own result is collected. NEVER target the lead's own session.
40
+ - **Do NOT call `TeamDelete` here.** The team stays alive for the remaining batches; only individual completed teammates are dismissed. `TeamDelete` remains a run-end-only step (`Run-end teammate teardown`).
41
+ - Silent-skip entirely in the no-`team_name` fallback (background subagents never join the roster) and in `tmux-pane` backend runs (external-lead workers are not session teammates).
42
+ - Both (a) and (b) are **automatic and silent** — NO user prompt. Report the pair in one short, plain-language line (e.g. `이전 배치 okstra pane 3개 · teammate 2명 정리`) and proceed — never expose internal identifiers (`%NNN`, `lead-pane.id`, raw Agent names, step names like "phase-start cleanup"). **Effect caveat:** Claude Code exposes no tool to surgically delete a roster entry mid-run, so a dismissed teammate may still show as an idle pill; the shutdown still reclaims its session and prevents the roster from growing with live members. On the FIRST batch of a run nothing has been dispatched yet — both steps no-op.
40
43
  - Phase wrap-up — okstra pane disposition (shared, runs AFTER Phase 7 persistence/token collection and BEFORE teammate teardown):
41
44
  - At run end the only residual okstra panes are the LAST phase's (e.g. the `report-writer-worker` agent pane and any codex/antigravity trace pane). `okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` returns one tab-separated `<pane_id>\t<pane_title>` line per residual okstra pane (worker-agent + trace) for this run.
42
45
  - When `<RUN_DIR>/state/lead-pane.id` is non-empty, 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 --run-dir "<RUN_DIR>"` exactly once. The output lists every residual okstra pane (worker-agent + trace) for this run, never the lead's own pane.
@@ -51,13 +54,15 @@ profile document.
51
54
  - This step is mandatory for every phase (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`). It is silent-skipped when `<RUN_DIR>/state/lead-pane.id` is empty/absent (lead running outside tmux); the lead MUST NOT fabricate a synthetic pane list in that case.
52
55
  - Run-end teammate teardown (shared — MUST run AFTER the pane disposition question and BEFORE `PROGRESS: complete`):
53
56
  - The lead created the worker team in Phase 3 (`TeamCreate` with the name recorded as team-state `teamName` — `okstra-<task-key>`, implementation stage runs `okstra-<task-key>-s<N>`). Worker teammates are NOT reclaimed on their own — without an explicit teardown they linger in the FleetView roster across this and later runs in the session. A lingering team also makes the next run of the same task-key collide on `TeamCreate` ("team already exists"), forcing the stale-team recovery path.
54
- - This step applies only when team-state's `teamCreate.status == "ok"` (Teams mode was actually used). In the no-`team_name` fallbackwhether `teamCreate.status` is `"error"` or `"skipped"` (`reason: "concurrent-run"`) there is no team to delete, so silent-skip.
57
+ - **Whether to run this step is decided by on-disk fact, NOT by `teamCreate.status` alone.** A mid-run context compaction can make the lead wrongly conclude Agent Teams is unavailable and record `teamCreate.status: "error"` even though `TeamCreate` had succeeded and workers joined so at run-end `status` is not a trustworthy "is there a team" signal. The harness names the team directory `session-<leadSessionId-prefix>` (NOT `okstra-<task-key>`; the `team_name` passed to `TeamCreate` is a logical label that never becomes a directory name). Resolve existence from disk: find the `~/.claude/teams/session-*/config.json` whose `leadSessionId` equals team-state `lead.sessionId`.
58
+ - **Matching on-disk config found, with members beyond the lead** → a real team exists; run the teardown sequence below regardless of what `teamCreate.status` says.
59
+ - **No matching on-disk config** → there is genuinely nothing to delete (the real no-`team_name` / concurrent-run case) → silent-skip.
55
60
  - After the pane disposition step above, the lead MUST ask the user whether to disband the worker teammates. This is a strict binary choice:
56
61
  > worker teammate 팀 `<teamName>` 을 해제할까요?
57
62
  > (예) 팀원 정리 / (아니오) 그대로 두기
58
63
  - On `아니오` / `n` / `keep` → do not call `TeamDelete()`. Tell the user that the team will remain visible in FleetView/Teams and can be removed later from the Teams UI, or by asking the lead in this thread to clean up `<teamName>` — and that a later run of the same task-key/stage will hit a "team already exists" collision until it is removed.
59
- - On `예` / `y` / `cleanup` → emit `PROGRESS: phase-7-teardown disbanding team`, then run the sequence below. Token-usage collection MUST already be complete — `TeamDelete` removes `~/.claude/teams/<team>/` + `~/.claude/tasks/<team>/` but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown.
60
- 1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "<teamName>"` (the team-state `teamName`) exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
64
+ - On `예` / `y` / `cleanup` → emit `PROGRESS: phase-7-teardown disbanding team`, then run the sequence below. Token-usage collection MUST already be complete — `TeamDelete` takes no argument and removes the current session's team directory (`~/.claude/teams/session-<leadSessionId-prefix>/` + the matching `~/.claude/tasks/...`) but NOT the `~/.claude/projects/` jsonls Phase 7 reads, yet the read MUST precede teardown.
65
+ 1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "<resolved-config-path>"` the on-disk `~/.claude/teams/session-<…>/config.json` resolved in the existence check above, NOT team-state `teamName` (which resolves to a non-existent `~/.claude/teams/okstra-<task-key>/` directory and silently makes reconcile a no-op, leaving dead-pane stale-active members that then block `TeamDelete`) exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
61
66
  2. Call `TeamDelete()` — the single synchronization point for teammate teardown. If it errors with an active-members message, send each named active member a structured `SendMessage(to: <name>, message: { type: "shutdown_request" })` — the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field — wait briefly, run `okstra-team-reconcile.sh` one more time, retry `TeamDelete()` once, and proceed regardless of the result. NEVER loop and never use `TaskStop` (teammates are not background tasks — `TaskStop` 404s on a member address).
62
67
  - If `TeamDelete()` is unavailable in the current host, or teardown still fails after the single retry, do not pretend cleanup succeeded. Report the exact residual action instead: `Teams/FleetView 에서 team <teamName> 삭제`, and if tmux panes were kept earlier also show the pane command `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`.
63
68
  - Report successful teardown in one short line (e.g. `worker teammate 팀 해제`, or `stale 멤버 1명 reconcile 후 팀 해제`) and proceed to the final `PROGRESS: complete ...` line.
@@ -66,6 +66,8 @@
66
66
  - trade-off matrix across options (rows = options, columns at minimum: complexity, risk, reversibility, test coverage cost, rollout cost)
67
67
  - recommended option with rationale tied to the design principles above
68
68
  - **Stage Map (mandatory — always emitted, even when N=1):** a table of all stages with `stage | title | depends-on | step-count | exit-contract-summary`. `depends-on` is `(none)` or a comma-separated stage number list. Stages with `depends-on (none)` can be implemented in parallel by two simultaneous `implementation` runs.
69
+ - **Keep the table at exactly 5 columns** — do NOT add a column. `validators/validate-implementation-plan-stages.py` parses `stage | title | depends-on | step-count | exit-contract-summary` and silently skips any row that is not exactly 5 cells, so a 6th column would drop every stage and bypass S2–S11.
70
+ - **Multi-project plans only** (the plan's work spans more than one project — see the Project-boundary partition rule below): prefix each stage's `title` cell with a `[<project>]` tag (e.g. `[okstra] Add X`) so the project each stage belongs to is readable at a glance, and add exactly one line directly under the Stage Map table — `Cross-project parallelism: <which per-project stages run in parallel, which are sequenced, and the cross-project dependency that forces each sequencing>`. Single-project plans omit both the tag and the line.
69
71
  - **Per-stage slice declaration (mandatory lines, directly under the `## 5.5.<i> Stage <i>:` heading, before `### Carry-In`):**
70
72
  - `Slice value: <the one user-observable increment this stage delivers, end-to-end>` — describe WHAT starts working from the consumer's view (e.g. "X 를 조회하면 Y 가 반환된다"), NOT a layer name ("repository 추가"). Validator S10a rejects a missing/empty value.
71
73
  - `Acceptance: <the observable pass condition or the exact command>` — the signal that proves the slice is done; normally the same test command that the `RED:` step below flips to PASS. Validator S10b rejects a missing/empty value.
@@ -84,7 +86,16 @@
84
86
  - `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
85
87
  - `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
86
88
  - **Vertical-slice-first partition rule (1st-class):** the grouping anchor is a **thin end-to-end vertical slice** — one stage delivers a single user-observable increment, crossing whatever layers are needed (data → service → API → UI) to make that one increment work. File/module proximity is demoted to the **intra-slice grouping rule**: within a slice, keep steps touching the same file/directory/module together so the diff, PR, and rollback unit stay cohesive. **Horizontal layer-splitting is forbidden** — never carve "the DB layer" into one stage and "the service layer" into the next; that produces stages that ship no standalone user value. A stage is split ONLY when (a) a real `depends-on` data/contract dependency exists, (b) effective steps would exceed 6, or (c) it is a distinct vertical slice (a different user-value increment). Maximising the number of parallel stages is NOT a reason to split — parallelism is an emergent property of independent stages, never a partitioning goal.
89
+ - **Project-boundary partition rule (hard boundary):** a *project* boundary is either (a) a different repository / `PROJECT_ROOT`, or (b) a different top-level independently-deployable module within one repo. A stage maps to a single worktree on one repo/branch, so **no stage may contain edits belonging to more than one project** — this is a hard split that overrides the ≤6-step merging allowance; never co-locate two projects' changes in one stage to save a stage. Two cases:
90
+ - **Same repo, different modules** — put each project/module's increment in its own stage. Their file sets are disjoint, so they default to `depends-on (none)` and run in parallel; add a `depends-on` link ONLY when a real cross-module contract / shared-schema / deploy-order dependency exists.
91
+ - **Different repos** — a single okstra task **cannot** span repos: every stage worktree is a `git worktree add` in one repo's main checkout (one `<project-id>`), the run-index / manifests / registry are keyed to that one project, and the edit allowlist only covers that project's tree plus `~/.okstra/worktrees/**`. This is a structural limit, not a style preference. Therefore cross-repo work MUST be split into **a separate okstra run per repo** — never modelled as stages of one task. State, in the Stage Map `Cross-project parallelism:` line, whether those per-repo runs can proceed in parallel. To avoid re-deriving shared analysis, the second repo's run should consume the first run's relevant plan/decision artifacts as brief Source Material (see the cross-repo carry note below).
92
+ - **Parallel-feasibility check (mandatory for every multi-project plan):** disjoint files (S9 below) is necessary but NOT sufficient for parallelism — a cross-project API/contract/schema/deploy-order dependency forces sequencing even when no file overlaps. For each pair of projects, explicitly determine and record (in the `Cross-project parallelism:` line) whether they are independent (run in parallel) or sequenced (and the exact dependency that forces the order).
87
93
  - **Parallel-safety invariant (BLOCKING):** any two stages that are both `depends-on (none)` MUST predict disjoint file sets in their `Stage Exit Contract`. Two parallel `implementation` runs would otherwise edit the same file concurrently. Work touching a shared file must either go in one stage or be ordered with `depends-on`. Enforced by `validators/validate-implementation-plan-stages.py` check S9.
94
+ - **Cross-repo carry packet (multi-repo plans only — the carry note referenced above):** when the plan identifies work in a repo *other* than this run's project, that work cannot be implemented here (Different-repos rule above). For each other repo, add a top-level report appendix section `## Cross-Repo Carry — <repo>` with these three mandatory subsections, written so a reader who has never seen this run can act cold:
95
+ - `### Preconditions already covered by this run` — what THIS run's repo delivers that `<repo>`'s work depends on (an endpoint, a published contract, a migration, a package version). For each, give the **observable verification signal** `<repo>`'s run must confirm before/while implementing (PR merged, endpoint live, version published) — NOT a `depends-on` stage link. A cross-repo dependency cannot be a `depends-on`: that gate resolves commits inside one repo's git graph only (`scripts/okstra_ctl/run.py` `_resolve_stage_base_commit`), so it is structurally incapable of pointing at another repo's commit.
96
+ - `### Work for <repo> to implement` — the self-contained B-portion: its requirements restated as fresh `R-NNN` from `<repo>`'s perspective, the proposed stages/steps, affected files, and any decision drafts. Carry ONLY what `<repo>` still has to build — never carry this run's already-`done` stages as if they were `<repo>` stages.
97
+ - `### How to start <repo>'s run` — the exact handoff: in `<repo>` the user runs `okstra-brief` citing **this report's absolute path as Source Material** (the only sanctioned cross-`<PROJECT_ROOT>` read — `okstra-brief` reads outside `<PROJECT_ROOT>/.okstra/**` only when the user explicitly cites the path), then `okstra-run` `<repo>`'s own planning → implementation seeded by it.
98
+ - **Recognition caveat (write it verbatim into the section):** `<repo>`'s run is structurally independent — its done-set comes only from its OWN `runs/.../consumers.jsonl`, its stage numbers are its own, and `depends-on` resolves against its own git graph. This run's completed stages are therefore NOT auto-recognised as `done` by `<repo>`'s gate (and must not be — they are this repo's work). The carry packet is narrative input that seeds `<repo>`'s planning; `<repo>` re-plans and implements only its own portion, treating this run's deliverables as the external preconditions above. This run MUST NOT write into `<repo>`'s tree or its `.okstra/`; it only emits the carry section in its own report.
88
99
  - **Stage exit contract is the carry surface:** keep it as narrow as possible. Wider surface = more downstream coupling.
89
100
  - dependency / migration risk assessment (ordering constraints, data backfills, feature-flag prerequisites, repo-internal sequencing)
90
101
  - validation checklist (pre / mid / post) — each item is an exact command or observable outcome
@@ -116,4 +127,4 @@
116
127
  5. **Scope check** — if the recommended plan now spans multiple independent subsystems, recommend splitting into separate planning runs rather than shipping an oversized plan.
117
128
  6. **Review-rule preflight check** — if a project review rule pack exists, map each relevant rule to the recommended option. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
118
129
  7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** Inspect the `### 5.5.9 Plan Body Verification` verdict table. For every plan-item row classified as `majority-disagree → C-<N>`, the corresponding `C-<N>` row MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. Do NOT create a parallel `Open Questions` block under the implementation plan body — the unified table is the single home. Conversely, the `Classification` column's `C-<N>` reference and the `## 1. Clarification Items` `ID` column MUST match 1:1; an orphan on either side is a contract violation. For `partial-consensus` and `worker-unique` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
119
- 8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 6. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap).
130
+ 8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 6. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap). **Project-boundary:** confirm no stage mixes edits from two projects (different repo/`PROJECT_ROOT` or different top-level deployable module); if any stage does, split it per project. For multi-project plans, confirm each stage's `title` carries its `[<project>]` tag and the `Cross-project parallelism:` line under the table records the parallel-vs-sequenced determination (with the forcing dependency) for every project pair; for cross-repo work, confirm it is split into separate per-repo runs (required — one run structurally cannot touch another repo) rather than crammed into one task's stages.
@@ -64,7 +64,7 @@ class ClarificationItem:
64
64
  # normalization so the ID parses as a bare token for clarification parsing AND
65
65
  # the HTML view's `C-\d+` form detection, and so the anchor never leaks into
66
66
  # the HTML view as html-escaped literal text.
67
- _CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
67
+ _CELL_ANCHOR_RE = re.compile(r'<a id="([^"]*)"></a>')
68
68
 
69
69
 
70
70
  def _strip_backticks(cell: str) -> str:
@@ -17,6 +17,7 @@ session id 등) 를 덧붙여 전달한다.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import hashlib
20
21
  import json
21
22
  import re
22
23
  import sys
@@ -1658,6 +1659,23 @@ def build_available_mcp_servers_block(project_root: Path) -> str:
1658
1659
  # --------------------------------------------------------------------------- #
1659
1660
 
1660
1661
 
1662
+ def sanitize_team_name(base: str, suffix: str = "") -> str:
1663
+ """`base`(+`suffix`)를 TeamCreate 의 team_name 패턴에 맞춘다.
1664
+
1665
+ TeamCreate 는 team_name 을 `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` 로 검증한다(콜론
1666
+ 불가·최대 64자). 그런데 okstra 의 base 는 `okstra-<TASK_KEY>` 이고 TASK_KEY 는
1667
+ `project:group:task-id` 라 콜론을 포함하며 64자를 쉽게 넘는다. 불허문자를 '-' 로
1668
+ 치환하고, 초과 시 stage 격리용 `suffix`(`-s<N>` / `-fv-s<N>`)는 보존하면서 원본
1669
+ 해시 8자로 절단해 유일성을 유지한다(suffix 만으로 stage 를 구분하는 호출부 계약).
1670
+ """
1671
+ safe = re.sub(r"[^A-Za-z0-9_-]+", "-", base).strip("-")
1672
+ limit = 64 - len(suffix)
1673
+ if len(safe) > limit:
1674
+ digest = hashlib.sha1(base.encode("utf-8")).hexdigest()[:8]
1675
+ safe = safe[: max(0, limit - 9)].rstrip("-") + "-" + digest
1676
+ return safe + suffix
1677
+
1678
+
1661
1679
  def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1662
1680
  """Populate ctx in-place with derived lead-prompt tokens.
1663
1681
 
@@ -1874,19 +1892,21 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1874
1892
  " Teams split-pane view is lost."
1875
1893
  )
1876
1894
  else:
1877
- team_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1895
+ base_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1878
1896
  impl_stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
1879
1897
  fv_stage = str(ctx.get("RUN_STAGE", "") or "").strip()
1898
+ stage_suffix = ""
1880
1899
  if task_type == "implementation" and impl_stage:
1881
1900
  # stage 격리 run 은 stage 별 team — 같은 task 의 다른 stage 가 남긴
1882
1901
  # team 과 이름이 충돌하지 않는다(worktree branch `-s<N>` 접미사와 동형).
1883
- team_name = f"{team_name}-s{impl_stage}"
1902
+ stage_suffix = f"-s{impl_stage}"
1884
1903
  elif task_type == "final-verification" and fv_stage:
1885
1904
  # 단일-stage final-verification 도 stage 별 team. `-fv-` 를 끼워
1886
1905
  # 같은 stage 의 implementation team(`-s<N>`)과도 구분한다 — 둘은
1887
1906
  # 동시에 살아 있을 수 있다. whole-task 검증(RUN_STAGE 빈 값)은
1888
1907
  # 기본 이름 유지.
1889
- team_name = f"{team_name}-fv-s{fv_stage}"
1908
+ stage_suffix = f"-fv-s{fv_stage}"
1909
+ team_name = sanitize_team_name(base_name, stage_suffix)
1890
1910
  team_creation_gate_block = (
1891
1911
  "## Team Creation Gate (BLOCKING)\n"
1892
1912
  "\n"
@@ -57,6 +57,7 @@ def _strip_leading_frontmatter(text: str) -> str:
57
57
  return _LEADING_FRONTMATTER_RE.sub("", text, count=1)
58
58
 
59
59
  from .clarification_items import (
60
+ _CELL_ANCHOR_RE,
60
61
  _section_1_slice,
61
62
  _split_pipe_row,
62
63
  parse_clarification_items,
@@ -336,6 +337,12 @@ def _section_forbids_form(path: Iterable[str]) -> bool:
336
337
  def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[str, int]:
337
338
  header_cells = _split_pipe_row(lines[start])
338
339
  rows: list[list[str]] = []
340
+ # `_split_pipe_row` strips the ID-defining scroll anchor (`<a id="c-001">`)
341
+ # out of every first cell, so the lowercase fragment that the ID-Index and
342
+ # in-body references link to (`[C-001](#c-001)`) would have no landing
343
+ # element. Capture each row's stripped anchor id here and re-attach it to
344
+ # the emitted `<tr>` below so those `href="#…"` links actually jump.
345
+ row_anchor_ids: list[Optional[str]] = []
339
346
  i = start + 2 # skip header + separator
340
347
  while i < len(lines) and lines[i].lstrip().startswith("|"):
341
348
  cells = _split_pipe_row(lines[i])
@@ -347,6 +354,8 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
347
354
  keep = len(header_cells) - 1
348
355
  cells = cells[:keep] + ["|".join(cells[keep:])]
349
356
  rows.append(cells)
357
+ anchor = _CELL_ANCHOR_RE.search(lines[i])
358
+ row_anchor_ids.append(anchor.group(1).lower() if anchor else None)
350
359
  i += 1
351
360
 
352
361
  # §1 Clarification Items is the only interactive table. Its short columns
@@ -390,7 +399,7 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
390
399
  header_cells.index("User input") if "User input" in header_cells else -1
391
400
  )
392
401
  body_rows: list[str] = []
393
- for row in rows:
402
+ for row, anchor_id in zip(rows, row_anchor_ids):
394
403
  meta = parse_meta_cell(row[0]) if (is_clarification_table and row) else None
395
404
  if (
396
405
  meta is not None
@@ -422,8 +431,11 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
422
431
  + "</tr>"
423
432
  )
424
433
  else:
434
+ tr_open = (
435
+ f'<tr id="{html.escape(anchor_id)}">' if anchor_id else "<tr>"
436
+ )
425
437
  body_rows.append(
426
- "<tr>"
438
+ tr_open
427
439
  + "".join(
428
440
  f"<td{_col_class(idx, narrow_cols)}>{_inline(c)}</td>"
429
441
  for idx, c in enumerate(row)
@@ -176,9 +176,20 @@ Order of operations:
176
176
  paths under `<task-group>/sub.../`, and those paths cannot be formed
177
177
  until `task_group` is known. Without this preflight the recursive walk
178
178
  would either stall mid-collection or invent a placeholder group.
179
+
180
+ **Sequential, never batched.** Run Step 2a as its **own**
181
+ `AskUserQuestion` call here, then ask sub-step 1's ticket question in a
182
+ **separate** call. Do NOT combine the task-group question and the
183
+ ticket-ref question into one `AskUserQuestion` — they are asked one at a
184
+ time, task group first (this preflight), ticket ref second. When you
185
+ relay this preflight, tell the user in one line *why* task group comes
186
+ before the ticket (it scopes where this ticket's brief — and any
187
+ sub-ticket briefs — are written).
179
188
  1. `AskUserQuestion` (free text):
180
189
  `"Ticket key or URL (e.g. LIN-1234, PROJ-42, https://linear.app/..., https://your.atlassian.net/browse/...)"` →
181
- `ticket_ref`.
190
+ `ticket_ref`. Issue this only **after** Step 2a's task-group answer is in
191
+ hand — never in the same `AskUserQuestion` call as the task-group
192
+ question.
182
193
  2. Auto-detect the tracker by URL host or key prefix (`linear.app` → Linear,
183
194
  `atlassian.net` or `JIRA_*` pattern → Jira, `github.com/.../issues/` →
184
195
  GitHub, `notion.so` → Notion). If detection fails, ask once more via
@@ -335,12 +346,38 @@ Once validation passes, jump to Step 2 (task key).
335
346
 
336
347
  ### 2a. Task group
337
348
 
338
- `AskUserQuestion` (free text):
349
+ Offer existing groups as recommendations before accepting a new one — never
350
+ ask a bare free-text question that forces the user to retype an existing
351
+ group from memory. First collect the project's known task groups (each
352
+ command is its own Bash call starting with the literal token `okstra`, per
353
+ Step 0):
354
+
355
+ ```bash
356
+ okstra task-list
357
+ ```
358
+
359
+ Parse the JSON `tasks[]` array; collect the **distinct** `taskGroup` values
360
+ ordered by `updatedAt` descending. Take the **2 most-recent** distinct
361
+ groups as recommendations.
362
+
363
+ `AskUserQuestion` (single-select):
364
+
365
+ - **Label**: `"Task group?"`
366
+ - **Options** (recommendations first, `직접 입력` always last):
367
+ 1. (and 2.) each recommended existing group, most-recent first —
368
+ label `<group> (기존)` (e.g. `uploadfont (기존)`).
369
+ 3. `직접 입력` — free-text for a new or other group.
370
+
371
+ When the user picks an existing group, use its value verbatim. When they
372
+ pick `직접 입력`, take a free-text value via a follow-up question.
339
373
 
340
- - `"Task group (e.g. backend-api, INV-1234, refactor)"` → `task_group`
374
+ If `okstra task-list` returns an empty `tasks[]` (no prior tasks), skip the
375
+ picker and ask one free-text question:
376
+ `"Task group (e.g. backend-api, INV-1234, refactor)"` → `task_group`.
341
377
 
342
378
  Slug rule: same as `okstra-run` Step 3 — slugify, must have at least one
343
- alphanumeric character.
379
+ alphanumeric character. Note that an existing group's value is already a
380
+ slug; a `직접 입력` value is slugified before use.
344
381
 
345
382
  If `task_group` was already collected by Step 1b sub-step 0 (tracker
346
383
  preflight), reuse that value silently — do NOT ask again.