@jaybeeuu/agent-cortex 1.38.1 → 1.40.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 (88) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +1 -1
  2. package/.github/workflows/ci.yml +47 -14
  3. package/AGENTS.md +19 -3
  4. package/CHANGELOG.md +42 -0
  5. package/README.md +118 -63
  6. package/agents/README.md +13 -10
  7. package/agents/ralph-plan/agent.md +1 -1
  8. package/agents/ralph-plan.agent.md +1 -1
  9. package/bin/agent-cortex.mjs +29 -8
  10. package/bin/installers/claude.mjs +569 -105
  11. package/bin/installers/copilot.mjs +22 -12
  12. package/bin/installers/pi.mjs +37 -34
  13. package/docs/claude-hooks.md +3 -3
  14. package/docs/ideas/extension-manifests.md +75 -0
  15. package/docs/ideas/record-ideas-as-beads.md +115 -0
  16. package/docs/ideas/subagent-visibility.md +72 -0
  17. package/docs/ideas/task-session-context.md +89 -0
  18. package/docs/research/skill-design-research.md +4 -4
  19. package/docs/research/skill-improvements-analysis.md +1 -1
  20. package/docs/skills/skill-anatomy.md +38 -2
  21. package/docs/technical-direction/migration-execution-labels.md +132 -0
  22. package/extensions/agent-modes/README.md +3 -2
  23. package/extensions/agent-modes/discover.test.ts +30 -7
  24. package/extensions/agent-modes/discover.ts +1 -1
  25. package/extensions/skill-stats/index.test.ts +3 -2
  26. package/lib/cli.mjs +24 -6
  27. package/package.json +4 -4
  28. package/plugin.json +1 -1
  29. package/pnpm-workspace.yaml +1 -1
  30. package/scripts/build-copilot-agents.mjs +1 -1
  31. package/scripts/check-no-sync.mjs +49 -0
  32. package/scripts/lib/compose-agent.mjs +32 -14
  33. package/skills/engineering/design-an-interface/SKILL.md +78 -58
  34. package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
  35. package/skills/engineering/grill-with-docs/SKILL.md +66 -63
  36. package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
  37. package/skills/engineering/technical-direction/FORMAT.md +38 -0
  38. package/skills/engineering/technical-direction/SKILL.md +70 -77
  39. package/skills/planning/classify-bead/SKILL.md +59 -35
  40. package/skills/planning/create-task/REFERENCE.md +40 -0
  41. package/skills/planning/create-task/SKILL.md +70 -66
  42. package/skills/planning/create-task/scripts/create-chores.ts +6 -1
  43. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
  44. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
  45. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
  46. package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
  47. package/skills/planning/prd-to-tasks/SKILL.md +86 -170
  48. package/skills/planning/record-idea/FORMAT.md +44 -0
  49. package/skills/planning/record-idea/SKILL.md +70 -53
  50. package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
  51. package/skills/planning/request-refactor-plan/SKILL.md +57 -46
  52. package/skills/planning/write-a-prd/FORMAT.md +54 -0
  53. package/skills/planning/write-a-prd/SKILL.md +49 -48
  54. package/skills/planning/write-a-ticket/SKILL.md +68 -53
  55. package/skills/productivity/handoff/SKILL.md +12 -0
  56. package/skills/productivity/init-beads/SKILL.md +48 -40
  57. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
  58. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
  59. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
  60. package/skills/productivity/write-a-skill/SKILL.md +2 -1
  61. package/skills/review/maintain-agent-docs/SKILL.md +71 -51
  62. package/skills/workflow/hitl-collab/SKILL.md +86 -17
  63. package/skills/workflow/ralph/REFERENCE.md +22 -17
  64. package/skills/workflow/ralph/SKILL.md +88 -74
  65. package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
  66. package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
  67. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
  68. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
  69. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
  70. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
  71. package/test/claude-installer.test.mjs +308 -0
  72. package/test/cli.test.mjs +40 -30
  73. package/test/copilot-installer.test.mjs +92 -82
  74. package/test/helpers/fake-claude.mjs +152 -0
  75. package/test/installer.test.mjs +309 -57
  76. package/test/pi-installer.test.mjs +115 -99
  77. package/token-map.README.md +21 -10
  78. package/token-map.json +7 -6
  79. package/.claude-plugin/marketplace.json +0 -16
  80. package/claude/.claude-plugin/plugin.json +0 -11
  81. package/claude/agents/plan.md +0 -58
  82. package/claude/agents/ralph-plan.md +0 -121
  83. package/claude/agents/ralph.md +0 -193
  84. package/claude/agents/strategy.md +0 -111
  85. package/claude/hooks/scripts/notify.mjs +0 -54
  86. package/claude/hooks.json +0 -38
  87. /package/{claude → claude-extras}/.mcp.json +0 -0
  88. /package/{claude → claude-extras}/scripts/statusline-command.sh +0 -0
@@ -58,9 +58,9 @@ Both repos follow the same two-part format: capability sentence + `"Use when..."
58
58
  > "Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says 'diagnose this' / 'debug this', reports a bug, says something is broken/throwing/failing, or describes a performance regression."
59
59
 
60
60
  **agent-cortex example** (vaguer triggers):
61
- > "Create a bead, classify it, and if AFK expand it into pipeline stage child beads plus a HITL PR gate task. Use when creating a new task that should be tracked and moved through implementation and human review gates."
61
+ > "Create a task bead, classify it as AFK or HITL, and if AFK expand it into pipeline stage chores plus a HITL PR gate. Use when starting tracked work with implementation and review gates — 'create a task for this', 'track this end-to-end', or 'get this into the ralph pipeline'."
62
62
 
63
- The agent-cortex description describes internal mechanics rather than what the user is trying to accomplish.
63
+ The agent-cortex description leads with internal mechanics ("task bead", "AFK", "HITL") rather than what the user is trying to accomplish — the quoted triggers were added in a later refactor, but the jargon remains.
64
64
 
65
65
  ---
66
66
 
@@ -180,7 +180,7 @@ Move to Phase 2 only when both boxes are checked.
180
180
 
181
181
  **Evidence**: Anthropic Building Effective Agents: *"Think of this as writing a great docstring for a junior developer on your team."* For agents, descriptions are the only routing signal. FLAN (Wei et al., ICLR 2022) showed instruction-tuned models generalise from natural language task descriptions — use the vocabulary a user would *actually speak*, not internal system jargon. Anthropic tool use note: `"Use when"` language *actively counteracts* the model's default tendency to solve problems without invoking tools.
182
182
 
183
- **Current state**: some agent-cortex descriptions describe internal mechanics rather than user intent. E.g., `create-task`: *"Use when creating a new task that should be tracked and moved through implementation and human review gates"* — references internal workflow terms a user wouldn't say.
183
+ **Current state**: some agent-cortex descriptions describe internal mechanics rather than user intent. E.g., `create-task`: *"Use when starting tracked work with implementation and review gates — 'create a task for this', 'track this end-to-end', or 'get this into the ralph pipeline'."* — rewritten to add quoted user triggers (per this rec), though it still leans on internal jargon like "ralph pipeline" a user wouldn't say.
184
184
 
185
185
  **Recommendation**: Rewrite descriptions to lead with user-vocabulary triggers. Include 2–4 quoted phrases or scenario patterns a user might say:
186
186
 
@@ -219,7 +219,7 @@ argument-hint: "Describe the bug or paste the error message"
219
219
 
220
220
  ### Rec 10 — Fix prd-to-epics Missing SKILL.md
221
221
 
222
- **Current state**: `skills/prd-to-epics/` directory exists in the repo but has no `SKILL.md`. It cannot be invoked.
222
+ **Current state**: Resolved in #16 — the broken `skills/prd-to-epics/` directory was removed; epic/task decomposition is consolidated into the `prd-to-tasks` skill (see CHANGELOG 1.0.0).
223
223
 
224
224
  **Recommendation**: Either write the missing `SKILL.md` or remove the directory. This is a straightforward bug.
225
225
 
@@ -110,7 +110,7 @@ REFERENCE.md" — keeping SKILL.md as the primary working document rather than g
110
110
  | Verification checklists | Planned |
111
111
  | Concrete examples | Planned |
112
112
  | Reconsider 100-line limit | Done (raised to 150) |
113
- | ASCII workflow diagrams | Not yet |
113
+ | ASCII workflow diagrams | Done (ASCII-only style guide in skill-anatomy.md) |
114
114
  | Cross-references between skills | Planned |
115
115
  | Consistent skeleton template | Partially adopted |
116
116
 
@@ -57,6 +57,23 @@ For each optional section, define **what it is**, include an **example snippet**
57
57
  | `Phase-gate checklists` | Mid-workflow gates that must pass before proceeding | `- [ ] Phase 1 complete: baseline tests captured.` | Multi-phase workflows (3+ distinct phases) | Linear single-phase tasks |
58
58
  | `Cross-skill references` | Explicit delegation to another skill for a real sub-task | `- Run \\`review-security\\` before committing.` | Another skill owns a sub-domain better than restating it | Self-contained skills |
59
59
  | `Examples` | Full worked sample of expected outcome/output | `- Input: ... Output: ...` | Any non-trivial output contract | Trivially self-evident skills |
60
+ | `Workflow diagrams` | Inline ASCII diagram showing step flow and decision points | Fenced block of `[Box]`, `|`, `-`, `>`, `v` arrows, e.g. `[Start] --> [Validate] --> [Run]` | Multi-step or branching workflows where a glanceable flow improves comprehension | Simple linear workflows already clear from numbered steps |
61
+
62
+ ## Workflow diagram style guide
63
+
64
+ When a skill includes a workflow diagram, follow these rules so it renders
65
+ reliably in any CLI:
66
+
67
+ - **ASCII only** — never Mermaid, images, or unicode box-drawing characters.
68
+ Keep to `|`, `-`, `>`, `+`, and `v`.
69
+ - **Fenced block** — wrap the diagram in a fenced code block so monospace is
70
+ preserved.
71
+ - **At most 80 characters per line** — narrow enough for terminal-width readers.
72
+ - **Short box labels** — imperative verbs or nouns (e.g. `[Validate scope]`),
73
+ never full sentences.
74
+ - **Read left-to-right, top-down** — prefer one branch level over dense joins.
75
+ - **Mirror the numbered workflow** — the diagram is a glanceable summary of the
76
+ `## Workflow` steps, never a source of truth that contradicts them.
60
77
 
61
78
  ## Section ordering
62
79
 
@@ -92,6 +109,7 @@ A skill passes anatomy review when:
92
109
  - [ ] Tool references use `{{TOOL:name}}` tokens, not hardcoded tool names.
93
110
  - [ ] Required sections exist and follow canonical ordering.
94
111
  - [ ] Optional sections (if present) include what/when/example guidance.
112
+ - [ ] Workflow diagrams (if present) are ASCII-only and follow the workflow diagram style guide.
95
113
  - [ ] Verification checklist contains concrete, machine-checkable items.
96
114
  - [ ] Supporting docs are split by purpose when `SKILL.md` would exceed ~150 lines.
97
115
 
@@ -119,8 +137,26 @@ disable-model-invocation: false
119
137
  ## Workflow
120
138
 
121
139
  1. Gather required context and validate scope boundaries.
122
- 2. Execute deterministic steps in order, recording key decisions.
123
- 3. Produce the required output artifact or terminal response.
140
+ 2. Reject early with the reason when validation fails; otherwise continue.
141
+ 3. Execute deterministic steps in order, recording key decisions.
142
+ 4. Produce the required output artifact or terminal response.
143
+
144
+ Workflow at a glance (include only when the flow branches):
145
+
146
+ [Trigger]
147
+ |
148
+ v
149
+ [Gather context]
150
+ |
151
+ v
152
+ [Validate scope] --(invalid)--> [Reject early]
153
+ |
154
+ (valid)
155
+ v
156
+ [Run deterministic steps]
157
+ |
158
+ v
159
+ [Produce output artifact]
124
160
 
125
161
  ## Red Flags
126
162
 
@@ -0,0 +1,132 @@
1
+ # Technical Direction: Execution-Label Migration Tooling (two-axis model)
2
+
3
+ ## Problem and target outcome
4
+
5
+ - Migrate all beads workspaces from `implementation-type:afk|hitl` to `execution:afk|hitl`
6
+ (strict rename, agent-agnostic) as part of the two-axis workflow model epic.
7
+ - Provide a repo-agnostic tool + skill any workspace can run, and a single-package.json
8
+ script tooling story across the agent-cortex repo.
9
+
10
+ ## Current-state constraints
11
+
12
+ - bd CLI is a global binary (`~/.local/bin/bd`); label ops: `bd label list-all [--json]`,
13
+ `bd list --label-pattern/regex [--json]`, `bd label add/remove` (batch multiple ids).
14
+ - `bd list` supports `--label-pattern`/`--json`; `bd count` does NOT support `--label-pattern`.
15
+ - `bd ready --label-pattern 'stage:*'` works; `bd ready -l execution:afk` verified.
16
+ - 3 script packages exist with per-skill `package.json` + own `pnpm-workspace.yaml` +
17
+ own `pnpm-lock.yaml` (create-task, validate-agent-dir, run-pipeline-stage). They are NOT
18
+ members of the root workspace (root glob `skills/*/scripts` does not match the nested
19
+ `skills/<domain>/<name>/scripts`; root lockfile has zero refs to them).
20
+ - All 3 scripts are stdlib-only (`node:child_process`, `node:fs`, `node:path`, `node:url`).
21
+ - Extensions (`extensions/skill-stats`) have real peer deps on `@earendil-works/pi-coding-agent`
22
+ — they remain workspace members with their own package.json.
23
+ - `node_modules` gitignored; per-skill modules are installed manually today — a fragility in
24
+ fresh clones and PI-installed contexts.
25
+
26
+ ## Decisions (locked in planning session)
27
+
28
+ ### D1: Script runtime — all skill scripts become zero-dep `.mjs` with `// @ts-check`
29
+
30
+ - Every script in `skills/*/*/scripts/` runs with bare `node` (built-ins + shelling out to
31
+ `bd`). No tsx, no node_modules at runtime, no skill-scripts-dir resolution problem in any
32
+ installed context.
33
+ - Build-in-publish (Option B) rejected: contradicts the repo's no-compilation PI package
34
+ model; 3 tiny stdlib-only scripts do not justify dist artifacts.
35
+ - Root keeps `tsx`/`typescript`/`@types/node` for extensions and for `tsc --noEmit`
36
+ typechecking of `.mjs` via `checkJs`.
37
+
38
+ ### D2: Single package.json (root workspace consolidation)
39
+
40
+ - Fold the 3 per-skill script packages into the root workspace: delete their `package.json`,
41
+ `pnpm-workspace.yaml`, `pnpm-lock.yaml`; convert scripts to `.mjs` (D1); root
42
+ `package.json` owns typecheck/test wiring.
43
+ - Extensions stay as workspace members (`extensions/*` glob) — their package.jsons are
44
+ semantically meaningful (peer deps on the pi runtime).
45
+ - Fresh-clone/installed-context verification: `pnpm install` at root then
46
+ `pnpm -r typecheck` + `pnpm -r test` (or root `node --test` wiring) cover all scripts.
47
+
48
+ ### D3: Migration tool shape
49
+
50
+ - Script: `skills/productivity/migration/scripts/rename-execution-labels.mjs` — zero-dep,
51
+ `// @ts-check` + JSDoc.
52
+ - Behavior:
53
+ 1. Enumerate: `bd list --label-pattern 'implementation-type:*' --json`.
54
+ 2. Group bead ids by label value (afk | hitl). A bead carrying both gets both execution labels.
55
+ 3. Dry-run (default): print per-bead plan + summary counts; zero writes; exit 0.
56
+ 4. Apply (`--apply`): batched `bd label add execution:<v> <ids...>` then
57
+ `bd label remove implementation-type:<v> <ids...>` per group.
58
+ 5. Idempotent: re-run finds zero targets → no-op.
59
+ 6. Fail loudly if `bd` unavailable or any subcommand errors.
60
+ - Scope: rename ALL beads carrying `implementation-type:*` (including closed) — the F7
61
+ acceptance "zero implementation-type:* labels remain" requires it, and bd label history
62
+ tolerates it.
63
+
64
+ ### D4: Migration skill
65
+
66
+ - Location: `skills/productivity/migration/SKILL.md` (productivity = workflow tooling).
67
+ - Docs the full sequence for any workspace: preflight (count via `bd label list-all`),
68
+ dry-run, apply, post-checks (grep stale refs; `bd ready -l execution:afk` smoke), plus
69
+ pointers to idea-file migration (F2) and skill/doc updates (F3/F4/F5/F6).
70
+ - Repo-agnostic: no agent-cortex-specific paths in the runnable steps.
71
+
72
+ ### D6: npm packaging hygiene (no build step — F9)
73
+
74
+ - The npm tarball currently ships the whole repo, including `.beads/` (the live task DB),
75
+ `.github/`, `.changeset/`, `.junie/`, tests, and token-map.json. No `.npmignore`, no
76
+ `files` field.
77
+ - Rejected: a compile build. Scripts are zero-dep `.mjs` (D1) — nothing to transpile;
78
+ extensions are TS by pi contract (`index.ts` entrypoint, loader transpiles; the pi runtime
79
+ import is type-only). Root build script is deliberately a no-op.
80
+ - Chosen: (1) `files` whitelist in package.json — ship only runtime assets (skills/, agents/,
81
+ extensions/, bin/, lib/, AGENTS.md, plugin.json, README, LICENSE); (2) `prepublishOnly`
82
+ gate running `pnpm typecheck && pnpm test`; (3) wire `test/cli.test.mjs` into root `test`.
83
+ - Revisit trigger: if an extension ever gains real runtime deps, bundle that extension with
84
+ esbuild at prepack — not a repo-wide build.
85
+
86
+ ### D5: Knocked-on invocation changes (F4/F5)
87
+
88
+ - ralph init step 9: `pnpm --prefix skills/workflow/run-pipeline-stage/scripts exec tsx
89
+ generate-progress.ts` → `node <abs>/generate-progress.mjs`.
90
+ - create-task chore creation: `<skill-scripts>/node_modules/.bin/tsx create-chores.ts` →
91
+ `node <abs>/create-chores.mjs`.
92
+
93
+ ## Options considered
94
+
95
+ ### Option A: all scripts zero-dep `.mjs` (chosen)
96
+ - Pros: node-only everywhere; no install step; kills resolution problem; `node --test` native.
97
+ - Cons: JSDoc typing instead of TS syntax; small convention shift (`.ts` → `.mjs`).
98
+
99
+ ### Option B: keep `.ts`, build in publish pipeline
100
+ - Pros: full TS syntax.
101
+ - Cons: compile step + dist artifacts contradict no-build PI package; installed contexts must
102
+ ship/install built output; over-engineered for 3 stdlib-only scripts.
103
+
104
+ ### Option C: per-skill tsx packages (status quo, rejected)
105
+ - Pros: existing pattern.
106
+ - Cons: 3 orphaned mini-workspaces, manual install fragility, resolution doc dance
107
+ (`<skill-scripts>/node_modules/.bin/tsx`), contradicts single-package.json preference.
108
+
109
+ ## Tradeoffs accepted
110
+
111
+ - Scripts use JSDoc annotations where types matter; `tsc --noEmit` with `checkJs` keeps a
112
+ typecheck gate at root.
113
+ - Closed beads get their history labels renamed — accepted for a clean label space.
114
+
115
+ ## Validation plan
116
+
117
+ - F1-T3: scratch beads workspace (temp dir): create test beads with both labels + a dep pair,
118
+ dry-run (assert zero writes), apply, verify rename, re-run idempotence, `bd ready -l
119
+ execution:afk` smoke, then run the migration skill end-to-end once.
120
+ - F8-T3: fresh clone (or temp copy) + `pnpm install` at root; `pnpm -r typecheck` and
121
+ `pnpm -r test` green; scripts execute via bare `node`.
122
+
123
+ ## Revisit triggers
124
+
125
+ - A future script needs third-party deps at runtime → revisit D1 (either root dep or
126
+ per-package re-introduction).
127
+ - New extension with conflicting devDeps → revisit D2 membership rules.
128
+
129
+ ## References
130
+
131
+ - Code evidence: `skills/*/scripts/package.json` (3 orphaned), root `package.json` +
132
+ `pnpm-workspace.yaml`, bd CLI help (`bd list`, `bd label`), epic `agnt-ctx-s5xd` notes.
@@ -34,8 +34,9 @@ contract:
34
34
  1. `{{SECTION:name}}` tokens are replaced with the agent's `pi/<name>.md`.
35
35
  2. `{{TOOL:key}}` / `{{PATH:key}}` tokens are substituted against the pi column
36
36
  of `token-map.json`, with the plugin root resolved from the actual extension
37
- location. Tools without a PI equivalent (`ask_user`, `skill`) are omitted
38
- from the tool set.
37
+ location. Tools map through that column (e.g. `ask_user` → `ask_questions`,
38
+ `skill` → `read`); a tool with a null mapping — any future one — is omitted
39
+ from the tool set with a warning.
39
40
 
40
41
  Flat `agents/*.agent.md` files are parsed as a fallback when an agent has no
41
42
  composable directory — they are generated output (scripts/build-copilot-agents.mjs)
@@ -137,8 +137,8 @@ describe("discoverAgents (composable source format)", () => {
137
137
 
138
138
  // {{TOOL:bash}} → bash.
139
139
  assert.match(plan.prompt, /Run with bash\./);
140
- // {{TOOL:ask_user}} → null for pi — token dropped (dangling prose is the author's
141
- // responsibility per token-map.README), not left as literal token syntax.
140
+ // {{TOOL:ask_user}} → ask_questions (pi's native structured-questions tool).
141
+ assert.match(plan.prompt, /Ask the user with ask_questions\./);
142
142
  assert.doesNotMatch(plan.prompt, /\{\{TOOL:ask_user\}\}/);
143
143
  // Relative {{PATH:...}} resolves against the actual package root (dir of agents/).
144
144
  assert.match(plan.prompt, new RegExp(`Read the plan skill at ${escapeRegExp(fx.dir)}/skills/workflow/plan/SKILL\\.md\\.`));
@@ -147,7 +147,7 @@ describe("discoverAgents (composable source format)", () => {
147
147
  assert.doesNotMatch(plan.prompt, /\{\{/);
148
148
  });
149
149
 
150
- it("drops null-mapped tools (ask_user, skill) from the PI tool set", () => {
150
+ it("maps ask_user → ask_questions and skill → read into the PI tool set", () => {
151
151
  const fx = track(makeFixture());
152
152
  writeComposableAgent(fx, "plan", {
153
153
  frontmatter: {
@@ -159,8 +159,30 @@ describe("discoverAgents (composable source format)", () => {
159
159
 
160
160
  const agents = discoverAgents(fx.agentsDir, REAL_TOKEN_MAP);
161
161
  assert.equal(agents.length, 1);
162
- // ask_user → null (omitted), skill → null (omitted), web_fetch → fetch_content.
163
- assert.deepEqual(agents[0].tools, ["bash", "fetch_content"]);
162
+ // ask_user → ask_questions, skill → read (pi reads skill files), web_fetch → fetch_content.
163
+ assert.deepEqual(agents[0].tools, ["ask_questions", "bash", "fetch_content", "read"]);
164
+ });
165
+
166
+ it("warns and omits a tool whose pi mapping is null", () => {
167
+ const fx = track(makeFixture());
168
+ const NULL_MAP = {
169
+ tools: { ask_user: { copilot: "ask_user", claude: "AskUserQuestion", pi: null } },
170
+ paths: {},
171
+ } as TokenMap;
172
+ writeComposableAgent(fx, "plan", {
173
+ frontmatter: {
174
+ name: "agent-cortex:plan",
175
+ description: "Planner",
176
+ tools: ["ask_user", "bash"],
177
+ },
178
+ });
179
+
180
+ const agents = discoverAgents(fx.agentsDir, NULL_MAP);
181
+ assert.equal(agents.length, 1);
182
+ // With a null pi column the entry is omitted (and warned about), per the contract.
183
+ // (bash survives so the result is observable — an all-dropped list would be
184
+ // normalised to DEFAULT_TOOLS, which is "no restriction", not the omit result.)
185
+ assert.deepEqual(agents[0].tools, ["bash"]);
164
186
  });
165
187
 
166
188
  it("uses the built-in fallback map when token-map.json is unavailable", () => {
@@ -230,8 +252,9 @@ describe("loadTokenMap", () => {
230
252
  it("reads the real token-map.json at the package root", () => {
231
253
  assert.ok(REAL_TOKEN_MAP);
232
254
  assert.equal(typeof REAL_TOKEN_MAP?.tools?.bash?.pi, "string");
233
- // ask_user has no PI equivalent per the contract — must stay null.
234
- assert.equal(REAL_TOKEN_MAP?.tools?.ask_user?.pi, null);
255
+ // ask_user → ask_questions (pi's native structured-questions tool); skill → read.
256
+ assert.equal(REAL_TOKEN_MAP?.tools?.ask_user?.pi, "ask_questions");
257
+ assert.equal(REAL_TOKEN_MAP?.tools?.skill?.pi, "read");
235
258
  assert.ok(REAL_TOKEN_MAP?.paths?.agents_dir);
236
259
  });
237
260
  });
@@ -120,7 +120,7 @@ function prettifyId(id: string): string {
120
120
  * Translate a frontmatter tool list into PI tool names.
121
121
  *
122
122
  * With token-map.json: canonical keys are translated via the pi column; a null
123
- * mapping means PI has no equivalent (ask_user, skill) and the entry is omitted;
123
+ * mapping means PI has no equivalent for that tool and the entry is omitted;
124
124
  * a name that is not a canonical key passes through untouched (it may be a
125
125
  * native PI tool such as `fetch_content`). Without the map, the built-in
126
126
  * COPILOT_TO_PI fallback applies and unknown names pass through.
@@ -331,17 +331,18 @@ describe("loadStore / saveStore", () => {
331
331
  const file = join(tmpDir, "test-store.json");
332
332
  const original = freshStore();
333
333
  original.totalTurns = 42;
334
+ const recentTs = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
334
335
  original.skills["test-skill"] = {
335
336
  loadedCount: 5,
336
337
  invokedCount: 2,
337
338
  readCount: 3,
338
- lastUsed: "2026-06-07T12:00:00.000Z",
339
+ lastUsed: recentTs,
339
340
  byProject: {
340
341
  "/project/a": {
341
342
  loadedCount: 3,
342
343
  invokedCount: 1,
343
344
  readCount: 2,
344
- lastUsed: "2026-06-07T12:00:00.000Z",
345
+ lastUsed: recentTs,
345
346
  },
346
347
  },
347
348
  };
package/lib/cli.mjs CHANGED
@@ -5,9 +5,10 @@ const SUPPORTED_HARNESSES = ["copilot", "claude", "pi"];
5
5
  // malformed options surface as `optionError` on the parsed result; the CLI
6
6
  // prints them and exits 1.
7
7
  const INSTALL_OPTIONS = {
8
- "--dry-run": { key: "dryRun" },
8
+ "--dry-run": { key: "dryRun", boolean: true },
9
9
  "--output": { key: "output" },
10
10
  "--plugin-root": { key: "pluginRoot" },
11
+ "--require-register": { key: "requireRegister", boolean: true },
11
12
  };
12
13
 
13
14
  export function parseArgs(argv) {
@@ -34,8 +35,8 @@ export function parseArgs(argv) {
34
35
  if (!spec) {
35
36
  return { command: "install", harness, optionError: `Unknown option "${arg}". Run "agent-cortex --help" for usage.` };
36
37
  }
37
- if (spec.key === "dryRun") {
38
- options.dryRun = true;
38
+ if (spec.boolean) {
39
+ options[spec.key] = true;
39
40
  continue;
40
41
  }
41
42
  if (inlineValue !== null) {
@@ -66,9 +67,26 @@ Commands:
66
67
  install <harness> Install agent-cortex for a harness
67
68
  Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}
68
69
  Options (claude):
69
- --output <dir> Write the plugin subtree to <dir>
70
- (default: <package root>/claude)
71
- --dry-run Print what would be generated without writing
70
+ (no --output) Materialise the plugin into
71
+ ~/.agent-cortex/claude, write the
72
+ marketplace manifest at
73
+ ~/.agent-cortex/.claude-plugin/
74
+ marketplace.json, and register it with
75
+ Claude Code via the "claude plugin" CLI
76
+ (marketplace add → install → update,
77
+ idempotent by state: a re-run is the
78
+ update path, repeat install a no-op;
79
+ requires the plugin CLI, v2+)
80
+ --output <dir> Write the plugin subtree to <dir> only —
81
+ no marketplace manifest or registration
82
+ (default: ~/.agent-cortex/claude)
83
+ --dry-run Print what would be generated/registered
84
+ without writing
85
+ --require-register
86
+ Fail the install when Claude Code
87
+ registration cannot run (missing CLI,
88
+ pre-v2 build); default warns and prints
89
+ the manual registration commands
72
90
  Options (copilot):
73
91
  --output <dir> Write the flat *.agent.md files to <dir>
74
92
  (default: <package root>/agents — the dir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaybeeuu/agent-cortex",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "description": "Personal PI package with custom agents, skills, and extensions",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -28,13 +28,13 @@
28
28
  },
29
29
  "scripts": {
30
30
  "build": "echo 'Build complete — no compilation needed for PI package'",
31
- "lint": "pnpm typecheck",
31
+ "lint": "pnpm typecheck && pnpm check:sync",
32
+ "check:sync": "node scripts/check-no-sync.mjs",
32
33
  "test": "node --test test/*.test.mjs && pnpm -r test",
33
34
  "typecheck": "pnpm -r typecheck",
34
- "build:claude": "node bin/agent-cortex.mjs install claude",
35
35
  "build:copilot": "node scripts/build-copilot-agents.mjs",
36
36
  "changeset": "changeset",
37
- "version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:claude && pnpm build:copilot",
37
+ "version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:copilot",
38
38
  "publish-package": "TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
39
39
  "release": "changeset publish",
40
40
  "postinstall": "bash scripts/install-instructions.sh"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-cortex",
3
3
  "description": "Personal Copilot CLI plugin with custom agents and skills",
4
- "version": "1.38.1",
4
+ "version": "1.40.0",
5
5
  "author": {
6
6
  "name": "jaybeeuu"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  packages:
2
2
  - "."
3
- - "skills/*/scripts"
3
+ - "skills/**/scripts"
4
4
  - "extensions/*"
5
5
  allowBuilds:
6
6
  '@google/genai': false
@@ -27,4 +27,4 @@ import { installCopilot } from "../bin/installers/copilot.mjs";
27
27
 
28
28
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
29
29
 
30
- installCopilot({ root: ROOT, output: join(ROOT, "agents") });
30
+ await installCopilot({ root: ROOT, output: join(ROOT, "agents") });
@@ -0,0 +1,49 @@
1
+ // Repo convention: never block the main thread — no `*Sync` functions
2
+ // (fs/child_process/...) in tooling code. Runs via `pnpm lint` (CI) and
3
+ // locally. Scans the toolchain and test dirs: bin/, scripts/, lib/, hooks/,
4
+ // test/. Skill-owned scripts (skills/*/scripts) and extensions are separate
5
+ // follow-up surfaces tracked outside this check.
6
+ import { readdir, readFile } from "node:fs/promises";
7
+ import { join, relative } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..");
11
+ const SCAN_DIRS = ["bin", "scripts", "lib", "hooks", "test"];
12
+ const EXTENSIONS = new Set([".mjs", ".js", ".ts"]);
13
+ const SELF = "scripts/check-no-sync.mjs";
14
+
15
+ const SYNC_CALL = /\b\w*Sync\s*\(/;
16
+
17
+ /** Recursively collect files under dir, skipping this script and node_modules. */
18
+ async function collect(dir) {
19
+ const out = [];
20
+ const entries = await readdir(dir, { withFileTypes: true });
21
+ for (const e of entries) {
22
+ if (e.name === "node_modules") continue;
23
+ const full = join(dir, e.name);
24
+ if (e.isDirectory()) out.push(...(await collect(full)));
25
+ else if (EXTENSIONS.has(e.name.slice(e.name.lastIndexOf(".")))) out.push(full);
26
+ }
27
+ return out;
28
+ }
29
+
30
+ const offenders = [];
31
+ for (const dir of SCAN_DIRS) {
32
+ const abs = join(ROOT, dir);
33
+ for (const file of await collect(abs)) {
34
+ const rel = relative(ROOT, file);
35
+ if (rel === SELF) continue;
36
+ const src = await readFile(file, "utf-8");
37
+ const hits = src.split("\n").map((line, i) => ({ line: i + 1, text: line })).filter((l) => SYNC_CALL.test(l.text));
38
+ if (hits.length) offenders.push({ rel, hits });
39
+ }
40
+ }
41
+
42
+ if (offenders.length) {
43
+ console.error("✗ Never block the main thread — *Sync function calls found:");
44
+ for (const { rel, hits } of offenders) {
45
+ for (const { line, text } of hits) console.error(` ${rel}:${line}: ${text.trim()}`);
46
+ }
47
+ process.exit(1);
48
+ }
49
+ console.log("✓ no *Sync calls in tooling code (bin/, scripts/, lib/, hooks/, test/)");
@@ -29,9 +29,19 @@
29
29
  //
30
30
  // Zero dependencies so it runs on the CI Node (20) and local Node alike.
31
31
 
32
- import { readFileSync, existsSync } from "node:fs";
32
+ import { readFile, stat } from "node:fs/promises";
33
33
  import { join } from "node:path";
34
34
 
35
+ /** True when a path exists (stat succeeds). */
36
+ async function pathExists(p) {
37
+ try {
38
+ await stat(p);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
35
45
  // ─── Token substitution ──────────────────────────────────────────────────────
36
46
 
37
47
  const TOOL_RE = /\{\{TOOL:([^}]+)\}\}/g;
@@ -92,22 +102,30 @@ export function substituteTokens(content, harness, tokenMap, options = {}) {
92
102
  * @param {string} harness Harness id: "copilot" | "claude" | "pi"
93
103
  * @returns {{ name: string, description: string, tools: string[], model?: string, argumentHint?: string, body: string }}
94
104
  */
95
- export function composeAgent(root, name, harness, options = {}) {
105
+ export async function composeAgent(root, name, harness, options = {}) {
96
106
  const { dropNullTools = false, pluginRoot, resolveRelativePaths = false, warn = noop } = options;
97
107
  const agentDir = join(root, "agents", name);
98
- const frontmatter = parseFrontmatter(join(agentDir, harness, "frontmatter.json"), name, harness);
99
- const tokenMap = loadTokenMap(root);
108
+ const frontmatter = await parseFrontmatter(join(agentDir, harness, "frontmatter.json"), name, harness);
109
+ const tokenMap = await loadTokenMap(root);
100
110
 
101
- let body = readFileSync(join(agentDir, "agent.md"), "utf-8");
111
+ let body = await readFile(join(agentDir, "agent.md"), "utf-8");
102
112
 
103
- // 1. {{SECTION:name}} → <harness>/<name>.md (may itself contain TOOL/PATH tokens)
104
- body = body.replace(/\{\{SECTION:([^}]+)\}\}/g, (_token, section) => {
113
+ // 1. {{SECTION:name}} → <harness>/<name>.md (may itself contain TOOL/PATH
114
+ // tokens). Single pass over the original body — inserted section content
115
+ // is never rescanned (same semantics as the sync replace()).
116
+ const sectionRe = /\{\{SECTION:([^}]+)\}\}/g;
117
+ let out = "";
118
+ let last = 0;
119
+ for (let m = sectionRe.exec(body); m; m = sectionRe.exec(body)) {
120
+ const section = m[1];
105
121
  const sectionPath = join(agentDir, harness, `${section}.md`);
106
- if (!existsSync(sectionPath)) {
122
+ if (!(await pathExists(sectionPath))) {
107
123
  throw new Error(`agent "${name}": missing section "${section}" for harness "${harness}" (${sectionPath})`);
108
124
  }
109
- return readFileSync(sectionPath, "utf-8").trim();
110
- });
125
+ out += body.slice(last, m.index) + (await readFile(sectionPath, "utf-8")).trim();
126
+ last = m.index + m[0].length;
127
+ }
128
+ body = out + body.slice(last);
111
129
 
112
130
  // 2. + 3. TOOL/PATH substitution (contextualised for error messages)
113
131
  body = substituteTokens(body, harness, tokenMap, {
@@ -127,8 +145,8 @@ const REQUIRED_FIELDS = ["name", "description", "tools"];
127
145
  const KNOWN_OPTIONAL_FIELDS = ["model", "argumentHint"];
128
146
 
129
147
  /** Parse and schema-check a harness frontmatter.json (strict, per agents/README.md). */
130
- function parseFrontmatter(path, name, harness) {
131
- const raw = readFileSync(path, "utf-8");
148
+ async function parseFrontmatter(path, name, harness) {
149
+ const raw = await readFile(path, "utf-8");
132
150
  let fm;
133
151
  try {
134
152
  fm = JSON.parse(raw);
@@ -225,11 +243,11 @@ export function translateToolList(tools, tokenMap, harness, { warn = noop } = {}
225
243
  // ─── Token map ───────────────────────────────────────────────────────────────
226
244
 
227
245
  /** Load token-map.json from the package root; missing/incomplete maps hard-fail. */
228
- export function loadTokenMap(root) {
246
+ export async function loadTokenMap(root) {
229
247
  const path = join(root, "token-map.json");
230
248
  let raw;
231
249
  try {
232
- raw = readFileSync(path, "utf-8");
250
+ raw = await readFile(path, "utf-8");
233
251
  } catch {
234
252
  throw new Error(`missing ${path} — the composer requires token-map.json`);
235
253
  }