@opengsd/gsd-pi 1.8.1-dev.897b3c90 → 1.9.0-dev.5c1a3ff2

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 (194) hide show
  1. package/README.md +10 -8
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +2 -2
  4. package/dist/resources/extensions/gsd/artifact-verification.js +3 -4
  5. package/dist/resources/extensions/gsd/auto-artifact-paths.js +14 -8
  6. package/dist/resources/extensions/gsd/auto-prompts.js +90 -17
  7. package/dist/resources/extensions/gsd/auto-recovery.js +10 -5
  8. package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +4 -0
  9. package/dist/resources/extensions/gsd/auto-worktree-teardown.js +1 -1
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -5
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +10 -3
  13. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -24
  14. package/dist/resources/extensions/gsd/constants.js +12 -13
  15. package/dist/resources/extensions/gsd/context-masker.js +97 -21
  16. package/dist/resources/extensions/gsd/docs/preferences-reference.md +22 -0
  17. package/dist/resources/extensions/gsd/doctor-engine-checks.js +3 -6
  18. package/dist/resources/extensions/gsd/git-service.js +19 -7
  19. package/dist/resources/extensions/gsd/guided-flow.js +9 -3
  20. package/dist/resources/extensions/gsd/markdown-renderer.js +2 -12
  21. package/dist/resources/extensions/gsd/md-importer.js +5 -6
  22. package/dist/resources/extensions/gsd/model-cost-table.js +2 -0
  23. package/dist/resources/extensions/gsd/model-router.js +11 -0
  24. package/dist/resources/extensions/gsd/paths.js +51 -10
  25. package/dist/resources/extensions/gsd/planning-subagent-policy.js +37 -0
  26. package/dist/resources/extensions/gsd/planning-subagent-registry.js +25 -0
  27. package/dist/resources/extensions/gsd/preferences-types.js +5 -0
  28. package/dist/resources/extensions/gsd/preferences-validation.js +48 -1
  29. package/dist/resources/extensions/gsd/preferences.js +17 -0
  30. package/dist/resources/extensions/gsd/provider-payload-policy.js +16 -5
  31. package/dist/resources/extensions/gsd/safety/destructive-guard.js +1 -1
  32. package/dist/resources/extensions/gsd/skill-activation.js +17 -3
  33. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
  34. package/dist/resources/extensions/gsd/tools/complete-task.js +12 -5
  35. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +14 -5
  36. package/dist/resources/extensions/gsd/tools/reopen-slice.js +15 -5
  37. package/dist/resources/extensions/gsd/tools/reopen-task.js +16 -10
  38. package/dist/resources/extensions/gsd/unit-context-composer.js +28 -5
  39. package/dist/resources/extensions/gsd/workflow-projections.js +2 -3
  40. package/dist/resources/extensions/gsd/workspace.js +2 -1
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.html +1 -1
  63. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  70. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  73. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  74. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  75. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  76. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  77. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  78. package/integrations/hermes/pyproject.toml +1 -1
  79. package/package.json +6 -6
  80. package/packages/cloud-mcp-gateway/package.json +2 -2
  81. package/packages/contracts/package.json +1 -1
  82. package/packages/daemon/package.json +4 -4
  83. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  84. package/packages/gsd-agent-core/dist/compaction/compaction.js +36 -4
  85. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  86. package/packages/gsd-agent-core/dist/sdk.d.ts.map +1 -1
  87. package/packages/gsd-agent-core/dist/sdk.js +1 -0
  88. package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
  89. package/packages/gsd-agent-core/package.json +5 -5
  90. package/packages/gsd-agent-modes/package.json +7 -7
  91. package/packages/gsd-cloud/package.json +1 -1
  92. package/packages/mcp-server/README.md +7 -2
  93. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/server.js +5 -5
  95. package/packages/mcp-server/dist/server.js.map +1 -1
  96. package/packages/mcp-server/dist/workflow-tools.d.ts +4 -3
  97. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  98. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  99. package/packages/mcp-server/package.json +4 -4
  100. package/packages/native/package.json +1 -1
  101. package/packages/pi-agent-core/dist/agent.d.ts +5 -1
  102. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  103. package/packages/pi-agent-core/dist/agent.js +2 -0
  104. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  105. package/packages/pi-agent-core/package.json +1 -1
  106. package/packages/pi-ai/dist/models.generated.d.ts +59 -0
  107. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.js +53 -0
  109. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/anthropic.js +6 -1
  111. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  112. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  113. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  114. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  115. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  116. package/packages/pi-ai/package.json +1 -1
  117. package/packages/pi-coding-agent/dist/core/capability-patches.js +1 -1
  118. package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  120. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +11 -1
  122. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  124. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  125. package/packages/pi-coding-agent/package.json +7 -7
  126. package/packages/pi-tui/package.json +2 -2
  127. package/packages/rpc-client/package.json +2 -2
  128. package/pkg/package.json +1 -1
  129. package/src/resources/GSD-WORKFLOW.md +2 -2
  130. package/src/resources/extensions/gsd/artifact-verification.ts +3 -8
  131. package/src/resources/extensions/gsd/auto-artifact-paths.ts +14 -7
  132. package/src/resources/extensions/gsd/auto-prompts.ts +104 -15
  133. package/src/resources/extensions/gsd/auto-recovery.ts +10 -8
  134. package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +4 -0
  135. package/src/resources/extensions/gsd/auto-worktree-teardown.ts +1 -1
  136. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -0
  137. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +39 -10
  138. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -3
  139. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +7 -29
  140. package/src/resources/extensions/gsd/constants.ts +14 -13
  141. package/src/resources/extensions/gsd/context-masker.ts +98 -2
  142. package/src/resources/extensions/gsd/docs/preferences-reference.md +22 -0
  143. package/src/resources/extensions/gsd/doctor-engine-checks.ts +3 -5
  144. package/src/resources/extensions/gsd/git-service.ts +19 -6
  145. package/src/resources/extensions/gsd/guided-flow.ts +9 -2
  146. package/src/resources/extensions/gsd/markdown-renderer.ts +2 -14
  147. package/src/resources/extensions/gsd/md-importer.ts +5 -5
  148. package/src/resources/extensions/gsd/model-cost-table.ts +2 -0
  149. package/src/resources/extensions/gsd/model-router.ts +12 -0
  150. package/src/resources/extensions/gsd/paths.ts +58 -10
  151. package/src/resources/extensions/gsd/planning-subagent-policy.ts +58 -0
  152. package/src/resources/extensions/gsd/planning-subagent-registry.ts +31 -0
  153. package/src/resources/extensions/gsd/preferences-types.ts +16 -0
  154. package/src/resources/extensions/gsd/preferences-validation.ts +62 -0
  155. package/src/resources/extensions/gsd/preferences.ts +25 -0
  156. package/src/resources/extensions/gsd/provider-payload-policy.ts +18 -4
  157. package/src/resources/extensions/gsd/safety/destructive-guard.ts +1 -1
  158. package/src/resources/extensions/gsd/skill-activation.ts +17 -3
  159. package/src/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
  160. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +7 -7
  161. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +39 -0
  162. package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +55 -0
  163. package/src/resources/extensions/gsd/tests/complete-task.test.ts +76 -1
  164. package/src/resources/extensions/gsd/tests/context-masker.test.ts +204 -8
  165. package/src/resources/extensions/gsd/tests/db-tools-alias-suppression.test.ts +62 -0
  166. package/src/resources/extensions/gsd/tests/destructive-confirmation.test.ts +33 -0
  167. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +14 -0
  168. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +22 -4
  169. package/src/resources/extensions/gsd/tests/integration-branch-meta-location.test.ts +62 -0
  170. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +5 -5
  171. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +9 -1
  172. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +7 -5
  174. package/src/resources/extensions/gsd/tests/preferences.test.ts +84 -0
  175. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +4 -0
  176. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +66 -0
  177. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +39 -0
  178. package/src/resources/extensions/gsd/tests/service-tier.test.ts +4 -0
  179. package/src/resources/extensions/gsd/tests/single-writer-v3-tool-surface.test.ts +33 -7
  180. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -16
  181. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +13 -7
  182. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +8 -0
  183. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +39 -0
  184. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +4 -4
  185. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +34 -0
  186. package/src/resources/extensions/gsd/tools/complete-task.ts +20 -5
  187. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +24 -5
  188. package/src/resources/extensions/gsd/tools/reopen-slice.ts +25 -5
  189. package/src/resources/extensions/gsd/tools/reopen-task.ts +23 -9
  190. package/src/resources/extensions/gsd/unit-context-composer.ts +34 -8
  191. package/src/resources/extensions/gsd/workflow-projections.ts +2 -3
  192. package/src/resources/extensions/gsd/workspace.ts +2 -1
  193. /package/dist/web/standalone/.next/static/{TezjdjSHBVRRr54XjAvte → 6YWV7VAPg-jqjfPswrKZb}/_buildManifest.js +0 -0
  194. /package/dist/web/standalone/.next/static/{TezjdjSHBVRRr54XjAvte → 6YWV7VAPg-jqjfPswrKZb}/_ssgManifest.js +0 -0
@@ -4,9 +4,12 @@ import assert from "node:assert/strict";
4
4
  import {
5
5
  createObservationMask,
6
6
  createResponsesInputObservationMask,
7
+ filterSupersededContextInjections,
8
+ filterSupersededResponsesContextInjections,
7
9
  truncateContextResultMessages,
8
10
  truncateResponsesInputResultItems,
9
11
  } from "../context-masker.js";
12
+ import { GSD_CONTEXT_MESSAGE_SENTINEL } from "../bootstrap/system-context.js";
10
13
 
11
14
  // These helpers produce messages in the pi-ai LLM payload format
12
15
  // (post-convertToLlm, pre-provider), which is what before_provider_request sees.
@@ -43,25 +46,35 @@ test("masks nothing when message count is within keepRecentTurns", () => {
43
46
  assert.deepEqual((result[2].content as any)[0].text, "file contents");
44
47
  });
45
48
 
46
- test("masks tool results older than keepRecentTurns", () => {
49
+ test("masks tool results older than a full quantization block", () => {
50
+ // With keepRecentTurns=2, masking is quantized in blocks of 2 turns: nothing
51
+ // is masked until a full block (2 turns) of excess history accumulates.
47
52
  const mask = createObservationMask(2);
48
53
  const messages = [
49
54
  userMsg("turn 1"),
50
- toolResult("old tool output"),
55
+ toolResult("turn 1 tool output"),
51
56
  assistantMsg("response 1"),
52
57
  userMsg("turn 2"),
53
- toolResult("newer tool output"),
58
+ toolResult("turn 2 tool output"),
54
59
  assistantMsg("response 2"),
55
60
  userMsg("turn 3"),
56
- toolResult("newest tool output"),
61
+ toolResult("turn 3 tool output"),
57
62
  assistantMsg("response 3"),
63
+ userMsg("turn 4"),
64
+ toolResult("turn 4 tool output"),
65
+ assistantMsg("response 4"),
66
+ userMsg("turn 5"),
67
+ toolResult("turn 5 tool output"),
68
+ assistantMsg("response 5"),
58
69
  ];
59
70
  const result = mask(messages as any);
60
- // Old tool result (before boundary) should be masked
71
+ // First block (turns 1-2) is masked
61
72
  assert.equal((result[1].content as any)[0].text, MASK_TEXT);
62
- // Recent tool results (within keep window) should be preserved
63
- assert.equal((result[4].content as any)[0].text, "newer tool output");
64
- assert.equal((result[7].content as any)[0].text, "newest tool output");
73
+ assert.equal((result[4].content as any)[0].text, MASK_TEXT);
74
+ // Remaining turns (3-5) are within the unmasked window
75
+ assert.equal((result[7].content as any)[0].text, "turn 3 tool output");
76
+ assert.equal((result[10].content as any)[0].text, "turn 4 tool output");
77
+ assert.equal((result[13].content as any)[0].text, "turn 5 tool output");
65
78
  });
66
79
 
67
80
  test("never masks assistant messages", () => {
@@ -126,6 +139,67 @@ test("masks toolResult by role, not by type field", () => {
126
139
  assert.equal((result[1].content as any)[0].text, MASK_TEXT);
127
140
  });
128
141
 
142
+ function buildTurns(count: number) {
143
+ const messages: unknown[] = [];
144
+ for (let i = 1; i <= count; i++) {
145
+ messages.push(userMsg(`turn ${i}`));
146
+ messages.push(toolResult(`turn ${i} tool output`));
147
+ messages.push(assistantMsg(`response ${i}`));
148
+ }
149
+ return messages;
150
+ }
151
+
152
+ function maskedTextsByTurn(mask: (m: any) => any[], messages: unknown[], turnCount: number): (string | undefined)[] {
153
+ const result = mask(messages as any);
154
+ const texts: (string | undefined)[] = [];
155
+ for (let i = 0; i < turnCount; i++) {
156
+ const toolResultIndex = i * 3 + 1;
157
+ texts.push((result[toolResultIndex].content as any)[0].text);
158
+ }
159
+ return texts;
160
+ }
161
+
162
+ test("quantized mask invariant: at least keepRecentTurns most recent turns are always unmasked", () => {
163
+ const N = 3;
164
+ const mask = createObservationMask(N);
165
+ for (let totalTurns = 1; totalTurns <= 20; totalTurns++) {
166
+ const messages = buildTurns(totalTurns);
167
+ const texts = maskedTextsByTurn(mask, messages, totalTurns);
168
+ for (let i = Math.max(0, totalTurns - N); i < totalTurns; i++) {
169
+ assert.notEqual(texts[i], MASK_TEXT, `turn ${i + 1} of ${totalTurns} should be unmasked`);
170
+ }
171
+ }
172
+ });
173
+
174
+ test("quantized mask invariant: at most 2*keepRecentTurns-1 turns are unmasked", () => {
175
+ const N = 3;
176
+ const mask = createObservationMask(N);
177
+ for (let totalTurns = 1; totalTurns <= 20; totalTurns++) {
178
+ const messages = buildTurns(totalTurns);
179
+ const texts = maskedTextsByTurn(mask, messages, totalTurns);
180
+ const unmaskedCount = texts.filter((t) => t !== MASK_TEXT).length;
181
+ assert.ok(unmaskedCount <= 2 * N - 1, `expected <= ${2 * N - 1} unmasked turns at total=${totalTurns}, got ${unmaskedCount}`);
182
+ }
183
+ });
184
+
185
+ test("quantized mask invariant: prefix is byte-stable except at block rollover", () => {
186
+ const N = 3;
187
+ const mask = createObservationMask(N);
188
+ let previousTexts: (string | undefined)[] = [];
189
+ for (let totalTurns = 1; totalTurns <= 20; totalTurns++) {
190
+ const messages = buildTurns(totalTurns);
191
+ const texts = maskedTextsByTurn(mask, messages, totalTurns);
192
+ // Compare the shared prefix (turns 1..totalTurns-1) against the previous turn's run.
193
+ let changedCount = 0;
194
+ for (let i = 0; i < previousTexts.length; i++) {
195
+ if (previousTexts[i] !== texts[i]) changedCount++;
196
+ }
197
+ // A block rollover flips at most N turns from unmasked to masked; otherwise 0 should change.
198
+ assert.ok(changedCount === 0 || changedCount <= N, `turn ${totalTurns}: ${changedCount} prefix turns changed status`);
199
+ previousTexts = texts;
200
+ }
201
+ });
202
+
129
203
  test("truncates recent bash result user messages", () => {
130
204
  const messages = [
131
205
  userMsg("turn 1"),
@@ -175,3 +249,125 @@ test("truncates Responses API function outputs and recent bash result items", ()
175
249
  assert.ok((result[1].output as string).length < (items[1].output as string).length);
176
250
  assert.ok((result[2].content as any)[0].text.length < (items[2].content as any)[0].text.length);
177
251
  });
252
+
253
+ function contextInjectionMsg(label: string) {
254
+ return userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[MEMORY] turn=${label}`);
255
+ }
256
+
257
+ test("filterSupersededContextInjections keeps only the latest of three same-kind injections", () => {
258
+ const messages = [
259
+ userMsg("turn 1 prompt"),
260
+ contextInjectionMsg("1"),
261
+ assistantMsg("response 1"),
262
+ userMsg("turn 2 prompt"),
263
+ contextInjectionMsg("2"),
264
+ assistantMsg("response 2"),
265
+ userMsg("turn 3 prompt"),
266
+ contextInjectionMsg("3"),
267
+ ];
268
+ const original = JSON.parse(JSON.stringify(messages));
269
+
270
+ const result = filterSupersededContextInjections(messages as any);
271
+
272
+ const survivors = result.filter((m: any) => (m.content?.[0]?.text ?? "").startsWith(GSD_CONTEXT_MESSAGE_SENTINEL));
273
+ assert.equal(survivors.length, 1);
274
+ assert.match((survivors[0] as any).content[0].text, /turn=3/);
275
+ assert.equal(result.length, messages.length - 2);
276
+ // Pure function — input array is untouched.
277
+ assert.deepEqual(messages, original);
278
+ });
279
+
280
+ test("filterSupersededContextInjections keeps the latest injection overall across mixed kinds", () => {
281
+ // buildContextMessage emits one message per turn (guided > forensics > memory);
282
+ // the latest one already embeds the memory block when present, so "keep the
283
+ // latest overall" is correct — no separate per-kind tracking needed.
284
+ const memoryOnly = userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]`);
285
+ const guided = userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[GSD Context Metadata]\n- Memory supplied: yes\n\n[GUIDED EXECUTE]`);
286
+ const messages = [userMsg("turn 1"), memoryOnly, assistantMsg("r1"), userMsg("turn 2"), guided];
287
+
288
+ const result = filterSupersededContextInjections(messages as any);
289
+
290
+ const survivors = result.filter((m: any) => (m.content?.[0]?.text ?? "").startsWith(GSD_CONTEXT_MESSAGE_SENTINEL));
291
+ assert.equal(survivors.length, 1);
292
+ assert.match((survivors[0] as any).content[0].text, /GUIDED EXECUTE/);
293
+ });
294
+
295
+ test("filterSupersededContextInjections dedupes legacy pre-sentinel injections via bracketed GSD markers", () => {
296
+ // Sessions created before the sentinel existed carry the stable
297
+ // "[GSD Context Metadata]" / "[GSD Guided Execute Context]" labels but no
298
+ // sentinel prefix; resuming them must still collapse to the latest injection.
299
+ const legacyMemory1 = userMsg("[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY] turn=1");
300
+ const legacyMemory2 = userMsg("[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY] turn=2");
301
+ const messages = [userMsg("turn 1"), legacyMemory1, assistantMsg("r1"), userMsg("turn 2"), legacyMemory2];
302
+
303
+ const result = filterSupersededContextInjections(messages as any);
304
+
305
+ const survivors = result.filter((m: any) => (m.content?.[0]?.text ?? "").startsWith("[GSD Context Metadata]"));
306
+ assert.equal(survivors.length, 1);
307
+ assert.match((survivors[0] as any).content[0].text, /turn=2/);
308
+ assert.equal(result.length, messages.length - 1);
309
+ });
310
+
311
+ test("filterSupersededContextInjections never drops a real user message that starts with forensics prose", () => {
312
+ // "Debug GSD itself." is the forensics prompt text — generic prose, not a
313
+ // bracketed GSD marker. A user can legitimately type it; treating it as an
314
+ // injection would silently delete real user input. Only sentinel/bracketed
315
+ // injections may be superseded.
316
+ const userAsk = userMsg("Debug GSD itself. Why does resume hang after a crash?");
317
+ const injection = userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]`);
318
+ const messages = [userAsk, assistantMsg("r1"), userMsg("turn 2"), injection];
319
+
320
+ const result = filterSupersededContextInjections(messages as any);
321
+
322
+ assert.ok(result.some((m: any) => (m.content?.[0]?.text ?? "").startsWith("Debug GSD itself.")));
323
+ assert.equal(result.length, messages.length);
324
+ });
325
+
326
+ test("filterSupersededContextInjections returns the array unchanged when no injections present", () => {
327
+ const messages = [userMsg("hi"), assistantMsg("hello"), toolResult("data")];
328
+ const result = filterSupersededContextInjections(messages as any);
329
+ assert.equal(result, messages);
330
+ });
331
+
332
+ test("filterSupersededContextInjections preserves forensics opening prompt across follow-up reinjection", () => {
333
+ const forensicsOpening =
334
+ "Debug GSD itself. Trace the symptom to root cause in current source and produce a filing-ready GitHub issue.";
335
+ const messages = [
336
+ userMsg(forensicsOpening),
337
+ assistantMsg("I found the root cause in auto-loop.ts."),
338
+ userMsg("yes, create the issue"),
339
+ userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n${forensicsOpening}`),
340
+ assistantMsg("Creating the issue now."),
341
+ userMsg("also add the stack trace"),
342
+ userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n${forensicsOpening}\n\n## Follow-up\nalso add the stack trace`),
343
+ ];
344
+
345
+ const result = filterSupersededContextInjections(messages as any);
346
+
347
+ assert.equal(result.length, messages.length - 1);
348
+ assert.equal((result[0] as any).content[0].text, forensicsOpening);
349
+ assert.match((result[5] as any).content[0].text, /Follow-up/);
350
+ });
351
+
352
+ test("filterSupersededResponsesContextInjections keeps only the latest injection in payload.input", () => {
353
+ const items = [
354
+ { role: "user", content: [{ type: "input_text", text: "turn 1" }] },
355
+ { role: "user", content: [{ type: "input_text", text: `${GSD_CONTEXT_MESSAGE_SENTINEL}\n[MEMORY] turn=1` }] },
356
+ { type: "function_call_output", call_id: "call_1", output: "ok" },
357
+ { role: "user", content: [{ type: "input_text", text: `${GSD_CONTEXT_MESSAGE_SENTINEL}\n[MEMORY] turn=2` }] },
358
+ ];
359
+ const original = JSON.parse(JSON.stringify(items));
360
+
361
+ const result = filterSupersededResponsesContextInjections(items as any);
362
+
363
+ const survivors = result.filter((i: any) => (i.content?.[0]?.text ?? "").startsWith(GSD_CONTEXT_MESSAGE_SENTINEL));
364
+ assert.equal(survivors.length, 1);
365
+ assert.match((survivors[0] as any).content[0].text, /turn=2/);
366
+ assert.deepEqual(items, original);
367
+ });
368
+
369
+ test("filterSupersededResponsesContextInjections returns the array unchanged when no injections present", () => {
370
+ const items = [{ role: "user", content: [{ type: "input_text", text: "hi" }] }];
371
+ const result = filterSupersededResponsesContextInjections(items as any);
372
+ assert.equal(result, items);
373
+ });
@@ -0,0 +1,62 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Verifies GSD_ADVERTISE_TOOL_ALIASES gates alias registration on the native tool path (plan 035).
3
+
4
+ import { test } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { registerDbTools } from '../bootstrap/db-tools.ts';
7
+ import { WORKFLOW_TOOL_ALIAS_NAMES, WORKFLOW_TOOL_CONTRACTS } from '../workflow-tool-surface.ts';
8
+
9
+ function makeMockPi() {
10
+ const tools: any[] = [];
11
+ return {
12
+ registerTool: (tool: any) => tools.push(tool),
13
+ tools,
14
+ } as any;
15
+ }
16
+
17
+ function withEnv(name: string, value: string | undefined, fn: () => void): void {
18
+ const previous = process.env[name];
19
+ if (value === undefined) delete process.env[name];
20
+ else process.env[name] = value;
21
+ try {
22
+ fn();
23
+ } finally {
24
+ if (previous === undefined) delete process.env[name];
25
+ else process.env[name] = previous;
26
+ }
27
+ }
28
+
29
+ test('default: only canonical names are registered, no aliases', () => {
30
+ withEnv('GSD_ADVERTISE_TOOL_ALIASES', undefined, () => {
31
+ const pi = makeMockPi();
32
+ registerDbTools(pi);
33
+ const names = new Set(pi.tools.map((tool: any) => tool.name));
34
+
35
+ for (const alias of WORKFLOW_TOOL_ALIAS_NAMES) {
36
+ assert.ok(!names.has(alias), `alias "${alias}" should not be registered by default`);
37
+ }
38
+ const registeredCanonical = WORKFLOW_TOOL_CONTRACTS
39
+ .map((tool) => tool.canonicalName)
40
+ .filter((name) => names.has(name));
41
+ assert.ok(registeredCanonical.length > 0, 'canonical tools should still be registered');
42
+ });
43
+ });
44
+
45
+ test('GSD_ADVERTISE_TOOL_ALIASES=1: aliases are registered with the "(alias for ...)" description suffix', () => {
46
+ withEnv('GSD_ADVERTISE_TOOL_ALIASES', '1', () => {
47
+ const pi = makeMockPi();
48
+ registerDbTools(pi);
49
+ const toolByName = new Map<string, any>(pi.tools.map((tool: any) => [tool.name, tool]));
50
+
51
+ const registeredAliases = WORKFLOW_TOOL_ALIAS_NAMES.filter((alias) => toolByName.has(alias));
52
+ assert.ok(registeredAliases.length > 0, 'at least one alias should be registered when opted in');
53
+
54
+ for (const alias of registeredAliases) {
55
+ const aliasTool = toolByName.get(alias);
56
+ assert.ok(
57
+ aliasTool.description.includes('(alias for '),
58
+ `alias "${alias}" description should include "(alias for ...)"`,
59
+ );
60
+ }
61
+ });
62
+ });
@@ -18,6 +18,7 @@ import test from "node:test";
18
18
  import assert from "node:assert/strict";
19
19
 
20
20
  import { registerHooks } from "../bootstrap/register-hooks.ts";
21
+ import { autoSession } from "../auto-runtime-state.ts";
21
22
  import { classifyCommand } from "../safety/destructive-guard.ts";
22
23
  import {
23
24
  DESTRUCTIVE_CONFIRM_GATE_MARKER,
@@ -232,6 +233,38 @@ test("integration: real hooks block a force push, then allow it once after confi
232
233
  assert.equal(thirdAttempt?.block, true, "second run of the same command re-blocks (one-shot)");
233
234
  });
234
235
 
236
+ test("integration: auto-mode destructive block does not pause before confirmation can be asked", async (t) => {
237
+ const dir = "/tmp/gsd-destructive-confirm-int-auto-defer";
238
+ const notifications: string[] = [];
239
+ const ctx = {
240
+ cwd: dir,
241
+ isIdle: () => true,
242
+ sessionManager: { getSessionFile: () => null },
243
+ ui: { notify: (message: string) => notifications.push(message) },
244
+ } as any;
245
+ resetDestructiveConfirmation(dir);
246
+ autoSession.reset();
247
+ autoSession.active = true;
248
+ autoSession.basePath = dir;
249
+ t.after(() => {
250
+ resetDestructiveConfirmation(dir);
251
+ autoSession.reset();
252
+ });
253
+
254
+ const { handlers, pi } = makeHookHarness();
255
+ registerHooks(pi, []);
256
+
257
+ const block = await runBashGuard(handlers, FORCE_PUSH, ctx);
258
+
259
+ assert.equal(block?.block, true, "real guard blocks the first destructive attempt");
260
+ assert.equal(autoSession.active, true, "auto-mode must remain active so ask_user_questions can run");
261
+ assert.deepEqual(
262
+ notifications.filter((message) => message.includes("Destructive-command confirmation")),
263
+ [],
264
+ "destructive confirmation pause must not fire from the bash tool_call hook",
265
+ );
266
+ });
267
+
235
268
  test("integration: declining the confirm gate leaves the command blocked", async (t) => {
236
269
  const dir = "/tmp/gsd-destructive-confirm-int-decline";
237
270
  const ctx = { cwd: dir, ui: { notify: () => undefined } } as any;
@@ -29,6 +29,7 @@ test("destructive guard still detects direct and wrapper-prefixed recursive dele
29
29
  "find . -exec rm -rf {} \\;",
30
30
  "cat doomed.txt | xargs rm -rf /tmp/x",
31
31
  'rm -rf "/tmp/my build"',
32
+ "rm --recursive build",
32
33
  ];
33
34
 
34
35
  for (const command of destructiveCommands) {
@@ -39,6 +40,19 @@ test("destructive guard still detects direct and wrapper-prefixed recursive dele
39
40
  }
40
41
  });
41
42
 
43
+ test("destructive guard does not classify force-only rm as recursive delete", () => {
44
+ const nonRecursiveCommands = [
45
+ "rm -f single-file.md",
46
+ "rm -fv single-file.md",
47
+ "rm single-file.md -f",
48
+ "rm --force single-file.md",
49
+ ];
50
+
51
+ for (const command of nonRecursiveCommands) {
52
+ assert.deepEqual(classifyCommand(command), { destructive: false, labels: [] }, command);
53
+ }
54
+ });
55
+
42
56
  test("destructive guard treats quoted interpreter payloads as literals", () => {
43
57
  assert.deepEqual(classifyCommand('bash -c "rm -rf /"'), { destructive: false, labels: [] });
44
58
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, rmSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, rmSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { spawnSync } from "node:child_process";
@@ -12,6 +12,8 @@ import {
12
12
  resolveGsdPathContract,
13
13
  resolveSliceFile,
14
14
  resolveTaskFile,
15
+ resolveTaskFiles,
16
+ taskIdFromTaskFileName,
15
17
  _clearGsdRootCache,
16
18
  } from "../../paths.ts";
17
19
  /** Create a tmp dir and resolve symlinks + 8.3 short names (macOS /var→/private/var, Windows RUNNER~1→runneradmin). */
@@ -171,21 +173,37 @@ describe('paths', () => {
171
173
  test('Case 9: flat-phase task SUMMARY resolves at phase root despite a stray slices/SID/ dir', () => {
172
174
  const root = tmp();
173
175
  try {
174
- // Hybrid flat-phase milestone: phase dir holds flat TID-SUMMARY.md, but a
176
+ // Hybrid flat-phase milestone: phase dir holds flat task summaries, but a
175
177
  // stray slices/S01/ folder also exists on disk. resolveSlicePath then points
176
178
  // under slices/, so layout (phases/ vs milestones/) — not slicePath===phaseDir
177
179
  // — must decide the flat-phase task summary location.
178
180
  const phaseDir = join(root, ".gsd", "phases", "01-foo");
179
181
  mkdirSync(join(phaseDir, "slices", "S01"), { recursive: true });
180
- const flatSummary = join(phaseDir, "T01-SUMMARY.md");
182
+ const flatSummary = join(phaseDir, "S01-T01-SUMMARY.md");
183
+ const legacyFlatSummary = join(phaseDir, "T01-SUMMARY.md");
181
184
  writeFileSync(flatSummary, "# task summary\n");
185
+ writeFileSync(legacyFlatSummary, "# legacy task summary\n");
182
186
 
183
187
  _clearGsdRootCache();
184
188
 
189
+ assert.deepStrictEqual(resolveTaskFiles(phaseDir, "SUMMARY").sort(), [
190
+ "S01-T01-SUMMARY.md",
191
+ "T01-SUMMARY.md",
192
+ ]);
193
+ assert.equal(taskIdFromTaskFileName("S01-T01-SUMMARY.md", "SUMMARY"), "T01");
194
+ assert.equal(taskIdFromTaskFileName("T01-SUMMARY.md", "SUMMARY"), "T01");
195
+
185
196
  assert.deepStrictEqual(
186
197
  resolveTaskFile(root, "M001", "S01", "T01", "SUMMARY"),
187
198
  flatSummary,
188
- "flat-phase TID-SUMMARY.md at phase root is found even when slices/S01/ exists",
199
+ "flat-phase S01-T01-SUMMARY.md at phase root is preferred even when slices/S01/ exists",
200
+ );
201
+
202
+ unlinkSync(flatSummary);
203
+ assert.deepStrictEqual(
204
+ resolveTaskFile(root, "M001", "S01", "T01", "SUMMARY"),
205
+ legacyFlatSummary,
206
+ "legacy flat T01-SUMMARY.md is still found as a fallback",
189
207
  );
190
208
  } finally { cleanup(root); }
191
209
  });
@@ -0,0 +1,62 @@
1
+ // gsd-pi — Integration-branch META lives OUTSIDE milestones/<MID>/ (ADR-045).
2
+ // Verifies the relocation from milestones/<MID>/<MID>-META.json to the flat
3
+ // .gsd/<MID>-META.json so it can never poison isLegacyMilestonesLayout.
4
+
5
+ import assert from "node:assert/strict";
6
+ import { afterEach, beforeEach, test } from "node:test";
7
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+
11
+ import { readIntegrationBranch, writeIntegrationBranch } from "../git-service.js";
12
+ import { gsdRoot, isLegacyMilestonesLayout } from "../paths.js";
13
+
14
+ let repo: string;
15
+
16
+ beforeEach(() => {
17
+ repo = mkdtempSync(join(tmpdir(), "gsd-meta-loc-"));
18
+ });
19
+
20
+ afterEach(() => {
21
+ rmSync(repo, { recursive: true, force: true });
22
+ });
23
+
24
+ test("write goes to the new flat .gsd/<MID>-META.json, not milestones/<MID>/", () => {
25
+ writeIntegrationBranch(repo, "M001", "main");
26
+
27
+ assert.ok(
28
+ existsSync(join(gsdRoot(repo), "M001-META.json")),
29
+ "META written to flat .gsd/<MID>-META.json",
30
+ );
31
+ assert.ok(
32
+ !existsSync(join(gsdRoot(repo), "milestones", "M001", "M001-META.json")),
33
+ "no META in legacy milestones/<MID>/ location",
34
+ );
35
+ });
36
+
37
+ test("round-trip: read returns what write recorded", () => {
38
+ writeIntegrationBranch(repo, "M001", "main");
39
+ assert.equal(readIntegrationBranch(repo, "M001"), "main");
40
+ });
41
+
42
+ test("read falls back to the legacy milestones/<MID>/ location", () => {
43
+ const legacyDir = join(gsdRoot(repo), "milestones", "M001");
44
+ mkdirSync(legacyDir, { recursive: true });
45
+ writeFileSync(
46
+ join(legacyDir, "M001-META.json"),
47
+ JSON.stringify({ integrationBranch: "develop" }) + "\n",
48
+ "utf-8",
49
+ );
50
+
51
+ assert.equal(readIntegrationBranch(repo, "M001"), "develop");
52
+ });
53
+
54
+ test("writing META does not create milestones/<MID>/ or flip layout detection", () => {
55
+ assert.equal(isLegacyMilestonesLayout(repo), false, "clean tree is not legacy layout");
56
+ writeIntegrationBranch(repo, "M001", "main");
57
+ assert.equal(
58
+ isLegacyMilestonesLayout(repo),
59
+ false,
60
+ "writing integration-branch META must not poison layout detection",
61
+ );
62
+ });
@@ -824,9 +824,9 @@ test('── markdown-renderer: renderTaskSummary round-trip ──', async () =
824
824
 
825
825
  // Verify file exists on disk
826
826
  const summaryPath = path.join(
827
- tmpDir, '.gsd', 'phases', '01-test', 'T01-SUMMARY.md',
827
+ tmpDir, '.gsd', 'phases', '01-test', 'S01-T01-SUMMARY.md',
828
828
  );
829
- assert.ok(fs.existsSync(summaryPath), 'T01-SUMMARY.md written to disk');
829
+ assert.ok(fs.existsSync(summaryPath), 'S01-T01-SUMMARY.md written to disk');
830
830
 
831
831
  // Parse and verify
832
832
  const rendered = fs.readFileSync(summaryPath, 'utf-8');
@@ -1020,7 +1020,7 @@ test('── markdown-renderer: renderAllFromDb produces all files ──', asyn
1020
1020
 
1021
1021
  // Verify task summary written
1022
1022
  const taskSummaryPath = path.join(
1023
- tmpDir, '.gsd', 'phases', '01-test', 'T01-SUMMARY.md',
1023
+ tmpDir, '.gsd', 'phases', '01-test', 'S01-T01-SUMMARY.md',
1024
1024
  );
1025
1025
  assert.ok(fs.existsSync(taskSummaryPath), 'T01 summary written by renderAll');
1026
1026
  } finally {
@@ -1546,9 +1546,9 @@ test('── markdown-renderer: repairStaleRenders writes missing task summary
1546
1546
 
1547
1547
  // Verify file written
1548
1548
  const summaryPath = path.join(
1549
- tmpDir, '.gsd', 'phases', '01-test', 'T01-SUMMARY.md',
1549
+ tmpDir, '.gsd', 'phases', '01-test', 'S01-T01-SUMMARY.md',
1550
1550
  );
1551
- assert.ok(fs.existsSync(summaryPath), 'T01-SUMMARY.md should exist after repair');
1551
+ assert.ok(fs.existsSync(summaryPath), 'S01-T01-SUMMARY.md should exist after repair');
1552
1552
 
1553
1553
  // Second detect should be empty
1554
1554
  clearAllCaches();
@@ -74,7 +74,7 @@ test("#2885: cost table includes openai-codex provider models", () => {
74
74
  const ids = BUNDLED_COST_TABLE.map(e => e.id);
75
75
  const codexModels = [
76
76
  "gpt-5.1", "gpt-5.1-codex-max", "gpt-5.1-codex-mini",
77
- "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5",
77
+ "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6",
78
78
  ];
79
79
  for (const model of codexModels) {
80
80
  assert.ok(ids.includes(model), `cost table should include openai-codex model "${model}"`);
@@ -109,3 +109,11 @@ test("gpt-5.5 uses official OpenAI list pricing", () => {
109
109
  assert.equal(entry.outputPer1k, 0.03);
110
110
  assert.equal(entry.updatedAt, "2026-04-23");
111
111
  });
112
+
113
+ test("gpt-5.6 mirrors gpt-5.5 pricing until official pricing is published", () => {
114
+ const entry = lookupModelCost("gpt-5.6");
115
+ assert.ok(entry, "lookupModelCost should find gpt-5.6");
116
+ assert.equal(entry.inputPer1k, 0.005);
117
+ assert.equal(entry.outputPer1k, 0.03);
118
+ assert.equal(entry.updatedAt, "2026-07-08");
119
+ });
@@ -900,7 +900,7 @@ test("#2885: openai-codex standard-tier models are recognized", () => {
900
900
 
901
901
  test("#2885: openai-codex heavy-tier models are recognized", () => {
902
902
  const config = { ...defaultRoutingConfig(), enabled: true };
903
- const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "o4-mini", "o4-mini-deep-research"];
903
+ const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "gpt-5.6", "o4-mini", "o4-mini-deep-research"];
904
904
  for (const model of heavyModels) {
905
905
  const result = resolveModelForComplexity(
906
906
  makeClassification("heavy"),
@@ -29,12 +29,14 @@ import {
29
29
 
30
30
  const SKILL_NAME = "testskill";
31
31
  const COMPLETE_SLICE_SKILL_NAME = "complete-slice-policies";
32
- const SKILL_ACTIVATION_SUBSTRING = `Read the installed '${SKILL_NAME}' skill file from <available_skills>`;
33
- const COMPLETE_SLICE_SKILL_ACTIVATION_SUBSTRING = `Read the installed '${COMPLETE_SLICE_SKILL_NAME}' skill file from <available_skills>`;
34
32
 
35
33
  const tmpDirs: string[] = [];
36
34
  let savedCwd: string | undefined;
37
35
 
36
+ function expectedSkillRead(base: string, name: string): string {
37
+ return `Read the skill file at \`${join(base, ".agents", "skills", name, "SKILL.md")}\``;
38
+ }
39
+
38
40
  function setupProjectWithSkill(options: {
39
41
  skillName?: string;
40
42
  preferencesLines?: string[];
@@ -139,7 +141,7 @@ test("worker prompt (buildResearchSlicePrompt) includes <skill_activation> from
139
141
  "research-slice prompt should contain a <skill_activation> block",
140
142
  );
141
143
  assert.ok(
142
- prompt.includes(SKILL_ACTIVATION_SUBSTRING),
144
+ prompt.includes(expectedSkillRead(base, SKILL_NAME)),
143
145
  `research-slice prompt should reference the always-used skill '${SKILL_NAME}'`,
144
146
  );
145
147
  });
@@ -164,7 +166,7 @@ test("complete-slice prompt includes <skill_activation> from unit-specific skill
164
166
  "complete-slice prompt should contain a <skill_activation> block",
165
167
  );
166
168
  assert.ok(
167
- prompt.includes(COMPLETE_SLICE_SKILL_ACTIVATION_SUBSTRING),
169
+ prompt.includes(expectedSkillRead(base, COMPLETE_SLICE_SKILL_NAME)),
168
170
  `complete-slice prompt should reference the skill-rule skill '${COMPLETE_SLICE_SKILL_NAME}'`,
169
171
  );
170
172
  });
@@ -192,7 +194,7 @@ test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skil
192
194
  `expected at least 2 <skill_activation> blocks (one per slice), got ${blockCount}`,
193
195
  );
194
196
  assert.ok(
195
- prompt.includes(SKILL_ACTIVATION_SUBSTRING),
197
+ prompt.includes(expectedSkillRead(base, SKILL_NAME)),
196
198
  `parallel-research-slices prompt should reference the always-used skill '${SKILL_NAME}'`,
197
199
  );
198
200
  assert.ok(