gsd-pi 2.80.0-dev.0aaa4fc17 → 2.80.0-dev.1486c1899
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/README.md +4 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +22 -15
- package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
- package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
- package/dist/resources/extensions/gsd/auto.js +52 -4
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
- package/dist/resources/extensions/gsd/guided-flow.js +19 -4
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
- package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
- package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +3 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +22 -17
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
- package/packages/mcp-server/src/workflow-tools.ts +30 -16
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +18 -8
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +20 -8
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +28 -20
- package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
- package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
- package/src/resources/extensions/gsd/auto.ts +60 -3
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
- package/src/resources/extensions/gsd/guided-flow.ts +32 -4
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
- package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
- package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
- package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
- /package/dist/web/standalone/.next/static/{YRK7-Es93MwpIn3VL35K0 → O-2rnxSJki_W-2fkK3zIj}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{YRK7-Es93MwpIn3VL35K0 → O-2rnxSJki_W-2fkK3zIj}/_ssgManifest.js +0 -0
|
@@ -108,6 +108,21 @@ test('executeExecSearch: returns helpful empty-state message when no matches', (
|
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
test('executeExecSearch: returns disabled error when context_mode.enabled=false', () => {
|
|
112
|
+
const base = freshBase();
|
|
113
|
+
try {
|
|
114
|
+
writeRun(base, 'should-not-surface', { stdout: 'hidden\n' });
|
|
115
|
+
const result = executeExecSearch(
|
|
116
|
+
{ query: 'hidden' },
|
|
117
|
+
{ baseDir: base, preferences: { context_mode: { enabled: false } } },
|
|
118
|
+
);
|
|
119
|
+
assert.equal(result.isError, true);
|
|
120
|
+
assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
|
|
121
|
+
} finally {
|
|
122
|
+
cleanup(base);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
111
126
|
test('executeExecSearch: includes stdout_path and preview in details', () => {
|
|
112
127
|
const base = freshBase();
|
|
113
128
|
try {
|
|
@@ -7,6 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
|
|
8
8
|
import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
|
|
9
9
|
import { isContextModeEnabled } from '../preferences-types.ts';
|
|
10
|
+
import { validatePreferences } from '../preferences-validation.ts';
|
|
10
11
|
|
|
11
12
|
function freshBase(): string {
|
|
12
13
|
return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
|
|
@@ -174,6 +175,52 @@ test('executeGsdExec: runs when enabled explicitly set to true', async () => {
|
|
|
174
175
|
}
|
|
175
176
|
});
|
|
176
177
|
|
|
178
|
+
test('executeGsdExec: forwards custom exec_env_allowlist from preferences', async () => {
|
|
179
|
+
const base = freshBase();
|
|
180
|
+
try {
|
|
181
|
+
const result = await executeGsdExec(
|
|
182
|
+
{
|
|
183
|
+
runtime: 'bash',
|
|
184
|
+
script: 'printf "allowed=%s blocked=%s\\n" "$GSD_ALLOWED" "$GSD_BLOCKED"',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
baseDir: base,
|
|
188
|
+
preferences: {
|
|
189
|
+
context_mode: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
exec_env_allowlist: ['GSD_ALLOWED'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
env: {
|
|
195
|
+
PATH: '/usr/bin:/bin',
|
|
196
|
+
HOME: '/tmp',
|
|
197
|
+
GSD_ALLOWED: 'yes',
|
|
198
|
+
GSD_BLOCKED: 'no',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
assert.ok(!result.isError);
|
|
203
|
+
assert.match(result.content[0].text, /allowed=yes blocked=/);
|
|
204
|
+
assert.doesNotMatch(result.content[0].text, /blocked=no/);
|
|
205
|
+
} finally {
|
|
206
|
+
cleanup(base);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('executeGsdExec: enforces per-call timeout override end-to-end', async () => {
|
|
211
|
+
const base = freshBase();
|
|
212
|
+
try {
|
|
213
|
+
const result = await executeGsdExec(
|
|
214
|
+
{ runtime: 'bash', script: 'sleep 2', timeout_ms: 1 },
|
|
215
|
+
{ baseDir: base, preferences: { context_mode: { enabled: true, exec_timeout_ms: 10_000 } } },
|
|
216
|
+
);
|
|
217
|
+
assert.equal(result.details.timed_out, true);
|
|
218
|
+
assert.equal(result.isError, true);
|
|
219
|
+
} finally {
|
|
220
|
+
cleanup(base);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
177
224
|
test('executeGsdExec: rejects empty script', async () => {
|
|
178
225
|
const base = freshBase();
|
|
179
226
|
try {
|
|
@@ -188,6 +235,24 @@ test('executeGsdExec: rejects empty script', async () => {
|
|
|
188
235
|
}
|
|
189
236
|
});
|
|
190
237
|
|
|
238
|
+
test('validatePreferences: rejects invalid context_mode preference values', () => {
|
|
239
|
+
const result = validatePreferences({
|
|
240
|
+
context_mode: {
|
|
241
|
+
enabled: 'false',
|
|
242
|
+
exec_timeout_ms: 999,
|
|
243
|
+
exec_stdout_cap_bytes: 1,
|
|
244
|
+
exec_digest_chars: -1,
|
|
245
|
+
exec_env_allowlist: ['GOOD_NAME', 'bad-name'],
|
|
246
|
+
},
|
|
247
|
+
} as any);
|
|
248
|
+
assert.ok(result.errors.length > 0);
|
|
249
|
+
assert.ok(result.errors.includes('context_mode.enabled must be a boolean'));
|
|
250
|
+
assert.ok(result.errors.includes('context_mode.exec_timeout_ms must be a number between 1000 and 600000'));
|
|
251
|
+
assert.ok(result.errors.includes('context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216'));
|
|
252
|
+
assert.ok(result.errors.includes('context_mode.exec_digest_chars must be a number between 0 and 4000'));
|
|
253
|
+
assert.ok(result.errors.includes('context_mode.exec_env_allowlist must be an array of valid env var names'));
|
|
254
|
+
});
|
|
255
|
+
|
|
191
256
|
test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
|
|
192
257
|
assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
|
|
193
258
|
assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
|
|
@@ -399,6 +399,240 @@ test("runDispatch pauses when complete-milestone summary exists on disk but the
|
|
|
399
399
|
assert.equal(stopCalls, 0, "mismatch pause should not hard-stop the loop");
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
+
test("runDispatch pauses when execute-task artifacts exist but DB status is still open", async (t) => {
|
|
403
|
+
const capture = createEventCapture();
|
|
404
|
+
let pauseCalls = 0;
|
|
405
|
+
let stopCalls = 0;
|
|
406
|
+
let invalidateCalls = 0;
|
|
407
|
+
const base = join(tmpdir(), `gsd-stuck-execute-task-${randomUUID()}`);
|
|
408
|
+
t.after(() => {
|
|
409
|
+
closeDatabase();
|
|
410
|
+
rmSync(base, { recursive: true, force: true });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
414
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
415
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
416
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
417
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
418
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "in_progress" });
|
|
419
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
|
|
420
|
+
writeFileSync(
|
|
421
|
+
join(sliceDir, "S01-PLAN.md"),
|
|
422
|
+
[
|
|
423
|
+
"# S01",
|
|
424
|
+
"",
|
|
425
|
+
"## Tasks",
|
|
426
|
+
"",
|
|
427
|
+
"- [x] **T01: First task** `est:1h`",
|
|
428
|
+
"",
|
|
429
|
+
].join("\n"),
|
|
430
|
+
);
|
|
431
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
|
|
432
|
+
writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# T01 Summary\n\nDone on disk.\n");
|
|
433
|
+
|
|
434
|
+
const deps = makeMockDeps(capture, {
|
|
435
|
+
pauseAuto: async () => { pauseCalls++; },
|
|
436
|
+
stopAuto: async () => { stopCalls++; },
|
|
437
|
+
invalidateAllCaches: () => { invalidateCalls++; },
|
|
438
|
+
resolveDispatch: async () => ({
|
|
439
|
+
action: "dispatch" as const,
|
|
440
|
+
unitType: "execute-task",
|
|
441
|
+
unitId: "M001/S01/T01",
|
|
442
|
+
prompt: "execute the task",
|
|
443
|
+
matchedRule: "executing → execute-task",
|
|
444
|
+
}),
|
|
445
|
+
});
|
|
446
|
+
const ic = makeIC(deps, {
|
|
447
|
+
s: {
|
|
448
|
+
...makeSession(),
|
|
449
|
+
basePath: base,
|
|
450
|
+
originalBasePath: base,
|
|
451
|
+
} as any,
|
|
452
|
+
});
|
|
453
|
+
const preData: PreDispatchData = {
|
|
454
|
+
state: {
|
|
455
|
+
phase: "executing",
|
|
456
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
457
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
458
|
+
activeTask: { id: "T01", title: "First task" },
|
|
459
|
+
registry: [{ id: "M001", status: "active" }],
|
|
460
|
+
blockers: [],
|
|
461
|
+
} as any,
|
|
462
|
+
mid: "M001",
|
|
463
|
+
midTitle: "Test Milestone",
|
|
464
|
+
};
|
|
465
|
+
const loopState: LoopState = {
|
|
466
|
+
recentUnits: [
|
|
467
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
468
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
469
|
+
],
|
|
470
|
+
stuckRecoveryAttempts: 0,
|
|
471
|
+
consecutiveFinalizeTimeouts: 0,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const result = await runDispatch(ic, preData, loopState);
|
|
475
|
+
|
|
476
|
+
assert.equal(result.action, "break");
|
|
477
|
+
assert.equal((result as any).reason, "execute-task-artifact-db-mismatch");
|
|
478
|
+
assert.equal(pauseCalls, 1, "execute-task disk/db mismatch should pause auto-mode");
|
|
479
|
+
assert.equal(stopCalls, 0, "execute-task disk/db mismatch should not hard-stop the loop");
|
|
480
|
+
assert.equal(invalidateCalls, 0, "mismatch should not clear caches and continue toward redispatch");
|
|
481
|
+
assert.equal(loopState.recentUnits.length, 3, "mismatch should keep the stuck window intact");
|
|
482
|
+
assert.equal(loopState.stuckRecoveryAttempts, 1, "mismatch should not reset the recovery counter");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("runDispatch pauses at Level 2 when execute-task artifacts exist but DB status is still open", async (t) => {
|
|
486
|
+
const capture = createEventCapture();
|
|
487
|
+
let pauseCalls = 0;
|
|
488
|
+
let stopCalls = 0;
|
|
489
|
+
let invalidateCalls = 0;
|
|
490
|
+
const base = join(tmpdir(), `gsd-stuck-execute-task-l2-${randomUUID()}`);
|
|
491
|
+
t.after(() => {
|
|
492
|
+
closeDatabase();
|
|
493
|
+
rmSync(base, { recursive: true, force: true });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
497
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
498
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
499
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
500
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
501
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "in_progress" });
|
|
502
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
|
|
503
|
+
writeFileSync(
|
|
504
|
+
join(sliceDir, "S01-PLAN.md"),
|
|
505
|
+
"# S01\n\n## Tasks\n\n- [x] **T01: First task** `est:1h`\n",
|
|
506
|
+
);
|
|
507
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
|
|
508
|
+
writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# T01 Summary\n\nDone on disk.\n");
|
|
509
|
+
|
|
510
|
+
const deps = makeMockDeps(capture, {
|
|
511
|
+
pauseAuto: async () => { pauseCalls++; },
|
|
512
|
+
stopAuto: async () => { stopCalls++; },
|
|
513
|
+
invalidateAllCaches: () => { invalidateCalls++; },
|
|
514
|
+
resolveDispatch: async () => ({
|
|
515
|
+
action: "dispatch" as const,
|
|
516
|
+
unitType: "execute-task",
|
|
517
|
+
unitId: "M001/S01/T01",
|
|
518
|
+
prompt: "execute the task",
|
|
519
|
+
matchedRule: "executing execute-task",
|
|
520
|
+
}),
|
|
521
|
+
});
|
|
522
|
+
const ic = makeIC(deps, {
|
|
523
|
+
s: {
|
|
524
|
+
...makeSession(),
|
|
525
|
+
basePath: base,
|
|
526
|
+
originalBasePath: base,
|
|
527
|
+
} as any,
|
|
528
|
+
});
|
|
529
|
+
const preData: PreDispatchData = {
|
|
530
|
+
state: {
|
|
531
|
+
phase: "executing",
|
|
532
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
533
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
534
|
+
activeTask: { id: "T01", title: "First task" },
|
|
535
|
+
registry: [{ id: "M001", status: "active" }],
|
|
536
|
+
blockers: [],
|
|
537
|
+
} as any,
|
|
538
|
+
mid: "M001",
|
|
539
|
+
midTitle: "Test Milestone",
|
|
540
|
+
};
|
|
541
|
+
const loopState: LoopState = {
|
|
542
|
+
recentUnits: [
|
|
543
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
544
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
545
|
+
],
|
|
546
|
+
stuckRecoveryAttempts: 1,
|
|
547
|
+
consecutiveFinalizeTimeouts: 0,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const result = await runDispatch(ic, preData, loopState);
|
|
551
|
+
|
|
552
|
+
assert.equal(result.action, "break");
|
|
553
|
+
assert.equal((result as any).reason, "execute-task-artifact-db-mismatch");
|
|
554
|
+
assert.equal(pauseCalls, 1, "Level 2 execute-task disk/db mismatch should pause auto-mode");
|
|
555
|
+
assert.equal(stopCalls, 0, "Level 2 execute-task disk/db mismatch should not hard-stop the loop");
|
|
556
|
+
assert.equal(invalidateCalls, 1, "Level 2 should invalidate caches before the final artifact recheck");
|
|
557
|
+
assert.equal(loopState.recentUnits.length, 3, "Level 2 mismatch should keep the stuck window intact");
|
|
558
|
+
assert.equal(loopState.stuckRecoveryAttempts, 1, "Level 2 mismatch should not reset the recovery counter");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("runDispatch clears execute-task stuck state when artifacts and DB status are complete", async (t) => {
|
|
562
|
+
const capture = createEventCapture();
|
|
563
|
+
let pauseCalls = 0;
|
|
564
|
+
let stopCalls = 0;
|
|
565
|
+
let invalidateCalls = 0;
|
|
566
|
+
const base = join(tmpdir(), `gsd-stuck-execute-task-complete-${randomUUID()}`);
|
|
567
|
+
t.after(() => {
|
|
568
|
+
closeDatabase();
|
|
569
|
+
rmSync(base, { recursive: true, force: true });
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
573
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
574
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
575
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
576
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
577
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "in_progress" });
|
|
578
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "complete" });
|
|
579
|
+
writeFileSync(
|
|
580
|
+
join(sliceDir, "S01-PLAN.md"),
|
|
581
|
+
"# S01\n\n## Tasks\n\n- [x] **T01: First task** `est:1h`\n",
|
|
582
|
+
);
|
|
583
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
|
|
584
|
+
writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# T01 Summary\n\nDone on disk.\n");
|
|
585
|
+
|
|
586
|
+
const deps = makeMockDeps(capture, {
|
|
587
|
+
pauseAuto: async () => { pauseCalls++; },
|
|
588
|
+
stopAuto: async () => { stopCalls++; },
|
|
589
|
+
invalidateAllCaches: () => { invalidateCalls++; },
|
|
590
|
+
resolveDispatch: async () => ({
|
|
591
|
+
action: "dispatch" as const,
|
|
592
|
+
unitType: "execute-task",
|
|
593
|
+
unitId: "M001/S01/T01",
|
|
594
|
+
prompt: "execute the task",
|
|
595
|
+
matchedRule: "executing execute-task",
|
|
596
|
+
}),
|
|
597
|
+
});
|
|
598
|
+
const ic = makeIC(deps, {
|
|
599
|
+
s: {
|
|
600
|
+
...makeSession(),
|
|
601
|
+
basePath: base,
|
|
602
|
+
originalBasePath: base,
|
|
603
|
+
} as any,
|
|
604
|
+
});
|
|
605
|
+
const preData: PreDispatchData = {
|
|
606
|
+
state: {
|
|
607
|
+
phase: "executing",
|
|
608
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
609
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
610
|
+
activeTask: { id: "T01", title: "First task" },
|
|
611
|
+
registry: [{ id: "M001", status: "active" }],
|
|
612
|
+
blockers: [],
|
|
613
|
+
} as any,
|
|
614
|
+
mid: "M001",
|
|
615
|
+
midTitle: "Test Milestone",
|
|
616
|
+
};
|
|
617
|
+
const loopState: LoopState = {
|
|
618
|
+
recentUnits: [
|
|
619
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
620
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
621
|
+
],
|
|
622
|
+
stuckRecoveryAttempts: 0,
|
|
623
|
+
consecutiveFinalizeTimeouts: 0,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const result = await runDispatch(ic, preData, loopState);
|
|
627
|
+
|
|
628
|
+
assert.equal(result.action, "continue");
|
|
629
|
+
assert.equal(pauseCalls, 0, "closed DB task should not pause auto-mode");
|
|
630
|
+
assert.equal(stopCalls, 0, "closed DB task should not hard-stop the loop");
|
|
631
|
+
assert.equal(invalidateCalls, 1, "closed DB task recovery should invalidate caches once");
|
|
632
|
+
assert.deepEqual(loopState.recentUnits, [], "closed DB task recovery should clear the stuck window");
|
|
633
|
+
assert.equal(loopState.stuckRecoveryAttempts, 0, "closed DB task recovery should reset the recovery counter");
|
|
634
|
+
});
|
|
635
|
+
|
|
402
636
|
test("runDispatch clears stuck state after Level 1 artifact recovery", async (t) => {
|
|
403
637
|
const capture = createEventCapture();
|
|
404
638
|
let invalidateCalls = 0;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const promptsDir = join(__dirname, "..", "prompts");
|
|
11
|
+
|
|
12
|
+
test("prompt templates do not reference legacy milestone-root .gsd paths", () => {
|
|
13
|
+
const offenders: string[] = [];
|
|
14
|
+
for (const file of readdirSync(promptsDir)) {
|
|
15
|
+
if (!file.endsWith(".md")) continue;
|
|
16
|
+
const content = readFileSync(join(promptsDir, file), "utf-8");
|
|
17
|
+
const legacyPatterns = [
|
|
18
|
+
/\.gsd\/\{\{(?:milestoneId|mid)\}\}\//g,
|
|
19
|
+
/\.gsd\/<milestone-id>\//g,
|
|
20
|
+
/\.gsd\/<ID>\//g,
|
|
21
|
+
];
|
|
22
|
+
for (const pattern of legacyPatterns) {
|
|
23
|
+
if (pattern.test(content)) {
|
|
24
|
+
offenders.push(`${file}: ${pattern.source}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
assert.deepEqual(
|
|
30
|
+
offenders,
|
|
31
|
+
[],
|
|
32
|
+
"Milestone artifacts must use .gsd/milestones/<MID>/..., not legacy .gsd/<MID>/...",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("quick task prompt delegates commit policy to quick.ts", () => {
|
|
37
|
+
const content = readFileSync(join(promptsDir, "quick-task.md"), "utf-8");
|
|
38
|
+
assert.match(content, /\{\{commitInstruction\}\}/);
|
|
39
|
+
assert.doesNotMatch(content, /Stage only relevant files/);
|
|
40
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { buildQuickCommitInstruction } from "../quick.ts";
|
|
8
|
+
|
|
9
|
+
function git(cwd: string, args: string[]): void {
|
|
10
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
11
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("quick task commit instruction does not ask agents to stage external .gsd quick files", { skip: process.platform === "win32" }, () => {
|
|
15
|
+
const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-quick-ext-")));
|
|
16
|
+
const repo = join(tempRoot, "repo");
|
|
17
|
+
const externalGsd = join(tempRoot, "state");
|
|
18
|
+
mkdirSync(repo);
|
|
19
|
+
mkdirSync(externalGsd);
|
|
20
|
+
|
|
21
|
+
const previousCwd = process.cwd();
|
|
22
|
+
try {
|
|
23
|
+
git(repo, ["init"]);
|
|
24
|
+
git(repo, ["config", "user.email", "test@example.com"]);
|
|
25
|
+
git(repo, ["config", "user.name", "Test User"]);
|
|
26
|
+
writeFileSync(join(repo, "README.md"), "# Test\n", "utf-8");
|
|
27
|
+
git(repo, ["add", "README.md"]);
|
|
28
|
+
git(repo, ["commit", "-m", "init"]);
|
|
29
|
+
symlinkSync(externalGsd, join(repo, ".gsd"), "dir");
|
|
30
|
+
|
|
31
|
+
const instruction = buildQuickCommitInstruction(repo, join(repo, ".gsd"));
|
|
32
|
+
|
|
33
|
+
assert.match(instruction, /do not stage or commit `\.gsd\/quick\/\.\.\.`/);
|
|
34
|
+
assert.match(instruction, /nothing in the project repo to commit/);
|
|
35
|
+
assert.match(instruction, /Write the quick summary file directly/);
|
|
36
|
+
} finally {
|
|
37
|
+
process.chdir(previousCwd);
|
|
38
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -101,3 +101,30 @@ test("registerSigtermHandler deregisters previous handler from all signals", ()
|
|
|
101
101
|
// Clean up
|
|
102
102
|
deregisterSigtermHandler(handler2);
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
test("registered signal handler runs best-effort cleanup before exiting", () => {
|
|
106
|
+
let cleanupCalled = false;
|
|
107
|
+
let exitCode: number | string | null | undefined;
|
|
108
|
+
const originalExit = process.exit;
|
|
109
|
+
const handler = registerSigtermHandler(
|
|
110
|
+
"/tmp/test-signal-cleanup",
|
|
111
|
+
null,
|
|
112
|
+
() => {
|
|
113
|
+
cleanupCalled = true;
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
(process as any).exit = ((code?: number | string | null) => {
|
|
118
|
+
exitCode = code;
|
|
119
|
+
throw new Error("process.exit intercepted");
|
|
120
|
+
}) as never;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
assert.throws(() => handler(), /process\.exit intercepted/);
|
|
124
|
+
assert.equal(cleanupCalled, true);
|
|
125
|
+
assert.equal(exitCode, 0);
|
|
126
|
+
} finally {
|
|
127
|
+
(process as any).exit = originalExit;
|
|
128
|
+
deregisterSigtermHandler(handler);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* (the fix) and verifies it does not crash.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
18
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { recoverTimedOutUnit, type RecoveryContext } from "../auto-timeout-recovery.ts";
|
|
22
|
+
import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from "../gsd-db.ts";
|
|
22
23
|
import { test } from 'node:test';
|
|
23
24
|
import assert from 'node:assert/strict';
|
|
24
25
|
|
|
@@ -39,6 +40,14 @@ function makeMockPi() {
|
|
|
39
40
|
} as any;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
function makeRecordingPi() {
|
|
44
|
+
const messages: unknown[] = [];
|
|
45
|
+
return {
|
|
46
|
+
messages,
|
|
47
|
+
sendMessage: (message: unknown) => { messages.push(message); },
|
|
48
|
+
} as any;
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
// ═══ #1855: empty RecoveryContext (basePath undefined) crashes ════════════════
|
|
43
52
|
|
|
44
53
|
{
|
|
@@ -63,6 +72,45 @@ function makeMockPi() {
|
|
|
63
72
|
assert.ok(crashed, "should crash when basePath is undefined (reproduces #1855)");
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
// ═══ DB-complete execute-task recovery advances without steering ═════════════
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
console.log("\n=== execute-task timeout recovery trusts closed DB status ===");
|
|
79
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-timeout-db-complete-"));
|
|
80
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
84
|
+
insertMilestone({ id: "M001", title: "Milestone", status: "active" });
|
|
85
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "in_progress" });
|
|
86
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete" });
|
|
87
|
+
writeFileSync(
|
|
88
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
|
|
89
|
+
"# S01\n\n## Tasks\n\n- [ ] **T01: Task** `est:10m`\n",
|
|
90
|
+
"utf-8",
|
|
91
|
+
);
|
|
92
|
+
writeFileSync(join(base, ".gsd", "STATE.md"), "## Next Action\nExecute T01 for S01: Task\n", "utf-8");
|
|
93
|
+
|
|
94
|
+
const ctx = makeMockCtx();
|
|
95
|
+
const pi = makeRecordingPi();
|
|
96
|
+
const result = await recoverTimedOutUnit(ctx, pi, "execute-task", "M001/S01/T01", "idle", {
|
|
97
|
+
basePath: base,
|
|
98
|
+
verbose: false,
|
|
99
|
+
currentUnitStartedAt: Date.now(),
|
|
100
|
+
unitRecoveryCount: new Map(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.equal(result, "recovered", "db-complete task should recover immediately");
|
|
104
|
+
assert.equal(pi.messages.length, 0, "db-complete task should not send steering recovery");
|
|
105
|
+
const runtime = JSON.parse(readFileSync(join(base, ".gsd", "runtime", "units", "execute-task-M001-S01-T01.json"), "utf-8"));
|
|
106
|
+
assert.equal(runtime.phase, "finalized", "db-complete task should be finalized");
|
|
107
|
+
assert.equal(runtime.recovery.dbComplete, true, "runtime recovery should record DB completion");
|
|
108
|
+
} finally {
|
|
109
|
+
closeDatabase();
|
|
110
|
+
rmSync(base, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
66
114
|
// ═══ #1855: valid RecoveryContext does not crash ═════════════════════════════
|
|
67
115
|
|
|
68
116
|
{
|
|
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
|
|
6
6
|
import { _withDetachedAutoKeepaliveForTest } from "../auto.ts";
|
|
7
|
+
import { _scheduleAutoStartAfterIdleForTest } from "../guided-flow.ts";
|
|
7
8
|
|
|
8
9
|
const gsdDir = resolve(import.meta.dirname, "..");
|
|
9
10
|
|
|
@@ -96,6 +97,23 @@ test("auto bootstrap validates blocked directories before touching .gsd migratio
|
|
|
96
97
|
);
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
test("fresh start registers the auto worker before bootstrap enters worktree flow (#5405)", () => {
|
|
101
|
+
const autoSrc = readGsdFile("auto.ts");
|
|
102
|
+
const startAutoIdx = autoSrc.indexOf("export async function startAuto(");
|
|
103
|
+
const startAutoBody = autoSrc.slice(startAutoIdx);
|
|
104
|
+
|
|
105
|
+
const preBootstrapRegisterIdx = startAutoBody.indexOf("registerAutoWorkerForSession(s, base);");
|
|
106
|
+
const bootstrapCallIdx = startAutoBody.indexOf("const ready = await bootstrapAutoSession(");
|
|
107
|
+
|
|
108
|
+
assert.ok(startAutoIdx > -1, "startAuto should exist");
|
|
109
|
+
assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
|
|
110
|
+
assert.ok(bootstrapCallIdx > -1, "startAuto should call bootstrapAutoSession");
|
|
111
|
+
assert.ok(
|
|
112
|
+
preBootstrapRegisterIdx < bootstrapCallIdx,
|
|
113
|
+
"worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
99
117
|
test("startAutoDetached reports failures asynchronously (#3733)", () => {
|
|
100
118
|
const autoSrc = readGsdFile("auto.ts");
|
|
101
119
|
|
|
@@ -185,3 +203,40 @@ test("detached auto-start preserves milestone lock across pause/stop cleanup (#3
|
|
|
185
203
|
"AutoSession should track the detached milestone lock explicitly",
|
|
186
204
|
);
|
|
187
205
|
});
|
|
206
|
+
|
|
207
|
+
test("discussion auto-start waits for the current command context to become idle", async () => {
|
|
208
|
+
let releaseIdle!: () => void;
|
|
209
|
+
const idle = new Promise<void>((resolveIdle) => {
|
|
210
|
+
releaseIdle = resolveIdle;
|
|
211
|
+
});
|
|
212
|
+
const launches: unknown[][] = [];
|
|
213
|
+
const ctx = {
|
|
214
|
+
waitForIdle: () => idle,
|
|
215
|
+
ui: {
|
|
216
|
+
notify: () => {},
|
|
217
|
+
},
|
|
218
|
+
} as any;
|
|
219
|
+
|
|
220
|
+
_scheduleAutoStartAfterIdleForTest(
|
|
221
|
+
ctx,
|
|
222
|
+
{} as any,
|
|
223
|
+
"/tmp/gsd-auto-start-idle-test",
|
|
224
|
+
false,
|
|
225
|
+
{ step: true },
|
|
226
|
+
(...args: unknown[]) => {
|
|
227
|
+
launches.push(args);
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await Promise.resolve();
|
|
232
|
+
assert.equal(launches.length, 0, "auto-start must not launch before waitForIdle resolves");
|
|
233
|
+
|
|
234
|
+
releaseIdle();
|
|
235
|
+
await Promise.resolve();
|
|
236
|
+
assert.equal(launches.length, 0, "auto-start should defer launch to the next timer turn");
|
|
237
|
+
|
|
238
|
+
await new Promise((resolveTimer) => setTimeout(resolveTimer, 0));
|
|
239
|
+
assert.equal(launches.length, 1);
|
|
240
|
+
assert.equal(launches[0][2], "/tmp/gsd-auto-start-idle-test");
|
|
241
|
+
assert.deepEqual(launches[0][4], { step: true });
|
|
242
|
+
});
|
|
@@ -44,4 +44,13 @@ describe('status opens DB before deriveState (#3691)', () => {
|
|
|
44
44
|
assert.match(quickSrc, /getIsolationMode\(\)\s*!==\s*"none"/,
|
|
45
45
|
'quick.ts should compare isolation mode against "none"');
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
test('quick task prompt handles external .gsd without staging quick files', () => {
|
|
49
|
+
assert.match(quickSrc, /isExternalGsdRoot/,
|
|
50
|
+
'quick.ts should detect whether .gsd resolves outside the project repo');
|
|
51
|
+
assert.match(quickSrc, /do not stage or commit `\.gsd\/quick\/\.\.\.`/,
|
|
52
|
+
'external-state quick tasks must tell the agent not to stage .gsd/quick files');
|
|
53
|
+
assert.match(quickSrc, /nothing in the project repo to commit/,
|
|
54
|
+
'external-state quick tasks should allow summary-only work without a git commit');
|
|
55
|
+
});
|
|
47
56
|
});
|