@jaybeeuu/agent-cortex 1.42.0 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +73 -2
  2. package/.github/workflows/ci.yml +47 -27
  3. package/AGENTS.md +23 -2
  4. package/CHANGELOG.md +166 -0
  5. package/README.md +125 -73
  6. package/agents/README.md +17 -4
  7. package/bin/agent-cortex.mjs +48 -30
  8. package/bin/installers/pi.mjs +885 -237
  9. package/docs/claude-hooks.md +5 -4
  10. package/docs/commit-signing.md +231 -0
  11. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +10 -1
  12. package/docs/ideas/extension-manifests.md +5 -0
  13. package/docs/research/disable-model-invocation.md +103 -0
  14. package/docs/research/skill-design-research.md +2 -2
  15. package/docs/skills/skill-anatomy.md +5 -1
  16. package/docs/technical-direction/harness-config.md +25 -1
  17. package/docs/technical-direction/migration-execution-labels.md +17 -9
  18. package/extensions/agent-modes/discover.test.ts +19 -0
  19. package/extensions/agent-modes/discover.ts +25 -3
  20. package/extensions/session-start/README.md +38 -0
  21. package/extensions/session-start/index.test.ts +122 -0
  22. package/extensions/session-start/index.ts +27 -0
  23. package/extensions/session-start/package.json +19 -0
  24. package/extensions/session-start/tsconfig.json +12 -0
  25. package/extensions/skill-stats/pnpm-lock.yaml +329 -0
  26. package/extensions/subagent/README.md +1 -1
  27. package/extensions/subagent/agents.test.ts +176 -0
  28. package/extensions/subagent/agents.ts +102 -11
  29. package/extensions/subagent/index.ts +2 -1
  30. package/extensions/subagent/package.json +1 -1
  31. package/lib/cli.mjs +35 -9
  32. package/package.json +6 -5
  33. package/pi/settings.json +1 -7
  34. package/plugin.json +1 -1
  35. package/pnpm-workspace.yaml +6 -0
  36. package/scripts/check-changeset.mjs +42 -0
  37. package/scripts/lib/release-gates.mjs +61 -0
  38. package/scripts/lib/verify-worker-tree.mjs +107 -0
  39. package/scripts/resolve-publish-command.mjs +17 -0
  40. package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
  41. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
  42. package/skills/workflow/git-workflow/SKILL.md +35 -1
  43. package/skills/workflow/ralph/REFERENCE.md +17 -1
  44. package/skills/workflow/ralph/SKILL.md +4 -1
  45. package/skills/workflow/run-pipeline-stage/SKILL.md +11 -4
  46. package/skills/workflow/run-pipeline-stage/playbooks/code.md +2 -0
  47. package/skills/workflow/run-pipeline-stage/playbooks/document.md +3 -0
  48. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -0
  49. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +11 -0
  50. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
  51. package/test/cli.test.mjs +26 -29
  52. package/test/helpers/fake-pi.mjs +132 -0
  53. package/test/pi-installer.test.mjs +345 -286
  54. package/test/pi-register.test.mjs +342 -0
  55. package/test/pi-sweep.test.mjs +619 -0
  56. package/test/release-gates.test.mjs +190 -0
  57. package/test/verify-worker-tree.test.mjs +120 -0
  58. package/token-map.README.md +7 -6
  59. package/token-map.json +6 -5
  60. package/extensions/session-start.ts +0 -44
  61. package/lib/pi-packages.mjs +0 -169
  62. package/test/pi-packages.test.mjs +0 -295
@@ -14,7 +14,7 @@ UI surface. Each decision below is grounded in the Claude Code hooks reference.
14
14
 
15
15
  | pi extension | Claude port | Decision |
16
16
  |---|---|---|
17
- | `session-start.ts` | `SessionStart` hook commands | **Port** (extended) |
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
20
  | `notify/` | `Notification` hook + `scripts/notify.mjs` | **Port** |
@@ -22,10 +22,11 @@ UI surface. Each decision below is grounded in the Claude Code hooks reference.
22
22
  | `subagent/` | — | **Reject** — native |
23
23
  | `agent-modes/` | — | **Reject** — no equivalent |
24
24
 
25
- ## `session-start.ts` → SessionStart (port)
25
+ ## `session-start/` → SessionStart (port)
26
26
 
27
- pi injects skill-availability hints + project context files (AGENTS.md,
28
- `docs/user-preferences.md`) into every session. Claude Code already loads
27
+ pi injects the project context files (AGENTS.md, `docs/user-preferences.md`)
28
+ into every session; it lists no skills, since pi's system prompt already names
29
+ every skill with its `SKILL.md` path. Claude Code already loads
29
30
  `AGENTS.md` natively and the existing SessionStart hook nudge ("Load AGENTS.md
30
31
  and active beads context…") plus the style-skill policy (PR #57) cover the
31
32
  context side. This bead closes the remaining gap: a skill-policy command listing
@@ -0,0 +1,231 @@
1
+ # Commit signing
2
+
3
+ GitHub has refused merges here with **"Merging is blocked — Commits must have verified
4
+ signatures"**. The instinctive reading — "agents don't sign, so of course" — is wrong:
5
+ agent and human commits on this repo are GPG-signed and verified. The unsigned commits
6
+ are the ones `github-actions[bot]` writes, and only because `GITHUB_TOKEN` cannot hold a
7
+ signing key. This note records what is actually signed, what the API says about
8
+ enforcement, and the release policy that follows.
9
+
10
+ ## What is signed, and what is not
11
+
12
+ The agent environment signs automatically. Global git config has `commit.gpgsign=true`
13
+ and the secret key `DF657A88675E4AF8F7D65192A8F2E11DE960BE54` (Josh Bickley-Wallace,
14
+ the `noreply` address) is present, so every commit an agent makes is signed without any
15
+ per-commit step. Verified against the API:
16
+
17
+ | Commit | Author | `verification` |
18
+ |---|---|---|
19
+ | `63f3bef` (PR #181 head) | jaybeeuu | `verified=true reason=valid` |
20
+ | `7f99e4c` (PR #182 head) | jaybeeuu | `verified=true reason=valid` |
21
+ | `9443f55` (PR #168 head) | github-actions[bot] | `verified=false reason=unsigned` |
22
+
23
+ `changesets/action` commits as `github-actions[bot]` using `secrets.GITHUB_TOKEN`, and
24
+ GitHub does not sign commits made with a token. That is a property of the token, not a
25
+ missing configuration step — no amount of workflow YAML changes it.
26
+
27
+ Landed history looks uniformly `valid` for a different reason: the commit the merge creates
28
+ is signed by GitHub. `dd347471b1`, the commit that landed PR #161 ("chore: version
29
+ packages", authored by `github-actions[bot]`), has `committer=GitHub` and
30
+ `verification.verified=true reason=valid`. The bot's unsigned branch commit never reached
31
+ `main`; the squash commit GitHub created did, and GitHub signed it.
32
+
33
+ That is what this repository's history proves, and it is all it proves. It does **not**
34
+ prove that a squash merge escapes an *enforced* signature rule.
35
+
36
+ ## Squash does not bypass an enforced signature rule
37
+
38
+ The landing commit being GitHub-signed does not exempt the pull request from the check:
39
+ a ruleset that requires signatures inspects the head-branch commits too. GitHub's ruleset
40
+ reference, ["Require signed commits"](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#require-signed-commits)
41
+ (quoted verbatim, checked 2026-10-27):
42
+
43
+ > When GitHub evaluates whether a pull request can be merged, it creates a test merge commit
44
+ > whose parents are the latest commit on the base branch and the pull request's head commit.
45
+ > GitHub checks the commits introduced by this test merge, including commits from the head
46
+ > branch. As a result, unsigned commits on the head branch can block a squash merge, even
47
+ > though GitHub would sign the final squash commit. This restriction can also apply to the
48
+ > author of the pull request.
49
+
50
+ and the remedy it documents:
51
+
52
+ > To merge a blocked pull request, rewrite and sign the unsigned commits on the head branch,
53
+ > or ask someone with permission to bypass the applicable protections to merge the pull
54
+ > request.
55
+
56
+ So "squash it and let GitHub re-sign" is not a way *around* the check — it is only the reason
57
+ landed history reads `valid` when no signature rule is in force. If a signature rule is
58
+ enforced, the remedy is a signed commit, not a different merge method.
59
+
60
+ ## What the ruleset API shows — and what it cannot
61
+
62
+ No `required_signatures` rule is visible to the tokens available here, at any level.
63
+ Read-only probes:
64
+
65
+ ```
66
+ $ gh api repos/jaybeeuu/agent-cortex/rules/branches/main --jq '[.[].type]'
67
+ ["deletion","non_fast_forward","required_linear_history",
68
+ "required_status_checks","pull_request"]
69
+ ```
70
+
71
+ Every effective rule on `main` comes from `ruleset_id 22101709` with
72
+ `ruleset_source_type: Repository`. That is evidence rather than silence, because the endpoint
73
+ is not repository-scoped by construction: it is documented to return rules "regardless of the
74
+ level at which they are configured (e.g. repository or organization)", and its own response
75
+ schema includes a `ruleset_source_type: "Organization"` example ([REST reference, "Get rules for a branch"](https://docs.github.com/en/rest/repos/rules#get-rules-for-a-branch), checked
76
+ 2026-10-27). There is no `required_signatures` rule in it, and its `pull_request` parameters
77
+ allow `["merge","squash","rebase"]`. The other read paths agree:
78
+
79
+ ```
80
+ $ gh api repos/jaybeeuu/agent-cortex/branches/main/protection
81
+ {"message":"Branch not protected", ... "status":"404"}
82
+
83
+ $ gh api orgs/jaybeeuu/rulesets
84
+ {"message":"Not Found", ... "status":"404"}
85
+ ```
86
+
87
+ Two explanations remain. The branch-rule read above weighs heavily against the first, without
88
+ formally eliminating it:
89
+
90
+ 1. **An organization-level ruleset enforces signatures.** This is the weaker of the two now:
91
+ the applied-rule read covers every level and shows no organization-sourced rule, so this
92
+ survives only on an undocumented possibility that the endpoint filters by caller
93
+ visibility. Org ruleset *definitions* do separately require organization administration
94
+ read, which the current token does not hold (scopes: `gist, read:org, repo, workflow`), so
95
+ a `404` on `orgs/jaybeeuu/rulesets` is inconclusive — it may mean "none exist" or "you may
96
+ not see them". Note that the ruleset's `bypass_actors` is empty and
97
+ `current_user_can_bypass` is `"never"`, so repository admin does **not** bypass it.
98
+ 2. **The UI message was stale** — shown while a signature rule was active, before ruleset
99
+ `22101709` was last updated (2026-09-13).
100
+
101
+ Closing this out needs a merge attempt (a mutation) or org-admin access: see *The enforcing
102
+ rule: still open (AC1)* and the human follow-ups below.
103
+
104
+ One adjacent rule is worth knowing, though it cannot fire here. Ruleset `22101709` sets
105
+ `require_extra_approval_for_unattributed_changes: true`, which adds one approval when a pull
106
+ request is an unattributed Copilot pull request — but GitHub documents that the setting "has
107
+ no effect if the ruleset requires zero approvals" ([available rules for rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#additional-approval-for-unattributed-copilot-pull-requests),
108
+ checked 2026-10-27), and this ruleset sets `required_approving_review_count: 0`. A blocked
109
+ merge box here is the missing `statusCheckRollup` on a Version Packages PR or a missing
110
+ code-owner review, not this setting.
111
+
112
+ ## Policy: land bot-authored PRs by squash merge
113
+
114
+ Adopted: **Option A — no signing key in CI.** So long as no signature rule is enforced, no
115
+ unsigned commit is required to land a release: the merge produces a commit GitHub signs, so
116
+ the only requirement is the merge method.
117
+
118
+ Squash-merge Version Packages PRs (`gh pr merge <n> --squash`). GitHub rewrites the bot's
119
+ unsigned branch commit into a single commit that it signs as committer, which is exactly
120
+ what `dd347471b1` demonstrates. Squash is the only path that lands a GitHub-signed commit:
121
+ `allow_merge_commit` is `false` in the repository merge settings, so a true merge commit is
122
+ impossible, and `required_linear_history` on `main` means a pull request "must use a squash
123
+ merge or a rebase merge". The ruleset's `allowed_merge_methods`
124
+ (`["merge","squash","rebase"]`) leaves both open — its `merge` entry is already dead because
125
+ the repository disables merge commits. Of those two, only squash produces a commit GitHub
126
+ creates and signs: rebase-and-merge is documented to add the head-branch commits "without
127
+ commit signature verification", because the commit it creates is modified and GitHub "can't
128
+ therefore sign it as a generic system user" ([about merge methods](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github),
129
+ checked 2026-10-27). For a `github-actions[bot]` head commit that means an unsigned commit on
130
+ `main`, so rebase is not an acceptable path here even though the settings permit it.
131
+
132
+ Note the limit of this remedy: squash re-signs the landing commit, it does not bypass an
133
+ enforced signature rule (see above). It works here because no such rule is currently
134
+ enforced against this repository.
135
+
136
+ Consequence: the release automation stays fully automated and no signing key enters CI —
137
+ provided no signature rule is enforced. See the contingency below.
138
+
139
+ ### Contingency: the enforcing rule turns out to be real at organization level
140
+
141
+ The policy above holds only while a squash merge is permitted. If the merge probe or an org
142
+ admin confirms an organization-level `required_signatures` rule, Option A's premise fails:
143
+ GitHub would then refuse the squash merge as well, because it validates the pull request's
144
+ head commits through the test merge. In that case:
145
+
146
+ 1. Do not keep re-attempting the squash merge, and do not treat a bypass as the workaround —
147
+ a bypass is a human permission decision, not a release path.
148
+ 2. The release path must produce signed commits, which means Option B (import a key into the
149
+ release job) or having a key-holding actor sign the release commit. That change must land
150
+ with the access audit and rotation procedure Option B requires.
151
+ 3. Until then, releasing is a human step performed with bypass permission, recorded as a
152
+ HITL note on the bead.
153
+
154
+ The applied-rule read returns no organization-sourced rule, but org ruleset definitions stay
155
+ unreadable from a repository-scoped token, so re-check the read before assuming the absence
156
+ persists.
157
+
158
+ ### Option B, rejected as the default
159
+
160
+ Importing a GPG key into the release job (`GPG_PRIVATE_KEY` + `GPG_PASSPHRASE` secrets and
161
+ a `git commit --amend -S` step) would make the *branch* commit signed too. It is rejected as
162
+ the default because, while no signature rule is enforced, it buys nothing the squash merge
163
+ does not already give — the branch commit is discarded on merge — while placing a
164
+ long-lived private key in repository secrets. That trade-off inverts if a signature rule is
165
+ confirmed enforced: Option B then becomes necessary (see the contingency above). Either way,
166
+ if it is revisited the same change must carry an access audit (who and what can read the
167
+ secret) and a rotation procedure (key expiry, re-issue, revocation). Neither exists today,
168
+ and adding the key without them is not acceptable.
169
+
170
+ ## When the signature check fires
171
+
172
+ | Situation | Remedy |
173
+ |---|---|
174
+ | Bot-authored PR blocked on signatures, rule **confirmed** enforced | A squash merge will not help — GitHub checks head-branch commits through its test merge. Rewrite the branch so its commits are signed (`git rebase --exec 'git commit --amend -S --no-edit'` or an amend plus force-push), then merge; otherwise a person with bypass permission merges it. |
175
+ | Bot-authored PR blocked, `rules/branches/main` lists no `required_signatures` rule | The block is most likely not signatures. Read the merge box first: missing required status checks (an empty `statusCheckRollup` on a Version Packages PR is the common case) or a missing code-owner review fully explain `mergeStateStatus=BLOCKED`. |
176
+ | An agent commit is reported unsigned | Check `git config --get commit.gpgsign` and `gpg --list-secret-keys`; re-sign with `git commit --amend -S --no-edit` and force-push the branch. |
177
+ | The block persists with no visible cause | Read `gh api repos/<owner>/<repo>/rules/branches/<branch>`. That endpoint covers every level, so no `required_signatures` entry means no active signature rule applies as far as this read can see — if the block still looks like signatures, have an org admin confirm the org rulesets rather than guessing. |
178
+
179
+ ## The enforcing rule: still open (AC1)
180
+
181
+ The parent bead's first acceptance criterion — "Enforcing rule identified, or the UI message
182
+ shown to be stale — with the merge API's error text quoted either way" — is **open**. No
183
+ merge API error text exists yet, because obtaining it requires a merge attempt, which is a
184
+ mutation and therefore a human step. Read-only evidence narrows the question sharply but
185
+ does not close it:
186
+
187
+ 1. **No repository-level signature rule was blocking on 2026-10-13.** PR #161's head
188
+ `fe7ef84` is `verification.verified=false reason=unsigned`, and the PR was squash-merged
189
+ at `2026-10-13T21:46:06Z` — after ruleset `22101709`'s last update
190
+ (`2026-09-13T21:11:23.985+01:00`, i.e. `2026-09-13T20:11:23Z`). An unsigned head landed
191
+ after the ruleset's last edit, so neither that ruleset nor anything older in it enforced
192
+ signatures then.
193
+ 2. **PR #168's current block needs no signature rule to explain it.**
194
+ `gh pr view 168 --json state,mergeable,mergeStateStatus,statusCheckRollup,reviews` returns
195
+ `state=OPEN`, `mergeable=MERGEABLE`, `mergeStateStatus=BLOCKED`,
196
+ `statusCheckRollup=[]`, `reviews=[]`. All five required checks (`Test`, `Lint`,
197
+ `Claude plugin materialiser check`, `Changeset check`, `Setup`) are unreported, and
198
+ `require_code_owner_review` is on with no reviews. `required_status_checks` plus the
199
+ missing code-owner review fully account for `BLOCKED`.
200
+ 3. **The applied-rule read comes back clean, including at organization level.**
201
+ `rules/branches/{branch}` is documented to return every active rule "regardless of the
202
+ level at which they are configured (e.g. repository or organization)", so it is the read
203
+ that *would* surface an organization-level rule. The live read returns five rules, all
204
+ `ruleset_source_type: Repository` from `ruleset_id 22101709`, and none of them is
205
+ `required_signatures`. On the documented contract that is positive evidence that no
206
+ organization-level signature rule is applied to `main` today, not an unreadable gap. The
207
+ honest residual is narrower: whether this endpoint filters by caller visibility is
208
+ undocumented, and org ruleset *definitions* stay unreadable with the current token —
209
+ `gh api orgs/jaybeeuu/rulesets` returns `{"message":"Not Found", ... "status":"404"}` and
210
+ `gh api orgs/jaybeeuu` adds `gh: This API operation needs the "admin:org" scope`. So "no
211
+ org rule is applied" is read; "no org ruleset exists" is not.
212
+
213
+ Status: **open, pending a human merge probe or org-admin inspection** (see below). The read
214
+ above narrows the question but cannot close AC1, which turns on the merge API's own error
215
+ text; the merge probe supplies that.
216
+
217
+ ## Human-only follow-ups
218
+
219
+ These need repository or organization settings and cannot be done by an agent:
220
+
221
+ - **Merge probe — closes AC1.** Run `gh pr merge 168 --squash` (or
222
+ `PUT /repos/jaybeeuu/agent-cortex/pulls/168/merge`) and record the verbatim error text
223
+ here. A signature error confirms an invisible rule and triggers the contingency above; a
224
+ status-check error confirms the message was stale and closes the criterion as
225
+ "UI message stale".
226
+ - **Inspect org rulesets.** Open `https://github.com/organizations/jaybeeuu/settings/rules`
227
+ and confirm whether a ruleset targets `jaybeeuu/agent-cortex` with "Require signed
228
+ commits". Record the answer here.
229
+ - **Confirm the merge method.** If squash-merge is the documented path, consider removing
230
+ `rebase` from `allowed_merge_methods` so a rebase merge cannot add an unsigned bot
231
+ commit to `main`.
@@ -30,4 +30,13 @@ The author. Main-branch history becomes verifiably authentic — every commit at
30
30
  Determine how the changesets commit gets signed in CI on this repo — cheapest prototype: scratch repo (or the version branch) exercising the signing path end-to-end without touching `main`'s rule, then evaluate the rule change on `main`.
31
31
 
32
32
  ## Notes
33
- Recorded 2026-09-02. Backlog capture only — no active work planned; revisit after the install-manager epic (pi/claude/copilot) settles. Adjacent to `docs/ideas/unified-ci-pipeline.md` (CI/release restructure).
33
+ Recorded 2026-09-02. Backlog capture only — no active work planned; revisit after the install-manager epic (pi/claude/copilot) settles. Adjacent to `docs/ideas/unified-ci-pipeline.md` (CI/release restructure).
34
+
35
+ Updated 2026-10-28: `docs/commit-signing.md` records the investigation and the decided policy.
36
+ The applied-rule read shows no `required_signatures` rule on `main` at repository or
37
+ organization level (org ruleset *definitions* stay unreadable with a repository-scoped token),
38
+ and Version Packages PRs land by squash merge, whose landing commit GitHub signs — so releases
39
+ stay automated with no key in CI, and the CI signing key this idea points at is rejected as the
40
+ default. That rejection does not settle the idea: GitHub checks head-branch commits through its
41
+ test merge, so enabling enforcement would still require the signed CI commit, exactly as
42
+ `Next validation step` describes.
@@ -7,6 +7,11 @@ harness's native dependency fields plus committed settings templates. See the de
7
7
  prune TUI was rejected (see "Prune rejected") and the proposed declared-vs-installed diff was
8
8
  dropped — `agent-cortex install <harness>` reconciles instead.
9
9
 
10
+ The mechanism recorded under "What shipped" has since been removed (2026-10-24,
11
+ `agnt-ctx-7kru.1`): `pi.packages`, `lib/pi-packages.mjs` and `--no-provision` are gone, and the
12
+ required pi packages now travel inside the staged package at `~/.agent-cortex/pi` instead of
13
+ being provisioned into pi's user scope.
14
+
10
15
  ## Created
11
16
  2026-08-25
12
17
 
@@ -0,0 +1,103 @@
1
+ # `disable-model-invocation` across harnesses
2
+
3
+ Decision record for the frontmatter field this repo uses to keep rare or destructive
4
+ skills out of automatic model routing (bead `agnt-ctx-yf24`).
5
+
6
+ **Verdict:** rely on it for pi and Claude Code. Do not rely on it in Copilot CLI as
7
+ "invoke-only" — there it means "unreachable through the model-facing skill tool".
8
+
9
+ ## What the field does
10
+
11
+ `disable-model-invocation: true` removes a skill from the list the model is offered.
12
+ The user can still invoke it explicitly. It does not summarise, rewrite, or
13
+ otherwise change how the body loads, which is what `docs/skills/skill-anatomy.md`
14
+ previously implied.
15
+
16
+ ## Preservation and honouring
17
+
18
+ | Harness | Field survives materialisation | Loader honours it | Evidence |
19
+ | --- | --- | --- | --- |
20
+ | pi 1.4.0 | Yes — byte-identical copy | Yes — hidden from the prompt, `/skill:name` still works | `bin/installers/pi.mjs:182-204`, `scripts/lib/compose-agent.mjs:69-95`; `dist/core/skills.js:262` (parse) and `:279` (filter); `docs/skills.md:53` |
21
+ | Claude Code 2.1.294 | Yes — byte-identical copy | Yes — Claude cannot invoke it, `/skill-name` still works | `bin/installers/claude.mjs:160-201`, same token helper; [Claude Code skills docs](https://code.claude.com/docs/en/skills) ("Control who invokes a skill") |
22
+ | Copilot CLI (1.0.55 locally, 1.0.80/1.0.83 upstream) | N/A — no copy; `plugin.json` `"skills": "skills/"` serves the repo tree in place | Auto-invocation suppressed, but explicit model invocation fails; `/skill-name` works | `plugin.json:12`; [github/copilot-cli#4438](https://github.com/github/copilot-cli/issues/4438) (open, retested 1.0.83) |
23
+
24
+ Both materialisers copy `SKILL.md` as text and rewrite only `{{TOOL:...}}` /
25
+ `{{PATH:...}}` tokens, so frontmatter keys they do not know pass through untouched —
26
+ nothing re-serialises YAML. Verified by installing all three harnesses into
27
+ `/var/tmp/dmi-check` and diffing `skills/productivity/teach/SKILL.md` (the repo's
28
+ only current user of the field): both copies are byte-identical to the source.
29
+ `claude plugin validate /var/tmp/dmi-check/claude/skills` passes, i.e. Claude Code's
30
+ own validator accepts the field rather than warning about it.
31
+
32
+ pi was probed against the materialised tree with its own loader: 32 skills loaded,
33
+ 31 advertised, `teach` parsed with `disableModelInvocation: true` and absent from
34
+ `formatSkillsForPrompt` output.
35
+
36
+ ## Per-harness detail
37
+
38
+ **pi.** Documented and implemented. `docs/skills.md:53`: "Set
39
+ `disable-model-invocation: true` in frontmatter when a skill should be available only
40
+ through its explicit command", with the frontmatter table listing it as "Hide the
41
+ skill from automatic model selection".
42
+
43
+ **Claude Code.** Documented, and the docs are explicit about the enforcement: "Claude
44
+ can't invoke the skill on its own … If Claude tries anyway, Claude Code blocks the
45
+ call and instructs it not to reproduce the deploy steps another way", and it "removes
46
+ the skill from Claude's context entirely".
47
+
48
+ Two Claude-specific caveats:
49
+
50
+ - The field is a Claude Code extension, not part of the
51
+ [Agent Skills spec](https://agentskills.io/specification), whose six fields
52
+ (`name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`)
53
+ are otherwise exhaustive. Claude Code ignores unknown fields, but packing or
54
+ uploading a skill to claude.ai (or through the Skills API) "fails with a hard error
55
+ instead of ignoring the field". Our plugin path is unaffected; a skill we ever
56
+ publish to claude.ai must drop the field.
57
+ - `skillOverrides` does not apply to plugin skills, and our install is a plugin. The
58
+ frontmatter field itself still applies there — only that settings-based alternative
59
+ is unavailable.
60
+
61
+ **Copilot CLI.** The field is not in GitHub's skill documentation, which lists only
62
+ `name`, `description`, `license`, and `allowed-tools`. In practice the CLI filters
63
+ flagged skills out of the `skill()` tool's resolution list, so the model cannot call
64
+ them even when the user names one: `Skill not found`. Only the interactive
65
+ `/skill-name` slash command reaches a flagged skill, and `-p` mode has no slash path.
66
+ Upstream issue #4438 is open.
67
+
68
+ Local probing was not possible: the installed CLI is 1.0.55 (no `copilot skill`
69
+ subcommand), and prompt runs are refused by this machine's org policy. The upstream
70
+ report is the evidence for Copilot, tested on 1.0.80 and 1.0.83 — newer than 1.0.55,
71
+ so this may differ locally, in either direction.
72
+
73
+ ## Options for "explicit invocation only", evaluated
74
+
75
+ | Option | Portable? | Enforced? | Verdict |
76
+ | --- | --- | --- | --- |
77
+ | `disable-model-invocation: true` | pi + Claude Code yes; Copilot suppresses but breaks tool-based invocation | Yes on pi/Claude | **Recommended** where suppressing auto-routing is the point and the explicit path is a human typing `/name` |
78
+ | Naming convention (e.g. `manual-*`) | Yes | No — no harness reads names | Rejected: a human signal we already get from the body |
79
+ | `metadata:` block | Yes (spec-legal) | No — pi and Claude read top-level keys only, Copilot undefined | Rejected: documentation that looks like enforcement |
80
+ | Keep such skills out of `skills/` | Yes | Yes, by not shipping the skill | Rejected: gives up the per-harness install entirely |
81
+
82
+ ## Recommendation
83
+
84
+ Use the field when the danger is fuzzy automatic matching and the intended explicit
85
+ path is a human typing `/skill-name`: that is the case for a migration skill whose
86
+ `--apply` renames labels workspace-wide. Accept, and document, that on Copilot CLI it
87
+ also blocks model-mediated invocation, including `-p` runs — if a workflow needs
88
+ non-interactive access to such a skill, do not set the field and instead disable the
89
+ skill in that user's Copilot config (`copilot skill disable <name>`, or the `/skills`
90
+ toggle), which is harness-native and per-user.
91
+
92
+ ## Reproducing
93
+
94
+ ```sh
95
+ node bin/agent-cortex.mjs install claude --output /var/tmp/dmi-check/claude
96
+ node bin/agent-cortex.mjs install pi --output /var/tmp/dmi-check/pi
97
+ node bin/agent-cortex.mjs install copilot --output /var/tmp/dmi-check/copilot
98
+ diff skills/productivity/teach/SKILL.md /var/tmp/dmi-check/claude/skills/teach/SKILL.md
99
+ diff skills/productivity/teach/SKILL.md /var/tmp/dmi-check/pi/skills/productivity/teach/SKILL.md
100
+ claude plugin validate /var/tmp/dmi-check/claude/skills
101
+ ```
102
+
103
+ The Copilot run writes only composed agent files — there is no skill copy to inspect.
@@ -35,7 +35,7 @@ The headline finding: **agent-cortex skills are well-structured but systematical
35
35
  | Cross-skill references | Explicit inline calls: "run `/grill-with-docs`" | Implicit — no inline cross-refs | agent-cortex weaker |
36
36
  | Shared vocabulary file | `CONTEXT.md` at repo root; skills told to read it | None | Missing in agent-cortex |
37
37
  | Skill manifest | Flat JSON array of skill paths | Same | ✅ Aligned |
38
- | `disable-model-invocation` | Used for pure prompt-injection skills | Not used | Feature gap |
38
+ | `disable-model-invocation` | Used for pure prompt-injection skills | Used by `teach`; semantics now documented in `docs/skills/skill-anatomy.md` | Field adopted; rationale corrected |
39
39
  | Missing SKILL.md | 0 | 1 (`prd-to-epics`) | Bug in agent-cortex |
40
40
 
41
41
  ### 2.2 Tone and Voice
@@ -251,7 +251,7 @@ These are patterns present in `mattpocock/skills` that have no equivalent here a
251
251
 
252
252
  2. **Philosophy sections in skills** — `tdd` doesn't just describe TDD, it argues *why* horizontal slicing is wrong. This bakes intent into the skill so the agent handles novel cases correctly (backed by CoT rationale evidence).
253
253
 
254
- 3. **`disable-model-invocation: true`** — for skills that *are* the prompt (pure injection, no interpretation). Prevents the model from summarising/transforming the skill content before executing it.
254
+ 3. **`disable-model-invocation: true`** — hides a skill from automatic model routing so only an explicit `/skill-name` invocation reaches it. See `docs/research/disable-model-invocation.md` for per-harness behaviour and caveats.
255
255
 
256
256
  4. **Composability by design** — skills reference each other by slash-command inline. This keeps each skill shorter and creates a network of reusable primitives rather than monolithic blobs.
257
257
 
@@ -11,7 +11,11 @@ Required fields:
11
11
 
12
12
  Optional fields:
13
13
  - `argument-hint`: input prompt shown to users at invocation.
14
- - `disable-model-invocation`: for deterministic prompt-injection skills that should not be summarized/interpreted.
14
+ - `disable-model-invocation`: hides the skill from automatic model routing; explicit
15
+ `/skill-name` invocation still works. Set it when the danger is a fuzzy description
16
+ match, e.g. a skill whose steps are destructive. Preserved and honoured by pi and
17
+ Claude Code; Copilot CLI suppresses auto-routing but then cannot reach the skill
18
+ through its model-facing tool. See `docs/research/disable-model-invocation.md`.
15
19
 
16
20
  ## Description format rules
17
21
 
@@ -1,5 +1,27 @@
1
1
  # Technical Direction: Harness config and third-party extensions
2
2
 
3
+ ## Status
4
+
5
+ **Partly superseded (2026-10-24) by the staged-package model** (`agnt-ctx-7kru`). Two premises
6
+ below no longer hold:
7
+
8
+ - **The pi half landed without bundling.** `agent-cortex install pi` stages a self-contained
9
+ package at `~/.agent-cortex/pi` and copies the required third-party packages — resolved
10
+ through Node's own resolution — into that tree's `node_modules/`, instead of declaring
11
+ `bundledDependencies` in the published tarball. pi loads a local directory and resolves no
12
+ npm packages itself, so a tarball's `node_modules/` reached nobody; bundling also forced a
13
+ repo-wide `node-linker=hoisted`, surrendering pnpm's isolated resolution for the workspace.
14
+ - **pi's own settings are no longer installer output.** The installer writes nothing into
15
+ `~/.pi/agent`; it registers the staged package by driving pi's own `pi install` /
16
+ `pi remove` commands, which own `settings.json` → `packages[]`. `pi/settings.json` and
17
+ `pi/keybindings.json` are reference templates. Registration's invariant is **one
18
+ agent-cortex source**: a source pointing at a raw agent-cortex checkout is removed first,
19
+ and one that survives `pi remove` fails the run rather than leave two sources configured
20
+ that both load.
21
+
22
+ `pi.packages` and `lib/pi-packages.mjs` are deleted, so the bespoke-pi-mechanism problem this
23
+ memo was written to settle is closed. The Claude and Copilot analysis stands.
24
+
3
25
  ## Problem and target outcome
4
26
 
5
27
  - **What we are solving.** agent-cortex ships agents and skills to three harnesses (pi, Claude
@@ -155,7 +177,9 @@ pi extension loads from `node_modules/` and an optional one is materialised from
155
177
  `extraKnownMarketplaces`).
156
178
  - GitHub Copilot CLI, *Plugin reference* — https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference
157
179
  and *Configuration directory* — https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
158
- - Code evidence: `package.json` (`pi.packages`), `lib/pi-packages.mjs` (main's provisioner);
180
+ - Code evidence (as of 2026-09 — `pi.packages` and `lib/pi-packages.mjs` were removed in
181
+ `agnt-ctx-7kru.1`, see Status): `package.json` (`pi.packages`), `lib/pi-packages.mjs` (main's
182
+ provisioner);
159
183
  `pi.extensions.json`, `claude.extensions.json`, `lib/extension-manifest.mjs` (epic F5, `1e0e970`);
160
184
  `bin/installers/pi.mjs` settings materialisation (epic F3, `afab2c6`);
161
185
  `docs/ideas/extension-manifests.md` (the originating idea).
@@ -13,14 +13,21 @@
13
13
 
14
14
  ## Current-state constraints
15
15
 
16
- - bd CLI is a global binary (`~/.local/bin/bd`); label ops: `bd label list-all [--json]`,
17
- `bd list --label-pattern/regex [--json]`, `bd label add/remove` (batch multiple ids).
16
+ - bd is a globally installed CLI resolved from `PATH` (whatever single `bd` is on PATH —
17
+ Homebrew, npm, or the install script); `BD_PATH` is the override for machines where `bd` is
18
+ not on PATH. Skill scripts invoke `BD_PATH || 'bd'` and never hardcode an install path.
19
+ Label ops: `bd label list-all [--json]`, `bd list --label-pattern/regex [--json]`,
20
+ `bd label add/remove` (batch multiple ids).
21
+ - A `bd` whose install target has been removed (`~/.local/bin` orphans, dangling pnpm shims)
22
+ must not be pinned: it can be a schema behind the workspace DB and fail with a schema
23
+ version mismatch. Keep exactly one `bd` on PATH so `bd` and the DB version agree.
18
24
  - `bd list` supports `--label-pattern`/`--json`; `bd count` does NOT support `--label-pattern`.
19
25
  - `bd ready --label-pattern 'stage:*'` works; `bd ready -l execution:afk` verified.
20
- - 3 script packages exist with per-skill `package.json` + own `pnpm-workspace.yaml` +
21
- own `pnpm-lock.yaml` (create-task, validate-agent-dir, run-pipeline-stage). They are NOT
22
- members of the root workspace (root glob `skills/*/scripts` does not match the nested
23
- `skills/<domain>/<name>/scripts`; root lockfile has zero refs to them).
26
+ - 3 script packages still exist with their own `package.json` (create-task,
27
+ validate-agent-dir, run-pipeline-stage). They ARE root workspace members (root glob
28
+ `skills/**/scripts`; all three are in the root `pnpm-lock.yaml`). Residual per-skill
29
+ `pnpm-lock.yaml` (all three) and `pnpm-workspace.yaml` (validate-agent-dir,
30
+ run-pipeline-stage) are stale artefacts of the pre-D2 layout.
24
31
  - All 3 scripts are stdlib-only (`node:child_process`, `node:fs`, `node:path`, `node:url`).
25
32
  - Extensions (`extensions/skill-stats`) have real peer deps on `@earendil-works/pi-coding-agent`
26
33
  — they remain workspace members with their own package.json.
@@ -162,8 +169,9 @@
162
169
 
163
170
  ## References
164
171
 
165
- - Code evidence: `bin/agent-cortex.mjs` + `lib/cli.mjs` (CLI surface), `skills/*/scripts/
166
- package.json` (3 orphaned), root `package.json` + `pnpm-workspace.yaml`, bd CLI help
167
- (`bd list`, `bd label`, `bd create`), epic `agnt-ctx-s5xd` notes.
172
+ - Code evidence: `bin/agent-cortex.mjs` + `lib/cli.mjs` (CLI surface),
173
+ `skills/*/*/scripts/package.json` (3, root workspace members), root `package.json` +
174
+ `pnpm-workspace.yaml`, bd CLI help (`bd list`, `bd label`, `bd create`), epic
175
+ `agnt-ctx-s5xd` notes.
168
176
  - Idea file: `docs/ideas/ctx-alias-version-and-update-commands.md` (ctx alias + version/update
169
177
  surface that `migrate` joins).
@@ -244,6 +244,25 @@ describe("discoverAgents (flat fallback)", () => {
244
244
  assert.deepEqual(strategy.tools, ["bash", "fetch_content", "read"]);
245
245
  assert.match(strategy.prompt, /Flat body of strategy\./);
246
246
  });
247
+ it("reads a space-separated scalar tools list instead of falling back to defaults", () => {
248
+ const fx = track(makeFixture());
249
+ writeFlatAgent(fx, "ralph", FLAT_FRONTMATTER("ralph", '"bash read grep task"'));
250
+
251
+ const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
252
+
253
+ // A collapsed scalar would land on DEFAULT_TOOLS, lifting the restriction
254
+ // rather than applying it — the tool names must survive the parse.
255
+ assert.deepEqual(agents[0].tools, ["bash", "grep", "read", "task"]);
256
+ });
257
+
258
+ it("reads a comma-separated scalar tools list", () => {
259
+ const fx = track(makeFixture());
260
+ writeFlatAgent(fx, "ralph", FLAT_FRONTMATTER("ralph", '"bash, view, task"'));
261
+
262
+ const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
263
+
264
+ assert.deepEqual(agents[0].tools, ["bash", "read", "task"]);
265
+ });
247
266
  });
248
267
 
249
268
  // ─── loadTokenMap ────────────────────────────────────────────────────────────
@@ -290,6 +290,21 @@ interface FlatParsed {
290
290
  prompt: string;
291
291
  }
292
292
 
293
+ /**
294
+ * Split a scalar `tools` value into tool names.
295
+ *
296
+ * Accepts the quoted string the pi installer renders (`tools: "bash, read"`)
297
+ * as well as an unquoted whitespace-separated list (`tools: bash read`).
298
+ * Commas alone are not enough: a whitespace-separated list would collapse to
299
+ * one bogus name and the agent would run with no usable tool allowlist.
300
+ */
301
+ function splitToolList(raw: string): string[] {
302
+ return raw
303
+ .replace(/^["']|["']$/g, "")
304
+ .split(/[,\s]+/)
305
+ .filter(Boolean);
306
+ }
307
+
293
308
  function parseFlatFrontmatter(raw: string): FlatParsed {
294
309
  const lines = raw.split(/\r?\n/);
295
310
  if (lines[0] !== "---") return { prompt: raw };
@@ -327,11 +342,16 @@ function parseFlatFrontmatter(raw: string): FlatParsed {
327
342
  }
328
343
 
329
344
  // Parse tools from the tools frontmatter.
330
- // Supports two formats:
331
- // tools: ["bash", "view"] (JSON array, single line)
332
- // tools: (YAML list, subsequent lines)
345
+ // Supports three formats:
346
+ // tools: ["bash", "view"] (JSON array, single line)
347
+ // tools: "bash, view" (scalar list, comma or whitespace separated)
348
+ // tools: (YAML list, subsequent lines)
333
349
  // - bash
334
350
  // - view
351
+ //
352
+ // The scalar form is what the pi installer renders for generated agents, so
353
+ // it must be read here rather than dropped to DEFAULT_TOOLS (which lifts the
354
+ // restriction instead of applying it).
335
355
  let inToolsBlock = false;
336
356
  for (const line of yamlLines) {
337
357
  const trimmed = line.trim();
@@ -354,6 +374,8 @@ function parseFlatFrontmatter(raw: string): FlatParsed {
354
374
  tools = [];
355
375
  continue;
356
376
  }
377
+ tools = splitToolList(val);
378
+ continue;
357
379
  }
358
380
  }
359
381
 
@@ -0,0 +1,38 @@
1
+ # session-start
2
+
3
+ A PI extension that injects, at the start of every session, the project's context files:
4
+
5
+ - `AGENTS.md`
6
+ - `docs/user-preferences.md`
7
+
8
+ Paths are relative to the session working directory (`ctx.cwd`); missing or empty files are
9
+ skipped. Content is injected as a hidden `session-start` message on `before_agent_start`, so it
10
+ reaches the model without cluttering the transcript UI.
11
+
12
+ ## No skill list here — on purpose
13
+
14
+ Earlier versions of this extension appended a hand-written blurb advertising three skills with
15
+ `/skill:<name>` syntax. Both halves were wrong:
16
+
17
+ - pi has no `/skill:` resolver — nothing maps that token to a read. Skills are used by reading
18
+ the `SKILL.md` path that pi lists.
19
+ - pi's own system prompt already lists **every** discovered skill with its absolute `SKILL.md`
20
+ path, so a local three-skill list was incomplete and drifted from `skills/` silently.
21
+
22
+ The blurb is gone and `index.test.ts` pins the absence of both `/skill:` syntax and hardcoded
23
+ skill names. Keep the authoritative list in `skills/`; do not reintroduce it here.
24
+
25
+ ## Installation
26
+
27
+ Bundled with the `agent-cortex` PI package — install the package for auto-discovery:
28
+
29
+ ```bash
30
+ pi install git:https://github.com/jaybeeuu/agent-cortex
31
+ ```
32
+
33
+ ## Development
34
+
35
+ ```bash
36
+ pnpm --filter @agent-cortex/session-start test
37
+ pnpm --filter @agent-cortex/session-start typecheck
38
+ ```