@jaybeeuu/agent-cortex 2.9.0 → 2.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,172 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0a98d7b: Open a `prioritise-work` run with a summary of the committed strategy instead of a question. A new
8
+ `strategy.mjs summary` subcommand validates the file first, then prints a handful of lines — the
9
+ path it read, the version, the newest change-log entry, the themes, the two admissibility lists as
10
+ up to two bullets each, and the deadline factors when the file states them — so the digest cannot
11
+ describe a strategy that is not on disk. The skill shows it and asks "what should change?" before
12
+ the interview; with no strategy file it exits `3` and the skill goes straight to the first-run
13
+ interview with no digest; with an invalid or partly missing one it exits `1` and names what is wrong
14
+ rather than presenting it as current. The change requires no new section of the strategy file, so
15
+ every strategy already committed still validates: `## What counts as value` and `## Explicitly out
16
+ of scope` are summarised as the admissibility lists they are, never as a ranking order, and
17
+ `## Deadline tiers` is summarised narrowest first, the order the queue applies the factors, whatever
18
+ order the file holds the rows in.
19
+
20
+ The script contract moved to the skill's `REFERENCE.md` so `SKILL.md` stays within its length target.
21
+
22
+ ### Patch Changes
23
+
24
+ - 5a6a14a: Let the content proof decide a closed bead's worktree, whatever its upstream is.
25
+ `prune-worktrees.mjs` still refused every worktree ralph created, because ralph
26
+ cuts its branch from the remote base (`git worktree add -b feature/<id> …
27
+ origin/main`), `branch.autoSetupMerge` makes that base the branch's upstream, and
28
+ the squash merge that lands the work rewrites its commits — so the branch stays
29
+ ahead of its own base for ever and the unpushed-commit check, which ran first,
30
+ always fired. A closed bead is now decided by content alone: the base must hold
31
+ the branch's own version of every path the branch changed since their merge base,
32
+ and the refusal names that content when it does not. An open bead keeps the
33
+ upstream refusal, and with no upstream it is refused whatever the content says.
34
+ The base guard now tests the ref instead of the tip: a base that names the
35
+ worktree's own branch (local or remote-tracking) is a self-comparison and
36
+ refuses, while a branch cut from the base and never committed to holds the base's
37
+ own commit and is removed — the case every fresh ralph worktree used to reach.
38
+
39
+ ## 2.10.0
40
+
41
+ ### Minor Changes
42
+
43
+ - 9d86a2d: Rank bd due dates in the `hitl-queue`. A bead's `--due` date now multiplies its total by the
44
+ factor of the narrowest tier its remaining time fits inside — `1` more than a week out, `1.5`
45
+ within a week, `2` within two days, `4` within a day, `8` once overdue. The numbers live in the
46
+ strategy's new optional `## Deadline tiers` table, so tuning them is a strategy edit and a
47
+ `bump`, never a code change; a strategy without that section ranks on the documented default,
48
+ and `validate` fails a table it cannot read. Each row shows the deadline, the time left and the
49
+ factor, an overdue bead is flagged `OVERDUE` rather than lifted in silence, and a bead with no
50
+ due date keeps the neutral factor. A deferred bead stays out of the queue until its
51
+ `defer_until` passes, applying bd's own wake rule to bd's own field.
52
+ - 32395f6: Re-rank at every item boundary, with the comparison and the reason on the trail. `hitl-queue` gains
53
+ `boundary.mjs`: one command compares the theme's next item (the highest-ranked bead under the epic)
54
+ against the queue's top item, prints **both stored totals**, and says whether the scores point to
55
+ `stay` or `switch`. It computes no score — the totals, components and multipliers are the queue's —
56
+ and it recommends rather than switches: a run with no `--decide` writes nothing.
57
+
58
+ `--decide stay|switch|move` records the human's answer as one timestamped `bd note` on the theme
59
+ bead, carrying both scores and the reason, so a later session reads why the theme was kept or left.
60
+ A decision that contradicts the scores needs `--reason`, so an unexplained override cannot reach
61
+ the trail. A missing theme item, a named bead outside the queue and an empty queue are stated with
62
+ one next step instead of guessed.
63
+
64
+ The guppi agent gains its work unit: the theme is the current epic, a boundary is the completion of
65
+ one item, and every boundary ends in the documented trade-off — continue in the theme, switch
66
+ because higher-priority work waits elsewhere, or move because a break improves the next item. It
67
+ never leaves a theme without both scores on screen, and a wish for a different order stays a
68
+ `prioritise-work` change.
69
+
70
+ - 763869c: Recommend one item from the queue and the strategy. `hitl-queue` gains `recommend.mjs`: one command
71
+ prints the queue's top row — its total, the four RICE components, the unblocking and
72
+ work-in-progress multipliers, what the answer unblocks — together with the line of the committed
73
+ strategy that justifies it, quoted from the file and refused when the file does not contain it.
74
+ `--bead <id>` recommends the bead a human named and notes the queue's own top choice in one line.
75
+
76
+ The guppi agent now opens a session with that one block and waits, answers "what should we look
77
+ at?" through the same steps, and states a condition with one next step instead of choosing when the
78
+ strategy is missing, nothing is scored, the queue is empty, or a named bead is not in the queue. It
79
+ still proposes only: no execution, no claim, no merge, no push.
80
+
81
+ - 64b5deb: Resolve `{{PATH:skills/...}}` rows to the tree each install stages, so an agent row
82
+ always names a file the install contains. Claude Code stages skills flat
83
+ (`skills/<name>/SKILL.md`), so a row authored against the repository's grouped tree lost
84
+ its group segment and rendered as a bare relative path that resolved to nothing. The
85
+ staged shape is now declared per harness in `token-map.json`'s `layouts` table, the
86
+ claude installer resolves agent rows against the plugin root as it already did for
87
+ skills, and `test/rendered-paths-resolve.test.mjs` stages both installs and fails when
88
+ any rendered row names a file that is not there.
89
+ - bed237e: Make the HITL queue scope-aware: it reads several `bd` workspaces and keeps one writable.
90
+
91
+ A scope list — `.agent-cortex/scopes.json` in the repository, then
92
+ `~/.agent-cortex/scopes.json` (`AGENT_CORTEX_HOME` overrides the root), then one implicit scope
93
+ for the current workspace — names each workspace, its kind (`code`, `personal`, `ops`) and whether
94
+ it is read-only. `readOnly` defaults to `true`, and a scope is writable only when it is not marked
95
+ read-only _and_ it is the current workspace, so a copied list cannot widen the writable set on
96
+ another machine.
97
+
98
+ Every configured scope contributes rows, each tagged with its scope. A scope whose directory is
99
+ missing or carries no `.beads` of its own is reported `unavailable` with the reason and skipped —
100
+ bd resolves a workspace by walking up, so the marker check is what stops one workspace's beads
101
+ being attributed to another. A failed `bd` call marks its own scope unavailable; the run still
102
+ exits `0`. A read-only scope passes through one guard that refuses any call other than the two
103
+ reads the queue issues, so the boundary survives a later edit.
104
+
105
+ One daily scope note sits above the ranked list, quoting the `- daily: <scope>, …` line from the
106
+ personal scope's own strategy and citing its version, section and scope. `--shared`
107
+ (`AGENT_CORTEX_QUEUE_SHARED=1`) marks output that leaves the machine: personal scopes are not read,
108
+ so personal rows and the daily note are withheld and counted.
109
+
110
+ `prioritise-work` documents the daily line, and guppi's agent text explains scopes, the note and the
111
+ privacy boundary.
112
+
113
+ ### Patch Changes
114
+
115
+ - 7418a33: Prove that a refused boundary decision records nothing. The two refusal cases in
116
+ `test/boundary.test.mjs` now assert an empty trail as well as the message: `--decide` with an
117
+ outcome outside the three, and a decision against the scores with no `--reason`. The fake `bd` in
118
+ `test/helpers/fake-bd-queue.mjs` already records every `note` call, so the shared harness now
119
+ returns the trail a refused build left behind. A comment that claimed the parser refused a
120
+ bad `--decide` before the builder saw it was wrong for this case: the command line does refuse it,
121
+ but the test calls the builder directly, and the builder checks the runtime value itself, because
122
+ the union is a compile-time constraint only. No behaviour changes.
123
+ - d8de6ba: Harden the guppi interrupt render test so a label that no longer sits with its own body fails.
124
+
125
+ The order assertion read the four step markers by offset, and the marker was the label, so the test
126
+ proved the four labels were ordered and nothing more. A render could keep every label in place and
127
+ exchange the two bodies — the record's text under `Rewind.`, the rewind's under `Record.` — inverting
128
+ the record-before-rewind rule the protocol exists to protect, and the test stayed green.
129
+
130
+ The test now reads the steps as (label, body) pairs and asserts the pair order, so each label must
131
+ carry its own body. No shipped text changed.
132
+
133
+ - fd9c9a3: Remove a landed closed bead's worktree when the merge deleted its remote branch.
134
+ `prune-worktrees.mjs` refused every such worktree with "has no remote-tracking
135
+ branch", because a merge that deletes the branch also removes the local branch's
136
+ upstream — so the one case the script exists to handle failed housekeeping. A
137
+ missing upstream is now distinguished by content: with the tree clean and the
138
+ base branch holding the branch's own version of every path the branch changed
139
+ since their merge base, the worktree and its local branch are removed; otherwise
140
+ the worktree is kept and the refusal names the risk. The test is one-sided, so a
141
+ base that moved on after the branch was cut still proves the branch landed. It
142
+ proves content and never history: a removed branch is discarded with every
143
+ commit the base does not contain, such as a reverted change or an empty commit.
144
+ The base is the remote's default branch, read from `origin/HEAD` and never
145
+ hardcoded, with `--base <ref>` as the documented override; an unresolvable base
146
+ keeps the worktree and refuses. A base that resolves to the worktree's own
147
+ commit also keeps the worktree and refuses, because comparing a branch with
148
+ itself proves nothing.
149
+
150
+ The content proof also survives a path git would otherwise rewrite. The branch's
151
+ changed paths are read with `git diff --name-only -z` and split on NUL, then fed
152
+ back under `git --literal-pathspecs`, so a path holding a non-ASCII byte, a double
153
+ quote, a backslash, a leading colon or a newline round-trips exactly. Without both,
154
+ such a path matched no pathspec, the second diff came back empty, and a branch the
155
+ base never received was removed with an empty reason list.
156
+
157
+ - 74ac309: Correct two shipped-text contradictions in the review docs: a count word that the data beside it
158
+ disproved, and a promise the sibling file did not keep.
159
+
160
+ `review-report`'s trend example is headed "re-run after two more rejections" but prints eight class
161
+ deltas, and one label per rejection makes that eight rejections. The heading now says eight, and the
162
+ sample is the script's own output over a tracker that took eight more rejections: eight classes
163
+ moved, the other four stayed, and the first-pass rate held because the rejections re-ran four
164
+ changes that already held a review cohort.
165
+
166
+ `review-defects` promised `EXAMPLES.md` carried "one real case per class". The file holds twelve
167
+ cases and eleven real observations, with `scope-creep` described as a situation because no recorded
168
+ case exists. The promise now says what the file delivers rather than inventing a case to satisfy it.
169
+
3
170
  ## 2.9.0
4
171
 
5
172
  ### Minor Changes
package/README.md CHANGED
@@ -454,16 +454,20 @@ guppi helps with the work an agent cannot — design, ideation, prioritisation,
454
454
  manual confirmations. It is resident and propose-only: it recommends the next item and names the
455
455
  reason, then loads `plan`, `grill-me`, `hitl-collab`, `flesh-out-idea` or `prioritise-work` for the
456
456
  chosen item. It never executes AFK work, never merges and never pushes, and a disagreement with a
457
- priority opens a strategy review rather than a silent reorder. Between tasks it clears the context
458
- with `/clear`, and only after writing what was decided, what is open and what comes next to the bead
459
- trail and the memory. The tracker, not the window, holds the state that matters.
457
+ priority opens a strategy review rather than a silent reorder. When that item is done it re-ranks
458
+ at the boundary: the theme's next item against the queue's top item, both scores on screen, then one
459
+ question — stay in the theme, switch to higher-priority work, or move for a single item. Between
460
+ tasks it clears the context with `/clear`, and only after writing what was decided, what is open and
461
+ what comes next to the bead trail and the memory. The tracker, not the window, holds the state that
462
+ matters.
460
463
 
461
464
  ```sh
462
465
  claude --agent agent-cortex:guppi
463
466
  ```
464
467
 
465
468
  Its authority is prioritisation, not action. `prioritise-work` owns the committed strategy file;
466
- `score-bead` records the score and `hitl-queue` prints the order. guppi reports them and never
469
+ `score-bead` records the score, and `hitl-queue` prints the order and recommends one item from it
470
+ with the line of the strategy that justifies the choice. guppi reports them and never
467
471
  recomputes them. Personal work is a town-level bd scope — no rig, no polecats, no refinery, no merge
468
472
  queue.
469
473
 
package/agents/README.md CHANGED
@@ -152,6 +152,11 @@ When composing the final agent file for a specific harness:
152
152
  files / installing for claude and pi; pi's `agent-modes` extension does the same at runtime. See the `contract` section of `token-map.json` and
153
153
  `token-map.README.md` for the rules.
154
154
 
155
+ A `{{PATH:skills/…}}` row is authored against this repository's tree, which nests every skill
156
+ one group deep, and resolves to the tree the harness stages. Claude Code stages skills flat, so
157
+ the row loses its group segment there; the pi install keeps it. `token-map.json`'s `layouts`
158
+ table records the shape per harness, so a row never names a file the install does not contain.
159
+
155
160
  The pi harness's `agent-modes` extension is a runtime consumer of this format: it
156
161
  composes agent prompts on the fly from `agent.md` + `pi/frontmatter.json`, substituting
157
162
  tokens against `token-map.json`'s pi column with no install step. See
@@ -23,7 +23,7 @@ This file never restates a skill's rules.
23
23
  |---|---|---|
24
24
  | `prioritise-work` | Choosing what to work on next, or the committed strategy is missing, stale or disputed. It owns the strategy file, runs the interview, and version-bumps the artefact. | {{PATH:skills/workflow/prioritise-work/SKILL.md}} |
25
25
  | `plan` | The human has chosen a feature, and it needs a PRD, a task breakdown and classified beads. | {{PATH:skills/workflow/plan/SKILL.md}} |
26
- | `hitl-queue` | The human needs to know which open decision to answer first, or a planning session must report that queue before it proposes anything. | {{PATH:skills/workflow/hitl-queue/SKILL.md}} |
26
+ | `hitl-queue` | The human needs to know which open decision to answer first, or the session must open with one recommendation drawn from that queue. | {{PATH:skills/workflow/hitl-queue/SKILL.md}} |
27
27
  | `hitl-collab` | Work needs a human: a review, a manual step, a confirmation, or a handoff document. | {{PATH:skills/workflow/hitl-collab/SKILL.md}} |
28
28
  | `grill-me` | A plan or a design needs stress-testing before anyone acts on it. | {{PATH:skills/productivity/grill-me/SKILL.md}} |
29
29
  | `flesh-out-idea` | An idea bead needs deepening, or promotion to an epic with task children. | {{PATH:skills/planning/flesh-out-idea/SKILL.md}} |
@@ -31,6 +31,56 @@ This file never restates a skill's rules.
31
31
  **The strategy skill is `prioritise-work`.** It owns the committed strategy file, and it is the only
32
32
  skill in this list that writes that file.
33
33
 
34
+ ## Session start
35
+
36
+ A session opens with **one** recommendation, never a list, and then waits. `hitl-queue` ranks, the
37
+ strategy justifies, and guppi says the one item the two agree on.
38
+
39
+ 1. **Read the rubric.** Run `validate` in `prioritise-work`. It prints the file when it passes, and
40
+ that printed text is the only place a justification may be quoted from.
41
+ 2. **Read the queue's top row.** Run `recommend --json` in `hitl-queue`. The row carries the bead,
42
+ its total, the four components, both multipliers and the beads it unblocks. Report them as given;
43
+ guppi computes no score.
44
+ 3. **Say the block.** Choose the one line of the rubric that justifies that bead, and run
45
+ `recommend --why "<that line>"`. The script refuses a line the file does not contain, so an
46
+ invented reason fails before it is spoken. Say the block it prints, then **wait** for the human.
47
+
48
+ **"What should we look at?"** is that same request in other words, and so is any opening that asks
49
+ what to do next. Nothing about the session — the theme, the backlog, the last decision — replaces
50
+ the three steps.
51
+
52
+ **When the human names a bead instead**, pass it as `--bead <id>`. The recommendation is their bead,
53
+ and the block adds one line naming the queue's own top choice. Where the two disagree, that line is
54
+ the whole of the disagreement: the human's choice stands, and a wish to see another bead first is a
55
+ strategy change through `prioritise-work`, never a silent override.
56
+
57
+ ### When nothing can be recommended
58
+
59
+ Four conditions each stop at a stated result and one offered next step. Never fill the gap with a
60
+ suggestion of your own, and never score or reorder anything to clear it.
61
+
62
+ | The block says | What guppi offers |
63
+ |---|---|
64
+ | `No recommendation · no usable strategy` | The `prioritise-work` interview, which writes or bumps the file |
65
+ | `No recommendation · nothing is scored` | `score-bead` on one bead, then the queue again |
66
+ | `No recommendation · the queue is empty` | Filing the decision as an `execution:hitl` bead, or lifting a deferral |
67
+ | `No recommendation · <id> is not in the queue` | `bd show <id>` to see why, then a bead from the queue |
68
+
69
+ ## The work unit
70
+
71
+ A session works one **theme** — the current epic — and re-ranks at every **boundary**: the
72
+ completion of one item (a decision recorded, a plan created, a handoff sent, a review given).
73
+
74
+ At each boundary, run `boundary` in `hitl-queue` and say the block it prints: the theme's next item
75
+ against the queue's top item, with both scores and the reason that names them. Then ask for one of
76
+ three outcomes — **stay**, **switch** because higher-priority work waits elsewhere, or **move**
77
+ because a break improves the next item — and pass the answer as `--decide`, which writes the
78
+ comparison and the reason to the theme bead's trail with a timestamp.
79
+
80
+ Never leave a theme without both scores on screen. Never record a decision the human did not give.
81
+ A wish for a different order is a `prioritise-work` change, never a boundary one.
82
+ {{PATH:skills/workflow/hitl-queue/BOUNDARY.md}} carries the block, the states and the options.
83
+
34
84
  ## Boundaries
35
85
 
36
86
  - **Recommends, never acts.** State the next item and the reason for it. The human chooses. The
@@ -42,7 +92,8 @@ skill in this list that writes that file.
42
92
  - **Never merges.** A pull request is a human gate. guppi may read one and hand it to a reviewer.
43
93
  - **Never pushes.** No `git push`, no release, no version bump of this package.
44
94
  - **Never overrides a priority silently.** "This should be higher" means the strategy is wrong,
45
- stale, or has shifted. Open the `prioritise-work` interview, record the change, then re-rank.
95
+ stale, or has shifted. Run `prioritise-work`, which opens with a digest of the current strategy,
96
+ record the change, then re-rank.
46
97
  - **Never invents a strategy.** If the strategy file is missing or invalid, say so plainly and offer
47
98
  the interview. Do not guess priorities.
48
99
  - **Propose-only and resident.** guppi is a standing collaborator in one session. It is not a task
@@ -55,6 +106,25 @@ skill in this list that writes that file.
55
106
  A scope is one bd workspace. guppi reads several scopes and writes to one: the **current
56
107
  workspace**. Every other scope is read-only, whatever the request.
57
108
 
109
+ The scope list is configuration, not this file: `.agent-cortex/scopes.json` in the repository, or
110
+ `~/.agent-cortex/scopes.json` for the scopes that belong to the person rather than the checkout.
111
+ The `hitl-queue` skill reads it and prints one ranked list across every scope it can read, tagging
112
+ each row with the scope it came from. A scope that is absent or carries no tracker of its own is
113
+ reported `unavailable` and skipped: a machine without the personal repo still has a queue. Read the
114
+ list and the privacy rules from {{PATH:skills/workflow/hitl-queue/SCOPES.md}} rather than
115
+ restating them here.
116
+
117
+ **The daily note is the queue's first line about the day.** It names the scopes the day belongs to
118
+ and quotes the personal strategy line it came from. Report it as printed, above the ranked list,
119
+ and never paraphrase the quote: the citation is the point. Which scopes the day belongs to is the
120
+ human's call, written in their own strategy — when the note says it is missing, offer the
121
+ `prioritise-work` interview rather than choosing a scope.
122
+
123
+ **Personal work stays in the personal scope.** Never copy a personal bead's text into a code
124
+ workspace, a handoff document or a pull request, and never write to a scope other than the current
125
+ workspace. An output that leaves the machine — a pasted report, a message to another agent, a PR
126
+ body — comes from a `--shared` run, which withholds personal rows and the daily note.
127
+
58
128
  **Personal work is a town-level scope, never a rig.** A rig is a code-project container — refinery,
59
129
  witness, polecats, seeded patrols — and it suits code projects. Personal work belongs to the mayor
60
130
  role at town level: no rig, no polecats, no refinery, no merge queue. Never describe personal work
@@ -156,10 +156,14 @@ function byName(a, b) {
156
156
 
157
157
  /**
158
158
  * Compose every agent from agents/<name>/ — all harnesses share the composable sources.
159
+ * `{{PATH:...}}` rows resolve against the plugin root, as they do in skills: a bare
160
+ * relative path would name a file relative to the reader's working directory, and the
161
+ * install ships the file under ${CLAUDE_PLUGIN_ROOT}.
159
162
  * @param {string} root
163
+ * @param {{ pluginRoot: string | undefined, warn: Warn }} options
160
164
  * @returns {Promise<{ files: { file: string, content: string }[] }>}
161
165
  */
162
- async function buildAgents(root) {
166
+ async function buildAgents(root, { pluginRoot, warn }) {
163
167
  const files = [];
164
168
  const entries = (await readdir(join(root, "agents"), { withFileTypes: true })).sort((a, b) =>
165
169
  byName(a.name, b.name),
@@ -170,7 +174,7 @@ async function buildAgents(root) {
170
174
  const dir = join(root, "agents", entry.name);
171
175
  if (!(await isFile(join(dir, "agent.md"))) || !(await isFile(join(dir, "claude", "frontmatter.json")))) continue;
172
176
 
173
- const fm = await composeAgent(root, entry.name, CLAUDE);
177
+ const fm = await composeAgent(root, entry.name, CLAUDE, { pluginRoot, resolveRelativePaths: true, warn });
174
178
  const slug = fm.name.startsWith(NAME_PREFIX) ? fm.name.slice(NAME_PREFIX.length) : fm.name;
175
179
  const header =
176
180
  `---\n` +
@@ -720,7 +724,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
720
724
  ? await installInstructions({ root, target: instructionsTarget, dryRun, warn: warnSink })
721
725
  : null;
722
726
 
723
- const agents = await buildAgents(root);
727
+ const agents = await buildAgents(root, { pluginRoot: rootPlugin, warn: warnSink });
724
728
  const skillOut = join(out, "skills");
725
729
  const skills = await buildSkills(root, skillOut, { dryRun, pluginRoot: rootPlugin, tokenMap, warn: warnSink });
726
730
  const docs = await installDocs({ root, output: out, dryRun, tokenMap });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaybeeuu/agent-cortex",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
4
4
  "description": "Personal PI package with custom agents, skills, and extensions",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -8,22 +8,25 @@
8
8
  // 1. {{SECTION:name}} is replaced with <dir>/<harness>/<name>.md content.
9
9
  // 2. {{TOOL:key}} is replaced with the harness column of token-map.json.
10
10
  // 3. {{PATH:arg}} with a named key resolves through token-map.json's paths
11
- // table; anything else is a relative path and stays verbatim.
11
+ // table; anything else is a relative path and stays verbatim, except that
12
+ // installers resolve it against the plugin root and map a `skills/…` row
13
+ // onto the tree that harness stages skills in (token-map.json `layouts`).
12
14
  // Substitution order is SECTION → TOOL → PATH (matches extensions/agent-modes).
13
15
  // Missing sections, unknown tool keys, or schema-invalid frontmatter are hard
14
16
  // errors: a committed generated file must never ship a literal token or a
15
17
  // silently dropped section.
16
18
  //
17
- // Options (the pi installer passes `dropNullTools`, `pluginRoot` and
18
- // `resolveRelativePaths`; the claude installer uses the strict defaults):
19
+ // Options (both installers pass `pluginRoot` and `resolveRelativePaths`; pi also
20
+ // passes `dropNullTools`):
19
21
  // dropNullTools replace null-mapped tool tokens with an empty string and
20
22
  // warn instead of throwing (pi's prose rule per the
21
23
  // token-map contract)
22
24
  // pluginRoot override the plugin_root used for resolved paths
23
25
  // (installers compute it from the actual install layout)
24
26
  // resolveRelativePaths non-keyed {{PATH:...}} args resolve to pluginRoot/arg
25
- // instead of staying verbatim (install time; without it
26
- // they stay bare for runtime resolution)
27
+ // (a `skills/…` row mapped onto the harness's staged
28
+ // skills shape) instead of staying verbatim (install
29
+ // time; without it they stay bare for runtime resolution)
27
30
  // warn callback for drop/omit warnings (default: no-op; the
28
31
  // pi installer collects them into its result)
29
32
  //
@@ -43,6 +46,7 @@ import { join } from "node:path";
43
46
  * @property {number} [version]
44
47
  * @property {Record<string, Record<string, string | null>>} tools
45
48
  * @property {Record<string, Record<string, string | PathSpec>>} paths
49
+ * @property {Record<string, Record<string, string>>} [layouts]
46
50
  */
47
51
 
48
52
  /**
@@ -100,6 +104,8 @@ function noop() {}
100
104
  * with a warning when `dropNullTools`, otherwise throw.
101
105
  * - PATH: named key → resolve through the paths table (plugin_root overridable);
102
106
  * non-keyed → join(pluginRoot, arg) when `resolveRelativePaths`, else verbatim.
107
+ * A non-keyed `skills/…` arg is mapped onto the harness's staged shape first
108
+ * (see stagedRelativePath).
103
109
  * - SECTION tokens are *not* handled here — the agent composer resolves them from
104
110
  * the harness's section files before calling this.
105
111
  *
@@ -231,6 +237,36 @@ async function parseFrontmatter(path, name, harness) {
231
237
 
232
238
  // ─── Path resolution ─────────────────────────────────────────────────────────
233
239
 
240
+ /**
241
+ * Map a repository-relative path onto the tree the harness stages it in.
242
+ *
243
+ * The repository nests every skill one group deep (skills/<group>/<name>/…), so
244
+ * an authored `skills/…` row is written in that grouped form. An installer that
245
+ * stages skills flat — Claude Code's plugin scan discovers skills/<name>/SKILL.md
246
+ * directly under skills/ — drops the group segment, and a row that kept it would
247
+ * name a directory the install does not have. The shape is a harness fact, read
248
+ * from token-map.json's layouts table, so a harness that stages skills
249
+ * differently needs no change here.
250
+ *
251
+ * @param {string} arg Non-keyed {{PATH:...}} argument
252
+ * @param {string} harness
253
+ * @param {TokenMap} tokenMap
254
+ * @returns {string}
255
+ */
256
+ function stagedRelativePath(arg, harness, tokenMap) {
257
+ const segments = arg.split("/");
258
+ if (segments[0] !== "skills") return arg;
259
+ // Every install ships a skill as a directory below skills/, so a shorter arg
260
+ // names nothing it can contain: fail now rather than render a dead row.
261
+ if (segments.length < 3) {
262
+ throw new Error(
263
+ `{{PATH:${arg}}} is not a staged path — a skill row is authored as skills/<group>/<name>/<file>`,
264
+ );
265
+ }
266
+ if (tokenMap.layouts?.skills?.[harness] !== "flat") return arg;
267
+ return [segments[0], segments[2], ...segments.slice(3)].join("/");
268
+ }
269
+
234
270
  /**
235
271
  * Resolve a {{PATH:...}} argument for a harness's generated output.
236
272
  * Named keys resolve through token-map.json's paths table (recursively for
@@ -238,7 +274,7 @@ async function parseFrontmatter(path, name, harness) {
238
274
  * the installer beads own precise runtime path resolution (see token-map.README.md).
239
275
  * With the installer options (`pluginRoot`, `resolveRelativePaths`) the
240
276
  * plugin_root base and bare relative paths resolve against the actual install
241
- * layout instead.
277
+ * layout instead, and a `skills/…` row is mapped onto that layout.
242
278
  * @param {string} arg
243
279
  * @param {string} harness
244
280
  * @param {TokenMap} tokenMap
@@ -248,7 +284,9 @@ async function parseFrontmatter(path, name, harness) {
248
284
  function resolvePath(arg, harness, tokenMap, opts = {}) {
249
285
  const entry = tokenMap.paths?.[arg];
250
286
  if (!entry) {
251
- if (opts.resolveRelativePaths && opts.pluginRoot) return join(opts.pluginRoot, arg);
287
+ if (opts.resolveRelativePaths && opts.pluginRoot) {
288
+ return join(opts.pluginRoot, stagedRelativePath(arg, harness, tokenMap));
289
+ }
252
290
  return arg;
253
291
  }
254
292
 
@@ -111,7 +111,8 @@ class that keeps appearing is a gap in the stage before this one.
111
111
 
112
112
  ## Worked examples
113
113
 
114
- `EXAMPLES.md` carries one real case per class, with the rejection text a reviewer should write.
114
+ `EXAMPLES.md` carries a case per class, with the rejection text a reviewer should write. Eleven are
115
+ real observations; `scope-creep` is a described situation, because no recorded case exists.
115
116
 
116
117
  ## See Also
117
118
 
@@ -99,16 +99,20 @@ the whole table rather than the top three.
99
99
 
100
100
  ## With a previous report
101
101
 
102
- The same project, recorded as a baseline one month earlier and re-run after two more rejections:
102
+ The same project, recorded as a baseline one month earlier and re-run after eight more rejections:
103
103
 
104
104
  ```
105
105
  Change since the previous report
106
106
  Against /tmp/rr-example/trend/.agent-cortex-docs/defect-report.json, reported 2026-04-01T09:00:00Z
107
107
  criteria-unmet 1 (+1), docs-drift 1 (+1), missing-proof 1 (+1), and 5 more
108
- First-pass review rate 75% (from 75%, no change)
108
+ First-pass review rate 33% (from 33%, no change)
109
+ Fix rounds review 12 (+8)
109
110
  ```
110
111
 
111
- The comparison reads the report the script wrote at `--baseline`, so it compares like with like.
112
+ One label per rejection, so the eight rejections added eight labels: eight classes moved and the
113
+ other four stayed where they were. The rejections re-ran four changes that already held a review
114
+ cohort, so the rate did not move while the fix-round count did. The comparison reads the report the
115
+ script wrote at `--baseline`, so it compares like with like.
112
116
  A class that did not change is not listed; when no class changed, the section says so. A baseline
113
117
  file with an unrecognised schema is refused rather than read as zeros, because a baseline that
114
118
  cannot be read must not become an invented trend.
@@ -0,0 +1,122 @@
1
+ # The boundary re-rank
2
+
3
+ A session that answers one item at a time drifts: the next item is whichever one was nearest, not
4
+ whichever one matters most. This file defines the unit a session works in, and the one comparison
5
+ it makes when that unit finishes.
6
+
7
+ ## The unit of work
8
+
9
+ | Term | Meaning |
10
+ |---|---|
11
+ | **Theme** | The current epic. One theme at a time, and its children are the items it needs. |
12
+ | **Item** | One thing the theme needs from a human: a decision answered, a plan shaped, a handoff written, a review given. |
13
+ | **Boundary** | The completion of one item. The point where the session stops and checks whether the theme is still the right place to be. |
14
+
15
+ The theme and its trail are the session's memory, not the window: a clear drops the window, and
16
+ `bd show <epic-id>` still holds what was decided and why. [RECOMMEND.md](RECOMMEND.md) carries the
17
+ one-item session start; this file carries what happens after that item is done.
18
+
19
+ ## The decision at every boundary
20
+
21
+ One trade-off, three outcomes, and no fourth:
22
+
23
+ | Outcome | When it is right | Read from the scores? |
24
+ |---|---|---|
25
+ | `stay` | Continue in the theme. The theme's next item still leads the queue. | Yes |
26
+ | `switch` | Leave the theme, because higher-priority work waits elsewhere. | Yes |
27
+ | `move` | Leave the theme for one item, because a break improves the next item. | No — a judgement, so it carries its own reason |
28
+
29
+ The first two are readable from the two totals. A `move` is not: it is about what the next item
30
+ needs, so it is always the human's reason and never the queue's.
31
+
32
+ ## The block
33
+
34
+ ```text
35
+ Boundary · theme tst-t · strategy v2
36
+ recommendation: switch — the queue's top item scores 8.0 against the theme's next 4.2, so higher-priority work waits elsewhere
37
+ theme next tst-a · total 4.2 · Decide the theme question
38
+ queue top tst-b · total 8.0 · Decide the outside question
39
+ next: stay, switch or move is the human's call — nothing is recorded until they answer
40
+ ```
41
+
42
+ **Both scores are always printed**, and the reason names both. That is the whole rule: an agent
43
+ that leaves a theme without showing what it left for has asserted a priority it never displayed.
44
+ A switch with no comparison on screen is a defect, not a summary.
45
+
46
+ The recommendation is derived from the two stored totals and nothing else. The totals, the
47
+ components and both multipliers are the queue's; this comparison computes no score, so the number
48
+ a reader checks is the number the queue ranked by. [SKILL.md](SKILL.md) owns that arithmetic.
49
+
50
+ ## The record
51
+
52
+ ```bash
53
+ node <skill-scripts>/boundary.mjs boundary --theme <epic-id>
54
+ node <skill-scripts>/boundary.mjs boundary --theme <epic-id> --decide switch
55
+ node <skill-scripts>/boundary.mjs boundary --theme <epic-id> --decide move --reason "<what the break buys>"
56
+ ```
57
+
58
+ A run with no `--decide` prints the comparison and writes nothing. A run that names a decision
59
+ writes one trail entry with `bd note` on the **theme bead** — the epic that owns the items — and
60
+ prints it back:
61
+
62
+ ```text
63
+ trail: tst-t · [2026-11-15T09:30:00.000Z] boundary: switch · theme next tst-a total 4.2 vs queue top tst-b total 8.0 · reason: the queue's top item scores 8.0 against the theme's next 4.2, so higher-priority work waits elsewhere
64
+ ```
65
+
66
+ Three parts, and all three are required: the **timestamp**, **both scores**, and the **reason**.
67
+ A later session reads `bd show tst-t` and sees why the theme was kept or left, without reading a
68
+ transcript. An entry with a reason but no numbers cannot be checked, and an entry with numbers but
69
+ no reason says nothing about intent.
70
+
71
+ ## Recommends, never switches
72
+
73
+ - **The human decides.** `--decide` is the recording step, and it is passed after an answer, never
74
+ instead of asking for one. Nothing switches on its own.
75
+ - **A decision against the recommendation needs `--reason`.** A `move` always does, and so does a
76
+ `stay` or `switch` that contradicts the two scores. The refusal is the point: an unexplained
77
+ override is the case the trail exists to catch.
78
+ - **The boundary changes what to do next, never what outranks what.** The order is the queue's and
79
+ the rubric is the strategy's. A wish for a different order is a `prioritise-work` change, not a
80
+ boundary decision.
81
+
82
+ ## Which item is compared
83
+
84
+ `--theme <epic-id>` names the theme. The theme's next item is the **highest-ranked item under that
85
+ theme** in the queue, so the comparison cannot be steered by naming a weak one. `--next <id>` names
86
+ another item when the session knows better, and the queue's top item is rank 1 either way.
87
+
88
+ ## The states that are stated instead of guessed
89
+
90
+ | First line | What it means | The step it offers |
91
+ |---|---|---|
92
+ | `No boundary · the queue is empty` | No open `execution:hitl` bead exists | File the theme's next item, or lift a deferral |
93
+ | `No boundary · theme <id> has no ranked item` | Nothing scored sits under that theme | Name the item with `--next`, or score it with `score-bead` |
94
+ | `No boundary · <id> is not in the queue` | The named item is not an open scored `execution:hitl` bead | Name one from the queue, or score that bead |
95
+
96
+ None of them guesses an item and none is a failure of the run: exit `0` means an answer was
97
+ printed, whichever answer it was.
98
+
99
+ | Option | Effect |
100
+ |---|---|
101
+ | `--theme <epic-id>` | The theme; required, because a boundary belongs to one |
102
+ | `--next <id>` | The theme's next item, when the session names it instead |
103
+ | `--decide <outcome>` | `stay`, `switch` or `move` — record the human's decision |
104
+ | `--reason "<text>"` | Why that decision; required when it contradicts the recommendation |
105
+ | `--file <path>` | Strategy file (default `.agent-cortex/working-docs/strategy.md`, relative to the repository root) |
106
+ | `--include-ideas` | Rank `kind:idea` beads too, as the queue does |
107
+ | `--cwd <dir>` | Directory the strategy path and `bd` resolve from |
108
+ | `--json` | Print the answer as JSON, including the decision and the trail entry |
109
+
110
+ Exit `1` means a `bd` call failed and exit `2` means a malformed call — a missing `--theme`, an
111
+ outcome outside the three, or a decision against the scores with no reason. `BD_PATH` pins the `bd`
112
+ binary.
113
+
114
+ ## Red Flags
115
+
116
+ - Red flag: leaving a theme without the two scores on screen, so the reader has to trust the agent.
117
+ - Red flag: recording a decision with no reason, or a reason with no numbers in the trail entry.
118
+ - Red flag: switching because the agent judged the priority itself, instead of reading the queue.
119
+ - Red flag: recomputing a total here, so the comparison and the queue can disagree.
120
+ - Red flag: recording a decision the human has not given, so the session switches on its own.
121
+ - Red flag: writing the trail entry to another scope, so a theme's history lands somewhere its
122
+ owner will not read.