gsd-pi 2.28.0-dev.4009980 → 2.28.0-dev.42019bd
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 +15 -9
- package/dist/resource-loader.js +26 -2
- package/dist/resources/extensions/gsd/auto-recovery.ts +17 -1
- package/dist/resources/extensions/gsd/auto-start.ts +1 -1
- package/dist/resources/extensions/gsd/auto-verification.ts +27 -7
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +14 -0
- package/dist/resources/extensions/gsd/auto.ts +20 -3
- package/dist/resources/extensions/gsd/export.ts +28 -2
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/dist/resources/extensions/gsd/types.ts +1 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
- package/package.json +3 -3
- package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
- package/src/resources/extensions/gsd/auto-recovery.ts +17 -1
- package/src/resources/extensions/gsd/auto-start.ts +1 -1
- package/src/resources/extensions/gsd/auto-verification.ts +27 -7
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +14 -0
- package/src/resources/extensions/gsd/auto.ts +20 -3
- package/src/resources/extensions/gsd/export.ts +28 -2
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/src/resources/extensions/gsd/verification-gate.ts +13 -2
package/dist/cli.js
CHANGED
|
@@ -71,6 +71,21 @@ function parseCliArgs(argv) {
|
|
|
71
71
|
}
|
|
72
72
|
const cliFlags = parseCliArgs(process.argv);
|
|
73
73
|
const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
|
|
74
|
+
// Early resource-skew check — must run before TTY gate so version mismatch
|
|
75
|
+
// errors surface even in non-TTY environments.
|
|
76
|
+
exitIfManagedResourcesAreNewer(agentDir);
|
|
77
|
+
// Early TTY check — must come before heavy initialization to avoid dangling
|
|
78
|
+
// handles that prevent process.exit() from completing promptly.
|
|
79
|
+
const hasSubcommand = cliFlags.messages.length > 0;
|
|
80
|
+
if (!process.stdin.isTTY && !isPrintMode && !hasSubcommand && !cliFlags.listModels) {
|
|
81
|
+
process.stderr.write('[gsd] Error: Interactive mode requires a terminal (TTY).\n');
|
|
82
|
+
process.stderr.write('[gsd] Non-interactive alternatives:\n');
|
|
83
|
+
process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n');
|
|
84
|
+
process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n');
|
|
85
|
+
process.stderr.write('[gsd] gsd --mode mcp MCP server over stdin/stdout\n');
|
|
86
|
+
process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
74
89
|
// `gsd <subcommand> --help` — show subcommand-specific help
|
|
75
90
|
const subcommand = cliFlags.messages[0];
|
|
76
91
|
if (subcommand && process.argv.includes('--help')) {
|
|
@@ -420,14 +435,5 @@ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
|
|
|
420
435
|
session.setScopedModels(scopedModels);
|
|
421
436
|
}
|
|
422
437
|
}
|
|
423
|
-
if (!process.stdin.isTTY) {
|
|
424
|
-
process.stderr.write('[gsd] Error: Interactive mode requires a terminal (TTY).\n');
|
|
425
|
-
process.stderr.write('[gsd] Non-interactive alternatives:\n');
|
|
426
|
-
process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n');
|
|
427
|
-
process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n');
|
|
428
|
-
process.stderr.write('[gsd] gsd --mode mcp MCP server over stdin/stdout\n');
|
|
429
|
-
process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n');
|
|
430
|
-
process.exit(1);
|
|
431
|
-
}
|
|
432
438
|
const interactiveMode = new InteractiveMode(session);
|
|
433
439
|
await interactiveMode.run();
|
package/dist/resource-loader.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { compareSemver } from './update-check.js';
|
|
@@ -111,10 +111,34 @@ function syncResourceDir(srcDir, destDir) {
|
|
|
111
111
|
rmSync(target, { recursive: true, force: true });
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
try {
|
|
115
|
+
cpSync(srcDir, destDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Fallback for Windows paths with non-ASCII characters where cpSync
|
|
119
|
+
// fails with the \\?\ extended-length prefix (#1178).
|
|
120
|
+
copyDirRecursive(srcDir, destDir);
|
|
121
|
+
}
|
|
115
122
|
makeTreeWritable(destDir);
|
|
116
123
|
}
|
|
117
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Recursive directory copy using copyFileSync — workaround for cpSync failures
|
|
127
|
+
* on Windows paths containing non-ASCII characters (#1178).
|
|
128
|
+
*/
|
|
129
|
+
function copyDirRecursive(src, dest) {
|
|
130
|
+
mkdirSync(dest, { recursive: true });
|
|
131
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
132
|
+
const srcPath = join(src, entry.name);
|
|
133
|
+
const destPath = join(dest, entry.name);
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
copyDirRecursive(srcPath, destPath);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
copyFileSync(srcPath, destPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
118
142
|
/**
|
|
119
143
|
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
|
120
144
|
*
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
clearPathCache,
|
|
37
37
|
resolveGsdRootFile,
|
|
38
38
|
} from "./paths.js";
|
|
39
|
+
import { isValidationTerminal } from "./state.js";
|
|
39
40
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
40
41
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
41
42
|
import { dirname, join } from "node:path";
|
|
@@ -137,6 +138,21 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
137
138
|
if (!absPath) return false;
|
|
138
139
|
if (!existsSync(absPath)) return false;
|
|
139
140
|
|
|
141
|
+
// validate-milestone must have a VALIDATION file with a terminal verdict
|
|
142
|
+
// (pass, needs-attention, or needs-remediation). Without this check, a
|
|
143
|
+
// VALIDATION file with missing/malformed frontmatter or an unrecognized
|
|
144
|
+
// verdict is treated as "complete" by the artifact check but deriveState
|
|
145
|
+
// still returns phase:"validating-milestone" (because isValidationTerminal
|
|
146
|
+
// returns false), creating an infinite skip loop that hits the lifetime cap.
|
|
147
|
+
if (unitType === "validate-milestone") {
|
|
148
|
+
try {
|
|
149
|
+
const validationContent = readFileSync(absPath, "utf-8");
|
|
150
|
+
if (!isValidationTerminal(validationContent)) return false;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
140
156
|
// plan-slice must produce a plan with actual task entries, not just a scaffold.
|
|
141
157
|
// The plan file may exist from a prior discussion/context step with only headings
|
|
142
158
|
// but no tasks. Without this check the artifact is considered "complete" and the
|
|
@@ -211,7 +227,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
211
227
|
try {
|
|
212
228
|
const roadmapContent = readFileSync(roadmapFile, "utf-8");
|
|
213
229
|
const roadmap = parseRoadmap(roadmapContent);
|
|
214
|
-
const slice = roadmap.slices.find(s => s.id === sid);
|
|
230
|
+
const slice = (roadmap.slices ?? []).find(s => s.id === sid);
|
|
215
231
|
if (slice && !slice.done) return false;
|
|
216
232
|
} catch {
|
|
217
233
|
// Corrupt/unparseable roadmap — fail verification so the unit
|
|
@@ -415,7 +415,7 @@ export async function bootstrapAutoSession(
|
|
|
415
415
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
416
416
|
ctx.ui.setFooter(hideFooter);
|
|
417
417
|
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
|
418
|
-
const pendingCount = state.registry.filter(m => m.status !== 'complete' && m.status !== 'parked').length;
|
|
418
|
+
const pendingCount = (state.registry ?? []).filter(m => m.status !== 'complete' && m.status !== 'parked').length;
|
|
419
419
|
const scopeMsg = pendingCount > 1
|
|
420
420
|
? `Will loop through ${pendingCount} milestones.`
|
|
421
421
|
: "Will loop until milestone complete.";
|
|
@@ -105,19 +105,39 @@ export async function runPostUnitVerification(
|
|
|
105
105
|
const completionKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
106
106
|
|
|
107
107
|
if (result.checks.length > 0) {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
108
|
+
const blockingChecks = result.checks.filter(c => c.blocking);
|
|
109
|
+
const advisoryChecks = result.checks.filter(c => !c.blocking);
|
|
110
|
+
const blockingPassCount = blockingChecks.filter(c => c.exitCode === 0).length;
|
|
111
|
+
const advisoryFailCount = advisoryChecks.filter(c => c.exitCode !== 0).length;
|
|
112
|
+
|
|
110
113
|
if (result.passed) {
|
|
111
|
-
|
|
114
|
+
let msg = blockingChecks.length > 0
|
|
115
|
+
? `Verification gate: ${blockingPassCount}/${blockingChecks.length} blocking checks passed`
|
|
116
|
+
: `Verification gate: passed (no blocking checks)`;
|
|
117
|
+
if (advisoryFailCount > 0) {
|
|
118
|
+
msg += ` (${advisoryFailCount} advisory warning${advisoryFailCount > 1 ? "s" : ""})`;
|
|
119
|
+
}
|
|
120
|
+
ctx.ui.notify(msg);
|
|
121
|
+
// Log advisory warnings to stderr for visibility
|
|
122
|
+
if (advisoryFailCount > 0) {
|
|
123
|
+
const advisoryFailures = advisoryChecks.filter(c => c.exitCode !== 0);
|
|
124
|
+
process.stderr.write(`verification-gate: ${advisoryFailCount} advisory (non-blocking) failure(s)\n`);
|
|
125
|
+
for (const f of advisoryFailures) {
|
|
126
|
+
process.stderr.write(` [advisory] ${f.command} exited ${f.exitCode}\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
112
129
|
} else {
|
|
113
|
-
const
|
|
114
|
-
const failNames =
|
|
130
|
+
const blockingFailures = blockingChecks.filter(c => c.exitCode !== 0);
|
|
131
|
+
const failNames = blockingFailures.map(f => f.command).join(", ");
|
|
115
132
|
ctx.ui.notify(`Verification gate: FAILED — ${failNames}`);
|
|
116
|
-
process.stderr.write(`verification-gate: ${
|
|
117
|
-
for (const f of
|
|
133
|
+
process.stderr.write(`verification-gate: ${blockingFailures.length}/${blockingChecks.length} blocking checks failed\n`);
|
|
134
|
+
for (const f of blockingFailures) {
|
|
118
135
|
process.stderr.write(` ${f.command} exited ${f.exitCode}\n`);
|
|
119
136
|
if (f.stderr) process.stderr.write(` stderr: ${f.stderr.slice(0, 500)}\n`);
|
|
120
137
|
}
|
|
138
|
+
if (advisoryFailCount > 0) {
|
|
139
|
+
process.stderr.write(`verification-gate: ${advisoryFailCount} additional advisory (non-blocking) failure(s)\n`);
|
|
140
|
+
}
|
|
121
141
|
}
|
|
122
142
|
}
|
|
123
143
|
|
|
@@ -36,6 +36,12 @@ export function syncProjectRootToWorktree(projectRoot: string, worktreePath: str
|
|
|
36
36
|
// has newer artifacts (e.g. slices that don't exist in the worktree yet)
|
|
37
37
|
safeCopyRecursive(join(prGsd, "milestones", milestoneId), join(wtGsd, "milestones", milestoneId))
|
|
38
38
|
|
|
39
|
+
// Copy living documents from project root to worktree so agents have the
|
|
40
|
+
// latest decisions, requirements, project state, and knowledge.
|
|
41
|
+
for (const doc of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "KNOWLEDGE.md"]) {
|
|
42
|
+
safeCopy(join(prGsd, doc), join(wtGsd, doc), { force: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
// Delete worktree gsd.db so it rebuilds from the freshly synced files.
|
|
40
46
|
// Stale DB rows are the root cause of the infinite skip loop (#853).
|
|
41
47
|
try {
|
|
@@ -89,6 +95,14 @@ export function syncStateToProjectRoot(worktreePath: string, projectRoot: string
|
|
|
89
95
|
// worktree. If the next session resolves basePath before worktree re-entry,
|
|
90
96
|
// selfHeal can't find or clear the stale record (#769).
|
|
91
97
|
safeCopyRecursive(join(wtGsd, "runtime", "units"), join(prGsd, "runtime", "units"), { force: true })
|
|
98
|
+
|
|
99
|
+
// 5. Living documents — decisions, requirements, project description, knowledge.
|
|
100
|
+
// Agents update these during slice execution. Without syncing, a new session
|
|
101
|
+
// reads stale copies from the project root, losing architectural decisions,
|
|
102
|
+
// requirement status updates, and accumulated knowledge (#1168).
|
|
103
|
+
for (const doc of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "KNOWLEDGE.md"]) {
|
|
104
|
+
safeCopy(join(wtGsd, doc), join(prGsd, doc), { force: true });
|
|
105
|
+
}
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
// ─── Resource Staleness ───────────────────────────────────────────────────
|
|
@@ -877,7 +877,7 @@ async function showStepWizard(
|
|
|
877
877
|
: "previous unit";
|
|
878
878
|
|
|
879
879
|
if (!mid || state.phase === "complete") {
|
|
880
|
-
const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked");
|
|
880
|
+
const incomplete = (state.registry ?? []).filter(m => m.status !== "complete" && m.status !== "parked");
|
|
881
881
|
if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked" && state.phase !== "pre-planning") {
|
|
882
882
|
const ids = incomplete.map(m => m.id).join(", ");
|
|
883
883
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -1171,7 +1171,7 @@ async function dispatchNextUnit(
|
|
|
1171
1171
|
}
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
-
const pendingIds = state.registry
|
|
1174
|
+
const pendingIds = (state.registry ?? [])
|
|
1175
1175
|
.filter(m => m.status !== "complete")
|
|
1176
1176
|
.map(m => m.id);
|
|
1177
1177
|
pruneQueueOrder(s.basePath, pendingIds);
|
|
@@ -1186,7 +1186,7 @@ async function dispatchNextUnit(
|
|
|
1186
1186
|
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
1187
1187
|
}
|
|
1188
1188
|
|
|
1189
|
-
const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked");
|
|
1189
|
+
const incomplete = (state.registry ?? []).filter(m => m.status !== "complete" && m.status !== "parked");
|
|
1190
1190
|
if (incomplete.length === 0) {
|
|
1191
1191
|
// Genuinely all complete (parked milestones excluded) — merge milestone branch to main before stopping (#962)
|
|
1192
1192
|
if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
|
|
@@ -1439,6 +1439,23 @@ async function dispatchNextUnit(
|
|
|
1439
1439
|
|
|
1440
1440
|
await runSecretsGate();
|
|
1441
1441
|
|
|
1442
|
+
// ── Interactive discussion gate ──
|
|
1443
|
+
// If the active milestone needs discussion (has CONTEXT-DRAFT.md but no roadmap),
|
|
1444
|
+
// stop auto-mode and route to the interactive discussion flow. The guided-flow
|
|
1445
|
+
// handles needs-discussion correctly — it just needs to be called instead of
|
|
1446
|
+
// letting the dispatch table fire "needs-discussion → stop" (#1170).
|
|
1447
|
+
if (state.phase === "needs-discussion") {
|
|
1448
|
+
if (s.currentUnit) {
|
|
1449
|
+
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
1450
|
+
}
|
|
1451
|
+
const cmdCtx = s.cmdCtx!;
|
|
1452
|
+
const basePath = s.basePath;
|
|
1453
|
+
await stopAuto(ctx, pi, `${mid}: ${midTitle} needs discussion before planning.`);
|
|
1454
|
+
const { showSmartEntry } = await import("./guided-flow.js");
|
|
1455
|
+
await showSmartEntry(cmdCtx, pi, basePath);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1442
1459
|
// ── Dispatch table ──
|
|
1443
1460
|
const dispatchResult = await resolveDispatch({ basePath: s.basePath, mid, midTitle: midTitle!, state, prefs,
|
|
1444
1461
|
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
5
5
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join, basename } from "node:path";
|
|
7
|
+
import { exec } from "node:child_process";
|
|
7
8
|
import {
|
|
8
9
|
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
|
|
9
10
|
aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk,
|
|
@@ -12,6 +13,28 @@ import type { UnitMetrics } from "./metrics.js";
|
|
|
12
13
|
import { gsdRoot } from "./paths.js";
|
|
13
14
|
import { formatDuration, fileLink } from "../shared/mod.js";
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Open a file in the user's default browser.
|
|
18
|
+
* Uses platform-specific commands: `open` (macOS), `xdg-open` (Linux), `start` (Windows).
|
|
19
|
+
* Non-blocking, non-fatal — failures are silently ignored.
|
|
20
|
+
*/
|
|
21
|
+
export function openInBrowser(filePath: string): void {
|
|
22
|
+
const cmd =
|
|
23
|
+
process.platform === "darwin" ? "open" :
|
|
24
|
+
process.platform === "win32" ? "start" :
|
|
25
|
+
"xdg-open";
|
|
26
|
+
|
|
27
|
+
// On Windows, `start` needs an empty title argument when the path has spaces
|
|
28
|
+
const args = process.platform === "win32"
|
|
29
|
+
? `"" "${filePath}"`
|
|
30
|
+
: `"${filePath}"`;
|
|
31
|
+
|
|
32
|
+
exec(`${cmd} ${args}`, (err) => {
|
|
33
|
+
// Non-fatal — if the browser can't be opened, the file path is still shown
|
|
34
|
+
if (err) void err;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
/**
|
|
16
39
|
* Write an export file directly, without requiring an ExtensionCommandContext.
|
|
17
40
|
* Used by the visualizer overlay export tab.
|
|
@@ -167,10 +190,12 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
|
|
|
167
190
|
paths.push(bn(outPath));
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
const indexPath = join(gsdRoot(basePath), "reports", "index.html");
|
|
170
194
|
ctx.ui.notify(
|
|
171
|
-
`Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\
|
|
195
|
+
`Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nOpening reports index in browser...`,
|
|
172
196
|
"success",
|
|
173
197
|
);
|
|
198
|
+
openInBrowser(indexPath);
|
|
174
199
|
} else {
|
|
175
200
|
// Single report for the active milestone (existing behavior)
|
|
176
201
|
const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
|
|
@@ -194,9 +219,10 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
|
|
|
194
219
|
phase: data.phase,
|
|
195
220
|
});
|
|
196
221
|
ctx.ui.notify(
|
|
197
|
-
`HTML report saved: .gsd/reports/${bn(outPath)}\
|
|
222
|
+
`HTML report saved: .gsd/reports/${bn(outPath)}\nOpening in browser...`,
|
|
198
223
|
"success",
|
|
199
224
|
);
|
|
225
|
+
openInBrowser(outPath);
|
|
200
226
|
}
|
|
201
227
|
} catch (err) {
|
|
202
228
|
ctx.ui.notify(
|
|
@@ -290,6 +290,61 @@ test("verifyExpectedArtifact fails when VALIDATION.md is missing", () => {
|
|
|
290
290
|
}
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
+
test("verifyExpectedArtifact rejects VALIDATION with missing frontmatter", () => {
|
|
294
|
+
const base = makeTmpBase();
|
|
295
|
+
try {
|
|
296
|
+
// A VALIDATION file without frontmatter should be treated as incomplete —
|
|
297
|
+
// matching what deriveState expects. Without this, the artifact check passes
|
|
298
|
+
// but deriveState still returns validating-milestone, causing the hard skip loop.
|
|
299
|
+
writeValidation(base, "M001", "# Validation\nNo frontmatter here.");
|
|
300
|
+
clearPathCache();
|
|
301
|
+
clearParseCache();
|
|
302
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
303
|
+
assert.equal(result, false, "VALIDATION without frontmatter should fail verification");
|
|
304
|
+
} finally {
|
|
305
|
+
cleanup(base);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("verifyExpectedArtifact rejects VALIDATION with missing verdict field", () => {
|
|
310
|
+
const base = makeTmpBase();
|
|
311
|
+
try {
|
|
312
|
+
writeValidation(base, "M001", "---\nremediation_round: 0\n---\n\n# Validation");
|
|
313
|
+
clearPathCache();
|
|
314
|
+
clearParseCache();
|
|
315
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
316
|
+
assert.equal(result, false, "VALIDATION without verdict field should fail verification");
|
|
317
|
+
} finally {
|
|
318
|
+
cleanup(base);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("verifyExpectedArtifact rejects VALIDATION with unrecognized verdict", () => {
|
|
323
|
+
const base = makeTmpBase();
|
|
324
|
+
try {
|
|
325
|
+
writeValidation(base, "M001", "---\nverdict: unknown-value\nremediation_round: 0\n---\n\n# Validation");
|
|
326
|
+
clearPathCache();
|
|
327
|
+
clearParseCache();
|
|
328
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
329
|
+
assert.equal(result, false, "VALIDATION with unrecognized verdict should fail verification");
|
|
330
|
+
} finally {
|
|
331
|
+
cleanup(base);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("verifyExpectedArtifact passes VALIDATION with needs-attention verdict", () => {
|
|
336
|
+
const base = makeTmpBase();
|
|
337
|
+
try {
|
|
338
|
+
writeValidation(base, "M001", "---\nverdict: needs-attention\nremediation_round: 0\n---\n\n# Validation\nNeeds attention.");
|
|
339
|
+
clearPathCache();
|
|
340
|
+
clearParseCache();
|
|
341
|
+
const result = verifyExpectedArtifact("validate-milestone", "M001", base);
|
|
342
|
+
assert.equal(result, true, "VALIDATION with needs-attention verdict should pass verification");
|
|
343
|
+
} finally {
|
|
344
|
+
cleanup(base);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
293
348
|
// ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
|
|
294
349
|
|
|
295
350
|
test("diagnoseExpectedArtifact returns validation path for validate-milestone", () => {
|
|
@@ -58,6 +58,7 @@ test("verification-evidence: writeVerificationJSON writes correct JSON shape", (
|
|
|
58
58
|
stdout: "all good",
|
|
59
59
|
stderr: "",
|
|
60
60
|
durationMs: 2340,
|
|
61
|
+
blocking: true,
|
|
61
62
|
},
|
|
62
63
|
],
|
|
63
64
|
});
|
|
@@ -105,9 +106,9 @@ test("verification-evidence: writeVerificationJSON maps exitCode to verdict corr
|
|
|
105
106
|
const result = makeResult({
|
|
106
107
|
passed: false,
|
|
107
108
|
checks: [
|
|
108
|
-
{ command: "lint", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
109
|
-
{ command: "test", exitCode: 1, stdout: "", stderr: "fail", durationMs: 200 },
|
|
110
|
-
{ command: "audit", exitCode: 2, stdout: "", stderr: "err", durationMs: 300 },
|
|
109
|
+
{ command: "lint", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
110
|
+
{ command: "test", exitCode: 1, stdout: "", stderr: "fail", durationMs: 200, blocking: true },
|
|
111
|
+
{ command: "audit", exitCode: 2, stdout: "", stderr: "err", durationMs: 300, blocking: true },
|
|
111
112
|
],
|
|
112
113
|
});
|
|
113
114
|
|
|
@@ -133,6 +134,7 @@ test("verification-evidence: writeVerificationJSON excludes stdout/stderr from o
|
|
|
133
134
|
stdout: "hello\n",
|
|
134
135
|
stderr: "some warning",
|
|
135
136
|
durationMs: 50,
|
|
137
|
+
blocking: true,
|
|
136
138
|
},
|
|
137
139
|
],
|
|
138
140
|
});
|
|
@@ -181,8 +183,8 @@ test("verification-evidence: writeVerificationJSON uses optional unitId when pro
|
|
|
181
183
|
test("verification-evidence: formatEvidenceTable returns markdown table with correct columns", () => {
|
|
182
184
|
const result = makeResult({
|
|
183
185
|
checks: [
|
|
184
|
-
{ command: "npm run typecheck", exitCode: 0, stdout: "", stderr: "", durationMs: 2340 },
|
|
185
|
-
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "err", durationMs: 1100 },
|
|
186
|
+
{ command: "npm run typecheck", exitCode: 0, stdout: "", stderr: "", durationMs: 2340, blocking: true },
|
|
187
|
+
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "err", durationMs: 1100, blocking: true },
|
|
186
188
|
],
|
|
187
189
|
});
|
|
188
190
|
|
|
@@ -214,9 +216,9 @@ test("verification-evidence: formatEvidenceTable returns no-checks message for e
|
|
|
214
216
|
test("verification-evidence: formatEvidenceTable formats duration as seconds with 1 decimal", () => {
|
|
215
217
|
const result = makeResult({
|
|
216
218
|
checks: [
|
|
217
|
-
{ command: "fast", exitCode: 0, stdout: "", stderr: "", durationMs: 150 },
|
|
218
|
-
{ command: "slow", exitCode: 0, stdout: "", stderr: "", durationMs: 2340 },
|
|
219
|
-
{ command: "zero", exitCode: 0, stdout: "", stderr: "", durationMs: 0 },
|
|
219
|
+
{ command: "fast", exitCode: 0, stdout: "", stderr: "", durationMs: 150, blocking: true },
|
|
220
|
+
{ command: "slow", exitCode: 0, stdout: "", stderr: "", durationMs: 2340, blocking: true },
|
|
221
|
+
{ command: "zero", exitCode: 0, stdout: "", stderr: "", durationMs: 0, blocking: true },
|
|
220
222
|
],
|
|
221
223
|
});
|
|
222
224
|
|
|
@@ -230,8 +232,8 @@ test("verification-evidence: formatEvidenceTable uses ✅/❌ emoji for pass/fai
|
|
|
230
232
|
const result = makeResult({
|
|
231
233
|
passed: false,
|
|
232
234
|
checks: [
|
|
233
|
-
{ command: "pass-cmd", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
234
|
-
{ command: "fail-cmd", exitCode: 1, stdout: "", stderr: "", durationMs: 200 },
|
|
235
|
+
{ command: "pass-cmd", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
236
|
+
{ command: "fail-cmd", exitCode: 1, stdout: "", stderr: "", durationMs: 200, blocking: true },
|
|
235
237
|
],
|
|
236
238
|
});
|
|
237
239
|
|
|
@@ -335,8 +337,8 @@ test("verification-evidence: integration — VerificationResult → JSON → tab
|
|
|
335
337
|
const result = makeResult({
|
|
336
338
|
passed: false,
|
|
337
339
|
checks: [
|
|
338
|
-
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500 },
|
|
339
|
-
{ command: "npm run test:unit", exitCode: 1, stdout: "", stderr: "1 failed", durationMs: 3200 },
|
|
340
|
+
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500, blocking: true },
|
|
341
|
+
{ command: "npm run test:unit", exitCode: 1, stdout: "", stderr: "1 failed", durationMs: 3200, blocking: true },
|
|
340
342
|
],
|
|
341
343
|
discoverySource: "package-json",
|
|
342
344
|
});
|
|
@@ -390,7 +392,7 @@ test("verification-evidence: writeVerificationJSON with retryAttempt and maxRetr
|
|
|
390
392
|
const result = makeResult({
|
|
391
393
|
passed: false,
|
|
392
394
|
checks: [
|
|
393
|
-
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "error", durationMs: 300 },
|
|
395
|
+
{ command: "npm run lint", exitCode: 1, stdout: "", stderr: "error", durationMs: 300, blocking: true },
|
|
394
396
|
],
|
|
395
397
|
});
|
|
396
398
|
|
|
@@ -415,7 +417,7 @@ test("verification-evidence: writeVerificationJSON without retry params omits re
|
|
|
415
417
|
const result = makeResult({
|
|
416
418
|
passed: true,
|
|
417
419
|
checks: [
|
|
418
|
-
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
|
|
420
|
+
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
|
|
419
421
|
],
|
|
420
422
|
});
|
|
421
423
|
|
|
@@ -441,7 +443,7 @@ test("verification-evidence: writeVerificationJSON includes runtimeErrors when p
|
|
|
441
443
|
const result = makeResult({
|
|
442
444
|
passed: false,
|
|
443
445
|
checks: [
|
|
444
|
-
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
|
|
446
|
+
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
|
|
445
447
|
],
|
|
446
448
|
runtimeErrors: [
|
|
447
449
|
{ source: "bg-shell", severity: "crash", message: "Server crashed", blocking: true },
|
|
@@ -473,7 +475,7 @@ test("verification-evidence: writeVerificationJSON omits runtimeErrors when abse
|
|
|
473
475
|
const result = makeResult({
|
|
474
476
|
passed: true,
|
|
475
477
|
checks: [
|
|
476
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50 },
|
|
478
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50, blocking: true },
|
|
477
479
|
],
|
|
478
480
|
});
|
|
479
481
|
|
|
@@ -512,7 +514,7 @@ test("verification-evidence: formatEvidenceTable appends runtime errors section"
|
|
|
512
514
|
const result = makeResult({
|
|
513
515
|
passed: false,
|
|
514
516
|
checks: [
|
|
515
|
-
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
517
|
+
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
516
518
|
],
|
|
517
519
|
runtimeErrors: [
|
|
518
520
|
{ source: "bg-shell", severity: "crash", message: "Server crashed with SIGKILL", blocking: true },
|
|
@@ -537,7 +539,7 @@ test("verification-evidence: formatEvidenceTable omits runtime errors section wh
|
|
|
537
539
|
const result = makeResult({
|
|
538
540
|
passed: true,
|
|
539
541
|
checks: [
|
|
540
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200 },
|
|
542
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200, blocking: true },
|
|
541
543
|
],
|
|
542
544
|
});
|
|
543
545
|
|
|
@@ -552,7 +554,7 @@ test("verification-evidence: formatEvidenceTable truncates runtime error message
|
|
|
552
554
|
const result = makeResult({
|
|
553
555
|
passed: false,
|
|
554
556
|
checks: [
|
|
555
|
-
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
557
|
+
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
556
558
|
],
|
|
557
559
|
runtimeErrors: [
|
|
558
560
|
{ source: "bg-shell", severity: "error", message: longMessage, blocking: false },
|
|
@@ -598,7 +600,7 @@ test("verification-evidence: writeVerificationJSON includes auditWarnings when p
|
|
|
598
600
|
const result = makeResult({
|
|
599
601
|
passed: true,
|
|
600
602
|
checks: [
|
|
601
|
-
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100 },
|
|
603
|
+
{ command: "npm run test", exitCode: 0, stdout: "ok", stderr: "", durationMs: 100, blocking: true },
|
|
602
604
|
],
|
|
603
605
|
auditWarnings: SAMPLE_AUDIT_WARNINGS,
|
|
604
606
|
});
|
|
@@ -627,7 +629,7 @@ test("verification-evidence: writeVerificationJSON omits auditWarnings when abse
|
|
|
627
629
|
const result = makeResult({
|
|
628
630
|
passed: true,
|
|
629
631
|
checks: [
|
|
630
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50 },
|
|
632
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 50, blocking: true },
|
|
631
633
|
],
|
|
632
634
|
});
|
|
633
635
|
|
|
@@ -666,7 +668,7 @@ test("verification-evidence: formatEvidenceTable appends audit warnings section"
|
|
|
666
668
|
const result = makeResult({
|
|
667
669
|
passed: true,
|
|
668
670
|
checks: [
|
|
669
|
-
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100 },
|
|
671
|
+
{ command: "npm run test", exitCode: 0, stdout: "", stderr: "", durationMs: 100, blocking: true },
|
|
670
672
|
],
|
|
671
673
|
auditWarnings: SAMPLE_AUDIT_WARNINGS,
|
|
672
674
|
});
|
|
@@ -689,7 +691,7 @@ test("verification-evidence: formatEvidenceTable omits audit warnings section wh
|
|
|
689
691
|
const result = makeResult({
|
|
690
692
|
passed: true,
|
|
691
693
|
checks: [
|
|
692
|
-
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200 },
|
|
694
|
+
{ command: "npm run lint", exitCode: 0, stdout: "", stderr: "", durationMs: 200, blocking: true },
|
|
693
695
|
],
|
|
694
696
|
});
|
|
695
697
|
|
|
@@ -705,7 +707,7 @@ test("verification-evidence: integration — VerificationResult with auditWarnin
|
|
|
705
707
|
const result = makeResult({
|
|
706
708
|
passed: true,
|
|
707
709
|
checks: [
|
|
708
|
-
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500 },
|
|
710
|
+
{ command: "npm run typecheck", exitCode: 0, stdout: "ok", stderr: "", durationMs: 1500, blocking: true },
|
|
709
711
|
],
|
|
710
712
|
auditWarnings: [
|
|
711
713
|
{
|