gsd-pi 2.78.1-dev.eccf86e27 → 2.79.0-dev.579e14e9b
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 +94 -47
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
- package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -37
- package/dist/resources/extensions/gsd/commands/context.js +1 -1
- package/dist/resources/extensions/gsd/preferences-types.js +20 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +41 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +53 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +116 -0
- package/packages/mcp-server/src/workflow-tools.ts +81 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
- package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +38 -38
- package/src/resources/extensions/gsd/commands/context.ts +1 -1
- package/src/resources/extensions/gsd/preferences-types.ts +23 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +68 -1
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +41 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
- /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_ssgManifest.js +0 -0
package/pkg/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
|
|
18
18
|
} from "./paths.js";
|
|
19
19
|
import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences, resolveAllSkillReferences } from "./preferences.js";
|
|
20
|
+
import { isContextModeEnabled } from "./preferences-types.js";
|
|
20
21
|
import { parseRoadmap } from "./parsers-legacy.js";
|
|
21
22
|
import type { GSDState, InlineLevel } from "./types.js";
|
|
22
23
|
import type { GSDPreferences } from "./preferences.js";
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
} from "./gate-registry.js";
|
|
34
35
|
import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
|
|
35
36
|
import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
|
|
36
|
-
import { composeInlinedContext, type ArtifactResolver } from "./unit-context-composer.js";
|
|
37
|
+
import { composeContextModeInstructions, composeInlinedContext, type ArtifactResolver, type ContextModeRenderMode } from "./unit-context-composer.js";
|
|
37
38
|
import { logWarning } from "./workflow-logger.js";
|
|
38
39
|
import { inlineGraphSubgraph } from "./graph-context.js";
|
|
39
40
|
import { buildExtractionStepsBlock } from "./commands-extract-learnings.js";
|
|
@@ -89,6 +90,30 @@ function capPreamble(preamble: string): string {
|
|
|
89
90
|
return truncateAtSectionBoundary(preamble, budget).content;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
function renderContextModeForPrompt(
|
|
94
|
+
unitType: string,
|
|
95
|
+
base: string,
|
|
96
|
+
renderMode: ContextModeRenderMode = "standalone",
|
|
97
|
+
): string {
|
|
98
|
+
const effectivePrefs = loadEffectiveGSDPreferences(base)?.preferences;
|
|
99
|
+
return composeContextModeInstructions(unitType, {
|
|
100
|
+
enabled: isContextModeEnabled(effectivePrefs),
|
|
101
|
+
renderMode,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function prependContextModeToBlock(
|
|
106
|
+
unitType: string,
|
|
107
|
+
base: string,
|
|
108
|
+
block: string,
|
|
109
|
+
renderMode: ContextModeRenderMode = "standalone",
|
|
110
|
+
): string {
|
|
111
|
+
const contextMode = renderContextModeForPrompt(unitType, base, renderMode);
|
|
112
|
+
if (!contextMode) return block;
|
|
113
|
+
if (!block.trim()) return contextMode;
|
|
114
|
+
return `${contextMode}\n\n${block}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
92
117
|
// ─── Executor Constraints ─────────────────────────────────────────────────────
|
|
93
118
|
|
|
94
119
|
/**
|
|
@@ -1266,6 +1291,7 @@ export async function buildDiscussMilestonePrompt(
|
|
|
1266
1291
|
structuredQuestionsAvailable = "false",
|
|
1267
1292
|
): Promise<string> {
|
|
1268
1293
|
const discussTemplates = inlineTemplate("context", "Context");
|
|
1294
|
+
const contextModeInstructions = renderContextModeForPrompt("discuss-milestone", base);
|
|
1269
1295
|
|
|
1270
1296
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
1271
1297
|
workingDirectory: base,
|
|
@@ -1276,16 +1302,17 @@ export async function buildDiscussMilestonePrompt(
|
|
|
1276
1302
|
commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
|
|
1277
1303
|
fastPathInstruction: "",
|
|
1278
1304
|
});
|
|
1305
|
+
const promptWithContextMode = prependContextModeToBlock("discuss-milestone", base, basePrompt);
|
|
1279
1306
|
|
|
1280
1307
|
// If a CONTEXT-DRAFT.md exists, append it as seed material
|
|
1281
1308
|
const draftPath = resolveMilestoneFile(base, mid, "CONTEXT-DRAFT");
|
|
1282
1309
|
const draftContent = draftPath ? await loadFile(draftPath) : null;
|
|
1283
1310
|
|
|
1284
1311
|
if (draftContent) {
|
|
1285
|
-
return `${
|
|
1312
|
+
return `${promptWithContextMode}\n\n## Prior Discussion (Draft Seed)\n\nThe following draft was captured from a prior multi-milestone discussion. Use it as seed material — the user has already provided this context. Start with a brief reflection on what the draft covers, then probe for any gaps or open questions before writing the full CONTEXT.md.\n\n${draftContent}`;
|
|
1286
1313
|
}
|
|
1287
1314
|
|
|
1288
|
-
return basePrompt;
|
|
1315
|
+
return contextModeInstructions ? promptWithContextMode : basePrompt;
|
|
1289
1316
|
}
|
|
1290
1317
|
|
|
1291
1318
|
/**
|
|
@@ -1298,10 +1325,10 @@ export async function buildWorkflowPreferencesPrompt(
|
|
|
1298
1325
|
base: string,
|
|
1299
1326
|
structuredQuestionsAvailable = "false",
|
|
1300
1327
|
): Promise<string> {
|
|
1301
|
-
return loadPrompt("guided-workflow-preferences", {
|
|
1328
|
+
return prependContextModeToBlock("workflow-preferences", base, loadPrompt("guided-workflow-preferences", {
|
|
1302
1329
|
workingDirectory: base,
|
|
1303
1330
|
structuredQuestionsAvailable,
|
|
1304
|
-
});
|
|
1331
|
+
}));
|
|
1305
1332
|
}
|
|
1306
1333
|
|
|
1307
1334
|
/**
|
|
@@ -1315,10 +1342,10 @@ export async function buildResearchProjectPrompt(
|
|
|
1315
1342
|
base: string,
|
|
1316
1343
|
structuredQuestionsAvailable = "false",
|
|
1317
1344
|
): Promise<string> {
|
|
1318
|
-
return loadPrompt("guided-research-project", {
|
|
1345
|
+
return prependContextModeToBlock("research-project", base, loadPrompt("guided-research-project", {
|
|
1319
1346
|
workingDirectory: base,
|
|
1320
1347
|
structuredQuestionsAvailable,
|
|
1321
|
-
});
|
|
1348
|
+
}));
|
|
1322
1349
|
}
|
|
1323
1350
|
|
|
1324
1351
|
/**
|
|
@@ -1331,10 +1358,10 @@ export async function buildResearchDecisionPrompt(
|
|
|
1331
1358
|
base: string,
|
|
1332
1359
|
structuredQuestionsAvailable = "false",
|
|
1333
1360
|
): Promise<string> {
|
|
1334
|
-
return loadPrompt("guided-research-decision", {
|
|
1361
|
+
return prependContextModeToBlock("research-decision", base, loadPrompt("guided-research-decision", {
|
|
1335
1362
|
workingDirectory: base,
|
|
1336
1363
|
structuredQuestionsAvailable,
|
|
1337
|
-
});
|
|
1364
|
+
}));
|
|
1338
1365
|
}
|
|
1339
1366
|
|
|
1340
1367
|
/**
|
|
@@ -1349,12 +1376,12 @@ export async function buildDiscussProjectPrompt(
|
|
|
1349
1376
|
): Promise<string> {
|
|
1350
1377
|
const inlinedTemplates = inlineTemplate("project", "Project");
|
|
1351
1378
|
|
|
1352
|
-
return loadPrompt("guided-discuss-project", {
|
|
1379
|
+
return prependContextModeToBlock("discuss-project", base, loadPrompt("guided-discuss-project", {
|
|
1353
1380
|
workingDirectory: base,
|
|
1354
1381
|
inlinedTemplates,
|
|
1355
1382
|
structuredQuestionsAvailable,
|
|
1356
1383
|
commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
|
|
1357
|
-
});
|
|
1384
|
+
}));
|
|
1358
1385
|
}
|
|
1359
1386
|
|
|
1360
1387
|
/**
|
|
@@ -1369,12 +1396,12 @@ export async function buildDiscussRequirementsPrompt(
|
|
|
1369
1396
|
): Promise<string> {
|
|
1370
1397
|
const inlinedTemplates = inlineTemplate("requirements", "Requirements");
|
|
1371
1398
|
|
|
1372
|
-
return loadPrompt("guided-discuss-requirements", {
|
|
1399
|
+
return prependContextModeToBlock("discuss-requirements", base, loadPrompt("guided-discuss-requirements", {
|
|
1373
1400
|
workingDirectory: base,
|
|
1374
1401
|
inlinedTemplates,
|
|
1375
1402
|
structuredQuestionsAvailable,
|
|
1376
1403
|
commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
|
|
1377
|
-
});
|
|
1404
|
+
}));
|
|
1378
1405
|
}
|
|
1379
1406
|
|
|
1380
1407
|
export async function buildResearchMilestonePrompt(mid: string, midTitle: string, base: string): Promise<string> {
|
|
@@ -1428,7 +1455,11 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
|
|
|
1428
1455
|
if (knowledgeInlineRM) parts.push(knowledgeInlineRM);
|
|
1429
1456
|
}
|
|
1430
1457
|
|
|
1431
|
-
const inlinedContext =
|
|
1458
|
+
const inlinedContext = prependContextModeToBlock(
|
|
1459
|
+
"research-milestone",
|
|
1460
|
+
base,
|
|
1461
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`),
|
|
1462
|
+
);
|
|
1432
1463
|
|
|
1433
1464
|
const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
|
|
1434
1465
|
return loadPrompt("research-milestone", {
|
|
@@ -1501,7 +1532,11 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
|
|
|
1501
1532
|
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
|
1502
1533
|
}
|
|
1503
1534
|
|
|
1504
|
-
const inlinedContext =
|
|
1535
|
+
const inlinedContext = prependContextModeToBlock(
|
|
1536
|
+
"plan-milestone",
|
|
1537
|
+
base,
|
|
1538
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`),
|
|
1539
|
+
);
|
|
1505
1540
|
|
|
1506
1541
|
const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
|
|
1507
1542
|
const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
|
|
@@ -1530,6 +1565,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
|
|
|
1530
1565
|
|
|
1531
1566
|
export async function buildResearchSlicePrompt(
|
|
1532
1567
|
mid: string, _midTitle: string, sid: string, sTitle: string, base: string,
|
|
1568
|
+
options?: { contextModeRenderMode?: ContextModeRenderMode },
|
|
1533
1569
|
): Promise<string> {
|
|
1534
1570
|
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1535
1571
|
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
@@ -1582,7 +1618,12 @@ export async function buildResearchSlicePrompt(
|
|
|
1582
1618
|
const overridesInline = formatOverridesSection(activeOverrides);
|
|
1583
1619
|
if (overridesInline) inlined.unshift(overridesInline);
|
|
1584
1620
|
|
|
1585
|
-
const inlinedContext =
|
|
1621
|
+
const inlinedContext = prependContextModeToBlock(
|
|
1622
|
+
"research-slice",
|
|
1623
|
+
base,
|
|
1624
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`),
|
|
1625
|
+
options?.contextModeRenderMode,
|
|
1626
|
+
);
|
|
1586
1627
|
|
|
1587
1628
|
const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
|
|
1588
1629
|
return loadPrompt("research-slice", {
|
|
@@ -1629,6 +1670,7 @@ async function renderSlicePrompt(options: {
|
|
|
1629
1670
|
sessionContextWindow?: number;
|
|
1630
1671
|
modelRegistry?: MinimalModelRegistry;
|
|
1631
1672
|
sessionProvider?: string;
|
|
1673
|
+
contextModeRenderMode?: ContextModeRenderMode;
|
|
1632
1674
|
}): Promise<string> {
|
|
1633
1675
|
const {
|
|
1634
1676
|
mid, sid, sTitle, base, level, promptTemplate, prependBlocks = [], extraVars = {},
|
|
@@ -1684,7 +1726,12 @@ async function renderSlicePrompt(options: {
|
|
|
1684
1726
|
const overridesInline = formatOverridesSection(await loadActiveOverrides(base));
|
|
1685
1727
|
if (overridesInline) inlined.unshift(overridesInline);
|
|
1686
1728
|
|
|
1687
|
-
const inlinedContext =
|
|
1729
|
+
const inlinedContext = prependContextModeToBlock(
|
|
1730
|
+
promptTemplate,
|
|
1731
|
+
base,
|
|
1732
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`),
|
|
1733
|
+
options.contextModeRenderMode,
|
|
1734
|
+
);
|
|
1688
1735
|
const executorContextConstraints = formatExecutorConstraints(sessionContextWindow, modelRegistry, sessionProvider);
|
|
1689
1736
|
const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
|
|
1690
1737
|
const commitInstruction = "Do not commit — .gsd/ planning docs are managed externally and not tracked in git.";
|
|
@@ -1820,6 +1867,8 @@ export interface ExecuteTaskPromptOptions {
|
|
|
1820
1867
|
modelRegistry?: MinimalModelRegistry;
|
|
1821
1868
|
/** Session model provider, used for provider-specific effective context windows. */
|
|
1822
1869
|
sessionProvider?: string;
|
|
1870
|
+
/** Render compact Context Mode guidance when embedded inside another prompt. */
|
|
1871
|
+
contextModeRenderMode?: ContextModeRenderMode;
|
|
1823
1872
|
}
|
|
1824
1873
|
|
|
1825
1874
|
export async function buildExecuteTaskPrompt(
|
|
@@ -1962,6 +2011,7 @@ export async function buildExecuteTaskPrompt(
|
|
|
1962
2011
|
getGatesForTurn("execute-task"),
|
|
1963
2012
|
{ pending: new Set(etPending.map((g) => g.gate_id)), allowOmit: true },
|
|
1964
2013
|
);
|
|
2014
|
+
phaseAnchorSection = prependContextModeToBlock("execute-task", base, phaseAnchorSection, opts.contextModeRenderMode);
|
|
1965
2015
|
|
|
1966
2016
|
return loadPrompt("execute-task", {
|
|
1967
2017
|
overridesSection,
|
|
@@ -1990,6 +2040,7 @@ export async function buildExecuteTaskPrompt(
|
|
|
1990
2040
|
taskTitle: tTitle,
|
|
1991
2041
|
taskPlanContent,
|
|
1992
2042
|
extraContext: [taskPlanInline, slicePlanExcerpt, finalCarryForward, resumeSection],
|
|
2043
|
+
unitType: "execute-task",
|
|
1993
2044
|
}),
|
|
1994
2045
|
});
|
|
1995
2046
|
}
|
|
@@ -2088,7 +2139,11 @@ export async function buildCompleteSlicePrompt(
|
|
|
2088
2139
|
? `${completeOverridesInline}\n\n---\n\n${body}`
|
|
2089
2140
|
: body;
|
|
2090
2141
|
|
|
2091
|
-
const inlinedContext =
|
|
2142
|
+
const inlinedContext = prependContextModeToBlock(
|
|
2143
|
+
"complete-slice",
|
|
2144
|
+
base,
|
|
2145
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${finalBody}`),
|
|
2146
|
+
);
|
|
2092
2147
|
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
2093
2148
|
|
|
2094
2149
|
const sliceRel = relSlicePath(base, mid, sid);
|
|
@@ -2187,7 +2242,11 @@ export async function buildCompleteMilestonePrompt(
|
|
|
2187
2242
|
if (contextInline) inlined.push(contextInline);
|
|
2188
2243
|
inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
|
|
2189
2244
|
|
|
2190
|
-
const inlinedContext =
|
|
2245
|
+
const inlinedContext = prependContextModeToBlock(
|
|
2246
|
+
"complete-milestone",
|
|
2247
|
+
base,
|
|
2248
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`),
|
|
2249
|
+
);
|
|
2191
2250
|
|
|
2192
2251
|
const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
|
|
2193
2252
|
|
|
@@ -2324,7 +2383,11 @@ export async function buildValidateMilestonePrompt(
|
|
|
2324
2383
|
const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
|
|
2325
2384
|
if (contextInline) inlined.push(contextInline);
|
|
2326
2385
|
|
|
2327
|
-
const inlinedContext =
|
|
2386
|
+
const inlinedContext = prependContextModeToBlock(
|
|
2387
|
+
"validate-milestone",
|
|
2388
|
+
base,
|
|
2389
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`),
|
|
2390
|
+
);
|
|
2328
2391
|
|
|
2329
2392
|
const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
|
|
2330
2393
|
const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
|
|
@@ -2400,7 +2463,11 @@ export async function buildReplanSlicePrompt(
|
|
|
2400
2463
|
const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
|
|
2401
2464
|
if (replanOverridesInline) inlined.unshift(replanOverridesInline);
|
|
2402
2465
|
|
|
2403
|
-
const inlinedContext =
|
|
2466
|
+
const inlinedContext = prependContextModeToBlock(
|
|
2467
|
+
"replan-slice",
|
|
2468
|
+
base,
|
|
2469
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`),
|
|
2470
|
+
);
|
|
2404
2471
|
|
|
2405
2472
|
const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
|
|
2406
2473
|
|
|
@@ -2474,7 +2541,11 @@ export async function buildRunUatPrompt(
|
|
|
2474
2541
|
};
|
|
2475
2542
|
|
|
2476
2543
|
const composed = await composeInlinedContext("run-uat", resolveArtifact);
|
|
2477
|
-
const inlinedContext =
|
|
2544
|
+
const inlinedContext = prependContextModeToBlock(
|
|
2545
|
+
"run-uat",
|
|
2546
|
+
base,
|
|
2547
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${composed}`),
|
|
2548
|
+
);
|
|
2478
2549
|
|
|
2479
2550
|
const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "ASSESSMENT"));
|
|
2480
2551
|
const uatType = getUatType(uatContent);
|
|
@@ -2548,7 +2619,11 @@ export async function buildReassessRoadmapPrompt(
|
|
|
2548
2619
|
const knowledgeInlineRA = await inlineKnowledgeBudgeted(base, extractKeywords(midTitle));
|
|
2549
2620
|
if (knowledgeInlineRA) parts.push(knowledgeInlineRA);
|
|
2550
2621
|
|
|
2551
|
-
const inlinedContext =
|
|
2622
|
+
const inlinedContext = prependContextModeToBlock(
|
|
2623
|
+
"reassess-roadmap",
|
|
2624
|
+
base,
|
|
2625
|
+
capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`),
|
|
2626
|
+
);
|
|
2552
2627
|
|
|
2553
2628
|
const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
|
|
2554
2629
|
|
|
@@ -2641,6 +2716,7 @@ export async function buildReactiveExecutePrompt(
|
|
|
2641
2716
|
sessionContextWindow: opts?.sessionContextWindow,
|
|
2642
2717
|
modelRegistry: opts?.modelRegistry,
|
|
2643
2718
|
sessionProvider: opts?.sessionProvider,
|
|
2719
|
+
contextModeRenderMode: "nested",
|
|
2644
2720
|
},
|
|
2645
2721
|
);
|
|
2646
2722
|
|
|
@@ -2664,7 +2740,7 @@ export async function buildReactiveExecutePrompt(
|
|
|
2664
2740
|
milestoneTitle: midTitle,
|
|
2665
2741
|
sliceId: sid,
|
|
2666
2742
|
sliceTitle: sTitle,
|
|
2667
|
-
graphContext,
|
|
2743
|
+
graphContext: prependContextModeToBlock("reactive-execute", base, graphContext),
|
|
2668
2744
|
readyTaskCount: String(readyTaskIds.length),
|
|
2669
2745
|
readyTaskList: readyTaskListLines.join("\n"),
|
|
2670
2746
|
subagentPrompts: subagentSections.join("\n\n---\n\n"),
|
|
@@ -2730,7 +2806,7 @@ export async function buildParallelResearchSlicesPrompt(
|
|
|
2730
2806
|
const subagentSections: string[] = [];
|
|
2731
2807
|
const modelSuffix = subagentModel ? ` with model: "${subagentModel}"` : "";
|
|
2732
2808
|
for (const slice of slices) {
|
|
2733
|
-
const slicePrompt = await buildResearchSlicePrompt(mid, midTitle, slice.id, slice.title, basePath);
|
|
2809
|
+
const slicePrompt = await buildResearchSlicePrompt(mid, midTitle, slice.id, slice.title, basePath, { contextModeRenderMode: "nested" });
|
|
2734
2810
|
subagentSections.push([
|
|
2735
2811
|
`### ${slice.id}: ${slice.title}`,
|
|
2736
2812
|
"",
|
|
@@ -2786,6 +2862,8 @@ export async function buildGateEvaluatePrompt(
|
|
|
2786
2862
|
gateListLines.push(`- **${def.id}**: ${def.question}`);
|
|
2787
2863
|
|
|
2788
2864
|
const subPrompt = [
|
|
2865
|
+
renderContextModeForPrompt("gate-evaluate", base, "nested"),
|
|
2866
|
+
"",
|
|
2789
2867
|
`You are evaluating quality gate **${def.id}** for slice ${sid} (${sTitle}).`,
|
|
2790
2868
|
"",
|
|
2791
2869
|
`**Working directory:** \`${normalizedBase}\`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT \`cd\` to any other directory.`,
|
|
@@ -2828,7 +2906,7 @@ export async function buildGateEvaluatePrompt(
|
|
|
2828
2906
|
milestoneTitle: midTitle,
|
|
2829
2907
|
sliceId: sid,
|
|
2830
2908
|
sliceTitle: sTitle,
|
|
2831
|
-
slicePlanContent: planContent,
|
|
2909
|
+
slicePlanContent: prependContextModeToBlock("gate-evaluate", base, planContent),
|
|
2832
2910
|
gateCount: String(pending.length),
|
|
2833
2911
|
gateList: gateListLines.join("\n"),
|
|
2834
2912
|
subagentPrompts: subagentSections.join("\n\n---\n\n"),
|
|
@@ -2905,7 +2983,7 @@ export async function buildRewriteDocsPrompt(
|
|
|
2905
2983
|
|
|
2906
2984
|
const documentList = docList.length > 0 ? docList.join("\n") : "- No active plan documents found.";
|
|
2907
2985
|
|
|
2908
|
-
return loadPrompt("rewrite-docs", {
|
|
2986
|
+
return prependContextModeToBlock("rewrite-docs", base, loadPrompt("rewrite-docs", {
|
|
2909
2987
|
workingDirectory: base,
|
|
2910
2988
|
milestoneId: mid,
|
|
2911
2989
|
milestoneTitle: midTitle,
|
|
@@ -2914,5 +2992,5 @@ export async function buildRewriteDocsPrompt(
|
|
|
2914
2992
|
overrideContent,
|
|
2915
2993
|
documentList,
|
|
2916
2994
|
overridesPath: relGsdRootFile("OVERRIDES"),
|
|
2917
|
-
});
|
|
2995
|
+
}));
|
|
2918
2996
|
}
|
|
@@ -308,7 +308,7 @@ function scanGsdTaggedCommits(
|
|
|
308
308
|
if (!commitMessageHasGsdTrailer(message)) continue;
|
|
309
309
|
|
|
310
310
|
const commitFiles = getChangedFilesForCommit(basePath, hash);
|
|
311
|
-
if (!commitMatchesMilestone(message, milestoneId, commitFiles)) continue;
|
|
311
|
+
if (!commitMatchesMilestone(basePath, message, milestoneId, commitFiles)) continue;
|
|
312
312
|
|
|
313
313
|
matched = true;
|
|
314
314
|
for (const file of commitFiles) {
|
|
@@ -336,22 +336,36 @@ function commitMessageHasGsdTrailer(message: string): boolean {
|
|
|
336
336
|
return /^GSD-(?:Task|Unit):\s*\S+/m.test(message);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
function commitMatchesMilestone(message: string, milestoneId: string, files: readonly string[]): boolean {
|
|
339
|
+
function commitMatchesMilestone(basePath: string, message: string, milestoneId: string, files: readonly string[]): boolean {
|
|
340
340
|
if (commitTrailerStartsWithMilestone(message, milestoneId)) return true;
|
|
341
341
|
|
|
342
342
|
// Meaningful execute-task commits currently store task scope as Sxx/Tyy
|
|
343
343
|
// rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
|
|
344
344
|
// either the commit touched this milestone's artifacts, or — for projects
|
|
345
345
|
// where .gsd/ is gitignored/external (#5033) — the message explicitly
|
|
346
|
-
// names the milestone.
|
|
346
|
+
// names the milestone or local GSD state proves the task belongs here.
|
|
347
347
|
if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
|
|
348
348
|
if (files.some((file) => isMilestoneArtifactPath(file, milestoneId))) return true;
|
|
349
349
|
if (commitMessageMentionsMilestone(message, milestoneId)) return true;
|
|
350
|
+
if (commitTaskTrailerBelongsToMilestone(basePath, message, milestoneId)) return true;
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
return false;
|
|
353
354
|
}
|
|
354
355
|
|
|
356
|
+
function commitTaskTrailerBelongsToMilestone(basePath: string, message: string, milestoneId: string): boolean {
|
|
357
|
+
const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
|
|
358
|
+
if (!match) return false;
|
|
359
|
+
const [, sliceId, taskId] = match;
|
|
360
|
+
|
|
361
|
+
if (getTask(milestoneId, sliceId, taskId)) return true;
|
|
362
|
+
|
|
363
|
+
const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
|
|
364
|
+
if (!tasksDir) return false;
|
|
365
|
+
return existsSync(join(tasksDir, `${taskId}-PLAN.md`))
|
|
366
|
+
|| existsSync(join(tasksDir, `${taskId}-SUMMARY.md`));
|
|
367
|
+
}
|
|
368
|
+
|
|
355
369
|
function commitMessageMentionsMilestone(message: string, milestoneId: string): boolean {
|
|
356
370
|
if (!MILESTONE_ID_RE.test(milestoneId)) return false;
|
|
357
371
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GSD2 — Exec (context-mode) tool registration.
|
|
2
2
|
//
|
|
3
|
-
// Exposes the
|
|
4
|
-
// `context_mode.enabled:
|
|
3
|
+
// Exposes the Context Mode runtime tools in-process. Default-on; opt out with
|
|
4
|
+
// `context_mode.enabled: false` in preferences.
|
|
5
5
|
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
@@ -64,6 +64,38 @@ async function applyDisabledModelProviderPolicy(ctx: ExtensionContext): Promise<
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
async function writeContextModeCompactionSnapshot(basePath: string): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
70
|
+
const { isContextModeEnabled } = await import("../preferences-types.js");
|
|
71
|
+
const prefs = loadEffectiveGSDPreferences(basePath);
|
|
72
|
+
if (!isContextModeEnabled(prefs?.preferences)) return;
|
|
73
|
+
|
|
74
|
+
const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
|
|
75
|
+
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
76
|
+
await ensureDbOpen(basePath);
|
|
77
|
+
|
|
78
|
+
let activeContext: string | null = null;
|
|
79
|
+
try {
|
|
80
|
+
const state = await deriveGsdState(basePath);
|
|
81
|
+
if (state.activeMilestone && state.activeSlice && state.activeTask) {
|
|
82
|
+
activeContext =
|
|
83
|
+
`Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
|
|
84
|
+
(state.activeTask.title ? ` - ${state.activeTask.title}` : "");
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
/* non-fatal */
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeCompactionSnapshot(basePath, { activeContext });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
safetyLogWarning(
|
|
93
|
+
"context-mode",
|
|
94
|
+
`failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
67
99
|
export function registerHooks(
|
|
68
100
|
pi: ExtensionAPI,
|
|
69
101
|
ecosystemHandlers: GSDEcosystemBeforeAgentStartHandler[],
|
|
@@ -229,15 +261,19 @@ export function registerHooks(
|
|
|
229
261
|
});
|
|
230
262
|
|
|
231
263
|
pi.on("session_before_compact", async () => {
|
|
264
|
+
const basePath = process.cwd();
|
|
265
|
+
// Context Mode is default-on. Write the resumable snapshot before any
|
|
266
|
+
// active-auto cancel return so auto sessions still leave re-entry context.
|
|
267
|
+
await writeContextModeCompactionSnapshot(basePath);
|
|
268
|
+
|
|
232
269
|
// Only cancel compaction while auto-mode is actively running.
|
|
233
270
|
// Paused auto-mode should allow compaction — the user may be doing
|
|
234
271
|
// interactive work (#3165).
|
|
235
272
|
if (isAutoActive()) {
|
|
236
273
|
return { cancel: true };
|
|
237
274
|
}
|
|
238
|
-
const basePath = process.cwd();
|
|
239
275
|
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
240
|
-
await ensureDbOpen();
|
|
276
|
+
await ensureDbOpen(basePath);
|
|
241
277
|
const state = await deriveGsdState(basePath);
|
|
242
278
|
if (!state.activeMilestone || !state.activeSlice) return;
|
|
243
279
|
// Write checkpoint for ALL phases, not just "executing" — discuss, research,
|
|
@@ -282,42 +318,6 @@ export function registerHooks(
|
|
|
282
318
|
}));
|
|
283
319
|
});
|
|
284
320
|
|
|
285
|
-
// Context-mode snapshot: write .gsd/last-snapshot.md before compaction so
|
|
286
|
-
// agents can call gsd_resume (or Read the file) to re-orient. Opt-in via
|
|
287
|
-
// preferences.context_mode.enabled. Runs after the auto-cancel handler
|
|
288
|
-
// above — if that one returned cancel:true, pi still fires us but the
|
|
289
|
-
// compaction won't actually happen; the snapshot is still useful then,
|
|
290
|
-
// since auto may pause and resume later.
|
|
291
|
-
pi.on("session_before_compact", async () => {
|
|
292
|
-
try {
|
|
293
|
-
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
294
|
-
const { isContextModeEnabled } = await import("../preferences-types.js");
|
|
295
|
-
const prefs = loadEffectiveGSDPreferences();
|
|
296
|
-
if (!isContextModeEnabled(prefs?.preferences)) return;
|
|
297
|
-
const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
|
|
298
|
-
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
299
|
-
await ensureDbOpen();
|
|
300
|
-
const basePath = process.cwd();
|
|
301
|
-
let activeContext: string | null = null;
|
|
302
|
-
try {
|
|
303
|
-
const state = await deriveGsdState(basePath);
|
|
304
|
-
if (state.activeMilestone && state.activeSlice && state.activeTask) {
|
|
305
|
-
activeContext =
|
|
306
|
-
`Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
|
|
307
|
-
(state.activeTask.title ? ` — ${state.activeTask.title}` : "");
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
/* non-fatal */
|
|
311
|
-
}
|
|
312
|
-
writeCompactionSnapshot(basePath, { activeContext });
|
|
313
|
-
} catch (err) {
|
|
314
|
-
safetyLogWarning(
|
|
315
|
-
"context-mode",
|
|
316
|
-
`failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
321
|
pi.on("message_update", async (event, ctx: ExtensionContext) => {
|
|
322
322
|
if (approvalQuestionAbortInFlight) return;
|
|
323
323
|
|
|
@@ -154,8 +154,26 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
154
154
|
"planning_depth",
|
|
155
155
|
]);
|
|
156
156
|
|
|
157
|
-
/**
|
|
158
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Broad union of every recognized unit-type *label* used across the codebase.
|
|
159
|
+
*
|
|
160
|
+
* This intentionally covers more than the manifest-tracked dispatch units in
|
|
161
|
+
* `unit-context-manifest.ts:KNOWN_UNIT_TYPES`. Examples that live here but not
|
|
162
|
+
* in the manifest:
|
|
163
|
+
* - `discuss-slice` — dispatched by `guided-flow.ts` rather than auto-mode;
|
|
164
|
+
* composer falls through to default behavior via `resolveManifest()` null path.
|
|
165
|
+
* - `worktree-merge` — used as a model-routing case, prompt-template name, and
|
|
166
|
+
* commit-message label, not as an LLM-dispatched unit.
|
|
167
|
+
*
|
|
168
|
+
* Used by `preferences-validation.ts` to validate user-provided unit-type
|
|
169
|
+
* references in preferences (model overrides, skill rules, etc.) — preferences
|
|
170
|
+
* may legitimately reference any label, including non-dispatched ones.
|
|
171
|
+
*
|
|
172
|
+
* The manifest-strict subset lives in `unit-context-manifest.ts:KNOWN_UNIT_TYPES`
|
|
173
|
+
* and is enforced 1:1 against `UNIT_MANIFESTS` by the parity test in
|
|
174
|
+
* `tests/unit-context-manifest.test.ts`.
|
|
175
|
+
*/
|
|
176
|
+
export const KNOWN_UNIT_LABELS = [
|
|
159
177
|
"research-milestone", "plan-milestone", "research-slice", "plan-slice", "refine-slice",
|
|
160
178
|
"execute-task", "reactive-execute", "gate-evaluate", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
161
179
|
"run-uat", "complete-milestone", "validate-milestone", "rewrite-docs",
|
|
@@ -164,7 +182,7 @@ export const KNOWN_UNIT_TYPES = [
|
|
|
164
182
|
"workflow-preferences", "discuss-project", "discuss-requirements",
|
|
165
183
|
"research-decision", "research-project",
|
|
166
184
|
] as const;
|
|
167
|
-
export type
|
|
185
|
+
export type UnitLabel = (typeof KNOWN_UNIT_LABELS)[number];
|
|
168
186
|
|
|
169
187
|
|
|
170
188
|
export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
|
|
@@ -343,7 +361,8 @@ export interface GSDPreferences {
|
|
|
343
361
|
/**
|
|
344
362
|
* Tool-output sandboxing via gsd_exec. Keeps sub-session context windows
|
|
345
363
|
* clean by running scripts in a subprocess and only surfacing a short
|
|
346
|
-
* digest. See `ContextModeConfig`. Default:
|
|
364
|
+
* digest. See `ContextModeConfig`. Default: enabled unless explicitly
|
|
365
|
+
* disabled with `context_mode.enabled: false`.
|
|
347
366
|
*/
|
|
348
367
|
context_mode?: ContextModeConfig;
|
|
349
368
|
token_profile?: TokenProfile;
|
|
@@ -14,7 +14,7 @@ import { normalizeStringArray } from "../shared/format-utils.js";
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
KNOWN_PREFERENCE_KEYS,
|
|
17
|
-
|
|
17
|
+
KNOWN_UNIT_LABELS,
|
|
18
18
|
|
|
19
19
|
SKILL_ACTIONS,
|
|
20
20
|
type WorkflowMode,
|
|
@@ -441,7 +441,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
441
441
|
if (preferences.post_unit_hooks && Array.isArray(preferences.post_unit_hooks)) {
|
|
442
442
|
const validHooks: PostUnitHookConfig[] = [];
|
|
443
443
|
const seenNames = new Set<string>();
|
|
444
|
-
const knownUnitTypes = new Set<string>(
|
|
444
|
+
const knownUnitTypes = new Set<string>(KNOWN_UNIT_LABELS);
|
|
445
445
|
for (const hook of preferences.post_unit_hooks) {
|
|
446
446
|
if (!hook || typeof hook !== "object") {
|
|
447
447
|
errors.push("post_unit_hooks entry must be an object");
|
|
@@ -503,7 +503,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
503
503
|
if (preferences.pre_dispatch_hooks && Array.isArray(preferences.pre_dispatch_hooks)) {
|
|
504
504
|
const validPreHooks: PreDispatchHookConfig[] = [];
|
|
505
505
|
const seenPreNames = new Set<string>();
|
|
506
|
-
const knownUnitTypes = new Set<string>(
|
|
506
|
+
const knownUnitTypes = new Set<string>(KNOWN_UNIT_LABELS);
|
|
507
507
|
const validActions = new Set(["modify", "skip", "replace"]);
|
|
508
508
|
for (const hook of preferences.pre_dispatch_hooks) {
|
|
509
509
|
if (!hook || typeof hook !== "object") {
|