gsd-pi 2.37.1 → 2.38.0-dev.63ad7e5
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 +1 -1
- package/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/onboarding.js +1 -0
- package/dist/remote-questions-config.js +2 -2
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
- package/dist/resources/extensions/gsd/auto-loop.js +68 -97
- package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
- package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +22 -2
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
- package/dist/resources/extensions/gsd/doctor.js +184 -11
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +43 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/index.js +2 -1
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +2 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +43 -11
- package/dist/resources/extensions/gsd/preferences.js +5 -5
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- 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/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/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
- package/src/resources/extensions/gsd/auto-loop.ts +88 -133
- package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
- package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +24 -2
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +177 -13
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +47 -2
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/index.ts +3 -1
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +5 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +42 -11
- package/src/resources/extensions/gsd/preferences.ts +5 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -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/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +43 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive Task Graph — derives dependency edges from task plan IO signatures.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that build a DAG from task IO intersections and resolve
|
|
5
|
+
* which tasks are currently ready for parallel dispatch. Used by the
|
|
6
|
+
* reactive-execute dispatch path (ADR-004).
|
|
7
|
+
*
|
|
8
|
+
* Graph derivation and resolution functions are pure (no filesystem access).
|
|
9
|
+
* The `loadSliceTaskIO` loader at the bottom is the only async/IO function.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TaskIO, DerivedTaskNode, ReactiveExecutionState } from "./types.js";
|
|
13
|
+
import { loadFile, parsePlan, parseTaskPlanIO } from "./files.js";
|
|
14
|
+
import { resolveTasksDir, resolveTaskFiles } from "./paths.js";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
17
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
// ─── Graph Construction ───────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build a dependency graph from task IO signatures.
|
|
23
|
+
*
|
|
24
|
+
* A task T_b depends on T_a when any of T_b's inputFiles appear in T_a's
|
|
25
|
+
* outputFiles. Self-references are excluded.
|
|
26
|
+
*
|
|
27
|
+
* Tasks are returned in the same order as the input array.
|
|
28
|
+
*/
|
|
29
|
+
export function deriveTaskGraph(tasks: TaskIO[]): DerivedTaskNode[] {
|
|
30
|
+
// Build output → producer lookup
|
|
31
|
+
const outputToProducer = new Map<string, string[]>();
|
|
32
|
+
for (const task of tasks) {
|
|
33
|
+
for (const outFile of task.outputFiles) {
|
|
34
|
+
const existing = outputToProducer.get(outFile);
|
|
35
|
+
if (existing) {
|
|
36
|
+
existing.push(task.id);
|
|
37
|
+
} else {
|
|
38
|
+
outputToProducer.set(outFile, [task.id]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return tasks.map((task) => {
|
|
44
|
+
const deps = new Set<string>();
|
|
45
|
+
for (const inFile of task.inputFiles) {
|
|
46
|
+
const producers = outputToProducer.get(inFile);
|
|
47
|
+
if (producers) {
|
|
48
|
+
for (const pid of producers) {
|
|
49
|
+
if (pid !== task.id) deps.add(pid);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...task,
|
|
55
|
+
dependsOn: [...deps].sort(),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Ready Set Resolution ─────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return task IDs whose dependencies are all in `completed`.
|
|
64
|
+
* Excludes tasks that are already done or in-flight.
|
|
65
|
+
*/
|
|
66
|
+
export function getReadyTasks(
|
|
67
|
+
graph: DerivedTaskNode[],
|
|
68
|
+
completed: Set<string>,
|
|
69
|
+
inFlight: Set<string>,
|
|
70
|
+
): string[] {
|
|
71
|
+
return graph
|
|
72
|
+
.filter((node) => {
|
|
73
|
+
if (node.done || completed.has(node.id) || inFlight.has(node.id)) return false;
|
|
74
|
+
return node.dependsOn.every((dep) => completed.has(dep));
|
|
75
|
+
})
|
|
76
|
+
.map((node) => node.id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Conflict-Free Subset Selection ──────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Greedy selection of non-conflicting tasks up to `maxParallel`.
|
|
83
|
+
*
|
|
84
|
+
* Two tasks conflict if they share any outputFile. We also exclude tasks
|
|
85
|
+
* whose outputs overlap with `inFlightOutputs` (files being written by
|
|
86
|
+
* tasks currently in progress).
|
|
87
|
+
*/
|
|
88
|
+
export function chooseNonConflictingSubset(
|
|
89
|
+
readyIds: string[],
|
|
90
|
+
graph: DerivedTaskNode[],
|
|
91
|
+
maxParallel: number,
|
|
92
|
+
inFlightOutputs: Set<string>,
|
|
93
|
+
): string[] {
|
|
94
|
+
const nodeMap = new Map(graph.map((n) => [n.id, n]));
|
|
95
|
+
const claimed = new Set(inFlightOutputs);
|
|
96
|
+
const selected: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (const id of readyIds) {
|
|
99
|
+
if (selected.length >= maxParallel) break;
|
|
100
|
+
const node = nodeMap.get(id);
|
|
101
|
+
if (!node) continue;
|
|
102
|
+
|
|
103
|
+
// Check for output overlap with already-selected or in-flight
|
|
104
|
+
const conflicts = node.outputFiles.some((f) => claimed.has(f));
|
|
105
|
+
if (conflicts) continue;
|
|
106
|
+
|
|
107
|
+
// Claim this task's outputs
|
|
108
|
+
for (const f of node.outputFiles) claimed.add(f);
|
|
109
|
+
selected.push(id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return selected;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Graph Quality Checks ─────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns true if any incomplete task has 0 inputFiles AND 0 outputFiles.
|
|
119
|
+
*
|
|
120
|
+
* An ambiguous graph means IO annotations are too sparse to derive reliable
|
|
121
|
+
* edges — the dispatcher should fall back to sequential execution.
|
|
122
|
+
*/
|
|
123
|
+
export function isGraphAmbiguous(graph: DerivedTaskNode[]): boolean {
|
|
124
|
+
return graph.some(
|
|
125
|
+
(node) =>
|
|
126
|
+
!node.done &&
|
|
127
|
+
node.inputFiles.length === 0 &&
|
|
128
|
+
node.outputFiles.length === 0,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
|
|
134
|
+
* tasks remain. This indicates a circular dependency or impossible state.
|
|
135
|
+
*/
|
|
136
|
+
export function detectDeadlock(
|
|
137
|
+
graph: DerivedTaskNode[],
|
|
138
|
+
completed: Set<string>,
|
|
139
|
+
inFlight: Set<string>,
|
|
140
|
+
): boolean {
|
|
141
|
+
const incomplete = graph.filter(
|
|
142
|
+
(n) => !n.done && !completed.has(n.id) && !inFlight.has(n.id),
|
|
143
|
+
);
|
|
144
|
+
if (incomplete.length === 0) return false; // all done
|
|
145
|
+
if (inFlight.size > 0) return false; // something is running, wait for it
|
|
146
|
+
|
|
147
|
+
// Nothing in flight, but incomplete tasks remain — check if any are ready
|
|
148
|
+
const ready = getReadyTasks(graph, completed, inFlight);
|
|
149
|
+
return ready.length === 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Graph Metrics ────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/** Compute summary metrics for logging. */
|
|
155
|
+
export function graphMetrics(graph: DerivedTaskNode[]): {
|
|
156
|
+
taskCount: number;
|
|
157
|
+
edgeCount: number;
|
|
158
|
+
readySetSize: number;
|
|
159
|
+
ambiguous: boolean;
|
|
160
|
+
} {
|
|
161
|
+
const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
|
|
162
|
+
const ready = getReadyTasks(graph, completed, new Set());
|
|
163
|
+
const edgeCount = graph.reduce((sum, n) => sum + n.dependsOn.length, 0);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
taskCount: graph.length,
|
|
167
|
+
edgeCount,
|
|
168
|
+
readySetSize: ready.length,
|
|
169
|
+
ambiguous: isGraphAmbiguous(graph),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── IO Loader (async, filesystem) ────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Load TaskIO for all tasks in a slice by reading the slice plan (for done
|
|
177
|
+
* status and task IDs) and individual task plan files (for IO sections).
|
|
178
|
+
*
|
|
179
|
+
* Returns [] when the slice plan or tasks directory doesn't exist.
|
|
180
|
+
*/
|
|
181
|
+
export async function loadSliceTaskIO(
|
|
182
|
+
basePath: string,
|
|
183
|
+
mid: string,
|
|
184
|
+
sid: string,
|
|
185
|
+
): Promise<TaskIO[]> {
|
|
186
|
+
const { resolveSliceFile } = await import("./paths.js");
|
|
187
|
+
const slicePlanPath = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
188
|
+
const planContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
|
|
189
|
+
if (!planContent) return [];
|
|
190
|
+
|
|
191
|
+
const plan = parsePlan(planContent);
|
|
192
|
+
const tDir = resolveTasksDir(basePath, mid, sid);
|
|
193
|
+
if (!tDir) return [];
|
|
194
|
+
|
|
195
|
+
const results: TaskIO[] = [];
|
|
196
|
+
|
|
197
|
+
for (const taskEntry of plan.tasks) {
|
|
198
|
+
const planFiles = resolveTaskFiles(tDir, "PLAN");
|
|
199
|
+
const taskFileName = planFiles.find((f) =>
|
|
200
|
+
f.toUpperCase().startsWith(taskEntry.id.toUpperCase() + "-"),
|
|
201
|
+
);
|
|
202
|
+
if (!taskFileName) {
|
|
203
|
+
// Task plan file missing — include with empty IO (will trigger ambiguous)
|
|
204
|
+
results.push({
|
|
205
|
+
id: taskEntry.id,
|
|
206
|
+
title: taskEntry.title,
|
|
207
|
+
inputFiles: [],
|
|
208
|
+
outputFiles: [],
|
|
209
|
+
done: taskEntry.done,
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const taskContent = await loadFile(join(tDir, taskFileName));
|
|
215
|
+
if (!taskContent) {
|
|
216
|
+
results.push({
|
|
217
|
+
id: taskEntry.id,
|
|
218
|
+
title: taskEntry.title,
|
|
219
|
+
inputFiles: [],
|
|
220
|
+
outputFiles: [],
|
|
221
|
+
done: taskEntry.done,
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const io = parseTaskPlanIO(taskContent);
|
|
227
|
+
results.push({
|
|
228
|
+
id: taskEntry.id,
|
|
229
|
+
title: taskEntry.title,
|
|
230
|
+
inputFiles: io.inputFiles,
|
|
231
|
+
outputFiles: io.outputFiles,
|
|
232
|
+
done: taskEntry.done,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── State Persistence ────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
function reactiveStatePath(basePath: string, mid: string, sid: string): string {
|
|
242
|
+
return join(basePath, ".gsd", "runtime", `${mid}-${sid}-reactive.json`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isReactiveState(data: unknown): data is ReactiveExecutionState {
|
|
246
|
+
if (!data || typeof data !== "object") return false;
|
|
247
|
+
const d = data as Record<string, unknown>;
|
|
248
|
+
return typeof d.sliceId === "string" && Array.isArray(d.completed) && Array.isArray(d.dispatched);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Load persisted reactive execution state for a slice.
|
|
253
|
+
* Returns null when no state file exists or the file is invalid.
|
|
254
|
+
*/
|
|
255
|
+
export function loadReactiveState(
|
|
256
|
+
basePath: string,
|
|
257
|
+
mid: string,
|
|
258
|
+
sid: string,
|
|
259
|
+
): ReactiveExecutionState | null {
|
|
260
|
+
return loadJsonFileOrNull(reactiveStatePath(basePath, mid, sid), isReactiveState);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Save reactive execution state to disk.
|
|
265
|
+
*/
|
|
266
|
+
export function saveReactiveState(
|
|
267
|
+
basePath: string,
|
|
268
|
+
mid: string,
|
|
269
|
+
sid: string,
|
|
270
|
+
state: ReactiveExecutionState,
|
|
271
|
+
): void {
|
|
272
|
+
saveJsonFile(reactiveStatePath(basePath, mid, sid), state);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Remove the reactive state file when a slice completes.
|
|
277
|
+
*/
|
|
278
|
+
export function clearReactiveState(
|
|
279
|
+
basePath: string,
|
|
280
|
+
mid: string,
|
|
281
|
+
sid: string,
|
|
282
|
+
): void {
|
|
283
|
+
const path = reactiveStatePath(basePath, mid, sid);
|
|
284
|
+
try {
|
|
285
|
+
if (existsSync(path)) unlinkSync(path);
|
|
286
|
+
} catch {
|
|
287
|
+
// Non-fatal
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -12,6 +12,8 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, s
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
14
|
|
|
15
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
16
|
+
|
|
15
17
|
// ─── Repo Identity ──────────────────────────────────────────────────────────
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -90,14 +92,31 @@ function resolveGitRoot(basePath: string): string {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Validate a GSD_PROJECT_ID value.
|
|
97
|
+
*
|
|
98
|
+
* Must contain only alphanumeric characters, hyphens, and underscores.
|
|
99
|
+
* Call this once at startup so the user gets immediate feedback on bad values.
|
|
100
|
+
*/
|
|
101
|
+
export function validateProjectId(id: string): boolean {
|
|
102
|
+
return /^[a-zA-Z0-9_-]+$/.test(id);
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
/**
|
|
94
106
|
* Compute a stable identity for a repository.
|
|
95
107
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
108
|
+
* If `GSD_PROJECT_ID` is set, returns it directly (validation is expected
|
|
109
|
+
* to have already happened at startup via `validateProjectId`).
|
|
110
|
+
*
|
|
111
|
+
* Otherwise returns SHA-256 of `${remoteUrl}\n${resolvedRoot}`, truncated
|
|
112
|
+
* to 12 hex chars. Deterministic: same repo always produces the same hash
|
|
113
|
+
* regardless of which worktree the caller is inside.
|
|
99
114
|
*/
|
|
100
115
|
export function repoIdentity(basePath: string): string {
|
|
116
|
+
const projectId = process.env.GSD_PROJECT_ID;
|
|
117
|
+
if (projectId) {
|
|
118
|
+
return projectId;
|
|
119
|
+
}
|
|
101
120
|
const remoteUrl = getRemoteUrl(basePath);
|
|
102
121
|
const root = resolveGitRoot(basePath);
|
|
103
122
|
const input = `${remoteUrl}\n${root}`;
|
|
@@ -113,7 +132,7 @@ export function repoIdentity(basePath: string): string {
|
|
|
113
132
|
* otherwise `~/.gsd/projects/<hash>`.
|
|
114
133
|
*/
|
|
115
134
|
export function externalGsdRoot(basePath: string): string {
|
|
116
|
-
const base = process.env.GSD_STATE_DIR ||
|
|
135
|
+
const base = process.env.GSD_STATE_DIR || gsdHome;
|
|
117
136
|
return join(base, "projects", repoIdentity(basePath));
|
|
118
137
|
}
|
|
119
138
|
|
|
@@ -11,6 +11,8 @@ import { join } from "node:path";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { resolveProjectRoot } from "./worktree.js";
|
|
13
13
|
|
|
14
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
15
|
+
|
|
14
16
|
// ─── Resource Staleness ───────────────────────────────────────────────────
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -23,7 +25,7 @@ function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function readResourceVersion(): string | null {
|
|
26
|
-
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(
|
|
28
|
+
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
|
|
27
29
|
const manifestPath = join(agentDir, "managed-resources.json");
|
|
28
30
|
const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
|
|
29
31
|
return manifest?.gsdVersion ?? null;
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
gsdRoot,
|
|
32
32
|
} from './paths.js';
|
|
33
33
|
|
|
34
|
-
import { milestoneIdSort, findMilestoneIds } from './
|
|
34
|
+
import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
|
|
35
35
|
import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
|
|
36
36
|
|
|
37
37
|
import { join, resolve } from 'path';
|
|
@@ -42,11 +42,19 @@ estimated_files: {{estimatedFiles}}
|
|
|
42
42
|
|
|
43
43
|
## Inputs
|
|
44
44
|
|
|
45
|
+
<!-- Every input MUST be a backtick-wrapped file path. These paths are machine-parsed to
|
|
46
|
+
derive task dependencies — vague descriptions without paths break dependency detection.
|
|
47
|
+
For the first task in a slice with no prior task outputs, list the existing source files
|
|
48
|
+
this task reads or modifies. -->
|
|
49
|
+
|
|
45
50
|
- `{{filePath}}` — {{whatThisTaskNeedsFromPriorWork}}
|
|
46
|
-
- {{priorTaskSummaryInsight}}
|
|
47
51
|
|
|
48
52
|
## Expected Output
|
|
49
53
|
|
|
50
|
-
<!--
|
|
54
|
+
<!-- Every output MUST be a backtick-wrapped file path — the specific files this task creates
|
|
55
|
+
or modifies. These paths are machine-parsed to derive task dependencies.
|
|
56
|
+
This task should produce a real increment toward making the slice goal/demo true. A full
|
|
57
|
+
slice plan should not be able to mark every task complete while the claimed slice behavior
|
|
58
|
+
still does not work at the stated proof level. -->
|
|
51
59
|
|
|
52
|
-
- `{{filePath}}` — {{
|
|
60
|
+
- `{{filePath}}` — {{whatThisTaskCreatesOrModifies}}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent-end-retry.test.ts — Regression checks for the
|
|
2
|
+
* agent-end-retry.test.ts — Regression checks for the agent_end model.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The per-unit one-shot resolve function lives at module level in auto-loop.ts
|
|
5
|
+
* (_currentResolve). handleAgentEnd is a thin compatibility wrapper around
|
|
6
|
+
* resolveAgentEnd().
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import test from "node:test";
|
|
@@ -14,40 +14,43 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const AUTO_TS_PATH = join(__dirname, "..", "auto.ts");
|
|
17
|
+
const AUTO_LOOP_TS_PATH = join(__dirname, "..", "auto-loop.ts");
|
|
17
18
|
const SESSION_TS_PATH = join(__dirname, "..", "auto", "session.ts");
|
|
18
19
|
|
|
19
20
|
function getAutoTsSource(): string {
|
|
20
21
|
return readFileSync(AUTO_TS_PATH, "utf-8");
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function getAutoLoopTsSource(): string {
|
|
25
|
+
return readFileSync(AUTO_LOOP_TS_PATH, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
function getSessionTsSource(): string {
|
|
24
29
|
return readFileSync(SESSION_TS_PATH, "utf-8");
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
test("
|
|
28
|
-
const source =
|
|
32
|
+
test("auto-loop.ts declares _currentResolve for per-unit one-shot promises", () => {
|
|
33
|
+
const source = getAutoLoopTsSource();
|
|
29
34
|
assert.ok(
|
|
30
|
-
source.includes("
|
|
31
|
-
"
|
|
35
|
+
source.includes("_currentResolve"),
|
|
36
|
+
"auto-loop.ts must declare _currentResolve for the per-unit resolve function",
|
|
32
37
|
);
|
|
33
38
|
assert.ok(
|
|
34
|
-
source.includes("
|
|
35
|
-
"
|
|
39
|
+
source.includes("_sessionSwitchInFlight"),
|
|
40
|
+
"auto-loop.ts must declare _sessionSwitchInFlight guard",
|
|
36
41
|
);
|
|
37
42
|
});
|
|
38
43
|
|
|
39
|
-
test("AutoSession
|
|
44
|
+
test("AutoSession no longer holds promise state (moved to auto-loop.ts module scope)", () => {
|
|
40
45
|
const source = getSessionTsSource();
|
|
41
|
-
|
|
42
|
-
assert.ok(resetIdx > -1, "AutoSession must have a reset() method");
|
|
43
|
-
const resetBlock = source.slice(resetIdx, resetIdx + 4000);
|
|
46
|
+
// Properties should NOT exist as class fields
|
|
44
47
|
assert.ok(
|
|
45
|
-
|
|
46
|
-
"
|
|
48
|
+
!source.includes("pendingResolve:"),
|
|
49
|
+
"AutoSession must not declare pendingResolve (moved to auto-loop.ts)",
|
|
47
50
|
);
|
|
48
51
|
assert.ok(
|
|
49
|
-
|
|
50
|
-
"
|
|
52
|
+
!source.includes("pendingAgentEndQueue:"),
|
|
53
|
+
"AutoSession must not declare pendingAgentEndQueue (removed — events are dropped)",
|
|
51
54
|
);
|
|
52
55
|
});
|
|
53
56
|
|
|
@@ -37,9 +37,6 @@ function makeMockSession(opts?: {
|
|
|
37
37
|
const session = {
|
|
38
38
|
active: true,
|
|
39
39
|
verbose: false,
|
|
40
|
-
sessionSwitchInFlight: false,
|
|
41
|
-
pendingResolve: null,
|
|
42
|
-
pendingAgentEndQueue: [],
|
|
43
40
|
cmdCtx: {
|
|
44
41
|
newSession: () => {
|
|
45
42
|
opts?.onNewSessionStart?.(session);
|
|
@@ -96,7 +93,6 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
96
93
|
const ctx = makeMockCtx();
|
|
97
94
|
const pi = makeMockPi();
|
|
98
95
|
const s = makeMockSession();
|
|
99
|
-
_setActiveSession(s);
|
|
100
96
|
const event = makeEvent();
|
|
101
97
|
|
|
102
98
|
// Start runUnit — it will create the promise and send a message,
|
|
@@ -122,25 +118,21 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
122
118
|
assert.deepEqual(result.event, event);
|
|
123
119
|
});
|
|
124
120
|
|
|
125
|
-
test("resolveAgentEnd
|
|
121
|
+
test("resolveAgentEnd drops event when no promise is pending", () => {
|
|
126
122
|
_resetPendingResolve();
|
|
127
|
-
const s = makeMockSession();
|
|
128
|
-
_setActiveSession(s);
|
|
129
123
|
|
|
130
|
-
// Should not throw —
|
|
124
|
+
// Should not throw — event is dropped (logged as warning)
|
|
131
125
|
assert.doesNotThrow(() => {
|
|
132
126
|
resolveAgentEnd(makeEvent());
|
|
133
127
|
});
|
|
134
|
-
assert.equal(s.pendingAgentEndQueue.length, 1, "event should be queued");
|
|
135
128
|
});
|
|
136
129
|
|
|
137
|
-
test("double resolveAgentEnd only resolves once (second is
|
|
130
|
+
test("double resolveAgentEnd only resolves once (second is dropped)", async () => {
|
|
138
131
|
_resetPendingResolve();
|
|
139
132
|
|
|
140
133
|
const ctx = makeMockCtx();
|
|
141
134
|
const pi = makeMockPi();
|
|
142
135
|
const s = makeMockSession();
|
|
143
|
-
_setActiveSession(s);
|
|
144
136
|
const event1 = makeEvent([{ id: 1 }]);
|
|
145
137
|
const event2 = makeEvent([{ id: 2 }]);
|
|
146
138
|
|
|
@@ -151,15 +143,10 @@ test("double resolveAgentEnd only resolves once (second is queued)", async () =>
|
|
|
151
143
|
// First resolve — should work
|
|
152
144
|
resolveAgentEnd(event1);
|
|
153
145
|
|
|
154
|
-
// Second resolve — should be
|
|
146
|
+
// Second resolve — should be dropped (no pending resolver)
|
|
155
147
|
assert.doesNotThrow(() => {
|
|
156
148
|
resolveAgentEnd(event2);
|
|
157
149
|
});
|
|
158
|
-
assert.equal(
|
|
159
|
-
s.pendingAgentEndQueue.length,
|
|
160
|
-
1,
|
|
161
|
-
"second event should be queued",
|
|
162
|
-
);
|
|
163
150
|
|
|
164
151
|
const result = await resultPromise;
|
|
165
152
|
assert.equal(result.status, "completed");
|
|
@@ -211,29 +198,25 @@ test("runUnit returns cancelled when s.active is false before sendMessage", asyn
|
|
|
211
198
|
assert.equal(pi.calls.length, 0);
|
|
212
199
|
});
|
|
213
200
|
|
|
214
|
-
test("runUnit only arms
|
|
201
|
+
test("runUnit only arms resolve after newSession completes", async () => {
|
|
215
202
|
_resetPendingResolve();
|
|
216
203
|
|
|
217
204
|
let sawSwitchFlag = false;
|
|
218
|
-
let sawPendingResolve: unknown = "unset";
|
|
219
205
|
|
|
220
206
|
const ctx = makeMockCtx();
|
|
221
207
|
const pi = makeMockPi();
|
|
222
208
|
const s = makeMockSession({
|
|
223
209
|
newSessionDelayMs: 20,
|
|
224
|
-
onNewSessionStart: (
|
|
225
|
-
sawSwitchFlag =
|
|
226
|
-
sawPendingResolve = session.pendingResolve;
|
|
210
|
+
onNewSessionStart: () => {
|
|
211
|
+
sawSwitchFlag = isSessionSwitchInFlight();
|
|
227
212
|
},
|
|
228
213
|
});
|
|
229
|
-
_setActiveSession(s);
|
|
230
214
|
|
|
231
215
|
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt", undefined);
|
|
232
216
|
|
|
233
217
|
await new Promise((r) => setTimeout(r, 30));
|
|
234
218
|
|
|
235
219
|
assert.equal(sawSwitchFlag, true, "session switch guard should be active during newSession");
|
|
236
|
-
assert.equal(sawPendingResolve, null, "pendingResolve should not be armed before newSession completes");
|
|
237
220
|
assert.equal(isSessionSwitchInFlight(), false, "session switch guard should clear after newSession settles");
|
|
238
221
|
|
|
239
222
|
resolveAgentEnd(makeEvent());
|
|
@@ -275,24 +258,23 @@ test("auto-loop.ts contains a while keyword", () => {
|
|
|
275
258
|
);
|
|
276
259
|
});
|
|
277
260
|
|
|
278
|
-
test("auto-loop.ts one-shot pattern:
|
|
261
|
+
test("auto-loop.ts one-shot pattern: _currentResolve is nulled before calling resolver", () => {
|
|
279
262
|
const src = readFileSync(
|
|
280
263
|
resolve(import.meta.dirname, "..", "auto-loop.ts"),
|
|
281
264
|
"utf-8",
|
|
282
265
|
);
|
|
283
266
|
// The one-shot pattern requires: save ref, null the variable, then call
|
|
284
|
-
// Look for the pattern: s.pendingResolve = null appearing before r(
|
|
285
267
|
const resolveBlock = src.slice(
|
|
286
268
|
src.indexOf("export function resolveAgentEnd"),
|
|
287
269
|
src.indexOf("export function resolveAgentEnd") + 600,
|
|
288
270
|
);
|
|
289
|
-
const nullIdx = resolveBlock.indexOf("
|
|
271
|
+
const nullIdx = resolveBlock.indexOf("_currentResolve = null");
|
|
290
272
|
const callIdx = resolveBlock.indexOf("r({");
|
|
291
|
-
assert.ok(nullIdx > 0, "should null
|
|
273
|
+
assert.ok(nullIdx > 0, "should null _currentResolve in resolveAgentEnd");
|
|
292
274
|
assert.ok(callIdx > 0, "should call resolver in resolveAgentEnd");
|
|
293
275
|
assert.ok(
|
|
294
276
|
nullIdx < callIdx,
|
|
295
|
-
"
|
|
277
|
+
"_currentResolve should be nulled before calling the resolver (one-shot)",
|
|
296
278
|
);
|
|
297
279
|
});
|
|
298
280
|
|
|
@@ -462,8 +444,6 @@ function makeLoopSession(overrides?: Partial<Record<string, unknown>>) {
|
|
|
462
444
|
pendingQuickTasks: [],
|
|
463
445
|
sidecarQueue: [],
|
|
464
446
|
autoModeStartModel: null,
|
|
465
|
-
pendingResolve: null,
|
|
466
|
-
pendingAgentEndQueue: [],
|
|
467
447
|
unitDispatchCount: new Map<string, number>(),
|
|
468
448
|
unitLifetimeDispatches: new Map<string, number>(),
|
|
469
449
|
unitRecoveryCount: new Map<string, number>(),
|