gsd-pi 2.47.0-dev.04be8c9 → 2.47.0-dev.f2e721d

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.
Files changed (65) hide show
  1. package/dist/resources/extensions/gsd/auto-start.js +8 -1
  2. package/dist/resources/extensions/gsd/forensics.js +292 -1
  3. package/dist/resources/extensions/gsd/guided-flow.js +85 -3
  4. package/dist/resources/extensions/gsd/prompts/forensics.md +37 -5
  5. package/dist/resources/extensions/gsd/session-forensics.js +10 -1
  6. package/dist/web/standalone/.next/BUILD_ID +1 -1
  7. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  8. package/dist/web/standalone/.next/build-manifest.json +2 -2
  9. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  10. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  11. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  27. package/dist/web/standalone/.next/server/app/index.html +1 -1
  28. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  35. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  36. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  37. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  38. package/package.json +1 -1
  39. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  40. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  41. package/packages/pi-agent-core/src/agent-loop.ts +3 -2
  42. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +43 -0
  43. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/model-registry.js +26 -3
  46. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  47. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +70 -0
  48. package/packages/pi-coding-agent/src/core/model-registry.ts +29 -2
  49. package/packages/pi-tui/dist/components/box.d.ts +1 -0
  50. package/packages/pi-tui/dist/components/box.d.ts.map +1 -1
  51. package/packages/pi-tui/dist/components/box.js +10 -0
  52. package/packages/pi-tui/dist/components/box.js.map +1 -1
  53. package/packages/pi-tui/src/components/box.ts +10 -0
  54. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  55. package/src/resources/extensions/gsd/forensics.ts +329 -2
  56. package/src/resources/extensions/gsd/guided-flow.ts +105 -3
  57. package/src/resources/extensions/gsd/prompts/forensics.md +37 -5
  58. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  59. package/src/resources/extensions/gsd/tests/discuss-queued-milestones.test.ts +241 -0
  60. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +121 -0
  61. package/src/resources/extensions/gsd/tests/forensics-journal.test.ts +162 -0
  62. package/src/resources/extensions/gsd/tests/preflight-context-draft-filter.test.ts +115 -0
  63. package/src/resources/extensions/gsd/tests/stale-milestone-id-reservation.test.ts +79 -0
  64. /package/dist/web/standalone/.next/static/{GR9tXQAPXXBL4AUugDPlJ → O3E7X3EJ2lEKs_0hIUzGd}/_buildManifest.js +0 -0
  65. /package/dist/web/standalone/.next/static/{GR9tXQAPXXBL4AUugDPlJ → O3E7X3EJ2lEKs_0hIUzGd}/_ssgManifest.js +0 -0
@@ -0,0 +1,162 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const gsdDir = join(__dirname, "..");
9
+
10
+ describe("forensics journal & activity log awareness", () => {
11
+ const forensicsSrc = readFileSync(join(gsdDir, "forensics.ts"), "utf-8");
12
+ const promptSrc = readFileSync(join(gsdDir, "prompts", "forensics.md"), "utf-8");
13
+
14
+ it("scanJournalForForensics reads journal files directly (no full queryJournal load)", () => {
15
+ // Must NOT use queryJournal which loads ALL entries into memory
16
+ assert.ok(
17
+ !forensicsSrc.includes('queryJournal('),
18
+ "forensics.ts must NOT call queryJournal() which loads all entries at once",
19
+ );
20
+ // Must have its own journal scanning with file-level limits
21
+ assert.ok(
22
+ forensicsSrc.includes("scanJournalForForensics"),
23
+ "forensics.ts must have scanJournalForForensics function",
24
+ );
25
+ });
26
+
27
+ it("journal scanning limits files parsed to avoid memory bloat", () => {
28
+ assert.ok(
29
+ forensicsSrc.includes("MAX_JOURNAL_RECENT_FILES"),
30
+ "must have MAX_JOURNAL_RECENT_FILES constant to limit parsed files",
31
+ );
32
+ assert.ok(
33
+ forensicsSrc.includes("MAX_JOURNAL_RECENT_EVENTS"),
34
+ "must have MAX_JOURNAL_RECENT_EVENTS constant to limit events extracted",
35
+ );
36
+ });
37
+
38
+ it("older journal files are line-counted without full JSON parse", () => {
39
+ assert.ok(
40
+ forensicsSrc.includes("olderEntryCount") || forensicsSrc.includes("olderFiles"),
41
+ "must handle older files separately from recent files",
42
+ );
43
+ });
44
+
45
+ it("ForensicReport includes journalSummary field", () => {
46
+ assert.ok(
47
+ forensicsSrc.includes("journalSummary"),
48
+ "ForensicReport must include journalSummary field",
49
+ );
50
+ });
51
+
52
+ it("ForensicReport includes activityLogMeta field", () => {
53
+ assert.ok(
54
+ forensicsSrc.includes("activityLogMeta"),
55
+ "ForensicReport must include activityLogMeta field",
56
+ );
57
+ });
58
+
59
+ it("buildForensicReport calls scanJournalForForensics", () => {
60
+ assert.ok(
61
+ forensicsSrc.includes("scanJournalForForensics"),
62
+ "buildForensicReport must call scanJournalForForensics",
63
+ );
64
+ });
65
+
66
+ it("buildForensicReport calls gatherActivityLogMeta", () => {
67
+ assert.ok(
68
+ forensicsSrc.includes("gatherActivityLogMeta"),
69
+ "buildForensicReport must call gatherActivityLogMeta",
70
+ );
71
+ });
72
+
73
+ it("forensics detects journal-based anomalies", () => {
74
+ assert.ok(
75
+ forensicsSrc.includes("detectJournalAnomalies"),
76
+ "forensics.ts must have detectJournalAnomalies function",
77
+ );
78
+ // Check for specific journal anomaly types
79
+ assert.ok(forensicsSrc.includes('"journal-stuck"'), "must detect journal-stuck anomalies");
80
+ assert.ok(forensicsSrc.includes('"journal-guard-block"'), "must detect journal-guard-block anomalies");
81
+ assert.ok(forensicsSrc.includes('"journal-rapid-iterations"'), "must detect journal-rapid-iterations anomalies");
82
+ assert.ok(forensicsSrc.includes('"journal-worktree-failure"'), "must detect journal-worktree-failure anomalies");
83
+ });
84
+
85
+ it("formatReportForPrompt includes journal summary section", () => {
86
+ assert.ok(
87
+ forensicsSrc.includes("Journal Summary"),
88
+ "prompt formatter must include a Journal Summary section",
89
+ );
90
+ });
91
+
92
+ it("formatReportForPrompt includes activity log overview section", () => {
93
+ assert.ok(
94
+ forensicsSrc.includes("Activity Log Overview"),
95
+ "prompt formatter must include an Activity Log Overview section",
96
+ );
97
+ });
98
+
99
+ it("activity log scanning uses tail-read with byte cap (not full file load)", () => {
100
+ // scanActivityLogs uses nativeParseJsonlTail + MAX_JSONL_BYTES for efficient reading
101
+ assert.ok(
102
+ forensicsSrc.includes("nativeParseJsonlTail"),
103
+ "activity log scanning must use nativeParseJsonlTail for tail-reading",
104
+ );
105
+ assert.ok(
106
+ forensicsSrc.includes("MAX_JSONL_BYTES"),
107
+ "activity log scanning must respect MAX_JSONL_BYTES cap",
108
+ );
109
+ // Only reads last 5 files
110
+ assert.ok(
111
+ forensicsSrc.includes("slice(-5)"),
112
+ "activity log scanning must limit to last 5 files",
113
+ );
114
+ });
115
+
116
+ it("activity log entries are distilled through extractTrace, not sent raw", () => {
117
+ assert.ok(
118
+ forensicsSrc.includes("extractTrace("),
119
+ "activity log entries must be distilled through extractTrace before reporting",
120
+ );
121
+ });
122
+
123
+ it("prompt output is hard-capped at 30KB", () => {
124
+ assert.ok(
125
+ forensicsSrc.includes("MAX_BYTES") && forensicsSrc.includes("30 * 1024"),
126
+ "formatReportForPrompt must have a 30KB hard cap",
127
+ );
128
+ assert.ok(
129
+ forensicsSrc.includes("truncated at 30KB"),
130
+ "prompt must show truncation message when capped",
131
+ );
132
+ });
133
+
134
+ it("forensics prompt documents journal format", () => {
135
+ assert.ok(
136
+ promptSrc.includes("### Journal Format"),
137
+ "forensics.md must document the journal format",
138
+ );
139
+ assert.ok(
140
+ promptSrc.includes("flowId"),
141
+ "forensics.md must reference flowId concept",
142
+ );
143
+ assert.ok(
144
+ promptSrc.includes("causedBy"),
145
+ "forensics.md must reference causedBy for causal chains",
146
+ );
147
+ });
148
+
149
+ it("forensics prompt includes journal directory in runtime path reference", () => {
150
+ assert.ok(
151
+ promptSrc.includes("journal/"),
152
+ "forensics.md runtime path reference must include journal/",
153
+ );
154
+ });
155
+
156
+ it("investigation protocol references journal data", () => {
157
+ assert.ok(
158
+ promptSrc.includes("journal timeline") || promptSrc.includes("journal events"),
159
+ "investigation protocol must reference journal data for tracing",
160
+ );
161
+ });
162
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Regression test for #2473: Pre-flight CONTEXT-DRAFT warning should skip
3
+ * completed and parked milestones.
4
+ *
5
+ * The pre-flight loop in auto-start.ts warns about CONTEXT-DRAFT.md files
6
+ * so the user knows which milestones will pause for discussion. But completed
7
+ * milestones with leftover CONTEXT-DRAFT.md files are not actionable — the
8
+ * warning is noise.
9
+ *
10
+ * This test exercises the filtering logic directly: given a set of milestones
11
+ * with CONTEXT-DRAFT files, only active/pending ones should produce warnings.
12
+ */
13
+ import { describe, test, beforeEach, afterEach } from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { tmpdir } from "node:os";
18
+
19
+ import {
20
+ openDatabase,
21
+ closeDatabase,
22
+ isDbAvailable,
23
+ insertMilestone,
24
+ getMilestone,
25
+ } from "../gsd-db.ts";
26
+ import { resolveMilestoneFile } from "../paths.ts";
27
+
28
+ describe("pre-flight CONTEXT-DRAFT filter (#2473)", () => {
29
+ let tmpBase: string;
30
+ let gsd: string;
31
+
32
+ beforeEach(() => {
33
+ tmpBase = mkdtempSync(join(tmpdir(), "gsd-preflight-draft-"));
34
+ gsd = join(tmpBase, ".gsd");
35
+
36
+ // Create milestone directories with CONTEXT-DRAFT files
37
+ for (const id of ["M001", "M002", "M003"]) {
38
+ const msDir = join(gsd, "milestones", id);
39
+ mkdirSync(msDir, { recursive: true });
40
+ writeFileSync(join(msDir, `${id}-CONTEXT-DRAFT.md`), `# ${id}: Draft\n`);
41
+ }
42
+
43
+ // Open DB and insert milestones with different statuses
44
+ const dbPath = join(gsd, "gsd.db");
45
+ openDatabase(dbPath);
46
+ insertMilestone({ id: "M001", title: "Complete milestone", status: "complete" });
47
+ insertMilestone({ id: "M002", title: "Active milestone", status: "active" });
48
+ insertMilestone({ id: "M003", title: "Parked milestone", status: "parked" });
49
+ });
50
+
51
+ afterEach(() => {
52
+ closeDatabase();
53
+ rmSync(tmpBase, { recursive: true, force: true });
54
+ });
55
+
56
+ test("completed milestone is skipped — no warning emitted", () => {
57
+ assert.ok(isDbAvailable(), "DB should be available");
58
+ const ms = getMilestone("M001");
59
+ assert.equal(ms?.status, "complete");
60
+ });
61
+
62
+ test("parked milestone is skipped — no warning emitted", () => {
63
+ const ms = getMilestone("M003");
64
+ assert.equal(ms?.status, "parked");
65
+ });
66
+
67
+ test("active milestone with CONTEXT-DRAFT produces warning", () => {
68
+ const ms = getMilestone("M002");
69
+ assert.equal(ms?.status, "active");
70
+
71
+ const draft = resolveMilestoneFile(tmpBase, "M002", "CONTEXT-DRAFT");
72
+ assert.ok(draft, "CONTEXT-DRAFT file should be found for active milestone");
73
+ });
74
+
75
+ test("full pre-flight filter produces warnings only for active milestones", () => {
76
+ const milestoneIds = ["M001", "M002", "M003"];
77
+ const issues: string[] = [];
78
+
79
+ for (const id of milestoneIds) {
80
+ // Replicate the fixed pre-flight logic from auto-start.ts
81
+ if (isDbAvailable()) {
82
+ const ms = getMilestone(id);
83
+ if (ms?.status === "complete" || ms?.status === "parked") continue;
84
+ }
85
+ const draft = resolveMilestoneFile(tmpBase, id, "CONTEXT-DRAFT");
86
+ if (draft) {
87
+ issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
88
+ }
89
+ }
90
+
91
+ assert.equal(issues.length, 1, "only one warning should be emitted");
92
+ assert.match(issues[0], /M002/, "warning should be for the active milestone only");
93
+ });
94
+
95
+ test("when DB is unavailable, all milestones with CONTEXT-DRAFT produce warnings (safe fallback)", () => {
96
+ closeDatabase();
97
+ assert.ok(!isDbAvailable(), "DB should be unavailable after close");
98
+
99
+ const milestoneIds = ["M001", "M002", "M003"];
100
+ const issues: string[] = [];
101
+
102
+ for (const id of milestoneIds) {
103
+ if (isDbAvailable()) {
104
+ const ms = getMilestone(id);
105
+ if (ms?.status === "complete" || ms?.status === "parked") continue;
106
+ }
107
+ const draft = resolveMilestoneFile(tmpBase, id, "CONTEXT-DRAFT");
108
+ if (draft) {
109
+ issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
110
+ }
111
+ }
112
+
113
+ assert.equal(issues.length, 3, "all milestones should warn when DB is unavailable");
114
+ });
115
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Regression test for #2488: Stale milestone ID reservations inflate next ID
3
+ * after cancelled /gsd sessions.
4
+ *
5
+ * The module-level `reservedMilestoneIds` Set persists across /gsd invocations
6
+ * within the same Node process. Without clearReservedMilestoneIds() at session
7
+ * start, each cancelled session permanently bumps the counter by 1.
8
+ */
9
+ import { describe, test, beforeEach } from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import {
13
+ nextMilestoneId,
14
+ reserveMilestoneId,
15
+ getReservedMilestoneIds,
16
+ clearReservedMilestoneIds,
17
+ } from "../milestone-ids.ts";
18
+
19
+ describe("stale milestone ID reservation cleanup (#2488)", () => {
20
+ beforeEach(() => {
21
+ clearReservedMilestoneIds();
22
+ });
23
+
24
+ test("without cleanup, cancelled sessions inflate the next ID", () => {
25
+ const diskIds = ["M001", "M002", "M003"];
26
+
27
+ // Session 1: user starts /gsd, ID is previewed and reserved, then cancelled
28
+ const allIds1 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
29
+ const preview1 = nextMilestoneId(allIds1);
30
+ reserveMilestoneId(preview1);
31
+ assert.equal(preview1, "M004");
32
+
33
+ // Session 2: user starts /gsd again — stale reservation still in Set
34
+ // WITHOUT clearing, the next ID skips M004 (reserved) and goes to M005
35
+ const allIds2 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
36
+ const preview2 = nextMilestoneId(allIds2);
37
+ assert.equal(preview2, "M005", "without cleanup, ID inflates to M005");
38
+ });
39
+
40
+ test("with cleanup at session start, next ID is correct", () => {
41
+ const diskIds = ["M001", "M002", "M003"];
42
+
43
+ // Session 1: user starts /gsd, ID is previewed and reserved, then cancelled
44
+ const allIds1 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
45
+ const preview1 = nextMilestoneId(allIds1);
46
+ reserveMilestoneId(preview1);
47
+ assert.equal(preview1, "M004");
48
+
49
+ // Session 2: clear stale reservations first (the fix)
50
+ clearReservedMilestoneIds();
51
+
52
+ // Now the next ID correctly returns M004 again
53
+ const allIds2 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
54
+ const preview2 = nextMilestoneId(allIds2);
55
+ assert.equal(preview2, "M004", "after cleanup, ID is correctly M004");
56
+ });
57
+
58
+ test("multiple cancelled sessions compound the inflation without cleanup", () => {
59
+ const diskIds = ["M001", "M002", "M003"];
60
+
61
+ // 3 cancelled sessions without cleanup
62
+ for (let i = 0; i < 3; i++) {
63
+ const allIds = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
64
+ const preview = nextMilestoneId(allIds);
65
+ reserveMilestoneId(preview);
66
+ }
67
+
68
+ // Without cleanup, we're now at M007 instead of M004
69
+ const allIds = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
70
+ const next = nextMilestoneId(allIds);
71
+ assert.equal(next, "M007", "3 cancelled sessions inflate ID by 3");
72
+
73
+ // With cleanup, we're back to M004
74
+ clearReservedMilestoneIds();
75
+ const allIdsClean = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
76
+ const nextClean = nextMilestoneId(allIdsClean);
77
+ assert.equal(nextClean, "M004", "cleanup restores correct next ID");
78
+ });
79
+ });