@jaybeeuu/agent-cortex 1.42.0 → 1.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/style-versioning/SKILL.md +73 -2
- package/.github/workflows/ci.yml +47 -27
- package/AGENTS.md +23 -2
- package/CHANGELOG.md +166 -0
- package/README.md +125 -73
- package/agents/README.md +17 -4
- package/bin/agent-cortex.mjs +48 -30
- package/bin/installers/pi.mjs +885 -237
- package/docs/claude-hooks.md +5 -4
- package/docs/commit-signing.md +231 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +10 -1
- package/docs/ideas/extension-manifests.md +5 -0
- package/docs/research/disable-model-invocation.md +103 -0
- package/docs/research/skill-design-research.md +2 -2
- package/docs/skills/skill-anatomy.md +5 -1
- package/docs/technical-direction/harness-config.md +25 -1
- package/docs/technical-direction/migration-execution-labels.md +17 -9
- package/extensions/agent-modes/discover.test.ts +19 -0
- package/extensions/agent-modes/discover.ts +25 -3
- package/extensions/session-start/README.md +38 -0
- package/extensions/session-start/index.test.ts +122 -0
- package/extensions/session-start/index.ts +27 -0
- package/extensions/session-start/package.json +19 -0
- package/extensions/session-start/tsconfig.json +12 -0
- package/extensions/skill-stats/pnpm-lock.yaml +329 -0
- package/extensions/subagent/README.md +1 -1
- package/extensions/subagent/agents.test.ts +176 -0
- package/extensions/subagent/agents.ts +102 -11
- package/extensions/subagent/index.ts +2 -1
- package/extensions/subagent/package.json +1 -1
- package/lib/cli.mjs +35 -9
- package/package.json +6 -5
- package/pi/settings.json +1 -7
- package/plugin.json +1 -1
- package/pnpm-workspace.yaml +6 -0
- package/scripts/check-changeset.mjs +42 -0
- package/scripts/lib/release-gates.mjs +61 -0
- package/scripts/lib/verify-worker-tree.mjs +107 -0
- package/scripts/resolve-publish-command.mjs +17 -0
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
- package/skills/workflow/git-workflow/SKILL.md +35 -1
- package/skills/workflow/ralph/REFERENCE.md +17 -1
- package/skills/workflow/ralph/SKILL.md +4 -1
- package/skills/workflow/run-pipeline-stage/SKILL.md +11 -4
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +2 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +3 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +11 -0
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
- package/test/cli.test.mjs +26 -29
- package/test/helpers/fake-pi.mjs +132 -0
- package/test/pi-installer.test.mjs +345 -286
- package/test/pi-register.test.mjs +342 -0
- package/test/pi-sweep.test.mjs +619 -0
- package/test/release-gates.test.mjs +190 -0
- package/test/verify-worker-tree.test.mjs +120 -0
- package/token-map.README.md +7 -6
- package/token-map.json +6 -5
- package/extensions/session-start.ts +0 -44
- package/lib/pi-packages.mjs +0 -169
- 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,
|
|
72
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
|
|
53
73
|
|
|
54
|
-
if (
|
|
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:
|
|
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,72 @@ 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) alongside flat
|
|
159
|
+
* renders for other harnesses — Copilot's, whose tool names pi does not have.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveUserAgentDirs(packageRoot: string = PACKAGE_ROOT): string[] {
|
|
162
|
+
const dirs: string[] = [];
|
|
163
|
+
const add = (dir: string) => {
|
|
164
|
+
const resolved = path.resolve(dir);
|
|
165
|
+
if (!dirs.includes(resolved) && isDirectory(resolved)) dirs.push(resolved);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
add(path.join(getAgentDir(), "agents"));
|
|
169
|
+
const packageAgentsDir = path.join(packageRoot, "agents");
|
|
170
|
+
if (isRenderedAgentsTree(packageAgentsDir)) add(packageAgentsDir);
|
|
171
|
+
|
|
172
|
+
return dirs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Load the user-scope agents from `dirs`, first directory wins on a name
|
|
177
|
+
* collision. Missing directories contribute nothing.
|
|
178
|
+
*/
|
|
179
|
+
export function loadUserAgents(dirs: string[]): AgentConfig[] {
|
|
180
|
+
const byName = new Map<string, AgentConfig>();
|
|
181
|
+
for (const dir of dirs) {
|
|
182
|
+
for (const agent of loadAgentsFromDir(dir, "user")) {
|
|
183
|
+
if (!byName.has(agent.name)) byName.set(agent.name, agent);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return Array.from(byName.values());
|
|
187
|
+
}
|
|
188
|
+
|
|
97
189
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
98
|
-
const userDir = path.join(getAgentDir(), "agents");
|
|
99
190
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
100
191
|
|
|
101
|
-
const userAgents = scope === "project" ? [] :
|
|
192
|
+
const userAgents = scope === "project" ? [] : loadUserAgents(resolveUserAgentDirs());
|
|
102
193
|
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
103
194
|
|
|
104
195
|
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"
|
|
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,
|
package/lib/cli.mjs
CHANGED
|
@@ -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) {
|
|
@@ -94,16 +93,43 @@ Commands:
|
|
|
94
93
|
plugin.json "agents" scans)
|
|
95
94
|
--dry-run Print what would be generated without writing
|
|
96
95
|
Options (pi):
|
|
97
|
-
--
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
(no --output) Materialise the pi package into
|
|
97
|
+
~/.agent-cortex/pi: package.json,
|
|
98
|
+
agents/, skills/, extensions/ and
|
|
99
|
+
the bundled third-party pi packages
|
|
100
|
+
(pi-questions, pi-web-access), then
|
|
101
|
+
REGISTER that directory with pi via
|
|
102
|
+
"pi install" (user scope, idempotent
|
|
103
|
+
by state: a re-run is a no-op update,
|
|
104
|
+
and stale raw-repo sources are
|
|
105
|
+
removed so one source remains).
|
|
106
|
+
Once registered, the artifacts the
|
|
107
|
+
pre-staged-model installer rendered
|
|
108
|
+
into ~/.pi/agent are removed in a
|
|
109
|
+
one-time migration: agents and
|
|
110
|
+
skills whose names this package
|
|
111
|
+
ships or once shipped. Anything
|
|
112
|
+
else there is left alone, including
|
|
113
|
+
the whole extensions/ directory.
|
|
114
|
+
A migration that removed every
|
|
115
|
+
copy it attributed is recorded in
|
|
116
|
+
~/.agent-cortex/legacy-sweep.json
|
|
117
|
+
(keyed by the pi user directory)
|
|
118
|
+
and never runs there twice; one
|
|
119
|
+
that could not be completed warns
|
|
120
|
+
and is retried on the next install.
|
|
121
|
+
--output <dir> Stage the package into <dir> only —
|
|
122
|
+
no registration (default:
|
|
123
|
+
~/.agent-cortex/pi)
|
|
124
|
+
--dry-run Show what would be staged and
|
|
125
|
+
registered without writing or spawning
|
|
101
126
|
--plugin-root <dir> Override the plugin root used for {{PATH:...}}
|
|
102
127
|
tokens (default: token-map.json's pi value)
|
|
103
|
-
--
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
--require-register
|
|
129
|
+
Fail the install when the pi CLI
|
|
130
|
+
cannot register the staged package
|
|
131
|
+
(missing from PATH); default warns and
|
|
132
|
+
prints the manual commands
|
|
107
133
|
--help, -h Show this help text`;
|
|
108
134
|
}
|
|
109
135
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaybeeuu/agent-cortex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.0",
|
|
4
4
|
"description": "Personal PI package with custom agents, skills, and extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -27,12 +27,12 @@
|
|
|
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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"changeset": "changeset",
|
|
44
44
|
"version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:copilot",
|
|
45
45
|
"publish-package": "TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
|
|
46
|
+
"publish-noop": "echo 'No publish: the merged PR carries the no-publish label.'",
|
|
46
47
|
"release": "changeset publish",
|
|
47
48
|
"postinstall": "bash scripts/install-instructions.sh"
|
|
48
49
|
}
|
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",
|
package/plugin.json
CHANGED
package/pnpm-workspace.yaml
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|