@selesai/code 0.3.0 → 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/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/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/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/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/history.ts +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +9 -16
- package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
- 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/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 +100 -15
- 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/src/changelog-notice.ts +5 -2
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
- 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/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/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 +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
captureSingleOutputSnapshot,
|
|
8
|
+
finalizeSingleOutput,
|
|
9
|
+
formatSavedOutputReference,
|
|
10
|
+
injectOutputPathSystemPrompt,
|
|
11
|
+
injectSingleOutputInstruction,
|
|
12
|
+
normalizeSingleOutputOverride,
|
|
13
|
+
resolveSingleOutput,
|
|
14
|
+
resolveSingleOutputPath,
|
|
15
|
+
validateFileOnlyOutputMode,
|
|
16
|
+
} from "../../src/runs/shared/single-output.ts";
|
|
17
|
+
|
|
18
|
+
const tempDirs: string[] = [];
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
while (tempDirs.length > 0) {
|
|
22
|
+
const dir = tempDirs.pop();
|
|
23
|
+
if (!dir) continue;
|
|
24
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("normalizeSingleOutputOverride", () => {
|
|
29
|
+
it("treats boolean and string false as disabled output", () => {
|
|
30
|
+
assert.equal(normalizeSingleOutputOverride(false, "default.md"), false);
|
|
31
|
+
assert.equal(normalizeSingleOutputOverride("false", "default.md"), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("treats boolean and string true as the configured default output", () => {
|
|
35
|
+
assert.equal(normalizeSingleOutputOverride(true, "default.md"), "default.md");
|
|
36
|
+
assert.equal(normalizeSingleOutputOverride("true", "default.md"), "default.md");
|
|
37
|
+
assert.equal(normalizeSingleOutputOverride("true", undefined), undefined);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("passes explicit non-empty output paths through", () => {
|
|
41
|
+
assert.equal(normalizeSingleOutputOverride("reports/out.md", "default.md"), "reports/out.md");
|
|
42
|
+
assert.equal(normalizeSingleOutputOverride("", "default.md"), undefined);
|
|
43
|
+
assert.equal(normalizeSingleOutputOverride(undefined, "default.md"), undefined);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("resolveSingleOutputPath", () => {
|
|
48
|
+
it("does not resolve disabled or boolean-like output values", () => {
|
|
49
|
+
assert.equal(resolveSingleOutputPath(false, "/repo"), undefined);
|
|
50
|
+
assert.equal(resolveSingleOutputPath("false", "/repo"), undefined);
|
|
51
|
+
assert.equal(resolveSingleOutputPath(true, "/repo"), undefined);
|
|
52
|
+
assert.equal(resolveSingleOutputPath("true", "/repo"), undefined);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("keeps absolute paths unchanged", () => {
|
|
56
|
+
const absolutePath = path.join(os.tmpdir(), "pi-subagents-abs", "report.md");
|
|
57
|
+
const resolved = resolveSingleOutputPath(absolutePath, "/repo", "/override");
|
|
58
|
+
assert.equal(resolved, absolutePath);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("resolves relative paths against requested cwd", () => {
|
|
62
|
+
const resolved = resolveSingleOutputPath("reviews/report.md", "/runtime", "/requested");
|
|
63
|
+
assert.equal(resolved, path.resolve("/requested", "reviews/report.md"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("resolves relative paths against runtime cwd when requested cwd is absent", () => {
|
|
67
|
+
const resolved = resolveSingleOutputPath("reviews/report.md", "/runtime");
|
|
68
|
+
assert.equal(resolved, path.resolve("/runtime", "reviews/report.md"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("resolves relative requested cwd from runtime cwd before resolving output", () => {
|
|
72
|
+
const resolved = resolveSingleOutputPath("reviews/report.md", "/runtime", "nested/work");
|
|
73
|
+
assert.equal(resolved, path.resolve("/runtime", "nested/work", "reviews/report.md"));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("resolves relative output paths against an explicit artifact base", () => {
|
|
77
|
+
const resolved = resolveSingleOutputPath("reviews/report.md", "/runtime", "/requested", "/repo/.pi-subagents/artifacts/outputs/run-1");
|
|
78
|
+
assert.equal(resolved, path.resolve("/repo/.pi-subagents/artifacts/outputs/run-1", "reviews/report.md"));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("injectSingleOutputInstruction", () => {
|
|
83
|
+
it("appends output instruction with resolved path", () => {
|
|
84
|
+
const output = injectSingleOutputInstruction("Analyze this", "/tmp/report.md");
|
|
85
|
+
assert.match(output, /Write your findings to exactly this path: \/tmp\/report.md/);
|
|
86
|
+
assert.match(output, /This path is authoritative for this run\./);
|
|
87
|
+
assert.match(output, /Ignore any other output filename or output path mentioned elsewhere/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("injectOutputPathSystemPrompt", () => {
|
|
92
|
+
it("adds the authoritative runtime output path to the system prompt", () => {
|
|
93
|
+
const output = injectOutputPathSystemPrompt("Output format (`old.md`):", "/tmp/new.md");
|
|
94
|
+
assert.match(output, /^Output format \(`old\.md`\):/);
|
|
95
|
+
assert.match(output, /Runtime output path override:/);
|
|
96
|
+
assert.match(output, /Write your findings to exactly this path: \/tmp\/new\.md/);
|
|
97
|
+
assert.match(output, /Ignore any other output filename or output path mentioned elsewhere/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("leaves prompts unchanged when no output path is active", () => {
|
|
101
|
+
assert.equal(injectOutputPathSystemPrompt("Base prompt", undefined), "Base prompt");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("resolveSingleOutput", () => {
|
|
106
|
+
it("keeps agent-written file content when the file changed during the run", () => {
|
|
107
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-output-test-"));
|
|
108
|
+
tempDirs.push(dir);
|
|
109
|
+
const outputPath = path.join(dir, "review.md");
|
|
110
|
+
const before = captureSingleOutputSnapshot(outputPath);
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(outputPath, "real file content", "utf-8");
|
|
113
|
+
|
|
114
|
+
const result = resolveSingleOutput(outputPath, "receipt text", before);
|
|
115
|
+
assert.equal(result.fullOutput, "real file content");
|
|
116
|
+
assert.equal(result.savedPath, outputPath);
|
|
117
|
+
assert.equal(fs.readFileSync(outputPath, "utf-8"), "real file content");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("falls back to persisting the assistant output when the file was not changed", () => {
|
|
121
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-output-test-"));
|
|
122
|
+
tempDirs.push(dir);
|
|
123
|
+
const outputPath = path.join(dir, "review.md");
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(outputPath, "stale content", "utf-8");
|
|
126
|
+
const before = captureSingleOutputSnapshot(outputPath);
|
|
127
|
+
const result = resolveSingleOutput(outputPath, "fresh assistant output", before);
|
|
128
|
+
|
|
129
|
+
assert.equal(result.fullOutput, "fresh assistant output");
|
|
130
|
+
assert.equal(result.savedPath, outputPath);
|
|
131
|
+
assert.equal(fs.readFileSync(outputPath, "utf-8"), "fresh assistant output");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("preserves read errors from changed output paths", () => {
|
|
135
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-output-test-"));
|
|
136
|
+
tempDirs.push(dir);
|
|
137
|
+
const outputPath = path.join(dir, "review.md");
|
|
138
|
+
const before = captureSingleOutputSnapshot(outputPath);
|
|
139
|
+
|
|
140
|
+
fs.mkdirSync(outputPath);
|
|
141
|
+
const result = resolveSingleOutput(outputPath, "fallback output", before);
|
|
142
|
+
|
|
143
|
+
assert.equal(result.fullOutput, "fallback output");
|
|
144
|
+
assert.equal(result.savedPath, undefined);
|
|
145
|
+
assert.match(result.saveError ?? "", /Failed to read changed output file/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("formatSavedOutputReference", () => {
|
|
150
|
+
it("includes absolute path, human-readable size, and line count", () => {
|
|
151
|
+
const reportPath = path.join(os.tmpdir(), "report.md");
|
|
152
|
+
const ref = formatSavedOutputReference(reportPath, "line 1\nline 2");
|
|
153
|
+
assert.equal(ref.path, path.resolve(reportPath));
|
|
154
|
+
assert.equal(ref.bytes, Buffer.byteLength("line 1\nline 2", "utf-8"));
|
|
155
|
+
assert.equal(ref.lines, 2);
|
|
156
|
+
assert.equal(ref.message, `Output saved to: ${ref.path} (13 B, 2 lines). Read this file if needed.`);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("formats larger byte sizes in KB", () => {
|
|
160
|
+
const ref = formatSavedOutputReference("/tmp/large.md", "a".repeat(49_357));
|
|
161
|
+
assert.match(ref.message, /\(48\.2 KB, 1 line\)/);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("validateFileOnlyOutputMode", () => {
|
|
166
|
+
it("requires an output path for file-only mode", () => {
|
|
167
|
+
assert.match(validateFileOnlyOutputMode("file-only", undefined, "Single run") ?? "", /Single run sets outputMode: "file-only"/);
|
|
168
|
+
assert.equal(validateFileOnlyOutputMode("file-only", "/tmp/report.md", "Single run"), undefined);
|
|
169
|
+
assert.equal(validateFileOnlyOutputMode("inline", undefined, "Single run"), undefined);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("finalizeSingleOutput", () => {
|
|
174
|
+
it("formats saved-path messaging around the already-resolved output", () => {
|
|
175
|
+
const result = finalizeSingleOutput({
|
|
176
|
+
fullOutput: "line 1\nline 2\nline 3",
|
|
177
|
+
truncatedOutput: "[TRUNCATED]\nline 1",
|
|
178
|
+
outputPath: "/tmp/review.md",
|
|
179
|
+
savedPath: "/tmp/review.md",
|
|
180
|
+
exitCode: 0,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
assert.match(result.displayOutput, /^\[TRUNCATED\]\nline 1/);
|
|
184
|
+
assert.match(result.displayOutput, /Output saved to:/);
|
|
185
|
+
assert.match(result.displayOutput, /3 lines/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("returns only the saved-output reference in file-only mode", () => {
|
|
189
|
+
const result = finalizeSingleOutput({
|
|
190
|
+
fullOutput: "line 1\nline 2\nline 3",
|
|
191
|
+
outputPath: "/tmp/review.md",
|
|
192
|
+
savedPath: "/tmp/review.md",
|
|
193
|
+
outputMode: "file-only",
|
|
194
|
+
exitCode: 0,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
assert.doesNotMatch(result.displayOutput, /line 1/);
|
|
198
|
+
assert.match(result.displayOutput, /^Output saved to:/);
|
|
199
|
+
assert.match(result.displayOutput, /3 lines/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("does not add save messaging on failed runs", () => {
|
|
203
|
+
const result = finalizeSingleOutput({
|
|
204
|
+
fullOutput: "full output",
|
|
205
|
+
truncatedOutput: "truncated output",
|
|
206
|
+
outputPath: "/tmp/review.md",
|
|
207
|
+
savedPath: "/tmp/review.md",
|
|
208
|
+
exitCode: 1,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.equal(result.displayOutput, "truncated output");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,438 @@
|
|
|
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 { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import {
|
|
8
|
+
buildSkillInjection,
|
|
9
|
+
clearSkillCache,
|
|
10
|
+
discoverAvailableSkills,
|
|
11
|
+
resolveSkills,
|
|
12
|
+
resolveSkillsWithFallback,
|
|
13
|
+
} from "../../src/agents/skills.ts";
|
|
14
|
+
|
|
15
|
+
let tempDir = "";
|
|
16
|
+
|
|
17
|
+
function writeSkillFile(skillDir: string, body: string, description = "Test description"): void {
|
|
18
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(skillDir, "SKILL.md"),
|
|
21
|
+
`---\ndescription: ${description}\n---\n\n${body}\n`,
|
|
22
|
+
"utf-8",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeProjectSkill(cwd: string, name: string, body: string, description = "Test description"): void {
|
|
27
|
+
const skillDir = path.join(cwd, ".selesai", "skills", name);
|
|
28
|
+
writeSkillFile(skillDir, body, description);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeProjectPackageSkill(cwd: string, packageName: string, name: string, body: string): void {
|
|
32
|
+
const packageRoot = path.join(cwd, ".selesai", "npm", "node_modules", packageName);
|
|
33
|
+
makePackageSkill(packageRoot, name, body, packageName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makePackageSkill(packageRoot: string, name: string, body: string, packageName = `${name}-pkg`): void {
|
|
37
|
+
const skillDir = path.join(packageRoot, "skills", name);
|
|
38
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.join(packageRoot, "package.json"),
|
|
41
|
+
JSON.stringify({ name: packageName, version: "1.0.0", pi: { skills: ["./skills"] } }, null, 2),
|
|
42
|
+
"utf-8",
|
|
43
|
+
);
|
|
44
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `${body}\n`, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function importSkillsFresh() {
|
|
48
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
49
|
+
const modulePath = path.resolve(projectRoot, "src/agents/skills.ts");
|
|
50
|
+
const bust = `${Date.now()}-${Math.random()}`;
|
|
51
|
+
return await import(`${pathToFileURL(modulePath).href}?bust=${bust}`) as typeof import("../../src/agents/skills.ts");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("skills filesystem fallback", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-skills-fallback-"));
|
|
57
|
+
clearSkillCache();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
clearSkillCache();
|
|
62
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("discovers project skills from filesystem paths", () => {
|
|
66
|
+
makeProjectSkill(tempDir, "fallback-skill", "Use fallback mode.");
|
|
67
|
+
|
|
68
|
+
const skills = discoverAvailableSkills(tempDir);
|
|
69
|
+
const discovered = skills.find((skill) => skill.name === "fallback-skill");
|
|
70
|
+
assert.ok(discovered, "expected fallback-skill to be discovered");
|
|
71
|
+
assert.equal(discovered?.source, "project");
|
|
72
|
+
assert.equal(discovered?.description, "Test description");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("discovers project skills nested below grouping directories", () => {
|
|
76
|
+
writeSkillFile(
|
|
77
|
+
path.join(tempDir, ".selesai", "skills", "shell", "issue-262-nested-skill"),
|
|
78
|
+
"Use nested project skill.",
|
|
79
|
+
"Nested issue 262 skill",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const skills = discoverAvailableSkills(tempDir);
|
|
83
|
+
const discovered = skills.find((skill) => skill.name === "issue-262-nested-skill");
|
|
84
|
+
assert.ok(discovered, "expected grouped nested skill to be discovered");
|
|
85
|
+
assert.equal(discovered?.source, "project");
|
|
86
|
+
assert.equal(discovered?.description, "Nested issue 262 skill");
|
|
87
|
+
|
|
88
|
+
const { resolved, missing } = resolveSkills(["issue-262-nested-skill"], tempDir);
|
|
89
|
+
assert.deepEqual(missing, []);
|
|
90
|
+
assert.equal(resolved.length, 1);
|
|
91
|
+
assert.match(resolved[0]?.content ?? "", /Use nested project skill\./);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("stops recursive project skill discovery at the first SKILL.md anchor", () => {
|
|
95
|
+
const groupedRoot = path.join(tempDir, ".selesai", "skills", "group");
|
|
96
|
+
writeSkillFile(path.join(groupedRoot, "issue-262-anchor"), "Use anchor skill.");
|
|
97
|
+
writeSkillFile(path.join(groupedRoot, "issue-262-anchor", "nested", "issue-262-leaked-skill"), "Should not leak.");
|
|
98
|
+
writeSkillFile(path.join(groupedRoot, "issue-262-sibling"), "Use sibling skill.");
|
|
99
|
+
|
|
100
|
+
const names = discoverAvailableSkills(tempDir).map((skill) => skill.name);
|
|
101
|
+
assert.equal(names.includes("issue-262-anchor"), true);
|
|
102
|
+
assert.equal(names.includes("issue-262-sibling"), true);
|
|
103
|
+
assert.equal(names.includes("issue-262-leaked-skill"), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("skips hidden directories and node_modules while recursing for project skills", () => {
|
|
107
|
+
const groupedRoot = path.join(tempDir, ".selesai", "skills", "group");
|
|
108
|
+
writeSkillFile(path.join(groupedRoot, ".hidden", "issue-262-hidden-skill"), "Should stay hidden.");
|
|
109
|
+
writeSkillFile(path.join(groupedRoot, "node_modules", "issue-262-node-skill"), "Should stay ignored.");
|
|
110
|
+
writeSkillFile(path.join(groupedRoot, "visible", "issue-262-visible-skill"), "Use visible nested skill.");
|
|
111
|
+
|
|
112
|
+
const names = discoverAvailableSkills(tempDir).map((skill) => skill.name);
|
|
113
|
+
assert.equal(names.includes("issue-262-visible-skill"), true);
|
|
114
|
+
assert.equal(names.includes("issue-262-hidden-skill"), false);
|
|
115
|
+
assert.equal(names.includes("issue-262-node-skill"), false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("keeps direct markdown skills from explicit settings roots after parent recursion", () => {
|
|
119
|
+
const groupedRoot = path.join(tempDir, ".selesai", "skills", "group");
|
|
120
|
+
fs.mkdirSync(groupedRoot, { recursive: true });
|
|
121
|
+
fs.writeFileSync(path.join(groupedRoot, "issue-262-direct.md"), "Use direct markdown skill.\n", "utf-8");
|
|
122
|
+
fs.writeFileSync(
|
|
123
|
+
path.join(tempDir, ".selesai", "settings.json"),
|
|
124
|
+
JSON.stringify({ skills: ["./skills/group"] }, null, 2),
|
|
125
|
+
"utf-8",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const { resolved, missing } = resolveSkills(["issue-262-direct"], tempDir);
|
|
129
|
+
assert.deepEqual(missing, []);
|
|
130
|
+
assert.equal(resolved.length, 1);
|
|
131
|
+
assert.equal(resolved[0]?.source, "project-settings");
|
|
132
|
+
assert.match(resolved[0]?.content ?? "", /Use direct markdown skill\./);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("keeps nested skills from higher-priority explicit settings roots after parent recursion", () => {
|
|
136
|
+
writeSkillFile(
|
|
137
|
+
path.join(tempDir, "skills", "group", "issue-262-settings-nested"),
|
|
138
|
+
"Use settings nested skill.",
|
|
139
|
+
);
|
|
140
|
+
fs.writeFileSync(
|
|
141
|
+
path.join(tempDir, "package.json"),
|
|
142
|
+
JSON.stringify({ name: "fixture", version: "1.0.0", pi: { skills: ["./skills"] } }, null, 2),
|
|
143
|
+
"utf-8",
|
|
144
|
+
);
|
|
145
|
+
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
path.join(tempDir, ".selesai", "settings.json"),
|
|
148
|
+
JSON.stringify({ skills: ["../skills/group"] }, null, 2),
|
|
149
|
+
"utf-8",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const { resolved, missing } = resolveSkills(["issue-262-settings-nested"], tempDir);
|
|
153
|
+
assert.deepEqual(missing, []);
|
|
154
|
+
assert.equal(resolved.length, 1);
|
|
155
|
+
assert.equal(resolved[0]?.source, "project-settings");
|
|
156
|
+
assert.match(resolved[0]?.content ?? "", /Use settings nested skill\./);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("keeps nested skills from higher-priority explicit settings roots when the root path is duplicated", () => {
|
|
160
|
+
writeSkillFile(
|
|
161
|
+
path.join(tempDir, "skills", "group", "issue-262-settings-same-root"),
|
|
162
|
+
"Use settings same root skill.",
|
|
163
|
+
);
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
path.join(tempDir, "package.json"),
|
|
166
|
+
JSON.stringify({ name: "fixture", version: "1.0.0", pi: { skills: ["./skills"] } }, null, 2),
|
|
167
|
+
"utf-8",
|
|
168
|
+
);
|
|
169
|
+
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
path.join(tempDir, ".selesai", "settings.json"),
|
|
172
|
+
JSON.stringify({ skills: ["../skills"] }, null, 2),
|
|
173
|
+
"utf-8",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const { resolved, missing } = resolveSkills(["issue-262-settings-same-root"], tempDir);
|
|
177
|
+
assert.deepEqual(missing, []);
|
|
178
|
+
assert.equal(resolved.length, 1);
|
|
179
|
+
assert.equal(resolved[0]?.source, "project-settings");
|
|
180
|
+
assert.match(resolved[0]?.content ?? "", /Use settings same root skill\./);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("resolves and reads skill content via filesystem fallback", () => {
|
|
184
|
+
makeProjectSkill(tempDir, "resolve-skill", "Run local fallback checks.");
|
|
185
|
+
|
|
186
|
+
const { resolved, missing } = resolveSkills(["resolve-skill"], tempDir);
|
|
187
|
+
assert.deepEqual(missing, []);
|
|
188
|
+
assert.equal(resolved.length, 1);
|
|
189
|
+
assert.equal(resolved[0]?.name, "resolve-skill");
|
|
190
|
+
assert.equal(resolved[0]?.source, "project");
|
|
191
|
+
assert.match(resolved[0]?.content ?? "", /Run local fallback checks\./);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("builds lazy skill references instead of inlining full skill bodies", () => {
|
|
195
|
+
makeProjectSkill(tempDir, "lazy-skill", "This body should stay out of the system prompt.");
|
|
196
|
+
|
|
197
|
+
const { resolved, missing } = resolveSkills(["lazy-skill"], tempDir);
|
|
198
|
+
assert.deepEqual(missing, []);
|
|
199
|
+
|
|
200
|
+
const injection = buildSkillInjection(resolved);
|
|
201
|
+
assert.match(injection, /The following configured skills are available to this subagent/);
|
|
202
|
+
assert.match(injection, /Use the read tool to load a skill's file/);
|
|
203
|
+
assert.match(injection, /<available_skills>/);
|
|
204
|
+
assert.match(injection, /<name>lazy-skill<\/name>/);
|
|
205
|
+
assert.match(injection, /<description>Test description<\/description>/);
|
|
206
|
+
assert.match(injection, /<location>.*lazy-skill.*SKILL\.md<\/location>/);
|
|
207
|
+
assert.doesNotMatch(injection, /This body should stay out/);
|
|
208
|
+
assert.doesNotMatch(injection, /<skill name=/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("escapes XML-sensitive skill metadata in lazy references", () => {
|
|
212
|
+
makeProjectSkill(tempDir, "amp&skill", "Body", "Use A & B <carefully>");
|
|
213
|
+
|
|
214
|
+
const { resolved } = resolveSkills(["amp&skill"], tempDir);
|
|
215
|
+
const injection = buildSkillInjection(resolved);
|
|
216
|
+
assert.match(injection, /<name>amp&skill<\/name>/);
|
|
217
|
+
assert.match(injection, /<description>Use A & B <carefully><\/description>/);
|
|
218
|
+
assert.match(injection, /amp&skill[\\/]SKILL\.md/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("does not expose pi-subagents as a child-injectable skill", () => {
|
|
222
|
+
makeProjectSkill(tempDir, "pi-subagents", "Parent orchestration only.");
|
|
223
|
+
makeProjectSkill(tempDir, "safe-bash", "Use safe bash.");
|
|
224
|
+
|
|
225
|
+
const available = discoverAvailableSkills(tempDir).map((skill) => skill.name);
|
|
226
|
+
assert.equal(available.includes("pi-subagents"), false);
|
|
227
|
+
assert.equal(available.includes("safe-bash"), true);
|
|
228
|
+
|
|
229
|
+
const { resolved, missing } = resolveSkills(["pi-subagents", "safe-bash"], tempDir);
|
|
230
|
+
assert.deepEqual(missing, ["pi-subagents"]);
|
|
231
|
+
assert.deepEqual(resolved.map((skill) => skill.name), ["safe-bash"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("classifies package-provided skills as project-package", () => {
|
|
235
|
+
makeProjectPackageSkill(tempDir, "test-skill-package", "pkg-skill", "Use package skill.");
|
|
236
|
+
|
|
237
|
+
const skills = discoverAvailableSkills(tempDir);
|
|
238
|
+
const discovered = skills.find((skill) => skill.name === "pkg-skill");
|
|
239
|
+
assert.ok(discovered, "expected pkg-skill to be discovered");
|
|
240
|
+
assert.equal(discovered?.source, "project-package");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("prefers project skills over project-package skills with the same name", () => {
|
|
244
|
+
makeProjectPackageSkill(tempDir, "test-skill-package", "shared-skill", "Package version");
|
|
245
|
+
makeProjectSkill(tempDir, "shared-skill", "Project version");
|
|
246
|
+
|
|
247
|
+
const { resolved, missing } = resolveSkills(["shared-skill"], tempDir);
|
|
248
|
+
assert.deepEqual(missing, []);
|
|
249
|
+
assert.equal(resolved.length, 1);
|
|
250
|
+
assert.equal(resolved[0]?.source, "project");
|
|
251
|
+
assert.match(resolved[0]?.content ?? "", /Project version/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("discovers skills from project settings packages", () => {
|
|
255
|
+
const packageRoot = path.join(tempDir, ".selesai", "packages", "local-skill-pkg");
|
|
256
|
+
makePackageSkill(packageRoot, "settings-package-skill", "Settings package skill.");
|
|
257
|
+
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
|
|
258
|
+
fs.writeFileSync(
|
|
259
|
+
path.join(tempDir, ".selesai", "settings.json"),
|
|
260
|
+
JSON.stringify({ packages: ["./packages/local-skill-pkg"] }, null, 2),
|
|
261
|
+
"utf-8",
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const { resolved, missing } = resolveSkills(["settings-package-skill"], tempDir);
|
|
265
|
+
assert.deepEqual(missing, []);
|
|
266
|
+
assert.equal(resolved.length, 1);
|
|
267
|
+
assert.equal(resolved[0]?.source, "project-package");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("discovers skills from project settings npm package sources", () => {
|
|
271
|
+
const packageRoot = path.join(tempDir, ".selesai", "npm", "node_modules", "@scope", "skill-package");
|
|
272
|
+
makePackageSkill(
|
|
273
|
+
packageRoot,
|
|
274
|
+
"project-settings-scoped-npm-package-skill",
|
|
275
|
+
"Project settings scoped npm package skill.",
|
|
276
|
+
"@scope/skill-package",
|
|
277
|
+
);
|
|
278
|
+
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
|
|
279
|
+
fs.writeFileSync(
|
|
280
|
+
path.join(tempDir, ".selesai", "settings.json"),
|
|
281
|
+
JSON.stringify({ packages: ["npm:@scope/skill-package@1.2.3"] }, null, 2),
|
|
282
|
+
"utf-8",
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const { resolved, missing } = resolveSkills(["project-settings-scoped-npm-package-skill"], tempDir);
|
|
286
|
+
assert.deepEqual(missing, []);
|
|
287
|
+
assert.equal(resolved.length, 1);
|
|
288
|
+
assert.equal(resolved[0]?.source, "project-package");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("discovers skills from the current cwd package", () => {
|
|
292
|
+
makePackageSkill(tempDir, "cwd-package-skill", "Cwd package skill.");
|
|
293
|
+
|
|
294
|
+
const { resolved, missing } = resolveSkills(["cwd-package-skill"], tempDir);
|
|
295
|
+
assert.deepEqual(missing, []);
|
|
296
|
+
assert.equal(resolved.length, 1);
|
|
297
|
+
assert.equal(resolved[0]?.source, "project-package");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("falls back to the runtime cwd when the execution cwd lacks the skill", () => {
|
|
301
|
+
const nestedDir = path.join(tempDir, "nested");
|
|
302
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
303
|
+
makePackageSkill(tempDir, "runtime-fallback-skill", "Runtime fallback skill.");
|
|
304
|
+
|
|
305
|
+
const { resolved, missing } = resolveSkillsWithFallback(["runtime-fallback-skill"], nestedDir, tempDir);
|
|
306
|
+
assert.deepEqual(missing, []);
|
|
307
|
+
assert.equal(resolved.length, 1);
|
|
308
|
+
assert.equal(resolved[0]?.source, "project-package");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("discovers skills from user settings packages", async () => {
|
|
312
|
+
const fakeHome = path.join(tempDir, "fake-home");
|
|
313
|
+
const userAgentDir = path.join(fakeHome, ".selesai", "agent");
|
|
314
|
+
const userPackageRoot = path.join(userAgentDir, "user-pkg");
|
|
315
|
+
const previousHome = process.env.HOME;
|
|
316
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
process.env.HOME = fakeHome;
|
|
320
|
+
process.env.USERPROFILE = fakeHome;
|
|
321
|
+
makePackageSkill(userPackageRoot, "user-settings-package-skill", "User settings package skill.");
|
|
322
|
+
fs.mkdirSync(userAgentDir, { recursive: true });
|
|
323
|
+
fs.writeFileSync(
|
|
324
|
+
path.join(userAgentDir, "settings.json"),
|
|
325
|
+
JSON.stringify({ packages: [{ source: "./user-pkg" }] }, null, 2),
|
|
326
|
+
"utf-8",
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const fresh = await importSkillsFresh();
|
|
330
|
+
fresh.clearSkillCache();
|
|
331
|
+
const discovered = fresh.discoverAvailableSkills(tempDir);
|
|
332
|
+
const skill = discovered.find((entry) => entry.name === "user-settings-package-skill");
|
|
333
|
+
assert.ok(skill);
|
|
334
|
+
assert.equal(skill?.source, "user-package");
|
|
335
|
+
} finally {
|
|
336
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
337
|
+
else process.env.HOME = previousHome;
|
|
338
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
339
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("discovers skills from user settings git package sources", async () => {
|
|
344
|
+
const fakeHome = path.join(tempDir, "fake-home");
|
|
345
|
+
const userAgentDir = path.join(fakeHome, ".selesai", "agent");
|
|
346
|
+
const packageRoot = path.join(userAgentDir, "git", "github.com", "user", "repo");
|
|
347
|
+
const previousHome = process.env.HOME;
|
|
348
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
process.env.HOME = fakeHome;
|
|
352
|
+
process.env.USERPROFILE = fakeHome;
|
|
353
|
+
makePackageSkill(packageRoot, "user-settings-git-package-skill", "User settings git package skill.");
|
|
354
|
+
fs.mkdirSync(userAgentDir, { recursive: true });
|
|
355
|
+
fs.writeFileSync(
|
|
356
|
+
path.join(userAgentDir, "settings.json"),
|
|
357
|
+
JSON.stringify({ packages: ["git:github.com/user/repo.git@main"] }, null, 2),
|
|
358
|
+
"utf-8",
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const fresh = await importSkillsFresh();
|
|
362
|
+
fresh.clearSkillCache();
|
|
363
|
+
const discovered = fresh.discoverAvailableSkills(tempDir);
|
|
364
|
+
const skill = discovered.find((entry) => entry.name === "user-settings-git-package-skill");
|
|
365
|
+
assert.ok(skill);
|
|
366
|
+
assert.equal(skill?.source, "user-package");
|
|
367
|
+
} finally {
|
|
368
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
369
|
+
else process.env.HOME = previousHome;
|
|
370
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
371
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("discovers skills from user settings scoped npm package sources", async () => {
|
|
376
|
+
const fakeHome = path.join(tempDir, "fake-home");
|
|
377
|
+
const userAgentDir = path.join(fakeHome, ".selesai", "agent");
|
|
378
|
+
const packageRoot = path.join(userAgentDir, "npm", "node_modules", "@scope", "skill-package");
|
|
379
|
+
const previousHome = process.env.HOME;
|
|
380
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
process.env.HOME = fakeHome;
|
|
384
|
+
process.env.USERPROFILE = fakeHome;
|
|
385
|
+
makePackageSkill(
|
|
386
|
+
packageRoot,
|
|
387
|
+
"user-settings-scoped-npm-package-skill",
|
|
388
|
+
"User settings scoped npm package skill.",
|
|
389
|
+
"@scope/skill-package",
|
|
390
|
+
);
|
|
391
|
+
fs.mkdirSync(userAgentDir, { recursive: true });
|
|
392
|
+
fs.writeFileSync(
|
|
393
|
+
path.join(userAgentDir, "settings.json"),
|
|
394
|
+
JSON.stringify({ packages: [{ source: "npm:@scope/skill-package@latest" }] }, null, 2),
|
|
395
|
+
"utf-8",
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const fresh = await importSkillsFresh();
|
|
399
|
+
fresh.clearSkillCache();
|
|
400
|
+
const discovered = fresh.discoverAvailableSkills(tempDir);
|
|
401
|
+
const skill = discovered.find((entry) => entry.name === "user-settings-scoped-npm-package-skill");
|
|
402
|
+
assert.ok(skill);
|
|
403
|
+
assert.equal(skill?.source, "user-package");
|
|
404
|
+
} finally {
|
|
405
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
406
|
+
else process.env.HOME = previousHome;
|
|
407
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
408
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("surfaces malformed project settings files instead of silently ignoring them", () => {
|
|
413
|
+
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
|
|
414
|
+
fs.writeFileSync(path.join(tempDir, ".selesai", "settings.json"), "{bad-json", "utf-8");
|
|
415
|
+
|
|
416
|
+
assert.throws(
|
|
417
|
+
() => resolveSkills(["missing-skill"], tempDir),
|
|
418
|
+
/Failed to read skills settings file .+\.pi[\\/]settings\.json/,
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("surfaces malformed explicit settings package manifests instead of silently ignoring them", () => {
|
|
423
|
+
const packageRoot = path.join(tempDir, ".selesai", "packages", "broken-package");
|
|
424
|
+
fs.mkdirSync(packageRoot, { recursive: true });
|
|
425
|
+
fs.writeFileSync(path.join(packageRoot, "package.json"), "{bad-json", "utf-8");
|
|
426
|
+
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
|
|
427
|
+
fs.writeFileSync(
|
|
428
|
+
path.join(tempDir, ".selesai", "settings.json"),
|
|
429
|
+
JSON.stringify({ packages: ["./packages/broken-package"] }, null, 2),
|
|
430
|
+
"utf-8",
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
assert.throws(
|
|
434
|
+
() => discoverAvailableSkills(tempDir),
|
|
435
|
+
/Failed to read package manifest .+broken-package[\\/]package\.json/,
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
});
|