@bonesofspring/ai-rules 0.2.21 → 0.2.22
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/package.json +1 -1
- package/presets/_shared/core/agent-team/agent-artifact-contracts.md +2 -0
- package/presets/_shared/core/meta/preset-twin-sync.md +1 -1
- package/presets/claude/android-kotlin/agents/build-verifier.md +2 -0
- package/presets/claude/android-kotlin/agents/code-reviewer.md +2 -0
- package/presets/claude/android-kotlin/agents/debugger.md +1 -0
- package/presets/claude/android-kotlin/agents/feature-developer.md +3 -0
- package/presets/claude/android-kotlin/agents/qa-tester.md +2 -0
- package/presets/claude/android-kotlin/agents/task-router.md +4 -0
- package/presets/claude/android-kotlin/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/go/agents/build-verifier.md +4 -0
- package/presets/claude/go/agents/code-reviewer.md +4 -0
- package/presets/claude/go/agents/debugger.md +4 -0
- package/presets/claude/go/agents/feature-developer.md +5 -0
- package/presets/claude/go/agents/qa-tester.md +4 -0
- package/presets/claude/go/agents/task-router.md +4 -0
- package/presets/claude/go/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/ios-swift/agents/build-verifier.md +2 -0
- package/presets/claude/ios-swift/agents/code-reviewer.md +2 -0
- package/presets/claude/ios-swift/agents/debugger.md +1 -0
- package/presets/claude/ios-swift/agents/feature-developer.md +3 -0
- package/presets/claude/ios-swift/agents/qa-tester.md +2 -0
- package/presets/claude/ios-swift/agents/task-router.md +4 -0
- package/presets/claude/ios-swift/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/java/agents/build-verifier.md +4 -0
- package/presets/claude/java/agents/code-reviewer.md +4 -0
- package/presets/claude/java/agents/debugger.md +4 -0
- package/presets/claude/java/agents/feature-developer.md +5 -0
- package/presets/claude/java/agents/qa-tester.md +4 -0
- package/presets/claude/java/agents/task-router.md +4 -0
- package/presets/claude/java/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/mcp-ts/agents/build-verifier.md +4 -0
- package/presets/claude/mcp-ts/agents/feature-developer.md +5 -0
- package/presets/claude/mcp-ts/agents/task-router.md +1 -1
- package/presets/claude/mcp-ts/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/next/agents/build-verifier.md +2 -0
- package/presets/claude/next/agents/code-reviewer.md +2 -0
- package/presets/claude/next/agents/debugger.md +4 -0
- package/presets/claude/next/agents/feature-developer.md +3 -0
- package/presets/claude/next/agents/qa-tester.md +4 -0
- package/presets/claude/next/agents/unit-test-generator.md +1 -1
- package/presets/claude/next/agents/unit-test-planner.md +1 -1
- package/presets/claude/next/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/next/rules/api-and-data/http-client.md +1 -1
- package/presets/claude/next/rules/architecture/reference-features.md +1 -1
- package/presets/claude/next/rules/testing/README.md +1 -1
- package/presets/claude/next/rules/testing/tests-e2e-structure.md +2 -0
- package/presets/claude/next/rules/testing/tests-unit.md +3 -5
- package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/next/rules/ui-and-accessibility/react-ui.md +1 -1
- package/presets/claude/next/skills/playwright-e2e/SKILL.md +5 -3
- package/presets/claude/next/skills/unit-testing/SKILL.md +6 -4
- package/presets/claude/nuxt/agents/build-verifier.md +2 -0
- package/presets/claude/nuxt/agents/code-reviewer.md +2 -0
- package/presets/claude/nuxt/agents/debugger.md +4 -0
- package/presets/claude/nuxt/agents/feature-developer.md +3 -0
- package/presets/claude/nuxt/agents/qa-tester.md +4 -0
- package/presets/claude/nuxt/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/php-hexagonal/agents/build-verifier.md +4 -0
- package/presets/claude/php-hexagonal/agents/code-reviewer.md +4 -0
- package/presets/claude/php-hexagonal/agents/debugger.md +4 -0
- package/presets/claude/php-hexagonal/agents/feature-developer.md +5 -0
- package/presets/claude/php-hexagonal/agents/qa-tester.md +4 -0
- package/presets/claude/php-hexagonal/agents/task-router.md +4 -0
- package/presets/claude/php-hexagonal/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/php-laravel/agents/build-verifier.md +4 -0
- package/presets/claude/php-laravel/agents/code-reviewer.md +4 -0
- package/presets/claude/php-laravel/agents/debugger.md +4 -0
- package/presets/claude/php-laravel/agents/feature-developer.md +5 -0
- package/presets/claude/php-laravel/agents/qa-tester.md +4 -0
- package/presets/claude/php-laravel/agents/task-router.md +1 -0
- package/presets/claude/php-laravel/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/claude/svelte/agents/build-verifier.md +2 -0
- package/presets/claude/svelte/agents/code-reviewer.md +2 -0
- package/presets/claude/svelte/agents/debugger.md +4 -0
- package/presets/claude/svelte/agents/feature-developer.md +3 -0
- package/presets/claude/svelte/agents/qa-tester.md +4 -0
- package/presets/claude/svelte/hooks/chain-team-phases.sh +51 -11
- package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +1 -1
- package/presets/cursor/android-kotlin/agents/build-verifier.md +2 -0
- package/presets/cursor/android-kotlin/agents/code-reviewer.md +2 -0
- package/presets/cursor/android-kotlin/agents/debugger.md +1 -0
- package/presets/cursor/android-kotlin/agents/feature-developer.md +3 -0
- package/presets/cursor/android-kotlin/agents/qa-tester.md +2 -0
- package/presets/cursor/android-kotlin/agents/task-router.md +4 -0
- package/presets/cursor/android-kotlin/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/go/agents/build-verifier.md +4 -0
- package/presets/cursor/go/agents/code-reviewer.md +4 -0
- package/presets/cursor/go/agents/debugger.md +4 -0
- package/presets/cursor/go/agents/feature-developer.md +5 -0
- package/presets/cursor/go/agents/qa-tester.md +4 -0
- package/presets/cursor/go/agents/task-router.md +4 -0
- package/presets/cursor/go/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/go/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/ios-swift/agents/build-verifier.md +2 -0
- package/presets/cursor/ios-swift/agents/code-reviewer.md +2 -0
- package/presets/cursor/ios-swift/agents/debugger.md +1 -0
- package/presets/cursor/ios-swift/agents/feature-developer.md +3 -0
- package/presets/cursor/ios-swift/agents/qa-tester.md +2 -0
- package/presets/cursor/ios-swift/agents/task-router.md +4 -0
- package/presets/cursor/ios-swift/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/java/agents/build-verifier.md +4 -0
- package/presets/cursor/java/agents/code-reviewer.md +4 -0
- package/presets/cursor/java/agents/debugger.md +4 -0
- package/presets/cursor/java/agents/feature-developer.md +5 -0
- package/presets/cursor/java/agents/qa-tester.md +4 -0
- package/presets/cursor/java/agents/task-router.md +4 -0
- package/presets/cursor/java/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/java/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/mcp-ts/agents/build-verifier.md +4 -0
- package/presets/cursor/mcp-ts/agents/feature-developer.md +5 -0
- package/presets/cursor/mcp-ts/agents/task-router.md +1 -1
- package/presets/cursor/mcp-ts/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/next/agents/build-verifier.md +2 -0
- package/presets/cursor/next/agents/code-reviewer.md +2 -0
- package/presets/cursor/next/agents/debugger.md +4 -0
- package/presets/cursor/next/agents/feature-developer.md +3 -0
- package/presets/cursor/next/agents/qa-tester.md +4 -0
- package/presets/cursor/next/agents/unit-test-generator.md +1 -1
- package/presets/cursor/next/agents/unit-test-planner.md +1 -1
- package/presets/cursor/next/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/next/rules/http-client.mdc +1 -1
- package/presets/cursor/next/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/next/rules/react-ui.mdc +1 -1
- package/presets/cursor/next/rules/reference-features.mdc +1 -1
- package/presets/cursor/next/rules/tests-e2e-structure.mdc +2 -0
- package/presets/cursor/next/rules/tests-unit.mdc +3 -5
- package/presets/cursor/next/skills/playwright-e2e/SKILL.md +5 -3
- package/presets/cursor/next/skills/unit-testing/SKILL.md +6 -4
- package/presets/cursor/nuxt/agents/build-verifier.md +2 -0
- package/presets/cursor/nuxt/agents/code-reviewer.md +2 -0
- package/presets/cursor/nuxt/agents/debugger.md +4 -0
- package/presets/cursor/nuxt/agents/feature-developer.md +3 -0
- package/presets/cursor/nuxt/agents/qa-tester.md +4 -0
- package/presets/cursor/nuxt/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/php-hexagonal/agents/build-verifier.md +4 -0
- package/presets/cursor/php-hexagonal/agents/code-reviewer.md +4 -0
- package/presets/cursor/php-hexagonal/agents/debugger.md +4 -0
- package/presets/cursor/php-hexagonal/agents/feature-developer.md +5 -0
- package/presets/cursor/php-hexagonal/agents/qa-tester.md +4 -0
- package/presets/cursor/php-hexagonal/agents/task-router.md +4 -0
- package/presets/cursor/php-hexagonal/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/php-laravel/agents/build-verifier.md +4 -0
- package/presets/cursor/php-laravel/agents/code-reviewer.md +4 -0
- package/presets/cursor/php-laravel/agents/debugger.md +4 -0
- package/presets/cursor/php-laravel/agents/feature-developer.md +5 -0
- package/presets/cursor/php-laravel/agents/qa-tester.md +4 -0
- package/presets/cursor/php-laravel/agents/task-router.md +1 -0
- package/presets/cursor/php-laravel/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +1 -1
- package/presets/cursor/svelte/agents/build-verifier.md +2 -0
- package/presets/cursor/svelte/agents/code-reviewer.md +2 -0
- package/presets/cursor/svelte/agents/debugger.md +4 -0
- package/presets/cursor/svelte/agents/feature-developer.md +3 -0
- package/presets/cursor/svelte/agents/qa-tester.md +4 -0
- package/presets/cursor/svelte/hooks/chain-team-phases.sh +51 -11
- package/presets/cursor/svelte/rules/preset-twin-sync.mdc +1 -1
- package/scripts/check-preset-structure.sh +10 -0
- package/scripts/check-preset-token-budget.sh +100 -0
- package/scripts/check-task-router-intents.sh +141 -0
- package/scripts/test-agent-task-metrics-hooks.mjs +99 -1
- package/scripts/test-chain-team-phases-coverage.mjs +100 -1
- package/scripts/test-task-router-intents-fixtures.mjs +109 -0
|
@@ -66,10 +66,20 @@ const STACK = 'php-laravel';
|
|
|
66
66
|
const PLATFORM = 'cursor';
|
|
67
67
|
|
|
68
68
|
function waitForMetricsLock() {
|
|
69
|
+
// OD-1: real advisory lock via create-exclusive (fs.openSync(_, 'wx')).
|
|
70
|
+
// The previous impl spun on Atomics.wait against an unrelated SharedArrayBuffer
|
|
71
|
+
// and never blocked on the lock file, so two concurrent subagentStop events
|
|
72
|
+
// could both acquire the lock and corrupt metrics.json. Retry EEXIST with a
|
|
73
|
+
// 10 ms sleep up to 200 attempts (2 s ceiling). On any other error, throw.
|
|
69
74
|
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
70
|
-
try {
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
try {
|
|
76
|
+
const fd = fs.openSync(METRICS_LOCK, 'wx');
|
|
77
|
+
try { fs.closeSync(fd); } catch {}
|
|
78
|
+
return;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
81
|
+
const until = Date.now() + 10;
|
|
82
|
+
while (Date.now() < until) { /* 10 ms busy-wait */ }
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
throw new Error('metrics lock timeout');
|
|
@@ -78,7 +88,7 @@ let taskLockHeld = false;
|
|
|
78
88
|
function releaseTaskLock() {
|
|
79
89
|
if (!taskLockHeld) return;
|
|
80
90
|
taskLockHeld = false;
|
|
81
|
-
try { fs.
|
|
91
|
+
try { fs.unlinkSync(METRICS_LOCK); } catch {}
|
|
82
92
|
}
|
|
83
93
|
waitForMetricsLock();
|
|
84
94
|
taskLockHeld = true;
|
|
@@ -106,7 +116,7 @@ function mutateMetrics(mutator) {
|
|
|
106
116
|
fs.writeFileSync(temporary, JSON.stringify(metrics, null, 2) + '\n');
|
|
107
117
|
fs.renameSync(temporary, METRICS_PATH);
|
|
108
118
|
} finally {
|
|
109
|
-
if (acquiredHere) { try { fs.
|
|
119
|
+
if (acquiredHere) { try { fs.unlinkSync(METRICS_LOCK); } catch {} }
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
function appendMetric(event) { mutateMetrics((metrics) => metrics.events.push({ eventId: `${metrics.task.runId}:${Date.now()}:${Math.random().toString(16).slice(2)}`, ...event })); }
|
|
@@ -481,7 +491,19 @@ function legacyFlow() {
|
|
|
481
491
|
|
|
482
492
|
|
|
483
493
|
if (hookInput.action === 'end_human_gate') {
|
|
484
|
-
|
|
494
|
+
status.awaitingHumanGate = false;
|
|
495
|
+
// A2 (retro §8): pin a single runId per pipeline so subsequent events don't
|
|
496
|
+
// fragment the metrics ledger across multiple runId sequences. Prefer
|
|
497
|
+
// metrics.json.task.runId when metrics already exist, then status.metricsRunId,
|
|
498
|
+
// then mint a fresh seed only on first ever emit.
|
|
499
|
+
try {
|
|
500
|
+
if (fs.existsSync(METRICS_PATH)) {
|
|
501
|
+
const existing = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
|
|
502
|
+
if (existing && existing.task && typeof existing.task.runId === 'string') {
|
|
503
|
+
status.metricsRunId = existing.task.runId;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} catch {}
|
|
485
507
|
if (status.state === 'awaiting_approval') {
|
|
486
508
|
status.state = 'in_progress';
|
|
487
509
|
status.phase = status.phase === 'human_gate' ? 'executing' : status.phase;
|
|
@@ -520,11 +542,29 @@ if (hookInput.action === 'end_human_gate') {
|
|
|
520
542
|
}
|
|
521
543
|
}
|
|
522
544
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
545
|
+
// A2 (retro §8): if gate closes the final human gate of the pipeline
|
|
546
|
+
// (no resume happens, or current step's gate was the last), surface
|
|
547
|
+
// state="awaiting_approval" so a subsequent /task-continue doesn't misinterpret
|
|
548
|
+
// the post-gate idle state as "still in progress".
|
|
549
|
+
{
|
|
550
|
+
let gateWasTerminal = false;
|
|
551
|
+
if (!hookInput.activateNext && fs.existsSync(pipelinePath)) {
|
|
552
|
+
try {
|
|
553
|
+
const pipe = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
|
|
554
|
+
const steps = pipe.steps || [];
|
|
555
|
+
const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
|
|
556
|
+
const cur = steps[idx];
|
|
557
|
+
const gates = Array.isArray(pipe.humanGates) ? pipe.humanGates : [];
|
|
558
|
+
const hasGate = (step) => Array.isArray(gates) && gates.some((g) => typeof g === 'string' && getStepAgents(step).some((a) => g === `after:${a}`));
|
|
559
|
+
if (cur && hasGate(cur)) gateWasTerminal = true;
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
writeStatus({
|
|
563
|
+
awaitingHumanGate: false,
|
|
564
|
+
state: gateWasTerminal ? 'awaiting_approval' : status.state,
|
|
565
|
+
phase: status.phase,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
528
568
|
process.stdout.write(JSON.stringify({ ok: true, action: 'end_human_gate', activeHumanGateEventId: status.activeHumanGateEventId || null }));
|
|
529
569
|
process.exit(0);
|
|
530
570
|
}
|
|
@@ -40,7 +40,7 @@ Cursor `.mdc` — **SoT per stack**. Claude topic `.md` — derived twin.
|
|
|
40
40
|
- **Domain** rules: ≥15 body lines (FAIL if thin without documented alias reason).
|
|
41
41
|
- **Thin aliases** (`agent-team-intake`, `technical-retro`): may be shorter (soft) if they only point to a command/skill.
|
|
42
42
|
- **Slim-companion rules** (UI-edit bundle, slim app-cores, slim tooling): may be shorter than 15 lines **if and only if** the body is a pointer into shared-core / stack-prose (no standalone prose). Examples live in stack `rules/README.md` (e.g. `cursor/next/rules/README.md` "UI-edit bundle" intent).
|
|
43
|
-
-
|
|
43
|
+
- Mechanical enforcement lives in `packages/ai-rules/scripts/check-preset-token-budget.sh` — see `SLIM_COMPANION_ALLOWLIST` (the 53 stems accepted as slim pointers) and `MIN_SLIM_BODY_LINES` (5-line floor). Slim-companion rules with a body of 6–14 lines whose stem is **not** in the allowlist are reported as slim violations by the script.
|
|
44
44
|
|
|
45
45
|
## Severity (build-verifier)
|
|
46
46
|
|
|
@@ -97,6 +97,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **FAI
|
|
|
97
97
|
|
|
98
98
|
Do not fix code — report only. Do not advance to code-reviewer until PASS.
|
|
99
99
|
|
|
100
|
+
**Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
|
|
101
|
+
|
|
100
102
|
## Artifact contract
|
|
101
103
|
|
|
102
104
|
- **Authoritative inputs:** read `.cursor/team/tasks/<slug>/artifact-manifest.json` first; consume handoff `artifactInputIds` when supplied, otherwise consume the manifest entries for brief/decomposition, implementation evidence, pipeline scope, and changed files.
|
|
@@ -55,6 +55,8 @@ Write the owned terminal receipt described under **Artifact contract**. If **REQ
|
|
|
55
55
|
|
|
56
56
|
Do not mix this output with retrospective facilitation — keep review and retro separate.
|
|
57
57
|
|
|
58
|
+
**Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
|
|
59
|
+
|
|
58
60
|
## Design guidance
|
|
59
61
|
|
|
60
62
|
- Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
|
|
@@ -81,3 +81,7 @@ Do not perform formal code review or write e2e plans — those are separate agen
|
|
|
81
81
|
|
|
82
82
|
- When writing or changing code, load / follow rule `anti-sycophancy-discipline`.
|
|
83
83
|
- When acting on review feedback (`changes_requested` / human comments): verify each item against the codebase before implementing; no performative agreement.
|
|
84
|
+
|
|
85
|
+
## Receipt binding
|
|
86
|
+
|
|
87
|
+
> **Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
|
|
@@ -13,6 +13,7 @@ You are a senior frontend developer working in a SvelteKit monorepo with strict
|
|
|
13
13
|
2. Read `status.json` — proceed if `in_progress`, `approved`, or `retryAfterFix`.
|
|
14
14
|
3. Read `pipeline.json` for step context and scope.
|
|
15
15
|
4. Follow the **`feature-delivery` skill** (layer order, reference features, validation handoff).
|
|
16
|
+
5. **Verify carry-over at HEAD before applying** (refactor intent only). When the brief comes from a prior review's carry-over findings list, for each item run `git show HEAD:<path>` or `grep` to confirm the finding still applies at the current HEAD; if the file already contains the fix (carry-over was closed in a prior uncommitted batch), mark it as **NO-OP at HEAD** in `implementation.md` and skip the edit; if the file is unchanged, apply the fix and cite file:line. Target ≤2 NO-OP items per refactor pipeline.
|
|
16
17
|
|
|
17
18
|
## Task execution rules
|
|
18
19
|
|
|
@@ -40,6 +41,8 @@ Emit the owned implementation receipt with outcome `completed`. Handoff: tasks d
|
|
|
40
41
|
|
|
41
42
|
Do not perform formal code review — that is the code-reviewer subagent's job.
|
|
42
43
|
|
|
44
|
+
**Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
|
|
45
|
+
|
|
43
46
|
## Design guidance
|
|
44
47
|
|
|
45
48
|
- Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
|
|
@@ -53,3 +53,7 @@ Provide summary:
|
|
|
53
53
|
## Design guidance
|
|
54
54
|
|
|
55
55
|
- Load `design-guidance` (rule stem) when assessing structure, smells, or pattern fit.
|
|
56
|
+
|
|
57
|
+
## Receipt binding
|
|
58
|
+
|
|
59
|
+
> **Receipt binding.** When writing the terminal receipt to `artifact-manifest.json`, set `entry.receipt.attemptId = <attemptId supplied by the orchestrator>` (the orchestrator provides this via `attemptId` on invocation). The hook at `chain-team-phases.sh:readBoundReceiptOutcome` rejects entries whose `attemptId` does not match the current attempt, or whose `Date.parse(timestamps.completedAt) < Date.parse(attemptStartedAt)`. A missing or stale attemptId causes `awaiting_artifact_receipt` stalls. <!-- shared-core: receipt-binding -->
|
|
@@ -64,10 +64,20 @@ const STACK = 'svelte';
|
|
|
64
64
|
const PLATFORM = 'cursor';
|
|
65
65
|
|
|
66
66
|
function waitForMetricsLock() {
|
|
67
|
+
// OD-1: real advisory lock via create-exclusive (fs.openSync(_, 'wx')).
|
|
68
|
+
// The previous impl spun on Atomics.wait against an unrelated SharedArrayBuffer
|
|
69
|
+
// and never blocked on the lock file, so two concurrent subagentStop events
|
|
70
|
+
// could both acquire the lock and corrupt metrics.json. Retry EEXIST with a
|
|
71
|
+
// 10 ms sleep up to 200 attempts (2 s ceiling). On any other error, throw.
|
|
67
72
|
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
68
|
-
try {
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
try {
|
|
74
|
+
const fd = fs.openSync(METRICS_LOCK, 'wx');
|
|
75
|
+
try { fs.closeSync(fd); } catch {}
|
|
76
|
+
return;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
79
|
+
const until = Date.now() + 10;
|
|
80
|
+
while (Date.now() < until) { /* 10 ms busy-wait */ }
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
throw new Error('metrics lock timeout');
|
|
@@ -76,7 +86,7 @@ let taskLockHeld = false;
|
|
|
76
86
|
function releaseTaskLock() {
|
|
77
87
|
if (!taskLockHeld) return;
|
|
78
88
|
taskLockHeld = false;
|
|
79
|
-
try { fs.
|
|
89
|
+
try { fs.unlinkSync(METRICS_LOCK); } catch {}
|
|
80
90
|
}
|
|
81
91
|
waitForMetricsLock();
|
|
82
92
|
taskLockHeld = true;
|
|
@@ -104,7 +114,7 @@ function mutateMetrics(mutator) {
|
|
|
104
114
|
fs.writeFileSync(temporary, JSON.stringify(metrics, null, 2) + '\n');
|
|
105
115
|
fs.renameSync(temporary, METRICS_PATH);
|
|
106
116
|
} finally {
|
|
107
|
-
if (acquiredHere) { try { fs.
|
|
117
|
+
if (acquiredHere) { try { fs.unlinkSync(METRICS_LOCK); } catch {} }
|
|
108
118
|
}
|
|
109
119
|
}
|
|
110
120
|
function appendMetric(event) { mutateMetrics((metrics) => metrics.events.push({ eventId: `${metrics.task.runId}:${Date.now()}:${Math.random().toString(16).slice(2)}`, ...event })); }
|
|
@@ -485,7 +495,19 @@ function legacyFlow() {
|
|
|
485
495
|
|
|
486
496
|
|
|
487
497
|
if (hookInput.action === 'end_human_gate') {
|
|
488
|
-
|
|
498
|
+
status.awaitingHumanGate = false;
|
|
499
|
+
// A2 (retro §8): pin a single runId per pipeline so subsequent events don't
|
|
500
|
+
// fragment the metrics ledger across multiple runId sequences. Prefer
|
|
501
|
+
// metrics.json.task.runId when metrics already exist, then status.metricsRunId,
|
|
502
|
+
// then mint a fresh seed only on first ever emit.
|
|
503
|
+
try {
|
|
504
|
+
if (fs.existsSync(METRICS_PATH)) {
|
|
505
|
+
const existing = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
|
|
506
|
+
if (existing && existing.task && typeof existing.task.runId === 'string') {
|
|
507
|
+
status.metricsRunId = existing.task.runId;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch {}
|
|
489
511
|
if (status.state === 'awaiting_approval') {
|
|
490
512
|
status.state = 'in_progress';
|
|
491
513
|
status.phase = status.phase === 'human_gate' ? 'executing' : status.phase;
|
|
@@ -524,11 +546,29 @@ if (hookInput.action === 'end_human_gate') {
|
|
|
524
546
|
}
|
|
525
547
|
}
|
|
526
548
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
549
|
+
// A2 (retro §8): if gate closes the final human gate of the pipeline
|
|
550
|
+
// (no resume happens, or current step's gate was the last), surface
|
|
551
|
+
// state="awaiting_approval" so a subsequent /task-continue doesn't misinterpret
|
|
552
|
+
// the post-gate idle state as "still in progress".
|
|
553
|
+
{
|
|
554
|
+
let gateWasTerminal = false;
|
|
555
|
+
if (!hookInput.activateNext && fs.existsSync(pipelinePath)) {
|
|
556
|
+
try {
|
|
557
|
+
const pipe = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
|
|
558
|
+
const steps = pipe.steps || [];
|
|
559
|
+
const idx = typeof status.pipelineIndex === 'number' ? status.pipelineIndex : 0;
|
|
560
|
+
const cur = steps[idx];
|
|
561
|
+
const gates = Array.isArray(pipe.humanGates) ? pipe.humanGates : [];
|
|
562
|
+
const hasGate = (step) => Array.isArray(gates) && gates.some((g) => typeof g === 'string' && getStepAgents(step).some((a) => g === `after:${a}`));
|
|
563
|
+
if (cur && hasGate(cur)) gateWasTerminal = true;
|
|
564
|
+
} catch {}
|
|
565
|
+
}
|
|
566
|
+
writeStatus({
|
|
567
|
+
awaitingHumanGate: false,
|
|
568
|
+
state: gateWasTerminal ? 'awaiting_approval' : status.state,
|
|
569
|
+
phase: status.phase,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
532
572
|
process.stdout.write(JSON.stringify({ ok: true, action: 'end_human_gate', activeHumanGateEventId: status.activeHumanGateEventId || null }));
|
|
533
573
|
process.exit(0);
|
|
534
574
|
}
|
|
@@ -40,7 +40,7 @@ Cursor `.mdc` — **SoT per stack**. Claude topic `.md` — derived twin.
|
|
|
40
40
|
- **Domain** rules: ≥15 body lines (FAIL if thin without documented alias reason).
|
|
41
41
|
- **Thin aliases** (`agent-team-intake`, `technical-retro`): may be shorter (soft) if they only point to a command/skill.
|
|
42
42
|
- **Slim-companion rules** (UI-edit bundle, slim app-cores, slim tooling): may be shorter than 15 lines **if and only if** the body is a pointer into shared-core / stack-prose (no standalone prose). Examples live in stack `rules/README.md` (e.g. `cursor/next/rules/README.md` "UI-edit bundle" intent).
|
|
43
|
-
-
|
|
43
|
+
- Mechanical enforcement lives in `packages/ai-rules/scripts/check-preset-token-budget.sh` — see `SLIM_COMPANION_ALLOWLIST` (the 53 stems accepted as slim pointers) and `MIN_SLIM_BODY_LINES` (5-line floor). Slim-companion rules with a body of 6–14 lines whose stem is **not** in the allowlist are reported as slim violations by the script.
|
|
44
44
|
|
|
45
45
|
## Severity (build-verifier)
|
|
46
46
|
|
|
@@ -36,6 +36,7 @@ run_gate "hook-only-lifecycle" "$SCRIPT_DIR/check-hook-only-lifecycle.sh"
|
|
|
36
36
|
run_gate "local-overlay" "$SCRIPT_DIR/check-local-overlay.sh"
|
|
37
37
|
run_gate "shared-core-drift" "$SCRIPT_DIR/check-shared-core-drift.sh" AI_RULES_DRIFT_FAIL=1
|
|
38
38
|
run_gate "chain-hook-coverage" "$SCRIPT_DIR/check-chain-hook-coverage.sh"
|
|
39
|
+
run_gate "task-router-intents" "$SCRIPT_DIR/check-task-router-intents.sh"
|
|
39
40
|
run_gate "i18n" "$SCRIPT_DIR/check-preset-i18n.sh" AI_RULES_I18N_FAIL=1 AI_RULES_I18N_SOFT=0
|
|
40
41
|
|
|
41
42
|
echo ">>> cli-mcp-merge"
|
|
@@ -65,6 +66,15 @@ else
|
|
|
65
66
|
fi
|
|
66
67
|
echo
|
|
67
68
|
|
|
69
|
+
echo ">>> task-router-intents fixtures"
|
|
70
|
+
if node "$SCRIPT_DIR/test-task-router-intents-fixtures.mjs"; then
|
|
71
|
+
echo "<<< task-router-intents fixtures: OK"
|
|
72
|
+
else
|
|
73
|
+
echo "<<< task-router-intents fixtures: FAIL"
|
|
74
|
+
status=1
|
|
75
|
+
fi
|
|
76
|
+
echo
|
|
77
|
+
|
|
68
78
|
# Soft WARN: dogfood product-specs (never fails the orchestrator).
|
|
69
79
|
# Default check-product-specs exits 0 even when issues are found unless --strict.
|
|
70
80
|
# Use --strict here only to surface problems as WARN in logs; do not flip status.
|
|
@@ -18,6 +18,69 @@ presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
|
|
|
18
18
|
STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
|
|
19
19
|
BODY_CAP = 140 # applied to 3-rule trios (Claude session-start, Cursor trio when len==3)
|
|
20
20
|
BODY_CAP_CURSOR_4 = 170 # Cursor now allows 4 always-on (next-app-core, package-manager, code-quality, ai-local-overlay)
|
|
21
|
+
# Slim-companion rules (UI-edit bundle, slim app-cores, slim tooling) may be
|
|
22
|
+
# shorter than 15 lines **if and only if** their stem is in the allow-list and
|
|
23
|
+
# body is <= MIN_SLIM_BODY_LINES. A slim rule whose body grows past this cap
|
|
24
|
+
# without being promoted to a domain rule FAILs the check. Reference:
|
|
25
|
+
# presets/_shared/core/meta/preset-twin-sync.md "Twin depth" section.
|
|
26
|
+
# Cursor and Claude share stems by convention; the list is stem-only and
|
|
27
|
+
# applied per platform.
|
|
28
|
+
SLIM_COMPANION_ALLOWLIST = frozenset({
|
|
29
|
+
"agent-team-intake",
|
|
30
|
+
"agent-team-orchestrator",
|
|
31
|
+
"ai-local-overlay",
|
|
32
|
+
"api-grpc",
|
|
33
|
+
"architecture-boundaries-ui",
|
|
34
|
+
"css-property-order-stylelint",
|
|
35
|
+
"design-guidance",
|
|
36
|
+
"domain-layer",
|
|
37
|
+
"e2e",
|
|
38
|
+
"feature-delivery-workflow",
|
|
39
|
+
"http-client",
|
|
40
|
+
"integration",
|
|
41
|
+
"ios-app-core",
|
|
42
|
+
"mcp-usage",
|
|
43
|
+
"messaging-adapters",
|
|
44
|
+
"mockup-review",
|
|
45
|
+
"module-public-api",
|
|
46
|
+
"navigation-coordinators",
|
|
47
|
+
"navigation-router-stack",
|
|
48
|
+
"navigation",
|
|
49
|
+
"navigation-router-ui",
|
|
50
|
+
"next-app-router",
|
|
51
|
+
"no-props-spread",
|
|
52
|
+
"no-type-assertion-as-import-export",
|
|
53
|
+
"no-v-bind-spread",
|
|
54
|
+
"package-manager",
|
|
55
|
+
"persistence",
|
|
56
|
+
"persistence-data",
|
|
57
|
+
"playwright-agents",
|
|
58
|
+
"post-change-build",
|
|
59
|
+
"post-change-lint",
|
|
60
|
+
"post-change-test",
|
|
61
|
+
"preset-layering",
|
|
62
|
+
"preset-no-cross-stack-leakage",
|
|
63
|
+
"preset-pr-checklist",
|
|
64
|
+
"preset-token-budget",
|
|
65
|
+
"preset-twin-sync",
|
|
66
|
+
"react-a11y-coding",
|
|
67
|
+
"script-setup-conventions",
|
|
68
|
+
"security-android",
|
|
69
|
+
"security-go",
|
|
70
|
+
"security-ios",
|
|
71
|
+
"security-next",
|
|
72
|
+
"security-php",
|
|
73
|
+
"state-and-viewmodels",
|
|
74
|
+
"technical-retro",
|
|
75
|
+
"tests-e2e",
|
|
76
|
+
"tests-integration",
|
|
77
|
+
"tests-unit",
|
|
78
|
+
"unit",
|
|
79
|
+
"ux-design",
|
|
80
|
+
"vue-a11y-coding",
|
|
81
|
+
"xcode-tooling",
|
|
82
|
+
})
|
|
83
|
+
MIN_SLIM_BODY_LINES = 5
|
|
21
84
|
MARKER_LINE_RE = re.compile(r"^<!--\s*shared-core:\s*[^>]+?\s*-->\s*$")
|
|
22
85
|
ALWAYS_APPLY_RE = re.compile(r"(?m)^alwaysApply:\s*true\s*$")
|
|
23
86
|
PATHS_RE = re.compile(r"(?m)^paths:\s*")
|
|
@@ -138,6 +201,43 @@ for stack in STACKS:
|
|
|
138
201
|
)
|
|
139
202
|
failed += 1
|
|
140
203
|
|
|
204
|
+
# --- Slim-companion enforcement (per stack, both platforms) ---
|
|
205
|
+
# All .mdc (Cursor) / .md (Claude) rules under the stack's rules/ directory
|
|
206
|
+
# are scanned. A rule whose body is > MIN_SLIM_BODY_LINES AND whose stem
|
|
207
|
+
# is NOT in SLIM_COMPANION_ALLOWLIST must be promoted to a domain rule
|
|
208
|
+
# (>=15 body lines per preset-twin-sync.md "Twin depth"). FAIL otherwise.
|
|
209
|
+
slim_violations: list[str] = []
|
|
210
|
+
for rules_dir in (cursor_rules, claude_rules):
|
|
211
|
+
if not rules_dir.exists():
|
|
212
|
+
continue
|
|
213
|
+
pattern = "*.mdc" if rules_dir is cursor_rules else "*.md"
|
|
214
|
+
for rule_path in sorted(rules_dir.rglob(pattern)):
|
|
215
|
+
if rule_path.name == "README.md":
|
|
216
|
+
continue
|
|
217
|
+
text = rule_path.read_text(encoding="utf-8")
|
|
218
|
+
_, body = strip_frontmatter(text)
|
|
219
|
+
n = body_nonempty_lines(body)
|
|
220
|
+
if n <= MIN_SLIM_BODY_LINES:
|
|
221
|
+
continue
|
|
222
|
+
# Skip rules explicitly in the slim-companion allow-list (they are
|
|
223
|
+
# allowed to be short pointers regardless of body length).
|
|
224
|
+
if rule_path.stem in SLIM_COMPANION_ALLOWLIST:
|
|
225
|
+
continue
|
|
226
|
+
# Domain rules (>=15 lines) are fine; we only flag the 6..14 line
|
|
227
|
+
# "slim-but-not-allowed" bucket.
|
|
228
|
+
if n >= 15:
|
|
229
|
+
continue
|
|
230
|
+
slim_violations.append(
|
|
231
|
+
f"{rule_path.relative_to(presets).as_posix()} ({n} lines, slim but not in allow-list)"
|
|
232
|
+
)
|
|
233
|
+
if slim_violations:
|
|
234
|
+
print(f" Slim-companion allow-list violations ({len(slim_violations)}):")
|
|
235
|
+
for v in slim_violations[:10]:
|
|
236
|
+
print(f" FAIL: {v}")
|
|
237
|
+
if len(slim_violations) > 10:
|
|
238
|
+
print(f" ... and {len(slim_violations) - 10} more")
|
|
239
|
+
failed += len(slim_violations)
|
|
240
|
+
|
|
141
241
|
print()
|
|
142
242
|
|
|
143
243
|
if failed:
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cross-check `task-router.md` intent rows against `agents/*.md` `name:` frontmatter.
|
|
3
|
+
#
|
|
4
|
+
# Two-sided verification:
|
|
5
|
+
# 1. Every agent file (frontmatter `name:` other than `task-router` itself) must
|
|
6
|
+
# be referenced somewhere in the stack's `task-router.md` so the router knows
|
|
7
|
+
# how to invoke it. Missing mention ⇒ FAIL (the agent will silently never
|
|
8
|
+
# receive a handoff).
|
|
9
|
+
# 2. Every agent stem referenced in `task-router.md` (in the `Default steps`
|
|
10
|
+
# column, the `Model tiers` table, or other inline mentions) must have a
|
|
11
|
+
# corresponding `agents/<stem>.md` file. Missing agent ⇒ FAIL (the router
|
|
12
|
+
# emits an intent that no agent can satisfy).
|
|
13
|
+
#
|
|
14
|
+
# The check is per `(platform, stack)` pair and runs all 20 ships (cursor + claude
|
|
15
|
+
# × 10 stacks). Exits non-zero on any FAIL.
|
|
16
|
+
#
|
|
17
|
+
# Stack-specific exemptions:
|
|
18
|
+
# - mcp-ts does not ship `playwright-test-*`, `xcuitest-test-*`, `unit-test-*`
|
|
19
|
+
# in the standard mix; intent rows that reference those agents are flagged
|
|
20
|
+
# only on stacks that ship the corresponding `agents/<stem>.md` file.
|
|
21
|
+
# - Stack leakage (positive agent stems on the wrong stack) is enforced in
|
|
22
|
+
# `check-preset-leakage.sh`; this check covers task-router sync only.
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
26
|
+
# Allow callers (notably the fixture test) to redirect the presets root via
|
|
27
|
+
# the env var. Default to the real repo path.
|
|
28
|
+
export AI_RULES_PRESETS_ROOT="${AI_RULES_PRESETS_ROOT:-$ROOT/packages/ai-rules/presets}"
|
|
29
|
+
|
|
30
|
+
python3 <<'PY'
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
|
|
39
|
+
PLATFORMS = ("cursor", "claude")
|
|
40
|
+
STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
|
|
41
|
+
|
|
42
|
+
# Agents that are shipped for orchestration purposes but do not need to appear
|
|
43
|
+
# in task-router.md intent rows (router-only / analyzer-only / orchestrator-only).
|
|
44
|
+
ROUTER_EXEMPT_FROM_INTENT = frozenset({
|
|
45
|
+
"task-router", # this file
|
|
46
|
+
"codebase-analyzer", # ad-hoc explain-as-is agent
|
|
47
|
+
"agent-team-intake", # one-off intake, not a pipeline target
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
# Agent-stem regex: kebab-case, lowercase, may include digits and dots (x.y).
|
|
51
|
+
# Two patterns: (a) hyphen/dot-bounded names like `feature-developer`, `unit.test`,
|
|
52
|
+
# (b) single-word agents that are present in `shipped_names`. Both feed the
|
|
53
|
+
# `agent_like` filter.
|
|
54
|
+
AGENT_NAME_RE = re.compile(r"\b([a-z][a-z0-9]*(?:[-.][a-z0-9]+)+)\b")
|
|
55
|
+
SINGLE_WORD_AGENT_RE = re.compile(r"\b([a-z][a-z0-9]+)\b")
|
|
56
|
+
FRONTMATTER_NAME_RE = re.compile(r"^name:\s*([a-z][a-z0-9]*(?:[-.][a-z0-9]+)+)\s*$", re.MULTILINE)
|
|
57
|
+
|
|
58
|
+
failures: list[str] = []
|
|
59
|
+
checks = 0
|
|
60
|
+
|
|
61
|
+
for platform in PLATFORMS:
|
|
62
|
+
for stack in STACKS:
|
|
63
|
+
agents_dir = presets / platform / stack / "agents"
|
|
64
|
+
if not agents_dir.is_dir():
|
|
65
|
+
continue
|
|
66
|
+
task_router = agents_dir / "task-router.md"
|
|
67
|
+
if not task_router.is_file():
|
|
68
|
+
failures.append(f"{platform}/{stack}: missing task-router.md")
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# 1) All agent `name:` frontmatter in this stack's agents dir.
|
|
72
|
+
shipped_names: set[str] = set()
|
|
73
|
+
for path in sorted(agents_dir.glob("*.md")):
|
|
74
|
+
text = path.read_text(encoding="utf-8")
|
|
75
|
+
m = FRONTMATTER_NAME_RE.search(text)
|
|
76
|
+
if m:
|
|
77
|
+
shipped_names.add(m.group(1))
|
|
78
|
+
# Also pick up the file stem as a fallback (in case frontmatter is missing).
|
|
79
|
+
if path.stem not in shipped_names and path.stem != "README":
|
|
80
|
+
shipped_names.add(path.stem)
|
|
81
|
+
|
|
82
|
+
# 2) All agent stems referenced in task-router.md (anywhere in body).
|
|
83
|
+
router_text = task_router.read_text(encoding="utf-8")
|
|
84
|
+
# Restrict to body (drop YAML frontmatter).
|
|
85
|
+
body = router_text
|
|
86
|
+
if router_text.startswith("---\n"):
|
|
87
|
+
end = router_text.find("\n---\n", 4)
|
|
88
|
+
if end >= 0:
|
|
89
|
+
body = router_text[end + 5 :]
|
|
90
|
+
referenced_in_router: set[str] = set(AGENT_NAME_RE.findall(body))
|
|
91
|
+
# Single-word tokens that match a shipped agent (e.g. `debugger`,
|
|
92
|
+
# `codebase-analyzer`, `ci-investigator`) are valid router mentions even
|
|
93
|
+
# without a hyphen/dot boundary.
|
|
94
|
+
for token in SINGLE_WORD_AGENT_RE.findall(body):
|
|
95
|
+
if token in shipped_names:
|
|
96
|
+
referenced_in_router.add(token)
|
|
97
|
+
agent_like: set[str] = referenced_in_router - ROUTER_EXEMPT_FROM_INTENT
|
|
98
|
+
|
|
99
|
+
# Subset of shipped agents that are not router-exempt AND not referenced.
|
|
100
|
+
shipped_pipeline_agents = shipped_names - ROUTER_EXEMPT_FROM_INTENT
|
|
101
|
+
# 1-sided check (FAIL when shipped but not mentioned).
|
|
102
|
+
not_referenced = sorted(shipped_pipeline_agents - agent_like)
|
|
103
|
+
for name in not_referenced:
|
|
104
|
+
checks += 1
|
|
105
|
+
# The README is not an agent.
|
|
106
|
+
if name == "README":
|
|
107
|
+
continue
|
|
108
|
+
# Skip agents only used as commands or skills (e.g. technical-retro,
|
|
109
|
+
# agent-team-intake when still present). These are explicitly
|
|
110
|
+
# exempt — but we already filter ROUTER_EXEMPT_FROM_INTENT, so any
|
|
111
|
+
# remaining mismatch is a real gap.
|
|
112
|
+
failures.append(
|
|
113
|
+
f"{platform}/{stack}: ships agent '{name}.md' but task-router.md does not mention '{name}'"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 2-sided check (FAIL when mentioned in router but not shipped).
|
|
117
|
+
not_shipped = sorted(agent_like - shipped_names)
|
|
118
|
+
for name in not_shipped:
|
|
119
|
+
checks += 1
|
|
120
|
+
# Allow mentions of common stacks/tools that aren't agents (filter).
|
|
121
|
+
# Common false positives: branch names, mixed-case strings, etc.
|
|
122
|
+
# The regex already constrains to lowercase kebab/dot; further filter
|
|
123
|
+
# by requiring the suffix to look like an agent suffix.
|
|
124
|
+
if name in {"feature-developer", "build-verifier", "code-reviewer"}:
|
|
125
|
+
# These should be shipped; surface as FAIL.
|
|
126
|
+
failures.append(
|
|
127
|
+
f"{platform}/{stack}: task-router.md references '{name}' but agents/{name}.md is missing"
|
|
128
|
+
)
|
|
129
|
+
# Otherwise ignore (likely prose token or matcher noise).
|
|
130
|
+
|
|
131
|
+
if failures:
|
|
132
|
+
print(f"FAIL: task-router vs agents drift ({len(failures)} issue(s) / {checks} checks):")
|
|
133
|
+
for f in failures[:30]:
|
|
134
|
+
print(f" - {f}")
|
|
135
|
+
if len(failures) > 30:
|
|
136
|
+
print(f" ... and {len(failures) - 30} more")
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
|
|
139
|
+
print(f"OK: task-router.md intent rows match agents/*.md ({checks} checks across {len(PLATFORMS) * len(STACKS)} (platform, stack) pairs)")
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
PY
|