@jaybeeuu/agent-cortex 1.37.1 → 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.
Files changed (92) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +9 -8
  2. package/.github/workflows/ci.yml +190 -8
  3. package/AGENTS.md +25 -6
  4. package/CHANGELOG.md +42 -0
  5. package/README.md +132 -33
  6. package/agents/README.md +47 -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 +5 -2
  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 +68 -1
  31. package/bin/installers/claude.mjs +262 -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 +5 -3
  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/improve-pi-system-prompt.md +34 -0
  43. package/docs/ideas/model-selector.md +40 -0
  44. package/docs/ideas/pi-task-memory.md +34 -0
  45. package/docs/ideas/qa-extension.md +43 -0
  46. package/docs/ideas/unified-ci-pipeline.md +28 -0
  47. package/docs/skills/skill-anatomy.md +9 -0
  48. package/extensions/agent-modes/README.md +54 -0
  49. package/extensions/agent-modes/discover.test.ts +241 -0
  50. package/extensions/agent-modes/discover.ts +458 -0
  51. package/extensions/agent-modes/index.ts +13 -224
  52. package/extensions/agent-modes/node_modules/.bin/pi +21 -0
  53. package/extensions/agent-modes/node_modules/.bin/tsc +21 -0
  54. package/extensions/agent-modes/node_modules/.bin/tsserver +21 -0
  55. package/extensions/agent-modes/node_modules/.bin/tsx +21 -0
  56. package/extensions/agent-modes/package.json +20 -0
  57. package/extensions/agent-modes/tsconfig.json +12 -0
  58. package/extensions/subagent/README.md +19 -0
  59. package/extensions/subagent/index.ts +131 -45
  60. package/extensions/subagent/node_modules/.bin/pi +21 -0
  61. package/extensions/subagent/node_modules/.bin/pi-ai +21 -0
  62. package/extensions/subagent/node_modules/.bin/tsc +21 -0
  63. package/extensions/subagent/node_modules/.bin/tsserver +21 -0
  64. package/extensions/subagent/node_modules/.bin/tsx +21 -0
  65. package/extensions/subagent/package.json +23 -0
  66. package/extensions/subagent/tsconfig.json +12 -0
  67. package/extensions/subagent/wait.test.ts +202 -0
  68. package/extensions/subagent/wait.ts +118 -0
  69. package/hooks/claude/hooks.json +38 -0
  70. package/hooks/claude/scripts/notify.mjs +54 -0
  71. package/lib/cli.mjs +62 -3
  72. package/package.json +6 -4
  73. package/plugin.json +1 -1
  74. package/scripts/build-copilot-agents.mjs +30 -0
  75. package/scripts/lib/compose-agent.mjs +241 -0
  76. package/skills/engineering/design-an-interface/SKILL.md +1 -1
  77. package/skills/engineering/improve-codebase-architecture/SKILL.md +1 -1
  78. package/skills/workflow/ralph/REFERENCE.md +2 -2
  79. package/skills/workflow/ralph/SKILL.md +6 -6
  80. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
  81. package/skills/workflow/using-agent-skills/SKILL.md +122 -0
  82. package/test/cli.test.mjs +115 -2
  83. package/test/copilot-installer.test.mjs +298 -0
  84. package/test/installer.test.mjs +179 -0
  85. package/test/notify-hook.test.mjs +63 -0
  86. package/test/pi-installer.test.mjs +371 -0
  87. package/token-map.README.md +128 -0
  88. package/token-map.json +106 -0
  89. package/.github/workflows/ci.test.ts +0 -217
  90. package/.github/workflows/release.yml +0 -58
  91. package/scripts/build-claude-agents.mjs +0 -179
  92. package/scripts/publish-release.sh +0 -32
@@ -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,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
+ }
@@ -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
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Blocking wait over background subagent runs.
3
+ *
4
+ * Factored out of the extension entrypoint so the wait semantics can be tested
5
+ * without spawning real `pi` processes: the run store is injected as a lookup
6
+ * function and completion is driven by each entry's `promise` (which resolves
7
+ * exactly when the run's result is committed).
8
+ */
9
+
10
+ export interface BackgroundEntry<T> {
11
+ /** The run's result once it finishes; null while the agent is still working. */
12
+ result: T | null;
13
+ /** Resolves when the run finishes (success, failure, or abort). */
14
+ promise: Promise<unknown>;
15
+ }
16
+
17
+ export interface CompletedRun<T> {
18
+ agentId: string;
19
+ result: T;
20
+ }
21
+
22
+ export interface WaitResult<T> {
23
+ /** Requested agents whose result is now available, in requested order. */
24
+ completed: CompletedRun<T>[];
25
+ /** Requested agents still working when the wait ended. */
26
+ running: string[];
27
+ /** Requested IDs with no tracked run (never dispatched or expired). */
28
+ notFound: string[];
29
+ /** True when the wait ended because the timeout elapsed with agents still running. */
30
+ timedOut: boolean;
31
+ /** True when the wait ended because the abort signal fired. */
32
+ aborted: boolean;
33
+ }
34
+
35
+ export interface WaitOptions {
36
+ /** Block for at most this many milliseconds before returning (default 120_000). */
37
+ timeoutMs?: number;
38
+ /** Abort the wait early (e.g. user Ctrl+C). */
39
+ signal?: AbortSignal;
40
+ }
41
+
42
+ export const DEFAULT_WAIT_TIMEOUT_MS = 120_000;
43
+
44
+ type GetEntry<T> = (agentId: string) => BackgroundEntry<T> | undefined;
45
+
46
+ function scan<T>(getEntry: GetEntry<T>, agentIds: string[], timedOut: boolean, aborted: boolean): WaitResult<T> {
47
+ const completed: CompletedRun<T>[] = [];
48
+ const running: string[] = [];
49
+ const notFound: string[] = [];
50
+ for (const agentId of agentIds) {
51
+ const entry = getEntry(agentId);
52
+ if (!entry) notFound.push(agentId);
53
+ else if (entry.result !== null) completed.push({ agentId, result: entry.result });
54
+ else running.push(agentId);
55
+ }
56
+ return { completed, running, notFound, timedOut, aborted };
57
+ }
58
+
59
+ export async function waitForAgents<T>(
60
+ getEntry: GetEntry<T>,
61
+ agentIds: string[],
62
+ options: WaitOptions = {},
63
+ ): Promise<WaitResult<T>> {
64
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
65
+ const { signal } = options;
66
+
67
+ const hasResolution = (result: WaitResult<T>): boolean => result.completed.length > 0 || result.notFound.length > 0;
68
+
69
+ // Fast path: any requested agent that already resolved (completed or unknown)
70
+ // ends the wait — the caller dispatches replacements before waiting again.
71
+ const initial = scan(getEntry, agentIds, false, false);
72
+ if (hasResolution(initial)) return initial;
73
+
74
+ // Single shared abort racer so a fired signal always wins the next race.
75
+ const abortRacer: Promise<"aborted"> | undefined = signal
76
+ ? new Promise((resolve) => {
77
+ if (signal.aborted) resolve("aborted");
78
+ else signal.addEventListener("abort", () => resolve("aborted"), { once: true });
79
+ })
80
+ : undefined;
81
+
82
+ const deadline = Date.now() + timeoutMs;
83
+ let timedOut = false;
84
+ let aborted = false;
85
+
86
+ while (!timedOut && !aborted) {
87
+ const runningPromises: Promise<unknown>[] = agentIds
88
+ .map(getEntry)
89
+ .filter((entry): entry is BackgroundEntry<T> => entry !== undefined && entry.result === null)
90
+ .map((entry) => entry.promise);
91
+
92
+ const remaining = deadline - Date.now();
93
+ if (remaining <= 0) {
94
+ timedOut = true;
95
+ break;
96
+ }
97
+
98
+ const racers: Promise<"completed" | "timeout" | "aborted">[] = [];
99
+ if (runningPromises.length > 0) {
100
+ // A settled run promise means its result was committed before settling.
101
+ racers.push(Promise.race(runningPromises.map((p) => p.then(() => "completed" as const, () => "completed" as const))));
102
+ }
103
+ racers.push(new Promise((resolve) => setTimeout(() => resolve("timeout" as const), remaining)));
104
+ if (abortRacer) racers.push(abortRacer);
105
+
106
+ const outcome = await Promise.race(racers);
107
+ if (outcome === "timeout") timedOut = true;
108
+ else if (outcome === "aborted") aborted = true;
109
+
110
+ // "completed": at least one run promise settled — re-scan. Entries whose
111
+ // result is now available end the wait; everything else loops back under
112
+ // the same deadline.
113
+ const current = scan(getEntry, agentIds, timedOut, aborted);
114
+ if (hasResolution(current) || timedOut || aborted) return current;
115
+ }
116
+
117
+ return scan(getEntry, agentIds, timedOut, aborted);
118
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "echo 'Load AGENTS.md and active beads context before taking action.'"
10
+ },
11
+ {
12
+ "type": "command",
13
+ "command": "echo 'Style policy: this project ships style skills — prefer them over ad-hoc choices. Invoke style-code BEFORE writing or editing code (and when refactoring); style-tests BEFORE writing or changing tests; style-documentation BEFORE writing or updating docs; style-comms BEFORE composing commit messages, PR descriptions, tickets, or other written deliverables.'"
14
+ },
15
+ {
16
+ "type": "command",
17
+ "command": "echo 'Skill policy: agent-cortex ships skills — prefer them over ad-hoc choices. using-agent-skills routes work to the right skill (check it first when unsure); bd-tool tracks beads tasks (run bd ready for available work); git-workflow enforces branch and PR discipline. Invoke skills via the Skill tool.'"
18
+ },
19
+ {
20
+ "type": "command",
21
+ "command": "echo 'Be extremely concise. Sacrifice grammar for the sake of concision.'"
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "Notification": [
27
+ {
28
+ "matcher": "agent_completed|agent_needs_input|permission_prompt",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/notify.mjs\""
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code Notification hook — desktop notification on task completion/error.
3
+ //
4
+ // Reads the Notification hook JSON payload from stdin and prints a
5
+ // `{ "terminalSequence": "..." }` response for Claude Code to emit. Hooks run
6
+ // without a controlling terminal, so OSC sequences must go through Claude Code
7
+ // itself (it handles tmux/screen re-wrapping); writing /dev/tty directly fails.
8
+ //
9
+ // Mapped from the pi `notify` extension (extensions/notify/): pi notifies on
10
+ // agent_end for multi-turn or long tasks. Claude Code's Notification matcher is an
11
+ // exact-string list over `notification_type` values (there are no `success`/
12
+ // `error` types) — hooks.json selects `agent_completed|agent_needs_input|permission_prompt`
13
+ // to mirror "task completed / failed / needs approval". The payload already carries
14
+ // a message + title, so no summarization LLM call is needed here.
15
+ //
16
+ // Zero dependencies; runs on the Node that ships with Claude Code.
17
+
18
+ let raw = "";
19
+ for await (const chunk of process.stdin) {
20
+ raw += chunk;
21
+ }
22
+
23
+ let payload = {};
24
+ try {
25
+ payload = raw.trim() ? JSON.parse(raw) : {};
26
+ } catch {
27
+ process.exit(0); // never fail a session on an unparsable payload
28
+ }
29
+
30
+ const message = typeof payload.message === "string" ? payload.message.trim() : "";
31
+ if (!message) process.exit(0); // nothing worth notifying (e.g. autocompact events)
32
+
33
+ const title =
34
+ typeof payload.title === "string" && payload.title.trim() ? payload.title.trim() : "Claude Code";
35
+ const type = typeof payload.notification_type === "string" ? payload.notification_type : "";
36
+
37
+ process.stdout.write(JSON.stringify({ terminalSequence: pickSequence(title, message, type) }) + "\n");
38
+
39
+ /** Pick an OSC notification escape sequence for the detected terminal. */
40
+ function pickSequence(title, message, type) {
41
+ const body = message.replace(/\u001b/g, "").replace(/\n/g, " ").trim();
42
+ const label = type ? `${type}: ${title}` : title;
43
+
44
+ if (process.env.KITTY_WINDOW_ID) {
45
+ // Kitty: two-part persistent notification (title + body) — same form as the pi notify extension.
46
+ return `\u001b]99;i=1:d=1;${title}\u001b\\\u001b]99;i=1:p=body;${body}\u001b\\`;
47
+ }
48
+ if (process.env.TERM_PROGRAM === "iTerm.app" || process.env.ITERM_SESSION_ID) {
49
+ // iTerm2 / ConEmu / Windows Terminal / WezTerm: OSC 9.
50
+ return `\u001b]9;${label}: ${body}\u0007`;
51
+ }
52
+ // urxvt / Ghostty / Warp, and default fallback: OSC 777.
53
+ return `\u001b]777;notify;${label};${body}\u0007`;
54
+ }
package/lib/cli.mjs CHANGED
@@ -1,5 +1,15 @@
1
1
  const SUPPORTED_HARNESSES = ["copilot", "claude", "pi"];
2
2
 
3
+ // Options accepted by `install <harness>`. `--output`/`--dry-run` are shared by
4
+ // the claude and pi installers; `--plugin-root` is pi-specific. Unknown or
5
+ // malformed options surface as `optionError` on the parsed result; the CLI
6
+ // prints them and exits 1.
7
+ const INSTALL_OPTIONS = {
8
+ "--dry-run": { key: "dryRun" },
9
+ "--output": { key: "output" },
10
+ "--plugin-root": { key: "pluginRoot" },
11
+ };
12
+
3
13
  export function parseArgs(argv) {
4
14
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
5
15
  return { command: "help" };
@@ -8,8 +18,42 @@ export function parseArgs(argv) {
8
18
  const [command, ...rest] = argv;
9
19
 
10
20
  if (command === "install") {
11
- const harness = rest[0] ?? null;
12
- return { command: "install", harness };
21
+ let harness = null;
22
+ let i = 0;
23
+ if (rest.length > 0 && !rest[0].startsWith("--")) {
24
+ harness = rest[0];
25
+ i = 1;
26
+ }
27
+
28
+ const options = {};
29
+ for (; i < rest.length; i++) {
30
+ const arg = rest[i];
31
+ // Support both `--output=<dir>` and `--output <dir>`.
32
+ const inlineValue = arg.startsWith("--output=") ? arg.slice("--output=".length) : null;
33
+ const spec = inlineValue !== null ? INSTALL_OPTIONS["--output"] : INSTALL_OPTIONS[arg];
34
+ if (!spec) {
35
+ return { command: "install", harness, optionError: `Unknown option "${arg}". Run "agent-cortex --help" for usage.` };
36
+ }
37
+ if (spec.key === "dryRun") {
38
+ options.dryRun = true;
39
+ continue;
40
+ }
41
+ if (inlineValue !== null) {
42
+ options[spec.key] = inlineValue;
43
+ continue;
44
+ }
45
+ const value = rest[i + 1];
46
+ if (value == null || value.startsWith("--")) {
47
+ return { command: "install", harness, optionError: `Missing value for "${arg}". Run "agent-cortex --help" for usage.` };
48
+ }
49
+ options[spec.key] = value;
50
+ i += 1;
51
+ }
52
+
53
+ if (Object.keys(options).length === 0) {
54
+ return { command: "install", harness };
55
+ }
56
+ return { command: "install", harness, ...options };
13
57
  }
14
58
 
15
59
  return { command: "unknown", name: command };
@@ -21,6 +65,21 @@ export function buildHelpText() {
21
65
  Commands:
22
66
  install <harness> Install agent-cortex for a harness
23
67
  Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}
68
+ Options (claude):
69
+ --output <dir> Write the plugin subtree to <dir>
70
+ (default: <package root>/claude)
71
+ --dry-run Print what would be generated without writing
72
+ Options (copilot):
73
+ --output <dir> Write the flat *.agent.md files to <dir>
74
+ (default: <package root>/agents — the dir
75
+ plugin.json "agents" scans)
76
+ --dry-run Print what would be generated without writing
77
+ Options (pi):
78
+ --dry-run Show what would be installed without writing
79
+ --output <dir> Install into <dir>/agents and <dir>/skills
80
+ (default: ~/.pi/agent)
81
+ --plugin-root <dir> Override the plugin root used for {{PATH:...}}
82
+ tokens (default: token-map.json's pi value)
24
83
  --help, -h Show this help text`;
25
84
  }
26
85
 
@@ -34,4 +93,4 @@ export function validateHarness(harness) {
34
93
  return { ok: true };
35
94
  }
36
95
 
37
- export { SUPPORTED_HARNESSES };
96
+ export { SUPPORTED_HARNESSES };