okstra 0.98.1 → 0.98.2

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.
@@ -0,0 +1,321 @@
1
+ # okstra-schedule AI Manual
2
+
3
+ ## 원천
4
+
5
+ - 스킬 원문: [`skills/okstra-schedule/SKILL.md`](../../../skills/okstra-schedule/SKILL.md)
6
+ - schedule 템플릿: [`templates/reports/schedule.template.md`](../../../templates/reports/schedule.template.md)
7
+ - schedule validator: [`validators/validate-schedule.py`](../../../validators/validate-schedule.py)
8
+ - workStatus inference 참조: [`skills/okstra-inspect/SKILL.md`](../../../skills/okstra-inspect/SKILL.md)
9
+
10
+ ## 목적
11
+
12
+ `okstra-schedule`은 task-group 안의 non-done task들을 모아 클라이언트용 작업 일정 Markdown을 만든다. 실행 task를 시작하는 스킬이 아니며, 단일 task 분석도 아니다.
13
+
14
+ 출력 위치:
15
+
16
+ ```text
17
+ <PROJECT_ROOT>/.okstra/tasks/<task-group-segment>/schedule/<task-group-segment>-plan-<YYYY-MM-DD_HH-MM-SS>.md
18
+ ```
19
+
20
+ ## 사용 조건
21
+
22
+ 사용한다:
23
+
24
+ - 사용자가 task-group 전체의 "일정", "schedule", "작업 계획표"를 요청한다.
25
+ - `.okstra/discovery/task-catalog.json`에 해당 task-group이 있고, `done`이 아닌 task가 하나 이상 있다.
26
+
27
+ 사용하지 않는다:
28
+
29
+ - 단일 task 상태/분석: `okstra-inspect status`
30
+ - 실제 phase 실행: `okstra-run`
31
+ - 이미 완료된 task-group: 파일을 만들지 않고 모든 task가 done이라고 말한다.
32
+
33
+ ## Preflight
34
+
35
+ 각각 별도 Bash 호출:
36
+
37
+ ```bash
38
+ okstra ensure-installed --runtime claude-code
39
+ okstra check-project --json
40
+ ```
41
+
42
+ runtime이나 project setup이 없으면 `/okstra-setup` 안내 후 멈춘다.
43
+
44
+ ## model gate
45
+
46
+ 스킬 frontmatter는 `model: opus`다. 활성 모델이 Opus-class 또는 Fable-class가 아니면 사용자에게 전환을 권한다. 사용자가 lower model로 진행하겠다고 명시하면 진행하되, 생성 schedule 상단에 reduced-quality warning을 넣는다.
47
+
48
+ ## task-group 해석
49
+
50
+ 1. `.okstra/discovery/task-catalog.json`을 읽는다.
51
+ 2. 사용자 입력 task-group을 lowercase하고 `[a-z0-9]` 외 문자를 제거한다.
52
+ 3. catalog entry의 `taskGroupPathSegment`에도 같은 변환을 적용해 비교한다.
53
+ 4. raw `taskGroup` fallback은 쓰지 않는다.
54
+ 5. 매칭된 각 task의 `task-manifest.json`을 직접 읽는다. catalog는 stale일 수 있고 manifest가 authoritative다.
55
+
56
+ ## workStatus filter
57
+
58
+ 각 manifest에서 workStatus를 확인한다. 없거나 비어 있으면 `okstra-inspect`의 `status.4` inference table을 적용한다.
59
+
60
+ 제외:
61
+
62
+ - explicit `done`
63
+ - inferred `done`
64
+
65
+ 포함:
66
+
67
+ - `todo`
68
+ - `in-progress`
69
+ - `blocked`
70
+ - `phase-done`
71
+ - 그 밖의 non-done inferred/display 상태
72
+
73
+ 필터 후 0개면 schedule file을 만들지 않는다.
74
+
75
+ ## per-task data extraction
76
+
77
+ manifest에서 읽는다:
78
+
79
+ - `taskId`, `taskGroup`, `taskKey`
80
+ - `workCategory`
81
+ - `workflow.currentPhase`
82
+ - `workflow.currentPhaseState`
83
+ - `taskType`
84
+ - `workStatus`
85
+ - `latestReportPath`
86
+
87
+ report에서 파싱한다:
88
+
89
+ - Title / Problem statement
90
+ - Solution / Architecture
91
+ - Work Breakdown
92
+ - Verification Commands
93
+ - Rollback strategy
94
+ - Effort, Risk, Priority, Scope, Repos
95
+
96
+ report가 없거나 manifest pointer가 stale이면 fallback:
97
+
98
+ 1. `taskRootPath`를 기준으로 `runs/<taskType>/reports/final-report-*.md`에서 mtime이 최신인 파일을 찾는다.
99
+ 2. 없으면 `runs/*/reports/final-report-*.md`도 본다.
100
+ 3. 찾으면 parse하고 schedule task block에 fallback note를 넣는다.
101
+ 4. 끝까지 없으면 `[NEEDS-OKSTRA-RUN]`으로 표시하고 manifest metadata만 사용한다.
102
+
103
+ 특정 section parse 실패는 `[PARSE-ERROR: <section>]`으로 표시하고 계속한다.
104
+
105
+ ## client-facing audience rule
106
+
107
+ schedule은 클라이언트용 작업 계획이다. 내부 report의 approval, blocking decision, 사용자 확인 필요 항목을 schedule에 노출하지 않는다.
108
+
109
+ 출력에서 제거한다:
110
+
111
+ - permission/authority 확인 step
112
+ - approval waiting buffer
113
+ - stakeholder coordination
114
+ - 법무/조직 sign-off
115
+ - `Consolidated User Decision Checklist`
116
+ - `#### 사용자 확인 필요 항목`
117
+ - `Done`, `Ready?`, `Blocking Decisions` column
118
+ - checkbox list/cell
119
+
120
+ effort와 Gantt는 engineering duration만 반영한다.
121
+
122
+ ## phase classification
123
+
124
+ 기본 mapping:
125
+
126
+ | workCategory | phase |
127
+ |---|---|
128
+ | `bugfix` | High/Med-High risk이면 Phase 1, 아니면 Phase 2 |
129
+ | `feature` | Phase 2 |
130
+ | `improvement` | Phase 2 |
131
+ | `refactor` | Phase 3 |
132
+ | `ops` | Phase 3 |
133
+ | `docs` / `doc` | Phase 2 |
134
+ | `unknown` 또는 미정의 | Phase 2, rationale note 추가 |
135
+
136
+ priority override:
137
+
138
+ - `P0`: Phase 1
139
+ - `P1`, `P2`: Phase 2
140
+ - `P3`: Phase 3
141
+
142
+ ambiguous하면 가장 가까운 phase에 두고 rationale을 phase section 상단에 쓴다. 이 rationale은 현재 validator gap으로 문서에서 책임진다.
143
+
144
+ ## section contract
145
+
146
+ 템플릿의 heading 순서와 spelling을 따른다. `validate-schedule.py`가 section order, title suffix, metadata, field labels, enum, Gantt axis 등을 검사한다.
147
+
148
+ top-level 계약 sections:
149
+
150
+ 1. `## At a Glance`
151
+ 2. `## Executive Summary`
152
+ 3. `## Task Dependency Graph`
153
+ 4. `## Phase 1: Critical Fixes`
154
+ 5. `## Phase 2: Enhancements`
155
+ 6. `## Phase 3: Architecture`
156
+ 7. `## Execution Priority Matrix`
157
+ 8. `## Cross-Task Dependencies & Shared Concerns`
158
+ 9. `## Risk Mitigation Strategy`
159
+ 10. `## Recommended Immediate Actions`
160
+
161
+ optional sections:
162
+
163
+ - `## Gantt Chart`: `Task Dependency Graph`와 `Phase 1` 사이.
164
+ - `## Glossary`: 마지막 section. opaque code가 본문에 남을 때만 사용.
165
+
166
+ phase에 task가 없어도 heading을 유지하고 `_없음_`을 쓴다.
167
+
168
+ ## top header
169
+
170
+ 형태:
171
+
172
+ ```markdown
173
+ # <Title> — Work Schedule
174
+
175
+ > Generated: <YYYY-MM-DD HH:MM> | Project: <project-id> | Task Group: <task-group>
176
+ > Source: okstra <mode> (<N> tasks included, <M> done excluded)
177
+ ```
178
+
179
+ title suffix `— Work Schedule`은 validator가 검사한다.
180
+
181
+ ## At a Glance
182
+
183
+ 다음 totals line이 정확히 있어야 한다.
184
+
185
+ ```markdown
186
+ **총 <N>개 task / 예상 소요: <X.X> ~ <Y.Y> days (Effort 합산)**
187
+ ```
188
+
189
+ Effort-to-day mapping은 template의 `### Effort Sizing 기준` table이 SSOT다. lower/upper 합산으로 total day range를 만든다.
190
+
191
+ enum 값:
192
+
193
+ | Field | 값 |
194
+ |---|---|
195
+ | Effort | `S`, `M`, `L`, `XL`, `XXL` |
196
+ | Priority | `P0`, `P1`, `P2`, `P3` |
197
+ | Risk | `Very Low`, `Low`, `Medium`, `Med-High`, `High` |
198
+ | Phase | `1`, `2`, `3` |
199
+
200
+ `Med-High`가 canonical이다.
201
+
202
+ ## per-task block
203
+
204
+ 모든 task block은 `| Item | Detail |` table을 가진다. field row 순서:
205
+
206
+ 1. `**Category**`
207
+ 2. `**Priority**`
208
+ 3. `**Effort**`
209
+ 4. `**Status**`
210
+ 5. `**Risk**`
211
+ 6. `**Scope**`
212
+ 7. `**Repo**`
213
+
214
+ 그 뒤 subsection 순서:
215
+
216
+ 1. `**Problem**:`
217
+ 2. `**Solution**:`
218
+ 3. `**Work Breakdown**:`
219
+ 4. `**Verification Commands**:`
220
+ 5. `**Rollback**:`
221
+
222
+ `[NEEDS-OKSTRA-RUN]` 또는 `[PARSE-ERROR: <section>]` task는 가능한 field만 채우되 marker를 task heading 바로 아래 둔다.
223
+
224
+ ## Task Dependency Graph
225
+
226
+ dependency가 없으면 literal:
227
+
228
+ ```markdown
229
+ _의존 정보 없음_
230
+ ```
231
+
232
+ dependency가 있으면 plain fenced block을 사용한다. language tag를 붙이지 않는다.
233
+
234
+ ````
235
+ ```
236
+ DEV-1 -> DEV-2, DEV-3
237
+ DEV-2 -> DEV-4
238
+ ```
239
+ ````
240
+
241
+ arrow는 ASCII `->`만 사용한다.
242
+
243
+ ## Gantt Chart
244
+
245
+ 기본은 렌더한다. day signal이 하나라도 있으면 rough estimate라도 chart를 만든다.
246
+
247
+ 렌더 조건 예:
248
+
249
+ - 2개 이상 task에 effort sizing이 있다.
250
+ - 1개 task라도 effort range가 있다.
251
+ - source report에 Part/Phase/Step decomposition이 있다.
252
+ - total effort가 3 days 이상이다.
253
+
254
+ skip은 모든 task가 XXL이고 decomposition이 없거나, 모든 task에 effort와 decomposition이 없을 때만 한다. skip하면 `Gantt Chart` 위치에 다음 blockquote를 둔다.
255
+
256
+ ```markdown
257
+ > _Gantt Chart 생략: <actual data를 근거로 한 이유>._
258
+ ```
259
+
260
+ Gantt는 plain fenced ASCII다. Mermaid, PlantUML, Graphviz, date axis를 쓰지 않는다.
261
+
262
+ ````
263
+ ```
264
+ Day: 1 5 10 15 20
265
+ | | | | |
266
+ Phase 1
267
+ DEV-1 (M) ██████ ! crit
268
+ Phase 2
269
+ DEV-2 (L) ██████░░ est
270
+ ```
271
+ ````
272
+
273
+ axis는 relative day count만 사용한다. calendar date, weekday, "오늘 + N"을 넣지 않는다.
274
+
275
+ ## opaque code 처리
276
+
277
+ 내부 report code(`FC-5`, `UC-3`, `M2` 등)는 그대로 노출하지 않는다.
278
+
279
+ 선택 A: 짧은 설명으로 inline replacement.
280
+
281
+ 선택 B: 반복되는 code가 많으면 마지막 `## Glossary`에 모든 code를 해석한다.
282
+
283
+ decision-item letter code(`A1`, `B2`, `C3`, `D4`)는 schedule에 넣지 않는다.
284
+
285
+ ## validation
286
+
287
+ 작성 후 완료 메시지 전에 다시 읽고 validator를 실행한다.
288
+
289
+ ```bash
290
+ python3 ~/.okstra/lib/validators/validate-schedule.py <output-path>
291
+ ```
292
+
293
+ 설치본 validator가 없으면 repo validator를 쓴다.
294
+
295
+ ```bash
296
+ python3 validators/validate-schedule.py <output-path>
297
+ ```
298
+
299
+ 실패하면 파일을 고치고 재검증한다. validator가 없을 때만 manual checklist로 대체한다.
300
+
301
+ ## 완료 메시지
302
+
303
+ ```text
304
+ Schedule 생성 완료: <relative-path>
305
+ - 포함 task: N개
306
+ - 제외(done) task: M개
307
+ - 예상 소요: X.X ~ Y.Y days (Effort 합산)
308
+ - 모드: lightweight
309
+ ```
310
+
311
+ ## 금지 패턴
312
+
313
+ - heading을 한국어/프랑스어 등으로 번역하기.
314
+ - `## Cumulative Timeline` 추가.
315
+ - `## Effort-to-Day 매핑` 같은 extra top-level section 추가.
316
+ - Gantt를 mermaid로 렌더하기.
317
+ - Gantt axis에 calendar date를 넣기.
318
+ - checkbox, Done, Ready, Blocking Decisions column 만들기.
319
+ - internal approval/blocker/user-decision 항목을 클라이언트 schedule에 노출하기.
320
+ - report가 없다고 task를 제외하기. `[NEEDS-OKSTRA-RUN]`으로 포함한다.
321
+ - timestamp collision 시 기존 파일을 덮어쓰기. `-2`, `-3` suffix를 붙인다.
@@ -0,0 +1,140 @@
1
+ # okstra-setup AI Manual
2
+
3
+ ## 원천
4
+
5
+ - 스킬 원문: [`skills/okstra-setup/SKILL.md`](../../../skills/okstra-setup/SKILL.md)
6
+ - CLI registry: [`src/cli-registry.mjs`](../../../src/cli-registry.mjs)
7
+ - project 등록 구현: `src/commands/lifecycle/setup.mjs`
8
+ - install/ensure-installed 구현: `src/commands/lifecycle/install.mjs`
9
+
10
+ ## 목적
11
+
12
+ `okstra-setup`은 두 가지를 처리한다.
13
+
14
+ 1. 머신 단위 runtime 설치: `~/.okstra/`, `~/.claude/skills/`, `~/.claude/agents/`
15
+ 2. 프로젝트 단위 등록: `<PROJECT_ROOT>/.okstra/project.json`
16
+
17
+ 일상적인 task 실행 스킬이 아니다. task가 이미 준비된 상태면 `okstra-run` 또는 `okstra-inspect`로 라우팅한다.
18
+
19
+ ## 사용 조건
20
+
21
+ 사용한다:
22
+
23
+ - 사용자가 "okstra setup", "setup okstra", "initialize okstra", "okstra init", "처음 설정"을 요청한다.
24
+ - `~/.okstra/version`이 없거나 오래된 것으로 보인다.
25
+ - 현재 프로젝트에 `.okstra/project.json`이 없다.
26
+
27
+ 사용하지 않는다:
28
+
29
+ - task run을 시작하려는 경우. 이때는 `okstra-run`.
30
+ - status/history/report를 보려는 경우. 이때는 `okstra-inspect`.
31
+ - 이미 `.okstra/project.json`이 있고 day-to-day 사용만 필요한 경우.
32
+
33
+ ## 실행 전 확인
34
+
35
+ 사용자에게 Node 18+, Python 3.10+가 필요하다고 알린다. 현재 working directory가 okstra 메타데이터를 둘 프로젝트 안인지 불명확하면, 프로젝트 루트를 먼저 확인한다.
36
+
37
+ 설치 명령:
38
+
39
+ ```bash
40
+ npx -y okstra@latest install --runtime claude-code
41
+ ```
42
+
43
+ 이 명령은 idempotent로 취급한다. 이미 설치되어 있어도 다시 실행해 runtime, skill, agent payload를 현재 package 버전과 맞춘다. 실패하면 stderr를 그대로 사용자에게 보여준다. legacy `okstra-install.sh`로 우회하지 않는다.
44
+
45
+ ## 명령 호출 규칙
46
+
47
+ `okstra install` 이후에는 후속 명령이 모두 `okstra` literal token으로 시작해야 한다.
48
+
49
+ 허용되는 형태:
50
+
51
+ ```bash
52
+ okstra check-project --json
53
+ okstra setup --yes --project-root /abs/project --project-id my-project
54
+ okstra doctor --runtime claude-code
55
+ ```
56
+
57
+ 피해야 하는 형태:
58
+
59
+ - `export PYTHONPATH=...`
60
+ - `eval "$(okstra paths --shell)"`
61
+ - `$PROJECT_ROOT` 같은 shell 변수
62
+ - `$(...)` command substitution
63
+ - `if`, `&&`, `||`로 감싼 okstra 호출
64
+
65
+ `okstra <subcmd>`가 자체적으로 Python path를 부트스트랩하므로 `okstra paths --shell`은 이 스킬에서 쓰지 않는다. 경로가 필요하면 `okstra paths --json`을 별도 호출로 실행하고 JSON 값을 읽는다.
66
+
67
+ ## 프로젝트 루트 해석
68
+
69
+ 먼저 실행:
70
+
71
+ ```bash
72
+ okstra check-project --json
73
+ ```
74
+
75
+ 결과 처리:
76
+
77
+ - `ok: true`: 이미 등록된 프로젝트다. `projectRoot`, `projectJsonPath`, `projectId`를 사용자에게 보여주고 유지 여부를 확인한다.
78
+ - `ok: false`, `stage: "resolve"`: 사용자에게 절대 project root를 받아 `okstra check-project --cwd /abs/path --json`을 다시 실행한다.
79
+ - `ok: false`, `stage: "project_json_missing"`: 정상 생성 경로로 진행한다.
80
+ - 그 외 실패 stage: JSON의 `reason`을 그대로 보여주고, 원문이 지시하는 recovery를 따른다.
81
+
82
+ ## project.json 생성 또는 유지
83
+
84
+ `<PROJECT_ROOT>/.okstra/project.json`이 있으면 `projectId`와 `projectRoot`를 보여주고 유지/덮어쓰기 여부를 확인한다. 기본은 유지다. 기존 `projectId`를 바꾸려면 파일 삭제가 필요하므로 자동으로 덮어쓰지 않는다.
85
+
86
+ 파일이 없으면 project id를 물어본다. 답변은 비어 있지 않고 alphanumeric 문자를 하나 이상 포함해야 한다. 빈 값으로 `okstra setup --yes`를 호출하면 실패하므로 다시 묻는다.
87
+
88
+ 생성 명령:
89
+
90
+ ```bash
91
+ okstra setup --yes --project-root /abs/project --project-id my-project
92
+ ```
93
+
94
+ ## 선택 설정
95
+
96
+ 원문상 Step 4.5-4.9는 사용자가 명시적으로 원할 때만 수행하는 선택 설정이다. 기본값으로 충분하면 건너뛰고 doctor로 간다.
97
+
98
+ 선택 설정:
99
+
100
+ - `worktreeSyncDirs`: task worktree에 symlink할 프로젝트 상대 디렉터리 목록. 기본은 `.project-docs`, `.scratch`, `graphify-out`, `.claude`.
101
+ - `qaCommands`: implementation verifier가 실행할 check-only lint/format/typecheck/test 명령.
102
+ - `qaEnv`: Tier 3 conformance script가 사용할 replica/test DB, local app URL, env file, surface patterns.
103
+ - PR body template: `okstra config set pr-template-path "<path>" --scope project|global`
104
+ - final report language: `okstra config set report-language <en|ko|auto> --scope project`
105
+
106
+ `qaCommands.cmd`는 mutation을 암시하는 token을 담으면 verifier가 거부한다. deny-list의 실제 권위는 `scripts/okstra_ctl/qa_commands.py`다.
107
+
108
+ ## 자동 Claude 설정 symlink
109
+
110
+ `okstra setup`은 `<PROJECT_ROOT>/.claude/settings.local.json`을 `~/.okstra/templates/settings.local.json` symlink로 provision한다. 기존 regular file이 있으면 `.bak.<timestamp>`로 백업한 뒤 symlink를 둔다. 실패 메시지가 나오면 사용자가 기존 project-specific rule을 수동 병합해야 한다.
111
+
112
+ ## 검증
113
+
114
+ 마지막에 실행:
115
+
116
+ ```bash
117
+ okstra doctor --runtime claude-code
118
+ ```
119
+
120
+ 모든 check가 OK면 setup 완료로 보고한다. 실패가 있으면 출력 내용을 그대로 보여주고, 재설치/스킵 여부는 사용자가 결정하게 한다.
121
+
122
+ ## 완료 메시지
123
+
124
+ 짧게 다음 정보를 포함한다.
125
+
126
+ - runtime 위치: `~/.okstra`
127
+ - project metadata: `<PROJECT_ROOT>/.okstra/project.json`
128
+ - `projectId`
129
+ - 다음 단계: `/okstra-run`
130
+
131
+ ## 흔한 실패 처리
132
+
133
+ | 증상 | 처리 |
134
+ |---|---|
135
+ | `command not found: npx` | Node 18+ 설치 안내 |
136
+ | `--project-id is required` | project id를 다시 물어보고 non-empty 값으로 재실행 |
137
+ | `projectId mismatch` | 어떤 id가 canonical인지 사용자에게 확인. 자동 삭제하지 않음 |
138
+ | `.okstra/` 쓰기 EACCES | ownership/writability 문제를 설명 |
139
+ | `.claude/settings.local.json` symlink warning | 백업 파일과 symlink 상태를 사용자에게 보여주고 수동 병합 안내 |
140
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.98.1",
3
+ "version": "0.98.2",
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.98.1",
3
- "builtAt": "2026-06-22T07:33:44.995Z",
2
+ "package": "0.98.2",
3
+ "builtAt": "2026-06-22T12:25:00.654Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -393,9 +393,12 @@ Step 0):
393
393
  okstra task-list
394
394
  ```
395
395
 
396
- Parse the JSON `tasks[]` array; collect the **distinct** `taskGroup` values
397
- ordered by `updatedAt` descending. Take the **2 most-recent** distinct
398
- groups as recommendations.
396
+ Parse the JSON `tasks[]` array. The `taskGroup` field is a **raw display
397
+ name, not a slug** — `slugify` each value (lowercase + non-alphanumeric
398
+ `-`) and **dedupe by slug** so case/format variants collapse (e.g.
399
+ `uploadFont` and `uploadfont` are one group, not two). Order the distinct
400
+ slugs by their most-recent `updatedAt`. Take the **2 most-recent** distinct
401
+ slugs as recommendations and show them in slug form.
399
402
 
400
403
  `AskUserQuestion` (single-select):
401
404
 
@@ -413,8 +416,14 @@ picker and ask one free-text question:
413
416
  `"Task group (e.g. backend-api, INV-1234, refactor)"` → `task_group`.
414
417
 
415
418
  Slug rule: same as `okstra-run` Step 3 — slugify, must have at least one
416
- alphanumeric character. Note that an existing group's value is already a
417
- slug; a `직접 입력` value is slugified before use.
419
+ alphanumeric character. **Always slugify the chosen value**, whether it came
420
+ from an existing-group selection or `직접 입력` the `taskGroup` returned by
421
+ `okstra task-list` is a raw display name, not a slug, so picking one verbatim
422
+ would create a non-standard directory (e.g. `briefs/uploadFont/`). The brief's
423
+ on-disk directory segment (`briefs/<task-group>/`) and the frontmatter
424
+ `task-group` value are both the slugified form, and `validate-brief.py`
425
+ (Step 6.6) rejects any brief whose directory segment differs from
426
+ `slugify(frontmatter task-group)`.
418
427
 
419
428
  If `task_group` was already collected by Step 1b sub-step 0 (tracker
420
429
  preflight), reuse that value silently — do NOT ask again.
@@ -752,7 +761,7 @@ Use the same template per brief file. In tracker mode producing a parent
752
761
  plus N children, you write **N+1 files** (each carries only its own Source
753
762
  Material).
754
763
 
755
- The installed template `~/.okstra/templates/reports/brief.template.md` is the byte-for-byte SSOT for the brief shape — frontmatter keys, top-header blockquote, section ordering, and inline `<!-- author guidance -->` HTML comments. Render that file with the per-brief values substituted; leave any section's body as `_(none)_` rather than fabricating content. Preserve the template's HTML comments verbatim (they are part of the contract) but do NOT promote them to body prose.
764
+ The installed template `~/.okstra/templates/reports/brief.template.md` is the byte-for-byte SSOT for the brief shape — frontmatter keys, top-header blockquote, and section ordering. The template's inline `<!-- author guidance -->` HTML comments are fill-in scaffolding for the author, NOT part of the artifact: render the file with per-brief values substituted and **strip every `<!-- ... -->` comment** so the final brief carries none. Leave any section's body as `_(none)_` rather than fabricating content.
756
765
 
757
766
  **Variant note:** The template file is the canonical reporter-input shape. For the codebase-scan variant (`scope: codebase` in frontmatter), OMIT the `## Source Material` and `## Problem / Symptom` headings entirely — do NOT include them with `_(none)_` bodies. Instead, KEEP the `## Scan Scope` and `## Priority Lenses` sections (already present in the template file, marked with HTML comments). For the error-feedback variant (`scope` omitted ⇒ `reporter-input`), use the reporter-input shape: KEEP `## Source Material` (the chosen cluster's anonymized error records) and `## Problem / Symptom`, and OMIT the codebase-only `## Scan Scope` / `## Priority Lenses` sections. The remaining sections (Context / Desired Outcome / Constraints / Related Artifacts / Open Questions / Reporter Confirmations / Augmentation) apply to all variants.
758
767
 
@@ -1010,15 +1019,17 @@ started.
1010
1019
  Augmentation / interpretation goes only into the `Augmentation` section
1011
1020
  or a `> augmented:` blockquote inside the required section.
1012
1021
  - **No template author-guidance in the artifact body.** The Step 5
1013
- template above carries author guidance in HTML comments
1014
- (`<!-- ... -->`); preserve those comments when writing the brief but do
1015
- NOT promote them to body prose. Section headings stay clean — never copy
1016
- parenthetical reviewer notes onto the heading itself (e.g. emit
1022
+ template carries author guidance in HTML comments (`<!-- ... -->`);
1023
+ **strip every such comment when writing the brief** the final artifact
1024
+ contains zero `<!-- ... -->` blocks. (The validator now ignores comment
1025
+ regions, but a clean artifact is the contract, and it is what the
1026
+ passing reference briefs look like.) Section headings stay clean — never
1027
+ copy parenthetical reviewer notes onto the heading itself (e.g. emit
1017
1028
  `## Source Material`, not `## Source Material (verbatim — do not modify)`).
1018
1029
  Placeholder prose inside angle brackets (`<Background / scope / why
1019
- now…>`) is intentional and is meant to be replaced with the reporter's
1020
- actual content; do not strip the angle-bracket placeholders, replace
1021
- them.
1030
+ now…>`) is NOT a comment — it is intentional scaffolding meant to be
1031
+ replaced with the reporter's actual content; replace those placeholders,
1032
+ do not leave them in.
1022
1033
  - If the tracker MCP is not connected, do not guess — ask the user to paste
1023
1034
  the body or skip the source.
1024
1035
  - If a URL fetch fails or hits an auth wall, do not guess — ask for a
@@ -6,7 +6,8 @@ Checks performed per brief file:
6
6
  1. YAML frontmatter exists on line 1 with required keys.
7
7
  2. brief-id matches the filename stem.
8
8
  3. depth equals the number of `sub/` segments in the path (relative to the
9
- `briefs/` root).
9
+ `briefs/` root), and the task-group directory segment equals the
10
+ slugified frontmatter `task-group` (lowercase, non-alphanumeric → `-`).
10
11
  4. Every Open Questions row starts with one of the five signal prefixes
11
12
  (general | terminology | intent-check | conversion-block | adr-candidate).
12
13
  `adr-candidate:` targets okstra-internal
@@ -56,6 +57,7 @@ from okstra_ctl.improvement_lenses import (
56
57
  MAX_PRIORITY_LENSES,
57
58
  MIN_PRIORITY_LENSES,
58
59
  )
60
+ from okstra_ctl.ids import slugify_task_segment
59
61
 
60
62
  REQUIRED_FRONTMATTER_KEYS = {
61
63
  "type",
@@ -89,6 +91,23 @@ REPORTER_CONFIRMATION_VALUES = {"complete", "partial", "pending", "skipped"}
89
91
 
90
92
  SCOPE_VALUES = {"reporter-input", "codebase"}
91
93
 
94
+ _HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
95
+
96
+
97
+ def strip_html_comments(text: str) -> str:
98
+ """Remove `<!-- ... -->` blocks before parsing.
99
+
100
+ The brief template carries author-guidance comments whose example rows
101
+ (`intent-check: <…>`, `conversion-block: <…>`, `terminology-mapping: <…>`)
102
+ are NOT real entries. Without this, every `- ` collector below
103
+ (`open_questions_rows`, `augmentation_entries`, `meaningful_bullets`) would
104
+ count them as data and raise false `reporter-confirmations` / `terminology`
105
+ failures. Comments are never brief content, so stripping them once here is
106
+ the single root fix. Frontmatter uses `#` comments only, so this is a no-op
107
+ for the YAML block.
108
+ """
109
+ return _HTML_COMMENT_RE.sub("", text)
110
+
92
111
 
93
112
  def strip_yaml_comment(line: str) -> str:
94
113
  """Drop a YAML inline comment, keeping `#` that is part of a value.
@@ -268,7 +287,7 @@ def check_reporter_confirmations(
268
287
 
269
288
 
270
289
  def validate_brief(path: Path, briefs_root: Path) -> list[str]:
271
- text = path.read_text(encoding="utf-8")
290
+ text = strip_html_comments(path.read_text(encoding="utf-8"))
272
291
  errors: list[str] = []
273
292
 
274
293
  # 1. frontmatter
@@ -315,8 +334,10 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
315
334
  # 3. depth equals path's `sub/` nesting depth
316
335
  try:
317
336
  rel = path.relative_to(briefs_root)
337
+ under_root = True
318
338
  except ValueError:
319
339
  rel = path
340
+ under_root = False
320
341
  # path components after the task-group dir: any number of `sub` segments + filename
321
342
  parts = list(rel.parts)
322
343
  if len(parts) >= 2:
@@ -332,6 +353,19 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
332
353
  f"frontmatter says depth={fm.get('depth')!r}"
333
354
  )
334
355
 
356
+ # 3b. task-group directory segment must equal the slugified frontmatter value.
357
+ # Catches case/format drift (e.g. dir `uploadFont` vs slug `uploadfont`) that
358
+ # case-insensitive filesystems hide but case-sensitive CI breaks on.
359
+ if under_root and len(parts) >= 2:
360
+ expected_segment = slugify_task_segment(fm.get("task-group", ""))
361
+ if expected_segment and parts[0] != expected_segment:
362
+ errors.append(
363
+ f"task-group directory segment {parts[0]!r} does not match the "
364
+ f"slugified frontmatter task-group {expected_segment!r} "
365
+ f"(frontmatter task-group={fm.get('task-group')!r}); rename the "
366
+ f"directory or fix the frontmatter so both use the slug"
367
+ )
368
+
335
369
  # 4. Open Questions prefixes
336
370
  oq_rows = open_questions_rows(text)
337
371
  intent_check_rows: list[str] = []