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,263 @@
1
+ /**
2
+ * GSD Queue Reorder UI
3
+ *
4
+ * Interactive TUI overlay for reordering pending milestones.
5
+ * ↑/↓ navigates cursor. Space grabs/releases item for moving.
6
+ * While grabbed, ↑/↓ swaps the item with its neighbor.
7
+ * Enter confirms all changes. Esc cancels.
8
+ * Conflicting depends_on entries are auto-removed on confirm.
9
+ */
10
+
11
+ import type { ExtensionContext } from "@gsd/pi-coding-agent";
12
+ import { type Theme } from "@gsd/pi-coding-agent";
13
+ import { Key, matchesKey, truncateToWidth, type TUI } from "@gsd/pi-tui";
14
+ import { makeUI, GLYPH } from "../shared/ui.js";
15
+ import { validateQueueOrder, type DependencyValidation } from "./queue-order.js";
16
+
17
+ export interface ReorderItem {
18
+ id: string;
19
+ title: string;
20
+ dependsOn?: string[];
21
+ }
22
+
23
+ export interface ReorderResult {
24
+ order: string[];
25
+ /** depends_on entries to remove from CONTEXT.md files */
26
+ depsToRemove: Array<{ milestone: string; dep: string }>;
27
+ }
28
+
29
+ /**
30
+ * Show the queue reorder overlay.
31
+ * Returns the new order + deps to remove, or null if cancelled.
32
+ */
33
+ export async function showQueueReorder(
34
+ ctx: ExtensionContext,
35
+ completed: ReorderItem[],
36
+ pending: ReorderItem[],
37
+ ): Promise<ReorderResult | null> {
38
+ if (!ctx.hasUI) return null;
39
+ if (pending.length < 2) return null;
40
+
41
+ return ctx.ui.custom<ReorderResult | null>((tui: TUI, theme: Theme, _kb, done) => {
42
+ const items = [...pending];
43
+ let cursor = 0;
44
+ let grabbed = false;
45
+ let cachedLines: string[] | undefined;
46
+ let validation: DependencyValidation;
47
+
48
+ // Mutable deps map — tracks removals during this session
49
+ const liveDeps = new Map<string, string[]>();
50
+ for (const item of [...completed, ...pending]) {
51
+ if (item.dependsOn && item.dependsOn.length > 0) {
52
+ liveDeps.set(item.id, [...item.dependsOn]);
53
+ }
54
+ }
55
+
56
+ const removedDeps: Array<{ milestone: string; dep: string }> = [];
57
+ const completedIds = new Set(completed.map(c => c.id));
58
+
59
+ function revalidate() {
60
+ validation = validateQueueOrder(items.map(i => i.id), liveDeps, completedIds);
61
+ }
62
+
63
+ revalidate();
64
+
65
+ function refresh() {
66
+ cachedLines = undefined;
67
+ tui.requestRender();
68
+ }
69
+
70
+ function swapItems(fromIdx: number, toIdx: number) {
71
+ if (toIdx < 0 || toIdx >= items.length) return;
72
+ const [item] = items.splice(fromIdx, 1);
73
+ items.splice(toIdx, 0, item);
74
+ cursor = toIdx;
75
+ revalidate();
76
+ refresh();
77
+ }
78
+
79
+ function removeDep(milestone: string, dep: string) {
80
+ const deps = liveDeps.get(milestone);
81
+ if (!deps) return;
82
+ const idx = deps.indexOf(dep);
83
+ if (idx >= 0) {
84
+ deps.splice(idx, 1);
85
+ if (deps.length === 0) liveDeps.delete(milestone);
86
+ removedDeps.push({ milestone, dep });
87
+ const item = items.find(i => i.id === milestone);
88
+ if (item?.dependsOn) {
89
+ item.dependsOn = item.dependsOn.filter(d => d !== dep);
90
+ }
91
+ revalidate();
92
+ refresh();
93
+ }
94
+ }
95
+
96
+ function handleInput(data: string) {
97
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
98
+ done(null);
99
+ return;
100
+ }
101
+
102
+ // Confirm — auto-resolve would_block violations
103
+ if (matchesKey(data, Key.enter)) {
104
+ const wouldBlock = validation.violations.filter(v => v.type === 'would_block');
105
+ for (const v of wouldBlock) {
106
+ removeDep(v.milestone, v.dependsOn);
107
+ }
108
+ done({ order: items.map(i => i.id), depsToRemove: removedDeps });
109
+ return;
110
+ }
111
+
112
+ // Space — toggle grab mode
113
+ if (data === " ") {
114
+ grabbed = !grabbed;
115
+ refresh();
116
+ return;
117
+ }
118
+
119
+ // ↑/↓ — move grabbed item OR navigate cursor
120
+ if (matchesKey(data, Key.up)) {
121
+ if (grabbed) {
122
+ swapItems(cursor, cursor - 1);
123
+ } else {
124
+ cursor = Math.max(0, cursor - 1);
125
+ refresh();
126
+ }
127
+ return;
128
+ }
129
+ if (matchesKey(data, Key.down)) {
130
+ if (grabbed) {
131
+ swapItems(cursor, cursor + 1);
132
+ } else {
133
+ cursor = Math.min(items.length - 1, cursor + 1);
134
+ refresh();
135
+ }
136
+ return;
137
+ }
138
+
139
+ // 'd' — manually remove a dep on the cursor item
140
+ if (data === "d" || data === "D") {
141
+ const item = items[cursor];
142
+ const deps = liveDeps.get(item.id);
143
+ if (deps) {
144
+ const activeDep = deps.find(d => !completedIds.has(d));
145
+ if (activeDep) removeDep(item.id, activeDep);
146
+ }
147
+ return;
148
+ }
149
+ }
150
+
151
+ function render(width: number): string[] {
152
+ if (cachedLines) return cachedLines;
153
+
154
+ const ui = makeUI(theme, width);
155
+ const lines: string[] = [];
156
+ const push = (...rows: string[][]) => { for (const r of rows) lines.push(...r); };
157
+ const add = (s: string) => truncateToWidth(s, width);
158
+
159
+ const headerText = grabbed ? " Queue Reorder — Moving Item" : " Queue Reorder";
160
+ push(ui.bar(), ui.blank(), ui.header(headerText), ui.blank());
161
+
162
+ // Completed milestones (dimmed)
163
+ if (completed.length > 0) {
164
+ lines.push(add(theme.fg("dim", " Completed:")));
165
+ for (const m of completed) {
166
+ const label = m.title && m.title !== m.id ? `${m.id} ${m.title}` : m.id;
167
+ lines.push(add(` ${theme.fg("dim", `${GLYPH.statusDone} ${label}`)}`));
168
+ }
169
+ push(ui.blank());
170
+ }
171
+
172
+ // Pending milestones
173
+ const queueLabel = grabbed ? " Queue (space to release, ↑/↓ to move):" : " Queue (space to grab, ↑/↓ to navigate):";
174
+ lines.push(add(theme.fg("text", queueLabel)));
175
+
176
+ const violatedPairs = new Set(
177
+ validation.violations.filter(v => v.type === 'would_block').map(v => `${v.milestone}:${v.dependsOn}`),
178
+ );
179
+ const redundantPairs = new Set(
180
+ validation.redundant.map(r => `${r.milestone}:${r.dependsOn}`),
181
+ );
182
+
183
+ for (let i = 0; i < items.length; i++) {
184
+ const item = items[i];
185
+ const isCursor = i === cursor;
186
+ const num = i + 1;
187
+ const label = item.title && item.title !== item.id ? `${item.id} ${item.title}` : item.id;
188
+
189
+ if (isCursor && grabbed) {
190
+ lines.push(add(` ${theme.fg("warning", `▸▸ ${num}. ${label}`)}`));
191
+ } else if (isCursor) {
192
+ lines.push(add(` ${theme.fg("accent", `${GLYPH.cursor} ${num}. ${label}`)}`));
193
+ } else {
194
+ lines.push(add(` ${theme.fg("text", `${num}. ${label}`)}`));
195
+ }
196
+
197
+ // depends_on annotations
198
+ const deps = liveDeps.get(item.id) ?? [];
199
+ for (const dep of deps) {
200
+ if (completedIds.has(dep)) continue;
201
+ const pairKey = `${item.id}:${dep}`;
202
+ if (violatedPairs.has(pairKey)) {
203
+ lines.push(add(` ${theme.fg("warning", `${GLYPH.statusWarning} depends_on: ${dep} — auto-removed on confirm`)}`));
204
+ } else if (redundantPairs.has(pairKey)) {
205
+ lines.push(add(` ${theme.fg("dim", `↳ depends_on: ${dep} (redundant)`)}`));
206
+ } else {
207
+ lines.push(add(` ${theme.fg("dim", `↳ depends_on: ${dep}`)}`));
208
+ }
209
+ }
210
+
211
+ // Missing deps
212
+ for (const v of validation.violations.filter(v => v.milestone === item.id && v.type === 'missing_dep')) {
213
+ lines.push(add(` ${theme.fg("error", `${GLYPH.statusWarning} depends_on: ${v.dependsOn} (does not exist)`)}`));
214
+ }
215
+ }
216
+
217
+ // Removed deps feedback
218
+ if (removedDeps.length > 0) {
219
+ push(ui.blank());
220
+ for (const r of removedDeps) {
221
+ lines.push(add(` ${theme.fg("success", `${GLYPH.statusDone} Removed: ${r.milestone} depends_on ${r.dep}`)}`));
222
+ }
223
+ }
224
+
225
+ // Circular warning
226
+ const circ = validation.violations.find(v => v.type === 'circular');
227
+ if (circ) {
228
+ push(ui.blank());
229
+ lines.push(add(` ${theme.fg("error", `${GLYPH.statusWarning} ${circ.message}`)}`));
230
+ }
231
+
232
+ push(ui.blank());
233
+
234
+ // Hints — context-sensitive based on grab state
235
+ const hints: string[] = [];
236
+ if (grabbed) {
237
+ hints.push("↑/↓ move item", "space release");
238
+ } else {
239
+ hints.push("↑/↓ navigate", "space grab");
240
+ }
241
+ const hasDeps = liveDeps.get(items[cursor]?.id)?.some(d => !completedIds.has(d));
242
+ if (hasDeps) hints.push("d del dep");
243
+
244
+ const wouldBlockCount = validation.violations.filter(v => v.type === 'would_block').length;
245
+ if (wouldBlockCount > 0) {
246
+ hints.push(`enter (fixes ${wouldBlockCount} dep)`);
247
+ } else {
248
+ hints.push("enter ok");
249
+ }
250
+ hints.push("esc");
251
+
252
+ push(ui.hints(hints), ui.bar());
253
+
254
+ cachedLines = lines;
255
+ return lines;
256
+ }
257
+
258
+ return { render, invalidate: () => { cachedLines = undefined; }, handleInput };
259
+ }, {
260
+ overlay: true,
261
+ overlayOptions: { width: "70%", minWidth: 50, maxHeight: "80%", anchor: "center" },
262
+ });
263
+ }
@@ -224,9 +224,21 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
224
224
  const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
225
225
  if (draftFile) activeMilestoneHasDraft = true;
226
226
  }
227
- activeMilestone = { id: mid, title: mid };
228
- activeMilestoneFound = true;
229
- registry.push({ id: mid, title: mid, status: 'active' });
227
+
228
+ // Check milestone-level dependencies before promoting to active.
229
+ // Without this, a queued milestone with depends_on in its CONTEXT
230
+ // frontmatter would be promoted to active even when its deps are unmet
231
+ // (the dep check only existed in the has-roadmap path previously).
232
+ const contextContent = contextFile ? await cachedLoadFile(contextFile) : null;
233
+ const deps = parseContextDependsOn(contextContent);
234
+ const depsUnmet = deps.some(dep => !completeMilestoneIds.has(dep));
235
+ if (depsUnmet) {
236
+ registry.push({ id: mid, title: mid, status: 'pending', dependsOn: deps });
237
+ } else {
238
+ activeMilestone = { id: mid, title: mid };
239
+ activeMilestoneFound = true;
240
+ registry.push({ id: mid, title: mid, status: 'active', ...(deps.length > 0 ? { dependsOn: deps } : {}) });
241
+ }
230
242
  } else {
231
243
  registry.push({ id: mid, title: mid, status: 'pending' });
232
244
  }
@@ -0,0 +1,19 @@
1
+ # Project Knowledge
2
+
3
+ Append-only register of project-specific rules, patterns, and lessons learned.
4
+ Agents read this before every unit. Add entries when you discover something worth remembering.
5
+
6
+ ## Rules
7
+
8
+ | # | Scope | Rule | Why | Added |
9
+ |---|-------|------|-----|-------|
10
+
11
+ ## Patterns
12
+
13
+ | # | Pattern | Where | Notes |
14
+ |---|---------|-------|-------|
15
+
16
+ ## Lessons Learned
17
+
18
+ | # | What Happened | Root Cause | Fix | Scope |
19
+ |---|--------------|------------|-----|-------|
@@ -15,7 +15,21 @@ git:
15
15
  snapshots:
16
16
  pre_merge_check:
17
17
  commit_type:
18
+ main_branch:
19
+ merge_strategy:
20
+ isolation:
18
21
  unique_milestone_ids:
22
+ budget_ceiling:
23
+ budget_enforcement:
24
+ context_pause_threshold:
25
+ notifications:
26
+ enabled:
27
+ on_complete:
28
+ on_error:
29
+ on_budget:
30
+ on_milestone:
31
+ on_attention:
32
+ uat_dispatch:
19
33
  ---
20
34
 
21
35
  # GSD Skill Preferences
@@ -17,6 +17,7 @@ import {
17
17
  getAutoWorktreePath,
18
18
  enterAutoWorktree,
19
19
  getAutoWorktreeOriginalBase,
20
+ getActiveAutoWorktreeContext,
20
21
  } from "../auto-worktree.ts";
21
22
 
22
23
  import { createTestContext } from "./test-helpers.ts";
@@ -76,6 +77,15 @@ async function main(): Promise<void> {
76
77
 
77
78
  // ─── getAutoWorktreeOriginalBase ─────────────────────────────────
78
79
  assertEq(getAutoWorktreeOriginalBase(), tempDir, "originalBase returns temp dir");
80
+ assertEq(
81
+ getActiveAutoWorktreeContext(),
82
+ {
83
+ originalBase: tempDir,
84
+ worktreeName: "M003",
85
+ branch: "milestone/M003",
86
+ },
87
+ "active auto-worktree context reflects the worktree cwd",
88
+ );
79
89
 
80
90
  // ─── getAutoWorktreePath ─────────────────────────────────────────
81
91
  assertEq(getAutoWorktreePath(tempDir, "M003"), wtPath, "getAutoWorktreePath returns correct path");
@@ -88,6 +98,7 @@ async function main(): Promise<void> {
88
98
  assertTrue(!existsSync(wtPath), "worktree directory removed after teardown");
89
99
  assertTrue(!isInAutoWorktree(tempDir), "isInAutoWorktree returns false after teardown");
90
100
  assertEq(getAutoWorktreeOriginalBase(), null, "originalBase is null after teardown");
101
+ assertEq(getActiveAutoWorktreeContext(), null, "active auto-worktree context clears after teardown");
91
102
 
92
103
  // ─── Re-entry: create again, exit without teardown, re-enter ─────
93
104
  console.log("\n=== re-entry ===");
@@ -103,6 +114,15 @@ async function main(): Promise<void> {
103
114
  assertEq(process.cwd(), entered, "re-entered worktree via enterAutoWorktree");
104
115
  assertEq(getAutoWorktreeOriginalBase(), tempDir, "originalBase restored on re-entry");
105
116
  assertTrue(isInAutoWorktree(tempDir), "isInAutoWorktree true after re-entry");
117
+ assertEq(
118
+ getActiveAutoWorktreeContext(),
119
+ {
120
+ originalBase: tempDir,
121
+ worktreeName: "M003",
122
+ branch: "milestone/M003",
123
+ },
124
+ "active auto-worktree context is restored on re-entry",
125
+ );
106
126
 
107
127
  // Cleanup
108
128
  teardownAutoWorktree(tempDir, "M003");
@@ -303,6 +303,105 @@ async function main(): Promise<void> {
303
303
  }
304
304
  }
305
305
 
306
+ // ─── Test Group 7: unique-id-deps ──────────────────────────────────────
307
+ // M004-0zjrg0 is complete, M005-b0m2hl depends_on M004-0zjrg0 → M005 should activate.
308
+ // Regression: parseContextDependsOn() used .toUpperCase(), converting "M004-0zjrg0"
309
+ // to "M004-0ZJRG0", breaking the case-sensitive lookup in completeMilestoneIds.
310
+ console.log('\n=== unique-id-deps: unique milestone IDs with lowercase hex suffix ===');
311
+ {
312
+ const base = createFixtureBase();
313
+ try {
314
+ // M004-0zjrg0: complete (all slices done + SUMMARY present)
315
+ writeRoadmap(base, 'M004-0zjrg0', `# M004-0zjrg0: First Unique Milestone
316
+
317
+ **Vision:** Complete milestone with unique ID.
318
+
319
+ ## Slices
320
+
321
+ - [x] **S01: Done** \`risk:low\` \`depends:[]\`
322
+ > After this: Done.
323
+ `);
324
+ writeMilestoneSummary(base, 'M004-0zjrg0', '# M004-0zjrg0 Summary\n\nComplete.');
325
+
326
+ // M005-b0m2hl: depends on M004-0zjrg0 (lowercase hex suffix)
327
+ writeContext(base, 'M005-b0m2hl', 'depends_on: [M004-0zjrg0]');
328
+
329
+ const state = await deriveState(base);
330
+
331
+ assertEq(state.registry.find(e => e.id === 'M004-0zjrg0')?.status, 'complete',
332
+ 'unique-id-deps: M004-0zjrg0 is complete');
333
+ assertEq(state.registry.find(e => e.id === 'M005-b0m2hl')?.status, 'active',
334
+ 'unique-id-deps: M005-b0m2hl is active (dep on M004-0zjrg0 met)');
335
+ assertEq(state.activeMilestone?.id, 'M005-b0m2hl',
336
+ 'unique-id-deps: activeMilestone is M005-b0m2hl');
337
+ assertTrue(state.phase !== 'blocked',
338
+ 'unique-id-deps: phase is not blocked');
339
+ } finally {
340
+ cleanup(base);
341
+ }
342
+ }
343
+
344
+ // ─── Test Group 8: unique-id-deps-blocked ─────────────────────────────
345
+ // M004-0zjrg0 is NOT complete, M005-b0m2hl depends_on M004-0zjrg0 → M005 should be pending
346
+ console.log('\n=== unique-id-deps-blocked: unique ID dep not yet met ===');
347
+ {
348
+ const base = createFixtureBase();
349
+ try {
350
+ // M004-0zjrg0: incomplete (slice not done)
351
+ writeRoadmap(base, 'M004-0zjrg0', `# M004-0zjrg0: Incomplete Unique Milestone
352
+
353
+ **Vision:** Still in progress.
354
+
355
+ ## Slices
356
+
357
+ - [ ] **S01: In Progress** \`risk:low\` \`depends:[]\`
358
+ > After this: Done.
359
+ `);
360
+ writeSlicePlan(base, 'M004-0zjrg0', 'S01', `# S01: In Progress
361
+
362
+ **Goal:** Test dep blocking with unique IDs.
363
+
364
+ ## Tasks
365
+
366
+ - [ ] **T01: Work** \`est:15m\`
367
+ Still doing work.
368
+ `);
369
+
370
+ // M005-b0m2hl: depends on M004-0zjrg0 (still incomplete)
371
+ writeContext(base, 'M005-b0m2hl', 'depends_on: [M004-0zjrg0]');
372
+
373
+ const state = await deriveState(base);
374
+
375
+ assertEq(state.activeMilestone?.id, 'M004-0zjrg0',
376
+ 'unique-id-deps-blocked: activeMilestone is M004-0zjrg0');
377
+ assertEq(state.registry.find(e => e.id === 'M005-b0m2hl')?.status, 'pending',
378
+ 'unique-id-deps-blocked: M005-b0m2hl is pending (dep not met)');
379
+ } finally {
380
+ cleanup(base);
381
+ }
382
+ }
383
+
384
+ // ─── Test Group 9: parseContextDependsOn preserves case ───────────────
385
+ // Direct unit test: verify the parsed dep ID matches the input exactly
386
+ console.log('\n=== parseContextDependsOn: preserves case of unique IDs ===');
387
+ {
388
+ const { parseContextDependsOn } = await import('../files.ts');
389
+
390
+ const deps1 = parseContextDependsOn('---\ndepends_on: [M004-0zjrg0]\n---\n');
391
+ assertEq(deps1[0], 'M004-0zjrg0',
392
+ 'parseContextDependsOn preserves lowercase hex suffix');
393
+
394
+ const deps2 = parseContextDependsOn('---\ndepends_on: [M001, M004-abc123]\n---\n');
395
+ assertEq(deps2[0], 'M001', 'preserves classic uppercase ID');
396
+ assertEq(deps2[1], 'M004-abc123', 'preserves mixed-case unique ID');
397
+
398
+ const deps3 = parseContextDependsOn('---\ndepends_on: []\n---\n');
399
+ assertEq(deps3.length, 0, 'empty deps returns empty array');
400
+
401
+ const deps4 = parseContextDependsOn(null);
402
+ assertEq(deps4.length, 0, 'null content returns empty array');
403
+ }
404
+
306
405
  report();
307
406
  }
308
407
 
@@ -0,0 +1,79 @@
1
+ /**
2
+ * In-flight tool tracking tests — verifies that markToolStart/markToolEnd
3
+ * correctly manage the in-flight tools set used by the idle watchdog to
4
+ * distinguish "agent waiting on long-running tool" from "agent is idle".
5
+ *
6
+ * Background: The idle watchdog checks every 15s for agent progress. Without
7
+ * in-flight tool tracking, agents waiting on await_job or async_bash (which
8
+ * can run 20+ minutes for evaluations, deployments, test suites) are falsely
9
+ * declared idle and interrupted by recovery steering messages.
10
+ *
11
+ * The fix hooks tool_execution_start/end events to track active tool calls.
12
+ * When tools are in-flight, the watchdog resets lastProgressAt instead of
13
+ * triggering idle recovery.
14
+ */
15
+
16
+ import { markToolStart, markToolEnd, isAutoActive } from "../auto.ts";
17
+ import { createTestContext } from './test-helpers.ts';
18
+
19
+ const { assertEq, assertTrue, report } = createTestContext();
20
+
21
+ // ═══ markToolStart / markToolEnd basic behavior ═════════════════════════════
22
+
23
+ {
24
+ console.log("\n=== markToolStart: no-op when auto-mode is not active ===");
25
+ // When auto-mode is not active, markToolStart should silently ignore
26
+ // (the guard `if (!active) return` prevents set pollution outside auto-mode)
27
+ assertTrue(!isAutoActive(), "auto-mode should not be active in tests");
28
+ markToolStart("tool-1");
29
+ // We can't directly inspect the set, but markToolEnd should be a safe no-op
30
+ markToolEnd("tool-1");
31
+ // If we got here without error, the guard works
32
+ assertTrue(true, "markToolStart/markToolEnd are safe no-ops when inactive");
33
+ }
34
+
35
+ {
36
+ console.log("\n=== markToolEnd: no-op for unknown toolCallId ===");
37
+ // Set.delete on non-existent key is a no-op — verify no crash
38
+ markToolEnd("nonexistent-tool-call-id");
39
+ assertTrue(true, "markToolEnd handles unknown IDs gracefully");
40
+ }
41
+
42
+ {
43
+ console.log("\n=== markToolEnd: idempotent — double-end does not crash ===");
44
+ markToolEnd("some-id");
45
+ markToolEnd("some-id");
46
+ assertTrue(true, "double markToolEnd is safe");
47
+ }
48
+
49
+ // ═══ Integration contract: expected exports from auto.ts ═════════════════════
50
+
51
+ {
52
+ console.log("\n=== auto.ts exports markToolStart and markToolEnd ===");
53
+ assertEq(typeof markToolStart, "function", "markToolStart should be a function");
54
+ assertEq(typeof markToolEnd, "function", "markToolEnd should be a function");
55
+ }
56
+
57
+ {
58
+ console.log("\n=== markToolStart accepts string toolCallId ===");
59
+ // Verify the function signature handles string input without error
60
+ // (when inactive, this is a no-op but should not throw)
61
+ try {
62
+ markToolStart("toolu_01ABC123");
63
+ assertTrue(true, "accepts standard Claude tool call ID format");
64
+ } catch (e) {
65
+ assertTrue(false, `should not throw: ${e}`);
66
+ }
67
+ }
68
+
69
+ {
70
+ console.log("\n=== markToolEnd accepts string toolCallId ===");
71
+ try {
72
+ markToolEnd("toolu_01ABC123");
73
+ assertTrue(true, "accepts standard Claude tool call ID format");
74
+ } catch (e) {
75
+ assertTrue(false, `should not throw: ${e}`);
76
+ }
77
+ }
78
+
79
+ report();