@jaybeeuu/agent-cortex 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +50 -0
- package/README.md +13 -2
- package/agents/ralph/agent.md +7 -8
- package/agents-native/ralph.md +15 -5
- package/bin/installers/claude.mjs +80 -9
- package/docs/claude-hooks.md +16 -0
- package/docs/ideas/improve-pi-system-prompt.md +9 -3
- package/docs/skills/skill-anatomy.md +3 -0
- package/extensions/output-style/README.md +58 -0
- package/extensions/output-style/index.ts +23 -0
- package/extensions/output-style/output-style.md +43 -0
- package/extensions/output-style/output-style.test.ts +50 -0
- package/extensions/output-style/output-style.ts +32 -0
- package/extensions/output-style/package.json +19 -0
- package/extensions/output-style/tsconfig.json +12 -0
- package/package.json +3 -2
- package/scripts/check-skill-frontmatter.mjs +21 -0
- package/scripts/lib/skill-frontmatter.mjs +135 -0
- package/skills/workflow/ralph/REFERENCE.md +31 -8
- package/skills/workflow/ralph/SKILL.md +6 -6
- package/skills/workflow/ralph/scripts/pending-gates.mjs +134 -0
- package/test/installer.test.mjs +143 -1
- package/test/pending-gates.test.mjs +147 -0
- package/test/skill-frontmatter.test.mjs +137 -0
package/AGENTS.md
CHANGED
|
@@ -71,7 +71,9 @@ Generated automatically by changesets. Do not edit `CHANGELOG.md` manually.
|
|
|
71
71
|
checkout (branch bases, merge method, signature rules, required checks, release mechanics) belong
|
|
72
72
|
in `AGENTS.md`, `docs/`, or the tracker. Repo-local `.agents/skills/` follows the same rule: a
|
|
73
73
|
skill is a skill, not a place for this repo's configuration.
|
|
74
|
-
- `SKILL.md` must include a YAML front-matter block with `name` and `description`.
|
|
74
|
+
- `SKILL.md` must include a YAML front-matter block with `name` and `description`. `pnpm lint`
|
|
75
|
+
enforces this (`pnpm check:skills`), because PI silently drops a skill whose frontmatter it
|
|
76
|
+
cannot parse.
|
|
75
77
|
- `description` must be ≤ 1024 characters. First sentence: what it does. Second: "Use when…".
|
|
76
78
|
- Keep `SKILL.md` near the ~150-line target in `docs/skills/skill-anatomy.md`, overflowing into
|
|
77
79
|
purpose-named support files (`REFERENCE.md`, `EXAMPLES.md`, `FORMAT.md`). `style-tests`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a4658cf: Add the `output-style` pi extension: it appends an always-on reply-shape block to the PI
|
|
8
|
+
system prompt — lead with the action, write in Simplified Technical English (ASD-STE100),
|
|
9
|
+
and restate context instead of citing beads, phases, or stages by ID alone.
|
|
10
|
+
|
|
11
|
+
The block lives in `extensions/output-style/output-style.md` and is also written to the managed
|
|
12
|
+
section of `~/.claude/CLAUDE.md` by `agent-cortex install claude`, so Claude Code receives the
|
|
13
|
+
same rules alongside pi.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- a30f3c4: Make ralph's PR gate brake depend on a gate being _unblocked_, and count gates
|
|
18
|
+
on the current HITL label axis.
|
|
19
|
+
|
|
20
|
+
`create-chores` pre-creates a `lifecycle:feature-pr` gate for every planned
|
|
21
|
+
feature, blocked behind that feature's own `document` chore. Ralph's rule — "do
|
|
22
|
+
not schedule new parent features while any HITL gate bead is open" — therefore
|
|
23
|
+
matched every unstarted feature, so the brake was permanently on: with eight
|
|
24
|
+
open gates, none of them actionable, no new feature could ever be dispatched.
|
|
25
|
+
The pause query had drifted the other way: it filtered on the legacy
|
|
26
|
+
`implementation-type:hitl` axis only, so gates migrated to `execution:hitl`
|
|
27
|
+
were invisible and pending human action was under-reported.
|
|
28
|
+
|
|
29
|
+
- `skills/workflow/ralph/scripts/pending-gates.mjs` resolves the question in one
|
|
30
|
+
place: `pendingHumanAction` holds unblocked gates plus epics tagged
|
|
31
|
+
`awaiting-epic-pr-merge` and is the only scheduling brake; `parked` holds
|
|
32
|
+
gates still blocked behind their own pipeline, which never stop dispatch and
|
|
33
|
+
never appear in the Pending Human Action table.
|
|
34
|
+
- Gate queries match `lifecycle:feature-pr` on either HITL axis, so gates
|
|
35
|
+
created on either side of the two-axis migration are counted.
|
|
36
|
+
- ralph's composable agent source (`agents/ralph/agent.md`) and the ralph skill
|
|
37
|
+
and REFERENCE now point at the script instead of restating ad-hoc `bd` queries
|
|
38
|
+
that had already drifted.
|
|
39
|
+
- `agents-native/ralph.md` (the Claude harness) keeps the **open**-gate form and
|
|
40
|
+
only takes the dual-axis query: its gates stay blocked for their whole life —
|
|
41
|
+
`create-task` blocks them behind a `document` chore no Claude ralph closes, and
|
|
42
|
+
the native route deps them onto the still-open feature bead — so the unblocked
|
|
43
|
+
predicate would never fire there and ralph would report "All beads complete."
|
|
44
|
+
with PRs unmerged. REFERENCE.md now states the invariant: a gate blocked by its
|
|
45
|
+
parent feature bead is not parked in any harness, so a gate must not carry a
|
|
46
|
+
`blocks` dependency onto its own feature.
|
|
47
|
+
|
|
48
|
+
Reconcile with agnt-ctx-s5xd.10 (ralph harness unification) when that rewrites
|
|
49
|
+
ralph's agent files — it owns collapsing the two predicates into one.
|
|
50
|
+
|
|
51
|
+
- e5e7683: Fail `pnpm lint` when a SKILL.md frontmatter cannot be parsed, is missing `name` or `description`, or has a description longer than 1024 characters. PI previously dropped such skills silently.
|
|
52
|
+
|
|
3
53
|
## 2.0.0
|
|
4
54
|
|
|
5
55
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ agent-cortex/
|
|
|
24
24
|
├── extensions/ # pi extensions (pi only)
|
|
25
25
|
│ ├── agent-guardrails/ # behavioural circuit-breakers in the system prompt
|
|
26
26
|
│ ├── agent-modes/ # switchable agent modes (reads composable agents/)
|
|
27
|
+
│ ├── output-style/ # reply shape: STE, action-first, restated context
|
|
27
28
|
│ ├── skill-stats/
|
|
28
29
|
│ └── notify/
|
|
29
30
|
├── pi/ # Committed pi config templates (see below)
|
|
@@ -279,7 +280,10 @@ manifest at `~/.agent-cortex/.claude-plugin/marketplace.json` exposing `./claude
|
|
|
279
280
|
registers it with Claude Code, and merges the committed `claude/settings.json` template into
|
|
280
281
|
`~/.claude/settings.json`: the template supplies defaults, personal values win, and the
|
|
281
282
|
installer owns exactly `enabledPlugins` + `extraKnownMarketplaces` — every other key
|
|
282
|
-
(`permissions`, `hooks`, `env`, `statusLine`, unknown keys) is preserved.
|
|
283
|
+
(`permissions`, `hooks`, `env`, `statusLine`, unknown keys) is preserved. It also writes the
|
|
284
|
+
managed section of `~/.claude/CLAUDE.md` — the reply-shape block the pi `output-style`
|
|
285
|
+
extension appends, taken from the same source file — and leaves every other line of that
|
|
286
|
+
file alone. The only committed
|
|
283
287
|
Claude config is that template plus the hand-authored extras in `claude-extras/`
|
|
284
288
|
(`.mcp.json`, `scripts/`); the plugin subtree is generated at install time — there is nothing
|
|
285
289
|
to drift.
|
|
@@ -341,6 +345,12 @@ supplies defaults and personal values win; the installer owns exactly `enabledPl
|
|
|
341
345
|
never touched. A legacy symlinked `settings.json` is replaced with a real file rather than
|
|
342
346
|
written through, and `--output` leaves the user's Claude config alone.
|
|
343
347
|
|
|
348
|
+
It also writes the managed section of `~/.claude/CLAUDE.md` (Claude's user memory), delimited
|
|
349
|
+
by `<!-- BEGIN agent-cortex:output-style -->` / `<!-- END agent-cortex:output-style -->`. The
|
|
350
|
+
content is `extensions/output-style/output-style.md` — the same file the pi extension appends
|
|
351
|
+
to its system prompt — so both harnesses carry one copy of the rules. Text outside the
|
|
352
|
+
markers is never touched; a file with only one marker is left unchanged with a warning.
|
|
353
|
+
|
|
344
354
|
The equivalent manual registration adds a marketplace root by **absolute path** (a bare `.` is
|
|
345
355
|
rejected) — the home install root (`~/.agent-cortex`) is the marketplace root written by
|
|
346
356
|
the plain install; the repo checkout ships no manifest:
|
|
@@ -419,7 +429,8 @@ extension→hook audit.
|
|
|
419
429
|
#### Not ported / follow-ups
|
|
420
430
|
|
|
421
431
|
- Only `session-start` and `notify` had Claude equivalents (both ported to hooks); the other
|
|
422
|
-
pi `extensions/` (auto-name, agent-guardrails, skill-stats, subagent, agent-modes) have none —
|
|
432
|
+
pi `extensions/` (auto-name, agent-guardrails, skill-stats, subagent, agent-modes) have none — `output-style`
|
|
433
|
+
reaches Claude through the managed `CLAUDE.md` section instead; the audit and
|
|
423
434
|
rejection rationale live in `docs/claude-hooks.md`.
|
|
424
435
|
- Ralph follow-ups: multi-feature epic branches, and a GitHub-trigger routine to auto-resume
|
|
425
436
|
after a PR merge (instead of manual re-invocation).
|
package/agents/ralph/agent.md
CHANGED
|
@@ -59,7 +59,7 @@ Run once at startup:
|
|
|
59
59
|
b. Claim the parent bead: `bd update <parent-id> --claim`.
|
|
60
60
|
c. Create the first stage chore bead and dispatch it (see _Creating and dispatching a stage chore_ below).
|
|
61
61
|
8. Record each launched agent in `.agent-cortex/ralph/state.json` (see format below).
|
|
62
|
-
9. Hand over to the completion-detection mechanism in the polling section: it blocks with `wait_for_agents` (PI) or waits for event-driven wake-ups (Claude), and proceeds to **HITL Pause** directly when no agent work was dispatched and
|
|
62
|
+
9. Hand over to the completion-detection mechanism in the polling section: it blocks with `wait_for_agents` (PI) or waits for event-driven wake-ups (Claude), and proceeds to **HITL Pause** directly when no agent work was dispatched and `skills/workflow/ralph/scripts/pending-gates.mjs` reports a non-empty `pendingHumanAction` — an **unblocked** `lifecycle:feature-pr` gate or an epic tagged `awaiting-epic-pr-merge`. Gates still parked behind their own pipeline do not count (REFERENCE.md, _Gate predicate_).
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
@@ -140,13 +140,13 @@ After initialization, Ralph waits for background agents to complete. Completion
|
|
|
140
140
|
- **Failure paths** (verify BLOCKED, review CHANGES_REQUESTED): check loop cap (see below), then create a feedback bead per the _Feedback Beads_ section in `run-pipeline-stage`. Do **not** dispatch immediately — the feedback bead appears in `bd ready` and is picked up in step 7. Remove the inflight entry for this chore; the feedback bead will create a new entry when dispatched.
|
|
141
141
|
7. **Check for newly ready beads**: run `bd ready`. For each bead not yet tracked:
|
|
142
142
|
- **Chore bead with `stage:*` label** (a feedback bead): find its parent via `bd show <chore-id>` (follow the `parent-child` dep). Check loop cap (see below). If under cap: dispatch it via _Creating and dispatching a stage chore_. If at cap: block it (`bd update <chore-id> --status blocked --notes "<cap> cap reached"`) and record for shutdown.
|
|
143
|
-
- **Task bead, AFK, in-flight count < 5, no
|
|
143
|
+
- **Task bead, AFK, in-flight count < 5, no pending human action**: claim the parent, create its first code chore, dispatch it. Check `pendingHumanAction` with `node skills/workflow/ralph/scripts/pending-gates.mjs` — an unblocked `lifecycle:feature-pr` gate means a feature is awaiting a human merge, so hold new parent features. Gates still blocked behind their own pipeline are parked and do not count.
|
|
144
144
|
- **Task bead, AFK, in-flight count at 5**: hold in memory as Waiting.
|
|
145
145
|
- **HITL task bead**: note for **Pending Human Action** shutdown summary.
|
|
146
146
|
- **NEEDS-REFINEMENT bead**: note for **Needs Refinement** shutdown summary.
|
|
147
147
|
When a parent is removed from `inflight` (completed or failed), immediately promote the first Waiting AFK parent.
|
|
148
148
|
8. **If no tasks remain in-flight** and `bd ready` returns no chore beads with `stage:*` labels and no AFK task beads:
|
|
149
|
-
- If
|
|
149
|
+
- If `pending-gates.mjs` reports a non-empty `pendingHumanAction` (an unblocked `lifecycle:feature-pr` gate, or an epic tagged `awaiting-epic-pr-merge`), proceed to **HITL Pause** (see below).
|
|
150
150
|
- Otherwise, proceed to **Shutdown** (see below).
|
|
151
151
|
|
|
152
152
|
### Loop cap check
|
|
@@ -165,15 +165,14 @@ Block command: `bd update <parent-id> --status blocked --notes "<cap> cap reache
|
|
|
165
165
|
|
|
166
166
|
## HITL Pause
|
|
167
167
|
|
|
168
|
-
Proceed here when no chore beads are in-flight, no `stage:*` chore beads are ready, and
|
|
168
|
+
Proceed here when no chore beads are in-flight, no `stage:*` chore beads are ready, and `pending-gates.mjs` reports a non-empty `pendingHumanAction` — an **unblocked** `lifecycle:feature-pr` gate (a feature genuinely awaiting human merge) or an epic tagged `awaiting-epic-pr-merge`. Gates still parked behind their own pipeline do not count. Ralph stops rather than burning requests on idle polls.
|
|
169
169
|
|
|
170
170
|
1. Regenerate `.agent-cortex/ralph/progress.md` one final time — do not delete it.
|
|
171
171
|
2. For each **multi-feature** epic whose feature beads are all closed but not yet tagged `awaiting-epic-pr-merge`, open/update an epic PR to main, then tag the epic `awaiting-epic-pr-merge`. Skip single-feature epics — their feature branch PRs already target `main` directly.
|
|
172
172
|
3. Run `bd dolt push`.
|
|
173
|
-
4. Collect
|
|
174
|
-
- Run `
|
|
175
|
-
-
|
|
176
|
-
- For each bead, run `bd show <id>` to retrieve the PR URL from bead notes.
|
|
173
|
+
4. Collect the table rows from the script's `pendingHumanAction` list:
|
|
174
|
+
- Run `node skills/workflow/ralph/scripts/pending-gates.mjs`
|
|
175
|
+
- For each entry, run `bd show <id>` to retrieve the PR URL from bead notes. `kind: "feature-pr"` rows read "review and merge the feature PR"; `kind: "epic-pr"` rows read "review and merge the epic PR into main".
|
|
177
176
|
5. Output the **Pending Human Action** summary:
|
|
178
177
|
```
|
|
179
178
|
⏸️ Ralph is paused — human action required before work can continue.
|
package/agents-native/ralph.md
CHANGED
|
@@ -35,7 +35,8 @@ created, so by the time a bead reaches `bd ready` here it should already carry a
|
|
|
35
35
|
`classify-bead` from the ralph loop:
|
|
36
36
|
|
|
37
37
|
- `implementation-type:afk` present → **AFK**. Done.
|
|
38
|
-
- `implementation-type:hitl` present → **HITL**. Skip; record for the
|
|
38
|
+
- `execution:hitl` (or the legacy `implementation-type:hitl`) present → **HITL**. Skip; record for the
|
|
39
|
+
pending-action summary.
|
|
39
40
|
- Neither present → treat as **NEEDS-REFINEMENT**: skip; record for the summary. A missing label
|
|
40
41
|
here means the bead skipped planning-stage classification — flag it, don't classify inline.
|
|
41
42
|
|
|
@@ -52,8 +53,8 @@ Run once at startup:
|
|
|
52
53
|
5. Read `maxFixRounds` from `${CLAUDE_PLUGIN_ROOT}/skills/create-task/pipeline.json` if present;
|
|
53
54
|
otherwise default to **4**.
|
|
54
55
|
6. Dispatch up to 5 AFK features (see below). Then end your turn — you will be woken on the
|
|
55
|
-
first completion. If there are zero AFK features but
|
|
56
|
-
to **Idle**.
|
|
56
|
+
first completion. If there are zero AFK features but an open `lifecycle:feature-pr` gate is
|
|
57
|
+
pending (see **Idle**), go straight to **Idle**.
|
|
57
58
|
|
|
58
59
|
## Dispatching a feature
|
|
59
60
|
|
|
@@ -105,10 +106,19 @@ beads per **Classifying a bead**, and promote AFK features into free slots.
|
|
|
105
106
|
|
|
106
107
|
When no workers are running and no AFK features remain:
|
|
107
108
|
|
|
108
|
-
- **PR gates pending** (open `lifecycle:feature-pr` beads
|
|
109
|
+
- **PR gates pending** (open `lifecycle:feature-pr` beads on either HITL axis):
|
|
110
|
+
`bd list -l lifecycle:feature-pr --label-any execution:hitl,implementation-type:hitl --limit 0`.
|
|
111
|
+
This harness keys the pause on a gate being **open**, not unblocked, because its gates stay
|
|
112
|
+
blocked for their whole life: `create-task` blocks each gate behind the feature's `document`
|
|
113
|
+
chore, which no Claude ralph claims or closes, and the native route in _When a reviewer
|
|
114
|
+
worker completes_ step 2 deps the gate onto the still-open feature bead. So never assume a gate
|
|
115
|
+
here is blocked only by its own pipeline, and never treat "no unblocked gate" as "no human
|
|
116
|
+
action owed" — that clears the loop with PRs unmerged. Run `bd dolt push`, then print a
|
|
109
117
|
**Pending Human Action** table (bead id, title, PR URL, "review & merge, then `bd close <gate>`"),
|
|
110
118
|
and **stop completely** — output nothing further. Do not poll. The user re-invokes you after
|
|
111
|
-
merging; you will resume from `bd ready`.
|
|
119
|
+
merging; you will resume from `bd ready`. The pi harness uses the unblocked-gate predicate
|
|
120
|
+
(`skills/workflow/ralph/scripts/pending-gates.mjs`); F10 (agnt-ctx-s5xd.10) unifies ralph's
|
|
121
|
+
agent files — reconcile the two predicates there rather than half-migrating this one.
|
|
112
122
|
- **Nothing pending**: run `bd dolt push`, print any recorded HITL / needs-refinement / blocked
|
|
113
123
|
beads as tables, then print **All beads complete.**
|
|
114
124
|
|
|
@@ -79,6 +79,14 @@ const DEFAULT_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
|
79
79
|
// The committed user-settings template.
|
|
80
80
|
const SETTINGS_TEMPLATE = "claude/settings.json";
|
|
81
81
|
|
|
82
|
+
// Claude user memory — the global prompt. The installer owns only the marked
|
|
83
|
+
// section; everything outside the markers is the user's own text.
|
|
84
|
+
const DEFAULT_INSTRUCTIONS_PATH = join(homedir(), ".claude", "CLAUDE.md");
|
|
85
|
+
const INSTRUCTIONS_BEGIN = "<!-- BEGIN agent-cortex:output-style -->";
|
|
86
|
+
const INSTRUCTIONS_END = "<!-- END agent-cortex:output-style -->";
|
|
87
|
+
// Canonical prose, shared with the pi extension (extensions/output-style).
|
|
88
|
+
const INSTRUCTIONS_SOURCE = join("extensions", "output-style", "output-style.md");
|
|
89
|
+
|
|
82
90
|
// The only keys the installer owns outright: everything else in the live file
|
|
83
91
|
// is personal config the merge must preserve.
|
|
84
92
|
const SETTINGS_OWNED_KEYS = ["enabledPlugins", "extraKnownMarketplaces"];
|
|
@@ -324,7 +332,7 @@ async function installSettings({ root, target, dryRun, warn }) {
|
|
|
324
332
|
}
|
|
325
333
|
|
|
326
334
|
const content = `${JSON.stringify(merged, null, 2)}\n`;
|
|
327
|
-
return
|
|
335
|
+
return writeManagedFile({ target, content, dryRun });
|
|
328
336
|
}
|
|
329
337
|
|
|
330
338
|
/**
|
|
@@ -350,12 +358,12 @@ function mergeSettings(template, existing) {
|
|
|
350
358
|
}
|
|
351
359
|
|
|
352
360
|
/**
|
|
353
|
-
* Write
|
|
354
|
-
*
|
|
355
|
-
*
|
|
361
|
+
* Write a managed file. A legacy symlink is replaced with a real file by writing
|
|
362
|
+
* a sibling temp file and renaming it over the target — rename replaces the link
|
|
363
|
+
* itself, so a committed template is never written through.
|
|
356
364
|
* Returns the action taken so the CLI can report the plan (dry-run included).
|
|
357
365
|
*/
|
|
358
|
-
async function
|
|
366
|
+
async function writeManagedFile({ target, content, dryRun }) {
|
|
359
367
|
const symlink = await isSymlink(target);
|
|
360
368
|
const current = await readFileMaybe(target);
|
|
361
369
|
|
|
@@ -382,6 +390,52 @@ async function writeManagedJson({ target, content, dryRun }) {
|
|
|
382
390
|
return { path: target, action: symlink ? "removed-symlink" : "written", removedSymlink: symlink };
|
|
383
391
|
}
|
|
384
392
|
|
|
393
|
+
// ─── Claude user memory (CLAUDE.md) ──────────
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Materialise the output-style block into the managed section of ~/.claude/CLAUDE.md.
|
|
397
|
+
*
|
|
398
|
+
* The section is delimited by HTML comments, so a re-install replaces only the
|
|
399
|
+
* text between them and every other line of the user's memory file survives. A
|
|
400
|
+
* file with exactly one marker (hand-edited or truncated) is left untouched with
|
|
401
|
+
* a warning, never guessed at.
|
|
402
|
+
*/
|
|
403
|
+
async function installInstructions({ root, target, dryRun, warn }) {
|
|
404
|
+
const block = await readFileMaybe(join(root, INSTRUCTIONS_SOURCE));
|
|
405
|
+
if (block === null) {
|
|
406
|
+
warn(`no ${INSTRUCTIONS_SOURCE} source in the package — skipping CLAUDE.md`);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const content = mergeManagedSection(await readFileMaybe(target), block);
|
|
410
|
+
if (content === null) {
|
|
411
|
+
warn(`${target} has an unterminated agent-cortex section — left unchanged; fix the markers by hand`);
|
|
412
|
+
return { path: target, action: "skipped-unmanaged", removedSymlink: false };
|
|
413
|
+
}
|
|
414
|
+
return writeManagedFile({ target, content, dryRun });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Return `existing` with the managed section replaced by `block`, or null when
|
|
419
|
+
* the file carries unmatched markers. A file without markers gets the section
|
|
420
|
+
* appended, so personal memory text is never disturbed.
|
|
421
|
+
*/
|
|
422
|
+
function mergeManagedSection(existing, block) {
|
|
423
|
+
const managed = `${INSTRUCTIONS_BEGIN}\n${block.trimEnd()}\n${INSTRUCTIONS_END}`;
|
|
424
|
+
if (existing === null) return `${managed}\n`;
|
|
425
|
+
|
|
426
|
+
const start = existing.indexOf(INSTRUCTIONS_BEGIN);
|
|
427
|
+
const end = existing.indexOf(INSTRUCTIONS_END);
|
|
428
|
+
if (start === -1 && end === -1) {
|
|
429
|
+
const head = existing.replace(/\s*$/, "");
|
|
430
|
+
return head === "" ? `${managed}\n` : `${head}\n\n${managed}\n`;
|
|
431
|
+
}
|
|
432
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
433
|
+
|
|
434
|
+
const before = existing.slice(0, start).replace(/\s*$/, "");
|
|
435
|
+
const after = existing.slice(end + INSTRUCTIONS_END.length).replace(/^\s*/, "").replace(/\s*$/, "");
|
|
436
|
+
return `${[before, managed, after].filter((part) => part !== "").join("\n\n")}\n`;
|
|
437
|
+
}
|
|
438
|
+
|
|
385
439
|
async function readTemplateJson(filePath, warn) {
|
|
386
440
|
const raw = await readFileMaybe(filePath);
|
|
387
441
|
if (raw === null) {
|
|
@@ -450,15 +504,20 @@ async function isSymlink(filePath) {
|
|
|
450
504
|
* (default ~/.claude/settings.json). Passing it
|
|
451
505
|
* explicitly materialises settings even with
|
|
452
506
|
* --output — the test/CI seam.
|
|
507
|
+
* @param {string} [options.instructionsPath] Claude user-memory target
|
|
508
|
+
* (default ~/.claude/CLAUDE.md). Passing it
|
|
509
|
+
* explicitly materialises the section even
|
|
510
|
+
* with --output — the test/CI seam.
|
|
453
511
|
* @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`.
|
|
454
512
|
* @returns {{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
|
|
455
513
|
* marketplaceManifest: object|null, agents: string[], natives: string[],
|
|
456
514
|
* skills: {names: string[], md: number, files: number, dir: string},
|
|
457
515
|
* hooks: boolean, hookFiles: string[], handAuthored: string[],
|
|
458
516
|
* settings: {path: string, action: string, removedSymlink: boolean}|null,
|
|
517
|
+
* instructions: {path: string, action: string, removedSymlink: boolean}|null,
|
|
459
518
|
* warnings: string[], dryRun: boolean }}
|
|
460
519
|
*/
|
|
461
|
-
export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, warn = DEFAULT_WARN } = {}) {
|
|
520
|
+
export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, instructionsPath, warn = DEFAULT_WARN } = {}) {
|
|
462
521
|
const tokenMap = await loadTokenMap(root);
|
|
463
522
|
if (typeof tokenMap.version === "number" && tokenMap.version > CONTRACT_VERSION) {
|
|
464
523
|
throw new Error(
|
|
@@ -498,15 +557,22 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
498
557
|
await mkdir(out, { recursive: true });
|
|
499
558
|
}
|
|
500
559
|
|
|
501
|
-
// The default install owns ~/.claude/settings.json
|
|
502
|
-
// generate-only form and leaves the
|
|
503
|
-
//
|
|
560
|
+
// The default install owns ~/.claude/settings.json and the managed section of
|
|
561
|
+
// ~/.claude/CLAUDE.md; --output is the generate-only form and leaves the
|
|
562
|
+
// user's Claude config alone. Explicit paths request the write regardless
|
|
563
|
+
// (the test/CI seam).
|
|
504
564
|
const settingsTarget = settingsPath ?? DEFAULT_SETTINGS_PATH;
|
|
505
565
|
const settings =
|
|
506
566
|
isDefaultInstall || settingsPath !== undefined
|
|
507
567
|
? await installSettings({ root, target: settingsTarget, dryRun, warn: warnSink })
|
|
508
568
|
: null;
|
|
509
569
|
|
|
570
|
+
const instructionsTarget = instructionsPath ?? DEFAULT_INSTRUCTIONS_PATH;
|
|
571
|
+
const instructions =
|
|
572
|
+
isDefaultInstall || instructionsPath !== undefined
|
|
573
|
+
? await installInstructions({ root, target: instructionsTarget, dryRun, warn: warnSink })
|
|
574
|
+
: null;
|
|
575
|
+
|
|
510
576
|
const agents = await buildAgents(root);
|
|
511
577
|
const natives = await buildNatives(root, join(root, "agents-native"));
|
|
512
578
|
const skillOut = join(out, "skills");
|
|
@@ -530,6 +596,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
530
596
|
hookFiles: hookFiles.map((f) => f.rel),
|
|
531
597
|
handAuthored: handAuthored.map((f) => f.rel),
|
|
532
598
|
settings,
|
|
599
|
+
instructions,
|
|
533
600
|
warnings,
|
|
534
601
|
dryRun,
|
|
535
602
|
};
|
|
@@ -551,11 +618,15 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
551
618
|
if (summary.handAuthored.length)
|
|
552
619
|
console.log(` hand-authored files: ${summary.handAuthored.join(", ")} (copied from repo claude-extras/)`);
|
|
553
620
|
if (summary.settings) console.log(` settings → ${summary.settings.path} (${summary.settings.action})`);
|
|
621
|
+
if (summary.instructions)
|
|
622
|
+
console.log(` CLAUDE.md → ${summary.instructions.path} (${summary.instructions.action})`);
|
|
554
623
|
return summary;
|
|
555
624
|
}
|
|
556
625
|
|
|
557
626
|
// Write phase — the cleanup before buildSkills already cleared stale children.
|
|
558
627
|
if (settings) console.log(`Materialised Claude settings → ${settings.path} (${settings.action})`);
|
|
628
|
+
if (instructions && instructions.action !== "skipped-unmanaged")
|
|
629
|
+
console.log(`Materialised CLAUDE.md section → ${instructions.path} (${instructions.action})`);
|
|
559
630
|
const agentOut = join(out, "agents");
|
|
560
631
|
await mkdir(agentOut, { recursive: true });
|
|
561
632
|
for (const { file, content } of [...agents.files, ...natives]) {
|
package/docs/claude-hooks.md
CHANGED
|
@@ -17,6 +17,7 @@ UI surface. Each decision below is grounded in the Claude Code hooks reference.
|
|
|
17
17
|
| `session-start/` | `SessionStart` hook commands | **Port** (extended) |
|
|
18
18
|
| `auto-name.ts` | — | **Reject** — built-in |
|
|
19
19
|
| `agent-guardrails/` | — | **Reject** — pi-only (no system-prompt hook) |
|
|
20
|
+
| `output-style/` | managed `CLAUDE.md` section | **Port** — user memory, not a hook |
|
|
20
21
|
| `notify/` | `Notification` hook + `scripts/notify.mjs` | **Port** |
|
|
21
22
|
| `skill-stats/` | — | **Reject** — no equivalent |
|
|
22
23
|
| `subagent/` | — | **Reject** — native |
|
|
@@ -89,6 +90,21 @@ context rather than system instructions. The rules are harness-agnostic prose, s
|
|
|
89
90
|
a future port could append them to the existing ported SessionStart hook — out of
|
|
90
91
|
scope for this pi-first slice.
|
|
91
92
|
|
|
93
|
+
## `output-style/` → Port (managed CLAUDE.md section)
|
|
94
|
+
|
|
95
|
+
pi appends the reply shape (action first, Simplified Technical English, restated
|
|
96
|
+
context) to the system prompt through `before_agent_start`. Claude Code cannot
|
|
97
|
+
modify the system prompt from a hook, so the installer writes the same block into
|
|
98
|
+
the managed section of `~/.claude/CLAUDE.md` — Claude's user memory, loaded every
|
|
99
|
+
session.
|
|
100
|
+
|
|
101
|
+
The content is not copied into a Claude-side template: `extensions/output-style/
|
|
102
|
+
output-style.md` is the single source, read by the pi extension at load and by the
|
|
103
|
+
installer at install time. Re-running the install replaces only the text between
|
|
104
|
+
the markers; personal memory text survives untouched. The installer never writes
|
|
105
|
+
the block by appending to a `SessionStart` hook — memory is the right surface
|
|
106
|
+
because the rules must hold for every turn, not just the first.
|
|
107
|
+
|
|
92
108
|
## `skill-stats/` → Reject (no equivalent)
|
|
93
109
|
|
|
94
110
|
pi tracks skills loaded into the system prompt, `/skill:` invocations, SKILL.md
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Idea: Improve PI System Prompt
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
|
-
Backlog idea (not implementation-ready) —
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Backlog idea (not implementation-ready) — two slices implemented as pi extensions:
|
|
5
|
+
`agent-guardrails` (circuit-breakers, check-in triggers, atomic-change discipline) and
|
|
6
|
+
`output-style` (action-first reply shape, Simplified Technical English, restated context,
|
|
7
|
+
shipped to pi via the system prompt and to Claude via a managed `CLAUDE.md` section).
|
|
8
|
+
The rest of the idea is unscoped.
|
|
7
9
|
|
|
8
10
|
## Why it might be useful
|
|
9
11
|
PI's current system prompt is weak. Agents consistently get off track, spend excessive tokens spinning their wheels or going off on tangents, fail to check in when problems are hit, and generally behave like inexperienced code goblins. This wastes tokens, slows down task completion, and forces the user to constantly redirect the agent. A stronger system prompt would produce more focused, disciplined agents that know when to stop and ask.
|
|
@@ -30,6 +32,10 @@ High — after current work lands. Directly impacts daily productivity and token
|
|
|
30
32
|
- Which guardrails from the Gemini starter are genuinely useful vs redundant with existing skills (e.g. `git-workflow` already covers destructive commands, `style-code` covers scope discipline)?
|
|
31
33
|
|
|
32
34
|
## Notes
|
|
35
|
+
- **Second slice (settled)**: reply shape is a separate extension from task discipline.
|
|
36
|
+
`agent-guardrails` decides whether work continues; `output-style` decides how the result
|
|
37
|
+
is reported (STE per ASD-STE100, action first, context restated — never a bead or phase
|
|
38
|
+
ID cited alone). Both are stable constants appended on `before_agent_start`.
|
|
33
39
|
- **Precedence (settled by the first slice)**: the generic circuit-breakers yield to a structured
|
|
34
40
|
workflow — a skill or pipeline stage with its own explicit retry and check-in rules wins.
|
|
35
41
|
- **Gemini starter prompt** exists (see conversation from 2026-08-05) covering loop prevention, action scope, context management, execution workflow order, and working state maintenance. Useful starting material but needs heavy customisation.
|
|
@@ -9,6 +9,9 @@ Required fields:
|
|
|
9
9
|
- `name`: kebab-case skill identifier.
|
|
10
10
|
- `description`: max 1024 characters.
|
|
11
11
|
|
|
12
|
+
Frontmatter is machine-validated: `pnpm lint` runs `check:skills`, which fails on a
|
|
13
|
+
frontmatter block that would make pi silently drop the skill.
|
|
14
|
+
|
|
12
15
|
Optional fields:
|
|
13
16
|
- `argument-hint`: input prompt shown to users at invocation.
|
|
14
17
|
- `disable-model-invocation`: hides the skill from automatic model routing; explicit
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Output Style
|
|
2
|
+
|
|
3
|
+
Appends the always-on reply shape to the PI system prompt once per user prompt.
|
|
4
|
+
|
|
5
|
+
This is the second slice of `docs/ideas/improve-pi-system-prompt.md`: the first
|
|
6
|
+
(`agent-guardrails`) stops wasted work; this one stops the answer getting buried in
|
|
7
|
+
it. The extension injects a stable block via the `before_agent_start` hook — which
|
|
8
|
+
fires once per user prompt, before the agent loop — so the rules apply to every
|
|
9
|
+
turn of the run without rewriting Pi's default prompt.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- **Lead with the action** — the first line is the answer, command, path, or next
|
|
14
|
+
action; multi-step work is numbered; the reply ends with one next step.
|
|
15
|
+
- **Keep the prose short** — no preamble, no recap, no closing offer; five items
|
|
16
|
+
per list at most; one topic per reply.
|
|
17
|
+
- **Write Simplified Technical English (ASD-STE100)** — active voice, 20-word
|
|
18
|
+
sentences for instructions and 25-word sentences for description, verb over
|
|
19
|
+
noun, one word one meaning, literal words, no marketing adjectives, articles
|
|
20
|
+
kept.
|
|
21
|
+
- **Restate the context** — never cite a bead, ticket, phase, or stage by ID alone;
|
|
22
|
+
say what it is in plain words first, and restate state after a gap or topic
|
|
23
|
+
switch.
|
|
24
|
+
|
|
25
|
+
The rules shape the reply, never the work. When a rule would delete the answer,
|
|
26
|
+
the task wins.
|
|
27
|
+
|
|
28
|
+
## Structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
output-style/
|
|
32
|
+
├── README.md # This file
|
|
33
|
+
├── index.ts # Extension entry point — appends on before_agent_start
|
|
34
|
+
├── output-style.ts # Loads the block + appendOutputStyle composition
|
|
35
|
+
├── output-style.md # The block text (single source, also shipped to Claude)
|
|
36
|
+
├── output-style.test.ts # Composition tests
|
|
37
|
+
├── package.json
|
|
38
|
+
└── tsconfig.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Testing
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd extensions/output-style
|
|
45
|
+
node --test --import tsx/esm output-style.test.ts
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Design notes
|
|
49
|
+
|
|
50
|
+
- The block is a stable constant appended after any chained system-prompt changes,
|
|
51
|
+
so the prompt-cache prefix is preserved.
|
|
52
|
+
- `appendOutputStyle` is idempotent — a prompt that already carries the marker is
|
|
53
|
+
returned unchanged, so a duplicate extension load cannot double the rules.
|
|
54
|
+
- Separate from `agent-guardrails` because it is a different failure: guardrails
|
|
55
|
+
govern *whether* work continues, this governs *how the result is reported*.
|
|
56
|
+
- The prose lives in `output-style.md`, not in the TypeScript, because there is a
|
|
57
|
+
second consumer: `agent-cortex install claude` wraps that file in the managed
|
|
58
|
+
`~/.claude/CLAUDE.md` section. One file, two harnesses, no drift.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Style Extension
|
|
3
|
+
*
|
|
4
|
+
* Appends the always-on reply shape to the PI system prompt on every turn: lead
|
|
5
|
+
* with the action, write in Simplified Technical English (ASD-STE100), restate the
|
|
6
|
+
* context the reader cannot remember (the second slice of
|
|
7
|
+
* docs/ideas/improve-pi-system-prompt.md).
|
|
8
|
+
*
|
|
9
|
+
* The block is stable text appended after any chained system-prompt changes, so it
|
|
10
|
+
* keeps the prompt-cache prefix intact while remaining authoritative for the model.
|
|
11
|
+
* See output-style.ts for the rules and appendOutputStyle for composition.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { appendOutputStyle } from "./output-style.ts";
|
|
16
|
+
|
|
17
|
+
export { OUTPUT_STYLE, OUTPUT_STYLE_MARKER, appendOutputStyle } from "./output-style.ts";
|
|
18
|
+
|
|
19
|
+
export default function outputStyleExtension(pi: ExtensionAPI): void {
|
|
20
|
+
pi.on("before_agent_start", (event) => {
|
|
21
|
+
return { systemPrompt: appendOutputStyle(event.systemPrompt) };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## Output style (agent-cortex)
|
|
2
|
+
|
|
3
|
+
How every reply is written. These rules shape the reply, never the work: do all
|
|
4
|
+
the analysis and tool calls the task needs, then report the result this way. If a
|
|
5
|
+
rule would delete the answer itself, the task wins; the shape stays.
|
|
6
|
+
|
|
7
|
+
### Lead with the action
|
|
8
|
+
|
|
9
|
+
- The first line is the answer, the command, the path, or the next action. Not
|
|
10
|
+
context, not a plan, not "Let me…".
|
|
11
|
+
- Number multi-step work. One bounded action per step.
|
|
12
|
+
- End with one concrete next step. Omit it when the work is finished.
|
|
13
|
+
|
|
14
|
+
### Keep the prose short
|
|
15
|
+
|
|
16
|
+
- No preamble ("Great question", "I'll now…"), no recap of work just completed,
|
|
17
|
+
no closing offer ("Let me know if…", "Hope this helps").
|
|
18
|
+
- Five items maximum per list. Rank them; group related items.
|
|
19
|
+
- One topic per reply. Name a tangent in one line at the end, then stop.
|
|
20
|
+
|
|
21
|
+
### Write Simplified Technical English (ASD-STE100)
|
|
22
|
+
|
|
23
|
+
- Active voice. Imperative for instructions.
|
|
24
|
+
- Sentence limit: 20 words for instructions, 25 for descriptive text. One
|
|
25
|
+
instruction per sentence.
|
|
26
|
+
- Use the verb, not the noun: "check the file", not "perform a check of the file".
|
|
27
|
+
- One word, one meaning: pick the term for a thing, then reuse it.
|
|
28
|
+
- Literal words only. No idioms, metaphors, or figures of speech ("circle back",
|
|
29
|
+
"under the hood", "ballpark").
|
|
30
|
+
- No marketing adjectives or filler ("seamless", "robust", "comprehensive").
|
|
31
|
+
- Keep articles and subjects. Do not compress into fragments that lose meaning.
|
|
32
|
+
- Give times in concrete units ("about 10 minutes"), never "a bit of work".
|
|
33
|
+
|
|
34
|
+
### Restate the context, never assume it
|
|
35
|
+
|
|
36
|
+
The reader cannot hold earlier turns in working memory.
|
|
37
|
+
|
|
38
|
+
- Never name a bead, ticket, phase, or pipeline stage by ID or number alone. Say
|
|
39
|
+
what it is in plain words first — "the login fix, verify step (bd-142)" — and
|
|
40
|
+
give the ID at most once.
|
|
41
|
+
- After a gap, a topic switch, or a long tool run, restate in one line: what is
|
|
42
|
+
done, what is next.
|
|
43
|
+
- Mark progress concretely: "Step 2 of 4 done: schema updated."
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { OUTPUT_STYLE, OUTPUT_STYLE_MARKER, appendOutputStyle } from "./output-style.ts";
|
|
5
|
+
|
|
6
|
+
// ─── appendOutputStyle ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe("appendOutputStyle", () => {
|
|
9
|
+
it("appends the output-style block after the base prompt", () => {
|
|
10
|
+
const result = appendOutputStyle("You are a helpful agent.");
|
|
11
|
+
|
|
12
|
+
assert.ok(result.startsWith("You are a helpful agent."));
|
|
13
|
+
assert.ok(result.includes(OUTPUT_STYLE));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("preserves a chained base prompt from earlier handlers", () => {
|
|
17
|
+
const base = "Base prompt.\n\n---\nAgent mode instructions.";
|
|
18
|
+
const result = appendOutputStyle(base);
|
|
19
|
+
|
|
20
|
+
assert.ok(result.startsWith(base));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("trims trailing whitespace before appending", () => {
|
|
24
|
+
const result = appendOutputStyle("Base prompt.\n\n");
|
|
25
|
+
|
|
26
|
+
assert.equal(result, `Base prompt.\n\n${OUTPUT_STYLE}`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("keeps a block appended by another extension", () => {
|
|
30
|
+
const withGuardrails = "Base prompt.\n\n## Operating discipline (agent-cortex)\n\nRules.\n";
|
|
31
|
+
const result = appendOutputStyle(withGuardrails);
|
|
32
|
+
|
|
33
|
+
assert.ok(result.includes("## Operating discipline (agent-cortex)"));
|
|
34
|
+
assert.ok(result.endsWith(OUTPUT_STYLE));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("is idempotent when applied twice", () => {
|
|
38
|
+
const once = appendOutputStyle("Base prompt.");
|
|
39
|
+
const twice = appendOutputStyle(once);
|
|
40
|
+
|
|
41
|
+
assert.equal(twice, once);
|
|
42
|
+
assert.equal(twice.split(OUTPUT_STYLE_MARKER).length - 1, 1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns the prompt unchanged when the output style is already present", () => {
|
|
46
|
+
const existing = `Base prompt.\n\n${OUTPUT_STYLE}`;
|
|
47
|
+
|
|
48
|
+
assert.equal(appendOutputStyle(existing), existing);
|
|
49
|
+
});
|
|
50
|
+
});
|