@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,420 @@
|
|
|
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 {
|
|
7
|
+
AGENT_MEMORY_DIR_NAME,
|
|
8
|
+
AGENT_MEMORY_FILE,
|
|
9
|
+
agentHasWriteTools,
|
|
10
|
+
buildAgentMemoryInjection,
|
|
11
|
+
parseMemoryFrontmatter,
|
|
12
|
+
readMemoryFile,
|
|
13
|
+
resolveMemoryDir,
|
|
14
|
+
} from "../../src/agents/agent-memory.ts";
|
|
15
|
+
import { serializeAgent } from "../../src/agents/agent-serializer.ts";
|
|
16
|
+
import { discoverAgents, findNearestProjectRoot, type AgentConfig, type AgentMemoryConfig } from "../../src/agents/agents.ts";
|
|
17
|
+
import { handleManagementAction } from "../../src/agents/agent-management.ts";
|
|
18
|
+
|
|
19
|
+
const tempDirs: string[] = [];
|
|
20
|
+
|
|
21
|
+
function makeAgent(overrides: Partial<AgentConfig> & { memory?: AgentMemoryConfig }): AgentConfig {
|
|
22
|
+
return {
|
|
23
|
+
name: "test-agent",
|
|
24
|
+
description: "test agent",
|
|
25
|
+
systemPrompt: "do the thing",
|
|
26
|
+
systemPromptMode: "replace",
|
|
27
|
+
inheritProjectContext: false,
|
|
28
|
+
inheritSkills: false,
|
|
29
|
+
source: "project",
|
|
30
|
+
filePath: "/tmp/test-agent.md",
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mkdtemp(prefix: string): string {
|
|
36
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
37
|
+
tempDirs.push(dir);
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function withTempHome<T>(fn: (home: string) => T): T {
|
|
42
|
+
const home = mkdtemp("pi-subagents-mem-home-");
|
|
43
|
+
const oldHome = process.env.HOME;
|
|
44
|
+
const oldUserProfile = process.env.USERPROFILE;
|
|
45
|
+
const oldPiCodingAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
46
|
+
process.env.HOME = home;
|
|
47
|
+
process.env.USERPROFILE = home;
|
|
48
|
+
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
49
|
+
try {
|
|
50
|
+
return fn(home);
|
|
51
|
+
} finally {
|
|
52
|
+
if (oldHome === undefined) delete process.env.HOME;
|
|
53
|
+
else process.env.HOME = oldHome;
|
|
54
|
+
if (oldUserProfile === undefined) delete process.env.USERPROFILE;
|
|
55
|
+
else process.env.USERPROFILE = oldUserProfile;
|
|
56
|
+
if (oldPiCodingAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
57
|
+
else process.env.SELESAI_CODING_AGENT_DIR = oldPiCodingAgentDir;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create a temp project root that findNearestProjectRoot will recognise (.pi dir present).
|
|
62
|
+
function mkProject(): string {
|
|
63
|
+
const dir = mkdtemp("pi-subagents-mem-project-");
|
|
64
|
+
fs.mkdirSync(path.join(dir, ".selesai", "agents"), { recursive: true });
|
|
65
|
+
return dir;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeMemoryFile(memoryDir: string, contents: string): void {
|
|
69
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
70
|
+
fs.writeFileSync(path.join(memoryDir, AGENT_MEMORY_FILE), contents, "utf-8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
while (tempDirs.length > 0) {
|
|
75
|
+
const dir = tempDirs.pop();
|
|
76
|
+
if (!dir) continue;
|
|
77
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("parseMemoryFrontmatter", () => {
|
|
82
|
+
it("parses a project scope block", () => {
|
|
83
|
+
assert.deepEqual(parseMemoryFrontmatter("scope: project\npath: security-reviewer"), {
|
|
84
|
+
scope: "project",
|
|
85
|
+
path: "security-reviewer",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("parses a user scope with nested path and strips quotes", () => {
|
|
90
|
+
assert.deepEqual(parseMemoryFrontmatter('scope: user\npath: "team/release-agent"'), {
|
|
91
|
+
scope: "user",
|
|
92
|
+
path: "team/release-agent",
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("parses an inline object memory block", () => {
|
|
97
|
+
assert.deepEqual(parseMemoryFrontmatter('{ scope: "project", path: "security-reviewer" }'), {
|
|
98
|
+
scope: "project",
|
|
99
|
+
path: "security-reviewer",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("rejects unknown scopes", () => {
|
|
104
|
+
assert.equal(parseMemoryFrontmatter("scope: global\npath: x"), undefined);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("rejects a missing path", () => {
|
|
108
|
+
assert.equal(parseMemoryFrontmatter("scope: project"), undefined);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("rejects a missing scope", () => {
|
|
112
|
+
assert.equal(parseMemoryFrontmatter("path: x"), undefined);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("treats empty or absent input as no memory config", () => {
|
|
116
|
+
assert.equal(parseMemoryFrontmatter(undefined), undefined);
|
|
117
|
+
assert.equal(parseMemoryFrontmatter(""), undefined);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("agentHasWriteTools", () => {
|
|
122
|
+
it("inherits write capability when tools are unset", () => {
|
|
123
|
+
assert.equal(agentHasWriteTools({}), true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("detects edit, write, and bash as write tools", () => {
|
|
127
|
+
assert.equal(agentHasWriteTools({ tools: ["read", "edit"] }), true);
|
|
128
|
+
assert.equal(agentHasWriteTools({ tools: ["write"] }), true);
|
|
129
|
+
assert.equal(agentHasWriteTools({ tools: ["bash"] }), true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("treats read-only and mcp-only tool sets as non-write", () => {
|
|
133
|
+
assert.equal(agentHasWriteTools({ tools: ["read", "grep", "find", "ls"] }), false);
|
|
134
|
+
assert.equal(agentHasWriteTools({ tools: ["mcp:filesystem"] }), false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("resolveMemoryDir", () => {
|
|
139
|
+
it("resolves a simple and nested path under the root", () => {
|
|
140
|
+
const root = mkdtemp("pi-subagents-mem-root-");
|
|
141
|
+
assert.deepEqual(resolveMemoryDir(root, "reviewer"), { dir: path.join(root, "reviewer") });
|
|
142
|
+
assert.deepEqual(resolveMemoryDir(root, "team/reviewer"), { dir: path.join(root, "team", "reviewer") });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects empty paths", () => {
|
|
146
|
+
const root = mkdtemp("pi-subagents-mem-root-");
|
|
147
|
+
assert.ok("error" in resolveMemoryDir(root, ""));
|
|
148
|
+
assert.ok("error" in resolveMemoryDir(root, " "));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("rejects dot and parent segments", () => {
|
|
152
|
+
const root = mkdtemp("pi-subagents-mem-root-");
|
|
153
|
+
assert.ok("error" in resolveMemoryDir(root, "."));
|
|
154
|
+
assert.ok("error" in resolveMemoryDir(root, ".."));
|
|
155
|
+
assert.ok("error" in resolveMemoryDir(root, "a/../b"));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("rejects absolute and Windows drive-like paths", () => {
|
|
159
|
+
const root = mkdtemp("pi-subagents-mem-root-");
|
|
160
|
+
assert.ok("error" in resolveMemoryDir(root, "/tmp/reviewer"));
|
|
161
|
+
assert.ok("error" in resolveMemoryDir(root, "C:\\Users\\reviewer"));
|
|
162
|
+
assert.ok("error" in resolveMemoryDir(root, "C:reviewer"));
|
|
163
|
+
assert.ok("error" in resolveMemoryDir(root, "team:C"));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("rejects a symlinked ancestor before prompting a first write", () => {
|
|
167
|
+
const root = mkdtemp("pi-subagents-mem-root-");
|
|
168
|
+
const outside = mkdtemp("pi-subagents-mem-outside-");
|
|
169
|
+
const linkPath = path.join(root, "leak");
|
|
170
|
+
try {
|
|
171
|
+
fs.symlinkSync(outside, linkPath);
|
|
172
|
+
} catch {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const resolved = resolveMemoryDir(root, "leak/new-agent");
|
|
176
|
+
assert.ok("error" in resolved, "expected symlink ancestor escape to be rejected");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("rejects a memory dir that is a symlink escaping the root", () => {
|
|
180
|
+
const root = mkdtemp("pi-subagents-mem-root-");
|
|
181
|
+
const outside = mkdtemp("pi-subagents-mem-outside-");
|
|
182
|
+
const linkPath = path.join(root, "leak");
|
|
183
|
+
try {
|
|
184
|
+
fs.symlinkSync(outside, linkPath);
|
|
185
|
+
} catch {
|
|
186
|
+
// Symlinks may be unavailable (e.g. Windows without dev mode); skip portably.
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const resolved = resolveMemoryDir(root, "leak");
|
|
190
|
+
assert.ok("error" in resolved, "expected symlink escape to be rejected");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("readMemoryFile", () => {
|
|
195
|
+
it("returns null when no memory file exists", () => {
|
|
196
|
+
const dir = mkdtemp("pi-subagents-mem-dir-");
|
|
197
|
+
assert.equal(readMemoryFile(dir), null);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("reads contents and reports byte capping", () => {
|
|
201
|
+
const dir = mkdtemp("pi-subagents-mem-dir-");
|
|
202
|
+
writeMemoryFile(dir, "line one\nline two\n");
|
|
203
|
+
const result = readMemoryFile(dir);
|
|
204
|
+
assert.ok(result && typeof result === "object" && !("error" in result) && result !== "unsafe");
|
|
205
|
+
assert.equal((result as { contents: string }).contents, "line one\nline two\n");
|
|
206
|
+
assert.equal((result as { byteCapped: boolean }).byteCapped, false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("flags byte-capped contents", () => {
|
|
210
|
+
const dir = mkdtemp("pi-subagents-mem-dir-");
|
|
211
|
+
const bigLine = "x".repeat(500);
|
|
212
|
+
writeMemoryFile(dir, Array.from({ length: 50 }, () => bigLine).join("\n"));
|
|
213
|
+
const result = readMemoryFile(dir);
|
|
214
|
+
assert.ok(result && result !== "unsafe");
|
|
215
|
+
assert.equal((result as { byteCapped: boolean }).byteCapped, true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("does not return more than the memory byte cap", () => {
|
|
219
|
+
const dir = mkdtemp("pi-subagents-mem-dir-");
|
|
220
|
+
writeMemoryFile(dir, "x".repeat(1024 * 1024));
|
|
221
|
+
const result = readMemoryFile(dir);
|
|
222
|
+
assert.ok(result && result !== "unsafe");
|
|
223
|
+
assert.equal(result.byteCapped, true);
|
|
224
|
+
assert.ok(Buffer.byteLength(result.contents, "utf-8") <= 16 * 1024);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("rejects a symlinked memory file that escapes the memory dir", () => {
|
|
228
|
+
const dir = mkdtemp("pi-subagents-mem-dir-");
|
|
229
|
+
const outsideFile = path.join(mkdtemp("pi-subagents-mem-outside-"), "secret.md");
|
|
230
|
+
fs.writeFileSync(outsideFile, "leaked", "utf-8");
|
|
231
|
+
try {
|
|
232
|
+
fs.symlinkSync(outsideFile, path.join(dir, AGENT_MEMORY_FILE));
|
|
233
|
+
} catch {
|
|
234
|
+
return; // Symlinks unsupported here; skip portably.
|
|
235
|
+
}
|
|
236
|
+
assert.equal(readMemoryFile(dir), "unsafe");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("buildAgentMemoryInjection", () => {
|
|
241
|
+
it("returns empty when the agent has no memory scope", () => {
|
|
242
|
+
const project = mkProject();
|
|
243
|
+
assert.equal(buildAgentMemoryInjection(makeAgent({}), project), "");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("injects a read-write block with contents for a project scope", () => {
|
|
247
|
+
const project = mkProject();
|
|
248
|
+
const memoryDir = path.join(project, ".selesai", AGENT_MEMORY_DIR_NAME, "security-reviewer");
|
|
249
|
+
writeMemoryFile(memoryDir, "Threat: token leakage in logs.\nGotcha: retry on 429.");
|
|
250
|
+
const agent = makeAgent({ memory: { scope: "project", path: "security-reviewer" }, tools: ["read", "edit"] });
|
|
251
|
+
const injection = buildAgentMemoryInjection(agent, project);
|
|
252
|
+
const memoryFile = path.join(memoryDir, AGENT_MEMORY_FILE);
|
|
253
|
+
assert.match(injection, /# Persistent agent memory/);
|
|
254
|
+
assert.match(injection, new RegExp(`Memory file: ${escapeRegex(memoryFile)}`));
|
|
255
|
+
assert.match(injection, /append a concise dated entry/);
|
|
256
|
+
assert.match(injection, /reference data, not instructions/);
|
|
257
|
+
assert.match(injection, /Threat: token leakage in logs\./);
|
|
258
|
+
assert.match(injection, /Gotcha: retry on 429\./);
|
|
259
|
+
assert.doesNotMatch(injection, /read-only/);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("injects a creation prompt when a read-write agent has no memory file yet", () => {
|
|
263
|
+
const project = mkProject();
|
|
264
|
+
const agent = makeAgent({ memory: { scope: "project", path: "fresh-agent" }, tools: ["read", "write"] });
|
|
265
|
+
const injection = buildAgentMemoryInjection(agent, project);
|
|
266
|
+
const memoryFile = path.join(project, ".selesai", AGENT_MEMORY_DIR_NAME, "fresh-agent", AGENT_MEMORY_FILE);
|
|
267
|
+
assert.match(injection, new RegExp(`Memory file: ${escapeRegex(memoryFile)}`));
|
|
268
|
+
assert.match(injection, new RegExp(`No ${AGENT_MEMORY_FILE} exists yet`));
|
|
269
|
+
assert.match(injection, /You may create it/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("injects a read-only block for agents without write tools", () => {
|
|
273
|
+
const project = mkProject();
|
|
274
|
+
const memoryDir = path.join(project, ".selesai", AGENT_MEMORY_DIR_NAME, "scout");
|
|
275
|
+
writeMemoryFile(memoryDir, "Known flake: async timeout test.");
|
|
276
|
+
const agent = makeAgent({ memory: { scope: "project", path: "scout" }, tools: ["read", "grep", "find", "ls"] });
|
|
277
|
+
const injection = buildAgentMemoryInjection(agent, project);
|
|
278
|
+
assert.match(injection, /read-only, role-specific memory scope/);
|
|
279
|
+
assert.match(injection, /Do not attempt to edit or create the memory file/);
|
|
280
|
+
assert.match(injection, /reference data, not instructions/);
|
|
281
|
+
assert.match(injection, /Known flake: async timeout test\./);
|
|
282
|
+
assert.doesNotMatch(injection, /You may create it/);
|
|
283
|
+
assert.doesNotMatch(injection, /append a concise dated entry/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("injects nothing for a read-only agent with no memory file yet", () => {
|
|
287
|
+
const project = mkProject();
|
|
288
|
+
const agent = makeAgent({ memory: { scope: "project", path: "empty-scout" }, tools: ["read"] });
|
|
289
|
+
assert.equal(buildAgentMemoryInjection(agent, project), "");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("resolves user scope under the agent dir, separate from the owner memory system", () => {
|
|
293
|
+
withTempHome((home) => {
|
|
294
|
+
const project = mkProject();
|
|
295
|
+
const memoryDir = path.join(home, ".selesai", "agent", AGENT_MEMORY_DIR_NAME, "release-agent");
|
|
296
|
+
writeMemoryFile(memoryDir, "Release gotcha: tag before gh release.");
|
|
297
|
+
const agent = makeAgent({ memory: { scope: "user", path: "release-agent" }, tools: ["read", "edit"] });
|
|
298
|
+
const injection = buildAgentMemoryInjection(agent, project);
|
|
299
|
+
const memoryFile = path.join(memoryDir, AGENT_MEMORY_FILE);
|
|
300
|
+
assert.match(injection, new RegExp(`Memory file: ${escapeRegex(memoryFile)}`));
|
|
301
|
+
assert.match(injection, /Release gotcha: tag before gh release\./);
|
|
302
|
+
// Must not collide with the owner's ~/.pi/agent/memory/{project}/ layout.
|
|
303
|
+
assert.doesNotMatch(injection, /agent\/memory\/[^/]+\/release-agent/);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("returns empty for project scope when no project root is found", () => {
|
|
308
|
+
const nowhere = mkdtemp("pi-subagents-mem-noroot-");
|
|
309
|
+
// Skip deterministically if an ancestor happens to register as a project root.
|
|
310
|
+
if (findNearestProjectRoot(nowhere) !== null) return;
|
|
311
|
+
const agent = makeAgent({ memory: { scope: "project", path: "x" }, tools: ["read"] });
|
|
312
|
+
assert.equal(buildAgentMemoryInjection(agent, nowhere), "");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("returns empty when the memory path is unsafe", () => {
|
|
316
|
+
const project = mkProject();
|
|
317
|
+
const agent = makeAgent({ memory: { scope: "project", path: ".." }, tools: ["read", "edit"] });
|
|
318
|
+
assert.equal(buildAgentMemoryInjection(agent, project), "");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("returns empty when the memory file is an escaping symlink", () => {
|
|
322
|
+
const project = mkProject();
|
|
323
|
+
const memoryDir = path.join(project, ".selesai", AGENT_MEMORY_DIR_NAME, "leak");
|
|
324
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
325
|
+
const outsideFile = path.join(mkdtemp("pi-subagents-mem-outside-"), "secret.md");
|
|
326
|
+
fs.writeFileSync(outsideFile, "leaked", "utf-8");
|
|
327
|
+
try {
|
|
328
|
+
fs.symlinkSync(outsideFile, path.join(memoryDir, AGENT_MEMORY_FILE));
|
|
329
|
+
} catch {
|
|
330
|
+
return; // Symlinks unsupported here; skip portably.
|
|
331
|
+
}
|
|
332
|
+
const agent = makeAgent({ memory: { scope: "project", path: "leak" }, tools: ["read", "edit"] });
|
|
333
|
+
assert.equal(buildAgentMemoryInjection(agent, project), "");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("caps memory contents to the first N lines without claiming a byte cap", () => {
|
|
337
|
+
const project = mkProject();
|
|
338
|
+
const memoryDir = path.join(project, ".selesai", AGENT_MEMORY_DIR_NAME, "capped");
|
|
339
|
+
writeMemoryFile(memoryDir, Array.from({ length: 210 }, (_, i) => `l-${i + 1}`).join("\n"));
|
|
340
|
+
const agent = makeAgent({ memory: { scope: "project", path: "capped" }, tools: ["read", "edit"] });
|
|
341
|
+
const injection = buildAgentMemoryInjection(agent, project);
|
|
342
|
+
assert.match(injection, /l-200\b/);
|
|
343
|
+
assert.doesNotMatch(injection, /l-201\b/);
|
|
344
|
+
assert.match(injection, /first 200 lines\)/);
|
|
345
|
+
assert.doesNotMatch(injection, /byte-capped/);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("agent memory frontmatter round-trip", () => {
|
|
350
|
+
it("parses memory frontmatter during discovery and keeps it out of extraFields", () => {
|
|
351
|
+
const project = mkProject();
|
|
352
|
+
fs.writeFileSync(path.join(project, ".selesai", "agents", "security-reviewer.md"), `---
|
|
353
|
+
name: security-reviewer
|
|
354
|
+
description: Recurring security reviewer
|
|
355
|
+
tools: read, grep, bash, edit
|
|
356
|
+
memory: { scope: project, path: security-reviewer }
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
Review for threats.
|
|
360
|
+
`, "utf-8");
|
|
361
|
+
|
|
362
|
+
const agent = discoverAgents(project, "project").agents.find((a) => a.name === "security-reviewer");
|
|
363
|
+
assert.ok(agent, "agent should be discovered");
|
|
364
|
+
assert.deepEqual(agent?.memory, { scope: "project", path: "security-reviewer" });
|
|
365
|
+
assert.equal(agent?.extraFields?.memory, undefined, "memory must not leak into extraFields");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("serializes memory back into frontmatter", () => {
|
|
369
|
+
const agent = makeAgent({ memory: { scope: "user", path: "release-agent" } });
|
|
370
|
+
const serialized = serializeAgent(agent);
|
|
371
|
+
assert.match(serialized, /^memory:$/m);
|
|
372
|
+
assert.match(serialized, /^ scope: user$/m);
|
|
373
|
+
assert.match(serialized, /^ path: release-agent$/m);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("ignores a malformed memory block without dropping the agent", () => {
|
|
377
|
+
const project = mkProject();
|
|
378
|
+
fs.writeFileSync(path.join(project, ".selesai", "agents", "bad-memory.md"), `---
|
|
379
|
+
name: bad-memory
|
|
380
|
+
description: Bad memory config
|
|
381
|
+
memory:
|
|
382
|
+
scope: galaxy
|
|
383
|
+
path: whatever
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
Still loads.
|
|
387
|
+
`, "utf-8");
|
|
388
|
+
|
|
389
|
+
const agent = discoverAgents(project, "project").agents.find((a) => a.name === "bad-memory");
|
|
390
|
+
assert.ok(agent, "agent should still be discovered");
|
|
391
|
+
assert.equal(agent?.memory, undefined);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("agent memory in management detail", () => {
|
|
396
|
+
it("surfaces the memory scope in the get action", () => {
|
|
397
|
+
const project = mkProject();
|
|
398
|
+
fs.writeFileSync(path.join(project, ".selesai", "agents", "security-reviewer.md"), `---
|
|
399
|
+
name: security-reviewer
|
|
400
|
+
description: Recurring security reviewer
|
|
401
|
+
memory:
|
|
402
|
+
scope: project
|
|
403
|
+
path: security-reviewer
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
Review for threats.
|
|
407
|
+
`, "utf-8");
|
|
408
|
+
|
|
409
|
+
const res = handleManagementAction("get", { agent: "security-reviewer" }, {
|
|
410
|
+
cwd: project,
|
|
411
|
+
modelRegistry: { getAvailable: () => [] },
|
|
412
|
+
});
|
|
413
|
+
assert.equal(res.isError, false);
|
|
414
|
+
assert.match(res.content[0]?.text ?? "", /Memory: project scope, path: security-reviewer/);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
function escapeRegex(value: string): string {
|
|
419
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
420
|
+
}
|