@jaybeeuu/agent-cortex 1.42.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +78 -8
  2. package/.github/workflows/ci.yml +51 -38
  3. package/AGENTS.md +32 -17
  4. package/CHANGELOG.md +182 -0
  5. package/README.md +167 -126
  6. package/agents/README.md +26 -27
  7. package/agents/ralph/agent.md +7 -10
  8. package/agents/ralph/claude/polling.md +1 -1
  9. package/bin/agent-cortex.mjs +46 -44
  10. package/bin/installers/claude.mjs +2 -2
  11. package/bin/installers/pi.mjs +885 -237
  12. package/docs/claude-hooks.md +5 -4
  13. package/docs/commit-signing.md +231 -0
  14. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +10 -1
  15. package/docs/ideas/extension-manifests.md +5 -0
  16. package/docs/research/disable-model-invocation.md +82 -0
  17. package/docs/research/skill-design-research.md +2 -2
  18. package/docs/skills/skill-anatomy.md +5 -2
  19. package/docs/technical-direction/harness-config.md +30 -14
  20. package/docs/technical-direction/migration-execution-labels.md +17 -9
  21. package/docs/user-preferences.md +2 -2
  22. package/extensions/agent-modes/README.md +3 -3
  23. package/extensions/agent-modes/discover.test.ts +19 -0
  24. package/extensions/agent-modes/discover.ts +31 -9
  25. package/extensions/session-start/README.md +38 -0
  26. package/extensions/session-start/index.test.ts +122 -0
  27. package/extensions/session-start/index.ts +27 -0
  28. package/extensions/session-start/package.json +19 -0
  29. package/extensions/session-start/tsconfig.json +12 -0
  30. package/extensions/skill-stats/pnpm-lock.yaml +329 -0
  31. package/extensions/subagent/README.md +3 -3
  32. package/extensions/subagent/agents.test.ts +176 -0
  33. package/extensions/subagent/agents.ts +101 -11
  34. package/extensions/subagent/index.ts +2 -1
  35. package/extensions/subagent/package.json +1 -1
  36. package/lib/cli.mjs +40 -16
  37. package/package.json +8 -9
  38. package/pi/settings.json +1 -7
  39. package/pnpm-workspace.yaml +6 -0
  40. package/scripts/check-changeset.mjs +42 -0
  41. package/scripts/lib/compose-agent.mjs +10 -10
  42. package/scripts/lib/release-gates.mjs +61 -0
  43. package/scripts/lib/verify-worker-tree.mjs +107 -0
  44. package/scripts/resolve-publish-command.mjs +17 -0
  45. package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
  46. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
  47. package/skills/workflow/git-workflow/SKILL.md +35 -1
  48. package/skills/workflow/ralph/REFERENCE.md +20 -4
  49. package/skills/workflow/ralph/SKILL.md +9 -6
  50. package/skills/workflow/run-pipeline-stage/SKILL.md +11 -4
  51. package/skills/workflow/run-pipeline-stage/playbooks/code.md +2 -0
  52. package/skills/workflow/run-pipeline-stage/playbooks/document.md +3 -0
  53. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -0
  54. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +11 -0
  55. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
  56. package/test/cli.test.mjs +37 -39
  57. package/test/helpers/fake-pi.mjs +132 -0
  58. package/test/pi-installer.test.mjs +345 -286
  59. package/test/pi-register.test.mjs +342 -0
  60. package/test/pi-sweep.test.mjs +619 -0
  61. package/test/release-gates.test.mjs +190 -0
  62. package/test/verify-worker-tree.test.mjs +120 -0
  63. package/token-map.README.md +16 -18
  64. package/token-map.json +29 -33
  65. package/agents/plan/copilot/frontmatter.json +0 -6
  66. package/agents/plan.agent.md +0 -59
  67. package/agents/ralph/copilot/frontmatter.json +0 -6
  68. package/agents/ralph/copilot/polling.md +0 -62
  69. package/agents/ralph-plan/copilot/frontmatter.json +0 -6
  70. package/agents/ralph-plan.agent.md +0 -126
  71. package/agents/ralph.agent.md +0 -331
  72. package/agents/strategy/copilot/frontmatter.json +0 -6
  73. package/agents/strategy.agent.md +0 -112
  74. package/bin/installers/copilot.mjs +0 -124
  75. package/extensions/session-start.ts +0 -44
  76. package/hooks.json +0 -10
  77. package/lib/pi-packages.mjs +0 -169
  78. package/plugin.json +0 -35
  79. package/scripts/build-copilot-agents.mjs +0 -30
  80. package/scripts/install-instructions.sh +0 -82
  81. package/scripts/sync-plugin-version.sh +0 -17
  82. package/test/copilot-installer.test.mjs +0 -308
  83. package/test/pi-packages.test.mjs +0 -295
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Subagent agent discovery — the directories the "user" scope reads.
3
+ *
4
+ * pi has no agent resource type: agents ship beside the extensions that serve
5
+ * them, so user-scope discovery covers pi's own user agent dir (hand-written
6
+ * personal agents, plus copies older installers made) and the package this
7
+ * extension ships inside. `agent-cortex install pi` renders the flat pi agents
8
+ * into `<package>/agents`; a source checkout keeps its composable tree there
9
+ * instead, plus flat renders for other harnesses, which are not pi's agents.
10
+ */
11
+
12
+ import assert from "node:assert/strict";
13
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { dirname, join } from "node:path";
16
+ import { describe, it } from "node:test";
17
+ import { fileURLToPath } from "node:url";
18
+ import { discoverAgents, loadUserAgents, parseToolList, resolveUserAgentDirs } from "./agents.ts";
19
+
20
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
21
+
22
+ /** Write a flat agent file the way a renderer does: frontmatter plus body. */
23
+ function writeAgent(dir: string, name: string, body = `# ${name}\n`): string {
24
+ const filePath = join(dir, `${name}.agent.md`);
25
+ mkdirSync(dir, { recursive: true });
26
+ writeFileSync(filePath, `---\nname: "${name}"\ndescription: "${name} agent"\n---\n\n${body}`);
27
+ return filePath;
28
+ }
29
+
30
+ /** Write a composable agent source dir: `<agentsDir>/<name>/agent.md`. */
31
+ function writeComposableAgent(agentsDir: string, name: string): void {
32
+ const dir = join(agentsDir, name);
33
+ mkdirSync(dir, { recursive: true });
34
+ writeFileSync(join(dir, "agent.md"), `# ${name}\n`);
35
+ }
36
+
37
+ describe("resolveUserAgentDirs", () => {
38
+ it("reads the agents a staged package rendered beside the extension", () => {
39
+ const root = mkdtempSync(join(tmpdir(), "subagent-package-"));
40
+ try {
41
+ const shipped = writeAgent(join(root, "agents"), "ralph", "Staged ralph body.\n");
42
+
43
+ const dirs = resolveUserAgentDirs(root);
44
+
45
+ assert.ok(dirs.includes(join(root, "agents")), `expected ${join(root, "agents")} in ${dirs.join(", ")}`);
46
+ assert.equal(loadUserAgents(dirs).find((agent) => agent.name === "ralph")?.filePath, shipped);
47
+ } finally {
48
+ rmSync(root, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ it("leaves a source checkout's composable agents tree to the agent-modes extension", () => {
53
+ const root = mkdtempSync(join(tmpdir(), "subagent-checkout-"));
54
+ try {
55
+ writeComposableAgent(join(root, "agents"), "ralph");
56
+ writeAgent(join(root, "agents"), "ralph", "Copilot render.\n");
57
+
58
+ const dirs = resolveUserAgentDirs(root);
59
+
60
+ assert.ok(!dirs.includes(join(root, "agents")), `checkout agents dir must not be read: ${dirs.join(", ")}`);
61
+ } finally {
62
+ rmSync(root, { recursive: true, force: true });
63
+ }
64
+ });
65
+ });
66
+
67
+ describe("discoverAgents (user scope)", () => {
68
+ it("does not hand pi the flat renders this checkout holds for other harnesses", () => {
69
+ const { agents } = discoverAgents(tmpdir(), "user");
70
+
71
+ assert.ok(
72
+ agents.every((agent) => !agent.filePath.startsWith(join(PACKAGE_ROOT, "agents"))),
73
+ `checkout renders leaked into discovery: ${agents.map((agent) => agent.filePath).join(", ")}`,
74
+ );
75
+ });
76
+ });
77
+
78
+ describe("loadUserAgents", () => {
79
+ it("merges every dir and lets the earlier dir win a name collision", () => {
80
+ const root = mkdtempSync(join(tmpdir(), "subagent-agents-"));
81
+ try {
82
+ const personal = join(root, "pi", "agent", "agents");
83
+ const shipped = join(root, "package", "agents");
84
+ writeAgent(personal, "plan", "Personal plan body.\n");
85
+ writeAgent(personal, "personal-only", "Personal agent body.\n");
86
+ writeAgent(shipped, "plan", "Shipped plan body.\n");
87
+ writeAgent(shipped, "shipped-only", "Shipped agent body.\n");
88
+
89
+ const agents = loadUserAgents([personal, shipped]);
90
+
91
+ assert.deepEqual(
92
+ agents.map((agent) => agent.name).sort(),
93
+ ["personal-only", "plan", "shipped-only"],
94
+ );
95
+ assert.equal(agents.find((agent) => agent.name === "plan")?.filePath, join(personal, "plan.agent.md"));
96
+ } finally {
97
+ rmSync(root, { recursive: true, force: true });
98
+ }
99
+ });
100
+
101
+ it("reads a tool list whether it is a YAML scalar or sequence", () => {
102
+ const root = mkdtempSync(join(tmpdir(), "subagent-agents-"));
103
+ try {
104
+ const shipped = join(root, "agents");
105
+ mkdirSync(shipped, { recursive: true });
106
+ writeFileSync(join(shipped, "scalar.agent.md"), `---\nname: "scalar"\ndescription: "d"\ntools: "bash, read"\n---\n\n# scalar\n`);
107
+ writeFileSync(join(shipped, "sequence.agent.md"), `---\nname: "sequence"\ndescription: "d"\ntools: ["bash", "read"]\n---\n\n# sequence\n`);
108
+
109
+ const tools = Object.fromEntries(loadUserAgents([shipped]).map((agent) => [agent.name, agent.tools]));
110
+
111
+ assert.deepEqual(tools, { scalar: ["bash", "read"], sequence: ["bash", "read"] });
112
+ } finally {
113
+ rmSync(root, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ it("ignores directories that do not exist", () => {
118
+ const root = mkdtempSync(join(tmpdir(), "subagent-agents-"));
119
+ try {
120
+ const shipped = join(root, "agents");
121
+ writeAgent(shipped, "ralph", "Shipped ralph body.\n");
122
+
123
+ const agents = loadUserAgents([join(root, "missing"), shipped]);
124
+
125
+ assert.deepEqual(
126
+ agents.map((agent) => agent.name),
127
+ ["ralph"],
128
+ );
129
+ } finally {
130
+ rmSync(root, { recursive: true, force: true });
131
+ }
132
+ });
133
+ });
134
+
135
+ // The frontmatter `tools` value reaches this extension as one scalar string
136
+ // (pi's parseFrontmatter does not split it) and is turned into `pi --tools`,
137
+ // whose parser splits on commas. A whitespace-separated list used to collapse
138
+ // into a single bogus name, producing an allowlist that matched no tool — the
139
+ // delegated agent then started with an empty tool block (agnt-ctx-rpm3).
140
+
141
+ describe("parseToolList", () => {
142
+ it("splits the comma-separated form", () => {
143
+ assert.deepEqual(parseToolList("bash, read, grep, find, task"), [
144
+ "bash",
145
+ "read",
146
+ "grep",
147
+ "find",
148
+ "task",
149
+ ]);
150
+ });
151
+
152
+ it("splits the space-separated form instead of collapsing it to one name", () => {
153
+ assert.deepEqual(parseToolList("bash read grep find task"), [
154
+ "bash",
155
+ "read",
156
+ "grep",
157
+ "find",
158
+ "task",
159
+ ]);
160
+ });
161
+
162
+ it("tolerates mixed and padded separators", () => {
163
+ assert.deepEqual(parseToolList(" bash,read grep ,\ttask "), [
164
+ "bash",
165
+ "read",
166
+ "grep",
167
+ "task",
168
+ ]);
169
+ });
170
+
171
+ it("returns undefined when there is no tool list", () => {
172
+ assert.equal(parseToolList(undefined), undefined);
173
+ assert.equal(parseToolList(""), undefined);
174
+ assert.equal(parseToolList(" "), undefined);
175
+ });
176
+ });
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
8
9
 
9
10
  export type AgentScope = "user" | "project" | "both";
@@ -23,6 +24,25 @@ export interface AgentDiscoveryResult {
23
24
  projectAgentsDir: string | null;
24
25
  }
25
26
 
27
+ /**
28
+ * Parse a frontmatter `tools` value into PI tool names.
29
+ *
30
+ * `parseFrontmatter` returns the scalar as a single string, and agent files are
31
+ * written with either a comma-separated list (`tools: read, grep`) or a
32
+ * whitespace-separated one. Splitting on commas alone collapses the whitespace
33
+ * form into one bogus name, which becomes a `--tools` allowlist matching no
34
+ * tool — the subagent then runs with an empty tool block. Accept both
35
+ * separators so neither form can zero out the tool list.
36
+ *
37
+ * @param {string | undefined} tools Raw frontmatter `tools` scalar
38
+ * @returns {string[] | undefined} Tool names, or undefined when there are none
39
+ */
40
+ export function parseToolList(tools: string | undefined): string[] | undefined {
41
+ const parsed = tools?.split(/[,\s]+/).filter(Boolean);
42
+
43
+ return parsed && parsed.length > 0 ? parsed : undefined;
44
+ }
45
+
26
46
  function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
27
47
  const agents: AgentConfig[] = [];
28
48
 
@@ -49,22 +69,17 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
49
69
  continue;
50
70
  }
51
71
 
52
- const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
72
+ const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
53
73
 
54
- if (!frontmatter.name || !frontmatter.description) {
74
+ if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") {
55
75
  continue;
56
76
  }
57
77
 
58
- const tools = frontmatter.tools
59
- ?.split(",")
60
- .map((t: string) => t.trim())
61
- .filter(Boolean);
62
-
63
78
  agents.push({
64
79
  name: frontmatter.name,
65
80
  description: frontmatter.description,
66
- tools: tools && tools.length > 0 ? tools : undefined,
67
- model: frontmatter.model,
81
+ tools: readTools(frontmatter.tools),
82
+ model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
68
83
  systemPrompt: body,
69
84
  source,
70
85
  filePath,
@@ -74,6 +89,21 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
74
89
  return agents;
75
90
  }
76
91
 
92
+ /**
93
+ * The tools of a frontmatter `tools` value, which is written either as a YAML
94
+ * scalar (one string) or as a YAML sequence (an array).
95
+ */
96
+ function readTools(value: unknown): string[] | undefined {
97
+ if (Array.isArray(value)) {
98
+ const tools = value
99
+ .filter((tool): tool is string => typeof tool === "string")
100
+ .map((tool) => tool.trim())
101
+ .filter(Boolean);
102
+ return tools.length > 0 ? tools : undefined;
103
+ }
104
+ return parseToolList(typeof value === "string" ? value : undefined);
105
+ }
106
+
77
107
  function isDirectory(p: string): boolean {
78
108
  try {
79
109
  return fs.statSync(p).isDirectory();
@@ -94,11 +124,71 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
94
124
  }
95
125
  }
96
126
 
127
+ /**
128
+ * The package this extension ships inside: `<extension>/../..`, i.e. the repo
129
+ * checkout or the staged pi package.
130
+ */
131
+ const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
132
+
133
+ /**
134
+ * True when `agentsDir` is a rendered tree — flat `*.agent.md` files and no
135
+ * composable `<name>/agent.md` source directories.
136
+ */
137
+ function isRenderedAgentsTree(agentsDir: string): boolean {
138
+ if (!isDirectory(agentsDir)) return false;
139
+ let entries: fs.Dirent[];
140
+ try {
141
+ entries = fs.readdirSync(agentsDir, { withFileTypes: true });
142
+ } catch {
143
+ return false;
144
+ }
145
+ return !entries.some((entry) => entry.isDirectory() && fs.existsSync(path.join(agentsDir, entry.name, "agent.md")));
146
+ }
147
+
148
+ /**
149
+ * Directories the "user" scope discovers agents from, in precedence order.
150
+ *
151
+ * pi's own user agent dir comes first: it holds hand-written personal agents
152
+ * and the copies older installers made, so existing setups keep resolving what
153
+ * they did before. Second is the package this extension ships inside, which is
154
+ * what makes a staged pi package self-contained — `agent-cortex install pi`
155
+ * renders the flat pi agents into `<package>/agents`.
156
+ *
157
+ * A source checkout contributes nothing: its `agents/` tree is the composable
158
+ * source, composed at runtime by the agent-modes extension.
159
+ */
160
+ export function resolveUserAgentDirs(packageRoot: string = PACKAGE_ROOT): string[] {
161
+ const dirs: string[] = [];
162
+ const add = (dir: string) => {
163
+ const resolved = path.resolve(dir);
164
+ if (!dirs.includes(resolved) && isDirectory(resolved)) dirs.push(resolved);
165
+ };
166
+
167
+ add(path.join(getAgentDir(), "agents"));
168
+ const packageAgentsDir = path.join(packageRoot, "agents");
169
+ if (isRenderedAgentsTree(packageAgentsDir)) add(packageAgentsDir);
170
+
171
+ return dirs;
172
+ }
173
+
174
+ /**
175
+ * Load the user-scope agents from `dirs`, first directory wins on a name
176
+ * collision. Missing directories contribute nothing.
177
+ */
178
+ export function loadUserAgents(dirs: string[]): AgentConfig[] {
179
+ const byName = new Map<string, AgentConfig>();
180
+ for (const dir of dirs) {
181
+ for (const agent of loadAgentsFromDir(dir, "user")) {
182
+ if (!byName.has(agent.name)) byName.set(agent.name, agent);
183
+ }
184
+ }
185
+ return Array.from(byName.values());
186
+ }
187
+
97
188
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
98
- const userDir = path.join(getAgentDir(), "agents");
99
189
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
100
190
 
101
- const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
191
+ const userAgents = scope === "project" ? [] : loadUserAgents(resolveUserAgentDirs());
102
192
  const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
103
193
 
104
194
  const agentMap = new Map<string, AgentConfig>();
@@ -461,7 +461,8 @@ export default function (pi: ExtensionAPI) {
461
461
  description: [
462
462
  "Delegate tasks to specialized subagents with isolated context.",
463
463
  "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
464
- 'Default agent scope is "user" (from ~/.pi/agent/agents).',
464
+ 'Default agent scope is "user": ~/.pi/agent/agents plus the agents/ tree of the package',
465
+ "this extension ships inside.",
465
466
  'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
466
467
  ].join(" "),
467
468
  parameters: SubagentParams,
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --test --import tsx/esm wait.test.ts",
7
+ "test": "node --test --import tsx/esm wait.test.ts agents.test.ts",
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "devDependencies": {
package/lib/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- const SUPPORTED_HARNESSES = ["copilot", "claude", "pi"];
1
+ const SUPPORTED_HARNESSES = ["claude", "pi"];
2
2
 
3
3
  // Options accepted by `install <harness>`. `--output`/`--dry-run` are shared by
4
4
  // the claude and pi installers; `--plugin-root` is pi-specific. Unknown or
@@ -9,7 +9,6 @@ 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 },
13
12
  };
14
13
 
15
14
  export function parseArgs(argv) {
@@ -88,28 +87,53 @@ Commands:
88
87
  registration cannot run (missing CLI,
89
88
  pre-v2 build); default warns and prints
90
89
  the manual registration commands
91
- Options (copilot):
92
- --output <dir> Write the flat *.agent.md files to <dir>
93
- (default: <package root>/agents — the dir
94
- plugin.json "agents" scans)
95
- --dry-run Print what would be generated without writing
96
90
  Options (pi):
97
- --dry-run Show what would be installed without writing
98
- --output <dir> Install into <dir> — agents/, skills/,
99
- settings.json (merged, packages managed)
100
- and keybindings.json (default: ~/.pi/agent)
91
+ (no --output) Materialise the pi package into
92
+ ~/.agent-cortex/pi: package.json,
93
+ agents/, skills/, extensions/ and
94
+ the bundled third-party pi packages
95
+ (pi-questions, pi-web-access), then
96
+ REGISTER that directory with pi via
97
+ "pi install" (user scope, idempotent
98
+ by state: a re-run is a no-op update,
99
+ and stale raw-repo sources are
100
+ removed so one source remains).
101
+ Once registered, the artifacts the
102
+ pre-staged-model installer rendered
103
+ into ~/.pi/agent are removed in a
104
+ one-time migration: agents and
105
+ skills whose names this package
106
+ ships or once shipped. Anything
107
+ else there is left alone, including
108
+ the whole extensions/ directory.
109
+ A migration that removed every
110
+ copy it attributed is recorded in
111
+ ~/.agent-cortex/legacy-sweep.json
112
+ (keyed by the pi user directory)
113
+ and never runs there twice; one
114
+ that could not be completed warns
115
+ and is retried on the next install.
116
+ --output <dir> Stage the package into <dir> only —
117
+ no registration (default:
118
+ ~/.agent-cortex/pi)
119
+ --dry-run Show what would be staged and
120
+ registered without writing or spawning
101
121
  --plugin-root <dir> Override the plugin root used for {{PATH:...}}
102
122
  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.
123
+ --require-register
124
+ Fail the install when the pi CLI
125
+ cannot register the staged package
126
+ (missing from PATH); default warns and
127
+ prints the manual commands
107
128
  --help, -h Show this help text`;
108
129
  }
109
130
 
110
131
  export function validateHarness(harness) {
111
132
  if (harness === null) {
112
- return { ok: false, error: "Missing harness argument. Supported harnesses: copilot, claude, pi" };
133
+ return {
134
+ ok: false,
135
+ error: `Missing harness argument. Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}`,
136
+ };
113
137
  }
114
138
  if (!SUPPORTED_HARNESSES.includes(harness)) {
115
139
  return { ok: false, error: `Unknown harness "${harness}". Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaybeeuu/agent-cortex",
3
- "version": "1.42.0",
3
+ "version": "2.0.0",
4
4
  "description": "Personal PI package with custom agents, skills, and extensions",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -27,23 +27,22 @@
27
27
  ],
28
28
  "skills": [
29
29
  "./skills"
30
- ],
31
- "packages": [
32
- "npm:pi-questions",
33
- "npm:pi-web-access@0.10.7"
34
30
  ]
35
31
  },
32
+ "dependencies": {
33
+ "pi-questions": "^0.3.4",
34
+ "pi-web-access": "0.10.7"
35
+ },
36
36
  "scripts": {
37
37
  "build": "echo 'Build complete — no compilation needed for PI package'",
38
38
  "lint": "pnpm typecheck && pnpm check:sync",
39
39
  "check:sync": "node scripts/check-no-sync.mjs",
40
40
  "test": "node --test test/*.test.mjs && pnpm -r test",
41
41
  "typecheck": "pnpm -r typecheck",
42
- "build:copilot": "node scripts/build-copilot-agents.mjs",
43
42
  "changeset": "changeset",
44
- "version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:copilot",
43
+ "version-packages": "changeset version",
45
44
  "publish-package": "TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
46
- "release": "changeset publish",
47
- "postinstall": "bash scripts/install-instructions.sh"
45
+ "publish-noop": "echo 'No publish: the merged PR carries the no-publish label.'",
46
+ "release": "changeset publish"
48
47
  }
49
48
  }
package/pi/settings.json CHANGED
@@ -6,15 +6,9 @@
6
6
  },
7
7
  "lastChangelogVersion": "0.87.1",
8
8
  "packages": [
9
- {
10
- "source": "../../src/agent-cortex",
11
- "skills": []
12
- },
13
- "npm:pi-web-access@0.10.7",
14
9
  "npm:@getpipher/vision",
15
10
  "npm:@hypabolic/pi-hypa",
16
- "npm:context-mode",
17
- "npm:pi-questions"
11
+ "npm:context-mode"
18
12
  ],
19
13
  "defaultProvider": "opencode-go",
20
14
  "defaultModel": "deepseek-v4.1-flash",
@@ -5,4 +5,10 @@ packages:
5
5
  allowBuilds:
6
6
  '@google/genai': false
7
7
  esbuild: false
8
+ # koffi (an optionalDependency of @mariozechner/pi-tui, which pi-questions
9
+ # peers on) ships prebuilt build/koffi/<platform>_<arch>/*.node binaries that
10
+ # index.js loads directly. Its install script (cnoke --prebuild) only
11
+ # matters for toolchains with no shipped binary, so deny it: acknowledged
12
+ # explicitly to satisfy the strict allowlist, no compilation, no download.
13
+ koffi: false
8
14
  protobufjs: false
@@ -0,0 +1,42 @@
1
+ // PR gate: a pull request must add a changeset (.changeset/*.md) unless it is
2
+ // the changesets bot's Version Packages PR or carries the `no-publish` label.
3
+ // Reads `git diff --name-status <base>...HEAD` on stdin and the PR metadata
4
+ // from the environment (CI supplies all three).
5
+ import { evaluateChangesetGate, parseNameStatus } from "./lib/release-gates.mjs";
6
+
7
+ async function readStdin() {
8
+ const chunks = [];
9
+ for await (const chunk of process.stdin) chunks.push(chunk);
10
+ return Buffer.concat(chunks).toString("utf8");
11
+ }
12
+
13
+ function readLabels(raw) {
14
+ try {
15
+ return JSON.parse(raw);
16
+ } catch {
17
+ console.error("::error::PR_LABELS must be a JSON array of label names.");
18
+ process.exit(1);
19
+ }
20
+ }
21
+
22
+ const labels = readLabels(process.env.PR_LABELS ?? "[]");
23
+ const changes = parseNameStatus(await readStdin());
24
+ const result = evaluateChangesetGate({
25
+ changes,
26
+ labels,
27
+ headRef: process.env.PR_HEAD_REF ?? "",
28
+ actor: process.env.PR_ACTOR ?? "",
29
+ });
30
+
31
+ if (result.ok) {
32
+ console.log(`✓ Changeset gate passed (${result.reason}).`);
33
+ } else {
34
+ console.error(
35
+ [
36
+ "::error::This PR adds no changeset (.changeset/*.md).",
37
+ "Publishing is the default: write a changeset file (see the style-versioning skill)",
38
+ "and commit it, or label the PR 'no-publish' when it genuinely ships nothing.",
39
+ ].join("\n"),
40
+ );
41
+ process.exitCode = 1;
42
+ }
@@ -1,8 +1,8 @@
1
1
  // Shared composer: reads a composable agent directory (agents/<name>/) and emits
2
- // the harness-specific agent content defined by agents/README.md. The Copilot
3
- // generator (scripts/build-copilot-agents.mjs) and the Claude install-time generator
4
- // (bin/installers/claude.mjs) compose from here, so the composable dirs are
5
- // the single source of truth and the flat/generated files stay in lockstep.
2
+ // the harness-specific agent content defined by agents/README.md. The Claude and pi
3
+ // install-time generators (bin/installers/claude.mjs, bin/installers/pi.mjs) compose
4
+ // from here, so the composable dirs are the single source of truth and the generated
5
+ // files stay in lockstep.
6
6
  //
7
7
  // Composition per the agents/README.md contract:
8
8
  // 1. {{SECTION:name}} is replaced with <dir>/<harness>/<name>.md content.
@@ -15,15 +15,15 @@
15
15
  // silently dropped section.
16
16
  //
17
17
  // Options (the pi installer passes `dropNullTools`, `pluginRoot` and
18
- // `resolveRelativePaths`; the copilot generator and claude installer use the strict defaults):
18
+ // `resolveRelativePaths`; the claude installer uses the strict defaults):
19
19
  // dropNullTools replace null-mapped tool tokens with an empty string and
20
20
  // warn instead of throwing (pi's prose rule per the
21
21
  // token-map contract)
22
22
  // pluginRoot override the plugin_root used for resolved paths
23
23
  // (installers compute it from the actual install layout)
24
24
  // resolveRelativePaths non-keyed {{PATH:...}} args resolve to pluginRoot/arg
25
- // instead of staying verbatim (install time; the build
26
- // scripts leave them bare for Copilot CLI resolution)
25
+ // instead of staying verbatim (install time; without it
26
+ // they stay bare for runtime resolution)
27
27
  // warn callback for drop/omit warnings (default: no-op; the
28
28
  // pi installer collects them into its result)
29
29
  //
@@ -61,7 +61,7 @@ function noop() {}
61
61
  * the harness's section files before calling this.
62
62
  *
63
63
  * @param {string} content Source text containing {{TOOL:...}}/{{PATH:...}} tokens
64
- * @param {string} harness Harness id: "copilot" | "claude" | "pi"
64
+ * @param {string} harness Harness id: "claude" | "pi"
65
65
  * @param {object} tokenMap Parsed token-map.json (from loadTokenMap)
66
66
  * @param {object} options { dropNullTools, pluginRoot, resolveRelativePaths, warn, context }
67
67
  * @returns {string}
@@ -99,7 +99,7 @@ export function substituteTokens(content, harness, tokenMap, options = {}) {
99
99
  *
100
100
  * @param {string} root Package root (contains agents/ and token-map.json)
101
101
  * @param {string} name Agent directory name (e.g. "ralph", "plan")
102
- * @param {string} harness Harness id: "copilot" | "claude" | "pi"
102
+ * @param {string} harness Harness id: "claude" | "pi"
103
103
  * @returns {{ name: string, description: string, tools: string[], model?: string, argumentHint?: string, body: string }}
104
104
  */
105
105
  export async function composeAgent(root, name, harness, options = {}) {
@@ -218,7 +218,7 @@ function resolvePath(arg, harness, tokenMap, opts = {}) {
218
218
  *
219
219
  * @param {string[]} tools Canonical tool names from a harness frontmatter.json
220
220
  * @param {object} tokenMap Parsed token-map.json (from loadTokenMap)
221
- * @param {string} harness Harness id: "copilot" | "claude" | "pi"
221
+ * @param {string} harness Harness id: "claude" | "pi"
222
222
  * @param {object} options { warn }
223
223
  * @returns {string[]}
224
224
  */
@@ -0,0 +1,61 @@
1
+ // Decision logic for the two release gates that stop `main` re-publishing an
2
+ // already-published version. Both gates are pure: the CLI adapters read git
3
+ // diffs, env vars, and the GitHub API, then delegate the decision here.
4
+ //
5
+ // Model: publishing is the default. The only path to "no publish" is an
6
+ // explicit `no-publish` label on the merged PR — skipping is never implicit.
7
+
8
+ export const NO_PUBLISH_LABEL = "no-publish";
9
+ export const PUBLISH_COMMAND = "pnpm publish-package";
10
+ export const NO_PUBLISH_COMMAND = "pnpm publish-noop";
11
+
12
+ // The changesets bot's Version Packages PR consumes changesets and adds none.
13
+ const VERSION_PACKAGES_HEAD_REF = "changeset-release/";
14
+ const GITHUB_ACTIONS_BOT = "github-actions[bot]";
15
+
16
+ const CHANGESET_PATH = /^\.changeset\/[^/]+\.md$/;
17
+ const CHANGESET_EXEMPT_PATHS = new Set([".changeset/README.md"]);
18
+
19
+ /**
20
+ * Parse `git diff --name-status` output into `{ status, path }` entries, where
21
+ * `status` is the single-letter change type and `path` is the new path.
22
+ */
23
+ export function parseNameStatus(output) {
24
+ return output
25
+ .split("\n")
26
+ .filter((line) => line.trim() !== "")
27
+ .map((line) => {
28
+ const [status, ...paths] = line.split("\t");
29
+ return { status: status.charAt(0), path: paths.at(-1) };
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Decide whether a pull request may skip adding a changeset. Exempt only the
35
+ * Version Packages PR and PRs explicitly labelled `no-publish`; every other PR
36
+ * must add its own changeset.
37
+ */
38
+ export function evaluateChangesetGate({ changes, labels, headRef, actor }) {
39
+ if (headRef.startsWith(VERSION_PACKAGES_HEAD_REF) || actor === GITHUB_ACTIONS_BOT) {
40
+ return { ok: true, reason: "version-packages-pr" };
41
+ }
42
+ if (labels.includes(NO_PUBLISH_LABEL)) {
43
+ return { ok: true, reason: "no-publish-label" };
44
+ }
45
+ if (changes.some((change) => change.status === "A" && isChangeset(change.path))) {
46
+ return { ok: true, reason: "changeset-added" };
47
+ }
48
+ return { ok: false, reason: "missing-changeset" };
49
+ }
50
+
51
+ /**
52
+ * Resolve the command changesets/action should exec for publishing, based on
53
+ * the labels of the PR merged for the pushed commit.
54
+ */
55
+ export function resolvePublishCommand(labels) {
56
+ return labels.includes(NO_PUBLISH_LABEL) ? NO_PUBLISH_COMMAND : PUBLISH_COMMAND;
57
+ }
58
+
59
+ function isChangeset(path) {
60
+ return CHANGESET_PATH.test(path) && !CHANGESET_EXEMPT_PATHS.has(path);
61
+ }