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.
- package/bin/okstra +1 -0
- package/docs/kr/architecture.md +2 -0
- package/docs/kr/cli.md +12 -4
- package/docs/kr/performance-improvement-plan-v2.md +2 -1
- package/docs/project-structure-overview.md +1 -0
- package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +1029 -0
- package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +168 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +4 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +1 -0
- package/runtime/agents/workers/gemini-worker.md +1 -0
- package/runtime/bin/lib/okstra/cli.sh +4 -0
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +4 -1
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/profiles/_implementation-executor.md +1 -0
- package/runtime/python/okstra_ctl/clarification_items.py +96 -37
- package/runtime/python/okstra_ctl/context_cost.py +86 -8
- package/runtime/python/okstra_ctl/locks.py +32 -0
- package/runtime/python/okstra_ctl/migrate.py +45 -6
- package/runtime/python/okstra_ctl/models.py +5 -0
- package/runtime/python/okstra_ctl/pr_template.py +2 -7
- package/runtime/python/okstra_ctl/render_final_report.py +2 -1
- package/runtime/python/okstra_ctl/run.py +58 -44
- package/runtime/python/okstra_ctl/run_context.py +3 -8
- package/runtime/python/okstra_ctl/seeding.py +25 -18
- package/runtime/python/okstra_ctl/wizard.py +9 -11
- package/runtime/python/okstra_ctl/worktree.py +13 -0
- package/runtime/python/okstra_project/dirs.py +10 -1
- package/runtime/python/okstra_token_usage/claude.py +226 -61
- package/runtime/python/okstra_token_usage/cli.py +10 -1
- package/runtime/python/okstra_token_usage/collect.py +34 -27
- package/runtime/python/okstra_token_usage/cursor.py +93 -0
- package/runtime/python/okstra_token_usage/paths.py +29 -2
- package/runtime/python/okstra_token_usage/pricing.py +7 -3
- package/runtime/skills/okstra-coding-preflight/clean-code.md +15 -0
- package/runtime/skills/okstra-inspect/SKILL.md +16 -11
- package/runtime/skills/okstra-run/templates/pr-body.template.md +13 -16
- package/runtime/skills/okstra-schedule/SKILL.md +3 -3
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
- package/runtime/validators/lib/fixtures.sh +73 -10
- package/runtime/validators/lib/runners.sh +4 -0
- package/runtime/validators/validate-run.py +53 -0
- package/runtime/validators/validate_session_conformance.py +430 -0
- 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
|
|
2
|
+
okstra release-handoff default PR body template.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
- [
|
|
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
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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)
|