gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
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/dist/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -1,691 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auto-dispatch-loop.test.ts — End-to-end regression tests for the
|
|
3
|
-
* auto-mode dispatch loop: deriveState() → resolveDispatch()
|
|
4
|
-
*
|
|
5
|
-
* Exercises the full state-machine chain WITHOUT an LLM. Each test
|
|
6
|
-
* creates a .gsd/ filesystem fixture, derives state, runs the dispatch
|
|
7
|
-
* table, and verifies the correct unit type/id is produced.
|
|
8
|
-
*
|
|
9
|
-
* Regression coverage for:
|
|
10
|
-
* #1270 Replaying completed run-uat units
|
|
11
|
-
* #1277 Non-artifact UATs dispatched, blocking progression
|
|
12
|
-
* #1241 Slice progression gated on file existence, not verdict content
|
|
13
|
-
* #909 Missing task plan files → infinite plan-slice loop
|
|
14
|
-
* #807 Prose slice headers not parsed → "No slice eligible" block
|
|
15
|
-
* #1248 Prose header regex only matched H2 with colon separator
|
|
16
|
-
* #1289 Crash recovery false-positive on own PID
|
|
17
|
-
* #1217 (orphaned processes — tested via post-unit, not dispatch)
|
|
18
|
-
*
|
|
19
|
-
* Pattern: create fixture → deriveState → resolveDispatch → assert
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
23
|
-
import { join } from 'node:path';
|
|
24
|
-
import { tmpdir } from 'node:os';
|
|
25
|
-
|
|
26
|
-
import { deriveState, invalidateStateCache } from '../state.ts';
|
|
27
|
-
import { resolveDispatch, type DispatchContext } from '../auto-dispatch.ts';
|
|
28
|
-
import { parseRoadmapSlices } from '../roadmap-slices.ts';
|
|
29
|
-
import { checkNeedsRunUat } from '../auto-prompts.ts';
|
|
30
|
-
import { checkIdempotency, type IdempotencyContext } from '../auto-idempotency.ts';
|
|
31
|
-
import { invalidateAllCaches } from '../cache.ts';
|
|
32
|
-
import { AutoSession } from '../auto/session.ts';
|
|
33
|
-
import { createTestContext } from './test-helpers.ts';
|
|
34
|
-
|
|
35
|
-
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
36
|
-
|
|
37
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
-
// Fixture Helpers
|
|
39
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
-
|
|
41
|
-
function createBase(): string {
|
|
42
|
-
const base = mkdtempSync(join(tmpdir(), 'gsd-dispatch-loop-'));
|
|
43
|
-
mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
|
|
44
|
-
return base;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function cleanup(base: string): void {
|
|
48
|
-
rmSync(base, { recursive: true, force: true });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function writeMilestoneFile(base: string, mid: string, suffix: string, content: string): void {
|
|
52
|
-
const dir = join(base, '.gsd', 'milestones', mid);
|
|
53
|
-
mkdirSync(dir, { recursive: true });
|
|
54
|
-
writeFileSync(join(dir, `${mid}-${suffix}.md`), content);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function writeSliceFile(base: string, mid: string, sid: string, suffix: string, content: string): void {
|
|
58
|
-
const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
|
|
59
|
-
mkdirSync(dir, { recursive: true });
|
|
60
|
-
writeFileSync(join(dir, `${sid}-${suffix}.md`), content);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function writeTaskFile(base: string, mid: string, sid: string, tid: string, suffix: string, content: string): void {
|
|
64
|
-
const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid, 'tasks');
|
|
65
|
-
mkdirSync(dir, { recursive: true });
|
|
66
|
-
writeFileSync(join(dir, `${tid}-${suffix}.md`), content);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Standard machine-readable roadmap with checkbox slices */
|
|
70
|
-
function standardRoadmap(mid: string, title: string, slices: Array<{ id: string; title: string; done: boolean; risk?: string; depends?: string[] }>): string {
|
|
71
|
-
const lines = [
|
|
72
|
-
`# ${mid}: ${title}`,
|
|
73
|
-
'',
|
|
74
|
-
'## Slices',
|
|
75
|
-
'',
|
|
76
|
-
];
|
|
77
|
-
for (const s of slices) {
|
|
78
|
-
const check = s.done ? 'x' : ' ';
|
|
79
|
-
const risk = s.risk ?? 'low';
|
|
80
|
-
const deps = s.depends ?? [];
|
|
81
|
-
lines.push(`- [${check}] **${s.id}: ${s.title}** \`risk:${risk}\` \`depends:[${deps.join(',')}]\``);
|
|
82
|
-
}
|
|
83
|
-
lines.push('', '## Boundary Map', '');
|
|
84
|
-
return lines.join('\n');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Standard slice plan with tasks */
|
|
88
|
-
function standardPlan(sid: string, title: string, tasks: Array<{ id: string; title: string; done: boolean; est?: string }>): string {
|
|
89
|
-
const lines = [
|
|
90
|
-
`# ${sid}: ${title}`,
|
|
91
|
-
'',
|
|
92
|
-
'## Tasks',
|
|
93
|
-
'',
|
|
94
|
-
];
|
|
95
|
-
for (const t of tasks) {
|
|
96
|
-
const check = t.done ? 'x' : ' ';
|
|
97
|
-
const est = t.est ?? '1h';
|
|
98
|
-
lines.push(`- [${check}] **${t.id}: ${t.title}** \`est:${est}\``);
|
|
99
|
-
}
|
|
100
|
-
return lines.join('\n');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function freshState(): void {
|
|
104
|
-
invalidateAllCaches();
|
|
105
|
-
invalidateStateCache();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function dispatchFor(base: string): Promise<ReturnType<typeof resolveDispatch>> {
|
|
109
|
-
freshState();
|
|
110
|
-
const state = await deriveState(base);
|
|
111
|
-
const mid = state.activeMilestone?.id;
|
|
112
|
-
if (!mid) return { action: 'stop', reason: 'No active milestone', level: 'info' };
|
|
113
|
-
const midTitle = state.activeMilestone?.title ?? mid;
|
|
114
|
-
const ctx: DispatchContext = { basePath: base, mid, midTitle, state, prefs: undefined };
|
|
115
|
-
return resolveDispatch(ctx);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
119
|
-
// Tests
|
|
120
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
121
|
-
|
|
122
|
-
async function main(): Promise<void> {
|
|
123
|
-
|
|
124
|
-
// ─── 1. Basic state derivation: pre-planning → plan-milestone ─────────
|
|
125
|
-
console.log('\n=== 1. pre-planning with context → plan-milestone (or research) ===');
|
|
126
|
-
{
|
|
127
|
-
const base = createBase();
|
|
128
|
-
try {
|
|
129
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001: Test Project\n\nBuild a thing.\n');
|
|
130
|
-
const result = await dispatchFor(base);
|
|
131
|
-
assertTrue(
|
|
132
|
-
result.action === 'dispatch',
|
|
133
|
-
'pre-planning with context dispatches a unit',
|
|
134
|
-
);
|
|
135
|
-
if (result.action === 'dispatch') {
|
|
136
|
-
assertTrue(
|
|
137
|
-
result.unitType === 'research-milestone' || result.unitType === 'plan-milestone',
|
|
138
|
-
`dispatches research-milestone or plan-milestone, got ${result.unitType}`,
|
|
139
|
-
);
|
|
140
|
-
assertEq(result.unitId, 'M001', 'unit ID is M001');
|
|
141
|
-
}
|
|
142
|
-
} finally {
|
|
143
|
-
cleanup(base);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ─── 2. Planning → plan-slice ─────────────────────────────────────────
|
|
148
|
-
console.log('\n=== 2. has roadmap, no slice plan → plan-slice ===');
|
|
149
|
-
{
|
|
150
|
-
const base = createBase();
|
|
151
|
-
try {
|
|
152
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001: Test\n\nDesc.\n');
|
|
153
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
154
|
-
{ id: 'S01', title: 'First Slice', done: false },
|
|
155
|
-
{ id: 'S02', title: 'Second Slice', done: false, depends: ['S01'] },
|
|
156
|
-
]));
|
|
157
|
-
const result = await dispatchFor(base);
|
|
158
|
-
assertTrue(result.action === 'dispatch', 'planning phase dispatches');
|
|
159
|
-
if (result.action === 'dispatch') {
|
|
160
|
-
assertTrue(
|
|
161
|
-
result.unitType === 'plan-slice' || result.unitType === 'research-slice',
|
|
162
|
-
`dispatches plan-slice or research-slice, got ${result.unitType}`,
|
|
163
|
-
);
|
|
164
|
-
assertMatch(result.unitId, /M001\/S01/, 'targets S01');
|
|
165
|
-
}
|
|
166
|
-
} finally {
|
|
167
|
-
cleanup(base);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ─── 3. Executing → execute-task ──────────────────────────────────────
|
|
172
|
-
console.log('\n=== 3. has plan with incomplete task → execute-task ===');
|
|
173
|
-
{
|
|
174
|
-
const base = createBase();
|
|
175
|
-
try {
|
|
176
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
177
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
178
|
-
{ id: 'S01', title: 'First Slice', done: false },
|
|
179
|
-
]));
|
|
180
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'First Slice', [
|
|
181
|
-
{ id: 'T01', title: 'First Task', done: false },
|
|
182
|
-
{ id: 'T02', title: 'Second Task', done: false },
|
|
183
|
-
]));
|
|
184
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01: First Task\n\nDo the thing.\n');
|
|
185
|
-
writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02: Second Task\n\nDo more.\n');
|
|
186
|
-
|
|
187
|
-
const result = await dispatchFor(base);
|
|
188
|
-
assertTrue(result.action === 'dispatch', 'executing phase dispatches');
|
|
189
|
-
if (result.action === 'dispatch') {
|
|
190
|
-
assertEq(result.unitType, 'execute-task', 'dispatches execute-task');
|
|
191
|
-
assertEq(result.unitId, 'M001/S01/T01', 'targets T01');
|
|
192
|
-
}
|
|
193
|
-
} finally {
|
|
194
|
-
cleanup(base);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ─── 4. All tasks done → complete-slice (summarizing) ─────────────────
|
|
199
|
-
console.log('\n=== 4. all tasks done → summarizing → complete-slice ===');
|
|
200
|
-
{
|
|
201
|
-
const base = createBase();
|
|
202
|
-
try {
|
|
203
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
204
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
205
|
-
{ id: 'S01', title: 'First Slice', done: false },
|
|
206
|
-
]));
|
|
207
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'First Slice', [
|
|
208
|
-
{ id: 'T01', title: 'First Task', done: true },
|
|
209
|
-
{ id: 'T02', title: 'Second Task', done: true },
|
|
210
|
-
]));
|
|
211
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDone.');
|
|
212
|
-
writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nDone.');
|
|
213
|
-
|
|
214
|
-
const result = await dispatchFor(base);
|
|
215
|
-
assertTrue(result.action === 'dispatch', 'summarizing phase dispatches');
|
|
216
|
-
if (result.action === 'dispatch') {
|
|
217
|
-
assertEq(result.unitType, 'complete-slice', 'dispatches complete-slice');
|
|
218
|
-
assertEq(result.unitId, 'M001/S01', 'targets S01');
|
|
219
|
-
}
|
|
220
|
-
} finally {
|
|
221
|
-
cleanup(base);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ─── 5. Regression #909: Missing task plan files → plan-slice ─────────
|
|
226
|
-
console.log('\n=== 5. #909: tasks in plan but empty tasks/ dir → plan-slice (not stuck loop) ===');
|
|
227
|
-
{
|
|
228
|
-
const base = createBase();
|
|
229
|
-
try {
|
|
230
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
231
|
-
// Add milestone research so research-slice doesn't fire first
|
|
232
|
-
writeMilestoneFile(base, 'M001', 'RESEARCH', '# Research\n\nDone.\n');
|
|
233
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
234
|
-
{ id: 'S01', title: 'First Slice', done: false },
|
|
235
|
-
]));
|
|
236
|
-
// Also write slice research so research-slice is skipped
|
|
237
|
-
writeSliceFile(base, 'M001', 'S01', 'RESEARCH', '# Slice Research\n\nDone.\n');
|
|
238
|
-
// Plan references tasks but tasks/ dir has no files
|
|
239
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'First Slice', [
|
|
240
|
-
{ id: 'T01', title: 'First Task', done: false },
|
|
241
|
-
]));
|
|
242
|
-
// Create empty tasks directory (no task plan files)
|
|
243
|
-
mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
|
|
244
|
-
|
|
245
|
-
freshState();
|
|
246
|
-
const state = await deriveState(base);
|
|
247
|
-
// Should fall back to planning phase since tasks dir is empty
|
|
248
|
-
assertEq(state.phase, 'planning', '#909: empty tasks dir → planning phase (not executing)');
|
|
249
|
-
|
|
250
|
-
const result = await dispatchFor(base);
|
|
251
|
-
assertTrue(result.action === 'dispatch', '#909: dispatches');
|
|
252
|
-
if (result.action === 'dispatch') {
|
|
253
|
-
assertEq(result.unitType, 'plan-slice', '#909: dispatches plan-slice to regenerate task plans');
|
|
254
|
-
}
|
|
255
|
-
} finally {
|
|
256
|
-
cleanup(base);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ─── 6. Regression #1277: Non-artifact UAT not dispatched ─────────────
|
|
261
|
-
console.log('\n=== 6. #1277: human-experience UAT → null (skip, not dispatch) ===');
|
|
262
|
-
{
|
|
263
|
-
const base = createBase();
|
|
264
|
-
try {
|
|
265
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
266
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
267
|
-
{ id: 'S01', title: 'Done Slice', done: true },
|
|
268
|
-
{ id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
|
|
269
|
-
]));
|
|
270
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: human-experience\n');
|
|
271
|
-
|
|
272
|
-
const state = {
|
|
273
|
-
activeMilestone: { id: 'M001', title: 'Test' },
|
|
274
|
-
activeSlice: { id: 'S02', title: 'Next Slice' },
|
|
275
|
-
activeTask: null,
|
|
276
|
-
phase: 'planning',
|
|
277
|
-
recentDecisions: [],
|
|
278
|
-
blockers: [],
|
|
279
|
-
nextAction: 'Plan S02',
|
|
280
|
-
registry: [],
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
freshState();
|
|
284
|
-
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
|
285
|
-
assertEq(result, null, '#1277: human-experience UAT returns null (not dispatched)');
|
|
286
|
-
} finally {
|
|
287
|
-
cleanup(base);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── 7. Regression #1277: artifact-driven UAT without result → dispatch ──
|
|
292
|
-
console.log('\n=== 7. artifact-driven UAT without result → dispatch ===');
|
|
293
|
-
{
|
|
294
|
-
const base = createBase();
|
|
295
|
-
try {
|
|
296
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
297
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
298
|
-
{ id: 'S01', title: 'Done Slice', done: true },
|
|
299
|
-
{ id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
|
|
300
|
-
]));
|
|
301
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
|
|
302
|
-
// No UAT-RESULT file
|
|
303
|
-
|
|
304
|
-
const state = {
|
|
305
|
-
activeMilestone: { id: 'M001', title: 'Test' },
|
|
306
|
-
activeSlice: { id: 'S02', title: 'Next Slice' },
|
|
307
|
-
activeTask: null,
|
|
308
|
-
phase: 'planning',
|
|
309
|
-
recentDecisions: [],
|
|
310
|
-
blockers: [],
|
|
311
|
-
nextAction: 'Plan S02',
|
|
312
|
-
registry: [],
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
freshState();
|
|
316
|
-
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
|
317
|
-
assertTrue(result !== null, 'artifact-driven UAT without result → dispatch (not null)');
|
|
318
|
-
if (result) {
|
|
319
|
-
assertEq(result.sliceId, 'S01', 'targets S01');
|
|
320
|
-
}
|
|
321
|
-
} finally {
|
|
322
|
-
cleanup(base);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ─── 8. Regression #1270: Existing UAT-RESULT never re-dispatches ─────
|
|
327
|
-
console.log('\n=== 8. #1270: UAT-RESULT exists → no re-dispatch (any verdict) ===');
|
|
328
|
-
{
|
|
329
|
-
const base = createBase();
|
|
330
|
-
try {
|
|
331
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
332
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
333
|
-
{ id: 'S01', title: 'Done Slice', done: true },
|
|
334
|
-
{ id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
|
|
335
|
-
]));
|
|
336
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
|
|
337
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: FAIL\n---\nFailed.\n');
|
|
338
|
-
|
|
339
|
-
const state = {
|
|
340
|
-
activeMilestone: { id: 'M001', title: 'Test' },
|
|
341
|
-
activeSlice: { id: 'S02', title: 'Next Slice' },
|
|
342
|
-
activeTask: null,
|
|
343
|
-
phase: 'planning',
|
|
344
|
-
recentDecisions: [],
|
|
345
|
-
blockers: [],
|
|
346
|
-
nextAction: 'Plan S02',
|
|
347
|
-
registry: [],
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
freshState();
|
|
351
|
-
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
|
352
|
-
assertEq(result, null, '#1270: existing UAT-RESULT with FAIL → null (no re-dispatch)');
|
|
353
|
-
} finally {
|
|
354
|
-
cleanup(base);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ─── 9. Regression #1241: UAT verdict gate blocks non-PASS ────────────
|
|
359
|
-
console.log('\n=== 9. #1241: UAT verdict gate blocks progression on non-PASS verdict ===');
|
|
360
|
-
{
|
|
361
|
-
const base = createBase();
|
|
362
|
-
try {
|
|
363
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
364
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
365
|
-
{ id: 'S01', title: 'Done Slice', done: true },
|
|
366
|
-
{ id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
|
|
367
|
-
]));
|
|
368
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Done Slice', [
|
|
369
|
-
{ id: 'T01', title: 'Task', done: true },
|
|
370
|
-
]));
|
|
371
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
|
|
372
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: FAIL\n---\nFailed some check.\n');
|
|
373
|
-
|
|
374
|
-
freshState();
|
|
375
|
-
const state = await deriveState(base);
|
|
376
|
-
const ctx: DispatchContext = {
|
|
377
|
-
basePath: base,
|
|
378
|
-
mid: 'M001',
|
|
379
|
-
midTitle: 'Test',
|
|
380
|
-
state,
|
|
381
|
-
prefs: { uat_dispatch: true } as any,
|
|
382
|
-
};
|
|
383
|
-
const result = await resolveDispatch(ctx);
|
|
384
|
-
// The uat-verdict-gate rule should stop progression
|
|
385
|
-
assertEq(result.action, 'stop', '#1241: non-PASS verdict → stop (blocks progression)');
|
|
386
|
-
} finally {
|
|
387
|
-
cleanup(base);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ─── 10. #1241: UAT verdict PASS allows progression ───────────────────
|
|
392
|
-
console.log('\n=== 10. UAT verdict PASS → allows progression ===');
|
|
393
|
-
{
|
|
394
|
-
const base = createBase();
|
|
395
|
-
try {
|
|
396
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
397
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
398
|
-
{ id: 'S01', title: 'Done Slice', done: true },
|
|
399
|
-
{ id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
|
|
400
|
-
]));
|
|
401
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
|
|
402
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: PASS\n---\nAll good.\n');
|
|
403
|
-
|
|
404
|
-
freshState();
|
|
405
|
-
const state = await deriveState(base);
|
|
406
|
-
const ctx: DispatchContext = {
|
|
407
|
-
basePath: base,
|
|
408
|
-
mid: 'M001',
|
|
409
|
-
midTitle: 'Test',
|
|
410
|
-
state,
|
|
411
|
-
prefs: { uat_dispatch: true } as any,
|
|
412
|
-
};
|
|
413
|
-
const result = await resolveDispatch(ctx);
|
|
414
|
-
// PASS verdict should NOT block — dispatch should continue to plan-slice for S02
|
|
415
|
-
assertTrue(result.action !== 'stop' || !('reason' in result && result.reason.includes('verdict')), 'PASS verdict does not block progression');
|
|
416
|
-
} finally {
|
|
417
|
-
cleanup(base);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// ─── 11. Complete state derivation: all slices done → completing ───────
|
|
422
|
-
console.log('\n=== 11. all slices done, no validation → validating-milestone ===');
|
|
423
|
-
{
|
|
424
|
-
const base = createBase();
|
|
425
|
-
try {
|
|
426
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
427
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
428
|
-
{ id: 'S01', title: 'First Slice', done: true },
|
|
429
|
-
]));
|
|
430
|
-
|
|
431
|
-
freshState();
|
|
432
|
-
const state = await deriveState(base);
|
|
433
|
-
assertEq(state.phase, 'validating-milestone', 'all slices done → validating-milestone');
|
|
434
|
-
} finally {
|
|
435
|
-
cleanup(base);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// ─── 12. Complete milestone → complete phase ──────────────────────────
|
|
440
|
-
console.log('\n=== 12. validated + summarized milestone → complete ===');
|
|
441
|
-
{
|
|
442
|
-
const base = createBase();
|
|
443
|
-
try {
|
|
444
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
445
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
446
|
-
{ id: 'S01', title: 'First Slice', done: true },
|
|
447
|
-
]));
|
|
448
|
-
writeMilestoneFile(base, 'M001', 'VALIDATION', '---\nverdict: pass\nremediation_round: 0\n---\n# Validation\nAll good.\n');
|
|
449
|
-
writeMilestoneFile(base, 'M001', 'SUMMARY', '---\nstatus: complete\n---\n# Summary\nDone.\n');
|
|
450
|
-
|
|
451
|
-
freshState();
|
|
452
|
-
const state = await deriveState(base);
|
|
453
|
-
assertEq(state.phase, 'complete', 'validated+summarized → complete');
|
|
454
|
-
} finally {
|
|
455
|
-
cleanup(base);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// ─── 13. Multi-milestone: M001 complete, M002 active ─────────────────
|
|
460
|
-
console.log('\n=== 13. multi-milestone: M001 complete, M002 becomes active ===');
|
|
461
|
-
{
|
|
462
|
-
const base = createBase();
|
|
463
|
-
try {
|
|
464
|
-
// M001 — complete
|
|
465
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDone.\n');
|
|
466
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'First', [
|
|
467
|
-
{ id: 'S01', title: 'Slice', done: true },
|
|
468
|
-
]));
|
|
469
|
-
writeMilestoneFile(base, 'M001', 'VALIDATION', '---\nverdict: pass\nremediation_round: 0\n---\n');
|
|
470
|
-
writeMilestoneFile(base, 'M001', 'SUMMARY', '---\nstatus: complete\n---\n# Summary\n');
|
|
471
|
-
|
|
472
|
-
// M002 — active
|
|
473
|
-
writeMilestoneFile(base, 'M002', 'CONTEXT', '# M002\n\nNext.\n');
|
|
474
|
-
|
|
475
|
-
freshState();
|
|
476
|
-
const state = await deriveState(base);
|
|
477
|
-
assertEq(state.activeMilestone?.id, 'M002', 'M002 is the active milestone');
|
|
478
|
-
assertEq(state.phase, 'pre-planning', 'M002 is in pre-planning');
|
|
479
|
-
assertEq(state.registry.length, 2, 'registry has 2 milestones');
|
|
480
|
-
assertEq(state.registry[0].status, 'complete', 'M001 is complete');
|
|
481
|
-
assertEq(state.registry[1].status, 'active', 'M002 is active');
|
|
482
|
-
} finally {
|
|
483
|
-
cleanup(base);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ─── 14. Dependency blocking: S02 depends on S01 ─────────────────────
|
|
488
|
-
console.log('\n=== 14. slice dependency: S02 blocked until S01 done ===');
|
|
489
|
-
{
|
|
490
|
-
const base = createBase();
|
|
491
|
-
try {
|
|
492
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
493
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
494
|
-
{ id: 'S01', title: 'First', done: false },
|
|
495
|
-
{ id: 'S02', title: 'Second', done: false, depends: ['S01'] },
|
|
496
|
-
]));
|
|
497
|
-
|
|
498
|
-
freshState();
|
|
499
|
-
const state = await deriveState(base);
|
|
500
|
-
// Active slice should be S01, not S02
|
|
501
|
-
assertEq(state.activeSlice?.id, 'S01', 'S01 is the active slice (S02 is dep-blocked)');
|
|
502
|
-
} finally {
|
|
503
|
-
cleanup(base);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// ─── 15. Blocker detection: task with blocker_discovered → replan ─────
|
|
508
|
-
console.log('\n=== 15. blocker_discovered in task summary → replanning-slice ===');
|
|
509
|
-
{
|
|
510
|
-
const base = createBase();
|
|
511
|
-
try {
|
|
512
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
513
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
514
|
-
{ id: 'S01', title: 'Slice', done: false },
|
|
515
|
-
]));
|
|
516
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
|
|
517
|
-
{ id: 'T01', title: 'Task One', done: true },
|
|
518
|
-
{ id: 'T02', title: 'Task Two', done: false },
|
|
519
|
-
]));
|
|
520
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
|
|
521
|
-
writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nDo other thing.');
|
|
522
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nblocker_discovered: true\n---\n# T01 Summary\nFound a blocker.');
|
|
523
|
-
|
|
524
|
-
freshState();
|
|
525
|
-
const state = await deriveState(base);
|
|
526
|
-
assertEq(state.phase, 'replanning-slice', 'blocker_discovered → replanning-slice');
|
|
527
|
-
} finally {
|
|
528
|
-
cleanup(base);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// ─── 16. Blocker + REPLAN exists → loop protection, resume executing ──
|
|
533
|
-
console.log('\n=== 16. blocker_discovered + REPLAN exists → loop protection (executing) ===');
|
|
534
|
-
{
|
|
535
|
-
const base = createBase();
|
|
536
|
-
try {
|
|
537
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
|
|
538
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
539
|
-
{ id: 'S01', title: 'Slice', done: false },
|
|
540
|
-
]));
|
|
541
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
|
|
542
|
-
{ id: 'T01', title: 'Task One', done: true },
|
|
543
|
-
{ id: 'T02', title: 'Task Two', done: false },
|
|
544
|
-
]));
|
|
545
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
|
|
546
|
-
writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nDo other thing.');
|
|
547
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nblocker_discovered: true\n---\n# T01\nBlocker.');
|
|
548
|
-
// REPLAN.md exists → loop protection
|
|
549
|
-
writeSliceFile(base, 'M001', 'S01', 'REPLAN', '# Replan\nAlready replanned.\n');
|
|
550
|
-
|
|
551
|
-
freshState();
|
|
552
|
-
const state = await deriveState(base);
|
|
553
|
-
assertEq(state.phase, 'executing', 'blocker + REPLAN exists → executing (loop protection)');
|
|
554
|
-
} finally {
|
|
555
|
-
cleanup(base);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// ─── 17. Needs-discussion phase ───────────────────────────────────────
|
|
560
|
-
console.log('\n=== 17. CONTEXT-DRAFT without CONTEXT → needs-discussion ===');
|
|
561
|
-
{
|
|
562
|
-
const base = createBase();
|
|
563
|
-
try {
|
|
564
|
-
const mDir = join(base, '.gsd', 'milestones', 'M001');
|
|
565
|
-
mkdirSync(mDir, { recursive: true });
|
|
566
|
-
writeFileSync(join(mDir, 'M001-CONTEXT-DRAFT.md'), '# Draft\n\nSome rough ideas.\n');
|
|
567
|
-
|
|
568
|
-
freshState();
|
|
569
|
-
const state = await deriveState(base);
|
|
570
|
-
assertEq(state.phase, 'needs-discussion', 'CONTEXT-DRAFT without CONTEXT → needs-discussion');
|
|
571
|
-
} finally {
|
|
572
|
-
cleanup(base);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// ─── 18. Idempotency: completed key → skip ───────────────────────────
|
|
577
|
-
console.log('\n=== 18. idempotency: completed key → skip ===');
|
|
578
|
-
{
|
|
579
|
-
const base = createBase();
|
|
580
|
-
try {
|
|
581
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n');
|
|
582
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
583
|
-
{ id: 'S01', title: 'Slice', done: false },
|
|
584
|
-
]));
|
|
585
|
-
// Task must be marked [x] in the plan for verifyExpectedArtifact to return true
|
|
586
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
|
|
587
|
-
{ id: 'T01', title: 'Task', done: true },
|
|
588
|
-
{ id: 'T02', title: 'Next Task', done: false },
|
|
589
|
-
]));
|
|
590
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
|
|
591
|
-
writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nNext.');
|
|
592
|
-
// Write SUMMARY as the expected artifact for execute-task
|
|
593
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nstatus: done\n---\n# T01 Summary\nDone.');
|
|
594
|
-
|
|
595
|
-
// Force cache clearance so verifyExpectedArtifact finds the file
|
|
596
|
-
freshState();
|
|
597
|
-
|
|
598
|
-
const session = new AutoSession();
|
|
599
|
-
session.basePath = base;
|
|
600
|
-
session.completedKeySet.add('execute-task/M001/S01/T01');
|
|
601
|
-
|
|
602
|
-
const notifications: string[] = [];
|
|
603
|
-
const result = checkIdempotency({
|
|
604
|
-
s: session,
|
|
605
|
-
unitType: 'execute-task',
|
|
606
|
-
unitId: 'M001/S01/T01',
|
|
607
|
-
basePath: base,
|
|
608
|
-
notify: (msg) => notifications.push(msg),
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
assertEq(result.action, 'skip', 'completed key → skip');
|
|
612
|
-
assertTrue('reason' in result && result.reason === 'completed', 'reason is completed');
|
|
613
|
-
} finally {
|
|
614
|
-
cleanup(base);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// ─── 19. Idempotency: stale key (artifact missing) → rerun ───────────
|
|
619
|
-
console.log('\n=== 19. idempotency: stale key (no artifact) → rerun ===');
|
|
620
|
-
{
|
|
621
|
-
const base = createBase();
|
|
622
|
-
try {
|
|
623
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n');
|
|
624
|
-
writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
|
|
625
|
-
{ id: 'S01', title: 'Slice', done: false },
|
|
626
|
-
]));
|
|
627
|
-
writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
|
|
628
|
-
{ id: 'T01', title: 'Task', done: false },
|
|
629
|
-
]));
|
|
630
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
|
|
631
|
-
// NO summary file — artifact missing
|
|
632
|
-
|
|
633
|
-
const session = new AutoSession();
|
|
634
|
-
session.basePath = base;
|
|
635
|
-
session.completedKeySet.add('execute-task/M001/S01/T01');
|
|
636
|
-
|
|
637
|
-
const notifications: string[] = [];
|
|
638
|
-
const result = checkIdempotency({
|
|
639
|
-
s: session,
|
|
640
|
-
unitType: 'execute-task',
|
|
641
|
-
unitId: 'M001/S01/T01',
|
|
642
|
-
basePath: base,
|
|
643
|
-
notify: (msg) => notifications.push(msg),
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
assertEq(result.action, 'rerun', 'stale key (no artifact) → rerun');
|
|
647
|
-
assertTrue(!session.completedKeySet.has('execute-task/M001/S01/T01'), 'stale key removed from set');
|
|
648
|
-
} finally {
|
|
649
|
-
cleanup(base);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// ─── 20. Idempotency: consecutive skip loop → evict ──────────────────
|
|
654
|
-
console.log('\n=== 20. idempotency: consecutive skip loop → evict ===');
|
|
655
|
-
{
|
|
656
|
-
const base = createBase();
|
|
657
|
-
try {
|
|
658
|
-
writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n');
|
|
659
|
-
writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nstatus: done\n---\n# Done');
|
|
660
|
-
|
|
661
|
-
const session = new AutoSession();
|
|
662
|
-
session.basePath = base;
|
|
663
|
-
session.completedKeySet.add('execute-task/M001/S01/T01');
|
|
664
|
-
// Pre-fill skip count to just below threshold
|
|
665
|
-
session.unitConsecutiveSkips.set('execute-task/M001/S01/T01', 3);
|
|
666
|
-
|
|
667
|
-
const notifications: string[] = [];
|
|
668
|
-
const result = checkIdempotency({
|
|
669
|
-
s: session,
|
|
670
|
-
unitType: 'execute-task',
|
|
671
|
-
unitId: 'M001/S01/T01',
|
|
672
|
-
basePath: base,
|
|
673
|
-
notify: (msg) => notifications.push(msg),
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
assertEq(result.action, 'skip', 'exceeds consecutive skip threshold → skip with eviction');
|
|
677
|
-
assertTrue('reason' in result && result.reason === 'evicted', 'reason is evicted');
|
|
678
|
-
assertTrue(!session.completedKeySet.has('execute-task/M001/S01/T01'), 'key evicted from completed set');
|
|
679
|
-
assertTrue(session.recentlyEvictedKeys.has('execute-task/M001/S01/T01'), 'key tracked in evicted set');
|
|
680
|
-
} finally {
|
|
681
|
-
cleanup(base);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
report();
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
main().catch((error) => {
|
|
689
|
-
console.error(error);
|
|
690
|
-
process.exit(1);
|
|
691
|
-
});
|