gsd-pi 2.37.1 → 2.38.0-dev.e40f839
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-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 +13 -5
- 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.js +22 -2
- package/dist/resources/extensions/gsd/detection.js +1 -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 +35 -1
- 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-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
- package/dist/resources/extensions/gsd/preferences.js +4 -3
- 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/repo-identity.js +2 -1
- 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-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 +15 -4
- 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.ts +24 -2
- package/src/resources/extensions/gsd/detection.ts +2 -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 +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/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-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
- package/src/resources/extensions/gsd/preferences.ts +5 -3
- 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/repo-identity.ts +3 -1
- 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/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/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
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
5
|
-
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
|
5
|
+
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
|
|
6
6
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
7
7
|
import { invalidateAllCaches } from "./cache.js";
|
|
8
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
9
9
|
|
|
10
|
-
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
|
|
10
|
+
import type { DoctorIssue, DoctorIssueCode, DoctorReport } from "./doctor-types.js";
|
|
11
11
|
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
12
|
+
import type { RoadmapSliceEntry } from "./types.js";
|
|
12
13
|
import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
|
|
13
14
|
import { checkEnvironmentHealth } from "./doctor-environment.js";
|
|
14
15
|
import { runProviderChecks } from "./doctor-providers.js";
|
|
@@ -17,7 +18,7 @@ import { runProviderChecks } from "./doctor-providers.js";
|
|
|
17
18
|
// All public types and functions from extracted modules are re-exported here
|
|
18
19
|
// so that existing imports from "./doctor.js" continue to work unchanged.
|
|
19
20
|
export type { DoctorSeverity, DoctorIssueCode, DoctorIssue, DoctorReport, DoctorSummary } from "./doctor-types.js";
|
|
20
|
-
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt } from "./doctor-format.js";
|
|
21
|
+
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt, formatDoctorReportJson } from "./doctor-format.js";
|
|
21
22
|
export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport, type EnvironmentCheckResult } from "./doctor-environment.js";
|
|
22
23
|
export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport, type ProgressScore, type ProgressLevel } from "./progress-score.js";
|
|
23
24
|
|
|
@@ -350,10 +351,60 @@ export async function selectDoctorScope(basePath: string, requestedScope?: strin
|
|
|
350
351
|
return state.registry[0]?.id;
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
|
|
354
|
+
// ── Helper: circular dependency detection ──────────────────────────────────
|
|
355
|
+
function detectCircularDependencies(slices: RoadmapSliceEntry[]): string[][] {
|
|
356
|
+
const known = new Set(slices.map(s => s.id));
|
|
357
|
+
const adj = new Map<string, string[]>();
|
|
358
|
+
for (const s of slices) adj.set(s.id, s.depends.filter(d => known.has(d)));
|
|
359
|
+
const state = new Map<string, "unvisited" | "visiting" | "done">();
|
|
360
|
+
for (const s of slices) state.set(s.id, "unvisited");
|
|
361
|
+
const cycles: string[][] = [];
|
|
362
|
+
function dfs(id: string, path: string[]): void {
|
|
363
|
+
const st = state.get(id);
|
|
364
|
+
if (st === "done") return;
|
|
365
|
+
if (st === "visiting") { cycles.push([...path.slice(path.indexOf(id)), id]); return; }
|
|
366
|
+
state.set(id, "visiting");
|
|
367
|
+
for (const dep of adj.get(id) ?? []) dfs(dep, [...path, id]);
|
|
368
|
+
state.set(id, "done");
|
|
369
|
+
}
|
|
370
|
+
for (const s of slices) if (state.get(s.id) === "unvisited") dfs(s.id, []);
|
|
371
|
+
return cycles;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Helper: doctor run history ──────────────────────────────────────────────
|
|
375
|
+
interface DoctorHistoryEntry { ts: string; ok: boolean; errors: number; warnings: number; fixes: number; codes: string[] }
|
|
376
|
+
|
|
377
|
+
async function appendDoctorHistory(basePath: string, report: DoctorReport): Promise<void> {
|
|
378
|
+
try {
|
|
379
|
+
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
380
|
+
const entry = JSON.stringify({
|
|
381
|
+
ts: new Date().toISOString(),
|
|
382
|
+
ok: report.ok,
|
|
383
|
+
errors: report.issues.filter(i => i.severity === "error").length,
|
|
384
|
+
warnings: report.issues.filter(i => i.severity === "warning").length,
|
|
385
|
+
fixes: report.fixesApplied.length,
|
|
386
|
+
codes: [...new Set(report.issues.map(i => i.code))],
|
|
387
|
+
} satisfies DoctorHistoryEntry);
|
|
388
|
+
const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
|
|
389
|
+
await saveFile(historyPath, existing + entry + "\n");
|
|
390
|
+
} catch { /* non-fatal */ }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Read the last N doctor history entries. Returns most-recent-first. */
|
|
394
|
+
export async function readDoctorHistory(basePath: string, lastN = 50): Promise<DoctorHistoryEntry[]> {
|
|
395
|
+
try {
|
|
396
|
+
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
397
|
+
if (!existsSync(historyPath)) return [];
|
|
398
|
+
const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
|
|
399
|
+
return lines.slice(-lastN).reverse().map(l => JSON.parse(l) as DoctorHistoryEntry);
|
|
400
|
+
} catch { return []; }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; dryRun?: boolean; scope?: string; fixLevel?: "task" | "all"; isolationMode?: "none" | "worktree" | "branch"; includeBuild?: boolean; includeTests?: boolean }): Promise<DoctorReport> {
|
|
354
404
|
const issues: DoctorIssue[] = [];
|
|
355
405
|
const fixesApplied: string[] = [];
|
|
356
406
|
const fix = options?.fix === true;
|
|
407
|
+
const dryRun = options?.dryRun === true;
|
|
357
408
|
const fixLevel = options?.fixLevel ?? "all";
|
|
358
409
|
|
|
359
410
|
// Issue codes that represent completion state transitions — creating summary
|
|
@@ -364,11 +415,18 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
364
415
|
|
|
365
416
|
/** Whether a given issue code should be auto-fixed at the current fixLevel. */
|
|
366
417
|
const shouldFix = (code: DoctorIssueCode): boolean => {
|
|
367
|
-
if (!fix) return false;
|
|
418
|
+
if (!fix || dryRun) return false;
|
|
368
419
|
if (fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code)) return false;
|
|
369
420
|
return true;
|
|
370
421
|
};
|
|
371
422
|
|
|
423
|
+
/** Log a dry-run "would fix" entry when fix=true but dryRun=true. */
|
|
424
|
+
const dryRunCanFix = (code: DoctorIssueCode, message: string): void => {
|
|
425
|
+
if (dryRun && fix && !(fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))) {
|
|
426
|
+
fixesApplied.push(`[dry-run] would fix: ${message}`);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
372
430
|
const prefs = loadEffectiveGSDPreferences();
|
|
373
431
|
if (prefs) {
|
|
374
432
|
const prefIssues = validatePreferenceShape(prefs.preferences);
|
|
@@ -385,21 +443,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
385
443
|
}
|
|
386
444
|
}
|
|
387
445
|
|
|
388
|
-
// Git health checks
|
|
446
|
+
// Git health checks — timed
|
|
447
|
+
const t0git = Date.now();
|
|
389
448
|
const isolationMode: "none" | "worktree" | "branch" = options?.isolationMode ??
|
|
390
449
|
(prefs?.preferences?.git?.isolation === "none" ? "none" :
|
|
391
450
|
prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree");
|
|
392
451
|
await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode);
|
|
452
|
+
const gitMs = Date.now() - t0git;
|
|
393
453
|
|
|
394
|
-
// Runtime health checks
|
|
454
|
+
// Runtime health checks — timed
|
|
455
|
+
const t0runtime = Date.now();
|
|
395
456
|
await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
|
|
457
|
+
const runtimeMs = Date.now() - t0runtime;
|
|
396
458
|
|
|
397
|
-
// Environment health checks
|
|
398
|
-
|
|
459
|
+
// Environment health checks — timed
|
|
460
|
+
const t0env = Date.now();
|
|
461
|
+
await checkEnvironmentHealth(basePath, issues, {
|
|
462
|
+
includeRemote: !options?.scope,
|
|
463
|
+
includeBuild: options?.includeBuild,
|
|
464
|
+
includeTests: options?.includeTests,
|
|
465
|
+
});
|
|
466
|
+
const envMs = Date.now() - t0env;
|
|
399
467
|
|
|
400
468
|
const milestonesPath = milestonesDir(basePath);
|
|
401
469
|
if (!existsSync(milestonesPath)) {
|
|
402
|
-
|
|
470
|
+
const report: DoctorReport = { ok: issues.every(i => i.severity !== "error"), basePath, issues, fixesApplied, timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: 0 } };
|
|
471
|
+
await appendDoctorHistory(basePath, report);
|
|
472
|
+
return report;
|
|
403
473
|
}
|
|
404
474
|
|
|
405
475
|
const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
|
|
@@ -465,6 +535,43 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
465
535
|
if (!roadmapContent) continue;
|
|
466
536
|
const roadmap = parseRoadmap(roadmapContent);
|
|
467
537
|
|
|
538
|
+
// ── Circular dependency detection ──────────────────────────────────────
|
|
539
|
+
for (const cycle of detectCircularDependencies(roadmap.slices)) {
|
|
540
|
+
issues.push({
|
|
541
|
+
severity: "error",
|
|
542
|
+
code: "circular_slice_dependency",
|
|
543
|
+
scope: "milestone",
|
|
544
|
+
unitId: milestoneId,
|
|
545
|
+
message: `Circular dependency detected: ${cycle.join(" → ")}`,
|
|
546
|
+
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
547
|
+
fixable: false,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── Orphaned slice directories ─────────────────────────────────────────
|
|
552
|
+
try {
|
|
553
|
+
const slicesDir = join(milestonePath, "slices");
|
|
554
|
+
if (existsSync(slicesDir)) {
|
|
555
|
+
const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
|
|
556
|
+
for (const entry of readdirSync(slicesDir)) {
|
|
557
|
+
try {
|
|
558
|
+
if (!lstatSync(join(slicesDir, entry)).isDirectory()) continue;
|
|
559
|
+
} catch { continue; }
|
|
560
|
+
if (!knownSliceIds.has(entry)) {
|
|
561
|
+
issues.push({
|
|
562
|
+
severity: "warning",
|
|
563
|
+
code: "orphaned_slice_directory",
|
|
564
|
+
scope: "milestone",
|
|
565
|
+
unitId: milestoneId,
|
|
566
|
+
message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap`,
|
|
567
|
+
file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
|
|
568
|
+
fixable: false,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
} catch { /* non-fatal */ }
|
|
574
|
+
|
|
468
575
|
for (const slice of roadmap.slices) {
|
|
469
576
|
const unitId = `${milestoneId}/${slice.id}`;
|
|
470
577
|
if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId) continue;
|
|
@@ -539,6 +646,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
539
646
|
continue;
|
|
540
647
|
}
|
|
541
648
|
|
|
649
|
+
// ── Duplicate task IDs ───────────────────────────────────────────────
|
|
650
|
+
const taskIdCounts = new Map<string, number>();
|
|
651
|
+
for (const task of plan.tasks) taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
|
|
652
|
+
for (const [taskId, count] of taskIdCounts) {
|
|
653
|
+
if (count > 1) {
|
|
654
|
+
issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
|
|
655
|
+
message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
|
|
656
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ── Task files on disk not in plan ────────────────────────────────────
|
|
661
|
+
try {
|
|
662
|
+
if (tasksDir) {
|
|
663
|
+
const planTaskIds = new Set(plan.tasks.map(t => t.id));
|
|
664
|
+
for (const f of readdirSync(tasksDir)) {
|
|
665
|
+
if (!f.endsWith("-SUMMARY.md")) continue;
|
|
666
|
+
const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
|
|
667
|
+
if (!planTaskIds.has(diskTaskId)) {
|
|
668
|
+
issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
|
|
669
|
+
message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
|
|
670
|
+
file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} catch { /* non-fatal */ }
|
|
675
|
+
|
|
542
676
|
let allTasksDone = plan.tasks.length > 0;
|
|
543
677
|
for (const task of plan.tasks) {
|
|
544
678
|
const taskUnitId = `${unitId}/${task.id}`;
|
|
@@ -555,6 +689,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
555
689
|
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
556
690
|
fixable: true,
|
|
557
691
|
});
|
|
692
|
+
dryRunCanFix("task_done_missing_summary", `create stub summary for ${taskUnitId}`);
|
|
558
693
|
if (shouldFix("task_done_missing_summary")) {
|
|
559
694
|
const stubPath = join(
|
|
560
695
|
basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
|
|
@@ -618,6 +753,22 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
618
753
|
}
|
|
619
754
|
}
|
|
620
755
|
|
|
756
|
+
// ── Future timestamp check ─────────────────────────────────────
|
|
757
|
+
if (task.done && hasSummary && summaryPath) {
|
|
758
|
+
try {
|
|
759
|
+
const rawSummary = await loadFile(summaryPath);
|
|
760
|
+
const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
|
|
761
|
+
if (m) {
|
|
762
|
+
const ts = new Date(m[1].trim());
|
|
763
|
+
if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
|
|
764
|
+
issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
|
|
765
|
+
message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
|
|
766
|
+
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} catch { /* non-fatal */ }
|
|
770
|
+
}
|
|
771
|
+
|
|
621
772
|
allTasksDone = allTasksDone && task.done;
|
|
622
773
|
}
|
|
623
774
|
|
|
@@ -646,6 +797,13 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
646
797
|
}
|
|
647
798
|
}
|
|
648
799
|
|
|
800
|
+
// ── Stale REPLAN: exists but all tasks done ────────────────────────
|
|
801
|
+
if (replanPath && allTasksDone) {
|
|
802
|
+
issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
|
|
803
|
+
message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
|
|
804
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
|
|
805
|
+
}
|
|
806
|
+
|
|
649
807
|
const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
|
|
650
808
|
const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
|
|
651
809
|
const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
|
|
@@ -661,6 +819,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
661
819
|
file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
|
|
662
820
|
fixable: true,
|
|
663
821
|
});
|
|
822
|
+
dryRunCanFix("all_tasks_done_missing_slice_summary", `create placeholder summary for ${unitId}`);
|
|
664
823
|
if (shouldFix("all_tasks_done_missing_slice_summary")) await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
665
824
|
}
|
|
666
825
|
|
|
@@ -674,6 +833,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
674
833
|
file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
|
|
675
834
|
fixable: true,
|
|
676
835
|
});
|
|
836
|
+
dryRunCanFix("all_tasks_done_missing_slice_uat", `create placeholder UAT for ${unitId}`);
|
|
677
837
|
if (shouldFix("all_tasks_done_missing_slice_uat")) await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
678
838
|
}
|
|
679
839
|
|
|
@@ -687,6 +847,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
687
847
|
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
688
848
|
fixable: true,
|
|
689
849
|
});
|
|
850
|
+
dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
|
|
690
851
|
if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
|
|
691
852
|
await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
692
853
|
}
|
|
@@ -744,14 +905,17 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
744
905
|
}
|
|
745
906
|
}
|
|
746
907
|
|
|
747
|
-
if (fix && fixesApplied.length > 0) {
|
|
908
|
+
if (fix && !dryRun && fixesApplied.length > 0) {
|
|
748
909
|
await updateStateFile(basePath, fixesApplied);
|
|
749
910
|
}
|
|
750
911
|
|
|
751
|
-
|
|
912
|
+
const report: DoctorReport = {
|
|
752
913
|
ok: issues.every(issue => issue.severity !== "error"),
|
|
753
914
|
basePath,
|
|
754
915
|
issues,
|
|
755
916
|
fixesApplied,
|
|
917
|
+
timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: Math.max(0, Date.now() - t0env - envMs) },
|
|
756
918
|
};
|
|
919
|
+
await appendDoctorHistory(basePath, report);
|
|
920
|
+
return report;
|
|
757
921
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./metrics.js";
|
|
12
12
|
import type { UnitMetrics } from "./metrics.js";
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
14
|
-
import { formatDuration, fileLink } from "../shared/
|
|
14
|
+
import { formatDuration, fileLink } from "../shared/format-utils.js";
|
|
15
15
|
import { getErrorMessage } from "./error-utils.js";
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -7,7 +7,7 @@ import { promises as fs } from 'node:fs';
|
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { atomicWriteAsync } from './atomic-write.js';
|
|
9
9
|
import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
|
|
10
|
-
import { milestoneIdSort, findMilestoneIds } from './
|
|
10
|
+
import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
13
|
Roadmap, BoundaryMapEntry,
|
|
@@ -15,11 +15,12 @@ import type {
|
|
|
15
15
|
Summary, SummaryFrontmatter, SummaryRequires, FileModified,
|
|
16
16
|
Continue, ContinueFrontmatter, ContinueStatus,
|
|
17
17
|
RequirementCounts,
|
|
18
|
+
TaskIO,
|
|
18
19
|
SecretsManifest, SecretsManifestEntry, SecretsManifestEntryStatus,
|
|
19
20
|
ManifestStatus,
|
|
20
21
|
} from './types.js';
|
|
21
22
|
|
|
22
|
-
import { checkExistingEnvKeys } from '../
|
|
23
|
+
import { checkExistingEnvKeys } from '../env-utils.js';
|
|
23
24
|
import { parseRoadmapSlices } from './roadmap-slices.js';
|
|
24
25
|
import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
|
|
25
26
|
import { debugTime, debugCount } from './debug-logger.js';
|
|
@@ -724,6 +725,50 @@ export function countMustHavesMentionedInSummary(
|
|
|
724
725
|
return count;
|
|
725
726
|
}
|
|
726
727
|
|
|
728
|
+
// ─── Task Plan IO Extractor ────────────────────────────────────────────────
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Extract input and output file paths from a task plan's `## Inputs` and
|
|
732
|
+
* `## Expected Output` sections. Looks for backtick-wrapped file paths on
|
|
733
|
+
* each line (e.g. `` `src/foo.ts` ``).
|
|
734
|
+
*
|
|
735
|
+
* Returns empty arrays for missing/empty sections — callers should treat
|
|
736
|
+
* tasks with no IO as ambiguous (sequential fallback trigger).
|
|
737
|
+
*/
|
|
738
|
+
export function parseTaskPlanIO(content: string): { inputFiles: string[]; outputFiles: string[] } {
|
|
739
|
+
const backtickPathRegex = /`([^`]+)`/g;
|
|
740
|
+
|
|
741
|
+
function extractPaths(sectionText: string | null): string[] {
|
|
742
|
+
if (!sectionText) return [];
|
|
743
|
+
const paths: string[] = [];
|
|
744
|
+
for (const line of sectionText.split("\n")) {
|
|
745
|
+
const trimmed = line.trim();
|
|
746
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
747
|
+
let match: RegExpExecArray | null;
|
|
748
|
+
backtickPathRegex.lastIndex = 0;
|
|
749
|
+
while ((match = backtickPathRegex.exec(trimmed)) !== null) {
|
|
750
|
+
const candidate = match[1];
|
|
751
|
+
// Filter out things that look like code tokens rather than file paths
|
|
752
|
+
// (e.g. `true`, `false`, `npm run test`). A file path has at least one
|
|
753
|
+
// dot or slash.
|
|
754
|
+
if (candidate.includes("/") || candidate.includes(".")) {
|
|
755
|
+
paths.push(candidate);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return paths;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const [, body] = splitFrontmatter(content);
|
|
763
|
+
const inputSection = extractSection(body, "Inputs");
|
|
764
|
+
const outputSection = extractSection(body, "Expected Output");
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
inputFiles: extractPaths(inputSection),
|
|
768
|
+
outputFiles: extractPaths(outputSection),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
727
772
|
// ─── UAT Type Extractor ────────────────────────────────────────────────────
|
|
728
773
|
|
|
729
774
|
/**
|
|
@@ -27,7 +27,7 @@ import { deriveState } from "./state.js";
|
|
|
27
27
|
import { isAutoActive } from "./auto.js";
|
|
28
28
|
import { loadPrompt } from "./prompt-loader.js";
|
|
29
29
|
import { gsdRoot } from "./paths.js";
|
|
30
|
-
import { formatDuration } from "../shared/
|
|
30
|
+
import { formatDuration } from "../shared/format-utils.js";
|
|
31
31
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
32
32
|
|
|
33
33
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -60,6 +60,8 @@ import { join } from "node:path";
|
|
|
60
60
|
import { existsSync, readFileSync } from "node:fs";
|
|
61
61
|
import { homedir } from "node:os";
|
|
62
62
|
import { shortcutDesc } from "../shared/mod.js";
|
|
63
|
+
|
|
64
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
63
65
|
import { Text } from "@gsd/pi-tui";
|
|
64
66
|
import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
|
|
65
67
|
import { toPosixPath } from "../shared/mod.js";
|
|
@@ -73,7 +75,7 @@ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js"
|
|
|
73
75
|
|
|
74
76
|
function warnDeprecatedAgentInstructions(): void {
|
|
75
77
|
const paths = [
|
|
76
|
-
join(
|
|
78
|
+
join(gsdHome, "agent-instructions.md"),
|
|
77
79
|
join(process.cwd(), ".gsd", "agent-instructions.md"),
|
|
78
80
|
];
|
|
79
81
|
for (const p of paths) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Zero Pi dependencies — uses only exported helpers from files.ts.
|
|
4
4
|
|
|
5
5
|
import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.js';
|
|
6
|
-
import { normalizeStringArray } from '../../shared/
|
|
6
|
+
import { normalizeStringArray } from '../../shared/format-utils.js';
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
9
|
PlanningRoadmap,
|
|
@@ -235,6 +235,33 @@ export function validateTaskPlanContent(file: string, content: string): Validati
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// Rule: Inputs and Expected Output should contain backtick-wrapped file paths
|
|
239
|
+
const inputsSection = getSection(content, "Inputs", 2);
|
|
240
|
+
const outputSection = getSection(content, "Expected Output", 2);
|
|
241
|
+
const backtickPathPattern = /`[^`]*[./][^`]*`/;
|
|
242
|
+
|
|
243
|
+
if (outputSection === null || !backtickPathPattern.test(outputSection)) {
|
|
244
|
+
issues.push({
|
|
245
|
+
severity: "warning",
|
|
246
|
+
scope: "task-plan",
|
|
247
|
+
file,
|
|
248
|
+
ruleId: "missing_output_file_paths",
|
|
249
|
+
message: "Task plan `## Expected Output` is missing or has no backtick-wrapped file paths.",
|
|
250
|
+
suggestion: "List concrete output file paths in backticks (e.g. `src/types.ts`). These are machine-parsed to derive task dependencies.",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (inputsSection !== null && inputsSection.trim().length > 0 && !backtickPathPattern.test(inputsSection)) {
|
|
255
|
+
issues.push({
|
|
256
|
+
severity: "info",
|
|
257
|
+
scope: "task-plan",
|
|
258
|
+
file,
|
|
259
|
+
ruleId: "missing_input_file_paths",
|
|
260
|
+
message: "Task plan `## Inputs` has content but no backtick-wrapped file paths.",
|
|
261
|
+
suggestion: "List input file paths in backticks (e.g. `src/config.json`). These are machine-parsed to derive task dependencies.",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
238
265
|
// ── Observability rules (gated by runtime relevance) ──
|
|
239
266
|
|
|
240
267
|
const relevant = textSuggestsObservabilityRelevant(content);
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
ParallelConfig,
|
|
19
19
|
CompressionStrategy,
|
|
20
20
|
ContextSelectionMode,
|
|
21
|
+
ReactiveExecutionConfig,
|
|
21
22
|
} from "./types.js";
|
|
22
23
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
23
24
|
|
|
@@ -86,12 +87,13 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
86
87
|
"compression_strategy",
|
|
87
88
|
"context_selection",
|
|
88
89
|
"widget_mode",
|
|
90
|
+
"reactive_execution",
|
|
89
91
|
]);
|
|
90
92
|
|
|
91
93
|
/** Canonical list of all dispatch unit types. */
|
|
92
94
|
export const KNOWN_UNIT_TYPES = [
|
|
93
95
|
"research-milestone", "plan-milestone", "research-slice", "plan-slice",
|
|
94
|
-
"execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
96
|
+
"execute-task", "reactive-execute", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
95
97
|
"run-uat", "complete-milestone",
|
|
96
98
|
] as const;
|
|
97
99
|
export type UnitType = (typeof KNOWN_UNIT_TYPES)[number];
|
|
@@ -215,6 +217,8 @@ export interface GSDPreferences {
|
|
|
215
217
|
context_selection?: ContextSelectionMode;
|
|
216
218
|
/** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */
|
|
217
219
|
widget_mode?: "full" | "small" | "min" | "off";
|
|
220
|
+
/** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
|
|
221
|
+
reactive_execution?: ReactiveExecutionConfig;
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
export interface LoadedGSDPreferences {
|
|
@@ -10,7 +10,7 @@ import type { GitPreferences } from "./git-service.js";
|
|
|
10
10
|
import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile, PhaseSkipPreferences } from "./types.js";
|
|
11
11
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
12
12
|
import { VALID_BRANCH_NAME } from "./git-service.js";
|
|
13
|
-
import { normalizeStringArray } from "../shared/
|
|
13
|
+
import { normalizeStringArray } from "../shared/format-utils.js";
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
KNOWN_PREFERENCE_KEYS,
|
|
@@ -496,6 +496,47 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
// ─── Reactive Execution ─────────────────────────────────────────────────
|
|
500
|
+
if (preferences.reactive_execution !== undefined) {
|
|
501
|
+
if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
|
|
502
|
+
const re = preferences.reactive_execution as unknown as Record<string, unknown>;
|
|
503
|
+
const validRe: Record<string, unknown> = {};
|
|
504
|
+
|
|
505
|
+
if (re.enabled !== undefined) {
|
|
506
|
+
if (typeof re.enabled === "boolean") validRe.enabled = re.enabled;
|
|
507
|
+
else errors.push("reactive_execution.enabled must be a boolean");
|
|
508
|
+
}
|
|
509
|
+
if (re.max_parallel !== undefined) {
|
|
510
|
+
const mp = typeof re.max_parallel === "number" ? re.max_parallel : Number(re.max_parallel);
|
|
511
|
+
if (Number.isFinite(mp) && mp >= 1 && mp <= 8) {
|
|
512
|
+
validRe.max_parallel = Math.floor(mp);
|
|
513
|
+
} else {
|
|
514
|
+
errors.push("reactive_execution.max_parallel must be a number between 1 and 8");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (re.isolation_mode !== undefined) {
|
|
518
|
+
if (re.isolation_mode === "same-tree") {
|
|
519
|
+
validRe.isolation_mode = "same-tree";
|
|
520
|
+
} else {
|
|
521
|
+
errors.push('reactive_execution.isolation_mode must be "same-tree"');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
|
|
526
|
+
for (const key of Object.keys(re)) {
|
|
527
|
+
if (!knownReKeys.has(key)) {
|
|
528
|
+
warnings.push(`unknown reactive_execution key "${key}" — ignored`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (Object.keys(validRe).length > 0) {
|
|
533
|
+
validated.reactive_execution = validRe as unknown as import("./types.js").ReactiveExecutionConfig;
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
errors.push("reactive_execution must be an object");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
499
540
|
// ─── Verification Preferences ───────────────────────────────────────────
|
|
500
541
|
if (preferences.verification_commands !== undefined) {
|
|
501
542
|
if (Array.isArray(preferences.verification_commands)) {
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
16
18
|
import { gsdRoot } from "./paths.js";
|
|
17
19
|
import { parse as parseYaml } from "yaml";
|
|
18
20
|
import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
|
|
19
21
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
20
|
-
import { normalizeStringArray } from "../shared/
|
|
22
|
+
import { normalizeStringArray } from "../shared/format-utils.js";
|
|
21
23
|
import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
|
|
22
24
|
|
|
23
25
|
import {
|
|
@@ -82,14 +84,14 @@ export {
|
|
|
82
84
|
|
|
83
85
|
// ─── Path Constants & Getters ───────────────────────────────────────────────
|
|
84
86
|
|
|
85
|
-
const GLOBAL_PREFERENCES_PATH = join(
|
|
87
|
+
const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
|
|
86
88
|
const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
|
|
87
89
|
function projectPreferencesPath(): string {
|
|
88
90
|
return join(gsdRoot(process.cwd()), "preferences.md");
|
|
89
91
|
}
|
|
90
92
|
// Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
|
|
91
93
|
// Check uppercase as a fallback so those files aren't silently ignored.
|
|
92
|
-
const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(
|
|
94
|
+
const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
|
|
93
95
|
function projectPreferencesPathUppercase(): string {
|
|
94
96
|
return join(gsdRoot(process.cwd()), "PREFERENCES.md");
|
|
95
97
|
}
|
|
@@ -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}}
|