@selesai/code 0.13.12 → 0.13.14

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 (203) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
  3. package/dist/extensions/pi-subagents/VISION.md +12 -1
  4. package/dist/extensions/pi-subagents/docs/agents.md +13 -6
  5. package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
  6. package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
  7. package/dist/extensions/pi-subagents/docs/missions.md +1 -1
  8. package/dist/extensions/pi-subagents/docs/models.md +6 -6
  9. package/dist/extensions/pi-subagents/docs/observability.md +6 -3
  10. package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
  11. package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
  12. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  13. package/dist/extensions/pi-subagents/install.mjs +1 -2
  14. package/dist/extensions/pi-subagents/package.json +1 -1
  15. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  16. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  20. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
  21. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
  22. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
  23. package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
  24. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
  25. package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
  26. package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
  27. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
  28. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  29. package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
  30. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
  31. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
  32. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
  34. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
  37. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
  38. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
  39. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
  40. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
  42. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
  43. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
  44. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
  45. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
  47. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
  48. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
  49. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
  51. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
  52. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
  53. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
  56. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
  57. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  59. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
  60. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
  62. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
  63. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
  64. package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
  65. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
  66. package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
  67. package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
  68. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  69. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
  70. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
  71. package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
  72. package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
  73. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
  74. package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
  75. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
  76. package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
  77. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
  78. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
  79. package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
  80. package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
  81. package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
  82. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
  83. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
  84. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
  85. package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
  86. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
  87. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
  88. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
  89. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
  90. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
  91. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
  92. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
  93. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
  94. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
  95. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
  96. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
  97. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
  98. package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
  99. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
  100. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
  101. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
  102. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
  103. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
  104. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
  105. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
  106. package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
  107. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
  108. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
  109. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  110. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
  111. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
  112. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
  113. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
  114. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  115. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
  116. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
  117. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
  118. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
  119. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
  120. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
  121. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
  122. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
  123. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
  124. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
  125. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
  126. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
  127. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
  128. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
  129. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
  130. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
  131. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
  132. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
  133. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
  134. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
  135. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
  136. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
  137. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
  138. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
  139. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
  140. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
  141. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
  142. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
  143. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
  144. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
  145. package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
  146. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
  147. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
  148. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
  149. package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
  150. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
  151. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
  152. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
  153. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
  154. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
  155. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
  156. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
  157. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
  158. package/dist/extensions/pi-zentui/README.md +19 -5
  159. package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
  160. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
  161. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
  162. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
  163. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
  164. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
  165. package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
  166. package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
  167. package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
  168. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
  169. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
  170. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  171. package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
  172. package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
  173. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
  174. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
  175. package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
  176. package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
  177. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
  178. package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
  179. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
  180. package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
  181. package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
  182. package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
  183. package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
  184. package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
  185. package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
  186. package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
  187. package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
  188. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
  189. package/dist/skills/brandkit/SKILL.md +0 -2
  190. package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
  191. package/dist/skills/gpt-taste/SKILL.md +0 -2
  192. package/dist/skills/image-to-code/SKILL.md +0 -2
  193. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
  194. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
  195. package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
  196. package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
  197. package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
  198. package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
  199. package/dist/skills/stitch/DESIGN.md +121 -0
  200. package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
  201. package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
  202. package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
  203. package/package.json +1 -1
@@ -11,9 +11,11 @@ import {
11
11
  aggregateAcceptanceReport,
12
12
  evaluateAcceptance,
13
13
  formatAcceptancePrompt,
14
+ normalizeAcceptanceInput,
14
15
  normalizeGateAcceptance,
15
16
  parseAcceptanceReport,
16
17
  quoteExecutableForShell,
18
+ resolveAcceptanceReportMode,
17
19
  resolveEffectiveAcceptance,
18
20
  stripAcceptanceReport,
19
21
  validateAcceptanceInput,
@@ -62,7 +64,7 @@ function tempGitRepo(): string {
62
64
 
63
65
  describe("acceptance gates", () => {
64
66
  it("infers evidence levels and review requirements independently", () => {
65
- assert.equal(resolveEffectiveAcceptance({ agentName: "reviewer", task: "Review-only. Do not edit.", mode: "single" }).level, "attested");
67
+ assert.equal(resolveEffectiveAcceptance({ agentName: "reviewer", task: "Review-only. Do not edit.", mode: "single" }).level, "none");
66
68
  assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single" }).level, "checked");
67
69
  for (const resolved of [
68
70
  resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single", async: true }),
@@ -73,7 +75,7 @@ describe("acceptance gates", () => {
73
75
  }
74
76
  });
75
77
 
76
- it("keeps async oracle review tasks on read-only acceptance despite implementation vocabulary", () => {
78
+ it("omits inferred acceptance for async oracle review tasks despite implementation vocabulary", () => {
77
79
  const resolved = resolveEffectiveAcceptance({
78
80
  agentName: "oracle",
79
81
  task: "Review prep findings and determine what to implement with playbooks instead of before.",
@@ -81,9 +83,9 @@ describe("acceptance gates", () => {
81
83
  async: true,
82
84
  });
83
85
 
84
- assert.equal(resolved.level, "attested");
86
+ assert.equal(resolved.level, "none");
85
87
  assert.deepEqual(resolved.inferredReason, ["read-only/reviewer-style agent"]);
86
- assert.deepEqual(resolved.criteria.map((criterion) => criterion.must), ["Return concrete findings with file paths and severity when applicable"]);
88
+ assert.deepEqual(resolved.criteria, []);
87
89
  });
88
90
 
89
91
  it("uses explicit agent roles for ambiguous tasks while preserving task-intent precedence", () => {
@@ -92,7 +94,7 @@ describe("acceptance gates", () => {
92
94
  acceptanceRole: "read-only",
93
95
  task: "Explore the authentication flow",
94
96
  mode: "single",
95
- }).level, "attested");
97
+ }).level, "none");
96
98
  assert.equal(resolveEffectiveAcceptance({
97
99
  agentName: "reviewer",
98
100
  acceptanceRole: "writer",
@@ -119,13 +121,13 @@ describe("acceptance gates", () => {
119
121
  acceptanceRole: "read-only",
120
122
  task: "Create a report",
121
123
  mode: "single",
122
- }).level, "attested");
124
+ }).level, "none");
123
125
  assert.equal(resolveEffectiveAcceptance({
124
126
  agentName: "worker",
125
127
  acceptanceRole: "writer",
126
128
  task: "Review only; do not edit files",
127
129
  mode: "single",
128
- }).level, "attested");
130
+ }).level, "none");
129
131
  assert.equal(resolveEffectiveAcceptance({
130
132
  agentName: "reviewer",
131
133
  acceptanceRole: "writer",
@@ -138,35 +140,35 @@ describe("acceptance gates", () => {
138
140
  acceptanceRole: "read-only",
139
141
  task: "Explore the authentication flow",
140
142
  mode: "single",
141
- }).level, "attested");
143
+ }).level, "none");
142
144
  assert.equal(resolveEffectiveAcceptance({
143
145
  agentName: "explorer",
144
146
  acceptanceRole: "read-only",
145
147
  task: "Audit the security posture",
146
148
  mode: "single",
147
- }).level, "attested");
149
+ }).level, "none");
148
150
  assert.equal(resolveEffectiveAcceptance({
149
151
  agentName: "explorer",
150
152
  acceptanceRole: "read-only",
151
153
  task: "Explore each target",
152
154
  mode: "chain",
153
155
  dynamic: true,
154
- }).level, "attested");
156
+ }).level, "none");
155
157
  assert.equal(resolveEffectiveAcceptance({
156
158
  agentName: "worker",
157
159
  acceptanceRole: "writer",
158
160
  task: "Review only; do not edit files",
159
161
  mode: "chain",
160
162
  dynamicGroup: true,
161
- }).level, "attested");
163
+ }).level, "none");
162
164
  const dynamicReviewer = resolveEffectiveAcceptance({
163
165
  agentName: "reviewer",
164
166
  task: "Review each target",
165
167
  mode: "chain",
166
168
  dynamic: true,
167
169
  });
168
- assert.equal(dynamicReviewer.level, "checked");
169
- assert.equal(dynamicReviewer.review && dynamicReviewer.review !== false ? dynamicReviewer.review.required : undefined, true);
170
+ assert.equal(dynamicReviewer.level, "none");
171
+ assert.equal(formatAcceptancePrompt(dynamicReviewer), "");
170
172
  });
171
173
 
172
174
  it("preserves risky keyword review inference when acceptance role metadata is omitted", () => {
@@ -254,6 +256,30 @@ describe("acceptance gates", () => {
254
256
  assert.match(prompt, /"reviewFindings": \[\n "blocker:/);
255
257
  });
256
258
 
259
+ it("omits inferred read-only prompts while preserving explicit acceptance", () => {
260
+ const inferred = resolveEffectiveAcceptance({
261
+ agentName: "reviewer",
262
+ task: "Review the diff and return findings only.",
263
+ });
264
+ assert.equal(inferred.level, "none");
265
+ assert.equal(formatAcceptancePrompt(inferred), "");
266
+
267
+ const explicit = resolveEffectiveAcceptance({
268
+ agentName: "reviewer",
269
+ task: "Review the diff and return findings only.",
270
+ explicit: { level: "checked", criteria: ["Return the review findings"] },
271
+ });
272
+ assert.match(formatAcceptancePrompt(explicit), /```acceptance-report/);
273
+
274
+ const explicitCriteria = resolveEffectiveAcceptance({
275
+ agentName: "reviewer",
276
+ task: "Review the diff and return findings only.",
277
+ explicit: { criteria: ["Return the review findings"] },
278
+ });
279
+ assert.equal(explicitCriteria.level, "attested");
280
+ assert.match(formatAcceptancePrompt(explicitCriteria), /```acceptance-report/);
281
+ });
282
+
257
283
  it("includes every required resolved criterion in report examples", () => {
258
284
  const inferred = resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single", async: true });
259
285
  const inferredExample = formatAcceptancePrompt(inferred).match(/```acceptance-report\n([\s\S]*?)\n```/);
@@ -774,6 +800,15 @@ describe("acceptance gates", () => {
774
800
  });
775
801
  assert.equal(malformedDirect.status, "rejected");
776
802
  assert.match(malformedDirect.childReportParseError ?? "", /unexpected: unsupported acceptance report field/);
803
+
804
+ const nullDirect = await evaluateAcceptance({
805
+ acceptance,
806
+ output: report({ changedFiles: [], testsAddedOrUpdated: [] }),
807
+ report: null as never,
808
+ cwd,
809
+ });
810
+ assert.equal(nullDirect.status, "rejected");
811
+ assert.match(nullDirect.childReportParseError ?? "", /acceptance-report: expected object; got null/);
777
812
  } finally {
778
813
  fs.rmSync(cwd, { recursive: true, force: true });
779
814
  }
@@ -1185,20 +1220,45 @@ describe("acceptance gates", () => {
1185
1220
  assert.match(errors.join("\n"), /acceptance\.review\.required/);
1186
1221
  });
1187
1222
 
1223
+ it("requires outputSchema for explicit structured acceptance report mode", () => {
1224
+ const schema = { type: "object" as const };
1225
+ const errors = validateExecutionAcceptance({
1226
+ acceptance: { level: "checked", report: "on" },
1227
+ tasks: [
1228
+ { acceptance: { level: "checked", report: "off" } },
1229
+ { outputSchema: schema, acceptance: { level: "checked", report: "on" } },
1230
+ ],
1231
+ chain: [
1232
+ { acceptance: { level: "checked", report: "on" } },
1233
+ { outputSchema: schema, acceptance: { level: "checked", report: "off" } },
1234
+ { parallel: [{ acceptance: { level: "checked", report: "on" } }, { outputSchema: schema, acceptance: { level: "checked", report: "on" } }] },
1235
+ { parallel: { acceptance: { level: "checked", report: "off" } } },
1236
+ ],
1237
+ });
1238
+
1239
+ assert.deepEqual(errors, [
1240
+ "acceptance.report requires outputSchema.",
1241
+ "tasks[0].acceptance.report requires outputSchema.",
1242
+ "chain[0].acceptance.report requires outputSchema.",
1243
+ "chain[2].parallel[0].acceptance.report requires outputSchema.",
1244
+ "chain[3].parallel.acceptance.report requires outputSchema.",
1245
+ ]);
1246
+ });
1247
+
1188
1248
  it("blanket read-only wording is not inferred as a risky write task", () => {
1189
1249
  const readOnlyWorker = resolveEffectiveAcceptance({
1190
1250
  agentName: "worker",
1191
1251
  task: "Report on the extraction pipeline. Do not modify project/source files.",
1192
1252
  async: true,
1193
1253
  });
1194
- assert.equal(readOnlyWorker.level, "attested");
1254
+ assert.equal(readOnlyWorker.level, "none");
1195
1255
  for (const task of [
1196
1256
  "Inspect the extraction pipeline",
1197
1257
  "Summarize the extraction pipeline",
1198
1258
  "Review only: return findings",
1199
1259
  "Analyze the extraction pipeline without edits",
1200
1260
  ]) {
1201
- assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task, async: true }).level, "attested", task);
1261
+ assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task, async: true }).level, "none", task);
1202
1262
  }
1203
1263
 
1204
1264
  assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Inspect the failure and implement the fix" }).level, "checked");
@@ -1332,6 +1392,17 @@ describe("acceptance gates", () => {
1332
1392
  }
1333
1393
  });
1334
1394
 
1395
+ it("normalizes JSON-encoded acceptance objects before existing validation", () => {
1396
+ const inline = { level: "checked" as const, evidence: ["commands-run" as const, "changed-files" as const] };
1397
+ const encoded = JSON.stringify(inline);
1398
+
1399
+ assert.deepEqual(normalizeAcceptanceInput(encoded), inline);
1400
+ assert.deepEqual(validateAcceptanceInput(encoded), validateAcceptanceInput(inline));
1401
+ assert.match(validateAcceptanceInput(JSON.stringify({ evidence: "commands-run" })).join("\n"), /acceptance\.evidence must be an array/);
1402
+ assert.match(validateAcceptanceInput('{"level":"checked"').join("\n"), /acceptance JSON string must encode a valid acceptance object/i);
1403
+ assert.match(validateAcceptanceInput(JSON.stringify(["checked"])).join("\n"), /acceptance JSON string must encode an object/i);
1404
+ });
1405
+
1335
1406
  it("validates invalid disable and verify shapes", () => {
1336
1407
  assert.deepEqual(validateAcceptanceInput({ level: "none" }), ["acceptance.reason is required when level is none."]);
1337
1408
  assert.deepEqual(validateAcceptanceInput("none"), ["acceptance level \"none\" requires a reason; use { level: \"none\", reason: \"...\" }."]);
@@ -1342,6 +1413,8 @@ describe("acceptance gates", () => {
1342
1413
  assert.deepEqual(validateAcceptanceInput({ level: "verified", verify: [{ id: "tests", command: "npm test" }] }), []);
1343
1414
  assert.deepEqual(validateAcceptanceInput({ level: "verified", verify: [{ id: "tests", command: "npm test" }, { id: "lint", command: "npm run lint" }] }), []);
1344
1415
  assert.deepEqual(validateAcceptanceInput({ level: "checked" }), []);
1416
+ assert.deepEqual(validateAcceptanceInput({ level: "checked", report: "on" }), []);
1417
+ assert.match(validateAcceptanceInput({ report: "sometimes" }).join("\n"), /acceptance\.report must be on or off/);
1345
1418
  assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "missing-command" }] }), ["acceptance.verify[0].command is required."]);
1346
1419
  assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "fractional", command: "npm test", timeoutMs: 1.5 }] }), ["acceptance.verify[0].timeoutMs must be an integer >= 1."]);
1347
1420
  assert.deepEqual(validateAcceptanceInput(false), []);
@@ -1363,6 +1436,14 @@ describe("acceptance gates", () => {
1363
1436
  assert.match(validateAcceptanceInput({ stopRules: [123] }).join("\n"), /acceptance\.stopRules\[0\] must be a string/);
1364
1437
  assert.match(validateAcceptanceInput({ surprise: true }).join("\n"), /acceptance\.surprise is not supported/);
1365
1438
  });
1439
+
1440
+ it("resolves structured acceptance report capture without changing the omitted default", () => {
1441
+ assert.equal(resolveAcceptanceReportMode(undefined), "optional");
1442
+ assert.equal(resolveAcceptanceReportMode("checked"), "optional");
1443
+ assert.equal(resolveAcceptanceReportMode(false), "off");
1444
+ assert.equal(resolveAcceptanceReportMode({ report: "off" }), "off");
1445
+ assert.equal(resolveAcceptanceReportMode({ report: "on" }), "required");
1446
+ });
1366
1447
  });
1367
1448
 
1368
1449
  describe("quoteExecutableForShell", () => {
@@ -5,6 +5,7 @@ import * as path from "node:path";
5
5
  import { describe, it } from "node:test";
6
6
  import {
7
7
  acquireActiveAsyncCapacity,
8
+ activeAsyncCapacitySessionKey,
8
9
  ActiveAsyncCapacityError,
9
10
  getActiveAsyncCapacitySnapshot,
10
11
  inspectActiveAsyncCapacityOwner,
@@ -194,7 +195,7 @@ describe("active async capacity", () => {
194
195
  }
195
196
  });
196
197
 
197
- it("releases a transferred failure that never started", () => {
198
+ it("restores a transferred source when revival fails before runner proceed", () => {
198
199
  const rootDir = tempRoot();
199
200
  const sourceDir = path.join(rootDir, "runs", "source");
200
201
  const failedDir = path.join(rootDir, "runs", "failed-revival");
@@ -206,8 +207,55 @@ describe("active async capacity", () => {
206
207
  const transferred = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "failed-revival", asyncDir: failedDir }, { rootDir });
207
208
  assert.ok(transferred);
208
209
  transferred.markStarted("failed-runner");
209
- writeJson(path.join(failedDir, "status.json"), { runId: "failed-revival", sessionId: "session-a", mode: "single", state: "failed", startedAt: 200, error: "Failed before startup proceed", processTerminal: { version: 1, state: "not-started", runId: "failed-revival", runnerProcessInstanceId: "failed-runner" } });
210
+ assert.equal(transferred.rollbackBeforeRunnerProceed("failed-runner"), true);
210
211
 
212
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
213
+ const sourceInspection = inspectActiveAsyncCapacityOwner({ sessionId: "session-a", runId: "source", asyncDir: sourceDir }, { rootDir });
214
+ assert.equal(sourceInspection.relation, "current");
215
+ assert.equal(sourceInspection.owner?.runId, "source");
216
+ assert.equal(inspectActiveAsyncCapacityOwner({ sessionId: "session-a", runId: "failed-revival", asyncDir: failedDir }, { rootDir }).relation, "none");
217
+ } finally {
218
+ fs.rmSync(rootDir, { recursive: true, force: true });
219
+ }
220
+ });
221
+
222
+ it("releases capacity bound before runner proceed when startup is terminated", () => {
223
+ const rootDir = tempRoot();
224
+ const unboundDir = path.join(rootDir, "runs", "pre-bind");
225
+ const asyncDir = path.join(rootDir, "runs", "pre-proceed");
226
+ try {
227
+ const unbound = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-bind", kind: "runner", asyncDir: unboundDir }, { rootDir });
228
+ assert.ok(unbound);
229
+ assert.equal(unbound.rollbackBeforeRunnerProceed("pre-bind-runner"), false);
230
+ assert.equal(unbound.rollback(), true);
231
+
232
+ const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-proceed", kind: "runner", asyncDir }, { rootDir });
233
+ assert.ok(handle);
234
+ handle.markStarted("pre-proceed-runner");
235
+ assert.equal(handle.rollback(), false);
236
+
237
+ assert.equal(handle.rollbackBeforeRunnerProceed("other-runner"), false);
238
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
239
+ assert.equal(handle.rollbackBeforeRunnerProceed("pre-proceed-runner"), true);
240
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
241
+
242
+ const bindFailure = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-proceed-bind-failure", kind: "runner", asyncDir: path.join(rootDir, "runs", "pre-proceed-bind-failure") }, {
243
+ rootDir,
244
+ writeOwner() { throw new Error("simulated durable bind failure"); },
245
+ });
246
+ assert.ok(bindFailure);
247
+ assert.throws(() => bindFailure.markStarted("pre-proceed-bind-failure-runner"), /simulated durable bind failure/);
248
+ assert.equal(bindFailure.rollback(), false);
249
+ assert.equal(bindFailure.rollbackBeforeRunnerProceed("pre-proceed-bind-failure-runner"), true);
250
+ assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
251
+
252
+ const divergent = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-proceed-divergent", kind: "runner", asyncDir: path.join(rootDir, "runs", "pre-proceed-divergent") }, { rootDir });
253
+ assert.ok(divergent);
254
+ const unstartedOwner = { ...divergent.owner };
255
+ divergent.markStarted("pre-proceed-divergent-runner");
256
+ writeJson(path.join(rootDir, activeAsyncCapacitySessionKey("session-a"), "slot-0", "owner.json"), unstartedOwner);
257
+ assert.equal(divergent.rollback(), false);
258
+ assert.equal(divergent.rollbackBeforeRunnerProceed("pre-proceed-divergent-runner"), true);
211
259
  assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
212
260
  } finally {
213
261
  fs.rmSync(rootDir, { recursive: true, force: true });
@@ -296,7 +344,7 @@ describe("active async capacity", () => {
296
344
  }
297
345
  });
298
346
 
299
- it("releases terminal workflows only after the controller is gone and async children have observed proof", () => {
347
+ it("releases terminal workflows despite stale step projections after the controller and async children stop", () => {
300
348
  const rootDir = tempRoot();
301
349
  const asyncRoot = path.join(rootDir, "runs");
302
350
  const workflowDir = path.join(asyncRoot, "workflow");
@@ -305,14 +353,14 @@ describe("active async capacity", () => {
305
353
  const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
306
354
  assert.ok(workflow);
307
355
  workflow.markWorkflowStarted();
308
- writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "foreground", async: false, status: "completed" }] });
356
+ writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "foreground", async: false, status: "running" }] });
309
357
  assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir, liveWorkflowRunIds: new Set(["workflow"]) }), { used: 1, limit: 1 });
310
358
  assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
311
359
 
312
360
  const second = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow-2", kind: "workflow", asyncDir: workflowDir }, { rootDir });
313
361
  assert.ok(second);
314
362
  second.markWorkflowStarted();
315
- writeJson(path.join(workflowDir, "status.json"), { runId: "workflow-2", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "async", runId: "child", async: true, status: "completed" }] });
363
+ writeJson(path.join(workflowDir, "status.json"), { runId: "workflow-2", sessionId: "session-a", mode: "workflow", state: "failed", startedAt: 100, steps: [{ agent: "worker", workflowKey: "async", runId: "child", async: true, status: "running" }] });
316
364
  writeJson(path.join(childDir, "status.json"), { runId: "child", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100, processTerminal: { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" } });
317
365
  writeJson(path.join(childDir, "process-terminal.json"), { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" });
318
366
  assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
@@ -515,6 +515,26 @@ Do work
515
515
  assert.deepEqual(worker?.extensions, [path.join(dir, ".selesai", "agents", "extension-one.ts"), path.join(dir, ".selesai", "agents", "extension-two.ts")]);
516
516
  assert.deepEqual(worker?.subagentOnlyExtensions, [path.join(dir, ".selesai", "agents", "child-only.ts"), path.join(dir, ".selesai", "agents", "child-helper.ts")]);
517
517
  });
518
+
519
+ it("discovers and serializes excludeTools without validating names", () => {
520
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-exclude-tools-frontmatter-"));
521
+ tempDirs.push(dir);
522
+ const agentPath = path.join(dir, ".selesai", "agents", "worker.md");
523
+ writeAgent(agentPath, `---
524
+ name: worker
525
+ description: Worker
526
+ excludeTools:
527
+ - write
528
+ - made-up-tool
529
+ ---
530
+
531
+ Do work
532
+ `);
533
+
534
+ const worker = discoverAgents(dir, "project").agents.find((agent) => agent.name === "worker");
535
+ assert.deepEqual(worker?.excludeTools, ["write", "made-up-tool"]);
536
+ assert.match(serializeAgent(worker!), /^excludeTools: write, made-up-tool$/m);
537
+ });
518
538
  });
519
539
 
520
540
  describe("agent permission frontmatter", () => {
@@ -8,6 +8,7 @@ import { EXTRA_AGENT_DIRS_ENV } from "../../src/agents/agents.ts";
8
8
  import { EXTERNAL_JOB_PROVIDER_REGISTRY_KEY, registerExternalJobProvider } from "../../src/api/external-job-provider.ts";
9
9
  import { clearSkillCache } from "../../src/agents/skills.ts";
10
10
  import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../src/shared/utils.ts";
11
+ import { openSubagentsAdmin } from "../../src/slash/subagents-admin.ts";
11
12
 
12
13
  let tempDir = "";
13
14
  let oldAgentDir: string | undefined;
@@ -891,7 +892,219 @@ Drive the failing test first.
891
892
  assert.match(afterText, /Inherit skills: true/);
892
893
  });
893
894
 
894
- it("preserves blank output and defaultReads frontmatter that blocks settings overrides during updates", () => {
895
+ it("does not serialize settings descriptions, fast, or defaults into custom agent frontmatter during updates", () => {
896
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
897
+ const settingsPath = path.join(tempDir, ".selesai", "settings.json");
898
+ const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
899
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
900
+ fs.writeFileSync(settingsPath, JSON.stringify({
901
+ subagents: {
902
+ defaultModel: "openai/gpt-default",
903
+ defaultThinking: "high",
904
+ agentOverrides: {
905
+ implementer: { description: "Settings description", output: "settings.md", fast: true },
906
+ },
907
+ },
908
+ }, null, 2), "utf-8");
909
+ fs.writeFileSync(agentPath, `---
910
+ name: implementer
911
+ description: Frontmatter description
912
+ ---
913
+
914
+ Drive the failing test first.
915
+ `, "utf-8");
916
+
917
+ const beforeText = readText(handleManagementAction("get", { agent: "implementer" }, ctx));
918
+ assert.match(beforeText, /Description: Settings description/);
919
+ assert.match(beforeText, /Model: openai\/gpt-default/);
920
+ assert.match(beforeText, /Thinking: high/);
921
+
922
+ const updated = handleUpdate({ agent: "implementer", config: { output: "local.md" } }, ctx);
923
+ assert.equal(updated.isError, false);
924
+
925
+ const content = fs.readFileSync(agentPath, "utf-8");
926
+ assert.match(content, /^description: Frontmatter description$/m);
927
+ assert.match(content, /^output: local\.md$/m);
928
+ assert.doesNotMatch(content, /^fast:/m);
929
+ assert.doesNotMatch(content, /^model:/m);
930
+ assert.doesNotMatch(content, /^thinking:/m);
931
+ });
932
+
933
+ it("keeps same-value custom override ownership for interactive admin edits", async () => {
934
+ const settingsPath = path.join(tempDir, ".selesai", "settings.json");
935
+ const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
936
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
937
+ fs.writeFileSync(settingsPath, JSON.stringify({
938
+ subagents: {
939
+ agentOverrides: { implementer: { model: "anthropic/claude-old" } },
940
+ },
941
+ }, null, 2), "utf-8");
942
+ fs.writeFileSync(agentPath, `---
943
+ name: implementer
944
+ description: Frontmatter description
945
+ model: anthropic/claude-old
946
+ ---
947
+
948
+ Drive the failing test first.
949
+ `, "utf-8");
950
+ const sent: unknown[] = [];
951
+ const notified: string[] = [];
952
+
953
+ await openSubagentsAdmin(
954
+ { sendMessage: (message: unknown) => sent.push(message) } as never,
955
+ {
956
+ cwd: tempDir,
957
+ hasUI: true,
958
+ modelRegistry: {
959
+ getAvailable: () => [
960
+ { provider: "anthropic", id: "claude-old" },
961
+ { provider: "anthropic", id: "claude-new" },
962
+ ],
963
+ },
964
+ ui: {
965
+ select: async () => "anthropic/claude-new",
966
+ notify: (message: string) => notified.push(message),
967
+ },
968
+ } as never,
969
+ "implementer model",
970
+ );
971
+
972
+ assert.match(notified[0] ?? "", /Saved project settings override/);
973
+ assert.match(JSON.stringify(sent), /Saved project settings override/);
974
+ assert.match(fs.readFileSync(agentPath, "utf-8"), /^model: anthropic\/claude-old$/m);
975
+ assert.equal(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-new");
976
+ const after = readText(handleManagementAction("get", { agent: "implementer" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
977
+ assert.match(after, /Model: anthropic\/claude-new/);
978
+ });
979
+
980
+ it("promotes cross-scope custom override edits to project settings", async () => {
981
+ const userSettingsPath = path.join(tempDir, "agent-home", "settings.json");
982
+ const projectSettingsPath = path.join(tempDir, ".selesai", "settings.json");
983
+ const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
984
+ fs.mkdirSync(path.dirname(userSettingsPath), { recursive: true });
985
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
986
+ fs.writeFileSync(userSettingsPath, JSON.stringify({
987
+ subagents: {
988
+ agentOverrides: {
989
+ implementer: { model: "anthropic/claude-old" },
990
+ critic: { thinking: "high" },
991
+ speaker: { systemPrompt: "Shared prompt" },
992
+ },
993
+ },
994
+ }, null, 2), "utf-8");
995
+ fs.writeFileSync(projectSettingsPath, JSON.stringify({
996
+ subagents: { defaultModel: "openai/gpt-default" },
997
+ }, null, 2), "utf-8");
998
+ fs.writeFileSync(agentPath, `---
999
+ name: implementer
1000
+ description: Frontmatter description
1001
+ ---
1002
+
1003
+ Drive the failing test first.
1004
+ `, "utf-8");
1005
+ fs.writeFileSync(path.join(path.dirname(agentPath), "critic.md"), `---
1006
+ name: critic
1007
+ description: Thinking critic
1008
+ thinking: false
1009
+ ---
1010
+
1011
+ Keep thinking off.
1012
+ `, "utf-8");
1013
+ fs.writeFileSync(path.join(path.dirname(agentPath), "speaker.md"), `---
1014
+ name: speaker
1015
+ description: Prompt speaker
1016
+ ---
1017
+
1018
+ Base prompt.
1019
+ `, "utf-8");
1020
+ const sent: unknown[] = [];
1021
+ const notified: string[] = [];
1022
+
1023
+ await openSubagentsAdmin(
1024
+ { sendMessage: (message: unknown) => sent.push(message) } as never,
1025
+ {
1026
+ cwd: tempDir,
1027
+ hasUI: true,
1028
+ modelRegistry: {
1029
+ getAvailable: () => [
1030
+ { provider: "anthropic", id: "claude-old" },
1031
+ { provider: "anthropic", id: "claude-new" },
1032
+ ],
1033
+ },
1034
+ ui: {
1035
+ select: async () => "anthropic/claude-new",
1036
+ notify: (message: string) => notified.push(message),
1037
+ },
1038
+ } as never,
1039
+ "implementer model",
1040
+ );
1041
+
1042
+ assert.match(notified[0] ?? "", /Saved project settings override/);
1043
+ assert.match(JSON.stringify(sent), /Saved project settings override/);
1044
+ assert.doesNotMatch(fs.readFileSync(agentPath, "utf-8"), /^model:/m);
1045
+ assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-old");
1046
+ assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-new");
1047
+ const after = readText(handleManagementAction("get", { agent: "implementer" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
1048
+ assert.match(after, /Model: anthropic\/claude-new/);
1049
+
1050
+ await openSubagentsAdmin(
1051
+ { sendMessage: (message: unknown) => sent.push(message) } as never,
1052
+ {
1053
+ cwd: tempDir,
1054
+ hasUI: true,
1055
+ modelRegistry: { getAvailable: () => [{ provider: "anthropic", id: "claude-new" }] },
1056
+ ui: {
1057
+ select: async () => "Default / inherit session model",
1058
+ notify: (message: string) => notified.push(message),
1059
+ },
1060
+ } as never,
1061
+ "implementer model",
1062
+ );
1063
+
1064
+ assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-old");
1065
+ assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "openai/gpt-default");
1066
+ const cleared = readText(handleManagementAction("get", { agent: "implementer" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
1067
+ assert.match(cleared, /Model: openai\/gpt-default/);
1068
+ assert.doesNotMatch(cleared, /Model: anthropic\/claude-old|Model: anthropic\/claude-new/);
1069
+
1070
+ await openSubagentsAdmin(
1071
+ { sendMessage: (message: unknown) => sent.push(message) } as never,
1072
+ {
1073
+ cwd: tempDir,
1074
+ hasUI: true,
1075
+ modelRegistry: { getAvailable: () => [] },
1076
+ ui: {
1077
+ select: async () => "Default / inherit session thinking",
1078
+ notify: (message: string) => notified.push(message),
1079
+ },
1080
+ } as never,
1081
+ "critic thinking",
1082
+ );
1083
+
1084
+ assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.critic.thinking, "high");
1085
+ assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.critic.thinking, "off");
1086
+ const thinkingCleared = readText(handleManagementAction("get", { agent: "critic" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
1087
+ assert.match(thinkingCleared, /Thinking: off/);
1088
+
1089
+ await openSubagentsAdmin(
1090
+ { sendMessage: (message: unknown) => sent.push(message) } as never,
1091
+ {
1092
+ cwd: tempDir,
1093
+ hasUI: true,
1094
+ modelRegistry: { getAvailable: () => [] },
1095
+ ui: {
1096
+ editor: async () => "Shared prompt",
1097
+ notify: (message: string) => notified.push(message),
1098
+ },
1099
+ } as never,
1100
+ "speaker system-prompt",
1101
+ );
1102
+
1103
+ assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.speaker.systemPrompt, "Shared prompt");
1104
+ assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.speaker.systemPrompt, "Shared prompt");
1105
+ });
1106
+
1107
+ it("preserves blank output and defaultReads frontmatter while settings overrides replace them", () => {
895
1108
  const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
896
1109
  const settingsPath = path.join(tempDir, ".selesai", "settings.json");
897
1110
  const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
@@ -916,11 +1129,11 @@ Drive the failing test first.
916
1129
  assert.match(content, /^output: ?$/m);
917
1130
  assert.match(content, /^defaultReads: ?$/m);
918
1131
  const after = readText(handleManagementAction("get", { agent: "implementer" }, ctx));
919
- assert.doesNotMatch(after, /Output: settings\.md/);
920
- assert.doesNotMatch(after, /Reads: settings\.md/);
1132
+ assert.match(after, /Output: settings\.md/);
1133
+ assert.match(after, /Reads: settings\.md/);
921
1134
  });
922
1135
 
923
- it("preserves explicit default-like frontmatter that blocks settings overrides during updates", () => {
1136
+ it("preserves explicit default-like frontmatter while settings overrides replace it", () => {
924
1137
  const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
925
1138
  const settingsPath = path.join(tempDir, ".selesai", "settings.json");
926
1139
  const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
@@ -958,8 +1171,8 @@ Drive the failing test first.
958
1171
  const got = handleManagementAction("get", { agent: "implementer" }, ctx);
959
1172
  assert.equal(got.isError, false);
960
1173
  const beforeText = readText(got);
961
- assert.match(beforeText, /Thinking: off/);
962
- assert.doesNotMatch(beforeText, /Thinking: high/);
1174
+ assert.match(beforeText, /Thinking: high/);
1175
+ assert.doesNotMatch(beforeText, /Thinking: off/);
963
1176
 
964
1177
  const updated = handleUpdate(
965
1178
  { agent: "implementer", config: { description: "Updated implementer" } },
@@ -980,8 +1193,8 @@ Drive the failing test first.
980
1193
  const gotAfter = handleManagementAction("get", { agent: "implementer" }, ctx);
981
1194
  assert.equal(gotAfter.isError, false);
982
1195
  const afterText = readText(gotAfter);
983
- assert.match(afterText, /Thinking: off/);
984
- assert.doesNotMatch(afterText, /Thinking: high/);
1196
+ assert.match(afterText, /Thinking: high/);
1197
+ assert.doesNotMatch(afterText, /Thinking: off/);
985
1198
  });
986
1199
 
987
1200
  it("reports builtin runtime-loaded model mappings from current session state", () => {