@jaybeeuu/agent-cortex 1.41.1 → 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 +52 -32
- package/AGENTS.md +23 -2
- package/CHANGELOG.md +189 -0
- package/README.md +125 -73
- package/agents/README.md +17 -4
- package/agents/ralph-plan/agent.md +7 -3
- package/agents/ralph-plan.agent.md +7 -3
- package/bin/agent-cortex.mjs +48 -30
- package/bin/installers/pi.mjs +885 -237
- package/claude-extras/scripts/statusline-command.sh +0 -0
- package/docs/claude-hooks.md +5 -4
- package/docs/commit-signing.md +231 -0
- package/docs/ideas/ban-content-assertion-tests.md +3 -1
- package/docs/ideas/dedicated-review-tests-skill.md +71 -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/ideas/unify-the-ticket-and-task-decomposition-workflows.md +71 -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/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 +10 -6
- 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/install-instructions.sh +0 -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/scripts/sync-plugin-version.sh +0 -0
- package/skills/engineering/grill-with-docs/SKILL.md +3 -2
- package/skills/engineering/improve-codebase-architecture/REFERENCE.md +5 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +2 -1
- package/skills/planning/create-task/SKILL.md +8 -3
- package/skills/planning/create-task/scripts/create-chores.ts +4 -1
- package/skills/planning/prd-to-tasks/REFERENCE.md +2 -2
- package/skills/planning/prd-to-tasks/SKILL.md +9 -4
- package/skills/planning/request-refactor-plan/FORMAT.md +5 -0
- package/skills/planning/request-refactor-plan/SKILL.md +3 -2
- package/skills/planning/spec-to-tickets/REFERENCE.md +82 -0
- package/skills/planning/spec-to-tickets/SKILL.md +143 -0
- package/skills/planning/write-a-prd/FORMAT.md +6 -0
- package/skills/planning/write-a-prd/SKILL.md +9 -6
- package/skills/planning/write-a-ticket/EXAMPLES.md +133 -0
- package/skills/planning/write-a-ticket/SKILL.md +77 -59
- package/skills/productivity/bd-tool/SKILL.md +27 -2
- package/skills/productivity/grill-me/SKILL.md +11 -0
- package/skills/review/review-security/scripts/install-gitleaks.sh +0 -0
- package/skills/workflow/git-workflow/SKILL.md +35 -1
- package/skills/workflow/plan/SKILL.md +20 -8
- package/skills/workflow/ralph/REFERENCE.md +20 -2
- package/skills/workflow/ralph/SKILL.md +4 -1
- package/skills/workflow/run-pipeline-stage/SKILL.md +17 -5
- 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/using-agent-skills/SKILL.md +1 -0
- package/test/cli.test.mjs +26 -29
- package/test/create-chores.test.mjs +12 -0
- package/test/fake-bd.test.mjs +72 -0
- package/test/helpers/fake-bd.mjs +26 -6
- 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/agent-guardrails/node_modules/.bin/pi +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsc +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsx +0 -21
- package/extensions/agent-modes/node_modules/.bin/pi +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsc +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsserver +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsx +0 -21
- package/extensions/session-start.ts +0 -44
- package/extensions/skill-stats/node_modules/.bin/pi +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsc +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsserver +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsx +0 -21
- package/extensions/subagent/node_modules/.bin/pi +0 -21
- package/extensions/subagent/node_modules/.bin/pi-ai +0 -21
- package/extensions/subagent/node_modules/.bin/tsc +0 -21
- package/extensions/subagent/node_modules/.bin/tsserver +0 -21
- package/extensions/subagent/node_modules/.bin/tsx +0 -21
- package/lib/pi-packages.mjs +0 -169
- package/skills/planning/create-task/scripts/node_modules/.bin/tsc +0 -21
- package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/planning/create-task/scripts/node_modules/.bin/tsx +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +0 -21
- package/test/pi-packages.test.mjs +0 -295
|
@@ -13,14 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
## Current-state constraints
|
|
15
15
|
|
|
16
|
-
- bd
|
|
17
|
-
|
|
16
|
+
- bd is a globally installed CLI resolved from `PATH` (whatever single `bd` is on PATH —
|
|
17
|
+
Homebrew, npm, or the install script); `BD_PATH` is the override for machines where `bd` is
|
|
18
|
+
not on PATH. Skill scripts invoke `BD_PATH || 'bd'` and never hardcode an install path.
|
|
19
|
+
Label ops: `bd label list-all [--json]`, `bd list --label-pattern/regex [--json]`,
|
|
20
|
+
`bd label add/remove` (batch multiple ids).
|
|
21
|
+
- A `bd` whose install target has been removed (`~/.local/bin` orphans, dangling pnpm shims)
|
|
22
|
+
must not be pinned: it can be a schema behind the workspace DB and fail with a schema
|
|
23
|
+
version mismatch. Keep exactly one `bd` on PATH so `bd` and the DB version agree.
|
|
18
24
|
- `bd list` supports `--label-pattern`/`--json`; `bd count` does NOT support `--label-pattern`.
|
|
19
25
|
- `bd ready --label-pattern 'stage:*'` works; `bd ready -l execution:afk` verified.
|
|
20
|
-
- 3 script packages exist with
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
26
|
+
- 3 script packages still exist with their own `package.json` (create-task,
|
|
27
|
+
validate-agent-dir, run-pipeline-stage). They ARE root workspace members (root glob
|
|
28
|
+
`skills/**/scripts`; all three are in the root `pnpm-lock.yaml`). Residual per-skill
|
|
29
|
+
`pnpm-lock.yaml` (all three) and `pnpm-workspace.yaml` (validate-agent-dir,
|
|
30
|
+
run-pipeline-stage) are stale artefacts of the pre-D2 layout.
|
|
24
31
|
- All 3 scripts are stdlib-only (`node:child_process`, `node:fs`, `node:path`, `node:url`).
|
|
25
32
|
- Extensions (`extensions/skill-stats`) have real peer deps on `@earendil-works/pi-coding-agent`
|
|
26
33
|
— they remain workspace members with their own package.json.
|
|
@@ -162,8 +169,9 @@
|
|
|
162
169
|
|
|
163
170
|
## References
|
|
164
171
|
|
|
165
|
-
- Code evidence: `bin/agent-cortex.mjs` + `lib/cli.mjs` (CLI surface),
|
|
166
|
-
package.json` (3
|
|
167
|
-
(`bd list`, `bd label`, `bd create`), epic
|
|
172
|
+
- Code evidence: `bin/agent-cortex.mjs` + `lib/cli.mjs` (CLI surface),
|
|
173
|
+
`skills/*/*/scripts/package.json` (3, root workspace members), root `package.json` +
|
|
174
|
+
`pnpm-workspace.yaml`, bd CLI help (`bd list`, `bd label`, `bd create`), epic
|
|
175
|
+
`agnt-ctx-s5xd` notes.
|
|
168
176
|
- Idea file: `docs/ideas/ctx-alias-version-and-update-commands.md` (ctx alias + version/update
|
|
169
177
|
surface that `migrate` joins).
|
|
@@ -244,6 +244,25 @@ describe("discoverAgents (flat fallback)", () => {
|
|
|
244
244
|
assert.deepEqual(strategy.tools, ["bash", "fetch_content", "read"]);
|
|
245
245
|
assert.match(strategy.prompt, /Flat body of strategy\./);
|
|
246
246
|
});
|
|
247
|
+
it("reads a space-separated scalar tools list instead of falling back to defaults", () => {
|
|
248
|
+
const fx = track(makeFixture());
|
|
249
|
+
writeFlatAgent(fx, "ralph", FLAT_FRONTMATTER("ralph", '"bash read grep task"'));
|
|
250
|
+
|
|
251
|
+
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
252
|
+
|
|
253
|
+
// A collapsed scalar would land on DEFAULT_TOOLS, lifting the restriction
|
|
254
|
+
// rather than applying it — the tool names must survive the parse.
|
|
255
|
+
assert.deepEqual(agents[0].tools, ["bash", "grep", "read", "task"]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("reads a comma-separated scalar tools list", () => {
|
|
259
|
+
const fx = track(makeFixture());
|
|
260
|
+
writeFlatAgent(fx, "ralph", FLAT_FRONTMATTER("ralph", '"bash, view, task"'));
|
|
261
|
+
|
|
262
|
+
const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
|
|
263
|
+
|
|
264
|
+
assert.deepEqual(agents[0].tools, ["bash", "read", "task"]);
|
|
265
|
+
});
|
|
247
266
|
});
|
|
248
267
|
|
|
249
268
|
// ─── loadTokenMap ────────────────────────────────────────────────────────────
|
|
@@ -290,6 +290,21 @@ interface FlatParsed {
|
|
|
290
290
|
prompt: string;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Split a scalar `tools` value into tool names.
|
|
295
|
+
*
|
|
296
|
+
* Accepts the quoted string the pi installer renders (`tools: "bash, read"`)
|
|
297
|
+
* as well as an unquoted whitespace-separated list (`tools: bash read`).
|
|
298
|
+
* Commas alone are not enough: a whitespace-separated list would collapse to
|
|
299
|
+
* one bogus name and the agent would run with no usable tool allowlist.
|
|
300
|
+
*/
|
|
301
|
+
function splitToolList(raw: string): string[] {
|
|
302
|
+
return raw
|
|
303
|
+
.replace(/^["']|["']$/g, "")
|
|
304
|
+
.split(/[,\s]+/)
|
|
305
|
+
.filter(Boolean);
|
|
306
|
+
}
|
|
307
|
+
|
|
293
308
|
function parseFlatFrontmatter(raw: string): FlatParsed {
|
|
294
309
|
const lines = raw.split(/\r?\n/);
|
|
295
310
|
if (lines[0] !== "---") return { prompt: raw };
|
|
@@ -327,11 +342,16 @@ function parseFlatFrontmatter(raw: string): FlatParsed {
|
|
|
327
342
|
}
|
|
328
343
|
|
|
329
344
|
// Parse tools from the tools frontmatter.
|
|
330
|
-
// Supports
|
|
331
|
-
// tools: ["bash", "view"]
|
|
332
|
-
// tools:
|
|
345
|
+
// Supports three formats:
|
|
346
|
+
// tools: ["bash", "view"] (JSON array, single line)
|
|
347
|
+
// tools: "bash, view" (scalar list, comma or whitespace separated)
|
|
348
|
+
// tools: (YAML list, subsequent lines)
|
|
333
349
|
// - bash
|
|
334
350
|
// - view
|
|
351
|
+
//
|
|
352
|
+
// The scalar form is what the pi installer renders for generated agents, so
|
|
353
|
+
// it must be read here rather than dropped to DEFAULT_TOOLS (which lifts the
|
|
354
|
+
// restriction instead of applying it).
|
|
335
355
|
let inToolsBlock = false;
|
|
336
356
|
for (const line of yamlLines) {
|
|
337
357
|
const trimmed = line.trim();
|
|
@@ -354,6 +374,8 @@ function parseFlatFrontmatter(raw: string): FlatParsed {
|
|
|
354
374
|
tools = [];
|
|
355
375
|
continue;
|
|
356
376
|
}
|
|
377
|
+
tools = splitToolList(val);
|
|
378
|
+
continue;
|
|
357
379
|
}
|
|
358
380
|
}
|
|
359
381
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# session-start
|
|
2
|
+
|
|
3
|
+
A PI extension that injects, at the start of every session, the project's context files:
|
|
4
|
+
|
|
5
|
+
- `AGENTS.md`
|
|
6
|
+
- `docs/user-preferences.md`
|
|
7
|
+
|
|
8
|
+
Paths are relative to the session working directory (`ctx.cwd`); missing or empty files are
|
|
9
|
+
skipped. Content is injected as a hidden `session-start` message on `before_agent_start`, so it
|
|
10
|
+
reaches the model without cluttering the transcript UI.
|
|
11
|
+
|
|
12
|
+
## No skill list here — on purpose
|
|
13
|
+
|
|
14
|
+
Earlier versions of this extension appended a hand-written blurb advertising three skills with
|
|
15
|
+
`/skill:<name>` syntax. Both halves were wrong:
|
|
16
|
+
|
|
17
|
+
- pi has no `/skill:` resolver — nothing maps that token to a read. Skills are used by reading
|
|
18
|
+
the `SKILL.md` path that pi lists.
|
|
19
|
+
- pi's own system prompt already lists **every** discovered skill with its absolute `SKILL.md`
|
|
20
|
+
path, so a local three-skill list was incomplete and drifted from `skills/` silently.
|
|
21
|
+
|
|
22
|
+
The blurb is gone and `index.test.ts` pins the absence of both `/skill:` syntax and hardcoded
|
|
23
|
+
skill names. Keep the authoritative list in `skills/`; do not reintroduce it here.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Bundled with the `agent-cortex` PI package — install the package for auto-discovery:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install git:https://github.com/jaybeeuu/agent-cortex
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm --filter @agent-cortex/session-start test
|
|
37
|
+
pnpm --filter @agent-cortex/session-start typecheck
|
|
38
|
+
```
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { after, before, describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
import sessionStartExtension from "./index.ts";
|
|
9
|
+
|
|
10
|
+
// ─── Harness ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
interface InjectedMessage {
|
|
13
|
+
customType: string;
|
|
14
|
+
content: string;
|
|
15
|
+
display: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A handler recorded from `pi.on`, invoked via Reflect.apply so its exact
|
|
19
|
+
* overload signature does not leak into the fake. */
|
|
20
|
+
type RecordedHandler = (...args: never[]) => unknown;
|
|
21
|
+
|
|
22
|
+
interface FakePi {
|
|
23
|
+
pi: ExtensionAPI;
|
|
24
|
+
/** Simulate pi dispatching `before_agent_start` and return the injected message. */
|
|
25
|
+
start(cwd: string): Promise<InjectedMessage | undefined>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createFakePi(): FakePi {
|
|
29
|
+
const handlers = new Map<string, RecordedHandler[]>();
|
|
30
|
+
|
|
31
|
+
const pi: ExtensionAPI = {
|
|
32
|
+
on: (event: string, handler: RecordedHandler) => {
|
|
33
|
+
const list = handlers.get(event) ?? [];
|
|
34
|
+
list.push(handler);
|
|
35
|
+
handlers.set(event, list);
|
|
36
|
+
},
|
|
37
|
+
} as unknown as ExtensionAPI;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
pi,
|
|
41
|
+
async start(cwd) {
|
|
42
|
+
const handler = handlers.get("before_agent_start")?.[0];
|
|
43
|
+
assert.ok(handler, "session-start should register a before_agent_start handler");
|
|
44
|
+
const run = handler as unknown as (
|
|
45
|
+
event: unknown,
|
|
46
|
+
ctx: { cwd: string },
|
|
47
|
+
) => Promise<{ message?: InjectedMessage } | undefined>;
|
|
48
|
+
const result = await run({}, { cwd });
|
|
49
|
+
return result?.message;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Run one simulated session start against `cwd` and return the injected text. */
|
|
55
|
+
async function inject(cwd: string): Promise<string | undefined> {
|
|
56
|
+
const fake = createFakePi();
|
|
57
|
+
sessionStartExtension(fake.pi);
|
|
58
|
+
const message = await fake.start(cwd);
|
|
59
|
+
return message?.content;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const AGENTS_MD = "# AGENTS\n\nProject instructions live here.";
|
|
65
|
+
const PREFERENCES_MD = "# User Preferences\n\nPrefer pnpm over npm.";
|
|
66
|
+
|
|
67
|
+
describe("session-start injection", () => {
|
|
68
|
+
let emptyProject: string;
|
|
69
|
+
let fullProject: string;
|
|
70
|
+
|
|
71
|
+
before(() => {
|
|
72
|
+
emptyProject = mkdtempSync(join(tmpdir(), "session-start-empty-"));
|
|
73
|
+
fullProject = mkdtempSync(join(tmpdir(), "session-start-full-"));
|
|
74
|
+
writeFileSync(join(fullProject, "AGENTS.md"), AGENTS_MD, "utf-8");
|
|
75
|
+
mkdirSync(join(fullProject, "docs"), { recursive: true });
|
|
76
|
+
writeFileSync(join(fullProject, "docs", "user-preferences.md"), PREFERENCES_MD, "utf-8");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
after(() => {
|
|
80
|
+
rmSync(emptyProject, { recursive: true, force: true });
|
|
81
|
+
rmSync(fullProject, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("injects AGENTS.md and docs/user-preferences.md content", async () => {
|
|
85
|
+
const content = await inject(fullProject);
|
|
86
|
+
assert.ok(content, "expected the context files to be injected");
|
|
87
|
+
assert.match(content, /--- AGENTS\.md ---/);
|
|
88
|
+
assert.match(content, /Project instructions live here\./);
|
|
89
|
+
assert.match(content, /--- user-preferences\.md ---/);
|
|
90
|
+
assert.match(content, /Prefer pnpm over npm\./);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("marks the injected message as a hidden session-start message", async () => {
|
|
94
|
+
const fake = createFakePi();
|
|
95
|
+
sessionStartExtension(fake.pi);
|
|
96
|
+
const message = await fake.start(fullProject);
|
|
97
|
+
|
|
98
|
+
assert.ok(message);
|
|
99
|
+
assert.equal(message.customType, "session-start");
|
|
100
|
+
assert.equal(message.display, false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("injects nothing for a project without context files", async () => {
|
|
104
|
+
const content = await inject(emptyProject);
|
|
105
|
+
assert.equal(content, undefined, "no context files means no injected message");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("never advertises skills with pi-unresolvable /skill: syntax", async () => {
|
|
109
|
+
const content = await inject(fullProject);
|
|
110
|
+
assert.ok(content);
|
|
111
|
+
assert.ok(!content.includes("/skill:"), "pi has no /skill: resolver — drop the hint blurb");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("never enumerates a fixed subset of skill names", async () => {
|
|
115
|
+
// pi's system prompt already lists every discovered skill with its SKILL.md
|
|
116
|
+
// path; a hand-maintained subset here drifts from skills/ silently.
|
|
117
|
+
const content = (await inject(fullProject)) ?? "";
|
|
118
|
+
for (const skill of ["using-agent-skills", "bd-tool", "git-workflow"]) {
|
|
119
|
+
assert.ok(!content.includes(skill), `${skill} should not be hardcoded in session-start`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadSessionContext } from "../lib/context-loader.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session-start extension.
|
|
6
|
+
*
|
|
7
|
+
* Injects the project context files (AGENTS.md, docs/user-preferences.md) at
|
|
8
|
+
* the start of every PI session.
|
|
9
|
+
*
|
|
10
|
+
* It deliberately injects nothing about skills: pi's own system prompt already
|
|
11
|
+
* lists every discovered skill with the absolute path to its SKILL.md, so a
|
|
12
|
+
* local copy is incomplete by construction and drifts from `skills/` silently.
|
|
13
|
+
*/
|
|
14
|
+
export default function (pi: ExtensionAPI) {
|
|
15
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
16
|
+
const contextFiles = loadSessionContext(ctx.cwd);
|
|
17
|
+
if (!contextFiles) return;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
message: {
|
|
21
|
+
customType: "session-start",
|
|
22
|
+
content: contextFiles,
|
|
23
|
+
display: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-cortex/session-start",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test --import tsx/esm index.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
|
+
}
|
|
@@ -58,7 +58,7 @@ This tool executes a separate `pi` subprocess with a delegated system prompt and
|
|
|
58
58
|
|
|
59
59
|
**Project-local agents** (`.pi/agents/*.md`) are repo-controlled prompts that can instruct the model to read files, run bash commands, etc.
|
|
60
60
|
|
|
61
|
-
**Default behavior:** Only loads **user-level agents**
|
|
61
|
+
**Default behavior:** Only loads **user-level agents** — pi's own `~/.pi/agent/agents`, plus the `agents/` tree of the package this extension ships inside when that is a rendered (staged) tree, e.g. `~/.agent-cortex/pi/agents` after `agent-cortex install pi`.
|
|
62
62
|
|
|
63
63
|
To enable project-local agents, pass `agentScope: "both"` (or `"project"`). Only do this for repositories you trust.
|
|
64
64
|
|
|
@@ -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
|
+
});
|