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
package/README.md
CHANGED
|
@@ -629,7 +629,7 @@ GSD isn't locked to one provider. It runs on the [Pi SDK](https://github.com/bad
|
|
|
629
629
|
|
|
630
630
|
### Built-in Providers
|
|
631
631
|
|
|
632
|
-
Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
|
|
632
|
+
Anthropic, Anthropic (Vertex AI), OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
|
|
633
633
|
|
|
634
634
|
### OAuth / Max Plans
|
|
635
635
|
|
package/dist/cli.js
CHANGED
|
@@ -505,6 +505,15 @@ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
|
|
|
505
505
|
session.setScopedModels(scopedModels);
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
|
+
// Welcome screen — shown on every fresh interactive session before TUI takes over
|
|
509
|
+
{
|
|
510
|
+
const { printWelcomeScreen } = await import('./welcome-screen.js');
|
|
511
|
+
printWelcomeScreen({
|
|
512
|
+
version: process.env.GSD_VERSION || '0.0.0',
|
|
513
|
+
modelName: settingsManager.getDefaultModel() || undefined,
|
|
514
|
+
provider: settingsManager.getDefaultProvider() || undefined,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
508
517
|
const interactiveMode = new InteractiveMode(session);
|
|
509
518
|
markStartup('InteractiveMode');
|
|
510
519
|
printStartupTimings();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolves the entry-point file(s) for a single extension directory.
|
|
3
3
|
*
|
|
4
|
-
* 1. If the directory contains a package.json with a `pi
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 1. If the directory contains a package.json with a `pi` manifest object,
|
|
5
|
+
* the manifest is authoritative:
|
|
6
|
+
* - `pi.extensions` array → resolve each entry relative to the directory.
|
|
7
|
+
* - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
|
|
8
|
+
* 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
|
|
7
9
|
*/
|
|
8
10
|
export declare function resolveExtensionEntries(dir: string): string[];
|
|
9
11
|
/**
|
|
@@ -6,24 +6,29 @@ function isExtensionFile(name) {
|
|
|
6
6
|
/**
|
|
7
7
|
* Resolves the entry-point file(s) for a single extension directory.
|
|
8
8
|
*
|
|
9
|
-
* 1. If the directory contains a package.json with a `pi
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 1. If the directory contains a package.json with a `pi` manifest object,
|
|
10
|
+
* the manifest is authoritative:
|
|
11
|
+
* - `pi.extensions` array → resolve each entry relative to the directory.
|
|
12
|
+
* - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
|
|
13
|
+
* 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
|
|
12
14
|
*/
|
|
13
15
|
export function resolveExtensionEntries(dir) {
|
|
14
16
|
const packageJsonPath = join(dir, 'package.json');
|
|
15
17
|
if (existsSync(packageJsonPath)) {
|
|
16
18
|
try {
|
|
17
19
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
if (pkg?.pi && typeof pkg.pi === 'object') {
|
|
21
|
+
// When a pi manifest exists, it is authoritative — don't fall through
|
|
22
|
+
// to index.ts/index.js auto-detection. This allows library directories
|
|
23
|
+
// (like cmux) to opt out by declaring "pi": {} with no extensions.
|
|
24
|
+
const declared = pkg.pi.extensions;
|
|
25
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return declared
|
|
21
29
|
.filter((entry) => typeof entry === 'string')
|
|
22
30
|
.map((entry) => resolve(dir, entry))
|
|
23
31
|
.filter((entry) => existsSync(entry));
|
|
24
|
-
if (resolved.length > 0) {
|
|
25
|
-
return resolved;
|
|
26
|
-
}
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
catch {
|
package/dist/onboarding.js
CHANGED
|
@@ -237,11 +237,14 @@ export class CmuxClient {
|
|
|
237
237
|
return extractSurfaceIds(parsed);
|
|
238
238
|
}
|
|
239
239
|
async createSplit(direction) {
|
|
240
|
+
return this.createSplitFrom(this.config.surfaceId, direction);
|
|
241
|
+
}
|
|
242
|
+
async createSplitFrom(sourceSurfaceId, direction) {
|
|
240
243
|
if (!this.config.splits)
|
|
241
244
|
return null;
|
|
242
245
|
const before = new Set(await this.listSurfaceIds());
|
|
243
246
|
const args = ["new-split", direction];
|
|
244
|
-
const scopedArgs = this.appendSurface(this.appendWorkspace(args),
|
|
247
|
+
const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
|
|
245
248
|
await this.runAsync(scopedArgs);
|
|
246
249
|
const after = await this.listSurfaceIds();
|
|
247
250
|
for (const id of after) {
|
|
@@ -250,6 +253,57 @@ export class CmuxClient {
|
|
|
250
253
|
}
|
|
251
254
|
return null;
|
|
252
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Create a grid of surfaces for parallel agent execution.
|
|
258
|
+
*
|
|
259
|
+
* Layout strategy (gsd stays in the original surface):
|
|
260
|
+
* 1 agent: [gsd | A]
|
|
261
|
+
* 2 agents: [gsd | A]
|
|
262
|
+
* [ | B]
|
|
263
|
+
* 3 agents: [gsd | A]
|
|
264
|
+
* [ C | B]
|
|
265
|
+
* 4 agents: [gsd | A]
|
|
266
|
+
* [ C | B] (D splits from B downward)
|
|
267
|
+
* [ | D]
|
|
268
|
+
*
|
|
269
|
+
* Returns surface IDs in order, or empty array on failure.
|
|
270
|
+
*/
|
|
271
|
+
async createGridLayout(count) {
|
|
272
|
+
if (!this.config.splits || count <= 0)
|
|
273
|
+
return [];
|
|
274
|
+
const surfaces = [];
|
|
275
|
+
// First split: create right column from the gsd surface
|
|
276
|
+
const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
|
|
277
|
+
if (!rightCol)
|
|
278
|
+
return [];
|
|
279
|
+
surfaces.push(rightCol);
|
|
280
|
+
if (count === 1)
|
|
281
|
+
return surfaces;
|
|
282
|
+
// Second split: split right column down → bottom-right
|
|
283
|
+
const bottomRight = await this.createSplitFrom(rightCol, "down");
|
|
284
|
+
if (!bottomRight)
|
|
285
|
+
return surfaces;
|
|
286
|
+
surfaces.push(bottomRight);
|
|
287
|
+
if (count === 2)
|
|
288
|
+
return surfaces;
|
|
289
|
+
// Third split: split gsd surface down → bottom-left
|
|
290
|
+
const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
|
|
291
|
+
if (!bottomLeft)
|
|
292
|
+
return surfaces;
|
|
293
|
+
surfaces.push(bottomLeft);
|
|
294
|
+
if (count === 3)
|
|
295
|
+
return surfaces;
|
|
296
|
+
// Fourth+: split subsequent surfaces down from the last created
|
|
297
|
+
let lastSurface = bottomRight;
|
|
298
|
+
for (let i = 3; i < count; i++) {
|
|
299
|
+
const next = await this.createSplitFrom(lastSurface, "down");
|
|
300
|
+
if (!next)
|
|
301
|
+
break;
|
|
302
|
+
surfaces.push(next);
|
|
303
|
+
lastSurface = next;
|
|
304
|
+
}
|
|
305
|
+
return surfaces;
|
|
306
|
+
}
|
|
253
307
|
async sendSurface(surfaceId, text) {
|
|
254
308
|
const payload = text.endsWith("\n") ? text : `${text}\n`;
|
|
255
309
|
const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
|
|
@@ -12,7 +12,7 @@ import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
|
|
|
12
12
|
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
|
|
13
13
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
|
|
15
|
+
import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, buildReactiveExecutePrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
|
|
16
16
|
function missingSliceStop(mid, phase) {
|
|
17
17
|
return {
|
|
18
18
|
action: "stop",
|
|
@@ -223,6 +223,72 @@ const DISPATCH_RULES = [
|
|
|
223
223
|
};
|
|
224
224
|
},
|
|
225
225
|
},
|
|
226
|
+
{
|
|
227
|
+
name: "executing → reactive-execute (parallel dispatch)",
|
|
228
|
+
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
229
|
+
if (state.phase !== "executing" || !state.activeTask)
|
|
230
|
+
return null;
|
|
231
|
+
if (!state.activeSlice)
|
|
232
|
+
return null; // fall through
|
|
233
|
+
// Only activate when reactive_execution is explicitly enabled
|
|
234
|
+
const reactiveConfig = prefs?.reactive_execution;
|
|
235
|
+
if (!reactiveConfig?.enabled)
|
|
236
|
+
return null;
|
|
237
|
+
const sid = state.activeSlice.id;
|
|
238
|
+
const sTitle = state.activeSlice.title;
|
|
239
|
+
const maxParallel = reactiveConfig.max_parallel ?? 2;
|
|
240
|
+
// Dry-run mode: max_parallel=1 means graph is derived and logged but
|
|
241
|
+
// execution remains sequential
|
|
242
|
+
if (maxParallel <= 1)
|
|
243
|
+
return null;
|
|
244
|
+
try {
|
|
245
|
+
const { loadSliceTaskIO, deriveTaskGraph, isGraphAmbiguous, getReadyTasks, chooseNonConflictingSubset, graphMetrics, } = await import("./reactive-graph.js");
|
|
246
|
+
const taskIO = await loadSliceTaskIO(basePath, mid, sid);
|
|
247
|
+
if (taskIO.length < 2)
|
|
248
|
+
return null; // single task, no point
|
|
249
|
+
const graph = deriveTaskGraph(taskIO);
|
|
250
|
+
// Ambiguous graph → fall through to sequential
|
|
251
|
+
if (isGraphAmbiguous(graph))
|
|
252
|
+
return null;
|
|
253
|
+
const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
|
|
254
|
+
const readyIds = getReadyTasks(graph, completed, new Set());
|
|
255
|
+
// Only activate reactive dispatch when >1 task is ready
|
|
256
|
+
if (readyIds.length <= 1)
|
|
257
|
+
return null;
|
|
258
|
+
const selected = chooseNonConflictingSubset(readyIds, graph, maxParallel, new Set());
|
|
259
|
+
if (selected.length <= 1)
|
|
260
|
+
return null;
|
|
261
|
+
// Log graph metrics for observability
|
|
262
|
+
const metrics = graphMetrics(graph);
|
|
263
|
+
process.stderr.write(`gsd-reactive: ${mid}/${sid} graph — tasks:${metrics.taskCount} edges:${metrics.edgeCount} ` +
|
|
264
|
+
`ready:${metrics.readySetSize} dispatching:${selected.length} ambiguous:${metrics.ambiguous}\n`);
|
|
265
|
+
// Persist dispatched batch so verification and recovery can check
|
|
266
|
+
// exactly which tasks were sent.
|
|
267
|
+
const { saveReactiveState } = await import("./reactive-graph.js");
|
|
268
|
+
saveReactiveState(basePath, mid, sid, {
|
|
269
|
+
sliceId: sid,
|
|
270
|
+
completed: [...completed],
|
|
271
|
+
dispatched: selected,
|
|
272
|
+
graphSnapshot: metrics,
|
|
273
|
+
updatedAt: new Date().toISOString(),
|
|
274
|
+
});
|
|
275
|
+
// Encode selected task IDs in unitId for artifact verification.
|
|
276
|
+
// Format: M001/S01/reactive+T02,T03
|
|
277
|
+
const batchSuffix = selected.join(",");
|
|
278
|
+
return {
|
|
279
|
+
action: "dispatch",
|
|
280
|
+
unitType: "reactive-execute",
|
|
281
|
+
unitId: `${mid}/${sid}/reactive+${batchSuffix}`,
|
|
282
|
+
prompt: await buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, selected, basePath),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
// Non-fatal — fall through to sequential execution
|
|
287
|
+
process.stderr.write(`gsd-reactive: graph derivation failed: ${err.message}\n`);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
},
|
|
226
292
|
{
|
|
227
293
|
name: "executing → execute-task (recover missing task plan → plan-slice)",
|
|
228
294
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
@@ -373,7 +373,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
373
373
|
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
374
374
|
}
|
|
375
375
|
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
376
|
-
if (incomplete.length === 0) {
|
|
376
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
377
377
|
// All milestones complete — merge milestone branch before stopping
|
|
378
378
|
if (s.currentMilestoneId) {
|
|
379
379
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
@@ -382,6 +382,12 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
382
382
|
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
|
|
383
383
|
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
384
384
|
}
|
|
385
|
+
else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
386
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
387
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
388
|
+
ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
|
|
389
|
+
await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
|
|
390
|
+
}
|
|
385
391
|
else if (state.phase === "blocked") {
|
|
386
392
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
387
393
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
@@ -171,6 +171,20 @@ export async function postUnitPreVerification(pctx) {
|
|
|
171
171
|
// Non-fatal
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
// Reactive state cleanup on slice completion
|
|
175
|
+
if (s.currentUnit.type === "complete-slice") {
|
|
176
|
+
try {
|
|
177
|
+
const parts = s.currentUnit.id.split("/");
|
|
178
|
+
const [mid, sid] = parts;
|
|
179
|
+
if (mid && sid) {
|
|
180
|
+
const { clearReactiveState } = await import("./reactive-graph.js");
|
|
181
|
+
clearReactiveState(s.basePath, mid, sid);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Non-fatal
|
|
186
|
+
}
|
|
187
|
+
}
|
|
174
188
|
// Post-triage: execute actionable resolutions
|
|
175
189
|
if (s.currentUnit.type === "triage-captures") {
|
|
176
190
|
try {
|
|
@@ -414,6 +414,35 @@ export async function getPriorTaskSummaryPaths(mid, sid, currentTid, base) {
|
|
|
414
414
|
})
|
|
415
415
|
.map(f => `${sRel}/tasks/${f}`);
|
|
416
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Get carry-forward summary paths scoped to a task's derived dependencies.
|
|
419
|
+
*
|
|
420
|
+
* Instead of all prior tasks (order-based), returns only summaries for task
|
|
421
|
+
* IDs in `dependsOn`. Used by reactive-execute to give each subagent only
|
|
422
|
+
* the context it actually needs — not sibling tasks from a parallel batch.
|
|
423
|
+
*
|
|
424
|
+
* Falls back to order-based when dependsOn is empty (root tasks still get
|
|
425
|
+
* any available prior summaries for continuity).
|
|
426
|
+
*/
|
|
427
|
+
export async function getDependencyTaskSummaryPaths(mid, sid, currentTid, dependsOn, base) {
|
|
428
|
+
// If no dependencies, fall back to order-based for root tasks
|
|
429
|
+
if (dependsOn.length === 0) {
|
|
430
|
+
return getPriorTaskSummaryPaths(mid, sid, currentTid, base);
|
|
431
|
+
}
|
|
432
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
433
|
+
if (!tDir)
|
|
434
|
+
return [];
|
|
435
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
436
|
+
const sRel = relSlicePath(base, mid, sid);
|
|
437
|
+
const depSet = new Set(dependsOn.map((d) => d.toUpperCase()));
|
|
438
|
+
return summaryFiles
|
|
439
|
+
.filter((f) => {
|
|
440
|
+
// Extract task ID from filename: "T02-SUMMARY.md" → "T02"
|
|
441
|
+
const tid = f.replace(/-SUMMARY\.md$/i, "").toUpperCase();
|
|
442
|
+
return depSet.has(tid);
|
|
443
|
+
})
|
|
444
|
+
.map((f) => `${sRel}/tasks/${f}`);
|
|
445
|
+
}
|
|
417
446
|
// ─── Adaptive Replanning Checks ────────────────────────────────────────────
|
|
418
447
|
/**
|
|
419
448
|
* Check if the most recently completed slice needs reassessment.
|
|
@@ -688,8 +717,11 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
|
|
|
688
717
|
});
|
|
689
718
|
}
|
|
690
719
|
export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, level) {
|
|
691
|
-
const
|
|
692
|
-
|
|
720
|
+
const opts = typeof level === "object" && level !== null && !Array.isArray(level)
|
|
721
|
+
? level
|
|
722
|
+
: { level: level };
|
|
723
|
+
const inlineLevel = opts.level ?? resolveInlineLevel();
|
|
724
|
+
const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
|
|
693
725
|
const priorLines = priorSummaries.length > 0
|
|
694
726
|
? priorSummaries.map(p => `- \`${p}\``).join("\n")
|
|
695
727
|
: "- (no prior tasks)";
|
|
@@ -1090,6 +1122,63 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
|
|
|
1090
1122
|
commitInstruction: reassessCommitInstruction,
|
|
1091
1123
|
});
|
|
1092
1124
|
}
|
|
1125
|
+
// ─── Reactive Execute Prompt ──────────────────────────────────────────────
|
|
1126
|
+
export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base) {
|
|
1127
|
+
const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
|
|
1128
|
+
// Build graph for context
|
|
1129
|
+
const taskIO = await loadSliceTaskIO(base, mid, sid);
|
|
1130
|
+
const graph = deriveTaskGraph(taskIO);
|
|
1131
|
+
const metrics = graphMetrics(graph);
|
|
1132
|
+
// Build graph context section
|
|
1133
|
+
const graphLines = [];
|
|
1134
|
+
for (const node of graph) {
|
|
1135
|
+
const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
|
|
1136
|
+
const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
|
|
1137
|
+
graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
|
|
1138
|
+
if (node.outputFiles.length > 0) {
|
|
1139
|
+
graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const graphContext = [
|
|
1143
|
+
`Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
|
|
1144
|
+
"",
|
|
1145
|
+
...graphLines,
|
|
1146
|
+
].join("\n");
|
|
1147
|
+
// Build individual subagent prompts for each ready task
|
|
1148
|
+
const subagentSections = [];
|
|
1149
|
+
const readyTaskListLines = [];
|
|
1150
|
+
for (const tid of readyTaskIds) {
|
|
1151
|
+
const node = graph.find((n) => n.id === tid);
|
|
1152
|
+
const tTitle = node?.title ?? tid;
|
|
1153
|
+
readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
|
|
1154
|
+
// Build dependency-scoped carry-forward paths for this task
|
|
1155
|
+
const depPaths = await getDependencyTaskSummaryPaths(mid, sid, tid, node?.dependsOn ?? [], base);
|
|
1156
|
+
// Build a full execute-task prompt with dependency-based carry-forward
|
|
1157
|
+
const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, { carryForwardPaths: depPaths });
|
|
1158
|
+
subagentSections.push([
|
|
1159
|
+
`### ${tid}: ${tTitle}`,
|
|
1160
|
+
"",
|
|
1161
|
+
"Use this as the prompt for a `subagent` call:",
|
|
1162
|
+
"",
|
|
1163
|
+
"```",
|
|
1164
|
+
taskPrompt,
|
|
1165
|
+
"```",
|
|
1166
|
+
].join("\n"));
|
|
1167
|
+
}
|
|
1168
|
+
const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
|
|
1169
|
+
return loadPrompt("reactive-execute", {
|
|
1170
|
+
workingDirectory: base,
|
|
1171
|
+
milestoneId: mid,
|
|
1172
|
+
milestoneTitle: midTitle,
|
|
1173
|
+
sliceId: sid,
|
|
1174
|
+
sliceTitle: sTitle,
|
|
1175
|
+
graphContext,
|
|
1176
|
+
readyTaskCount: String(readyTaskIds.length),
|
|
1177
|
+
readyTaskList: readyTaskListLines.join("\n"),
|
|
1178
|
+
subagentPrompts: subagentSections.join("\n\n---\n\n"),
|
|
1179
|
+
inlinedTemplates,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1093
1182
|
export async function buildRewriteDocsPrompt(mid, midTitle, activeSlice, base, overrides) {
|
|
1094
1183
|
const sid = activeSlice?.id;
|
|
1095
1184
|
const sTitle = activeSlice?.title ?? "";
|
|
@@ -11,7 +11,7 @@ import { clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
|
11
11
|
import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
|
|
12
12
|
import { isValidationTerminal } from "./state.js";
|
|
13
13
|
import { nativeConflictFiles, nativeCommit, nativeCheckoutTheirs, nativeAddPaths, nativeMergeAbort, nativeResetHard, } from "./native-git-bridge.js";
|
|
14
|
-
import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
|
|
14
|
+
import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, resolveTaskFiles, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
|
|
15
15
|
import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from "node:fs";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
@@ -73,6 +73,9 @@ export function resolveExpectedArtifactPath(unitType, unitId, base) {
|
|
|
73
73
|
}
|
|
74
74
|
case "rewrite-docs":
|
|
75
75
|
return null;
|
|
76
|
+
case "reactive-execute":
|
|
77
|
+
// Reactive execute produces multiple task summaries — verified separately
|
|
78
|
+
return null;
|
|
76
79
|
default:
|
|
77
80
|
return null;
|
|
78
81
|
}
|
|
@@ -105,6 +108,39 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
|
|
|
105
108
|
const content = readFileSync(overridesPath, "utf-8");
|
|
106
109
|
return !content.includes("**Scope:** active");
|
|
107
110
|
}
|
|
111
|
+
// Reactive-execute: verify that each dispatched task's summary exists.
|
|
112
|
+
// The unitId encodes the batch: "{mid}/{sid}/reactive+T02,T03"
|
|
113
|
+
if (unitType === "reactive-execute") {
|
|
114
|
+
const parts = unitId.split("/");
|
|
115
|
+
const mid = parts[0];
|
|
116
|
+
const sidAndBatch = parts[1];
|
|
117
|
+
const batchPart = parts[2]; // "reactive+T02,T03"
|
|
118
|
+
if (!mid || !sidAndBatch || !batchPart)
|
|
119
|
+
return false;
|
|
120
|
+
const sid = sidAndBatch;
|
|
121
|
+
const plusIdx = batchPart.indexOf("+");
|
|
122
|
+
if (plusIdx === -1) {
|
|
123
|
+
// Legacy format "reactive" without batch IDs — fall back to "any summary"
|
|
124
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
125
|
+
if (!tDir)
|
|
126
|
+
return false;
|
|
127
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
128
|
+
return summaryFiles.length > 0;
|
|
129
|
+
}
|
|
130
|
+
const batchIds = batchPart.slice(plusIdx + 1).split(",").filter(Boolean);
|
|
131
|
+
if (batchIds.length === 0)
|
|
132
|
+
return false;
|
|
133
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
134
|
+
if (!tDir)
|
|
135
|
+
return false;
|
|
136
|
+
const existingSummaries = new Set(resolveTaskFiles(tDir, "SUMMARY").map((f) => f.replace(/-SUMMARY\.md$/i, "").toUpperCase()));
|
|
137
|
+
// Every dispatched task must have a summary file
|
|
138
|
+
for (const tid of batchIds) {
|
|
139
|
+
if (!existingSummaries.has(tid.toUpperCase()))
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
108
144
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
109
145
|
// For unit types with no verifiable artifact (null path), the parent directory
|
|
110
146
|
// is missing on disk — treat as stale completion state so the key gets evicted (#313).
|
|
@@ -303,11 +303,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
303
303
|
// ── Auto-worktree setup ──
|
|
304
304
|
s.originalBasePath = base;
|
|
305
305
|
const isUnderGsdWorktrees = (p) => {
|
|
306
|
+
// Direct layout: /.gsd/worktrees/
|
|
306
307
|
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
307
308
|
if (p.includes(marker))
|
|
308
309
|
return true;
|
|
309
310
|
const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
|
|
310
|
-
|
|
311
|
+
if (p.endsWith(worktreesSuffix))
|
|
312
|
+
return true;
|
|
313
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
314
|
+
const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`);
|
|
315
|
+
return symlinkRe.test(p);
|
|
311
316
|
};
|
|
312
317
|
if (s.currentMilestoneId &&
|
|
313
318
|
shouldUseWorktreeIsolation() &&
|
|
@@ -115,10 +115,17 @@ export function checkResourcesStale(versionOnStart) {
|
|
|
115
115
|
* Returns the corrected base path.
|
|
116
116
|
*/
|
|
117
117
|
export function escapeStaleWorktree(base) {
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// Direct layout: /.gsd/worktrees/
|
|
119
|
+
const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
120
|
+
let idx = base.indexOf(directMarker);
|
|
121
|
+
if (idx === -1) {
|
|
122
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
123
|
+
const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`);
|
|
124
|
+
const match = base.match(symlinkRe);
|
|
125
|
+
if (!match || match.index === undefined)
|
|
126
|
+
return base;
|
|
127
|
+
idx = match.index;
|
|
128
|
+
}
|
|
122
129
|
// base is inside .gsd/worktrees/<something> — extract the project root
|
|
123
130
|
const projectRoot = base.slice(0, idx);
|
|
124
131
|
try {
|
|
@@ -30,8 +30,16 @@ const VALID_CLASSIFICATIONS = [
|
|
|
30
30
|
*/
|
|
31
31
|
export function resolveCapturesPath(basePath) {
|
|
32
32
|
const resolved = resolve(basePath);
|
|
33
|
+
// Direct layout: /.gsd/worktrees/
|
|
33
34
|
const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
34
|
-
|
|
35
|
+
let idx = resolved.indexOf(worktreeMarker);
|
|
36
|
+
if (idx === -1) {
|
|
37
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
38
|
+
const symlinkRe = new RegExp(`\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`);
|
|
39
|
+
const match = resolved.match(symlinkRe);
|
|
40
|
+
if (match && match.index !== undefined)
|
|
41
|
+
idx = match.index;
|
|
42
|
+
}
|
|
35
43
|
if (idx !== -1) {
|
|
36
44
|
// basePath is inside a worktree — resolve to project root
|
|
37
45
|
const projectRoot = resolved.slice(0, idx);
|
|
@@ -10,7 +10,7 @@ import { deriveState } from "./state.js";
|
|
|
10
10
|
import { gsdRoot } from "./paths.js";
|
|
11
11
|
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
12
12
|
import { appendOverride, appendKnowledge } from "./files.js";
|
|
13
|
-
import { formatDoctorIssuesForPrompt, formatDoctorReport, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
|
|
13
|
+
import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
|
|
14
14
|
import { isAutoActive } from "./auto.js";
|
|
15
15
|
import { projectRoot } from "./commands.js";
|
|
16
16
|
import { loadPrompt } from "./prompt-loader.js";
|
|
@@ -28,15 +28,28 @@ export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
|
28
28
|
}
|
|
29
29
|
export async function handleDoctor(args, ctx, pi) {
|
|
30
30
|
const trimmed = args.trim();
|
|
31
|
-
|
|
31
|
+
// Extract flags before positional parsing
|
|
32
|
+
const jsonMode = trimmed.includes("--json");
|
|
33
|
+
const dryRun = trimmed.includes("--dry-run");
|
|
34
|
+
const includeBuild = trimmed.includes("--build");
|
|
35
|
+
const includeTests = trimmed.includes("--test");
|
|
36
|
+
const stripped = trimmed.replace(/--json|--dry-run|--build|--test/g, "").trim();
|
|
37
|
+
const parts = stripped ? stripped.split(/\s+/) : [];
|
|
32
38
|
const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
|
|
33
39
|
const requestedScope = mode === "doctor" ? parts[0] : parts[1];
|
|
34
40
|
const scope = await selectDoctorScope(projectRoot(), requestedScope);
|
|
35
41
|
const effectiveScope = mode === "audit" ? requestedScope : scope;
|
|
36
42
|
const report = await runGSDDoctor(projectRoot(), {
|
|
37
|
-
fix: mode === "fix" || mode === "heal",
|
|
43
|
+
fix: mode === "fix" || mode === "heal" || dryRun,
|
|
44
|
+
dryRun,
|
|
38
45
|
scope: effectiveScope,
|
|
46
|
+
includeBuild,
|
|
47
|
+
includeTests,
|
|
39
48
|
});
|
|
49
|
+
if (jsonMode) {
|
|
50
|
+
ctx.ui.notify(formatDoctorReportJson(report), "info");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
40
53
|
const reportText = formatDoctorReport(report, {
|
|
41
54
|
scope: effectiveScope,
|
|
42
55
|
includeWarnings: mode === "audit",
|
|
@@ -135,7 +135,7 @@ async function guardRemoteSession(ctx, pi) {
|
|
|
135
135
|
}
|
|
136
136
|
export function registerGSDCommand(pi) {
|
|
137
137
|
pi.registerCommand("gsd", {
|
|
138
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
138
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|capture|triage|dispatch|history|undo|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update",
|
|
139
139
|
getArgumentCompletions: (prefix) => {
|
|
140
140
|
const subcommands = [
|
|
141
141
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -186,7 +186,11 @@ export function registerGSDCommand(pi) {
|
|
|
186
186
|
{ cmd: "templates", desc: "List available workflow templates" },
|
|
187
187
|
{ cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
|
|
188
188
|
];
|
|
189
|
+
const hasTrailingSpace = prefix.endsWith(" ");
|
|
189
190
|
const parts = prefix.trim().split(/\s+/);
|
|
191
|
+
if (hasTrailingSpace && parts.length >= 1) {
|
|
192
|
+
parts.push("");
|
|
193
|
+
}
|
|
190
194
|
if (parts.length <= 1) {
|
|
191
195
|
return subcommands
|
|
192
196
|
.filter((item) => item.cmd.startsWith(parts[0] ?? ""))
|
|
@@ -468,6 +472,10 @@ export function registerGSDCommand(pi) {
|
|
|
468
472
|
{ cmd: "fix", desc: "Auto-fix detected issues" },
|
|
469
473
|
{ cmd: "heal", desc: "AI-driven deep healing" },
|
|
470
474
|
{ cmd: "audit", desc: "Run health audit without fixing" },
|
|
475
|
+
{ cmd: "--dry-run", desc: "Show what --fix would change without applying" },
|
|
476
|
+
{ cmd: "--json", desc: "Output report as JSON (CI/tooling friendly)" },
|
|
477
|
+
{ cmd: "--build", desc: "Include slow build health check (npm run build)" },
|
|
478
|
+
{ cmd: "--test", desc: "Include slow test health check (npm test)" },
|
|
471
479
|
];
|
|
472
480
|
if (parts.length <= 2) {
|
|
473
481
|
return modes
|
|
@@ -491,6 +499,17 @@ export function registerGSDCommand(pi) {
|
|
|
491
499
|
.filter((p) => p.cmd.startsWith(phasePrefix))
|
|
492
500
|
.map((p) => ({ value: `dispatch ${p.cmd}`, label: p.cmd, description: p.desc }));
|
|
493
501
|
}
|
|
502
|
+
if (parts[0] === "rate" && parts.length <= 2) {
|
|
503
|
+
const tierPrefix = parts[1] ?? "";
|
|
504
|
+
const tiers = [
|
|
505
|
+
{ cmd: "over", desc: "Model was overqualified for this task" },
|
|
506
|
+
{ cmd: "ok", desc: "Model was appropriate for this task" },
|
|
507
|
+
{ cmd: "under", desc: "Model was underqualified for this task" },
|
|
508
|
+
];
|
|
509
|
+
return tiers
|
|
510
|
+
.filter((t) => t.cmd.startsWith(tierPrefix))
|
|
511
|
+
.map((t) => ({ value: `rate ${t.cmd}`, label: t.cmd, description: t.desc }));
|
|
512
|
+
}
|
|
494
513
|
return [];
|
|
495
514
|
},
|
|
496
515
|
async handler(args, ctx) {
|