gsd-pi 2.37.0 → 2.37.1-dev.3bbb0a9
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 +21 -20
- package/dist/onboarding.js +1 -0
- package/dist/resources/extensions/cmux/package.json +7 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
- package/dist/resources/extensions/gsd/auto-loop.js +18 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto.js +42 -5
- package/dist/resources/extensions/gsd/commands.js +80 -33
- package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
- package/dist/resources/extensions/gsd/files.js +41 -0
- package/dist/resources/extensions/gsd/git-service.js +9 -1
- package/dist/resources/extensions/gsd/history.js +2 -1
- package/dist/resources/extensions/gsd/metrics.js +4 -2
- package/dist/resources/extensions/gsd/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/session-lock.js +26 -6
- package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/dist/resources/extensions/shared/format-utils.js +5 -41
- package/dist/resources/extensions/shared/layout-utils.js +46 -0
- package/dist/resources/extensions/shared/mod.js +2 -1
- package/package.json +2 -1
- package/packages/pi-ai/dist/env-api-keys.js +13 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +172 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +172 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +47 -764
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
- package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +14 -0
- package/packages/pi-ai/src/models.generated.ts +172 -0
- package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
- package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
- package/packages/pi-ai/src/providers/anthropic.ts +76 -868
- package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
- package/packages/pi-ai/src/types.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/package.json +7 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
- package/src/resources/extensions/gsd/auto-loop.ts +24 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto.ts +56 -5
- package/src/resources/extensions/gsd/commands.ts +85 -31
- package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
- package/src/resources/extensions/gsd/files.ts +45 -0
- package/src/resources/extensions/gsd/git-service.ts +12 -1
- package/src/resources/extensions/gsd/history.ts +2 -1
- package/src/resources/extensions/gsd/metrics.ts +4 -2
- package/src/resources/extensions/gsd/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/session-lock.ts +41 -6
- package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +37 -1
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
- package/src/resources/extensions/gsd/types.ts +43 -0
- package/src/resources/extensions/shared/format-utils.ts +5 -44
- package/src/resources/extensions/shared/layout-utils.ts +49 -0
- package/src/resources/extensions/shared/mod.ts +7 -4
- package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
deriveTaskGraph,
|
|
5
|
+
getReadyTasks,
|
|
6
|
+
chooseNonConflictingSubset,
|
|
7
|
+
isGraphAmbiguous,
|
|
8
|
+
detectDeadlock,
|
|
9
|
+
graphMetrics,
|
|
10
|
+
} from "../reactive-graph.ts";
|
|
11
|
+
import { parseTaskPlanIO } from "../files.ts";
|
|
12
|
+
import type { TaskIO, DerivedTaskNode } from "../types.ts";
|
|
13
|
+
|
|
14
|
+
// ─── parseTaskPlanIO ──────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
test("parseTaskPlanIO extracts backtick-wrapped file paths from Inputs and Expected Output", () => {
|
|
17
|
+
const content = `---
|
|
18
|
+
estimated_steps: 3
|
|
19
|
+
estimated_files: 2
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# T01: Setup Models
|
|
23
|
+
|
|
24
|
+
**Slice:** S01 — Core Setup
|
|
25
|
+
**Milestone:** M001
|
|
26
|
+
|
|
27
|
+
## Description
|
|
28
|
+
|
|
29
|
+
Create the core data models.
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
1. Create types file
|
|
34
|
+
2. Create models file
|
|
35
|
+
|
|
36
|
+
## Must-Haves
|
|
37
|
+
|
|
38
|
+
- [ ] Type definitions complete
|
|
39
|
+
|
|
40
|
+
## Verification
|
|
41
|
+
|
|
42
|
+
- Run type checker
|
|
43
|
+
|
|
44
|
+
## Inputs
|
|
45
|
+
|
|
46
|
+
- \`src/types.ts\` — Existing type definitions from prior work
|
|
47
|
+
- \`src/config.json\` — Configuration schema
|
|
48
|
+
|
|
49
|
+
## Expected Output
|
|
50
|
+
|
|
51
|
+
- \`src/models.ts\` — New data model definitions
|
|
52
|
+
- \`src/models.test.ts\` — Unit tests for models
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const io = parseTaskPlanIO(content);
|
|
56
|
+
assert.deepEqual(io.inputFiles, ["src/types.ts", "src/config.json"]);
|
|
57
|
+
assert.deepEqual(io.outputFiles, ["src/models.ts", "src/models.test.ts"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("parseTaskPlanIO returns empty arrays for missing sections", () => {
|
|
61
|
+
const content = `# T01: Something\n\n## Description\n\nNo IO sections here.\n`;
|
|
62
|
+
const io = parseTaskPlanIO(content);
|
|
63
|
+
assert.deepEqual(io.inputFiles, []);
|
|
64
|
+
assert.deepEqual(io.outputFiles, []);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("parseTaskPlanIO ignores non-file-path backtick tokens", () => {
|
|
68
|
+
const content = `# T01: Test
|
|
69
|
+
|
|
70
|
+
## Inputs
|
|
71
|
+
|
|
72
|
+
- \`true\` — a boolean flag
|
|
73
|
+
- \`src/index.ts\` — main entry
|
|
74
|
+
- \`npm run test\` — a command, not a file
|
|
75
|
+
|
|
76
|
+
## Expected Output
|
|
77
|
+
|
|
78
|
+
- \`dist/bundle.js\` — compiled output
|
|
79
|
+
- \`false\` — not a file
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
const io = parseTaskPlanIO(content);
|
|
83
|
+
assert.deepEqual(io.inputFiles, ["src/index.ts"]);
|
|
84
|
+
assert.deepEqual(io.outputFiles, ["dist/bundle.js"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("parseTaskPlanIO handles multiple backtick tokens on one line", () => {
|
|
88
|
+
const content = `# T01: Multi
|
|
89
|
+
|
|
90
|
+
## Inputs
|
|
91
|
+
|
|
92
|
+
- \`src/a.ts\` and \`src/b.ts\` — both needed
|
|
93
|
+
|
|
94
|
+
## Expected Output
|
|
95
|
+
|
|
96
|
+
- \`src/c.ts\` — output
|
|
97
|
+
`;
|
|
98
|
+
const io = parseTaskPlanIO(content);
|
|
99
|
+
assert.deepEqual(io.inputFiles, ["src/a.ts", "src/b.ts"]);
|
|
100
|
+
assert.deepEqual(io.outputFiles, ["src/c.ts"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── deriveTaskGraph ──────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
test("deriveTaskGraph: linear chain T01→T02→T03", () => {
|
|
106
|
+
const tasks: TaskIO[] = [
|
|
107
|
+
{ id: "T01", title: "First", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
108
|
+
{ id: "T02", title: "Second", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
109
|
+
{ id: "T03", title: "Third", inputFiles: ["src/b.ts"], outputFiles: ["src/c.ts"], done: false },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const graph = deriveTaskGraph(tasks);
|
|
113
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
114
|
+
assert.deepEqual(graph[1].dependsOn, ["T01"]);
|
|
115
|
+
assert.deepEqual(graph[2].dependsOn, ["T02"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("deriveTaskGraph: diamond dependency", () => {
|
|
119
|
+
const tasks: TaskIO[] = [
|
|
120
|
+
{ id: "T01", title: "Base", inputFiles: [], outputFiles: ["src/base.ts"], done: false },
|
|
121
|
+
{ id: "T02", title: "Left", inputFiles: ["src/base.ts"], outputFiles: ["src/left.ts"], done: false },
|
|
122
|
+
{ id: "T03", title: "Right", inputFiles: ["src/base.ts"], outputFiles: ["src/right.ts"], done: false },
|
|
123
|
+
{ id: "T04", title: "Merge", inputFiles: ["src/left.ts", "src/right.ts"], outputFiles: ["src/final.ts"], done: false },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const graph = deriveTaskGraph(tasks);
|
|
127
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
128
|
+
assert.deepEqual(graph[1].dependsOn, ["T01"]);
|
|
129
|
+
assert.deepEqual(graph[2].dependsOn, ["T01"]);
|
|
130
|
+
assert.deepEqual(graph[3].dependsOn, ["T02", "T03"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("deriveTaskGraph: fully independent tasks", () => {
|
|
134
|
+
const tasks: TaskIO[] = [
|
|
135
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
136
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
137
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const graph = deriveTaskGraph(tasks);
|
|
141
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
142
|
+
assert.deepEqual(graph[1].dependsOn, []);
|
|
143
|
+
assert.deepEqual(graph[2].dependsOn, []);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("deriveTaskGraph: self-referencing output→input is excluded", () => {
|
|
147
|
+
const tasks: TaskIO[] = [
|
|
148
|
+
{ id: "T01", title: "Self", inputFiles: ["src/a.ts"], outputFiles: ["src/a.ts"], done: false },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const graph = deriveTaskGraph(tasks);
|
|
152
|
+
assert.deepEqual(graph[0].dependsOn, []);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── getReadyTasks ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
test("getReadyTasks: partially completed graph", () => {
|
|
158
|
+
const tasks: TaskIO[] = [
|
|
159
|
+
{ id: "T01", title: "Base", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
|
|
160
|
+
{ id: "T02", title: "Dep", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
161
|
+
{ id: "T03", title: "Blocked", inputFiles: ["src/b.ts"], outputFiles: ["src/c.ts"], done: false },
|
|
162
|
+
];
|
|
163
|
+
const graph = deriveTaskGraph(tasks);
|
|
164
|
+
const ready = getReadyTasks(graph, new Set(["T01"]), new Set());
|
|
165
|
+
assert.deepEqual(ready, ["T02"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("getReadyTasks: nothing complete → only root tasks ready", () => {
|
|
169
|
+
const tasks: TaskIO[] = [
|
|
170
|
+
{ id: "T01", title: "Root", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
171
|
+
{ id: "T02", title: "Dep", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
172
|
+
];
|
|
173
|
+
const graph = deriveTaskGraph(tasks);
|
|
174
|
+
const ready = getReadyTasks(graph, new Set(), new Set());
|
|
175
|
+
assert.deepEqual(ready, ["T01"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("getReadyTasks: all complete → empty", () => {
|
|
179
|
+
const tasks: TaskIO[] = [
|
|
180
|
+
{ id: "T01", title: "Done", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
|
|
181
|
+
];
|
|
182
|
+
const graph = deriveTaskGraph(tasks);
|
|
183
|
+
const ready = getReadyTasks(graph, new Set(["T01"]), new Set());
|
|
184
|
+
assert.deepEqual(ready, []);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("getReadyTasks: in-flight tasks excluded", () => {
|
|
188
|
+
const tasks: TaskIO[] = [
|
|
189
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
190
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
191
|
+
];
|
|
192
|
+
const graph = deriveTaskGraph(tasks);
|
|
193
|
+
const ready = getReadyTasks(graph, new Set(), new Set(["T01"]));
|
|
194
|
+
assert.deepEqual(ready, ["T02"]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ─── chooseNonConflictingSubset ───────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
test("chooseNonConflictingSubset: output conflicts", () => {
|
|
200
|
+
const tasks: TaskIO[] = [
|
|
201
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/shared.ts"], done: false },
|
|
202
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/shared.ts"], done: false },
|
|
203
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/other.ts"], done: false },
|
|
204
|
+
];
|
|
205
|
+
const graph = deriveTaskGraph(tasks);
|
|
206
|
+
const selected = chooseNonConflictingSubset(["T01", "T02", "T03"], graph, 3, new Set());
|
|
207
|
+
// T01 claims shared.ts, T02 conflicts, T03 is fine
|
|
208
|
+
assert.deepEqual(selected, ["T01", "T03"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("chooseNonConflictingSubset: respects maxParallel", () => {
|
|
212
|
+
const tasks: TaskIO[] = [
|
|
213
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
214
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
215
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
|
|
216
|
+
];
|
|
217
|
+
const graph = deriveTaskGraph(tasks);
|
|
218
|
+
const selected = chooseNonConflictingSubset(["T01", "T02", "T03"], graph, 2, new Set());
|
|
219
|
+
assert.deepEqual(selected, ["T01", "T02"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("chooseNonConflictingSubset: respects inFlightOutputs", () => {
|
|
223
|
+
const tasks: TaskIO[] = [
|
|
224
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false },
|
|
225
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false },
|
|
226
|
+
];
|
|
227
|
+
const graph = deriveTaskGraph(tasks);
|
|
228
|
+
const selected = chooseNonConflictingSubset(["T01", "T02"], graph, 4, new Set(["src/a.ts"]));
|
|
229
|
+
assert.deepEqual(selected, ["T02"]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ─── isGraphAmbiguous ─────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
test("isGraphAmbiguous: task with no IO → ambiguous", () => {
|
|
235
|
+
const graph: DerivedTaskNode[] = [
|
|
236
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
|
|
237
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
|
|
238
|
+
];
|
|
239
|
+
assert.equal(isGraphAmbiguous(graph), true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("isGraphAmbiguous: all tasks have IO → not ambiguous", () => {
|
|
243
|
+
const graph: DerivedTaskNode[] = [
|
|
244
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false, dependsOn: [] },
|
|
245
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
|
|
246
|
+
];
|
|
247
|
+
assert.equal(isGraphAmbiguous(graph), false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("isGraphAmbiguous: done tasks with no IO are ignored", () => {
|
|
251
|
+
const graph: DerivedTaskNode[] = [
|
|
252
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: true, dependsOn: [] },
|
|
253
|
+
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
|
|
254
|
+
];
|
|
255
|
+
assert.equal(isGraphAmbiguous(graph), false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── detectDeadlock ───────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
test("detectDeadlock: circular dependency detected", () => {
|
|
261
|
+
// T01 depends on T02, T02 depends on T01 — deadlock
|
|
262
|
+
const graph: DerivedTaskNode[] = [
|
|
263
|
+
{ id: "T01", title: "A", inputFiles: ["src/b.ts"], outputFiles: ["src/a.ts"], done: false, dependsOn: ["T02"] },
|
|
264
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
|
|
265
|
+
];
|
|
266
|
+
assert.equal(detectDeadlock(graph, new Set(), new Set()), true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("detectDeadlock: normal blocked-waiting-for-in-flight → not deadlock", () => {
|
|
270
|
+
const graph: DerivedTaskNode[] = [
|
|
271
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: false, dependsOn: [] },
|
|
272
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: ["T01"] },
|
|
273
|
+
];
|
|
274
|
+
// T01 is in-flight, T02 is waiting → not deadlock
|
|
275
|
+
assert.equal(detectDeadlock(graph, new Set(), new Set(["T01"])), false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("detectDeadlock: all complete → not deadlock", () => {
|
|
279
|
+
const graph: DerivedTaskNode[] = [
|
|
280
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: true, dependsOn: [] },
|
|
281
|
+
];
|
|
282
|
+
assert.equal(detectDeadlock(graph, new Set(["T01"]), new Set()), false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ─── graphMetrics ─────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
test("graphMetrics computes correct values", () => {
|
|
288
|
+
const tasks: TaskIO[] = [
|
|
289
|
+
{ id: "T01", title: "A", inputFiles: [], outputFiles: ["src/a.ts"], done: true },
|
|
290
|
+
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false },
|
|
291
|
+
{ id: "T03", title: "C", inputFiles: [], outputFiles: ["src/c.ts"], done: false },
|
|
292
|
+
];
|
|
293
|
+
const graph = deriveTaskGraph(tasks);
|
|
294
|
+
const metrics = graphMetrics(graph);
|
|
295
|
+
assert.equal(metrics.taskCount, 3);
|
|
296
|
+
assert.equal(metrics.edgeCount, 1); // T02 depends on T01
|
|
297
|
+
assert.equal(metrics.readySetSize, 2); // T02 (T01 done) and T03 (no deps)
|
|
298
|
+
assert.equal(metrics.ambiguous, false);
|
|
299
|
+
});
|
|
@@ -17,6 +17,7 @@ import { tmpdir } from 'node:os';
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
acquireSessionLock,
|
|
20
|
+
getSessionLockStatus,
|
|
20
21
|
validateSessionLock,
|
|
21
22
|
releaseSessionLock,
|
|
22
23
|
readSessionLockData,
|
|
@@ -201,6 +202,50 @@ async function main(): Promise<void> {
|
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
// ─── 7b. getSessionLockStatus with missing metadata → reason surfaced ──
|
|
206
|
+
console.log('\n=== 7b. missing lock metadata → structured reason ===');
|
|
207
|
+
{
|
|
208
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
|
|
209
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const status = getSessionLockStatus(base);
|
|
213
|
+
assertEq(status.valid, false, 'missing lock metadata is invalid');
|
|
214
|
+
assertEq(status.failureReason, 'missing-metadata', 'missing metadata reason is surfaced');
|
|
215
|
+
assertEq(status.expectedPid, process.pid, 'expected PID is included');
|
|
216
|
+
} finally {
|
|
217
|
+
rmSync(base, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── 7c. getSessionLockStatus with foreign PID → reason surfaced ───────
|
|
222
|
+
console.log('\n=== 7c. foreign PID in lock file → structured reason ===');
|
|
223
|
+
{
|
|
224
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
|
|
225
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const foreignPid = process.pid + 1000;
|
|
229
|
+
const lockFile = join(gsdRoot(base), 'auto.lock');
|
|
230
|
+
writeFileSync(lockFile, JSON.stringify({
|
|
231
|
+
pid: foreignPid,
|
|
232
|
+
startedAt: new Date().toISOString(),
|
|
233
|
+
unitType: 'execute-task',
|
|
234
|
+
unitId: 'M001/S01/T01',
|
|
235
|
+
unitStartedAt: new Date().toISOString(),
|
|
236
|
+
completedUnits: 0,
|
|
237
|
+
}, null, 2));
|
|
238
|
+
|
|
239
|
+
const status = getSessionLockStatus(base);
|
|
240
|
+
assertEq(status.valid, false, 'foreign PID lock is invalid');
|
|
241
|
+
assertEq(status.failureReason, 'pid-mismatch', 'PID mismatch reason is surfaced');
|
|
242
|
+
assertEq(status.existingPid, foreignPid, 'existing PID is included');
|
|
243
|
+
assertEq(status.expectedPid, process.pid, 'expected PID is included');
|
|
244
|
+
} finally {
|
|
245
|
+
rmSync(base, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
204
249
|
// ─── 8. Acquire after release is possible ─────────────────────────────
|
|
205
250
|
console.log('\n=== 8. acquire after release → re-acquirable ===');
|
|
206
251
|
{
|
|
@@ -436,3 +436,46 @@ export interface ParallelConfig {
|
|
|
436
436
|
merge_strategy: MergeStrategy;
|
|
437
437
|
auto_merge: AutoMergeMode;
|
|
438
438
|
}
|
|
439
|
+
|
|
440
|
+
// ─── Reactive Task Execution Types ───────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/** IO signature extracted from a single task plan's Inputs/Expected Output sections. */
|
|
443
|
+
export interface TaskIO {
|
|
444
|
+
id: string; // e.g. "T01"
|
|
445
|
+
title: string;
|
|
446
|
+
inputFiles: string[];
|
|
447
|
+
outputFiles: string[];
|
|
448
|
+
done: boolean;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** A task node with derived dependency edges from input/output intersection. */
|
|
452
|
+
export interface DerivedTaskNode extends TaskIO {
|
|
453
|
+
/** IDs of tasks whose outputFiles overlap with this task's inputFiles. */
|
|
454
|
+
dependsOn: string[];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Configuration for reactive (graph-derived parallel) task execution within a slice. */
|
|
458
|
+
export interface ReactiveExecutionConfig {
|
|
459
|
+
enabled: boolean;
|
|
460
|
+
/** Maximum number of tasks to dispatch in parallel. Clamped to 1–8. */
|
|
461
|
+
max_parallel: number;
|
|
462
|
+
/** Isolation mode for parallel tasks within a slice. Currently only "same-tree" is supported. */
|
|
463
|
+
isolation_mode: "same-tree";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Per-slice reactive execution runtime state, persisted to disk. */
|
|
467
|
+
export interface ReactiveExecutionState {
|
|
468
|
+
sliceId: string;
|
|
469
|
+
/** Task IDs that have been verified as completed. */
|
|
470
|
+
completed: string[];
|
|
471
|
+
/** Task IDs dispatched in the current/most recent reactive batch. */
|
|
472
|
+
dispatched: string[];
|
|
473
|
+
/** Snapshot of the graph at last dispatch. */
|
|
474
|
+
graphSnapshot: {
|
|
475
|
+
taskCount: number;
|
|
476
|
+
edgeCount: number;
|
|
477
|
+
readySetSize: number;
|
|
478
|
+
ambiguous: boolean;
|
|
479
|
+
};
|
|
480
|
+
updatedAt: string;
|
|
481
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared formatting
|
|
2
|
+
* Shared pure formatting utilities — no @gsd/pi-tui dependency.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns)
|
|
5
|
+
* live in layout-utils.ts to avoid pulling @gsd/pi-tui into modules that
|
|
6
|
+
* run outside jiti's alias resolution (e.g. HTML report generation via
|
|
7
|
+
* dynamic import in auto-loop).
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
9
|
-
|
|
10
10
|
// ─── Duration Formatting ──────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
/** Format a millisecond duration as a compact human-readable string. */
|
|
@@ -31,45 +31,6 @@ export function formatTokenCount(count: number): string {
|
|
|
31
31
|
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// ─── Layout Helpers ───────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
|
|
37
|
-
export function padRight(content: string, width: number): string {
|
|
38
|
-
const vis = visibleWidth(content);
|
|
39
|
-
return content + " ".repeat(Math.max(0, width - vis));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Build a line with left-aligned and right-aligned content. */
|
|
43
|
-
export function joinColumns(left: string, right: string, width: number): string {
|
|
44
|
-
const leftW = visibleWidth(left);
|
|
45
|
-
const rightW = visibleWidth(right);
|
|
46
|
-
if (leftW + rightW + 2 > width) {
|
|
47
|
-
return truncateToWidth(`${left} ${right}`, width);
|
|
48
|
-
}
|
|
49
|
-
return left + " ".repeat(width - leftW - rightW) + right;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Center content within `width` (ANSI-aware). */
|
|
53
|
-
export function centerLine(content: string, width: number): string {
|
|
54
|
-
const vis = visibleWidth(content);
|
|
55
|
-
if (vis >= width) return truncateToWidth(content, width);
|
|
56
|
-
const leftPad = Math.floor((width - vis) / 2);
|
|
57
|
-
return " ".repeat(leftPad) + content;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Join as many parts as fit within `width`, separated by `separator`. */
|
|
61
|
-
export function fitColumns(parts: string[], width: number, separator = " "): string {
|
|
62
|
-
const filtered = parts.filter(Boolean);
|
|
63
|
-
if (filtered.length === 0) return "";
|
|
64
|
-
let result = filtered[0];
|
|
65
|
-
for (let i = 1; i < filtered.length; i++) {
|
|
66
|
-
const candidate = `${result}${separator}${filtered[i]}`;
|
|
67
|
-
if (visibleWidth(candidate) > width) break;
|
|
68
|
-
result = candidate;
|
|
69
|
-
}
|
|
70
|
-
return truncateToWidth(result, width);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
34
|
// ─── Text Truncation ─────────────────────────────────────────────────────────
|
|
74
35
|
|
|
75
36
|
/** Truncate a string to `maxLength` characters, replacing the last character with an ellipsis if needed. */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI-aware TUI layout utilities that depend on @gsd/pi-tui.
|
|
3
|
+
*
|
|
4
|
+
* Separated from format-utils.ts so that modules needing only pure
|
|
5
|
+
* formatting (e.g. HTML report generation) can import format-utils
|
|
6
|
+
* without pulling in the @gsd/pi-tui dependency — which fails when
|
|
7
|
+
* loaded outside jiti's alias resolution context.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
11
|
+
|
|
12
|
+
// ─── Layout Helpers ───────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */
|
|
15
|
+
export function padRight(content: string, width: number): string {
|
|
16
|
+
const vis = visibleWidth(content);
|
|
17
|
+
return content + " ".repeat(Math.max(0, width - vis));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Build a line with left-aligned and right-aligned content. */
|
|
21
|
+
export function joinColumns(left: string, right: string, width: number): string {
|
|
22
|
+
const leftW = visibleWidth(left);
|
|
23
|
+
const rightW = visibleWidth(right);
|
|
24
|
+
if (leftW + rightW + 2 > width) {
|
|
25
|
+
return truncateToWidth(`${left} ${right}`, width);
|
|
26
|
+
}
|
|
27
|
+
return left + " ".repeat(width - leftW - rightW) + right;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Center content within `width` (ANSI-aware). */
|
|
31
|
+
export function centerLine(content: string, width: number): string {
|
|
32
|
+
const vis = visibleWidth(content);
|
|
33
|
+
if (vis >= width) return truncateToWidth(content, width);
|
|
34
|
+
const leftPad = Math.floor((width - vis) / 2);
|
|
35
|
+
return " ".repeat(leftPad) + content;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Join as many parts as fit within `width`, separated by `separator`. */
|
|
39
|
+
export function fitColumns(parts: string[], width: number, separator = " "): string {
|
|
40
|
+
const filtered = parts.filter(Boolean);
|
|
41
|
+
if (filtered.length === 0) return "";
|
|
42
|
+
let result = filtered[0];
|
|
43
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
44
|
+
const candidate = `${result}${separator}${filtered[i]}`;
|
|
45
|
+
if (visibleWidth(candidate) > width) break;
|
|
46
|
+
result = candidate;
|
|
47
|
+
}
|
|
48
|
+
return truncateToWidth(result, width);
|
|
49
|
+
}
|
|
@@ -13,15 +13,18 @@ export {
|
|
|
13
13
|
stripAnsi,
|
|
14
14
|
formatTokenCount,
|
|
15
15
|
formatDuration,
|
|
16
|
-
padRight,
|
|
17
|
-
joinColumns,
|
|
18
|
-
centerLine,
|
|
19
|
-
fitColumns,
|
|
20
16
|
sparkline,
|
|
21
17
|
normalizeStringArray,
|
|
22
18
|
fileLink,
|
|
23
19
|
} from "./format-utils.js";
|
|
24
20
|
|
|
21
|
+
export {
|
|
22
|
+
padRight,
|
|
23
|
+
joinColumns,
|
|
24
|
+
centerLine,
|
|
25
|
+
fitColumns,
|
|
26
|
+
} from "./layout-utils.js";
|
|
27
|
+
|
|
25
28
|
export { shortcutDesc } from "./terminal.js";
|
|
26
29
|
export { toPosixPath } from "./path-display.js";
|
|
27
30
|
export { showInterviewRound } from "./interview-ui.js";
|
|
@@ -2,13 +2,15 @@ import { describe, it } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import {
|
|
4
4
|
formatDuration,
|
|
5
|
+
sparkline,
|
|
6
|
+
stripAnsi,
|
|
7
|
+
} from "../format-utils.js";
|
|
8
|
+
import {
|
|
5
9
|
padRight,
|
|
6
10
|
joinColumns,
|
|
7
11
|
centerLine,
|
|
8
12
|
fitColumns,
|
|
9
|
-
|
|
10
|
-
stripAnsi,
|
|
11
|
-
} from "../format-utils.js";
|
|
13
|
+
} from "../layout-utils.js";
|
|
12
14
|
|
|
13
15
|
describe("formatDuration", () => {
|
|
14
16
|
it("formats seconds", () => {
|