@selesai/code 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/models.json +5 -0
- package/dist/defaults/settings.json +1 -0
- 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/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -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/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- 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 +33 -0
- package/dist/extensions/question/question-list.ts +28 -0
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +75 -35
- package/dist/extensions/question/tests/question-list.test.ts +640 -198
- package/dist/extensions/question/tests/row-layout.test.ts +193 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- 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/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/settings.md +6 -0
- package/package.json +4 -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
|
}
|
|
@@ -8,7 +8,7 @@ description: |
|
|
|
8
8
|
other agents contribute context, planning, or execution.
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# Selesai Subagents
|
|
12
12
|
|
|
13
13
|
This skill is for the main parent orchestrator only. Do not inject or follow it inside spawned child subagents. The parent session owns delegation, orchestration, review fanout, and final fix-worker launches. Ordinary children should not run their own subagent workflows; the explicit exception is a delegated fanout child whose resolved builtin `tools` includes `subagent`, and that child may use `subagent` only for the fanout work the parent assigned.
|
|
14
14
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Selesai Subagents: Constraints And Recipes
|
|
2
2
|
|
|
3
3
|
This file is a detailed reference loaded from `skills/pi-subagents/SKILL.md`.
|
|
4
4
|
|
|
@@ -29,7 +29,7 @@ Launch every subagent asynchronously by default. Use `async: true` for scouts, r
|
|
|
29
29
|
|
|
30
30
|
### Use subagent_wait() to block until async runs finish
|
|
31
31
|
|
|
32
|
-
In an interactive chat, do not call `subagent_wait()` merely to wait after launching background work; return control to the user and
|
|
32
|
+
In an interactive chat, do not call `subagent_wait()` merely to wait after launching background work; return control to the user and Selesai will wake the session on completion. Override that default when the current request is run-to-completion — for example, the user asked you to stay with the task and report results back this turn or a skill must finish in one turn. In a headless run, Selesai auto-drains exact current-session work at `agent_end`; call `subagent_wait()` when this turn must receive results before it ends. In either case, `subagent_wait()` blocks the current turn until the next run completes or needs attention, keeps the turn alive for normal notification delivery, then returns.
|
|
33
33
|
|
|
34
34
|
- `subagent_wait()` — return when the next initially active async run or registered provider item finishes, or a subagent needs attention.
|
|
35
35
|
- `subagent_wait({ all: true })` — block until every async run and provider item active at call time finishes, or a subagent needs attention.
|
|
@@ -61,7 +61,7 @@ Give subagents specific tasks rather than vague mandates.
|
|
|
61
61
|
|
|
62
62
|
### Escalate decisions upward
|
|
63
63
|
|
|
64
|
-
If a subagent encounters an unapproved product, architecture, scope, merge, release, credential, or authority choice, it should use `contact_supervisor` and wait for the reply instead of deciding alone. Generic `intercom` is
|
|
64
|
+
If a subagent encounters an unapproved product, architecture, scope, merge, release, credential, or authority choice, it should use `contact_supervisor` and wait for the reply instead of deciding alone. Generic `intercom` is external or provider-supplied only. Use it only when external bridge instructions provide an explicit safe target. External checks, receipts, and review bots provide evidence only; they do not grant authority.
|
|
65
65
|
|
|
66
66
|
### Intervene only on clear control signals
|
|
67
67
|
|
|
@@ -211,7 +211,8 @@ Use distinct keys, prompts, and output paths. Do not launch parallel writers int
|
|
|
211
211
|
**"Unknown agent"**
|
|
212
212
|
```typescript
|
|
213
213
|
subagent({ action: "list" })
|
|
214
|
-
// Check available agents
|
|
214
|
+
// Check available agents, then confirm scope/precedence. Saved chains are not a
|
|
215
|
+
// public execution surface; author orchestration with workflowScript.
|
|
215
216
|
```
|
|
216
217
|
|
|
217
218
|
**Setup, discovery, or intercom confusion**
|