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.
Files changed (96) hide show
  1. package/README.md +94 -47
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
  4. package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
  5. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -37
  7. package/dist/resources/extensions/gsd/commands/context.js +1 -1
  8. package/dist/resources/extensions/gsd/preferences-types.js +20 -2
  9. package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
  10. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +41 -2
  11. package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
  12. package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
  13. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  14. package/dist/web/standalone/.next/BUILD_ID +1 -1
  15. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  16. package/dist/web/standalone/.next/build-manifest.json +2 -2
  17. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  18. package/dist/web/standalone/.next/required-server-files.json +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.html +1 -1
  36. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  43. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  45. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  46. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  47. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
  48. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  49. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
  50. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
  51. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
  52. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  53. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
  54. package/dist/web/standalone/server.js +1 -1
  55. package/package.json +1 -1
  56. package/packages/daemon/package.json +2 -2
  57. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
  58. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  59. package/packages/mcp-server/dist/workflow-tools.js +53 -0
  60. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  61. package/packages/mcp-server/package.json +2 -2
  62. package/packages/mcp-server/src/workflow-tools.test.ts +116 -0
  63. package/packages/mcp-server/src/workflow-tools.ts +81 -0
  64. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  65. package/packages/native/package.json +1 -1
  66. package/packages/pi-agent-core/package.json +1 -1
  67. package/packages/pi-ai/package.json +1 -1
  68. package/packages/pi-coding-agent/package.json +1 -1
  69. package/packages/pi-tui/package.json +1 -1
  70. package/packages/rpc-client/package.json +1 -1
  71. package/pkg/package.json +1 -1
  72. package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
  73. package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
  74. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  75. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +38 -38
  76. package/src/resources/extensions/gsd/commands/context.ts +1 -1
  77. package/src/resources/extensions/gsd/preferences-types.ts +23 -4
  78. package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
  79. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +68 -1
  80. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
  81. package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
  82. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
  83. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
  84. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
  85. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
  86. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
  87. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
  88. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
  89. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
  90. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +100 -0
  91. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
  92. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +41 -1
  93. package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
  94. package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
  95. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_buildManifest.js +0 -0
  96. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_ssgManifest.js +0 -0
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.78.1",
3
+ "version": "2.79.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -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 `${basePrompt}\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}`;
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${finalBody}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${composed}`);
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 = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`);
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 `gsd_exec` tool over MCP. Opt-in: disabled unless
4
- // `context_mode.enabled: true` is set in preferences.
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
 
@@ -65,7 +65,7 @@ export function currentDirectoryRoot(): string {
65
65
  try {
66
66
  cwd = process.cwd();
67
67
  } catch {
68
- cwd = process.env.HOME ?? "/";
68
+ cwd = homedir();
69
69
  }
70
70
  }
71
71
  const result = validateDirectory(cwd);
@@ -154,8 +154,26 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
154
154
  "planning_depth",
155
155
  ]);
156
156
 
157
- /** Canonical list of all dispatch unit types. */
158
- export const KNOWN_UNIT_TYPES = [
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 UnitType = (typeof KNOWN_UNIT_TYPES)[number];
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: disabled.
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
- KNOWN_UNIT_TYPES,
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>(KNOWN_UNIT_TYPES);
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>(KNOWN_UNIT_TYPES);
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") {