okstra 0.96.1 → 0.97.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 (121) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/okstra +1 -1
  4. package/docs/contributor-change-matrix.md +1 -1
  5. package/docs/kr/architecture/storage-model.md +273 -0
  6. package/docs/kr/architecture.md +6 -277
  7. package/docs/kr/cli.md +1 -1
  8. package/docs/project-structure-overview.md +26 -22
  9. package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +1019 -0
  10. package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +583 -0
  11. package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +819 -0
  12. package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +348 -0
  13. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +2 -2
  14. package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +171 -0
  15. package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +112 -0
  16. package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +145 -0
  17. package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +113 -0
  18. package/docs/task-process/release-handoff.md +3 -3
  19. package/package.json +1 -1
  20. package/runtime/BUILD.json +2 -2
  21. package/runtime/bin/lib/okstra/cli.sh +1 -1
  22. package/runtime/bin/lib/okstra/interactive.sh +22 -4
  23. package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +18 -5
  24. package/runtime/bin/okstra-render-final-report.py +4 -15
  25. package/runtime/bin/okstra.sh +5 -2
  26. package/runtime/prompts/launch.template.md +1 -0
  27. package/runtime/prompts/lead/report-writer.md +1 -1
  28. package/runtime/prompts/lead/team-contract.md +1 -1
  29. package/runtime/prompts/profiles/final-verification.md +1 -0
  30. package/runtime/prompts/profiles/forbidden-actions.json +1 -1
  31. package/runtime/prompts/profiles/release-handoff.md +10 -4
  32. package/runtime/python/okstra_ctl/__init__.py +3 -2
  33. package/runtime/python/okstra_ctl/backfill.py +12 -21
  34. package/runtime/python/okstra_ctl/codex_dispatch.py +7 -2
  35. package/runtime/python/okstra_ctl/conformance.py +21 -3
  36. package/runtime/python/okstra_ctl/consumers.py +7 -1
  37. package/runtime/python/okstra_ctl/dispatch_core.py +10 -2
  38. package/runtime/python/okstra_ctl/doctor.py +29 -12
  39. package/runtime/python/okstra_ctl/error_log_core.py +72 -0
  40. package/runtime/python/okstra_ctl/error_report.py +8 -58
  41. package/runtime/python/okstra_ctl/error_zip.py +319 -0
  42. package/runtime/python/okstra_ctl/fanout.py +5 -2
  43. package/runtime/python/okstra_ctl/fix_cycles.py +14 -2
  44. package/runtime/python/okstra_ctl/git_reconcile.py +7 -1
  45. package/runtime/python/okstra_ctl/handoff.py +125 -26
  46. package/runtime/python/okstra_ctl/implementation_stage.py +9 -2
  47. package/runtime/python/okstra_ctl/improvement_lenses.py +2 -0
  48. package/runtime/python/okstra_ctl/index.py +34 -91
  49. package/runtime/python/okstra_ctl/jsonl.py +29 -18
  50. package/runtime/python/okstra_ctl/listing.py +30 -5
  51. package/runtime/python/okstra_ctl/md_table.py +1 -1
  52. package/runtime/python/okstra_ctl/migrate.py +17 -7
  53. package/runtime/python/okstra_ctl/paths.py +1 -1
  54. package/runtime/python/okstra_ctl/recap.py +12 -3
  55. package/runtime/python/okstra_ctl/reconcile.py +13 -11
  56. package/runtime/python/okstra_ctl/render.py +49 -12
  57. package/runtime/python/okstra_ctl/render_final_report.py +4 -0
  58. package/runtime/python/okstra_ctl/report_views.py +47 -6
  59. package/runtime/python/okstra_ctl/run.py +128 -100
  60. package/runtime/python/okstra_ctl/run_index_row.py +118 -0
  61. package/runtime/python/okstra_ctl/schema_excerpt.py +8 -2
  62. package/runtime/python/okstra_ctl/sequence.py +2 -2
  63. package/runtime/python/okstra_ctl/stage_integrate.py +193 -0
  64. package/runtime/python/okstra_ctl/stage_targets.py +2 -8
  65. package/runtime/python/okstra_ctl/task_target.py +4 -1
  66. package/runtime/python/okstra_ctl/team_reconcile.py +6 -1
  67. package/runtime/python/okstra_ctl/wizard.py +27 -12
  68. package/runtime/python/okstra_ctl/workflow.py +9 -1
  69. package/runtime/python/okstra_ctl/worktree.py +93 -4
  70. package/runtime/python/okstra_ctl/worktree_registry.py +57 -8
  71. package/runtime/python/okstra_token_usage/collect.py +43 -38
  72. package/runtime/skills/okstra-brief/SKILL.md +43 -12
  73. package/runtime/skills/okstra-inspect/SKILL.md +44 -1
  74. package/runtime/templates/reports/final-report.template.md +2 -2
  75. package/runtime/templates/reports/release-handoff-input.template.md +1 -1
  76. package/runtime/validators/forbidden_actions.py +8 -1
  77. package/runtime/validators/validate-brief.py +13 -2
  78. package/runtime/validators/validate-run.py +27 -6
  79. package/runtime/validators/validate-schedule.py +1 -2
  80. package/runtime/validators/validate_improvement_report.py +14 -0
  81. package/src/cli-registry.mjs +45 -31
  82. package/src/{codex-dispatch.mjs → commands/execute/codex-dispatch.mjs} +3 -3
  83. package/src/{codex-run.mjs → commands/execute/codex-run.mjs} +3 -3
  84. package/src/{error-log.mjs → commands/execute/error-log.mjs} +1 -1
  85. package/src/{git-reconcile.mjs → commands/execute/git-reconcile.mjs} +1 -1
  86. package/src/{handoff.mjs → commands/execute/handoff.mjs} +3 -1
  87. package/src/commands/execute/integrate-stages.mjs +25 -0
  88. package/src/{plan-validate.mjs → commands/execute/plan-validate.mjs} +1 -1
  89. package/src/{render-bundle.mjs → commands/execute/render-bundle.mjs} +4 -4
  90. package/src/{run.mjs → commands/execute/run.mjs} +4 -4
  91. package/src/{spawn-followups.mjs → commands/execute/spawn-followups.mjs} +1 -1
  92. package/src/{team.mjs → commands/execute/team.mjs} +3 -3
  93. package/src/{token-usage.mjs → commands/execute/token-usage.mjs} +1 -1
  94. package/src/{wizard.mjs → commands/execute/wizard.mjs} +15 -6
  95. package/src/{worktree-lookup.mjs → commands/execute/worktree-lookup.mjs} +1 -1
  96. package/src/{context-cost.mjs → commands/inspect/context-cost.mjs} +2 -2
  97. package/src/{error-report.mjs → commands/inspect/error-report.mjs} +2 -2
  98. package/src/commands/inspect/error-zip.mjs +25 -0
  99. package/src/{recap.mjs → commands/inspect/recap.mjs} +2 -2
  100. package/src/{task-list.mjs → commands/inspect/task-list.mjs} +1 -1
  101. package/src/{task-show.mjs → commands/inspect/task-show.mjs} +1 -1
  102. package/src/{check-project.mjs → commands/lifecycle/check-project.mjs} +2 -2
  103. package/src/{config.mjs → commands/lifecycle/config.mjs} +3 -3
  104. package/src/{doctor.mjs → commands/lifecycle/doctor.mjs} +4 -4
  105. package/src/{install.mjs → commands/lifecycle/install.mjs} +18 -13
  106. package/src/{migrate.mjs → commands/lifecycle/migrate.mjs} +1 -1
  107. package/src/{paths.mjs → commands/lifecycle/paths.mjs} +1 -60
  108. package/src/{setup.mjs → commands/lifecycle/setup.mjs} +4 -4
  109. package/src/{uninstall.mjs → commands/lifecycle/uninstall.mjs} +20 -32
  110. package/src/{memory.mjs → commands/memory/memory.mjs} +32 -7
  111. package/src/{inject-report-index.mjs → commands/report/inject-report-index.mjs} +1 -1
  112. package/src/{render-final-report.mjs → commands/report/render-final-report.mjs} +1 -1
  113. package/src/{render-views.mjs → commands/report/render-views.mjs} +1 -1
  114. package/src/lib/paths.mjs +60 -0
  115. package/src/{_python-helper.mjs → lib/python-helper.mjs} +35 -13
  116. package/src/{version.mjs → lib/version.mjs} +2 -2
  117. /package/src/{okstra-dirs.mjs → lib/okstra-dirs.mjs} +0 -0
  118. /package/src/{_proc.mjs → lib/proc.mjs} +0 -0
  119. /package/src/{runtime-manifest.mjs → lib/runtime-manifest.mjs} +0 -0
  120. /package/src/{runtime-resolver.mjs → lib/runtime-resolver.mjs} +0 -0
  121. /package/src/{skill-catalog.mjs → lib/skill-catalog.mjs} +0 -0
@@ -13,10 +13,9 @@ from .claude import (
13
13
  find_claude_agent_sessions,
14
14
  find_claude_team_sessions,
15
15
  )
16
- from .codex import codex_session_total, find_codex_session, find_codex_sessions
16
+ from .codex import codex_session_total, find_codex_sessions
17
17
  from .antigravity import (
18
18
  antigravity_session_total,
19
- find_antigravity_session,
20
19
  find_antigravity_sessions,
21
20
  )
22
21
  from .paths import claude_project_dir, utc_now
@@ -342,6 +341,39 @@ def _cli_usage_block(provider: str, totals: dict, session_paths: list[Path]) ->
342
341
  return block
343
342
 
344
343
 
344
+ def _attach_cli_usage(
345
+ block: dict,
346
+ provider: str,
347
+ project_root: Path,
348
+ wrapper_start: str,
349
+ wrapper_end: str,
350
+ ) -> None:
351
+ """Layer aggregated CLI token/cost onto a Claude-side worker ``block``.
352
+
353
+ A codex/antigravity worker re-dispatched within one aggregated wrapper
354
+ window produces several rollout jsonls; pricing only the latest (the old
355
+ ``find_codex_session`` -> ``sessions[-1]`` behavior) dropped the earlier
356
+ attempts' tokens. We sum every in-window session here so the redispatched
357
+ CLI spend is fully reported. (agy print mode leaves no transcript, so the
358
+ antigravity path still resolves to "session not found" → na cost.)
359
+ """
360
+ session_paths = _cli_sessions_for_windows(
361
+ provider, project_root, [(wrapper_start, wrapper_end)]
362
+ )
363
+ if not session_paths:
364
+ block["cliNote"] = f"{provider} CLI session not found in wrapper window (fallback or never invoked)"
365
+ return
366
+ block["cliSessionPaths"] = [str(path) for path in session_paths]
367
+ totals = _cli_session_totals(provider, session_paths)
368
+ if not totals:
369
+ block["cliNote"] = f"{provider} CLI session found but no usage recorded (likely errored before completion)"
370
+ return
371
+ cli = _cli_usage_block(provider, _aggregate_totals(totals), session_paths)
372
+ for key in ("cliTotalTokens", "cliEstimatedCostUsd", "cliModel"):
373
+ if key in cli:
374
+ block[key] = cli[key]
375
+
376
+
345
377
  def _collect_codex_runtime_usage(state: dict, project_root: Path) -> dict:
346
378
  windows_by_worker = _codex_worker_windows(project_root, state)
347
379
  state["leadUsage"] = na_block(
@@ -551,43 +583,16 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
551
583
  block["additionalSessionIds"] = [sid for sid, _, _ in matched[1:]]
552
584
  block["matchedAgentNames"] = sorted({t.get("agentName") for _, _, t in matched if t.get("agentName")})
553
585
 
554
- # For codex/antigravity workers, find every CLI session that fell inside
555
- # the aggregated wrapper window. (agy print mode leaves no transcript, so
556
- # the antigravity path always resolves to "session not found" → na cost.)
557
- wrapper_start = aggregate.get("startedAt") or ""
558
- wrapper_end = aggregate.get("endedAt") or ""
586
+ # For codex/antigravity workers, sum every CLI session that fell inside
587
+ # the aggregated wrapper window (re-dispatches leave one rollout each).
559
588
  if agent in ("codex", "antigravity"):
560
- if agent == "codex":
561
- cli = find_codex_session(cwd, wrapper_start, wrapper_end)
562
- cli_totals = codex_session_total(cli) if cli else None
563
- else:
564
- cli = find_antigravity_session(cwd, wrapper_start, wrapper_end)
565
- cli_totals = antigravity_session_total(cli) if cli else None
566
- if cli is None:
567
- block["cliNote"] = f"{agent} CLI session not found in wrapper window (fallback or never invoked)"
568
- else:
569
- block["cliSessionPath"] = str(cli)
570
- if cli_totals.get("model"):
571
- block["cliModel"] = cli_totals["model"]
572
- if cli_totals.get("available"):
573
- block["cliTotalTokens"] = cli_totals["totalTokens"]
574
- if agent == "codex":
575
- cost = codex_cost_usd(
576
- cli_totals.get("model"),
577
- cli_totals.get("inputTokens", 0) or 0,
578
- cli_totals.get("cachedInputTokens", 0) or 0,
579
- cli_totals.get("outputTokens", 0) or 0,
580
- )
581
- else:
582
- cost = antigravity_cost_usd(
583
- cli_totals.get("model"),
584
- cli_totals.get("inputTokens", 0) or 0,
585
- cli_totals.get("outputTokens", 0) or 0,
586
- )
587
- if cost is not None:
588
- block["cliEstimatedCostUsd"] = cost
589
- else:
590
- block["cliNote"] = f"{agent} CLI session found but no usage recorded (likely errored before completion)"
589
+ _attach_cli_usage(
590
+ block,
591
+ agent,
592
+ cwd,
593
+ aggregate.get("startedAt") or "",
594
+ aggregate.get("endedAt") or "",
595
+ )
591
596
  worker["usage"] = block
592
597
 
593
598
  # Fold team-tagged worker sessions that carry no agentName into the worker
@@ -54,6 +54,13 @@ okstra-brief (codebase-scan variant)
54
54
  → okstra-run (implementation)
55
55
  → okstra-run (final-verification)
56
56
  → okstra-run (release-handoff)
57
+
58
+ okstra-brief (error-feedback variant)
59
+ → okstra-run (error-analysis)
60
+ → okstra-run (implementation-planning)
61
+ → okstra-run (implementation)
62
+ → okstra-run (final-verification)
63
+ → okstra-run (release-handoff)
57
64
  ```
58
65
 
59
66
  ## When to use
@@ -135,6 +142,7 @@ Parse the JSON from stdout:
135
142
  - **Options**:
136
143
  1. `Reporter input` — reporter 발화 / 티켓 / 링크 / 자유 텍스트를 verbatim 으로 흡수. 기존 4-source 흐름.
137
144
  2. `Codebase scan` — 코드베이스 범위 + lens 우선순위만 받아 `improvement-discovery` phase 의 입력을 생성.
145
+ 3. `Error feedback` (`error-feedback`) — `okstra error-zip` 이 만든 `.zip` 의 빈발 클러스터 1개를 `error-analysis` brief 로 변환 (okstra 자가수정용).
138
146
 
139
147
  ### 1.A. Reporter input (variant 1)
140
148
 
@@ -342,6 +350,29 @@ Validation (before Step 4 sharpening):
342
350
 
343
351
  Once validation passes, jump to Step 2 (task key).
344
352
 
353
+ ### 1.C. Error feedback (`error-feedback`, variant 3) — okstra 자가수정용
354
+
355
+ If the user picked `Error feedback`, the input is a `.zip` produced by `okstra error-zip --out <path>`. 이 variant 는 빈발 에러 클러스터 1개를 골라 `error-analysis` brief 로 변환하며, **수정 대상은 okstra 레포 자신**이다.
356
+
357
+ 입력: `okstra error-zip` 이 만든 `.zip`. 사용자에게 zip 경로를 묻는다(이전에 받은 경로가 있으면 `Recommended:` 로 재사용 제안).
358
+
359
+ 1. zip 의 `report.md` 를 읽어 빈발 클러스터 표를 사용자에게 보여주고, **클러스터 1개**를 고르게 한다.
360
+ `AskUserQuestion` (single-select, `직접 지정` always last):
361
+ 1. top-1 클러스터 (빈도 최다) — `Recommended:` 표시.
362
+ 2. top-2 클러스터.
363
+ 3. `직접 지정` — 사용자가 표에서 직접 클러스터를 지정.
364
+ 2. 고른 클러스터의 익명화 레코드를 brief 의 `## Source Material` 에 인용한다 — zip 내부 `errors/anonymized.jsonl` 에서 **선택한 클러스터 번호(`report.md` 표의 `#`)와 일치하는 `clusterId` 레코드**만 추려, errorType · command · exitCode · phase · agent · 대표 메시지를 표/리스트로 옮긴다. (`#` ↔ `clusterId` 는 1:1 대응이며, 같은 errorType+phase+agent 라도 정규화 메시지가 다르면 별개 클러스터다 — 병합하지 않는다.)
365
+ 3. frontmatter `scope` 는 reporter-input 으로 둔다 — 이 variant 는 익명화 에러 레코드를 `## Source Material` 에 verbatim 으로 담는 reporter-input 계열이므로 `scope` 키를 생략한다(⇒ `reporter-input`). codebase-scan 전용 키(`priority-lenses`·`scan-scope` 등)는 쓰지 않는다. 다음 phase 신호는 Step 6 의 `Recommended next phase: error-analysis` 헤더 줄로 보낸다(별도 frontmatter 키를 만들지 않는다). 본문에는 수정 대상을 명시한다: "이 에러는 okstra 런타임/프롬프트/래퍼/권한 seed 결함 신호이며, 수정 대상은 okstra 소스(`prompts/`·`agents/`·`scripts/`·`templates/`)다."
366
+ 4. **단일 클러스터 = 단일 실패조건** 원칙을 지킨다 — 이질적 errorType 을 한 brief 에 섞지 않는다(`error-analysis` 의 symptom-lock 요건). 다른 클러스터도 고치고 싶으면 클러스터마다 별도 brief 를 만든다.
367
+ 5. 생성된 brief 끝에 다음 단계를 인라인으로 적는다: "okstra 레포에서 `okstra-run --task-type error-analysis` 로 이 brief 를 실행하세요."
368
+
369
+ Validation (before Step 4 sharpening):
370
+
371
+ - zip 경로가 실제 존재하고 `report.md` + `errors/anonymized.jsonl` 을 포함하는지 확인 — 없으면 한 질문으로 정정.
372
+ - 고른 클러스터가 단일 errorType 인지 확인 — 여러 errorType 이 섞이면 한 질문으로 1개만 남긴다.
373
+
374
+ Once validation passes, jump to Step 2 (task key).
375
+
345
376
  ## Step 2: Identify task key & filename
346
377
 
347
378
  ### 2a. Task group
@@ -717,21 +748,21 @@ Material).
717
748
 
718
749
  The installed template `~/.okstra/templates/reports/brief.template.md` is the byte-for-byte SSOT for the brief shape — frontmatter keys, top-header blockquote, section ordering, and inline `<!-- author guidance -->` HTML comments. Render that file with the per-brief values substituted; leave any section's body as `_(none)_` rather than fabricating content. Preserve the template's HTML comments verbatim (they are part of the contract) but do NOT promote them to body prose.
719
750
 
720
- **Variant note:** The template file is the canonical reporter-input shape. For the codebase-scan variant (`scope: codebase` in frontmatter), OMIT the `## Source Material` and `## Problem / Symptom` headings entirely — do NOT include them with `_(none)_` bodies. Instead, KEEP the `## Scan Scope` and `## Priority Lenses` sections (already present in the template file, marked with HTML comments). The remaining sections (Context / Desired Outcome / Constraints / Related Artifacts / Open Questions / Reporter Confirmations / Augmentation) apply to both variants.
751
+ **Variant note:** The template file is the canonical reporter-input shape. For the codebase-scan variant (`scope: codebase` in frontmatter), OMIT the `## Source Material` and `## Problem / Symptom` headings entirely — do NOT include them with `_(none)_` bodies. Instead, KEEP the `## Scan Scope` and `## Priority Lenses` sections (already present in the template file, marked with HTML comments). For the error-feedback variant (`scope` omitted ⇒ `reporter-input`), use the reporter-input shape: KEEP `## Source Material` (the chosen cluster's anonymized error records) and `## Problem / Symptom`, and OMIT the codebase-only `## Scan Scope` / `## Priority Lenses` sections. The remaining sections (Context / Desired Outcome / Constraints / Related Artifacts / Open Questions / Reporter Confirmations / Augmentation) apply to all variants.
721
752
 
722
753
  ### Required sections by variant
723
754
 
724
- | Section | `reporter-input` variant | `codebase` variant |
725
- |---|---|---|
726
- | `## Source Material` | Required | Not required — omit |
727
- | `## Problem / Symptom` | Required | Not required — omit |
728
- | `## Context` | Required | Required |
729
- | `## Desired Outcome` | Required | Required |
730
- | `## Constraints` | Required | Required |
731
- | `## Related Artifacts` | Required | Required |
732
- | `## Open Questions` | Required | Required |
733
- | `## Scan Scope` | Not applicable — omit | Required — one bullet per `scan-scope` path with a short description of what lives there |
734
- | `## Priority Lenses` | Not applicable — omit | Required — one bullet per priority lens with a short rationale for why that lens applies to this scope |
755
+ | Section | `reporter-input` variant | `codebase` variant | `error-feedback` variant |
756
+ |---|---|---|---|
757
+ | `## Source Material` | Required | Not required — omit | Required — anonymized error records of the chosen cluster |
758
+ | `## Problem / Symptom` | Required | Not required — omit | Required |
759
+ | `## Context` | Required | Required | Required |
760
+ | `## Desired Outcome` | Required | Required | Required |
761
+ | `## Constraints` | Required | Required | Required |
762
+ | `## Related Artifacts` | Required | Required | Required |
763
+ | `## Open Questions` | Required | Required | Required |
764
+ | `## Scan Scope` | Not applicable — omit | Required — one bullet per `scan-scope` path with a short description of what lives there | Not applicable — omit |
765
+ | `## Priority Lenses` | Not applicable — omit | Required — one bullet per priority lens with a short rationale for why that lens applies to this scope | Not applicable — omit |
735
766
 
736
767
  ### Frontmatter rules
737
768
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: okstra-inspect
3
3
  description: |
4
- Use for any read-side okstra inspection or status mutation. Single skill dispatches by sub-command to eight facets — status, history, report, time, logs, cost, errors, recap. Trigger words include "okstra status", "task status", "current phase", "next phase", "okstra status set", "okstra mark", "<task-id> done|in-progress|진행중|완료", "okstra history", "past runs", "re-run", "resume", "list tasks", "find report", "show report for", "read the okstra report", "continue from report", "작업 시간", "소요 시간", "time summary", "duration", "elapsed", "얼마나 걸렸", "시간 분석", "okstra logs", "로그 현황", "로그 파일", "log size", "log status", "로그 정리", "log cleanup", "okstra context-cost", "context cost", "context-cost", "컨텍스트 비용", "읽기 비용", "산출물 비용", "okstra errors", "error report", "에러 리포트", "에러 보고", "에러 모아줘", "okstra recap", "recap", "작업 요약", "이 task 요약", "전후 요약", "이 작업 설명해줘", "task 질문", "실패 로그 정리".
4
+ Use for any read-side okstra inspection or status mutation. Single skill dispatches by sub-command to nine facets — status, history, report, time, logs, cost, errors, error-zip, recap. Trigger words include "okstra status", "task status", "current phase", "next phase", "okstra status set", "okstra mark", "<task-id> done|in-progress|진행중|완료", "okstra history", "past runs", "re-run", "resume", "list tasks", "find report", "show report for", "read the okstra report", "continue from report", "작업 시간", "소요 시간", "time summary", "duration", "elapsed", "얼마나 걸렸", "시간 분석", "okstra logs", "로그 현황", "로그 파일", "log size", "log status", "로그 정리", "log cleanup", "okstra context-cost", "context cost", "context-cost", "컨텍스트 비용", "읽기 비용", "산출물 비용", "okstra errors", "error report", "에러 리포트", "에러 보고", "에러 모아줘", "okstra error-zip", "에러 zip", "에러 환류", "에러 번들", "cross-project 에러", "okstra recap", "recap", "작업 요약", "이 task 요약", "전후 요약", "이 작업 설명해줘", "task 질문", "실패 로그 정리".
5
5
  ---
6
6
 
7
7
  # OKSTRA Inspect
@@ -17,6 +17,7 @@ Single read-side entry point for okstra runtime inspection plus the one status m
17
17
  | `logs` | Inventory codex/antigravity wrapper `.log` sidecars; emit cleanup commands. |
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
+ | `error-zip` | Collect cross-project okstra error logs into an anonymized zip (report + raw) and summarize clusters. |
20
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
22
 
22
23
  ## Step 0: Verify okstra runtime + project setup (shared)
@@ -38,6 +39,8 @@ Subsequent `okstra <subcmd>` calls self-bootstrap their Python path, so this ski
38
39
 
39
40
  Classify the user's request into one sub-command using the trigger table above. If ambiguous (e.g. "okstra 보여줘"), ask which facet — do not silently default. After routing, the chosen sub-command's section below governs the rest of the response.
40
41
 
42
+ When you ask which facet, the option list is **authoritative**: enumerate every sub-command in the `Sub-command | What it does` table above as a numbered text list, in table order, each with its one-line description, and let the user pick by number or name. Never present a hand-picked subset — the AskUserQuestion 4-option cap is not a reason to drop facets; render the full menu as text in the question body so no facet (e.g. a newly added one) is ever hidden. The table is the single source of truth for this menu; adding a row there is all it takes for the facet to appear here.
43
+
41
44
  When the user chains multiple facets in one message (e.g. "status, and then time for DEV-9047"), execute them sequentially — Step 0 runs once, each sub-command section runs once.
42
45
 
43
46
  ---
@@ -759,6 +762,46 @@ Parse the stdout JSON and report:
759
762
 
760
763
  ---
761
764
 
765
+ ## error-zip
766
+
767
+ Trigger phrases: "okstra error-zip", "에러 zip", "에러 환류", "에러 번들", "cross-project 에러".
768
+
769
+ 머신 내 모든 타겟 프로젝트의 okstra 실행 에러(`runs/*/logs/errors-*.jsonl`)를 글로벌 run-index 로 수집·익명화해 `.zip`(집계 리포트 + 익명화 JSONL)으로 묶는다. 모든 타겟의 `.okstra/` 에 대해 read-only.
770
+
771
+ ### error-zip.1 — 출력 경로 결정 (picker)
772
+
773
+ 이전 출력 경로를 `~/.okstra/error-zip.json` 의 `lastOutputPath` 에서 읽는다. 그 값이 있으면 3-옵션 picker 로 제시:
774
+
775
+ 1. (이전 경로 있음) `<lastOutputPath>` 재사용 — 추천, 첫 옵션.
776
+ (이전 경로 없음) 기본 경로 `~/okstra-error-feedback-<YYYY-MM-DD>.zip` 제안 — 추천.
777
+ 2. 직접 입력 (항상 마지막 옵션).
778
+
779
+ `~/.okstra/error-zip.json` 읽기는 Read 로 직접 수행한다(없으면 이전 경로 없음으로 간주).
780
+
781
+ ### error-zip.2 — 렌더 실행
782
+
783
+ ```bash
784
+ okstra error-zip --out <결정된 경로>
785
+ ```
786
+
787
+ ### error-zip.3 — 요약 보고
788
+
789
+ stdout JSON 을 파싱해 보고:
790
+
791
+ | 필드 | 출처 |
792
+ |---|---|
793
+ | 출력 zip | `outPath` |
794
+ | 총 에러 | `errorCount` |
795
+ | 로그(run) | `runCount` |
796
+ | 도달 불가 run | `unreachableRuns` |
797
+ | 클러스터 수 | `clusterCount` |
798
+ | 프로젝트 수 | `projectCount` |
799
+
800
+ - `unreachableRuns > 0` 이면 표면화한다(침묵 누락 금지).
801
+ - 끝에 다음 단계를 안내한다: "이 zip 으로 okstra 자신을 고치려면 `/okstra-brief` 의 error-feedback variant 로 brief 를 만든 뒤 `okstra-run --task-type error-analysis` 를 okstra 레포에서 실행하세요."
802
+
803
+ ---
804
+
762
805
  ## recap
763
806
 
764
807
  Trigger phrases: "okstra recap", "recap", "작업 요약", "이 task 요약", "전후 요약", "이 작업 설명해줘", "task 질문".
@@ -340,7 +340,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
340
340
 
341
341
  | {{ t("releaseHandoff.questionsTableHeader.questionId") }} | {{ t("releaseHandoff.questionsTableHeader.questionBody") }} | {{ t("releaseHandoff.questionsTableHeader.userResponse") }} | {{ t("releaseHandoff.questionsTableHeader.allowedOptions") }} |
342
342
  |---------|-----------|--------------------|--------------------|
343
- | H1 | {{ t("releaseHandoff.h1Body") }} | `{{ releaseHandoff.userSelections.h1 | mdcell }}` | `local only` / `push + PR` / `skip` |
343
+ | H1 | {{ t("releaseHandoff.h1Body") }} | `{{ releaseHandoff.userSelections.h1 | mdcell }}` | `local checkout` / `push + PR` / `skip` |
344
344
  | H2 | {{ t("releaseHandoff.h2Body") }} | `{{ (releaseHandoff.userSelections.h2 or t("releaseHandoff.h2DefaultLabel")) | mdcell }}` | {{ t("releaseHandoff.h2OptionsLabel") }} |
345
345
  | H2b | Merge conflict probe | `{{ (releaseHandoff.userSelections.h2b or releaseHandoff.mergeConflictProbe.kind) | mdcell }}` | `not-run` / `clean` / `proceed anyway` / `change base branch` / `cancel` |
346
346
  | H3 | {{ t("releaseHandoff.h3Body") }} | `{{ releaseHandoff.userSelections.h3 | mdcell }}` | `use as-is` / `edit then proceed` / `cancel` |
@@ -372,7 +372,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
372
372
  ### 5.6.6 Merge Conflict Probe
373
373
 
374
374
  {% if releaseHandoff.mergeConflictProbe.kind == 'not-run' -%}
375
- - Not run (user picked local only or skip).
375
+ - Not run (user picked local checkout or skip).
376
376
  {%- elif releaseHandoff.mergeConflictProbe.kind == 'clean' -%}
377
377
  - Clean — no conflicts against {{ releaseHandoff.mergeConflictProbe.baseBranch }} at {{ releaseHandoff.mergeConflictProbe.baseSha }}.
378
378
  {%- else -%}
@@ -63,7 +63,7 @@ taskType: "{{FM_TASK_TYPE}}"
63
63
 
64
64
  ## User-Selection Defaults (advisory only — the user still chooses interactively)
65
65
 
66
- - Suggested action (Q1): `local only` | `push + PR` | `skip`
66
+ - Suggested action (Q1): `local checkout` | `push + PR` | `skip`
67
67
  - Suggested base (Q2): one of the candidate base branches above
68
68
  - Suggested message handling (Q3): `use as-is` | `edit then proceed`
69
69
 
@@ -36,7 +36,14 @@ _UNIVERSAL = [
36
36
  ("docker push", re.compile(r"\bdocker\s+push\b")),
37
37
  ("terraform apply", re.compile(r"\bterraform\s+apply\b")),
38
38
  ("kubectl apply", re.compile(r"\bkubectl\s+apply\b")),
39
- ("git push --force", re.compile(r"\bgit\s+push\b.*(?:--force(-with-lease)?|\s-[A-Za-z]*f)")),
39
+ # 강제 push 만 잡는다: force 플래그가 같은 명령 절에 있어야 하므로 git push
40
+ # 플래그 사이의 gap 이 shell 구분자(`&&`/`||`/`;`/`|`/개행)를 넘지 못하게 막아
41
+ # `git push origin && rm -rf …` 의 `-rf` 같은 후속 명령 토큰을 오탐하지 않는다.
42
+ # 단문자 플래그 클러스터는 `f` 가 어디에 있든(`-qf`/`-fq`) 매칭한다.
43
+ (
44
+ "git push --force",
45
+ re.compile(r"\bgit\s+push\b[^\n;&|]*(?:--force(-with-lease)?\b|\s-[A-Za-z]*f[A-Za-z]*\b)"),
46
+ ),
40
47
  ]
41
48
  _NON_HANDOFF_PUSH = ("git push", re.compile(r"\bgit\s+push\b"))
42
49
 
@@ -90,6 +90,18 @@ REPORTER_CONFIRMATION_VALUES = {"complete", "partial", "pending", "skipped"}
90
90
  SCOPE_VALUES = {"reporter-input", "codebase"}
91
91
 
92
92
 
93
+ def strip_yaml_comment(line: str) -> str:
94
+ """Drop a YAML inline comment, keeping `#` that is part of a value.
95
+
96
+ Per YAML, `#` starts a comment only at line start or after whitespace;
97
+ a `#` glued to a non-space char (e.g. `ticket-id: PROJ-42#thread`) is data.
98
+ """
99
+ for i, ch in enumerate(line):
100
+ if ch == "#" and (i == 0 or line[i - 1].isspace()):
101
+ return line[:i]
102
+ return line
103
+
104
+
93
105
  def parse_frontmatter(text: str) -> tuple[dict[str, str], int]:
94
106
  """Return (frontmatter dict, line after closing `---`)."""
95
107
  lines = text.splitlines()
@@ -100,8 +112,7 @@ def parse_frontmatter(text: str) -> tuple[dict[str, str], int]:
100
112
  line = lines[idx]
101
113
  if line.strip() == "---":
102
114
  return out, idx + 1
103
- # naive key: value (comments after #)
104
- bare = line.split("#", 1)[0].strip()
115
+ bare = strip_yaml_comment(line).strip()
105
116
  if not bare:
106
117
  continue
107
118
  if ":" not in bare:
@@ -1225,6 +1225,16 @@ _GATE_RESULT_RE = re.compile(
1225
1225
  re.IGNORECASE,
1226
1226
  )
1227
1227
 
1228
+ # §5.5.9 Plan Body Verification section body — scopes the gate-result search
1229
+ # so an earlier prose mention of "Gate result" elsewhere in the report cannot
1230
+ # hijack the authoritative value. Matched from the `Plan Body Verification`
1231
+ # heading to the next heading (or end-of-file).
1232
+ _PLAN_BODY_VERIFICATION_BLOCK_RE = re.compile(
1233
+ r"^#{2,}[ \t][^\n]*\bPlan Body Verification\b[^\n]*\n"
1234
+ r"(?P<body>.*?)(?=^#{2,}[ \t]|\Z)",
1235
+ re.DOTALL | re.MULTILINE,
1236
+ )
1237
+
1228
1238
  # Frontmatter approval flag — `approved: true|false` line inside the
1229
1239
  # leading `---` YAML block. Mirrors `APPROVED_FRONTMATTER_PATTERN` in
1230
1240
  # scripts/okstra_ctl/run.py.
@@ -1232,7 +1242,10 @@ _APPROVED_FRONTMATTER_RE = re.compile(
1232
1242
  r"^approved:[ \t]+(true|false)[ \t]*$",
1233
1243
  re.IGNORECASE | re.MULTILINE,
1234
1244
  )
1235
- _FRONTMATTER_BLOCK_RE = re.compile(r"\A---\n(.*?)\n---\n", re.DOTALL)
1245
+ # Tolerate a leading UTF-8 BOM and/or blank lines before the opening `---`
1246
+ # so a hand-edited or differently-rendered report does not silently bypass
1247
+ # the approved-frontmatter gate (the `.match` would otherwise return None).
1248
+ _FRONTMATTER_BLOCK_RE = re.compile(r"\A\ufeff?\s*---\n(.*?)\n---\n", re.DOTALL)
1236
1249
  _REQUIREMENT_COVERAGE_HEADING_RE = re.compile(
1237
1250
  r"^###[ \t]+(?:5\.5\.8[ \t]+)?Requirement Coverage\b",
1238
1251
  re.IGNORECASE | re.MULTILINE,
@@ -1339,7 +1352,10 @@ def _append_requirement_coverage_failures(
1339
1352
  return
1340
1353
 
1341
1354
  for row_id, status in rows:
1342
- if not re.fullmatch(r"covered|gap|blocked C-\d{3,}", status):
1355
+ # `status` is already lower-cased at parse time, so the blocker form
1356
+ # arrives as `blocked c-001`; match lower-case here even though the
1357
+ # canonical authored form (and the remedy message) is `blocked C-NNN`.
1358
+ if not re.fullmatch(r"covered|gap|blocked c-\d{3,}", status):
1343
1359
  failures.append(
1344
1360
  "implementation-planning Requirement Coverage row "
1345
1361
  f"`{row_id}` has invalid Status `{status}`; expected "
@@ -1697,8 +1713,8 @@ def validate_phase_boundary(
1697
1713
  "release-handoff report is missing `### 5.6.6 Merge Conflict "
1698
1714
  "Probe` sub-section — required by the release-handoff profile "
1699
1715
  "(self-review 6, merge-conflict probe audit). When the run is "
1700
- "`local only` / `skip`, record the single line `- Not run "
1701
- "(user picked local only or skip).` under the heading."
1716
+ "`local checkout` / `skip`, record the single line `- Not run "
1717
+ "(user picked local checkout or skip).` under the heading."
1702
1718
  )
1703
1719
 
1704
1720
  if task_type != "implementation-planning":
@@ -1710,7 +1726,12 @@ def validate_phase_boundary(
1710
1726
  f"`{needle}`"
1711
1727
  )
1712
1728
 
1713
- gate_match = _GATE_RESULT_RE.search(content)
1729
+ # Scope the gate-result search to the §5.5.9 Plan Body Verification block
1730
+ # so an earlier prose mention of "Gate result" cannot hijack the value.
1731
+ pbv_block = _PLAN_BODY_VERIFICATION_BLOCK_RE.search(content)
1732
+ gate_match = (
1733
+ _GATE_RESULT_RE.search(pbv_block.group("body")) if pbv_block else None
1734
+ )
1714
1735
  if gate_match is None:
1715
1736
  # The `Plan Body Verification` heading check above already covers
1716
1737
  # the wholly-missing case; a heading present without a `Gate result`
@@ -2044,7 +2065,7 @@ def _needs_token_autofix(team_state: dict, report_path: Path) -> bool:
2044
2065
  (team_state.get("leadUsage") or {}).get("source") == "unavailable"
2045
2066
  )
2046
2067
  workers_unavailable = any(
2047
- (w or {}).get("usage", {}).get("source") == "unavailable"
2068
+ ((w or {}).get("usage") or {}).get("source") == "unavailable"
2048
2069
  for w in (team_state.get("workers") or [])
2049
2070
  )
2050
2071
  if lead_unavailable or workers_unavailable:
@@ -128,7 +128,7 @@ DAYS_TOTAL_RE = re.compile(
128
128
  # generically as `<2+ uppercase letters>-<digits>` and then filtered against
129
129
  # the TASK-ID whitelist (extracted from At a Glance) so legitimate task
130
130
  # references (`DEV-1234`, `MOBILE-42`) pass through.
131
- OPAQUE_DASHED_CODE_RE = re.compile(r"\b([A-Z]{1,5})-(\d+)\b")
131
+ OPAQUE_DASHED_CODE_RE = re.compile(r"\b([A-Z]{2,5})-(\d+)\b")
132
132
 
133
133
  # Decision-item letter codes (`A1`, `B2`, `C3`, `D4`). These represent
134
134
  # approval/decision items in source reports and are forbidden in the schedule
@@ -229,7 +229,6 @@ def validate(path: Path) -> list[str]:
229
229
  task_heading_re = re.compile(r"^###\s+\d+-\d+\.\s+", re.MULTILINE)
230
230
  for m in task_heading_re.finditer(text):
231
231
  start = m.start()
232
- block_end = start
233
232
  # find the next ### or ## heading
234
233
  next_h = re.search(r"^(##\s|###\s)", text[start + len(m.group(0)):], re.MULTILINE)
235
234
  block = text[start: start + len(m.group(0)) + (next_h.start() if next_h else len(text))]
@@ -75,11 +75,25 @@ def _candidate_cap(brief_frontmatter: dict) -> int:
75
75
  return DEFAULT_CANDIDATE_CAP
76
76
 
77
77
 
78
+ def _as_path_list(value) -> list[str]:
79
+ """Normalize a frontmatter scope field to a list of path strings.
80
+
81
+ ``_parse_brief_frontmatter`` only returns a list for inline-flow ``[a, b]``
82
+ syntax; a scalar ``scan-scope: src/`` arrives as a bare string. Iterating a
83
+ string yields characters, so coerce a scalar into a single-element list.
84
+ """
85
+ if isinstance(value, str):
86
+ return [value] if value.strip() else []
87
+ return list(value)
88
+
89
+
78
90
  def _scope_subset(
79
91
  candidate_scope_csv: str,
80
92
  scan_scope: list[str],
81
93
  out_of_scope: list[str],
82
94
  ) -> tuple[bool, str]:
95
+ scan_scope = _as_path_list(scan_scope)
96
+ out_of_scope = _as_path_list(out_of_scope)
83
97
  paths = [p.strip() for p in candidate_scope_csv.split(",") if p.strip()]
84
98
  if not paths:
85
99
  return False, "empty Scope"