gsd-pi 2.79.0 → 2.80.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.
Files changed (151) 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/contracts.js +1 -0
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +61 -7
  6. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  7. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
  10. package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
  11. package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
  12. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  13. package/dist/resources/extensions/gsd/auto.js +159 -2
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
  15. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +41 -45
  17. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
  18. package/dist/resources/extensions/gsd/commands/context.js +1 -1
  19. package/dist/resources/extensions/gsd/gsd-db.js +34 -1
  20. package/dist/resources/extensions/gsd/guided-flow.js +40 -0
  21. package/dist/resources/extensions/gsd/paths.js +5 -1
  22. package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
  23. package/dist/resources/extensions/gsd/preferences-types.js +20 -2
  24. package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
  25. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +82 -2
  26. package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
  27. package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
  28. package/dist/resources/extensions/gsd/uok/audit.js +23 -9
  29. package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
  30. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
  31. package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
  32. package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
  33. package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
  34. package/dist/resources/extensions/shared/interview-ui.js +15 -4
  35. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  36. package/dist/web/standalone/.next/BUILD_ID +1 -1
  37. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  38. package/dist/web/standalone/.next/build-manifest.json +2 -2
  39. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.html +1 -1
  56. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  63. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  65. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  66. package/package.json +1 -1
  67. package/packages/daemon/package.json +2 -2
  68. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
  69. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  70. package/packages/mcp-server/dist/workflow-tools.js +53 -0
  71. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  72. package/packages/mcp-server/package.json +2 -2
  73. package/packages/mcp-server/src/workflow-tools.test.ts +129 -2
  74. package/packages/mcp-server/src/workflow-tools.ts +81 -0
  75. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  76. package/packages/native/package.json +1 -1
  77. package/packages/pi-agent-core/package.json +1 -1
  78. package/packages/pi-ai/package.json +1 -1
  79. package/packages/pi-coding-agent/package.json +1 -1
  80. package/packages/pi-tui/package.json +1 -1
  81. package/packages/rpc-client/package.json +1 -1
  82. package/pkg/package.json +1 -1
  83. package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
  84. package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
  85. package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
  86. package/src/resources/extensions/gsd/auto/phases.ts +88 -9
  87. package/src/resources/extensions/gsd/auto/session.ts +11 -0
  88. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  89. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  90. package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
  91. package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
  92. package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
  93. package/src/resources/extensions/gsd/auto-start.ts +3 -2
  94. package/src/resources/extensions/gsd/auto.ts +167 -1
  95. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
  96. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  97. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +49 -46
  98. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
  99. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
  100. package/src/resources/extensions/gsd/commands/context.ts +1 -1
  101. package/src/resources/extensions/gsd/gsd-db.ts +35 -1
  102. package/src/resources/extensions/gsd/guided-flow.ts +47 -0
  103. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  104. package/src/resources/extensions/gsd/paths.ts +6 -1
  105. package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
  106. package/src/resources/extensions/gsd/preferences-types.ts +23 -4
  107. package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
  108. package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
  109. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
  110. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
  111. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
  112. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
  113. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
  114. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
  115. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
  116. package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
  117. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
  118. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
  119. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
  120. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
  121. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
  122. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
  123. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
  124. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
  125. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
  126. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
  127. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +46 -0
  128. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
  129. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
  130. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
  131. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
  132. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
  133. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
  134. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
  135. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
  136. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +132 -3
  137. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
  138. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +84 -1
  139. package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
  140. package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
  141. package/src/resources/extensions/gsd/uok/audit.ts +25 -9
  142. package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
  143. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
  144. package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
  145. package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
  146. package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
  147. package/src/resources/extensions/shared/interview-ui.ts +18 -5
  148. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
  149. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
  150. /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
  151. /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
@@ -11,7 +11,9 @@ import {
11
11
  _getAdapter,
12
12
  insertGateRow,
13
13
  upsertRequirement,
14
+ getAllMilestones,
14
15
  } from "../gsd-db.ts";
16
+ import { deriveState, invalidateStateCache } from "../state.ts";
15
17
  import { markApprovalGateVerified, markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot, setPendingGate } from "../bootstrap/write-gate.ts";
16
18
  import {
17
19
  executeCompleteMilestone,
@@ -690,7 +692,18 @@ test("executeSummarySave supports root-level deep planning artifacts", async ()
690
692
 
691
693
  const project = await inProjectDir(base, () => executeSummarySave({
692
694
  artifact_type: "PROJECT",
693
- content: "# Project\n\n## What This Is\n\nA root project artifact.",
695
+ content: [
696
+ "# Project",
697
+ "",
698
+ "## What This Is",
699
+ "",
700
+ "A root project artifact.",
701
+ "",
702
+ "## Milestone Sequence",
703
+ "",
704
+ "- [ ] M001: Foundation - Establish the first runnable slice.",
705
+ "",
706
+ ].join("\n"),
694
707
  }, base));
695
708
  assert.equal(project.isError, undefined);
696
709
  assert.equal(project.details.path, "PROJECT.md");
@@ -737,6 +750,109 @@ test("executeSummarySave supports root-level deep planning artifacts", async ()
737
750
  }
738
751
  });
739
752
 
753
+ test("executeSummarySave registers PROJECT milestone sequence for the next run", async () => {
754
+ const base = makeTmpBase();
755
+ try {
756
+ openTestDb(base);
757
+
758
+ const result = await inProjectDir(base, () => executeSummarySave({
759
+ artifact_type: "PROJECT",
760
+ content: [
761
+ "# Project",
762
+ "",
763
+ "## What This Is",
764
+ "",
765
+ "Deep project setup output.",
766
+ "",
767
+ "## Project Shape",
768
+ "",
769
+ "**Complexity:** complex",
770
+ "**Why:** It spans multiple delivery steps.",
771
+ "",
772
+ "## Capability Contract",
773
+ "",
774
+ "See .gsd/REQUIREMENTS.md.",
775
+ "",
776
+ "## Milestone Sequence",
777
+ "",
778
+ "- [ ] M001: Foundation - Establish the first runnable slice.",
779
+ "- [ ] M002: Polish - Follow-up experience work.",
780
+ "",
781
+ ].join("\n"),
782
+ }, base));
783
+
784
+ assert.equal(result.isError, undefined);
785
+ assert.deepEqual(result.details.registeredMilestones, ["M001", "M002"]);
786
+
787
+ const milestones = getAllMilestones();
788
+ assert.deepEqual(
789
+ milestones.map((m) => [m.id, m.title, m.status]),
790
+ [
791
+ ["M001", "Foundation", "queued"],
792
+ ["M002", "Polish", "queued"],
793
+ ],
794
+ );
795
+
796
+ invalidateStateCache();
797
+ const state = await deriveState(base);
798
+ assert.equal(state.activeMilestone?.id, "M001");
799
+ assert.equal(state.phase, "pre-planning");
800
+ assert.equal(state.registry[0]?.status, "active");
801
+ assert.equal(state.registry[1]?.status, "pending");
802
+ } finally {
803
+ closeDatabase();
804
+ cleanup(base);
805
+ }
806
+ });
807
+
808
+ test("executeSummarySave hard-fails when milestone registration throws so silent No-Active-Milestone is impossible", async () => {
809
+ const base = makeTmpBase();
810
+ try {
811
+ openTestDb(base);
812
+ const db = _getAdapter();
813
+ assert.ok(db, "DB should be open");
814
+ const originalPrepare = db.prepare.bind(db);
815
+ (db as any).prepare = (sql: string) => {
816
+ if (sql.includes("INSERT OR IGNORE INTO milestones")) {
817
+ throw new Error("simulated milestone registration failure");
818
+ }
819
+ return originalPrepare(sql);
820
+ };
821
+
822
+ const result = await inProjectDir(base, () => executeSummarySave({
823
+ artifact_type: "PROJECT",
824
+ content: [
825
+ "# Project",
826
+ "",
827
+ "## What This Is",
828
+ "",
829
+ "Deep project setup output.",
830
+ "",
831
+ "## Milestone Sequence",
832
+ "",
833
+ "- [ ] M001: Foundation - Establish the first runnable slice.",
834
+ "",
835
+ ].join("\n"),
836
+ }, base));
837
+
838
+ // The artifact is persisted before registration runs, but registration must
839
+ // surface as isError so the LLM retries (INSERT OR IGNORE makes it idempotent)
840
+ // instead of announcing "ready" while the DB has zero milestone rows.
841
+ assert.equal(result.isError, true);
842
+ assert.equal(result.details.path, "PROJECT.md");
843
+ assert.equal(result.details.error, "milestone_registration_threw");
844
+ assert.match(String(result.details.registration_error), /simulated milestone registration failure/);
845
+ assert.match(result.content[0].text, /milestone registration failed/);
846
+ assert.match(result.content[0].text, /idempotent/);
847
+ assert.ok(existsSync(join(base, ".gsd", "PROJECT.md")));
848
+ const artifact = originalPrepare("SELECT path FROM artifacts WHERE path = ?").get("PROJECT.md");
849
+ assert.equal(artifact?.path, "PROJECT.md");
850
+ } finally {
851
+ closeDatabase();
852
+ cleanup(base);
853
+ }
854
+ });
855
+
740
856
  test("executeSummarySave blocks final root artifacts while approval gate is pending", async () => {
741
857
  const base = makeTmpBase();
742
858
  try {
@@ -772,9 +888,22 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
772
888
  writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\nplanning_depth: deep\n---\n");
773
889
  openTestDb(base);
774
890
 
891
+ const projectFixture = [
892
+ "# Project",
893
+ "",
894
+ "## What This Is",
895
+ "",
896
+ "A root project artifact.",
897
+ "",
898
+ "## Milestone Sequence",
899
+ "",
900
+ "- [ ] M001: Foundation - Establish the first runnable slice.",
901
+ "",
902
+ ].join("\n");
903
+
775
904
  const blocked = await inProjectDir(base, () => executeSummarySave({
776
905
  artifact_type: "PROJECT",
777
- content: "# Project\n\n## What This Is\n\nA root project artifact.",
906
+ content: projectFixture,
778
907
  }, base));
779
908
 
780
909
  assert.equal(blocked.isError, true);
@@ -786,7 +915,7 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
786
915
 
787
916
  const unblocked = await inProjectDir(base, () => executeSummarySave({
788
917
  artifact_type: "PROJECT",
789
- content: "# Project\n\n## What This Is\n\nA root project artifact.",
918
+ content: projectFixture,
790
919
  }, base));
791
920
 
792
921
  assert.equal(unblocked.isError, undefined);
@@ -229,6 +229,9 @@ test("worktree-aware prompt builders include the explicit working directory", as
229
229
  ),
230
230
  ]);
231
231
 
232
+ assert.ok(prompts[0].includes("## Context Mode"), "discuss-milestone should include standalone Context Mode guidance");
233
+ assert.ok(prompts[0].includes("interview lane"), "discuss-milestone should render the interview lane");
234
+
232
235
  for (const prompt of prompts) {
233
236
  assert.match(prompt, /working directory/i);
234
237
  assert.ok(prompt.includes(base), "prompt should include the provided working directory");
@@ -3,6 +3,7 @@ import { sanitizeCompleteMilestoneParams } from "../bootstrap/sanitize-complete-
3
3
  import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot, shouldBlockRootArtifactSaveInSnapshot } from "../bootstrap/write-gate.js";
4
4
  import {
5
5
  getActiveRequirements,
6
+ insertMilestone,
6
7
  getMilestone,
7
8
  getSliceStatusSummary,
8
9
  getSliceTaskCounts,
@@ -31,6 +32,7 @@ import { handleValidateMilestone } from "./validate-milestone.js";
31
32
  import { logError, logWarning } from "../workflow-logger.js";
32
33
  import { invalidateStateCache } from "../state.js";
33
34
  import { loadEffectiveGSDPreferences } from "../preferences.js";
35
+ import { parseProject } from "../schemas/parsers.js";
34
36
 
35
37
  export const SUPPORTED_SUMMARY_ARTIFACT_TYPES = [
36
38
  "SUMMARY",
@@ -71,6 +73,20 @@ export interface SummarySaveParams {
71
73
  content: string;
72
74
  }
73
75
 
76
+ function registerProjectMilestoneSequence(content: string): string[] {
77
+ const parsed = parseProject(content);
78
+ const registered: string[] = [];
79
+ for (const milestone of parsed.milestones) {
80
+ insertMilestone({
81
+ id: milestone.id,
82
+ title: milestone.title,
83
+ status: milestone.done ? "complete" : "queued",
84
+ });
85
+ registered.push(milestone.id);
86
+ }
87
+ return registered;
88
+ }
89
+
74
90
  export async function executeSummarySave(
75
91
  params: SummarySaveParams,
76
92
  basePath: string = process.cwd(),
@@ -173,6 +189,67 @@ export async function executeSummarySave(
173
189
  basePath,
174
190
  );
175
191
 
192
+ let registeredMilestones: string[] = [];
193
+ if (params.artifact_type === "PROJECT") {
194
+ try {
195
+ registeredMilestones = registerProjectMilestoneSequence(contentToSave);
196
+ if (registeredMilestones.length > 0) invalidateStateCache();
197
+ } catch (err) {
198
+ const msg = err instanceof Error ? err.message : String(err);
199
+ logError("tool", `gsd_summary_save: PROJECT artifact persisted but milestone registration threw: ${msg}`, {
200
+ tool: "gsd_summary_save",
201
+ error: String(err),
202
+ stack: err instanceof Error ? err.stack ?? "" : "",
203
+ });
204
+ // PROJECT.md was persisted by saveArtifactToDb above; the artifacts row
205
+ // changed even though no milestones registered. Invalidate so subsequent
206
+ // /gsd reads see the persisted artifact instead of the pre-save cache.
207
+ invalidateStateCache();
208
+ return {
209
+ content: [{
210
+ type: "text",
211
+ text:
212
+ `Error: PROJECT.md was saved to ${relativePath} but milestone registration failed: ${msg}. ` +
213
+ `The DB has no milestone rows for this project, so /gsd will report "No Active Milestone". ` +
214
+ `Re-call gsd_summary_save(PROJECT) once the underlying error is resolved — INSERT OR IGNORE makes registration idempotent.`,
215
+ }],
216
+ details: {
217
+ operation: "save_summary",
218
+ path: relativePath,
219
+ artifact_type: params.artifact_type,
220
+ error: "milestone_registration_threw",
221
+ registration_error: msg,
222
+ },
223
+ isError: true,
224
+ };
225
+ }
226
+ if (registeredMilestones.length === 0) {
227
+ logError("tool", `gsd_summary_save: PROJECT.md saved to ${relativePath} but parsed zero milestones — registration produced no DB rows`, {
228
+ tool: "gsd_summary_save",
229
+ });
230
+ // PROJECT.md was persisted; invalidate so subsequent reads see the new
231
+ // artifacts row even though no milestones registered.
232
+ invalidateStateCache();
233
+ return {
234
+ content: [{
235
+ type: "text",
236
+ text:
237
+ `Error: PROJECT.md was saved to ${relativePath} but contains zero parseable milestone lines, ` +
238
+ `so no milestones were registered in the DB. /gsd will report "No Active Milestone". ` +
239
+ `Rewrite PROJECT.md so the "Milestone Sequence" section uses canonical lines: ` +
240
+ `\`- [ ] M001: <Title> — <One-liner>\` (em-dash, double-dash \`--\`, or single-dash \`-\` separator), then re-call gsd_summary_save(PROJECT).`,
241
+ }],
242
+ details: {
243
+ operation: "save_summary",
244
+ path: relativePath,
245
+ artifact_type: params.artifact_type,
246
+ error: "milestone_registration_empty_parse",
247
+ },
248
+ isError: true,
249
+ };
250
+ }
251
+ }
252
+
176
253
  if (params.artifact_type === "CONTEXT" && !params.task_id) {
177
254
  try {
178
255
  const draftFile = params.slice_id
@@ -186,7 +263,13 @@ export async function executeSummarySave(
186
263
 
187
264
  return {
188
265
  content: [{ type: "text", text: `Saved ${params.artifact_type} artifact to ${relativePath}` }],
189
- details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type, content_source: contentSource },
266
+ details: {
267
+ operation: "save_summary",
268
+ path: relativePath,
269
+ artifact_type: params.artifact_type,
270
+ content_source: contentSource,
271
+ ...(registeredMilestones.length > 0 ? { registeredMilestones } : {}),
272
+ },
190
273
  };
191
274
  } catch (err) {
192
275
  const msg = err instanceof Error ? err.message : String(err);
@@ -42,6 +42,7 @@ import {
42
42
  type BaseResolverContext,
43
43
  type ComputedArtifactId,
44
44
  type ComputedArtifactRegistry,
45
+ type ContextModePolicy,
45
46
  type UnitContextManifest,
46
47
  } from "./unit-context-manifest.js";
47
48
 
@@ -92,6 +93,54 @@ export function manifestBudgetChars(unitType: string): number | null {
92
93
  return manifest ? manifest.maxSystemPromptChars : null;
93
94
  }
94
95
 
96
+ // ─── Context Mode lane guidance ──────────────────────────────────────────
97
+
98
+ export type ContextModeRenderMode = "standalone" | "nested";
99
+
100
+ export interface ComposeContextModeInstructionOptions {
101
+ readonly enabled: boolean;
102
+ readonly renderMode: ContextModeRenderMode;
103
+ }
104
+
105
+ const CONTEXT_MODE_LANE_LABELS: Record<Exclude<ContextModePolicy, "none">, string> = {
106
+ interview: "interview",
107
+ research: "research",
108
+ planning: "planning",
109
+ execution: "execution",
110
+ verification: "verification",
111
+ orchestration: "orchestration",
112
+ docs: "documentation",
113
+ };
114
+
115
+ const CONTEXT_MODE_GUIDANCE =
116
+ "Use `gsd_exec` for noisy scans, builds, and tests so full output stays out of prompt context; call `gsd_exec_search` before repeating prior runs; call `gsd_resume` after compaction or resume to recover stored execution context.";
117
+
118
+ /**
119
+ * Render the Context Mode instruction lane for a unit type. Unknown unit
120
+ * types, disabled config, and explicit `contextMode: "none"` all omit the
121
+ * block so callers can prefix this safely without extra branching.
122
+ */
123
+ export function composeContextModeInstructions(
124
+ unitType: string,
125
+ opts: ComposeContextModeInstructionOptions,
126
+ ): string {
127
+ if (!opts.enabled) return "";
128
+ const manifest = resolveManifest(unitType);
129
+ if (!manifest || manifest.contextMode === "none") return "";
130
+
131
+ const lane = CONTEXT_MODE_LANE_LABELS[manifest.contextMode];
132
+ if (opts.renderMode === "nested") {
133
+ return `Context Mode (${lane} lane): ${CONTEXT_MODE_GUIDANCE}`;
134
+ }
135
+
136
+ return [
137
+ "## Context Mode",
138
+ "",
139
+ `Lane: **${lane} lane**.`,
140
+ CONTEXT_MODE_GUIDANCE,
141
+ ].join("\n");
142
+ }
143
+
95
144
  // ─── v2 surface (#4924) ───────────────────────────────────────────────────
96
145
 
97
146
  /**
@@ -90,6 +90,17 @@ export type MemoryPolicy = "none" | "critical-only" | "prompt-relevant";
90
90
  /** Preferences block policy. */
91
91
  export type PreferencesPolicy = "none" | "active-only" | "full";
92
92
 
93
+ /** Context Mode lane guidance policy for each auto-mode unit. */
94
+ export type ContextModePolicy =
95
+ | "none"
96
+ | "interview"
97
+ | "research"
98
+ | "planning"
99
+ | "execution"
100
+ | "verification"
101
+ | "orchestration"
102
+ | "docs";
103
+
93
104
  /**
94
105
  * Tool-access policy per unit type (#4934).
95
106
  *
@@ -220,6 +231,8 @@ export interface UnitContextManifest {
220
231
  readonly codebaseMap: boolean;
221
232
  /** Preferences block policy. */
222
233
  readonly preferences: PreferencesPolicy;
234
+ /** Context Mode guidance lane. */
235
+ readonly contextMode: ContextModePolicy;
223
236
  /**
224
237
  * Tool-access policy (#4934). Runtime enforcement covers path-scoped write
225
238
  * blocking, subagent denial, and bash allowlisting for active auto-mode
@@ -343,6 +356,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
343
356
  memory: "prompt-relevant",
344
357
  codebaseMap: true,
345
358
  preferences: "active-only",
359
+ contextMode: "research",
346
360
  tools: TOOLS_PLANNING,
347
361
  artifacts: {
348
362
  // Phase 3 migration (#4782): matches today's actual
@@ -359,6 +373,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
359
373
  memory: "prompt-relevant",
360
374
  codebaseMap: true,
361
375
  preferences: "active-only",
376
+ contextMode: "planning",
362
377
  tools: TOOLS_PLANNING,
363
378
  artifacts: {
364
379
  inline: ["project", "requirements", "decisions", "milestone-research", "templates"],
@@ -373,6 +388,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
373
388
  memory: "prompt-relevant",
374
389
  codebaseMap: true,
375
390
  preferences: "active-only",
391
+ contextMode: "interview",
376
392
  tools: TOOLS_PLANNING,
377
393
  artifacts: {
378
394
  inline: ["project", "requirements", "decisions", "milestone-context", "templates"],
@@ -387,6 +403,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
387
403
  memory: "prompt-relevant",
388
404
  codebaseMap: false,
389
405
  preferences: "active-only",
406
+ contextMode: "verification",
390
407
  // planning-dispatch: validation is a verification-fan-out unit. It reads
391
408
  // the milestone surface and dispatches reviewer/security/tester subagents
392
409
  // to report findings without touching user source. Mirrors
@@ -405,6 +422,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
405
422
  memory: "prompt-relevant",
406
423
  codebaseMap: false,
407
424
  preferences: "active-only",
425
+ contextMode: "verification",
408
426
  // planning-dispatch: completion is a high-leverage place to fan out to
409
427
  // reviewer / security / tester subagents. They read the diff and report
410
428
  // findings; they do not write user source. Write isolation to .gsd/ is
@@ -428,6 +446,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
428
446
  memory: "prompt-relevant",
429
447
  codebaseMap: true,
430
448
  preferences: "active-only",
449
+ contextMode: "research",
431
450
  tools: TOOLS_PLANNING,
432
451
  artifacts: {
433
452
  inline: ["roadmap", "milestone-research", "dependency-summaries", "templates"],
@@ -442,6 +461,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
442
461
  memory: "prompt-relevant",
443
462
  codebaseMap: true,
444
463
  preferences: "active-only",
464
+ contextMode: "planning",
445
465
  // planning-dispatch: allows subagent dispatch so the planner can fan out
446
466
  // to scout for codebase recon and to planner/decompose-style specialists
447
467
  // for sub-decomposition. Write-isolation to .gsd/ is preserved.
@@ -459,6 +479,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
459
479
  memory: "prompt-relevant",
460
480
  codebaseMap: true,
461
481
  preferences: "active-only",
482
+ contextMode: "planning",
462
483
  // See plan-slice — same rationale: dispatch to scout/planner-style
463
484
  // specialists during refinement is materially better than re-doing recon
464
485
  // inline.
@@ -476,6 +497,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
476
497
  memory: "prompt-relevant",
477
498
  codebaseMap: true,
478
499
  preferences: "active-only",
500
+ contextMode: "planning",
479
501
  tools: TOOLS_PLANNING,
480
502
  artifacts: {
481
503
  inline: ["slice-plan", "slice-research", "dependency-summaries", "prior-task-summaries", "templates"],
@@ -490,6 +512,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
490
512
  memory: "prompt-relevant",
491
513
  codebaseMap: false,
492
514
  preferences: "active-only",
515
+ contextMode: "verification",
493
516
  // See complete-milestone — same rationale: dispatch to reviewer / security /
494
517
  // tester subagents to fan out review work without bloating this unit's
495
518
  // context.
@@ -511,6 +534,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
511
534
  memory: "critical-only",
512
535
  codebaseMap: false,
513
536
  preferences: "none",
537
+ contextMode: "planning",
514
538
  tools: TOOLS_PLANNING,
515
539
  artifacts: {
516
540
  // Phase 2 pilot (#4782): manifest now matches today's actual
@@ -530,6 +554,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
530
554
  memory: "prompt-relevant",
531
555
  codebaseMap: true,
532
556
  preferences: "active-only",
557
+ contextMode: "execution",
533
558
  tools: TOOLS_ALL,
534
559
  artifacts: {
535
560
  inline: ["task-plan", "slice-plan", "prior-task-summaries", "templates"],
@@ -544,6 +569,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
544
569
  memory: "prompt-relevant",
545
570
  codebaseMap: true,
546
571
  preferences: "active-only",
572
+ contextMode: "execution",
547
573
  tools: TOOLS_ALL,
548
574
  artifacts: {
549
575
  inline: ["slice-plan", "prior-task-summaries", "templates"],
@@ -560,6 +586,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
560
586
  memory: "critical-only",
561
587
  codebaseMap: false,
562
588
  preferences: "active-only",
589
+ contextMode: "verification",
563
590
  tools: TOOLS_PLANNING,
564
591
  artifacts: {
565
592
  // Phase 3 migration (#4782): manifest matches today's actual
@@ -578,6 +605,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
578
605
  memory: "critical-only",
579
606
  codebaseMap: false,
580
607
  preferences: "active-only",
608
+ contextMode: "verification",
581
609
  tools: TOOLS_PLANNING,
582
610
  artifacts: {
583
611
  inline: ["slice-plan", "prior-task-summaries"],
@@ -592,6 +620,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
592
620
  memory: "prompt-relevant",
593
621
  codebaseMap: true,
594
622
  preferences: "active-only",
623
+ contextMode: "docs",
595
624
  tools: TOOLS_DOCS,
596
625
  artifacts: {
597
626
  inline: ["project", "requirements", "decisions", "templates"],
@@ -611,6 +640,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
611
640
  memory: "none",
612
641
  codebaseMap: false,
613
642
  preferences: "none",
643
+ contextMode: "none",
614
644
  tools: TOOLS_PLANNING,
615
645
  artifacts: {
616
646
  inline: [],
@@ -628,6 +658,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
628
658
  memory: "prompt-relevant",
629
659
  codebaseMap: true,
630
660
  preferences: "active-only",
661
+ contextMode: "interview",
631
662
  tools: TOOLS_PLANNING,
632
663
  artifacts: {
633
664
  inline: ["templates"],
@@ -644,6 +675,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
644
675
  memory: "prompt-relevant",
645
676
  codebaseMap: true,
646
677
  preferences: "active-only",
678
+ contextMode: "interview",
647
679
  tools: TOOLS_PLANNING,
648
680
  artifacts: {
649
681
  inline: ["project", "templates"],
@@ -660,6 +692,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
660
692
  memory: "none",
661
693
  codebaseMap: false,
662
694
  preferences: "none",
695
+ contextMode: "none",
663
696
  tools: TOOLS_PLANNING,
664
697
  artifacts: {
665
698
  inline: [],
@@ -678,6 +711,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
678
711
  memory: "prompt-relevant",
679
712
  codebaseMap: true,
680
713
  preferences: "active-only",
714
+ contextMode: "research",
681
715
  tools: { mode: "planning-dispatch", allowedSubagents: ["scout"] },
682
716
  artifacts: {
683
717
  inline: ["project", "requirements", "templates"],
@@ -1,3 +1,5 @@
1
+ // GSD2 UOK Audit Events and DB-First Projection Writes
2
+
1
3
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { randomUUID } from "node:crypto";
@@ -6,7 +8,7 @@ import { isStaleWrite } from "../auto/turn-epoch.js";
6
8
  import { withFileLockSync } from "../file-lock.js";
7
9
  import { gsdRoot } from "../paths.js";
8
10
  import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
9
- import type { AuditEventEnvelope } from "./contracts.js";
11
+ import { CURRENT_UOK_CONTRACT_VERSION, validateAuditEvent, type AuditEventEnvelope } from "./contracts.js";
10
12
 
11
13
  function auditLogPath(basePath: string): string {
12
14
  return join(gsdRoot(basePath), "audit", "events.jsonl");
@@ -25,6 +27,7 @@ export function buildAuditEnvelope(args: {
25
27
  payload?: Record<string, unknown>;
26
28
  }): AuditEventEnvelope {
27
29
  return {
30
+ version: CURRENT_UOK_CONTRACT_VERSION,
28
31
  eventId: randomUUID(),
29
32
  traceId: args.traceId,
30
33
  turnId: args.turnId,
@@ -39,6 +42,26 @@ export function buildAuditEnvelope(args: {
39
42
  export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope): void {
40
43
  // Drop writes from a turn superseded by timeout recovery / cancellation.
41
44
  if (isStaleWrite("uok-audit")) return;
45
+ const validation = validateAuditEvent(event);
46
+ if (!validation.ok) {
47
+ throw new Error(`Invalid UOK audit event: ${validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
48
+ }
49
+ const canonical = validation.value;
50
+
51
+ if (isDbAvailable()) {
52
+ try {
53
+ insertAuditEvent({
54
+ ...canonical,
55
+ payload: {
56
+ ...canonical.payload,
57
+ contractVersion: canonical.version ?? CURRENT_UOK_CONTRACT_VERSION,
58
+ },
59
+ });
60
+ } catch (err) {
61
+ throw new Error(`DB authoritative audit write failed: ${err instanceof Error ? err.message : String(err)}`);
62
+ }
63
+ }
64
+
42
65
  try {
43
66
  ensureAuditDir(basePath);
44
67
  const path = auditLogPath(basePath);
@@ -52,18 +75,11 @@ export function emitUokAuditEvent(basePath: string, event: AuditEventEnvelope):
52
75
  withFileLockSync(
53
76
  path,
54
77
  () => {
55
- appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
78
+ appendFileSync(path, `${JSON.stringify(canonical)}\n`, "utf-8");
56
79
  },
57
80
  { onLocked: "skip" },
58
81
  );
59
82
  } catch {
60
83
  // Best-effort: audit writes must never break orchestration.
61
84
  }
62
-
63
- if (!isDbAvailable()) return;
64
- try {
65
- insertAuditEvent(event);
66
- } catch {
67
- // Projection failures are non-fatal while legacy readers are still active.
68
- }
69
85
  }