gsd-pi 2.37.1 → 2.38.0-dev.96dc7fb
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/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/onboarding.js +1 -0
- 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/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
- package/dist/resources/extensions/gsd/auto-loop.js +7 -1
- 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-start.js +6 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +11 -4
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands.js +20 -1
- 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 +35 -1
- package/dist/resources/extensions/gsd/doctor.js +184 -11
- package/dist/resources/extensions/gsd/files.js +41 -0
- 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-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/templates/task-plan.md +11 -3
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/subagent/index.js +12 -3
- 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/gsd/auto-dispatch.ts +93 -0
- package/src/resources/extensions/gsd/auto-loop.ts +13 -1
- 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-start.ts +7 -1
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +12 -3
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands.ts +21 -1
- 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 +38 -1
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +177 -13
- package/src/resources/extensions/gsd/files.ts +45 -0
- 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/templates/task-plan.md +11 -3
- 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 +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/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +43 -0
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/subagent/index.ts +12 -3
|
@@ -65,11 +65,12 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
|
|
|
65
65
|
"compression_strategy",
|
|
66
66
|
"context_selection",
|
|
67
67
|
"widget_mode",
|
|
68
|
+
"reactive_execution",
|
|
68
69
|
]);
|
|
69
70
|
/** Canonical list of all dispatch unit types. */
|
|
70
71
|
export const KNOWN_UNIT_TYPES = [
|
|
71
72
|
"research-milestone", "plan-milestone", "research-slice", "plan-slice",
|
|
72
|
-
"execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
73
|
+
"execute-task", "reactive-execute", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
73
74
|
"run-uat", "complete-milestone",
|
|
74
75
|
];
|
|
75
76
|
export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
|
|
@@ -500,6 +500,48 @@ export function validatePreferences(preferences) {
|
|
|
500
500
|
validated.parallel = parallel;
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
|
+
// ─── Reactive Execution ─────────────────────────────────────────────────
|
|
504
|
+
if (preferences.reactive_execution !== undefined) {
|
|
505
|
+
if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
|
|
506
|
+
const re = preferences.reactive_execution;
|
|
507
|
+
const validRe = {};
|
|
508
|
+
if (re.enabled !== undefined) {
|
|
509
|
+
if (typeof re.enabled === "boolean")
|
|
510
|
+
validRe.enabled = re.enabled;
|
|
511
|
+
else
|
|
512
|
+
errors.push("reactive_execution.enabled must be a boolean");
|
|
513
|
+
}
|
|
514
|
+
if (re.max_parallel !== undefined) {
|
|
515
|
+
const mp = typeof re.max_parallel === "number" ? re.max_parallel : Number(re.max_parallel);
|
|
516
|
+
if (Number.isFinite(mp) && mp >= 1 && mp <= 8) {
|
|
517
|
+
validRe.max_parallel = Math.floor(mp);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
errors.push("reactive_execution.max_parallel must be a number between 1 and 8");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (re.isolation_mode !== undefined) {
|
|
524
|
+
if (re.isolation_mode === "same-tree") {
|
|
525
|
+
validRe.isolation_mode = "same-tree";
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
errors.push('reactive_execution.isolation_mode must be "same-tree"');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
|
|
532
|
+
for (const key of Object.keys(re)) {
|
|
533
|
+
if (!knownReKeys.has(key)) {
|
|
534
|
+
warnings.push(`unknown reactive_execution key "${key}" — ignored`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (Object.keys(validRe).length > 0) {
|
|
538
|
+
validated.reactive_execution = validRe;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
errors.push("reactive_execution must be an object");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
503
545
|
// ─── Verification Preferences ───────────────────────────────────────────
|
|
504
546
|
if (preferences.verification_commands !== undefined) {
|
|
505
547
|
if (Array.isArray(preferences.verification_commands)) {
|
|
@@ -61,13 +61,14 @@ Then:
|
|
|
61
61
|
- a concrete, action-oriented title
|
|
62
62
|
- the inline task entry fields defined in the plan.md template (Why / Files / Do / Verify / Done when)
|
|
63
63
|
- a matching task plan file with description, steps, must-haves, verification, inputs, and expected output
|
|
64
|
+
- **Inputs and Expected Output must list concrete backtick-wrapped file paths** (e.g. `` `src/types.ts` ``). These are machine-parsed to derive task dependencies — vague prose without paths breaks parallel execution. Every task must have at least one output file path.
|
|
64
65
|
- Observability Impact section **only if the task touches runtime boundaries, async flows, or error paths** — omit it otherwise
|
|
65
66
|
6. Write `{{outputPath}}`
|
|
66
67
|
7. Write individual task plans in `{{slicePath}}/tasks/`: `T01-PLAN.md`, `T02-PLAN.md`, etc.
|
|
67
68
|
8. **Self-audit the plan.** Walk through each check — if any fail, fix the plan files before moving on:
|
|
68
69
|
- **Completion semantics:** If every task were completed exactly as written, the slice goal/demo should actually be true.
|
|
69
70
|
- **Requirement coverage:** Every must-have in the slice maps to at least one task. No must-have is orphaned. If `REQUIREMENTS.md` exists, every Active requirement this slice owns maps to at least one task.
|
|
70
|
-
- **Task completeness:** Every task has steps, must-haves, verification, inputs, and expected output — none are blank or vague.
|
|
71
|
+
- **Task completeness:** Every task has steps, must-haves, verification, inputs, and expected output — none are blank or vague. Inputs and Expected Output list backtick-wrapped file paths, not prose descriptions.
|
|
71
72
|
- **Dependency correctness:** Task ordering is consistent. No task references work from a later task.
|
|
72
73
|
- **Key links planned:** For every pair of artifacts that must connect, there is an explicit step that wires them.
|
|
73
74
|
- **Scope sanity:** Target 2–5 steps and 3–8 files per task. 10+ steps or 12+ files — must split. Each task must be completable in a single fresh context window.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Reactive Task Execution — Parallel Dispatch
|
|
2
|
+
|
|
3
|
+
**Working directory:** `{{workingDirectory}}`
|
|
4
|
+
**Milestone:** {{milestoneId}} — {{milestoneTitle}}
|
|
5
|
+
**Slice:** {{sliceId}} — {{sliceTitle}}
|
|
6
|
+
|
|
7
|
+
## Mission
|
|
8
|
+
|
|
9
|
+
You are executing **multiple tasks in parallel** for this slice. The task graph below shows which tasks are ready for simultaneous execution based on their input/output dependencies.
|
|
10
|
+
|
|
11
|
+
**Critical rule:** Use the `subagent` tool in **parallel mode** to dispatch all ready tasks simultaneously. Each subagent gets a self-contained execute-task prompt. After all subagents return, verify each task's outputs and write summaries.
|
|
12
|
+
|
|
13
|
+
## Task Dependency Graph
|
|
14
|
+
|
|
15
|
+
{{graphContext}}
|
|
16
|
+
|
|
17
|
+
## Ready Tasks for Parallel Dispatch
|
|
18
|
+
|
|
19
|
+
{{readyTaskCount}} tasks are ready for parallel execution:
|
|
20
|
+
|
|
21
|
+
{{readyTaskList}}
|
|
22
|
+
|
|
23
|
+
## Execution Protocol
|
|
24
|
+
|
|
25
|
+
1. **Dispatch all ready tasks** using `subagent` in parallel mode. Each subagent prompt is provided below.
|
|
26
|
+
2. **Wait for all subagents** to complete.
|
|
27
|
+
3. **Verify each task's outputs** — check that expected files were created/modified and that verification commands pass.
|
|
28
|
+
4. **Write task summaries** for each completed task using the task-summary template.
|
|
29
|
+
5. **Mark completed tasks** as done in the slice plan (checkbox `[x]`).
|
|
30
|
+
6. **Commit** all changes with a clear message covering the parallel batch.
|
|
31
|
+
|
|
32
|
+
If any subagent fails:
|
|
33
|
+
- Write a summary for the failed task with `blocker_discovered: true`
|
|
34
|
+
- Continue marking the successful tasks as done
|
|
35
|
+
- The orchestrator will handle re-dispatch on the next iteration
|
|
36
|
+
|
|
37
|
+
## Subagent Prompts
|
|
38
|
+
|
|
39
|
+
{{subagentPrompts}}
|
|
40
|
+
|
|
41
|
+
{{inlinedTemplates}}
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
import { loadFile, parsePlan, parseTaskPlanIO } from "./files.js";
|
|
12
|
+
import { resolveTasksDir, resolveTaskFiles } from "./paths.js";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
15
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
16
|
+
// ─── Graph Construction ───────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Build a dependency graph from task IO signatures.
|
|
19
|
+
*
|
|
20
|
+
* A task T_b depends on T_a when any of T_b's inputFiles appear in T_a's
|
|
21
|
+
* outputFiles. Self-references are excluded.
|
|
22
|
+
*
|
|
23
|
+
* Tasks are returned in the same order as the input array.
|
|
24
|
+
*/
|
|
25
|
+
export function deriveTaskGraph(tasks) {
|
|
26
|
+
// Build output → producer lookup
|
|
27
|
+
const outputToProducer = new Map();
|
|
28
|
+
for (const task of tasks) {
|
|
29
|
+
for (const outFile of task.outputFiles) {
|
|
30
|
+
const existing = outputToProducer.get(outFile);
|
|
31
|
+
if (existing) {
|
|
32
|
+
existing.push(task.id);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
outputToProducer.set(outFile, [task.id]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return tasks.map((task) => {
|
|
40
|
+
const deps = new Set();
|
|
41
|
+
for (const inFile of task.inputFiles) {
|
|
42
|
+
const producers = outputToProducer.get(inFile);
|
|
43
|
+
if (producers) {
|
|
44
|
+
for (const pid of producers) {
|
|
45
|
+
if (pid !== task.id)
|
|
46
|
+
deps.add(pid);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
...task,
|
|
52
|
+
dependsOn: [...deps].sort(),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// ─── Ready Set Resolution ─────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Return task IDs whose dependencies are all in `completed`.
|
|
59
|
+
* Excludes tasks that are already done or in-flight.
|
|
60
|
+
*/
|
|
61
|
+
export function getReadyTasks(graph, completed, inFlight) {
|
|
62
|
+
return graph
|
|
63
|
+
.filter((node) => {
|
|
64
|
+
if (node.done || completed.has(node.id) || inFlight.has(node.id))
|
|
65
|
+
return false;
|
|
66
|
+
return node.dependsOn.every((dep) => completed.has(dep));
|
|
67
|
+
})
|
|
68
|
+
.map((node) => node.id);
|
|
69
|
+
}
|
|
70
|
+
// ─── Conflict-Free Subset Selection ──────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Greedy selection of non-conflicting tasks up to `maxParallel`.
|
|
73
|
+
*
|
|
74
|
+
* Two tasks conflict if they share any outputFile. We also exclude tasks
|
|
75
|
+
* whose outputs overlap with `inFlightOutputs` (files being written by
|
|
76
|
+
* tasks currently in progress).
|
|
77
|
+
*/
|
|
78
|
+
export function chooseNonConflictingSubset(readyIds, graph, maxParallel, inFlightOutputs) {
|
|
79
|
+
const nodeMap = new Map(graph.map((n) => [n.id, n]));
|
|
80
|
+
const claimed = new Set(inFlightOutputs);
|
|
81
|
+
const selected = [];
|
|
82
|
+
for (const id of readyIds) {
|
|
83
|
+
if (selected.length >= maxParallel)
|
|
84
|
+
break;
|
|
85
|
+
const node = nodeMap.get(id);
|
|
86
|
+
if (!node)
|
|
87
|
+
continue;
|
|
88
|
+
// Check for output overlap with already-selected or in-flight
|
|
89
|
+
const conflicts = node.outputFiles.some((f) => claimed.has(f));
|
|
90
|
+
if (conflicts)
|
|
91
|
+
continue;
|
|
92
|
+
// Claim this task's outputs
|
|
93
|
+
for (const f of node.outputFiles)
|
|
94
|
+
claimed.add(f);
|
|
95
|
+
selected.push(id);
|
|
96
|
+
}
|
|
97
|
+
return selected;
|
|
98
|
+
}
|
|
99
|
+
// ─── Graph Quality Checks ─────────────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Returns true if any incomplete task has 0 inputFiles AND 0 outputFiles.
|
|
102
|
+
*
|
|
103
|
+
* An ambiguous graph means IO annotations are too sparse to derive reliable
|
|
104
|
+
* edges — the dispatcher should fall back to sequential execution.
|
|
105
|
+
*/
|
|
106
|
+
export function isGraphAmbiguous(graph) {
|
|
107
|
+
return graph.some((node) => !node.done &&
|
|
108
|
+
node.inputFiles.length === 0 &&
|
|
109
|
+
node.outputFiles.length === 0);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
|
|
113
|
+
* tasks remain. This indicates a circular dependency or impossible state.
|
|
114
|
+
*/
|
|
115
|
+
export function detectDeadlock(graph, completed, inFlight) {
|
|
116
|
+
const incomplete = graph.filter((n) => !n.done && !completed.has(n.id) && !inFlight.has(n.id));
|
|
117
|
+
if (incomplete.length === 0)
|
|
118
|
+
return false; // all done
|
|
119
|
+
if (inFlight.size > 0)
|
|
120
|
+
return false; // something is running, wait for it
|
|
121
|
+
// Nothing in flight, but incomplete tasks remain — check if any are ready
|
|
122
|
+
const ready = getReadyTasks(graph, completed, inFlight);
|
|
123
|
+
return ready.length === 0;
|
|
124
|
+
}
|
|
125
|
+
// ─── Graph Metrics ────────────────────────────────────────────────────────
|
|
126
|
+
/** Compute summary metrics for logging. */
|
|
127
|
+
export function graphMetrics(graph) {
|
|
128
|
+
const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
|
|
129
|
+
const ready = getReadyTasks(graph, completed, new Set());
|
|
130
|
+
const edgeCount = graph.reduce((sum, n) => sum + n.dependsOn.length, 0);
|
|
131
|
+
return {
|
|
132
|
+
taskCount: graph.length,
|
|
133
|
+
edgeCount,
|
|
134
|
+
readySetSize: ready.length,
|
|
135
|
+
ambiguous: isGraphAmbiguous(graph),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// ─── IO Loader (async, filesystem) ────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Load TaskIO for all tasks in a slice by reading the slice plan (for done
|
|
141
|
+
* status and task IDs) and individual task plan files (for IO sections).
|
|
142
|
+
*
|
|
143
|
+
* Returns [] when the slice plan or tasks directory doesn't exist.
|
|
144
|
+
*/
|
|
145
|
+
export async function loadSliceTaskIO(basePath, mid, sid) {
|
|
146
|
+
const { resolveSliceFile } = await import("./paths.js");
|
|
147
|
+
const slicePlanPath = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
148
|
+
const planContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
|
|
149
|
+
if (!planContent)
|
|
150
|
+
return [];
|
|
151
|
+
const plan = parsePlan(planContent);
|
|
152
|
+
const tDir = resolveTasksDir(basePath, mid, sid);
|
|
153
|
+
if (!tDir)
|
|
154
|
+
return [];
|
|
155
|
+
const results = [];
|
|
156
|
+
for (const taskEntry of plan.tasks) {
|
|
157
|
+
const planFiles = resolveTaskFiles(tDir, "PLAN");
|
|
158
|
+
const taskFileName = planFiles.find((f) => f.toUpperCase().startsWith(taskEntry.id.toUpperCase() + "-"));
|
|
159
|
+
if (!taskFileName) {
|
|
160
|
+
// Task plan file missing — include with empty IO (will trigger ambiguous)
|
|
161
|
+
results.push({
|
|
162
|
+
id: taskEntry.id,
|
|
163
|
+
title: taskEntry.title,
|
|
164
|
+
inputFiles: [],
|
|
165
|
+
outputFiles: [],
|
|
166
|
+
done: taskEntry.done,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const taskContent = await loadFile(join(tDir, taskFileName));
|
|
171
|
+
if (!taskContent) {
|
|
172
|
+
results.push({
|
|
173
|
+
id: taskEntry.id,
|
|
174
|
+
title: taskEntry.title,
|
|
175
|
+
inputFiles: [],
|
|
176
|
+
outputFiles: [],
|
|
177
|
+
done: taskEntry.done,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const io = parseTaskPlanIO(taskContent);
|
|
182
|
+
results.push({
|
|
183
|
+
id: taskEntry.id,
|
|
184
|
+
title: taskEntry.title,
|
|
185
|
+
inputFiles: io.inputFiles,
|
|
186
|
+
outputFiles: io.outputFiles,
|
|
187
|
+
done: taskEntry.done,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
// ─── State Persistence ────────────────────────────────────────────────────
|
|
193
|
+
function reactiveStatePath(basePath, mid, sid) {
|
|
194
|
+
return join(basePath, ".gsd", "runtime", `${mid}-${sid}-reactive.json`);
|
|
195
|
+
}
|
|
196
|
+
function isReactiveState(data) {
|
|
197
|
+
if (!data || typeof data !== "object")
|
|
198
|
+
return false;
|
|
199
|
+
const d = data;
|
|
200
|
+
return typeof d.sliceId === "string" && Array.isArray(d.completed) && Array.isArray(d.dispatched);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Load persisted reactive execution state for a slice.
|
|
204
|
+
* Returns null when no state file exists or the file is invalid.
|
|
205
|
+
*/
|
|
206
|
+
export function loadReactiveState(basePath, mid, sid) {
|
|
207
|
+
return loadJsonFileOrNull(reactiveStatePath(basePath, mid, sid), isReactiveState);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Save reactive execution state to disk.
|
|
211
|
+
*/
|
|
212
|
+
export function saveReactiveState(basePath, mid, sid, state) {
|
|
213
|
+
saveJsonFile(reactiveStatePath(basePath, mid, sid), state);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Remove the reactive state file when a slice completes.
|
|
217
|
+
*/
|
|
218
|
+
export function clearReactiveState(basePath, mid, sid) {
|
|
219
|
+
const path = reactiveStatePath(basePath, mid, sid);
|
|
220
|
+
try {
|
|
221
|
+
if (existsSync(path))
|
|
222
|
+
unlinkSync(path);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Non-fatal
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -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}}
|
|
@@ -57,42 +57,61 @@ export function captureIntegrationBranch(basePath, milestoneId, options) {
|
|
|
57
57
|
writeIntegrationBranch(basePath, milestoneId, current, options);
|
|
58
58
|
}
|
|
59
59
|
// ─── Pure Utility Functions (unchanged) ────────────────────────────────────
|
|
60
|
+
/**
|
|
61
|
+
* Find the worktrees segment in a path, supporting both direct
|
|
62
|
+
* (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
|
|
63
|
+
* layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
|
|
64
|
+
* paths contain the intermediate `projects/<hash>/` segment that the old
|
|
65
|
+
* single-marker check missed.
|
|
66
|
+
*/
|
|
67
|
+
function findWorktreeSegment(normalizedPath) {
|
|
68
|
+
// Direct layout: /.gsd/worktrees/<name>
|
|
69
|
+
const directMarker = "/.gsd/worktrees/";
|
|
70
|
+
const idx = normalizedPath.indexOf(directMarker);
|
|
71
|
+
if (idx !== -1) {
|
|
72
|
+
return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
|
|
73
|
+
}
|
|
74
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
|
|
75
|
+
const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
|
|
76
|
+
const match = normalizedPath.match(symlinkRe);
|
|
77
|
+
if (match && match.index !== undefined) {
|
|
78
|
+
return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
60
82
|
/**
|
|
61
83
|
* Detect the active worktree name from the current working directory.
|
|
62
84
|
* Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
|
|
63
85
|
*/
|
|
64
86
|
export function detectWorktreeName(basePath) {
|
|
65
87
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (idx === -1)
|
|
88
|
+
const seg = findWorktreeSegment(normalizedPath);
|
|
89
|
+
if (!seg)
|
|
69
90
|
return null;
|
|
70
|
-
const afterMarker = normalizedPath.slice(
|
|
91
|
+
const afterMarker = normalizedPath.slice(seg.afterWorktrees);
|
|
71
92
|
const name = afterMarker.split("/")[0];
|
|
72
93
|
return name || null;
|
|
73
94
|
}
|
|
74
95
|
/**
|
|
75
96
|
* Resolve the project root from a path that may be inside a worktree.
|
|
76
|
-
* If the path contains
|
|
77
|
-
*
|
|
97
|
+
* If the path contains a worktrees segment, returns the portion before
|
|
98
|
+
* `/.gsd/`. Otherwise returns the input unchanged.
|
|
78
99
|
*
|
|
79
100
|
* Use this in commands that call `process.cwd()` to ensure they always
|
|
80
101
|
* operate against the real project root, not a worktree subdirectory.
|
|
81
102
|
*/
|
|
82
103
|
export function resolveProjectRoot(basePath) {
|
|
83
104
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
if (idx === -1)
|
|
105
|
+
const seg = findWorktreeSegment(normalizedPath);
|
|
106
|
+
if (!seg)
|
|
87
107
|
return basePath;
|
|
88
|
-
// Return the original path up to the
|
|
89
|
-
// Account for potential OS-specific separators
|
|
108
|
+
// Return the original path up to the /.gsd/ boundary
|
|
90
109
|
const sep = basePath.includes("\\") ? "\\" : "/";
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
return basePath.slice(0,
|
|
95
|
-
return basePath.slice(0,
|
|
110
|
+
const gsdMarker = `${sep}.gsd${sep}`;
|
|
111
|
+
const gsdIdx = basePath.indexOf(gsdMarker);
|
|
112
|
+
if (gsdIdx !== -1)
|
|
113
|
+
return basePath.slice(0, gsdIdx);
|
|
114
|
+
return basePath.slice(0, seg.gsdIdx);
|
|
96
115
|
}
|
|
97
116
|
/**
|
|
98
117
|
* Get the slice branch name, namespaced by worktree when inside one.
|
|
@@ -360,7 +360,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
-
async function runSingleAgentInCmuxSplit(cmuxClient,
|
|
363
|
+
async function runSingleAgentInCmuxSplit(cmuxClient, directionOrSurfaceId, defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails) {
|
|
364
364
|
const agent = agents.find((a) => a.name === agentName);
|
|
365
365
|
if (!agent) {
|
|
366
366
|
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
@@ -397,7 +397,12 @@ async function runSingleAgentInCmuxSplit(cmuxClient, direction, defaultCwd, agen
|
|
|
397
397
|
const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
|
|
398
398
|
const stderrPath = path.join(tmpOutputDir, "stderr.log");
|
|
399
399
|
const exitPath = path.join(tmpOutputDir, "exit.code");
|
|
400
|
-
|
|
400
|
+
// Accept either a pre-created surface ID or a direction to create a new split
|
|
401
|
+
const isDirection = directionOrSurfaceId === "right" || directionOrSurfaceId === "down"
|
|
402
|
+
|| directionOrSurfaceId === "left" || directionOrSurfaceId === "up";
|
|
403
|
+
const cmuxSurfaceId = isDirection
|
|
404
|
+
? await cmuxClient.createSplit(directionOrSurfaceId)
|
|
405
|
+
: directionOrSurfaceId;
|
|
401
406
|
if (!cmuxSurfaceId) {
|
|
402
407
|
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
403
408
|
}
|
|
@@ -656,10 +661,14 @@ export default function (pi) {
|
|
|
656
661
|
const MAX_RETRIES = 1; // Retry failed tasks once
|
|
657
662
|
const batchId = crypto.randomUUID();
|
|
658
663
|
const batchSize = params.tasks.length;
|
|
664
|
+
// Pre-create a grid layout for cmux splits so agents get a clean tiled arrangement
|
|
665
|
+
const gridSurfaces = cmuxSplitsEnabled
|
|
666
|
+
? await cmuxClient.createGridLayout(Math.min(batchSize, MAX_CONCURRENCY))
|
|
667
|
+
: [];
|
|
659
668
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
660
669
|
const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
|
|
661
670
|
const runTask = () => cmuxSplitsEnabled
|
|
662
|
-
? runSingleAgentInCmuxSplit(cmuxClient, index % 2 === 0 ? "right" : "down", ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
|
|
671
|
+
? runSingleAgentInCmuxSplit(cmuxClient, gridSurfaces[index] ?? (index % 2 === 0 ? "right" : "down"), ctx.cwd, agents, t.agent, t.task, t.cwd, undefined, signal, (partial) => {
|
|
663
672
|
if (partial.details?.results[0]) {
|
|
664
673
|
allResults[index] = partial.details.results[0];
|
|
665
674
|
emitParallelUpdate();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Welcome Screen
|
|
3
|
+
*
|
|
4
|
+
* Rendered to stderr before the TUI takes over.
|
|
5
|
+
* No box, no panels — logo with metadata alongside, dim hint below.
|
|
6
|
+
*/
|
|
7
|
+
export interface WelcomeScreenOptions {
|
|
8
|
+
version: string;
|
|
9
|
+
modelName?: string;
|
|
10
|
+
provider?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function printWelcomeScreen(opts: WelcomeScreenOptions): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Welcome Screen
|
|
3
|
+
*
|
|
4
|
+
* Rendered to stderr before the TUI takes over.
|
|
5
|
+
* No box, no panels — logo with metadata alongside, dim hint below.
|
|
6
|
+
*/
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { GSD_LOGO } from './logo.js';
|
|
10
|
+
function getShortCwd() {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
return cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd;
|
|
14
|
+
}
|
|
15
|
+
export function printWelcomeScreen(opts) {
|
|
16
|
+
if (!process.stderr.isTTY)
|
|
17
|
+
return;
|
|
18
|
+
const { version, modelName, provider } = opts;
|
|
19
|
+
const shortCwd = getShortCwd();
|
|
20
|
+
// Info lines to sit alongside the logo (one per logo row)
|
|
21
|
+
const modelLine = [modelName, provider].filter(Boolean).join(' · ');
|
|
22
|
+
const INFO = [
|
|
23
|
+
` ${chalk.bold('Get Shit Done')} ${chalk.dim('v' + version)}`,
|
|
24
|
+
undefined,
|
|
25
|
+
modelLine ? ` ${chalk.dim(modelLine)}` : undefined,
|
|
26
|
+
` ${chalk.dim(shortCwd)}`,
|
|
27
|
+
undefined,
|
|
28
|
+
undefined,
|
|
29
|
+
];
|
|
30
|
+
const lines = [''];
|
|
31
|
+
for (let i = 0; i < GSD_LOGO.length; i++) {
|
|
32
|
+
lines.push(chalk.cyan(GSD_LOGO[i]) + (INFO[i] ?? ''));
|
|
33
|
+
}
|
|
34
|
+
// Tool status + hint — dim, aligned under the info text
|
|
35
|
+
const pad = ' '.repeat(28) + ' '; // aligns with the info text column
|
|
36
|
+
const toolParts = [];
|
|
37
|
+
if (process.env.BRAVE_API_KEY)
|
|
38
|
+
toolParts.push('Brave ✓');
|
|
39
|
+
if (process.env.BRAVE_ANSWERS_KEY)
|
|
40
|
+
toolParts.push('Answers ✓');
|
|
41
|
+
if (process.env.JINA_API_KEY)
|
|
42
|
+
toolParts.push('Jina ✓');
|
|
43
|
+
if (process.env.TAVILY_API_KEY)
|
|
44
|
+
toolParts.push('Tavily ✓');
|
|
45
|
+
if (process.env.CONTEXT7_API_KEY)
|
|
46
|
+
toolParts.push('Context7 ✓');
|
|
47
|
+
if (toolParts.length > 0) {
|
|
48
|
+
lines.push(chalk.dim(pad + ['Web search loaded', ...toolParts].join(' · ')));
|
|
49
|
+
}
|
|
50
|
+
lines.push(chalk.dim(pad + '/gsd to begin · /gsd help for all commands'));
|
|
51
|
+
lines.push('');
|
|
52
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.38.0-dev.96dc7fb",
|
|
4
4
|
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@anthropic-ai/sdk": "^0.73.0",
|
|
87
|
+
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
|
87
88
|
"@aws-sdk/client-bedrock-runtime": "^3.983.0",
|
|
88
89
|
"@clack/prompts": "^1.1.0",
|
|
89
90
|
"@google/genai": "^1.40.0",
|
|
@@ -53,6 +53,19 @@ export function getEnvApiKey(provider) {
|
|
|
53
53
|
if (provider === "anthropic") {
|
|
54
54
|
return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
|
55
55
|
}
|
|
56
|
+
// Anthropic on Vertex AI uses Application Default Credentials.
|
|
57
|
+
// Detected via ANTHROPIC_VERTEX_PROJECT_ID (same env var as Claude Code).
|
|
58
|
+
if (provider === "anthropic-vertex") {
|
|
59
|
+
const hasProject = !!process.env.ANTHROPIC_VERTEX_PROJECT_ID;
|
|
60
|
+
if (hasProject) {
|
|
61
|
+
return "<authenticated>";
|
|
62
|
+
}
|
|
63
|
+
// Fall back to Google Cloud project env vars
|
|
64
|
+
const hasGoogleProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);
|
|
65
|
+
if (hasGoogleProject && hasVertexAdcCredentials()) {
|
|
66
|
+
return "<authenticated>";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
// Vertex AI uses Application Default Credentials, not API keys.
|
|
57
70
|
// Auth is configured via `gcloud auth application-default login`.
|
|
58
71
|
if (provider === "google-vertex") {
|