@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
|
@@ -892,4 +892,102 @@ for (const hook of hooks) {
|
|
|
892
892
|
}
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
-
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
// Test — OD-1: parallel-stop lock race. The hook's waitForMetricsLock must
|
|
897
|
+
// actually block a second writer when the lock file is held. We pre-acquire
|
|
898
|
+
// `metrics.json.lock` (fs.openSync(_, 'wx') — same primitive the hook uses)
|
|
899
|
+
// in the test, then invoke the hook concurrently; the hook must NOT acquire
|
|
900
|
+
// the lock until the test releases it. Without the real lock primitive the
|
|
901
|
+
// hook would race through, run the metrics write, and leave a corrupt ledger.
|
|
902
|
+
// ---------------------------------------------------------------------------
|
|
903
|
+
for (const hook of hooks) {
|
|
904
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.agent-task-metrics-lock-race-'));
|
|
905
|
+
try {
|
|
906
|
+
const platform = hook.includes(`${path.sep}cursor${path.sep}`) ? 'cursor' : 'claude';
|
|
907
|
+
const runId = 'lock-race-run';
|
|
908
|
+
const attemptId = `${runId}:step-0:feature-developer:attempt-1`;
|
|
909
|
+
const taskDirectory = writeTask(
|
|
910
|
+
root,
|
|
911
|
+
hook,
|
|
912
|
+
'lock-race',
|
|
913
|
+
{ autoChain: true, steps: [{ agent: 'feature-developer' }] },
|
|
914
|
+
{
|
|
915
|
+
slug: 'lock-race',
|
|
916
|
+
intent: 'feature',
|
|
917
|
+
pipelineIndex: 0,
|
|
918
|
+
currentAgent: 'feature-developer',
|
|
919
|
+
phase: 'executing',
|
|
920
|
+
state: 'completed',
|
|
921
|
+
activeAttempts: {
|
|
922
|
+
[attemptId]: {
|
|
923
|
+
agent: 'feature-developer',
|
|
924
|
+
stepIndex: 0,
|
|
925
|
+
attempt: 1,
|
|
926
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
927
|
+
parallelGroupId: null,
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
metricsRunId: runId,
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
schemaVersion: 1,
|
|
934
|
+
task: { runId, slug: 'lock-race', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'test' },
|
|
935
|
+
events: [
|
|
936
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: '2026-01-01T00:00:00.000Z' },
|
|
937
|
+
{ eventId: `${runId}:start`, type: 'attempt_started', attemptId, agent: 'feature-developer', stepIndex: 0, attempt: 1, at: '2026-01-01T00:00:00.000Z' },
|
|
938
|
+
],
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
const metricsPath = path.join(taskDirectory, 'metrics.json');
|
|
942
|
+
const lockPath = `${metricsPath}.lock`;
|
|
943
|
+
|
|
944
|
+
// Pre-acquire the lock the same way the hook does (fs.openSync 'wx').
|
|
945
|
+
const holderFd = fs.openSync(lockPath, 'wx');
|
|
946
|
+
|
|
947
|
+
// Spawn the hook while the lock is held. The hook must wait.
|
|
948
|
+
const child = spawn('bash', [hook], { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
949
|
+
let stderr = '';
|
|
950
|
+
let stdout = '';
|
|
951
|
+
child.stderr.setEncoding('utf8');
|
|
952
|
+
child.stdout.setEncoding('utf8');
|
|
953
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
954
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
955
|
+
child.stdin.end(JSON.stringify({ status: 'completed', attemptId }));
|
|
956
|
+
|
|
957
|
+
// Give the child time to enter waitForMetricsLock and hit the EEXIST retry path.
|
|
958
|
+
const holdMs = 60;
|
|
959
|
+
await new Promise((resolve) => setTimeout(resolve, holdMs));
|
|
960
|
+
assert.ok(
|
|
961
|
+
child.exitCode === null,
|
|
962
|
+
`${hook}: hook exited prematurely while lock was held (stdout=${stdout.slice(0, 200)} stderr=${stderr.slice(0, 200)})`,
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Release the lock — child should now proceed.
|
|
966
|
+
const releaseAt = Date.now();
|
|
967
|
+
fs.closeSync(holderFd);
|
|
968
|
+
fs.unlinkSync(lockPath);
|
|
969
|
+
|
|
970
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
971
|
+
child.on('error', reject);
|
|
972
|
+
child.on('close', resolve);
|
|
973
|
+
});
|
|
974
|
+
const elapsed = Date.now() - releaseAt;
|
|
975
|
+
assert.equal(exitCode, 0, `${hook}: hook did not exit 0 after lock release (stderr=${stderr.slice(0, 400)})`);
|
|
976
|
+
|
|
977
|
+
// The hook's waitForMetricsLock busy-waits on EEXIST in 10 ms slices; with a
|
|
978
|
+
// 60 ms hold the child should have been blocked for >= holdMs and run to
|
|
979
|
+
// completion in well under the 2 s ceiling. Both assertions guard against
|
|
980
|
+
// regressions to the fake Atomics.wait impl (which never blocked).
|
|
981
|
+
assert.ok(elapsed >= 1, `${hook}: hook finished suspiciously fast after lock release (${elapsed} ms)`);
|
|
982
|
+
assert.ok(elapsed < 1500, `${hook}: hook took ${elapsed} ms after lock release — lock impl may be stuck`);
|
|
983
|
+
const ledger = readJson(metricsPath);
|
|
984
|
+
const finished = ledger.events.find(
|
|
985
|
+
(event) => event.type === 'attempt_finished' && event.attemptId === attemptId,
|
|
986
|
+
);
|
|
987
|
+
assert.ok(finished, `${hook}: lock-race did not emit attempt_finished; ledger=${JSON.stringify(ledger.events.map((e) => e.type))}`);
|
|
988
|
+
} finally {
|
|
989
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
console.log(`Verified retry, parallel, skip, gate, manifest-reconcile, lock-race, and aggregation lifecycle in ${hooks.length} hooks.`);
|
|
@@ -408,4 +408,103 @@ for (const hook of HOOKS_WITH_OPENSPEC) {
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Test 6 — stale `attemptId` rejection (WP-1 receipt-binding callout):
|
|
413
|
+
// the hook's `readBoundReceiptOutcome` rejects entries whose `receipt.attemptId`
|
|
414
|
+
// does not match the active attempt, even when `completedAt` is fresh. A
|
|
415
|
+
// receipt bound to a prior attempt ID (e.g. attempt-1 reused after a retry
|
|
416
|
+
// recycled the slot) must not advance the pipeline.
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
for (const hook of hooks) {
|
|
419
|
+
const root = fs.mkdtempSync(path.join(packageDirectory, '.stale-attempt-id-'));
|
|
420
|
+
try {
|
|
421
|
+
const platform = platformFor(hook);
|
|
422
|
+
const runId = 'stale-attempt-id-run';
|
|
423
|
+
const devAttemptId = `${runId}:step-0:feature-developer:attempt-2`;
|
|
424
|
+
const freshIso = new Date().toISOString();
|
|
425
|
+
writeTask(
|
|
426
|
+
root,
|
|
427
|
+
hook,
|
|
428
|
+
'stale-attempt-id',
|
|
429
|
+
{
|
|
430
|
+
autoChain: true,
|
|
431
|
+
steps: [
|
|
432
|
+
{ agent: 'feature-developer', artifactOutputId: 'implementation' },
|
|
433
|
+
{ agent: 'build-verifier', artifactInputIds: ['implementation'], artifactOutputId: 'validation' },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
slug: 'stale-attempt-id',
|
|
438
|
+
intent: 'feature',
|
|
439
|
+
pipelineIndex: 0,
|
|
440
|
+
currentAgent: 'feature-developer',
|
|
441
|
+
phase: 'executing',
|
|
442
|
+
state: 'in_progress',
|
|
443
|
+
activeAttempts: {
|
|
444
|
+
[devAttemptId]: {
|
|
445
|
+
agent: 'feature-developer',
|
|
446
|
+
stepIndex: 0,
|
|
447
|
+
attempt: 2,
|
|
448
|
+
startedAt: freshIso,
|
|
449
|
+
parallelGroupId: null,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
metricsRunId: runId,
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
schemaVersion: 1,
|
|
456
|
+
task: { runId, slug: 'stale-attempt-id', preset: 'ai-rules', stack: 'test', platform, intent: 'feature', profile: 'standard' },
|
|
457
|
+
events: [
|
|
458
|
+
{ eventId: `${runId}:task_started`, type: 'task_started', at: freshIso },
|
|
459
|
+
{ eventId: `${runId}:dev-start`, type: 'attempt_started', attemptId: devAttemptId, agent: 'feature-developer', stepIndex: 0, attempt: 2, at: freshIso },
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
'artifact-manifest.json': `${JSON.stringify(
|
|
464
|
+
{
|
|
465
|
+
schemaVersion: 1,
|
|
466
|
+
slug: 'stale-attempt-id',
|
|
467
|
+
updatedAt: freshIso,
|
|
468
|
+
artifacts: [
|
|
469
|
+
{
|
|
470
|
+
id: 'implementation',
|
|
471
|
+
producer: 'feature-developer',
|
|
472
|
+
status: 'completed',
|
|
473
|
+
authoritativePaths: ['implementation.md'],
|
|
474
|
+
acceptanceCriteriaIds: ['AC-1'],
|
|
475
|
+
taskIds: ['T1'],
|
|
476
|
+
timestamps: { createdAt: freshIso, updatedAt: freshIso, completedAt: freshIso },
|
|
477
|
+
receipt: {
|
|
478
|
+
outcome: 'completed',
|
|
479
|
+
// Stale: belongs to attempt-1 of the same (step, agent); the active attempt is attempt-2.
|
|
480
|
+
attemptId: `${runId}:step-0:feature-developer:attempt-1`,
|
|
481
|
+
summary: 'Receipt bound to a prior attempt; should be rejected.',
|
|
482
|
+
evidencePaths: ['implementation.md'],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
null,
|
|
488
|
+
2,
|
|
489
|
+
)}\n`,
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const taskDirectory = path.join(root, `.${platform}`, 'team', 'tasks', 'stale-attempt-id');
|
|
494
|
+
const parsed = JSON.parse(invokeHook(hook, root, devAttemptId));
|
|
495
|
+
assert.deepEqual(parsed, {}, `${hook}: stale-attemptId receipt must not hand off`);
|
|
496
|
+
const status = readJson(path.join(taskDirectory, 'status.json'));
|
|
497
|
+
assert.equal(status.pipelineIndex, 0, `${hook}: stale-attemptId receipt advanced the pipeline`);
|
|
498
|
+
assert.equal(status.currentAgent, 'feature-developer', `${hook}: stale-attemptId receipt changed the active producer`);
|
|
499
|
+
assert.equal(status.state, 'awaiting_artifact_receipt', `${hook}: stale-attemptId did not enter receipt-wait state (got: ${status.state})`);
|
|
500
|
+
assert.ok(status.activeAttempts[devAttemptId], `${hook}: stale-attemptId consumed the active attempt`);
|
|
501
|
+
const finished = readJson(path.join(taskDirectory, 'metrics.json')).events.filter(
|
|
502
|
+
(event) => event.type === 'attempt_finished' && event.attemptId === devAttemptId,
|
|
503
|
+
);
|
|
504
|
+
assert.equal(finished.length, 0, `${hook}: stale-attemptId emitted terminal metrics`);
|
|
505
|
+
} finally {
|
|
506
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
console.log(`Verified openspec-author handoff, qa-tester handoff, pipeline misparse blocking, legacy fallback, writeStatus durability, and stale attemptId rejection in ${hooks.length} hooks.`);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fixtures for check-task-router-intents.sh.
|
|
3
|
+
//
|
|
4
|
+
// Scenario: rename the frontmatter `name:` of an existing shipped agent
|
|
5
|
+
// (cursor/next/agents/build-verifier.md → `build-verifier-renamed`) and
|
|
6
|
+
// confirm the check FAILS. Then revert and confirm it PASSES.
|
|
7
|
+
//
|
|
8
|
+
// Why a separate fixture file: the check is a shell + Python script under
|
|
9
|
+
// `scripts/`, not an in-process module, so we exercise it end-to-end via
|
|
10
|
+
// `execFileSync` against a temp copy of `agents/` (mirroring the pattern in
|
|
11
|
+
// `test-chain-team-phases-coverage.mjs` and `test-cli-mcp-merge.mjs`).
|
|
12
|
+
//
|
|
13
|
+
// The fixture operates on a *copy* of `presets/cursor/next/agents/` so the
|
|
14
|
+
// real preset tree is never mutated, even on assertion failure.
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const packageDirectory = path.resolve(scriptDirectory, '..');
|
|
25
|
+
const checkScript = path.join(scriptDirectory, 'check-task-router-intents.sh');
|
|
26
|
+
|
|
27
|
+
function runCheck(env) {
|
|
28
|
+
try {
|
|
29
|
+
return {
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
stdout: execFileSync('bash', [checkScript], { env, encoding: 'utf8' }),
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
exitCode: error.status ?? 1,
|
|
36
|
+
stdout: `${error.stdout ?? ''}${error.stderr ?? ''}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function copyAgentsDir(sourceAgentsDir) {
|
|
42
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'task-router-intents-'));
|
|
43
|
+
// Mirror full preset tree so the Python script's iteration works.
|
|
44
|
+
const sourcePresetsDir = path.join(packageDirectory, 'presets');
|
|
45
|
+
const targetPresetsDir = path.join(tempRoot, 'packages', 'ai-rules', 'presets');
|
|
46
|
+
fs.mkdirSync(targetPresetsDir, { recursive: true });
|
|
47
|
+
copyTree(sourcePresetsDir, targetPresetsDir);
|
|
48
|
+
return tempRoot;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function copyTree(source, destination) {
|
|
52
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
53
|
+
const src = path.join(source, entry.name);
|
|
54
|
+
const dest = path.join(destination, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
57
|
+
copyTree(src, dest);
|
|
58
|
+
} else if (entry.isFile()) {
|
|
59
|
+
fs.copyFileSync(src, dest);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setEnvPresetRoot(tempRoot) {
|
|
65
|
+
return { ...process.env, AI_RULES_PRESETS_ROOT: path.join(tempRoot, 'packages', 'ai-rules', 'presets') };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Test 1 — rename frontmatter `name:` and assert the script returns non-zero.
|
|
69
|
+
{
|
|
70
|
+
const tempRoot = copyAgentsDir(path.join(packageDirectory, 'presets', 'cursor', 'next', 'agents'));
|
|
71
|
+
try {
|
|
72
|
+
const agentFile = path.join(tempRoot, 'packages', 'ai-rules', 'presets', 'cursor', 'next', 'agents', 'build-verifier.md');
|
|
73
|
+
const original = fs.readFileSync(agentFile, 'utf8');
|
|
74
|
+
const renamed = original.replace(/^name:\s*build-verifier\s*$/m, 'name: build-verifier-renamed');
|
|
75
|
+
assert.notStrictEqual(renamed, original, 'rename pattern must match exactly one line');
|
|
76
|
+
fs.writeFileSync(agentFile, renamed);
|
|
77
|
+
|
|
78
|
+
const result = runCheck(setEnvPresetRoot(tempRoot));
|
|
79
|
+
assert.notStrictEqual(
|
|
80
|
+
result.exitCode,
|
|
81
|
+
0,
|
|
82
|
+
`expected FAIL when name frontmatter does not match stem; got exit=${result.exitCode}\n${result.stdout}`,
|
|
83
|
+
);
|
|
84
|
+
assert.match(
|
|
85
|
+
result.stdout,
|
|
86
|
+
/task-router vs agents drift/,
|
|
87
|
+
'expected drift failure message in output',
|
|
88
|
+
);
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Test 2 — revert and assert the script returns zero.
|
|
95
|
+
{
|
|
96
|
+
const tempRoot = copyAgentsDir(path.join(packageDirectory, 'presets', 'cursor', 'next', 'agents'));
|
|
97
|
+
try {
|
|
98
|
+
const result = runCheck(setEnvPresetRoot(tempRoot));
|
|
99
|
+
assert.strictEqual(
|
|
100
|
+
result.exitCode,
|
|
101
|
+
0,
|
|
102
|
+
`expected PASS on unmodified copy; got exit=${result.exitCode}\n${result.stdout}`,
|
|
103
|
+
);
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log('OK: check-task-router-intents.sh fixture tests passed');
|