okstra 0.121.1 → 0.122.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/docs/architecture.md +4 -2
- package/docs/project-structure-overview.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/prompts/launch.template.md +4 -4
- package/runtime/prompts/lead/adapters/claude-code.md +104 -0
- package/runtime/prompts/lead/adapters/codex.md +45 -0
- package/runtime/prompts/lead/adapters/external.md +46 -0
- package/runtime/prompts/lead/context-loader.md +5 -5
- package/runtime/prompts/lead/convergence.md +11 -28
- package/runtime/prompts/lead/okstra-lead-contract.md +42 -92
- package/runtime/prompts/lead/plan-body-verification.md +21 -5
- package/runtime/prompts/lead/report-writer.md +50 -51
- package/runtime/prompts/lead/team-contract.md +33 -106
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +3 -3
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/python/okstra_ctl/lead_runtime.py +4 -0
- package/runtime/python/okstra_ctl/render.py +76 -194
- package/runtime/python/okstra_ctl/report_views.py +22 -7
- package/runtime/python/okstra_ctl/team_reconcile.py +3 -1
- package/runtime/schemas/final-report-v1.0.schema.json +3 -0
- package/runtime/skills/okstra-schedule/SKILL.md +60 -18
- package/runtime/templates/reports/final-report.template.md +4 -4
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/i18n/ko.json +1 -1
- package/runtime/validators/validate-run.py +89 -0
- package/runtime/validators/validate-schedule.py +7 -3
- package/runtime/validators/validate_session_conformance.py +58 -33
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/stage-map.mjs +100 -0
- package/src/commands/lifecycle/install.mjs +3 -0
- package/src/commands/lifecycle/uninstall.mjs +8 -23
- package/src/lib/skill-catalog.mjs +8 -0
|
@@ -6,7 +6,7 @@ model: opus
|
|
|
6
6
|
|
|
7
7
|
# OKSTRA Schedule
|
|
8
8
|
|
|
9
|
-
Generate a consolidated work schedule for
|
|
9
|
+
Generate a consolidated work schedule for the selected `implementation-planning` stages of every non-done task in a given `task-group` (or a single `task-id`). For each task the skill reads its `implementation-planning` final-report **Stage Map** as the authoritative forward-looking decomposition, asks the user which stages to include, drafts a single Markdown plan, and only writes the final file after an **independent verifier subagent** confirms the draft covers exactly the selected stages. It runs as a **lead + verifier** flow; the frontmatter `model: opus` switches supporting harnesses to Opus-class for the turn — stage-level cross-task synthesis needs that reasoning depth.
|
|
10
10
|
|
|
11
11
|
## When to Use
|
|
12
12
|
|
|
@@ -47,20 +47,23 @@ The installed template `~/.okstra/templates/reports/schedule.template.md` is the
|
|
|
47
47
|
|
|
48
48
|
`~/.okstra/lib/validators/validate-schedule.py` is the enforcement for all of the above (heading order, field labels, controlled vocabulary — e.g. `Med-High` is the canonical risk form — forbidden translations, checkbox bans, Gantt fence rules, unresolved-code detection). The Step 4 ambiguous-classification rationale line is the one rule the validator does not yet enforce — emit it yourself.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
One computation rule the template scaffold cannot carry inline:
|
|
51
51
|
|
|
52
52
|
- **Effort-to-Day mapping**: day ranges per size are defined once in the template's `### Effort Sizing 기준` table. For the At a Glance totals line, sum that table's lower bounds across in-scope tasks for the lower total and upper bounds for the upper total.
|
|
53
|
-
- **`[NEEDS-OKSTRA-RUN]` / `[PARSE-ERROR: <section>]` placement**: put the marker immediately under the per-task heading inside its Phase section and fill only the available fields — never relocate it to a sibling section.
|
|
54
53
|
|
|
55
54
|
## Procedure
|
|
56
55
|
|
|
57
56
|
### Step 1: Resolve task-group and collect tasks
|
|
58
57
|
|
|
59
58
|
1. Read `.okstra/discovery/task-catalog.json`.
|
|
60
|
-
2. **
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
2. **Resolve which task-group to schedule — never silently guess.**
|
|
60
|
+
- The user **explicitly named a task-group** (as the `okstra schedule <task-group>` argument or unambiguously in the request) → use that token; skip the picker and go to sub-step 3.
|
|
61
|
+
- The user **named no task-group, or the named token matches 0 or ≥2 groups** → present a 3-option picker via `AskUserQuestion` and do NOT proceed until the user chooses. Build the options from the catalog: walk `tasks[]` in catalog order (already `updatedAt` desc — see `scripts/okstra_ctl/render.py:654`), collect distinct `taskGroupPathSegment` values that have ≥1 entry whose resolved `workStatus` is **non-done** (defer to Step 2's inference table), and offer the newest **1–2** such groups as recommendations. The **last option is always `직접 입력`** (free-text group token, fed into sub-step 3). Label each recommendation with its non-done task count (e.g. `uploadFont (non-done 3)`).
|
|
62
|
+
- If **zero groups have a non-done task** (or the catalog is empty), do NOT open a picker — emit `해당 task-group의 모든 task가 done 상태입니다. 생성할 schedule이 없습니다.` (or `해당 task-group을 찾을 수 없습니다.` when the catalog has no tasks at all) and stop **without creating a file**.
|
|
63
|
+
3. **Normalise the resolved `<task-group>`:** lowercase it, then strip every character that is not `[a-z0-9]`. Apply the same transform to each entry's `taskGroupPathSegment`. Match on equality — this is the single comparison rule; do NOT also fall back to the raw `taskGroup` field.
|
|
64
|
+
4. If no tasks found, output `해당 task-group을 찾을 수 없습니다.` and stop.
|
|
65
|
+
5. For each matched task, read `.okstra/tasks/<task-group-segment>/<task-id-segment>/task-manifest.json` directly. Catalog data may be stale; the manifest is authoritative.
|
|
66
|
+
6. **Derive `<project-id>`** for the header: prefer `task-catalog.json`'s top-level `projectId`, otherwise the first matched manifest's `projectId`. Do not invent a value.
|
|
64
67
|
|
|
65
68
|
### Step 2: Filter by workStatus
|
|
66
69
|
|
|
@@ -68,18 +71,35 @@ For inference when `workStatus` is missing or empty, defer to the inference tabl
|
|
|
68
71
|
|
|
69
72
|
If 0 tasks remain, output `해당 task-group의 모든 task가 done 상태입니다. 생성할 schedule이 없습니다.` and stop **without creating a file**.
|
|
70
73
|
|
|
71
|
-
### Step 3: Per-task
|
|
74
|
+
### Step 3: Per-task stage extraction (Stage Map source)
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
For each in-scope task, the **authoritative source is its `implementation-planning` final-report**, NOT `latestReportPath`. Resolve and parse it as follows:
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
1. Resolve `<task-root>` from the manifest's `taskRootPath`. The planning report lives under `<task-root>/runs/implementation-planning/**/reports/final-report-*.md`. If several exist, pick the **newest by mtime**.
|
|
79
|
+
2. Do NOT hand-parse the Stage Map markdown. Call `okstra stage-map <task-key> --json` (Task 0). It returns `{ok, taskRoot, stages:[{stage_number,title,depends_on,step_count}], doneStages:[int]}` by wrapping `_load_stage_map` + `read_stage_consumer_state().done_stages`. `stages: []` means no planning Stage Map exists → treat as **no planning report** below.
|
|
80
|
+
3. From each stage row keep `stage_number`, `title`, `depends_on`, `step_count`. Compute `remainingStages = stages − doneStages`.
|
|
81
|
+
4. From `task-manifest.json` still read task-level metadata for the header and phase bucket: `taskId`, `taskGroup`, `taskKey`, `workCategory`, `workStatus`, `taskType`, `workflow.currentPhase`.
|
|
82
|
+
5. From the planning report also read task-level Priority / Risk / Scope / Repos for phase classification and the At a Glance row. Blocking / approval items are **not** extracted (see Audience & authority).
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
**No planning Stage Map** (`stage-map` returned `stages: []`) → this task cannot be stage-scheduled. Tag it `[NEEDS-PLANNING]`, skip the Step 3.5 stage picker for it, and render it under its phase section as a single banner line with task-level metadata only (no Gantt bars, no day total). Continue with the remaining tasks.
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
3.
|
|
82
|
-
|
|
86
|
+
**`remainingStages` is empty** (every stage done but `workStatus` not `done`) → not a scheduling target. Render the task as `_완료 — 남은 stage 없음_` under its phase section; contribute no forward day total.
|
|
87
|
+
|
|
88
|
+
### Step 3.5: Stage selection (per task, user input)
|
|
89
|
+
|
|
90
|
+
Run this **once per in-scope task that has a non-empty `remainingStages`**, sequentially. A single `task-id` invocation runs it once; a `task-group` runs it per task-id in At-a-Glance order.
|
|
91
|
+
|
|
92
|
+
**Build dependency-closed cumulative bundles over `remainingStages`:**
|
|
93
|
+
|
|
94
|
+
1. Order `remainingStages` topologically by `depends_on` (a stage may only appear after all its `depends_on` stages).
|
|
95
|
+
2. Pick natural cut points to form up to **3 cumulative bundles**: `B1` = through the first milestone, `B2` = through the core, `B3` = all remaining. Each bundle is a **dependency-closed prefix** — selecting it never omits a prerequisite stage. Label each bundle with its stage list and summed `step_count` (e.g. `B2: stage 1–4 (steps 12)`).
|
|
96
|
+
3. If `remainingStages` has ≤2 stages, emit only the bundles that are distinct (1–2), never pad to 3.
|
|
97
|
+
|
|
98
|
+
**Render the picker** with `AskUserQuestion` (one question for this task):
|
|
99
|
+
- Options = the distinct bundles + a final `"남은 stage 전부"` option. `AskUserQuestion`'s built-in Other slot serves the `직접 입력` (arbitrary stage subset) case; when the user supplies a custom subset, close it under `depends_on` before accepting.
|
|
100
|
+
- **Degenerate skip:** if only one distinct bundle exists AND it already equals all remaining stages, skip the picker for this task and set `selectedStages = remainingStages` (log `> _Stage picker 생략: 남은 stage가 단일 의존성 체인._`).
|
|
101
|
+
|
|
102
|
+
Record the chosen `selectedStages` for this task. Any custom selection that breaks `depends_on` closure is rejected — re-prompt the same task.
|
|
83
103
|
|
|
84
104
|
### Step 4: Phase classification
|
|
85
105
|
|
|
@@ -94,16 +114,35 @@ If the report file does not exist or cannot be parsed, attempt the **manifest-st
|
|
|
94
114
|
|
|
95
115
|
Priority overrides category: `P0` → Phase 1; `P1`/`P2` → Phase 2; `P3` or multi-repo + infrastructure scope → Phase 3. When still ambiguous, place the task in the closest phase and add a one-line rationale at the top of that phase section (not validator-enforced — emit it yourself).
|
|
96
116
|
|
|
117
|
+
Phase bucketing stays **task-level** (a task lands in one Phase by its `workCategory`/Priority). Within a task's per-task section, the **selected stages become the Work Breakdown rows**: one row per `selectedStages` entry with its `title`, `step_count`-derived effort, and `depends_on`. Stages excluded from `selectedStages` because they are already done are listed once as `> _완료 stage: stage <n>, …_` and carry no forward effort. This keeps the mandatory heading skeleton unchanged while moving the unit of work to the stage.
|
|
118
|
+
|
|
97
119
|
### Step 5: Gantt decision (render by default)
|
|
98
120
|
|
|
99
121
|
`## Gantt Chart` is **rendered by default** — skip ONLY when literally no day signal exists (every task is effort=XXL with no visible decomposition, or all tasks lack both effort sizing and decomposition). Render whenever any of these hold: 2+ tasks with effort sizing; 1 task whose effort yields a range (mid-point bar, or `lo`/`hi` two-bar form); 1 task with Part/Phase/Step decomposition in the source (bars at decomposition-unit level); total estimated effort ≥ 3 days. When per-unit day allocations aren't itemized, split the parent range across the visible units yourself and append the `est` annotation (or add `> 일별 배분은 추정치이며 차단 항목 해소 후 갱신 권장.`). "Range is wide", "single task", "user decisions pending" are NOT skip reasons — render an estimate-tagged chart instead.
|
|
100
122
|
|
|
123
|
+
When the source is a Stage Map, the Gantt **bars are the selected stages** (one bar per `selectedStages` entry), day length split from `step_count` (or the effort range across the stage's steps), and cross-stage `(after stage <n>)` / `(after <TASK-ID>)` edges follow `depends_on`. Already-done stages never get a bar. A task tagged `[NEEDS-PLANNING]` contributes no bars.
|
|
124
|
+
|
|
101
125
|
When you do skip, insert in the section's position exactly: `> _Gantt Chart 생략: <concrete reason referencing the actual data>._`
|
|
102
126
|
|
|
103
127
|
**Directive override (highest priority).** Before applying the heuristic, check for a `## Directive` section, first hit wins: (1) the `--directive-file <abs-path>` argument; (2) `<PROJECT_ROOT>/.okstra/tasks/<task-group-segment>/schedule/instruction-set/analysis-material.md`; (3) none → apply the default heuristic silently. A found directive overrides the render/skip heuristic for the affected section — note it inline as `> _Per Directive directive: <verbatim short excerpt>._` — and its pre-supplied day allocations / phase weights are used verbatim as bar lengths. A directive file without a `## Directive` heading counts as "no directive".
|
|
104
128
|
|
|
129
|
+
### Step 5.5: Draft, verify, gate
|
|
130
|
+
|
|
131
|
+
1. **Draft to staging (never the final path yet).** Write the assembled schedule to `.okstra/tasks/<task-group-segment>/schedule/.draft/<YYYY-MM-DD_HH-MM-SS>.md`. Auto-create the parent.
|
|
132
|
+
2. **Dispatch an independent verifier subagent** with the `Agent` tool. It receives, per task: the `selectedStages` list (stage numbers + titles) and `depends_on` edges, plus the draft file path. It must NOT see the lead's reasoning — only these inputs and the draft. It returns a structured verdict:
|
|
133
|
+
- `coverage`: every `selectedStages` entry appears as a forward Work Breakdown / Gantt unit in the draft (nothing missing).
|
|
134
|
+
- `noExcess`: no non-selected stage and no done stage appears as forward work.
|
|
135
|
+
- `depOrder`: stage ordering and `(after …)` edges honor `depends_on`.
|
|
136
|
+
- `pass = coverage && noExcess && depOrder`; on fail it lists the offending stages as `missing[]` / `extra[]` / `orderViolations[]`.
|
|
137
|
+
3. **Gate:**
|
|
138
|
+
- `pass` → promote the draft to the final path (Step 6) and continue.
|
|
139
|
+
- `fail` → hand the verifier's `missing`/`extra`/`orderViolations` back to the lead, revise the draft in place, and re-dispatch the verifier. **Max 2 revise cycles.**
|
|
140
|
+
- Still failing after the 2nd cycle → do **NOT** write the final file. Remove the staging draft, and report the residual `missing`/`extra`/`orderViolations` to the user in Korean. Stop.
|
|
141
|
+
|
|
105
142
|
### Step 6: Write the schedule file
|
|
106
143
|
|
|
144
|
+
Reached only after Step 5.5 returns `pass`. **Promote** the verified staging draft to the final path (do not re-render from scratch):
|
|
145
|
+
|
|
107
146
|
```
|
|
108
147
|
.okstra/tasks/<task-group-segment>/schedule/<task-group-segment>-plan-<YYYY-MM-DD_HH-MM-SS>.md
|
|
109
148
|
```
|
|
@@ -124,7 +163,7 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart 생
|
|
|
124
163
|
- 포함 task: N개
|
|
125
164
|
- 제외(done) task: M개
|
|
126
165
|
- 예상 소요: X.X ~ Y.Y days (Effort 합산)
|
|
127
|
-
- 모드:
|
|
166
|
+
- 모드: lead + verifier
|
|
128
167
|
```
|
|
129
168
|
|
|
130
169
|
## Edge Cases
|
|
@@ -133,8 +172,10 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart 생
|
|
|
133
172
|
|------|----------|
|
|
134
173
|
| `workStatus` absent or empty | Resolve via the `okstra-inspect status.4` inference table; include unless resolved `done` |
|
|
135
174
|
| Filtered task count is 0 | Emit "모든 task가 done" message; do NOT create a file |
|
|
136
|
-
|
|
|
137
|
-
|
|
|
175
|
+
| implementation-planning 리포트 없음 | `[NEEDS-PLANNING]` 배너로만 나열, stage picker·Gantt·day total 없음 |
|
|
176
|
+
| 남은 stage 0개(전부 done, workStatus 미표기) | `_완료 — 남은 stage 없음_`, forward 산출 없음 |
|
|
177
|
+
| 남은 stage ≤2개 | 누적 묶음을 나오는 만큼만(1~2), 단일 체인이면 picker 생략·전부 진행 |
|
|
178
|
+
| verifier 2회 미통과 | 최종 파일 미작성, 잔여 missing/extra/order 사용자 보고 |
|
|
138
179
|
| `task-group` matches no tasks | "해당 task-group을 찾을 수 없습니다." and stop |
|
|
139
180
|
| Catalog and manifest disagree on `workStatus` | Manifest wins (catalog may be stale) |
|
|
140
181
|
| task-group casing / punctuation variants | Normalise both sides (lowercase + strip non-`[a-z0-9]`), compare against `taskGroupPathSegment` only; use the manifest's segment verbatim for path output |
|
|
@@ -144,3 +185,4 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart 생
|
|
|
144
185
|
- All user-facing messages in Korean; schedule body prose Korean, identifiers/headings/field labels English (template literals).
|
|
145
186
|
- Use project-relative paths in completion messages.
|
|
146
187
|
- Use `taskType` (not `workStatus`) for the At a Glance status column — it carries lifecycle phase information.
|
|
188
|
+
- Per-task section Work Breakdown rows are **selected stages**, not free-form items; done stages are summarized, never scheduled forward.
|
|
@@ -368,12 +368,12 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
368
368
|
{{ t("implementationPlanning.planBodyBreakageLegend") }}
|
|
369
369
|
|
|
370
370
|
{% for item in implementationPlanning.planBodyVerification.planItems %}
|
|
371
|
-
**{{ item.id }} — {{ item.subject }}**{% if item.sourceSection %} ({{ t("implementationPlanning.planBodySourceLabel") }} {{ item.sourceSection }}){% endif %}{% if item.clarificationId %} — {{ t("implementationPlanning.planBodyBlockerLabel") }} {{ item.clarificationId }}{% endif %}
|
|
371
|
+
**{{ item.id }} — {{ item.subject }}**{% if item.sourceSection %} ({{ t("implementationPlanning.planBodySourceLabel") }} {{ item.sourceSection }}){% endif %}{% if item.clarificationId %} — {{ t("implementationPlanning.planBodyBlockerLabel") }} {{ item.clarificationId }}{% endif %}{% if item.selfFixNote %} — {{ item.selfFixNote }}{% endif %}
|
|
372
372
|
|
|
373
373
|
|
|
374
|
-
| Worker | Verdict | Breakage kind | Note |
|
|
375
|
-
|
|
376
|
-
{% for v in item.verdicts %}| {{ v.worker | mdcell }} | {{ v.verdict | mdcell }} | {{ (v.breakageKind or '--') | mdcell }} | {{ (v.note or '') | mdcell }} |
|
|
374
|
+
| Worker | Verdict | Breakage kind | Fixability | Note |
|
|
375
|
+
|--------|---------|---------------|------------|------|
|
|
376
|
+
{% for v in item.verdicts %}| {{ v.worker | mdcell }} | {{ v.verdict | mdcell }} | {{ (v.breakageKind or '--') | mdcell }} | {{ (v.fixability or '--') | mdcell }} | {{ (v.note or '') | mdcell }} |
|
|
377
377
|
{% endfor %}
|
|
378
378
|
|
|
379
379
|
{% endfor %}
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"implementationPlanning": {
|
|
105
105
|
"planBodyGateLegend": "Gate values — `passed`: agreed, no dissent · `passed-with-dissent`: a minority dissent remains but the gate passes (a majority dissent would block approval) · `blocked-by-disagreement`: majority dissent blocks approval · `aborted-non-result`: verification itself produced no result.",
|
|
106
106
|
"planBodyVerdictLegend": "Verdict — **AGREE**: executable as written and internally consistent with other items · **SUPPLEMENT**: item is sound but a dependency / edge case / precondition is missing · **DISAGREE**: has a defect (see Breakage kind) · **verification-error**: the worker produced no result.",
|
|
107
|
-
"planBodyBreakageLegend": "Breakage kind — a: cited file path/symbol mismatches another step or option · b: command is not executable or is ambiguous · c: validation signal is not observable · d: rollback violates commit/dependency order · e: contradicts the trade-off matrix · f: requirement-coverage row does not map to an option/stage/step that actually satisfies the requirement. (`--` = not applicable)",
|
|
107
|
+
"planBodyBreakageLegend": "Breakage kind — a: cited file path/symbol mismatches another step or option · b: command is not executable or is ambiguous · c: validation signal is not observable · d: rollback violates commit/dependency order · e: contradicts the trade-off matrix · f: requirement-coverage row does not map to an option/stage/step that actually satisfies the requirement. (`--` = not applicable) Fixability: planner-fixable = correctable from code + plan + brief; needs-user-input = requires an external decision.",
|
|
108
108
|
"planBodySourceLabel": "source §",
|
|
109
109
|
"planBodyBlockerLabel": "blocks approval →",
|
|
110
110
|
"optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"implementationPlanning": {
|
|
105
105
|
"planBodyGateLegend": "게이트 결과 값 뜻 — `passed`: 이견 없이 통과 · `passed-with-dissent`: 소수 워커의 반대가 남았으나 통과(반대가 다수였다면 승인 차단) · `blocked-by-disagreement`: 다수 반대로 승인 차단 · `aborted-non-result`: 검증 자체가 결과를 내지 못함.",
|
|
106
106
|
"planBodyVerdictLegend": "판정(Verdict) — **AGREE**: 적힌 대로 실행 가능하고 다른 항목과 내부적으로 일관됨 · **SUPPLEMENT**: 항목 자체는 타당하나 의존성·엣지케이스·전제조건이 누락됨 · **DISAGREE**: 결함이 있음(결함 유형 참조) · **verification-error**: 워커 검증이 결과를 내지 못함.",
|
|
107
|
-
"planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음)",
|
|
107
|
+
"planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음) · 자가수정 가능 여부(Fixability) — planner-fixable: 코드·계획·브리프만으로 수정 가능 · needs-user-input: 외부 결정 필요",
|
|
108
108
|
"planBodySourceLabel": "출처 §",
|
|
109
109
|
"planBodyBlockerLabel": "승인 차단 →",
|
|
110
110
|
"optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
|
|
@@ -1590,6 +1590,8 @@ def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
|
|
|
1590
1590
|
_validate_plan_item_extraction_completeness(data, failures)
|
|
1591
1591
|
_validate_plan_item_subject_substance(data, failures)
|
|
1592
1592
|
_validate_plan_body_clarification_matching(data, failures)
|
|
1593
|
+
_validate_disagree_has_fixability(data, failures)
|
|
1594
|
+
_validate_self_fix_before_clarification(data, failures)
|
|
1593
1595
|
_validate_requirement_coverage_covered_by(data, failures)
|
|
1594
1596
|
|
|
1595
1597
|
|
|
@@ -1986,6 +1988,93 @@ def _validate_plan_body_clarification_matching(data: dict, failures: list[str])
|
|
|
1986
1988
|
)
|
|
1987
1989
|
|
|
1988
1990
|
|
|
1991
|
+
def _validate_self_fix_before_clarification(data: dict, failures: list[str]) -> None:
|
|
1992
|
+
"""A planner-fixable defect MUST pass through a self-fix round before it is
|
|
1993
|
+
promoted to a `## 1. Clarification Items` row. Closes the hole where the
|
|
1994
|
+
lead dumps a fixable plan defect (abbreviated path, prose command,
|
|
1995
|
+
placeholder, coverage remap) onto the user instead of having report-writer
|
|
1996
|
+
correct it (plan-body-verification.md "Self-fix round").
|
|
1997
|
+
"""
|
|
1998
|
+
ip = data.get("implementationPlanning")
|
|
1999
|
+
if not isinstance(ip, dict):
|
|
2000
|
+
return
|
|
2001
|
+
pbv = ip.get("planBodyVerification")
|
|
2002
|
+
if not isinstance(pbv, dict):
|
|
2003
|
+
return
|
|
2004
|
+
round_count = pbv.get("roundCount")
|
|
2005
|
+
if not isinstance(round_count, int) or round_count < 1:
|
|
2006
|
+
return
|
|
2007
|
+
if pbv.get("selfFixRoundApplied") is True:
|
|
2008
|
+
return
|
|
2009
|
+
for item in pbv.get("planItems") or []:
|
|
2010
|
+
if not isinstance(item, dict):
|
|
2011
|
+
continue
|
|
2012
|
+
if _classify_plan_item_gate(item) != "majority-disagree":
|
|
2013
|
+
continue
|
|
2014
|
+
disagrees = [
|
|
2015
|
+
v for v in (item.get("verdicts") or [])
|
|
2016
|
+
if isinstance(v, dict) and str(v.get("verdict") or "").upper() == "DISAGREE"
|
|
2017
|
+
]
|
|
2018
|
+
fixable = [v for v in disagrees if v.get("fixability") == "planner-fixable"]
|
|
2019
|
+
if disagrees and len(fixable) * 2 > len(disagrees):
|
|
2020
|
+
failures.append(
|
|
2021
|
+
"final-report data.json: plan item "
|
|
2022
|
+
f"`{item.get('id') or '<unknown>'}` is majority-disagree with a "
|
|
2023
|
+
"planner-fixable majority but `selfFixRoundApplied` is not true. A "
|
|
2024
|
+
"planner-fixable defect MUST be corrected by a report-writer self-fix "
|
|
2025
|
+
"round before it becomes a clarification row "
|
|
2026
|
+
"(plan-body-verification.md Self-fix round)."
|
|
2027
|
+
)
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
# Allowed `fixability` values, mirroring the schema enum
|
|
2031
|
+
# (schemas/final-report-v1.0.schema.json planItems[].verdicts[].fixability).
|
|
2032
|
+
_FIXABILITY_VALUES = frozenset({"planner-fixable", "needs-user-input"})
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
def _validate_disagree_has_fixability(data: dict, failures: list[str]) -> None:
|
|
2036
|
+
"""Every `DISAGREE` verdict MUST carry a valid `fixability`
|
|
2037
|
+
(`planner-fixable` / `needs-user-input`). The schema enum only constrains a
|
|
2038
|
+
*present* value; it does not require the field, so a DISAGREE with a missing
|
|
2039
|
+
or mislabelled fixability is schema-valid. That silently degrades
|
|
2040
|
+
`_validate_self_fix_before_clarification`, which counts a non-`planner-fixable`
|
|
2041
|
+
DISAGREE as non-fixable and thus lets a genuinely planner-fixable defect
|
|
2042
|
+
skip the self-fix round and land on the user. This is the enforcement point
|
|
2043
|
+
for the "fixability is DISAGREE-only 필수" MUST in plan-body-verification.md.
|
|
2044
|
+
"""
|
|
2045
|
+
ip = data.get("implementationPlanning")
|
|
2046
|
+
if not isinstance(ip, dict):
|
|
2047
|
+
return
|
|
2048
|
+
pbv = ip.get("planBodyVerification")
|
|
2049
|
+
if not isinstance(pbv, dict):
|
|
2050
|
+
return
|
|
2051
|
+
round_count = pbv.get("roundCount")
|
|
2052
|
+
if not isinstance(round_count, int) or round_count < 1:
|
|
2053
|
+
return
|
|
2054
|
+
for item in pbv.get("planItems") or []:
|
|
2055
|
+
if not isinstance(item, dict):
|
|
2056
|
+
continue
|
|
2057
|
+
item_id = item.get("id") or "<unknown>"
|
|
2058
|
+
for verdict in item.get("verdicts") or []:
|
|
2059
|
+
if not isinstance(verdict, dict):
|
|
2060
|
+
continue
|
|
2061
|
+
if str(verdict.get("verdict") or "").upper() != "DISAGREE":
|
|
2062
|
+
continue
|
|
2063
|
+
fixability = verdict.get("fixability")
|
|
2064
|
+
if fixability not in _FIXABILITY_VALUES:
|
|
2065
|
+
worker = verdict.get("worker") or "<worker>"
|
|
2066
|
+
allowed = " / ".join(sorted(_FIXABILITY_VALUES))
|
|
2067
|
+
failures.append(
|
|
2068
|
+
f"final-report data.json: plan item `{item_id}` has a "
|
|
2069
|
+
f"`DISAGREE` verdict from `{worker}` with "
|
|
2070
|
+
f"fixability `{fixability}` — a DISAGREE MUST declare a "
|
|
2071
|
+
f"fixability of {allowed}. A missing/invalid value is "
|
|
2072
|
+
"counted as non-fixable and lets a planner-fixable defect "
|
|
2073
|
+
"skip the self-fix round (plan-body-verification.md "
|
|
2074
|
+
"\"fixability (DISAGREE 전용, 필수)\")."
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
|
|
1989
2078
|
_COVERED_BY_ANCHOR_RE = re.compile(r"option|stage|step", re.IGNORECASE)
|
|
1990
2079
|
_COVERED_BY_STAGE_REF_RE = re.compile(r"stage\s*(\d+)", re.IGNORECASE)
|
|
1991
2080
|
_COVERED_BY_VAGUE = {"recommended option", "the recommended option", "recommended"}
|
|
@@ -442,9 +442,13 @@ def validate(path: Path) -> list[str]:
|
|
|
442
442
|
block = text[block_start:block_end]
|
|
443
443
|
heading_line = text[block_start : text.find("\n", block_start)].strip()
|
|
444
444
|
|
|
445
|
-
# Skip blocks marked as NEEDS-OKSTRA-RUN
|
|
446
|
-
# intentionally partial.
|
|
447
|
-
if
|
|
445
|
+
# Skip blocks marked as NEEDS-OKSTRA-RUN, PARSE-ERROR, or NEEDS-PLANNING
|
|
446
|
+
# — these are intentionally partial.
|
|
447
|
+
if (
|
|
448
|
+
"[NEEDS-OKSTRA-RUN]" in block
|
|
449
|
+
or "[PARSE-ERROR" in block
|
|
450
|
+
or "[NEEDS-PLANNING]" in block
|
|
451
|
+
):
|
|
448
452
|
continue
|
|
449
453
|
|
|
450
454
|
last_pos = -1
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
|
6
|
-
|
|
7
|
-
| 1. lead PROGRESS
|
|
8
|
-
| 2.
|
|
9
|
-
| 3. implementation sidecar entry guard | prompts/lead/okstra-lead-contract.md "Entry guard (BLOCKING)" |
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- `
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- validator 실행 시점(Phase 7) **이후** 에 출력되는 체크포인트
|
|
19
|
-
(`phase-7-teardown`, `complete`)는 구조적으로 검사 불가 — 요구하지 않는다.
|
|
1
|
+
"""Post-hoc checks for three BLOCKING neutral lead lifecycle contracts.
|
|
2
|
+
|
|
3
|
+
Design: docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
|
|
4
|
+
|
|
5
|
+
| Check | Contract | Evidence |
|
|
6
|
+
|-------|----------|----------|
|
|
7
|
+
| 1. lead PROGRESS checkpoints | prompts/lead/okstra-lead-contract.md "Progress reporting (BLOCKING)" | selected adapter evidence source |
|
|
8
|
+
| 2. worker-provider heartbeat | worker provider contract | audit sidecar `- PROGRESS: <stage> <ISO>` lines |
|
|
9
|
+
| 3. implementation sidecar entry guard | prompts/lead/okstra-lead-contract.md "Entry guard (BLOCKING)" | selected adapter evidence source |
|
|
10
|
+
|
|
11
|
+
Evidence rules prevent false passes:
|
|
12
|
+
- `claude-jsonl` accepts only assistant text and Read tool-use records, excluding
|
|
13
|
+
injected skill text and sidechain records.
|
|
14
|
+
- Session evidence is scoped to the current run window so a previous run cannot
|
|
15
|
+
satisfy the neutral lead lifecycle contract.
|
|
16
|
+
- Checkpoints emitted after validation begins (`phase-7-teardown`, `complete`)
|
|
17
|
+
cannot be observed and are not required.
|
|
20
18
|
"""
|
|
21
19
|
from __future__ import annotations
|
|
22
20
|
|
|
@@ -202,7 +200,8 @@ def _collect_lead_evidence(
|
|
|
202
200
|
if not evidence.scanned_files:
|
|
203
201
|
return None, (
|
|
204
202
|
f"lead session jsonl not found under {proj_dir} "
|
|
205
|
-
f"(lead.sessionId={lead_sid or '<empty>'}) —
|
|
203
|
+
f"(lead.sessionId={lead_sid or '<empty>'}) — selected adapter evidence "
|
|
204
|
+
"source `claude-jsonl` cannot verify the PROGRESS checkpoint / "
|
|
206
205
|
"implementation entry-guard conformance cannot be verified, which "
|
|
207
206
|
"fails the run (same principle as the token-usage accuracy contract)."
|
|
208
207
|
)
|
|
@@ -223,15 +222,16 @@ def _resolve_lead_events_path(
|
|
|
223
222
|
if not raw:
|
|
224
223
|
return None, (
|
|
225
224
|
"artifact lead event log path missing from team-state "
|
|
226
|
-
"(`leadEventsPath`) — artifact-only
|
|
225
|
+
"(`leadEventsPath`) — selected adapter evidence source `artifact-only` "
|
|
226
|
+
"cannot verify conformance."
|
|
227
227
|
)
|
|
228
228
|
path = Path(str(raw))
|
|
229
229
|
if not path.is_absolute():
|
|
230
230
|
path = project_root / path
|
|
231
231
|
if not path.is_file():
|
|
232
232
|
return None, (
|
|
233
|
-
f"artifact lead event log not found: {path} —
|
|
234
|
-
"
|
|
233
|
+
f"artifact lead event log not found: {path} — selected adapter evidence "
|
|
234
|
+
"source `artifact-only` cannot verify conformance."
|
|
235
235
|
)
|
|
236
236
|
return path, None
|
|
237
237
|
|
|
@@ -301,7 +301,10 @@ def _collect_artifact_lead_evidence(
|
|
|
301
301
|
try:
|
|
302
302
|
events = read_lead_events(events_path)
|
|
303
303
|
except LeadEventParseError as exc:
|
|
304
|
-
return None,
|
|
304
|
+
return None, (
|
|
305
|
+
"artifact lead event log is malformed — selected adapter evidence "
|
|
306
|
+
f"source `artifact-only` cannot verify conformance: {exc}"
|
|
307
|
+
)
|
|
305
308
|
|
|
306
309
|
run_seq = suffix.rsplit("-", 1)[1]
|
|
307
310
|
evidence = _LeadEvidence(scanned_files=[events_path])
|
|
@@ -324,6 +327,27 @@ def _collect_artifact_lead_evidence(
|
|
|
324
327
|
return evidence, None
|
|
325
328
|
|
|
326
329
|
|
|
330
|
+
def _conformance_evidence_source(
|
|
331
|
+
team_state: dict,
|
|
332
|
+
) -> tuple[str | None, str | None]:
|
|
333
|
+
dispatch_mode = str(team_state.get("dispatchMode", "")).strip()
|
|
334
|
+
if dispatch_mode in _WORKER_DISPATCH_MODES:
|
|
335
|
+
return "artifact-only", None
|
|
336
|
+
|
|
337
|
+
adapter = team_state.get("leadAdapter")
|
|
338
|
+
if isinstance(adapter, dict):
|
|
339
|
+
accounting = str(adapter.get("sessionAccounting", "")).strip()
|
|
340
|
+
if accounting in {"claude-jsonl", "artifact-only"}:
|
|
341
|
+
return accounting, None
|
|
342
|
+
if accounting:
|
|
343
|
+
return None, f"unsupported sessionAccounting: `{accounting}`"
|
|
344
|
+
|
|
345
|
+
legacy_runtime = str(team_state.get("leadRuntime", "") or "claude-code")
|
|
346
|
+
if legacy_runtime in {"codex", "external"}:
|
|
347
|
+
return "artifact-only", None
|
|
348
|
+
return "claude-jsonl", None
|
|
349
|
+
|
|
350
|
+
|
|
327
351
|
def _convergence_rounds_ran(run_dir: Path, suffix: str | None) -> bool:
|
|
328
352
|
"""이 run 의 convergence state artifact 가 실제 round 를 1회 이상 돌았는지.
|
|
329
353
|
auto-disable(`totalRounds: 0`)·artifact 부재는 phase-5.5 라인을 요구하지 않는다."""
|
|
@@ -569,7 +593,8 @@ def _check_implementation_sidecar_reads(evidence: _LeadEvidence, errors: list[st
|
|
|
569
593
|
if not ts_list:
|
|
570
594
|
errors.append(
|
|
571
595
|
f"implementation entry guard: no `Read` of `{basename}` found in "
|
|
572
|
-
|
|
596
|
+
"the selected adapter evidence source within this run's window — "
|
|
597
|
+
"the sidecar "
|
|
573
598
|
f"MUST be read fresh at {read_at} every implementation run "
|
|
574
599
|
"(prompts/lead/okstra-lead-contract.md 'Entry guard (BLOCKING)')."
|
|
575
600
|
)
|
|
@@ -593,10 +618,10 @@ def validate_session_conformance(
|
|
|
593
618
|
task_type: str,
|
|
594
619
|
claude_projects_dir: Path | None = None,
|
|
595
620
|
) -> SessionConformanceResult:
|
|
596
|
-
"""post-hoc
|
|
621
|
+
"""Run three post-hoc checks for the neutral lead lifecycle contract.
|
|
597
622
|
|
|
598
|
-
`claude_projects_dir`
|
|
599
|
-
|
|
623
|
+
`claude_projects_dir` injects the Claude project root for tests and diagnostics.
|
|
624
|
+
Heartbeat validation runs before selecting the adapter evidence source.
|
|
600
625
|
"""
|
|
601
626
|
result = SessionConformanceResult()
|
|
602
627
|
_ensure_token_usage_importable()
|
|
@@ -610,11 +635,11 @@ def validate_session_conformance(
|
|
|
610
635
|
suffix = run_artifact_suffix(team_state_path)
|
|
611
636
|
_check_heartbeat_sidecars(run_dir, task_type, suffix, result.errors)
|
|
612
637
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
638
|
+
evidence_source, source_error = _conformance_evidence_source(team_state)
|
|
639
|
+
if source_error:
|
|
640
|
+
result.errors.append(source_error)
|
|
641
|
+
return result
|
|
642
|
+
if evidence_source == "artifact-only":
|
|
618
643
|
evidence, error = _collect_artifact_lead_evidence(
|
|
619
644
|
team_state, project_root, task_type, suffix
|
|
620
645
|
)
|
package/src/cli-registry.mjs
CHANGED
|
@@ -110,6 +110,13 @@ export const COMMAND_REGISTRY = [
|
|
|
110
110
|
category: "introspection",
|
|
111
111
|
summary: ["Summarize a task's manifest + workflow phase state"],
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: "stage-map",
|
|
115
|
+
module: "./commands/inspect/stage-map.mjs",
|
|
116
|
+
export: "run",
|
|
117
|
+
category: "introspection",
|
|
118
|
+
summary: ["Dump a task's implementation-planning Stage Map + done stages"],
|
|
119
|
+
},
|
|
113
120
|
{
|
|
114
121
|
name: "context-cost",
|
|
115
122
|
module: "./commands/inspect/context-cost.mjs",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { runPythonSnippet, emitJsonError } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra stage-map — dump a task's implementation-planning Stage Map + done stages
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra stage-map <task-key> task-key is project-id:task-group:task-id
|
|
7
|
+
okstra stage-map <task-key> --cwd <dir> Resolve PROJECT_ROOT from <dir>
|
|
8
|
+
okstra stage-map <task-key> --project <dir> Use <dir> directly as PROJECT_ROOT
|
|
9
|
+
|
|
10
|
+
Output: JSON { ok, taskKey, taskRoot, stages:[{stage_number,title,depends_on,step_count}], doneStages:[int] }.
|
|
11
|
+
stages is [] when no implementation-planning Stage Map exists.
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
const opts = { cwd: process.cwd(), projectRoot: "", taskKey: "" };
|
|
16
|
+
const positional = [];
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const a = args[i];
|
|
19
|
+
if (a === "--cwd") {
|
|
20
|
+
opts.cwd = args[++i];
|
|
21
|
+
if (!opts.cwd) throw new Error("--cwd requires a path");
|
|
22
|
+
} else if (a === "--project") {
|
|
23
|
+
opts.projectRoot = args[++i];
|
|
24
|
+
if (!opts.projectRoot) throw new Error("--project requires a path");
|
|
25
|
+
} else if (a === "--json") {
|
|
26
|
+
// JSON is always emitted; accept the flag for parity with sibling commands.
|
|
27
|
+
} else if (a.startsWith("--")) {
|
|
28
|
+
throw new Error(`unknown argument '${a}'`);
|
|
29
|
+
} else {
|
|
30
|
+
positional.push(a);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (positional.length !== 1) {
|
|
34
|
+
throw new Error("expected exactly one positional argument: <task-key>");
|
|
35
|
+
}
|
|
36
|
+
opts.taskKey = positional[0];
|
|
37
|
+
return opts;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SCRIPT = `
|
|
41
|
+
import json, sys
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from okstra_project import resolve_project_root, find_task_root, read_task_manifest, ResolverError
|
|
44
|
+
from okstra_ctl.implementation_outcome import _load_stage_map
|
|
45
|
+
from okstra_ctl.consumers import read_stage_consumer_state
|
|
46
|
+
|
|
47
|
+
explicit, cwd, task_key = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
pr = resolve_project_root(explicit_root=explicit, cwd=cwd)
|
|
51
|
+
except ResolverError as e:
|
|
52
|
+
print(json.dumps({"ok": False, "stage": "resolve", "reason": str(e)}))
|
|
53
|
+
sys.exit(2)
|
|
54
|
+
|
|
55
|
+
task_root = find_task_root(Path(pr), task_key)
|
|
56
|
+
if task_root is None:
|
|
57
|
+
print(json.dumps({"ok": False, "stage": "task_root", "reason": f"task root not found for {task_key}"}))
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
manifest = read_task_manifest(task_root) or {}
|
|
61
|
+
stages = _load_stage_map(task_root, manifest)
|
|
62
|
+
|
|
63
|
+
plan_run_root = task_root / "runs" / "implementation-planning"
|
|
64
|
+
done = sorted(read_stage_consumer_state(plan_run_root, recover_from_carry=True).done_stages) if plan_run_root.is_dir() else []
|
|
65
|
+
|
|
66
|
+
print(json.dumps({
|
|
67
|
+
"ok": True,
|
|
68
|
+
"taskKey": task_key,
|
|
69
|
+
"taskRoot": str(task_root),
|
|
70
|
+
"stages": stages,
|
|
71
|
+
"doneStages": done,
|
|
72
|
+
}, ensure_ascii=False, indent=2))
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
export async function run(args) {
|
|
76
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
77
|
+
process.stdout.write(USAGE);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
let opts;
|
|
81
|
+
try {
|
|
82
|
+
opts = parseArgs(args);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
87
|
+
const result = await runPythonSnippet({
|
|
88
|
+
script: SCRIPT,
|
|
89
|
+
args: [opts.projectRoot, opts.cwd, opts.taskKey],
|
|
90
|
+
});
|
|
91
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
92
|
+
emitJsonError({
|
|
93
|
+
stage: "python",
|
|
94
|
+
reason: `python invocation failed: ${result.stderr.trim() || "no output"}`,
|
|
95
|
+
});
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write(result.stdout);
|
|
99
|
+
return result.code === 0 ? 0 : result.code;
|
|
100
|
+
}
|
|
@@ -45,6 +45,9 @@ const BIN_ENTRYPOINTS = [
|
|
|
45
45
|
// depend on. Intentionally an exact allowlist, not a directory wildcard.
|
|
46
46
|
const REQUIRED_PROMPT_RESOURCE_FILES = Object.freeze([
|
|
47
47
|
["prompts", "lead", "okstra-lead-contract.md"],
|
|
48
|
+
["prompts", "lead", "adapters", "claude-code.md"],
|
|
49
|
+
["prompts", "lead", "adapters", "codex.md"],
|
|
50
|
+
["prompts", "lead", "adapters", "external.md"],
|
|
48
51
|
["prompts", "lead", "context-loader.md"],
|
|
49
52
|
["prompts", "lead", "team-contract.md"],
|
|
50
53
|
["prompts", "lead", "convergence.md"],
|