@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.738160b9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +17 -4
- package/dist/headless-answers.js +1 -1
- package/dist/headless-ui.d.ts +1 -1
- package/dist/headless-ui.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/models.js +9 -0
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
- package/dist/resources/extensions/gsd/auto-budget.js +5 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
- package/dist/resources/extensions/gsd/auto-verification.js +5 -2
- package/dist/resources/extensions/gsd/auto.js +19 -10
- package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -2
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands-usage.js +1 -1
- package/dist/resources/extensions/gsd/db/engine.js +4 -1
- package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
- package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
- package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
- package/dist/resources/extensions/gsd/git-service.js +57 -0
- package/dist/resources/extensions/gsd/gitignore.js +4 -1
- package/dist/resources/extensions/gsd/json-persistence.js +20 -0
- package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
- package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
- package/dist/resources/extensions/gsd/metrics.js +4 -4
- package/dist/resources/extensions/gsd/migrate-external.js +11 -2
- package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
- package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
- package/dist/resources/extensions/gsd/undo.js +43 -6
- package/dist/resources/extensions/gsd/unit-registry.js +4 -2
- package/dist/resources/extensions/gsd/verification-gate.js +7 -0
- package/dist/resources/extensions/gsd/workflow-manifest.js +126 -13
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- 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 +9 -9
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +3 -2
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +46 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +38 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/models.ts +9 -0
- package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/session.ts +9 -0
- package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
- package/src/resources/extensions/gsd/auto-budget.ts +12 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto-verification.ts +5 -1
- package/src/resources/extensions/gsd/auto.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +41 -2
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands-usage.ts +1 -1
- package/src/resources/extensions/gsd/db/engine.ts +8 -1
- package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
- package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
- package/src/resources/extensions/gsd/doctor-types.ts +3 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
- package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
- package/src/resources/extensions/gsd/git-service.ts +64 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/json-persistence.ts +20 -0
- package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
- package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
- package/src/resources/extensions/gsd/metrics.ts +4 -4
- package/src/resources/extensions/gsd/migrate-external.ts +8 -2
- package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
- package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
- package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +37 -2
- package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +96 -15
- package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
- package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
- package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
- package/src/resources/extensions/gsd/undo.ts +47 -6
- package/src/resources/extensions/gsd/unit-registry.ts +4 -2
- package/src/resources/extensions/gsd/verification-gate.ts +8 -0
- package/src/resources/extensions/gsd/workflow-manifest.ts +141 -7
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { classifyCommand } from "../safety/destructive-guard.ts";
|
|
5
|
+
|
|
6
|
+
test("destructive guard ignores rm -rf text inside quoted literals and comments", () => {
|
|
7
|
+
const readOnlyCommands = [
|
|
8
|
+
'grep -rn "rm -rf" .',
|
|
9
|
+
'echo "rm -rf /tmp/x"',
|
|
10
|
+
'git commit -m "remove stray rm -rf usage"',
|
|
11
|
+
"cat notes.md # remove stray rm -rf usage",
|
|
12
|
+
'printf "rm -rf /tmp/x\\n"',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
for (const command of readOnlyCommands) {
|
|
16
|
+
assert.deepEqual(classifyCommand(command), { destructive: false, labels: [] }, command);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("destructive guard still detects direct and wrapper-prefixed recursive deletes", () => {
|
|
21
|
+
const destructiveCommands = [
|
|
22
|
+
"rm -rf build",
|
|
23
|
+
"rm build -rf",
|
|
24
|
+
"sudo rm -rf /",
|
|
25
|
+
"doas rm -rf /",
|
|
26
|
+
"sudo -u bob rm -rf /x",
|
|
27
|
+
"timeout 5 rm -rf /x",
|
|
28
|
+
"time rm -rf build",
|
|
29
|
+
"find . -exec rm -rf {} \\;",
|
|
30
|
+
"cat doomed.txt | xargs rm -rf /tmp/x",
|
|
31
|
+
'rm -rf "/tmp/my build"',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const command of destructiveCommands) {
|
|
35
|
+
assert.deepEqual(classifyCommand(command), {
|
|
36
|
+
destructive: true,
|
|
37
|
+
labels: ["recursive delete"],
|
|
38
|
+
}, command);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("destructive guard treats quoted interpreter payloads as literals", () => {
|
|
43
|
+
assert.deepEqual(classifyCommand('bash -c "rm -rf /"'), { destructive: false, labels: [] });
|
|
44
|
+
});
|
|
@@ -96,6 +96,40 @@ function scaffoldMilestoneContext(basePath: string, mid: string): void {
|
|
|
96
96
|
].join("\n"));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function scaffoldLegacyMilestoneContext(basePath: string, mid: string): void {
|
|
100
|
+
const dir = join(basePath, ".gsd", "milestones", mid);
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
writeFileSync(join(dir, `${mid}-CONTEXT.md`), [
|
|
103
|
+
`# ${mid}: Test Milestone`,
|
|
104
|
+
"",
|
|
105
|
+
"Context for legacy dispatch recovery tests.",
|
|
106
|
+
"",
|
|
107
|
+
].join("\n"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function scaffoldLegacySlicePlan(basePath: string, mid: string, sid: string): void {
|
|
111
|
+
const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid);
|
|
112
|
+
mkdirSync(join(dir, "tasks"), { recursive: true });
|
|
113
|
+
writeFileSync(join(dir, `${sid}-PLAN.md`), [
|
|
114
|
+
`# ${sid}: Legacy Slice`,
|
|
115
|
+
"",
|
|
116
|
+
"## Tasks",
|
|
117
|
+
"- T01: Do something",
|
|
118
|
+
"",
|
|
119
|
+
].join("\n"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scaffoldLegacyTaskPlan(basePath: string, mid: string, sid: string, tid: string): void {
|
|
123
|
+
const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks");
|
|
124
|
+
mkdirSync(dir, { recursive: true });
|
|
125
|
+
writeFileSync(join(dir, `${tid}-PLAN.md`), [
|
|
126
|
+
`# ${tid}: First Task`,
|
|
127
|
+
"",
|
|
128
|
+
"Implement the task.",
|
|
129
|
+
"",
|
|
130
|
+
].join("\n"));
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
function scaffoldTaskPlan(basePath: string, mid: string, sid: string, tid: string): void {
|
|
100
134
|
// Flat-phase: no per-task plan files. This is a no-op — tasks live as
|
|
101
135
|
// checkboxes inside the slice plan. Kept for backward-compat with tests
|
|
@@ -167,6 +201,115 @@ test("dispatch: present task plan proceeds to execute-task normally", async (t)
|
|
|
167
201
|
`unitId should be M002/S03/T01, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
|
|
168
202
|
});
|
|
169
203
|
|
|
204
|
+
test("dispatch: missing legacy task plan recovery increments a per-slice retry counter", async (t) => {
|
|
205
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-retry-"));
|
|
206
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
207
|
+
|
|
208
|
+
scaffoldLegacyMilestoneContext(tmp, "M002");
|
|
209
|
+
scaffoldLegacySlicePlan(tmp, "M002", "S03");
|
|
210
|
+
|
|
211
|
+
const session = {
|
|
212
|
+
missingTaskPlanRetryCount: new Map<string, number>(),
|
|
213
|
+
};
|
|
214
|
+
const result = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
|
|
215
|
+
|
|
216
|
+
assert.equal(result.action, "dispatch");
|
|
217
|
+
assert.ok(result.action === "dispatch" && result.unitType === "plan-slice",
|
|
218
|
+
`unitType should be plan-slice, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
|
|
219
|
+
assert.ok(result.action === "dispatch" && result.unitId === "M002/S03",
|
|
220
|
+
`unitId should be M002/S03, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
|
|
221
|
+
assert.equal(session.missingTaskPlanRetryCount.get("M002/S03"), 1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("dispatch: missing legacy task plan recovery stops when retry counter is exhausted", async (t) => {
|
|
225
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-stop-"));
|
|
226
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
227
|
+
|
|
228
|
+
scaffoldLegacyMilestoneContext(tmp, "M002");
|
|
229
|
+
scaffoldLegacySlicePlan(tmp, "M002", "S03");
|
|
230
|
+
|
|
231
|
+
const session = {
|
|
232
|
+
missingTaskPlanRetryCount: new Map<string, number>([["M002/S03", 2]]),
|
|
233
|
+
};
|
|
234
|
+
const result = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
|
|
235
|
+
|
|
236
|
+
assert.equal(result.action, "stop");
|
|
237
|
+
assert.ok(result.action === "stop");
|
|
238
|
+
assert.equal(result.level, "error");
|
|
239
|
+
assert.match(result.reason, /Missing task-plan recovery failed 2 times for M002\/S03/);
|
|
240
|
+
assert.match(result.reason, /manual intervention required/);
|
|
241
|
+
assert.equal(session.missingTaskPlanRetryCount.has("M002/S03"), false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("dispatch: present legacy task plan clears missing-plan recovery retry counter", async (t) => {
|
|
245
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-clear-"));
|
|
246
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
247
|
+
|
|
248
|
+
scaffoldLegacyMilestoneContext(tmp, "M002");
|
|
249
|
+
scaffoldLegacySlicePlan(tmp, "M002", "S03");
|
|
250
|
+
scaffoldLegacyTaskPlan(tmp, "M002", "S03", "T01");
|
|
251
|
+
|
|
252
|
+
const session = {
|
|
253
|
+
missingTaskPlanRetryCount: new Map<string, number>([["M002/S03", 1]]),
|
|
254
|
+
preExecRetryCount: new Map<string, number>([["M002/S03", 2]]),
|
|
255
|
+
};
|
|
256
|
+
const result = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
|
|
257
|
+
|
|
258
|
+
assert.equal(result.action, "dispatch");
|
|
259
|
+
assert.ok(result.action === "dispatch" && result.unitType === "execute-task",
|
|
260
|
+
`unitType should be execute-task, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
|
|
261
|
+
assert.equal(session.missingTaskPlanRetryCount.has("M002/S03"), false);
|
|
262
|
+
assert.equal(session.preExecRetryCount.get("M002/S03"), 2,
|
|
263
|
+
"pre-exec retry counter must not be cleared when task plan is present");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("dispatch: missing-task-plan recovery loop terminates even when the shared pre-exec key is reset between rounds (#1087)", async (t) => {
|
|
267
|
+
// The recovery rule re-dispatches plan-slice with unitId "${mid}/${sid}".
|
|
268
|
+
// That regenerated plan-slice carries the same currentUnit.id, and on a
|
|
269
|
+
// pre-execution pass the post-unit hook deletes that key from
|
|
270
|
+
// preExecRetryCount (auto-post-unit.ts). Missing per-task PLAN projection
|
|
271
|
+
// files do not fail pre-exec checks, so the regenerated plan-slice typically
|
|
272
|
+
// passes and that delete fires every cycle. If recovery shared
|
|
273
|
+
// preExecRetryCount, its counter would be wiped to 0 each round and the loop
|
|
274
|
+
// would never reach the cap. Here we simulate that reset between rounds and
|
|
275
|
+
// assert the dedicated counter still climbs to MAX and stops.
|
|
276
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-1087-loop-term-"));
|
|
277
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
278
|
+
|
|
279
|
+
scaffoldLegacyMilestoneContext(tmp, "M002");
|
|
280
|
+
scaffoldLegacySlicePlan(tmp, "M002", "S03");
|
|
281
|
+
|
|
282
|
+
const session = {
|
|
283
|
+
missingTaskPlanRetryCount: new Map<string, number>(),
|
|
284
|
+
preExecRetryCount: new Map<string, number>(),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Round 1: missing task plan → recover (counter 0 → 1).
|
|
288
|
+
const r1 = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
|
|
289
|
+
assert.ok(r1.action === "dispatch" && r1.unitType === "plan-slice",
|
|
290
|
+
`round 1 should re-dispatch plan-slice, got: ${r1.action === "dispatch" ? r1.unitType : "(stop)"}`);
|
|
291
|
+
assert.equal(session.missingTaskPlanRetryCount.get("M002/S03"), 1);
|
|
292
|
+
|
|
293
|
+
// The regenerated plan-slice passes its post-unit pre-exec check, deleting
|
|
294
|
+
// the shared "${mid}/${sid}" key from preExecRetryCount.
|
|
295
|
+
session.preExecRetryCount.delete("M002/S03");
|
|
296
|
+
|
|
297
|
+
// Round 2: still missing → recover (counter 1 → 2), unaffected by the reset.
|
|
298
|
+
const r2 = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
|
|
299
|
+
assert.ok(r2.action === "dispatch" && r2.unitType === "plan-slice",
|
|
300
|
+
`round 2 should re-dispatch plan-slice, got: ${r2.action === "dispatch" ? r2.unitType : "(stop)"}`);
|
|
301
|
+
assert.equal(session.missingTaskPlanRetryCount.get("M002/S03"), 2);
|
|
302
|
+
|
|
303
|
+
session.preExecRetryCount.delete("M002/S03");
|
|
304
|
+
|
|
305
|
+
// Round 3: cap reached → stop. The loop terminates despite the resets.
|
|
306
|
+
const r3 = await resolveDispatch(makeContextFor(tmp, "M002", "S03", "T01", session));
|
|
307
|
+
assert.equal(r3.action, "stop");
|
|
308
|
+
assert.ok(r3.action === "stop" && r3.level === "error");
|
|
309
|
+
assert.match(r3.reason, /Missing task-plan recovery failed 2 times for M002\/S03/);
|
|
310
|
+
assert.equal(session.missingTaskPlanRetryCount.has("M002/S03"), false);
|
|
311
|
+
});
|
|
312
|
+
|
|
170
313
|
test("dispatch: session milestone mismatch stops before missing-task-plan recovery", async (t) => {
|
|
171
314
|
const tmp = mkdtempSync(join(tmpdir(), "gsd-session-milestone-mismatch-"));
|
|
172
315
|
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { afterEach, test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
_getAdapter,
|
|
5
|
+
closeDatabase,
|
|
6
|
+
insertArtifact,
|
|
7
|
+
insertMilestone,
|
|
8
|
+
insertSlice,
|
|
9
|
+
insertTask,
|
|
10
|
+
openDatabase,
|
|
11
|
+
} from "../gsd-db.ts";
|
|
12
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
13
|
import { join } from "node:path";
|
|
6
14
|
import { tmpdir } from "node:os";
|
|
7
15
|
import { filterDoctorIssues } from "../doctor-format.ts";
|
|
8
16
|
import { checkEngineHealth } from "../doctor-engine-checks.ts";
|
|
17
|
+
import { MEMORIES_FTS_REBUILT_KEY } from "../db-memory-fts-schema.ts";
|
|
9
18
|
import { appendEvent } from "../workflow-events.ts";
|
|
19
|
+
import { renderPlanFromDb, renderRoadmapFromDb } from "../markdown-renderer.ts";
|
|
10
20
|
|
|
11
21
|
afterEach(() => {
|
|
12
22
|
closeDatabase();
|
|
@@ -61,6 +71,139 @@ test("checkEngineHealth reports db_unavailable when gsd.db exists but the DB is
|
|
|
61
71
|
assert.equal(dbIssue.file, ".gsd/gsd.db");
|
|
62
72
|
});
|
|
63
73
|
|
|
74
|
+
test("checkEngineHealth reports memories_fts without the rebuild marker", async (t) => {
|
|
75
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-memory-fts-marker-"));
|
|
76
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
77
|
+
|
|
78
|
+
const gsdDir = join(base, ".gsd");
|
|
79
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
80
|
+
|
|
81
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
82
|
+
const adapter = _getAdapter()!;
|
|
83
|
+
const fts = adapter.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
|
|
84
|
+
if (!fts) return;
|
|
85
|
+
|
|
86
|
+
adapter.prepare(
|
|
87
|
+
"DELETE FROM runtime_kv WHERE scope = 'global' AND scope_id = '' AND key = :key",
|
|
88
|
+
).run({ ":key": MEMORIES_FTS_REBUILT_KEY });
|
|
89
|
+
|
|
90
|
+
const issues: any[] = [];
|
|
91
|
+
await checkEngineHealth(base, issues, []);
|
|
92
|
+
|
|
93
|
+
const ftsIssue = issues.find((issue) => issue.code === "memories_fts_rebuild_missing");
|
|
94
|
+
assert.ok(ftsIssue, "doctor should surface a potentially desynced memory FTS index");
|
|
95
|
+
assert.equal(ftsIssue.severity, "warning");
|
|
96
|
+
assert.equal(ftsIssue.unitId, "project");
|
|
97
|
+
assert.equal(ftsIssue.file, ".gsd/gsd.db");
|
|
98
|
+
assert.equal(ftsIssue.fixable, false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("checkEngineHealth reports checkbox divergence against DB status", async (t) => {
|
|
102
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-drift-"));
|
|
103
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
104
|
+
|
|
105
|
+
const gsdDir = join(base, ".gsd");
|
|
106
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
109
|
+
insertMilestone({ id: "M001", title: "Foundation", status: "active" });
|
|
110
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
|
|
111
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
|
|
112
|
+
|
|
113
|
+
const roadmap = await renderRoadmapFromDb(base, "M001");
|
|
114
|
+
if ("skipped" in roadmap) assert.fail("planned milestone should render a roadmap");
|
|
115
|
+
const plan = await renderPlanFromDb(base, "M001", "S01");
|
|
116
|
+
|
|
117
|
+
writeFileSync(roadmap.roadmapPath, readFileSync(roadmap.roadmapPath, "utf-8").replace("- [ ] **S01:", "- [x] **S01:"), "utf-8");
|
|
118
|
+
writeFileSync(plan.planPath, readFileSync(plan.planPath, "utf-8").replace("- [x] **T01**:", "- [ ] **T01**:"), "utf-8");
|
|
119
|
+
|
|
120
|
+
const issues: any[] = [];
|
|
121
|
+
await checkEngineHealth(base, issues, []);
|
|
122
|
+
|
|
123
|
+
const divergences = issues.filter((issue) => issue.code === "checkbox_db_status_divergence");
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
divergences.map((issue) => issue.unitId).sort(),
|
|
126
|
+
["M001/S01", "M001/S01/T01"],
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("checkEngineHealth keeps PLAN checkbox divergence after stale projection flush", async (t) => {
|
|
131
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-plan-drift-"));
|
|
132
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
133
|
+
|
|
134
|
+
const gsdDir = join(base, ".gsd");
|
|
135
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
136
|
+
|
|
137
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
138
|
+
insertMilestone({ id: "M001", title: "Foundation", status: "active" });
|
|
139
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
|
|
140
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
|
|
141
|
+
|
|
142
|
+
const roadmap = await renderRoadmapFromDb(base, "M001");
|
|
143
|
+
if ("skipped" in roadmap) assert.fail("planned milestone should render a roadmap");
|
|
144
|
+
const plan = await renderPlanFromDb(base, "M001", "S01");
|
|
145
|
+
|
|
146
|
+
writeFileSync(roadmap.roadmapPath, readFileSync(roadmap.roadmapPath, "utf-8").replace("- [ ] **S01:", "- [x] **S01:"), "utf-8");
|
|
147
|
+
writeFileSync(plan.planPath, readFileSync(plan.planPath, "utf-8").replace("- [x] **T01**:", "- [ ] **T01**:"), "utf-8");
|
|
148
|
+
appendEvent(base, {
|
|
149
|
+
cmd: "complete-task",
|
|
150
|
+
params: { milestoneId: "M001", sliceId: "S01", taskId: "T01" },
|
|
151
|
+
ts: "2999-01-01T00:00:00.000Z",
|
|
152
|
+
actor: "agent",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const issues: any[] = [];
|
|
156
|
+
const fixes: string[] = [];
|
|
157
|
+
await checkEngineHealth(base, issues, fixes);
|
|
158
|
+
|
|
159
|
+
const divergences = issues.filter((issue) => issue.code === "checkbox_db_status_divergence");
|
|
160
|
+
assert.deepEqual(
|
|
161
|
+
divergences.map((issue) => issue.unitId),
|
|
162
|
+
["M001/S01/T01"],
|
|
163
|
+
"stale ROADMAP divergence is cleared after re-render, but stale PLAN task divergence remains",
|
|
164
|
+
);
|
|
165
|
+
assert.ok(fixes.includes("re-rendered stale projections for M001"));
|
|
166
|
+
assert.match(readFileSync(plan.planPath, "utf-8"), /- \[ \] \*\*T01\*\*:/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("checkEngineHealth reads task checkboxes from the <tasks> block, not a stray line above it", async (t) => {
|
|
170
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-task-section-"));
|
|
171
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
172
|
+
|
|
173
|
+
const gsdDir = join(base, ".gsd");
|
|
174
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
175
|
+
|
|
176
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
177
|
+
insertMilestone({ id: "M001", title: "Foundation", status: "active" });
|
|
178
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
|
|
179
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
|
|
180
|
+
|
|
181
|
+
const roadmap = await renderRoadmapFromDb(base, "M001");
|
|
182
|
+
if ("skipped" in roadmap) assert.fail("planned milestone should render a roadmap");
|
|
183
|
+
const plan = await renderPlanFromDb(base, "M001", "S01");
|
|
184
|
+
|
|
185
|
+
// Flip the authoritative <tasks> checkbox so it disagrees with the DB
|
|
186
|
+
// (DB: complete, markdown: unchecked = real drift), then inject a *checked*
|
|
187
|
+
// decoy line for the same task above the <tasks> block. The old whole-file
|
|
188
|
+
// regex matched the decoy first and hid the real drift; parsePlan reads the
|
|
189
|
+
// authoritative <tasks> block and still reports it. No events are appended,
|
|
190
|
+
// so the projection-drift re-render never runs and the crafted file stands.
|
|
191
|
+
const rendered = readFileSync(plan.planPath, "utf-8")
|
|
192
|
+
.replace("- [x] **T01**:", "- [ ] **T01**:")
|
|
193
|
+
.replace("<tasks>\n", "- [x] **T01**: stale duplicate above the tasks block\n\n<tasks>\n");
|
|
194
|
+
writeFileSync(plan.planPath, rendered, "utf-8");
|
|
195
|
+
|
|
196
|
+
const issues: any[] = [];
|
|
197
|
+
await checkEngineHealth(base, issues, []);
|
|
198
|
+
|
|
199
|
+
const divergences = issues.filter((issue) => issue.code === "checkbox_db_status_divergence");
|
|
200
|
+
assert.deepEqual(
|
|
201
|
+
divergences.map((issue) => issue.unitId),
|
|
202
|
+
["M001/S01/T01"],
|
|
203
|
+
"task drift must be read from the authoritative <tasks> block, not a stray checkbox above it",
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
64
207
|
test("checkEngineHealth reads canonical reopen events from worktree bases", async (t) => {
|
|
65
208
|
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-reopen-worktree-"));
|
|
66
209
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -149,3 +292,97 @@ test("checkEngineHealth treats explicit reopen as authoritative when dispatch ti
|
|
|
149
292
|
"explicit reopen should exempt reopened milestone even when completion dispatch timestamps are absent",
|
|
150
293
|
);
|
|
151
294
|
});
|
|
295
|
+
|
|
296
|
+
test("checkEngineHealth reports artifact rows whose files are missing on disk", async (t) => {
|
|
297
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-missing-artifact-"));
|
|
298
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
299
|
+
|
|
300
|
+
const gsdDir = join(base, ".gsd");
|
|
301
|
+
const worktree = join(gsdDir, "worktrees", "M001");
|
|
302
|
+
mkdirSync(join(worktree, ".gsd", "milestones", "M001"), { recursive: true });
|
|
303
|
+
writeFileSync(join(worktree, ".git"), "gitdir: ../../../../.git/worktrees/M001\n", "utf-8");
|
|
304
|
+
writeFileSync(join(worktree, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# Context\n", "utf-8");
|
|
305
|
+
|
|
306
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
307
|
+
insertArtifact({
|
|
308
|
+
path: "milestones/M001/M001-CONTEXT.md",
|
|
309
|
+
artifact_type: "CONTEXT",
|
|
310
|
+
milestone_id: "M001",
|
|
311
|
+
slice_id: null,
|
|
312
|
+
task_id: null,
|
|
313
|
+
full_content: "# Context\n",
|
|
314
|
+
});
|
|
315
|
+
insertArtifact({
|
|
316
|
+
path: "milestones/M001/M001-MISSING.md",
|
|
317
|
+
artifact_type: "CONTEXT",
|
|
318
|
+
milestone_id: "M001",
|
|
319
|
+
slice_id: null,
|
|
320
|
+
task_id: null,
|
|
321
|
+
full_content: "# Missing\n",
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const issues: any[] = [];
|
|
325
|
+
await checkEngineHealth(worktree, issues, []);
|
|
326
|
+
|
|
327
|
+
assert.equal(
|
|
328
|
+
issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === "milestones/M001/M001-CONTEXT.md"),
|
|
329
|
+
false,
|
|
330
|
+
"worktree-local artifacts should resolve through the projection root",
|
|
331
|
+
);
|
|
332
|
+
const missing = issues.find((issue) => issue.code === "artifact_file_missing" && issue.file === "milestones/M001/M001-MISSING.md");
|
|
333
|
+
assert.ok(missing, "missing artifact rows should be reported");
|
|
334
|
+
assert.equal(missing.unitId, "M001");
|
|
335
|
+
assert.equal(missing.fixable, false);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("checkEngineHealth clears artifact_file_missing after projection re-render recreates the file", async (t) => {
|
|
339
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-stale-missing-artifact-"));
|
|
340
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
341
|
+
|
|
342
|
+
const gsdDir = join(base, ".gsd");
|
|
343
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
344
|
+
|
|
345
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
346
|
+
insertMilestone({ id: "M001", title: "Foundation", status: "active", planning: { vision: "Ship the foundation." } });
|
|
347
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
|
|
348
|
+
insertArtifact({
|
|
349
|
+
path: "phases/01-foundation/01-ROADMAP.md",
|
|
350
|
+
artifact_type: "ROADMAP",
|
|
351
|
+
milestone_id: "M001",
|
|
352
|
+
slice_id: null,
|
|
353
|
+
task_id: null,
|
|
354
|
+
full_content: "# stale row only\n",
|
|
355
|
+
});
|
|
356
|
+
// CONTEXT is not re-rendered by flushWorkflowProjections, so its missing-file
|
|
357
|
+
// diagnostic must survive the post-re-render cleanup (guards against the
|
|
358
|
+
// clearing logic over-broadening to artifacts the same run did not recreate).
|
|
359
|
+
insertArtifact({
|
|
360
|
+
path: "phases/01-foundation/01-CONTEXT.md",
|
|
361
|
+
artifact_type: "CONTEXT",
|
|
362
|
+
milestone_id: "M001",
|
|
363
|
+
slice_id: null,
|
|
364
|
+
task_id: null,
|
|
365
|
+
full_content: "# stale context row only\n",
|
|
366
|
+
});
|
|
367
|
+
appendEvent(base, {
|
|
368
|
+
cmd: "plan-milestone",
|
|
369
|
+
params: { milestoneId: "M001" },
|
|
370
|
+
ts: "2999-01-01T00:00:00.000Z",
|
|
371
|
+
actor: "agent",
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const issues: any[] = [];
|
|
375
|
+
const fixes: string[] = [];
|
|
376
|
+
await checkEngineHealth(base, issues, fixes);
|
|
377
|
+
|
|
378
|
+
assert.ok(fixes.includes("re-rendered missing projections for M001"));
|
|
379
|
+
assert.equal(
|
|
380
|
+
issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === "phases/01-foundation/01-ROADMAP.md"),
|
|
381
|
+
false,
|
|
382
|
+
"doctor should not report a missing artifact that projection repair recreated in the same run",
|
|
383
|
+
);
|
|
384
|
+
assert.ok(
|
|
385
|
+
issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === "phases/01-foundation/01-CONTEXT.md"),
|
|
386
|
+
"doctor should still report a missing artifact that projection repair did not recreate",
|
|
387
|
+
);
|
|
388
|
+
});
|
|
@@ -7,6 +7,7 @@ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:f
|
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
10
|
+
import { getShellConfig } from '@gsd/pi-coding-agent';
|
|
10
11
|
import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
|
|
11
12
|
import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
|
|
12
13
|
import { isContextModeEnabled } from '../preferences-types.ts';
|
|
@@ -53,6 +54,21 @@ test('runExecSandbox: captures stdout, persists artifacts, returns digest', asyn
|
|
|
53
54
|
}
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
test('runExecSandbox: bash runtime uses resolved shell config', async () => {
|
|
58
|
+
const base = freshBase();
|
|
59
|
+
try {
|
|
60
|
+
const { shell } = getShellConfig();
|
|
61
|
+
const result = await runExecSandbox(
|
|
62
|
+
{ runtime: 'bash', script: 'printf "%s" "$0"' },
|
|
63
|
+
baseOpts(base),
|
|
64
|
+
);
|
|
65
|
+
assert.equal(result.exit_code, 0);
|
|
66
|
+
assert.equal(readFileSync(result.stdout_path, 'utf-8'), shell);
|
|
67
|
+
} finally {
|
|
68
|
+
cleanup(base);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
56
72
|
test('runExecSandbox: persists optional request metadata', async () => {
|
|
57
73
|
const base = freshBase();
|
|
58
74
|
try {
|
|
@@ -116,6 +116,46 @@ test("effectiveFileChangeAllowlist keeps .gitignore auditable when management is
|
|
|
116
116
|
assert.deepEqual(effectiveFileChangeAllowlist(["docs/**"], false), ["docs/**"]);
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
test("validateFileChanges excludes .gsd-backups/ migration snapshots from unexpected-change warnings", (t) => {
|
|
120
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
|
121
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
122
|
+
|
|
123
|
+
const backupFile = join(
|
|
124
|
+
base,
|
|
125
|
+
".gsd-backups",
|
|
126
|
+
"migrate-1782703701330",
|
|
127
|
+
"M010",
|
|
128
|
+
"slices",
|
|
129
|
+
"S01",
|
|
130
|
+
"S01-ASSESSMENT.md",
|
|
131
|
+
);
|
|
132
|
+
mkdirSync(join(base, "src"), { recursive: true });
|
|
133
|
+
mkdirSync(join(backupFile, ".."), { recursive: true });
|
|
134
|
+
|
|
135
|
+
git(base, "init");
|
|
136
|
+
git(base, "config", "user.email", "test@example.com");
|
|
137
|
+
git(base, "config", "user.name", "Test User");
|
|
138
|
+
|
|
139
|
+
writeFileSync(join(base, "src", "app.ts"), "initial\n");
|
|
140
|
+
writeFileSync(backupFile, "legacy assessment\n");
|
|
141
|
+
git(base, "add", ".");
|
|
142
|
+
git(base, "commit", "-m", "initial");
|
|
143
|
+
|
|
144
|
+
writeFileSync(join(base, "src", "app.ts"), "updated\n");
|
|
145
|
+
writeFileSync(backupFile, "legacy assessment touched\n");
|
|
146
|
+
git(base, "add", ".");
|
|
147
|
+
git(base, "commit", "-m", "task commit");
|
|
148
|
+
|
|
149
|
+
const audit = validateFileChanges(base, ["src/app.ts"], []);
|
|
150
|
+
assert.ok(audit, "audit should be produced");
|
|
151
|
+
assert.deepEqual(audit!.unexpectedFiles, [], ".gsd-backups/ must not trigger warnings");
|
|
152
|
+
assert.equal(
|
|
153
|
+
audit!.violations.filter(v => v.severity === "warning").length,
|
|
154
|
+
0,
|
|
155
|
+
"no warnings when only source and migration backup files changed",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
119
159
|
test("GSD-managed .gitignore edit swept into a task commit is not flagged", (t) => {
|
|
120
160
|
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
|
121
161
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
// File Purpose: Tests the one-time migration from nested to flat-phase layout.
|
|
3
3
|
import test, { afterEach } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync, readFileSync, utimesSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
migrateToFlatPhase,
|
|
12
|
+
needsFlatPhaseMigration,
|
|
13
|
+
pruneStaleFlatPhaseBackups,
|
|
14
|
+
} from "../flat-phase-migration.ts";
|
|
11
15
|
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getAllMilestones, getMilestoneSlices, getSliceTasks } from "../gsd-db.ts";
|
|
12
16
|
|
|
13
17
|
const tmpDirs: string[] = [];
|
|
@@ -112,3 +116,34 @@ test("migrateToFlatPhase preserves slice sidecar artifacts and skips recovery pl
|
|
|
112
116
|
"recovery placeholder PLAN should not be promoted when no DB tasks can render a real plan",
|
|
113
117
|
);
|
|
114
118
|
});
|
|
119
|
+
|
|
120
|
+
test("pruneStaleFlatPhaseBackups removes migrate-* dirs older than retention window", async () => {
|
|
121
|
+
const base = makeTmp();
|
|
122
|
+
await migrateToFlatPhase(base);
|
|
123
|
+
|
|
124
|
+
const backupRoot = join(base, ".gsd-backups");
|
|
125
|
+
assert.ok(existsSync(backupRoot), "backup should exist immediately after migration");
|
|
126
|
+
|
|
127
|
+
const staleDir = join(backupRoot, "migrate-stale");
|
|
128
|
+
mkdirSync(staleDir, { recursive: true });
|
|
129
|
+
writeFileSync(join(staleDir, "marker.txt"), "old backup\n", "utf-8");
|
|
130
|
+
const staleDate = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000);
|
|
131
|
+
utimesSync(staleDir, staleDate, staleDate);
|
|
132
|
+
|
|
133
|
+
const removed = pruneStaleFlatPhaseBackups(base);
|
|
134
|
+
assert.equal(removed, 1, "stale migrate-* dir should be pruned");
|
|
135
|
+
assert.equal(existsSync(staleDir), false, "stale backup dir should be gone");
|
|
136
|
+
assert.ok(existsSync(backupRoot), "fresh migration backup should remain");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("pruneStaleFlatPhaseBackups is a no-op while flat-phase migration is still needed", () => {
|
|
140
|
+
const base = makeTmp();
|
|
141
|
+
const backupRoot = join(base, ".gsd-backups", "migrate-stale");
|
|
142
|
+
mkdirSync(backupRoot, { recursive: true });
|
|
143
|
+
writeFileSync(join(backupRoot, "marker.txt"), "old backup\n", "utf-8");
|
|
144
|
+
const staleDate = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000);
|
|
145
|
+
utimesSync(backupRoot, staleDate, staleDate);
|
|
146
|
+
|
|
147
|
+
assert.equal(pruneStaleFlatPhaseBackups(base), 0, "must not prune while migration is pending");
|
|
148
|
+
assert.ok(existsSync(backupRoot), "backup must remain until migration completes");
|
|
149
|
+
});
|
|
@@ -40,6 +40,10 @@ describe('ensureGitignore writes .bg-shell/ baseline (#4902)', () => {
|
|
|
40
40
|
lines.has('.bg-shell/'),
|
|
41
41
|
`.gitignore should include .bg-shell/. Got:\n${ignore}`,
|
|
42
42
|
);
|
|
43
|
+
assert.ok(
|
|
44
|
+
lines.has('.gsd-backups/'),
|
|
45
|
+
`.gitignore should include .gsd-backups/. Got:\n${ignore}`,
|
|
46
|
+
);
|
|
43
47
|
} finally {
|
|
44
48
|
cleanup(dir);
|
|
45
49
|
}
|
|
@@ -294,12 +294,13 @@ describe('git-service', async () => {
|
|
|
294
294
|
|
|
295
295
|
assert.deepStrictEqual(
|
|
296
296
|
RUNTIME_EXCLUSION_PATHS.length,
|
|
297
|
-
|
|
298
|
-
"exactly
|
|
297
|
+
18,
|
|
298
|
+
"exactly 18 runtime exclusion paths"
|
|
299
299
|
);
|
|
300
300
|
|
|
301
301
|
const expectedPaths = [
|
|
302
302
|
".gsd-worktrees/",
|
|
303
|
+
".gsd-backups/**",
|
|
303
304
|
".gsd/activity/",
|
|
304
305
|
".gsd/audit/",
|
|
305
306
|
".gsd/forensics/",
|
|
@@ -1623,11 +1624,13 @@ process.exit(result.status ?? 0);
|
|
|
1623
1624
|
// Create and track runtime files (simulates pre-.gitignore state)
|
|
1624
1625
|
mkdirSync(join(repo, ".gsd", "activity"), { recursive: true });
|
|
1625
1626
|
mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true });
|
|
1627
|
+
mkdirSync(join(repo, ".gsd-backups", "migrate-old"), { recursive: true });
|
|
1626
1628
|
writeFileSync(join(repo, ".gsd", "completed-units.json"), '["u1"]');
|
|
1627
1629
|
writeFileSync(join(repo, ".gsd", "metrics.json"), '{}');
|
|
1628
1630
|
writeFileSync(join(repo, ".gsd", "STATE.md"), "# State");
|
|
1629
1631
|
writeFileSync(join(repo, ".gsd", "activity", "log.jsonl"), "{}");
|
|
1630
1632
|
writeFileSync(join(repo, ".gsd", "runtime", "data.json"), "{}");
|
|
1633
|
+
writeFileSync(join(repo, ".gsd-backups", "migrate-old", "marker.txt"), "backup");
|
|
1631
1634
|
writeFileSync(join(repo, "src.ts"), "code");
|
|
1632
1635
|
runGit(repo, ["add", "-A"]);
|
|
1633
1636
|
runGit(repo, ["commit", "-m", "init"]);
|
|
@@ -1636,6 +1639,8 @@ process.exit(result.status ?? 0);
|
|
|
1636
1639
|
const trackedBefore = run("git ls-files .gsd/", repo);
|
|
1637
1640
|
assert.ok(trackedBefore.includes("completed-units.json"), "untrack: precondition — completed-units tracked");
|
|
1638
1641
|
assert.ok(trackedBefore.includes("metrics.json"), "untrack: precondition — metrics tracked");
|
|
1642
|
+
const backupTrackedBefore = run("git ls-files .gsd-backups/", repo);
|
|
1643
|
+
assert.ok(backupTrackedBefore.includes("marker.txt"), "untrack: precondition — migration backup tracked");
|
|
1639
1644
|
|
|
1640
1645
|
// Run untrackRuntimeFiles
|
|
1641
1646
|
untrackRuntimeFiles(repo);
|
|
@@ -1643,6 +1648,8 @@ process.exit(result.status ?? 0);
|
|
|
1643
1648
|
// Runtime files should be removed from the index
|
|
1644
1649
|
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
1645
1650
|
assert.deepStrictEqual(trackedAfter, "", "untrack: all runtime files removed from index");
|
|
1651
|
+
const backupTrackedAfter = run("git ls-files .gsd-backups/", repo);
|
|
1652
|
+
assert.deepStrictEqual(backupTrackedAfter, "", "untrack: migration backups removed from index");
|
|
1646
1653
|
|
|
1647
1654
|
// Non-runtime files remain tracked
|
|
1648
1655
|
const srcTracked = run("git ls-files src.ts", repo);
|
|
@@ -1653,6 +1660,8 @@ process.exit(result.status ?? 0);
|
|
|
1653
1660
|
"untrack: completed-units.json still on disk");
|
|
1654
1661
|
assert.ok(existsSync(join(repo, ".gsd", "metrics.json")),
|
|
1655
1662
|
"untrack: metrics.json still on disk");
|
|
1663
|
+
assert.ok(existsSync(join(repo, ".gsd-backups", "migrate-old", "marker.txt")),
|
|
1664
|
+
"untrack: migration backup still on disk");
|
|
1656
1665
|
|
|
1657
1666
|
// Idempotent — running again doesn't error
|
|
1658
1667
|
untrackRuntimeFiles(repo);
|