gsd-pi 2.11.0 → 2.12.0
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/dist/onboarding.js +3 -0
- package/dist/resources/extensions/bg-shell/index.ts +51 -7
- package/dist/resources/extensions/gsd/auto.ts +159 -2
- package/dist/resources/extensions/gsd/commands.ts +9 -3
- package/dist/resources/extensions/gsd/doctor.ts +60 -3
- package/dist/resources/extensions/gsd/guided-flow.ts +81 -9
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/dist/resources/extensions/gsd/preferences.ts +192 -0
- package/dist/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/templates/context.md +1 -1
- package/dist/resources/extensions/gsd/templates/state.md +3 -3
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/dist/resources/extensions/gsd/types.ts +109 -0
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/dist/resources/extensions/search-the-web/provider.ts +19 -2
- package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/dist/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/dist/resources/extensions/search-the-web/tool-search.ts +62 -3
- package/dist/wizard.js +1 -0
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +169 -55
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +13 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +16 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +91 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +273 -63
- package/packages/pi-agent-core/src/agent.ts +24 -0
- package/packages/pi-agent-core/src/types.ts +98 -0
- package/packages/pi-ai/dist/env-api-keys.js +1 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +314 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +236 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -1
- 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/src/env-api-keys.ts +1 -0
- package/packages/pi-ai/src/models.generated.ts +236 -0
- package/packages/pi-ai/src/types.ts +2 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +1 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +69 -8
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +1 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +76 -7
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/src/resources/extensions/bg-shell/index.ts +51 -7
- package/src/resources/extensions/gsd/auto.ts +159 -2
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/doctor.ts +60 -3
- package/src/resources/extensions/gsd/guided-flow.ts +81 -9
- package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/src/resources/extensions/gsd/preferences.ts +192 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/queue.md +3 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/templates/context.md +1 -1
- package/src/resources/extensions/gsd/templates/state.md +3 -3
- package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/src/resources/extensions/gsd/types.ts +109 -0
- package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/src/resources/extensions/search-the-web/provider.ts +19 -2
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
package/dist/onboarding.js
CHANGED
|
@@ -48,6 +48,7 @@ const LLM_PROVIDER_IDS = [
|
|
|
48
48
|
'xai',
|
|
49
49
|
'openrouter',
|
|
50
50
|
'mistral',
|
|
51
|
+
'ollama-cloud',
|
|
51
52
|
'custom-openai',
|
|
52
53
|
];
|
|
53
54
|
/** API key prefix validation — loose checks to catch obvious mistakes */
|
|
@@ -61,6 +62,7 @@ const OTHER_PROVIDERS = [
|
|
|
61
62
|
{ value: 'xai', label: 'xAI (Grok)' },
|
|
62
63
|
{ value: 'openrouter', label: 'OpenRouter' },
|
|
63
64
|
{ value: 'mistral', label: 'Mistral' },
|
|
65
|
+
{ value: 'ollama-cloud', label: 'Ollama Cloud' },
|
|
64
66
|
{ value: 'custom-openai', label: 'Custom (OpenAI-compatible)' },
|
|
65
67
|
];
|
|
66
68
|
// ─── Dynamic imports ──────────────────────────────────────────────────────────
|
|
@@ -755,6 +757,7 @@ export function loadStoredEnvKeys(authStorage) {
|
|
|
755
757
|
['slack_bot', 'SLACK_BOT_TOKEN'],
|
|
756
758
|
['discord_bot', 'DISCORD_BOT_TOKEN'],
|
|
757
759
|
['groq', 'GROQ_API_KEY'],
|
|
760
|
+
['ollama-cloud', 'OLLAMA_API_KEY'],
|
|
758
761
|
['custom-openai', 'CUSTOM_OPENAI_API_KEY'],
|
|
759
762
|
];
|
|
760
763
|
for (const [provider, envVar] of providers) {
|
|
@@ -574,6 +574,7 @@ interface StartOptions {
|
|
|
574
574
|
type?: ProcessType;
|
|
575
575
|
readyPattern?: string;
|
|
576
576
|
readyPort?: number;
|
|
577
|
+
readyTimeout?: number;
|
|
577
578
|
group?: string;
|
|
578
579
|
env?: Record<string, string>;
|
|
579
580
|
}
|
|
@@ -689,7 +690,7 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
689
690
|
|
|
690
691
|
// Port probing for server-type processes
|
|
691
692
|
if (bg.readyPort) {
|
|
692
|
-
startPortProbing(bg, bg.readyPort);
|
|
693
|
+
startPortProbing(bg, bg.readyPort, opts.readyTimeout);
|
|
693
694
|
}
|
|
694
695
|
|
|
695
696
|
// Shell sessions are ready immediately after spawn
|
|
@@ -707,9 +708,17 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
707
708
|
|
|
708
709
|
// ── Port Probing Loop ──────────────────────────────────────────────────────
|
|
709
710
|
|
|
710
|
-
function startPortProbing(bg: BgProcess, port: number): void {
|
|
711
|
+
function startPortProbing(bg: BgProcess, port: number, customTimeout?: number): void {
|
|
712
|
+
const timeout = customTimeout || DEFAULT_READY_TIMEOUT;
|
|
711
713
|
const interval = setInterval(async () => {
|
|
712
|
-
if (!bg.alive
|
|
714
|
+
if (!bg.alive) {
|
|
715
|
+
clearInterval(interval);
|
|
716
|
+
const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-10).map(l => l.line);
|
|
717
|
+
const detail = `Process exited (code ${bg.exitCode}) before port ${port} opened${stderrLines.length > 0 ? ` — ${stderrLines.join("; ").slice(0, 200)}` : ""}`;
|
|
718
|
+
addEvent(bg, { type: "port_timeout", detail, data: { port, exitCode: bg.exitCode } });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (bg.status !== "starting") {
|
|
713
722
|
clearInterval(interval);
|
|
714
723
|
return;
|
|
715
724
|
}
|
|
@@ -722,8 +731,18 @@ function startPortProbing(bg: BgProcess, port: number): void {
|
|
|
722
731
|
}
|
|
723
732
|
}, READY_POLL_INTERVAL);
|
|
724
733
|
|
|
725
|
-
// Stop probing after timeout
|
|
726
|
-
|
|
734
|
+
// Stop probing after timeout — transition to error state so the process
|
|
735
|
+
// doesn't stay in "starting" forever (fixes #428)
|
|
736
|
+
setTimeout(() => {
|
|
737
|
+
clearInterval(interval);
|
|
738
|
+
if (bg.alive && bg.status === "starting") {
|
|
739
|
+
const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-10).map(l => l.line);
|
|
740
|
+
const detail = `Port ${port} not open after ${timeout}ms${stderrLines.length > 0 ? ` — ${stderrLines.join("; ").slice(0, 200)}` : ""}`;
|
|
741
|
+
bg.status = "error";
|
|
742
|
+
addEvent(bg, { type: "port_timeout", detail, data: { port, timeout } });
|
|
743
|
+
pushAlert(bg, `Port ${port} readiness timeout after ${timeout / 1000}s`);
|
|
744
|
+
}
|
|
745
|
+
}, timeout);
|
|
727
746
|
}
|
|
728
747
|
|
|
729
748
|
// ── Process Kill ───────────────────────────────────────────────────────────
|
|
@@ -864,9 +883,19 @@ async function waitForReady(bg: BgProcess, timeout: number, signal?: AbortSignal
|
|
|
864
883
|
return { ready: false, detail: "Cancelled" };
|
|
865
884
|
}
|
|
866
885
|
if (!bg.alive) {
|
|
886
|
+
const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-5).map(l => l.line);
|
|
887
|
+
const stderrContext = stderrLines.length > 0 ? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}` : "";
|
|
888
|
+
return {
|
|
889
|
+
ready: false,
|
|
890
|
+
detail: `Process exited before becoming ready (code ${bg.exitCode})${bg.recentErrors.length > 0 ? ` — ${bg.recentErrors.slice(-1)[0]}` : ""}${stderrContext}`,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (bg.status === "error") {
|
|
894
|
+
const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-5).map(l => l.line);
|
|
895
|
+
const stderrContext = stderrLines.length > 0 ? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}` : "";
|
|
867
896
|
return {
|
|
868
897
|
ready: false,
|
|
869
|
-
detail: `Process
|
|
898
|
+
detail: `Process entered error state${bg.readyPort ? ` (port ${bg.readyPort} never opened)` : ""}${stderrContext}`,
|
|
870
899
|
};
|
|
871
900
|
}
|
|
872
901
|
if (bg.status === "ready") {
|
|
@@ -887,7 +916,9 @@ async function waitForReady(bg: BgProcess, timeout: number, signal?: AbortSignal
|
|
|
887
916
|
}
|
|
888
917
|
}
|
|
889
918
|
|
|
890
|
-
|
|
919
|
+
const stderrLines = bg.output.filter(l => l.stream === "stderr").slice(-5).map(l => l.line);
|
|
920
|
+
const stderrContext = stderrLines.length > 0 ? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}` : "";
|
|
921
|
+
return { ready: false, detail: `Timed out after ${timeout}ms waiting for ready signal${stderrContext}` };
|
|
891
922
|
}
|
|
892
923
|
|
|
893
924
|
// ── Query Shell Environment ────────────────────────────────────────────────
|
|
@@ -1234,6 +1265,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1234
1265
|
cleanupAll();
|
|
1235
1266
|
});
|
|
1236
1267
|
|
|
1268
|
+
// Register signal handlers to clean up bg processes on unexpected exit (fixes #428)
|
|
1269
|
+
// This prevents orphan processes and helps the parent restore terminal state
|
|
1270
|
+
const signalCleanup = () => {
|
|
1271
|
+
cleanupAll();
|
|
1272
|
+
};
|
|
1273
|
+
process.on("SIGTERM", signalCleanup);
|
|
1274
|
+
process.on("SIGINT", signalCleanup);
|
|
1275
|
+
process.on("beforeExit", signalCleanup);
|
|
1276
|
+
|
|
1237
1277
|
// ── Compaction Awareness: Survive Context Resets ───────────────────
|
|
1238
1278
|
|
|
1239
1279
|
/** Build a compact state summary of all alive processes for context re-injection */
|
|
@@ -1424,6 +1464,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1424
1464
|
ready_port: Type.Optional(
|
|
1425
1465
|
Type.Number({ description: "Port to probe for readiness (for start). When open, process is considered ready." }),
|
|
1426
1466
|
),
|
|
1467
|
+
ready_timeout: Type.Optional(
|
|
1468
|
+
Type.Number({ description: "Max milliseconds to wait for ready_port/ready_pattern before marking as error (default: 30000)" }),
|
|
1469
|
+
),
|
|
1427
1470
|
group: Type.Optional(
|
|
1428
1471
|
Type.String({ description: "Group name for related processes (for start, group_status)" }),
|
|
1429
1472
|
),
|
|
@@ -1449,6 +1492,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1449
1492
|
type: params.type as ProcessType | undefined,
|
|
1450
1493
|
readyPattern: params.ready_pattern,
|
|
1451
1494
|
readyPort: params.ready_port,
|
|
1495
|
+
readyTimeout: params.ready_timeout,
|
|
1452
1496
|
group: params.group,
|
|
1453
1497
|
});
|
|
1454
1498
|
|
|
@@ -18,17 +18,18 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
import { deriveState, invalidateStateCache } from "./state.js";
|
|
20
20
|
import type { GSDState } from "./types.js";
|
|
21
|
-
import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary, getManifestStatus } from "./files.js";
|
|
21
|
+
import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary, getManifestStatus, clearParseCache } from "./files.js";
|
|
22
22
|
export { inlinePriorMilestoneSummary };
|
|
23
23
|
import type { UatType } from "./files.js";
|
|
24
24
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
25
|
-
import { loadPrompt } from "./prompt-loader.js";
|
|
25
|
+
import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
|
|
26
26
|
import {
|
|
27
27
|
gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
|
|
28
28
|
resolveMilestonePath, resolveDir, resolveTasksDir, resolveTaskFiles, resolveTaskFile,
|
|
29
29
|
relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relMilestonePath,
|
|
30
30
|
milestonesDir, resolveGsdRootFile, relGsdRootFile,
|
|
31
31
|
buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
|
|
32
|
+
clearPathCache,
|
|
32
33
|
} from "./paths.js";
|
|
33
34
|
import { saveActivityLog } from "./activity-log.js";
|
|
34
35
|
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
|
@@ -42,6 +43,18 @@ import {
|
|
|
42
43
|
} from "./unit-runtime.js";
|
|
43
44
|
import { resolveAutoSupervisorConfig, resolveModelForUnit, resolveModelWithFallbacksForUnit, resolveSkillDiscoveryMode, loadEffectiveGSDPreferences } from "./preferences.js";
|
|
44
45
|
import type { GSDPreferences } from "./preferences.js";
|
|
46
|
+
import {
|
|
47
|
+
checkPostUnitHooks,
|
|
48
|
+
getActiveHook,
|
|
49
|
+
resetHookState,
|
|
50
|
+
isRetryPending,
|
|
51
|
+
consumeRetryTrigger,
|
|
52
|
+
runPreDispatchHooks,
|
|
53
|
+
persistHookState,
|
|
54
|
+
restoreHookState,
|
|
55
|
+
clearPersistedHookState,
|
|
56
|
+
formatHookStatus,
|
|
57
|
+
} from "./post-unit-hooks.js";
|
|
45
58
|
import {
|
|
46
59
|
validatePlanBoundary,
|
|
47
60
|
validateExecuteBoundary,
|
|
@@ -347,6 +360,8 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
347
360
|
}
|
|
348
361
|
|
|
349
362
|
resetMetrics();
|
|
363
|
+
resetHookState();
|
|
364
|
+
if (basePath) clearPersistedHookState(basePath);
|
|
350
365
|
active = false;
|
|
351
366
|
paused = false;
|
|
352
367
|
stepMode = false;
|
|
@@ -564,6 +579,8 @@ export async function startAuto(
|
|
|
564
579
|
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
565
580
|
ctx.ui.setFooter(hideFooter);
|
|
566
581
|
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
582
|
+
// Restore hook state from disk in case session was interrupted
|
|
583
|
+
restoreHookState(base);
|
|
567
584
|
// Rebuild disk state before resuming — user interaction during pause may have changed files
|
|
568
585
|
try { await rebuildState(base); } catch { /* non-fatal */ }
|
|
569
586
|
try {
|
|
@@ -575,6 +592,8 @@ export async function startAuto(
|
|
|
575
592
|
// Self-heal: clear stale runtime records where artifacts already exist
|
|
576
593
|
await selfHealRuntimeRecords(base, ctx);
|
|
577
594
|
invalidateStateCache();
|
|
595
|
+
clearParseCache();
|
|
596
|
+
clearPathCache();
|
|
578
597
|
await dispatchNextUnit(ctx, pi);
|
|
579
598
|
return;
|
|
580
599
|
}
|
|
@@ -670,6 +689,8 @@ export async function startAuto(
|
|
|
670
689
|
unitRecoveryCount.clear();
|
|
671
690
|
completedKeySet.clear();
|
|
672
691
|
loadPersistedKeys(base, completedKeySet);
|
|
692
|
+
resetHookState();
|
|
693
|
+
restoreHookState(base);
|
|
673
694
|
autoStartTime = Date.now();
|
|
674
695
|
completedUnits = [];
|
|
675
696
|
currentUnit = null;
|
|
@@ -767,6 +788,8 @@ export async function handleAgentEnd(
|
|
|
767
788
|
// Invalidate deriveState() cache — the unit just completed and may have
|
|
768
789
|
// written planning files (task summaries, roadmap checkboxes, etc.)
|
|
769
790
|
invalidateStateCache();
|
|
791
|
+
clearParseCache();
|
|
792
|
+
clearPathCache();
|
|
770
793
|
|
|
771
794
|
// Small delay to let files settle (git commits, file writes)
|
|
772
795
|
await new Promise(r => setTimeout(r, 500));
|
|
@@ -806,6 +829,79 @@ export async function handleAgentEnd(
|
|
|
806
829
|
}
|
|
807
830
|
}
|
|
808
831
|
|
|
832
|
+
// ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
|
|
833
|
+
if (currentUnit && !stepMode) {
|
|
834
|
+
const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
|
|
835
|
+
if (hookUnit) {
|
|
836
|
+
// Dispatch the hook unit instead of normal flow
|
|
837
|
+
const hookStartedAt = Date.now();
|
|
838
|
+
if (currentUnit) {
|
|
839
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
840
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
841
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
842
|
+
}
|
|
843
|
+
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
844
|
+
writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
|
|
845
|
+
phase: "dispatched",
|
|
846
|
+
wrapupWarningSent: false,
|
|
847
|
+
timeoutAt: null,
|
|
848
|
+
lastProgressAt: hookStartedAt,
|
|
849
|
+
progressCount: 0,
|
|
850
|
+
lastProgressKind: "dispatch",
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const state = await deriveState(basePath);
|
|
854
|
+
updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
|
|
855
|
+
const hookState = getActiveHook();
|
|
856
|
+
ctx.ui.notify(
|
|
857
|
+
`Running post-unit hook: ${hookUnit.hookName} (cycle ${hookState?.cycle ?? 1})`,
|
|
858
|
+
"info",
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
// Switch model if the hook specifies one
|
|
862
|
+
if (hookUnit.model) {
|
|
863
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
864
|
+
const match = availableModels.find(m =>
|
|
865
|
+
m.id === hookUnit.model || `${m.provider}/${m.id}` === hookUnit.model,
|
|
866
|
+
);
|
|
867
|
+
if (match) {
|
|
868
|
+
try {
|
|
869
|
+
await pi.setModel(match);
|
|
870
|
+
} catch { /* non-fatal — use current model */ }
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const result = await cmdCtx!.newSession();
|
|
875
|
+
if (result.cancelled) {
|
|
876
|
+
resetHookState();
|
|
877
|
+
await stopAuto(ctx, pi);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
881
|
+
writeLock(basePath, hookUnit.unitType, hookUnit.unitId, completedUnits.length, sessionFile);
|
|
882
|
+
// Persist hook state so cycle counts survive crashes
|
|
883
|
+
persistHookState(basePath);
|
|
884
|
+
pi.sendMessage(
|
|
885
|
+
{ customType: "gsd-auto", content: hookUnit.prompt, display: verbose },
|
|
886
|
+
{ triggerTurn: true },
|
|
887
|
+
);
|
|
888
|
+
return; // handleAgentEnd will fire again when hook session completes
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Check if a hook requested a retry of the trigger unit
|
|
892
|
+
if (isRetryPending()) {
|
|
893
|
+
const trigger = consumeRetryTrigger();
|
|
894
|
+
if (trigger) {
|
|
895
|
+
ctx.ui.notify(
|
|
896
|
+
`Hook requested retry of ${trigger.unitType} ${trigger.unitId}.`,
|
|
897
|
+
"info",
|
|
898
|
+
);
|
|
899
|
+
// Fall through to normal dispatchNextUnit — state derivation will
|
|
900
|
+
// re-select the same unit since it hasn't been marked complete
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
809
905
|
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
810
906
|
if (stepMode) {
|
|
811
907
|
await showStepWizard(ctx, pi);
|
|
@@ -949,6 +1045,7 @@ export function describeNextUnit(state: GSDState): { label: string; description:
|
|
|
949
1045
|
// ─── Progress Widget ──────────────────────────────────────────────────────
|
|
950
1046
|
|
|
951
1047
|
function unitVerb(unitType: string): string {
|
|
1048
|
+
if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
|
|
952
1049
|
switch (unitType) {
|
|
953
1050
|
case "research-milestone":
|
|
954
1051
|
case "research-slice": return "researching";
|
|
@@ -965,6 +1062,7 @@ function unitVerb(unitType: string): string {
|
|
|
965
1062
|
}
|
|
966
1063
|
|
|
967
1064
|
function unitPhaseLabel(unitType: string): string {
|
|
1065
|
+
if (unitType.startsWith("hook/")) return "HOOK";
|
|
968
1066
|
switch (unitType) {
|
|
969
1067
|
case "research-milestone": return "RESEARCH";
|
|
970
1068
|
case "research-slice": return "RESEARCH";
|
|
@@ -981,7 +1079,14 @@ function unitPhaseLabel(unitType: string): string {
|
|
|
981
1079
|
}
|
|
982
1080
|
|
|
983
1081
|
function peekNext(unitType: string, state: GSDState): string {
|
|
1082
|
+
// Show active hook info in progress display
|
|
1083
|
+
const activeHookState = getActiveHook();
|
|
1084
|
+
if (activeHookState) {
|
|
1085
|
+
return `hook: ${activeHookState.hookName} (cycle ${activeHookState.cycle})`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
984
1088
|
const sid = state.activeSlice?.id ?? "";
|
|
1089
|
+
if (unitType.startsWith("hook/")) return `continue ${sid}`;
|
|
985
1090
|
switch (unitType) {
|
|
986
1091
|
case "research-milestone": return "plan milestone roadmap";
|
|
987
1092
|
case "plan-milestone": return "plan or execute first slice";
|
|
@@ -1275,6 +1380,9 @@ async function dispatchNextUnit(
|
|
|
1275
1380
|
return;
|
|
1276
1381
|
}
|
|
1277
1382
|
|
|
1383
|
+
// Clear stale directory listing cache so deriveState sees fresh disk state (#431)
|
|
1384
|
+
clearPathCache();
|
|
1385
|
+
|
|
1278
1386
|
let state = await deriveState(basePath);
|
|
1279
1387
|
let mid = state.activeMilestone?.id;
|
|
1280
1388
|
let midTitle = state.activeMilestone?.title;
|
|
@@ -1338,6 +1446,8 @@ async function dispatchNextUnit(
|
|
|
1338
1446
|
}
|
|
1339
1447
|
// Re-derive state from the now-merged working tree
|
|
1340
1448
|
invalidateStateCache();
|
|
1449
|
+
clearParseCache();
|
|
1450
|
+
clearPathCache();
|
|
1341
1451
|
state = await deriveState(basePath);
|
|
1342
1452
|
mid = state.activeMilestone?.id;
|
|
1343
1453
|
midTitle = state.activeMilestone?.title;
|
|
@@ -1403,6 +1513,8 @@ async function dispatchNextUnit(
|
|
|
1403
1513
|
);
|
|
1404
1514
|
// Re-derive state from main so downstream logic sees merged state
|
|
1405
1515
|
invalidateStateCache();
|
|
1516
|
+
clearParseCache();
|
|
1517
|
+
clearPathCache();
|
|
1406
1518
|
state = await deriveState(basePath);
|
|
1407
1519
|
mid = state.activeMilestone?.id;
|
|
1408
1520
|
midTitle = state.activeMilestone?.title;
|
|
@@ -1715,6 +1827,28 @@ async function dispatchNextUnit(
|
|
|
1715
1827
|
}
|
|
1716
1828
|
}
|
|
1717
1829
|
|
|
1830
|
+
// ── Pre-dispatch hooks: modify, skip, or replace the unit before dispatch ──
|
|
1831
|
+
const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, basePath);
|
|
1832
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
1833
|
+
ctx.ui.notify(
|
|
1834
|
+
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
1835
|
+
"info",
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
if (preDispatchResult.action === "skip") {
|
|
1839
|
+
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
1840
|
+
// Yield then re-dispatch to advance to next unit
|
|
1841
|
+
await new Promise(r => setImmediate(r));
|
|
1842
|
+
await dispatchNextUnit(ctx, pi);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (preDispatchResult.action === "replace") {
|
|
1846
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
1847
|
+
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1848
|
+
} else if (preDispatchResult.prompt) {
|
|
1849
|
+
prompt = preDispatchResult.prompt;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1718
1852
|
const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
|
|
1719
1853
|
if (priorSliceBlocker) {
|
|
1720
1854
|
await stopAuto(ctx, pi);
|
|
@@ -2285,6 +2419,7 @@ async function buildResearchMilestonePrompt(mid: string, midTitle: string, base:
|
|
|
2285
2419
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2286
2420
|
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
|
2287
2421
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2422
|
+
inlined.push(inlineTemplate("research", "Research"));
|
|
2288
2423
|
|
|
2289
2424
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2290
2425
|
|
|
@@ -2317,6 +2452,11 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
|
|
|
2317
2452
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2318
2453
|
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
|
2319
2454
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2455
|
+
inlined.push(inlineTemplate("roadmap", "Roadmap"));
|
|
2456
|
+
inlined.push(inlineTemplate("decisions", "Decisions"));
|
|
2457
|
+
inlined.push(inlineTemplate("plan", "Slice Plan"));
|
|
2458
|
+
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
|
2459
|
+
inlined.push(inlineTemplate("secrets-manifest", "Secrets Manifest"));
|
|
2320
2460
|
|
|
2321
2461
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2322
2462
|
|
|
@@ -2353,6 +2493,7 @@ async function buildResearchSlicePrompt(
|
|
|
2353
2493
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2354
2494
|
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
|
2355
2495
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2496
|
+
inlined.push(inlineTemplate("research", "Research"));
|
|
2356
2497
|
|
|
2357
2498
|
const depContent = await inlineDependencySummaries(mid, sid, base);
|
|
2358
2499
|
|
|
@@ -2388,6 +2529,8 @@ async function buildPlanSlicePrompt(
|
|
|
2388
2529
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2389
2530
|
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
|
2390
2531
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2532
|
+
inlined.push(inlineTemplate("plan", "Slice Plan"));
|
|
2533
|
+
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
|
2391
2534
|
|
|
2392
2535
|
const depContent = await inlineDependencySummaries(mid, sid, base);
|
|
2393
2536
|
|
|
@@ -2449,6 +2592,10 @@ async function buildExecuteTaskPrompt(
|
|
|
2449
2592
|
);
|
|
2450
2593
|
|
|
2451
2594
|
const carryForwardSection = await buildCarryForwardSection(priorSummaries, base);
|
|
2595
|
+
const inlinedTemplates = [
|
|
2596
|
+
inlineTemplate("task-summary", "Task Summary"),
|
|
2597
|
+
inlineTemplate("decisions", "Decisions"),
|
|
2598
|
+
].join("\n\n---\n\n");
|
|
2452
2599
|
|
|
2453
2600
|
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
|
|
2454
2601
|
|
|
@@ -2463,6 +2610,7 @@ async function buildExecuteTaskPrompt(
|
|
|
2463
2610
|
resumeSection,
|
|
2464
2611
|
priorTaskLines: priorLines,
|
|
2465
2612
|
taskSummaryPath,
|
|
2613
|
+
inlinedTemplates,
|
|
2466
2614
|
});
|
|
2467
2615
|
}
|
|
2468
2616
|
|
|
@@ -2495,6 +2643,8 @@ async function buildCompleteSlicePrompt(
|
|
|
2495
2643
|
}
|
|
2496
2644
|
}
|
|
2497
2645
|
}
|
|
2646
|
+
inlined.push(inlineTemplate("slice-summary", "Slice Summary"));
|
|
2647
|
+
inlined.push(inlineTemplate("uat", "UAT"));
|
|
2498
2648
|
|
|
2499
2649
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2500
2650
|
|
|
@@ -2547,6 +2697,7 @@ async function buildCompleteMilestonePrompt(
|
|
|
2547
2697
|
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
2548
2698
|
const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
|
|
2549
2699
|
if (contextInline) inlined.push(contextInline);
|
|
2700
|
+
inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
|
|
2550
2701
|
|
|
2551
2702
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2552
2703
|
|
|
@@ -2993,6 +3144,9 @@ async function collectObservabilityWarnings(
|
|
|
2993
3144
|
unitType: string,
|
|
2994
3145
|
unitId: string,
|
|
2995
3146
|
): Promise<import("./observability-validator.ts").ValidationIssue[]> {
|
|
3147
|
+
// Hook units have custom artifacts — skip standard observability checks
|
|
3148
|
+
if (unitType.startsWith("hook/")) return [];
|
|
3149
|
+
|
|
2996
3150
|
const parts = unitId.split("/");
|
|
2997
3151
|
const mid = parts[0];
|
|
2998
3152
|
const sid = parts[1];
|
|
@@ -3404,6 +3558,9 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
3404
3558
|
* skipped writing the UAT file (see #176).
|
|
3405
3559
|
*/
|
|
3406
3560
|
export function verifyExpectedArtifact(unitType: string, unitId: string, base: string): boolean {
|
|
3561
|
+
// Clear stale directory listing cache so artifact checks see fresh disk state (#431)
|
|
3562
|
+
clearPathCache();
|
|
3563
|
+
|
|
3407
3564
|
// fix-merge has no file artifact — verify by checking git state
|
|
3408
3565
|
if (unitType === "fix-merge") {
|
|
3409
3566
|
const unmerged = runGit(base, ["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
|
|
@@ -53,10 +53,10 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
|
|
|
53
53
|
|
|
54
54
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
55
55
|
pi.registerCommand("gsd", {
|
|
56
|
-
description: "GSD — Get Shit Done: /gsd next|auto|stop|status|queue|prefs|doctor|migrate|remote",
|
|
56
|
+
description: "GSD — Get Shit Done: /gsd next|auto|stop|status|queue|prefs|hooks|doctor|migrate|remote",
|
|
57
57
|
|
|
58
58
|
getArgumentCompletions: (prefix: string) => {
|
|
59
|
-
const subcommands = ["next", "auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate", "remote"];
|
|
59
|
+
const subcommands = ["next", "auto", "stop", "status", "queue", "discuss", "prefs", "hooks", "doctor", "migrate", "remote"];
|
|
60
60
|
const parts = prefix.trim().split(/\s+/);
|
|
61
61
|
|
|
62
62
|
if (parts.length <= 1) {
|
|
@@ -151,6 +151,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
if (trimmed === "hooks") {
|
|
155
|
+
const { formatHookStatus } = await import("./post-unit-hooks.js");
|
|
156
|
+
ctx.ui.notify(formatHookStatus(), "info");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
if (trimmed === "migrate" || trimmed.startsWith("migrate ")) {
|
|
155
161
|
await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi);
|
|
156
162
|
return;
|
|
@@ -168,7 +174,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
ctx.ui.notify(
|
|
171
|
-
`Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status|wizard|setup], /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
|
|
177
|
+
`Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status|wizard|setup], /gsd hooks, /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
|
|
172
178
|
"warning",
|
|
173
179
|
);
|
|
174
180
|
},
|
|
@@ -22,7 +22,8 @@ export type DoctorIssueCode =
|
|
|
22
22
|
| "task_done_must_haves_not_verified"
|
|
23
23
|
| "active_requirement_missing_owner"
|
|
24
24
|
| "blocked_requirement_missing_reason"
|
|
25
|
-
| "blocker_discovered_no_replan"
|
|
25
|
+
| "blocker_discovered_no_replan"
|
|
26
|
+
| "delimiter_in_title";
|
|
26
27
|
|
|
27
28
|
export interface DoctorIssue {
|
|
28
29
|
severity: DoctorSeverity;
|
|
@@ -91,15 +92,43 @@ function validatePreferenceShape(preferences: GSDPreferences): string[] {
|
|
|
91
92
|
return issues;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Characters that are used as delimiters in GSD state management documents
|
|
97
|
+
* and should not appear in milestone or slice titles.
|
|
98
|
+
*
|
|
99
|
+
* - "—" (em dash, U+2014): used as a display separator in STATE.md and other docs.
|
|
100
|
+
* A title containing "—" makes the separator ambiguous, corrupting state display
|
|
101
|
+
* and confusing the LLM agent that reads and writes these files.
|
|
102
|
+
* - "–" (en dash, U+2013): visually similar to em dash; same ambiguity risk.
|
|
103
|
+
* - "/" (forward slash, U+002F): used as the path separator in unit IDs (M001/S01)
|
|
104
|
+
* and git branch names (gsd/M001/S01). A slash in a title can break path resolution.
|
|
105
|
+
*/
|
|
106
|
+
const TITLE_DELIMITER_RE = /[\u2014\u2013\/]/; // em dash, en dash, forward slash
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check whether a milestone or slice title contains characters that conflict
|
|
110
|
+
* with GSD's state document delimiter conventions.
|
|
111
|
+
* Returns a human-readable description of the problem, or null if the title is safe.
|
|
112
|
+
*/
|
|
113
|
+
export function validateTitle(title: string): string | null {
|
|
114
|
+
if (TITLE_DELIMITER_RE.test(title)) {
|
|
115
|
+
const found: string[] = [];
|
|
116
|
+
if (/[\u2014\u2013]/.test(title)) found.push("em/en dash (\u2014 or \u2013)");
|
|
117
|
+
if (/\//.test(title)) found.push("forward slash (/)");
|
|
118
|
+
return `title contains ${found.join(" and ")}, which conflict with GSD state document delimiters`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
94
123
|
function buildStateMarkdown(state: Awaited<ReturnType<typeof deriveState>>): string {
|
|
95
124
|
const lines: string[] = [];
|
|
96
125
|
lines.push("# GSD State", "");
|
|
97
126
|
|
|
98
127
|
const activeMilestone = state.activeMilestone
|
|
99
|
-
? `${state.activeMilestone.id}
|
|
128
|
+
? `${state.activeMilestone.id}: ${state.activeMilestone.title}`
|
|
100
129
|
: "None";
|
|
101
130
|
const activeSlice = state.activeSlice
|
|
102
|
-
? `${state.activeSlice.id}
|
|
131
|
+
? `${state.activeSlice.id}: ${state.activeSlice.title}`
|
|
103
132
|
: "None";
|
|
104
133
|
|
|
105
134
|
lines.push(`**Active Milestone:** ${activeMilestone}`);
|
|
@@ -477,6 +506,20 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
477
506
|
const milestonePath = resolveMilestonePath(basePath, milestoneId);
|
|
478
507
|
if (!milestonePath) continue;
|
|
479
508
|
|
|
509
|
+
// Validate milestone title for delimiter characters that break state documents.
|
|
510
|
+
const milestoneTitleIssue = validateTitle(milestone.title);
|
|
511
|
+
if (milestoneTitleIssue) {
|
|
512
|
+
issues.push({
|
|
513
|
+
severity: "warning",
|
|
514
|
+
code: "delimiter_in_title",
|
|
515
|
+
scope: "milestone",
|
|
516
|
+
unitId: milestoneId,
|
|
517
|
+
message: `Milestone ${milestoneId} ${milestoneTitleIssue}. Rename the milestone to remove these characters to prevent state corruption.`,
|
|
518
|
+
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
519
|
+
fixable: false,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
480
523
|
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
481
524
|
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
482
525
|
if (!roadmapContent) continue;
|
|
@@ -486,6 +529,20 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
486
529
|
const unitId = `${milestoneId}/${slice.id}`;
|
|
487
530
|
if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId) continue;
|
|
488
531
|
|
|
532
|
+
// Validate slice title for delimiter characters.
|
|
533
|
+
const sliceTitleIssue = validateTitle(slice.title);
|
|
534
|
+
if (sliceTitleIssue) {
|
|
535
|
+
issues.push({
|
|
536
|
+
severity: "warning",
|
|
537
|
+
code: "delimiter_in_title",
|
|
538
|
+
scope: "slice",
|
|
539
|
+
unitId,
|
|
540
|
+
message: `Slice ${unitId} ${sliceTitleIssue}. Rename the slice to remove these characters to prevent state corruption.`,
|
|
541
|
+
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
542
|
+
fixable: false,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
489
546
|
const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
|
|
490
547
|
if (!slicePath) continue;
|
|
491
548
|
|