@jaybeeuu/agent-cortex 1.40.0 → 1.41.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 (74) hide show
  1. package/AGENTS.md +21 -8
  2. package/CHANGELOG.md +85 -0
  3. package/README.md +78 -38
  4. package/agents/README.md +5 -3
  5. package/bin/agent-cortex.mjs +36 -4
  6. package/bin/installers/claude.mjs +171 -21
  7. package/bin/installers/copilot.mjs +2 -17
  8. package/bin/installers/pi.mjs +261 -22
  9. package/claude/settings.json +4 -0
  10. package/claude-extras/scripts/statusline-command.sh +32 -4
  11. package/docs/claude-hooks.md +12 -0
  12. package/docs/ideas/ban-content-assertion-tests.md +73 -0
  13. package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
  14. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
  15. package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
  16. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
  17. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
  18. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
  19. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
  20. package/docs/ideas/extension-manifests.md +55 -14
  21. package/docs/ideas/improve-pi-system-prompt.md +5 -2
  22. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
  23. package/docs/ideas/record-idea-implementation-questions.md +68 -0
  24. package/docs/ideas/self-flushing-session.md +72 -0
  25. package/docs/ideas/state-in-beads-not-md.md +69 -0
  26. package/docs/ideas/task-session-context.local.md +95 -0
  27. package/docs/ideas/test-data-creation-helpers.md +92 -0
  28. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
  29. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
  30. package/docs/technical-direction/harness-config.md +161 -0
  31. package/docs/technical-direction/migration-execution-labels.md +60 -23
  32. package/extensions/agent-guardrails/README.md +45 -0
  33. package/extensions/agent-guardrails/guardrails.test.ts +42 -0
  34. package/extensions/agent-guardrails/guardrails.ts +66 -0
  35. package/extensions/agent-guardrails/index.ts +22 -0
  36. package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
  37. package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
  38. package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
  39. package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
  40. package/extensions/agent-guardrails/package.json +19 -0
  41. package/extensions/agent-guardrails/tsconfig.json +12 -0
  42. package/extensions/agent-modes/README.md +9 -3
  43. package/extensions/agent-modes/index.test.ts +208 -0
  44. package/extensions/agent-modes/index.ts +23 -6
  45. package/extensions/agent-modes/package.json +1 -1
  46. package/lib/cli.mjs +8 -2
  47. package/lib/pi-packages.mjs +169 -0
  48. package/package.json +5 -1
  49. package/pi/settings.json +23 -12
  50. package/plugin.json +1 -1
  51. package/scripts/lib/fs.mjs +31 -0
  52. package/skills/planning/classify-bead/REFERENCE.md +85 -0
  53. package/skills/planning/classify-bead/SKILL.md +32 -36
  54. package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
  55. package/skills/planning/create-task/REFERENCE.md +1 -1
  56. package/skills/planning/create-task/SKILL.md +20 -11
  57. package/skills/planning/create-task/scripts/create-chores.ts +4 -0
  58. package/skills/planning/write-a-prd/SKILL.md +2 -2
  59. package/skills/style/style-code/REFERENCE.md +32 -2
  60. package/skills/style/style-code/SKILL.md +9 -21
  61. package/skills/style/style-tests/EXAMPLES.md +548 -0
  62. package/skills/style/style-tests/SKILL.md +84 -33
  63. package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
  64. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
  65. package/test/classify-bead.test.mjs +277 -0
  66. package/test/cli.test.mjs +55 -11
  67. package/test/create-chores.test.mjs +73 -0
  68. package/test/fs.test.mjs +76 -0
  69. package/test/helpers/fake-bd.mjs +67 -0
  70. package/test/installer.test.mjs +237 -4
  71. package/test/pi-installer.test.mjs +343 -4
  72. package/test/pi-packages.test.mjs +295 -0
  73. package/token-map.README.md +4 -2
  74. 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
+ }
@@ -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
+ }
@@ -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` only wires discovery
50
- into the extension API.
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();
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --test --import tsx/esm discover.test.ts",
7
+ "test": "node --test --import tsx/esm discover.test.ts index.test.ts",
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
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>/agents and <dir>/skills
98
- (default: ~/.pi/agent)
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.40.0",
3
+ "version": "1.41.1",
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.83.0",
7
+ "lastChangelogVersion": "0.87.1",
8
8
  "packages": [
9
- "../../src/agent-cortex",
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": "qwen3.7-max",
20
+ "defaultModel": "deepseek-v4.1-flash",
17
21
  "theme": "dark",
18
22
  "defaultThinkingLevel": "high",
19
23
  "enabledModels": [
20
- "opencode/nemotron-3-ultra-free",
21
- "opencode-go/qwen3.7-max",
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/big-pickle"
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-cortex",
3
3
  "description": "Personal Copilot CLI plugin with custom agents and skills",
4
- "version": "1.40.0",
4
+ "version": "1.41.1",
5
5
  "author": {
6
6
  "name": "jaybeeuu"
7
7
  },
@@ -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
+ }