okstra 0.96.1 → 0.97.1
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 +1 -1
- package/README.md +1 -1
- package/bin/okstra +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/kr/architecture/storage-model.md +273 -0
- package/docs/kr/architecture.md +6 -277
- package/docs/kr/cli.md +1 -1
- package/docs/project-structure-overview.md +26 -22
- package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +1019 -0
- package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +583 -0
- package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +819 -0
- package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +348 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +2 -2
- package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +171 -0
- package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +112 -0
- package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +145 -0
- package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +113 -0
- package/docs/task-process/release-handoff.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +1 -1
- package/runtime/bin/lib/okstra/interactive.sh +30 -4
- package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +18 -5
- package/runtime/bin/okstra-render-final-report.py +4 -15
- package/runtime/bin/okstra.sh +5 -2
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -0
- package/runtime/prompts/profiles/forbidden-actions.json +1 -1
- package/runtime/prompts/profiles/release-handoff.md +10 -4
- package/runtime/python/okstra_ctl/__init__.py +3 -2
- package/runtime/python/okstra_ctl/backfill.py +12 -21
- package/runtime/python/okstra_ctl/codex_dispatch.py +7 -2
- package/runtime/python/okstra_ctl/conformance.py +21 -3
- package/runtime/python/okstra_ctl/consumers.py +7 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +10 -2
- package/runtime/python/okstra_ctl/doctor.py +29 -12
- package/runtime/python/okstra_ctl/error_log_core.py +72 -0
- package/runtime/python/okstra_ctl/error_report.py +8 -58
- package/runtime/python/okstra_ctl/error_zip.py +319 -0
- package/runtime/python/okstra_ctl/fanout.py +5 -2
- package/runtime/python/okstra_ctl/fix_cycles.py +14 -2
- package/runtime/python/okstra_ctl/git_reconcile.py +7 -1
- package/runtime/python/okstra_ctl/handoff.py +125 -26
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -2
- package/runtime/python/okstra_ctl/improvement_lenses.py +2 -0
- package/runtime/python/okstra_ctl/index.py +34 -91
- package/runtime/python/okstra_ctl/jsonl.py +29 -18
- package/runtime/python/okstra_ctl/listing.py +30 -5
- package/runtime/python/okstra_ctl/md_table.py +1 -1
- package/runtime/python/okstra_ctl/migrate.py +17 -7
- package/runtime/python/okstra_ctl/paths.py +1 -1
- package/runtime/python/okstra_ctl/recap.py +12 -3
- package/runtime/python/okstra_ctl/reconcile.py +13 -11
- package/runtime/python/okstra_ctl/render.py +49 -12
- package/runtime/python/okstra_ctl/render_final_report.py +4 -0
- package/runtime/python/okstra_ctl/report_views.py +47 -6
- package/runtime/python/okstra_ctl/run.py +128 -100
- package/runtime/python/okstra_ctl/run_index_row.py +118 -0
- package/runtime/python/okstra_ctl/schema_excerpt.py +8 -2
- package/runtime/python/okstra_ctl/sequence.py +2 -2
- package/runtime/python/okstra_ctl/stage_integrate.py +193 -0
- package/runtime/python/okstra_ctl/stage_targets.py +2 -8
- package/runtime/python/okstra_ctl/task_target.py +4 -1
- package/runtime/python/okstra_ctl/team_reconcile.py +6 -1
- package/runtime/python/okstra_ctl/wizard.py +27 -12
- package/runtime/python/okstra_ctl/workflow.py +9 -1
- package/runtime/python/okstra_ctl/worktree.py +93 -4
- package/runtime/python/okstra_ctl/worktree_registry.py +57 -8
- package/runtime/python/okstra_token_usage/collect.py +43 -38
- package/runtime/skills/okstra-brief/SKILL.md +43 -12
- package/runtime/skills/okstra-inspect/SKILL.md +44 -1
- package/runtime/templates/reports/final-report.template.md +2 -2
- package/runtime/templates/reports/release-handoff-input.template.md +1 -1
- package/runtime/validators/forbidden_actions.py +8 -1
- package/runtime/validators/validate-brief.py +13 -2
- package/runtime/validators/validate-run.py +27 -6
- package/runtime/validators/validate-schedule.py +1 -2
- package/runtime/validators/validate_improvement_report.py +14 -0
- package/src/cli-registry.mjs +45 -31
- package/src/{codex-dispatch.mjs → commands/execute/codex-dispatch.mjs} +3 -3
- package/src/{codex-run.mjs → commands/execute/codex-run.mjs} +3 -3
- package/src/{error-log.mjs → commands/execute/error-log.mjs} +1 -1
- package/src/{git-reconcile.mjs → commands/execute/git-reconcile.mjs} +1 -1
- package/src/{handoff.mjs → commands/execute/handoff.mjs} +3 -1
- package/src/commands/execute/integrate-stages.mjs +25 -0
- package/src/{plan-validate.mjs → commands/execute/plan-validate.mjs} +1 -1
- package/src/{render-bundle.mjs → commands/execute/render-bundle.mjs} +4 -4
- package/src/{run.mjs → commands/execute/run.mjs} +4 -4
- package/src/{spawn-followups.mjs → commands/execute/spawn-followups.mjs} +1 -1
- package/src/{team.mjs → commands/execute/team.mjs} +3 -3
- package/src/{token-usage.mjs → commands/execute/token-usage.mjs} +1 -1
- package/src/{wizard.mjs → commands/execute/wizard.mjs} +15 -6
- package/src/{worktree-lookup.mjs → commands/execute/worktree-lookup.mjs} +1 -1
- package/src/{context-cost.mjs → commands/inspect/context-cost.mjs} +2 -2
- package/src/{error-report.mjs → commands/inspect/error-report.mjs} +2 -2
- package/src/commands/inspect/error-zip.mjs +25 -0
- package/src/{recap.mjs → commands/inspect/recap.mjs} +2 -2
- package/src/{task-list.mjs → commands/inspect/task-list.mjs} +1 -1
- package/src/{task-show.mjs → commands/inspect/task-show.mjs} +1 -1
- package/src/{check-project.mjs → commands/lifecycle/check-project.mjs} +2 -2
- package/src/{config.mjs → commands/lifecycle/config.mjs} +3 -3
- package/src/{doctor.mjs → commands/lifecycle/doctor.mjs} +4 -4
- package/src/{install.mjs → commands/lifecycle/install.mjs} +18 -13
- package/src/{migrate.mjs → commands/lifecycle/migrate.mjs} +1 -1
- package/src/{paths.mjs → commands/lifecycle/paths.mjs} +1 -60
- package/src/{setup.mjs → commands/lifecycle/setup.mjs} +4 -4
- package/src/{uninstall.mjs → commands/lifecycle/uninstall.mjs} +20 -32
- package/src/{memory.mjs → commands/memory/memory.mjs} +32 -7
- package/src/{inject-report-index.mjs → commands/report/inject-report-index.mjs} +1 -1
- package/src/{render-final-report.mjs → commands/report/render-final-report.mjs} +1 -1
- package/src/{render-views.mjs → commands/report/render-views.mjs} +1 -1
- package/src/lib/paths.mjs +60 -0
- package/src/{_python-helper.mjs → lib/python-helper.mjs} +35 -13
- package/src/{version.mjs → lib/version.mjs} +2 -2
- /package/src/{okstra-dirs.mjs → lib/okstra-dirs.mjs} +0 -0
- /package/src/{_proc.mjs → lib/proc.mjs} +0 -0
- /package/src/{runtime-manifest.mjs → lib/runtime-manifest.mjs} +0 -0
- /package/src/{runtime-resolver.mjs → lib/runtime-resolver.mjs} +0 -0
- /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,
|
|
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,
|
|
555
|
-
# the aggregated wrapper window
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1701
|
-
"(user picked local
|
|
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
|
-
|
|
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"
|
|
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]{
|
|
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"
|