okstra 0.117.0 → 0.119.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.md +115 -104
- package/docs/architecture/storage-model.md +300 -0
- package/docs/architecture.md +802 -0
- package/docs/cli.md +658 -0
- package/docs/container.md +124 -0
- package/docs/contributor-change-matrix.md +5 -4
- package/docs/follow-ups/2026-07-10-final-report-option-3.md +51 -0
- package/docs/performance-improvement-plan-v2.md +374 -0
- package/docs/project-structure-overview.md +21 -10
- package/package.json +10 -5
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-render-report-views.py +5 -35
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/python/okstra_ctl/clarification_items.py +48 -0
- package/runtime/python/okstra_ctl/recap.py +80 -3
- package/runtime/python/okstra_ctl/report_views.py +46 -6
- package/runtime/python/okstra_ctl/user_response.py +190 -0
- package/runtime/skills/okstra-inspect/SKILL.md +37 -2
- package/runtime/skills/okstra-pr-gen/SKILL.md +9 -8
- package/runtime/skills/okstra-user-response/SKILL.md +121 -0
- package/runtime/templates/reports/report.js +6 -3
- package/runtime/templates/reports/user-response.template.md +1 -0
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/recap.mjs +6 -1
- package/src/commands/inspect/user-response.mjs +26 -0
- package/src/lib/skill-catalog.mjs +1 -0
- package/README.kr.md +0 -231
- package/docs/kr/architecture/storage-model.md +0 -297
- package/docs/kr/architecture.md +0 -815
- package/docs/kr/cli.md +0 -657
- package/docs/kr/container.md +0 -122
- package/docs/kr/follow-ups/2026-07-10-final-report-option-3.md +0 -51
- package/docs/kr/performance-improvement-plan-v2.md +0 -374
- package/docs/kr/performance-improvement-plan.md +0 -147
|
@@ -8,10 +8,26 @@ the ``## APPROVAL`` block — the implementation wizard consumes it to offer
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import argparse
|
|
12
|
+
import datetime as dt
|
|
13
|
+
import json
|
|
11
14
|
import re
|
|
15
|
+
import sys
|
|
12
16
|
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
13
18
|
from typing import Optional
|
|
14
19
|
|
|
20
|
+
from okstra_ctl.report_views import (
|
|
21
|
+
serialize_user_response, UserResponseEntry, UserResponseApproval, infer_run_meta,
|
|
22
|
+
)
|
|
23
|
+
from okstra_ctl.report_view_artifacts import user_responses_dir_for_report
|
|
24
|
+
from okstra_ctl.listing import list_runs, absolute_final_report_path
|
|
25
|
+
from okstra_ctl.clarification_items import (
|
|
26
|
+
parse_clarification_rows,
|
|
27
|
+
scan_approval_gate,
|
|
28
|
+
section_1_present_but_unparsed,
|
|
29
|
+
)
|
|
30
|
+
|
|
15
31
|
_APPROVAL_HEADING_RE = re.compile(r"^## APPROVAL\s*$", re.MULTILINE)
|
|
16
32
|
_NEXT_RESPONSE_HEADING_RE = re.compile(r"^## ", re.MULTILINE)
|
|
17
33
|
|
|
@@ -53,3 +69,177 @@ def parse_user_response_approval(
|
|
|
53
69
|
source_report=rm.group(1) if rm else "",
|
|
54
70
|
seq=sm.group(1) if sm else "",
|
|
55
71
|
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_RESPONSE_HEADING_RE = re.compile(r"^## (?P<id>[A-Za-z][A-Za-z0-9]*-\d+)\s*$", re.MULTILINE)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _field(block: str, key: str) -> Optional[str]:
|
|
78
|
+
m = re.search(rf"^- {re.escape(key)}:\s*(\S.*?)\s*$", block, re.MULTILINE)
|
|
79
|
+
return m.group(1) if m else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _value(block: str) -> str:
|
|
83
|
+
# Value 는 "- Value:" 다음 줄들의 " > " 인용 블록.
|
|
84
|
+
m = re.search(r"^- Value:\s*\n((?:\s*>.*\n?)+)", block, re.MULTILINE)
|
|
85
|
+
if not m:
|
|
86
|
+
return ""
|
|
87
|
+
lines = [re.sub(r"^\s*>\s?", "", ln) for ln in m.group(1).splitlines()]
|
|
88
|
+
return "\n".join(lines).strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_user_response_entries(sidecar_text: str) -> list[UserResponseEntry]:
|
|
92
|
+
"""Reverse of ``serialize_user_response`` for the per-response ``## C-*``
|
|
93
|
+
blocks. The ``## APPROVAL`` block is skipped (read separately by
|
|
94
|
+
``parse_user_response_approval``)."""
|
|
95
|
+
entries: list[UserResponseEntry] = []
|
|
96
|
+
matches = list(_RESPONSE_HEADING_RE.finditer(sidecar_text))
|
|
97
|
+
for i, m in enumerate(matches):
|
|
98
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(sidecar_text)
|
|
99
|
+
block = sidecar_text[m.end():end]
|
|
100
|
+
rationale = _field(block, "Rationale")
|
|
101
|
+
entries.append(UserResponseEntry(
|
|
102
|
+
response_id=m.group("id"),
|
|
103
|
+
kind=_field(block, "Kind") or "",
|
|
104
|
+
value=_value(block),
|
|
105
|
+
rationale=rationale,
|
|
106
|
+
disposition=_field(block, "Disposition") or "answer",
|
|
107
|
+
))
|
|
108
|
+
return entries
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_SEQ_FROM_REPORT_RE = re.compile(r"final-report-.+-(\w+)\.md$")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _seq_from_report(report: Path) -> str:
|
|
115
|
+
m = _SEQ_FROM_REPORT_RE.search(report.name)
|
|
116
|
+
return m.group(1) if m else ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def list_awaiting_tasks(home: Path, project_id: str, limit: int) -> list[dict]:
|
|
120
|
+
"""approval-open clarification 이 있는 태스크를 최신 report mtime 순으로."""
|
|
121
|
+
seen: set[str] = set()
|
|
122
|
+
out: list[dict] = []
|
|
123
|
+
for row in list_runs(home, project=project_id, limit=0): # startedAt desc
|
|
124
|
+
grp, tid = row.get("taskGroup", ""), row.get("taskId", "")
|
|
125
|
+
key = f"{grp}/{tid}" if grp and tid else ""
|
|
126
|
+
if not key or key in seen:
|
|
127
|
+
continue
|
|
128
|
+
seen.add(key)
|
|
129
|
+
report = absolute_final_report_path(row)
|
|
130
|
+
if report is None or not report.is_file():
|
|
131
|
+
continue
|
|
132
|
+
text = report.read_text(encoding="utf-8")
|
|
133
|
+
scan = scan_approval_gate(text)
|
|
134
|
+
base = {"taskKey": key, "taskType": row.get("taskType", ""),
|
|
135
|
+
"seq": _seq_from_report(report), "reportPath": str(report),
|
|
136
|
+
"reportMtime": report.stat().st_mtime}
|
|
137
|
+
if scan.unreadable_reason is not None:
|
|
138
|
+
# §1 heading exists but drifted → real warning; §1 simply absent → skip.
|
|
139
|
+
if section_1_present_but_unparsed(text):
|
|
140
|
+
out.append({**base, "openApprovalCount": 0, "unreadable": True})
|
|
141
|
+
continue
|
|
142
|
+
if not scan.blockers:
|
|
143
|
+
continue
|
|
144
|
+
out.append({**base, "openApprovalCount": len(scan.blockers), "unreadable": False})
|
|
145
|
+
out.sort(key=lambda t: t["reportMtime"], reverse=True)
|
|
146
|
+
return out[:limit] if limit > 0 else out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_SECTION_REF_RE = re.compile(r"§[\d.]+|C-\d+|[\w./-]+\.\w+:\d+")
|
|
150
|
+
_ALTERNATIVES_CUE = "Alternatives:"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _alternatives_from_expected(expected_form: str) -> list[str]:
|
|
154
|
+
idx = expected_form.find(_ALTERNATIVES_CUE)
|
|
155
|
+
if idx < 0:
|
|
156
|
+
return []
|
|
157
|
+
tail = expected_form[idx + len(_ALTERNATIVES_CUE):]
|
|
158
|
+
parts = re.split(r"[/;]", tail)
|
|
159
|
+
return [p.strip(" .,;—-") for p in parts if p.strip(" .,;—-")]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def show_open_rows(report_path: Path) -> dict:
|
|
163
|
+
text = report_path.read_text(encoding="utf-8")
|
|
164
|
+
rows = []
|
|
165
|
+
for r in parse_clarification_rows(text):
|
|
166
|
+
it = r["item"]
|
|
167
|
+
if it.status not in ("open", "answered"):
|
|
168
|
+
continue
|
|
169
|
+
statement, expected = r["statement"], r["expected_form"]
|
|
170
|
+
refs = sorted(set(_SECTION_REF_RE.findall(statement + " " + expected)))
|
|
171
|
+
rows.append({"id": it.row_id, "kind": it.kind, "blocks": it.blocks,
|
|
172
|
+
"status": it.status, "statement": statement,
|
|
173
|
+
"recommended": expected, # raw cell keeps answer + rationale
|
|
174
|
+
"alternatives": _alternatives_from_expected(expected),
|
|
175
|
+
"contextRefs": refs})
|
|
176
|
+
return {"reportPath": str(report_path), "rows": rows}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def write_sidecar(report_path: Path, answers: list[dict],
|
|
180
|
+
approval: Optional[dict], created_at: str,
|
|
181
|
+
task_key: str = "") -> Path:
|
|
182
|
+
run_meta = infer_run_meta(report_path, task_key=task_key or None)
|
|
183
|
+
out_dir = user_responses_dir_for_report(report_path)
|
|
184
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
sidecar = out_dir / f"user-response-{run_meta.task_type}-{run_meta.seq}.md"
|
|
186
|
+
|
|
187
|
+
merged: dict[str, UserResponseEntry] = {}
|
|
188
|
+
if sidecar.is_file():
|
|
189
|
+
for e in parse_user_response_entries(sidecar.read_text(encoding="utf-8")):
|
|
190
|
+
merged[e.response_id] = e
|
|
191
|
+
for a in answers:
|
|
192
|
+
merged[a["id"]] = UserResponseEntry(
|
|
193
|
+
response_id=a["id"], kind=a.get("kind", ""), value=a["value"],
|
|
194
|
+
rationale=a.get("rationale"), disposition=a.get("disposition", "answer"))
|
|
195
|
+
|
|
196
|
+
appr = None
|
|
197
|
+
if approval and approval.get("approved"):
|
|
198
|
+
appr = UserResponseApproval(
|
|
199
|
+
approved=True, implementation_option=approval.get("implementationOption", ""))
|
|
200
|
+
sidecar.write_text(
|
|
201
|
+
serialize_user_response(run_meta=run_meta, entries=list(merged.values()),
|
|
202
|
+
created_at=created_at, approval=appr),
|
|
203
|
+
encoding="utf-8")
|
|
204
|
+
return sidecar
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
208
|
+
parser = argparse.ArgumentParser(prog="okstra user-response")
|
|
209
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
210
|
+
|
|
211
|
+
pl = sub.add_parser("list")
|
|
212
|
+
pl.add_argument("--home", required=True)
|
|
213
|
+
pl.add_argument("--project", required=True)
|
|
214
|
+
pl.add_argument("--limit", type=int, default=3)
|
|
215
|
+
|
|
216
|
+
ps = sub.add_parser("show")
|
|
217
|
+
ps.add_argument("--report", required=True)
|
|
218
|
+
|
|
219
|
+
pw = sub.add_parser("write")
|
|
220
|
+
pw.add_argument("--report", required=True)
|
|
221
|
+
pw.add_argument("--answers", required=True, help="JSON array of answer entries")
|
|
222
|
+
pw.add_argument("--approval", default="", help="JSON approval object")
|
|
223
|
+
pw.add_argument("--task-key", default="", help="task-key from list/show context")
|
|
224
|
+
|
|
225
|
+
ns = parser.parse_args(argv)
|
|
226
|
+
if ns.cmd == "list":
|
|
227
|
+
out = list_awaiting_tasks(Path(ns.home), ns.project, ns.limit)
|
|
228
|
+
json.dump(out, sys.stdout, ensure_ascii=False)
|
|
229
|
+
return 0
|
|
230
|
+
if ns.cmd == "show":
|
|
231
|
+
json.dump(show_open_rows(Path(ns.report)), sys.stdout, ensure_ascii=False)
|
|
232
|
+
return 0
|
|
233
|
+
if ns.cmd == "write":
|
|
234
|
+
answers = json.loads(ns.answers)
|
|
235
|
+
approval = json.loads(ns.approval) if ns.approval else None
|
|
236
|
+
created_at = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
237
|
+
p = write_sidecar(Path(ns.report), answers, approval, created_at,
|
|
238
|
+
task_key=ns.task_key)
|
|
239
|
+
json.dump({"sidecar": str(p)}, sys.stdout, ensure_ascii=False)
|
|
240
|
+
return 0
|
|
241
|
+
return 1
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
raise SystemExit(main())
|
|
@@ -18,7 +18,7 @@ Single read-side entry point for okstra runtime inspection plus the one status m
|
|
|
18
18
|
| `cost` | Estimate file/read context cost for a task bundle. |
|
|
19
19
|
| `errors` | Aggregate okstra-run error logs for a task into a timestamped markdown report; print a summary. |
|
|
20
20
|
| `error-zip` | Collect cross-project okstra error logs into an anonymized zip (report + raw) and summarize clusters. |
|
|
21
|
-
| `recap` | Summarize a task's run-to-run phase transitions, then answer free-form questions over its `.okstra` artifacts. Appends each summary/Q&A to `recap/recap-log.jsonl
|
|
21
|
+
| `recap` | Summarize a task's run-to-run phase transitions, then answer free-form questions over its `.okstra` artifacts. Appends each summary/Q&A to `recap/recap-log.jsonl`, and writes agent-authored notes to `notes/` for feeding into later runs. |
|
|
22
22
|
|
|
23
23
|
## Step 0: Preflight (shared)
|
|
24
24
|
|
|
@@ -690,7 +690,7 @@ stdout JSON 을 파싱해 보고:
|
|
|
690
690
|
|
|
691
691
|
Trigger phrases: "okstra recap", "recap", "작업 요약", "이 task 요약", "전후 요약", "이 작업 설명해줘", "task 질문".
|
|
692
692
|
|
|
693
|
-
task-id 하나에 쌓인 `.okstra` 산출물 위에서 (a) 처리 전/후 요약을 내고, (b) 그 작업에 대한 자유 질문에 답한다. 기본은 `.okstra/` 서브트리만 읽는다(artifact 모드). 사용자가 명시적으로 코드 변경까지 봐 달라고 요청할 때만 code 모드로 확장한다. 이 sub-command 는 `recap/recap-log.jsonl`
|
|
693
|
+
task-id 하나에 쌓인 `.okstra` 산출물 위에서 (a) 처리 전/후 요약을 내고, (b) 그 작업에 대한 자유 질문에 답한다. 기본은 `.okstra/` 서브트리만 읽는다(artifact 모드). 사용자가 명시적으로 코드 변경까지 봐 달라고 요청할 때만 code 모드로 확장한다. 이 sub-command 는 `recap/recap-log.jsonl` append 와 `notes/` 노트 작성(recap.5)만 수행하고 `task-manifest.json` / catalog / timeline 은 절대 mutate 하지 않는다.
|
|
694
694
|
|
|
695
695
|
### recap.1 — Resolve target
|
|
696
696
|
|
|
@@ -748,6 +748,41 @@ okstra recap record <resolved-target> --project-root <projectRoot> \
|
|
|
748
748
|
<이후 자유 질문을 받습니다. 코드 변경까지 보려면 "diff 까지 봐줘" 라고 요청하세요.>
|
|
749
749
|
```
|
|
750
750
|
|
|
751
|
+
### recap.5 — Agent 저작 노트 (`notes/`)
|
|
752
|
+
|
|
753
|
+
recap 중에 네가 **okstra task 를 위해** 만든 산출물 — 검증 증거, 설계·decision 초안, 분석 노트 — 이 앞으로의 run 에 근거로 쓰일 수 있고, 그 내용이 **렌더된 report 도 아니고 사용자 결정도 아닐 때**만 `notes/` 에 남긴다.
|
|
754
|
+
|
|
755
|
+
**소유권으로 라우팅한다.** 지금 쓰려는 것이 아래 어디에 해당하는지 먼저 판별한다:
|
|
756
|
+
|
|
757
|
+
| 내용 | 레인(경로) | 저작 주체 |
|
|
758
|
+
|---|---|---|
|
|
759
|
+
| final report 본문 | `runs/<type>/reports/*.md` (`*.data.json` 에서 렌더) | okstra 렌더러 — 절대 수기 편집 금지 |
|
|
760
|
+
| clarification 에 대한 사용자 답변 | `runs/<type>/user-responses/*.md` (`created-by: user`) | 사용자만 |
|
|
761
|
+
| 확정된 ADR | `.okstra/decisions/NNNN-*.md` | 승격·관리됨 |
|
|
762
|
+
| **Agent 증거 / 설계 초안 / 분석** | **`.okstra/tasks/<group>/<id>/notes/`** | 너(AI) |
|
|
763
|
+
|
|
764
|
+
렌더 산출물도, 사용자 결정도 아닌 **너 자신의 근거·초안**이면 `notes/` 로 간다. 노트는 손으로 찍지 말고 CLI 로 쓴다 — 경로·frontmatter·날짜를 코드가 보장하고, 다음 run 에 넘길 인자를 출력해 준다:
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
okstra recap note <resolved-target> --project-root <projectRoot> \
|
|
768
|
+
--kind <verification-evidence|decision-draft|analysis-note> \
|
|
769
|
+
--slug <short-topic-slug> \
|
|
770
|
+
--purpose "<한 줄 — 무엇이며 어느 run/decision 에 투입되는지>" \
|
|
771
|
+
--scope-note "<한 줄 — 무엇이 아닌지, 예: C-00x 에 대한 사용자 결정이 아님>" \
|
|
772
|
+
--body-file <노트 본문 마크다운 경로>
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
본문은 먼저 scratchpad 에 마크다운으로 쓴 뒤 `--body-file` 로 넘긴다(짧으면 `--body "<md>"` 인라인도 가능). CLI 는 `.okstra/tasks/<group>/<id>/notes/<slug>-<YYYY-MM-DD>.md` 를 생성하고 stdout JSON 으로 `notePath` 와 `clarificationResponseArg` 를 돌려준다.
|
|
776
|
+
|
|
777
|
+
**self-check 규칙 (validator 가 없으니 스스로 지킨다):**
|
|
778
|
+
|
|
779
|
+
1. **렌더된 report 를 수기 편집하지 않는다** (`runs/*/reports/` 아래 `*.md` / `*.data.json` / `*.html`). `data.json` 에서 재생성되므로 편집은 덮어써지고 SSOT 와 어긋난다. 발견 내용은 `notes/` 에 쓴다.
|
|
780
|
+
2. **`user-responses/` 파일을 작성하거나 `created-by: user` 를 달지 않는다.** 그건 사용자의 결정이며, 대신 쓰면 사용자가 내리지 않은 결정을 위조하는 것이다. 네 증거가 특정 답을 뒷받침하면 `notes/` 에 그렇게 적고 결정은 사용자에게 맡긴다. 단, `okstra-user-response` 스킬의 에코백 확인 흐름은 예외다 — 그 흐름에서는 사용자가 세션에서 직접 결정을 내리고 CLI 는 전사 채널일 뿐이므로, `write` 로 `created-by: user` sidecar 를 기록하는 것이 정당하다. 이 예외는 사용자가 명시적으로 "맞음" 을 확인한 뒤에만, verbatim 입력에 대해서만 성립한다.
|
|
781
|
+
3. **okstra 관리 디렉토리에 쓰지 않는다** (`runs/`, `instruction-set/`, `history/`, `recap/`, `.okstra/decisions/`). `notes/` 만이 agent 소유 레인이다. `recap/recap-log.jsonl` 은 예외적으로 쓰되 손으로 편집하지 말고 `okstra recap record` CLI 로만 남긴다.
|
|
782
|
+
4. **`notes/` 는 okstra 에 inert 하다** — 어떤 run 도 자동으로 읽지 않는다. 작성 후 CLI 가 출력한 `clarificationResponseArg`(예: `--clarification-response <notePath>`)를 사용자에게 그대로 알려, 다음 run 을 그 인자와 함께 실행해야만 반영된다는 것을 전한다.
|
|
783
|
+
|
|
784
|
+
**Guardrail:** `.okstra/` 는 gitignored 다 — `notes/` 는 로컬 scratch 로 취급하고 절대 `git add` 하지 않는다. 새 노트 생성은 되돌리기 쉬우니(파일 삭제), 생성물이나 사용자 소유 파일을 편집하는 것보다 항상 이쪽을 택한다.
|
|
785
|
+
|
|
751
786
|
---
|
|
752
787
|
|
|
753
788
|
## Output Rules (shared)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: okstra-pr-gen
|
|
3
|
-
description: Use when the user wants to register a PR body template or generate a PR description from a branch diff using a stored (or default) template. Manages templates under ~/.okstra/template/pr and drives okstra pr {template,branches,gen}. Trigger words include "okstra pr", "PR
|
|
3
|
+
description: Use when the user wants to register a PR body template or generate a PR description from a branch diff using a stored (or default) template. Manages templates under ~/.okstra/template/pr and drives okstra pr {template,branches,gen}. Trigger words include "okstra pr", "register PR template", "generate PR body", "generate PR message", "generate PR", "pr gen", and "register template".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# okstra-pr-gen
|
|
@@ -31,11 +31,11 @@ If `okstra` is not on PATH, tell the user:
|
|
|
31
31
|
|
|
32
32
|
Present a 3-option picker (`AskUserQuestion`):
|
|
33
33
|
|
|
34
|
-
1. `PR
|
|
35
|
-
2.
|
|
36
|
-
3.
|
|
34
|
+
1. `Generate PR` — generate a PR body from a branch diff.
|
|
35
|
+
2. `Register template` — save a new PR body template.
|
|
36
|
+
3. `Enter manually` — always the last option (okstra picker convention).
|
|
37
37
|
|
|
38
|
-
## Mode A — PR
|
|
38
|
+
## Mode A — Generate PR
|
|
39
39
|
|
|
40
40
|
### A1. Pick a template
|
|
41
41
|
|
|
@@ -44,7 +44,8 @@ okstra pr template list --json
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
- If `templates` is non-empty, present a picker: 1–2 recommended templates from
|
|
47
|
-
the list, plus
|
|
47
|
+
the list, plus `Default template` (the bundled default), plus `Enter manually`
|
|
48
|
+
last.
|
|
48
49
|
- If empty, tell the user the bundled default template will be used.
|
|
49
50
|
|
|
50
51
|
Carry the chosen template name as `<template>` (`default` for the bundled one).
|
|
@@ -55,7 +56,7 @@ Carry the chosen template name as `<template>` (`default` for the bundled one).
|
|
|
55
56
|
okstra pr branches --json
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
Present a 3-option base picker from `recommended` (top entries) plus
|
|
59
|
+
Present a 3-option base picker from `recommended` (top entries) plus `Enter manually`
|
|
59
60
|
last. Carry the choice as `<base>`.
|
|
60
61
|
|
|
61
62
|
### A3. Build the generation bundle and fill the template
|
|
@@ -91,7 +92,7 @@ or not authenticated (`gh auth status` fails), leave the text in chat and tell
|
|
|
91
92
|
the user to create the PR manually. Do not push or create the PR without the
|
|
92
93
|
user's explicit confirmation.
|
|
93
94
|
|
|
94
|
-
## Mode B —
|
|
95
|
+
## Mode B — Register template
|
|
95
96
|
|
|
96
97
|
1. Ask for a template name (`AskUserQuestion`, free text). It must match
|
|
97
98
|
`^[A-Za-z0-9._-]+$`; re-ask on invalid input.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: okstra-user-response
|
|
3
|
+
description: >-
|
|
4
|
+
Use this to answer an okstra task's open clarification questions in-session, without hand-editing any file. The tell is a request to respond to an okstra run's clarification items or its approval gate — "answer okstra", "I'll answer the questions", "clarification response", "user response", "approve and move on". This skill lists tasks whose latest report still has open clarification blockers, presents each open C-id (statement + recommended answer + alternatives), collects the user's own verbatim answers (or a reframe/hold), echoes the full sidecar back for an explicit "confirmed" acknowledgement, optionally records approval, and writes the `user-responses/` sidecar via `okstra user-response write`. NOT for starting a run (okstra-run), inspecting a finished task (okstra-inspect), or generating a brief (okstra-brief-gen). The skill never picks an answer for the user — it only relays recommendations and transcribes what the user decides.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# OKSTRA User Response
|
|
8
|
+
|
|
9
|
+
Single entry point for answering the clarification questions an okstra run left behind (the open `C-*` rows under the final report's `## 1. Clarification Items`) **in-session**, and recording those answers as a `runs/<type>/user-responses/` sidecar. The next `/okstra-run` auto-attaches this sidecar via `--clarification-response`.
|
|
10
|
+
|
|
11
|
+
**Core principle — the skill never picks an answer for the user.** This skill only *presents* recommendations and alternatives; every `value` is the user's own verbatim input. It never calls `write` until the user has explicitly confirmed.
|
|
12
|
+
|
|
13
|
+
| Sub-command | What it does |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `list` | List tasks that still have approval-open clarification, newest report first. |
|
|
16
|
+
| `show` | Expand one report's open `C-*` rows (statement + recommended + alternatives + contextRefs). |
|
|
17
|
+
| `write` | Record the collected answers (+ optional approval) as a `user-responses/` sidecar. |
|
|
18
|
+
|
|
19
|
+
## Step 0: Preflight (shared)
|
|
20
|
+
|
|
21
|
+
Before anything, run one Bash tool call whose leading token is the literal `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` — a non-literal leading token defeats the `Bash(okstra:*)` permission match):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
okstra preflight --runtime claude-code --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Branch on the stdout JSON:
|
|
28
|
+
- `ok: true` → carry `projectRoot` and `projectId` as literal strings; they are the base for every step below.
|
|
29
|
+
- `ok: false` → this project has no okstra setup. Tell the user: "this project has no okstra setup. Run `/okstra-setup` first." Then stop. If the user pointed at a specific project directory, re-run targeting it: `okstra preflight --runtime claude-code --cwd <that-dir> --json` (`--cwd` is the sanctioned way to target a project — a leading `cd` would break the permission match); only if that **also** returns `ok:false` do you 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).
|
|
30
|
+
|
|
31
|
+
Then resolve the okstra home once (the `list` sub-command needs it):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
okstra paths --field home
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Paste the printed path literally into `--home` below. Every subsequent `okstra <subcmd>` call self-bootstraps its Python path, so this skill never needs `okstra paths --shell` / `export PYTHONPATH=...`.
|
|
38
|
+
|
|
39
|
+
## Step 1: List awaiting tasks → picker
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
okstra user-response list --home <resolved-home> --project <projectId> --limit 3
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Returns a JSON array (latest report mtime first); each entry:
|
|
46
|
+
`{taskKey, taskType, seq, reportPath, reportMtime, openApprovalCount, unreadable}`.
|
|
47
|
+
|
|
48
|
+
- Empty array → answer `No task has open clarification items.` and stop.
|
|
49
|
+
- Otherwise present a **3-option picker**: the top recommendations from the list (each shown as `taskKey (taskType, N open items)`), and the **final option is always "Enter directly"** — where the user pastes a `reportPath` or a `task-key` directly (for a task not in the top-3 window).
|
|
50
|
+
- An entry with `unreadable: true` means its `## 1. Clarification Items` heading exists but drifted from the expected format. Flag it as `⚠ §1 format drift — the CLI could not parse its items` and do not proceed on it until the report is regenerated; do not fabricate rows for it.
|
|
51
|
+
|
|
52
|
+
Carry the chosen entry's `reportPath` and `taskKey` forward.
|
|
53
|
+
|
|
54
|
+
## Step 2: Show open rows
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
okstra user-response show --report <reportPath>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Returns `{reportPath, rows: [{id, kind, blocks, status, statement, recommended, alternatives, contextRefs}]}`. For each open `C-*` row, present to the user:
|
|
61
|
+
|
|
62
|
+
- **`id`** and its **`statement`** (the original question).
|
|
63
|
+
- **`recommended`** — the answer okstra proposed (+ rationale). It is only a recommendation.
|
|
64
|
+
- **`alternatives[]`** — list them if present.
|
|
65
|
+
- **`contextRefs[]`** — the report `§`/`C-id`/`path:line` references this question points at.
|
|
66
|
+
|
|
67
|
+
## Step 3: Per-item — collect the user's decision (4 branches)
|
|
68
|
+
|
|
69
|
+
For each open item, the user picks one of four dispositions. You relay and transcribe; you never choose:
|
|
70
|
+
|
|
71
|
+
1. **Answer** — the user accepts the recommendation or gives their own answer. `disposition: "answer"`, `value` = the user's utterance verbatim.
|
|
72
|
+
2. **Ask-to-explain** — if the user asks "what does this mean?", open the report `§`/`path:line` that this item's `contextRefs[]` points at **using Read** and explain it in plain language. Only explain — **do not pick the answer for the user**; after explaining, hand the decision back to the user.
|
|
73
|
+
3. **Free-form** — the user gives a narrative answer that matches neither a recommendation nor an alternative. `value` = that narrative verbatim, with the rationale in `rationale` if needed. `disposition: "answer"`.
|
|
74
|
+
4. **Hold + reframe** — if the user asks to "re-frame this question itself", record it as `disposition: "reframe"`. Put what the user wants re-asked into `value` verbatim. A reframe is not an answer, so it does not satisfy the approval gate — the next run re-frames that question.
|
|
75
|
+
|
|
76
|
+
Each answer item's JSON shape: `{id, kind, value, rationale?, disposition}`. `kind` uses the same `kind` that `show` gave for that row.
|
|
77
|
+
|
|
78
|
+
## Step 4: Echo the full sidecar back → explicit "confirmed" gate
|
|
79
|
+
|
|
80
|
+
Once every open item is handled, **before** calling `write`, **echo back** everything collected (each `id`, `disposition`, `value`, `rationale` if present, and the approval decision from Step 5 below) to the user as-is. Then ask for explicit confirmation:
|
|
81
|
+
|
|
82
|
+
> Record it as shown above? (Reply `confirmed` to write the sidecar.)
|
|
83
|
+
|
|
84
|
+
- **Never call `write`** until the user says `confirmed` (or gives clear approval).
|
|
85
|
+
- If the user changes any item, show the echo-back again with the changed value and re-confirm.
|
|
86
|
+
- Every `value` must be the user's utterance verbatim — this gate guarantees that.
|
|
87
|
+
|
|
88
|
+
## Step 5: (Optional) Record approval
|
|
89
|
+
|
|
90
|
+
Record approval alongside only when the report's approval-blocking clarification items are **all filled with an answer** and the user explicitly said "approve with this option":
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
--approval '{"approved":true,"implementationOption":"<the option the user chose>"}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If any approval-target item is not filled with an answer, or is a reframe/hold, do not approve — tell the user the gate is still open.
|
|
97
|
+
|
|
98
|
+
## Step 6: Write the sidecar
|
|
99
|
+
|
|
100
|
+
Run only after the user has confirmed the echo-back with "confirmed":
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
okstra user-response write --report <reportPath> --answers '<answers-json>' --approval '<approval-json>' --task-key <taskKey>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `--answers` = the JSON array of `{id, kind, value, rationale?, disposition}` objects collected in Step 3.
|
|
107
|
+
- `--approval` is attached only when recording approval in Step 5 (otherwise omit it).
|
|
108
|
+
- `--task-key` is the value carried forward from Step 1/2.
|
|
109
|
+
- Report the path from the returned JSON `{sidecar: <path>}` to the user. (`write` merges items when a sidecar already exists — the same `id` is overwritten with the new value.)
|
|
110
|
+
|
|
111
|
+
## Step 7: Self-describing next-step guidance
|
|
112
|
+
|
|
113
|
+
Leave the following in the final output as-is:
|
|
114
|
+
|
|
115
|
+
> This answer was recorded in the `user-responses/` sidecar (`<sidecar path>`). Re-running this task's `<task-type>` with `/okstra-run` auto-attaches this answer (if you approved all approval-blocking items, the next phase is `implementation`).
|
|
116
|
+
|
|
117
|
+
## Output Rules (shared)
|
|
118
|
+
|
|
119
|
+
- Keep responses concise and in the language the user is using, unless they ask otherwise.
|
|
120
|
+
- Give path guidance host-relative (`~/.okstra/...` or relative to the `projectRoot` preflight returned). Use repo paths only when pointing at a code source.
|
|
121
|
+
- This skill never hand-edits a rendered report (`runs/*/reports/*.md` / `*.data.json`). Writing the `user-responses/` sidecar goes only through the `okstra user-response write` CLI.
|
|
@@ -144,9 +144,12 @@
|
|
|
144
144
|
var e = entries[i];
|
|
145
145
|
var chunk =
|
|
146
146
|
"\n## " + e.responseId + "\n" +
|
|
147
|
-
"- Kind: " + e.kind + "\n"
|
|
148
|
-
|
|
149
|
-
"
|
|
147
|
+
"- Kind: " + e.kind + "\n";
|
|
148
|
+
if (e.disposition && e.disposition !== "answer") {
|
|
149
|
+
chunk += "- Disposition: " + e.disposition + "\n";
|
|
150
|
+
}
|
|
151
|
+
var valueLines = trimMultiline(e.value).split("\n");
|
|
152
|
+
chunk += "- Value:\n" + valueLines.map(function (ln) { return " > " + ln + "\n"; }).join("");
|
|
150
153
|
if (e.rationale) {
|
|
151
154
|
chunk += "- Rationale: " + trimMultiline(e.rationale) + "\n";
|
|
152
155
|
}
|
|
@@ -25,6 +25,7 @@ created-at: <ISO 8601 UTC timestamp>
|
|
|
25
25
|
```markdown
|
|
26
26
|
## <Response ID>
|
|
27
27
|
- Kind: <material | decision | data-point>
|
|
28
|
+
- Disposition: <answer | reframe> # answer(기본)면 라인 생략. reframe = 답 보류 + 재질문(행 미해결 유지)
|
|
28
29
|
- Value:
|
|
29
30
|
> <multi-line value, trimmed>
|
|
30
31
|
- Rationale: <optional one-line rationale>
|
package/src/cli-registry.mjs
CHANGED
|
@@ -166,6 +166,13 @@ export const COMMAND_REGISTRY = [
|
|
|
166
166
|
category: "introspection",
|
|
167
167
|
summary: ["Assemble a task's run-to-run phase recap or append a recap log entry"],
|
|
168
168
|
},
|
|
169
|
+
{
|
|
170
|
+
name: "user-response",
|
|
171
|
+
module: "./commands/inspect/user-response.mjs",
|
|
172
|
+
export: "run",
|
|
173
|
+
category: "introspection",
|
|
174
|
+
summary: ["Answer a task's open clarification questions in-session and write the response sidecar"],
|
|
175
|
+
},
|
|
169
176
|
{
|
|
170
177
|
name: "rollup",
|
|
171
178
|
module: "./commands/inspect/rollup.mjs",
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
2
|
|
|
3
|
-
const USAGE = `okstra recap — assemble a task's run-to-run phase recap,
|
|
3
|
+
const USAGE = `okstra recap — assemble a task's run-to-run phase recap, append a recap log entry, or write an agent note
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
6
|
okstra recap assemble <task-root|task-key> [--project-root <dir>] [--cwd <dir>]
|
|
7
7
|
okstra recap record <task-root|task-key> --kind <summary|qa> --mode <artifact|code> \\
|
|
8
8
|
[--question <text>] --answer <text> [--citation <path:line> ...]
|
|
9
|
+
okstra recap note <task-root|task-key> \\
|
|
10
|
+
--kind <verification-evidence|decision-draft|analysis-note> --slug <topic> \\
|
|
11
|
+
--purpose <text> --scope-note <text> (--body <markdown> | --body-file <path>)
|
|
9
12
|
|
|
10
13
|
\`assemble\` is read-only and prints a JSON summary of phase transitions across runs.
|
|
11
14
|
\`record\` appends one line to <task-root>/recap/recap-log.jsonl; it never mutates
|
|
12
15
|
other task artifacts.
|
|
16
|
+
\`note\` writes an agent-authored markdown note to <task-root>/notes/ and prints its
|
|
17
|
+
path plus the \`--clarification-response\` argument for feeding it into a later run.
|
|
13
18
|
`;
|
|
14
19
|
|
|
15
20
|
export async function run(args) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra user-response — 태스크의 열린 clarification 질문에 세션 내에서 답하고 sidecar 기록
|
|
4
|
+
|
|
5
|
+
A thin shim over \`python3 -m okstra_ctl.user_response\`. JSON 출력.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
okstra user-response list --home <dir> --project <id> [--limit <n>]
|
|
9
|
+
okstra user-response show --report <md>
|
|
10
|
+
okstra user-response write --report <md> --answers <json> \\
|
|
11
|
+
[--approval <json>] [--task-key <key>]
|
|
12
|
+
|
|
13
|
+
Exit codes: 0 ok / 1 오류
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function run(args) {
|
|
17
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
18
|
+
process.stdout.write(USAGE);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const { code } = await runPythonModule({
|
|
22
|
+
module: "okstra_ctl.user_response",
|
|
23
|
+
args,
|
|
24
|
+
});
|
|
25
|
+
return code ?? 1;
|
|
26
|
+
}
|