@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.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +46 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
- package/dist/extensions/pi-subagents/README.md +2 -0
- package/dist/extensions/pi-subagents/agents/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
- package/dist/extensions/pi-subagents/docs/observability.md +1 -1
- package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
- package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
- package/dist/extensions/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +18 -0
- package/dist/extensions/question/question-list.ts +22 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/helpers.test.ts +68 -35
- package/dist/extensions/question/tests/question-list.test.ts +506 -204
- package/dist/extensions/question/tests/row-layout.test.ts +180 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +5 -1
package/dist/extensions/undo.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
3
|
-
// until the reviewer reports clean. A workflowScript has no
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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`
|