okstra 0.64.1 → 0.66.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 (47) hide show
  1. package/bin/okstra +1 -0
  2. package/docs/kr/architecture.md +2 -0
  3. package/docs/kr/cli.md +12 -4
  4. package/docs/kr/performance-improvement-plan-v2.md +2 -1
  5. package/docs/project-structure-overview.md +1 -0
  6. package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +1029 -0
  7. package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +168 -0
  8. package/package.json +1 -1
  9. package/runtime/BUILD.json +2 -2
  10. package/runtime/agents/SKILL.md +4 -2
  11. package/runtime/agents/workers/claude-worker.md +1 -1
  12. package/runtime/agents/workers/codex-worker.md +1 -0
  13. package/runtime/agents/workers/gemini-worker.md +1 -0
  14. package/runtime/bin/lib/okstra/cli.sh +4 -0
  15. package/runtime/bin/lib/okstra/globals.sh +1 -0
  16. package/runtime/bin/lib/okstra/usage.sh +4 -1
  17. package/runtime/bin/okstra.sh +1 -0
  18. package/runtime/prompts/profiles/_implementation-executor.md +1 -0
  19. package/runtime/python/okstra_ctl/clarification_items.py +96 -37
  20. package/runtime/python/okstra_ctl/context_cost.py +86 -8
  21. package/runtime/python/okstra_ctl/locks.py +32 -0
  22. package/runtime/python/okstra_ctl/migrate.py +45 -6
  23. package/runtime/python/okstra_ctl/models.py +5 -0
  24. package/runtime/python/okstra_ctl/pr_template.py +2 -7
  25. package/runtime/python/okstra_ctl/render_final_report.py +2 -1
  26. package/runtime/python/okstra_ctl/run.py +58 -44
  27. package/runtime/python/okstra_ctl/run_context.py +3 -8
  28. package/runtime/python/okstra_ctl/seeding.py +25 -18
  29. package/runtime/python/okstra_ctl/wizard.py +9 -11
  30. package/runtime/python/okstra_ctl/worktree.py +13 -0
  31. package/runtime/python/okstra_project/dirs.py +10 -1
  32. package/runtime/python/okstra_token_usage/claude.py +226 -61
  33. package/runtime/python/okstra_token_usage/cli.py +10 -1
  34. package/runtime/python/okstra_token_usage/collect.py +34 -27
  35. package/runtime/python/okstra_token_usage/cursor.py +93 -0
  36. package/runtime/python/okstra_token_usage/paths.py +29 -2
  37. package/runtime/python/okstra_token_usage/pricing.py +7 -3
  38. package/runtime/skills/okstra-coding-preflight/clean-code.md +15 -0
  39. package/runtime/skills/okstra-inspect/SKILL.md +16 -11
  40. package/runtime/skills/okstra-run/templates/pr-body.template.md +13 -16
  41. package/runtime/skills/okstra-schedule/SKILL.md +3 -3
  42. package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
  43. package/runtime/validators/lib/fixtures.sh +73 -10
  44. package/runtime/validators/lib/runners.sh +4 -0
  45. package/runtime/validators/validate-run.py +53 -0
  46. package/runtime/validators/validate_session_conformance.py +430 -0
  47. package/src/migrate.mjs +31 -0
@@ -253,6 +253,21 @@ Good:
253
253
  cache.invalidate(user.id);
254
254
  ```
255
255
 
256
+ **A comment is not a change log.** Never record change history in code — ticket IDs (`FU-003`, `DEV-9185`), verification logs (`verified parity-neutral on the real DB`), `retained from …`, or the reason something *changed*. That belongs to git blame, the commit message, and the PR; the code reader six months later does not have those tickets open. A comment earns its place only by stating a constraint or invariant that is **still true and load-bearing for someone reading the code cold, with zero knowledge of the diff that introduced it**. If it stops making sense once that diff is forgotten, delete it.
257
+
258
+ Bad (change-log noise — delete):
259
+ ```javascript
260
+ // FU-003 read-path deltas — verified parity-neutral against the publish grid
261
+ // on the real DB (DEV-9185 Stage 1). Retained as read-side display alignment.
262
+ applyYearlyMultiplier(rows);
263
+ ```
264
+
265
+ Good (constraint a cold reader needs):
266
+ ```javascript
267
+ // Publish callers omit both fields, so the 0 multiplier is intentional here.
268
+ applyYearlyMultiplier(rows);
269
+ ```
270
+
256
271
  Default to writing **no comment**. Only add one when removing it would confuse a future reader.
257
272
 
258
273
  ## Boy Scout Rule
@@ -524,25 +524,29 @@ Parse the JSON and report these fields:
524
524
  | Current run | `totals.currentRunFileCount`, `totals.currentRunBytes`, `currentRunPath` |
525
525
  | Legacy timestamp artifacts | `totals.legacyTimestampFileCount` |
526
526
  | Instruction set | `instructionSet.fileCount`, `instructionSet.bytes`, `instructionSet.analysisPacketBytes`, `instructionSet.legacyTaskPacketBytes` |
527
- | Lead Phase 1 | `leadPhase1.mode`, `leadPhase1.fileCount`, `leadPhase1.bytes` |
528
- | Analysis worker | `analysisWorker.mode`, `analysisWorker.fileCount`, `analysisWorker.bytesPerWorker`, `analysisWorker.legacyFullContractBytesPerWorker`, `analysisWorker.estimatedPacketModeBytesPerWorker`, `analysisWorker.estimatedReductionPercent` |
529
- | Report writer | `reportWriter.fileCount`, `reportWriter.bytes` |
527
+ | Lead Phase 1 | `leadPhase1.mode`, `leadPhase1.fileCount`, `leadPhase1.bytes`, `leadPhase1.estimatedTokens` |
528
+ | Analysis worker | `analysisWorker.mode`, `analysisWorker.fileCount`, `analysisWorker.bytesPerWorker`, `analysisWorker.estimatedTokensPerWorker`, `analysisWorker.legacyFullContractBytesPerWorker`, `analysisWorker.estimatedPacketModeBytesPerWorker`, `analysisWorker.estimatedReductionPercent` |
529
+ | Report writer | `reportWriter.fileCount`, `reportWriter.bytes`, `reportWriter.estimatedTokens` |
530
+ | Skill assets (hot path) | `skillAssets.fileCount`, `skillAssets.bytes`, `skillAssets.estimatedTokens`, top entries of `skillAssets.files[]` |
530
531
 
531
532
  Format bytes as both raw bytes and rounded KB/MB where useful. Use `analysisWorker.estimatedReductionPercent` for the worker-input reduction. Do not recompute it from `bytesPerWorker` when `analysisWorker.mode == "analysis-packet-primary"` because `bytesPerWorker` is already the packet-primary cost.
532
533
 
534
+ `estimatedTokens*` fields are a static heuristic (~4 ASCII chars/token, non-ASCII ≈ 1 token/char) for ranking instruction surfaces — actual billable cost is the token-usage collector's domain; never present these as billing numbers. `skillAssets` measures the per-run hot-path instruction assets loaded OUTSIDE the task bundle (lifecycle skill bodies + worker agent specs, installed copy preferred) — the prompt-diet target list, sorted by size descending.
535
+
533
536
  ### cost.4 — Output template
534
537
 
535
538
  ```markdown
536
539
  ## okstra Context Cost — <task-key>
537
540
 
538
- | Surface | Files | Size |
539
- |---|---:|---:|
540
- | Task bundle | <N> | <bytes> (<human>) |
541
- | Current run | <N> | <bytes> (<human>) |
542
- | Instruction set | <N> | <bytes> (<human>) |
543
- | Lead Phase 1 (`<mode>`) | <N> | <bytes> (<human>) |
544
- | Analysis worker / worker (`<mode>`) | <N> | <bytes> (<human>) |
545
- | Report writer synthesis | <N> | <bytes> (<human>) |
541
+ | Surface | Files | Size | ~Tokens |
542
+ |---|---:|---:|---:|
543
+ | Task bundle | <N> | <bytes> (<human>) | - |
544
+ | Current run | <N> | <bytes> (<human>) | - |
545
+ | Instruction set | <N> | <bytes> (<human>) | <estimatedTokens> |
546
+ | Lead Phase 1 (`<mode>`) | <N> | <bytes> (<human>) | <estimatedTokens> |
547
+ | Analysis worker / worker (`<mode>`) | <N> | <bytes> (<human>) | <estimatedTokensPerWorker> |
548
+ | Report writer synthesis | <N> | <bytes> (<human>) | <estimatedTokens> |
549
+ | Skill assets (hot path) | <N> | <bytes> (<human>) | <estimatedTokens> |
546
550
 
547
551
  - Current run: `<currentRunPath-or-->`
548
552
  - Legacy timestamp artifacts: `<N>`
@@ -562,6 +566,7 @@ Interpretation rules:
562
566
  - `analysisWorker.mode == "analysis-packet-primary"` means new workers should read `analysis-packet.md` first and open full source inputs only for evidence checks or missing detail.
563
567
  - If `analysisWorker.mode == "full-input-contract"` and `estimatedReductionPercent` is low, the next target is worker prompt/input contract slimming.
564
568
  - If `reportWriter.bytes` dominates, the next target is a compact `synthesis-input` artifact.
569
+ - If `skillAssets.estimatedTokens` dominates the per-run fixed cost, the next target is slimming the largest `skillAssets.files[]` entries (prompt diet — perf plan v2 P2), e.g. thin-core + lazy-sidecar split.
565
570
  - If `legacyTimestampFileCount` is high, recommend current-view/cold-artifact separation or retention cleanup, not destructive deletion by default.
566
571
 
567
572
  ---
@@ -1,28 +1,25 @@
1
1
  <!--
2
- okstra release-handoff 기본 PR 본문 템플릿.
2
+ okstra release-handoff default PR body template.
3
3
 
4
- 파일은 사용자 정의 PR 템플릿이 없을 사용됩니다. 우선순위:
5
- 1. okstra-run Step 6 에서 입력한 per-run override 경로
6
- 2. <project-root>/.okstra/project.json 의 `prTemplatePath`
7
- 3. ~/.okstra/config.json 의 `prTemplatePath`
8
- 4. 디폴트 파일
4
+ This file is used when there is no custom PR template. Priority:
5
+ 1. The per-run override path entered in okstra-run Step 6
6
+ 2. `prTemplatePath` of <project-root>/.okstra/project.json
7
+ 3. `prTemplatePath` of ~/.okstra/config.json
8
+ 4. This default file
9
9
 
10
- 프로젝트 또는 전역 설정으로 자체 템플릿을 쓰려면 경로 하나에
11
- `prTemplatePath` 키를 추가하세요. (절대경로 또는 project_root 기준 상대경로)
12
-
13
- 플레이스홀더는 release-handoff 의 Claude lead 가 다음 입력을 근거로
14
- 직접 채웁니다:
15
- - run brief 의 의도/스코프
16
- - 인용된 final-verification 리포트의 verdict 근거
17
- - `git log --oneline <base>..HEAD` 의 commit 범위
18
- - `git diff <base>..HEAD --stat` 의 변경 파일 통계
10
+ To use your own templates in project or global settings, add the `prTemplatePath` key to one of the paths above. (Absolute path or relative path based on `project_root`)
19
11
 
12
+ The placeholder is filled in directly by the Claude lead for the release-handoff based on the following inputs:
13
+ - The intent and scope of the run brief
14
+ - The grounds for the verdict in the referenced final-verification report
15
+ - The commit range from `git log --oneline <base>..HEAD`
16
+ - The file change statistics from `git diff <base>..HEAD --stat`
20
17
  -->
21
18
 
22
19
  ## **Please check if the PR fulfills these requirements**
23
20
  - [ ] Commits have a single intent
24
21
  - [ ] Tests for the changes have been added (for bug fixes / features)
25
- - [ ] I reviewed my own code
22
+ - [x] I reviewed my own code
26
23
  - [ ] I tested the changes (if not, explain why in the "Other information" section)
27
24
  - [ ] Docs have been added / updated
28
25
 
@@ -56,15 +56,15 @@ Subsequent `okstra <subcmd>` calls self-bootstrap their Python path, so this ski
56
56
 
57
57
  This skill performs cross-task synthesis (multi-task classification, dependency reasoning, phase placement, Gantt/timeline assembly) which benefits substantially from Opus-class reasoning. The frontmatter `model: opus` field above instructs supporting Claude Code harness versions to switch automatically; if the harness ignores it, this gate catches the case explicitly.
58
58
 
59
- 1. Inspect the active session model. The model is shown in the status line, accessible via `/model`, and embedded in the runtime context as the model name (e.g. `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-*`).
60
- 2. If the active model is **Opus-class** (`claude-opus-*`): proceed to Step 1.
59
+ 1. Inspect the active session model. The model is shown in the status line, accessible via `/model`, and embedded in the runtime context as the model name (e.g. `claude-fable-5`, `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-*`).
60
+ 2. If the active model is **Opus-class or above** (`claude-opus-*`, `claude-fable-*`): proceed to Step 1.
61
61
  3. If the active model is **Sonnet or Haiku-class**: STOP and output the following message verbatim, then wait for user response:
62
62
  ```
63
63
  okstra-schedule는 Opus-class 모델에서 실행하는 것을 권장합니다 (현재: <active-model>).
64
64
  /model opus 로 전환 후 다시 호출하시거나, 'sonnet으로 진행' 이라고 명시하시면 그대로 실행합니다.
65
65
  ```
66
66
  4. If the user explicitly insists on the lower model ("sonnet으로 진행", "그대로 진행", "force", or similar): proceed to Step 1, but prepend a single-line warning at the top of the generated schedule file: `> ⚠️ Generated with <model> (not Opus). Cross-task synthesis quality may be reduced.`
67
- 5. Skip this gate ONLY when the harness has clearly enforced `model: opus` from the frontmatter — verifiable by the active model already being Opus-class without manual switching.
67
+ 5. Skip this gate ONLY when the harness has clearly enforced `model: opus` from the frontmatter — verifiable by the active model already being Opus-class or above without manual switching.
68
68
 
69
69
  ### Step 1: Resolve task-group and collect tasks
70
70
 
@@ -38,7 +38,7 @@ okstra tasks are always operated using the `Claude lead` + required worker team
38
38
  1. `resultContract.requiredWorkerRoles` in `task-manifest.json` (and the lead model metadata) is the canonical source. There is no role-level fallback — a missing assignment is a manifest defect, not a license to invent one.
39
39
  2. If `modelExecutionValue` differs from `model`, use `modelExecutionValue` during execution.
40
40
  3. **Spawn-time enforcement for in-process Claude subagents (BLOCKING).** `Claude worker` and `Report writer worker` are in-process Claude subagents whose agent definitions declare `model: inherit` (`agents/workers/claude-worker.md`, `agents/workers/report-writer-worker.md`). `inherit` follows the **lead's** runtime model, NOT the role's assignment — so an opus assignment silently runs on a sonnet lead. To make the assignment binding (not merely declared), lead MUST pass an explicit `model:` parameter on every `Agent(...)` dispatch for these two roles, derived from that role's `modelExecutionValue`. The dispatch `model:` parameter overrides the `inherit` frontmatter; the frontmatter remains only as the fallback when no parameter is supplied. Omitting `model:` on a Claude-side dispatch is a contract violation that reproduces the assigned-vs-actual model deviation.
41
- 4. **`modelExecutionValue` → Agent `model:` family token.** The Agent tool's `model` parameter accepts family tokens only — `opus` / `sonnet` / `haiku` (an exact version such as `claude-opus-4-7` is NOT a valid value). Map by prefix: a `modelExecutionValue` of `opus*` / `claude-opus*` → `"opus"`, `sonnet*` / `claude-sonnet*` → `"sonnet"`, `haiku*` / `claude-haiku*` → `"haiku"`. This enforces the assignment at **family granularity** (opus vs sonnet vs haiku); the exact version within a family is still inherited from the lead session and cannot be pinned via this parameter.
41
+ 4. **`modelExecutionValue` → Agent `model:` family token.** The Agent tool's `model` parameter accepts family tokens only — `fable` / `opus` / `sonnet` / `haiku` (an exact version such as `claude-opus-4-7` is NOT a valid value). Map by prefix: a `modelExecutionValue` of `fable*` / `claude-fable*` → `"fable"`, `opus*` / `claude-opus*` → `"opus"`, `sonnet*` / `claude-sonnet*` → `"sonnet"`, `haiku*` / `claude-haiku*` → `"haiku"`. This enforces the assignment at **family granularity** (fable vs opus vs sonnet vs haiku); the exact version within a family is still inherited from the lead session and cannot be pinned via this parameter.
42
42
  5. **Codex / Gemini wrappers are out of scope for the Agent `model:` rule.** `Codex worker` / `Gemini worker` subagents are Claude wrappers that shell out to an external CLI; the role's `modelExecutionValue` is already applied via the CLI's own `--model <modelExecutionValue>` argument (see `agents/workers/_cli-wrapper-template.md`). The Agent `model:` parameter for these wrappers would only set the wrapper's own orchestration model, not the external CLI's model — leave it at `inherit` and do NOT map it from `modelExecutionValue`.
43
43
 
44
44
  ### Dynamic Worker Role Determination
@@ -147,6 +147,7 @@ prepare_run_validator_fixture() {
147
147
  expected_task_manifest_relative_path="$(task_manifest_relative_path "$task_group" "$task_id")"
148
148
 
149
149
  python3 - "$PROJECT_ROOT" "$expected_task_manifest_relative_path" "$omitted_worker_id" <<'PY'
150
+ from datetime import datetime, timezone
150
151
  from pathlib import Path
151
152
  import json
152
153
  import sys
@@ -258,20 +259,33 @@ for worker in team_state.get("workers", []):
258
259
  result_stem = result_path.stem # e.g. claude-worker-error-analysis-001
259
260
  audit_stem = result_stem.replace("-worker-", "-worker-audit-", 1)
260
261
  audit_path = result_path.with_name(f"{audit_stem}{result_path.suffix}")
261
- audit_path.write_text(
262
- "\n".join(
263
- [
264
- f"# {worker.get('role', worker_id)} Audit",
265
- "",
266
- "- Read task-brief.md end-to-end (validation fixture).",
267
- ]
268
- )
269
- + "\n"
270
- )
262
+ audit_lines = [
263
+ f"# {worker.get('role', worker_id)} Audit",
264
+ "",
265
+ "- Read task-brief.md end-to-end (validation fixture).",
266
+ ]
267
+ if worker_id == "claude":
268
+ # Heartbeat 계약 (agents/workers/claude-worker.md "Heartbeat") —
269
+ # validate_session_conformance 가 claude-worker audit 사이드카의
270
+ # `- PROGRESS:` cadence 를 검사하므로 fixture 도 계약을 준수한다.
271
+ hb_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
272
+ audit_lines += [
273
+ "",
274
+ f"- PROGRESS: started {hb_ts}",
275
+ f"- PROGRESS: read-task-brief.md {hb_ts}",
276
+ f"- PROGRESS: analysis-start {hb_ts}",
277
+ f"- PROGRESS: findings-draft-complete {hb_ts}",
278
+ f"- PROGRESS: write-result-start {hb_ts}",
279
+ ]
280
+ audit_path.write_text("\n".join(audit_lines) + "\n")
271
281
 
272
282
  lead = team_state.get("lead")
273
283
  if isinstance(lead, dict):
274
284
  lead["status"] = "completed"
285
+ # render-only fixture 에는 실 Claude 세션이 없어 sessionId 가 비어 있을 수
286
+ # 있다 — session-conformance fixture jsonl 의 파일명과 맞춘 고정 id 를 부여.
287
+ if not str(lead.get("sessionId") or "").strip():
288
+ lead["sessionId"] = "fixture-lead-session-0001"
275
289
  team_state["workflowState"] = "worker-results-collected"
276
290
 
277
291
  # validate-run.py requires team-state.teamCreate.attempted=true with a
@@ -436,6 +450,55 @@ if WORKSPACE_ROOT:
436
450
  if final_status_path.exists():
437
451
  final_status_path.unlink()
438
452
 
453
+ # session-conformance fixture — validate_session_conformance 는 lead 세션
454
+ # jsonl 에서 run 윈도우 내 PROGRESS 체크포인트를 스캔한다. 계약을 준수한
455
+ # 합성 jsonl 을 주입 시드 디렉터리(.claude-projects-fixture)에 만들어 두고,
456
+ # 러너(run_validator_expectation)가 --claude-projects-dir 로 넘긴다.
457
+ lead_sid = str((team_state.get("lead") or {}).get("sessionId") or "").strip()
458
+ if lead_sid:
459
+ now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
460
+ progress_lines = [
461
+ "PROGRESS: phase-1-intake reading task bundle",
462
+ "PROGRESS: phase-1-intake complete",
463
+ f"PROGRESS: phase-2-prompts preparing {len(team_state.get('workers') or [])} worker prompts",
464
+ "PROGRESS: phase-3-team-create attempting TeamCreate",
465
+ ]
466
+ for worker in team_state.get("workers", []):
467
+ if not isinstance(worker, dict):
468
+ continue
469
+ worker_id = str(worker.get("workerId", "")).strip()
470
+ status = str(worker.get("status", "")).strip()
471
+ if not worker_id or worker_id == "report-writer":
472
+ continue
473
+ if status in ("completed", "timeout", "error"):
474
+ progress_lines.append(
475
+ f"PROGRESS: phase-4-dispatch worker={worker_id}-worker model=fixture"
476
+ )
477
+ if status == "completed":
478
+ progress_lines.append(
479
+ f"PROGRESS: phase-5-collect worker={worker_id}-worker status=completed"
480
+ )
481
+ progress_lines.append("PROGRESS: phase-6-synthesis dispatching report-writer-worker")
482
+ progress_lines.append("PROGRESS: phase-7-persist updating manifests")
483
+ records = [
484
+ {
485
+ "type": "assistant",
486
+ "timestamp": now_iso,
487
+ "message": {"content": [{"type": "text", "text": line}]},
488
+ }
489
+ for line in progress_lines
490
+ ]
491
+ # 인코딩 기준은 validator 가 쓰는 task-manifest.projectRoot — macOS 의
492
+ # /tmp 심링크 때문에 셸의 $PROJECT_ROOT(/tmp/...)와 manifest 의
493
+ # projectRoot(/private/tmp/...)가 다른 문자열일 수 있다.
494
+ manifest_project_root = str(task_manifest.get("projectRoot") or project_root)
495
+ encoded_cwd = "-" + manifest_project_root.strip("/").replace("/", "-")
496
+ session_dir = project_root / ".claude-projects-fixture" / encoded_cwd
497
+ session_dir.mkdir(parents=True, exist_ok=True)
498
+ (session_dir / f"{lead_sid}.jsonl").write_text(
499
+ "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in records)
500
+ )
501
+
439
502
  write_json(team_state_path, team_state)
440
503
  write_json(run_manifest_path, run_manifest)
441
504
  write_json(task_manifest_path, task_manifest)
@@ -87,6 +87,10 @@ process = subprocess.run(
87
87
  str(task_manifest_path),
88
88
  "--final-status",
89
89
  str(final_status_path),
90
+ # session-conformance 주입 시드 — prepare_run_validator_fixture 가
91
+ # 만든 합성 lead jsonl 디렉터리 (실제 ~/.claude/projects 미오염).
92
+ "--claude-projects-dir",
93
+ str(project_root / ".claude-projects-fixture"),
90
94
  ],
91
95
  capture_output=True,
92
96
  text=True,
@@ -1681,6 +1681,41 @@ def _validate_improvement_discovery(
1681
1681
  failures.append(f"improvement-discovery: {err}")
1682
1682
 
1683
1683
 
1684
+ def _validate_session_conformance(
1685
+ team_state: dict,
1686
+ team_state_path: Path,
1687
+ project_root: Path,
1688
+ report_path: Path,
1689
+ task_type: str,
1690
+ claude_projects_dir: str | None,
1691
+ failures: list[str],
1692
+ ) -> None:
1693
+ """agents/SKILL.md BLOCKING 계약 3종(PROGRESS 체크포인트 / claude-worker
1694
+ heartbeat / implementation entry guard)의 post-hoc 검사를 위임하고 실패를
1695
+ ``session-conformance: `` 접두로 folding 한다. 설계:
1696
+ docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md
1697
+ """
1698
+ _validators_dir = Path(__file__).resolve().parent
1699
+ if str(_validators_dir) not in sys.path:
1700
+ sys.path.insert(0, str(_validators_dir))
1701
+ try:
1702
+ from validate_session_conformance import validate_session_conformance # noqa: E402
1703
+ except ImportError as exc:
1704
+ failures.append(
1705
+ f"session-conformance: validate_session_conformance import failed — {exc}"
1706
+ )
1707
+ return
1708
+ result = validate_session_conformance(
1709
+ team_state=team_state,
1710
+ team_state_path=team_state_path,
1711
+ project_root=project_root,
1712
+ report_path=report_path,
1713
+ task_type=task_type,
1714
+ claude_projects_dir=Path(claude_projects_dir) if claude_projects_dir else None,
1715
+ )
1716
+ failures.extend(f"session-conformance: {err}" for err in result.errors)
1717
+
1718
+
1684
1719
  def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
1685
1720
  """requirements-discovery run 에 fan-out/ 이 있으면 packet+index 를 검증해
1686
1721
  실패를 ``requirements-discovery: `` 접두로 folding 한다. fan-out 이 없으면 no-op.
@@ -1993,6 +2028,15 @@ def main() -> int:
1993
2028
  parser.add_argument(
1994
2029
  "--final-status", required=False, help="Optional final status file to write."
1995
2030
  )
2031
+ parser.add_argument(
2032
+ "--claude-projects-dir",
2033
+ required=False,
2034
+ default=None,
2035
+ help=(
2036
+ "Override the Claude Code projects root used for session-conformance "
2037
+ "jsonl lookup (test/diagnostic seam; default: ~/.claude/projects)."
2038
+ ),
2039
+ )
1996
2040
  args = parser.parse_args()
1997
2041
 
1998
2042
  run_manifest_path = Path(args.run_manifest).resolve()
@@ -2043,6 +2087,15 @@ def main() -> int:
2043
2087
  validate_phase_boundary(task_type, report_path, failures)
2044
2088
  if task_type:
2045
2089
  validate_worker_results_audit(report_path, task_type, failures)
2090
+ _validate_session_conformance(
2091
+ team_state,
2092
+ team_state_path,
2093
+ project_root,
2094
+ report_path,
2095
+ task_type,
2096
+ args.claude_projects_dir,
2097
+ failures,
2098
+ )
2046
2099
  if task_type in ("implementation", "final-verification"):
2047
2100
  _sp = None
2048
2101
  _pj = project_json_path(project_root)