gsd-pi 2.78.1-dev.eccf86e27 → 2.79.0-dev.487011134

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 (117) 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/phases.js +6 -1
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  5. package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
  6. package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
  7. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  8. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  9. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -37
  10. package/dist/resources/extensions/gsd/commands/context.js +1 -1
  11. package/dist/resources/extensions/gsd/preferences-types.js +20 -2
  12. package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
  13. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +41 -2
  14. package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
  15. package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
  16. package/dist/resources/extensions/gsd/uok/audit.js +23 -9
  17. package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
  18. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
  19. package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
  20. package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
  21. package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  53. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  54. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  55. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
  56. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  57. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
  58. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
  59. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
  60. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  61. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
  62. package/package.json +1 -1
  63. package/packages/daemon/package.json +2 -2
  64. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  66. package/packages/mcp-server/dist/workflow-tools.js +53 -0
  67. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  68. package/packages/mcp-server/package.json +2 -2
  69. package/packages/mcp-server/src/workflow-tools.test.ts +116 -0
  70. package/packages/mcp-server/src/workflow-tools.ts +81 -0
  71. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  72. package/packages/native/package.json +1 -1
  73. package/packages/pi-agent-core/package.json +1 -1
  74. package/packages/pi-ai/package.json +1 -1
  75. package/packages/pi-coding-agent/package.json +1 -1
  76. package/packages/pi-tui/package.json +1 -1
  77. package/packages/rpc-client/package.json +1 -1
  78. package/pkg/package.json +1 -1
  79. package/src/resources/extensions/gsd/auto/phases.ts +6 -1
  80. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  81. package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
  82. package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
  83. package/src/resources/extensions/gsd/auto-start.ts +3 -2
  84. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  85. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +38 -38
  86. package/src/resources/extensions/gsd/commands/context.ts +1 -1
  87. package/src/resources/extensions/gsd/preferences-types.ts +23 -4
  88. package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
  89. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +68 -1
  90. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
  91. package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
  92. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
  93. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
  94. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
  95. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
  96. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
  97. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
  98. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
  99. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
  100. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
  101. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
  102. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
  103. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
  104. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +100 -0
  105. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
  106. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +41 -1
  107. package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
  108. package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
  109. package/src/resources/extensions/gsd/uok/audit.ts +25 -9
  110. package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
  111. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
  112. package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
  113. package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
  114. package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
  115. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
  116. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → SVXdEZJVE2uo2osZ3EE21}/_buildManifest.js +0 -0
  117. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → SVXdEZJVE2uo2osZ3EE21}/_ssgManifest.js +0 -0
@@ -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") {
@@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
 
8
8
  import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder } from "../auto-recovery.ts";
9
9
  import { resolveMilestoneFile } from "../paths.ts";
10
- import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow } from "../gsd-db.ts";
10
+ import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask } from "../gsd-db.ts";
11
11
  import { clearParseCache } from "../files.ts";
12
12
  import { parseRoadmap } from "../parsers-legacy.ts";
13
13
  import { invalidateAllCaches } from "../cache.ts";
@@ -764,6 +764,73 @@ test("hasImplementationArtifacts finds implementation commits when .gsd/ is giti
764
764
  }
765
765
  });
766
766
 
767
+ test("hasImplementationArtifacts binds GSD-Task trailer to milestone via DB state when .gsd/ is gitignored", () => {
768
+ const base = makeGitBase();
769
+ try {
770
+ writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
771
+ mkdirSync(join(base, ".gsd"), { recursive: true });
772
+ openDatabase(join(base, ".gsd", "gsd.db"));
773
+ insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
774
+ insertSlice({
775
+ id: "S01",
776
+ milestoneId: "M001",
777
+ title: "Slice One",
778
+ status: "complete",
779
+ risk: "low",
780
+ depends: [],
781
+ });
782
+ insertTask({
783
+ id: "T01",
784
+ sliceId: "S01",
785
+ milestoneId: "M001",
786
+ title: "Task One",
787
+ status: "complete",
788
+ });
789
+
790
+ mkdirSync(join(base, "src"), { recursive: true });
791
+ writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
792
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
793
+ execFileSync(
794
+ "git",
795
+ ["commit", "-m", "feat: add feature\n\nGSD-Task: S01/T01"],
796
+ { cwd: base, stdio: "ignore" },
797
+ );
798
+
799
+ const result = hasImplementationArtifacts(base, "M001");
800
+ assert.equal(
801
+ result,
802
+ "present",
803
+ "DB task ownership should bind S01/T01 implementation commits to M001 without explicit M001 text",
804
+ );
805
+ } finally {
806
+ cleanup(base);
807
+ }
808
+ });
809
+
810
+ test("hasImplementationArtifacts does not bind GSD-Task trailer without milestone ownership evidence", () => {
811
+ const base = makeGitBase();
812
+ try {
813
+ writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
814
+ mkdirSync(join(base, "src"), { recursive: true });
815
+ writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
816
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
817
+ execFileSync(
818
+ "git",
819
+ ["commit", "-m", "feat: add feature\n\nGSD-Task: S01/T01"],
820
+ { cwd: base, stdio: "ignore" },
821
+ );
822
+
823
+ const result = hasImplementationArtifacts(base, "M001");
824
+ assert.equal(
825
+ result,
826
+ "absent",
827
+ "S01/T01 shape alone must not bind an implementation commit to M001",
828
+ );
829
+ } finally {
830
+ cleanup(base);
831
+ }
832
+ });
833
+
767
834
  test("hasImplementationArtifacts ignores malformed milestone IDs in commit-message fallback", () => {
768
835
  const base = makeGitBase();
769
836
  try {
@@ -31,9 +31,9 @@ describe("bootstrap deriveState DB guards (#3844)", () => {
31
31
  const compactIdx = registerHooksSrc.indexOf('pi.on("session_before_compact"');
32
32
  assert.ok(compactIdx > -1, "register-hooks should define session_before_compact");
33
33
  const compactSection = extractSourceRegion(registerHooksSrc, 'pi.on("session_before_compact"');
34
- const ensureIdx = compactSection.indexOf("ensureDbOpen()");
34
+ const ensureIdx = compactSection.indexOf("ensureDbOpen(basePath)");
35
35
  const deriveIdx = compactSection.indexOf("deriveGsdState(basePath)");
36
- assert.ok(ensureIdx > -1, "session_before_compact should call ensureDbOpen()");
36
+ assert.ok(ensureIdx > -1, "session_before_compact should call ensureDbOpen(basePath)");
37
37
  assert.ok(deriveIdx > -1, "session_before_compact should derive state");
38
38
  assert.ok(ensureIdx < deriveIdx, "session_before_compact should open DB before deriveState");
39
39
  });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Regression test — currentDirectoryRoot() uses os.homedir() as fallback
3
+ * when process.cwd() throws (e.g. worktree teardown deletes the cwd).
4
+ *
5
+ * Before the fix, the catch block used `process.env.HOME ?? "/"`. On
6
+ * Windows, HOME is typically unset so this resolved to "/", an invalid
7
+ * path. After the fix, os.homedir() is used — it checks USERPROFILE,
8
+ * HOMEDRIVE+HOMEPATH, etc., returning a valid path on all platforms.
9
+ *
10
+ * The test monkey-patches process.cwd() to throw ENOENT, simulating a
11
+ * deleted cwd. currentDirectoryRoot() must NOT propagate the raw error;
12
+ * instead it falls back to homedir(), which validateDirectory correctly
13
+ * rejects as "blocked", yielding GSDNoProjectError — the same controlled
14
+ * error path handlers already know how to catch.
15
+ *
16
+ * The error message is also asserted to match validateDirectory(homedir()),
17
+ * confirming the fallback resolved to homedir() specifically (not "/" or
18
+ * any other path).
19
+ */
20
+ import { describe, test, beforeEach, afterEach } from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import { homedir } from "node:os";
23
+
24
+ import { currentDirectoryRoot, GSDNoProjectError } from "../commands/context.ts";
25
+ import { validateDirectory } from "../validate-directory.ts";
26
+
27
+ describe("currentDirectoryRoot() homedir() fallback on deleted cwd", () => {
28
+ const originalCwd = process.cwd.bind(process);
29
+
30
+ beforeEach(() => {
31
+ process.cwd = () => {
32
+ const err = new Error("ENOENT: no such file or directory, uv_cwd");
33
+ (err as NodeJS.ErrnoException).code = "ENOENT";
34
+ throw err;
35
+ };
36
+ });
37
+
38
+ afterEach(() => {
39
+ process.cwd = originalCwd;
40
+ });
41
+
42
+ test("does not propagate ENOENT — throws GSDNoProjectError via homedir() fallback", () => {
43
+ const expected = validateDirectory(homedir());
44
+ assert.equal(expected.severity, "blocked", "homedir() itself should be blocked");
45
+
46
+ assert.throws(
47
+ () => currentDirectoryRoot(),
48
+ (err: unknown) => {
49
+ assert.ok(
50
+ err instanceof GSDNoProjectError,
51
+ `expected GSDNoProjectError, got: ${err}`,
52
+ );
53
+ assert.equal(
54
+ (err as Error).message,
55
+ expected.reason ?? "GSD must be run inside a project directory.",
56
+ "error message must match validateDirectory(homedir()), confirming homedir() was the fallback",
57
+ );
58
+ return true;
59
+ },
60
+ "should throw GSDNoProjectError (homedir fallback validated), not raw ENOENT",
61
+ );
62
+ });
63
+ });
@@ -14,6 +14,7 @@ import {
14
14
  DISPATCH_RULES,
15
15
  getDeepStageGate,
16
16
  hasPendingDeepStage,
17
+ resolveDispatch,
17
18
  setResearchProjectPromptBuilderForTest,
18
19
  type DispatchContext,
19
20
  } from "../auto-dispatch.ts";
@@ -248,6 +249,11 @@ function writeCapturedDeepPrefs(base: string): void {
248
249
  );
249
250
  }
250
251
 
252
+ function writeSkippedProjectResearchDecision(base: string): void {
253
+ mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
254
+ writeFileSync(join(base, ".gsd", "runtime", "research-decision.json"), JSON.stringify({ decision: "skip" }));
255
+ }
256
+
251
257
  function makeCtx(
252
258
  basePath: string,
253
259
  prefs: GSDPreferences | undefined,
@@ -785,6 +791,42 @@ test("Deep mode: research-project DOES dispatch when only 3 of 4 research files
785
791
  assert.ok(result && result.action === "dispatch", "any missing dimension must trigger re-run");
786
792
  });
787
793
 
794
+ test("Deep mode: queued milestone without CONTEXT.md routes to milestone research after project setup", async (t) => {
795
+ const base = makeIsolatedBaseWithCleanup(t);
796
+
797
+ writeCapturedDeepPrefs(base);
798
+ writeValidProject(base);
799
+ writeValidRequirements(base);
800
+ writeSkippedProjectResearchDecision(base);
801
+
802
+ const prefs = { planning_depth: "deep" } as GSDPreferences;
803
+ const result = await resolveDispatch(makeCtx(base, prefs));
804
+
805
+ assert.equal(result.action, "dispatch");
806
+ if (result.action === "dispatch") {
807
+ assert.equal(result.unitType, "research-milestone");
808
+ assert.equal(result.unitId, "M001");
809
+ }
810
+ });
811
+
812
+ test("Deep mode: queued milestone without CONTEXT.md can route directly to milestone planning", async (t) => {
813
+ const base = makeIsolatedBaseWithCleanup(t);
814
+
815
+ writeCapturedDeepPrefs(base);
816
+ writeValidProject(base);
817
+ writeValidRequirements(base);
818
+ writeSkippedProjectResearchDecision(base);
819
+
820
+ const prefs = { planning_depth: "deep", phases: { skip_research: true } } as GSDPreferences;
821
+ const result = await resolveDispatch(makeCtx(base, prefs));
822
+
823
+ assert.equal(result.action, "dispatch");
824
+ if (result.action === "dispatch") {
825
+ assert.equal(result.unitType, "plan-milestone");
826
+ assert.equal(result.unitId, "M001");
827
+ }
828
+ });
829
+
788
830
  // ─── centralized deep-stage gate ─────────────────────────────────────────
789
831
 
790
832
  test("Deep mode gate reports the earliest missing section", (t) => {
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
@@ -23,6 +23,11 @@ import {
23
23
  showSmartEntry,
24
24
  startDeepProjectSetupForeground,
25
25
  } from "../guided-flow.ts";
26
+ import {
27
+ closeDatabase,
28
+ insertMilestone,
29
+ openDatabase,
30
+ } from "../gsd-db.ts";
26
31
  import type { GSDPreferences } from "../preferences.ts";
27
32
  import type { GSDState } from "../types.ts";
28
33
 
@@ -342,6 +347,62 @@ test("deep project setup: pre-dispatch can run before the first milestone exists
342
347
  }
343
348
  });
344
349
 
350
+ test("deep project setup: bootstrap continues queued M002 without milestone context", async () => {
351
+ const base = makeRepo();
352
+ try {
353
+ writeCapturedDeepPrefs(base);
354
+ writeValidProjectAndRequirements(base);
355
+ mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
356
+ writeFileSync(join(base, ".gsd", "runtime", "research-decision.json"), '{"decision":"skip"}\n');
357
+
358
+ openDatabase(join(base, ".gsd", "gsd.db"));
359
+ insertMilestone({ id: "M001", title: "First milestone", status: "complete" });
360
+ insertMilestone({ id: "M002", title: "Second milestone", status: "queued" });
361
+ closeDatabase();
362
+
363
+ const messages: unknown[] = [];
364
+ const pi = {
365
+ ...makePi(messages),
366
+ getThinkingLevel: () => "medium",
367
+ };
368
+ const s = new AutoSession();
369
+ const ready = await bootstrapAutoSession(
370
+ s,
371
+ makeCtx(`queued-${randomUUID()}`) as any,
372
+ pi as any,
373
+ base,
374
+ false,
375
+ false,
376
+ {
377
+ shouldUseWorktreeIsolation: () => false,
378
+ registerSigtermHandler: () => {},
379
+ lockBase: () => base,
380
+ buildResolver: () => ({}) as any,
381
+ },
382
+ {
383
+ classification: "none",
384
+ lock: null,
385
+ pausedSession: null,
386
+ state: null,
387
+ recovery: null,
388
+ recoveryPrompt: null,
389
+ recoveryToolCallCount: 0,
390
+ artifactSatisfied: false,
391
+ hasResumableDiskState: false,
392
+ isBootstrapCrash: false,
393
+ },
394
+ );
395
+
396
+ assert.equal(ready, true);
397
+ assert.equal(s.active, true);
398
+ assert.equal(s.currentMilestoneId, "M002");
399
+ assert.equal(messages.length, 0, "queued deep milestone must not re-enter smart new-milestone discussion");
400
+ } finally {
401
+ try { closeDatabase(); } catch {}
402
+ rmSync(base, { recursive: true, force: true });
403
+ }
404
+ });
405
+
345
406
  test("deep project setup: pre-dispatch takes precedence over an existing draft milestone", async () => {
346
407
  const base = makeBase();
347
408
  try {
@@ -1020,7 +1081,7 @@ test("deep project setup: research-project blocker placeholder is a file, not th
1020
1081
  const base = makeBase();
1021
1082
  try {
1022
1083
  const expectedPath = resolveExpectedArtifactPath("research-project", "PROJECT-RESEARCH", base);
1023
- assert.equal(expectedPath, join(base, ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
1084
+ assert.equal(expectedPath, join(realpathSync(base), ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
1024
1085
 
1025
1086
  mkdirSync(join(base, ".gsd", "research"), { recursive: true });
1026
1087
  const diagnosis = writeBlockerPlaceholder(
@@ -101,6 +101,20 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
101
101
  prompt.includes("Implement the thing"),
102
102
  "must include task plan body content from disk",
103
103
  );
104
+ assert.ok(prompt.includes("## Context Mode"), "execute-task should include standalone Context Mode guidance");
105
+ assert.ok(prompt.includes("execution lane"), "execute-task should render the execution lane");
106
+ });
107
+
108
+ test("buildExecuteTaskPrompt omits Context Mode when disabled", async () => {
109
+ writeFileSync(
110
+ join(base, ".gsd", "PREFERENCES.md"),
111
+ ["---", "context_mode:", " enabled: false", "---", ""].join("\n"),
112
+ );
113
+
114
+ const prompt = await buildExecuteTaskPrompt(MID, SID, S_TITLE, TID, T_TITLE, base);
115
+
116
+ assert.ok(!prompt.includes("## Context Mode"));
117
+ assert.ok(!prompt.includes("Context Mode (execution lane)"));
104
118
  });
105
119
 
106
120
  test("buildCompleteSlicePrompt carries the complete-slice contract", async () => {
@@ -147,4 +147,12 @@ test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skil
147
147
  prompt.includes(SKILL_ACTIVATION_SUBSTRING),
148
148
  `parallel-research-slices prompt should reference the always-used skill '${SKILL_NAME}'`,
149
149
  );
150
+ assert.ok(
151
+ prompt.includes("Context Mode (research lane):"),
152
+ "embedded parallel research subagent prompts should use nested Context Mode guidance",
153
+ );
154
+ assert.ok(
155
+ !prompt.includes("## Context Mode\n\nLane: **research lane**."),
156
+ "embedded parallel research subagent prompts should not use standalone Context Mode heading",
157
+ );
150
158
  });
@@ -111,6 +111,9 @@ test("#4551: buildPlanSlicePrompt injects fix section when priorPreExecFailure p
111
111
  },
112
112
  );
113
113
 
114
+ assert.ok(prompt.includes("## Context Mode"), "plan-slice should include standalone Context Mode guidance");
115
+ assert.ok(prompt.includes("planning lane"), "plan-slice should render the planning lane");
116
+
114
117
  assert.ok(
115
118
  prompt.includes("Fix these specific issues from the prior pre-exec check"),
116
119
  "prompt must contain the fix section heading",
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
7
7
  import { registerHooks } from "../bootstrap/register-hooks.ts";
8
+ import { autoSession } from "../auto-runtime-state.ts";
8
9
  import { parseContinue } from "../files.ts";
9
10
  import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
10
11
  import { deriveState, invalidateStateCache } from "../state.ts";
@@ -96,3 +97,87 @@ test("register-hooks writes CONTINUE checkpoint during planning phase without ac
96
97
  assert.match(parsed.completedWork, /planning phase/i, "completed-work should capture non-executing phase context");
97
98
  assert.match(parsed.nextAction, /slice S01/i, "next action should route resume to the active slice");
98
99
  });
100
+
101
+ test("register-hooks writes Context Mode snapshot before active auto cancels compaction", async (t) => {
102
+ const base = createPlanningFixtureBase();
103
+ const originalCwd = process.cwd();
104
+ process.chdir(base);
105
+ invalidateStateCache();
106
+ closeDatabase();
107
+ autoSession.reset();
108
+ autoSession.active = true;
109
+
110
+ t.after(() => {
111
+ autoSession.reset();
112
+ invalidateStateCache();
113
+ closeDatabase();
114
+ process.chdir(originalCwd);
115
+ rmSync(base, { recursive: true, force: true });
116
+ });
117
+
118
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
119
+ const pi = {
120
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
121
+ const existing = handlers.get(event) ?? [];
122
+ existing.push(handler);
123
+ handlers.set(event, existing);
124
+ },
125
+ } as any;
126
+
127
+ registerHooks(pi, []);
128
+
129
+ const compactHandlers = handlers.get("session_before_compact");
130
+ assert.ok(compactHandlers && compactHandlers.length > 0, "session_before_compact handler should be registered");
131
+
132
+ const result = await compactHandlers![0]({});
133
+
134
+ assert.deepEqual(result, { cancel: true }, "active auto should still cancel compaction");
135
+ const snapshotPath = join(base, ".gsd", "last-snapshot.md");
136
+ assert.ok(existsSync(snapshotPath), "active auto cancel should still leave a Context Mode snapshot");
137
+ assert.match(readFileSync(snapshotPath, "utf-8"), /GSD context snapshot/);
138
+ });
139
+
140
+ test("register-hooks does not write Context Mode snapshot when disabled", async (t) => {
141
+ const base = createPlanningFixtureBase();
142
+ writeFileSync(
143
+ join(base, ".gsd", "PREFERENCES.md"),
144
+ "---\ncontext_mode:\n enabled: false\n---\n",
145
+ "utf-8",
146
+ );
147
+ const originalCwd = process.cwd();
148
+ process.chdir(base);
149
+ invalidateStateCache();
150
+ closeDatabase();
151
+ autoSession.reset();
152
+
153
+ t.after(() => {
154
+ autoSession.reset();
155
+ invalidateStateCache();
156
+ closeDatabase();
157
+ process.chdir(originalCwd);
158
+ rmSync(base, { recursive: true, force: true });
159
+ });
160
+
161
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
162
+ const pi = {
163
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
164
+ const existing = handlers.get(event) ?? [];
165
+ existing.push(handler);
166
+ handlers.set(event, existing);
167
+ },
168
+ } as any;
169
+
170
+ registerHooks(pi, []);
171
+
172
+ const compactHandlers = handlers.get("session_before_compact");
173
+ assert.ok(compactHandlers && compactHandlers.length > 0, "session_before_compact handler should be registered");
174
+
175
+ for (const handler of compactHandlers ?? []) {
176
+ await handler({});
177
+ }
178
+
179
+ assert.ok(
180
+ !existsSync(join(base, ".gsd", "last-snapshot.md")),
181
+ "disabled Context Mode should not write a snapshot",
182
+ );
183
+ });
@@ -95,6 +95,8 @@ test("#4782 phase 3: buildRunUatPrompt inlines slice UAT, slice summary, project
95
95
 
96
96
  // Context wrapper present
97
97
  assert.match(prompt, /## Inlined Context \(preloaded — do not re-read these files\)/);
98
+ assert.match(prompt, /## Context Mode/);
99
+ assert.match(prompt, /verification lane/);
98
100
 
99
101
  // Artifacts from the manifest inline list, in declared order:
100
102
  // slice-uat → slice-summary → project (#4925 review).
@@ -211,6 +211,14 @@ test("buildReactiveExecutePrompt: output contains model string when subagentMode
211
211
  prompt.includes('model: "claude-opus-4-6"'),
212
212
  `Prompt should contain model instruction. Got:\n${prompt.slice(0, 500)}`,
213
213
  );
214
+ assert.ok(
215
+ prompt.includes("Context Mode (execution lane):"),
216
+ "embedded reactive-execute task prompt should use nested Context Mode guidance",
217
+ );
218
+ assert.ok(
219
+ prompt.includes("## Context Mode"),
220
+ "reactive-execute parent prompt should include standalone Context Mode guidance",
221
+ );
214
222
  });
215
223
 
216
224
  test("buildReactiveExecutePrompt: no model instruction when subagentModel omitted", async (t) => {
@@ -266,3 +274,54 @@ test("buildReactiveExecutePrompt: no model instruction when subagentModel omitte
266
274
  "Prompt should not contain model instruction when subagentModel is omitted",
267
275
  );
268
276
  });
277
+
278
+ test("buildGateEvaluatePrompt: embedded gate prompts use nested Context Mode guidance", async (t) => {
279
+ const { buildGateEvaluatePrompt } = await import("../auto-prompts.ts");
280
+ const { closeDatabase, insertGateRow, insertMilestone, insertSlice, openDatabase } = await import("../gsd-db.ts");
281
+ const repo = mkdtempSync(join(tmpdir(), "gsd-subagent-model-gate-"));
282
+ t.after(() => {
283
+ try { closeDatabase(); } catch { /* noop */ }
284
+ rmSync(repo, { recursive: true, force: true });
285
+ });
286
+
287
+ const sliceDir = join(repo, ".gsd", "milestones", "M001", "slices", "S01");
288
+ mkdirSync(sliceDir, { recursive: true });
289
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01 Plan\n\n## Verification\n- Run checks.\n");
290
+ openDatabase(join(repo, ".gsd", "gsd.db"));
291
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "active", depends_on: [] });
292
+ insertSlice({
293
+ id: "S01",
294
+ milestoneId: "M001",
295
+ title: "Test Slice",
296
+ status: "planned",
297
+ risk: "low",
298
+ depends: [],
299
+ demo: "",
300
+ sequence: 1,
301
+ });
302
+ insertGateRow({
303
+ milestoneId: "M001",
304
+ sliceId: "S01",
305
+ gateId: "Q3",
306
+ scope: "slice",
307
+ });
308
+
309
+ const prompt = await buildGateEvaluatePrompt(
310
+ "M001",
311
+ "Test Milestone",
312
+ "S01",
313
+ "Test Slice",
314
+ repo,
315
+ "claude-opus-4-6",
316
+ );
317
+
318
+ assert.ok(
319
+ prompt.includes("Context Mode (verification lane):"),
320
+ "embedded gate-evaluate prompt should use nested Context Mode guidance",
321
+ );
322
+ assert.ok(
323
+ prompt.includes("## Context Mode"),
324
+ "gate-evaluate parent prompt should include standalone Context Mode guidance",
325
+ );
326
+ assert.ok(prompt.includes('model: "claude-opus-4-6"'), "gate subagent prompt should preserve model instruction");
327
+ });
@@ -9,6 +9,7 @@ import { join } from "node:path";
9
9
  import { tmpdir } from "node:os";
10
10
 
11
11
  import {
12
+ composeContextModeInstructions,
12
13
  composeInlinedContext,
13
14
  composeUnitContext,
14
15
  manifestBudgetChars,
@@ -101,6 +102,43 @@ test("#4782 composer: manifestBudgetChars returns declared budget", () => {
101
102
  assert.strictEqual(manifestBudgetChars("never-dispatched"), null);
102
103
  });
103
104
 
105
+ test("Context Mode composer: disabled, unknown, and none modes return empty string", () => {
106
+ assert.strictEqual(
107
+ composeContextModeInstructions("execute-task", { enabled: false, renderMode: "standalone" }),
108
+ "",
109
+ );
110
+ assert.strictEqual(
111
+ composeContextModeInstructions("never-dispatched", { enabled: true, renderMode: "standalone" }),
112
+ "",
113
+ );
114
+ assert.strictEqual(
115
+ composeContextModeInstructions("workflow-preferences", { enabled: true, renderMode: "standalone" }),
116
+ "",
117
+ );
118
+ });
119
+
120
+ test("Context Mode composer: standalone output starts with heading and includes required tools", () => {
121
+ const out = composeContextModeInstructions("execute-task", { enabled: true, renderMode: "standalone" });
122
+ assert.ok(out.startsWith("## Context Mode"));
123
+ assert.match(out, /execution lane/i);
124
+ assert.match(out, /`gsd_exec`/);
125
+ assert.match(out, /noisy scans, builds, and tests/);
126
+ assert.match(out, /`gsd_exec_search`/);
127
+ assert.match(out, /before repeating prior runs/);
128
+ assert.match(out, /`gsd_resume`/);
129
+ assert.match(out, /after compaction or resume/);
130
+ });
131
+
132
+ test("Context Mode composer: nested output is compact single sentence", () => {
133
+ const out = composeContextModeInstructions("gate-evaluate", { enabled: true, renderMode: "nested" });
134
+ assert.ok(!out.startsWith("## Context Mode"));
135
+ assert.match(out, /^Context Mode \(verification lane\): /);
136
+ assert.strictEqual(out.split(/\n/).length, 1);
137
+ assert.match(out, /`gsd_exec`/);
138
+ assert.match(out, /`gsd_exec_search`/);
139
+ assert.match(out, /`gsd_resume`/);
140
+ });
141
+
104
142
  // ─── Integration: migrated buildReassessRoadmapPrompt ─────────────────────
105
143
 
106
144
  function makeFixtureBase(): string {
@@ -12,6 +12,7 @@ import {
12
12
  UNIT_MANIFESTS,
13
13
  resolveManifest,
14
14
  type ArtifactKey,
15
+ type ContextModePolicy,
15
16
  type SkillsPolicy,
16
17
  type UnitContextManifest,
17
18
  } from "../unit-context-manifest.ts";
@@ -105,6 +106,37 @@ test("#4782 phase 1: every manifest has a positive maxSystemPromptChars", () =>
105
106
  }
106
107
  });
107
108
 
109
+ test("Context Mode: every manifest declares the expected contextMode lane", () => {
110
+ const expected: Record<string, ContextModePolicy> = {
111
+ "workflow-preferences": "none",
112
+ "research-decision": "none",
113
+ "discuss-project": "interview",
114
+ "discuss-requirements": "interview",
115
+ "discuss-milestone": "interview",
116
+ "research-project": "research",
117
+ "research-milestone": "research",
118
+ "research-slice": "research",
119
+ "plan-milestone": "planning",
120
+ "plan-slice": "planning",
121
+ "refine-slice": "planning",
122
+ "replan-slice": "planning",
123
+ "reassess-roadmap": "planning",
124
+ "execute-task": "execution",
125
+ "reactive-execute": "execution",
126
+ "run-uat": "verification",
127
+ "gate-evaluate": "verification",
128
+ "validate-milestone": "verification",
129
+ "complete-slice": "verification",
130
+ "complete-milestone": "verification",
131
+ "rewrite-docs": "docs",
132
+ };
133
+
134
+ assert.deepEqual(Object.keys(expected).sort(), [...KNOWN_UNIT_TYPES].sort());
135
+ for (const unitType of KNOWN_UNIT_TYPES) {
136
+ assert.strictEqual(UNIT_MANIFESTS[unitType].contextMode, expected[unitType]);
137
+ }
138
+ });
139
+
108
140
  test("#4782 phase 1: skills policy shapes are valid discriminated-union members", () => {
109
141
  for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
110
142
  const p = manifest.skills as SkillsPolicy;