@selesai/code 0.2.1 → 0.3.1
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/dist/config.d.ts +4 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -45
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +8 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/diagnostics.d.ts +1 -0
- package/dist/core/diagnostics.d.ts.map +1 -1
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +101 -51
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +10 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +93 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manager.test.d.ts +2 -0
- package/dist/core/package-manager.test.d.ts.map +1 -0
- package/dist/core/package-manager.test.js +20 -0
- package/dist/core/package-manager.test.js.map +1 -0
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +10 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/defaults/models.json +64 -113
- package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
- package/dist/extensions/caveman/index.js +116 -0
- package/dist/extensions/caveman/package.json +8 -0
- package/dist/extensions/caveman/test/extension.test.js +185 -0
- package/dist/extensions/caveman/test/helpers.test.js +67 -0
- package/dist/extensions/copy-turn.ts +2 -2
- package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
- package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
- package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
- package/dist/extensions/pi-intercom/config.ts +2 -2
- package/dist/extensions/pi-intercom/index.ts +2 -2
- package/dist/extensions/pi-intercom/package.json +2 -2
- package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
- package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
- package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
- package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +40 -47
- package/dist/extensions/pi-powerline-footer/package.json +2 -2
- package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
- package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
- package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
- package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
- package/dist/extensions/pi-powerline-footer/types.ts +7 -7
- package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
- package/dist/extensions/pi-rewind-hook/index.ts +1 -1
- package/dist/extensions/pi-rewind-hook/package.json +1 -1
- package/dist/extensions/pi-subagents/README.md +224 -52
- package/dist/extensions/pi-subagents/agents/architect.md +188 -0
- package/dist/extensions/pi-subagents/agents/builder.md +120 -0
- package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
- package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
- package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
- package/dist/extensions/pi-subagents/install.mjs +93 -0
- package/dist/extensions/pi-subagents/package.json +4 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
- package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
- package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
- package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
- package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
- package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
- package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
- package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
- package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
- package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
- package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
- package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
- package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
- package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
- package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
- package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
- package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
- package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
- package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
- package/dist/extensions/pi-web-agent/package.json +2 -2
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
- package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
- package/dist/extensions/question/helpers.ts +3 -3
- package/dist/extensions/question/index.ts +2 -2
- package/dist/extensions/question/navigation.ts +2 -2
- package/dist/extensions/question/package.json +2 -2
- package/dist/extensions/question/question-list.ts +2 -2
- package/dist/extensions/question/tests/question-list.test.ts +2 -2
- package/dist/extensions/question/tui-adapter.ts +2 -2
- package/dist/extensions/question/types.ts +2 -2
- package/dist/extensions/question/ui-protocol.ts +2 -2
- package/dist/extensions/rtk.ts +2 -2
- package/dist/extensions/tokenin-onboarding.ts +2 -2
- package/dist/extensions/undo.ts +1 -1
- package/dist/extensions/web-agent-onboarding.ts +3 -3
- package/dist/extensions/workflow/adapter.ts +16 -8
- package/dist/extensions/workflow/extension.ts +1 -1
- package/dist/extensions/workflow/package.json +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +19 -5
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/skills/caveman/SKILL.md +49 -0
- package/dist/skills/handoff-text/SKILL.md +13 -0
- package/dist/utils/thinking-tags.d.ts +7 -0
- package/dist/utils/thinking-tags.d.ts.map +1 -0
- package/dist/utils/thinking-tags.js +63 -0
- package/dist/utils/thinking-tags.js.map +1 -0
- package/docs/docs.json +4 -0
- package/docs/extensions.md +2 -0
- package/docs/settings.md +15 -0
- package/docs/shared-host-extensions.md +109 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
6
|
+
import { handleManagementAction } from "../../src/agents/agent-management.ts";
|
|
7
|
+
import { serializeAgent } from "../../src/agents/agent-serializer.ts";
|
|
8
|
+
import { parseChain, serializeChain } from "../../src/agents/chain-serializer.ts";
|
|
9
|
+
import { discoverAgents, discoverAgentsAll, type AgentConfig } from "../../src/agents/agents.ts";
|
|
10
|
+
|
|
11
|
+
const tempDirs: string[] = [];
|
|
12
|
+
|
|
13
|
+
function writeJson(filePath: string, value: unknown): void {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeAgent(filePath: string, body: string): void {
|
|
19
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(filePath, body, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function withTempHome<T>(fn: (home: string) => T): T {
|
|
24
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-home-"));
|
|
25
|
+
tempDirs.push(home);
|
|
26
|
+
const oldHome = process.env.HOME;
|
|
27
|
+
const oldUserProfile = process.env.USERPROFILE;
|
|
28
|
+
const oldPiCodingAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
29
|
+
const oldExtraAgentDirs = process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
|
|
30
|
+
process.env.HOME = home;
|
|
31
|
+
process.env.USERPROFILE = home;
|
|
32
|
+
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
33
|
+
delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
|
|
34
|
+
try {
|
|
35
|
+
return fn(home);
|
|
36
|
+
} finally {
|
|
37
|
+
if (oldHome === undefined) delete process.env.HOME;
|
|
38
|
+
else process.env.HOME = oldHome;
|
|
39
|
+
if (oldUserProfile === undefined) delete process.env.USERPROFILE;
|
|
40
|
+
else process.env.USERPROFILE = oldUserProfile;
|
|
41
|
+
if (oldPiCodingAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
42
|
+
else process.env.SELESAI_CODING_AGENT_DIR = oldPiCodingAgentDir;
|
|
43
|
+
if (oldExtraAgentDirs === undefined) delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
|
|
44
|
+
else process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = oldExtraAgentDirs;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
while (tempDirs.length > 0) {
|
|
50
|
+
const dir = tempDirs.pop();
|
|
51
|
+
if (!dir) continue;
|
|
52
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("agent permission frontmatter", () => {
|
|
57
|
+
it("preserves nested permission YAML blocks through discovery and serialization", () => {
|
|
58
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-permission-frontmatter-"));
|
|
59
|
+
tempDirs.push(dir);
|
|
60
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
61
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
62
|
+
fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
|
|
63
|
+
name: worker
|
|
64
|
+
description: Worker
|
|
65
|
+
tools: bash,read,write
|
|
66
|
+
permission:
|
|
67
|
+
"*": ask
|
|
68
|
+
read: allow
|
|
69
|
+
bash:
|
|
70
|
+
"*": ask
|
|
71
|
+
"git *": allow
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
Do work
|
|
75
|
+
`, "utf-8");
|
|
76
|
+
|
|
77
|
+
const result = discoverAgents(dir, "project");
|
|
78
|
+
const worker = result.agents.find((agent) => agent.name === "worker");
|
|
79
|
+
assert.equal(worker?.extraFields?.permission, `"*": ask
|
|
80
|
+
read: allow
|
|
81
|
+
bash:
|
|
82
|
+
"*": ask
|
|
83
|
+
"git *": allow`);
|
|
84
|
+
|
|
85
|
+
const serialized = serializeAgent(worker!);
|
|
86
|
+
assert.match(serialized, /^permission:\n "\*": ask\n read: allow\n bash:\n "\*": ask\n "git \*": allow$/m);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("agent frontmatter defaultContext", () => {
|
|
91
|
+
it("serializes defaultContext into agent frontmatter", () => {
|
|
92
|
+
const agent: AgentConfig = {
|
|
93
|
+
name: "worker",
|
|
94
|
+
description: "Worker",
|
|
95
|
+
systemPrompt: "Do work",
|
|
96
|
+
systemPromptMode: "replace",
|
|
97
|
+
inheritProjectContext: true,
|
|
98
|
+
inheritSkills: false,
|
|
99
|
+
source: "project",
|
|
100
|
+
filePath: "/tmp/worker.md",
|
|
101
|
+
defaultContext: "fork",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const serialized = serializeAgent(agent);
|
|
105
|
+
assert.match(serialized, /defaultContext: fork/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("parses defaultContext from discovered agent frontmatter", () => {
|
|
109
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-default-context-"));
|
|
110
|
+
tempDirs.push(dir);
|
|
111
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
112
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
113
|
+
fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
|
|
114
|
+
name: worker
|
|
115
|
+
description: Worker
|
|
116
|
+
defaultContext: fork
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
Do work
|
|
120
|
+
`, "utf-8");
|
|
121
|
+
|
|
122
|
+
const result = discoverAgents(dir, "project");
|
|
123
|
+
const worker = result.agents.find((agent) => agent.name === "worker");
|
|
124
|
+
assert.equal(worker?.defaultContext, "fork");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("loads packaged planner, worker, and oracle with fork defaultContext", () => {
|
|
128
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-default-context-"));
|
|
129
|
+
tempDirs.push(dir);
|
|
130
|
+
const agents = discoverAgentsAll(dir).builtin;
|
|
131
|
+
|
|
132
|
+
for (const name of ["planner", "worker", "oracle"]) {
|
|
133
|
+
const agent = agents.find((candidate) => candidate.name === name);
|
|
134
|
+
assert.equal(agent?.defaultContext, "fork", `${name} should default to fork context`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("chain discovery", () => {
|
|
140
|
+
it("prefers same-scope .chain.json over .chain.md for the same runtime name", () => {
|
|
141
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-chain-format-precedence-"));
|
|
142
|
+
tempDirs.push(dir);
|
|
143
|
+
const chainsDir = path.join(dir, ".selesai", "chains");
|
|
144
|
+
fs.mkdirSync(chainsDir, { recursive: true });
|
|
145
|
+
fs.writeFileSync(path.join(chainsDir, "dynamic-review.chain.md"), `---
|
|
146
|
+
name: dynamic-review
|
|
147
|
+
description: Markdown fallback
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## scout
|
|
151
|
+
|
|
152
|
+
Run the markdown chain
|
|
153
|
+
`, "utf-8");
|
|
154
|
+
fs.writeFileSync(path.join(chainsDir, "dynamic-review.chain.json"), JSON.stringify({
|
|
155
|
+
name: "dynamic-review",
|
|
156
|
+
description: "JSON dynamic chain",
|
|
157
|
+
chain: [
|
|
158
|
+
{
|
|
159
|
+
agent: "scout",
|
|
160
|
+
task: "Return targets",
|
|
161
|
+
as: "targets",
|
|
162
|
+
outputSchema: { type: "object" },
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
|
|
166
|
+
parallel: { agent: "reviewer", task: "Review {item.path}" },
|
|
167
|
+
collect: { as: "reviews" },
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
}), "utf-8");
|
|
171
|
+
|
|
172
|
+
const result = discoverAgentsAll(dir);
|
|
173
|
+
const chain = result.chains.find((candidate) => candidate.name === "dynamic-review");
|
|
174
|
+
assert.equal(chain?.description, "JSON dynamic chain");
|
|
175
|
+
assert.equal(chain?.filePath.endsWith(".chain.json"), true);
|
|
176
|
+
assert.equal("expand" in (chain?.steps[1] ?? {}), true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("package-provided agents and chains", () => {
|
|
181
|
+
it("discovers package agents and chains from installed package manifests", () => withTempHome(() => {
|
|
182
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-discovery-"));
|
|
183
|
+
tempDirs.push(dir);
|
|
184
|
+
const workflowRoot = path.join(dir, ".selesai", "npm", "node_modules", "my-pi-workflow");
|
|
185
|
+
const chainsRoot = path.join(dir, ".selesai", "npm", "node_modules", "@scope", "chain-workflow");
|
|
186
|
+
writeJson(path.join(workflowRoot, "package.json"), {
|
|
187
|
+
name: "my-pi-workflow",
|
|
188
|
+
"pi-subagents": {
|
|
189
|
+
agents: ["./agents"],
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
writeAgent(path.join(workflowRoot, "agents", "reviewer.md"), `---
|
|
193
|
+
name: reviewer
|
|
194
|
+
package: my-workflow
|
|
195
|
+
description: Review changes for this workflow.
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
Review the workflow.
|
|
199
|
+
`);
|
|
200
|
+
writeJson(path.join(chainsRoot, "package.json"), {
|
|
201
|
+
name: "@scope/chain-workflow",
|
|
202
|
+
pi: {
|
|
203
|
+
subagents: {
|
|
204
|
+
chains: ["./chains"],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
writeAgent(path.join(chainsRoot, "chains", "review.chain.md"), `---
|
|
209
|
+
name: review
|
|
210
|
+
package: my-workflow
|
|
211
|
+
description: Run workflow review.
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## my-workflow.reviewer
|
|
215
|
+
|
|
216
|
+
Review the task.
|
|
217
|
+
`);
|
|
218
|
+
|
|
219
|
+
const all = discoverAgentsAll(dir);
|
|
220
|
+
const packagedAgent = all.package.find((agent) => agent.name === "my-workflow.reviewer");
|
|
221
|
+
assert.ok(packagedAgent);
|
|
222
|
+
assert.equal(packagedAgent.source, "package");
|
|
223
|
+
assert.equal(packagedAgent.filePath, path.join(workflowRoot, "agents", "reviewer.md"));
|
|
224
|
+
assert.equal(discoverAgents(dir, "both").agents.find((agent) => agent.name === "my-workflow.reviewer")?.source, "package");
|
|
225
|
+
|
|
226
|
+
const packagedChain = all.chains.find((chain) => chain.name === "my-workflow.review");
|
|
227
|
+
assert.ok(packagedChain);
|
|
228
|
+
assert.equal(packagedChain.source, "package");
|
|
229
|
+
assert.equal(packagedChain.steps[0]?.agent, "my-workflow.reviewer");
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
it("loads packages referenced from Pi settings", () => withTempHome(() => {
|
|
233
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-settings-package-"));
|
|
234
|
+
tempDirs.push(dir);
|
|
235
|
+
const packageRoot = path.join(dir, ".selesai", "vendor", "workflow");
|
|
236
|
+
writeJson(path.join(dir, ".selesai", "settings.json"), {
|
|
237
|
+
packages: [{ source: "file:./vendor/workflow" }],
|
|
238
|
+
});
|
|
239
|
+
writeJson(path.join(packageRoot, "package.json"), {
|
|
240
|
+
name: "settings-workflow",
|
|
241
|
+
pi: {
|
|
242
|
+
subagents: {
|
|
243
|
+
agents: ["./agents"],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
writeAgent(path.join(packageRoot, "agents", "planner.md"), `---
|
|
248
|
+
name: planner
|
|
249
|
+
package: settings-workflow
|
|
250
|
+
description: Plan from a settings-installed package.
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
Plan the work.
|
|
254
|
+
`);
|
|
255
|
+
|
|
256
|
+
const agent = discoverAgents(dir, "both").agents.find((candidate) => candidate.name === "settings-workflow.planner");
|
|
257
|
+
assert.ok(agent);
|
|
258
|
+
assert.equal(agent.source, "package");
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
it("discovers project package agents when cwd is nested below the project root", () => withTempHome(() => {
|
|
262
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-nested-package-discovery-"));
|
|
263
|
+
tempDirs.push(dir);
|
|
264
|
+
const nested = path.join(dir, "packages", "app", "src");
|
|
265
|
+
const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "nested-workflow");
|
|
266
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
267
|
+
writeJson(path.join(packageRoot, "package.json"), {
|
|
268
|
+
name: "nested-workflow",
|
|
269
|
+
"pi-subagents": {
|
|
270
|
+
agents: ["./agents"],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
writeAgent(path.join(packageRoot, "agents", "reviewer.md"), `---
|
|
274
|
+
name: reviewer
|
|
275
|
+
package: nested-workflow
|
|
276
|
+
description: Review from a project package.
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
Review nested project work.
|
|
280
|
+
`);
|
|
281
|
+
|
|
282
|
+
const agent = discoverAgents(nested, "both").agents.find((candidate) => candidate.name === "nested-workflow.reviewer");
|
|
283
|
+
assert.ok(agent);
|
|
284
|
+
assert.equal(agent.source, "package");
|
|
285
|
+
assert.equal(agent.filePath, path.join(packageRoot, "agents", "reviewer.md"));
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
it("does not register legacy skill files from broad package agent roots", () => withTempHome(() => {
|
|
289
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-broad-package-skills-"));
|
|
290
|
+
tempDirs.push(dir);
|
|
291
|
+
const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "broad-workflow");
|
|
292
|
+
writeJson(path.join(packageRoot, "package.json"), {
|
|
293
|
+
name: "broad-workflow",
|
|
294
|
+
"pi-subagents": {
|
|
295
|
+
agents: ["."],
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
writeAgent(path.join(packageRoot, "agent.md"), `---
|
|
299
|
+
name: package-agent
|
|
300
|
+
description: Package agent
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
Package prompt
|
|
304
|
+
`);
|
|
305
|
+
writeAgent(path.join(packageRoot, ".agents", "skills", "package-skill", "SKILL.md"), `---
|
|
306
|
+
name: package-skill
|
|
307
|
+
description: Package skill
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
Skill prompt
|
|
311
|
+
`);
|
|
312
|
+
writeAgent(path.join(packageRoot, "agents", "SKILL.md"), `---
|
|
313
|
+
name: skill-named-package-agent
|
|
314
|
+
description: Skill-named package agent
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
Agent prompt
|
|
318
|
+
`);
|
|
319
|
+
|
|
320
|
+
const packageAgents = discoverAgentsAll(dir).package;
|
|
321
|
+
assert.ok(packageAgents.find((agent) => agent.name === "package-agent" && agent.filePath === path.join(packageRoot, "agent.md")));
|
|
322
|
+
assert.ok(packageAgents.find((agent) => agent.name === "skill-named-package-agent" && agent.filePath === path.join(packageRoot, "agents", "SKILL.md")));
|
|
323
|
+
assert.equal(packageAgents.some((agent) => agent.filePath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)), false);
|
|
324
|
+
assert.equal(packageAgents.some((agent) => agent.name === "package-skill"), false);
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
it("keeps package definitions below user and project overrides", () => withTempHome((home) => {
|
|
328
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-precedence-"));
|
|
329
|
+
tempDirs.push(dir);
|
|
330
|
+
const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "override-workflow");
|
|
331
|
+
writeJson(path.join(packageRoot, "package.json"), {
|
|
332
|
+
name: "override-workflow",
|
|
333
|
+
"pi-subagents": {
|
|
334
|
+
agents: ["./agents"],
|
|
335
|
+
chains: ["./chains"],
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
writeAgent(path.join(packageRoot, "agents", "scout.md"), `---
|
|
339
|
+
name: scout
|
|
340
|
+
description: Package scout
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
Package scout.
|
|
344
|
+
`);
|
|
345
|
+
writeAgent(path.join(packageRoot, "chains", "shared.chain.md"), `---
|
|
346
|
+
name: shared
|
|
347
|
+
description: Package chain
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## scout
|
|
351
|
+
|
|
352
|
+
Package chain.
|
|
353
|
+
`);
|
|
354
|
+
writeAgent(path.join(home, ".selesai", "agent", "agents", "scout.md"), `---
|
|
355
|
+
name: scout
|
|
356
|
+
description: User scout
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
User scout.
|
|
360
|
+
`);
|
|
361
|
+
writeAgent(path.join(dir, ".selesai", "agents", "scout.md"), `---
|
|
362
|
+
name: scout
|
|
363
|
+
description: Project scout
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
Project scout.
|
|
367
|
+
`);
|
|
368
|
+
writeAgent(path.join(home, ".selesai", "agent", "chains", "shared.chain.md"), `---
|
|
369
|
+
name: shared
|
|
370
|
+
description: User chain
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## scout
|
|
374
|
+
|
|
375
|
+
User chain.
|
|
376
|
+
`);
|
|
377
|
+
writeAgent(path.join(dir, ".selesai", "chains", "shared.chain.md"), `---
|
|
378
|
+
name: shared
|
|
379
|
+
description: Project chain
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## scout
|
|
383
|
+
|
|
384
|
+
Project chain.
|
|
385
|
+
`);
|
|
386
|
+
|
|
387
|
+
assert.equal(discoverAgents(dir, "user").agents.find((agent) => agent.name === "scout")?.source, "user");
|
|
388
|
+
assert.equal(discoverAgents(dir, "project").agents.find((agent) => agent.name === "scout")?.source, "project");
|
|
389
|
+
const chainByName = new Map(discoverAgentsAll(dir).chains.map((chain) => [chain.name, chain]));
|
|
390
|
+
assert.equal(chainByName.get("shared")?.source, "project");
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
it("does not allow management updates to package agents", () => withTempHome(() => {
|
|
394
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-readonly-"));
|
|
395
|
+
tempDirs.push(dir);
|
|
396
|
+
const packageRoot = path.join(dir, ".selesai", "npm", "node_modules", "readonly-workflow");
|
|
397
|
+
writeJson(path.join(packageRoot, "package.json"), {
|
|
398
|
+
name: "readonly-workflow",
|
|
399
|
+
"pi-subagents": {
|
|
400
|
+
agents: ["./agents"],
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
writeAgent(path.join(packageRoot, "agents", "reviewer.md"), `---
|
|
404
|
+
name: reviewer
|
|
405
|
+
package: readonly-workflow
|
|
406
|
+
description: Read-only package reviewer.
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
Review only.
|
|
410
|
+
`);
|
|
411
|
+
|
|
412
|
+
const result = handleManagementAction("update", {
|
|
413
|
+
agent: "readonly-workflow.reviewer",
|
|
414
|
+
config: { description: "Changed" },
|
|
415
|
+
}, {
|
|
416
|
+
cwd: dir,
|
|
417
|
+
modelRegistry: { getAvailable: () => [] },
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
assert.equal(result.isError, true);
|
|
421
|
+
assert.match(result.content[0]?.text ?? "", /read-only/);
|
|
422
|
+
}));
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("agent frontmatter completionGuard", () => {
|
|
426
|
+
it("serializes disabled completion guard into agent frontmatter", () => {
|
|
427
|
+
const agent: AgentConfig = {
|
|
428
|
+
name: "test-runner",
|
|
429
|
+
description: "Test runner",
|
|
430
|
+
systemPrompt: "Validate changes",
|
|
431
|
+
systemPromptMode: "replace",
|
|
432
|
+
inheritProjectContext: false,
|
|
433
|
+
inheritSkills: false,
|
|
434
|
+
source: "project",
|
|
435
|
+
filePath: "/tmp/test-runner.md",
|
|
436
|
+
completionGuard: false,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const serialized = serializeAgent(agent);
|
|
440
|
+
assert.match(serialized, /completionGuard: false/);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("omits enabled completion guard from serialized frontmatter", () => {
|
|
444
|
+
const agent: AgentConfig = {
|
|
445
|
+
name: "test-runner",
|
|
446
|
+
description: "Test runner",
|
|
447
|
+
systemPrompt: "Validate changes",
|
|
448
|
+
systemPromptMode: "replace",
|
|
449
|
+
inheritProjectContext: false,
|
|
450
|
+
inheritSkills: false,
|
|
451
|
+
source: "project",
|
|
452
|
+
filePath: "/tmp/test-runner.md",
|
|
453
|
+
completionGuard: true,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const serialized = serializeAgent(agent);
|
|
457
|
+
assert.doesNotMatch(serialized, /completionGuard:/);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("parses completionGuard from discovered agent frontmatter", () => {
|
|
461
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-completion-guard-"));
|
|
462
|
+
tempDirs.push(dir);
|
|
463
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
464
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
465
|
+
fs.writeFileSync(path.join(agentsDir, "test-runner.md"), `---
|
|
466
|
+
name: test-runner
|
|
467
|
+
description: Test runner
|
|
468
|
+
completionGuard: false
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
Validate changes
|
|
472
|
+
`, "utf-8");
|
|
473
|
+
|
|
474
|
+
const result = discoverAgents(dir, "project");
|
|
475
|
+
const runner = result.agents.find((agent) => agent.name === "test-runner");
|
|
476
|
+
assert.equal(runner?.completionGuard, false);
|
|
477
|
+
assert.equal(runner?.extraFields?.completionGuard, undefined);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe("agent frontmatter maxSubagentDepth", () => {
|
|
482
|
+
it("serializes maxSubagentDepth into agent frontmatter", () => {
|
|
483
|
+
const agent: AgentConfig = {
|
|
484
|
+
name: "scout",
|
|
485
|
+
description: "Scout",
|
|
486
|
+
systemPrompt: "Inspect code",
|
|
487
|
+
systemPromptMode: "replace",
|
|
488
|
+
inheritProjectContext: false,
|
|
489
|
+
inheritSkills: false,
|
|
490
|
+
source: "project",
|
|
491
|
+
filePath: "/tmp/scout.md",
|
|
492
|
+
maxSubagentDepth: 1,
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const serialized = serializeAgent(agent);
|
|
496
|
+
assert.match(serialized, /maxSubagentDepth: 1/);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("parses maxSubagentDepth from discovered agent frontmatter", () => {
|
|
500
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-frontmatter-"));
|
|
501
|
+
tempDirs.push(dir);
|
|
502
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
503
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
504
|
+
fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
|
|
505
|
+
name: scout
|
|
506
|
+
description: Scout
|
|
507
|
+
maxSubagentDepth: 1
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
Inspect code
|
|
511
|
+
`, "utf-8");
|
|
512
|
+
|
|
513
|
+
const result = discoverAgents(dir, "project");
|
|
514
|
+
const scout = result.agents.find((agent) => agent.name === "scout");
|
|
515
|
+
assert.equal(scout?.maxSubagentDepth, 1);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe("agent frontmatter fallbackModels", () => {
|
|
520
|
+
it("serializes fallbackModels into agent frontmatter", () => {
|
|
521
|
+
const agent: AgentConfig = {
|
|
522
|
+
name: "worker",
|
|
523
|
+
description: "Worker",
|
|
524
|
+
systemPrompt: "Do work",
|
|
525
|
+
systemPromptMode: "replace",
|
|
526
|
+
inheritProjectContext: false,
|
|
527
|
+
inheritSkills: false,
|
|
528
|
+
source: "project",
|
|
529
|
+
filePath: "/tmp/worker.md",
|
|
530
|
+
fallbackModels: ["openai/gpt-5-mini", "anthropic/claude-sonnet-4"],
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const serialized = serializeAgent(agent);
|
|
534
|
+
assert.match(serialized, /fallbackModels: openai\/gpt-5-mini, anthropic\/claude-sonnet-4/);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("parses fallbackModels from discovered agent frontmatter", () => {
|
|
538
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-fallback-frontmatter-"));
|
|
539
|
+
tempDirs.push(dir);
|
|
540
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
541
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
542
|
+
fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
|
|
543
|
+
name: worker
|
|
544
|
+
description: Worker
|
|
545
|
+
fallbackModels: openai/gpt-5-mini, anthropic/claude-sonnet-4
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
Do work
|
|
549
|
+
`, "utf-8");
|
|
550
|
+
|
|
551
|
+
const result = discoverAgents(dir, "project");
|
|
552
|
+
const worker = result.agents.find((agent) => agent.name === "worker");
|
|
553
|
+
assert.deepEqual(worker?.fallbackModels, ["openai/gpt-5-mini", "anthropic/claude-sonnet-4"]);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe("agent frontmatter systemPromptMode", () => {
|
|
558
|
+
it("serializes systemPromptMode into agent frontmatter", () => {
|
|
559
|
+
const agent: AgentConfig = {
|
|
560
|
+
name: "worker",
|
|
561
|
+
description: "Worker",
|
|
562
|
+
systemPrompt: "Do work",
|
|
563
|
+
systemPromptMode: "replace",
|
|
564
|
+
inheritProjectContext: false,
|
|
565
|
+
inheritSkills: false,
|
|
566
|
+
source: "project",
|
|
567
|
+
filePath: "/tmp/worker.md",
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const serialized = serializeAgent(agent);
|
|
571
|
+
assert.match(serialized, /systemPromptMode: replace/);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("parses systemPromptMode from discovered agent frontmatter", () => {
|
|
575
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-prompt-mode-frontmatter-"));
|
|
576
|
+
tempDirs.push(dir);
|
|
577
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
578
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
579
|
+
fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
|
|
580
|
+
name: worker
|
|
581
|
+
description: Worker
|
|
582
|
+
systemPromptMode: replace
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
Do work
|
|
586
|
+
`, "utf-8");
|
|
587
|
+
|
|
588
|
+
const result = discoverAgents(dir, "project");
|
|
589
|
+
const worker = result.agents.find((agent) => agent.name === "worker");
|
|
590
|
+
assert.equal(worker?.systemPromptMode, "replace");
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe("agent frontmatter prompt inheritance flags", () => {
|
|
595
|
+
it("serializes inheritProjectContext and inheritSkills into agent frontmatter", () => {
|
|
596
|
+
const agent: AgentConfig = {
|
|
597
|
+
name: "worker",
|
|
598
|
+
description: "Worker",
|
|
599
|
+
systemPrompt: "Do work",
|
|
600
|
+
systemPromptMode: "replace",
|
|
601
|
+
inheritProjectContext: true,
|
|
602
|
+
inheritSkills: true,
|
|
603
|
+
source: "project",
|
|
604
|
+
filePath: "/tmp/worker.md",
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const serialized = serializeAgent(agent);
|
|
608
|
+
assert.match(serialized, /inheritProjectContext: true/);
|
|
609
|
+
assert.match(serialized, /inheritSkills: true/);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("parses inheritProjectContext and inheritSkills from discovered agent frontmatter", () => {
|
|
613
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-prompt-inheritance-frontmatter-"));
|
|
614
|
+
tempDirs.push(dir);
|
|
615
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
616
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
617
|
+
fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
|
|
618
|
+
name: worker
|
|
619
|
+
description: Worker
|
|
620
|
+
inheritProjectContext: true
|
|
621
|
+
inheritSkills: true
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
Do work
|
|
625
|
+
`, "utf-8");
|
|
626
|
+
|
|
627
|
+
const result = discoverAgents(dir, "project");
|
|
628
|
+
const worker = result.agents.find((agent) => agent.name === "worker");
|
|
629
|
+
assert.equal(worker?.inheritProjectContext, true);
|
|
630
|
+
assert.equal(worker?.inheritSkills, true);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe("agent frontmatter subagentOnlyExtensions", () => {
|
|
635
|
+
it("serializes subagentOnlyExtensions into agent frontmatter", () => {
|
|
636
|
+
const agent: AgentConfig = {
|
|
637
|
+
name: "worker",
|
|
638
|
+
description: "Worker",
|
|
639
|
+
systemPrompt: "Do work",
|
|
640
|
+
systemPromptMode: "replace",
|
|
641
|
+
inheritProjectContext: false,
|
|
642
|
+
inheritSkills: false,
|
|
643
|
+
source: "project",
|
|
644
|
+
filePath: "/tmp/worker.md",
|
|
645
|
+
subagentOnlyExtensions: ["./tools/child-search.ts", "/opt/pi/child-only.ts"],
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const serialized = serializeAgent(agent);
|
|
649
|
+
assert.match(serialized, /subagentOnlyExtensions: \.\/tools\/child-search\.ts, \/opt\/pi\/child-only\.ts/);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("parses subagentOnlyExtensions from discovered agent frontmatter", () => {
|
|
653
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-child-ext-"));
|
|
654
|
+
tempDirs.push(dir);
|
|
655
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
656
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
657
|
+
fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
|
|
658
|
+
name: worker
|
|
659
|
+
description: Worker
|
|
660
|
+
subagentOnlyExtensions: ./tools/child-search.ts, /opt/pi/child-only.ts
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
Do work
|
|
664
|
+
`, "utf-8");
|
|
665
|
+
|
|
666
|
+
const result = discoverAgents(dir, "project");
|
|
667
|
+
const worker = result.agents.find((agent) => agent.name === "worker");
|
|
668
|
+
assert.deepEqual(worker?.subagentOnlyExtensions, ["./tools/child-search.ts", "/opt/pi/child-only.ts"]);
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
describe("agent frontmatter prompt assembly defaults", () => {
|
|
673
|
+
it("defaults ordinary agents to replace mode with no inherited context or skills", () => {
|
|
674
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-default-prompt-settings-"));
|
|
675
|
+
tempDirs.push(dir);
|
|
676
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
677
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
678
|
+
fs.writeFileSync(path.join(agentsDir, "worker.md"), `---
|
|
679
|
+
name: worker
|
|
680
|
+
description: Worker
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
Do work
|
|
684
|
+
`, "utf-8");
|
|
685
|
+
|
|
686
|
+
const result = discoverAgents(dir, "project");
|
|
687
|
+
const worker = result.agents.find((agent) => agent.name === "worker");
|
|
688
|
+
assert.equal(worker?.systemPromptMode, "replace");
|
|
689
|
+
assert.equal(worker?.inheritProjectContext, false);
|
|
690
|
+
assert.equal(worker?.inheritSkills, false);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("builtin agents inherit project context by default", () => {
|
|
694
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-default-prompt-settings-"));
|
|
695
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-default-home-"));
|
|
696
|
+
tempDirs.push(dir);
|
|
697
|
+
tempDirs.push(homeDir);
|
|
698
|
+
const previousHome = process.env.HOME;
|
|
699
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
process.env.HOME = homeDir;
|
|
703
|
+
process.env.USERPROFILE = homeDir;
|
|
704
|
+
|
|
705
|
+
const result = discoverAgents(dir, "both");
|
|
706
|
+
const scout = result.agents.find((agent) => agent.name === "scout");
|
|
707
|
+
const reviewer = result.agents.find((agent) => agent.name === "reviewer");
|
|
708
|
+
const delegate = result.agents.find((agent) => agent.name === "delegate");
|
|
709
|
+
assert.equal(scout?.inheritProjectContext, true);
|
|
710
|
+
assert.equal(reviewer?.inheritProjectContext, true);
|
|
711
|
+
assert.equal(delegate?.inheritProjectContext, true);
|
|
712
|
+
} finally {
|
|
713
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
714
|
+
else process.env.HOME = previousHome;
|
|
715
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
716
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("bundled agents all have explicit tool allowlists", () => {
|
|
721
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-tools-"));
|
|
722
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-tools-home-"));
|
|
723
|
+
tempDirs.push(dir);
|
|
724
|
+
tempDirs.push(homeDir);
|
|
725
|
+
const previousHome = process.env.HOME;
|
|
726
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
process.env.HOME = homeDir;
|
|
730
|
+
process.env.USERPROFILE = homeDir;
|
|
731
|
+
const builtins = discoverAgentsAll(dir).builtin;
|
|
732
|
+
assert.ok(builtins.length > 0);
|
|
733
|
+
for (const agent of builtins) {
|
|
734
|
+
assert.ok(agent.tools && agent.tools.length > 0, `${agent.name} should have explicit tools frontmatter`);
|
|
735
|
+
}
|
|
736
|
+
} finally {
|
|
737
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
738
|
+
else process.env.HOME = previousHome;
|
|
739
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
740
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("worker and delegate include the child-facing supervisor tool", () => {
|
|
745
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-supervisor-tool-"));
|
|
746
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-supervisor-tool-home-"));
|
|
747
|
+
tempDirs.push(dir);
|
|
748
|
+
tempDirs.push(homeDir);
|
|
749
|
+
const previousHome = process.env.HOME;
|
|
750
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
process.env.HOME = homeDir;
|
|
754
|
+
process.env.USERPROFILE = homeDir;
|
|
755
|
+
const agents = discoverAgentsAll(dir).builtin;
|
|
756
|
+
for (const name of ["worker", "delegate"]) {
|
|
757
|
+
const agent = agents.find((candidate) => candidate.name === name);
|
|
758
|
+
assert.ok(agent, `${name} builtin should be discovered`);
|
|
759
|
+
assert.deepEqual(agent?.tools, ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"]);
|
|
760
|
+
}
|
|
761
|
+
} finally {
|
|
762
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
763
|
+
else process.env.HOME = previousHome;
|
|
764
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
765
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("defaults delegate to append mode with inherited project context", () => {
|
|
770
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-delegate-default-prompt-settings-"));
|
|
771
|
+
tempDirs.push(dir);
|
|
772
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
773
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
774
|
+
fs.writeFileSync(path.join(agentsDir, "delegate.md"), `---
|
|
775
|
+
name: delegate
|
|
776
|
+
description: Delegate
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
Do work
|
|
780
|
+
`, "utf-8");
|
|
781
|
+
|
|
782
|
+
const result = discoverAgents(dir, "project");
|
|
783
|
+
const delegate = result.agents.find((agent) => agent.name === "delegate");
|
|
784
|
+
assert.equal(delegate?.systemPromptMode, "append");
|
|
785
|
+
assert.equal(delegate?.inheritProjectContext, true);
|
|
786
|
+
assert.equal(delegate?.inheritSkills, false);
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
describe("packaged agent and chain discovery", () => {
|
|
791
|
+
it("recursively discovers nested project agents while keeping chain files separate", () => {
|
|
792
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-recursive-agent-discovery-"));
|
|
793
|
+
tempDirs.push(dir);
|
|
794
|
+
const nestedDir = path.join(dir, ".selesai", "agents", "code-analysis", "deep");
|
|
795
|
+
const nestedChainDir = path.join(dir, ".selesai", "chains", "code-analysis", "deep");
|
|
796
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
797
|
+
fs.mkdirSync(nestedChainDir, { recursive: true });
|
|
798
|
+
fs.writeFileSync(path.join(nestedDir, "scout.md"), `---
|
|
799
|
+
name: scout
|
|
800
|
+
description: Nested scout
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
Inspect code
|
|
804
|
+
`, "utf-8");
|
|
805
|
+
fs.writeFileSync(path.join(nestedChainDir, "review.chain.md"), `---
|
|
806
|
+
name: review-flow
|
|
807
|
+
description: Review flow
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## scout
|
|
811
|
+
|
|
812
|
+
Review
|
|
813
|
+
`, "utf-8");
|
|
814
|
+
|
|
815
|
+
const result = discoverAgentsAll(dir);
|
|
816
|
+
assert.ok(result.project.find((agent) => agent.name === "scout" && agent.filePath === path.join(nestedDir, "scout.md")));
|
|
817
|
+
assert.ok(result.chains.find((chain) => chain.name === "review-flow" && chain.filePath === path.join(nestedChainDir, "review.chain.md")));
|
|
818
|
+
assert.equal(result.project.some((agent) => agent.filePath.endsWith("review.chain.md")), false);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("registers packaged agents by runtime name and serializes local name plus package", () => {
|
|
822
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-packaged-agent-"));
|
|
823
|
+
tempDirs.push(dir);
|
|
824
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
825
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
826
|
+
fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
|
|
827
|
+
name: scout
|
|
828
|
+
package: code-analysis
|
|
829
|
+
description: Fast recon
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
Inspect code
|
|
833
|
+
`, "utf-8");
|
|
834
|
+
|
|
835
|
+
const scout = discoverAgents(dir, "project").agents.find((agent) => agent.name === "code-analysis.scout");
|
|
836
|
+
assert.ok(scout);
|
|
837
|
+
assert.equal(scout.localName, "scout");
|
|
838
|
+
assert.equal(scout.packageName, "code-analysis");
|
|
839
|
+
const serialized = serializeAgent(scout);
|
|
840
|
+
assert.match(serialized, /^name: scout$/m);
|
|
841
|
+
assert.match(serialized, /^package: code-analysis$/m);
|
|
842
|
+
assert.doesNotMatch(serialized, /^name: code-analysis\.scout$/m);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("recursively discovers packaged chains by runtime name and preserves package on serialize", () => {
|
|
846
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-packaged-chain-"));
|
|
847
|
+
tempDirs.push(dir);
|
|
848
|
+
const nestedDir = path.join(dir, ".selesai", "chains", "flows");
|
|
849
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
850
|
+
const content = `---
|
|
851
|
+
name: review-flow
|
|
852
|
+
package: code-analysis
|
|
853
|
+
description: Review flow
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
## code-analysis.scout
|
|
857
|
+
|
|
858
|
+
Inspect {task}
|
|
859
|
+
`;
|
|
860
|
+
fs.writeFileSync(path.join(nestedDir, "review.chain.md"), content, "utf-8");
|
|
861
|
+
|
|
862
|
+
const chain = discoverAgentsAll(dir).chains.find((candidate) => candidate.name === "code-analysis.review-flow");
|
|
863
|
+
assert.ok(chain);
|
|
864
|
+
assert.equal(chain.localName, "review-flow");
|
|
865
|
+
assert.equal(chain.packageName, "code-analysis");
|
|
866
|
+
assert.equal(chain.steps[0]?.agent, "code-analysis.scout");
|
|
867
|
+
const serialized = serializeChain(chain);
|
|
868
|
+
assert.match(serialized, /^name: review-flow$/m);
|
|
869
|
+
assert.match(serialized, /^package: code-analysis$/m);
|
|
870
|
+
assert.match(serialized, /^## code-analysis\.scout$/m);
|
|
871
|
+
assert.doesNotMatch(serialized, /^name: code-analysis\.review-flow$/m);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("keeps packaged and un-packaged runtime names distinct while preserving un-packaged precedence", () => {
|
|
875
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-packaged-collisions-"));
|
|
876
|
+
tempDirs.push(dir);
|
|
877
|
+
fs.mkdirSync(path.join(dir, ".agents"), { recursive: true });
|
|
878
|
+
fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
|
|
879
|
+
fs.writeFileSync(path.join(dir, ".agents", "scout.md"), `---
|
|
880
|
+
name: scout
|
|
881
|
+
description: Legacy scout
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
Legacy
|
|
885
|
+
`, "utf-8");
|
|
886
|
+
fs.writeFileSync(path.join(dir, ".selesai", "agents", "scout.md"), `---
|
|
887
|
+
name: scout
|
|
888
|
+
description: Project scout
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
Project
|
|
892
|
+
`, "utf-8");
|
|
893
|
+
fs.writeFileSync(path.join(dir, ".selesai", "agents", "packaged.md"), `---
|
|
894
|
+
name: scout
|
|
895
|
+
package: code-analysis
|
|
896
|
+
description: Packaged scout
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
Packaged
|
|
900
|
+
`, "utf-8");
|
|
901
|
+
|
|
902
|
+
const agents = discoverAgents(dir, "project").agents;
|
|
903
|
+
const unqualified = agents.find((agent) => agent.name === "scout");
|
|
904
|
+
const packaged = agents.find((agent) => agent.name === "code-analysis.scout");
|
|
905
|
+
assert.equal(unqualified?.description, "Project scout");
|
|
906
|
+
assert.equal(unqualified?.filePath, path.join(dir, ".selesai", "agents", "scout.md"));
|
|
907
|
+
assert.equal(packaged?.description, "Packaged scout");
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it("parses packaged chains directly from serializer helpers", () => {
|
|
911
|
+
const parsed = parseChain(`---
|
|
912
|
+
name: review-flow
|
|
913
|
+
package: code-analysis
|
|
914
|
+
description: Review flow
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
## code-analysis.scout
|
|
918
|
+
|
|
919
|
+
Inspect
|
|
920
|
+
`, "project", "/tmp/review.chain.md");
|
|
921
|
+
|
|
922
|
+
assert.equal(parsed.name, "code-analysis.review-flow");
|
|
923
|
+
assert.equal(parsed.localName, "review-flow");
|
|
924
|
+
assert.equal(parsed.packageName, "code-analysis");
|
|
925
|
+
assert.match(serializeChain(parsed), /^name: review-flow$/m);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it("normalizes package frontmatter consistently for agents and chains", () => {
|
|
929
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-package-normalize-"));
|
|
930
|
+
tempDirs.push(dir);
|
|
931
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
932
|
+
const chainsDir = path.join(dir, ".selesai", "chains");
|
|
933
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
934
|
+
fs.mkdirSync(chainsDir, { recursive: true });
|
|
935
|
+
fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
|
|
936
|
+
name: scout
|
|
937
|
+
package: Code Analysis!
|
|
938
|
+
description: Fast recon
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
Inspect
|
|
942
|
+
`, "utf-8");
|
|
943
|
+
fs.writeFileSync(path.join(chainsDir, "review.chain.md"), `---
|
|
944
|
+
name: review-flow
|
|
945
|
+
package: Code Analysis!
|
|
946
|
+
description: Review flow
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
## code-analysis.scout
|
|
950
|
+
|
|
951
|
+
Review
|
|
952
|
+
`, "utf-8");
|
|
953
|
+
|
|
954
|
+
const result = discoverAgentsAll(dir);
|
|
955
|
+
assert.ok(result.project.find((agent) => agent.name === "code-analysis.scout"));
|
|
956
|
+
assert.ok(result.chains.find((chain) => chain.name === "code-analysis.review-flow"));
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it("skips invalid package frontmatter that cannot be normalized", () => {
|
|
960
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-invalid-package-"));
|
|
961
|
+
tempDirs.push(dir);
|
|
962
|
+
const agentsDir = path.join(dir, ".selesai", "agents");
|
|
963
|
+
const chainsDir = path.join(dir, ".selesai", "chains");
|
|
964
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
965
|
+
fs.mkdirSync(chainsDir, { recursive: true });
|
|
966
|
+
fs.writeFileSync(path.join(agentsDir, "scout.md"), `---
|
|
967
|
+
name: scout
|
|
968
|
+
package: !!!
|
|
969
|
+
description: Fast recon
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
Inspect
|
|
973
|
+
`, "utf-8");
|
|
974
|
+
fs.writeFileSync(path.join(chainsDir, "review.chain.md"), `---
|
|
975
|
+
name: review-flow
|
|
976
|
+
package: !!!
|
|
977
|
+
description: Review flow
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
## scout
|
|
981
|
+
|
|
982
|
+
Review
|
|
983
|
+
`, "utf-8");
|
|
984
|
+
|
|
985
|
+
const result = discoverAgentsAll(dir);
|
|
986
|
+
assert.equal(result.project.some((agent) => agent.filePath.endsWith("scout.md")), false);
|
|
987
|
+
assert.equal(result.chains.some((chain) => chain.filePath.endsWith("review.chain.md")), false);
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
describe("project agent directory discovery", () => {
|
|
992
|
+
it("discovers project agents from both .agents and .pi/agents", () => {
|
|
993
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-agent-dirs-"));
|
|
994
|
+
tempDirs.push(dir);
|
|
995
|
+
fs.mkdirSync(path.join(dir, ".agents", "skills"), { recursive: true });
|
|
996
|
+
fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
|
|
997
|
+
fs.writeFileSync(path.join(dir, ".agents", "legacy.md"), `---
|
|
998
|
+
name: legacy
|
|
999
|
+
description: Legacy
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
Legacy prompt
|
|
1003
|
+
`, "utf-8");
|
|
1004
|
+
fs.writeFileSync(path.join(dir, ".selesai", "agents", "canonical.md"), `---
|
|
1005
|
+
name: canonical
|
|
1006
|
+
description: Canonical
|
|
1007
|
+
---
|
|
1008
|
+
|
|
1009
|
+
Canonical prompt
|
|
1010
|
+
`, "utf-8");
|
|
1011
|
+
fs.writeFileSync(path.join(dir, ".selesai", "agents", "SKILL.md"), `---
|
|
1012
|
+
name: skill-named-agent
|
|
1013
|
+
description: Skill-named agent
|
|
1014
|
+
---
|
|
1015
|
+
|
|
1016
|
+
Skill-named agent prompt
|
|
1017
|
+
`, "utf-8");
|
|
1018
|
+
|
|
1019
|
+
const result = discoverAgents(dir, "project");
|
|
1020
|
+
assert.ok(result.agents.find((agent) => agent.name === "legacy" && agent.filePath === path.join(dir, ".agents", "legacy.md")));
|
|
1021
|
+
assert.ok(result.agents.find((agent) => agent.name === "canonical" && agent.filePath === path.join(dir, ".selesai", "agents", "canonical.md")));
|
|
1022
|
+
assert.ok(result.agents.find((agent) => agent.name === "skill-named-agent" && agent.filePath === path.join(dir, ".selesai", "agents", "SKILL.md")));
|
|
1023
|
+
assert.equal(result.projectAgentsDir, path.join(dir, ".selesai", "agents"));
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it("does not register legacy project skill files as agents", () => {
|
|
1027
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-skills-not-agents-"));
|
|
1028
|
+
tempDirs.push(dir);
|
|
1029
|
+
writeAgent(path.join(dir, ".agents", "legacy.md"), `---
|
|
1030
|
+
name: legacy
|
|
1031
|
+
description: Legacy
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
Legacy prompt
|
|
1035
|
+
`);
|
|
1036
|
+
writeAgent(path.join(dir, ".agents", "skills", "directory-skill", "SKILL.md"), `---
|
|
1037
|
+
name: directory-skill
|
|
1038
|
+
description: Directory skill
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1041
|
+
Skill prompt
|
|
1042
|
+
`);
|
|
1043
|
+
writeAgent(path.join(dir, ".agents", "skills", "file-skill.md"), `---
|
|
1044
|
+
name: file-skill
|
|
1045
|
+
description: File skill
|
|
1046
|
+
---
|
|
1047
|
+
|
|
1048
|
+
Skill prompt
|
|
1049
|
+
`);
|
|
1050
|
+
|
|
1051
|
+
const agents = discoverAgents(dir, "project").agents;
|
|
1052
|
+
assert.ok(agents.find((agent) => agent.name === "legacy"));
|
|
1053
|
+
assert.equal(agents.some((agent) => agent.filePath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)), false);
|
|
1054
|
+
assert.equal(agents.some((agent) => agent.name === "directory-skill"), false);
|
|
1055
|
+
assert.equal(agents.some((agent) => agent.name === "file-skill"), false);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it("does not register user SKILL.md files as agents", () => withTempHome((home) => {
|
|
1059
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-user-skills-not-agents-"));
|
|
1060
|
+
tempDirs.push(dir);
|
|
1061
|
+
writeAgent(path.join(home, ".agents", "user-agent.md"), `---
|
|
1062
|
+
name: user-agent
|
|
1063
|
+
description: User agent
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
User prompt
|
|
1067
|
+
`);
|
|
1068
|
+
writeAgent(path.join(home, ".agents", "skills", "user-skill", "SKILL.md"), `---
|
|
1069
|
+
name: user-skill
|
|
1070
|
+
description: User skill
|
|
1071
|
+
---
|
|
1072
|
+
|
|
1073
|
+
Skill prompt
|
|
1074
|
+
`);
|
|
1075
|
+
|
|
1076
|
+
const agents = discoverAgents(dir, "user").agents;
|
|
1077
|
+
assert.ok(agents.find((agent) => agent.name === "user-agent"));
|
|
1078
|
+
assert.equal(agents.some((agent) => agent.filePath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)), false);
|
|
1079
|
+
assert.equal(agents.some((agent) => agent.name === "user-skill"), false);
|
|
1080
|
+
}));
|
|
1081
|
+
|
|
1082
|
+
it("prefers .pi/agents over .agents on project agent name collisions", () => {
|
|
1083
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-agent-collision-"));
|
|
1084
|
+
tempDirs.push(dir);
|
|
1085
|
+
fs.mkdirSync(path.join(dir, ".agents"), { recursive: true });
|
|
1086
|
+
fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
|
|
1087
|
+
fs.writeFileSync(path.join(dir, ".agents", "shared.md"), `---
|
|
1088
|
+
name: shared
|
|
1089
|
+
description: Legacy shared
|
|
1090
|
+
---
|
|
1091
|
+
|
|
1092
|
+
Legacy prompt
|
|
1093
|
+
`, "utf-8");
|
|
1094
|
+
fs.writeFileSync(path.join(dir, ".selesai", "agents", "shared.md"), `---
|
|
1095
|
+
name: shared
|
|
1096
|
+
description: Canonical shared
|
|
1097
|
+
---
|
|
1098
|
+
|
|
1099
|
+
Canonical prompt
|
|
1100
|
+
`, "utf-8");
|
|
1101
|
+
|
|
1102
|
+
const shared = discoverAgents(dir, "project").agents.find((agent) => agent.name === "shared");
|
|
1103
|
+
assert.ok(shared);
|
|
1104
|
+
assert.equal(shared.filePath, path.join(dir, ".selesai", "agents", "shared.md"));
|
|
1105
|
+
assert.equal(shared.description, "Canonical shared");
|
|
1106
|
+
assert.equal(shared.systemPrompt.trim(), "Canonical prompt");
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it("uses the project root for the canonical project agent dir even when only .agents exists", () => {
|
|
1110
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-agent-root-"));
|
|
1111
|
+
tempDirs.push(dir);
|
|
1112
|
+
const nested = path.join(dir, "packages", "app");
|
|
1113
|
+
fs.mkdirSync(path.join(dir, ".agents", "skills"), { recursive: true });
|
|
1114
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
1115
|
+
|
|
1116
|
+
const result = discoverAgentsAll(nested);
|
|
1117
|
+
assert.equal(result.projectDir, path.join(dir, ".selesai", "agents"));
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it("discovers project chains from .pi/chains", () => {
|
|
1121
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-chain-dirs-"));
|
|
1122
|
+
tempDirs.push(dir);
|
|
1123
|
+
fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
|
|
1124
|
+
fs.mkdirSync(path.join(dir, ".selesai", "chains", "flows"), { recursive: true });
|
|
1125
|
+
fs.writeFileSync(path.join(dir, ".selesai", "agents", "ignored.chain.md"), `---
|
|
1126
|
+
name: ignored-chain
|
|
1127
|
+
description: Ignored chain
|
|
1128
|
+
---
|
|
1129
|
+
|
|
1130
|
+
## scout
|
|
1131
|
+
|
|
1132
|
+
Ignore
|
|
1133
|
+
`, "utf-8");
|
|
1134
|
+
fs.writeFileSync(path.join(dir, ".selesai", "chains", "flows", "canonical.chain.md"), `---
|
|
1135
|
+
name: canonical-chain
|
|
1136
|
+
description: Canonical chain
|
|
1137
|
+
---
|
|
1138
|
+
|
|
1139
|
+
## worker
|
|
1140
|
+
|
|
1141
|
+
Inspect canonical
|
|
1142
|
+
`, "utf-8");
|
|
1143
|
+
|
|
1144
|
+
const result = discoverAgentsAll(dir);
|
|
1145
|
+
assert.equal(result.chains.some((chain) => chain.name === "ignored-chain"), false);
|
|
1146
|
+
assert.ok(result.chains.find((chain) => chain.name === "canonical-chain" && chain.filePath === path.join(dir, ".selesai", "chains", "flows", "canonical.chain.md")));
|
|
1147
|
+
assert.equal(result.projectDir, path.join(dir, ".selesai", "agents"));
|
|
1148
|
+
assert.equal(result.projectChainDir, path.join(dir, ".selesai", "chains"));
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it("prefers project .pi/chains over user chains on name collisions", () => {
|
|
1152
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-chain-collision-"));
|
|
1153
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-user-chain-home-"));
|
|
1154
|
+
tempDirs.push(dir, home);
|
|
1155
|
+
const oldHome = process.env.HOME;
|
|
1156
|
+
const oldUserProfile = process.env.USERPROFILE;
|
|
1157
|
+
process.env.HOME = home;
|
|
1158
|
+
process.env.USERPROFILE = home;
|
|
1159
|
+
try {
|
|
1160
|
+
const userChainsDir = path.join(home, ".selesai", "agent", "chains");
|
|
1161
|
+
fs.mkdirSync(userChainsDir, { recursive: true });
|
|
1162
|
+
fs.mkdirSync(path.join(dir, ".selesai", "chains"), { recursive: true });
|
|
1163
|
+
fs.writeFileSync(path.join(userChainsDir, "shared.chain.md"), `---
|
|
1164
|
+
name: shared-chain
|
|
1165
|
+
description: User chain
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
## scout
|
|
1169
|
+
|
|
1170
|
+
Inspect user
|
|
1171
|
+
`, "utf-8");
|
|
1172
|
+
fs.writeFileSync(path.join(dir, ".selesai", "chains", "shared.chain.md"), `---
|
|
1173
|
+
name: shared-chain
|
|
1174
|
+
description: Project chain
|
|
1175
|
+
---
|
|
1176
|
+
|
|
1177
|
+
## worker
|
|
1178
|
+
|
|
1179
|
+
Inspect project
|
|
1180
|
+
`, "utf-8");
|
|
1181
|
+
|
|
1182
|
+
const sharedChains = discoverAgentsAll(dir).chains.filter((chain) => chain.name === "shared-chain");
|
|
1183
|
+
assert.equal(sharedChains.length, 2);
|
|
1184
|
+
assert.deepEqual(sharedChains.map((chain) => chain.source), ["user", "project"]);
|
|
1185
|
+
const savedChainLookup = new Map(sharedChains.map((chain) => [chain.name, chain]));
|
|
1186
|
+
const shared = savedChainLookup.get("shared-chain");
|
|
1187
|
+
assert.ok(shared);
|
|
1188
|
+
assert.equal(shared.filePath, path.join(dir, ".selesai", "chains", "shared.chain.md"));
|
|
1189
|
+
assert.equal(shared.description, "Project chain");
|
|
1190
|
+
assert.equal(shared.steps[0]?.agent, "worker");
|
|
1191
|
+
assert.equal(shared.steps[0]?.task, "Inspect project");
|
|
1192
|
+
} finally {
|
|
1193
|
+
if (oldHome === undefined) delete process.env.HOME;
|
|
1194
|
+
else process.env.HOME = oldHome;
|
|
1195
|
+
if (oldUserProfile === undefined) delete process.env.USERPROFILE;
|
|
1196
|
+
else process.env.USERPROFILE = oldUserProfile;
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
});
|