gsd-pi 2.17.0 → 2.18.0

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 (153) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-prompts.ts +20 -1
  7. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  8. package/dist/resources/extensions/gsd/auto.ts +123 -10
  9. package/dist/resources/extensions/gsd/commands.ts +245 -22
  10. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  11. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  12. package/dist/resources/extensions/gsd/files.ts +123 -1
  13. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  14. package/dist/resources/extensions/gsd/index.ts +47 -3
  15. package/dist/resources/extensions/gsd/paths.ts +9 -0
  16. package/dist/resources/extensions/gsd/preferences.ts +59 -1
  17. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  18. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  19. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  20. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  21. package/dist/resources/extensions/gsd/state.ts +15 -3
  22. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  23. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  24. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  25. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  26. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  27. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  28. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  29. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  30. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  31. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  32. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  33. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  34. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  35. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  36. package/package.json +1 -1
  37. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  38. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  40. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  42. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  43. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  44. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  46. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  47. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  48. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  49. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  50. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  51. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  52. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  53. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  54. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  55. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  56. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  57. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  58. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  59. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  60. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  62. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  63. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  64. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  65. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  66. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  68. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  70. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  72. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  74. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  76. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  78. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  80. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  83. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  85. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/index.js +4 -1
  87. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/main.js +17 -2
  90. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  105. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  106. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  107. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  108. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  109. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  110. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  111. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  112. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  113. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  114. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  115. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  116. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  117. package/packages/pi-coding-agent/src/index.ts +5 -0
  118. package/packages/pi-coding-agent/src/main.ts +19 -2
  119. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  120. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  121. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  122. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  123. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  124. package/src/resources/extensions/gsd/auto-prompts.ts +20 -1
  125. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  126. package/src/resources/extensions/gsd/auto.ts +123 -10
  127. package/src/resources/extensions/gsd/commands.ts +245 -22
  128. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  129. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  130. package/src/resources/extensions/gsd/files.ts +123 -1
  131. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  132. package/src/resources/extensions/gsd/index.ts +47 -3
  133. package/src/resources/extensions/gsd/paths.ts +9 -0
  134. package/src/resources/extensions/gsd/preferences.ts +59 -1
  135. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  136. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  137. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  138. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  139. package/src/resources/extensions/gsd/state.ts +15 -3
  140. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  141. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  142. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  143. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  145. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  146. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  147. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  148. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  149. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  150. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  151. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  152. package/src/resources/extensions/gsd/worktree.ts +22 -0
  153. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Unit tests for KNOWLEDGE.md integration.
3
+ *
4
+ * Tests:
5
+ * - KNOWLEDGE is registered in GSD_ROOT_FILES
6
+ * - resolveGsdRootFile resolves KNOWLEDGE paths correctly
7
+ * - inlineGsdRootFile works with the KNOWLEDGE key
8
+ * - before_agent_start hook includes/omits knowledge block appropriately
9
+ */
10
+
11
+ import test from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { tmpdir } from 'node:os';
16
+ import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
17
+ import { inlineGsdRootFile } from '../auto-prompts.ts';
18
+ import { appendKnowledge } from '../files.ts';
19
+
20
+ // ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
21
+
22
+ test('knowledge: KNOWLEDGE key exists in GSD_ROOT_FILES', () => {
23
+ assert.ok('KNOWLEDGE' in GSD_ROOT_FILES, 'GSD_ROOT_FILES should have KNOWLEDGE key');
24
+ assert.strictEqual(GSD_ROOT_FILES.KNOWLEDGE, 'KNOWLEDGE.md');
25
+ });
26
+
27
+ // ─── resolveGsdRootFile resolves KNOWLEDGE.md ───────────────────────────────
28
+
29
+ test('knowledge: resolveGsdRootFile returns canonical path when KNOWLEDGE.md exists', () => {
30
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
31
+ const gsdDir = join(tmp, '.gsd');
32
+ mkdirSync(gsdDir, { recursive: true });
33
+ writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n');
34
+
35
+ const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
36
+ assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
37
+
38
+ rmSync(tmp, { recursive: true, force: true });
39
+ });
40
+
41
+ test('knowledge: resolveGsdRootFile resolves when legacy knowledge.md exists', () => {
42
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
43
+ const gsdDir = join(tmp, '.gsd');
44
+ mkdirSync(gsdDir, { recursive: true });
45
+ writeFileSync(join(gsdDir, 'knowledge.md'), '# Project Knowledge\n');
46
+
47
+ const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
48
+ // On case-insensitive filesystems (macOS), canonical path matches;
49
+ // on case-sensitive (Linux), legacy path matches. Either is valid.
50
+ const canonical = join(gsdDir, 'KNOWLEDGE.md');
51
+ const legacy = join(gsdDir, 'knowledge.md');
52
+ assert.ok(
53
+ resolved === canonical || resolved === legacy,
54
+ `resolved path should be canonical or legacy, got: ${resolved}`,
55
+ );
56
+
57
+ rmSync(tmp, { recursive: true, force: true });
58
+ });
59
+
60
+ test('knowledge: resolveGsdRootFile returns canonical path when file does not exist', () => {
61
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
62
+ const gsdDir = join(tmp, '.gsd');
63
+ mkdirSync(gsdDir, { recursive: true });
64
+
65
+ const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
66
+ assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
67
+
68
+ rmSync(tmp, { recursive: true, force: true });
69
+ });
70
+
71
+ // ─── inlineGsdRootFile works with knowledge.md ─────────────────────────────
72
+
73
+ test('knowledge: inlineGsdRootFile returns content when KNOWLEDGE.md exists', async () => {
74
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
75
+ const gsdDir = join(tmp, '.gsd');
76
+ mkdirSync(gsdDir, { recursive: true });
77
+ writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n\n## Rules\n\nK001: Use real DB');
78
+
79
+ const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
80
+ assert.ok(result !== null, 'should return content');
81
+ assert.ok(result!.includes('Project Knowledge'), 'should include label');
82
+ assert.ok(result!.includes('K001'), 'should include knowledge content');
83
+
84
+ rmSync(tmp, { recursive: true, force: true });
85
+ });
86
+
87
+ test('knowledge: inlineGsdRootFile returns null when KNOWLEDGE.md does not exist', async () => {
88
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
89
+ const gsdDir = join(tmp, '.gsd');
90
+ mkdirSync(gsdDir, { recursive: true });
91
+
92
+ const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
93
+ assert.strictEqual(result, null, 'should return null when file does not exist');
94
+
95
+ rmSync(tmp, { recursive: true, force: true });
96
+ });
97
+
98
+ // ─── appendKnowledge creates file and appends entries ──────────────────────
99
+
100
+ test('knowledge: appendKnowledge creates KNOWLEDGE.md with rule when file does not exist', async () => {
101
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
102
+ const gsdDir = join(tmp, '.gsd');
103
+ mkdirSync(gsdDir, { recursive: true });
104
+
105
+ await appendKnowledge(tmp, 'rule', 'Use real DB for integration tests', 'M001/S01');
106
+
107
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
108
+ assert.ok(content.includes('# Project Knowledge'), 'should have header');
109
+ assert.ok(content.includes('K001'), 'should have K001 id');
110
+ assert.ok(content.includes('Use real DB for integration tests'), 'should have rule text');
111
+ assert.ok(content.includes('M001/S01'), 'should have scope');
112
+
113
+ rmSync(tmp, { recursive: true, force: true });
114
+ });
115
+
116
+ test('knowledge: appendKnowledge appends to existing KNOWLEDGE.md with auto-incrementing ID', async () => {
117
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
118
+ const gsdDir = join(tmp, '.gsd');
119
+ mkdirSync(gsdDir, { recursive: true });
120
+
121
+ // Create initial file with one rule
122
+ await appendKnowledge(tmp, 'rule', 'First rule', 'M001');
123
+ // Add second rule
124
+ await appendKnowledge(tmp, 'rule', 'Second rule', 'M001/S02');
125
+
126
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
127
+ assert.ok(content.includes('K001'), 'should have K001');
128
+ assert.ok(content.includes('K002'), 'should have K002');
129
+ assert.ok(content.includes('First rule'), 'should have first rule');
130
+ assert.ok(content.includes('Second rule'), 'should have second rule');
131
+
132
+ rmSync(tmp, { recursive: true, force: true });
133
+ });
134
+
135
+ test('knowledge: appendKnowledge handles pattern type', async () => {
136
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
137
+ const gsdDir = join(tmp, '.gsd');
138
+ mkdirSync(gsdDir, { recursive: true });
139
+
140
+ await appendKnowledge(tmp, 'pattern', 'Middleware chain for auth', 'M001');
141
+
142
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
143
+ assert.ok(content.includes('P001'), 'should have P001 id');
144
+ assert.ok(content.includes('Middleware chain for auth'), 'should have pattern text');
145
+
146
+ rmSync(tmp, { recursive: true, force: true });
147
+ });
148
+
149
+ test('knowledge: appendKnowledge handles lesson type', async () => {
150
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
151
+ const gsdDir = join(tmp, '.gsd');
152
+ mkdirSync(gsdDir, { recursive: true });
153
+
154
+ await appendKnowledge(tmp, 'lesson', 'API timeout on large payloads', 'M002');
155
+
156
+ const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
157
+ assert.ok(content.includes('L001'), 'should have L001 id');
158
+ assert.ok(content.includes('API timeout on large payloads'), 'should have lesson text');
159
+
160
+ rmSync(tmp, { recursive: true, force: true });
161
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * memory-leak-guards.test.ts — Tests for #611 memory leak fixes.
3
+ *
4
+ * Verifies that module-level state accumulators are properly bounded
5
+ * and cleared to prevent OOM during long-running auto-mode sessions.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtempSync, rmSync, existsSync, readdirSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+
14
+ import { saveActivityLog, clearActivityLogState } from "../activity-log.ts";
15
+ import { clearPathCache } from "../paths.ts";
16
+ import type { ExtensionContext } from "@gsd/pi-coding-agent";
17
+
18
+ function createCtx(entries: unknown[]) {
19
+ return { sessionManager: { getEntries: () => entries } } as unknown as ExtensionContext;
20
+ }
21
+
22
+ // ─── activity-log: clearActivityLogState ─────────────────────────────────────
23
+
24
+ test("clearActivityLogState resets dedup state so identical saves write again", () => {
25
+ clearActivityLogState();
26
+ const baseDir = mkdtempSync(join(tmpdir(), "gsd-memleak-test-"));
27
+ try {
28
+ const entries = [{ role: "assistant", content: "test entry" }];
29
+ const ctx = createCtx(entries);
30
+
31
+ // First save
32
+ saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
33
+
34
+ const actDir = join(baseDir, ".gsd", "activity");
35
+ assert.equal(readdirSync(actDir).length, 1, "first save creates one file");
36
+
37
+ // Same content, same unit — deduped
38
+ saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
39
+ assert.equal(readdirSync(actDir).length, 1, "dedup prevents duplicate write");
40
+
41
+ // Clear state
42
+ clearActivityLogState();
43
+
44
+ // Same content again — after clear, writes again (fresh state)
45
+ saveActivityLog(ctx, baseDir, "execute-task", "M001/S01/T01");
46
+ assert.equal(readdirSync(actDir).length, 2, "after clear, dedup state is reset");
47
+ } finally {
48
+ rmSync(baseDir, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ // ─── activity-log: streaming JSONL write ────────────────────────────────────
53
+
54
+ test("saveActivityLog writes valid JSONL via streaming", () => {
55
+ clearActivityLogState();
56
+ const baseDir = mkdtempSync(join(tmpdir(), "gsd-memleak-jsonl-"));
57
+ try {
58
+ const entries = [
59
+ { type: "message", message: { role: "user", content: "hello" } },
60
+ { type: "message", message: { role: "assistant", content: "world" } },
61
+ { type: "message", message: { role: "user", content: "test" } },
62
+ ];
63
+ const ctx = createCtx(entries);
64
+
65
+ saveActivityLog(ctx, baseDir, "execute-task", "M002/S01/T01");
66
+
67
+ const actDir = join(baseDir, ".gsd", "activity");
68
+ const files = readdirSync(actDir);
69
+ assert.equal(files.length, 1, "one file written");
70
+
71
+ const content = readFileSync(join(actDir, files[0]), "utf-8");
72
+ const lines = content.trim().split("\n");
73
+ assert.equal(lines.length, 3, "three JSONL lines");
74
+
75
+ for (const line of lines) {
76
+ assert.doesNotThrow(() => JSON.parse(line), `line is valid JSON`);
77
+ }
78
+ } finally {
79
+ rmSync(baseDir, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ // ─── paths.ts: directory cache bounds ───────────────────────────────────────
84
+
85
+ test("clearPathCache does not throw", () => {
86
+ assert.doesNotThrow(() => clearPathCache(), "clearPathCache should not throw");
87
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * preferences-wizard-fields.test.ts — Validates that all wizard-configurable
3
+ * preference fields are properly validated and round-trip through the schema.
4
+ */
5
+
6
+ import { createTestContext } from "./test-helpers.ts";
7
+ import { validatePreferences } from "../preferences.ts";
8
+ import type { GSDPreferences } from "../preferences.ts";
9
+
10
+ const { assertEq, assertTrue, report } = createTestContext();
11
+
12
+ async function main(): Promise<void> {
13
+ console.log("\n=== budget fields validate correctly ===");
14
+
15
+ {
16
+ const { preferences, errors } = validatePreferences({
17
+ budget_ceiling: 25.50,
18
+ budget_enforcement: "warn",
19
+ context_pause_threshold: 80,
20
+ });
21
+ assertEq(errors.length, 0, "valid budget fields produce no errors");
22
+ assertEq(preferences.budget_ceiling, 25.50, "budget_ceiling passes through");
23
+ assertEq(preferences.budget_enforcement, "warn", "budget_enforcement passes through");
24
+ assertEq(preferences.context_pause_threshold, 80, "context_pause_threshold passes through");
25
+ }
26
+
27
+ {
28
+ const { preferences, errors } = validatePreferences({
29
+ budget_enforcement: "pause",
30
+ });
31
+ assertEq(errors.length, 0, "budget_enforcement 'pause' is valid");
32
+ assertEq(preferences.budget_enforcement, "pause", "pause passes through");
33
+ }
34
+
35
+ {
36
+ const { preferences, errors } = validatePreferences({
37
+ budget_enforcement: "halt",
38
+ });
39
+ assertEq(errors.length, 0, "budget_enforcement 'halt' is valid");
40
+ assertEq(preferences.budget_enforcement, "halt", "halt passes through");
41
+ }
42
+
43
+ {
44
+ const { errors } = validatePreferences({
45
+ budget_enforcement: "invalid",
46
+ } as unknown as GSDPreferences);
47
+ assertTrue(errors.some(e => e.includes("budget_enforcement")), "invalid budget_enforcement rejected");
48
+ }
49
+
50
+ console.log("\n=== notification fields validate correctly ===");
51
+
52
+ {
53
+ const { preferences, errors } = validatePreferences({
54
+ notifications: {
55
+ enabled: true,
56
+ on_complete: false,
57
+ on_error: true,
58
+ on_budget: true,
59
+ on_milestone: false,
60
+ on_attention: true,
61
+ },
62
+ });
63
+ assertEq(errors.length, 0, "valid notifications produce no errors");
64
+ assertEq(preferences.notifications?.enabled, true, "notifications.enabled passes through");
65
+ assertEq(preferences.notifications?.on_complete, false, "notifications.on_complete passes through");
66
+ assertEq(preferences.notifications?.on_milestone, false, "notifications.on_milestone passes through");
67
+ }
68
+
69
+ {
70
+ const { errors } = validatePreferences({
71
+ notifications: "invalid",
72
+ } as unknown as GSDPreferences);
73
+ assertTrue(errors.some(e => e.includes("notifications")), "invalid notifications rejected");
74
+ }
75
+
76
+ console.log("\n=== git fields validate correctly ===");
77
+
78
+ {
79
+ const { preferences, errors } = validatePreferences({
80
+ git: {
81
+ auto_push: true,
82
+ push_branches: false,
83
+ remote: "upstream",
84
+ snapshots: true,
85
+ pre_merge_check: "auto",
86
+ commit_type: "feat",
87
+ main_branch: "develop",
88
+ merge_strategy: "squash",
89
+ isolation: "branch",
90
+ },
91
+ });
92
+ assertEq(errors.length, 0, "valid git fields produce no errors");
93
+ assertEq(preferences.git?.auto_push, true, "git.auto_push passes through");
94
+ assertEq(preferences.git?.push_branches, false, "git.push_branches passes through");
95
+ assertEq(preferences.git?.remote, "upstream", "git.remote passes through");
96
+ assertEq(preferences.git?.snapshots, true, "git.snapshots passes through");
97
+ assertEq(preferences.git?.pre_merge_check, "auto", "git.pre_merge_check passes through");
98
+ assertEq(preferences.git?.commit_type, "feat", "git.commit_type passes through");
99
+ assertEq(preferences.git?.main_branch, "develop", "git.main_branch passes through");
100
+ assertEq(preferences.git?.merge_strategy, "squash", "git.merge_strategy passes through");
101
+ assertEq(preferences.git?.isolation, "branch", "git.isolation passes through");
102
+ }
103
+
104
+ console.log("\n=== uat_dispatch validates correctly ===");
105
+
106
+ {
107
+ const { preferences, errors } = validatePreferences({ uat_dispatch: true });
108
+ assertEq(errors.length, 0, "valid uat_dispatch produces no errors");
109
+ assertEq(preferences.uat_dispatch, true, "uat_dispatch true passes through");
110
+ }
111
+
112
+ {
113
+ const { preferences, errors } = validatePreferences({ uat_dispatch: false });
114
+ assertEq(errors.length, 0, "valid uat_dispatch false produces no errors");
115
+ assertEq(preferences.uat_dispatch, false, "uat_dispatch false passes through");
116
+ }
117
+
118
+ console.log("\n=== unique_milestone_ids validates correctly ===");
119
+
120
+ {
121
+ const { preferences, errors } = validatePreferences({ unique_milestone_ids: true });
122
+ assertEq(errors.length, 0, "valid unique_milestone_ids produces no errors");
123
+ assertEq(preferences.unique_milestone_ids, true, "unique_milestone_ids passes through");
124
+ }
125
+
126
+ console.log("\n=== all wizard fields together produce no errors ===");
127
+
128
+ {
129
+ const fullPrefs: GSDPreferences = {
130
+ version: 1,
131
+ models: { research: "claude-opus-4-6", planning: "claude-sonnet-4-6" },
132
+ auto_supervisor: { soft_timeout_minutes: 15, idle_timeout_minutes: 5, hard_timeout_minutes: 25 },
133
+ git: {
134
+ main_branch: "main",
135
+ auto_push: true,
136
+ push_branches: false,
137
+ remote: "origin",
138
+ snapshots: true,
139
+ pre_merge_check: "auto",
140
+ commit_type: "feat",
141
+ merge_strategy: "squash",
142
+ isolation: "worktree",
143
+ },
144
+ skill_discovery: "suggest",
145
+ unique_milestone_ids: false,
146
+ budget_ceiling: 50,
147
+ budget_enforcement: "pause",
148
+ context_pause_threshold: 75,
149
+ notifications: {
150
+ enabled: true,
151
+ on_complete: true,
152
+ on_error: true,
153
+ on_budget: true,
154
+ on_milestone: true,
155
+ on_attention: true,
156
+ },
157
+ uat_dispatch: false,
158
+ };
159
+ const { errors, warnings } = validatePreferences(fullPrefs);
160
+ const unknownWarnings = warnings.filter(w => w.includes("unknown"));
161
+ assertEq(errors.length, 0, "full wizard prefs produce no errors");
162
+ assertEq(unknownWarnings.length, 0, "full wizard prefs produce no unknown-key warnings");
163
+ }
164
+
165
+ report();
166
+ }
167
+
168
+ main();
@@ -0,0 +1,204 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+
5
+ import {
6
+ loadQueueOrder,
7
+ saveQueueOrder,
8
+ sortByQueueOrder,
9
+ pruneQueueOrder,
10
+ validateQueueOrder,
11
+ } from '../queue-order.ts';
12
+ import { createTestContext } from './test-helpers.ts';
13
+
14
+ const { assertEq, assertTrue, report } = createTestContext();
15
+
16
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
17
+
18
+ function createFixtureBase(): string {
19
+ const base = mkdtempSync(join(tmpdir(), 'gsd-queue-order-'));
20
+ mkdirSync(join(base, '.gsd'), { recursive: true });
21
+ return base;
22
+ }
23
+
24
+ function cleanup(base: string): void {
25
+ rmSync(base, { recursive: true, force: true });
26
+ }
27
+
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // sortByQueueOrder
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+
32
+ console.log('\n=== sortByQueueOrder ===');
33
+
34
+ // Null order → default milestoneIdSort
35
+ {
36
+ const result = sortByQueueOrder(['M003', 'M001', 'M002'], null);
37
+ assertEq(result, ['M001', 'M002', 'M003'], 'null order falls back to numeric sort');
38
+ }
39
+
40
+ // Custom order → exact sequence
41
+ {
42
+ const result = sortByQueueOrder(['M001', 'M002', 'M003'], ['M003', 'M001', 'M002']);
43
+ assertEq(result, ['M003', 'M001', 'M002'], 'custom order produces exact sequence');
44
+ }
45
+
46
+ // Custom order with new IDs → appended at end in numeric order
47
+ {
48
+ const result = sortByQueueOrder(['M001', 'M002', 'M003', 'M004'], ['M003', 'M001']);
49
+ assertEq(result, ['M003', 'M001', 'M002', 'M004'], 'new IDs appended in numeric order');
50
+ }
51
+
52
+ // Custom order with deleted IDs → silently skipped
53
+ {
54
+ const result = sortByQueueOrder(['M001', 'M003'], ['M003', 'M002', 'M001']);
55
+ assertEq(result, ['M003', 'M001'], 'deleted IDs in order are skipped');
56
+ }
57
+
58
+ // Empty custom order → all IDs in numeric order
59
+ {
60
+ const result = sortByQueueOrder(['M002', 'M001'], []);
61
+ assertEq(result, ['M001', 'M002'], 'empty custom order falls back to numeric sort');
62
+ }
63
+
64
+ // ═══════════════════════════════════════════════════════════════════════════
65
+ // loadQueueOrder / saveQueueOrder
66
+ // ═══════════════════════════════════════════════════════════════════════════
67
+
68
+ console.log('\n=== loadQueueOrder / saveQueueOrder ===');
69
+
70
+ // Load returns null when file doesn't exist
71
+ {
72
+ const base = createFixtureBase();
73
+ assertEq(loadQueueOrder(base), null, 'returns null when file missing');
74
+ cleanup(base);
75
+ }
76
+
77
+ // Save then load round-trip
78
+ {
79
+ const base = createFixtureBase();
80
+ saveQueueOrder(base, ['M003', 'M001', 'M002']);
81
+ const loaded = loadQueueOrder(base);
82
+ assertEq(loaded, ['M003', 'M001', 'M002'], 'round-trip preserves order');
83
+
84
+ // Verify file contains updatedAt
85
+ const raw = JSON.parse(readFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), 'utf-8'));
86
+ assertTrue(typeof raw.updatedAt === 'string' && raw.updatedAt.length > 0, 'file contains updatedAt');
87
+
88
+ cleanup(base);
89
+ }
90
+
91
+ // Load returns null on corrupt JSON
92
+ {
93
+ const base = createFixtureBase();
94
+ writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), 'not json');
95
+ assertEq(loadQueueOrder(base), null, 'returns null on corrupt JSON');
96
+ cleanup(base);
97
+ }
98
+
99
+ // Load returns null when order field is not an array
100
+ {
101
+ const base = createFixtureBase();
102
+ writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), '{"order": "invalid"}');
103
+ assertEq(loadQueueOrder(base), null, 'returns null when order is not array');
104
+ cleanup(base);
105
+ }
106
+
107
+ // ═══════════════════════════════════════════════════════════════════════════
108
+ // pruneQueueOrder
109
+ // ═══════════════════════════════════════════════════════════════════════════
110
+
111
+ console.log('\n=== pruneQueueOrder ===');
112
+
113
+ // Prune removes invalid IDs
114
+ {
115
+ const base = createFixtureBase();
116
+ saveQueueOrder(base, ['M001', 'M002', 'M003']);
117
+ pruneQueueOrder(base, ['M001', 'M003']);
118
+ assertEq(loadQueueOrder(base), ['M001', 'M003'], 'prune removes invalid IDs');
119
+ cleanup(base);
120
+ }
121
+
122
+ // Prune no-ops when file doesn't exist
123
+ {
124
+ const base = createFixtureBase();
125
+ pruneQueueOrder(base, ['M001']); // should not throw
126
+ assertTrue(!existsSync(join(base, '.gsd', 'QUEUE-ORDER.json')), 'prune does not create file');
127
+ cleanup(base);
128
+ }
129
+
130
+ // Prune no-ops when all IDs are valid
131
+ {
132
+ const base = createFixtureBase();
133
+ saveQueueOrder(base, ['M001', 'M002']);
134
+ pruneQueueOrder(base, ['M001', 'M002', 'M003']);
135
+ assertEq(loadQueueOrder(base), ['M001', 'M002'], 'prune is no-op when all valid');
136
+ cleanup(base);
137
+ }
138
+
139
+ // ═══════════════════════════════════════════════════════════════════════════
140
+ // validateQueueOrder
141
+ // ═══════════════════════════════════════════════════════════════════════════
142
+
143
+ console.log('\n=== validateQueueOrder ===');
144
+
145
+ // Valid order with no dependencies
146
+ {
147
+ const depsMap = new Map<string, string[]>();
148
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
149
+ assertTrue(result.valid, 'valid when no dependencies');
150
+ assertEq(result.violations.length, 0, 'no violations');
151
+ assertEq(result.redundant.length, 0, 'no redundancies');
152
+ }
153
+
154
+ // Dependency violation: M002 before M001, but M002 depends on M001
155
+ {
156
+ const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
157
+ const result = validateQueueOrder(['M002', 'M001'], depsMap, new Set());
158
+ assertTrue(!result.valid, 'invalid when dep violated');
159
+ assertEq(result.violations.length, 1, 'one violation');
160
+ assertEq(result.violations[0].type, 'would_block', 'violation type is would_block');
161
+ assertEq(result.violations[0].milestone, 'M002', 'violation milestone is M002');
162
+ assertEq(result.violations[0].dependsOn, 'M001', 'violation dep is M001');
163
+ }
164
+
165
+ // Redundant dependency: M002 depends on M001, M001 comes first in order
166
+ {
167
+ const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
168
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
169
+ assertTrue(result.valid, 'valid when dep satisfied by position');
170
+ assertEq(result.redundant.length, 1, 'one redundancy');
171
+ assertEq(result.redundant[0].milestone, 'M002', 'redundant milestone is M002');
172
+ }
173
+
174
+ // Completed dep is always satisfied
175
+ {
176
+ const depsMap = new Map<string, string[]>([['M002', ['M001']]]);
177
+ const result = validateQueueOrder(['M002'], depsMap, new Set(['M001']));
178
+ assertTrue(result.valid, 'valid when dep is already completed');
179
+ assertEq(result.violations.length, 0, 'no violations for completed dep');
180
+ }
181
+
182
+ // Missing dependency
183
+ {
184
+ const depsMap = new Map<string, string[]>([['M002', ['M099']]]);
185
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
186
+ assertTrue(!result.valid, 'invalid when dep does not exist');
187
+ assertEq(result.violations[0].type, 'missing_dep', 'violation type is missing_dep');
188
+ }
189
+
190
+ // Circular dependency
191
+ {
192
+ const depsMap = new Map<string, string[]>([
193
+ ['M001', ['M002']],
194
+ ['M002', ['M001']],
195
+ ]);
196
+ const result = validateQueueOrder(['M001', 'M002'], depsMap, new Set());
197
+ assertTrue(!result.valid, 'invalid on circular dependency');
198
+ const circularViolation = result.violations.find(v => v.type === 'circular');
199
+ assertTrue(!!circularViolation, 'circular violation detected');
200
+ }
201
+
202
+ // ═══════════════════════════════════════════════════════════════════════════
203
+
204
+ report();