okstra 0.28.0 → 0.30.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.
@@ -73,12 +73,93 @@ GEMINI_MODEL_OPTIONS = ["default", "gemini-3-pro-preview", "gemini-3-flash-previ
73
73
  # special pick value: start a brand-new task
74
74
  TASK_PICK_NEW_TOKEN = "__new__"
75
75
 
76
+ # Pick-vs-free-text tokens shared by suggestion-aware prompts.
77
+ PICK_USE_SUGGESTED = "__use_suggested__"
78
+ PICK_TYPE_CUSTOM = "__free_input__"
79
+
80
+ # Lines of `key: value` we pull from a brief markdown frontmatter. The
81
+ # parser is intentionally lightweight (no yaml dep) and tolerant — a
82
+ # malformed brief returns an empty dict.
83
+ _BRIEF_FRONTMATTER_LINE_RE = re.compile(r"^([a-zA-Z0-9_\-]+)\s*:\s*(.*)$")
84
+
85
+
86
+ def _parse_brief_frontmatter(path: Path) -> dict[str, str]:
87
+ """Read the YAML-style frontmatter at the top of a brief markdown file
88
+ and return a flat ``{key: value}`` map.
89
+
90
+ Returns ``{}`` if the file is unreadable, has no frontmatter, or the
91
+ frontmatter is malformed. Comments (``# ...``) and quoted values are
92
+ stripped. Placeholder values like ``<task-group>`` are kept verbatim;
93
+ callers decide whether to treat them as a real suggestion.
94
+ """
95
+ try:
96
+ text = path.read_text(encoding="utf-8")
97
+ except OSError:
98
+ return {}
99
+ if not text.startswith("---"):
100
+ return {}
101
+ lines = text.splitlines()
102
+ if not lines or lines[0].strip() != "---":
103
+ return {}
104
+ out: dict[str, str] = {}
105
+ for line in lines[1:]:
106
+ if line.strip() == "---":
107
+ break
108
+ # strip trailing inline comment
109
+ comment_idx = line.find("#")
110
+ if comment_idx >= 0:
111
+ line = line[:comment_idx]
112
+ m = _BRIEF_FRONTMATTER_LINE_RE.match(line.strip())
113
+ if not m:
114
+ continue
115
+ key, val = m.group(1), m.group(2).strip()
116
+ # strip matching quotes
117
+ if (len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"')):
118
+ val = val[1:-1]
119
+ out[key] = val
120
+ return out
121
+
122
+
123
+ def _looks_like_template_placeholder(value: str) -> bool:
124
+ """Treat ``<task-group>``, ``<...>``, empty strings, and ``self`` as
125
+ non-suggestions. Anything else (a real slug-like value) is honored."""
126
+ v = (value or "").strip()
127
+ if not v:
128
+ return True
129
+ if v.startswith("<") and v.endswith(">"):
130
+ return True
131
+ if v.lower() in ("self", "tbd", "n/a", "na", "none"):
132
+ return True
133
+ return False
134
+
135
+
136
+ def _brief_suggestions(path: Path) -> tuple[str, str]:
137
+ """Return ``(task_group_suggestion, task_id_suggestion)`` extracted from
138
+ the brief's frontmatter, or empty strings when no usable value exists.
139
+
140
+ - ``task_group`` ← frontmatter ``task-group``.
141
+ - ``task_id`` ← frontmatter ``brief-id`` (which matches the
142
+ filename stem in okstra-brief output and is the
143
+ strongest single identifier of the task).
144
+
145
+ A brief without frontmatter, or with placeholder values, yields two
146
+ empty strings — callers fall back to plain-text input.
147
+ """
148
+ fm = _parse_brief_frontmatter(path)
149
+ tg_raw = fm.get("task-group", "")
150
+ bid_raw = fm.get("brief-id", "")
151
+ tg = "" if _looks_like_template_placeholder(tg_raw) else tg_raw
152
+ tid = "" if _looks_like_template_placeholder(bid_raw) else bid_raw
153
+ return tg, tid
154
+
76
155
 
77
156
  # ---- Step IDs ------------------------------------------------------------
78
157
 
79
158
  S_TASK_PICK = "task_pick"
80
159
  S_TASK_GROUP = "task_group"
160
+ S_TASK_GROUP_TEXT = "task_group_text"
81
161
  S_TASK_ID = "task_id"
162
+ S_TASK_ID_TEXT = "task_id_text"
82
163
  S_TASK_TYPE = "task_type"
83
164
  S_BRIEF_KEEP = "brief_keep"
84
165
  S_BRIEF_PATH = "brief_path"
@@ -123,6 +204,13 @@ class WizardState:
123
204
  task_group: str = ""
124
205
  task_id: str = ""
125
206
  existing_brief_path: str = ""
207
+ # brief-derived suggestions (new-task flow only; set when brief is
208
+ # accepted, cleared if the user picks "type custom" so the next
209
+ # `_build_*` falls back to plain text input)
210
+ task_group_suggestion: str = ""
211
+ task_id_suggestion: str = ""
212
+ task_group_pending_text: bool = False
213
+ task_id_pending_text: bool = False
126
214
 
127
215
  # task-type + dependents
128
216
  task_type: str = ""
@@ -421,24 +509,98 @@ def _submit_task_pick(state: WizardState, value: str) -> Optional[str]:
421
509
 
422
510
 
423
511
  def _build_task_group(state: WizardState) -> Prompt:
512
+ sugg = state.task_group_suggestion
513
+ if sugg:
514
+ return Prompt(
515
+ step=S_TASK_GROUP, kind="pick",
516
+ label=f"Task group? (brief 추천: {sugg})",
517
+ options=[
518
+ _opt(PICK_USE_SUGGESTED, f"brief 값 사용: {sugg}"),
519
+ _opt(PICK_TYPE_CUSTOM, "다른 값 입력"),
520
+ ],
521
+ echo_template="task-group: {value}",
522
+ )
424
523
  return Prompt(step=S_TASK_GROUP, kind="text",
425
524
  label="Task group 을 알려주세요 (예: backend-api, INV-1234, refactor)",
426
525
  echo_template="task-group: {value}")
427
526
 
428
527
 
429
528
  def _submit_task_group(state: WizardState, value: str) -> Optional[str]:
529
+ if state.task_group_suggestion:
530
+ if value == PICK_USE_SUGGESTED:
531
+ state.task_group = _slug_or_die(
532
+ state.task_group_suggestion, "task_group"
533
+ )
534
+ state.task_group_pending_text = False
535
+ return f"task-group: {state.task_group} (brief)"
536
+ if value == PICK_TYPE_CUSTOM:
537
+ state.task_group_pending_text = True
538
+ return f"task-group: (직접 입력)"
539
+ raise WizardError(
540
+ f"expected {PICK_USE_SUGGESTED!r} or {PICK_TYPE_CUSTOM!r}, "
541
+ f"got: {value!r}"
542
+ )
543
+ state.task_group = _slug_or_die(value, "task_group")
544
+ state.task_group_pending_text = False
545
+ return f"task-group: {state.task_group}"
546
+
547
+
548
+ def _build_task_group_text(state: WizardState) -> Prompt:
549
+ return Prompt(step=S_TASK_GROUP_TEXT, kind="text",
550
+ label="Task group 을 입력해주세요 (예: backend-api, INV-1234, refactor)",
551
+ echo_template="task-group: {value}")
552
+
553
+
554
+ def _submit_task_group_text(state: WizardState, value: str) -> Optional[str]:
430
555
  state.task_group = _slug_or_die(value, "task_group")
556
+ state.task_group_pending_text = False
431
557
  return f"task-group: {state.task_group}"
432
558
 
433
559
 
434
560
  def _build_task_id(state: WizardState) -> Prompt:
561
+ sugg = state.task_id_suggestion
562
+ if sugg:
563
+ return Prompt(
564
+ step=S_TASK_ID, kind="pick",
565
+ label=f"Task id? (brief 추천: {sugg})",
566
+ options=[
567
+ _opt(PICK_USE_SUGGESTED, f"brief 값 사용: {sugg}"),
568
+ _opt(PICK_TYPE_CUSTOM, "다른 값 입력"),
569
+ ],
570
+ echo_template="task-id: {value}",
571
+ )
435
572
  return Prompt(step=S_TASK_ID, kind="text",
436
573
  label="Task id 를 알려주세요 (예: login-error-analysis, dev-9043)",
437
574
  echo_template="task-id: {value}")
438
575
 
439
576
 
440
577
  def _submit_task_id(state: WizardState, value: str) -> Optional[str]:
578
+ if state.task_id_suggestion:
579
+ if value == PICK_USE_SUGGESTED:
580
+ state.task_id = _slug_or_die(state.task_id_suggestion, "task_id")
581
+ state.task_id_pending_text = False
582
+ return f"task-id: {state.task_id} (brief)"
583
+ if value == PICK_TYPE_CUSTOM:
584
+ state.task_id_pending_text = True
585
+ return f"task-id: (직접 입력)"
586
+ raise WizardError(
587
+ f"expected {PICK_USE_SUGGESTED!r} or {PICK_TYPE_CUSTOM!r}, "
588
+ f"got: {value!r}"
589
+ )
441
590
  state.task_id = _slug_or_die(value, "task_id")
591
+ state.task_id_pending_text = False
592
+ return f"task-id: {state.task_id}"
593
+
594
+
595
+ def _build_task_id_text(state: WizardState) -> Prompt:
596
+ return Prompt(step=S_TASK_ID_TEXT, kind="text",
597
+ label="Task id 를 입력해주세요 (예: login-error-analysis, dev-9043)",
598
+ echo_template="task-id: {value}")
599
+
600
+
601
+ def _submit_task_id_text(state: WizardState, value: str) -> Optional[str]:
602
+ state.task_id = _slug_or_die(value, "task_id")
603
+ state.task_id_pending_text = False
442
604
  return f"task-id: {state.task_id}"
443
605
 
444
606
 
@@ -502,6 +664,14 @@ def _build_brief_path(state: WizardState) -> Prompt:
502
664
  def _submit_brief_path(state: WizardState, value: str) -> Optional[str]:
503
665
  p = _require_file(value, Path(state.project_root), "task brief")
504
666
  state.brief_path = str(p)
667
+ # When the user is starting a brand-new task, pull task-group /
668
+ # task-id candidates from the brief frontmatter so the next two
669
+ # prompts can offer them as a one-click pick instead of forcing a
670
+ # free-text retype of what the brief already declares.
671
+ if state.is_new_task and not state.task_group and not state.task_id:
672
+ tg, tid = _brief_suggestions(p)
673
+ state.task_group_suggestion = tg
674
+ state.task_id_suggestion = tid
505
675
  return f"brief: {p}"
506
676
 
507
677
 
@@ -1002,15 +1172,57 @@ STEPS: list[Step] = [
1002
1172
  applies=lambda s: s.is_new_task is None,
1003
1173
  build=_build_task_pick, submit=_submit_task_pick,
1004
1174
  owns=("is_new_task", "task_group", "task_id", "task_type",
1005
- "existing_brief_path", "profile_workers")),
1175
+ "existing_brief_path", "profile_workers",
1176
+ "task_group_suggestion", "task_id_suggestion",
1177
+ "task_group_pending_text", "task_id_pending_text")),
1178
+ Step(S_BRIEF_PATH,
1179
+ applies=lambda s: (
1180
+ not s.brief_path
1181
+ and (
1182
+ # new-task flow: collect brief FIRST so task-group /
1183
+ # task-id can be offered as one-click picks from the
1184
+ # brief's frontmatter.
1185
+ (s.is_new_task is True)
1186
+ # existing-task flow: brief comes after task-type and
1187
+ # the optional brief-keep step (unchanged behavior).
1188
+ or (s.is_new_task is False
1189
+ and S_TASK_TYPE in s.answered
1190
+ and (s.keep_existing_brief is False
1191
+ or not s.existing_brief_path))
1192
+ )
1193
+ ),
1194
+ build=_build_brief_path, submit=_submit_brief_path,
1195
+ owns=("brief_path", "task_group_suggestion", "task_id_suggestion")),
1006
1196
  Step(S_TASK_GROUP,
1007
- applies=lambda s: bool(s.is_new_task) and not s.task_group,
1197
+ applies=lambda s: (bool(s.is_new_task)
1198
+ and bool(s.brief_path)
1199
+ and not s.task_group
1200
+ and not s.task_group_pending_text),
1008
1201
  build=_build_task_group, submit=_submit_task_group,
1009
- owns=("task_group",)),
1202
+ owns=("task_group", "task_group_pending_text")),
1203
+ Step(S_TASK_GROUP_TEXT,
1204
+ applies=lambda s: (bool(s.is_new_task)
1205
+ and bool(s.brief_path)
1206
+ and not s.task_group
1207
+ and s.task_group_pending_text),
1208
+ build=_build_task_group_text, submit=_submit_task_group_text,
1209
+ owns=("task_group", "task_group_pending_text")),
1010
1210
  Step(S_TASK_ID,
1011
- applies=lambda s: bool(s.is_new_task) and bool(s.task_group) and not s.task_id,
1211
+ applies=lambda s: (bool(s.is_new_task)
1212
+ and bool(s.brief_path)
1213
+ and bool(s.task_group)
1214
+ and not s.task_id
1215
+ and not s.task_id_pending_text),
1012
1216
  build=_build_task_id, submit=_submit_task_id,
1013
- owns=("task_id",)),
1217
+ owns=("task_id", "task_id_pending_text")),
1218
+ Step(S_TASK_ID_TEXT,
1219
+ applies=lambda s: (bool(s.is_new_task)
1220
+ and bool(s.brief_path)
1221
+ and bool(s.task_group)
1222
+ and not s.task_id
1223
+ and s.task_id_pending_text),
1224
+ build=_build_task_id_text, submit=_submit_task_id_text,
1225
+ owns=("task_id", "task_id_pending_text")),
1014
1226
  Step(S_TASK_TYPE,
1015
1227
  applies=lambda s: (s.is_new_task is not None
1016
1228
  and (s.is_new_task is False or bool(s.task_id))
@@ -1023,15 +1235,7 @@ STEPS: list[Step] = [
1023
1235
  and s.keep_existing_brief is None
1024
1236
  and S_TASK_TYPE in s.answered),
1025
1237
  build=_build_brief_keep, submit=_submit_brief_keep,
1026
- owns=("keep_existing_brief", "brief_path")),
1027
- Step(S_BRIEF_PATH,
1028
- applies=lambda s: (S_TASK_TYPE in s.answered
1029
- and not s.brief_path
1030
- and (s.is_new_task
1031
- or s.keep_existing_brief is False
1032
- or (not s.is_new_task and not s.existing_brief_path))),
1033
- build=_build_brief_path, submit=_submit_brief_path,
1034
- owns=("brief_path",)),
1238
+ owns=("keep_existing_brief",)),
1035
1239
  Step(S_BASE_REF_PICK,
1036
1240
  applies=lambda s: (S_TASK_TYPE in s.answered
1037
1241
  and s.reuse_worktree is False
@@ -1240,6 +1444,8 @@ def _reset_from(state: WizardState, target_step: str) -> None:
1240
1444
  _FIELD_DEFAULTS: dict[str, Any] = {
1241
1445
  "is_new_task": None, "task_group": "", "task_id": "",
1242
1446
  "existing_brief_path": "", "task_type": "",
1447
+ "task_group_suggestion": "", "task_id_suggestion": "",
1448
+ "task_group_pending_text": False, "task_id_pending_text": False,
1243
1449
  "profile_workers": [], "keep_existing_brief": None,
1244
1450
  "brief_path": "", "reuse_worktree": None, "base_ref": "",
1245
1451
  "base_ref_pending_text": False, "approved_plan_path": "",
@@ -648,11 +648,15 @@ reporter-confirmations: <complete | partial | pending | skipped> # set by Step
648
648
  > Recommended next phase: <requirements-discovery | error-analysis> ← from Step 6
649
649
  > Handoff contract: see `prompts/profiles/_common-contract.md` § "Brief handoff contract"
650
650
 
651
- ## Source Material (verbatim — do not modify)
651
+ ## Source Material
652
652
 
653
+ <!-- author guidance — strip out at fill-in time:
653
654
  Paste each source separately and as-is. No paraphrasing, summarizing, or
654
655
  restructuring. Format conversion (e.g. Jira ADF → Markdown) is allowed and
655
- must be annotated in the header meta.
656
+ must be annotated in the header meta. Heading was originally
657
+ "Source Material (verbatim — do not modify)" — the parenthetical is a
658
+ reviewer note, not body text.
659
+ -->
656
660
 
657
661
  ### Source 1 — <type: file | linear | jira | github | notion | url | user-input>
658
662
 
@@ -665,30 +669,31 @@ must be annotated in the header meta.
665
669
  <Paste the raw source here without changing a single character.>
666
670
  ```
667
671
 
668
- ### Source 2...
669
-
670
- (Repeat as needed.)
672
+ <!-- Repeat `### Source N…` blocks as needed. -->
671
673
 
672
674
  ## Context
673
675
 
674
676
  <Background / scope / why now. If self-evident from Source Material, quote
675
677
  it briefly and stop. Use the blockquote below when augmentation is needed.>
676
678
 
677
- > augmented: <label> — <Interpretation added by the skill or user. Label
678
- > MUST be one of: `evidence-link` / `format-conversion` /
679
- > `terminology-mapping` / `intent-inference`. Do NOT add any extra
680
- > interpretation outside this blockquote.>
679
+ > augmented: <label> — <Interpretation added by the skill or user.>
680
+
681
+ <!-- label MUST be one of: `evidence-link` / `format-conversion` /
682
+ `terminology-mapping` / `intent-inference`. Do NOT add any extra
683
+ interpretation outside the `> augmented:` blockquote. -->
681
684
 
682
685
  ## Problem / Symptom
683
686
 
684
687
  <Current state. For bugs: repro / observed / expected. For greenfield: gap
685
- between current and desired. Same source-quote + `> augmented:` rule as
686
- above.>
688
+ between current and desired.>
689
+
690
+ <!-- Same source-quote + `> augmented:` rule as the Context section. -->
687
691
 
688
692
  ## Desired Outcome
689
693
 
690
- <Shape of success. Do NOT prescribe a solution — that belongs to
691
- implementation-planning.>
694
+ <Shape of success.>
695
+
696
+ <!-- Do NOT prescribe a solution — that belongs to implementation-planning. -->
692
697
 
693
698
  ## Constraints
694
699
 
@@ -701,14 +706,18 @@ none.>
701
706
 
702
707
  ## Open Questions
703
708
 
709
+ <!-- author guidance — strip out at fill-in time:
704
710
  Prefix every row with one of these signals so the next phase knows how to
705
711
  handle it. Free-form rows are allowed only as `general:`.
706
712
 
713
+ Allowed signals:
707
714
  - `general: <unresolved question the user flagged>`
708
- - `terminology: <reporter word> — needs canonical resolution against <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
715
+ - `terminology: <reporter word> — needs canonical resolution against
716
+ <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
709
717
  - `intent-check: <restated inference> — confirm with reporter`
710
718
  (auto-paired with every `intent-inference` augmentation)
711
- - `conversion-block: <reporter statement> — could not be mapped to project vocabulary; reporter query required`
719
+ - `conversion-block: <reporter statement> — could not be mapped to project
720
+ vocabulary; reporter query required`
712
721
  - `adr-candidate: <topic>` — signal only; `implementation-planning`
713
722
  evaluates and, if accepted, drafts a decision file at
714
723
  `<PROJECT_ROOT>/.project-docs/okstra/decisions/<NNNN>-<slug>.md`.
@@ -718,11 +727,14 @@ Use `_(none)_` only if every signal is empty. `intent-check:` and
718
727
  from this list — they receive a `[CONFIRMED <YYYY-MM-DD> → RC-N]`
719
728
  marker that links to the corresponding entry under
720
729
  `## Reporter Confirmations`.
730
+ -->
731
+
732
+ - <fill in one row per signal, or replace with `_(none)_`>
721
733
 
722
734
  ## Reporter Confirmations
723
735
 
724
- Populated by Step 6.5. Each subsection records one reporter answer
725
- verbatim, with a link back to the originating `Open Questions` row.
736
+ <!-- Populated by Step 6.5. Each subsection records one reporter answer
737
+ verbatim, with a link back to the originating `Open Questions` row. -->
726
738
 
727
739
  _(none — pending or skipped)_
728
740
 
@@ -737,6 +749,7 @@ _(none — pending or skipped)_
737
749
 
738
750
  ## Augmentation
739
751
 
752
+ <!-- author guidance — strip out at fill-in time:
740
753
  Cross-references / interpretation / context added by the user or skill that
741
754
  is not in the original source. May be empty. Keep this section visually
742
755
  separated from Source Material — never inline it inside Source Material.
@@ -744,44 +757,59 @@ separated from Source Material — never inline it inside Source Material.
744
757
  Every entry below must start with one of the four labels:
745
758
  `evidence-link` / `format-conversion` / `terminology-mapping` /
746
759
  `intent-inference`. Unlabelled entries are forbidden.
760
+ -->
747
761
 
748
762
  ### Domain alignment
749
763
 
750
- Observations from Step 3b and the outcome of Step 4.5 (glossary
751
- applied vs. skipped). The actual glossary edits live in
764
+ <!-- author guidance strip out at fill-in time:
765
+ Observations from Step 3b and the outcome of Step 4.5 (glossary applied
766
+ vs. skipped). The actual glossary edits live in
752
767
  `<PROJECT_ROOT>/.project-docs/okstra/glossary.md` when applied; this
753
- section records what happened. Decision candidates are NOT recorded
754
- here — they flow through `Open Questions` as `adr-candidate:` rows for
768
+ section records what happened. Decision candidates are NOT recorded here —
769
+ they flow through `Open Questions` as `adr-candidate:` rows for
755
770
  `implementation-planning` to evaluate (and, if accepted, draft into
756
771
  `<PROJECT_ROOT>/.project-docs/okstra/decisions/`).
757
772
 
773
+ Allowed entry shapes:
758
774
  - `terminology-mapping: <reporter word> → <okstra glossary canonical>` —
759
775
  routine glossary alignment, paired with `terminology:` in Open Questions
760
776
  when unresolved.
761
- - `terminology-mapping: applied glossary: <term> → <PROJECT_ROOT>/.project-docs/okstra/glossary.md` /
762
- `terminology-mapping: skipped glossary: <term> = <definition>` — Step
763
- 4.5 outcomes.
764
- - Use `_(none)_` if every alignment entry is empty.
777
+ - `terminology-mapping: applied glossary: <term> → <PROJECT_ROOT>/.project-docs/okstra/glossary.md`
778
+ - `terminology-mapping: skipped glossary: <term> = <definition>` —
779
+ Step 4.5 outcomes.
780
+ Use `_(none)_` if every alignment entry is empty.
781
+ -->
782
+
783
+ - <fill in one entry per alignment, or replace with `_(none)_`>
765
784
 
766
785
  ### Evidence links (file / symbol resolution)
767
786
 
768
- - `evidence-link: <reporter phrase> → <relative path>:<line>` /
769
- `evidence-link: <reporter phrase> → <symbol> in <relative path>`
770
- - `_(none)_` if none.
787
+ <!-- Allowed entry shapes:
788
+ `evidence-link: <reporter phrase> → <relative path>:<line>` or
789
+ `evidence-link: <reporter phrase> → <symbol> in <relative path>`.
790
+ Use `_(none)_` if none. -->
791
+
792
+ - <fill in one entry per link, or replace with `_(none)_`>
771
793
 
772
794
  ### Intent inferences
773
795
 
774
- Every entry here is an unverified hypothesis. Each one MUST have a paired
775
- `intent-check:` row under Open Questions.
796
+ <!-- Every entry here is an unverified hypothesis. Each one MUST have a
797
+ paired `intent-check:` row under Open Questions.
798
+
799
+ Allowed entry shape:
800
+ `intent-inference: <reporter phrase> → <qualitative restatement>`
801
+ (qualitative only — never invent numeric thresholds).
802
+ Use `_(none)_` if none. -->
776
803
 
777
- - `intent-inference: <reporter phrase> <qualitative restatement>`
778
- (qualitative only — never invent numeric thresholds)
779
- - `_(none)_` if none.
804
+ - <fill in one entry per inference, or replace with `_(none)_`>
780
805
 
781
806
  ### Format conversions
782
807
 
783
- - `format-conversion: <ref> — <e.g. Jira ADF → Markdown, semantics preserved>`
784
- - `_(none)_` if none.
808
+ <!-- Allowed entry shape:
809
+ `format-conversion: <ref> <e.g. Jira ADF → Markdown, semantics preserved>`.
810
+ Use `_(none)_` if none. -->
811
+
812
+ - <fill in one entry per conversion, or replace with `_(none)_`>
785
813
  ````
786
814
 
787
815
  ### Frontmatter rules
@@ -966,6 +994,16 @@ started.
966
994
  is allowed, and the conversion must be annotated in the `format:` meta.
967
995
  Augmentation / interpretation goes only into the `Augmentation` section
968
996
  or a `> augmented:` blockquote inside the required section.
997
+ - **No template author-guidance in the artifact body.** The Step 5
998
+ template above carries author guidance in HTML comments
999
+ (`<!-- ... -->`); preserve those comments when writing the brief but do
1000
+ NOT promote them to body prose. Section headings stay clean — never copy
1001
+ parenthetical reviewer notes onto the heading itself (e.g. emit
1002
+ `## Source Material`, not `## Source Material (verbatim — do not modify)`).
1003
+ Placeholder prose inside angle brackets (`<Background / scope / why
1004
+ now…>`) is intentional and is meant to be replaced with the reporter's
1005
+ actual content; do not strip the angle-bracket placeholders, replace
1006
+ them.
969
1007
  - If the tracker MCP is not connected, do not guess — ask the user to paste
970
1008
  the body or skip the source.
971
1009
  - If a URL fetch fails or hits an auth wall, do not guess — ask for a
@@ -83,7 +83,19 @@ The four steps below MUST execute in this exact order. Reordering them is the re
83
83
  ```
84
84
 
85
85
  The 10 substituted placeholders: `{{LEAD_TOTAL_TOKENS}}`, `{{LEAD_BILLABLE_TOKENS}}`, `{{LEAD_COST_USD}}`, `{{WORKER_TOTAL_TOKENS}}`, `{{WORKER_BILLABLE_TOKENS}}`, `{{WORKER_COST_USD}}`, `{{GRAND_TOTAL_TOKENS}}`, `{{GRAND_BILLABLE_TOKENS}}`, `{{GRAND_COST_USD}}`, `{{CLI_COST_USD}}`. The final-report file MUST already exist (Phase 6 output).
86
- 3. **Phase 7 step 2Follow-up task spawner** (BLOCKING when Section 7 is non-empty). Turns the report's `## 7. Follow-up Tasks (후속 작업)` rows into `tasks/<task-group>/<new-task-id>/` stubs.
86
+ 3. **Phase 7 step 1.5Render report views** (BLOCKING). Produces two derived artifacts from the now-substituted final-report MD:
87
+
88
+ ```bash
89
+ python3 scripts/okstra-render-report-views.py \
90
+ <runDirectoryPath>/reports/final-report-<task-type>-<seq>.md
91
+ ```
92
+
93
+ Outputs (idempotent — re-running overwrites):
94
+ - `runs/<task-type>/reports/final-report-<task-type>-<seq>.slim.md` — token-saving AI-consumption copy.
95
+ - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view. Section 5 `C-*` clarification rows with `Status` ∈ {`open`, `answered`} embed `<textarea>` controls; an `Export user response` button serialises form values to a markdown sidecar (schema in [`templates/reports/user-response.template.md`](../../templates/reports/user-response.template.md)) that the user pastes to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`. The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
96
+
97
+ Must run AFTER step 1 (so token placeholders are substituted in both derived views) and BEFORE step 2 (so the slim/html artifacts exist for any validator step that checks them).
98
+ 4. **Phase 7 step 2 — Follow-up task spawner** (BLOCKING when Section 7 is non-empty). Turns the report's `## 7. Follow-up Tasks (후속 작업)` rows into `tasks/<task-group>/<new-task-id>/` stubs.
87
99
 
88
100
  ```bash
89
101
  python3 scripts/okstra-spawn-followups.py \
@@ -98,7 +110,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
98
110
  - Rows with `Auto-spawn? != yes` are reported as `skipped` and never written; surface them in Section 6 if manual action is still needed.
99
111
  - An invalid `Origin`, `Suggested task-type`, missing `Title`, or missing `Reason / Why deferred` exits `1`. The report-writer MUST refuse to ship a Section 7 with such rows.
100
112
  - **Canonical spawn rule (single source of truth):** the spawner runs when `task-type` ∈ {`implementation`, `final-verification`, `release-handoff`}, OR when Section 7 is non-empty for any other task-type. For the listed task-types Section 7 must be present in the report; an empty section renders as `- 후속 작업 없음.`. Missing / empty sections are no-ops (exit `0`). All other references to this rule (including the Persistence Checklist) defer to this statement.
101
- 4. **Phase 7 step 3 — Update Section 6** after the spawner. The report-writer MUST append one row per newly spawned task-key with its entry command:
113
+ 5. **Phase 7 step 3 — Update Section 6** after the spawner. The report-writer MUST append one row per newly spawned task-key with its entry command:
102
114
 
103
115
  ```
104
116
  - Follow-up: `<task-group>/<new-task-id>` — Claude Code 세션 안 `/okstra-run task-key=<task-group>/<new-task-id> task-type=<suggested>` / 별도 터미널 `scripts/okstra.sh --task-key <task-group>/<new-task-id> --task-type <suggested>`
@@ -117,7 +129,8 @@ The final report follows the structure below. If `instruction-set/final-report-t
117
129
  - Date: <ISO 8601 timestamp>
118
130
  - Task Key: <task-key>
119
131
  - Task Type: <task-type>
120
- - Author: `<Report writer worker if in roster, else Claude lead>`
132
+ - Report Owner: `Claude lead`
133
+ - Report Author: `<Report writer worker if in roster, else Claude lead (release-handoff or recorded-fallback only)>`
121
134
  - Lead model: `<lead-model>`
122
135
  - Preparation Method: Final report authored by Report writer worker (or lead-authored fallback — record the documented dispatch failure reason here when applicable)
123
136
  ```
@@ -208,7 +221,7 @@ The final-report template `okstra-final-report.template.md` Section 2 already en
208
221
 
209
222
  ### Release-handoff section contract (release-handoff runs only)
210
223
 
211
- When the run's `task-type` is `release-handoff`, the final report MUST include Section `## 4.6 Release Handoff Deliverables` with all seven sub-sections (`4.6.1` Source Verification Report, `4.6.2` Feature Branch & Working-Tree State, `4.6.3` User Selections, `4.6.4` Executed Commands, `4.6.5` Commit List, `4.6.6` Pull Request Outcome, `4.6.7` Routing Recommendation). Every entry is dictated by the lead's recorded git/gh command log and the user's verbatim answers to the H1/H2/H3 menu prompts. H1 choices are `local only`, `push + PR`, or `skip`; release-handoff records existing implementation commits and MUST NOT create new commits. If the user picked `skip` (H1) or `cancel` (H3), keep 4.6.3 populated but leave 4.6.4–4.6.6 explicitly empty per the template's empty-state lines.
224
+ When the run's `task-type` is `release-handoff`, the final report MUST include Section `## 4.6 Release Handoff Deliverables` with all eight sub-sections (`4.6.1` Source Verification Report, `4.6.2` Feature Branch & Working-Tree State, `4.6.3` User Selections, `4.6.4` Executed Commands, `4.6.5` Commit List, `4.6.6` Merge Conflict Probe, `4.6.7` Pull Request Outcome, `4.6.8` Routing Recommendation). Every entry is dictated by the lead's recorded git/gh command log and the user's verbatim answers to the H1/H2/H3 menu prompts. H1 choices are `local only`, `push + PR`, or `skip`; release-handoff records existing implementation commits and MUST NOT create new commits. If the user picked `skip` (H1) or `cancel` (H3), keep 4.6.3 populated but leave 4.6.4–4.6.6 explicitly empty per the template's empty-state lines.
212
225
 
213
226
  **Single-lead authorship (release-handoff only):** release-handoff has no worker roster (no `Report writer worker`, no `Claude worker` drafter). The Claude lead authors the final-report file directly — there is no `Report writer worker` dispatch to perform in Phase 6, no resume-safe dispatch concern, and no mandatory worker-results file for a report-writer role. The rest of this skill's dispatch / resume / fallback machinery applies ONLY when `Report writer worker` is in the roster (i.e. every task-type other than `release-handoff`).
214
227
 
@@ -278,6 +291,8 @@ Persistence steps that must be performed in Phase 7:
278
291
  - [ ] 6. **Generate final status file**: `runs/<task-type>/status/final-<task-type>-<seq>.status` (if necessary)
279
292
  - [ ] 7. **Save convergence state**: `runs/<task-type>/state/convergence-<task-type>-<seq>.json` (when convergence is enabled)
280
293
  - [ ] 8. **Spawn follow-up task stubs**: run `scripts/okstra-spawn-followups.py` against the final-report per the canonical spawn rule defined in "Phase 7 follow-up task spawner" above. Do not restate the trigger condition here — that section is the single source of truth. The script is idempotent across reruns.
294
+ - [ ] 9. **Slim AI report**: `runs/<task-type>/reports/final-report-<task-type>-<seq>.slim.md` (produced by Phase 7 step 1.5 — see "Phase 6 → Phase 7 execution sequence" above)
295
+ - [ ] 10. **Human HTML report**: `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` (same step 1.5; self-contained, embeds `Export user response` button)
281
296
 
282
297
  ### Response after Persistence
283
298
 
@@ -213,7 +213,7 @@ Terminal statuses that can be recorded for a worker:
213
213
 
214
214
  **Authoritative source.** If other documents (SKILL.md, worker agent definitions) disagree with this section, this section wins.
215
215
 
216
- ### Result Frontmatter (mandatory, precedes Section 0)
216
+ ### Result Frontmatter (mandatory, precedes Section 1)
217
217
 
218
218
  Every worker result file MUST begin with a YAML frontmatter block. The values are sourced from the corresponding fields of the input files' frontmatter (e.g. `analysis-material.md`, `task-brief.md`) — copy them verbatim; do NOT regenerate them. Only `workerId` and `title` are worker-specific.
219
219
 
@@ -260,11 +260,8 @@ The same frontmatter contract applies to the `Report writer worker`'s final-repo
260
260
 
261
261
  A successful worker result must include the following sections in this exact order, beneath the frontmatter block:
262
262
 
263
- 0. **Reading Confirmation** one short line per input file stating that the worker read it end-to-end. Each line takes the form `- Read <file-name> end-to-end (<line-count> lines).`. The enumerated files are audience-scoped they MUST match the recipient's row in the "Audience-scoped enumeration" table above:
264
- - **Claude / Codex / Gemini analysis workers**: `task-brief.md`, `analysis-profile.md`, `analysis-material.md` (if present), `reference-expectations.md`, `clarification-response.md` (if a carry-in was provided). Analysis workers MUST NOT include `final-report-template.md` — it is not in their `[Required reading]` block.
265
- - **Report writer worker (Phase 6)**: all of the above **plus** `final-report-template.md`.
263
+ > **Reading Confirmation lives in the audit sidecar, NOT in the main worker result.** Section 0 is intentionally absent from the main file. The worker writes Reading Confirmation to `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md` per the dispatch-prompt clause above; the validator FAILS any main worker-result file that contains a `## 0. Reading Confirmation` heading. If a file was skipped or only partially read, the worker MUST NOT produce sections 1–5 — instead it records a `tool-failure` in the errors sidecar and stops.
266
264
 
267
- If a file was skipped or only partially read, the worker MUST NOT produce sections 1–5; instead it records a `tool-failure` in the errors sidecar and stops. This section exists specifically to counteract the common failure mode where workers skim long inputs because they share structure with the file the run will eventually write into.
268
265
  1. Findings
269
266
  2. Missing Information or Assumptions
270
267
  3. Safe or Reasonable Areas