@selesai/code 0.8.5 → 0.8.7

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 (231) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/models.json +5 -0
  16. package/dist/defaults/settings.json +1 -0
  17. package/dist/extensions/agent-browser.test.ts +178 -2
  18. package/dist/extensions/agent-browser.ts +2 -0
  19. package/dist/extensions/copy-turn.test.ts +229 -0
  20. package/dist/extensions/copy-turn.ts +11 -0
  21. package/dist/extensions/grep-app/index.test.ts +356 -1
  22. package/dist/extensions/grep-app/index.ts +6 -1
  23. package/dist/extensions/handoff-new.test.ts +351 -161
  24. package/dist/extensions/handoff-new.ts +3 -0
  25. package/dist/extensions/inline-skills.test.ts +93 -0
  26. package/dist/extensions/inline-skills.ts +3 -0
  27. package/dist/extensions/model-prompt-injector/config.json +10 -0
  28. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  29. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  30. package/dist/extensions/package.json +1 -0
  31. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  32. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  33. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  34. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  35. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  36. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  37. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  38. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  39. package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
  40. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  41. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  42. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  43. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  44. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  45. package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
  46. package/dist/extensions/pi-subagents/install.mjs +2 -2
  47. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  48. package/dist/extensions/pi-subagents/package.json +1 -1
  49. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  50. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  51. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  53. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  54. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  55. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  56. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  57. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  58. package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
  59. package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
  60. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  61. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  62. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  63. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  64. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  65. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  66. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  67. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  68. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  69. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  70. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  71. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  72. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  73. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  74. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  75. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
  76. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  77. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  80. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  82. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  83. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
  85. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  86. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  87. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
  88. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
  89. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  90. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  91. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  92. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  93. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  94. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  95. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  96. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  97. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  98. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  99. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  100. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
  101. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  102. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  104. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  105. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  106. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  112. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  113. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  114. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  115. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  116. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  117. package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  120. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  121. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  122. package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
  123. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  124. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  125. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  126. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  127. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  128. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  129. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  133. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  134. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  135. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  136. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  137. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  138. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  139. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  140. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  141. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  142. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  143. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  144. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  145. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  146. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  148. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  149. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  152. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  153. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  154. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  155. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  156. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  157. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  158. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  159. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
  160. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  161. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  162. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  163. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  164. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  165. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  166. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  167. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  168. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  169. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  170. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  171. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  172. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  173. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  174. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  175. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  176. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  177. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  178. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  179. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  180. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  181. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  182. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  183. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  193. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  194. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  195. package/dist/extensions/ponytail/index.js +11 -0
  196. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  197. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  198. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  199. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  200. package/dist/extensions/question/index.ts +33 -0
  201. package/dist/extensions/question/question-list.ts +28 -0
  202. package/dist/extensions/question/row-layout.ts +3 -0
  203. package/dist/extensions/question/tests/batch.test.ts +103 -65
  204. package/dist/extensions/question/tests/exp.test.ts +70 -0
  205. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  206. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  207. package/dist/extensions/question/tests/helpers.test.ts +75 -35
  208. package/dist/extensions/question/tests/question-list.test.ts +640 -198
  209. package/dist/extensions/question/tests/row-layout.test.ts +193 -114
  210. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  211. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  212. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  213. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  214. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  215. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  216. package/dist/extensions/rtk.test.ts +180 -1
  217. package/dist/extensions/tokenin-onboarding.ts +3 -0
  218. package/dist/extensions/undo.test.ts +720 -0
  219. package/dist/extensions/undo.ts +6 -0
  220. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  221. package/dist/extensions/workflow/extension.ts +3 -3
  222. package/dist/extensions/workflow/modes.ts +41 -19
  223. package/dist/skills/pi-subagents/SKILL.md +1 -1
  224. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  225. package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
  226. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  227. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  228. package/docs/plans/workflow-autoloop-reference.md +1 -2
  229. package/docs/plans/workflow-handoff-carryover.md +108 -0
  230. package/docs/settings.md +6 -0
  231. package/package.json +4 -1
@@ -160,6 +160,9 @@ async function restoreWrite(op: Extract<UndoOp, { type: "write" }>, cwd: string,
160
160
  if (op.oldContentTooLarge) throw new Error(`${op.path}: old content too large; cannot restore`);
161
161
  if (op.existed) {
162
162
  await mkdir(path.dirname(absPath), { recursive: true });
163
+ // oldContent is always set when existed && !oldContentTooLarge (the
164
+ // too-large case throws above), so the ?? fallback is unreachable.
165
+ /* v8 ignore next 1 */
163
166
  await writeFile(absPath, op.oldContent ?? "", "utf8");
164
167
  } else {
165
168
  if (existsSync(absPath)) await rm(absPath, { force: true });
@@ -173,6 +176,9 @@ async function restoreCheckpoint(checkpoint: Checkpoint, cwd: string, force: boo
173
176
  if (op.type === "edit") await restoreEdit(op, cwd, force);
174
177
  else await restoreWrite(op, cwd, force);
175
178
  } catch (error: any) {
179
+ // restoreEdit/restoreWrite always throw Error instances, so the
180
+ // String(error) fallback is unreachable.
181
+ /* v8 ignore next 1 */
176
182
  errors.push(error?.message ?? String(error));
177
183
  if (!force) break;
178
184
  }
@@ -0,0 +1,415 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ vi.mock("@selesai/code", () => ({
7
+ getAgentDir: () => process.env.SELESAI_CODING_AGENT_DIR ?? join(require("node:os").homedir(), ".selesai", "agent"),
8
+ getSettingsPath: () => join(process.env.SELESAI_CODING_AGENT_DIR ?? join(require("node:os").homedir(), ".selesai", "agent"), "settings.json"),
9
+ }));
10
+
11
+ import {
12
+ BRAVE_KEYS_URL,
13
+ getBraveApiKey,
14
+ getWebAgentConfigPath,
15
+ isPlaceholderKey,
16
+ ONBOARDING_MARKER_NAME,
17
+ readSettingsJson,
18
+ setBraveApiKey,
19
+ validateBraveKey,
20
+ WEB_AGENT_CONFIG_SUBPATH,
21
+ writeSettingsJson,
22
+ writeWebAgentBraveBackendConfig,
23
+ } from "./web-agent-onboarding.ts";
24
+ import webAgentOnboardingExtension from "./web-agent-onboarding.ts";
25
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionStartEvent } from "../core/extensions/types.ts";
26
+
27
+ let agentDir: string;
28
+
29
+ beforeEach(() => {
30
+ agentDir = mkdtempSync(join(tmpdir(), "web-agent-ob-"));
31
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
32
+ });
33
+
34
+ afterEach(() => {
35
+ rmSync(agentDir, { recursive: true, force: true });
36
+ delete process.env.SELESAI_CODING_AGENT_DIR;
37
+ });
38
+
39
+ describe("web-agent-onboarding helpers", () => {
40
+ it("isPlaceholderKey returns true for missing/empty/non-string values", () => {
41
+ expect(isPlaceholderKey(undefined)).toBe(true);
42
+ expect(isPlaceholderKey(null)).toBe(true);
43
+ expect(isPlaceholderKey("")).toBe(true);
44
+ expect(isPlaceholderKey(" ")).toBe(true);
45
+ expect(isPlaceholderKey(42)).toBe(true);
46
+ expect(isPlaceholderKey("BSA123abc")).toBe(false);
47
+ });
48
+
49
+ it("getBraveApiKey reads the nested value safely", () => {
50
+ expect(getBraveApiKey({})).toBeUndefined();
51
+ expect(getBraveApiKey({ webAgent: "not-an-object" })).toBeUndefined();
52
+ expect(getBraveApiKey({ webAgent: [] })).toBeUndefined();
53
+ expect(getBraveApiKey({ webAgent: { braveApiKey: "key-1" } })).toBe("key-1");
54
+ });
55
+
56
+ it("setBraveApiKey clones and sets the nested value", () => {
57
+ const original = { other: 1, webAgent: { braveApiKey: "old" } };
58
+ const next = setBraveApiKey(original, "new-key");
59
+ expect(next.webAgent).toEqual({ braveApiKey: "new-key" });
60
+ expect(next.other).toBe(1);
61
+ // original is not mutated
62
+ expect(original.webAgent.braveApiKey).toBe("old");
63
+ // missing/non-object webAgent is replaced
64
+ expect(setBraveApiKey({}, "k").webAgent).toEqual({ braveApiKey: "k" });
65
+ expect(setBraveApiKey({ webAgent: "x" }, "k").webAgent).toEqual({ braveApiKey: "k" });
66
+ expect(setBraveApiKey({ webAgent: [] }, "k").webAgent).toEqual({ braveApiKey: "k" });
67
+ });
68
+
69
+ it("validateBraveKey trims and reports errors", () => {
70
+ expect(validateBraveKey(" BSA-key ")).toEqual({ key: "BSA-key" });
71
+ expect(validateBraveKey("")).toEqual({ error: "Key cannot be empty." });
72
+ expect(validateBraveKey(" ")).toEqual({ error: "Key cannot be empty." });
73
+ expect(validateBraveKey(undefined)).toEqual({ error: "No key entered." });
74
+ expect(validateBraveKey(null as unknown as string)).toEqual({ error: "No key entered." });
75
+ });
76
+
77
+ it("readSettingsJson parses existing file, defaults to {} for missing/corrupt", () => {
78
+ const settingsPath = join(agentDir, "settings.json");
79
+ writeFileSync(settingsPath, '{"webAgent":{"braveApiKey":"k"}}', "utf-8");
80
+ expect(readSettingsJson(settingsPath)).toEqual({ webAgent: { braveApiKey: "k" } });
81
+ expect(readSettingsJson(join(agentDir, "missing.json"))).toEqual({});
82
+ writeFileSync(settingsPath, "{not json", "utf-8");
83
+ expect(readSettingsJson(settingsPath)).toEqual({});
84
+ });
85
+
86
+ it("writeSettingsJson creates dirs and writes pretty JSON with trailing newline", () => {
87
+ const deep = join(agentDir, "nested", "dir");
88
+ const settingsPath = join(deep, "settings.json");
89
+ writeSettingsJson({ a: 1 }, settingsPath);
90
+ expect(readFileSync(settingsPath, "utf-8")).toBe('{\n "a": 1\n}\n');
91
+ });
92
+
93
+ it("getWebAgentConfigPath joins agentDir with the config subpath", () => {
94
+ expect(getWebAgentConfigPath(agentDir)).toBe(join(agentDir, "extensions", "pi-web-agent", "config.json"));
95
+ expect(WEB_AGENT_CONFIG_SUBPATH).toBe(join("extensions", "pi-web-agent", "config.json"));
96
+ });
97
+
98
+ it("writeWebAgentBraveBackendConfig writes brave with duckduckgo fallback, merging existing", () => {
99
+ const configPath = getWebAgentConfigPath(agentDir);
100
+ // Pre-seed an existing config to verify merge-not-clobber.
101
+ mkdirSync(join(agentDir, "extensions", "pi-web-agent"), { recursive: true });
102
+ writeFileSync(configPath, '{"presentation":{"theme":"dark"}}', "utf-8");
103
+ writeWebAgentBraveBackendConfig(agentDir);
104
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
105
+ expect(parsed.presentation).toEqual({ theme: "dark" });
106
+ expect(parsed.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
107
+ });
108
+
109
+ it("writeWebAgentBraveBackendConfig tolerates missing/corrupt existing config", () => {
110
+ const configPath = getWebAgentConfigPath(agentDir);
111
+ mkdirSync(join(agentDir, "extensions", "pi-web-agent"), { recursive: true });
112
+ writeFileSync(configPath, "{corrupt", "utf-8");
113
+ writeWebAgentBraveBackendConfig(agentDir);
114
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
115
+ expect(parsed.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
116
+
117
+ // Missing file entirely -> starts fresh.
118
+ const freshDir = mkdtempSync(join(tmpdir(), "web-agent-fresh-"));
119
+ writeWebAgentBraveBackendConfig(freshDir);
120
+ const freshParsed = JSON.parse(readFileSync(join(freshDir, WEB_AGENT_CONFIG_SUBPATH), "utf-8"));
121
+ expect(freshParsed.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
122
+ rmSync(freshDir, { recursive: true, force: true });
123
+ });
124
+ });
125
+
126
+ describe("web-agent-onboarding constants and markers", () => {
127
+ it("exports the Brave keys URL and marker name", () => {
128
+ expect(BRAVE_KEYS_URL).toContain("brave.com");
129
+ expect(ONBOARDING_MARKER_NAME).toBe(".webAgentOnboardingComplete");
130
+ });
131
+ });
132
+
133
+ describe("web-agent-onboarding extension lifecycle", () => {
134
+ function makePi(overrides: Partial<ExtensionAPI> = {}): { pi: ExtensionAPI; on: ReturnType<typeof vi.fn>; registerCommand: ReturnType<typeof vi.fn> } {
135
+ const on = vi.fn();
136
+ const registerCommand = vi.fn();
137
+ const pi = { on, registerCommand, ...overrides } as unknown as ExtensionAPI;
138
+ webAgentOnboardingExtension(pi);
139
+ return { pi, on, registerCommand };
140
+ }
141
+
142
+ function sessionStartHandler(pi: ExtensionAPI): (event: SessionStartEvent, ctx: ExtensionContext) => Promise<void> {
143
+ return (pi.on as ReturnType<typeof vi.fn>).mock.calls.find(([event]: [string]) => event === "session_start")![1];
144
+ }
145
+
146
+ it("registers session_start and setup-web-search command", () => {
147
+ const { on, registerCommand } = makePi();
148
+ expect(on).toHaveBeenCalledWith("session_start", expect.any(Function));
149
+ expect(registerCommand).toHaveBeenCalledWith("setup-web-search", expect.objectContaining({ description: expect.stringContaining("Brave") }));
150
+ });
151
+
152
+ it("session_start ignores non-startup reasons", async () => {
153
+ const { pi } = makePi();
154
+ const handler = sessionStartHandler(pi);
155
+ const ctx = { hasUI: true } as unknown as ExtensionContext;
156
+ await handler({ reason: "reload" } as SessionStartEvent, ctx);
157
+ // No marker written, no prompt: marker missing proves no completion occurred.
158
+ expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(false);
159
+ });
160
+
161
+ it("session_start skips when onboarding is already complete", async () => {
162
+ writeFileSync(join(agentDir, ONBOARDING_MARKER_NAME), "1", "utf-8");
163
+ const { pi } = makePi();
164
+ const handler = sessionStartHandler(pi);
165
+ const input = vi.fn();
166
+ const ctx = { hasUI: true, ui: { input } } as unknown as ExtensionContext;
167
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
168
+ expect(input).not.toHaveBeenCalled();
169
+ });
170
+
171
+ it("session_start defers when SELESAI_SKIP_WEB_AGENT_ONBOARDING=1", async () => {
172
+ process.env.SELESAI_SKIP_WEB_AGENT_ONBOARDING = "1";
173
+ try {
174
+ const { pi } = makePi();
175
+ const handler = sessionStartHandler(pi);
176
+ const input = vi.fn();
177
+ const ctx = { hasUI: true, ui: { input } } as unknown as ExtensionContext;
178
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
179
+ expect(input).not.toHaveBeenCalled();
180
+ } finally {
181
+ delete process.env.SELESAI_SKIP_WEB_AGENT_ONBOARDING;
182
+ }
183
+ });
184
+
185
+ it("session_start skips when no UI is available", async () => {
186
+ const { pi } = makePi();
187
+ const handler = sessionStartHandler(pi);
188
+ const input = vi.fn();
189
+ const ctx = { hasUI: false, ui: { input } } as unknown as ExtensionContext;
190
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
191
+ expect(input).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it("already-configured key defers and marks complete", async () => {
195
+ const settingsPath = join(agentDir, "settings.json");
196
+ writeFileSync(settingsPath, '{"webAgent":{"braveApiKey":"existing-key"}}', "utf-8");
197
+ const { pi } = makePi();
198
+ const handler = sessionStartHandler(pi);
199
+ const confirm = vi.fn();
200
+ const ctx = { hasUI: true, ui: { confirm } } as unknown as ExtensionContext;
201
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
202
+ expect(confirm).not.toHaveBeenCalled();
203
+ expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(true);
204
+ });
205
+
206
+ it("declining Brave skips onboarding and notifies", async () => {
207
+ const { pi } = makePi();
208
+ const handler = sessionStartHandler(pi);
209
+ const notify = vi.fn();
210
+ const ctx = { hasUI: true, ui: { confirm: async () => false, notify } } as unknown as ExtensionContext;
211
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
212
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("skipped"), "info");
213
+ expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(true);
214
+ });
215
+
216
+ it("accepting Brave saves the key and config", async () => {
217
+ const { pi } = makePi();
218
+ const handler = sessionStartHandler(pi);
219
+ const notify = vi.fn();
220
+ const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
221
+ const ctx = {
222
+ hasUI: true,
223
+ ui: { confirm: async () => true, input: async () => " BSA-my-key ", notify },
224
+ } as unknown as ExtensionContext;
225
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
226
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("saved"), "info");
227
+ expect(exec).not.toHaveBeenCalled(); // openBraveKeysUrl uses pi.exec — not provided here
228
+ const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
229
+ expect(settings.webAgent.braveApiKey).toBe("BSA-my-key");
230
+ const config = JSON.parse(readFileSync(join(agentDir, WEB_AGENT_CONFIG_SUBPATH), "utf-8"));
231
+ expect(config.backends.search).toEqual({ provider: "brave", fallback: "duckduckgo" });
232
+ expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(true);
233
+ });
234
+
235
+ it("accepting Brave with browser open failure still saves", async () => {
236
+ const { pi } = makePi();
237
+ const handler = sessionStartHandler(pi);
238
+ const notify = vi.fn();
239
+ const exec = vi.fn().mockResolvedValue({ code: 1, stdout: "", stderr: "no browser" });
240
+ const ctx = {
241
+ hasUI: true,
242
+ ui: { confirm: async () => true, input: async () => "BSA-key", notify },
243
+ } as unknown as ExtensionContext;
244
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
245
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("Could not open browser"), "warning");
246
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("manually"), "info");
247
+ const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
248
+ expect(settings.webAgent.braveApiKey).toBe("BSA-key");
249
+ });
250
+
251
+ describe("openBraveKeysUrl platform branches", () => {
252
+ it("uses win32 command and still saves on zero exit", async () => {
253
+ const original = process.platform;
254
+ const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
255
+ try {
256
+ Object.defineProperty(process, "platform", { value: "win32", configurable: true });
257
+ const { pi } = makePi({ exec } as unknown as ExtensionAPI);
258
+ const handler = sessionStartHandler(pi);
259
+ const notify = vi.fn();
260
+ const ctx = {
261
+ hasUI: true,
262
+ ui: { confirm: async () => true, input: async () => "BSA-win", notify },
263
+ } as unknown as ExtensionContext;
264
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
265
+ expect(exec).toHaveBeenCalledWith("cmd", ["/c", "start", "", BRAVE_KEYS_URL], { timeout: 10_000 });
266
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("Opened the Brave API key dashboard"), "info");
267
+ } finally {
268
+ Object.defineProperty(process, "platform", { value: original, configurable: true });
269
+ }
270
+ });
271
+
272
+ it("uses xdg-open on other platforms", async () => {
273
+ const original = process.platform;
274
+ const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
275
+ try {
276
+ Object.defineProperty(process, "platform", { value: "linux", configurable: true });
277
+ const { pi } = makePi({ exec } as unknown as ExtensionAPI);
278
+ const handler = sessionStartHandler(pi);
279
+ const notify = vi.fn();
280
+ const ctx = {
281
+ hasUI: true,
282
+ ui: { confirm: async () => true, input: async () => "BSA-lin", notify },
283
+ } as unknown as ExtensionContext;
284
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
285
+ expect(exec).toHaveBeenCalledWith("xdg-open", [BRAVE_KEYS_URL], { timeout: 10_000 });
286
+ } finally {
287
+ Object.defineProperty(process, "platform", { value: original, configurable: true });
288
+ }
289
+ });
290
+
291
+ it("non-zero exit warns and opens the manual URL even after a string error", async () => {
292
+ const original = process.platform;
293
+ const exec = vi.fn().mockResolvedValue({ code: 1, stdout: "", stderr: "browser failed" });
294
+ try {
295
+ Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
296
+ const { pi } = makePi({ exec } as unknown as ExtensionAPI);
297
+ const handler = sessionStartHandler(pi);
298
+ const notify = vi.fn();
299
+ const ctx = {
300
+ hasUI: true,
301
+ ui: { confirm: async () => true, input: async () => "BSA-err", notify },
302
+ } as unknown as ExtensionContext;
303
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
304
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("Could not open browser: Failed to open"), "warning");
305
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("manually"), "info");
306
+ // Even though the browser failed, the key is still saved.
307
+ const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
308
+ expect(settings.webAgent.braveApiKey).toBe("BSA-err");
309
+ } finally {
310
+ Object.defineProperty(process, "platform", { value: original, configurable: true });
311
+ }
312
+ });
313
+
314
+ it("a rejecting exec (non-Error) still warns without crashing the flow", async () => {
315
+ const original = process.platform;
316
+ const exec = vi.fn().mockRejectedValue("plain string failure");
317
+ try {
318
+ Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
319
+ const { pi } = makePi({ exec } as unknown as ExtensionAPI);
320
+ const handler = sessionStartHandler(pi);
321
+ const notify = vi.fn();
322
+ const ctx = {
323
+ hasUI: true,
324
+ ui: { confirm: async () => true, input: async () => "BSA-rej", notify },
325
+ } as unknown as ExtensionContext;
326
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
327
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("Could not open browser: plain string failure"), "warning");
328
+ const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
329
+ expect(settings.webAgent.braveApiKey).toBe("BSA-rej");
330
+ } finally {
331
+ Object.defineProperty(process, "platform", { value: original, configurable: true });
332
+ }
333
+ });
334
+ it("non-zero exit with both stderr and stdout empty falls back to unknown error", async () => {
335
+ const original = process.platform;
336
+ const exec = vi.fn().mockResolvedValue({ code: 1, stdout: "", stderr: "" });
337
+ try {
338
+ Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
339
+ const { pi } = makePi({ exec } as unknown as ExtensionAPI);
340
+ const handler = sessionStartHandler(pi);
341
+ const notify = vi.fn();
342
+ const ctx = {
343
+ hasUI: true,
344
+ ui: { confirm: async () => true, input: async () => "BSA-un", notify },
345
+ } as unknown as ExtensionContext;
346
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
347
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("unknown error"), "warning");
348
+ const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
349
+ expect(settings.webAgent.braveApiKey).toBe("BSA-un");
350
+ } finally {
351
+ Object.defineProperty(process, "platform", { value: original, configurable: true });
352
+ }
353
+ });
354
+ });
355
+
356
+ it("empty pasted key defers without saving", async () => {
357
+ const { pi } = makePi();
358
+ const handler = sessionStartHandler(pi);
359
+ const notify = vi.fn();
360
+ const ctx = {
361
+ hasUI: true,
362
+ ui: { confirm: async () => true, input: async () => " ", notify },
363
+ } as unknown as ExtensionContext;
364
+ await handler({ reason: "startup" } as SessionStartEvent, ctx);
365
+ expect(notify).toHaveBeenCalledWith("Key cannot be empty.", "warning");
366
+ expect(existsSync(join(agentDir, ONBOARDING_MARKER_NAME))).toBe(false);
367
+ expect(existsSync(join(agentDir, "settings.json"))).toBe(false);
368
+ });
369
+
370
+ it("setup-web-search command saves and reloads extensions", async () => {
371
+ const exec = vi.fn().mockResolvedValue({ code: 0, stdout: "", stderr: "" });
372
+ const { registerCommand } = makePi({ exec } as unknown as ExtensionAPI);
373
+ const { handler } = registerCommand.mock.calls.find(([name]: [string]) => name === "setup-web-search")![1] as { handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> };
374
+ const reload = vi.fn().mockResolvedValue(undefined);
375
+ const notify = vi.fn();
376
+ const ctx = {
377
+ hasUI: true,
378
+ reload,
379
+ ui: { confirm: async () => true, input: async () => "BSA-key", notify },
380
+ } as unknown as ExtensionCommandContext;
381
+ await handler("", ctx);
382
+ expect(reload).toHaveBeenCalled();
383
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("reloading extensions"), "info");
384
+ const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf-8"));
385
+ expect(settings.webAgent.braveApiKey).toBe("BSA-key");
386
+ });
387
+
388
+ it("setup-web-search command does not reload when skipped", async () => {
389
+ const { registerCommand } = makePi();
390
+ const { handler } = registerCommand.mock.calls.find(([name]: [string]) => name === "setup-web-search")![1] as { handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> };
391
+ const reload = vi.fn();
392
+ const notify = vi.fn();
393
+ const ctx = {
394
+ hasUI: true,
395
+ reload,
396
+ ui: { confirm: async () => false, notify },
397
+ } as unknown as ExtensionCommandContext;
398
+ await handler("", ctx);
399
+ expect(reload).not.toHaveBeenCalled();
400
+ });
401
+
402
+ it("setup-web-search command defers (no reload) when key is invalid", async () => {
403
+ const { registerCommand } = makePi();
404
+ const { handler } = registerCommand.mock.calls.find(([name]: [string]) => name === "setup-web-search")![1] as { handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> };
405
+ const reload = vi.fn();
406
+ const notify = vi.fn();
407
+ const ctx = {
408
+ hasUI: true,
409
+ reload,
410
+ ui: { confirm: async () => true, input: async () => "", notify },
411
+ } as unknown as ExtensionCommandContext;
412
+ await handler("", ctx);
413
+ expect(reload).not.toHaveBeenCalled();
414
+ });
415
+ });
@@ -15,12 +15,12 @@ export default function workflowModesExtension(pi: ExtensionAPI): void {
15
15
  ctx.ui.notify(`${description}\nUsage: /${name} <goal>`, "info");
16
16
  return;
17
17
  }
18
- launchSlashSubagent(pi, ctx, { workflowScript: build(goal), async: true, agentScope: "both" });
18
+ launchSlashSubagent(pi, ctx, { workflowScript: build(goal), async: true, agentScope: "both", mission: { title: goal } });
19
19
  },
20
20
  });
21
21
 
22
- register("workflow-task", "Run the task workflow (plan → reuse → handoff → auto build↔review loop) as a scripted workflow.", buildTaskScript);
22
+ register("workflow-task", "Run the task workflow (plan → reuse → handoff → auto build→review→fix loop) as a scripted workflow.", buildTaskScript);
23
23
  register("workflow-prototype", "Run the full prototype workflow (research → plan → reuse → handoff → auto loop → audit) as a scripted workflow.", buildPrototypeScript);
24
24
  register("workflow-quicktype", "Run the quicker prototype workflow without research (plan → reuse → handoff → auto loop → audit) as a scripted workflow.", buildQuicktypeScript);
25
- register("workflow-loop", "Run a direct auto build↔review loop for an already-agreed plan as a scripted workflow.", buildLoopScript);
25
+ register("workflow-loop", "Run a direct auto build→review→fix loop for an already-agreed plan as a scripted workflow.", buildLoopScript);
26
26
  }
@@ -1,35 +1,57 @@
1
1
  // ponytail: four workflow modes as pi-subagents workflowScript builders.
2
- // One-shot-and-sleep: phases run as runs.run steps and the loop auto-repeats
3
- // until the reviewer reports clean. A workflowScript has no checkpoints, so
4
- // there are no human gates.
2
+ // One-shot-and-sleep: phases run as runs.run steps and the build → review → fix
3
+ // round auto-repeats until the reviewer reports clean. A workflowScript has no
4
+ // checkpoints, so there are no human gates.
5
5
 
6
6
  function js(value: string): string {
7
7
  return JSON.stringify(value);
8
8
  }
9
9
 
10
10
  const AUTO_LOOP = String.raw`
11
- const autoLoop = async (goal, context) => {
11
+ const autoLoop = async (goal, context, progressFile) => {
12
+ emit({ phase: 'start', goal });
12
13
  let round = 1;
14
+ let previousReview = '';
15
+ let completed = 0;
13
16
  while (true) {
14
- const build = await runs.run('build-' + round, {
15
- agent: 'builder',
16
- task: 'Implement the approved work in the workspace and run relevant checks.\n\nSource of truth (handoff/plan):\n' + context + '\n\nGoal:\n' + goal + (round > 1 ? '\n\nAddress the previous review feedback first.' : ''),
17
- });
18
- const review = await runs.run('review-' + round, {
19
- agent: 'commentator',
20
- task: 'Independently review the uncommitted diff and report concrete evidence (what you inspected and what you ran). Do not modify the workspace.\n\nAcceptance criteria (source of truth):\n' + context + '\n\nBuilder completion summary:\n' + build.output + '\n\nEnd with exactly one line: WORKFLOW_REVIEW_STATUS: clean OR WORKFLOW_REVIEW_STATUS: blocking.',
21
- });
22
- if (/WORKFLOW_REVIEW_STATUS\s*:\s*clean/i.test(review.output)) {
23
- return { result: 'clean', rounds: round };
17
+ try {
18
+ const build = await runs.run('build-' + round, {
19
+ agent: 'builder',
20
+ timeoutMs: 45 * 60 * 1000,
21
+ task: 'Implement the next bounded step of the approved work in the workspace and run relevant checks. Work on one small, self-contained slice this round; do not attempt the whole plan.\n\nSource of truth (handoff/plan):\n' + context + '\n\nGoal:\n' + goal + (round > 1 ? '\n\nPrevious review (its findings were addressed in the fix round; use its "Remaining work" notes to pick your next slice, do not re-apply findings):\n' + previousReview : '') + '\n\nRead the progress file first for prior-round context.\n\nProgress ledger: append a "## Round ' + round + '" entry to the progress file at ' + progressFile + ' before finishing. List every file you changed and a short summary of the work.',
22
+ });
23
+ const review = await runs.run('review-' + round, {
24
+ agent: 'commentator',
25
+ timeoutMs: 15 * 60 * 1000,
26
+ task: 'Independently review the builder work for this round and report concrete evidence (what you inspected and what you ran). Do not modify the workspace.\n\nAcceptance criteria (source of truth):\n' + context + '\n\nProgress file (scope your review to its latest round entry; also re-check the files from the immediately preceding fix entry if one exists; fall back to the full uncommitted diff if it is missing or empty):\n' + progressFile + '\n\nBuilder completion summary:\n' + build.output + '\n\nIf the plan is not yet complete, add a "Remaining work:" section listing the next concrete step(s). End with exactly one line: WORKFLOW_REVIEW_STATUS: clean OR WORKFLOW_REVIEW_STATUS: blocking.',
27
+ });
28
+ if (/WORKFLOW_REVIEW_STATUS\s*:\s*clean/i.test(review.output)) {
29
+ return { result: 'clean', rounds: completed + 1 };
30
+ }
31
+ previousReview = review.output;
32
+ await runs.run('fix-' + round, {
33
+ agent: 'builder',
34
+ timeoutMs: 45 * 60 * 1000,
35
+ task: 'Address ONLY the findings from the review below. The "Remaining work:" section (if present) is for the next round; do not act on it.\n\nProgress ledger: append a "## Round ' + round + ' fix" entry to the progress file at ' + progressFile + ' before finishing. List every file you changed and a short summary of the fixes.\n\nReviewer findings:\n' + review.output,
36
+ });
37
+ completed += 1;
38
+ round += 1;
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ if (/Run fan-out limit reached/i.test(message)) {
42
+ return { result: 'budget', rounds: completed, note: 'Run fan-out budget exhausted before the goal was reached. The progress file is current.' };
43
+ }
44
+ throw error;
24
45
  }
25
- round += 1;
26
46
  }
27
47
  };`;
28
48
 
49
+ const PROGRESS_DIR = ".pi-subagents/progress/";
50
+
29
51
  export function buildLoopScript(goal: string): string {
30
52
  return String.raw`const goal = ${js(goal)};
31
53
  ${AUTO_LOOP}
32
- return await autoLoop(goal, goal);`;
54
+ return await autoLoop(goal, goal, ${js(PROGRESS_DIR + "loop.md")});`;
33
55
  }
34
56
 
35
57
  export function buildTaskScript(goal: string): string {
@@ -38,7 +60,7 @@ const plan = await runs.run('plan', { agent: 'architect', task: 'Produce a concr
38
60
  const reuse = await runs.run('reuse', { agent: 'explorer', task: 'Explore the codebase for reusable patterns relevant to: ' + plan.output + '. Point at relevant areas and dependencies; skip cleanly if wholly new. Return inline.' });
39
61
  const handoff = await runs.run('handoff', { agent: 'recapper', task: 'Compile a self-contained handoff from the plan and reuse findings so fresh agents understand the goal, constraints, and acceptance criteria without re-planning.\n\nPlan:\n' + plan.output + '\n\nReuse findings:\n' + reuse.output + '\n\nReturn inline.' });
40
62
  ${AUTO_LOOP}
41
- return await autoLoop(goal, handoff.output);`;
63
+ return await autoLoop(goal, handoff.output, ${js(PROGRESS_DIR + "task.md")});`;
42
64
  }
43
65
 
44
66
  export function buildPrototypeScript(goal: string): string {
@@ -48,7 +70,7 @@ const plan = await runs.run('plan', { agent: 'architect', task: 'Produce a concr
48
70
  const reuse = await runs.run('reuse', { agent: 'explorer', task: 'Explore the codebase for reusable patterns relevant to: ' + plan.output + '. Return inline.' });
49
71
  const handoff = await runs.run('handoff', { agent: 'recapper', task: 'Compile a self-contained handoff from the plan and reuse findings.\n\nPlan:\n' + plan.output + '\n\nReuse:\n' + reuse.output + '\n\nReturn inline.' });
50
72
  ${AUTO_LOOP}
51
- const loop = await autoLoop(goal, handoff.output);
73
+ const loop = await autoLoop(goal, handoff.output, ${js(PROGRESS_DIR + "prototype.md")});
52
74
  const audit = await runs.run('audit', { agent: 'commentator', task: 'Final audit of the uncommitted changes for correctness, plan adherence, and over-engineering (cut bloat, dead flexibility, reinvented stdlib). Plan:\n' + plan.output + '\n\nReport concrete evidence. Do not modify the workspace.' });
53
75
  return { ...loop, audited: true };`;
54
76
  }
@@ -59,7 +81,7 @@ const plan = await runs.run('plan', { agent: 'architect', task: 'Produce a concr
59
81
  const reuse = await runs.run('reuse', { agent: 'explorer', task: 'Explore the codebase for reusable patterns relevant to: ' + plan.output + '. Return inline.' });
60
82
  const handoff = await runs.run('handoff', { agent: 'recapper', task: 'Compile a self-contained handoff from the plan and reuse findings.\n\nPlan:\n' + plan.output + '\n\nReuse:\n' + reuse.output + '\n\nReturn inline.' });
61
83
  ${AUTO_LOOP}
62
- const loop = await autoLoop(goal, handoff.output);
84
+ const loop = await autoLoop(goal, handoff.output, ${js(PROGRESS_DIR + "quicktype.md")});
63
85
  const audit = await runs.run('audit', { agent: 'commentator', task: 'Final audit of the uncommitted changes for correctness, plan adherence, and over-engineering (cut bloat, dead flexibility, reinvented stdlib). Plan:\n' + plan.output + '\n\nReport concrete evidence. Do not modify the workspace.' });
64
86
  return { ...loop, audited: true };`;
65
87
  }
@@ -8,7 +8,7 @@ description: |
8
8
  other agents contribute context, planning, or execution.
9
9
  ---
10
10
 
11
- # Pi Subagents
11
+ # Selesai Subagents
12
12
 
13
13
  This skill is for the main parent orchestrator only. Do not inject or follow it inside spawned child subagents. The parent session owns delegation, orchestration, review fanout, and final fix-worker launches. Ordinary children should not run their own subagent workflows; the explicit exception is a delegated fanout child whose resolved builtin `tools` includes `subagent`, and that child may use `subagent` only for the fanout work the parent assigned.
14
14
 
@@ -1,4 +1,4 @@
1
- # Pi Subagents: Constraints And Recipes
1
+ # Selesai Subagents: Constraints And Recipes
2
2
 
3
3
  This file is a detailed reference loaded from `skills/pi-subagents/SKILL.md`.
4
4
 
@@ -29,7 +29,7 @@ Launch every subagent asynchronously by default. Use `async: true` for scouts, r
29
29
 
30
30
  ### Use subagent_wait() to block until async runs finish
31
31
 
32
- In an interactive chat, do not call `subagent_wait()` merely to wait after launching background work; return control to the user and Pi will wake the session on completion. Override that default when the current request is run-to-completion — for example, the user asked you to stay with the task and report results back this turn or a skill must finish in one turn. In a headless run, Pi auto-drains exact current-session work at `agent_end`; call `subagent_wait()` when this turn must receive results before it ends. In either case, `subagent_wait()` blocks the current turn until the next run completes or needs attention, keeps the turn alive for normal notification delivery, then returns.
32
+ In an interactive chat, do not call `subagent_wait()` merely to wait after launching background work; return control to the user and Selesai will wake the session on completion. Override that default when the current request is run-to-completion — for example, the user asked you to stay with the task and report results back this turn or a skill must finish in one turn. In a headless run, Selesai auto-drains exact current-session work at `agent_end`; call `subagent_wait()` when this turn must receive results before it ends. In either case, `subagent_wait()` blocks the current turn until the next run completes or needs attention, keeps the turn alive for normal notification delivery, then returns.
33
33
 
34
34
  - `subagent_wait()` — return when the next initially active async run or registered provider item finishes, or a subagent needs attention.
35
35
  - `subagent_wait({ all: true })` — block until every async run and provider item active at call time finishes, or a subagent needs attention.
@@ -61,7 +61,7 @@ Give subagents specific tasks rather than vague mandates.
61
61
 
62
62
  ### Escalate decisions upward
63
63
 
64
- If a subagent encounters an unapproved product, architecture, scope, merge, release, credential, or authority choice, it should use `contact_supervisor` and wait for the reply instead of deciding alone. Generic `intercom` is a fallback only when the bridge-provided supervisor tool is unavailable. External checks, receipts, and review bots provide evidence only; they do not grant authority.
64
+ If a subagent encounters an unapproved product, architecture, scope, merge, release, credential, or authority choice, it should use `contact_supervisor` and wait for the reply instead of deciding alone. Generic `intercom` is external or provider-supplied only. Use it only when external bridge instructions provide an explicit safe target. External checks, receipts, and review bots provide evidence only; they do not grant authority.
65
65
 
66
66
  ### Intervene only on clear control signals
67
67
 
@@ -211,7 +211,8 @@ Use distinct keys, prompts, and output paths. Do not launch parallel writers int
211
211
  **"Unknown agent"**
212
212
  ```typescript
213
213
  subagent({ action: "list" })
214
- // Check available agents and chains, then confirm scope/precedence.
214
+ // Check available agents, then confirm scope/precedence. Saved chains are not a
215
+ // public execution surface; author orchestration with workflowScript.
215
216
  ```
216
217
 
217
218
  **Setup, discovery, or intercom confusion**