@jaybeeuu/agent-cortex 1.37.1 → 1.39.0

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.
Files changed (135) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +9 -8
  2. package/.github/workflows/ci.yml +190 -8
  3. package/AGENTS.md +27 -6
  4. package/CHANGELOG.md +72 -0
  5. package/README.md +156 -39
  6. package/agents/README.md +49 -13
  7. package/agents/plan/agent.md +51 -0
  8. package/agents/plan/claude/frontmatter.json +6 -0
  9. package/agents/plan/copilot/frontmatter.json +6 -0
  10. package/agents/plan/pi/frontmatter.json +6 -0
  11. package/agents/plan.agent.md +3 -0
  12. package/agents/ralph/agent.md +262 -0
  13. package/agents/ralph/claude/frontmatter.json +6 -0
  14. package/agents/ralph/claude/polling.md +31 -0
  15. package/agents/ralph/copilot/frontmatter.json +6 -0
  16. package/agents/ralph/copilot/polling.md +62 -0
  17. package/agents/ralph/pi/frontmatter.json +6 -0
  18. package/agents/ralph/pi/polling.md +74 -0
  19. package/agents/ralph-plan/agent.md +114 -0
  20. package/agents/ralph-plan/claude/frontmatter.json +6 -0
  21. package/agents/ralph-plan/copilot/frontmatter.json +6 -0
  22. package/agents/ralph-plan/pi/frontmatter.json +6 -0
  23. package/agents/ralph-plan.agent.md +6 -3
  24. package/agents/ralph.agent.md +83 -57
  25. package/agents/strategy/agent.md +104 -0
  26. package/agents/strategy/claude/frontmatter.json +6 -0
  27. package/agents/strategy/copilot/frontmatter.json +6 -0
  28. package/agents/strategy/pi/frontmatter.json +6 -0
  29. package/agents/strategy.agent.md +3 -0
  30. package/bin/agent-cortex.mjs +76 -1
  31. package/bin/installers/claude.mjs +376 -0
  32. package/bin/installers/copilot.mjs +129 -0
  33. package/bin/installers/pi.mjs +197 -0
  34. package/claude/.claude-plugin/plugin.json +1 -1
  35. package/claude/agents/plan.md +3 -1
  36. package/claude/agents/ralph-plan.md +6 -4
  37. package/claude/agents/strategy.md +3 -1
  38. package/claude/hooks/scripts/notify.mjs +54 -0
  39. package/claude/hooks.json +15 -0
  40. package/docs/claude-hooks.md +115 -0
  41. package/docs/ideas/auto-model-selection.md +75 -0
  42. package/docs/ideas/extension-manifests.md +75 -0
  43. package/docs/ideas/improve-pi-system-prompt.md +34 -0
  44. package/docs/ideas/model-selector.md +40 -0
  45. package/docs/ideas/pi-task-memory.md +34 -0
  46. package/docs/ideas/qa-extension.md +43 -0
  47. package/docs/ideas/record-ideas-as-beads.md +115 -0
  48. package/docs/ideas/subagent-visibility.md +72 -0
  49. package/docs/ideas/task-session-context.md +89 -0
  50. package/docs/ideas/unified-ci-pipeline.md +28 -0
  51. package/docs/research/skill-design-research.md +4 -4
  52. package/docs/research/skill-improvements-analysis.md +1 -1
  53. package/docs/skills/skill-anatomy.md +47 -2
  54. package/docs/technical-direction/migration-execution-labels.md +132 -0
  55. package/extensions/agent-modes/README.md +54 -0
  56. package/extensions/agent-modes/discover.test.ts +241 -0
  57. package/extensions/agent-modes/discover.ts +458 -0
  58. package/extensions/agent-modes/index.ts +13 -224
  59. package/extensions/agent-modes/node_modules/.bin/pi +21 -0
  60. package/extensions/agent-modes/node_modules/.bin/tsc +21 -0
  61. package/extensions/agent-modes/node_modules/.bin/tsserver +21 -0
  62. package/extensions/agent-modes/node_modules/.bin/tsx +21 -0
  63. package/extensions/agent-modes/package.json +20 -0
  64. package/extensions/agent-modes/tsconfig.json +12 -0
  65. package/extensions/subagent/README.md +19 -0
  66. package/extensions/subagent/index.ts +131 -45
  67. package/extensions/subagent/node_modules/.bin/pi +21 -0
  68. package/extensions/subagent/node_modules/.bin/pi-ai +21 -0
  69. package/extensions/subagent/node_modules/.bin/tsc +21 -0
  70. package/extensions/subagent/node_modules/.bin/tsserver +21 -0
  71. package/extensions/subagent/node_modules/.bin/tsx +21 -0
  72. package/extensions/subagent/package.json +23 -0
  73. package/extensions/subagent/tsconfig.json +12 -0
  74. package/extensions/subagent/wait.test.ts +202 -0
  75. package/extensions/subagent/wait.ts +118 -0
  76. package/hooks/claude/hooks.json +38 -0
  77. package/hooks/claude/scripts/notify.mjs +54 -0
  78. package/lib/cli.mjs +68 -3
  79. package/package.json +6 -4
  80. package/plugin.json +1 -1
  81. package/pnpm-workspace.yaml +1 -1
  82. package/scripts/build-copilot-agents.mjs +30 -0
  83. package/scripts/lib/compose-agent.mjs +241 -0
  84. package/skills/engineering/design-an-interface/SKILL.md +78 -58
  85. package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
  86. package/skills/engineering/grill-with-docs/SKILL.md +66 -63
  87. package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
  88. package/skills/engineering/technical-direction/FORMAT.md +38 -0
  89. package/skills/engineering/technical-direction/SKILL.md +70 -77
  90. package/skills/planning/classify-bead/SKILL.md +59 -35
  91. package/skills/planning/create-task/REFERENCE.md +40 -0
  92. package/skills/planning/create-task/SKILL.md +70 -66
  93. package/skills/planning/create-task/scripts/create-chores.ts +6 -1
  94. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
  95. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
  96. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
  97. package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
  98. package/skills/planning/prd-to-tasks/SKILL.md +86 -170
  99. package/skills/planning/record-idea/FORMAT.md +44 -0
  100. package/skills/planning/record-idea/SKILL.md +70 -53
  101. package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
  102. package/skills/planning/request-refactor-plan/SKILL.md +57 -46
  103. package/skills/planning/write-a-prd/FORMAT.md +54 -0
  104. package/skills/planning/write-a-prd/SKILL.md +49 -48
  105. package/skills/planning/write-a-ticket/SKILL.md +68 -53
  106. package/skills/productivity/handoff/SKILL.md +12 -0
  107. package/skills/productivity/init-beads/SKILL.md +48 -40
  108. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
  109. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
  110. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
  111. package/skills/productivity/write-a-skill/SKILL.md +2 -1
  112. package/skills/review/maintain-agent-docs/SKILL.md +71 -51
  113. package/skills/workflow/hitl-collab/SKILL.md +86 -17
  114. package/skills/workflow/ralph/REFERENCE.md +24 -19
  115. package/skills/workflow/ralph/SKILL.md +88 -74
  116. package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
  117. package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
  118. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
  119. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
  120. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
  121. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
  122. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
  123. package/skills/workflow/using-agent-skills/SKILL.md +122 -0
  124. package/test/claude-installer.test.mjs +248 -0
  125. package/test/cli.test.mjs +115 -2
  126. package/test/copilot-installer.test.mjs +298 -0
  127. package/test/installer.test.mjs +179 -0
  128. package/test/notify-hook.test.mjs +63 -0
  129. package/test/pi-installer.test.mjs +371 -0
  130. package/token-map.README.md +130 -0
  131. package/token-map.json +106 -0
  132. package/.github/workflows/ci.test.ts +0 -217
  133. package/.github/workflows/release.yml +0 -58
  134. package/scripts/build-claude-agents.mjs +0 -179
  135. package/scripts/publish-release.sh +0 -32
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent Modes Extension
3
3
  *
4
- * Makes the agent-cortex agents (ralph, ralph-plan, strategy) available as
4
+ * Makes the agent-cortex agents (ralph, ralph-plan, plan, strategy) available as
5
5
  * switchable PI modes. Each mode:
6
6
  * - Injects the agent's full system prompt into the running context
7
7
  * - Restricts tools based on the agent's declared tool set
@@ -9,6 +9,10 @@
9
9
  *
10
10
  * Modeled on Pi's built-in preset.ts example and R-Dson/pi-modes.
11
11
  *
12
+ * Agents are discovered from the composable source format (`agents/<name>/` +
13
+ * `agents/<name>/pi/frontmatter.json`) — see agents/README.md. Discovery and
14
+ * PI-prompt composition live in discover.ts.
15
+ *
12
16
  * Commands:
13
17
  * /agent — show mode selector
14
18
  * /agent <name> — switch to agent mode directly
@@ -16,242 +20,27 @@
16
20
  * --agent <name> — start in a specific agent mode
17
21
  */
18
22
 
19
- import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
20
23
  import { join, dirname } from "node:path";
21
24
  import { fileURLToPath } from "node:url";
22
25
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
23
26
  import { Key } from "@earendil-works/pi-tui";
24
-
25
- // ─── Types ───────────────────────────────────────────────────────────────────
26
-
27
- interface AgentDef {
28
- /** Lowercase id derived from filename (e.g. "ralph", "ralph-plan") */
29
- id: string;
30
- /** Display name from frontmatter or prettified id */
31
- name: string;
32
- /** Description from frontmatter */
33
- description: string;
34
- /** Colour for status indicator */
35
- color: string;
36
- /** PI tool names to enable (empty = all tools) */
37
- tools: string[];
38
- /** Full agent body (the system prompt to inject) */
39
- prompt: string;
40
- }
41
-
42
- interface OriginalState {
43
- tools: string[];
44
- }
45
-
46
- // ─── Discovery ───────────────────────────────────────────────────────────────
27
+ import { discoverAgents, DEFAULT_TOOLS, type AgentDef } from "./discover.ts";
28
+ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
47
29
 
48
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
49
31
  // Agents dir is <extension-root>/../../agents/ — two levels up from extensions/agent-modes/
50
32
  const AGENTS_DIR = join(__dirname, "..", "..", "agents");
51
33
 
52
- /**
53
- * Copilot CLI tool names to PI built-in tool names.
54
- * Tools not in this map are left as-is (they may be custom tools).
55
- */
56
- const COPILOT_TO_PI: Record<string, string[]> = {
57
- bash: ["bash"],
58
- view: ["read"],
59
- read: ["read"],
60
- edit: ["edit"],
61
- create: ["write"],
62
- write: ["write"],
63
- grep: ["grep"],
64
- rg: ["grep", "find"],
65
- glob: ["find", "ls"],
66
- find: ["find"],
67
- ls: ["ls"],
68
- };
69
-
70
- /**
71
- * Mapping from agent id → colour for the status indicator.
72
- */
73
- const AGENT_COLORS: Record<string, string> = {
74
- ralph: "accent",
75
- "ralph-plan": "warning",
76
- strategy: "accent",
77
- };
78
-
79
- const PERSIST_KEY = "agent-modes-state";
80
-
81
- /** Modes with no tool restriction gets the full default set. */
82
- const DEFAULT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
83
-
84
- // ─── Agent file parsing ──────────────────────────────────────────────────────
85
-
86
- function parseFrontmatter(raw: string): {
87
- name?: string;
88
- description?: string;
89
- tools?: string[];
90
- prompt: string;
91
- } {
92
- const lines = raw.split(/\r?\n/);
93
- if (lines[0] !== "---") return { prompt: raw };
94
-
95
- // Find closing ---
96
- let sep = -1;
97
- for (let i = 1; i < lines.length; i++) {
98
- if (lines[i].trim() === "---") {
99
- sep = i;
100
- break;
101
- }
102
- }
103
- if (sep === -1) return { prompt: raw };
104
-
105
- const yamlLines = lines.slice(1, sep);
106
- const prompt = lines.slice(sep + 1).join("\n").trim();
107
-
108
- let name: string | undefined;
109
- let description: string | undefined;
110
- let tools: string[] | undefined;
111
-
112
- for (const line of yamlLines) {
113
- const trimmed = line.trim();
114
- const colonIdx = trimmed.indexOf(":");
115
- if (colonIdx === -1) continue;
116
-
117
- const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
118
- const val = trimmed.slice(colonIdx + 1).trim();
119
-
120
- if (key === "name") {
121
- // Strip surrounding quotes
122
- name = val.replace(/^["']|["']$/g, "");
123
- } else if (key === "description") {
124
- description = val.replace(/^["']|["']$/g, "");
125
- }
126
- }
127
-
128
- // Parse tools from the tools frontmatter.
129
- // Supports two formats:
130
- // tools: ["bash", "view"] (JSON array, single line)
131
- // tools: (YAML list, subsequent lines)
132
- // - bash
133
- // - view
134
- let inToolsBlock = false;
135
- for (const line of yamlLines) {
136
- const trimmed = line.trim();
137
-
138
- // Check for inline JSON array: tools: ["foo", "bar"]
139
- const colonIdx = trimmed.indexOf(":");
140
- if (colonIdx !== -1) {
141
- const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
142
- const val = trimmed.slice(colonIdx + 1).trim();
143
- if (key === "tools") {
144
- if (val.startsWith("[")) {
145
- // Inline JSON array
146
- try {
147
- tools = JSON.parse(val);
148
- } catch {
149
- // Malformed JSON — fall through
150
- }
151
- continue;
152
- }
153
- if (val === "[]" || val === "") {
154
- // Empty tools — explicit
155
- tools = [];
156
- continue;
157
- }
158
- }
159
- }
160
-
161
- // YAML block format: "tools:" on its own line, items on subsequent lines
162
- if (trimmed === "tools:") {
163
- inToolsBlock = true;
164
- if (!tools) tools = [];
165
- continue;
166
- }
167
- if (inToolsBlock) {
168
- // Check if this line is still part of the tools block (indented or starts with -)
169
- if (line.startsWith(" ") || line.startsWith("\t") || trimmed.startsWith("- ")) {
170
- const item = trimmed.replace(/^- /, "").trim().replace(/^["']|["']$/g, "");
171
- if (item) {
172
- if (!tools) tools = [];
173
- tools.push(item);
174
- }
175
- } else if (trimmed !== "") {
176
- // Non-empty, non-indented line → tools block ended
177
- inToolsBlock = false;
178
- }
179
- }
180
- }
181
-
182
- return { name, description, tools, prompt };
183
- }
184
-
185
- function mapCopilotToolsToPi(copilotTools: string[] | undefined): string[] | undefined {
186
- if (!copilotTools || copilotTools.length === 0) return undefined;
187
-
188
- const piTools = new Set<string>();
189
- for (const ct of copilotTools) {
190
- const mapped = COPILOT_TO_PI[ct];
191
- if (mapped) {
192
- for (const t of mapped) piTools.add(t);
193
- } else {
194
- // Pass through unknown tool names (could be custom tools)
195
- piTools.add(ct);
196
- }
197
- }
198
- return [...piTools].sort();
199
- }
200
-
201
- function discoverAgents(): AgentDef[] {
202
- if (!existsSync(AGENTS_DIR)) {
203
- console.warn(`[agent-modes] Agents directory not found: ${AGENTS_DIR}`);
204
- return [];
205
- }
206
-
207
- const files = readdirSync(AGENTS_DIR)
208
- .filter((f) => f.endsWith(".agent.md"))
209
- .sort();
210
-
211
- const agents: AgentDef[] = [];
212
-
213
- for (const file of files) {
214
- const filePath = join(AGENTS_DIR, file);
215
- if (!statSync(filePath).isFile()) continue;
216
-
217
- let raw: string;
218
- try {
219
- raw = readFileSync(filePath, "utf-8");
220
- } catch (err) {
221
- console.warn(`[agent-modes] Failed to read ${file}: ${err}`);
222
- continue;
223
- }
224
-
225
- const parsed = parseFrontmatter(raw);
226
- const id = file.replace(/\.agent\.md$/i, "").toLowerCase();
227
- const name = parsed.name || prettifyId(id);
228
- const tools = mapCopilotToolsToPi(parsed.tools);
229
- const color = AGENT_COLORS[id] || "accent";
230
-
231
- agents.push({
232
- id,
233
- name,
234
- description: parsed.description || "",
235
- color,
236
- tools: tools || [...DEFAULT_TOOLS],
237
- prompt: parsed.prompt || `You are ${name}. ${parsed.description || ""}`,
238
- });
239
- }
240
-
241
- return agents;
34
+ interface OriginalState {
35
+ tools: string[];
242
36
  }
243
37
 
244
- function prettifyId(id: string): string {
245
- return id
246
- .split("-")
247
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
248
- .join(" ");
249
- }
38
+ const PERSIST_KEY = "agent-modes-state";
250
39
 
251
40
  // ─── Extension ───────────────────────────────────────────────────────────────
252
41
 
253
42
  export default function agentModesExtension(pi: ExtensionAPI): void {
254
- const agents = discoverAgents();
43
+ const agents = discoverAgents(AGENTS_DIR);
255
44
 
256
45
  if (agents.length === 0) {
257
46
  console.warn("[agent-modes] No agents found. Extension disabled.");
@@ -270,7 +59,7 @@ export default function agentModesExtension(pi: ExtensionAPI): void {
270
59
  return agents[activeAgentIndex].name;
271
60
  }
272
61
 
273
- function getActiveColor(): string {
62
+ function getActiveColor(): ThemeColor {
274
63
  if (activeAgentIndex >= agents.length || activeAgentIndex < 0) return "accent";
275
64
  return agents[activeAgentIndex].color;
276
65
  }
@@ -504,4 +293,4 @@ export default function agentModesExtension(pi: ExtensionAPI): void {
504
293
  }
505
294
  }
506
295
  });
507
- }
296
+ }
@@ -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/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,20 @@
1
+ {
2
+ "name": "@agent-cortex/agent-modes",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "node --test --import tsx/esm discover.test.ts",
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "devDependencies": {
11
+ "@earendil-works/pi-coding-agent": "^0.78.1",
12
+ "@earendil-works/pi-tui": "^0.78.1",
13
+ "@types/node": "^22.0.0",
14
+ "tsx": "^4.0.0",
15
+ "typescript": "^5.0.0"
16
+ },
17
+ "peerDependencies": {
18
+ "@earendil-works/pi-coding-agent": "^0.78.1"
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "allowImportingTsExtensions": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["*.ts"]
12
+ }
@@ -96,6 +96,25 @@ Use a chain: first have scout find the read tool, then have planner suggest impr
96
96
  | Parallel | `{ tasks: [...] }` | Multiple agents run concurrently (max 8, 4 concurrent) |
97
97
  | Chain | `{ chain: [...] }` | Sequential with `{previous}` placeholder |
98
98
 
99
+ ## Background subagents (task / wait_for_agents / read_agent)
100
+
101
+ Beyond the all-in-one `subagent` tool, the extension ships three bottom-up tools for
102
+ fire-and-forget orchestration:
103
+
104
+ - **`task`** — spawn a background sub-agent and return its agent ID immediately. The
105
+ agent runs detached from the current tool call.
106
+ - **`wait_for_agents`** — block until at least one background sub-agent completes.
107
+ Pass the `agentIds` (plus optional `timeout` in seconds, default 120); it returns the
108
+ results of every agent that has finished and `STILL RUNNING` for the rest. This is the
109
+ event-driven way to run parallel orchestration: dispatch 5 tasks, then one
110
+ `wait_for_agents` call returns as soon as any of them finishes.
111
+ - **`read_agent`** — fallback: read one specific agent's output once you know it has
112
+ finished (or query its `STILL RUNNING` state directly).
113
+
114
+ These mirror Copilot CLI's `task`/`read_agent` model; `wait_for_agents` adds the
115
+ blocking wait that Copilot's poll loop does manually. Abort (Ctrl+C) propagates: a
116
+ pending `wait_for_agents` returns immediately with whatever is known so far.
117
+
99
118
  ## Output Display
100
119
 
101
120
  **Collapsed view** (default):
@@ -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 the read_agent tool to retrieve the result when ready. " +
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: (async () => {
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: { error: "not found", agentId: params.agentId },
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: output }],
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 — check if the promise is still active
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: { status: "running", agentId: params.agentId },
1220
+ details,
1135
1221
  };
1136
1222
  },
1137
1223
  });