@jaybeeuu/agent-cortex 2.9.0 → 2.10.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,136 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9d86a2d: Rank bd due dates in the `hitl-queue`. A bead's `--due` date now multiplies its total by the
8
+ factor of the narrowest tier its remaining time fits inside — `1` more than a week out, `1.5`
9
+ within a week, `2` within two days, `4` within a day, `8` once overdue. The numbers live in the
10
+ strategy's new optional `## Deadline tiers` table, so tuning them is a strategy edit and a
11
+ `bump`, never a code change; a strategy without that section ranks on the documented default,
12
+ and `validate` fails a table it cannot read. Each row shows the deadline, the time left and the
13
+ factor, an overdue bead is flagged `OVERDUE` rather than lifted in silence, and a bead with no
14
+ due date keeps the neutral factor. A deferred bead stays out of the queue until its
15
+ `defer_until` passes, applying bd's own wake rule to bd's own field.
16
+ - 32395f6: Re-rank at every item boundary, with the comparison and the reason on the trail. `hitl-queue` gains
17
+ `boundary.mjs`: one command compares the theme's next item (the highest-ranked bead under the epic)
18
+ against the queue's top item, prints **both stored totals**, and says whether the scores point to
19
+ `stay` or `switch`. It computes no score — the totals, components and multipliers are the queue's —
20
+ and it recommends rather than switches: a run with no `--decide` writes nothing.
21
+
22
+ `--decide stay|switch|move` records the human's answer as one timestamped `bd note` on the theme
23
+ bead, carrying both scores and the reason, so a later session reads why the theme was kept or left.
24
+ A decision that contradicts the scores needs `--reason`, so an unexplained override cannot reach
25
+ the trail. A missing theme item, a named bead outside the queue and an empty queue are stated with
26
+ one next step instead of guessed.
27
+
28
+ The guppi agent gains its work unit: the theme is the current epic, a boundary is the completion of
29
+ one item, and every boundary ends in the documented trade-off — continue in the theme, switch
30
+ because higher-priority work waits elsewhere, or move because a break improves the next item. It
31
+ never leaves a theme without both scores on screen, and a wish for a different order stays a
32
+ `prioritise-work` change.
33
+
34
+ - 763869c: Recommend one item from the queue and the strategy. `hitl-queue` gains `recommend.mjs`: one command
35
+ prints the queue's top row — its total, the four RICE components, the unblocking and
36
+ work-in-progress multipliers, what the answer unblocks — together with the line of the committed
37
+ strategy that justifies it, quoted from the file and refused when the file does not contain it.
38
+ `--bead <id>` recommends the bead a human named and notes the queue's own top choice in one line.
39
+
40
+ The guppi agent now opens a session with that one block and waits, answers "what should we look
41
+ at?" through the same steps, and states a condition with one next step instead of choosing when the
42
+ strategy is missing, nothing is scored, the queue is empty, or a named bead is not in the queue. It
43
+ still proposes only: no execution, no claim, no merge, no push.
44
+
45
+ - 64b5deb: Resolve `{{PATH:skills/...}}` rows to the tree each install stages, so an agent row
46
+ always names a file the install contains. Claude Code stages skills flat
47
+ (`skills/<name>/SKILL.md`), so a row authored against the repository's grouped tree lost
48
+ its group segment and rendered as a bare relative path that resolved to nothing. The
49
+ staged shape is now declared per harness in `token-map.json`'s `layouts` table, the
50
+ claude installer resolves agent rows against the plugin root as it already did for
51
+ skills, and `test/rendered-paths-resolve.test.mjs` stages both installs and fails when
52
+ any rendered row names a file that is not there.
53
+ - bed237e: Make the HITL queue scope-aware: it reads several `bd` workspaces and keeps one writable.
54
+
55
+ A scope list — `.agent-cortex/scopes.json` in the repository, then
56
+ `~/.agent-cortex/scopes.json` (`AGENT_CORTEX_HOME` overrides the root), then one implicit scope
57
+ for the current workspace — names each workspace, its kind (`code`, `personal`, `ops`) and whether
58
+ it is read-only. `readOnly` defaults to `true`, and a scope is writable only when it is not marked
59
+ read-only _and_ it is the current workspace, so a copied list cannot widen the writable set on
60
+ another machine.
61
+
62
+ Every configured scope contributes rows, each tagged with its scope. A scope whose directory is
63
+ missing or carries no `.beads` of its own is reported `unavailable` with the reason and skipped —
64
+ bd resolves a workspace by walking up, so the marker check is what stops one workspace's beads
65
+ being attributed to another. A failed `bd` call marks its own scope unavailable; the run still
66
+ exits `0`. A read-only scope passes through one guard that refuses any call other than the two
67
+ reads the queue issues, so the boundary survives a later edit.
68
+
69
+ One daily scope note sits above the ranked list, quoting the `- daily: <scope>, …` line from the
70
+ personal scope's own strategy and citing its version, section and scope. `--shared`
71
+ (`AGENT_CORTEX_QUEUE_SHARED=1`) marks output that leaves the machine: personal scopes are not read,
72
+ so personal rows and the daily note are withheld and counted.
73
+
74
+ `prioritise-work` documents the daily line, and guppi's agent text explains scopes, the note and the
75
+ privacy boundary.
76
+
77
+ ### Patch Changes
78
+
79
+ - 7418a33: Prove that a refused boundary decision records nothing. The two refusal cases in
80
+ `test/boundary.test.mjs` now assert an empty trail as well as the message: `--decide` with an
81
+ outcome outside the three, and a decision against the scores with no `--reason`. The fake `bd` in
82
+ `test/helpers/fake-bd-queue.mjs` already records every `note` call, so the shared harness now
83
+ returns the trail a refused build left behind. A comment that claimed the parser refused a
84
+ bad `--decide` before the builder saw it was wrong for this case: the command line does refuse it,
85
+ but the test calls the builder directly, and the builder checks the runtime value itself, because
86
+ the union is a compile-time constraint only. No behaviour changes.
87
+ - d8de6ba: Harden the guppi interrupt render test so a label that no longer sits with its own body fails.
88
+
89
+ The order assertion read the four step markers by offset, and the marker was the label, so the test
90
+ proved the four labels were ordered and nothing more. A render could keep every label in place and
91
+ exchange the two bodies — the record's text under `Rewind.`, the rewind's under `Record.` — inverting
92
+ the record-before-rewind rule the protocol exists to protect, and the test stayed green.
93
+
94
+ The test now reads the steps as (label, body) pairs and asserts the pair order, so each label must
95
+ carry its own body. No shipped text changed.
96
+
97
+ - fd9c9a3: Remove a landed closed bead's worktree when the merge deleted its remote branch.
98
+ `prune-worktrees.mjs` refused every such worktree with "has no remote-tracking
99
+ branch", because a merge that deletes the branch also removes the local branch's
100
+ upstream — so the one case the script exists to handle failed housekeeping. A
101
+ missing upstream is now distinguished by content: with the tree clean and the
102
+ base branch holding the branch's own version of every path the branch changed
103
+ since their merge base, the worktree and its local branch are removed; otherwise
104
+ the worktree is kept and the refusal names the risk. The test is one-sided, so a
105
+ base that moved on after the branch was cut still proves the branch landed. It
106
+ proves content and never history: a removed branch is discarded with every
107
+ commit the base does not contain, such as a reverted change or an empty commit.
108
+ The base is the remote's default branch, read from `origin/HEAD` and never
109
+ hardcoded, with `--base <ref>` as the documented override; an unresolvable base
110
+ keeps the worktree and refuses. A base that resolves to the worktree's own
111
+ commit also keeps the worktree and refuses, because comparing a branch with
112
+ itself proves nothing.
113
+
114
+ The content proof also survives a path git would otherwise rewrite. The branch's
115
+ changed paths are read with `git diff --name-only -z` and split on NUL, then fed
116
+ back under `git --literal-pathspecs`, so a path holding a non-ASCII byte, a double
117
+ quote, a backslash, a leading colon or a newline round-trips exactly. Without both,
118
+ such a path matched no pathspec, the second diff came back empty, and a branch the
119
+ base never received was removed with an empty reason list.
120
+
121
+ - 74ac309: Correct two shipped-text contradictions in the review docs: a count word that the data beside it
122
+ disproved, and a promise the sibling file did not keep.
123
+
124
+ `review-report`'s trend example is headed "re-run after two more rejections" but prints eight class
125
+ deltas, and one label per rejection makes that eight rejections. The heading now says eight, and the
126
+ sample is the script's own output over a tracker that took eight more rejections: eight classes
127
+ moved, the other four stayed, and the first-pass rate held because the rejections re-ran four
128
+ changes that already held a review cohort.
129
+
130
+ `review-defects` promised `EXAMPLES.md` carried "one real case per class". The file holds twelve
131
+ cases and eleven real observations, with `scope-creep` described as a situation because no recorded
132
+ case exists. The promise now says what the file delivers rather than inventing a case to satisfy it.
133
+
3
134
  ## 2.9.0
4
135
 
5
136
  ### 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
@@ -55,6 +105,25 @@ skill in this list that writes that file.
55
105
  A scope is one bd workspace. guppi reads several scopes and writes to one: the **current
56
106
  workspace**. Every other scope is read-only, whatever the request.
57
107
 
108
+ The scope list is configuration, not this file: `.agent-cortex/scopes.json` in the repository, or
109
+ `~/.agent-cortex/scopes.json` for the scopes that belong to the person rather than the checkout.
110
+ The `hitl-queue` skill reads it and prints one ranked list across every scope it can read, tagging
111
+ each row with the scope it came from. A scope that is absent or carries no tracker of its own is
112
+ reported `unavailable` and skipped: a machine without the personal repo still has a queue. Read the
113
+ list and the privacy rules from {{PATH:skills/workflow/hitl-queue/SCOPES.md}} rather than
114
+ restating them here.
115
+
116
+ **The daily note is the queue's first line about the day.** It names the scopes the day belongs to
117
+ and quotes the personal strategy line it came from. Report it as printed, above the ranked list,
118
+ and never paraphrase the quote: the citation is the point. Which scopes the day belongs to is the
119
+ human's call, written in their own strategy — when the note says it is missing, offer the
120
+ `prioritise-work` interview rather than choosing a scope.
121
+
122
+ **Personal work stays in the personal scope.** Never copy a personal bead's text into a code
123
+ workspace, a handoff document or a pull request, and never write to a scope other than the current
124
+ workspace. An output that leaves the machine — a pasted report, a message to another agent, a PR
125
+ body — comes from a `--shared` run, which withholds personal rows and the daily note.
126
+
58
127
  **Personal work is a town-level scope, never a rig.** A rig is a code-project container — refinery,
59
128
  witness, polecats, seeded patrols — and it suits code projects. Personal work belongs to the mayor
60
129
  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.10.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.
@@ -0,0 +1,59 @@
1
+ # Deadlines
2
+
3
+ A `--due` date is the one deadline field, and `bd` already stores it. The queue multiplies the
4
+ total by the factor of the narrowest tier the remaining time fits inside. The table is the
5
+ strategy's to tune, the boundary is arithmetic, and the row prints what it applied.
6
+
7
+ ## The shipped table
8
+
9
+ | Time left | Factor |
10
+ |---|---|
11
+ | more than a week | 1 |
12
+ | within a week | 1.5 |
13
+ | within two days | 2 |
14
+ | within a day | 4 |
15
+ | overdue | 8 |
16
+
17
+ Those numbers are the **shipped default**: a strategy with no `## Deadline tiers` section ranks on
18
+ this table, and so does a run whose header says `strategy unknown`.
19
+
20
+ ## A strategy's table
21
+
22
+ The strategy's optional `## Deadline tiers` section carries the table a repository tunes, so
23
+ moving a tier is a strategy edit and a `bump`, never a code change. A row reads
24
+ `| <when> | <factor> |`, narrowest first, and `<when>` is `within <n><m|h|d>`,
25
+ `more than <n><m|h|d>` or `overdue`.
26
+
27
+ A table that is present must carry exactly one `overdue` row, exactly one `more than` row and at
28
+ least one `within` row, and `validate` exits 1 naming every row it cannot read — a malformed table
29
+ never falls back to the default in silence.
30
+
31
+ ## Where a boundary falls
32
+
33
+ A boundary falls on the side that names it: with exactly one day left a bead earns 4, and a
34
+ millisecond more earns 2. A bead whose deadline is exactly now has not slipped yet, so it is not
35
+ overdue. A bead with no due date sits outside the tiers and keeps the factor 1, so tuning a row
36
+ never lifts undated work by accident.
37
+
38
+ ## The deadline clause in a row
39
+
40
+ A ranked row shows the deadline, the time left and the factor, and an overdue bead is flagged
41
+ rather than lifted in silence:
42
+
43
+ ```text
44
+ 2 bd-2 8.0 rice 4.2 = R4 · I3 · C0.7 · E2 ×1.00 dependents(0) ×1.0 deadline ×8 OVERDUE by 1d (due 2026-11-14T08:59Z)
45
+ ```
46
+
47
+ An unscored row has no total to multiply, so it shows the due moment and `OVERDUE` with no factor
48
+ at all:
49
+
50
+ ```text
51
+ - bd-6 — unscored — no rice metadata OVERDUE by 2d 11h (due 2026-11-13T00:00Z)
52
+ ```
53
+
54
+ ## Deferral
55
+
56
+ `--defer` is bd's own field and bd's own rule, so the queue reads it rather than inventing a second
57
+ one. `bd ready` wakes a bead whose defer date has passed, while `bd list` keeps reporting the stale
58
+ `deferred` status it was parked in, so the queue applies that same wake rule to `defer_until`. A
59
+ bead deferred into the future is absent and counted; a bead whose defer date has passed is listed.