@opengsd/gsd-pi 1.3.0-dev.965d1788 → 1.3.0-dev.cca9c184
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/cli.js +17 -4
- package/dist/headless-answers.js +1 -1
- package/dist/headless-ui.d.ts +1 -1
- package/dist/headless-ui.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/models.js +9 -0
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-budget.js +5 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
- package/dist/resources/extensions/gsd/auto.js +19 -10
- package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +25 -2
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +15 -4
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands-usage.js +1 -1
- package/dist/resources/extensions/gsd/db/engine.js +4 -1
- package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
- package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
- package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
- package/dist/resources/extensions/gsd/git-service.js +56 -0
- package/dist/resources/extensions/gsd/gitignore.js +1 -1
- package/dist/resources/extensions/gsd/migrate-external.js +11 -2
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
- package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
- package/dist/resources/extensions/gsd/unit-registry.js +4 -2
- package/dist/resources/extensions/gsd/workflow-manifest.js +122 -12
- 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 +11 -11
- 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 +11 -11
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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 +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +3 -2
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +46 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +38 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/models.ts +9 -0
- package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- package/src/resources/extensions/gsd/auto-budget.ts +12 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -2
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +49 -1
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +16 -4
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands-usage.ts +1 -1
- package/src/resources/extensions/gsd/db/engine.ts +8 -1
- package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
- package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
- package/src/resources/extensions/gsd/doctor-types.ts +3 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
- package/src/resources/extensions/gsd/git-service.ts +63 -0
- package/src/resources/extensions/gsd/gitignore.ts +1 -1
- package/src/resources/extensions/gsd/migrate-external.ts +8 -2
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
- package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
- package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +44 -17
- package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
- package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
- package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
- package/src/resources/extensions/gsd/unit-registry.ts +4 -2
- package/src/resources/extensions/gsd/workflow-manifest.ts +137 -6
- /package/dist/web/standalone/.next/static/{-ji77QJck2IKIwrGza1Bs → Eud_8lddsyi_13HIlB2i7}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-ji77QJck2IKIwrGza1Bs → Eud_8lddsyi_13HIlB2i7}/_ssgManifest.js +0 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '../gsd-db.ts';
|
|
25
25
|
import {
|
|
26
26
|
writeManifest,
|
|
27
|
+
flushManifest,
|
|
27
28
|
readManifest,
|
|
28
29
|
snapshotState,
|
|
29
30
|
bootstrapFromManifest,
|
|
@@ -42,6 +43,11 @@ function cleanupDir(dirPath: string): void {
|
|
|
42
43
|
try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
async function writeManifestAndFlush(base: string): Promise<void> {
|
|
47
|
+
writeManifest(base);
|
|
48
|
+
await flushManifest(base);
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
function insertMemoryBackedDecision(id: string): void {
|
|
46
52
|
const now = '2026-01-01T00:00:00.000Z';
|
|
47
53
|
insertMemoryRow({
|
|
@@ -83,12 +89,14 @@ test('workflow-manifest: readManifest returns null when file does not exist', ()
|
|
|
83
89
|
|
|
84
90
|
// ─── writeManifest + readManifest round-trip ─────────────────────────────
|
|
85
91
|
|
|
86
|
-
test('workflow-manifest: writeManifest creates state-manifest.json with version 1', () => {
|
|
92
|
+
test('workflow-manifest: writeManifest creates state-manifest.json with version 1 after flush', async () => {
|
|
87
93
|
const base = tempDir();
|
|
88
94
|
openDatabase(tempDbPath(base));
|
|
89
95
|
try {
|
|
90
96
|
writeManifest(base);
|
|
91
97
|
const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
|
|
98
|
+
assert.equal(fs.existsSync(manifestPath), false, 'writeManifest should not synchronously write state-manifest.json');
|
|
99
|
+
await flushManifest(base);
|
|
92
100
|
assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist');
|
|
93
101
|
const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
94
102
|
assert.strictEqual(raw.version, 1);
|
|
@@ -98,11 +106,11 @@ test('workflow-manifest: writeManifest creates state-manifest.json with version
|
|
|
98
106
|
}
|
|
99
107
|
});
|
|
100
108
|
|
|
101
|
-
test('workflow-manifest: readManifest parses manifest written by writeManifest', () => {
|
|
109
|
+
test('workflow-manifest: readManifest parses manifest written by writeManifest', async () => {
|
|
102
110
|
const base = tempDir();
|
|
103
111
|
openDatabase(tempDbPath(base));
|
|
104
112
|
try {
|
|
105
|
-
|
|
113
|
+
await writeManifestAndFlush(base);
|
|
106
114
|
const manifest = readManifest(base);
|
|
107
115
|
assert.ok(manifest !== null);
|
|
108
116
|
assert.strictEqual(manifest!.version, 1);
|
|
@@ -124,6 +132,25 @@ test('workflow-manifest: readManifest parses manifest written by writeManifest',
|
|
|
124
132
|
}
|
|
125
133
|
});
|
|
126
134
|
|
|
135
|
+
test('workflow-manifest: flushManifest writes the latest queued payload', async () => {
|
|
136
|
+
const base = tempDir();
|
|
137
|
+
openDatabase(tempDbPath(base));
|
|
138
|
+
try {
|
|
139
|
+
insertMilestone({ id: 'M001', title: 'First Milestone' });
|
|
140
|
+
writeManifest(base);
|
|
141
|
+
insertMilestone({ id: 'M002', title: 'Latest Milestone' });
|
|
142
|
+
writeManifest(base);
|
|
143
|
+
|
|
144
|
+
await flushManifest(base);
|
|
145
|
+
|
|
146
|
+
const manifest = readManifest(base);
|
|
147
|
+
assert.ok(manifest?.milestones.some((m) => m.id === 'M002'), 'latest queued write should be durable');
|
|
148
|
+
} finally {
|
|
149
|
+
closeDatabase();
|
|
150
|
+
cleanupDir(base);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
127
154
|
// ─── snapshotState: captures DB rows ─────────────────────────────────────
|
|
128
155
|
|
|
129
156
|
test('workflow-manifest: snapshotState includes inserted milestone', () => {
|
|
@@ -181,7 +208,7 @@ test('workflow-manifest: snapshotState captures memory-backed decisions', () =>
|
|
|
181
208
|
}
|
|
182
209
|
});
|
|
183
210
|
|
|
184
|
-
test('workflow-manifest: bootstrapFromManifest restores extended correctness rows', () => {
|
|
211
|
+
test('workflow-manifest: bootstrapFromManifest restores extended correctness rows', async () => {
|
|
185
212
|
const base = tempDir();
|
|
186
213
|
openDatabase(tempDbPath(base));
|
|
187
214
|
try {
|
|
@@ -256,7 +283,7 @@ test('workflow-manifest: bootstrapFromManifest restores extended correctness row
|
|
|
256
283
|
createdAt: '2026-06-05T00:00:00.000Z',
|
|
257
284
|
});
|
|
258
285
|
|
|
259
|
-
|
|
286
|
+
await writeManifestAndFlush(base);
|
|
260
287
|
closeDatabase();
|
|
261
288
|
|
|
262
289
|
const newDbPath = path.join(base, 'new.db');
|
|
@@ -306,7 +333,7 @@ test('workflow-manifest: bootstrapFromManifest returns false when no manifest fi
|
|
|
306
333
|
}
|
|
307
334
|
});
|
|
308
335
|
|
|
309
|
-
test('workflow-manifest: bootstrapFromManifest restores DB from manifest (round-trip)', () => {
|
|
336
|
+
test('workflow-manifest: bootstrapFromManifest restores DB from manifest (round-trip)', async () => {
|
|
310
337
|
const base = tempDir();
|
|
311
338
|
openDatabase(tempDbPath(base));
|
|
312
339
|
try {
|
|
@@ -326,7 +353,7 @@ test('workflow-manifest: bootstrapFromManifest restores DB from manifest (round-
|
|
|
326
353
|
status: 'complete',
|
|
327
354
|
planning: { fullPlanMd: '# Full Task Plan\n\nKeep this body.', targetRepositories: ['project'] },
|
|
328
355
|
});
|
|
329
|
-
|
|
356
|
+
await writeManifestAndFlush(base);
|
|
330
357
|
closeDatabase();
|
|
331
358
|
|
|
332
359
|
// Open a fresh DB and bootstrap from manifest
|
|
@@ -356,14 +383,14 @@ test('workflow-manifest: bootstrapFromManifest restores DB from manifest (round-
|
|
|
356
383
|
}
|
|
357
384
|
});
|
|
358
385
|
|
|
359
|
-
test('workflow-manifest: bootstrapFromManifest preserves target_repositories', () => {
|
|
386
|
+
test('workflow-manifest: bootstrapFromManifest preserves target_repositories', async () => {
|
|
360
387
|
const base = tempDir();
|
|
361
388
|
openDatabase(tempDbPath(base));
|
|
362
389
|
try {
|
|
363
390
|
insertMilestone({ id: 'M001', title: 'Restored Milestone' });
|
|
364
391
|
insertSlice({ id: 'S01', milestoneId: 'M001', planning: { targetRepositories: ['frontend'] } });
|
|
365
392
|
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', planning: { targetRepositories: ['backend'] } });
|
|
366
|
-
|
|
393
|
+
await writeManifestAndFlush(base);
|
|
367
394
|
closeDatabase();
|
|
368
395
|
|
|
369
396
|
const newDbPath = path.join(base, 'new.db');
|
|
@@ -380,7 +407,7 @@ test('workflow-manifest: bootstrapFromManifest preserves target_repositories', (
|
|
|
380
407
|
}
|
|
381
408
|
});
|
|
382
409
|
|
|
383
|
-
test('workflow-manifest: bootstrapFromManifest does not truncate projections when artifact content is empty', () => {
|
|
410
|
+
test('workflow-manifest: bootstrapFromManifest does not truncate projections when artifact content is empty', async () => {
|
|
384
411
|
const base = tempDir();
|
|
385
412
|
openDatabase(tempDbPath(base));
|
|
386
413
|
try {
|
|
@@ -401,7 +428,7 @@ test('workflow-manifest: bootstrapFromManifest does not truncate projections whe
|
|
|
401
428
|
task_id: null,
|
|
402
429
|
full_content: '# Manifest Validation',
|
|
403
430
|
});
|
|
404
|
-
|
|
431
|
+
await writeManifestAndFlush(base);
|
|
405
432
|
|
|
406
433
|
const planPath = path.join(base, '.gsd', 'milestones', 'M001', 'M001-PLAN.md');
|
|
407
434
|
const validationPath = path.join(base, '.gsd', 'milestones', 'M001', 'M001-VALIDATION.md');
|
|
@@ -434,14 +461,14 @@ test('workflow-manifest: bootstrapFromManifest does not truncate projections whe
|
|
|
434
461
|
}
|
|
435
462
|
});
|
|
436
463
|
|
|
437
|
-
test('workflow-manifest: bootstrapFromManifest preserves optional rows from old manifests', () => {
|
|
464
|
+
test('workflow-manifest: bootstrapFromManifest preserves optional rows from old manifests', async () => {
|
|
438
465
|
const base = tempDir();
|
|
439
466
|
openDatabase(tempDbPath(base));
|
|
440
467
|
try {
|
|
441
468
|
insertMilestone({ id: 'M001', title: 'Old Manifest Milestone' });
|
|
442
469
|
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Old Manifest Slice' });
|
|
443
470
|
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Old Manifest Task' });
|
|
444
|
-
|
|
471
|
+
await writeManifestAndFlush(base);
|
|
445
472
|
|
|
446
473
|
const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
|
|
447
474
|
const oldManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;
|
|
@@ -504,12 +531,12 @@ test('workflow-manifest: bootstrapFromManifest preserves optional rows from old
|
|
|
504
531
|
}
|
|
505
532
|
});
|
|
506
533
|
|
|
507
|
-
test('workflow-manifest: bootstrapFromManifest restores memory-backed decisions', () => {
|
|
534
|
+
test('workflow-manifest: bootstrapFromManifest restores memory-backed decisions', async () => {
|
|
508
535
|
const base = tempDir();
|
|
509
536
|
openDatabase(tempDbPath(base));
|
|
510
537
|
try {
|
|
511
538
|
insertMemoryBackedDecision('D901');
|
|
512
|
-
|
|
539
|
+
await writeManifestAndFlush(base);
|
|
513
540
|
closeDatabase();
|
|
514
541
|
|
|
515
542
|
const newDbPath = path.join(base, 'new.db');
|
|
@@ -526,12 +553,12 @@ test('workflow-manifest: bootstrapFromManifest restores memory-backed decisions'
|
|
|
526
553
|
}
|
|
527
554
|
});
|
|
528
555
|
|
|
529
|
-
test('workflow-manifest: bootstrapFromManifest replaces stale memory-backed decisions', () => {
|
|
556
|
+
test('workflow-manifest: bootstrapFromManifest replaces stale memory-backed decisions', async () => {
|
|
530
557
|
const base = tempDir();
|
|
531
558
|
openDatabase(tempDbPath(base));
|
|
532
559
|
try {
|
|
533
560
|
insertMemoryBackedDecision('D902');
|
|
534
|
-
|
|
561
|
+
await writeManifestAndFlush(base);
|
|
535
562
|
closeDatabase();
|
|
536
563
|
|
|
537
564
|
const newDbPath = path.join(base, 'new.db');
|
|
@@ -29,6 +29,7 @@ test("preflight uses inline workflow MCP config when no project config is persis
|
|
|
29
29
|
`const { StdioServerTransport } = await import(${JSON.stringify(stdioModuleUrl)});`,
|
|
30
30
|
'const server = new McpServer({ name: "fake", version: "1.0.0" }, { capabilities: { tools: {} } });',
|
|
31
31
|
'server.tool("gsd_plan_slice", "Plan slice", {}, async () => ({ content: [{ type: "text", text: "ok" }] }));',
|
|
32
|
+
'server.tool("gsd_plan_task", "Plan task", {}, async () => ({ content: [{ type: "text", text: "ok" }] }));',
|
|
32
33
|
'server.tool("gsd_reassess_roadmap", "Reassess roadmap", {}, async () => ({ content: [{ type: "text", text: "ok" }] }));',
|
|
33
34
|
'await server.connect(new StdioServerTransport());',
|
|
34
35
|
].join("\n"),
|
|
@@ -65,14 +65,18 @@ test("auto execute-task requires canonical task completion tool", () => {
|
|
|
65
65
|
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("execute-task"), expected);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
test("plan-slice requires planning and roadmap reassessment tools", () => {
|
|
69
|
-
|
|
68
|
+
test("plan-slice requires slice + incremental task planning and roadmap reassessment tools", () => {
|
|
69
|
+
// Incremental planning (#1027): gsd_plan_slice persists metadata, then
|
|
70
|
+
// gsd_plan_task adds tasks one at a time, so both must be on the surface.
|
|
71
|
+
const expected = ["gsd_plan_slice", "gsd_plan_task", "gsd_reassess_roadmap"];
|
|
70
72
|
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("plan-slice"), expected);
|
|
71
73
|
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("plan-slice"), expected);
|
|
72
74
|
});
|
|
73
75
|
|
|
74
|
-
test("plan-milestone requires status, roadmap, and
|
|
75
|
-
|
|
76
|
+
test("plan-milestone requires status, roadmap, and slice + task planning tools", () => {
|
|
77
|
+
// gsd_plan_task is required alongside gsd_plan_slice for incremental
|
|
78
|
+
// milestone planning (#1027).
|
|
79
|
+
const expected = ["gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice", "gsd_plan_task"];
|
|
76
80
|
assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("plan-milestone"), expected);
|
|
77
81
|
assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("plan-milestone"), expected);
|
|
78
82
|
});
|
|
@@ -781,22 +781,23 @@ test("executePlanSlice writes task planning state and rendered plan artifacts",
|
|
|
781
781
|
}
|
|
782
782
|
});
|
|
783
783
|
|
|
784
|
-
test("executePlanSlice
|
|
784
|
+
test("executePlanSlice accepts metadata-only incremental planning payloads", async () => {
|
|
785
785
|
const base = makeTmpBase();
|
|
786
786
|
try {
|
|
787
787
|
openTestDb(base);
|
|
788
|
+
await inProjectDir(base, () => executePlanMilestone(validMilestonePlan(), base));
|
|
788
789
|
|
|
789
790
|
const result = await inProjectDir(base, () => executePlanSlice({
|
|
790
791
|
milestoneId: "M001",
|
|
791
792
|
sliceId: "S01",
|
|
792
|
-
goal: "
|
|
793
|
+
goal: "Persist slice metadata before tasks.",
|
|
793
794
|
tasks: [],
|
|
794
795
|
}, base));
|
|
795
796
|
|
|
796
|
-
assert.equal(result.isError, true);
|
|
797
797
|
assert.equal(result.details.operation, "plan_slice");
|
|
798
|
-
assert.
|
|
799
|
-
assert.
|
|
798
|
+
assert.equal(result.details.sliceId, "S01");
|
|
799
|
+
assert.equal(result.isError, undefined);
|
|
800
|
+
assert.match(result.content[0].text, /Planned slice S01/);
|
|
800
801
|
} finally {
|
|
801
802
|
closeDatabase();
|
|
802
803
|
cleanup(base);
|
|
@@ -51,7 +51,7 @@ export interface PlanSliceParams {
|
|
|
51
51
|
milestoneId: string;
|
|
52
52
|
sliceId: string;
|
|
53
53
|
goal: string;
|
|
54
|
-
tasks
|
|
54
|
+
tasks?: PlanSliceTaskInput[];
|
|
55
55
|
/** @optional — omitted fields render as conservative defaults */
|
|
56
56
|
successCriteria?: string;
|
|
57
57
|
/** @optional — omitted fields render as conservative defaults */
|
|
@@ -86,10 +86,12 @@ function validateRepositoryTargetIds(
|
|
|
86
86
|
return deduped;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function validateTasks(value: unknown): PlanSliceTaskInput[] {
|
|
90
|
-
if (
|
|
91
|
-
|
|
89
|
+
function validateTasks(value: unknown): PlanSliceTaskInput[] | undefined {
|
|
90
|
+
if (value === undefined) return undefined;
|
|
91
|
+
if (!Array.isArray(value)) {
|
|
92
|
+
throw new Error("tasks must be an array");
|
|
92
93
|
}
|
|
94
|
+
if (value.length === 0) return undefined;
|
|
93
95
|
|
|
94
96
|
const seen = new Set<string>();
|
|
95
97
|
return value.map((entry, index) => {
|
|
@@ -200,7 +202,7 @@ function validateReferencedRepositories(
|
|
|
200
202
|
};
|
|
201
203
|
|
|
202
204
|
for (const id of params.targetRepositories ?? defaultTargets) noteMissing(id);
|
|
203
|
-
for (const task of params.tasks) {
|
|
205
|
+
for (const task of params.tasks ?? []) {
|
|
204
206
|
for (const id of task.targetRepositories ?? params.targetRepositories ?? defaultTargets) noteMissing(id);
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -211,7 +213,7 @@ function validateReferencedRepositories(
|
|
|
211
213
|
function resolveAllowedRootsForPathScope(params: PlanSliceParams, registry: RepositoryRegistry, defaultTargets: string[]): string[] {
|
|
212
214
|
const requested = new Set<string>();
|
|
213
215
|
for (const id of params.targetRepositories ?? defaultTargets) requested.add(id);
|
|
214
|
-
for (const task of params.tasks) {
|
|
216
|
+
for (const task of params.tasks ?? []) {
|
|
215
217
|
for (const id of task.targetRepositories ?? params.targetRepositories ?? defaultTargets) requested.add(id);
|
|
216
218
|
}
|
|
217
219
|
if (requested.size === 0) return [registry.projectRoot];
|
|
@@ -222,7 +224,7 @@ function resolveAllowedRootsForPathScope(params: PlanSliceParams, registry: Repo
|
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
function toTaskRows(params: PlanSliceParams, defaultTargets: string[]): TaskRow[] {
|
|
225
|
-
return params.tasks.map((task, index) => ({
|
|
227
|
+
return (params.tasks ?? []).map((task, index) => ({
|
|
226
228
|
milestone_id: params.milestoneId,
|
|
227
229
|
slice_id: params.sliceId,
|
|
228
230
|
id: task.taskId,
|
|
@@ -309,6 +311,8 @@ export async function handlePlanSlice(
|
|
|
309
311
|
return { error: `validation failed: ${message}` };
|
|
310
312
|
}
|
|
311
313
|
const defaultTargets = defaultRepositoryTargets(repositoryRegistry);
|
|
314
|
+
const taskPayload = params.tasks ?? [];
|
|
315
|
+
const hasTaskPayload = taskPayload.length > 0;
|
|
312
316
|
const repoValidationError = validateReferencedRepositories(params, repositoryRegistry, defaultTargets);
|
|
313
317
|
if (repoValidationError) {
|
|
314
318
|
return { error: `validation failed: ${repoValidationError}` };
|
|
@@ -317,7 +321,7 @@ export async function handlePlanSlice(
|
|
|
317
321
|
const allowedAbsoluteRoots = resolveAllowedRootsForPathScope(params, repositoryRegistry, defaultTargets);
|
|
318
322
|
|
|
319
323
|
const pathOnlyError = validatePathOnlyPlanningFields(
|
|
320
|
-
|
|
324
|
+
taskPayload.map((task, index) => ({
|
|
321
325
|
field: `tasks[${index}].expectedOutput`,
|
|
322
326
|
values: task.expectedOutput,
|
|
323
327
|
})),
|
|
@@ -328,7 +332,7 @@ export async function handlePlanSlice(
|
|
|
328
332
|
|
|
329
333
|
const pathScopeError = validatePlanningPathScope(
|
|
330
334
|
basePath,
|
|
331
|
-
|
|
335
|
+
taskPayload.flatMap((task, index) => [
|
|
332
336
|
{ field: `tasks[${index}].files`, values: task.files },
|
|
333
337
|
{ field: `tasks[${index}].inputs`, values: task.inputs },
|
|
334
338
|
{ field: `tasks[${index}].expectedOutput`, values: task.expectedOutput },
|
|
@@ -372,16 +376,18 @@ export async function handlePlanSlice(
|
|
|
372
376
|
return;
|
|
373
377
|
}
|
|
374
378
|
|
|
375
|
-
const newTaskIds = new Set(
|
|
379
|
+
const newTaskIds = new Set(taskPayload.map((task) => task.taskId));
|
|
376
380
|
const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
381
|
+
if (hasTaskPayload) {
|
|
382
|
+
omittedTaskIds = existingTasks
|
|
383
|
+
.filter((task) => !newTaskIds.has(task.id))
|
|
384
|
+
.map((task) => task.id);
|
|
385
|
+
|
|
386
|
+
for (const task of existingTasks) {
|
|
387
|
+
if (!newTaskIds.has(task.id) && isClosedStatus(task.status)) {
|
|
388
|
+
guardError = `cannot remove completed task ${task.id}`;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
385
391
|
}
|
|
386
392
|
}
|
|
387
393
|
|
|
@@ -398,35 +404,37 @@ export async function handlePlanSlice(
|
|
|
398
404
|
targetRepositories: params.targetRepositories ?? defaultTargets,
|
|
399
405
|
});
|
|
400
406
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
407
|
+
if (hasTaskPayload) {
|
|
408
|
+
for (const taskId of omittedTaskIds) {
|
|
409
|
+
deleteTask(params.milestoneId, params.sliceId, taskId);
|
|
410
|
+
}
|
|
404
411
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
title: task.title,
|
|
409
|
-
description: task.description,
|
|
410
|
-
estimate: task.estimate,
|
|
411
|
-
files: task.files,
|
|
412
|
-
verify: task.verify,
|
|
413
|
-
inputs: task.inputs,
|
|
414
|
-
expectedOutput: task.expectedOutput,
|
|
415
|
-
observabilityImpact: task.observabilityImpact ?? "",
|
|
416
|
-
fullPlanMd: task.fullPlanMd,
|
|
417
|
-
targetRepositories: task.targetRepositories ?? params.targetRepositories ?? defaultTargets,
|
|
418
|
-
};
|
|
419
|
-
const existingTask = existingTaskById.get(task.taskId);
|
|
420
|
-
if (!existingTask || !isClosedStatus(existingTask.status)) {
|
|
421
|
-
insertTask({
|
|
422
|
-
id: task.taskId,
|
|
423
|
-
sliceId: params.sliceId,
|
|
424
|
-
milestoneId: params.milestoneId,
|
|
412
|
+
const existingTaskById = new Map(existingTasks.map((task) => [task.id, task]));
|
|
413
|
+
for (const task of taskPayload) {
|
|
414
|
+
const planning = {
|
|
425
415
|
title: task.title,
|
|
426
|
-
|
|
427
|
-
|
|
416
|
+
description: task.description,
|
|
417
|
+
estimate: task.estimate,
|
|
418
|
+
files: task.files,
|
|
419
|
+
verify: task.verify,
|
|
420
|
+
inputs: task.inputs,
|
|
421
|
+
expectedOutput: task.expectedOutput,
|
|
422
|
+
observabilityImpact: task.observabilityImpact ?? "",
|
|
423
|
+
fullPlanMd: task.fullPlanMd,
|
|
424
|
+
targetRepositories: task.targetRepositories ?? params.targetRepositories ?? defaultTargets,
|
|
425
|
+
};
|
|
426
|
+
const existingTask = existingTaskById.get(task.taskId);
|
|
427
|
+
if (!existingTask || !isClosedStatus(existingTask.status)) {
|
|
428
|
+
insertTask({
|
|
429
|
+
id: task.taskId,
|
|
430
|
+
sliceId: params.sliceId,
|
|
431
|
+
milestoneId: params.milestoneId,
|
|
432
|
+
title: task.title,
|
|
433
|
+
status: "pending",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
upsertTaskPlanning(params.milestoneId, params.sliceId, task.taskId, planning);
|
|
428
437
|
}
|
|
429
|
-
upsertTaskPlanning(params.milestoneId, params.sliceId, task.taskId, planning);
|
|
430
438
|
}
|
|
431
439
|
|
|
432
440
|
// Seed quality gate rows inside the transaction — all-or-nothing with
|
|
@@ -436,7 +444,7 @@ export async function handlePlanSlice(
|
|
|
436
444
|
insertGateRow({ milestoneId: params.milestoneId, sliceId: params.sliceId, gateId: gid, scope: "slice" });
|
|
437
445
|
}
|
|
438
446
|
const taskGates = resolveTaskGates(gateEvaluation);
|
|
439
|
-
for (const task of
|
|
447
|
+
for (const task of taskPayload) {
|
|
440
448
|
for (const gid of taskGates) {
|
|
441
449
|
insertGateRow({ milestoneId: params.milestoneId, sliceId: params.sliceId, gateId: gid, scope: "task", taskId: task.taskId });
|
|
442
450
|
}
|
|
@@ -460,8 +468,13 @@ export async function handlePlanSlice(
|
|
|
460
468
|
deleteArtifactByPath(artifactPath);
|
|
461
469
|
}
|
|
462
470
|
|
|
463
|
-
const
|
|
464
|
-
|
|
471
|
+
const sliceTasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
472
|
+
const renderResult = sliceTasks.length === 0
|
|
473
|
+
? { planPath: "", taskPlanPaths: [] as string[] }
|
|
474
|
+
: await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
|
|
475
|
+
if (sliceTasks.length > 0) {
|
|
476
|
+
setSliceSketchFlag(params.milestoneId, params.sliceId, false);
|
|
477
|
+
}
|
|
465
478
|
invalidateStateCache();
|
|
466
479
|
clearParseCache();
|
|
467
480
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
2
|
import { isClosedStatus } from "../status-guards.js";
|
|
3
3
|
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getGateIdsForTurn } from "../gate-registry.js";
|
|
5
|
+
import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning, insertGateRow, setSliceSketchFlag } from "../gsd-db.js";
|
|
5
6
|
import { invalidateStateCache } from "../state.js";
|
|
6
7
|
import { renderTaskPlanFromDb, renderPlanFromDb } from "../markdown-renderer.js";
|
|
7
|
-
import {
|
|
8
|
+
import { resolveTasksDir } from "../paths.js";
|
|
8
9
|
import { flushWorkflowProjections } from "../projection-flush.js";
|
|
9
10
|
import { writeManifest } from "../workflow-manifest.js";
|
|
10
11
|
import { appendEvent } from "../workflow-events.js";
|
|
11
12
|
import { logWarning } from "../workflow-logger.js";
|
|
13
|
+
import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
12
14
|
import { validatePathOnlyPlanningFields, validatePlanningPathScope } from "../planning-path-scope.js";
|
|
15
|
+
import type { GateId } from "../types.js";
|
|
13
16
|
|
|
14
17
|
export interface PlanTaskParams {
|
|
15
18
|
milestoneId: string;
|
|
@@ -57,6 +60,12 @@ function validateParams(params: PlanTaskParams): PlanTaskParams {
|
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function resolveTaskGates(basePath: string): GateId[] {
|
|
64
|
+
const loaded = loadEffectiveGSDPreferences(basePath);
|
|
65
|
+
if (loaded?.preferences?.gate_evaluation?.task_gates === false) return [];
|
|
66
|
+
return [...getGateIdsForTurn("execute-task")];
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
export async function handlePlanTask(
|
|
61
70
|
rawParams: PlanTaskParams,
|
|
62
71
|
basePath: string,
|
|
@@ -84,6 +93,14 @@ export async function handlePlanTask(
|
|
|
84
93
|
return { error: `validation failed: ${pathScopeError}` };
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
let taskGates: GateId[];
|
|
97
|
+
try {
|
|
98
|
+
taskGates = resolveTaskGates(basePath);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
101
|
+
return { error: `validation failed: ${message}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
|
|
88
105
|
// Guards must be inside the transaction so the state they check cannot
|
|
89
106
|
// change between the read and the write (#2723).
|
|
@@ -127,6 +144,9 @@ export async function handlePlanTask(
|
|
|
127
144
|
observabilityImpact: params.observabilityImpact ?? "",
|
|
128
145
|
fullPlanMd: params.fullPlanMd,
|
|
129
146
|
});
|
|
147
|
+
for (const gid of taskGates) {
|
|
148
|
+
insertGateRow({ milestoneId: params.milestoneId, sliceId: params.sliceId, gateId: gid, scope: "task", taskId: params.taskId });
|
|
149
|
+
}
|
|
130
150
|
});
|
|
131
151
|
} catch (err) {
|
|
132
152
|
return { error: `db write failed: ${(err as Error).message}` };
|
|
@@ -142,17 +162,22 @@ export async function handlePlanTask(
|
|
|
142
162
|
// Flat-phase: tasks live as checkboxes in the slice plan's <tasks> block,
|
|
143
163
|
// not as standalone TID-PLAN.md files. Re-render the slice plan so the
|
|
144
164
|
// new/updated task appears in the plan file that gsd-core reads.
|
|
145
|
-
// Guard: resolveTasksDir is null in flat-phase (no tasks/ subdir exists)
|
|
146
|
-
|
|
165
|
+
// Guard: resolveTasksDir is null in flat-phase (no tasks/ subdir exists).
|
|
166
|
+
let slicePlanSynced = false;
|
|
147
167
|
try {
|
|
148
168
|
const tDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
|
|
149
|
-
if (!tDir
|
|
169
|
+
if (!tDir) {
|
|
150
170
|
await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
|
|
171
|
+
slicePlanSynced = true;
|
|
151
172
|
}
|
|
152
173
|
} catch (syncErr) {
|
|
153
174
|
logWarning("tool", `plan-task: slice-plan sync failed: ${(syncErr as Error).message}`);
|
|
154
175
|
}
|
|
155
176
|
|
|
177
|
+
if (slicePlanSynced) {
|
|
178
|
+
setSliceSketchFlag(params.milestoneId, params.sliceId, false);
|
|
179
|
+
}
|
|
180
|
+
|
|
156
181
|
invalidateStateCache();
|
|
157
182
|
clearParseCache();
|
|
158
183
|
|
|
@@ -117,10 +117,11 @@ export const UNIT_REGISTRY = {
|
|
|
117
117
|
"gsd_milestone_status",
|
|
118
118
|
"gsd_plan_milestone",
|
|
119
119
|
"gsd_plan_slice",
|
|
120
|
+
"gsd_plan_task",
|
|
120
121
|
"gsd_decision_save",
|
|
121
122
|
"gsd_requirement_update",
|
|
122
123
|
],
|
|
123
|
-
requiredWorkflowTools: ["gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice"],
|
|
124
|
+
requiredWorkflowTools: ["gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice", "gsd_plan_task"],
|
|
124
125
|
},
|
|
125
126
|
},
|
|
126
127
|
"discuss-milestone": {
|
|
@@ -219,13 +220,14 @@ export const UNIT_REGISTRY = {
|
|
|
219
220
|
allowedGsdTools: [
|
|
220
221
|
"gsd_milestone_status",
|
|
221
222
|
"gsd_plan_slice",
|
|
223
|
+
"gsd_plan_task",
|
|
222
224
|
"gsd_reassess_roadmap",
|
|
223
225
|
"gsd_decision_save",
|
|
224
226
|
"gsd_exec",
|
|
225
227
|
"gsd_exec_search",
|
|
226
228
|
"gsd_resume",
|
|
227
229
|
],
|
|
228
|
-
requiredWorkflowTools: ["gsd_plan_slice", "gsd_reassess_roadmap"],
|
|
230
|
+
requiredWorkflowTools: ["gsd_plan_slice", "gsd_plan_task", "gsd_reassess_roadmap"],
|
|
229
231
|
},
|
|
230
232
|
},
|
|
231
233
|
"refine-slice": {
|