@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.965d1788
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-verification.js +5 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +6 -0
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
- package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
- package/dist/resources/extensions/gsd/git-service.js +1 -0
- package/dist/resources/extensions/gsd/gitignore.js +3 -0
- 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/orphan-stash-audit.js +92 -16
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -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/undo.js +43 -6
- package/dist/resources/extensions/gsd/verification-gate.js +7 -0
- package/dist/resources/extensions/gsd/workflow-manifest.js +4 -1
- 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 +10 -10
- 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 +10 -10
- 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/package.json +7 -7
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +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/gsd/auto/session.ts +1 -0
- package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- package/src/resources/extensions/gsd/auto-verification.ts +5 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -0
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
- package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
- package/src/resources/extensions/gsd/git-service.ts +1 -0
- package/src/resources/extensions/gsd/gitignore.ts +3 -0
- 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/orphan-stash-audit.ts +108 -20
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -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/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/decisions-projection-from-memories.test.ts +51 -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/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/orphan-stash-audit.test.ts +91 -7
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +54 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
- package/src/resources/extensions/gsd/undo.ts +47 -6
- package/src/resources/extensions/gsd/verification-gate.ts +8 -0
- package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → -ji77QJck2IKIwrGza1Bs}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → -ji77QJck2IKIwrGza1Bs}/_ssgManifest.js +0 -0
|
@@ -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);
|
|
@@ -19,6 +19,7 @@ import { tmpdir } from "node:os";
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
saveJsonFile,
|
|
22
|
+
saveJsonFileCompact,
|
|
22
23
|
loadJsonFile,
|
|
23
24
|
writeJsonFileAtomic,
|
|
24
25
|
} from "../json-persistence.ts";
|
|
@@ -56,6 +57,37 @@ test("saveJsonFile creates file with valid JSON content", () => {
|
|
|
56
57
|
}
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
test("saveJsonFile keeps pretty-printed output for human-readable state", () => {
|
|
61
|
+
const dir = makeTempDir();
|
|
62
|
+
const filePath = join(dir, "pretty.json");
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const data = { foo: "bar", nested: { count: 42 } };
|
|
66
|
+
saveJsonFile(filePath, data);
|
|
67
|
+
|
|
68
|
+
const content = readFileSync(filePath, "utf-8");
|
|
69
|
+
assert.equal(content, JSON.stringify(data, null, 2) + "\n");
|
|
70
|
+
} finally {
|
|
71
|
+
cleanup(dir);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("saveJsonFileCompact writes valid compact JSON content", () => {
|
|
76
|
+
const dir = makeTempDir();
|
|
77
|
+
const filePath = join(dir, "compact.json");
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const data = { foo: "bar", nested: { count: 42 } };
|
|
81
|
+
saveJsonFileCompact(filePath, data);
|
|
82
|
+
|
|
83
|
+
const content = readFileSync(filePath, "utf-8");
|
|
84
|
+
assert.equal(content, JSON.stringify(data) + "\n");
|
|
85
|
+
assert.deepEqual(JSON.parse(content), data);
|
|
86
|
+
} finally {
|
|
87
|
+
cleanup(dir);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
59
91
|
test("saveJsonFile does not leave .tmp files on success", () => {
|
|
60
92
|
const dir = makeTempDir();
|
|
61
93
|
const filePath = join(dir, "clean.json");
|
|
@@ -72,6 +104,21 @@ test("saveJsonFile does not leave .tmp files on success", () => {
|
|
|
72
104
|
}
|
|
73
105
|
});
|
|
74
106
|
|
|
107
|
+
test("saveJsonFileCompact does not leave .tmp files on success", () => {
|
|
108
|
+
const dir = makeTempDir();
|
|
109
|
+
const filePath = join(dir, "compact-clean.json");
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
saveJsonFileCompact(filePath, { test: true });
|
|
113
|
+
|
|
114
|
+
const files = readdirSync(dir);
|
|
115
|
+
const tmpFiles = files.filter(f => f.includes(".tmp"));
|
|
116
|
+
assert.equal(tmpFiles.length, 0, `Unexpected .tmp files: ${tmpFiles.join(", ")}`);
|
|
117
|
+
} finally {
|
|
118
|
+
cleanup(dir);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
75
122
|
test("saveJsonFile creates parent directories", () => {
|
|
76
123
|
const dir = makeTempDir();
|
|
77
124
|
const filePath = join(dir, "deep", "nested", "data.json");
|
|
@@ -161,6 +161,41 @@ test("backfill is idempotent — second run on the same file is a no-op", () =>
|
|
|
161
161
|
}
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
test("backfill skips a knowledge row whose memory insert throws and continues later rows", () => {
|
|
165
|
+
const base = makeTmpBase();
|
|
166
|
+
try {
|
|
167
|
+
writeKnowledgeMd(base, FIXTURE);
|
|
168
|
+
|
|
169
|
+
const adapter = _getAdapter();
|
|
170
|
+
assert.ok(adapter);
|
|
171
|
+
adapter.exec(`
|
|
172
|
+
CREATE TRIGGER fail_p002_memory_insert
|
|
173
|
+
BEFORE INSERT ON memories
|
|
174
|
+
WHEN json_extract(NEW.structured_fields, '$.sourceKnowledgeId') = 'P002'
|
|
175
|
+
BEGIN
|
|
176
|
+
SELECT RAISE(ABORT, 'poison knowledge P002');
|
|
177
|
+
END
|
|
178
|
+
`);
|
|
179
|
+
|
|
180
|
+
const written = backfillKnowledgeToMemories(base);
|
|
181
|
+
assert.equal(written, 2, "one poisoned row should not abort the batch");
|
|
182
|
+
|
|
183
|
+
const rows = adapter
|
|
184
|
+
.prepare(
|
|
185
|
+
"SELECT structured_fields FROM memories WHERE structured_fields LIKE '%\"sourceKnowledgeId\":\"%' ORDER BY seq",
|
|
186
|
+
)
|
|
187
|
+
.all() as Array<{ structured_fields: string }>;
|
|
188
|
+
|
|
189
|
+
assert.deepEqual(
|
|
190
|
+
rows.map((row) => JSON.parse(row.structured_fields).sourceKnowledgeId),
|
|
191
|
+
["P001", "L001"],
|
|
192
|
+
"rows after the poisoned knowledge entry should still be migrated",
|
|
193
|
+
);
|
|
194
|
+
} finally {
|
|
195
|
+
cleanup(base);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
164
199
|
test("backfill returns 0 when KNOWLEDGE.md is absent", () => {
|
|
165
200
|
const base = makeTmpBase();
|
|
166
201
|
try {
|
|
@@ -9,7 +9,15 @@ import { tmpdir } from "node:os";
|
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
10
|
|
|
11
11
|
import { mergeMilestoneToMain } from "../auto-worktree.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { checkCloseoutConsistencyGate } from "../closeout-consistency-gate.ts";
|
|
13
|
+
import {
|
|
14
|
+
closeDatabase,
|
|
15
|
+
insertAssessment,
|
|
16
|
+
insertMilestone,
|
|
17
|
+
insertSlice,
|
|
18
|
+
insertTask,
|
|
19
|
+
openDatabase,
|
|
20
|
+
} from "../gsd-db.ts";
|
|
13
21
|
|
|
14
22
|
function git(args: string[], cwd: string): string {
|
|
15
23
|
return execFileSync("git", args, {
|
|
@@ -61,3 +69,25 @@ test("mergeMilestoneToMain blocks when project DB closeout is still open", () =>
|
|
|
61
69
|
if (existsSync(repo)) rmSync(repo, { recursive: true, force: true });
|
|
62
70
|
}
|
|
63
71
|
});
|
|
72
|
+
|
|
73
|
+
test("closeout consistency treats deferred slices as inactive", () => {
|
|
74
|
+
try {
|
|
75
|
+
assert.equal(openDatabase(":memory:"), true);
|
|
76
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "complete" });
|
|
77
|
+
insertSlice({ milestoneId: "M001", id: "S01", title: "Done", status: "complete" });
|
|
78
|
+
insertTask({ milestoneId: "M001", sliceId: "S01", id: "T01", title: "Done", status: "complete" });
|
|
79
|
+
insertSlice({ milestoneId: "M001", id: "S02", title: "Deferred", status: "deferred" });
|
|
80
|
+
insertTask({ milestoneId: "M001", sliceId: "S02", id: "T02", title: "Deferred", status: "pending" });
|
|
81
|
+
insertAssessment({
|
|
82
|
+
path: "milestones/M001/M001-VALIDATION.md",
|
|
83
|
+
milestoneId: "M001",
|
|
84
|
+
status: "pass",
|
|
85
|
+
scope: "milestone-validation",
|
|
86
|
+
fullContent: "verdict: pass",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.deepEqual(checkCloseoutConsistencyGate("M001"), { ok: true });
|
|
90
|
+
} finally {
|
|
91
|
+
closeDatabase();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
@@ -112,6 +112,12 @@ describe("pruneMetricsLedger: invalidates scoped ledger cache", () => {
|
|
|
112
112
|
// 3. Prune to keepCount=2 — should evict 3 old units from disk AND clear scopedLedgers.
|
|
113
113
|
const removed = pruneMetricsLedger(tmpDir, 2);
|
|
114
114
|
assert.equal(removed, 3, "pruneMetricsLedger should report 3 removed units");
|
|
115
|
+
const prunedRaw = readFileSync(metricsPath(tmpDir), "utf-8");
|
|
116
|
+
assert.equal(
|
|
117
|
+
prunedRaw,
|
|
118
|
+
JSON.stringify(JSON.parse(prunedRaw)) + "\n",
|
|
119
|
+
"pruneMetricsLedger should persist compact metrics.json",
|
|
120
|
+
);
|
|
115
121
|
|
|
116
122
|
// 4. Snapshot a new unit via scope. This exercises the lazy-reload path
|
|
117
123
|
// (scopedLedgers was cleared by prune) and writes the result to disk.
|
|
@@ -259,6 +259,7 @@ test("initMetrics creates ledger, snapshotUnitMetrics persists across resets", (
|
|
|
259
259
|
// Verify file content
|
|
260
260
|
const raw = readFileSync(join(tmpBase, ".gsd", "metrics.json"), "utf-8");
|
|
261
261
|
const parsed: MetricsLedger = JSON.parse(raw);
|
|
262
|
+
assert.equal(raw, JSON.stringify(parsed) + "\n", "metrics.json should be serialized compactly");
|
|
262
263
|
assert.equal(parsed.version, 1);
|
|
263
264
|
assert.equal(parsed.units.length, 1);
|
|
264
265
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// gsd-pi + src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts
|
|
2
|
-
// Regression
|
|
3
|
-
//
|
|
2
|
+
// Regression coverage for orphaned gsd-preflight-stash entries from completed
|
|
3
|
+
// milestones: safe startup recovery for untracked work, manual warnings for
|
|
4
|
+
// tracked or dirty-tree cases that would silently mutate user state.
|
|
4
5
|
|
|
5
6
|
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
6
7
|
import assert from "node:assert/strict";
|
|
@@ -37,6 +38,13 @@ function pushPreflightStash(repo: string, milestoneId: string, fileName: string,
|
|
|
37
38
|
return marker;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function pushTrackedPreflightStash(repo: string, milestoneId: string, fileName: string, content: string): string {
|
|
42
|
+
writeFileSync(join(repo, fileName), content);
|
|
43
|
+
const marker = `gsd-preflight-stash:${milestoneId}:42:1700000000000:tracked`;
|
|
44
|
+
git(repo, "stash", "push", "-m", `gsd-preflight-stash [${marker}]`);
|
|
45
|
+
return marker;
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
describe("auditOrphanedPreflightStashes", () => {
|
|
41
49
|
let repo: string;
|
|
42
50
|
|
|
@@ -83,6 +91,23 @@ describe("auditOrphanedPreflightStashes", () => {
|
|
|
83
91
|
assert.match(list, /gsd-preflight-stash:M002:/);
|
|
84
92
|
});
|
|
85
93
|
|
|
94
|
+
test("does not auto-apply a tracked orphan preflight stash onto a clean working tree", () => {
|
|
95
|
+
pushTrackedPreflightStash(repo, "M006", "seed.txt", "seed\ntracked pre-merge work\n");
|
|
96
|
+
assert.equal(readFileSync(join(repo, "seed.txt"), "utf-8"), "seed\n");
|
|
97
|
+
|
|
98
|
+
const result = auditOrphanedPreflightStashes(repo, () => true);
|
|
99
|
+
|
|
100
|
+
assert.equal(result.applied.length, 0);
|
|
101
|
+
assert.equal(result.warnings.length, 1);
|
|
102
|
+
assert.match(result.warnings[0], /not auto-applied/);
|
|
103
|
+
assert.match(result.warnings[0], /stash modifies tracked files/);
|
|
104
|
+
assert.match(result.warnings[0], /git stash apply stash@\{\d+\}/);
|
|
105
|
+
assert.equal(readFileSync(join(repo, "seed.txt"), "utf-8"), "seed\n");
|
|
106
|
+
|
|
107
|
+
const list = git(repo, "stash", "list");
|
|
108
|
+
assert.match(list, /gsd-preflight-stash:M006:/);
|
|
109
|
+
});
|
|
110
|
+
|
|
86
111
|
test("ignores stashes whose milestone is not complete", () => {
|
|
87
112
|
pushPreflightStash(repo, "M003", "wip.txt", "still-working\n");
|
|
88
113
|
|
|
@@ -115,23 +140,25 @@ describe("auditOrphanedPreflightStashes", () => {
|
|
|
115
140
|
assert.match(result.warnings[0], /db unavailable/);
|
|
116
141
|
});
|
|
117
142
|
|
|
118
|
-
test("collects a warning
|
|
119
|
-
// Push a stash containing a change to seed.txt; then dirty seed.txt
|
|
120
|
-
//
|
|
143
|
+
test("collects a warning instead of applying when the working tree is not clean", () => {
|
|
144
|
+
// Push a stash containing a change to seed.txt; then dirty seed.txt before
|
|
145
|
+
// the audit runs. Startup recovery must leave the tree untouched and ask
|
|
146
|
+
// the user to apply the retained backup stash manually.
|
|
121
147
|
writeFileSync(join(repo, "seed.txt"), "stashed\n");
|
|
122
148
|
const marker = `gsd-preflight-stash:M005:42:1700:zz`;
|
|
123
149
|
git(repo, "stash", "push", "-m", `gsd-preflight-stash [${marker}]`);
|
|
124
150
|
|
|
125
|
-
// Dirty the working tree so apply will conflict.
|
|
126
151
|
writeFileSync(join(repo, "seed.txt"), "conflicting modification\n");
|
|
127
152
|
|
|
128
153
|
const result = auditOrphanedPreflightStashes(repo, () => true);
|
|
129
154
|
|
|
130
155
|
assert.equal(result.applied.length, 0);
|
|
131
156
|
assert.equal(result.warnings.length, 1);
|
|
132
|
-
assert.match(result.warnings[0], /
|
|
157
|
+
assert.match(result.warnings[0], /not auto-applied/);
|
|
158
|
+
assert.match(result.warnings[0], /working tree not clean/);
|
|
133
159
|
assert.match(result.warnings[0], /M005/);
|
|
134
160
|
assert.match(result.warnings[0], /git stash apply/);
|
|
161
|
+
assert.equal(readFileSync(join(repo, "seed.txt"), "utf-8"), "conflicting modification\n");
|
|
135
162
|
|
|
136
163
|
const list = git(repo, "stash", "list");
|
|
137
164
|
assert.match(list, /gsd-preflight-stash:M005:/);
|
|
@@ -169,6 +196,63 @@ describe("auditOrphanedPreflightStashes", () => {
|
|
|
169
196
|
"second run must NOT warn — files are already restored from first run",
|
|
170
197
|
);
|
|
171
198
|
});
|
|
199
|
+
|
|
200
|
+
test("warns instead of silently skipping when only some stash untracked files were restored", () => {
|
|
201
|
+
// Cursor Bugbot caught: isAlreadyRestoredUntrackedStatus treated a dirty
|
|
202
|
+
// tree as fully restored when every *current* untracked path appeared in
|
|
203
|
+
// the stash, without requiring every *stash* untracked path to be present.
|
|
204
|
+
// A partial restore (only some files recovered) was misread as the
|
|
205
|
+
// idempotent steady state, so the audit no-oped and the remaining pre-merge
|
|
206
|
+
// files stayed missing with no warning.
|
|
207
|
+
writeFileSync(join(repo, "first.txt"), "first\n");
|
|
208
|
+
writeFileSync(join(repo, "second.txt"), "second\n");
|
|
209
|
+
const marker = `gsd-preflight-stash:M007:42:1700000000000:partial`;
|
|
210
|
+
git(repo, "stash", "push", "--include-untracked", "-m", `gsd-preflight-stash [${marker}]`);
|
|
211
|
+
|
|
212
|
+
// Both files are stashed away.
|
|
213
|
+
assert.equal(existsSync(join(repo, "first.txt")), false);
|
|
214
|
+
assert.equal(existsSync(join(repo, "second.txt")), false);
|
|
215
|
+
|
|
216
|
+
// Simulate a partial restore: only first.txt came back; second.txt is still
|
|
217
|
+
// missing from the working tree.
|
|
218
|
+
writeFileSync(join(repo, "first.txt"), "first\n");
|
|
219
|
+
|
|
220
|
+
const result = auditOrphanedPreflightStashes(repo, () => true);
|
|
221
|
+
|
|
222
|
+
// Must NOT be treated as already-restored: second.txt is still missing, so
|
|
223
|
+
// the audit must surface a manual-recovery warning rather than no-op.
|
|
224
|
+
assert.equal(result.applied.length, 0);
|
|
225
|
+
assert.equal(result.warnings.length, 1, "partial restore must warn, not silently no-op");
|
|
226
|
+
assert.match(result.warnings[0], /not auto-applied/);
|
|
227
|
+
assert.match(result.warnings[0], /working tree not clean/);
|
|
228
|
+
assert.match(result.warnings[0], /M007/);
|
|
229
|
+
assert.match(result.warnings[0], /git stash apply/);
|
|
230
|
+
|
|
231
|
+
// The stash entry must remain so the user can recover second.txt manually.
|
|
232
|
+
const list = git(repo, "stash", "list");
|
|
233
|
+
assert.match(list, /gsd-preflight-stash:M007:/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("fully restored multi-file untracked stash stays a silent no-op", () => {
|
|
237
|
+
// Guards the other direction of the set-equality fix: when EVERY stash
|
|
238
|
+
// untracked file is present in the working tree, the genuine idempotent
|
|
239
|
+
// steady state must still be recognized and skipped without warning, even
|
|
240
|
+
// when the stash captured more than one file.
|
|
241
|
+
writeFileSync(join(repo, "alpha.txt"), "alpha\n");
|
|
242
|
+
writeFileSync(join(repo, "beta.txt"), "beta\n");
|
|
243
|
+
const marker = `gsd-preflight-stash:M008:42:1700000000000:multi`;
|
|
244
|
+
git(repo, "stash", "push", "--include-untracked", "-m", `gsd-preflight-stash [${marker}]`);
|
|
245
|
+
|
|
246
|
+
const first = auditOrphanedPreflightStashes(repo, () => true);
|
|
247
|
+
assert.equal(first.applied.length, 1, "first run applies both files");
|
|
248
|
+
assert.equal(first.warnings.length, 0);
|
|
249
|
+
assert.equal(existsSync(join(repo, "alpha.txt")), true);
|
|
250
|
+
assert.equal(existsSync(join(repo, "beta.txt")), true);
|
|
251
|
+
|
|
252
|
+
const second = auditOrphanedPreflightStashes(repo, () => true);
|
|
253
|
+
assert.equal(second.applied.length, 0, "second run skips");
|
|
254
|
+
assert.equal(second.warnings.length, 0, "fully restored multi-file stash must not warn");
|
|
255
|
+
});
|
|
172
256
|
});
|
|
173
257
|
|
|
174
258
|
test("_isAlreadyRestoredApplyError detects the git stash apply already-exists error", () => {
|
|
@@ -94,6 +94,16 @@ test("plan-slice prompt: absence checks use negated quiet searches", () => {
|
|
|
94
94
|
assert.ok(result.includes("count commands exit 1 when they find zero matches"));
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
test("plan-slice prompt: npm package existence rule is stated during generation", () => {
|
|
98
|
+
const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
|
|
99
|
+
assert.ok(result.includes("reference only packages that exist on the public npm registry"));
|
|
100
|
+
assert.ok(result.includes("remove or correct any package name that is not real"));
|
|
101
|
+
// The post-unit pre-execution gate parses package names from require/import
|
|
102
|
+
// patterns too, not just install commands, so the rule must cover that scope.
|
|
103
|
+
assert.ok(result.includes("require('...')"));
|
|
104
|
+
assert.ok(result.includes("import ... from '...'"));
|
|
105
|
+
});
|
|
106
|
+
|
|
97
107
|
test("plan-slice prompt: footer references gsd_plan_slice tool, not direct write", () => {
|
|
98
108
|
const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
|
|
99
109
|
assert.ok(
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
handleUndo,
|
|
11
11
|
handleUndoTask,
|
|
12
12
|
handleResetSlice,
|
|
13
|
+
parseActivityLogFilename,
|
|
13
14
|
uncheckTaskInPlan,
|
|
14
15
|
} from "../undo.ts";
|
|
15
16
|
import {
|
|
@@ -67,6 +68,37 @@ test("handleUndo without --force only warns and leaves completed units intact",
|
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
test("handleUndo execute-task with --force reopens the task row and re-renders plan", async () => {
|
|
72
|
+
const base = makeTempDir("gsd-undo-execute-task");
|
|
73
|
+
try {
|
|
74
|
+
setupTaskFixture(base);
|
|
75
|
+
mkdirSync(join(base, ".gsd", "activity"), { recursive: true });
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(base, ".gsd", "activity", "001-execute-task-M001-S01-T01.jsonl"),
|
|
78
|
+
"",
|
|
79
|
+
"utf-8",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { notifications, ctx } = makeCtx();
|
|
83
|
+
await handleUndo("--force", ctx, {} as any, base);
|
|
84
|
+
|
|
85
|
+
const task = getTask("M001", "S01", "T01");
|
|
86
|
+
assert.equal(task?.status, "pending");
|
|
87
|
+
|
|
88
|
+
const planContent = readFileSync(
|
|
89
|
+
join(base, ".gsd", "phases", "01-test", "01-01-PLAN.md"),
|
|
90
|
+
"utf-8",
|
|
91
|
+
);
|
|
92
|
+
assert.match(planContent, /\[ \] \*\*T01\*\*:/);
|
|
93
|
+
|
|
94
|
+
assert.equal(notifications[0]?.level, "success");
|
|
95
|
+
assert.match(notifications[0]?.message ?? "", /Unchecked task in PLAN/);
|
|
96
|
+
} finally {
|
|
97
|
+
closeDatabase();
|
|
98
|
+
rmSync(base, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
70
102
|
test("uncheckTaskInPlan flips a checked task back to unchecked", () => {
|
|
71
103
|
const base = makeTempDir("gsd-undo-plan");
|
|
72
104
|
try {
|
|
@@ -154,6 +186,48 @@ test("extractCommitShas ignores malformed commit tokens", () => {
|
|
|
154
186
|
assert.deepEqual(extractCommitShas(content), ["1234567"]);
|
|
155
187
|
});
|
|
156
188
|
|
|
189
|
+
test("parseActivityLogFilename splits hyphenated unit types from their IDs", () => {
|
|
190
|
+
// Task-level: unit type and ID both contain hyphens.
|
|
191
|
+
assert.deepEqual(parseActivityLogFilename("001-execute-task-M001-S01-T01.jsonl"), {
|
|
192
|
+
unitType: "execute-task",
|
|
193
|
+
unitId: "M001-S01-T01",
|
|
194
|
+
});
|
|
195
|
+
// Variant must win over its shorter prefix ("execute-task").
|
|
196
|
+
assert.deepEqual(parseActivityLogFilename("002-execute-task-simple-M001-S02-T03.jsonl"), {
|
|
197
|
+
unitType: "execute-task-simple",
|
|
198
|
+
unitId: "M001-S02-T03",
|
|
199
|
+
});
|
|
200
|
+
// Milestone- and slice-shaped IDs.
|
|
201
|
+
assert.deepEqual(parseActivityLogFilename("003-research-milestone-M001.jsonl"), {
|
|
202
|
+
unitType: "research-milestone",
|
|
203
|
+
unitId: "M001",
|
|
204
|
+
});
|
|
205
|
+
assert.deepEqual(parseActivityLogFilename("004-plan-slice-M001-S01.jsonl"), {
|
|
206
|
+
unitType: "plan-slice",
|
|
207
|
+
unitId: "M001-S01",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("parseActivityLogFilename accepts non-milestone-shaped unit IDs", () => {
|
|
212
|
+
// Regression (#1057): project-level units use IDs that do not start with
|
|
213
|
+
// "M<digit>". A milestone-shaped regex made /gsd undo bail out with
|
|
214
|
+
// "could not parse latest activity log" when one of these was most recent.
|
|
215
|
+
assert.deepEqual(parseActivityLogFilename("005-discuss-project-PROJECT.jsonl"), {
|
|
216
|
+
unitType: "discuss-project",
|
|
217
|
+
unitId: "PROJECT",
|
|
218
|
+
});
|
|
219
|
+
assert.deepEqual(parseActivityLogFilename("006-workflow-preferences-WORKFLOW-PREFS.jsonl"), {
|
|
220
|
+
unitType: "workflow-preferences",
|
|
221
|
+
unitId: "WORKFLOW-PREFS",
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("parseActivityLogFilename returns null for unrecognized names", () => {
|
|
226
|
+
assert.equal(parseActivityLogFilename("007-not-a-real-unit-M001.jsonl"), null);
|
|
227
|
+
assert.equal(parseActivityLogFilename("no-sequence-prefix.jsonl"), null);
|
|
228
|
+
assert.equal(parseActivityLogFilename("001-execute-task-M001-S01-T01.txt"), null);
|
|
229
|
+
});
|
|
230
|
+
|
|
157
231
|
// ─── handleUndoTask tests ────────────────────────────────────────────────────
|
|
158
232
|
|
|
159
233
|
function makeCtx(): { notifications: Array<{ message: string; level: string }>; ctx: any } {
|