gsd-pi 2.73.1-dev.1040fb0 → 2.73.1-dev.49397e5
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-web-branch.d.ts +4 -3
- package/dist/cli-web-branch.js +10 -7
- package/dist/cli.js +99 -206
- package/dist/headless-query.js +4 -1
- package/dist/logo.d.ts +1 -1
- package/dist/logo.js +1 -1
- package/dist/onboarding.js +59 -53
- package/dist/resource-loader.js +2 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +59 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
- package/dist/resources/extensions/gsd/auto/phases.js +60 -10
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +93 -57
- package/dist/resources/extensions/gsd/auto-start.js +3 -0
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-verification.js +88 -3
- package/dist/resources/extensions/gsd/auto.js +37 -10
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
- package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
- package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
- package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
- package/dist/resources/extensions/gsd/commands-do.js +79 -0
- package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
- package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
- package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
- package/dist/resources/extensions/gsd/commands-ship.js +187 -0
- package/dist/resources/extensions/gsd/db-writer.js +3 -5
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +321 -0
- package/dist/resources/extensions/gsd/index.js +15 -2
- package/dist/resources/extensions/gsd/md-importer.js +3 -4
- package/dist/resources/extensions/gsd/memory-store.js +19 -51
- package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
- package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
- package/dist/resources/extensions/gsd/notification-widget.js +2 -2
- package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
- package/dist/resources/extensions/gsd/state.js +66 -15
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
- package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
- package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
- package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
- package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
- package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
- package/dist/tsconfig.extensions.tsbuildinfo +1 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- 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 +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +3 -3
- package/packages/mcp-server/tsconfig.json +1 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
- package/packages/native/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-agent-core/tsconfig.json +1 -0
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-ai/tsconfig.json +1 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +138 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +10 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +21 -4
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +11 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +157 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +12 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -4
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
- package/packages/pi-coding-agent/tsconfig.json +1 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
- package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +8 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +32 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
- package/packages/pi-tui/src/tui.ts +31 -3
- package/packages/pi-tui/tsconfig.json +1 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
- package/packages/rpc-client/tsconfig.json +1 -0
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +95 -1
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +88 -0
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
- package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
- package/src/resources/extensions/gsd/auto/phases.ts +90 -10
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +107 -58
- package/src/resources/extensions/gsd/auto-start.ts +3 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
- package/src/resources/extensions/gsd/auto-verification.ts +98 -3
- package/src/resources/extensions/gsd/auto.ts +38 -14
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
- package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
- package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
- package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
- package/src/resources/extensions/gsd/commands-do.ts +109 -0
- package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
- package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
- package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
- package/src/resources/extensions/gsd/commands-ship.ts +219 -0
- package/src/resources/extensions/gsd/db-writer.ts +3 -5
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +467 -0
- package/src/resources/extensions/gsd/index.ts +18 -2
- package/src/resources/extensions/gsd/md-importer.ts +3 -5
- package/src/resources/extensions/gsd/memory-store.ts +31 -62
- package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
- package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
- package/src/resources/extensions/gsd/notification-widget.ts +2 -2
- package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
- package/src/resources/extensions/gsd/state.ts +80 -17
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
- package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
- package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
- package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
- package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
- package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
- package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
- /package/dist/web/standalone/.next/static/{5dzOW4v8Vz23I5xRsiNSk → tAsSblRKK1lG7MVOwsO92}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{5dzOW4v8Vz23I5xRsiNSk → tAsSblRKK1lG7MVOwsO92}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Structural invariant: gsd-db.ts is the single writer for .gsd/gsd.db.
|
|
2
|
+
//
|
|
3
|
+
// No file under src/resources/extensions/gsd/ may issue raw write SQL
|
|
4
|
+
// (INSERT/UPDATE/DELETE/REPLACE) or raw transaction control (BEGIN/COMMIT/
|
|
5
|
+
// ROLLBACK via `.exec(...)`) against the engine database. Every bypass must
|
|
6
|
+
// route through a typed wrapper exported from gsd-db.ts.
|
|
7
|
+
//
|
|
8
|
+
// Allowlist:
|
|
9
|
+
// - gsd-db.ts itself — the single writer
|
|
10
|
+
// - unit-ownership.ts — manages a separate .gsd/unit-claims.db for
|
|
11
|
+
// cross-worktree claim races; intentionally outside this invariant
|
|
12
|
+
// - tests/** — fixtures and direct DB inspection are fair game
|
|
13
|
+
//
|
|
14
|
+
// When this test fails, do not add a new suppression. Instead:
|
|
15
|
+
// 1. Add a typed wrapper to gsd-db.ts that captures the SQL
|
|
16
|
+
// 2. Switch the flagged site to call the wrapper
|
|
17
|
+
//
|
|
18
|
+
// See `.claude/plans/joyful-doodling-pony.md` for the full rationale.
|
|
19
|
+
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
23
|
+
import { join, relative } from "node:path";
|
|
24
|
+
|
|
25
|
+
const gsdDir = join(process.cwd(), "src/resources/extensions/gsd");
|
|
26
|
+
|
|
27
|
+
const ALLOWLIST = new Set([
|
|
28
|
+
"gsd-db.ts",
|
|
29
|
+
"unit-ownership.ts",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** Walk the gsd extension dir and return all .ts files outside tests/. */
|
|
33
|
+
function walkTsFiles(root: string): string[] {
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
const stack: string[] = [root];
|
|
36
|
+
|
|
37
|
+
while (stack.length > 0) {
|
|
38
|
+
const dir = stack.pop()!;
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const ent of entries) {
|
|
47
|
+
const full = join(dir, ent.name);
|
|
48
|
+
if (ent.isDirectory()) {
|
|
49
|
+
// Skip tests/ — fixtures and direct DB inspection are expected there
|
|
50
|
+
if (ent.name === "tests") continue;
|
|
51
|
+
stack.push(full);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!ent.isFile()) continue;
|
|
55
|
+
if (!ent.name.endsWith(".ts")) continue;
|
|
56
|
+
// Skip dotfiles and backup/generated files
|
|
57
|
+
if (ent.name.startsWith(".")) continue;
|
|
58
|
+
out.push(full);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Violation {
|
|
66
|
+
file: string;
|
|
67
|
+
line: number;
|
|
68
|
+
snippet: string;
|
|
69
|
+
kind: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Match .prepare("... INSERT|UPDATE|DELETE|REPLACE ...") in any quoting style.
|
|
73
|
+
const PREPARE_WRITE_RE = /\.prepare\s*\(\s*[`'"][^`'"]*\b(INSERT|UPDATE|DELETE|REPLACE)\b/i;
|
|
74
|
+
|
|
75
|
+
// Match .exec("... INSERT|UPDATE|DELETE|REPLACE ...") or raw BEGIN/COMMIT/ROLLBACK.
|
|
76
|
+
const EXEC_WRITE_RE = /\.exec\s*\(\s*[`'"][^`'"]*\b(INSERT|UPDATE|DELETE|REPLACE|BEGIN|COMMIT|ROLLBACK)\b/i;
|
|
77
|
+
|
|
78
|
+
test("no module outside gsd-db.ts issues raw write SQL against the engine DB", () => {
|
|
79
|
+
const files = walkTsFiles(gsdDir);
|
|
80
|
+
assert.ok(files.length >= 20, `Expected at least 20 .ts files under gsd/, found ${files.length}`);
|
|
81
|
+
|
|
82
|
+
const violations: Violation[] = [];
|
|
83
|
+
|
|
84
|
+
for (const abs of files) {
|
|
85
|
+
const rel = relative(gsdDir, abs);
|
|
86
|
+
const base = rel.split("/").pop()!;
|
|
87
|
+
if (ALLOWLIST.has(base)) continue;
|
|
88
|
+
|
|
89
|
+
let content: string;
|
|
90
|
+
try {
|
|
91
|
+
content = readFileSync(abs, "utf-8");
|
|
92
|
+
} catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lines = content.split("\n");
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
const line = lines[i];
|
|
99
|
+
|
|
100
|
+
const prepareMatch = PREPARE_WRITE_RE.exec(line);
|
|
101
|
+
if (prepareMatch) {
|
|
102
|
+
violations.push({
|
|
103
|
+
file: rel,
|
|
104
|
+
line: i + 1,
|
|
105
|
+
snippet: line.trim(),
|
|
106
|
+
kind: `prepare(${prepareMatch[1].toUpperCase()})`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const execMatch = EXEC_WRITE_RE.exec(line);
|
|
111
|
+
if (execMatch) {
|
|
112
|
+
violations.push({
|
|
113
|
+
file: rel,
|
|
114
|
+
line: i + 1,
|
|
115
|
+
snippet: line.trim(),
|
|
116
|
+
kind: `exec(${execMatch[1].toUpperCase()})`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (violations.length > 0) {
|
|
123
|
+
const lines = violations.map(
|
|
124
|
+
(v) => ` ${v.file}:${v.line} [${v.kind}] — ${v.snippet}`,
|
|
125
|
+
);
|
|
126
|
+
assert.fail(
|
|
127
|
+
`Found ${violations.length} raw write SQL bypass(es) outside gsd-db.ts:\n` +
|
|
128
|
+
lines.join("\n") +
|
|
129
|
+
"\n\nEach of these must be replaced with a typed wrapper exported from gsd-db.ts.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("gsd-db.ts exports the expected single-writer wrappers", async () => {
|
|
135
|
+
// Positive assertion — fail loudly if the module layout changes so this
|
|
136
|
+
// structural test can't silently become a no-op.
|
|
137
|
+
const db = await import("../gsd-db.js");
|
|
138
|
+
|
|
139
|
+
const expected = [
|
|
140
|
+
"deleteDecisionById",
|
|
141
|
+
"deleteRequirementById",
|
|
142
|
+
"deleteArtifactByPath",
|
|
143
|
+
"clearEngineHierarchy",
|
|
144
|
+
"insertOrIgnoreSlice",
|
|
145
|
+
"insertOrIgnoreTask",
|
|
146
|
+
"setSliceReplanTriggeredAt",
|
|
147
|
+
"upsertQualityGate",
|
|
148
|
+
"restoreManifest",
|
|
149
|
+
"bulkInsertLegacyHierarchy",
|
|
150
|
+
"readTransaction",
|
|
151
|
+
"insertMemoryRow",
|
|
152
|
+
"rewriteMemoryId",
|
|
153
|
+
"updateMemoryContentRow",
|
|
154
|
+
"incrementMemoryHitCount",
|
|
155
|
+
"supersedeMemoryRow",
|
|
156
|
+
"markMemoryUnitProcessed",
|
|
157
|
+
"decayMemoriesBefore",
|
|
158
|
+
"supersedeLowestRankedMemories",
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
for (const name of expected) {
|
|
162
|
+
assert.ok(
|
|
163
|
+
typeof (db as Record<string, unknown>)[name] === "function",
|
|
164
|
+
`gsd-db.ts must export ${name} as a function`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("the invariant test touches every .ts module under gsd/ (sanity check)", () => {
|
|
170
|
+
const files = walkTsFiles(gsdDir);
|
|
171
|
+
// Rough sanity: ensure we're not accidentally walking an empty tree
|
|
172
|
+
assert.ok(files.length >= 30, `Expected to scan at least 30 .ts files, scanned ${files.length}`);
|
|
173
|
+
|
|
174
|
+
// Spot-check a couple of known files that must be included
|
|
175
|
+
const rels = files.map((f) => relative(gsdDir, f));
|
|
176
|
+
assert.ok(rels.includes("gsd-db.ts"), "walker must include gsd-db.ts");
|
|
177
|
+
assert.ok(rels.includes("memory-store.ts"), "walker must include memory-store.ts");
|
|
178
|
+
assert.ok(rels.includes("workflow-manifest.ts"), "walker must include workflow-manifest.ts");
|
|
179
|
+
});
|
|
180
|
+
|
|
@@ -811,9 +811,9 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
811
811
|
assert.ok(state.blockers.length > 0, "should have blockers");
|
|
812
812
|
});
|
|
813
813
|
|
|
814
|
-
test("no eligible slice (all deps unmet) →
|
|
814
|
+
test("no eligible slice (all deps unmet) → fallback picks slice with most deps satisfied", async () => {
|
|
815
815
|
const base = createFixtureBase();
|
|
816
|
-
// S01 depends on S00 which doesn't exist
|
|
816
|
+
// S01 depends on S00 which doesn't exist — fallback picks S01 anyway
|
|
817
817
|
writeRoadmap(base, "M001", [
|
|
818
818
|
"# M001: Test Milestone",
|
|
819
819
|
"",
|
|
@@ -827,11 +827,9 @@ describe("state-machine-full-walkthrough", () => {
|
|
|
827
827
|
invalidateStateCache();
|
|
828
828
|
const state = await deriveState(base);
|
|
829
829
|
|
|
830
|
-
|
|
831
|
-
assert.
|
|
832
|
-
|
|
833
|
-
"blockers should mention dependency or eligibility",
|
|
834
|
-
);
|
|
830
|
+
// With partial-dep fallback, S01 is picked despite unmet dep on S00
|
|
831
|
+
assert.equal(state.phase, "planning");
|
|
832
|
+
assert.equal(state.activeSlice?.id, "S01");
|
|
835
833
|
});
|
|
836
834
|
});
|
|
837
835
|
|
|
@@ -263,6 +263,6 @@ test("dispatch: phase skip guards return null (not stop)", () => {
|
|
|
263
263
|
const researchGuard = dispatchSrc.match(/skip_research\).*?return null/s);
|
|
264
264
|
assert.ok(researchGuard, "skip_research guard should return null (fall-through)");
|
|
265
265
|
|
|
266
|
-
const reassessGuard = dispatchSrc.match(/reassess_after_slice
|
|
266
|
+
const reassessGuard = dispatchSrc.match(/reassess_after_slice.*?return null/s);
|
|
267
267
|
assert.ok(reassessGuard, "reassess_after_slice guard should return null (fall-through)");
|
|
268
268
|
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// gsd-pi — Regression tests for the validate-milestone stuck-loop guard (#4094)
|
|
2
|
+
|
|
3
|
+
import { describe, test, mock, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { runPostUnitVerification, type VerificationContext } from "../auto-verification.ts";
|
|
10
|
+
import { AutoSession } from "../auto/session.ts";
|
|
11
|
+
import {
|
|
12
|
+
openDatabase,
|
|
13
|
+
closeDatabase,
|
|
14
|
+
insertMilestone,
|
|
15
|
+
insertSlice,
|
|
16
|
+
} from "../gsd-db.ts";
|
|
17
|
+
import { invalidateAllCaches } from "../cache.ts";
|
|
18
|
+
import { _clearGsdRootCache } from "../paths.ts";
|
|
19
|
+
|
|
20
|
+
let tempDir: string;
|
|
21
|
+
let dbPath: string;
|
|
22
|
+
let originalCwd: string;
|
|
23
|
+
|
|
24
|
+
function makeMockCtx() {
|
|
25
|
+
return {
|
|
26
|
+
ui: {
|
|
27
|
+
notify: mock.fn(),
|
|
28
|
+
setStatus: () => {},
|
|
29
|
+
setWidget: () => {},
|
|
30
|
+
setFooter: () => {},
|
|
31
|
+
},
|
|
32
|
+
model: { id: "test-model" },
|
|
33
|
+
} as any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeMockPi() {
|
|
37
|
+
return {
|
|
38
|
+
sendMessage: mock.fn(),
|
|
39
|
+
setModel: mock.fn(async () => true),
|
|
40
|
+
} as any;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeMockSession(basePath: string, unitType: string, unitId: string): AutoSession {
|
|
44
|
+
const s = new AutoSession();
|
|
45
|
+
s.basePath = basePath;
|
|
46
|
+
s.active = true;
|
|
47
|
+
s.pendingVerificationRetry = null;
|
|
48
|
+
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
49
|
+
return s;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setupTestEnvironment(): void {
|
|
53
|
+
originalCwd = process.cwd();
|
|
54
|
+
tempDir = join(tmpdir(), `validate-milestone-guard-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
55
|
+
mkdirSync(tempDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
const milestoneDir = join(tempDir, ".gsd", "milestones", "M001");
|
|
58
|
+
mkdirSync(milestoneDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
process.chdir(tempDir);
|
|
61
|
+
_clearGsdRootCache();
|
|
62
|
+
|
|
63
|
+
dbPath = join(tempDir, ".gsd", "gsd.db");
|
|
64
|
+
openDatabase(dbPath);
|
|
65
|
+
invalidateAllCaches();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cleanupTestEnvironment(): void {
|
|
69
|
+
try { process.chdir(originalCwd); } catch { /* ignore */ }
|
|
70
|
+
try { closeDatabase(); } catch { /* ignore */ }
|
|
71
|
+
try { rmSync(tempDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeValidationFile(verdict: string): void {
|
|
75
|
+
const path = join(tempDir, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
|
|
76
|
+
const content = `---
|
|
77
|
+
verdict: ${verdict}
|
|
78
|
+
remediation_round: 1
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Milestone Validation: M001
|
|
82
|
+
|
|
83
|
+
## Verdict Rationale
|
|
84
|
+
Test fixture
|
|
85
|
+
`;
|
|
86
|
+
writeFileSync(path, content, "utf-8");
|
|
87
|
+
invalidateAllCaches();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe("validate-milestone stuck-loop guard (#4094)", () => {
|
|
91
|
+
beforeEach(() => setupTestEnvironment());
|
|
92
|
+
afterEach(() => cleanupTestEnvironment());
|
|
93
|
+
|
|
94
|
+
test("pauses when verdict=needs-remediation and all slices are closed", async () => {
|
|
95
|
+
insertMilestone({ id: "M001" });
|
|
96
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" });
|
|
97
|
+
insertSlice({ id: "S02", milestoneId: "M001", title: "Slice 2", status: "done" });
|
|
98
|
+
writeValidationFile("needs-remediation");
|
|
99
|
+
|
|
100
|
+
const ctx = makeMockCtx();
|
|
101
|
+
const pi = makeMockPi();
|
|
102
|
+
const pauseAutoMock = mock.fn(async () => {});
|
|
103
|
+
const s = makeMockSession(tempDir, "validate-milestone", "M001");
|
|
104
|
+
|
|
105
|
+
const result = await runPostUnitVerification({ s, ctx, pi } as VerificationContext, pauseAutoMock);
|
|
106
|
+
|
|
107
|
+
assert.equal(result, "pause");
|
|
108
|
+
assert.equal(pauseAutoMock.mock.callCount(), 1);
|
|
109
|
+
assert.equal(ctx.ui.notify.mock.callCount(), 1);
|
|
110
|
+
const notifyArgs = ctx.ui.notify.mock.calls[0].arguments;
|
|
111
|
+
assert.match(notifyArgs[0], /needs-remediation/);
|
|
112
|
+
assert.equal(notifyArgs[1], "error");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("treats skipped slices as closed", async () => {
|
|
116
|
+
insertMilestone({ id: "M001" });
|
|
117
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" });
|
|
118
|
+
insertSlice({ id: "S02", milestoneId: "M001", title: "Slice 2", status: "skipped" });
|
|
119
|
+
writeValidationFile("needs-remediation");
|
|
120
|
+
|
|
121
|
+
const ctx = makeMockCtx();
|
|
122
|
+
const pi = makeMockPi();
|
|
123
|
+
const pauseAutoMock = mock.fn(async () => {});
|
|
124
|
+
const s = makeMockSession(tempDir, "validate-milestone", "M001");
|
|
125
|
+
|
|
126
|
+
const result = await runPostUnitVerification({ s, ctx, pi } as VerificationContext, pauseAutoMock);
|
|
127
|
+
|
|
128
|
+
assert.equal(result, "pause");
|
|
129
|
+
assert.equal(pauseAutoMock.mock.callCount(), 1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("continues when verdict=needs-remediation but a queued remediation slice exists", async () => {
|
|
133
|
+
insertMilestone({ id: "M001" });
|
|
134
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" });
|
|
135
|
+
insertSlice({ id: "S02", milestoneId: "M001", title: "Remediation", status: "queued" });
|
|
136
|
+
writeValidationFile("needs-remediation");
|
|
137
|
+
|
|
138
|
+
const ctx = makeMockCtx();
|
|
139
|
+
const pi = makeMockPi();
|
|
140
|
+
const pauseAutoMock = mock.fn(async () => {});
|
|
141
|
+
const s = makeMockSession(tempDir, "validate-milestone", "M001");
|
|
142
|
+
|
|
143
|
+
const result = await runPostUnitVerification({ s, ctx, pi } as VerificationContext, pauseAutoMock);
|
|
144
|
+
|
|
145
|
+
assert.equal(result, "continue");
|
|
146
|
+
assert.equal(pauseAutoMock.mock.callCount(), 0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("continues when verdict is pass", async () => {
|
|
150
|
+
insertMilestone({ id: "M001" });
|
|
151
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" });
|
|
152
|
+
writeValidationFile("pass");
|
|
153
|
+
|
|
154
|
+
const ctx = makeMockCtx();
|
|
155
|
+
const pi = makeMockPi();
|
|
156
|
+
const pauseAutoMock = mock.fn(async () => {});
|
|
157
|
+
const s = makeMockSession(tempDir, "validate-milestone", "M001");
|
|
158
|
+
|
|
159
|
+
const result = await runPostUnitVerification({ s, ctx, pi } as VerificationContext, pauseAutoMock);
|
|
160
|
+
|
|
161
|
+
assert.equal(result, "continue");
|
|
162
|
+
assert.equal(pauseAutoMock.mock.callCount(), 0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("continues when no VALIDATION file exists yet", async () => {
|
|
166
|
+
insertMilestone({ id: "M001" });
|
|
167
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" });
|
|
168
|
+
|
|
169
|
+
const ctx = makeMockCtx();
|
|
170
|
+
const pi = makeMockPi();
|
|
171
|
+
const pauseAutoMock = mock.fn(async () => {});
|
|
172
|
+
const s = makeMockSession(tempDir, "validate-milestone", "M001");
|
|
173
|
+
|
|
174
|
+
const result = await runPostUnitVerification({ s, ctx, pi } as VerificationContext, pauseAutoMock);
|
|
175
|
+
|
|
176
|
+
assert.equal(result, "continue");
|
|
177
|
+
assert.equal(pauseAutoMock.mock.callCount(), 0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// GSD Extension — workflow-logger wiring regression tests
|
|
2
|
+
//
|
|
3
|
+
// Verifies the plumbing between workflow-logger and the rest of the state
|
|
4
|
+
// system (auto-loop phases, detect-stuck, notification store). Without this
|
|
5
|
+
// wiring, warnings/errors logged during a unit leak across units, never
|
|
6
|
+
// reach the user as a consolidated post-unit alert, and don't enrich
|
|
7
|
+
// stuck-detection reasons.
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
logWarning,
|
|
16
|
+
logError,
|
|
17
|
+
peekLogs,
|
|
18
|
+
_resetLogs,
|
|
19
|
+
setStderrLoggingEnabled,
|
|
20
|
+
} from "../workflow-logger.ts";
|
|
21
|
+
import { detectStuck } from "../auto/detect-stuck.ts";
|
|
22
|
+
|
|
23
|
+
const phasesSrc = readFileSync(
|
|
24
|
+
join(import.meta.dirname, "..", "auto", "phases.ts"),
|
|
25
|
+
"utf-8",
|
|
26
|
+
);
|
|
27
|
+
const autoSrc = readFileSync(
|
|
28
|
+
join(import.meta.dirname, "..", "auto.ts"),
|
|
29
|
+
"utf-8",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// ─── Source-scan: phases.ts calls the logger lifecycle API ─────────────────
|
|
33
|
+
|
|
34
|
+
test("auto/phases.ts imports _resetLogs, drainAndSummarize, formatForNotification, hasAnyIssues", () => {
|
|
35
|
+
assert.match(
|
|
36
|
+
phasesSrc,
|
|
37
|
+
/from\s+"\.\.\/workflow-logger\.js"/,
|
|
38
|
+
"phases.ts imports from workflow-logger",
|
|
39
|
+
);
|
|
40
|
+
for (const name of [
|
|
41
|
+
"_resetLogs",
|
|
42
|
+
"drainLogs",
|
|
43
|
+
"drainAndSummarize",
|
|
44
|
+
"formatForNotification",
|
|
45
|
+
"hasAnyIssues",
|
|
46
|
+
]) {
|
|
47
|
+
assert.ok(
|
|
48
|
+
phasesSrc.includes(name),
|
|
49
|
+
`phases.ts should reference ${name}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("runUnitPhase calls _resetLogs() before assigning s.currentUnit", () => {
|
|
55
|
+
// Find the "s.currentUnit = { type: unitType" assignment line and check
|
|
56
|
+
// the preceding ~500 chars contain a _resetLogs() call.
|
|
57
|
+
const idx = phasesSrc.indexOf("s.currentUnit = { type: unitType");
|
|
58
|
+
assert.ok(idx > 0, "runUnitPhase should assign s.currentUnit");
|
|
59
|
+
const before = phasesSrc.slice(Math.max(0, idx - 500), idx);
|
|
60
|
+
assert.match(
|
|
61
|
+
before,
|
|
62
|
+
/_resetLogs\(\)/,
|
|
63
|
+
"_resetLogs() must be called immediately before s.currentUnit assignment",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("runFinalize drains and surfaces logger buffer via ctx.ui.notify", () => {
|
|
68
|
+
// Locate the runFinalize success path and verify it calls drainAndSummarize
|
|
69
|
+
// and routes the result through ctx.ui.notify.
|
|
70
|
+
const runFinalizeIdx = phasesSrc.indexOf("export async function runFinalize");
|
|
71
|
+
assert.ok(runFinalizeIdx > 0, "runFinalize export should exist");
|
|
72
|
+
const finalizeBody = phasesSrc.slice(runFinalizeIdx);
|
|
73
|
+
assert.match(
|
|
74
|
+
finalizeBody,
|
|
75
|
+
/hasAnyIssues\(\)/,
|
|
76
|
+
"runFinalize should gate drain on hasAnyIssues",
|
|
77
|
+
);
|
|
78
|
+
assert.match(
|
|
79
|
+
finalizeBody,
|
|
80
|
+
/drainAndSummarize\(\)/,
|
|
81
|
+
"runFinalize should call drainAndSummarize on success",
|
|
82
|
+
);
|
|
83
|
+
assert.match(
|
|
84
|
+
finalizeBody,
|
|
85
|
+
/formatForNotification\(logs\)/,
|
|
86
|
+
"runFinalize should format drained logs for the notification",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("runFinalize timeout branches drain the buffer to prevent bleed", () => {
|
|
91
|
+
// Both timeout branches null out s.currentUnit — they should also drain
|
|
92
|
+
// so accumulated logs for the timed-out unit don't leak into the next.
|
|
93
|
+
const runFinalizeIdx = phasesSrc.indexOf("export async function runFinalize");
|
|
94
|
+
const finalizeBody = phasesSrc.slice(runFinalizeIdx);
|
|
95
|
+
const drainCallCount =
|
|
96
|
+
(finalizeBody.match(/drainLogs\(\)/g) ?? []).length;
|
|
97
|
+
assert.ok(
|
|
98
|
+
drainCallCount >= 2,
|
|
99
|
+
`runFinalize timeout branches should each call drainLogs() (found ${drainCallCount}, expected >= 2)`,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── Source-scan: auto.ts calls setLogBasePath in startAuto ────────────────
|
|
104
|
+
|
|
105
|
+
test("startAuto calls setLogBasePath(base) so audit log is pinned on resume", () => {
|
|
106
|
+
const startAutoIdx = autoSrc.indexOf("export async function startAuto");
|
|
107
|
+
assert.ok(startAutoIdx > 0, "startAuto export should exist");
|
|
108
|
+
const body = autoSrc.slice(startAutoIdx);
|
|
109
|
+
assert.match(
|
|
110
|
+
body,
|
|
111
|
+
/setLogBasePath\(base\)/,
|
|
112
|
+
"startAuto must call setLogBasePath(base) to pin the audit log",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ─── Runtime: detect-stuck enriches reason with summarizeLogs() ────────────
|
|
117
|
+
|
|
118
|
+
test("detectStuck reason includes workflow-logger summary when logs present", () => {
|
|
119
|
+
setStderrLoggingEnabled(false);
|
|
120
|
+
try {
|
|
121
|
+
_resetLogs();
|
|
122
|
+
logWarning("projection", "STATE.md render failed");
|
|
123
|
+
logError("db", "WAL checkpoint failed");
|
|
124
|
+
|
|
125
|
+
const result = detectStuck([
|
|
126
|
+
{ key: "execute-task/slice-A/task-1", error: "ENOENT: no such file" },
|
|
127
|
+
{ key: "execute-task/slice-A/task-1", error: "ENOENT: no such file" },
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
assert.notEqual(result, null);
|
|
131
|
+
assert.equal(result!.stuck, true);
|
|
132
|
+
assert.match(
|
|
133
|
+
result!.reason,
|
|
134
|
+
/Same error repeated:/,
|
|
135
|
+
"reason should still start with the rule string",
|
|
136
|
+
);
|
|
137
|
+
assert.match(
|
|
138
|
+
result!.reason,
|
|
139
|
+
/STATE\.md render failed/,
|
|
140
|
+
"reason should include the accumulated logger warning",
|
|
141
|
+
);
|
|
142
|
+
assert.match(
|
|
143
|
+
result!.reason,
|
|
144
|
+
/WAL checkpoint failed/,
|
|
145
|
+
"reason should include the accumulated logger error",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Critical: summarizeLogs must not drain — the auto-loop's finalize
|
|
149
|
+
// step owns the buffer lifecycle, detect-stuck is read-only.
|
|
150
|
+
assert.equal(
|
|
151
|
+
peekLogs().length,
|
|
152
|
+
2,
|
|
153
|
+
"detect-stuck must not drain the buffer",
|
|
154
|
+
);
|
|
155
|
+
} finally {
|
|
156
|
+
_resetLogs();
|
|
157
|
+
setStderrLoggingEnabled(true);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("detectStuck reason unchanged when logger buffer is empty", () => {
|
|
162
|
+
setStderrLoggingEnabled(false);
|
|
163
|
+
try {
|
|
164
|
+
_resetLogs();
|
|
165
|
+
const result = detectStuck([
|
|
166
|
+
{ key: "A", error: "boom" },
|
|
167
|
+
{ key: "A", error: "boom" },
|
|
168
|
+
]);
|
|
169
|
+
assert.notEqual(result, null);
|
|
170
|
+
// No trailing " — " suffix when there are no logs to summarize.
|
|
171
|
+
assert.doesNotMatch(
|
|
172
|
+
result!.reason,
|
|
173
|
+
/ — \d+ (error|warning)/,
|
|
174
|
+
"reason should have no logger suffix when buffer is empty",
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
setStderrLoggingEnabled(true);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── Runtime: readTransaction rollback failure surfaces via logError ────────
|
|
182
|
+
//
|
|
183
|
+
// snapshotState now delegates its transaction to readTransaction() in
|
|
184
|
+
// gsd-db.ts (single-writer refactor in #4198), so the split-brain
|
|
185
|
+
// ROLLBACK-failure log lives there, not in workflow-manifest.ts.
|
|
186
|
+
|
|
187
|
+
test("readTransaction logs ROLLBACK failures as split-brain signal", () => {
|
|
188
|
+
const dbSrc = readFileSync(
|
|
189
|
+
join(import.meta.dirname, "..", "gsd-db.ts"),
|
|
190
|
+
"utf-8",
|
|
191
|
+
);
|
|
192
|
+
assert.match(
|
|
193
|
+
dbSrc,
|
|
194
|
+
/logError\("db",\s*"snapshotState ROLLBACK failed"/,
|
|
195
|
+
"readTransaction ROLLBACK catch should call logError",
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ─── Runtime: state.ts and workflow-projections.ts log silent bailouts ─────
|
|
200
|
+
|
|
201
|
+
test("state.ts logs roadmap read failures instead of silently continuing", () => {
|
|
202
|
+
const stateSrc = readFileSync(
|
|
203
|
+
join(import.meta.dirname, "..", "state.ts"),
|
|
204
|
+
"utf-8",
|
|
205
|
+
);
|
|
206
|
+
assert.match(
|
|
207
|
+
stateSrc,
|
|
208
|
+
/logWarning\("state",\s*"reconcileDiskToDb: roadmap read failed/,
|
|
209
|
+
"state.ts reconcileDiskToDb should log roadmap read failures",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("workflow-projections.ts logs DB probe failures instead of silent return", () => {
|
|
214
|
+
const projectionsSrc = readFileSync(
|
|
215
|
+
join(import.meta.dirname, "..", "workflow-projections.ts"),
|
|
216
|
+
"utf-8",
|
|
217
|
+
);
|
|
218
|
+
assert.match(
|
|
219
|
+
projectionsSrc,
|
|
220
|
+
/logWarning\("projection",\s*"renderStateProjection: DB handle probe failed/,
|
|
221
|
+
"renderStateProjection DB probe should log on failure",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getMilestone,
|
|
6
6
|
getSliceStatusSummary,
|
|
7
7
|
getSliceTaskCounts,
|
|
8
|
-
|
|
8
|
+
readTransaction,
|
|
9
9
|
saveGateResult,
|
|
10
10
|
} from "../gsd-db.js";
|
|
11
11
|
import { GATE_REGISTRY } from "../gate-registry.js";
|
|
@@ -616,12 +616,9 @@ export async function executeMilestoneStatus(
|
|
|
616
616
|
};
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
|
|
620
|
-
adapter.exec("BEGIN");
|
|
621
|
-
try {
|
|
619
|
+
return readTransaction(() => {
|
|
622
620
|
const milestone = getMilestone(params.milestoneId);
|
|
623
621
|
if (!milestone) {
|
|
624
|
-
adapter.exec("COMMIT");
|
|
625
622
|
return {
|
|
626
623
|
content: [{ type: "text", text: `Milestone ${params.milestoneId} not found in database.` }],
|
|
627
624
|
details: { operation: "milestone_status", milestoneId: params.milestoneId, found: false },
|
|
@@ -635,8 +632,6 @@ export async function executeMilestoneStatus(
|
|
|
635
632
|
taskCounts: getSliceTaskCounts(params.milestoneId, s.id),
|
|
636
633
|
}));
|
|
637
634
|
|
|
638
|
-
adapter.exec("COMMIT");
|
|
639
|
-
|
|
640
635
|
const result = {
|
|
641
636
|
milestoneId: milestone.id,
|
|
642
637
|
title: milestone.title,
|
|
@@ -651,10 +646,7 @@ export async function executeMilestoneStatus(
|
|
|
651
646
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
652
647
|
details: { operation: "milestone_status", milestoneId: milestone.id, sliceCount: slices.length },
|
|
653
648
|
};
|
|
654
|
-
}
|
|
655
|
-
try { adapter.exec("ROLLBACK"); } catch { /* swallow */ }
|
|
656
|
-
throw txErr;
|
|
657
|
-
}
|
|
649
|
+
});
|
|
658
650
|
} catch (err) {
|
|
659
651
|
const msg = err instanceof Error ? err.message : String(err);
|
|
660
652
|
logWarning("tool", `gsd_milestone_status tool failed: ${msg}`);
|
|
@@ -111,14 +111,9 @@ export function executeReplan(
|
|
|
111
111
|
// Also write replan_triggered_at column for DB-backed detection
|
|
112
112
|
try {
|
|
113
113
|
const req = createRequire(import.meta.url);
|
|
114
|
-
const { isDbAvailable,
|
|
114
|
+
const { isDbAvailable, setSliceReplanTriggeredAt } = req("./gsd-db.js");
|
|
115
115
|
if (isDbAvailable()) {
|
|
116
|
-
|
|
117
|
-
if (adapter) {
|
|
118
|
-
adapter.prepare(
|
|
119
|
-
"UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid",
|
|
120
|
-
).run({ ":ts": ts, ":mid": mid, ":sid": sid });
|
|
121
|
-
}
|
|
116
|
+
setSliceReplanTriggeredAt(mid, sid, ts);
|
|
122
117
|
}
|
|
123
118
|
} catch {
|
|
124
119
|
// DB write is best-effort — disk file is the primary trigger for fallback path
|