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.
- package/README.kr.md +4 -0
- package/README.md +4 -0
- package/bin/okstra +1 -0
- package/docs/kr/architecture.md +74 -13
- package/docs/kr/cli.md +6 -1
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +167 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +2 -2
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/agents/workers/gemini-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +3 -1
- package/runtime/bin/okstra-render-report-views.py +129 -0
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/report_views.py +701 -0
- package/runtime/python/okstra_ctl/wizard.py +220 -14
- package/runtime/skills/okstra-brief/SKILL.md +73 -35
- package/runtime/skills/okstra-report-writer/SKILL.md +19 -4
- package/runtime/skills/okstra-team-contract/SKILL.md +2 -5
- package/runtime/templates/reports/final-report.template.md +169 -2
- package/runtime/templates/reports/report.css +151 -0
- package/runtime/templates/reports/report.js +163 -0
- package/runtime/templates/reports/user-response.template.md +69 -0
- package/runtime/validators/lib/fixtures.sh +40 -3
- package/runtime/validators/validate-report-views.py +283 -0
- package/runtime/validators/validate-run.py +251 -3
- package/runtime/validators/validate-workflow.sh +4 -0
- package/src/install.mjs +1 -0
- package/src/render-views.mjs +67 -0
|
@@ -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)
|
|
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)
|
|
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",
|
|
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
|
|
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
|
-
|
|
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
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
|
686
|
-
|
|
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
|
|
691
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
751
|
-
|
|
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
|
-
|
|
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
|
-
|
|
763
|
-
4.5 outcomes.
|
|
764
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
784
|
-
-
|
|
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
|
|
86
|
+
3. **Phase 7 step 1.5 — Render 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
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|