okstra 0.111.0 → 0.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.kr.md +2 -2
  2. package/README.md +2 -2
  3. package/bin/okstra +7 -1
  4. package/docs/for-ai/README.md +2 -2
  5. package/docs/for-ai/skills/okstra-brief.md +2 -3
  6. package/docs/for-ai/skills/okstra-container-build.md +2 -3
  7. package/docs/for-ai/skills/okstra-inspect.md +5 -12
  8. package/docs/for-ai/skills/okstra-rollup.md +3 -4
  9. package/docs/for-ai/skills/okstra-run.md +3 -5
  10. package/docs/for-ai/skills/okstra-schedule.md +2 -7
  11. package/docs/for-ai/skills/okstra-setup.md +1 -1
  12. package/docs/kr/architecture/storage-model.md +1 -2
  13. package/docs/kr/architecture.md +2 -2
  14. package/docs/kr/cli.md +4 -2
  15. package/docs/project-structure-overview.md +5 -3
  16. package/package.json +1 -1
  17. package/runtime/BUILD.json +2 -2
  18. package/runtime/agents/workers/antigravity-worker.md +1 -1
  19. package/runtime/agents/workers/claude-worker.md +1 -1
  20. package/runtime/agents/workers/codex-worker.md +1 -1
  21. package/runtime/prompts/coding-preflight/overview.md +3 -1
  22. package/runtime/prompts/launch.template.md +1 -5
  23. package/runtime/prompts/lead/context-loader.md +2 -4
  24. package/runtime/prompts/lead/convergence.md +11 -240
  25. package/runtime/prompts/lead/okstra-lead-contract.md +70 -34
  26. package/runtime/prompts/lead/plan-body-verification.md +240 -0
  27. package/runtime/prompts/lead/report-writer.md +7 -7
  28. package/runtime/prompts/lead/team-contract.md +15 -17
  29. package/runtime/prompts/profiles/_common-contract.md +1 -37
  30. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  31. package/runtime/prompts/profiles/_implementation-self-check.md +1 -0
  32. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  33. package/runtime/prompts/profiles/final-verification.md +6 -5
  34. package/runtime/prompts/profiles/implementation-planning.md +6 -4
  35. package/runtime/prompts/profiles/implementation.md +1 -1
  36. package/runtime/prompts/profiles/improvement-discovery.md +1 -0
  37. package/runtime/prompts/profiles/requirements-discovery.md +1 -0
  38. package/runtime/python/okstra_ctl/path_hints.py +1 -0
  39. package/runtime/python/okstra_ctl/paths.py +3 -0
  40. package/runtime/python/okstra_ctl/render.py +8 -0
  41. package/runtime/python/okstra_ctl/set_work_status.py +147 -0
  42. package/runtime/python/okstra_ctl/team_reconcile.py +1 -1
  43. package/runtime/schemas/final-report-v1.0.schema.json +6 -4
  44. package/runtime/skills/okstra-brief/SKILL.md +32 -176
  45. package/runtime/skills/okstra-brief/references/reporter-confirmations.md +71 -0
  46. package/runtime/skills/okstra-brief/references/tracker-recursion.md +90 -0
  47. package/runtime/skills/okstra-container-build/SKILL.md +7 -20
  48. package/runtime/skills/okstra-graphify/SKILL.md +8 -8
  49. package/runtime/skills/okstra-inspect/SKILL.md +27 -43
  50. package/runtime/skills/okstra-rollup/SKILL.md +6 -6
  51. package/runtime/skills/okstra-run/SKILL.md +7 -16
  52. package/runtime/skills/okstra-schedule/SKILL.md +64 -419
  53. package/runtime/skills/okstra-setup/SKILL.md +25 -223
  54. package/runtime/skills/okstra-setup/references/project-config.md +188 -0
  55. package/runtime/templates/reports/final-report.template.md +24 -1
  56. package/runtime/templates/reports/improvement-discovery-input.template.md +3 -2
  57. package/runtime/templates/reports/schedule.template.md +2 -2
  58. package/runtime/templates/worker-prompt-preamble.md +5 -1
  59. package/runtime/validators/validate-run.py +8 -7
  60. package/src/cli-registry.mjs +14 -0
  61. package/src/commands/inspect/set-work-status.mjs +31 -0
  62. package/src/commands/lifecycle/check-project.mjs +68 -56
  63. package/src/commands/lifecycle/install.mjs +1 -0
  64. package/src/commands/lifecycle/preflight.mjs +82 -0
@@ -0,0 +1,147 @@
1
+ """task-manifest.json 의 workStatus 를 갱신하는 단일 CLI 진입점.
2
+
3
+ okstra-inspect status.4 가 Edit 도구로 하던 수동 JSON 편집(키 순서·개행 보존
4
+ 규칙을 프롬프트로 강제)을 CLI 로 수렴시킨다. 직렬화는 render._write_json 과
5
+ 동일한 json.dumps(indent=2, ensure_ascii=False) + "\n" 이므로 재렌더와 byte
6
+ 규칙이 일치한다. discovery/task-catalog.json 은 여기서 재생성하지 않는다 —
7
+ 다음 run 렌더가 manifest 를 재투영할 때까지 stale 할 수 있다.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ from okstra_ctl.ids import slugify_task_segment
18
+ from okstra_project import (
19
+ ResolverError,
20
+ StateError,
21
+ resolve_project_root,
22
+ resolve_task_reference,
23
+ )
24
+
25
+ ALLOWED_WORK_STATUSES = ("todo", "in-progress", "blocked", "done")
26
+
27
+
28
+ def _emit(payload: dict) -> None:
29
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
30
+
31
+
32
+ def _manifest_path(project_root: Path, entry: dict) -> Path:
33
+ rel = entry.get("taskManifestPath") or ""
34
+ if rel:
35
+ candidate = Path(rel)
36
+ return candidate if candidate.is_absolute() else project_root / candidate
37
+ group_seg = entry.get("taskGroupPathSegment") or slugify_task_segment(
38
+ entry.get("taskGroup", "")
39
+ )
40
+ id_seg = entry.get("taskIdPathSegment") or slugify_task_segment(
41
+ entry.get("taskId", "")
42
+ )
43
+ return project_root / ".okstra" / "tasks" / group_seg / id_seg / "task-manifest.json"
44
+
45
+
46
+ def main(argv: list[str] | None = None) -> int:
47
+ parser = argparse.ArgumentParser(
48
+ description="Set a task's user-managed workStatus in task-manifest.json."
49
+ )
50
+ parser.add_argument(
51
+ "token",
52
+ help="full task-key (<project-id>:<task-group>:<task-id>) or bare task-id",
53
+ )
54
+ parser.add_argument("status", choices=ALLOWED_WORK_STATUSES)
55
+ parser.add_argument(
56
+ "--note",
57
+ default=None,
58
+ help="set workStatusNote; omit to leave any existing note untouched",
59
+ )
60
+ parser.add_argument(
61
+ "--task-group", default="", help="scope a bare task-id match to this task-group"
62
+ )
63
+ parser.add_argument("--project-root", default="", help="project root for catalog lookup")
64
+ parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
65
+ parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
66
+ args = parser.parse_args(argv)
67
+
68
+ try:
69
+ project_root = resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
70
+ except ResolverError as exc:
71
+ _emit({"ok": False, "stage": "resolve", "reason": str(exc)})
72
+ return 2
73
+
74
+ token = args.token
75
+ task_group = args.task_group
76
+ if token.count(":") == 2:
77
+ _, key_group, key_id = token.split(":")
78
+ token = key_id
79
+ task_group = key_group or task_group
80
+
81
+ try:
82
+ matches = resolve_task_reference(
83
+ project_root, token, task_group=task_group or None
84
+ )
85
+ except StateError as exc:
86
+ _emit({"ok": False, "stage": "catalog", "reason": str(exc)})
87
+ return 2
88
+
89
+ if not matches:
90
+ _emit({"ok": False, "stage": "not-found", "token": args.token})
91
+ return 1
92
+ if len(matches) > 1:
93
+ _emit({"ok": False, "stage": "ambiguous", "matches": matches})
94
+ return 2
95
+
96
+ entry = matches[0]
97
+ manifest_path = _manifest_path(project_root, entry)
98
+ if not manifest_path.exists():
99
+ _emit(
100
+ {
101
+ "ok": False,
102
+ "stage": "manifest-missing",
103
+ "taskKey": entry.get("taskKey", ""),
104
+ "taskManifestPath": str(manifest_path),
105
+ }
106
+ )
107
+ return 1
108
+ try:
109
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
110
+ except ValueError as exc:
111
+ _emit(
112
+ {
113
+ "ok": False,
114
+ "stage": "manifest-invalid",
115
+ "taskManifestPath": str(manifest_path),
116
+ "reason": str(exc),
117
+ }
118
+ )
119
+ return 1
120
+
121
+ previous = manifest.get("workStatus", "")
122
+ manifest["workStatus"] = args.status
123
+ manifest["workStatusUpdatedAt"] = datetime.now(timezone.utc).strftime(
124
+ "%Y-%m-%dT%H:%M:%SZ"
125
+ )
126
+ if args.note is not None:
127
+ manifest["workStatusNote"] = args.note
128
+ manifest_path.write_text(
129
+ json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
130
+ )
131
+
132
+ _emit(
133
+ {
134
+ "ok": True,
135
+ "taskKey": entry.get("taskKey", ""),
136
+ "previousWorkStatus": previous,
137
+ "workStatus": args.status,
138
+ "workStatusUpdatedAt": manifest["workStatusUpdatedAt"],
139
+ "workStatusNote": manifest.get("workStatusNote", ""),
140
+ "taskManifestPath": str(manifest_path),
141
+ }
142
+ )
143
+ return 0
144
+
145
+
146
+ if __name__ == "__main__":
147
+ raise SystemExit(main(sys.argv[1:]))
@@ -15,7 +15,7 @@ recorded pane — those are left for graceful shutdown.
15
15
  CC v2.1.178 removed ``TeamDelete``: the implicit team ends with the session and
16
16
  run-end teardown only dismisses teammates via ``shutdown_request``. Reconcile
17
17
  keeps the roster honest so a dismissed-but-dead member does not linger as a
18
- live pill — see _common-contract.md "Run-end teammate teardown".
18
+ live pill — see okstra-lead-contract.md "Run-scoped pane & teammate lifecycle".
19
19
 
20
20
  Roster resolution (``--project-root``) exists because ``team-state.lead.sessionId``
21
21
  is a phase-3 snapshot: Claude Code re-issues the session id across resume /
@@ -696,7 +696,7 @@
696
696
  "properties": {
697
697
  "target": { "type": "string" },
698
698
  "performed": { "type": "string" },
699
- "result": { "type": "string" },
699
+ "result": { "enum": ["pass", "fail", "blocked"] },
700
700
  "observed": { "type": "string" }
701
701
  }
702
702
  }
@@ -1691,16 +1691,18 @@
1691
1691
  "number": { "type": "integer", "minimum": 1 },
1692
1692
  "tier": { "enum": [1, 2] },
1693
1693
  "command": { "type": "string", "minLength": 1 },
1694
- "status": { "enum": ["executed", "rejected", "not-configured"] },
1694
+ "status": { "enum": ["executed", "rejected", "not-configured", "env-unavailable"] },
1695
1695
  "exitCode": { "type": ["integer", "null"] },
1696
- "rejectionReason": { "type": ["string", "null"] },
1696
+ "statusReason": { "type": ["string", "null"] },
1697
1697
  "outputTail": { "type": "string" }
1698
1698
  },
1699
1699
  "allOf": [
1700
1700
  { "if": { "properties": { "status": { "const": "executed" } } },
1701
1701
  "then": { "properties": { "exitCode": { "type": "integer" } } } },
1702
1702
  { "if": { "properties": { "status": { "const": "rejected" } } },
1703
- "then": { "required": ["rejectionReason"] } }
1703
+ "then": { "required": ["statusReason"] } },
1704
+ { "if": { "properties": { "status": { "const": "env-unavailable" } } },
1705
+ "then": { "required": ["statusReason"] } }
1704
1706
  ]
1705
1707
  },
1706
1708
 
@@ -117,26 +117,15 @@ as source material, and never writes them. Decision files (when raised by
117
117
  via `implementation`):
118
118
  `<PROJECT_ROOT>/.okstra/decisions/<NNNN>-<slug>.md`.
119
119
 
120
- ## Step 0: Resolve project root
120
+ ## Step 0: Preflight
121
121
 
122
- Reuse the same literal-token-first rule as `okstra-run`. Each command below is a **separate Bash tool call** starting with the literal token `okstra` so the `Bash(okstra:*)` permission match succeeds. Do **not** wrap any of them in `if`, `eval`, `export`, `$(...)`, `VAR=...`, `||`, or `&&`, and do **not** introduce a `$OKSTRA_CMD` variable or an `npx -y okstra@latest` fallback — those leading tokens defeat the permission match and force a confirmation prompt on every call.
122
+ Every Bash command in this skill is a **separate Bash tool call** starting with the literal token `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` a non-literal leading token defeats the `Bash(okstra:*)` permission match).
123
123
 
124
124
  ```bash
125
- okstra ensure-installed --runtime claude-code
125
+ okstra preflight --runtime claude-code --json
126
126
  ```
127
127
 
128
- If this exits non-zero, tell the user: "okstra not installed — run `/okstra-setup` first." Then stop. Do **not** try to invoke `npx -y okstra@latest ...` as a fallback.
129
-
130
- ```bash
131
- okstra check-project --json
132
- ```
133
-
134
- Parse the JSON from stdout:
135
-
136
- - `ok: true` → use `projectRoot` from the JSON output. Carry it as a literal string into the steps below.
137
- - `ok: false` → tell the user: "this project has no okstra setup. Run `/okstra-setup` first." Then stop.
138
-
139
- > If the `okstra` binary is not on `PATH` at all, the commands above will not run. In that case tell the user verbatim: "okstra not installed — run `/okstra-setup` first." Do **not** try to invoke `npx -y okstra@latest ...` from this skill — `npx` is not on the literal-token allow-list this skill targets and will force a confirmation prompt on every subsequent call.
128
+ Parse the stdout JSON. `ok: true` → carry `projectRoot` as a literal string into the steps below. `ok: false` (or `okstra` not on `PATH` at all) → tell the user: "okstra not set up — run `/okstra-setup` first." Then stop. If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary). Do **not** try `npx -y okstra@latest ...` as a fallback.
140
129
 
141
130
  ## Step 1: Choose input source
142
131
 
@@ -234,86 +223,15 @@ Order of operations:
234
223
  preserved)`).
235
224
 
236
225
  6. **Sub-ticket / child-issue / sub-task handling — recursive**:
237
- - Look at child references in the fetched ticket response:
238
- - Linear: `children` / `subIssues` field
239
- - Jira: `subtasks` (or `issuelinks` with the `is parent of` relation)
240
- - GitHub: `gh issue view --json title,body,comments,labels,state` only
241
- returns the issue body and comments child relations are NOT in that
242
- JSON. Resolve children via two paths:
243
- 1. **task-list parsing** scan the fetched body and comments for
244
- markdown task-list checkboxes referencing other issues
245
- (`- [ ] #NNN`, `- [x] owner/repo#NNN`, `- [ ] https://github.com/...`).
246
- Capture each as a child candidate.
247
- 2. **tracked / sub-issue GraphQL** — for GitHub Projects v2
248
- "tracked issues" / sub-issue relations, the REST `gh issue view`
249
- response does NOT carry them. Query GraphQL explicitly, e.g.
250
- `gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){issue(number:$n){trackedIssues(first:50){nodes{number repository{nameWithOwner}}} subIssues:trackedInIssues(first:50){nodes{number repository{nameWithOwner}}}}}}' -f o=<owner> -f r=<repo> -F n=<num>`.
251
- If the GraphQL request fails (auth scope missing, feature
252
- unavailable on this repo, network error), **do NOT silently
253
- continue with full recursion**. Disable the "Generate the full
254
- tree recursively" option for this branch, default to
255
- `Parent only; list children in Related Artifacts and Related Task
256
- Graph`, and surface the GraphQL failure to the user in one line so
257
- they can decide whether to paste child bodies manually.
258
- - Notion: child pages / sub-pages
259
- - If there is **one or more** child, ask **once at the top parent**:
260
- - `AskUserQuestion` (single-select)
261
- - **Label**: `"This ticket has sub-tickets. How should the child tree be handled?"`
262
- - **Options**:
263
- 1. `Full tree` (recommended) — walk children, grandchildren, …
264
- via BFS/DFS and emit one brief per node.
265
- 2. `Parent only` — single brief. Children are not fetched; their
266
- keys/URLs are recorded under Related Artifacts and as `parent-of`
267
- rows in Related Task Graph.
268
- 3. `Selected` — multi-select the direct children; for each chosen
269
- child, apply option-1 policy recursively to that branch.
270
- - For options 1 / 3, **recurse into Step 1b sub-steps 3–5** for every
271
- descendant. No depth limit. Maintain a visited set of
272
- `<tracker>:<ticket-id>` to prevent cycles; on revisit, do not emit a
273
- new brief — only add a link back to the existing brief.
274
- - **Visited-set across re-runs (rebuild from disk)** — the in-memory
275
- visited set is per-run and disappears between invocations. When this
276
- skill runs again over a tree that already has briefs on disk, it MUST
277
- reseed the visited set by scanning
278
- `<PROJECT_ROOT>/.okstra/briefs/<task-group>/` recursively
279
- and reading each brief's frontmatter `ticket-id` + `source-type`.
280
- Reseed precedence:
281
- 1. On-disk frontmatter (`ticket-id` ≠ "") populates visited entries
282
- as `<source-type>:<ticket-id>` — these win.
283
- 2. Step 2c collision policy (`Skip` / `Append timestamp` /
284
- `Overwrite`) applies to any node whose path is already taken.
285
- 3. The fresh in-memory walk fills any gaps for tickets not yet on
286
- disk.
287
- A ticket present on disk is NOT refetched unless the user explicitly
288
- answers `Overwrite` for that path.
289
- - Each descendant's brief file at depth N is created under
290
- `<task-group>/<sub/ nested N times>/<ticket-id>-<file-title>.md` per
291
- Step 2b.
292
- - Each node's `Related Artifacts` should list:
293
- - The parent brief's relative path (`../<ticket-id>-<file-title>.md`)
294
- - All direct children briefs' relative paths
295
- (`sub/<ticket-id>-<file-title>.md`)
296
- bidirectionally. (Non-direct ancestors/descendants are tracked via the
297
- frontmatter `parent-id` chain.)
298
- - Each generated brief's `Related Task Graph` should contain the same full
299
- split topology, not only the current node's direct edges, so downstream
300
- phases can start from any child brief without losing order or dependency
301
- context. Use one row per source-backed edge with these columns:
302
- `From | Relation | To | Direction | Source | Impact`.
303
- - `From` / `To` use task key, brief id, tracker id, or URL. The edge
304
- direction is `From` → `To`.
305
- - `Relation` is one of `parent-of`, `child-of`, `depends-on`, `blocks`,
306
- `blocked-by`, `follow-up-of`, `split-from`, `duplicates`, `related-to`.
307
- - `Direction` is `directed` for parentage/ordering relations and
308
- `undirected` for `duplicates` / `related-to`.
309
- - `Source` names the evidence for the edge: tracker linked issue,
310
- markdown task-list checkbox, reporter statement, manual split, or prior
311
- okstra task.
312
- - `Impact` states what the next phase must preserve, such as "run after
313
- API contract" or "avoid duplicate implementation".
314
- Do not invent graph edges from filename similarity or topic overlap; if a
315
- relation is unclear, use `related-to` + `undirected` only when the source
316
- explicitly says the items are related.
226
+ - If the fetched ticket response shows **any** child reference (Linear
227
+ `children`/`subIssues`, Jira `subtasks`/`issuelinks`, GitHub task-list
228
+ checkboxes or tracked/sub-issues, Notion child pages), **read
229
+ `references/tracker-recursion.md` next to this SKILL.md and follow it**
230
+ before writing any briefit defines per-tracker child discovery
231
+ (including the GitHub GraphQL query and its failure handling), the
232
+ Full tree / Parent only / Selected picker, the cross-run visited-set
233
+ reseed from on-disk brief frontmatter, and the Related Artifacts /
234
+ Related Task Graph topology every emitted brief must carry.
317
235
  - If there are no children, proceed with a single brief (depth 0).
318
236
 
319
237
  ### 1c. `Link URL`
@@ -584,13 +502,11 @@ project's domain.
584
502
 
585
503
  ## Step 4: Fill in missing fields (NO full interview)
586
504
 
587
- > **Verbatim-source rule**: Source Material collected in Step 1 must never
588
- > be paraphrased, summarized, or restructured. When supplementing missing
589
- > information, write only into the dedicated `Augmentation` section (see
590
- > Step 5); never modify Source Material by a single byte. **Anything
591
- > learned by grilling in this step lands in `Augmentation` or `Open
592
- > Questions` only.** brief is a pre-discovery artifact, not a PRD —
593
- > decisions belong to later phases.
505
+ > **Verbatim-source rule** (the first non-negotiable at the top of this
506
+ > file): never modify Source Material by a single byte. **Anything learned
507
+ > by grilling in this step lands in `Augmentation` or `Open Questions`
508
+ > only.** brief is a pre-discovery artifact, not a PRD decisions belong
509
+ > to later phases.
594
510
 
595
511
  Inspect the synthesized brief draft. For each REQUIRED section below that is
596
512
  empty or trivially thin **after** reading Source Material verbatim, ask **at
@@ -903,72 +819,16 @@ If the list is non-empty, run **one** `AskUserQuestion`:
903
819
  `implementation-planning`); see each phase profile's "Brief
904
820
  consumption" addendum.
905
821
 
906
- ### 6.5c. Batch the questions
907
-
908
- For each pending row, formulate one question. Because the
909
- `AskUserQuestion` tool caps options at 4 per call and questions per call
910
- also at 4, split into batches of ≤ 4 questions.
911
-
912
- **Cap — 12 questions per Step 6.5 run.** When the pending list
913
- exceeds 12 rows, sort by priority and ask only the top 12 this round:
914
-
915
- 1. `conversion-block:` rows first (translation failure — highest
916
- downstream risk).
917
- 2. `intent-check:` rows next, in the order they appear in `Open
918
- Questions`.
919
-
920
- Any rows beyond the cap are left unanswered in this run; set
921
- `reporter-confirmations: partial` in the frontmatter and tell the user
922
- which rows remain. The next `okstra-brief` re-run (or the downstream
923
- phase that consumes `Blocks=next-phase`) handles the remainder.
924
-
925
- Each question carries:
926
-
927
- - The reporter-facing phrasing (plain language, no jargon).
928
- - A recommended answer drawn from the brief's `intent-inference` /
929
- `conversion-block:` content, marked `(Recommended)`.
930
- - 2–3 alternative options when the answer space is naturally enumerable;
931
- otherwise a free-text fallback.
932
-
933
- ### 6.5d. Record verbatim
934
-
935
- Create a new top-level section `## Reporter Confirmations` in the brief
936
- (if not present) with one subsection per answered row:
937
-
938
- ```markdown
939
- ## Reporter Confirmations
940
-
941
- ### RC-1 — <intent-check: or conversion-block: row id / topic>
942
- - asked: <YYYY-MM-DD HH:MM>
943
- - linked-row: `<exact Open Questions row text>`
944
- - answer (verbatim):
945
-
946
- > <reporter's answer, byte-for-byte>
947
- ```
948
-
949
- Mark the linked `Open Questions` row by appending `[CONFIRMED <YYYY-MM-DD> → RC-N]` to the end of the row's line. Do NOT delete the row — the
950
- `CONFIRMED` marker keeps the audit trail intact.
951
-
952
- For every `intent-inference` augmentation whose paired `intent-check:`
953
- was confirmed, add one new line directly below the `> augmented:
954
- intent-inference …` blockquote:
955
-
956
- ```
957
- > confirmed-by-reporter: RC-N — <one-line summary of confirmation>
958
- ```
959
-
960
- This converts the inference from "unverified hypothesis" to "verified
961
- hypothesis" without rewriting the original augmentation.
962
-
963
- ### 6.5e. Frontmatter state
964
-
965
- Update `reporter-confirmations` in the brief frontmatter:
822
+ ### 6.5c–e. Collect, record, and set frontmatter state
966
823
 
967
- - `complete` every pending row was answered.
968
- - `partial` some rows answered, some skipped within this run.
969
- - `skipped` user picked option 2 in 6.5b.
970
- - `pending` should never appear at this point; if it does, treat the
971
- brief as not yet handed off and refuse to proceed.
824
+ When 6.5b answered "Yes collect now", **read
825
+ `references/reporter-confirmations.md` next to this SKILL.md and follow it**:
826
+ it defines the 4-question batching, the 12-question cap with
827
+ `conversion-block:`-first priority, the `## Reporter Confirmations` RC-N
828
+ record shape with `[CONFIRMED <date> RC-N]` row markers and
829
+ `> confirmed-by-reporter:` blockquote lines, and the final
830
+ `reporter-confirmations` frontmatter value (`complete` / `partial` /
831
+ `skipped`).
972
832
 
973
833
  Step 6.5 ends here. The brief now has either zero pending reporter-only
974
834
  rows (`complete` / `partial` with remainder in the same file) or an
@@ -1038,11 +898,11 @@ started.
1038
898
  stays under `<PROJECT_ROOT>/.okstra/` — briefs in `.okstra/briefs/`, glossary
1039
899
  in `.okstra/glossary.md` (Step 4.5). Paths outside that subtree are read-only
1040
900
  source material, and only when the reporter cited them.
1041
- - **Verbatim source**: never paraphrase, summarize, restructure, or reorder
1042
- the Source Material section. Only format conversion (ADF MD, HTML → MD)
1043
- is allowed, and the conversion must be annotated in the `format:` meta.
1044
- Augmentation / interpretation goes only into the `Augmentation` section
1045
- or a `> augmented:` blockquote inside the required section.
901
+ - **Verbatim source** (the two non-negotiables at the top of this file
902
+ govern): no line budget overrides them if source content is large,
903
+ preserve it exactly as separate `Source Material` entries; if a tool
904
+ returns a truncated body, ask the user for the missing content or emit a
905
+ `conversion-block:` row. Never silently excerpt or summarize.
1046
906
  - **No template author-guidance in the artifact body.** The Step 5
1047
907
  template carries author guidance in HTML comments (`<!-- ... -->`);
1048
908
  **strip every such comment when writing the brief** — the final artifact
@@ -1061,10 +921,6 @@ started.
1061
921
  paste.
1062
922
  - Never fabricate content for empty sections; use `_(none)_`.
1063
923
  - Echo each `AskUserQuestion` outcome on one short line.
1064
- - No line budget overrides the verbatim-source rule. If source content is
1065
- large, preserve it exactly as separate `Source Material` entries; if a
1066
- tool only returns a truncated body, ask the user for the missing content
1067
- or emit a `conversion-block:` row. Never silently excerpt or summarize.
1068
924
 
1069
925
  ## Failure Modes
1070
926
 
@@ -0,0 +1,71 @@
1
+ # Reporter batch confirmation — collection procedure (Step 6.5c–e)
2
+
3
+ Read this only when Step 6.5b answered "Yes — collect now" for a non-empty
4
+ pending list of `intent-check:` / `conversion-block:` rows.
5
+
6
+ ## 6.5c. Batch the questions
7
+
8
+ For each pending row, formulate one question. Because the `AskUserQuestion`
9
+ tool caps options at 4 per call and questions per call also at 4, split into
10
+ batches of ≤ 4 questions.
11
+
12
+ **Cap — 12 questions per Step 6.5 run.** When the pending list exceeds 12
13
+ rows, sort by priority and ask only the top 12 this round:
14
+
15
+ 1. `conversion-block:` rows first (translation failure — highest downstream
16
+ risk).
17
+ 2. `intent-check:` rows next, in the order they appear in `Open Questions`.
18
+
19
+ Any rows beyond the cap are left unanswered in this run; set
20
+ `reporter-confirmations: partial` in the frontmatter and tell the user which
21
+ rows remain. The next `okstra-brief` re-run (or the downstream phase that
22
+ consumes `Blocks=next-phase`) handles the remainder.
23
+
24
+ Each question carries:
25
+
26
+ - The reporter-facing phrasing (plain language, no jargon).
27
+ - A recommended answer drawn from the brief's `intent-inference` /
28
+ `conversion-block:` content, marked `(Recommended)`.
29
+ - 2–3 alternative options when the answer space is naturally enumerable;
30
+ otherwise a free-text fallback.
31
+
32
+ ## 6.5d. Record verbatim
33
+
34
+ Create a new top-level section `## Reporter Confirmations` in the brief (if
35
+ not present) with one subsection per answered row:
36
+
37
+ ```markdown
38
+ ## Reporter Confirmations
39
+
40
+ ### RC-1 — <intent-check: or conversion-block: row id / topic>
41
+ - asked: <YYYY-MM-DD HH:MM>
42
+ - linked-row: `<exact Open Questions row text>`
43
+ - answer (verbatim):
44
+
45
+ > <reporter's answer, byte-for-byte>
46
+ ```
47
+
48
+ Mark the linked `Open Questions` row by appending
49
+ `[CONFIRMED <YYYY-MM-DD> → RC-N]` to the end of the row's line. Do NOT delete
50
+ the row — the `CONFIRMED` marker keeps the audit trail intact.
51
+
52
+ For every `intent-inference` augmentation whose paired `intent-check:` was
53
+ confirmed, add one new line directly below the `> augmented: intent-inference …`
54
+ blockquote:
55
+
56
+ ```
57
+ > confirmed-by-reporter: RC-N — <one-line summary of confirmation>
58
+ ```
59
+
60
+ This converts the inference from "unverified hypothesis" to "verified
61
+ hypothesis" without rewriting the original augmentation.
62
+
63
+ ## 6.5e. Frontmatter state
64
+
65
+ Update `reporter-confirmations` in the brief frontmatter:
66
+
67
+ - `complete` — every pending row was answered.
68
+ - `partial` — some rows answered, some skipped within this run.
69
+ - `skipped` — user picked option 2 in 6.5b.
70
+ - `pending` — should never appear at this point; if it does, treat the brief
71
+ as not yet handed off and refuse to proceed.
@@ -0,0 +1,90 @@
1
+ # Sub-ticket / child-issue recursion (tracker sources)
2
+
3
+ Read this only when a fetched tracker ticket shows one or more child references
4
+ (okstra-brief Step 1b sub-step 6). It defines how the child tree is discovered,
5
+ which briefs are emitted, and how re-runs stay idempotent.
6
+
7
+ ## 1. Discover child references per tracker
8
+
9
+ - Linear: `children` / `subIssues` field.
10
+ - Jira: `subtasks` (or `issuelinks` with the `is parent of` relation).
11
+ - GitHub: `gh issue view --json title,body,comments,labels,state` only returns
12
+ the issue body and comments — child relations are NOT in that JSON. Resolve
13
+ children via two paths:
14
+ 1. **task-list parsing** — scan the fetched body and comments for markdown
15
+ task-list checkboxes referencing other issues (`- [ ] #NNN`,
16
+ `- [x] owner/repo#NNN`, `- [ ] https://github.com/...`). Capture each as a
17
+ child candidate.
18
+ 2. **tracked / sub-issue GraphQL** — for GitHub Projects v2 "tracked issues" /
19
+ sub-issue relations, query GraphQL explicitly, e.g.
20
+ `gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){issue(number:$n){trackedIssues(first:50){nodes{number repository{nameWithOwner}}} subIssues:trackedInIssues(first:50){nodes{number repository{nameWithOwner}}}}}}' -f o=<owner> -f r=<repo> -F n=<num>`.
21
+ If the GraphQL request fails (auth scope missing, feature unavailable,
22
+ network error), do NOT silently continue with full recursion: disable the
23
+ "Full tree" option for this branch, default to `Parent only`, and surface
24
+ the GraphQL failure to the user in one line so they can decide whether to
25
+ paste child bodies manually.
26
+ - Notion: child pages / sub-pages.
27
+
28
+ ## 2. Ask once at the top parent
29
+
30
+ If there is one or more child, `AskUserQuestion` (single-select):
31
+
32
+ - **Label**: `"This ticket has sub-tickets. How should the child tree be handled?"`
33
+ - **Options**:
34
+ 1. `Full tree` (recommended) — walk children, grandchildren, … via BFS/DFS
35
+ and emit one brief per node.
36
+ 2. `Parent only` — single brief. Children are not fetched; their keys/URLs are
37
+ recorded under Related Artifacts and as `parent-of` rows in Related Task
38
+ Graph.
39
+ 3. `Selected` — multi-select the direct children; for each chosen child,
40
+ apply option-1 policy recursively to that branch.
41
+
42
+ For options 1 / 3, recurse into Step 1b sub-steps 3–5 for every descendant. No
43
+ depth limit. Maintain a visited set of `<tracker>:<ticket-id>` to prevent
44
+ cycles; on revisit, do not emit a new brief — only add a link back to the
45
+ existing brief.
46
+
47
+ ## 3. Visited-set across re-runs (rebuild from disk)
48
+
49
+ The in-memory visited set is per-run. When this skill runs again over a tree
50
+ that already has briefs on disk, reseed the visited set by scanning
51
+ `<PROJECT_ROOT>/.okstra/briefs/<task-group>/` recursively and reading each
52
+ brief's frontmatter `ticket-id` + `source-type`. Reseed precedence:
53
+
54
+ 1. On-disk frontmatter (`ticket-id` ≠ "") populates visited entries as
55
+ `<source-type>:<ticket-id>` — these win.
56
+ 2. Step 2c collision policy (`Skip` / `Append timestamp` / `Overwrite`) applies
57
+ to any node whose path is already taken.
58
+ 3. The fresh in-memory walk fills any gaps for tickets not yet on disk.
59
+
60
+ A ticket present on disk is NOT refetched unless the user explicitly answers
61
+ `Overwrite` for that path.
62
+
63
+ ## 4. Per-node output
64
+
65
+ - Each descendant's brief file at depth N is created under
66
+ `<task-group>/<sub/ nested N times>/<ticket-id>-<file-title>.md` per Step 2b.
67
+ - Each node's `Related Artifacts` lists, bidirectionally:
68
+ - the parent brief's relative path (`../<ticket-id>-<file-title>.md`)
69
+ - all direct children briefs' relative paths
70
+ (`sub/<ticket-id>-<file-title>.md`)
71
+ (Non-direct ancestors/descendants are tracked via the frontmatter
72
+ `parent-id` chain.)
73
+ - Each generated brief's `Related Task Graph` contains the same **full split
74
+ topology**, not only the current node's direct edges, so downstream phases
75
+ can start from any child brief without losing order or dependency context.
76
+ One row per source-backed edge with columns
77
+ `From | Relation | To | Direction | Source | Impact`:
78
+ - `From` / `To` use task key, brief id, tracker id, or URL; edge direction is
79
+ `From` → `To`.
80
+ - `Relation` is one of `parent-of`, `child-of`, `depends-on`, `blocks`,
81
+ `blocked-by`, `follow-up-of`, `split-from`, `duplicates`, `related-to`.
82
+ - `Direction` is `directed` for parentage/ordering relations, `undirected`
83
+ for `duplicates` / `related-to`.
84
+ - `Source` names the evidence for the edge: tracker linked issue, markdown
85
+ task-list checkbox, reporter statement, manual split, or prior okstra task.
86
+ - `Impact` states what the next phase must preserve (e.g. "run after API
87
+ contract", "avoid duplicate implementation").
88
+ Do not invent graph edges from filename similarity or topic overlap; if a
89
+ relation is unclear, use `related-to` + `undirected` only when the source
90
+ explicitly says the items are related.