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.
- package/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-prompts.ts +20 -1
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +123 -10
- package/dist/resources/extensions/gsd/commands.ts +245 -22
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +59 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +123 -10
- package/src/resources/extensions/gsd/commands.ts +245 -22
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +59 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- 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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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();
|