okstra 0.121.1 → 0.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +4 -2
  2. package/docs/architecture/storage-model.md +14 -0
  3. package/docs/architecture.md +38 -8
  4. package/docs/cli.md +45 -5
  5. package/docs/for-ai/README.md +2 -2
  6. package/docs/for-ai/skills/okstra-rollup.md +1 -1
  7. package/docs/for-ai/skills/{okstra-schedule.md → okstra-schedule-gen.md} +3 -3
  8. package/docs/project-structure-overview.md +4 -3
  9. package/docs/task-process/implementation-planning.md +33 -9
  10. package/docs/task-process/implementation.md +21 -2
  11. package/package.json +1 -1
  12. package/runtime/BUILD.json +2 -2
  13. package/runtime/agents/workers/claude-worker.md +1 -1
  14. package/runtime/bin/lib/okstra/usage.sh +3 -3
  15. package/runtime/prompts/launch.template.md +9 -6
  16. package/runtime/prompts/lead/adapters/claude-code.md +104 -0
  17. package/runtime/prompts/lead/adapters/codex.md +45 -0
  18. package/runtime/prompts/lead/adapters/external.md +46 -0
  19. package/runtime/prompts/lead/context-loader.md +5 -5
  20. package/runtime/prompts/lead/convergence.md +11 -28
  21. package/runtime/prompts/lead/okstra-lead-contract.md +44 -94
  22. package/runtime/prompts/lead/plan-body-verification.md +50 -5
  23. package/runtime/prompts/lead/report-writer.md +59 -54
  24. package/runtime/prompts/lead/team-contract.md +33 -106
  25. package/runtime/prompts/profiles/_common-contract.md +3 -3
  26. package/runtime/prompts/profiles/_implementation-deliverable.md +3 -1
  27. package/runtime/prompts/profiles/_implementation-executor.md +6 -1
  28. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  29. package/runtime/prompts/profiles/final-verification.md +2 -0
  30. package/runtime/prompts/profiles/implementation-planning.md +14 -4
  31. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  32. package/runtime/prompts/wizard/prompts.ko.json +44 -0
  33. package/runtime/python/okstra_ctl/codex_dispatch.py +23 -1
  34. package/runtime/python/okstra_ctl/design_prep.py +1462 -0
  35. package/runtime/python/okstra_ctl/design_surfaces.py +243 -0
  36. package/runtime/python/okstra_ctl/final_report_schema.py +33 -1
  37. package/runtime/python/okstra_ctl/implementation_stage.py +35 -0
  38. package/runtime/python/okstra_ctl/incremental_carry.py +294 -21
  39. package/runtime/python/okstra_ctl/incremental_scope.py +51 -5
  40. package/runtime/python/okstra_ctl/lead_runtime.py +4 -0
  41. package/runtime/python/okstra_ctl/material.py +1 -1
  42. package/runtime/python/okstra_ctl/model_discovery.py +98 -0
  43. package/runtime/python/okstra_ctl/models.py +8 -3
  44. package/runtime/python/okstra_ctl/render.py +81 -194
  45. package/runtime/python/okstra_ctl/report_views.py +22 -7
  46. package/runtime/python/okstra_ctl/run.py +53 -5
  47. package/runtime/python/okstra_ctl/team_reconcile.py +3 -1
  48. package/runtime/python/okstra_ctl/user_response.py +67 -2
  49. package/runtime/python/okstra_ctl/wizard.py +283 -3
  50. package/runtime/python/okstra_token_usage/report.py +11 -0
  51. package/runtime/schemas/final-report-v1.0.schema.json +339 -0
  52. package/runtime/skills/okstra-inspect/SKILL.md +2 -2
  53. package/runtime/skills/okstra-rollup/SKILL.md +1 -1
  54. package/runtime/skills/{okstra-schedule → okstra-schedule-gen}/SKILL.md +62 -20
  55. package/runtime/skills/okstra-user-response/SKILL.md +8 -6
  56. package/runtime/templates/reports/final-report.template.md +71 -4
  57. package/runtime/templates/reports/i18n/en.json +32 -1
  58. package/runtime/templates/reports/i18n/ko.json +32 -1
  59. package/runtime/validators/validate-run.py +515 -5
  60. package/runtime/validators/validate-schedule.py +11 -7
  61. package/runtime/validators/validate_session_conformance.py +58 -33
  62. package/src/cli-registry.mjs +14 -0
  63. package/src/commands/inspect/design-prep.mjs +23 -0
  64. package/src/commands/inspect/stage-map.mjs +100 -0
  65. package/src/commands/lifecycle/install.mjs +3 -0
  66. package/src/commands/lifecycle/uninstall.mjs +8 -23
  67. package/src/lib/skill-catalog.mjs +10 -1
@@ -1,22 +1,20 @@
1
- """prompts/lead/okstra-lead-contract.md BLOCKING 계약 3종의 post-hoc conformance 검사.
2
-
3
- 설계: docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
4
-
5
- | 검사 | 선언 위치 | 증거 |
6
- |------|----------|------|
7
- | 1. lead PROGRESS 체크포인트 라인 | prompts/lead/okstra-lead-contract.md "Progress reporting (BLOCKING)" | lead 세션 jsonl assistant text 블록 |
8
- | 2. claude-worker 5분 heartbeat | agents/workers/claude-worker.md "Heartbeat" | audit 사이드카의 `- PROGRESS: <stage> <ISO>` 라인 |
9
- | 3. implementation sidecar entry guard | prompts/lead/okstra-lead-contract.md "Entry guard (BLOCKING)" | lead 세션 jsonl Read tool_use |
10
-
11
- 스캔 규칙 (false-pass 방지가 핵심):
12
- - `type == "assistant"` 레코드만 본다 Skill 호출 시 SKILL.md 본문(체크포인트
13
- 라인 예시 포함)이 tool_result(user 레코드)로 transcript 에 주입되므로,
14
- assistant 레코드를 보면 즉시 false pass 난다. `isSidechain` 레코드도 제외.
15
- - run 윈도우(`resolve_run_window`)로 스코핑한다 — in-session lead 세션 전체
16
- jsonl 기록되므로, 같은 세션의 직전 run 이 남긴 라인이 증거로 오인된다.
17
- (실측: dev-9692 lead 세션 1개에 implementation run 001–003 이 모두 들어 있음.)
18
- - validator 실행 시점(Phase 7) **이후** 에 출력되는 체크포인트
19
- (`phase-7-teardown`, `complete`)는 구조적으로 검사 불가 — 요구하지 않는다.
1
+ """Post-hoc checks for three BLOCKING neutral lead lifecycle contracts.
2
+
3
+ Design: docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
4
+
5
+ | Check | Contract | Evidence |
6
+ |-------|----------|----------|
7
+ | 1. lead PROGRESS checkpoints | prompts/lead/okstra-lead-contract.md "Progress reporting (BLOCKING)" | selected adapter evidence source |
8
+ | 2. worker-provider heartbeat | worker provider contract | audit sidecar `- PROGRESS: <stage> <ISO>` lines |
9
+ | 3. implementation sidecar entry guard | prompts/lead/okstra-lead-contract.md "Entry guard (BLOCKING)" | selected adapter evidence source |
10
+
11
+ Evidence rules prevent false passes:
12
+ - `claude-jsonl` accepts only assistant text and Read tool-use records, excluding
13
+ injected skill text and sidechain records.
14
+ - Session evidence is scoped to the current run window so a previous run cannot
15
+ satisfy the neutral lead lifecycle contract.
16
+ - Checkpoints emitted after validation begins (`phase-7-teardown`, `complete`)
17
+ cannot be observed and are not required.
20
18
  """
21
19
  from __future__ import annotations
22
20
 
@@ -202,7 +200,8 @@ def _collect_lead_evidence(
202
200
  if not evidence.scanned_files:
203
201
  return None, (
204
202
  f"lead session jsonl not found under {proj_dir} "
205
- f"(lead.sessionId={lead_sid or '<empty>'}) — PROGRESS checkpoint / "
203
+ f"(lead.sessionId={lead_sid or '<empty>'}) — selected adapter evidence "
204
+ "source `claude-jsonl` cannot verify the PROGRESS checkpoint / "
206
205
  "implementation entry-guard conformance cannot be verified, which "
207
206
  "fails the run (same principle as the token-usage accuracy contract)."
208
207
  )
@@ -223,15 +222,16 @@ def _resolve_lead_events_path(
223
222
  if not raw:
224
223
  return None, (
225
224
  "artifact lead event log path missing from team-state "
226
- "(`leadEventsPath`) — artifact-only conformance cannot be verified."
225
+ "(`leadEventsPath`) — selected adapter evidence source `artifact-only` "
226
+ "cannot verify conformance."
227
227
  )
228
228
  path = Path(str(raw))
229
229
  if not path.is_absolute():
230
230
  path = project_root / path
231
231
  if not path.is_file():
232
232
  return None, (
233
- f"artifact lead event log not found: {path} — artifact-only "
234
- "conformance cannot be verified."
233
+ f"artifact lead event log not found: {path} — selected adapter evidence "
234
+ "source `artifact-only` cannot verify conformance."
235
235
  )
236
236
  return path, None
237
237
 
@@ -301,7 +301,10 @@ def _collect_artifact_lead_evidence(
301
301
  try:
302
302
  events = read_lead_events(events_path)
303
303
  except LeadEventParseError as exc:
304
- return None, f"artifact lead event log is malformed — {exc}"
304
+ return None, (
305
+ "artifact lead event log is malformed — selected adapter evidence "
306
+ f"source `artifact-only` cannot verify conformance: {exc}"
307
+ )
305
308
 
306
309
  run_seq = suffix.rsplit("-", 1)[1]
307
310
  evidence = _LeadEvidence(scanned_files=[events_path])
@@ -324,6 +327,27 @@ def _collect_artifact_lead_evidence(
324
327
  return evidence, None
325
328
 
326
329
 
330
+ def _conformance_evidence_source(
331
+ team_state: dict,
332
+ ) -> tuple[str | None, str | None]:
333
+ dispatch_mode = str(team_state.get("dispatchMode", "")).strip()
334
+ if dispatch_mode in _WORKER_DISPATCH_MODES:
335
+ return "artifact-only", None
336
+
337
+ adapter = team_state.get("leadAdapter")
338
+ if isinstance(adapter, dict):
339
+ accounting = str(adapter.get("sessionAccounting", "")).strip()
340
+ if accounting in {"claude-jsonl", "artifact-only"}:
341
+ return accounting, None
342
+ if accounting:
343
+ return None, f"unsupported sessionAccounting: `{accounting}`"
344
+
345
+ legacy_runtime = str(team_state.get("leadRuntime", "") or "claude-code")
346
+ if legacy_runtime in {"codex", "external"}:
347
+ return "artifact-only", None
348
+ return "claude-jsonl", None
349
+
350
+
327
351
  def _convergence_rounds_ran(run_dir: Path, suffix: str | None) -> bool:
328
352
  """이 run 의 convergence state artifact 가 실제 round 를 1회 이상 돌았는지.
329
353
  auto-disable(`totalRounds: 0`)·artifact 부재는 phase-5.5 라인을 요구하지 않는다."""
@@ -569,7 +593,8 @@ def _check_implementation_sidecar_reads(evidence: _LeadEvidence, errors: list[st
569
593
  if not ts_list:
570
594
  errors.append(
571
595
  f"implementation entry guard: no `Read` of `{basename}` found in "
572
- f"the lead session jsonl within this run's window — the sidecar "
596
+ "the selected adapter evidence source within this run's window — "
597
+ "the sidecar "
573
598
  f"MUST be read fresh at {read_at} every implementation run "
574
599
  "(prompts/lead/okstra-lead-contract.md 'Entry guard (BLOCKING)')."
575
600
  )
@@ -593,10 +618,10 @@ def validate_session_conformance(
593
618
  task_type: str,
594
619
  claude_projects_dir: Path | None = None,
595
620
  ) -> SessionConformanceResult:
596
- """post-hoc conformance 검사 3종을 수행하고 실패 목록을 돌려준다.
621
+ """Run three post-hoc checks for the neutral lead lifecycle contract.
597
622
 
598
- `claude_projects_dir` 테스트/진단용 주입 시드 (기본: 실제 ~/.claude/projects).
599
- 검사 2(heartbeat)는 디스크 사이드카만 보므로 lead jsonl 미발견 시에도 수행된다.
623
+ `claude_projects_dir` injects the Claude project root for tests and diagnostics.
624
+ Heartbeat validation runs before selecting the adapter evidence source.
600
625
  """
601
626
  result = SessionConformanceResult()
602
627
  _ensure_token_usage_importable()
@@ -610,11 +635,11 @@ def validate_session_conformance(
610
635
  suffix = run_artifact_suffix(team_state_path)
611
636
  _check_heartbeat_sidecars(run_dir, task_type, suffix, result.errors)
612
637
 
613
- lead_runtime = team_state.get("leadRuntime", "") or "claude-code"
614
- dispatch_mode = str(team_state.get("dispatchMode", "")).strip()
615
- if lead_runtime in {"codex", "external"} or (
616
- lead_runtime == "claude-code" and dispatch_mode in _WORKER_DISPATCH_MODES
617
- ):
638
+ evidence_source, source_error = _conformance_evidence_source(team_state)
639
+ if source_error:
640
+ result.errors.append(source_error)
641
+ return result
642
+ if evidence_source == "artifact-only":
618
643
  evidence, error = _collect_artifact_lead_evidence(
619
644
  team_state, project_root, task_type, suffix
620
645
  )
@@ -110,6 +110,13 @@ export const COMMAND_REGISTRY = [
110
110
  category: "introspection",
111
111
  summary: ["Summarize a task's manifest + workflow phase state"],
112
112
  },
113
+ {
114
+ name: "stage-map",
115
+ module: "./commands/inspect/stage-map.mjs",
116
+ export: "run",
117
+ category: "introspection",
118
+ summary: ["Dump a task's implementation-planning Stage Map + done stages"],
119
+ },
113
120
  {
114
121
  name: "context-cost",
115
122
  module: "./commands/inspect/context-cost.mjs",
@@ -180,6 +187,13 @@ export const COMMAND_REGISTRY = [
180
187
  category: "introspection",
181
188
  summary: ["Answer a task's open clarification questions in-session and write the response sidecar"],
182
189
  },
190
+ {
191
+ name: "design-prep",
192
+ module: "./commands/inspect/design-prep.mjs",
193
+ export: "run",
194
+ category: "introspection",
195
+ summary: ["Review and record AI-prepared implementation design inputs"],
196
+ },
183
197
  {
184
198
  name: "rollup",
185
199
  module: "./commands/inspect/rollup.mjs",
@@ -0,0 +1,23 @@
1
+ import { runPythonModule } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra design-prep — review AI-prepared implementation design inputs
4
+
5
+ Usage:
6
+ okstra design-prep list [--project-root <path>] [--report <path>]
7
+ okstra design-prep show --report <path> [--item PREP-001]
8
+ okstra design-prep write --report <path> --item PREP-001 \\
9
+ --decision <accept-draft|modify-draft|reject-draft|defer> \\
10
+ [--overrides <json-object>] [--notes <text>] --confirmed
11
+ `;
12
+
13
+ export async function run(args) {
14
+ if (args.includes("--help") || args.includes("-h")) {
15
+ process.stdout.write(USAGE);
16
+ return 0;
17
+ }
18
+ const { code } = await runPythonModule({
19
+ module: "okstra_ctl.design_prep",
20
+ args,
21
+ });
22
+ return code ?? 1;
23
+ }
@@ -0,0 +1,100 @@
1
+ import { runPythonSnippet, emitJsonError } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra stage-map — dump a task's implementation-planning Stage Map + done stages
4
+
5
+ Usage:
6
+ okstra stage-map <task-key> task-key is project-id:task-group:task-id
7
+ okstra stage-map <task-key> --cwd <dir> Resolve PROJECT_ROOT from <dir>
8
+ okstra stage-map <task-key> --project <dir> Use <dir> directly as PROJECT_ROOT
9
+
10
+ Output: JSON { ok, taskKey, taskRoot, stages:[{stage_number,title,depends_on,step_count}], doneStages:[int] }.
11
+ stages is [] when no implementation-planning Stage Map exists.
12
+ `;
13
+
14
+ function parseArgs(args) {
15
+ const opts = { cwd: process.cwd(), projectRoot: "", taskKey: "" };
16
+ const positional = [];
17
+ for (let i = 0; i < args.length; i++) {
18
+ const a = args[i];
19
+ if (a === "--cwd") {
20
+ opts.cwd = args[++i];
21
+ if (!opts.cwd) throw new Error("--cwd requires a path");
22
+ } else if (a === "--project") {
23
+ opts.projectRoot = args[++i];
24
+ if (!opts.projectRoot) throw new Error("--project requires a path");
25
+ } else if (a === "--json") {
26
+ // JSON is always emitted; accept the flag for parity with sibling commands.
27
+ } else if (a.startsWith("--")) {
28
+ throw new Error(`unknown argument '${a}'`);
29
+ } else {
30
+ positional.push(a);
31
+ }
32
+ }
33
+ if (positional.length !== 1) {
34
+ throw new Error("expected exactly one positional argument: <task-key>");
35
+ }
36
+ opts.taskKey = positional[0];
37
+ return opts;
38
+ }
39
+
40
+ const SCRIPT = `
41
+ import json, sys
42
+ from pathlib import Path
43
+ from okstra_project import resolve_project_root, find_task_root, read_task_manifest, ResolverError
44
+ from okstra_ctl.implementation_outcome import _load_stage_map
45
+ from okstra_ctl.consumers import read_stage_consumer_state
46
+
47
+ explicit, cwd, task_key = sys.argv[1], sys.argv[2], sys.argv[3]
48
+
49
+ try:
50
+ pr = resolve_project_root(explicit_root=explicit, cwd=cwd)
51
+ except ResolverError as e:
52
+ print(json.dumps({"ok": False, "stage": "resolve", "reason": str(e)}))
53
+ sys.exit(2)
54
+
55
+ task_root = find_task_root(Path(pr), task_key)
56
+ if task_root is None:
57
+ print(json.dumps({"ok": False, "stage": "task_root", "reason": f"task root not found for {task_key}"}))
58
+ sys.exit(1)
59
+
60
+ manifest = read_task_manifest(task_root) or {}
61
+ stages = _load_stage_map(task_root, manifest)
62
+
63
+ plan_run_root = task_root / "runs" / "implementation-planning"
64
+ done = sorted(read_stage_consumer_state(plan_run_root, recover_from_carry=True).done_stages) if plan_run_root.is_dir() else []
65
+
66
+ print(json.dumps({
67
+ "ok": True,
68
+ "taskKey": task_key,
69
+ "taskRoot": str(task_root),
70
+ "stages": stages,
71
+ "doneStages": done,
72
+ }, ensure_ascii=False, indent=2))
73
+ `;
74
+
75
+ export async function run(args) {
76
+ if (args.includes("--help") || args.includes("-h")) {
77
+ process.stdout.write(USAGE);
78
+ return 0;
79
+ }
80
+ let opts;
81
+ try {
82
+ opts = parseArgs(args);
83
+ } catch (err) {
84
+ process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
85
+ return 2;
86
+ }
87
+ const result = await runPythonSnippet({
88
+ script: SCRIPT,
89
+ args: [opts.projectRoot, opts.cwd, opts.taskKey],
90
+ });
91
+ if (result.code !== 0 && !result.stdout.trim()) {
92
+ emitJsonError({
93
+ stage: "python",
94
+ reason: `python invocation failed: ${result.stderr.trim() || "no output"}`,
95
+ });
96
+ return 1;
97
+ }
98
+ process.stdout.write(result.stdout);
99
+ return result.code === 0 ? 0 : result.code;
100
+ }
@@ -45,6 +45,9 @@ const BIN_ENTRYPOINTS = [
45
45
  // depend on. Intentionally an exact allowlist, not a directory wildcard.
46
46
  const REQUIRED_PROMPT_RESOURCE_FILES = Object.freeze([
47
47
  ["prompts", "lead", "okstra-lead-contract.md"],
48
+ ["prompts", "lead", "adapters", "claude-code.md"],
49
+ ["prompts", "lead", "adapters", "codex.md"],
50
+ ["prompts", "lead", "adapters", "external.md"],
48
51
  ["prompts", "lead", "context-loader.md"],
49
52
  ["prompts", "lead", "team-contract.md"],
50
53
  ["prompts", "lead", "convergence.md"],
@@ -6,30 +6,15 @@ import {
6
6
  RUNTIMES_MANIFEST_REL,
7
7
  runtimeManifestIncludesClaudeAssets,
8
8
  } from "../../lib/runtime-manifest.mjs";
9
+ import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
9
10
 
10
- const FALLBACK_SKILL_NAMES = [
11
- "okstra",
12
- "okstra-setup",
13
- "okstra-brief-gen",
14
- "okstra-run",
15
- "okstra-memory",
16
- "okstra-inspect",
17
- "okstra-schedule",
18
- "okstra-container-build",
19
- "okstra-container",
20
- "okstra-brief",
21
- "okstra-context-loader",
22
- "okstra-team-contract",
23
- "okstra-convergence",
24
- "okstra-report-writer",
25
- // Pre-merge names — kept so uninstall on an upgraded machine still cleans
26
- // them out of ~/.claude/skills/ once the user runs `okstra uninstall`.
27
- // Safe to drop after a few releases when nobody has them installed.
28
- "okstra-status",
29
- "okstra-history",
30
- "okstra-logs",
31
- "okstra-report-finder",
32
- "okstra-time-summary",
11
+ // Manifest-less uninstall must clean out every okstra skill directory — the
12
+ // live names AND every former/renamed one. Both sets are the single source of
13
+ // truth in skill-catalog.mjs (install prunes OBSOLETE_SKILL_NAMES from each
14
+ // skill home), so deriving the fallback from their union keeps uninstall from
15
+ // drifting behind skill additions or renames.
16
+ export const FALLBACK_SKILL_NAMES = [
17
+ ...new Set([...USER_SKILL_NAMES, ...OBSOLETE_SKILL_NAMES]),
33
18
  ];
34
19
 
35
20
  const FALLBACK_AGENT_NAMES = [
@@ -13,7 +13,7 @@ export const USER_SKILL_NAMES = Object.freeze([
13
13
  "okstra-inspect",
14
14
  "okstra-rollup",
15
15
  "okstra-usage",
16
- "okstra-schedule",
16
+ "okstra-schedule-gen",
17
17
  "okstra-container-build",
18
18
  "okstra-graphify",
19
19
  "okstra-pr-gen",
@@ -33,6 +33,15 @@ export const OBSOLETE_SKILL_NAMES = Object.freeze([
33
33
  "okstra-coding-preflight",
34
34
  "okstra-container",
35
35
  "okstra-brief",
36
+ "okstra-schedule",
37
+ // Pre-merge status/history/logs/report/time skills, since folded into
38
+ // okstra-inspect. Absent here, upgrades left them stranded in agent skill
39
+ // homes and their pre-.okstra-migration bodies kept triggering.
40
+ "okstra-status",
41
+ "okstra-history",
42
+ "okstra-logs",
43
+ "okstra-report-finder",
44
+ "okstra-time-summary",
36
45
  ]);
37
46
 
38
47
  export function userSkillNames() {