@selesai/code 0.8.4 → 0.8.6

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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -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
  }
@@ -398,6 +398,7 @@ export declare class InteractiveMode {
398
398
  private notifyAuthDialog;
399
399
  private loginProvider;
400
400
  private showLoginDialog;
401
+ private handleSettingsFactoryReset;
401
402
  private handleReloadCommand;
402
403
  private handleExportCommand;
403
404
  private getPathCommandArgument;
@@ -9,7 +9,7 @@ import * as TuiLayouts from "@earendil-works/pi-tui";
9
9
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
10
10
  import chalk from "chalk";
11
11
  import { spawn, spawnSync } from "child_process";
12
- import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, getUpdateInstruction, PACKAGE_NAME, VERSION, } from "../../config.js";
12
+ import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getBundledDefaultsDir, getDebugLogPath, getDocsPath, getShareViewerUrl, getUpdateInstruction, PACKAGE_NAME, VERSION, } from "../../config.js";
13
13
  import { parseSkillBlocks } from "../../core/agent-session.js";
14
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
15
15
  import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
@@ -2319,6 +2319,11 @@ export class InteractiveMode {
2319
2319
  this.editor.setText("");
2320
2320
  return;
2321
2321
  }
2322
+ if (text === "/settings-factory-reset") {
2323
+ this.editor.setText("");
2324
+ await this.handleSettingsFactoryReset();
2325
+ return;
2326
+ }
2322
2327
  if (text === "/scoped-models") {
2323
2328
  this.editor.setText("");
2324
2329
  await this.showModelsSelector();
@@ -4649,6 +4654,48 @@ export class InteractiveMode {
4649
4654
  // =========================================================================
4650
4655
  // Command handlers
4651
4656
  // =========================================================================
4657
+ async handleSettingsFactoryReset() {
4658
+ if (this.session.isStreaming) {
4659
+ this.showWarning("Wait for the current response to finish before resetting settings.");
4660
+ return;
4661
+ }
4662
+ if (this.session.isCompacting) {
4663
+ this.showWarning("Wait for compaction to finish before resetting settings.");
4664
+ return;
4665
+ }
4666
+ const settingsPath = path.join(getAgentDir(), "settings.json");
4667
+ const confirmed = await this.showExtensionConfirm("Factory reset settings", `This replaces ${settingsPath} with the bundled factory defaults.\n\n`
4668
+ + "Your credentials (auth.json), sessions, extensions, skills, and themes are NOT touched.\n"
4669
+ + "A backup is saved next to the file before resetting.\n\n"
4670
+ + "Continue?");
4671
+ if (!confirmed) {
4672
+ this.showStatus("Settings factory reset cancelled.");
4673
+ return;
4674
+ }
4675
+ const bundledDefaultsDir = getBundledDefaultsDir();
4676
+ const bundledPath = path.join(bundledDefaultsDir, "settings.json");
4677
+ if (!fs.existsSync(bundledPath)) {
4678
+ this.showError(`Bundled defaults not found at ${bundledPath}`);
4679
+ return;
4680
+ }
4681
+ try {
4682
+ if (fs.existsSync(settingsPath)) {
4683
+ fs.copyFileSync(settingsPath, `${settingsPath}.bak`);
4684
+ }
4685
+ fs.copyFileSync(bundledPath, settingsPath);
4686
+ try {
4687
+ fs.chmodSync(settingsPath, 0o600);
4688
+ }
4689
+ catch {
4690
+ // chmod is best-effort (no-op on platforms that don't support it).
4691
+ }
4692
+ await this.session.reload();
4693
+ this.showStatus(`Settings reset to factory defaults (backup: ${settingsPath}.bak)`);
4694
+ }
4695
+ catch (error) {
4696
+ this.showError(`Settings factory reset failed: ${error instanceof Error ? error.message : String(error)}`);
4697
+ }
4698
+ }
4652
4699
  async handleReloadCommand() {
4653
4700
  if (this.session.isStreaming) {
4654
4701
  this.showWarning("Wait for the current response to finish before reloading.");
@@ -55,7 +55,7 @@ its resolved launch context as `[fresh]` or `[fork]`. Aggregate headers show
55
55
 
56
56
  `workflowScript` is the scripted orchestration surface. Use `runs.run(key, { agent, task, ... })` for one child, `runs.all([...])` for parallel children, and ordinary JavaScript for sequence, branching, filtering, retries, and aggregation. Scripts are ordinary JavaScript statement bodies, so use an explicit return such as `workflowScript: "return runs.run('main', { agent: 'worker', task: '...' })"` for a useful one-child result. Prefer a single scripted workflow whenever the parent is starting a coordinated wave, such as multiple reviews, review plus gate monitor, worker then monitor setup, cross-repo prep lanes, or a fanout that the parent will consume together; use the declarative `chain` / `tasks` modes when the shape is known up front.
57
57
 
58
- **Workflow modes as `workflowScript` auto-loops.** The bundled `workflow` extension ships its four mode shapes (task, prototype, quicktype, loop) as `workflowScript` auto-loops launched via `/workflow-*`. Each mode runs its phases as `runs.run` steps and loops the build↔review round until the commentator reports clean. There are no checkpoints (one-shot-and-sleep); durable state is the auto-created mission per launch; recover via `mission.list`/`status`.
58
+ **Workflow modes as `workflowScript` auto-loops.** The bundled `workflow` extension ships its four mode shapes (task, prototype, quicktype, loop) as `workflowScript` auto-loops launched via `/workflow-*`. Each mode runs its phases as `runs.run` steps and loops the build → review → fix round (a blocking review triggers a fix round that addresses only the findings) until the commentator reports clean. There are no checkpoints (one-shot-and-sleep); durable state is the auto-created mission per launch; recover via `mission.list`/`status`.
59
59
 
60
60
  ```js
61
61
  subagent({
@@ -1,7 +1,6 @@
1
1
  # Workflow auto-loop reference (implement exactly)
2
2
 
3
- > **Superseded:** the 3-round cap was later removed — the loop now runs until
4
- > clean. See `src/extensions/workflow/modes.ts` for the final implementation.
3
+ > **Superseded:** the 3-round cap was later removed and a per-round fix step was added — the loop now runs build → review → fix until clean. See `src/extensions/workflow/modes.ts` for the final implementation.
5
4
 
6
5
  Convert the four `/workflow-*` modes from static chains to pi-subagents
7
6
  `workflowScript` auto-loops. One-shot-and-sleep: the phases run as `runs.run`