@opengsd/gsd-pi 1.1.1-dev.2034b16 → 1.1.1-dev.595401e
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +21 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +15 -6
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
- package/dist/resources/extensions/gsd/browser-evidence.js +29 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +2 -2
- package/dist/resources/extensions/gsd/post-unit-hooks.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +40 -22
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
- package/dist/resources/extensions/gsd/rule-registry.js +428 -52
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +46 -16
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +29 -14
- package/dist/resources/extensions/gsd/verdict-parser.js +59 -15
- package/dist/rtk.d.ts +7 -1
- package/dist/rtk.js +27 -11
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- 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 +7 -7
- 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/dist/session/agent-session-compaction.d.ts +2 -0
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.js +8 -2
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +23 -9
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- 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 +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +17 -17
- package/packages/pi-ai/dist/models.generated.js +19 -19
- 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 +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +28 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +16 -6
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
- package/src/resources/extensions/gsd/browser-evidence.ts +26 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +2 -2
- package/src/resources/extensions/gsd/post-unit-hooks.ts +14 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +36 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +40 -22
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
- package/src/resources/extensions/gsd/rule-registry.ts +558 -58
- package/src/resources/extensions/gsd/rule-types.ts +2 -0
- package/src/resources/extensions/gsd/tests/browser-evidence.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime-checks.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +66 -10
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +74 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +46 -15
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +31 -14
- package/src/resources/extensions/gsd/types.ts +63 -0
- package/src/resources/extensions/gsd/verdict-parser.ts +54 -13
- /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → IDKjyRHLIaumjgonPcYiX}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → IDKjyRHLIaumjgonPcYiX}/_ssgManifest.js +0 -0
|
@@ -8,6 +8,7 @@ import { mock } from "node:test";
|
|
|
8
8
|
import { postUnitPostVerification, type PostUnitContext } from "../auto-post-unit.ts";
|
|
9
9
|
import { AutoSession } from "../auto/session.ts";
|
|
10
10
|
import { checkPostUnitHooks, resetHookState, resolveHookArtifactPath } from "../post-unit-hooks.ts";
|
|
11
|
+
import { emitJournalEvent } from "../journal.ts";
|
|
11
12
|
import { _clearGsdRootCache } from "../paths.ts";
|
|
12
13
|
import { invalidateAllCaches } from "../cache.ts";
|
|
13
14
|
|
|
@@ -28,6 +29,43 @@ post_unit_hooks:
|
|
|
28
29
|
writeFileSync(join(basePath, ".gsd", "PREFERENCES.md"), content, "utf-8");
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function writeFailingHookPreferences(basePath: string): void {
|
|
33
|
+
const content = `---
|
|
34
|
+
post_unit_hooks:
|
|
35
|
+
- name: review-arbiter
|
|
36
|
+
after:
|
|
37
|
+
- execute-task
|
|
38
|
+
prompt: Review {taskId}
|
|
39
|
+
artifact: REVIEW-DEBATE.md
|
|
40
|
+
max_cycles: 1
|
|
41
|
+
enabled: true
|
|
42
|
+
- name: follow-up-review
|
|
43
|
+
after:
|
|
44
|
+
- execute-task
|
|
45
|
+
prompt: Follow-up review {taskId}
|
|
46
|
+
enabled: true
|
|
47
|
+
---
|
|
48
|
+
`;
|
|
49
|
+
writeFileSync(join(basePath, ".gsd", "PREFERENCES.md"), content, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeBlockingPreferences(basePath: string): void {
|
|
53
|
+
const content = `---
|
|
54
|
+
post_unit_hooks:
|
|
55
|
+
- name: review-arbiter
|
|
56
|
+
after:
|
|
57
|
+
- execute-task
|
|
58
|
+
prompt: Review {taskId}
|
|
59
|
+
agent: arbiter
|
|
60
|
+
artifact: REVIEW-DEBATE.md
|
|
61
|
+
criticality: blocking
|
|
62
|
+
max_cycles: 2
|
|
63
|
+
enabled: true
|
|
64
|
+
---
|
|
65
|
+
`;
|
|
66
|
+
writeFileSync(join(basePath, ".gsd", "PREFERENCES.md"), content, "utf-8");
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
test("post-unit retry_on marks trigger unit as retry in orchestrator before redispatch", async () => {
|
|
32
70
|
const originalCwd = process.cwd();
|
|
33
71
|
const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-retry-"));
|
|
@@ -91,3 +129,144 @@ test("post-unit retry_on marks trigger unit as retry in orchestrator before redi
|
|
|
91
129
|
rmSync(base, { recursive: true, force: true });
|
|
92
130
|
}
|
|
93
131
|
});
|
|
132
|
+
|
|
133
|
+
test("failed post-unit hook pauses auto-mode even when its artifact exists", async () => {
|
|
134
|
+
const originalCwd = process.cwd();
|
|
135
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-hook-failed-"));
|
|
136
|
+
const taskDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
|
|
137
|
+
mkdirSync(taskDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
process.chdir(base);
|
|
141
|
+
_clearGsdRootCache();
|
|
142
|
+
invalidateAllCaches();
|
|
143
|
+
resetHookState();
|
|
144
|
+
writeFailingHookPreferences(base);
|
|
145
|
+
|
|
146
|
+
const hookDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
147
|
+
assert.equal(hookDispatch?.hookName, "review-arbiter");
|
|
148
|
+
|
|
149
|
+
const artifactPath = resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-DEBATE.md");
|
|
150
|
+
writeFileSync(artifactPath, "partial review", "utf-8");
|
|
151
|
+
emitJournalEvent(base, {
|
|
152
|
+
ts: "2026-06-03T12:00:00.000Z",
|
|
153
|
+
flowId: "flow-hook-failed",
|
|
154
|
+
seq: 3,
|
|
155
|
+
eventType: "unit-end",
|
|
156
|
+
data: {
|
|
157
|
+
unitType: "hook/review-arbiter",
|
|
158
|
+
unitId: "M001/S01/T01",
|
|
159
|
+
status: "cancelled",
|
|
160
|
+
artifactVerified: false,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const pauseAuto = mock.fn(async () => {});
|
|
165
|
+
const notifications: string[] = [];
|
|
166
|
+
const s = new AutoSession();
|
|
167
|
+
s.basePath = base;
|
|
168
|
+
s.active = true;
|
|
169
|
+
s.currentUnit = { type: "hook/review-arbiter", id: "M001/S01/T01", startedAt: Date.now() };
|
|
170
|
+
|
|
171
|
+
const pctx: PostUnitContext = {
|
|
172
|
+
s,
|
|
173
|
+
ctx: {
|
|
174
|
+
ui: {
|
|
175
|
+
notify: (message: string) => { notifications.push(message); },
|
|
176
|
+
setStatus: () => {},
|
|
177
|
+
setWidget: () => {},
|
|
178
|
+
setFooter: () => {},
|
|
179
|
+
},
|
|
180
|
+
model: { id: "test-model" },
|
|
181
|
+
} as any,
|
|
182
|
+
pi: { sendMessage: async () => {}, setModel: async () => true } as any,
|
|
183
|
+
buildSnapshotOpts: () => ({}),
|
|
184
|
+
lockBase: () => base,
|
|
185
|
+
stopAuto: async () => {},
|
|
186
|
+
pauseAuto,
|
|
187
|
+
updateProgressWidget: () => {},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = await postUnitPostVerification(pctx);
|
|
191
|
+
assert.equal(result, "stopped");
|
|
192
|
+
assert.equal(pauseAuto.mock.callCount(), 1);
|
|
193
|
+
assert.ok(
|
|
194
|
+
notifications.some(message => message.includes("Post-unit hook review-arbiter failed")),
|
|
195
|
+
"pause notification should explain the failed hook",
|
|
196
|
+
);
|
|
197
|
+
} finally {
|
|
198
|
+
process.chdir(originalCwd);
|
|
199
|
+
resetHookState();
|
|
200
|
+
invalidateAllCaches();
|
|
201
|
+
_clearGsdRootCache();
|
|
202
|
+
rmSync(base, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("post-unit blocking gate pauses auto-mode on needs-attention verdict", async () => {
|
|
207
|
+
const originalCwd = process.cwd();
|
|
208
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-gate-"));
|
|
209
|
+
const taskDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
|
|
210
|
+
mkdirSync(taskDir, { recursive: true });
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
process.chdir(base);
|
|
214
|
+
_clearGsdRootCache();
|
|
215
|
+
invalidateAllCaches();
|
|
216
|
+
resetHookState();
|
|
217
|
+
writeBlockingPreferences(base);
|
|
218
|
+
|
|
219
|
+
const hookDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
220
|
+
assert.ok(hookDispatch, "hook should dispatch for execute-task");
|
|
221
|
+
|
|
222
|
+
const artifactPath = resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-DEBATE.md");
|
|
223
|
+
writeFileSync(artifactPath, "---\nverdict: needs-attention\n---\n\nManual review required.\n", "utf-8");
|
|
224
|
+
|
|
225
|
+
const pauseAuto = mock.fn(async () => {});
|
|
226
|
+
const s = new AutoSession();
|
|
227
|
+
s.basePath = base;
|
|
228
|
+
s.active = true;
|
|
229
|
+
s.currentUnit = { type: "hook/review-arbiter", id: "M001/S01/T01", startedAt: Date.now() };
|
|
230
|
+
s.orchestration = {
|
|
231
|
+
start: async () => ({ kind: "started" }),
|
|
232
|
+
advance: async () => ({ kind: "stopped", reason: "unused" }),
|
|
233
|
+
completeActiveUnit: async () => {},
|
|
234
|
+
retryActiveUnit: async () => {},
|
|
235
|
+
resume: async () => ({ kind: "resumed" }),
|
|
236
|
+
stop: async (reason: string) => ({ kind: "stopped", reason }),
|
|
237
|
+
getStatus: () => ({ phase: "running", transitionCount: 0 }),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const notifications: string[] = [];
|
|
241
|
+
const pctx: PostUnitContext = {
|
|
242
|
+
s,
|
|
243
|
+
ctx: {
|
|
244
|
+
ui: {
|
|
245
|
+
notify: (message: string) => { notifications.push(message); },
|
|
246
|
+
setStatus: () => {},
|
|
247
|
+
setWidget: () => {},
|
|
248
|
+
setFooter: () => {},
|
|
249
|
+
},
|
|
250
|
+
model: { id: "test-model" },
|
|
251
|
+
} as any,
|
|
252
|
+
pi: { sendMessage: async () => {}, setModel: async () => true } as any,
|
|
253
|
+
buildSnapshotOpts: () => ({}),
|
|
254
|
+
lockBase: () => base,
|
|
255
|
+
stopAuto: async () => {},
|
|
256
|
+
pauseAuto,
|
|
257
|
+
updateProgressWidget: () => {},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = await postUnitPostVerification(pctx);
|
|
261
|
+
assert.equal(result, "stopped");
|
|
262
|
+
assert.equal(pauseAuto.mock.callCount(), 1);
|
|
263
|
+
assert.match(notifications.join("\n"), /Post-unit gate "review-arbiter" blocked execute-task M001\/S01\/T01/);
|
|
264
|
+
assert.match(notifications.join("\n"), /\/gsd status/);
|
|
265
|
+
} finally {
|
|
266
|
+
process.chdir(originalCwd);
|
|
267
|
+
resetHookState();
|
|
268
|
+
invalidateAllCaches();
|
|
269
|
+
_clearGsdRootCache();
|
|
270
|
+
rmSync(base, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
@@ -561,6 +561,35 @@ test("post-unit hook max_cycles clamping via validatePreferences", () => {
|
|
|
561
561
|
assert.equal(p4.post_unit_hooks![0].max_cycles, 3, "valid value passes through");
|
|
562
562
|
});
|
|
563
563
|
|
|
564
|
+
test("post-unit hook criticality and on_block validation", () => {
|
|
565
|
+
const base = { name: "h", after: ["execute-task"], prompt: "do something", artifact: "REVIEW.md" };
|
|
566
|
+
|
|
567
|
+
const { preferences, errors } = validatePreferences({
|
|
568
|
+
post_unit_hooks: [{
|
|
569
|
+
...base,
|
|
570
|
+
criticality: "blocking",
|
|
571
|
+
on_block: { action: "retry-unit", artifact: "NEEDS-REWORK.md" },
|
|
572
|
+
}],
|
|
573
|
+
} as any);
|
|
574
|
+
assert.equal(errors.length, 0);
|
|
575
|
+
assert.equal(preferences.post_unit_hooks![0].criticality, "blocking");
|
|
576
|
+
assert.deepEqual(preferences.post_unit_hooks![0].on_block, {
|
|
577
|
+
action: "retry-unit",
|
|
578
|
+
artifact: "NEEDS-REWORK.md",
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const missingArtifact = validatePreferences({
|
|
582
|
+
post_unit_hooks: [{ name: "blocking", after: ["execute-task"], prompt: "do something", criticality: "blocking" }],
|
|
583
|
+
} as any);
|
|
584
|
+
assert.match(missingArtifact.errors.join("\n"), /criticality blocking requires artifact/);
|
|
585
|
+
assert.equal(missingArtifact.preferences.post_unit_hooks, undefined);
|
|
586
|
+
|
|
587
|
+
const invalidAction = validatePreferences({
|
|
588
|
+
post_unit_hooks: [{ ...base, on_block: { action: "teleport" } }],
|
|
589
|
+
} as any);
|
|
590
|
+
assert.match(invalidAction.errors.join("\n"), /invalid on_block action/);
|
|
591
|
+
});
|
|
592
|
+
|
|
564
593
|
test("pre-dispatch hook action validation via validatePreferences", () => {
|
|
565
594
|
const base = { name: "h", before: ["execute-task"] };
|
|
566
595
|
|
|
@@ -2,6 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.ts";
|
|
5
6
|
|
|
6
7
|
const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
|
|
7
8
|
const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
|
|
@@ -25,11 +26,15 @@ test("reactive-execute prompt keeps task summaries with subagents and avoids bat
|
|
|
25
26
|
test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence", () => {
|
|
26
27
|
const prompt = readPrompt("run-uat");
|
|
27
28
|
assert.match(prompt, /\*\*Detected UAT mode:\*\*\s*`\{\{uatType\}\}`/);
|
|
28
|
-
assert.match(prompt, /uatType:\s
|
|
29
|
+
assert.match(prompt, /uatType:\s*"\{\{uatType\}\}"/);
|
|
30
|
+
assert.match(prompt, /gsd_uat_result_save/);
|
|
31
|
+
assert.match(prompt, /presentedTools/);
|
|
32
|
+
assert.match(prompt, /blockedTools/);
|
|
29
33
|
assert.match(prompt, /live-runtime/);
|
|
30
34
|
assert.match(prompt, /browser\/runtime\/network/i);
|
|
31
35
|
assert.match(prompt, /NEEDS-HUMAN/);
|
|
32
36
|
assert.doesNotMatch(prompt, /uatType:\s*artifact-driven/);
|
|
37
|
+
assert.doesNotMatch(prompt, /Call `gsd_summary_save`/);
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
test("run-uat prompt lists canonical gsd_uat_exec intent values", () => {
|
|
@@ -42,6 +47,22 @@ test("run-uat prompt lists canonical gsd_uat_exec intent values", () => {
|
|
|
42
47
|
assert.match(prompt, /do not use `artifact`, `runtime`, or `human-follow-up` as `intent`/i);
|
|
43
48
|
});
|
|
44
49
|
|
|
50
|
+
test("run-uat prompt gives the complete UAT result-save presentation contract", () => {
|
|
51
|
+
const prompt = readPrompt("run-uat");
|
|
52
|
+
assert.match(prompt, /Call `gsd_uat_result_save` once after all checks are complete/);
|
|
53
|
+
assert.doesNotMatch(prompt, /Call `gsd_summary_save` with `artifact_type: "ASSESSMENT"`/);
|
|
54
|
+
|
|
55
|
+
for (const toolName of RUN_UAT_WORKFLOW_TOOL_NAMES) {
|
|
56
|
+
assert.ok(prompt.includes(`"${toolName}"`), `prompt should include required presented tool ${toolName}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const toolName of ["gsd_exec", "gsd_summary_save", "gsd_save_gate_result"] as const) {
|
|
60
|
+
assert.ok(prompt.includes(`name: "${toolName}"`), `prompt should include blocked tool ${toolName}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
assert.ok(prompt.includes("forbidden during run-uat"), "prompt should explain blocked run-uat tools");
|
|
64
|
+
});
|
|
65
|
+
|
|
45
66
|
test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
|
|
46
67
|
const prompt = readPrompt("workflow-start");
|
|
47
68
|
assert.match(prompt, /Keep moving by default/i);
|
|
@@ -22,6 +22,20 @@ test("resolveExtensionDirFromCandidates prefers user-local dir when both trees a
|
|
|
22
22
|
assert.equal(resolved, agentDir);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test("resolveExtensionDirFromCandidates prefers source checkout dir when both trees are valid", () => {
|
|
26
|
+
const moduleDir = "/repo/src/resources/extensions/gsd";
|
|
27
|
+
const agentDir = "/home/user/.gsd/agent/extensions/gsd";
|
|
28
|
+
const paths = new Set<string>([
|
|
29
|
+
join(moduleDir, "prompts"),
|
|
30
|
+
join(moduleDir, "templates", "task-summary.md"),
|
|
31
|
+
join(agentDir, "prompts"),
|
|
32
|
+
join(agentDir, "templates", "task-summary.md"),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const resolved = resolveExtensionDirFromCandidates(moduleDir, agentDir, makeExists(paths));
|
|
36
|
+
assert.equal(resolved, moduleDir);
|
|
37
|
+
});
|
|
38
|
+
|
|
25
39
|
test("resolveExtensionDirFromCandidates rejects module dir missing task-summary template", () => {
|
|
26
40
|
const moduleDir = "/npm/global/gsd";
|
|
27
41
|
const agentDir = "/home/user/.gsd/agent/extensions/gsd";
|
|
@@ -8,6 +8,7 @@ import { test, describe, beforeEach } from "node:test";
|
|
|
8
8
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { emitJournalEvent } from "../journal.ts";
|
|
11
12
|
import {
|
|
12
13
|
RuleRegistry,
|
|
13
14
|
getRegistry,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
resetRegistry,
|
|
17
18
|
convertDispatchRules,
|
|
18
19
|
getOrCreateRegistry,
|
|
20
|
+
resolveHookArtifactPath,
|
|
19
21
|
} from "../rule-registry.ts";
|
|
20
22
|
import type { UnifiedRule } from "../rule-types.ts";
|
|
21
23
|
import type { DispatchAction, DispatchContext } from "../auto-dispatch.ts";
|
|
@@ -443,6 +445,79 @@ describe("RuleRegistry", () => {
|
|
|
443
445
|
}
|
|
444
446
|
});
|
|
445
447
|
|
|
448
|
+
test("failed hook completion with an artifact does not dequeue the next hook", () => {
|
|
449
|
+
const originalGsdHome = process.env.GSD_HOME;
|
|
450
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-failed-"));
|
|
451
|
+
const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
|
|
452
|
+
const unitId = "M001/S01/T01";
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
mkdirSync(join(projectRoot, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
456
|
+
writeFileSync(
|
|
457
|
+
join(projectRoot, ".gsd", "PREFERENCES.md"),
|
|
458
|
+
[
|
|
459
|
+
"---",
|
|
460
|
+
"version: 1",
|
|
461
|
+
"post_unit_hooks:",
|
|
462
|
+
" - name: review-arbiter",
|
|
463
|
+
" after: [execute-task]",
|
|
464
|
+
" prompt: Review {taskId}",
|
|
465
|
+
" artifact: REVIEW.md",
|
|
466
|
+
" max_cycles: 1",
|
|
467
|
+
" - name: follow-up-review",
|
|
468
|
+
" after: [execute-task]",
|
|
469
|
+
" prompt: Follow-up review {taskId}",
|
|
470
|
+
"---",
|
|
471
|
+
].join("\n"),
|
|
472
|
+
"utf-8",
|
|
473
|
+
);
|
|
474
|
+
process.env.GSD_HOME = tempGsdHome;
|
|
475
|
+
|
|
476
|
+
const registry = new RuleRegistry([]);
|
|
477
|
+
const firstHook = registry.evaluatePostUnit("execute-task", unitId, projectRoot);
|
|
478
|
+
assert.equal(firstHook?.hookName, "review-arbiter");
|
|
479
|
+
|
|
480
|
+
writeFileSync(
|
|
481
|
+
resolveHookArtifactPath(projectRoot, unitId, "REVIEW.md"),
|
|
482
|
+
"partial review output",
|
|
483
|
+
"utf-8",
|
|
484
|
+
);
|
|
485
|
+
emitJournalEvent(projectRoot, {
|
|
486
|
+
ts: "2026-06-03T12:00:00.000Z",
|
|
487
|
+
flowId: "flow-hook-failed",
|
|
488
|
+
seq: 3,
|
|
489
|
+
eventType: "unit-end",
|
|
490
|
+
data: {
|
|
491
|
+
unitType: "hook/review-arbiter",
|
|
492
|
+
unitId,
|
|
493
|
+
status: "cancelled",
|
|
494
|
+
artifactVerified: false,
|
|
495
|
+
errorContext: {
|
|
496
|
+
message: "Provider error: Stream ended without finish_reason",
|
|
497
|
+
category: "provider",
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const nextHook = registry.evaluatePostUnit("hook/review-arbiter", unitId, projectRoot);
|
|
503
|
+
assert.equal(nextHook, null, "failed hook must not allow follow-up hook dispatch");
|
|
504
|
+
const failure = registry.consumeHookFailure();
|
|
505
|
+
assert.equal(failure?.hookName, "review-arbiter");
|
|
506
|
+
assert.match(failure?.reason ?? "", /status cancelled/);
|
|
507
|
+
|
|
508
|
+
const resumedRegistry = new RuleRegistry([]);
|
|
509
|
+
resumedRegistry.restoreState(projectRoot);
|
|
510
|
+
const resumedHook = resumedRegistry.evaluatePostUnit("execute-task", unitId, projectRoot);
|
|
511
|
+
assert.equal(resumedHook, null, "resumed hook evaluation must not skip failed hook artifact");
|
|
512
|
+
assert.equal(resumedRegistry.consumeHookFailure()?.hookName, "review-arbiter");
|
|
513
|
+
} finally {
|
|
514
|
+
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
|
|
515
|
+
else process.env.GSD_HOME = originalGsdHome;
|
|
516
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
517
|
+
rmSync(tempGsdHome, { recursive: true, force: true });
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
446
521
|
// ── matchedRule provenance (S02 journal support) ───────────────────
|
|
447
522
|
|
|
448
523
|
test("evaluateDispatch result includes matchedRule on dispatch match", async () => {
|
package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts
CHANGED
|
@@ -11,11 +11,14 @@ const prompt = readFileSync(promptPath, "utf-8");
|
|
|
11
11
|
|
|
12
12
|
test("validate-milestone reviewer C requires canonical verification class names", () => {
|
|
13
13
|
assert.match(prompt, /\*\*Reviewer C[\s\S]*Verification Classes/i);
|
|
14
|
-
assert.match(prompt, /
|
|
14
|
+
assert.match(prompt, /must be exactly `Contract`, `Integration`, `Operational`, or `UAT`/i);
|
|
15
|
+
assert.match(prompt, /Preserve every planned non-empty class row/i);
|
|
16
|
+
assert.match(prompt, /first cell of each row must be exactly `Contract`, `Integration`, `Operational`, or `UAT`/i);
|
|
15
17
|
assert.match(prompt, /If no verification classes were planned, say that explicitly/i);
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
test("validate-milestone prompt routes verification class analysis into verificationClasses", () => {
|
|
19
|
-
assert.match(prompt, /pass
|
|
20
|
-
assert.match(prompt, /
|
|
21
|
+
assert.match(prompt, /pass a complete canonical table in `verificationClasses`/i);
|
|
22
|
+
assert.match(prompt, /If Reviewer C omitted a planned class, reconstruct the missing row/i);
|
|
23
|
+
assert.match(prompt, /Do not call `gsd_validate_milestone` with a partial `verificationClasses` table/i);
|
|
21
24
|
});
|
|
@@ -180,6 +180,35 @@ describe("handleValidateMilestone write ordering (#2725)", () => {
|
|
|
180
180
|
assert.equal(row, undefined, "assessment row should not be written when verification classes are invalid");
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
it("reports all missing planned verification class rows at once", async () => {
|
|
184
|
+
base = makeTmpBase();
|
|
185
|
+
const dbPath = join(base, ".gsd", "gsd.db");
|
|
186
|
+
openDatabase(dbPath);
|
|
187
|
+
insertMilestone({
|
|
188
|
+
id: "M001",
|
|
189
|
+
planning: {
|
|
190
|
+
verificationContract: "Contract command exits 0",
|
|
191
|
+
verificationOperational: "Process lifecycle proof",
|
|
192
|
+
verificationUat: "Browser-observable UAT proof",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
insertSlice({ id: "S01", milestoneId: "M001" });
|
|
196
|
+
|
|
197
|
+
const result = await handleValidateMilestone(
|
|
198
|
+
{ ...VALID_PARAMS, verificationClasses: "| Check | Result |\n| --- | --- |\n| Generic verification | PASS |" },
|
|
199
|
+
base,
|
|
200
|
+
);
|
|
201
|
+
assert.ok("error" in result, "expected validation to fail");
|
|
202
|
+
assert.match(result.error, /canonical rows "Contract", "Operational", "UAT"/);
|
|
203
|
+
assert.match(result.error, /planned contract, operational, uat verification/);
|
|
204
|
+
|
|
205
|
+
const adapter = _getAdapter()!;
|
|
206
|
+
const row = adapter.prepare(
|
|
207
|
+
`SELECT status FROM assessments WHERE milestone_id = 'M001' AND scope = 'milestone-validation'`,
|
|
208
|
+
).get() as { status: string } | undefined;
|
|
209
|
+
assert.equal(row, undefined, "assessment row should not be written when verification classes are invalid");
|
|
210
|
+
});
|
|
211
|
+
|
|
183
212
|
it("accepts verificationClasses when planned Operational class is present", async () => {
|
|
184
213
|
base = makeTmpBase();
|
|
185
214
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
@@ -406,6 +435,110 @@ describe("handleValidateMilestone write ordering (#2725)", () => {
|
|
|
406
435
|
assert.equal(result.verdict, "pass");
|
|
407
436
|
});
|
|
408
437
|
|
|
438
|
+
it("keeps pass when browser-like criteria are verified by runtime-executable UAT", async () => {
|
|
439
|
+
base = makeTmpBase();
|
|
440
|
+
const dbPath = join(base, ".gsd", "gsd.db");
|
|
441
|
+
openDatabase(dbPath);
|
|
442
|
+
insertMilestone({
|
|
443
|
+
id: "M001",
|
|
444
|
+
planning: {
|
|
445
|
+
successCriteria: [
|
|
446
|
+
"Clicking Mark All Complete sets all todos completed",
|
|
447
|
+
"Reload keeps completed state",
|
|
448
|
+
],
|
|
449
|
+
verificationUat: "Run the Node.js DOM-state script against the static app source.",
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
insertSlice({
|
|
453
|
+
id: "S01",
|
|
454
|
+
milestoneId: "M001",
|
|
455
|
+
// Uses localhost so hasBrowserRequiredText returns true and the gate is
|
|
456
|
+
// actually triggered before the runtime evidence bypasses it.
|
|
457
|
+
demo: "Visit localhost:3000 to verify DOM state after clicking Mark All Complete.",
|
|
458
|
+
});
|
|
459
|
+
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
460
|
+
mkdirSync(sliceDir, { recursive: true });
|
|
461
|
+
writeFileSync(
|
|
462
|
+
join(sliceDir, "S01-ASSESSMENT.md"),
|
|
463
|
+
[
|
|
464
|
+
"---",
|
|
465
|
+
"sliceId: S01",
|
|
466
|
+
"uatType: runtime-executable",
|
|
467
|
+
"verdict: PASS",
|
|
468
|
+
"attempt: 1",
|
|
469
|
+
"---",
|
|
470
|
+
"# UAT Result - S01",
|
|
471
|
+
"",
|
|
472
|
+
"## Checks",
|
|
473
|
+
"",
|
|
474
|
+
"| Check | Mode | Result | Evidence | Notes |",
|
|
475
|
+
"|-------|------|--------|----------|-------|",
|
|
476
|
+
"| DOM-state script | runtime | PASS | gsd_uat_exec:.gsd/evidence/uat/M001/S01/dom-state.json | Runtime assertion verified completed state and reload persistence. |",
|
|
477
|
+
"",
|
|
478
|
+
].join("\n"),
|
|
479
|
+
"utf-8",
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const result = await handleValidateMilestone(
|
|
483
|
+
{
|
|
484
|
+
...VALID_PARAMS,
|
|
485
|
+
verificationClasses:
|
|
486
|
+
`${VALID_PARAMS.verificationClasses}\n| UAT | Runtime executable UAT verified static-app behavior. |`,
|
|
487
|
+
},
|
|
488
|
+
base,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
|
|
492
|
+
assert.equal(result.verdict, "pass");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("downgrades to needs-attention when only one of two browser-requiring slices has runtime evidence", async () => {
|
|
496
|
+
base = makeTmpBase();
|
|
497
|
+
const dbPath = join(base, ".gsd", "gsd.db");
|
|
498
|
+
openDatabase(dbPath);
|
|
499
|
+
insertMilestone({ id: "M001" });
|
|
500
|
+
insertSlice({
|
|
501
|
+
id: "S01",
|
|
502
|
+
milestoneId: "M001",
|
|
503
|
+
demo: "Visit localhost:3000 to verify DOM state.",
|
|
504
|
+
});
|
|
505
|
+
insertSlice({
|
|
506
|
+
id: "S02",
|
|
507
|
+
milestoneId: "M001",
|
|
508
|
+
demo: "Visit localhost:3000 to confirm persistence after reload.",
|
|
509
|
+
});
|
|
510
|
+
// S01 has runtime-executable evidence; S02 has none.
|
|
511
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
512
|
+
writeFileSync(
|
|
513
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-ASSESSMENT.md"),
|
|
514
|
+
[
|
|
515
|
+
"---",
|
|
516
|
+
"sliceId: S01",
|
|
517
|
+
"uatType: runtime-executable",
|
|
518
|
+
"verdict: PASS",
|
|
519
|
+
"---",
|
|
520
|
+
"| DOM check | runtime | PASS | gsd_uat_exec:.gsd/evidence/uat/M001/S01/dom.json | Verified. |",
|
|
521
|
+
"",
|
|
522
|
+
].join("\n"),
|
|
523
|
+
"utf-8",
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const result = await handleValidateMilestone(
|
|
527
|
+
{
|
|
528
|
+
...VALID_PARAMS,
|
|
529
|
+
verificationClasses: `${VALID_PARAMS.verificationClasses}\n| UAT | S01 runtime verified; S02 still needs evidence. |`,
|
|
530
|
+
},
|
|
531
|
+
base,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
|
|
535
|
+
assert.equal(
|
|
536
|
+
result.verdict,
|
|
537
|
+
"needs-attention",
|
|
538
|
+
"S01 runtime evidence must not bypass the gate for S02 which has browser requirements but no evidence",
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
409
542
|
it("ignores slice full_uat_md planning text for browser requirement detection", async () => {
|
|
410
543
|
base = makeTmpBase();
|
|
411
544
|
const dbPath = join(base, ".gsd", "gsd.db");
|
|
@@ -643,6 +643,80 @@ test("executeUatResultSave accepts gsd_uat_exec evidence written in a milestone
|
|
|
643
643
|
}
|
|
644
644
|
});
|
|
645
645
|
|
|
646
|
+
test("executeUatResultSave rejects artifact-driven PASS with human follow-up checks", async () => {
|
|
647
|
+
const base = makeTmpBase();
|
|
648
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
649
|
+
const evidenceId = "uat-artifact-nonautomatable";
|
|
650
|
+
const worktreeExecDir = join(worktree, ".gsd", "exec");
|
|
651
|
+
try {
|
|
652
|
+
openTestDb(base);
|
|
653
|
+
seedMilestone("M001", "Milestone One");
|
|
654
|
+
seedSlice("M001", "S01", "complete");
|
|
655
|
+
mkdirSync(worktreeExecDir, { recursive: true });
|
|
656
|
+
writeFileSync(
|
|
657
|
+
join(worktreeExecDir, `${evidenceId}.meta.json`),
|
|
658
|
+
JSON.stringify({
|
|
659
|
+
id: evidenceId,
|
|
660
|
+
metadata: {
|
|
661
|
+
kind: "uat_exec",
|
|
662
|
+
milestoneId: "M001",
|
|
663
|
+
sliceId: "S01",
|
|
664
|
+
checkId: "UAT-01",
|
|
665
|
+
intent: "uat-artifact-check",
|
|
666
|
+
},
|
|
667
|
+
}),
|
|
668
|
+
"utf-8",
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
const result = await inProjectDir(worktree, () => executeUatResultSave({
|
|
672
|
+
milestoneId: "M001",
|
|
673
|
+
sliceId: "S01",
|
|
674
|
+
uatType: "artifact-driven",
|
|
675
|
+
verdict: "PASS",
|
|
676
|
+
checks: [
|
|
677
|
+
{
|
|
678
|
+
id: "UAT-01",
|
|
679
|
+
description: "Static contract passes",
|
|
680
|
+
mode: "artifact",
|
|
681
|
+
result: "PASS",
|
|
682
|
+
evidence: [{ kind: "gsd_uat_exec", ref: evidenceId }],
|
|
683
|
+
notes: "Artifact check passed.",
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "UAT-02",
|
|
687
|
+
description: "Browser polish is deferred to the next slice",
|
|
688
|
+
mode: "human-follow-up",
|
|
689
|
+
result: "NEEDS-HUMAN",
|
|
690
|
+
notes: "Out of scope for this artifact-driven UAT.",
|
|
691
|
+
nonAutomatable: true,
|
|
692
|
+
},
|
|
693
|
+
],
|
|
694
|
+
presentation: {
|
|
695
|
+
surface: "mcp",
|
|
696
|
+
presentedTools: [
|
|
697
|
+
"gsd_uat_exec",
|
|
698
|
+
"gsd_uat_result_save",
|
|
699
|
+
"gsd_resume",
|
|
700
|
+
"gsd_milestone_status",
|
|
701
|
+
"gsd_journal_query",
|
|
702
|
+
],
|
|
703
|
+
blockedTools: [
|
|
704
|
+
{ name: "gsd_exec", reason: "forbidden during run-uat" },
|
|
705
|
+
{ name: "gsd_summary_save", reason: "forbidden during run-uat" },
|
|
706
|
+
{ name: "gsd_save_gate_result", reason: "forbidden during run-uat" },
|
|
707
|
+
],
|
|
708
|
+
},
|
|
709
|
+
notes: "UAT passed; non-automatable browser polish is deferred.",
|
|
710
|
+
}, worktree));
|
|
711
|
+
|
|
712
|
+
assert.equal(result.isError, true);
|
|
713
|
+
assert.match(String(result.content[0]?.text), /artifact-driven UAT cannot PASS with human-only checks/);
|
|
714
|
+
} finally {
|
|
715
|
+
closeDatabase();
|
|
716
|
+
cleanup(base);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
646
720
|
test("executeSliceComplete coerces string enrichment entries and writes summary/UAT artifacts", async () => {
|
|
647
721
|
const base = makeTmpBase();
|
|
648
722
|
try {
|