@ionivetech/mugiwara 0.8.0 → 0.8.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +4 -3
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-gates/SKILL.md +6 -0
- package/content/skills/mugiwara-healing/SKILL.md +5 -1
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +5 -4
- package/content/skills/mugiwara-planning/SKILL.md +2 -0
- package/content/skills/mugiwara-quality/SKILL.md +3 -14
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -14
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +3 -15
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +1 -0
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +10 -7
- package/dist/mugiwara.js +1190 -376
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/wave-banners.md +1 -2
- package/scripts/gate-selftest.ts +84 -21
- package/scripts/savepoint.sh +22 -2
- package/scripts/validate-content.ts +60 -0
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/budget.ts +11 -0
- package/src/cli.ts +128 -13
- package/src/config.ts +6 -0
- package/src/continue.ts +29 -0
- package/src/cost.ts +3 -0
- package/src/integrity.ts +64 -15
- package/src/mission.ts +123 -7
- package/src/policy.ts +355 -2
- package/src/provenance.ts +29 -9
- package/src/sign.ts +45 -3
- package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
- package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
- package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +0 -14
package/gemini-extension.json
CHANGED
|
@@ -54,12 +54,22 @@ function applyModeChange(mode) {
|
|
|
54
54
|
writeFileSync(tmp, body);
|
|
55
55
|
renameSync(tmp, file);
|
|
56
56
|
}
|
|
57
|
+
function isCodexInput(parsed) {
|
|
58
|
+
if (process.env.CODEX_HOME || process.env.CODEX_THREAD_ID)
|
|
59
|
+
return true;
|
|
60
|
+
if (typeof parsed.turn_id === "string")
|
|
61
|
+
return true;
|
|
62
|
+
if (typeof parsed.cwd === "string" && typeof parsed.model === "string")
|
|
63
|
+
return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
57
66
|
async function main() {
|
|
58
67
|
let input = "";
|
|
59
68
|
for await (const chunk of process.stdin)
|
|
60
69
|
input += chunk;
|
|
61
70
|
if (!input.trim()) {
|
|
62
|
-
process.
|
|
71
|
+
const codexEmpty = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
|
|
72
|
+
process.stdout.write(JSON.stringify(codexEmpty ? {} : { prompt: "" }));
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
65
75
|
let parsed;
|
|
@@ -68,12 +78,22 @@ async function main() {
|
|
|
68
78
|
} catch {
|
|
69
79
|
parsed = { prompt: input };
|
|
70
80
|
}
|
|
71
|
-
const prompt = parsed.prompt
|
|
81
|
+
const prompt = typeof parsed.prompt === "string" ? parsed.prompt : "";
|
|
72
82
|
const change = parseModeChange(prompt);
|
|
73
83
|
if (change)
|
|
74
84
|
applyModeChange(change);
|
|
75
|
-
|
|
85
|
+
const codex = isCodexInput(parsed);
|
|
86
|
+
if (codex) {
|
|
87
|
+
if (change) {
|
|
88
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: `Mugiwara mode changed to ${change}` } }));
|
|
89
|
+
} else {
|
|
90
|
+
process.stdout.write(JSON.stringify({}));
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
process.stdout.write(JSON.stringify({ prompt }));
|
|
94
|
+
}
|
|
76
95
|
}
|
|
77
96
|
main().catch(() => {
|
|
78
|
-
process.
|
|
97
|
+
const codexFallback = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
|
|
98
|
+
process.stdout.write(JSON.stringify(codexFallback ? {} : { prompt: "" }));
|
|
79
99
|
});
|
|
@@ -57,24 +57,53 @@ function applyModeChange(mode: string) {
|
|
|
57
57
|
renameSync(tmp, file);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Codex vs Claude: Codex UserPromptSubmit expects {} or {hookSpecificOutput:{hookEventName,additionalContext}}
|
|
61
|
+
// Claude expects {prompt:""} — Codex rejects "prompt" (additionalProperties:false).
|
|
62
|
+
// Detect Codex via env or input shape (turn_id/cwd/model are Codex-only).
|
|
63
|
+
function isCodexInput(parsed: Record<string, unknown>): boolean {
|
|
64
|
+
if (process.env.CODEX_HOME || process.env.CODEX_THREAD_ID) return true;
|
|
65
|
+
if (typeof parsed.turn_id === 'string') return true;
|
|
66
|
+
if (typeof parsed.cwd === 'string' && typeof parsed.model === 'string') return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
// main
|
|
61
71
|
async function main() {
|
|
62
72
|
let input = '';
|
|
63
73
|
for await (const chunk of process.stdin) input += chunk;
|
|
64
|
-
if (!input.trim()) {
|
|
74
|
+
if (!input.trim()) {
|
|
75
|
+
// empty stdin — Codex expects {}, Claude expects {prompt:""}
|
|
76
|
+
// Emit Codex-safe empty ( {} ) — Claude also accepts {} as no-op (prompt passthrough)
|
|
77
|
+
// but to keep Claude behavior, sniff env: if Codex-like env, emit {}, else prompt
|
|
78
|
+
const codexEmpty = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
|
|
79
|
+
process.stdout.write(JSON.stringify(codexEmpty ? {} : { prompt: '' }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
65
82
|
|
|
66
|
-
let parsed:
|
|
67
|
-
try { parsed = JSON.parse(input)
|
|
68
|
-
const prompt = parsed.prompt
|
|
83
|
+
let parsed: Record<string, unknown>;
|
|
84
|
+
try { parsed = JSON.parse(input) as Record<string, unknown>; } catch { parsed = { prompt: input }; }
|
|
85
|
+
const prompt = typeof parsed.prompt === 'string' ? parsed.prompt : '';
|
|
69
86
|
|
|
70
87
|
const change = parseModeChange(prompt);
|
|
71
88
|
if (change) applyModeChange(change);
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
const codex = isCodexInput(parsed);
|
|
91
|
+
if (codex) {
|
|
92
|
+
// Codex schema: additionalProperties:false — "prompt" is invalid. Use hookSpecificOutput or {}.
|
|
93
|
+
if (change) {
|
|
94
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: `Mugiwara mode changed to ${change}` } }));
|
|
95
|
+
} else {
|
|
96
|
+
process.stdout.write(JSON.stringify({}));
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
// Claude: pass-through prompt
|
|
100
|
+
process.stdout.write(JSON.stringify({ prompt }));
|
|
101
|
+
}
|
|
75
102
|
}
|
|
76
103
|
|
|
77
104
|
main().catch(() => {
|
|
78
105
|
// silent — hook must never block the conversation
|
|
79
|
-
|
|
106
|
+
// Codex-safe fallback: {} (valid for both, but Claude prefers prompt — however {} is also accepted as no-op)
|
|
107
|
+
const codexFallback = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
|
|
108
|
+
process.stdout.write(JSON.stringify(codexFallback ? {} : { prompt: '' }));
|
|
80
109
|
});
|
package/hooks/session-start.js
CHANGED
|
@@ -104,6 +104,11 @@ if (active.length === 1 && mode === "auto") {
|
|
|
104
104
|
${lines}
|
|
105
105
|
` + `Run /mugiwara continue <mission> [member] to resume one explicitly.`;
|
|
106
106
|
}
|
|
107
|
+
var isCodexSession = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID || process.env.CODEX_HOME_DIR);
|
|
107
108
|
if (resumeContext) {
|
|
108
|
-
|
|
109
|
+
if (isCodexSession) {
|
|
110
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: resumeContext } }));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(JSON.stringify({ additionalContext: resumeContext }));
|
|
113
|
+
}
|
|
109
114
|
}
|
package/hooks/session-start.ts
CHANGED
|
@@ -131,6 +131,13 @@ if (active.length === 1 && mode === 'auto') {
|
|
|
131
131
|
|
|
132
132
|
// Silent unless there is in-flight work: a session that never used mugiwara
|
|
133
133
|
// gets zero injected context.
|
|
134
|
+
// Codex vs Claude: Codex SessionStart expects {hookSpecificOutput:{hookEventName,additionalContext}}
|
|
135
|
+
// Claude expects {additionalContext} — Codex rejects top-level additionalContext (additionalProperties:false).
|
|
136
|
+
const isCodexSession = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID || process.env.CODEX_HOME_DIR);
|
|
134
137
|
if (resumeContext) {
|
|
135
|
-
|
|
138
|
+
if (isCodexSession) {
|
|
139
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: resumeContext } }));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(JSON.stringify({ additionalContext: resumeContext }));
|
|
142
|
+
}
|
|
136
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ionivetech/mugiwara",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, self-healing. Installs into Claude Code, opencode, Copilot, Gemini, Codex, Cursor, Kimi, pi, Windsurf, Cline, Kilo, Antigravity.",
|
|
5
5
|
"homepage": "https://github.com/ionivetech/mugiwara#readme",
|
|
6
6
|
"repository": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"sync-version": "bun scripts/sync-version.ts",
|
|
64
64
|
"build-hooks": "bun scripts/build-hooks.ts",
|
|
65
65
|
"build-hooks:check": "bun scripts/build-hooks.ts --check",
|
|
66
|
-
"gate": "bun run build-hooks:check && bun run typecheck && bun run test:coverage && bun run build && bun scripts/validate-content.ts --check-manifest --check-docs --check-doc-integrity && bun scripts/lane-base.ts && bun scripts/check-doc-links.ts && bun run verify-pack && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts && bun scripts/benchmark-governor.ts && bun scripts/verify-install.ts && bun scripts/conformance.ts && bun run coverage-gate",
|
|
66
|
+
"gate": "bun run build-hooks:check && bun run typecheck && bun run test:coverage && bun run build && bun scripts/write-metrics.ts && bun scripts/validate-content.ts --check-manifest --check-docs --check-doc-integrity --check-readme-metrics && bun scripts/lane-base.ts && bun scripts/check-doc-links.ts && bun run verify-pack && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts && bun scripts/benchmark-governor.ts && bun scripts/verify-install.ts && bun scripts/conformance.ts && bun run coverage-gate",
|
|
67
67
|
"verify-pack": "npm pack --dry-run 2>&1 | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{if(!s.includes('ionivetech-mugiwara')){console.error('npm pack failed');process.exit(1)};console.log('npm package clean')})\"",
|
|
68
68
|
"prepack": "bun run build && bun run sync-version"
|
|
69
69
|
},
|
package/plugin.json
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Cost Governor — Terse & Low-Cost Execution
|
|
2
|
+
|
|
3
|
+
Single source for Work, Scope/Code, Cognitive/Output, Stop-Slop, Adaptive Budget, Benchmark. Verdicts recommended, not enforced; crew acts. Trail rows → `.mugiwara/missions/<mission>/decisions.md` → `## Cost governor decisions`. `savepoint`/`lane-base`/`config` untouched.
|
|
4
|
+
|
|
5
|
+
## Ladder — before adding code, run top to bottom, stop at first that holds
|
|
6
|
+
|
|
7
|
+
1. Does this need to exist at all? Speculative need → skip, one line why (YAGNI).
|
|
8
|
+
2. Already in codebase? Reuse helper/util/pattern nearby → reuse it.
|
|
9
|
+
3. Stdlib does it? Use it.
|
|
10
|
+
4. Native platform covers it? `<input type="date">` over picker lib, CSS over JS, DB constraint over app code → native.
|
|
11
|
+
5. Already-installed dependency solves it? Use it. Never add new dep for a few lines.
|
|
12
|
+
6. Can it be one line? One line.
|
|
13
|
+
7. Only then: minimum code that works. No unrequested abstraction (one impl → no interface/factory/config), no boilerplate for later, deletion over addition, fewest files, shortest diff.
|
|
14
|
+
|
|
15
|
+
Each step: grep callers first; fix root cause in shared function, not symptom in caller. One guard in shared path beats guards in every caller.
|
|
16
|
+
|
|
17
|
+
## Output — terse, deduped
|
|
18
|
+
|
|
19
|
+
Reasoning: Question → Evidence → Decision → Action. No speculative architecture, hypothetical requirements, repeated reconsideration, unrelated implementations. Investigation ends when `acceptance_mapped + surface_understood + path_established` or limits hit with concrete reason (§13). Alternatives ≤3, evidence-backed only.
|
|
20
|
+
|
|
21
|
+
Output: Decision / Action / Result / Evidence / Blocker only, mission-focused. Duplicate explanations fingerprinted and dropped. Every verdict → `cognitive-governor` trail row.
|
|
22
|
+
|
|
23
|
+
## Scope & code
|
|
24
|
+
|
|
25
|
+
Prefer smallest correct scope — reuse + local modification over new architecture (§14). Abstraction justified only when used in ≥2 places or required by contract, never speculative (§15). Dependency added only with explicit justification (§16). Minimum sufficient implementation, never minimum LOC at expense of verification/quality (§15/§38). Measure change surface. Code waste (unnecessary helper/abstraction/wrapper/interface/config/dependency/generated code/refactor) named. Trail row `scope-governor`.
|
|
26
|
+
|
|
27
|
+
## Slop — taxonomy, signals, measurement, intervention
|
|
28
|
+
|
|
29
|
+
Taxonomy 8 kinds (§21): investigation, context, reasoning, output, code, retry, healing, scope.
|
|
30
|
+
|
|
31
|
+
Signals (§22): repeated reads/commands, token-without-evidence, LOC-without-acceptance, abstraction-without-justification.
|
|
32
|
+
|
|
33
|
+
Measure (§23): evidence/criteria/tests/code vs cost delta — cost grows without progress → slop. Flag anomaly (§24): 5k tokens zero progress, work-to-cost drop.
|
|
34
|
+
|
|
35
|
+
Intervene (§20): tolerate / stop / compress / escalate by severity.
|
|
36
|
+
|
|
37
|
+
Detectors — six categories:
|
|
38
|
+
- retry §21.6/§31 same-action same-evidence same-failure → STOP
|
|
39
|
+
- healing §21.7/§32 no progress → stop; `heal_cycle ≥ 3` → halt
|
|
40
|
+
- scope §21.8 out-of-scope without acceptance → reject
|
|
41
|
+
- context §21.2 duplicate/irrelevant → discard/compress
|
|
42
|
+
- investigation §21.1 unbounded exploration → stop
|
|
43
|
+
- code §21.5 unnecessary abstraction/dependency/boilerplate → remove/simplify
|
|
44
|
+
|
|
45
|
+
Trail row `slop-governor`.
|
|
46
|
+
|
|
47
|
+
## Budget — reserve, projection, thresholds, breaker
|
|
48
|
+
|
|
49
|
+
Reserve expected max before expensive stages (Review/Security/Healing). Continuously project `current + remaining required + expected conditional + possible healing` (§26).
|
|
50
|
+
|
|
51
|
+
Expand only with evidence (§27 valid: scope legitimately expanded, security-sensitive path, test surface larger, architecture dependency, legitimate healing; invalid: verbosity/reread/repeat/unnecessary code).
|
|
52
|
+
|
|
53
|
+
Thresholds (§28): 60% → optimize, 75% → aggressive, 90% → protect, 100% → pause, 150% → warning, 300% → stop.
|
|
54
|
+
|
|
55
|
+
Breaker (§29): actual ≥ 2× expected without progress/scope/evidence → trip.
|
|
56
|
+
|
|
57
|
+
Anomaly (§24): flag 5k-zero-progress, re-consumes slop signal.
|
|
58
|
+
|
|
59
|
+
Record every non-ok verdict via `recordBudgetDecision` (§41). Trail row `budget-governor`. Ledger aggregates envelope+events+registry+trail; `mugiwara cost` surfaces ledger (--json); report Cost section renders ledger+avoided+efficiency+trail (§43).
|
|
60
|
+
|
|
61
|
+
## Benchmark & hardening
|
|
62
|
+
|
|
63
|
+
Tracks `scripts/benchmark-governor.ts` harness (deterministic, no network).
|
|
64
|
+
|
|
65
|
+
Cost suite (§48) — 4 workloads:
|
|
66
|
+
- lean-trivial: projected 8000 + overhead 1000, context ≤20000, evidence ≥1, surface 2 files 50 LOC
|
|
67
|
+
- standard-feature: projected 15000 + overhead 1500, context ≤40000, evidence ≥3
|
|
68
|
+
- large-repo: projected 22000 + overhead 2200, context ≤80000, evidence ≥5, surface 50 files
|
|
69
|
+
- long-mission: projected 23000 + overhead 2300, context ≤90000, 9 stages projection ≤ budget
|
|
70
|
+
Check: `measured.tokens ≤ projected + overhead` else fail; `measured.context ≤ max` else fail
|
|
71
|
+
|
|
72
|
+
Stop-Slop suite (§45) — 12 scenarios detect→classify→intervene:
|
|
73
|
+
- endless-exploration → investigation slop → stop
|
|
74
|
+
- repeated-reads (3× no evidence) → context slop → stop; with concrete reason → tolerate
|
|
75
|
+
- repeated-commands (same cmd+evidence fail) → retry slop → stop
|
|
76
|
+
- repeated-failed-test → retry slop → stop
|
|
77
|
+
- repeated-reasoning → reasoning slop → stop
|
|
78
|
+
- unnecessary-abstraction → code slop → stop
|
|
79
|
+
- unnecessary-dependency → code slop → stop
|
|
80
|
+
- unrelated-refactor → scope slop → stop
|
|
81
|
+
- verbose-output → output slop → stop
|
|
82
|
+
- no-progress-healing (cycle ≥3, 0 fixes) → healing slop → stop
|
|
83
|
+
- premature-completion → scope slop → escalate
|
|
84
|
+
- excessive-context (repeated reads + duplicate chars) → context slop → stop
|
|
85
|
+
|
|
86
|
+
Stress (bench-only, no runtime): large repository 50 files within scope → pass; long mission 9 stages projection ≤ full budget → pass; runaway 2× expected no progress → breaker tripped.
|
|
87
|
+
|
|
88
|
+
Thresholds live in `scripts/benchmark-thresholds.json` (or THRESHOLDS const) — `tokens > projected+overhead` fail, `context > max` fail, only move on explicit fixture update, ratchet like retrieval-eval. Regression (§49): cost down but correctness/evidence/security/quality/scope down → fail. Determinism: harness pure over explicit fixture inputs, no Date.now/Math.random/network. CI: `package.json:gate` includes harness; `gate-selftest` tampers thresholds → harness must exit 1. Harness measures, not enforces.
|
|
89
|
+
|
|
90
|
+
## Reporting & trail
|
|
91
|
+
|
|
92
|
+
Ledger aggregates envelope+events+registry+trail; `mugiwara cost` surfaces ledger; report Cost section renders ledger+avoided+efficiency+trail (§43). Every verdict lands as trail row in decisions.md.
|
|
93
|
+
|
|
94
|
+
## Checklist
|
|
95
|
+
|
|
96
|
+
- [ ] ladder run before each code addition, no skipped rung
|
|
97
|
+
- [ ] output Decision/Action/Result/Evidence/Blocker, deduped
|
|
98
|
+
- [ ] scope/code: reuse checked, abstraction & dep justified
|
|
99
|
+
- [ ] slop: taxonomy classified, signals measured, intervention applied
|
|
100
|
+
- [ ] budget: reserved, projected, thresholds respected, breaker armed
|
|
101
|
+
- [ ] benchmark: 4 cost + 12 slop + 3 stress green, thresholds ratcheted
|
|
102
|
+
- [ ] trail rows written for every non-trivial verdict
|
|
103
|
+
|
|
104
|
+
Unchecked boxes are not done.
|
|
@@ -53,8 +53,7 @@ their banners appear only when a wave or worker names them.
|
|
|
53
53
|
|
|
54
54
|
## Rules
|
|
55
55
|
|
|
56
|
-
1. Banner before EVERY flow stage; handoff after it. No flow stage starts without its
|
|
57
|
-
banner (orchestration red flag).
|
|
56
|
+
1. Banner before EVERY flow stage; handoff after it. Main thread emits `===== FLOW N — CREW =====` FIRST line and `→ Flow N+1 — Crew` LAST line even when subagent does work — covers Flow 0 Luffy, 1 Usopp, 2 Nami, 3 Zoro, 4 Chopper, 5 Sanji, 6 Franky, 7 Robin/Jinbe, 8 Brook, 9 Luffy. No flow stage starts without its banner (orchestration red flag).
|
|
58
57
|
2. The color comes from this table only — never invent a hex mid-mission.
|
|
59
58
|
3. One form everywhere: equals line `===== <emoji> FLOW N — <CREW> (ROLE) =====` — five `=` per side, the crew emoji from the table leading the line, ANSI-wrapped in terminals, plain in markdown-rendering UIs. When unsure, the plain form is safe everywhere.
|
|
60
59
|
4. Only the crew table's colors and the two SGR forms above (truecolor,
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { execSync } from 'node:child_process';
|
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, copyFileSync, renameSync, unlinkSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
7
7
|
import { join, dirname } from 'node:path';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
|
+
import { gatesForLane } from '../src/policy.ts';
|
|
10
|
+
import { budgetForLane } from '../src/cost.ts';
|
|
9
11
|
|
|
10
12
|
const root = join(import.meta.dirname, '..');
|
|
11
13
|
let passed = 0;
|
|
@@ -91,9 +93,11 @@ console.log('\nCost gate — measured vs stated index chars');
|
|
|
91
93
|
const costFile = join(root, 'docs', 'concepts', 'cost.md');
|
|
92
94
|
const original = readFileSync(costFile, 'utf8');
|
|
93
95
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const costPattern = /\*\*Current:\*\* \d[\d,]* chars/;
|
|
97
|
+
const drifted = original.replace(costPattern, '**Current:** 1 chars');
|
|
98
|
+
if (!costPattern.test(original) || drifted === original) {
|
|
99
|
+
console.error('✗ COST: mutation target not found — the gate it guards may be dead.');
|
|
100
|
+
failed++;
|
|
97
101
|
} else {
|
|
98
102
|
writeFileSync(costFile, drifted);
|
|
99
103
|
assert('drifted stated index chars → exit 1', false, () => run('COST', 'bun scripts/validate-content.ts'));
|
|
@@ -134,12 +138,14 @@ if (!existsSync(savepointFile)) {
|
|
|
134
138
|
const original = readFileSync(savepointFile, 'utf8');
|
|
135
139
|
try {
|
|
136
140
|
// reintroduce the D1 defect: read lane_prev with require() of a relative path
|
|
141
|
+
const d1Pattern = /PREV_JSON=\$\(node -e "try\{const fs=require\('fs'\);const s=JSON\.parse\(fs\.readFileSync\(process\.argv\[1\],'utf8'\)\);process\.stdout\.write\(JSON\.stringify\(\{mission:s\.mission\|\|'',lane:s\.lane\|\|'',peak:s\.lane_peak\|\|''\}\)\)\}catch\(e\)\{process\.stdout\.write\('\{\}'\)\}" "\$STATE_FILE" 2>\/dev\/null \|\| true\)/;
|
|
137
142
|
const broken = original.replace(
|
|
138
|
-
|
|
143
|
+
d1Pattern,
|
|
139
144
|
"PREV_JSON=$(node -e \"try{const s=require(process.argv[1]);process.stdout.write(JSON.stringify({mission:s.mission||'',lane:s.lane||'',peak:s.lane_peak||''}))}catch(e){process.stdout.write('{}')}\" \"$STATE_FILE\" 2>/dev/null || true)"
|
|
140
145
|
);
|
|
141
|
-
if (broken === original) {
|
|
142
|
-
console.
|
|
146
|
+
if (!d1Pattern.test(original) || broken === original) {
|
|
147
|
+
console.error('✗ D1: mutation target not found — the gate it guards may be dead.');
|
|
148
|
+
failed++;
|
|
143
149
|
} else {
|
|
144
150
|
writeFileSync(savepointFile, broken);
|
|
145
151
|
assert('broken LANE_PREV resolve → lane-integrity fails', false, () => run('D1', 'bun run test -- lane-integrity -t "lane_prev"'));
|
|
@@ -158,12 +164,14 @@ if (!existsSync(savepointFile)) {
|
|
|
158
164
|
const original = readFileSync(savepointFile, 'utf8');
|
|
159
165
|
try {
|
|
160
166
|
// neuter the clamp: make lane_rank always return 0 so a drop never holds
|
|
167
|
+
const d2Pattern = /lane_rank\(\) \{\n case "\$1" in\n direct\) echo 0 ;;[\s\S]*?\n esac\n\}/;
|
|
161
168
|
const broken = original.replace(
|
|
162
|
-
|
|
169
|
+
d2Pattern,
|
|
163
170
|
'lane_rank() {\n echo 0\n}'
|
|
164
171
|
);
|
|
165
|
-
if (broken === original) {
|
|
166
|
-
console.
|
|
172
|
+
if (!d2Pattern.test(original) || broken === original) {
|
|
173
|
+
console.error('✗ D2: mutation target not found — the gate it guards may be dead.');
|
|
174
|
+
failed++;
|
|
167
175
|
} else {
|
|
168
176
|
writeFileSync(savepointFile, broken);
|
|
169
177
|
assert('broken clamp → lane-integrity fails', false, () => run('D2', 'bun run test -- lane-integrity -t "clamp"'));
|
|
@@ -183,12 +191,14 @@ if (!existsSync(patternsFile)) {
|
|
|
183
191
|
const original = readFileSync(patternsFile, 'utf8');
|
|
184
192
|
try {
|
|
185
193
|
// reintroduce the D3 defect: singular-only list (no payments/, migrations/)
|
|
194
|
+
const d3Pattern = /SENSITIVE_PATS=.*/;
|
|
186
195
|
const broken = original.replace(
|
|
187
|
-
|
|
196
|
+
d3Pattern,
|
|
188
197
|
'SENSITIVE_PATS="auth/|payment/|billing/|crypto/|secrets/|\\.env$|config/.*key|migration/|\\.sql$|schema\\.|\\.prisma$|\\.terraform|\\.tf$"'
|
|
189
198
|
);
|
|
190
|
-
if (broken === original) {
|
|
191
|
-
console.
|
|
199
|
+
if (!d3Pattern.test(original) || broken === original) {
|
|
200
|
+
console.error('✗ D3: mutation target not found — the gate it guards may be dead.');
|
|
201
|
+
failed++;
|
|
192
202
|
} else {
|
|
193
203
|
writeFileSync(patternsFile, broken);
|
|
194
204
|
assert('singular sensitive patterns → lane-integrity fails', false, () => run('D3', 'bun run test -- lane-integrity -t "payments"'));
|
|
@@ -218,10 +228,12 @@ if (!existsSync(patternsFile)) {
|
|
|
218
228
|
const live = original.match(/SENSITIVE_PATS="([^"]+)"/)?.[1] ?? '';
|
|
219
229
|
const broken = live.split('|').filter(t => !D3B_FAMILY_TOKENS.has(t)).join('|');
|
|
220
230
|
const brokenLine = `SENSITIVE_PATS="${broken}"`;
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
const d3bPattern = /SENSITIVE_PATS="[^"]*"/;
|
|
232
|
+
if (!d3bPattern.test(original) || broken === live || !live) {
|
|
233
|
+
console.error('✗ D3b: mutation target not found — the gate it guards may be dead.');
|
|
234
|
+
failed++;
|
|
223
235
|
} else {
|
|
224
|
-
writeFileSync(patternsFile, original.replace(
|
|
236
|
+
writeFileSync(patternsFile, original.replace(d3bPattern, brokenLine));
|
|
225
237
|
assert('missing new categories → lane-integrity fails', false, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
|
|
226
238
|
}
|
|
227
239
|
} finally {
|
|
@@ -238,12 +250,14 @@ if (!existsSync(savepointFile)) {
|
|
|
238
250
|
const original = readFileSync(savepointFile, 'utf8');
|
|
239
251
|
try {
|
|
240
252
|
// revert to delta-based (0 on deletions/refactors)
|
|
253
|
+
const d4Pattern = /LOC_TOKENS=\$\(\( LOC_CHURN \* 12 \)\)/;
|
|
241
254
|
const broken = original.replace(
|
|
242
|
-
|
|
255
|
+
d4Pattern,
|
|
243
256
|
'LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))'
|
|
244
257
|
);
|
|
245
|
-
if (broken === original) {
|
|
246
|
-
console.
|
|
258
|
+
if (!d4Pattern.test(original) || broken === original) {
|
|
259
|
+
console.error('✗ D4: mutation target not found — the gate it guards may be dead.');
|
|
260
|
+
failed++;
|
|
247
261
|
} else {
|
|
248
262
|
writeFileSync(savepointFile, broken);
|
|
249
263
|
assert('zero churn tokens → lane-integrity fails', false, () => run('D4', 'bun run test -- lane-integrity -t "churn"'));
|
|
@@ -263,12 +277,14 @@ if (!existsSync(savepointFile)) {
|
|
|
263
277
|
try {
|
|
264
278
|
// silently drop the continue writer block (make it a no-op). Anchor on the
|
|
265
279
|
// D10 header comment so the regex hits the writer, not the STATE_FILE if.
|
|
280
|
+
const d10Pattern = /# --- continue.*\(D10\): machine-written resume point ---[\s\S]*?\nfi\n\n/;
|
|
266
281
|
const broken = original.replace(
|
|
267
|
-
|
|
282
|
+
d10Pattern,
|
|
268
283
|
'# --- continue writer disabled (D10) ---\n\n'
|
|
269
284
|
);
|
|
270
|
-
if (broken === original) {
|
|
271
|
-
console.
|
|
285
|
+
if (!d10Pattern.test(original) || broken === original) {
|
|
286
|
+
console.error('✗ D10: mutation target not found — the gate it guards may be dead.');
|
|
287
|
+
failed++;
|
|
272
288
|
} else {
|
|
273
289
|
writeFileSync(savepointFile, broken);
|
|
274
290
|
assert('broken continue writer → savepoint fails', false, () => run('D10', 'bun run test -- savepoint -t "D10"'));
|
|
@@ -612,5 +628,52 @@ console.log('\nDOCLINKS — doc link resolution');
|
|
|
612
628
|
}
|
|
613
629
|
}
|
|
614
630
|
|
|
631
|
+
// --- T3: lane-aware gates — direct 3 steps, full 12 steps ---
|
|
632
|
+
console.log('\nT3 — lane-aware gates');
|
|
633
|
+
{
|
|
634
|
+
const policyFile = join(root, 'src', 'policy.ts');
|
|
635
|
+
const originalPolicy = readFileSync(policyFile, 'utf8');
|
|
636
|
+
try {
|
|
637
|
+
assert('direct lane → 3 steps', true, () => gatesForLane('direct').length === 3);
|
|
638
|
+
assert('direct lane includes typecheck+build', true, () => {
|
|
639
|
+
const s = gatesForLane('direct');
|
|
640
|
+
return s.includes('typecheck') && s.includes('build');
|
|
641
|
+
});
|
|
642
|
+
assert('lean lane → 6 steps with validate-content', true, () => {
|
|
643
|
+
const s = gatesForLane('lean');
|
|
644
|
+
return s.length === 6 && s.includes('validate-content');
|
|
645
|
+
});
|
|
646
|
+
assert('standard lane → 9 steps', true, () => gatesForLane('standard').length === 9);
|
|
647
|
+
assert('full lane → 12 steps with evals/retrieval/conformance', true, () => {
|
|
648
|
+
const s = gatesForLane('full');
|
|
649
|
+
return s.length === 12 && s.includes('run-evals') && s.includes('retrieval-eval') && s.includes('conformance');
|
|
650
|
+
});
|
|
651
|
+
assert('budget direct → 0, full → 50000', true, () => budgetForLane('direct') === 0 && budgetForLane('full') === 50000);
|
|
652
|
+
assert('budget spike → 3000 (direct fixture 3k)', true, () => budgetForLane('spike') === 3000);
|
|
653
|
+
// mutation: break direct step count → should fail (file content shows not 3)
|
|
654
|
+
const broken = originalPolicy.replace(
|
|
655
|
+
"direct: ['build-hooks:check', 'typecheck', 'build']",
|
|
656
|
+
"direct: ['typecheck']"
|
|
657
|
+
);
|
|
658
|
+
if (broken !== originalPolicy) {
|
|
659
|
+
writeFileSync(policyFile, broken);
|
|
660
|
+
assert('broken direct gate → not 3 steps', false, () => readFileSync(policyFile, 'utf8').includes("direct: ['build-hooks:check', 'typecheck', 'build']"));
|
|
661
|
+
} else {
|
|
662
|
+
console.error('✗ T3: mutation target not found — the gate it guards may be dead.');
|
|
663
|
+
failed++;
|
|
664
|
+
}
|
|
665
|
+
} finally {
|
|
666
|
+
writeFileSync(policyFile, originalPolicy);
|
|
667
|
+
assert('restored → direct 3 steps', true, () => {
|
|
668
|
+
const txt = readFileSync(policyFile, 'utf8');
|
|
669
|
+
return txt.includes("direct: ['build-hooks:check', 'typecheck', 'build']");
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
assert('full still includes conformance (conformance 71→74)', true, () => {
|
|
673
|
+
const txt = readFileSync(policyFile, 'utf8');
|
|
674
|
+
return txt.includes("'conformance'") && txt.includes("full:");
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
615
678
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
616
679
|
process.exit(failed > 0 ? 1 : 0);
|
package/scripts/savepoint.sh
CHANGED
|
@@ -331,6 +331,13 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
|
|
|
331
331
|
TASKS_TOTAL=$(grep -cE '^\s*-\s*\[[ xX]\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
332
332
|
TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
333
333
|
fi
|
|
334
|
+
# Fallback for large campaigns (>3 phases, >1500 lines) where master plan.md is an index
|
|
335
|
+
# and tasks live in sub-plan/*.md — only when plan.md has zero checkbox tasks to
|
|
336
|
+
# keep simple missions unchanged.
|
|
337
|
+
if [ "${TASKS_TOTAL:-0}" -eq 0 ] 2>/dev/null && [ -d "$MISSION_DIR/sub-plan" ]; then
|
|
338
|
+
TASKS_TOTAL=$(grep -rcE '^\s*-\s*\[[ xX]\]' "$MISSION_DIR/sub-plan" 2>/dev/null | awk -F: '{s+=$2} END {print s+0}' || true)
|
|
339
|
+
TASKS_DONE=$(grep -rc '\[x\]' "$MISSION_DIR/sub-plan" 2>/dev/null | awk -F: '{s+=$2} END {print s+0}' || true)
|
|
340
|
+
fi
|
|
334
341
|
|
|
335
342
|
# blocker count
|
|
336
343
|
BLOCKERS_FILE="$MISSION_DIR/blockers.md"
|
|
@@ -365,6 +372,16 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
|
|
|
365
372
|
HEAL_HALT=true
|
|
366
373
|
fi
|
|
367
374
|
|
|
375
|
+
# slop — context (repeated reads) per cost-governor §§21-24,31-32 — T5 wire all crews Luffy/Nami/Zoro/Brook
|
|
376
|
+
REPEATED_READS=0
|
|
377
|
+
REPEATED_THRESHOLD=3
|
|
378
|
+
REGISTRY_FILE="$MISSION_DIR/context-registry.jsonl"
|
|
379
|
+
if [ -f "$REGISTRY_FILE" ]; then
|
|
380
|
+
REPEATED_READS=$(node -e "try{const fs=require('fs');const t=fs.readFileSync(process.argv[1],'utf8');let s=0;for(const l of t.split(/\r?\n/)){if(!l.trim())continue;try{const e=JSON.parse(l);if(typeof e.reads==='number'&&e.reads>=2)s+=Math.floor(e.reads)-1}catch{}}console.log(s)}catch(e){console.log(0)}" "$REGISTRY_FILE" 2>/dev/null || echo 0)
|
|
381
|
+
REPEATED_READS=$(( ${REPEATED_READS:-0} + 0 ))
|
|
382
|
+
fi
|
|
383
|
+
# repeated_reads > threshold → context slop — crew must skip re-read/compress before dispatch (§22,31); heal_cycle≥max → halt/escalate (§21.7/32) — cost-governor §§20,21-24
|
|
384
|
+
|
|
368
385
|
# depth flags — advisory → measured (roadmap v0.8 item 4). Read from config
|
|
369
386
|
# like the other keys; computed into state.json so enforcement is a fact the
|
|
370
387
|
# gates flow stage can read, not prose.
|
|
@@ -523,7 +540,9 @@ const data = {
|
|
|
523
540
|
budget_status: process.argv[19],
|
|
524
541
|
skill_version: process.argv[20],
|
|
525
542
|
evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
|
|
526
|
-
updated_at: process.argv[22]
|
|
543
|
+
updated_at: process.argv[22],
|
|
544
|
+
schema_version: 2,
|
|
545
|
+
repeated_reads: parseInt(process.argv[41], 10) || 0
|
|
527
546
|
};
|
|
528
547
|
require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
|
|
529
548
|
" \
|
|
@@ -536,7 +555,8 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
|
|
|
536
555
|
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
|
|
537
556
|
"$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
|
|
538
557
|
"$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
|
|
539
|
-
"$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY"
|
|
558
|
+
"$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY" \
|
|
559
|
+
"$REPEATED_READS"
|
|
540
560
|
|
|
541
561
|
if [ "$LANE_ROSE" = true ]; then
|
|
542
562
|
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
@@ -417,6 +417,66 @@ if (integrityArg !== -1) {
|
|
|
417
417
|
}
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
// --- README metrics gate (D3): README table must match .metrics/latest.json ---
|
|
421
|
+
if (process.argv.includes('--check-readme-metrics')) {
|
|
422
|
+
const metricsPath = join(import.meta.dirname, '..', '.metrics', 'latest.json');
|
|
423
|
+
if (!existsSync(metricsPath)) {
|
|
424
|
+
errors.push(`README metrics: ${metricsPath} not found — run bun scripts/write-metrics.ts`);
|
|
425
|
+
} else {
|
|
426
|
+
let metrics: any;
|
|
427
|
+
try { metrics = JSON.parse(readFileSync(metricsPath, 'utf8')); }
|
|
428
|
+
catch (e) { errors.push(`README metrics: invalid JSON in ${metricsPath}: ${e}`); }
|
|
429
|
+
if (metrics) {
|
|
430
|
+
const readmePath = join(import.meta.dirname, '..', 'README.md');
|
|
431
|
+
if (!existsSync(readmePath)) {
|
|
432
|
+
errors.push('README metrics: README.md not found');
|
|
433
|
+
} else {
|
|
434
|
+
const readme = readFileSync(readmePath, 'utf8');
|
|
435
|
+
// rank-1: **95.9%**, 216 probes
|
|
436
|
+
const rankMatch = readme.match(/Retrieval routing rank-1[^\n]*?(\d+\.\d+)%[^\n]*?(\d+)\s+probes/i);
|
|
437
|
+
if (!rankMatch) {
|
|
438
|
+
errors.push('README metrics: could not parse Retrieval routing rank-1 row (expected "**X.Y%**, N probes")');
|
|
439
|
+
} else {
|
|
440
|
+
const readmeRank = parseFloat(rankMatch[1]);
|
|
441
|
+
const readmeProbes = parseInt(rankMatch[2], 10);
|
|
442
|
+
const wantRank = Number(metrics.retrieval_rank1);
|
|
443
|
+
const wantProbes = Number(metrics.retrieval_probes);
|
|
444
|
+
if (Math.abs(readmeRank - wantRank) > 0.05) {
|
|
445
|
+
errors.push(`README metrics: rank-1 ${readmeRank}% != metrics ${wantRank}% (probes ${readmeProbes} vs ${wantProbes}) — run bun scripts/write-metrics.ts and update README`);
|
|
446
|
+
}
|
|
447
|
+
if (readmeProbes !== wantProbes) {
|
|
448
|
+
errors.push(`README metrics: probes ${readmeProbes} != metrics ${wantProbes} (rank ${readmeRank}% vs ${wantRank}%) — run bun scripts/write-metrics.ts and update README`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// pointers: **286/286**, 9 targets (or tiers)
|
|
452
|
+
const ptrMatch = readme.match(/Reference pointers resolve[^\n]*?\*\*(\d+)\/(\d+)\*\*[^\n]*?(\d+)\s+(tiers|targets)/i);
|
|
453
|
+
if (!ptrMatch) {
|
|
454
|
+
errors.push('README metrics: could not parse Reference pointers row (expected "**N/N**, M targets")');
|
|
455
|
+
} else {
|
|
456
|
+
const a = parseInt(ptrMatch[1], 10);
|
|
457
|
+
const b = parseInt(ptrMatch[2], 10);
|
|
458
|
+
const count = parseInt(ptrMatch[3], 10);
|
|
459
|
+
const wantTotal = Number(metrics.pointers_total);
|
|
460
|
+
const wantTargets = Number(metrics.pointers_targets);
|
|
461
|
+
if (a !== wantTotal || b !== wantTotal) {
|
|
462
|
+
errors.push(`README metrics: pointers ${a}/${b} != metrics ${wantTotal}/${wantTotal} — run bun scripts/write-metrics.ts and update README`);
|
|
463
|
+
}
|
|
464
|
+
if (count !== wantTargets) {
|
|
465
|
+
errors.push(`README metrics: targets/tiers ${count} != metrics ${wantTargets} — run bun scripts/write-metrics.ts and update README (expected ${wantTargets} targets)`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// sanity: table still claims "Nothing in this table is an estimate"
|
|
469
|
+
if (!readme.includes('Nothing in this table is an estimate')) {
|
|
470
|
+
errors.push('README metrics: missing "Nothing in this table is an estimate" line');
|
|
471
|
+
}
|
|
472
|
+
if (errors.filter(e => e.startsWith('README metrics:')).length === 0) {
|
|
473
|
+
console.log(`✓ README metrics match .metrics/latest.json (rank-1 ${metrics.retrieval_rank1}% ${metrics.retrieval_probes} probes, ${metrics.pointers_total}/${metrics.pointers_total} pointers, ${metrics.pointers_targets} targets)`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
420
480
|
// Conditional-assertion guard: an expect() reachable only inside a truthiness
|
|
421
481
|
// check silently passes when the value is absent. This class produced 9 defects.
|
|
422
482
|
// Allowed: checks keyed on a declared invariant (tier, fixture keys).
|
|
@@ -18,6 +18,7 @@ import { targets, TARGET_IDS } from '../src/targets/index.ts';
|
|
|
18
18
|
|
|
19
19
|
const repoRoot = join(import.meta.dirname, '..');
|
|
20
20
|
const fail: string[] = [];
|
|
21
|
+
const isJson = process.argv.includes('--json');
|
|
21
22
|
|
|
22
23
|
function findMd(root: string, out: string[] = []): string[] {
|
|
23
24
|
if (!existsSync(root)) return out;
|
|
@@ -169,6 +170,25 @@ if (orphans.length > ORPHAN_BASELINE) {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// ---------------------------------------------------------------------------
|
|
173
|
+
if (isJson) {
|
|
174
|
+
const payload = {
|
|
175
|
+
pointers_total: pointers,
|
|
176
|
+
pointers_targets: TARGET_IDS.length,
|
|
177
|
+
pointers_broken: brokenPointers,
|
|
178
|
+
prose_paths: prosePaths,
|
|
179
|
+
prose_files: proseFiles.length,
|
|
180
|
+
orphans,
|
|
181
|
+
orphans_count: orphans.length,
|
|
182
|
+
ref_files: refFiles.length,
|
|
183
|
+
targets: TARGET_IDS.length,
|
|
184
|
+
pointers: pointers,
|
|
185
|
+
broken_pointers: brokenPointers,
|
|
186
|
+
};
|
|
187
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
188
|
+
if (fail.length) process.exit(1);
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
console.log(` ${pointers} pointers checked across ${TARGET_IDS.length} targets`);
|
|
173
193
|
console.log(` ${prosePaths} prose paths checked in ${proseFiles.length} files`);
|
|
174
194
|
console.log(` ${orphans.length}/${refFiles.length} reference files unreachable (baseline ${ORPHAN_BASELINE})`);
|