@jaybeeuu/agent-cortex 1.39.0 → 1.41.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.
- package/.agents/skills/style-versioning/SKILL.md +1 -1
- package/.github/workflows/ci.yml +47 -14
- package/AGENTS.md +35 -8
- package/CHANGELOG.md +60 -0
- package/README.md +174 -97
- package/agents/README.md +14 -11
- package/bin/agent-cortex.mjs +55 -10
- package/bin/installers/claude.mjs +674 -174
- package/bin/installers/copilot.mjs +13 -18
- package/bin/installers/pi.mjs +293 -51
- package/claude/settings.json +4 -0
- package/{claude → claude-extras}/scripts/statusline-command.sh +32 -4
- package/docs/claude-hooks.md +15 -3
- package/docs/ideas/ban-content-assertion-tests.md +73 -0
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
- package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
- package/docs/ideas/extension-manifests.md +55 -14
- package/docs/ideas/improve-pi-system-prompt.md +5 -2
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
- package/docs/ideas/record-idea-implementation-questions.md +68 -0
- package/docs/ideas/self-flushing-session.md +72 -0
- package/docs/ideas/state-in-beads-not-md.md +69 -0
- package/docs/ideas/task-session-context.local.md +95 -0
- package/docs/ideas/test-data-creation-helpers.md +92 -0
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
- package/docs/technical-direction/harness-config.md +161 -0
- package/docs/technical-direction/migration-execution-labels.md +60 -23
- package/extensions/agent-guardrails/README.md +45 -0
- package/extensions/agent-guardrails/guardrails.test.ts +42 -0
- package/extensions/agent-guardrails/guardrails.ts +66 -0
- package/extensions/agent-guardrails/index.ts +22 -0
- package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
- package/extensions/agent-guardrails/package.json +19 -0
- package/extensions/agent-guardrails/tsconfig.json +12 -0
- package/extensions/agent-modes/README.md +12 -5
- package/extensions/agent-modes/discover.test.ts +30 -7
- package/extensions/agent-modes/discover.ts +1 -1
- package/extensions/agent-modes/index.test.ts +208 -0
- package/extensions/agent-modes/index.ts +23 -6
- package/extensions/agent-modes/package.json +1 -1
- package/extensions/skill-stats/index.test.ts +3 -2
- package/lib/cli.mjs +30 -12
- package/lib/pi-packages.mjs +169 -0
- package/package.json +8 -4
- package/pi/settings.json +23 -12
- package/plugin.json +1 -1
- package/scripts/build-copilot-agents.mjs +1 -1
- package/scripts/check-no-sync.mjs +49 -0
- package/scripts/lib/compose-agent.mjs +32 -14
- package/scripts/lib/fs.mjs +31 -0
- package/skills/engineering/grill-with-docs/SKILL.md +1 -1
- package/skills/planning/classify-bead/REFERENCE.md +85 -0
- package/skills/planning/classify-bead/SKILL.md +32 -36
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
- package/skills/planning/create-task/SKILL.md +19 -10
- package/skills/planning/write-a-prd/SKILL.md +2 -2
- package/skills/style/style-code/REFERENCE.md +32 -2
- package/skills/style/style-code/SKILL.md +5 -21
- package/skills/style/style-tests/SKILL.md +69 -17
- package/skills/workflow/hitl-collab/SKILL.md +1 -1
- package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
- package/test/classify-bead.test.mjs +277 -0
- package/test/claude-installer.test.mjs +199 -139
- package/test/cli.test.mjs +95 -41
- package/test/copilot-installer.test.mjs +92 -82
- package/test/fs.test.mjs +76 -0
- package/test/helpers/fake-claude.mjs +152 -0
- package/test/installer.test.mjs +542 -57
- package/test/pi-installer.test.mjs +457 -102
- package/test/pi-packages.test.mjs +295 -0
- package/token-map.README.md +23 -12
- package/token-map.json +8 -7
- package/.claude-plugin/marketplace.json +0 -16
- package/claude/.claude-plugin/plugin.json +0 -11
- package/claude/agents/plan.md +0 -58
- package/claude/agents/ralph-plan.md +0 -121
- package/claude/agents/ralph.md +0 -193
- package/claude/agents/strategy.md +0 -111
- package/claude/hooks/scripts/notify.mjs +0 -54
- package/claude/hooks.json +0 -38
- /package/{claude → claude-extras}/.mcp.json +0 -0
|
@@ -137,8 +137,8 @@ describe("discoverAgents (composable source format)", () => {
|
|
|
137
137
|
|
|
138
138
|
// {{TOOL:bash}} → bash.
|
|
139
139
|
assert.match(plan.prompt, /Run with bash\./);
|
|
140
|
-
// {{TOOL:ask_user}} →
|
|
141
|
-
|
|
140
|
+
// {{TOOL:ask_user}} → ask_questions (pi's native structured-questions tool).
|
|
141
|
+
assert.match(plan.prompt, /Ask the user with ask_questions\./);
|
|
142
142
|
assert.doesNotMatch(plan.prompt, /\{\{TOOL:ask_user\}\}/);
|
|
143
143
|
// Relative {{PATH:...}} resolves against the actual package root (dir of agents/).
|
|
144
144
|
assert.match(plan.prompt, new RegExp(`Read the plan skill at ${escapeRegExp(fx.dir)}/skills/workflow/plan/SKILL\\.md\\.`));
|
|
@@ -147,7 +147,7 @@ describe("discoverAgents (composable source format)", () => {
|
|
|
147
147
|
assert.doesNotMatch(plan.prompt, /\{\{/);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
it("
|
|
150
|
+
it("maps ask_user → ask_questions and skill → read into the PI tool set", () => {
|
|
151
151
|
const fx = track(makeFixture());
|
|
152
152
|
writeComposableAgent(fx, "plan", {
|
|
153
153
|
frontmatter: {
|
|
@@ -159,8 +159,30 @@ describe("discoverAgents (composable source format)", () => {
|
|
|
159
159
|
|
|
160
160
|
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
161
161
|
assert.equal(agents.length, 1);
|
|
162
|
-
// ask_user →
|
|
163
|
-
assert.deepEqual(agents[0].tools, ["bash", "fetch_content"]);
|
|
162
|
+
// ask_user → ask_questions, skill → read (pi reads skill files), web_fetch → fetch_content.
|
|
163
|
+
assert.deepEqual(agents[0].tools, ["ask_questions", "bash", "fetch_content", "read"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("warns and omits a tool whose pi mapping is null", () => {
|
|
167
|
+
const fx = track(makeFixture());
|
|
168
|
+
const NULL_MAP = {
|
|
169
|
+
tools: { ask_user: { copilot: "ask_user", claude: "AskUserQuestion", pi: null } },
|
|
170
|
+
paths: {},
|
|
171
|
+
} as TokenMap;
|
|
172
|
+
writeComposableAgent(fx, "plan", {
|
|
173
|
+
frontmatter: {
|
|
174
|
+
name: "agent-cortex:plan",
|
|
175
|
+
description: "Planner",
|
|
176
|
+
tools: ["ask_user", "bash"],
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const agents = discoverAgents(fx.agentsDir, NULL_MAP);
|
|
181
|
+
assert.equal(agents.length, 1);
|
|
182
|
+
// With a null pi column the entry is omitted (and warned about), per the contract.
|
|
183
|
+
// (bash survives so the result is observable — an all-dropped list would be
|
|
184
|
+
// normalised to DEFAULT_TOOLS, which is "no restriction", not the omit result.)
|
|
185
|
+
assert.deepEqual(agents[0].tools, ["bash"]);
|
|
164
186
|
});
|
|
165
187
|
|
|
166
188
|
it("uses the built-in fallback map when token-map.json is unavailable", () => {
|
|
@@ -230,8 +252,9 @@ describe("loadTokenMap", () => {
|
|
|
230
252
|
it("reads the real token-map.json at the package root", () => {
|
|
231
253
|
assert.ok(REAL_TOKEN_MAP);
|
|
232
254
|
assert.equal(typeof REAL_TOKEN_MAP?.tools?.bash?.pi, "string");
|
|
233
|
-
// ask_user
|
|
234
|
-
assert.equal(REAL_TOKEN_MAP?.tools?.ask_user?.pi,
|
|
255
|
+
// ask_user → ask_questions (pi's native structured-questions tool); skill → read.
|
|
256
|
+
assert.equal(REAL_TOKEN_MAP?.tools?.ask_user?.pi, "ask_questions");
|
|
257
|
+
assert.equal(REAL_TOKEN_MAP?.tools?.skill?.pi, "read");
|
|
235
258
|
assert.ok(REAL_TOKEN_MAP?.paths?.agents_dir);
|
|
236
259
|
});
|
|
237
260
|
});
|
|
@@ -120,7 +120,7 @@ function prettifyId(id: string): string {
|
|
|
120
120
|
* Translate a frontmatter tool list into PI tool names.
|
|
121
121
|
*
|
|
122
122
|
* With token-map.json: canonical keys are translated via the pi column; a null
|
|
123
|
-
* mapping means PI has no equivalent
|
|
123
|
+
* mapping means PI has no equivalent for that tool and the entry is omitted;
|
|
124
124
|
* a name that is not a canonical key passes through untouched (it may be a
|
|
125
125
|
* native PI tool such as `fetch_content`). Without the map, the built-in
|
|
126
126
|
* COPILOT_TO_PI fallback applies and unknown names pass through.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { type AgentDef, discoverAgents } from "./discover.ts";
|
|
8
|
+
import agentModesExtension, { registerAgentFlag } from "./index.ts";
|
|
9
|
+
|
|
10
|
+
const AGENTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
11
|
+
|
|
12
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function agent(id: string): AgentDef {
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
name: `agent-cortex:${id}`,
|
|
18
|
+
description: `${id} agent`,
|
|
19
|
+
color: "accent",
|
|
20
|
+
tools: [],
|
|
21
|
+
prompt: "",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type RegisterFlagArgs = Parameters<ExtensionAPI["registerFlag"]>;
|
|
26
|
+
/** A handler recorded from `pi.on`, invoked via Reflect.apply so its exact
|
|
27
|
+
* overload signature does not leak into the fake. */
|
|
28
|
+
type RecordedHandler = (...args: never[]) => unknown;
|
|
29
|
+
|
|
30
|
+
interface FakeContext {
|
|
31
|
+
ui: {
|
|
32
|
+
statuses: Array<{ id: string; text: string | undefined }>;
|
|
33
|
+
notifications: Array<{ message: string; level: string }>;
|
|
34
|
+
setStatus(id: string, text: string | undefined): void;
|
|
35
|
+
notify(message: string, level: string): void;
|
|
36
|
+
theme: { fg(color: string, text: string): string };
|
|
37
|
+
};
|
|
38
|
+
sessionManager: { getEntries(): unknown[] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FakePi {
|
|
42
|
+
pi: ExtensionAPI;
|
|
43
|
+
flags: RegisterFlagArgs[];
|
|
44
|
+
handlers: Map<string, RecordedHandler[]>;
|
|
45
|
+
setActiveToolsCalls: string[][];
|
|
46
|
+
/** Simulate pi dispatching an event to the handlers registered for it. */
|
|
47
|
+
fire(event: string, ...args: unknown[]): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a full ExtensionAPI test double that records the calls agent-modes
|
|
52
|
+
* makes. `getFlag` resolves against `flagValues`, and `getActiveTools` returns
|
|
53
|
+
* a fixed baseline so setActiveTools calls are observable.
|
|
54
|
+
*/
|
|
55
|
+
function createFakePi(flagValues: Record<string, boolean | string> = {}): FakePi {
|
|
56
|
+
const flags: RegisterFlagArgs[] = [];
|
|
57
|
+
const handlers = new Map<string, RecordedHandler[]>();
|
|
58
|
+
const setActiveToolsCalls: string[][] = [];
|
|
59
|
+
const values = new Map<string, boolean | string>(Object.entries(flagValues));
|
|
60
|
+
const baselineTools = ["read", "bash"];
|
|
61
|
+
|
|
62
|
+
const pi: ExtensionAPI = {
|
|
63
|
+
on: (event: string, handler: RecordedHandler) => {
|
|
64
|
+
const list = handlers.get(event) ?? [];
|
|
65
|
+
list.push(handler);
|
|
66
|
+
handlers.set(event, list);
|
|
67
|
+
},
|
|
68
|
+
registerFlag: (name, options) => {
|
|
69
|
+
flags.push([name, options]);
|
|
70
|
+
},
|
|
71
|
+
getFlag: (name) => values.get(name),
|
|
72
|
+
getActiveTools: () => [...baselineTools],
|
|
73
|
+
setActiveTools: (toolNames) => {
|
|
74
|
+
setActiveToolsCalls.push(toolNames);
|
|
75
|
+
},
|
|
76
|
+
registerCommand: () => {},
|
|
77
|
+
registerShortcut: () => {},
|
|
78
|
+
registerTool: () => {},
|
|
79
|
+
registerMessageRenderer: () => {},
|
|
80
|
+
registerProvider: () => {},
|
|
81
|
+
unregisterProvider: () => {},
|
|
82
|
+
sendMessage: () => {},
|
|
83
|
+
sendUserMessage: () => {},
|
|
84
|
+
appendEntry: () => {},
|
|
85
|
+
setSessionName: () => {},
|
|
86
|
+
getSessionName: () => undefined,
|
|
87
|
+
setLabel: () => {},
|
|
88
|
+
exec: async () => {
|
|
89
|
+
throw new Error("not implemented");
|
|
90
|
+
},
|
|
91
|
+
getAllTools: () => [],
|
|
92
|
+
getCommands: () => [],
|
|
93
|
+
setModel: async () => false,
|
|
94
|
+
getThinkingLevel: () => "off",
|
|
95
|
+
setThinkingLevel: () => {},
|
|
96
|
+
events: { on: () => () => {}, emit: () => {} },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
pi,
|
|
101
|
+
flags,
|
|
102
|
+
handlers,
|
|
103
|
+
setActiveToolsCalls,
|
|
104
|
+
fire(event, ...args) {
|
|
105
|
+
for (const handler of handlers.get(event) ?? []) {
|
|
106
|
+
Reflect.apply(handler, undefined, args);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createFakeContext(): FakeContext {
|
|
113
|
+
return {
|
|
114
|
+
ui: {
|
|
115
|
+
statuses: [],
|
|
116
|
+
notifications: [],
|
|
117
|
+
setStatus(id, text) {
|
|
118
|
+
this.statuses.push({ id, text });
|
|
119
|
+
},
|
|
120
|
+
notify(message, level) {
|
|
121
|
+
this.notifications.push({ message, level });
|
|
122
|
+
},
|
|
123
|
+
theme: { fg: (_color, text) => text },
|
|
124
|
+
},
|
|
125
|
+
sessionManager: { getEntries: () => [] },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── registerAgentFlag ───────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
describe("registerAgentFlag", () => {
|
|
132
|
+
it("registers --agent as a string flag listing the discovered agent ids", () => {
|
|
133
|
+
const calls: RegisterFlagArgs[] = [];
|
|
134
|
+
const pi: Pick<ExtensionAPI, "registerFlag"> = {
|
|
135
|
+
registerFlag: (...args) => calls.push(args),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
registerAgentFlag(pi, [
|
|
139
|
+
agent("ralph"),
|
|
140
|
+
agent("ralph-plan"),
|
|
141
|
+
agent("plan"),
|
|
142
|
+
agent("strategy"),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
assert.equal(calls.length, 1);
|
|
146
|
+
const [name, options] = calls[0];
|
|
147
|
+
assert.equal(name, "agent");
|
|
148
|
+
assert.equal(options.type, "string");
|
|
149
|
+
assert.equal(
|
|
150
|
+
options.description,
|
|
151
|
+
"Start in a specific agent mode (ralph, ralph-plan, plan, strategy)",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ─── Registration timing ─────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe("agentModesExtension flag registration", () => {
|
|
159
|
+
it("registers --agent during load, before any session event fires", () => {
|
|
160
|
+
const fake = createFakePi();
|
|
161
|
+
|
|
162
|
+
agentModesExtension(fake.pi);
|
|
163
|
+
|
|
164
|
+
// The flag must exist straight after the factory returns: pi validates CLI
|
|
165
|
+
// flags against extensions loaded at startup, so registering it from the
|
|
166
|
+
// session_start handler would be too late and `pi --agent <name>` would fail.
|
|
167
|
+
assert.equal(fake.flags.length, 1);
|
|
168
|
+
const [name, options] = fake.flags[0];
|
|
169
|
+
assert.equal(name, "agent");
|
|
170
|
+
assert.equal(options.type, "string");
|
|
171
|
+
assert.match(options.description ?? "", /^Start in a specific agent mode \(/);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ─── --agent flag contract ───────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
describe("agentModesExtension --agent flag", () => {
|
|
178
|
+
it("activates the named agent on session start", () => {
|
|
179
|
+
const fake = createFakePi({ agent: "plan" });
|
|
180
|
+
const ctx = createFakeContext();
|
|
181
|
+
|
|
182
|
+
agentModesExtension(fake.pi);
|
|
183
|
+
fake.fire("session_start", {}, ctx);
|
|
184
|
+
|
|
185
|
+
const plan = discoverAgents(AGENTS_DIR).find((a) => a.id === "plan");
|
|
186
|
+
assert.ok(plan, "the plan agent should be discoverable");
|
|
187
|
+
assert.deepEqual(fake.setActiveToolsCalls.at(-1), plan.tools);
|
|
188
|
+
assert.ok(
|
|
189
|
+
ctx.ui.statuses.some((s) => s.id === "agent-mode" && s.text?.includes("plan")),
|
|
190
|
+
"the status indicator should show the active agent",
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("falls back to default mode for an unknown agent and leaves tools untouched", () => {
|
|
195
|
+
const fake = createFakePi({ agent: "does-not-exist" });
|
|
196
|
+
const ctx = createFakeContext();
|
|
197
|
+
|
|
198
|
+
agentModesExtension(fake.pi);
|
|
199
|
+
fake.fire("session_start", {}, ctx);
|
|
200
|
+
|
|
201
|
+
// Unknown flag → default mode: baseline tools restored, no agent status.
|
|
202
|
+
assert.deepEqual(fake.setActiveToolsCalls.at(-1), ["read", "bash"]);
|
|
203
|
+
assert.ok(
|
|
204
|
+
ctx.ui.statuses.every((s) => !s.text?.includes("agent:")),
|
|
205
|
+
"no agent mode should be reported",
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -37,6 +37,27 @@ interface OriginalState {
|
|
|
37
37
|
|
|
38
38
|
const PERSIST_KEY = "agent-modes-state";
|
|
39
39
|
|
|
40
|
+
// ─── CLI flag ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register the `--agent <name>` CLI flag.
|
|
44
|
+
*
|
|
45
|
+
* This must run while the extension is loading (i.e. from the factory), not
|
|
46
|
+
* from a `session_start` handler: PI matches CLI flags against the extensions
|
|
47
|
+
* loaded at startup, so a flag registered during `session_start` arrives after
|
|
48
|
+
* the CLI has already been parsed and `pi --agent <name>` fails with
|
|
49
|
+
* `Unknown option: --agent`.
|
|
50
|
+
*/
|
|
51
|
+
export function registerAgentFlag(
|
|
52
|
+
pi: Pick<ExtensionAPI, "registerFlag">,
|
|
53
|
+
agents: AgentDef[],
|
|
54
|
+
): void {
|
|
55
|
+
pi.registerFlag("agent", {
|
|
56
|
+
description: `Start in a specific agent mode (${agents.map((a) => a.id).join(", ")})`,
|
|
57
|
+
type: "string",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
// ─── Extension ───────────────────────────────────────────────────────────────
|
|
41
62
|
|
|
42
63
|
export default function agentModesExtension(pi: ExtensionAPI): void {
|
|
@@ -47,6 +68,8 @@ export default function agentModesExtension(pi: ExtensionAPI): void {
|
|
|
47
68
|
return;
|
|
48
69
|
}
|
|
49
70
|
|
|
71
|
+
registerAgentFlag(pi, agents);
|
|
72
|
+
|
|
50
73
|
let activeAgentIndex = agents.length; // default mode is beyond the last agent
|
|
51
74
|
let originalState: OriginalState | undefined;
|
|
52
75
|
let baselineTools: string[] = [];
|
|
@@ -186,12 +209,6 @@ export default function agentModesExtension(pi: ExtensionAPI): void {
|
|
|
186
209
|
},
|
|
187
210
|
});
|
|
188
211
|
|
|
189
|
-
// --agent CLI flag
|
|
190
|
-
pi.registerFlag("agent", {
|
|
191
|
-
description: `Start in a specific agent mode (${agents.map((a) => a.id).join(", ")})`,
|
|
192
|
-
type: "string",
|
|
193
|
-
});
|
|
194
|
-
|
|
195
212
|
// inject agent prompt before each agent start
|
|
196
213
|
pi.on("before_agent_start", async (event) => {
|
|
197
214
|
const agent = getActiveAgent();
|
|
@@ -331,17 +331,18 @@ describe("loadStore / saveStore", () => {
|
|
|
331
331
|
const file = join(tmpDir, "test-store.json");
|
|
332
332
|
const original = freshStore();
|
|
333
333
|
original.totalTurns = 42;
|
|
334
|
+
const recentTs = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
|
|
334
335
|
original.skills["test-skill"] = {
|
|
335
336
|
loadedCount: 5,
|
|
336
337
|
invokedCount: 2,
|
|
337
338
|
readCount: 3,
|
|
338
|
-
lastUsed:
|
|
339
|
+
lastUsed: recentTs,
|
|
339
340
|
byProject: {
|
|
340
341
|
"/project/a": {
|
|
341
342
|
loadedCount: 3,
|
|
342
343
|
invokedCount: 1,
|
|
343
344
|
readCount: 2,
|
|
344
|
-
lastUsed:
|
|
345
|
+
lastUsed: recentTs,
|
|
345
346
|
},
|
|
346
347
|
},
|
|
347
348
|
};
|
package/lib/cli.mjs
CHANGED
|
@@ -5,9 +5,11 @@ const SUPPORTED_HARNESSES = ["copilot", "claude", "pi"];
|
|
|
5
5
|
// malformed options surface as `optionError` on the parsed result; the CLI
|
|
6
6
|
// prints them and exits 1.
|
|
7
7
|
const INSTALL_OPTIONS = {
|
|
8
|
-
"--dry-run": { key: "dryRun" },
|
|
8
|
+
"--dry-run": { key: "dryRun", boolean: true },
|
|
9
9
|
"--output": { key: "output" },
|
|
10
10
|
"--plugin-root": { key: "pluginRoot" },
|
|
11
|
+
"--require-register": { key: "requireRegister", boolean: true },
|
|
12
|
+
"--no-provision": { key: "noProvision", boolean: true },
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export function parseArgs(argv) {
|
|
@@ -34,8 +36,8 @@ export function parseArgs(argv) {
|
|
|
34
36
|
if (!spec) {
|
|
35
37
|
return { command: "install", harness, optionError: `Unknown option "${arg}". Run "agent-cortex --help" for usage.` };
|
|
36
38
|
}
|
|
37
|
-
if (spec.
|
|
38
|
-
options.
|
|
39
|
+
if (spec.boolean) {
|
|
40
|
+
options[spec.key] = true;
|
|
39
41
|
continue;
|
|
40
42
|
}
|
|
41
43
|
if (inlineValue !== null) {
|
|
@@ -66,15 +68,26 @@ Commands:
|
|
|
66
68
|
install <harness> Install agent-cortex for a harness
|
|
67
69
|
Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}
|
|
68
70
|
Options (claude):
|
|
69
|
-
(no --output)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
(no --output) Materialise the plugin into
|
|
72
|
+
~/.agent-cortex/claude, write the
|
|
73
|
+
marketplace manifest at
|
|
74
|
+
~/.agent-cortex/.claude-plugin/
|
|
75
|
+
marketplace.json, and register it with
|
|
76
|
+
Claude Code via the "claude plugin" CLI
|
|
77
|
+
(marketplace add → install → update,
|
|
78
|
+
idempotent by state: a re-run is the
|
|
79
|
+
update path, repeat install a no-op;
|
|
80
|
+
requires the plugin CLI, v2+)
|
|
81
|
+
--output <dir> Write the plugin subtree to <dir> only —
|
|
82
|
+
no marketplace manifest or registration
|
|
83
|
+
(default: ~/.agent-cortex/claude)
|
|
76
84
|
--dry-run Print what would be generated/registered
|
|
77
85
|
without writing
|
|
86
|
+
--require-register
|
|
87
|
+
Fail the install when Claude Code
|
|
88
|
+
registration cannot run (missing CLI,
|
|
89
|
+
pre-v2 build); default warns and prints
|
|
90
|
+
the manual registration commands
|
|
78
91
|
Options (copilot):
|
|
79
92
|
--output <dir> Write the flat *.agent.md files to <dir>
|
|
80
93
|
(default: <package root>/agents — the dir
|
|
@@ -82,10 +95,15 @@ Commands:
|
|
|
82
95
|
--dry-run Print what would be generated without writing
|
|
83
96
|
Options (pi):
|
|
84
97
|
--dry-run Show what would be installed without writing
|
|
85
|
-
--output <dir> Install into <dir
|
|
86
|
-
(
|
|
98
|
+
--output <dir> Install into <dir> — agents/, skills/,
|
|
99
|
+
settings.json (merged, packages managed)
|
|
100
|
+
and keybindings.json (default: ~/.pi/agent)
|
|
87
101
|
--plugin-root <dir> Override the plugin root used for {{PATH:...}}
|
|
88
102
|
tokens (default: token-map.json's pi value)
|
|
103
|
+
--no-provision Skip installing the third-party pi packages
|
|
104
|
+
declared in the package manifest (e.g.
|
|
105
|
+
pi-questions, pi-web-access). They are
|
|
106
|
+
installed by default on a real install.
|
|
89
107
|
--help, -h Show this help text`;
|
|
90
108
|
}
|
|
91
109
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Pi third-party package provisioning.
|
|
2
|
+
//
|
|
3
|
+
// agent-cortex's pi harness needs tools that live in third-party pi packages:
|
|
4
|
+
// ask_questions comes from pi-questions, fetch_content from pi-web-access (see
|
|
5
|
+
// token-map.json notes and docs/ideas/extension-manifests.md). Those packages
|
|
6
|
+
// are declared in the package manifest — package.json → pi.packages — and this
|
|
7
|
+
// module makes them real: `agent-cortex install pi` reads the declaration and
|
|
8
|
+
// installs any package the pi user scope is missing.
|
|
9
|
+
//
|
|
10
|
+
// Provisioning goes through the pi CLI (`pi install <source>`), which both
|
|
11
|
+
// installs the package under ~/.pi/agent/npm and declares it in
|
|
12
|
+
// ~/.pi/agent/settings.json — the same store pi loads packages from. It is
|
|
13
|
+
// opt-in (installPi option `provisionPackages`) and idempotent: a package that
|
|
14
|
+
// is already declared and installed is skipped. A failed install warns and the
|
|
15
|
+
// rest continue, so an offline machine never breaks the agent install.
|
|
16
|
+
|
|
17
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_WARN = (msg) => console.warn(`[pi-packages] ${msg}`);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read the pi package sources agent-cortex declares it needs.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} root Package root containing package.json
|
|
27
|
+
* @returns {Promise<string[]>} Declared sources, e.g. ["npm:pi-questions"]
|
|
28
|
+
*/
|
|
29
|
+
export async function loadRequiredPackages(root) {
|
|
30
|
+
const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
|
|
31
|
+
const packages = manifest?.pi?.packages;
|
|
32
|
+
return Array.isArray(packages) ? packages.filter((entry) => typeof entry === "string") : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract the npm package name from a pi source string.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} source e.g. "npm:pi-web-access@0.10.7", "npm:@scope/pkg"
|
|
39
|
+
* @returns {string | null} The name, or null for sources we cannot inspect locally
|
|
40
|
+
*/
|
|
41
|
+
export function npmSourceName(source) {
|
|
42
|
+
if (typeof source !== "string" || !source.startsWith("npm:")) return null;
|
|
43
|
+
const spec = source.slice("npm:".length);
|
|
44
|
+
const at = spec.lastIndexOf("@");
|
|
45
|
+
return at > 0 ? spec.slice(0, at) : spec;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Decide which required packages still need installing.
|
|
50
|
+
*
|
|
51
|
+
* A package is satisfied only when it is both declared in settings (so pi loads
|
|
52
|
+
* it) and present in the npm store (so it is installed) — otherwise it needs a
|
|
53
|
+
* `pi install` to reconcile.
|
|
54
|
+
*
|
|
55
|
+
* @param {string[]} required
|
|
56
|
+
* @param {{ declaredSources: string[], installedNames: string[] }} state
|
|
57
|
+
* @returns {string[]} The sources still to install
|
|
58
|
+
*/
|
|
59
|
+
export function planPackageInstalls(required, { declaredSources, installedNames }) {
|
|
60
|
+
const declared = new Set(declaredSources.map(npmSourceName).filter(Boolean));
|
|
61
|
+
const installed = new Set(installedNames);
|
|
62
|
+
return required.filter((source) => {
|
|
63
|
+
const name = npmSourceName(source);
|
|
64
|
+
return name === null || !declared.has(name) || !installed.has(name);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Install every required package the pi user scope is missing.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} options
|
|
72
|
+
* @param {string[]} options.required Declared pi sources (see loadRequiredPackages)
|
|
73
|
+
* @param {string} options.piRoot Pi user scope dir (default ~/.pi/agent)
|
|
74
|
+
* @param {boolean} [options.dryRun] Report the plan without installing
|
|
75
|
+
* @param {(source: string) => Promise<{ok: boolean, error: string | null}>} [options.runInstall]
|
|
76
|
+
* @param {(msg: string) => void} [options.warn]
|
|
77
|
+
* @returns {Promise<{planned: string[], installed: string[], failed: {source: string, error: string}[]}>}
|
|
78
|
+
*/
|
|
79
|
+
export async function provisionPiPackages({
|
|
80
|
+
required,
|
|
81
|
+
piRoot,
|
|
82
|
+
dryRun = false,
|
|
83
|
+
runInstall = runPiInstall,
|
|
84
|
+
warn = DEFAULT_WARN,
|
|
85
|
+
}) {
|
|
86
|
+
const declaredSources = await readDeclaredSources(join(piRoot, "settings.json"));
|
|
87
|
+
const installedNames = await readInstalledNames(join(piRoot, "npm", "node_modules"));
|
|
88
|
+
const planned = planPackageInstalls(required, { declaredSources, installedNames });
|
|
89
|
+
|
|
90
|
+
if (dryRun) return { planned, installed: [], failed: [] };
|
|
91
|
+
|
|
92
|
+
const installed = [];
|
|
93
|
+
const failed = [];
|
|
94
|
+
for (const source of planned) {
|
|
95
|
+
const result = await runInstall(source);
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
installed.push(source);
|
|
98
|
+
} else {
|
|
99
|
+
failed.push({ source, error: result.error });
|
|
100
|
+
warn(`could not install pi package ${source}: ${result.error}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { planned, installed, failed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Install one pi package source through the pi CLI. */
|
|
107
|
+
export function runPiInstall(source) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const child = spawn("pi", ["install", source], { stdio: ["ignore", "pipe", "pipe"] });
|
|
110
|
+
let stderr = "";
|
|
111
|
+
child.stdout.resume(); // drain; only failures are interesting
|
|
112
|
+
child.stderr.on("data", (chunk) => {
|
|
113
|
+
stderr += chunk;
|
|
114
|
+
});
|
|
115
|
+
child.on("error", (err) => {
|
|
116
|
+
resolve({
|
|
117
|
+
ok: false,
|
|
118
|
+
error: err.code === "ENOENT" ? "pi CLI not found on PATH" : err.message,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
child.on("close", (code) => {
|
|
122
|
+
resolve({
|
|
123
|
+
ok: code === 0,
|
|
124
|
+
error: code === 0 ? null : stderr.trim() || `pi install exited with code ${code}`,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Pi user-scope inspection ────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/** Read the package sources declared in a pi settings.json (missing or unreadable → none). */
|
|
133
|
+
async function readDeclaredSources(settingsPath) {
|
|
134
|
+
let settings;
|
|
135
|
+
try {
|
|
136
|
+
settings = JSON.parse(await readFile(settingsPath, "utf-8"));
|
|
137
|
+
} catch {
|
|
138
|
+
// Nothing declared yet, or a file pi itself owns and repairs. Provisioning is
|
|
139
|
+
// a non-essential step — it must never fail the agent install.
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const packages = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
143
|
+
return packages
|
|
144
|
+
.map((entry) => (typeof entry === "string" ? entry : entry?.source))
|
|
145
|
+
.filter((source) => typeof source === "string");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** List installed package names in the pi npm store (missing dir → none). */
|
|
149
|
+
async function readInstalledNames(nodeModulesDir) {
|
|
150
|
+
let entries;
|
|
151
|
+
try {
|
|
152
|
+
entries = await readdir(nodeModulesDir, { withFileTypes: true });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err.code === "ENOENT") return [];
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
const names = [];
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (!entry.isDirectory()) continue;
|
|
160
|
+
if (!entry.name.startsWith("@")) {
|
|
161
|
+
names.push(entry.name);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
for (const scoped of await readdir(join(nodeModulesDir, entry.name), { withFileTypes: true })) {
|
|
165
|
+
if (scoped.isDirectory()) names.push(`${entry.name}/${scoped.name}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return names;
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaybeeuu/agent-cortex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"description": "Personal PI package with custom agents, skills, and extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -24,17 +24,21 @@
|
|
|
24
24
|
],
|
|
25
25
|
"skills": [
|
|
26
26
|
"./skills"
|
|
27
|
+
],
|
|
28
|
+
"packages": [
|
|
29
|
+
"npm:pi-questions",
|
|
30
|
+
"npm:pi-web-access@0.10.7"
|
|
27
31
|
]
|
|
28
32
|
},
|
|
29
33
|
"scripts": {
|
|
30
34
|
"build": "echo 'Build complete — no compilation needed for PI package'",
|
|
31
|
-
"lint": "pnpm typecheck",
|
|
35
|
+
"lint": "pnpm typecheck && pnpm check:sync",
|
|
36
|
+
"check:sync": "node scripts/check-no-sync.mjs",
|
|
32
37
|
"test": "node --test test/*.test.mjs && pnpm -r test",
|
|
33
38
|
"typecheck": "pnpm -r typecheck",
|
|
34
|
-
"build:claude": "node bin/agent-cortex.mjs install claude --output claude",
|
|
35
39
|
"build:copilot": "node scripts/build-copilot-agents.mjs",
|
|
36
40
|
"changeset": "changeset",
|
|
37
|
-
"version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:
|
|
41
|
+
"version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:copilot",
|
|
38
42
|
"publish-package": "TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
|
|
39
43
|
"release": "changeset publish",
|
|
40
44
|
"postinstall": "bash scripts/install-instructions.sh"
|