@jaybeeuu/agent-cortex 1.20.0 → 1.38.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/.agents/skills/style-versioning/SKILL.md +102 -0
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/ci.yml +227 -6
- package/AGENTS.md +29 -24
- package/CHANGELOG.md +119 -5
- package/README.md +132 -33
- package/agents/README.md +47 -13
- package/agents/plan/agent.md +51 -0
- package/agents/plan/claude/frontmatter.json +6 -0
- package/agents/plan/copilot/frontmatter.json +6 -0
- package/agents/plan/pi/frontmatter.json +6 -0
- package/agents/plan.agent.md +3 -0
- package/agents/ralph/agent.md +262 -0
- package/agents/ralph/claude/frontmatter.json +6 -0
- package/agents/ralph/claude/polling.md +31 -0
- package/agents/ralph/copilot/frontmatter.json +6 -0
- package/agents/ralph/copilot/polling.md +62 -0
- package/agents/ralph/pi/frontmatter.json +6 -0
- package/agents/ralph/pi/polling.md +74 -0
- package/agents/ralph-plan/agent.md +114 -0
- package/agents/ralph-plan/claude/frontmatter.json +6 -0
- package/agents/ralph-plan/copilot/frontmatter.json +6 -0
- package/agents/ralph-plan/pi/frontmatter.json +6 -0
- package/agents/ralph-plan.agent.md +5 -2
- package/agents/ralph.agent.md +83 -57
- package/agents/strategy/agent.md +104 -0
- package/agents/strategy/claude/frontmatter.json +6 -0
- package/agents/strategy/copilot/frontmatter.json +6 -0
- package/agents/strategy/pi/frontmatter.json +6 -0
- package/agents/strategy.agent.md +3 -0
- package/bin/agent-cortex.mjs +68 -1
- package/bin/installers/claude.mjs +262 -0
- package/bin/installers/copilot.mjs +129 -0
- package/bin/installers/pi.mjs +197 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/agents/plan.md +3 -1
- package/claude/agents/ralph-plan.md +5 -3
- package/claude/agents/strategy.md +3 -1
- package/claude/hooks/scripts/notify.mjs +54 -0
- package/claude/hooks.json +15 -0
- package/docs/claude-hooks.md +115 -0
- package/docs/ideas/unified-ci-pipeline.md +28 -0
- package/docs/skills/skill-anatomy.md +9 -0
- package/extensions/agent-modes/README.md +54 -0
- package/extensions/agent-modes/discover.test.ts +241 -0
- package/extensions/agent-modes/discover.ts +458 -0
- package/extensions/agent-modes/index.ts +13 -224
- package/extensions/agent-modes/node_modules/.bin/pi +21 -0
- package/extensions/agent-modes/node_modules/.bin/tsc +21 -0
- package/extensions/agent-modes/node_modules/.bin/tsserver +21 -0
- package/extensions/agent-modes/node_modules/.bin/tsx +21 -0
- package/extensions/agent-modes/package.json +20 -0
- package/extensions/agent-modes/tsconfig.json +12 -0
- package/extensions/session-start.ts +1 -1
- package/extensions/skill-stats/context-loader.test.ts +1 -1
- package/extensions/skill-stats/node_modules/.bin/pi +21 -0
- package/extensions/skill-stats/node_modules/.bin/tsc +21 -0
- package/extensions/skill-stats/node_modules/.bin/tsserver +21 -0
- package/extensions/skill-stats/node_modules/.bin/tsx +21 -0
- package/extensions/subagent/README.md +19 -0
- package/extensions/subagent/index.ts +131 -45
- package/extensions/subagent/node_modules/.bin/pi +21 -0
- package/extensions/subagent/node_modules/.bin/pi-ai +21 -0
- package/extensions/subagent/node_modules/.bin/tsc +21 -0
- package/extensions/subagent/node_modules/.bin/tsserver +21 -0
- package/extensions/subagent/node_modules/.bin/tsx +21 -0
- package/extensions/subagent/package.json +23 -0
- package/extensions/subagent/tsconfig.json +12 -0
- package/extensions/subagent/wait.test.ts +202 -0
- package/extensions/subagent/wait.ts +118 -0
- package/hooks/claude/hooks.json +38 -0
- package/hooks/claude/scripts/notify.mjs +54 -0
- package/lib/cli.mjs +62 -3
- package/package.json +10 -7
- package/plugin.json +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/build-copilot-agents.mjs +30 -0
- package/scripts/lib/compose-agent.mjs +241 -0
- package/scripts/sync-plugin-version.sh +17 -0
- package/skills/engineering/design-an-interface/SKILL.md +1 -1
- package/skills/engineering/improve-codebase-architecture/SKILL.md +1 -1
- package/skills/workflow/ralph/REFERENCE.md +2 -2
- package/skills/workflow/ralph/SKILL.md +6 -6
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
- package/skills/workflow/using-agent-skills/SKILL.md +15 -2
- package/test/cli.test.mjs +115 -2
- package/test/copilot-installer.test.mjs +298 -0
- package/test/installer.test.mjs +179 -0
- package/test/notify-hook.test.mjs +63 -0
- package/test/pi-installer.test.mjs +371 -0
- package/token-map.README.md +128 -0
- package/token-map.json +106 -0
- package/.beads/README.md +0 -81
- package/.beads/config.yaml +0 -1
- package/.beads/issues.jsonl +0 -236
- package/.beads/metadata.json +0 -7
- package/.github/workflows/ci.test.ts +0 -217
- package/.github/workflows/publish.yml +0 -36
- package/scripts/build-claude-agents.mjs +0 -179
- package/skills/workflow/using-agent-skills/scripts/validate-skill-structure.test.ts +0 -73
- /package/extensions/{context-loader.md → lib/context-loader.md} +0 -0
- /package/extensions/{context-loader.ts → lib/context-loader.ts} +0 -0
|
@@ -20,6 +20,7 @@ import * as path from "node:path";
|
|
|
20
20
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
21
21
|
import type { Message } from "@earendil-works/pi-ai";
|
|
22
22
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts";
|
|
23
|
+
import { waitForAgents } from "./wait.ts";
|
|
23
24
|
import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
24
25
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
25
26
|
import { Type } from "typebox";
|
|
@@ -1019,6 +1020,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1019
1020
|
cwd: string;
|
|
1020
1021
|
}
|
|
1021
1022
|
|
|
1023
|
+
interface ReadAgentDetails {
|
|
1024
|
+
agentId: string;
|
|
1025
|
+
error?: string;
|
|
1026
|
+
status?: string;
|
|
1027
|
+
exitCode?: number;
|
|
1028
|
+
usage?: UsageStats;
|
|
1029
|
+
model?: string;
|
|
1030
|
+
stopReason?: string;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1022
1033
|
const backgroundRuns = new Map<string, BackgroundRun>();
|
|
1023
1034
|
|
|
1024
1035
|
pi.registerTool({
|
|
@@ -1027,7 +1038,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1027
1038
|
description:
|
|
1028
1039
|
"Spawn a background sub-agent with the given prompt and return an agent ID immediately. " +
|
|
1029
1040
|
"This is the ONLY tool for spawning subagents — do not use bash or any other tool. " +
|
|
1030
|
-
"Use
|
|
1041
|
+
"Use wait_for_agents to block until results are ready, or read_agent to check a specific agent. " +
|
|
1031
1042
|
"Compatible with Copilot CLI's task/read_agent subagent model.",
|
|
1032
1043
|
parameters: Type.Object({
|
|
1033
1044
|
prompt: Type.String({ description: "Task prompt for the sub-agent to execute" }),
|
|
@@ -1050,43 +1061,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
1050
1061
|
|
|
1051
1062
|
const entry: BackgroundRun = {
|
|
1052
1063
|
result: null,
|
|
1053
|
-
promise: (
|
|
1054
|
-
try {
|
|
1055
|
-
const agents = [backgroundAgent, ...discoverAgents(ctx.cwd, "user").agents];
|
|
1056
|
-
const result = await runSingleAgent(
|
|
1057
|
-
cwd,
|
|
1058
|
-
agents,
|
|
1059
|
-
"background-agent",
|
|
1060
|
-
params.prompt,
|
|
1061
|
-
cwd,
|
|
1062
|
-
undefined,
|
|
1063
|
-
signal,
|
|
1064
|
-
undefined,
|
|
1065
|
-
(results: SingleResult[]) => ({
|
|
1066
|
-
mode: "single" as const,
|
|
1067
|
-
agentScope: "user" as AgentScope,
|
|
1068
|
-
projectAgentsDir: null,
|
|
1069
|
-
results,
|
|
1070
|
-
}),
|
|
1071
|
-
);
|
|
1072
|
-
entry.result = result;
|
|
1073
|
-
} catch (err) {
|
|
1074
|
-
entry.result = {
|
|
1075
|
-
agent: "background-agent",
|
|
1076
|
-
agentSource: "user",
|
|
1077
|
-
task: params.prompt,
|
|
1078
|
-
exitCode: 1,
|
|
1079
|
-
messages: [],
|
|
1080
|
-
stderr: String(err),
|
|
1081
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
})(),
|
|
1064
|
+
promise: Promise.resolve(),
|
|
1085
1065
|
startTime: Date.now(),
|
|
1086
1066
|
prompt: params.prompt,
|
|
1087
1067
|
cwd,
|
|
1088
1068
|
};
|
|
1089
1069
|
|
|
1070
|
+
const run = (async () => {
|
|
1071
|
+
try {
|
|
1072
|
+
const agents = [backgroundAgent, ...discoverAgents(ctx.cwd, "user").agents];
|
|
1073
|
+
const result = await runSingleAgent(
|
|
1074
|
+
cwd,
|
|
1075
|
+
agents,
|
|
1076
|
+
"background-agent",
|
|
1077
|
+
params.prompt,
|
|
1078
|
+
cwd,
|
|
1079
|
+
undefined,
|
|
1080
|
+
signal,
|
|
1081
|
+
undefined,
|
|
1082
|
+
(results: SingleResult[]) => ({
|
|
1083
|
+
mode: "single" as const,
|
|
1084
|
+
agentScope: "user" as AgentScope,
|
|
1085
|
+
projectAgentsDir: null,
|
|
1086
|
+
results,
|
|
1087
|
+
}),
|
|
1088
|
+
);
|
|
1089
|
+
entry.result = result;
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
entry.result = {
|
|
1092
|
+
agent: "background-agent",
|
|
1093
|
+
agentSource: "user",
|
|
1094
|
+
task: params.prompt,
|
|
1095
|
+
exitCode: 1,
|
|
1096
|
+
messages: [],
|
|
1097
|
+
stderr: String(err),
|
|
1098
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
})();
|
|
1102
|
+
// Attach the real run promise after construction so the run's completion
|
|
1103
|
+
// (entry.result committed before the promise settles) is what waiters race on.
|
|
1104
|
+
entry.promise = run;
|
|
1105
|
+
|
|
1090
1106
|
backgroundRuns.set(agentId, entry);
|
|
1091
1107
|
|
|
1092
1108
|
return {
|
|
@@ -1096,6 +1112,73 @@ export default function (pi: ExtensionAPI) {
|
|
|
1096
1112
|
},
|
|
1097
1113
|
});
|
|
1098
1114
|
|
|
1115
|
+
pi.registerTool({
|
|
1116
|
+
name: "wait_for_agents",
|
|
1117
|
+
label: "Wait for Background Agents",
|
|
1118
|
+
description:
|
|
1119
|
+
"Block until at least one background sub-agent spawned with the task tool completes. " +
|
|
1120
|
+
"Takes an array of agentIds and an optional timeout; returns the results of every agent " +
|
|
1121
|
+
"that has completed, and STILL RUNNING for agents that have not. " +
|
|
1122
|
+
"Preserves parallelism: dispatch several tasks, then wait_for_agents returns as soon as " +
|
|
1123
|
+
"any of them finishes. Use read_agent as a fallback to re-read a specific result.",
|
|
1124
|
+
parameters: Type.Object({
|
|
1125
|
+
agentIds: Type.Array(Type.String({ description: "Agent IDs returned by the task tool to wait on" })),
|
|
1126
|
+
timeout: Type.Optional(
|
|
1127
|
+
Type.Number({
|
|
1128
|
+
description:
|
|
1129
|
+
"Maximum seconds to block (default 120) before returning STILL RUNNING for unfinished agents",
|
|
1130
|
+
}),
|
|
1131
|
+
),
|
|
1132
|
+
}),
|
|
1133
|
+
async execute(_toolCallId, params, signal) {
|
|
1134
|
+
const timeoutSeconds = params.timeout ?? 120;
|
|
1135
|
+
const outcome = await waitForAgents(
|
|
1136
|
+
(agentId) => backgroundRuns.get(agentId),
|
|
1137
|
+
params.agentIds,
|
|
1138
|
+
{ timeoutMs: timeoutSeconds * 1000, signal },
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
const lines: string[] = [];
|
|
1142
|
+
if (outcome.aborted) lines.push("(aborted)");
|
|
1143
|
+
if (outcome.timedOut) lines.push(`(timed out after ${timeoutSeconds}s)`);
|
|
1144
|
+
if (lines.length > 0) lines.push("");
|
|
1145
|
+
|
|
1146
|
+
for (const { agentId, result } of outcome.completed) {
|
|
1147
|
+
lines.push(`### ${agentId} — completed`);
|
|
1148
|
+
lines.push(truncateParallelOutput(getResultOutput(result)));
|
|
1149
|
+
lines.push("");
|
|
1150
|
+
}
|
|
1151
|
+
if (outcome.running.length > 0) {
|
|
1152
|
+
lines.push(`### STILL RUNNING (${outcome.running.length})`);
|
|
1153
|
+
lines.push(outcome.running.map((id) => `- ${id}`).join("\n"));
|
|
1154
|
+
lines.push("");
|
|
1155
|
+
}
|
|
1156
|
+
if (outcome.notFound.length > 0) {
|
|
1157
|
+
lines.push(`### NOT FOUND (${outcome.notFound.length})`);
|
|
1158
|
+
lines.push(outcome.notFound.map((id) => `- ${id}`).join("\n"));
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const summary = `completed: ${outcome.completed.length}, still running: ${outcome.running.length}, not found: ${outcome.notFound.length}`;
|
|
1162
|
+
const body = lines.join("\n").trim();
|
|
1163
|
+
return {
|
|
1164
|
+
content: [{ type: "text", text: body ? `${summary}\n\n${body}` : summary }],
|
|
1165
|
+
details: {
|
|
1166
|
+
agentIds: params.agentIds,
|
|
1167
|
+
completed: outcome.completed.map(({ agentId, result }) => ({
|
|
1168
|
+
agentId,
|
|
1169
|
+
exitCode: result.exitCode,
|
|
1170
|
+
stopReason: result.stopReason,
|
|
1171
|
+
output: truncateParallelOutput(getResultOutput(result)),
|
|
1172
|
+
})),
|
|
1173
|
+
running: outcome.running,
|
|
1174
|
+
notFound: outcome.notFound,
|
|
1175
|
+
timedOut: outcome.timedOut,
|
|
1176
|
+
aborted: outcome.aborted,
|
|
1177
|
+
},
|
|
1178
|
+
};
|
|
1179
|
+
},
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1099
1182
|
pi.registerTool({
|
|
1100
1183
|
name: "read_agent",
|
|
1101
1184
|
label: "Read Background Agent Output",
|
|
@@ -1107,31 +1190,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
1107
1190
|
}),
|
|
1108
1191
|
async execute(_toolCallId, params) {
|
|
1109
1192
|
const entry = backgroundRuns.get(params.agentId);
|
|
1193
|
+
const details: ReadAgentDetails = {
|
|
1194
|
+
agentId: params.agentId,
|
|
1195
|
+
error: !entry ? "not found" : undefined,
|
|
1196
|
+
status: entry && !entry.result ? "running" : undefined,
|
|
1197
|
+
exitCode: entry?.result?.exitCode,
|
|
1198
|
+
usage: entry?.result?.usage,
|
|
1199
|
+
model: entry?.result?.model,
|
|
1200
|
+
stopReason: entry?.result?.stopReason,
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1110
1203
|
if (!entry) {
|
|
1111
1204
|
return {
|
|
1112
1205
|
content: [{ type: "text", text: `ERROR: No agent found with ID "${params.agentId}"` }],
|
|
1113
|
-
details
|
|
1206
|
+
details,
|
|
1114
1207
|
};
|
|
1115
1208
|
}
|
|
1116
1209
|
|
|
1117
1210
|
if (entry.result) {
|
|
1118
|
-
const output = getResultOutput(entry.result);
|
|
1119
1211
|
return {
|
|
1120
|
-
content: [{ type: "text", text:
|
|
1121
|
-
details
|
|
1122
|
-
agentId: params.agentId,
|
|
1123
|
-
exitCode: entry.result.exitCode,
|
|
1124
|
-
usage: entry.result.usage,
|
|
1125
|
-
model: entry.result.model,
|
|
1126
|
-
stopReason: entry.result.stopReason,
|
|
1127
|
-
},
|
|
1212
|
+
content: [{ type: "text", text: getResultOutput(entry.result) }],
|
|
1213
|
+
details,
|
|
1128
1214
|
};
|
|
1129
1215
|
}
|
|
1130
1216
|
|
|
1131
|
-
// Still running —
|
|
1217
|
+
// Still running — the wait_for_agents tool can block until it finishes.
|
|
1132
1218
|
return {
|
|
1133
1219
|
content: [{ type: "text", text: "STILL RUNNING" }],
|
|
1134
|
-
details
|
|
1220
|
+
details,
|
|
1135
1221
|
};
|
|
1136
1222
|
},
|
|
1137
1223
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-coding-agent/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-coding-agent/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-coding-agent@0.78.1_ws@8.21.0_zod@4.4.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../@earendil-works/pi-coding-agent/dist/cli.js" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../@earendil-works/pi-coding-agent/dist/cli.js" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-ai@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-ai/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-ai@0.78.1_ws@8.21.0_zod@4.4.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-ai@0.78.1_ws@8.21.0_zod@4.4.3/node_modules/@earendil-works/pi-ai/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/@earendil-works+pi-ai@0.78.1_ws@8.21.0_zod@4.4.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../@earendil-works/pi-ai/dist/cli.js" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../@earendil-works/pi-ai/dist/cli.js" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../typescript/bin/tsc" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-cortex/subagent-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test --import tsx/esm wait.test.ts",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
12
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
13
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
14
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
15
|
+
"@types/node": "^22.0.0",
|
|
16
|
+
"tsx": "^4.0.0",
|
|
17
|
+
"typebox": "^1.1.38",
|
|
18
|
+
"typescript": "^5.0.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@earendil-works/pi-coding-agent": "^0.78.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { waitForAgents, type BackgroundEntry } from "./wait.ts";
|
|
5
|
+
|
|
6
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function delay(ms: number): Promise<void> {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Deferred {
|
|
13
|
+
promise: Promise<void>;
|
|
14
|
+
resolve: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function deferred(): Deferred {
|
|
18
|
+
let resolve!: () => void;
|
|
19
|
+
const promise = new Promise<void>((res) => {
|
|
20
|
+
resolve = res;
|
|
21
|
+
});
|
|
22
|
+
return { promise, resolve };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A background-run ledger matching the extension's `backgroundRuns` map:
|
|
27
|
+
* each entry holds a result (null while running) and a promise that resolves
|
|
28
|
+
* when the run finishes. `finish` mirrors the real run path — result is set
|
|
29
|
+
* before the promise settles.
|
|
30
|
+
*/
|
|
31
|
+
function makeLedger() {
|
|
32
|
+
const entries = new Map<string, { entry: BackgroundEntry<string>; done: Deferred }>();
|
|
33
|
+
|
|
34
|
+
const spawn: () => string = () => {
|
|
35
|
+
const agentId = `agent-${entries.size + 1}`;
|
|
36
|
+
const done = deferred();
|
|
37
|
+
const entry: BackgroundEntry<string> = {
|
|
38
|
+
result: null,
|
|
39
|
+
promise: done.promise,
|
|
40
|
+
};
|
|
41
|
+
entries.set(agentId, { entry, done });
|
|
42
|
+
return agentId;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const finish = async (agentId: string, result: string): Promise<void> => {
|
|
46
|
+
const run = entries.get(agentId);
|
|
47
|
+
if (!run) throw new Error(`no run for ${agentId}`);
|
|
48
|
+
// Match production ordering: result committed before the promise settles.
|
|
49
|
+
run.entry.result = result;
|
|
50
|
+
run.done.resolve();
|
|
51
|
+
await delay(0);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const finishLater = (agentId: string, result: string, ms: number): void => {
|
|
55
|
+
const run = entries.get(agentId);
|
|
56
|
+
if (!run) throw new Error(`no run for ${agentId}`);
|
|
57
|
+
void delay(ms).then(() => finish(agentId, result));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const get = (agentId: string): BackgroundEntry<string> | undefined => entries.get(agentId)?.entry;
|
|
61
|
+
|
|
62
|
+
return { spawn, finish, finishLater, get };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── waitForAgents ────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe("waitForAgents", () => {
|
|
68
|
+
it("returns immediately with the result when an agent already completed", async () => {
|
|
69
|
+
const ledger = makeLedger();
|
|
70
|
+
const agentId = ledger.spawn();
|
|
71
|
+
ledger.finishLater(agentId, "done", 1);
|
|
72
|
+
await delay(15);
|
|
73
|
+
|
|
74
|
+
const outcome = await waitForAgents(ledger.get, [agentId], { timeoutMs: 1000 });
|
|
75
|
+
assert.deepEqual(outcome.completed, [{ agentId, result: "done" }]);
|
|
76
|
+
assert.deepEqual(outcome.running, []);
|
|
77
|
+
assert.deepEqual(outcome.notFound, []);
|
|
78
|
+
assert.equal(outcome.timedOut, false);
|
|
79
|
+
assert.equal(outcome.aborted, false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns immediately when an agent id has no tracked run", async () => {
|
|
83
|
+
const ledger = makeLedger();
|
|
84
|
+
const runningId = ledger.spawn();
|
|
85
|
+
|
|
86
|
+
const outcome = await waitForAgents(ledger.get, [runningId, "agent-unknown"], { timeoutMs: 1000 });
|
|
87
|
+
assert.deepEqual(outcome.notFound, ["agent-unknown"]);
|
|
88
|
+
assert.deepEqual(outcome.running, [runningId]);
|
|
89
|
+
assert.deepEqual(outcome.completed, []);
|
|
90
|
+
assert.equal(outcome.timedOut, false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns the first completion while slower agents stay running", async () => {
|
|
94
|
+
const ledger = makeLedger();
|
|
95
|
+
const fastId = ledger.spawn();
|
|
96
|
+
const slowId = ledger.spawn();
|
|
97
|
+
ledger.finishLater(fastId, "fast result", 10);
|
|
98
|
+
|
|
99
|
+
const outcome = await waitForAgents(ledger.get, [fastId, slowId], { timeoutMs: 1000 });
|
|
100
|
+
assert.deepEqual(outcome.completed, [{ agentId: fastId, result: "fast result" }]);
|
|
101
|
+
assert.deepEqual(outcome.running, [slowId]);
|
|
102
|
+
assert.equal(outcome.timedOut, false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns the agent that finished first when completions stagger", async () => {
|
|
106
|
+
const ledger = makeLedger();
|
|
107
|
+
const a = ledger.spawn();
|
|
108
|
+
const b = ledger.spawn();
|
|
109
|
+
ledger.finishLater(b, "bee", 10);
|
|
110
|
+
ledger.finishLater(a, "aye", 40);
|
|
111
|
+
|
|
112
|
+
const outcome = await waitForAgents(ledger.get, [a, b], { timeoutMs: 1000 });
|
|
113
|
+
assert.deepEqual(
|
|
114
|
+
outcome.completed.map((c) => ({ id: c.agentId, result: c.result })),
|
|
115
|
+
[{ id: b, result: "bee" }],
|
|
116
|
+
);
|
|
117
|
+
assert.deepEqual(outcome.running, [a]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("preserves request order when several agents already completed", async () => {
|
|
121
|
+
const ledger = makeLedger();
|
|
122
|
+
const a = ledger.spawn();
|
|
123
|
+
const b = ledger.spawn();
|
|
124
|
+
await ledger.finish(a, "aye");
|
|
125
|
+
await ledger.finish(b, "bee");
|
|
126
|
+
|
|
127
|
+
const outcome = await waitForAgents(ledger.get, [a, b], { timeoutMs: 1000 });
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
outcome.completed.map((c) => ({ id: c.agentId, result: c.result })),
|
|
130
|
+
[
|
|
131
|
+
{ id: a, result: "aye" },
|
|
132
|
+
{ id: b, result: "bee" },
|
|
133
|
+
],
|
|
134
|
+
);
|
|
135
|
+
assert.deepEqual(outcome.running, []);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("marks still-running agents STILL RUNNING when the timeout elapses with no completion", async () => {
|
|
139
|
+
const ledger = makeLedger();
|
|
140
|
+
const agentId = ledger.spawn(); // never finishes
|
|
141
|
+
|
|
142
|
+
const outcome = await waitForAgents(ledger.get, [agentId], { timeoutMs: 30 });
|
|
143
|
+
assert.deepEqual(outcome.completed, []);
|
|
144
|
+
assert.deepEqual(outcome.running, [agentId]);
|
|
145
|
+
assert.equal(outcome.timedOut, true);
|
|
146
|
+
assert.equal(outcome.aborted, false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("can wait again on the same still-running id and pick up its later result", async () => {
|
|
150
|
+
const ledger = makeLedger();
|
|
151
|
+
const agentId = ledger.spawn();
|
|
152
|
+
ledger.finishLater(agentId, "eventually", 60);
|
|
153
|
+
|
|
154
|
+
const first = await waitForAgents(ledger.get, [agentId], { timeoutMs: 20 });
|
|
155
|
+
assert.deepEqual(first.completed, []);
|
|
156
|
+
assert.deepEqual(first.running, [agentId]);
|
|
157
|
+
assert.equal(first.timedOut, true);
|
|
158
|
+
|
|
159
|
+
const second = await waitForAgents(ledger.get, [agentId], { timeoutMs: 1000 });
|
|
160
|
+
assert.deepEqual(second.completed, [{ agentId, result: "eventually" }]);
|
|
161
|
+
assert.deepEqual(second.running, []);
|
|
162
|
+
assert.equal(second.timedOut, false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns immediately when the abort signal already fired", async () => {
|
|
166
|
+
const ledger = makeLedger();
|
|
167
|
+
const agentId = ledger.spawn(); // never finishes
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
controller.abort();
|
|
170
|
+
|
|
171
|
+
const outcome = await waitForAgents(ledger.get, [agentId], { timeoutMs: 1000, signal: controller.signal });
|
|
172
|
+
assert.deepEqual(outcome.completed, []);
|
|
173
|
+
assert.deepEqual(outcome.running, [agentId]);
|
|
174
|
+
assert.equal(outcome.timedOut, false);
|
|
175
|
+
assert.equal(outcome.aborted, true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("stops waiting when the abort signal fires mid-wait instead of timing out", async () => {
|
|
179
|
+
const ledger = makeLedger();
|
|
180
|
+
const agentId = ledger.spawn(); // never finishes
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
setTimeout(() => controller.abort(), 20);
|
|
183
|
+
const started = Date.now();
|
|
184
|
+
|
|
185
|
+
const outcome = await waitForAgents(ledger.get, [agentId], { timeoutMs: 1000, signal: controller.signal });
|
|
186
|
+
assert.deepEqual(outcome.completed, []);
|
|
187
|
+
assert.deepEqual(outcome.running, [agentId]);
|
|
188
|
+
assert.equal(outcome.timedOut, false);
|
|
189
|
+
assert.equal(outcome.aborted, true);
|
|
190
|
+
assert.ok(Date.now() - started < 500, "returns on abort rather than waiting out the timeout");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("applies no timeout when all requested agents resolve", async () => {
|
|
194
|
+
const ledger = makeLedger();
|
|
195
|
+
const agentId = ledger.spawn();
|
|
196
|
+
ledger.finishLater(agentId, "done", 20);
|
|
197
|
+
|
|
198
|
+
const outcome = await waitForAgents(ledger.get, [agentId], { timeoutMs: 1000 });
|
|
199
|
+
assert.deepEqual(outcome.completed, [{ agentId, result: "done" }]);
|
|
200
|
+
assert.equal(outcome.timedOut, false);
|
|
201
|
+
});
|
|
202
|
+
});
|