@jaybeeuu/agent-cortex 1.40.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.md +21 -8
- package/CHANGELOG.md +48 -0
- package/README.md +78 -38
- package/agents/README.md +5 -3
- package/bin/agent-cortex.mjs +36 -4
- package/bin/installers/claude.mjs +171 -21
- package/bin/installers/copilot.mjs +2 -17
- package/bin/installers/pi.mjs +261 -22
- package/claude/settings.json +4 -0
- package/claude-extras/scripts/statusline-command.sh +32 -4
- package/docs/claude-hooks.md +12 -0
- 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 +9 -3
- 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/lib/cli.mjs +8 -2
- package/lib/pi-packages.mjs +169 -0
- package/package.json +5 -1
- package/pi/settings.json +23 -12
- package/plugin.json +1 -1
- package/scripts/lib/fs.mjs +31 -0
- 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/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/cli.test.mjs +55 -11
- package/test/fs.test.mjs +76 -0
- package/test/installer.test.mjs +237 -4
- package/test/pi-installer.test.mjs +343 -4
- package/test/pi-packages.test.mjs +295 -0
- package/token-map.README.md +4 -2
- package/token-map.json +3 -3
|
@@ -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,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-cortex/agent-guardrails",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test --import tsx/esm guardrails.test.ts",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
12
|
+
"@types/node": "^22.0.0",
|
|
13
|
+
"tsx": "^4.0.0",
|
|
14
|
+
"typescript": "^5.0.0"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@earendil-works/pi-coding-agent": "^0.78.1"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -46,10 +46,16 @@ flat counterpart for the same agent id.
|
|
|
46
46
|
## Development
|
|
47
47
|
|
|
48
48
|
Discovery and prompt composition live in `discover.ts` (unit-tested against
|
|
49
|
-
fixture directories in `discover.test.ts`); `index.ts`
|
|
50
|
-
|
|
49
|
+
fixture directories in `discover.test.ts`); `index.ts` wires discovery into the
|
|
50
|
+
extension API and registers `--agent` at load time (`index.test.ts` covers that
|
|
51
|
+
timing and the flag contract).
|
|
52
|
+
|
|
53
|
+
`--agent` must be registered from the extension factory, not from a
|
|
54
|
+
`session_start` handler: PI matches parsed CLI flags against the extensions
|
|
55
|
+
loaded at startup, so late registration makes `pi --agent <name>` fail with
|
|
56
|
+
`Unknown option: --agent`.
|
|
51
57
|
|
|
52
58
|
```bash
|
|
53
|
-
pnpm test # run discover.test.ts
|
|
59
|
+
pnpm test # run discover.test.ts and index.test.ts
|
|
54
60
|
pnpm typecheck # tsc --noEmit
|
|
55
61
|
```
|
|
@@ -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();
|
package/lib/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const INSTALL_OPTIONS = {
|
|
|
9
9
|
"--output": { key: "output" },
|
|
10
10
|
"--plugin-root": { key: "pluginRoot" },
|
|
11
11
|
"--require-register": { key: "requireRegister", boolean: true },
|
|
12
|
+
"--no-provision": { key: "noProvision", boolean: true },
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export function parseArgs(argv) {
|
|
@@ -94,10 +95,15 @@ Commands:
|
|
|
94
95
|
--dry-run Print what would be generated without writing
|
|
95
96
|
Options (pi):
|
|
96
97
|
--dry-run Show what would be installed without writing
|
|
97
|
-
--output <dir> Install into <dir
|
|
98
|
-
(
|
|
98
|
+
--output <dir> Install into <dir> — agents/, skills/,
|
|
99
|
+
settings.json (merged, packages managed)
|
|
100
|
+
and keybindings.json (default: ~/.pi/agent)
|
|
99
101
|
--plugin-root <dir> Override the plugin root used for {{PATH:...}}
|
|
100
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.
|
|
101
107
|
--help, -h Show this help text`;
|
|
102
108
|
}
|
|
103
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,6 +24,10 @@
|
|
|
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": {
|
package/pi/settings.json
CHANGED
|
@@ -4,26 +4,37 @@
|
|
|
4
4
|
"maxNameLength": 40,
|
|
5
5
|
"maxSummaryLength": 100
|
|
6
6
|
},
|
|
7
|
-
"lastChangelogVersion": "0.
|
|
7
|
+
"lastChangelogVersion": "0.87.1",
|
|
8
8
|
"packages": [
|
|
9
|
-
|
|
9
|
+
{
|
|
10
|
+
"source": "../../src/agent-cortex",
|
|
11
|
+
"skills": []
|
|
12
|
+
},
|
|
10
13
|
"npm:pi-web-access@0.10.7",
|
|
11
14
|
"npm:@getpipher/vision",
|
|
12
15
|
"npm:@hypabolic/pi-hypa",
|
|
13
|
-
"npm:context-mode"
|
|
16
|
+
"npm:context-mode",
|
|
17
|
+
"npm:pi-questions"
|
|
14
18
|
],
|
|
15
19
|
"defaultProvider": "opencode-go",
|
|
16
|
-
"defaultModel": "
|
|
20
|
+
"defaultModel": "deepseek-v4.1-flash",
|
|
17
21
|
"theme": "dark",
|
|
18
22
|
"defaultThinkingLevel": "high",
|
|
19
23
|
"enabledModels": [
|
|
20
|
-
"opencode/
|
|
21
|
-
"opencode-go/qwen3.7-
|
|
22
|
-
"opencode/kimi-k2.7-code",
|
|
23
|
-
"opencode/mimo-v2.5-free",
|
|
24
|
-
"opencode-go/mimo-v2.5",
|
|
25
|
-
"opencode/deepseek-v4-flash-free",
|
|
24
|
+
"opencode/big-pickle",
|
|
25
|
+
"opencode-go/qwen3.7-plus",
|
|
26
26
|
"opencode-go/deepseek-v4-flash",
|
|
27
|
-
"opencode/
|
|
28
|
-
|
|
27
|
+
"opencode-go/kimi-k3",
|
|
28
|
+
"opencode/deepseek-v4-flash",
|
|
29
|
+
"opencode/mimo-v2.5-free",
|
|
30
|
+
"opencode-go/deepseek-v4.1-flash"
|
|
31
|
+
],
|
|
32
|
+
"compaction": {
|
|
33
|
+
"enabled": true
|
|
34
|
+
},
|
|
35
|
+
"steeringMode": "all",
|
|
36
|
+
"followUpMode": "all",
|
|
37
|
+
"tuiMode": "regular",
|
|
38
|
+
"treeFilterMode": "default",
|
|
39
|
+
"hideThinkingBlock": true
|
|
29
40
|
}
|
package/plugin.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Shared async filesystem predicates. The install-time generators all need the
|
|
2
|
+
// same "does this path exist and is it a file/directory?" probe, and the repo
|
|
3
|
+
// convention is async-only (no *Sync) in tooling code — see scripts/check-no-sync.mjs.
|
|
4
|
+
//
|
|
5
|
+
// Both probes are stat-based and resolve `false` on any error (missing path,
|
|
6
|
+
// permission denied, broken symlink), so callers can branch on existence
|
|
7
|
+
// without a separate try/catch:
|
|
8
|
+
//
|
|
9
|
+
// if (await isFile(join(dir, "agent.md"))) { … }
|
|
10
|
+
//
|
|
11
|
+
// Zero dependencies so it runs on the CI Node (20) and local Node alike.
|
|
12
|
+
|
|
13
|
+
import { stat } from "node:fs/promises";
|
|
14
|
+
|
|
15
|
+
/** True when `p` exists and is a regular file; false on any error. */
|
|
16
|
+
export async function isFile(p) {
|
|
17
|
+
try {
|
|
18
|
+
return (await stat(p)).isFile();
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** True when `p` exists and is a directory; false on any error. */
|
|
25
|
+
export async function isDirectory(p) {
|
|
26
|
+
try {
|
|
27
|
+
return (await stat(p)).isDirectory();
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|