@jaybeeuu/agent-cortex 2.0.0 → 2.2.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 (183) hide show
  1. package/CHANGELOG.md +437 -0
  2. package/README.md +46 -15
  3. package/agents/README.md +22 -10
  4. package/agents/plan/agent.md +2 -2
  5. package/agents/ralph/agent.md +128 -91
  6. package/agents/ralph/claude/frontmatter.json +3 -3
  7. package/agents/ralph/claude/wake.md +46 -0
  8. package/agents/ralph/pi/{polling.md → wake.md} +35 -20
  9. package/agents/ralph-plan/agent.md +23 -11
  10. package/bin/agent-cortex.mjs +66 -4
  11. package/bin/installers/claude.mjs +262 -77
  12. package/bin/installers/pi.mjs +200 -24
  13. package/extensions/agent-guardrails/README.md +1 -1
  14. package/extensions/agent-guardrails/guardrails.ts +1 -1
  15. package/extensions/agent-guardrails/index.ts +1 -1
  16. package/extensions/agent-modes/discover.test.ts +2 -2
  17. package/extensions/output-style/README.md +58 -0
  18. package/extensions/output-style/index.ts +23 -0
  19. package/extensions/output-style/output-style.md +43 -0
  20. package/extensions/output-style/output-style.test.ts +50 -0
  21. package/extensions/output-style/output-style.ts +32 -0
  22. package/extensions/output-style/package.json +19 -0
  23. package/hooks/claude/scripts/notify.mjs +8 -1
  24. package/lib/cli.mjs +91 -2
  25. package/package.json +29 -4
  26. package/scripts/check-changeset.mjs +4 -0
  27. package/scripts/check-no-sync.mjs +5 -1
  28. package/scripts/check-skill-frontmatter.mjs +21 -0
  29. package/scripts/lib/compose-agent.mjs +69 -9
  30. package/scripts/lib/fs.mjs +10 -2
  31. package/scripts/lib/migrate.mjs +736 -0
  32. package/scripts/lib/release-gates.mjs +21 -1
  33. package/scripts/lib/skill-frontmatter.mjs +152 -0
  34. package/scripts/lib/skill-links.mjs +52 -0
  35. package/scripts/lib/verify-worker-tree.mjs +25 -0
  36. package/scripts/resolve-publish-command.mjs +4 -0
  37. package/skills/engineering/design-an-interface/SKILL.md +3 -3
  38. package/skills/engineering/grill-with-docs/SKILL.md +3 -5
  39. package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
  40. package/skills/engineering/tdd/SKILL.md +5 -7
  41. package/skills/engineering/technical-direction/SKILL.md +5 -5
  42. package/skills/planning/classify-bead/REFERENCE.md +3 -3
  43. package/skills/planning/classify-bead/SKILL.md +14 -9
  44. package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
  45. package/skills/planning/create-task/REFERENCE.md +52 -3
  46. package/skills/planning/create-task/SKILL.md +70 -26
  47. package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
  48. package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
  49. package/skills/planning/flesh-out-idea/SKILL.md +146 -0
  50. package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
  51. package/skills/planning/prd-to-tasks/SKILL.md +22 -16
  52. package/skills/planning/record-idea/FORMAT.md +66 -17
  53. package/skills/planning/record-idea/SKILL.md +48 -24
  54. package/skills/planning/request-refactor-plan/SKILL.md +4 -4
  55. package/skills/planning/spec-to-tickets/SKILL.md +5 -5
  56. package/skills/planning/write-a-prd/SKILL.md +8 -9
  57. package/skills/planning/write-a-ticket/SKILL.md +4 -6
  58. package/skills/productivity/bd-tool/SKILL.md +15 -10
  59. package/skills/productivity/grill-me/SKILL.md +5 -0
  60. package/skills/productivity/init-beads/SKILL.md +3 -3
  61. package/skills/productivity/migration/REFERENCE.md +227 -0
  62. package/skills/productivity/migration/SKILL.md +188 -0
  63. package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
  64. package/skills/productivity/write-a-skill/SKILL.md +5 -5
  65. package/skills/review/maintain-agent-docs/SKILL.md +5 -7
  66. package/skills/review/refactor-skill/SKILL.md +4 -3
  67. package/skills/review/review-security/SKILL.md +3 -5
  68. package/skills/style/style-code/SKILL.md +5 -10
  69. package/skills/style/style-comms/SKILL.md +5 -7
  70. package/skills/style/style-documentation/SKILL.md +5 -6
  71. package/skills/style/style-tests/SKILL.md +5 -7
  72. package/skills/workflow/git-workflow/SKILL.md +28 -4
  73. package/skills/workflow/hitl-collab/SKILL.md +4 -6
  74. package/skills/workflow/plan/SKILL.md +41 -30
  75. package/skills/workflow/ralph/REFERENCE.md +209 -55
  76. package/skills/workflow/ralph/SKILL.md +89 -50
  77. package/skills/workflow/ralph/scripts/pending-gates.mjs +134 -0
  78. package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
  79. package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
  80. package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
  81. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
  82. package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
  83. package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
  84. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
  85. package/token-map.README.md +9 -4
  86. package/token-map.json +13 -6
  87. package/.agents/skills/style-versioning/SKILL.md +0 -172
  88. package/.changeset/README.md +0 -8
  89. package/.changeset/config.json +0 -11
  90. package/.github/workflows/ci.yml +0 -302
  91. package/.junie/guidelines.md +0 -87
  92. package/.junie/mcp/mcp.json +0 -10
  93. package/AGENTS.md +0 -151
  94. package/agents/ralph/claude/polling.md +0 -31
  95. package/agents-native/ralph.md +0 -193
  96. package/docs/claude-hooks.md +0 -128
  97. package/docs/commit-signing.md +0 -231
  98. package/docs/ideas/auto-model-selection.md +0 -75
  99. package/docs/ideas/ban-content-assertion-tests.md +0 -75
  100. package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
  101. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
  102. package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
  103. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
  104. package/docs/ideas/dedicated-review-tests-skill.md +0 -71
  105. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
  106. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
  107. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
  108. package/docs/ideas/extension-manifests.md +0 -121
  109. package/docs/ideas/improve-pi-system-prompt.md +0 -37
  110. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
  111. package/docs/ideas/model-selector.md +0 -40
  112. package/docs/ideas/pi-task-memory.md +0 -34
  113. package/docs/ideas/qa-extension.md +0 -43
  114. package/docs/ideas/record-idea-implementation-questions.md +0 -68
  115. package/docs/ideas/record-ideas-as-beads.md +0 -115
  116. package/docs/ideas/self-flushing-session.md +0 -72
  117. package/docs/ideas/state-in-beads-not-md.md +0 -69
  118. package/docs/ideas/subagent-visibility.md +0 -72
  119. package/docs/ideas/task-session-context.local.md +0 -95
  120. package/docs/ideas/task-session-context.md +0 -89
  121. package/docs/ideas/test-data-creation-helpers.md +0 -92
  122. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
  123. package/docs/ideas/unified-ci-pipeline.md +0 -28
  124. package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
  125. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
  126. package/docs/inspirations.md +0 -33
  127. package/docs/research/disable-model-invocation.md +0 -82
  128. package/docs/research/gas-city-research.md +0 -256
  129. package/docs/research/skill-design-research.md +0 -286
  130. package/docs/research/skill-improvements-analysis.md +0 -117
  131. package/docs/research/skill-patterns-research.md +0 -24
  132. package/docs/skills/skill-anatomy.md +0 -197
  133. package/docs/technical-direction/harness-config.md +0 -177
  134. package/docs/technical-direction/migration-execution-labels.md +0 -177
  135. package/docs/user-preferences.md +0 -17
  136. package/extensions/agent-guardrails/tsconfig.json +0 -12
  137. package/extensions/agent-modes/tsconfig.json +0 -12
  138. package/extensions/session-start/tsconfig.json +0 -12
  139. package/extensions/skill-stats/pnpm-lock.yaml +0 -329
  140. package/extensions/skill-stats/tsconfig.json +0 -12
  141. package/extensions/subagent/agents/planner.md +0 -37
  142. package/extensions/subagent/agents/reviewer.md +0 -35
  143. package/extensions/subagent/agents/scout.md +0 -50
  144. package/extensions/subagent/agents/worker.md +0 -24
  145. package/extensions/subagent/prompts/implement-and-review.md +0 -10
  146. package/extensions/subagent/prompts/implement.md +0 -10
  147. package/extensions/subagent/prompts/scout-and-plan.md +0 -9
  148. package/extensions/subagent/tsconfig.json +0 -12
  149. package/pi/keybindings.json +0 -3
  150. package/pi/settings.json +0 -34
  151. package/pnpm-workspace.yaml +0 -14
  152. package/skills/planning/create-task/scripts/package.json +0 -14
  153. package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
  154. package/skills/planning/create-task/scripts/tsconfig.json +0 -12
  155. package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
  156. package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
  157. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
  158. package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
  159. package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
  160. package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
  161. package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
  162. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
  163. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
  164. package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
  165. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
  166. package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
  167. package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
  168. package/test/classify-bead.test.mjs +0 -277
  169. package/test/claude-installer.test.mjs +0 -308
  170. package/test/cli.test.mjs +0 -314
  171. package/test/create-chores.test.mjs +0 -85
  172. package/test/fake-bd.test.mjs +0 -72
  173. package/test/fs.test.mjs +0 -76
  174. package/test/helpers/fake-bd.mjs +0 -87
  175. package/test/helpers/fake-claude.mjs +0 -152
  176. package/test/helpers/fake-pi.mjs +0 -132
  177. package/test/installer.test.mjs +0 -664
  178. package/test/notify-hook.test.mjs +0 -63
  179. package/test/pi-installer.test.mjs +0 -785
  180. package/test/pi-register.test.mjs +0 -342
  181. package/test/pi-sweep.test.mjs +0 -619
  182. package/test/release-gates.test.mjs +0 -190
  183. package/test/verify-worker-tree.test.mjs +0 -120
@@ -0,0 +1,58 @@
1
+ # Output Style
2
+
3
+ Appends the always-on reply shape to the PI system prompt once per user prompt.
4
+
5
+ This is the second slice of `docs/ideas/improve-pi-system-prompt.md`: the first
6
+ (`agent-guardrails`) stops wasted work; this one stops the answer getting buried in
7
+ it. The extension injects a stable block via the `before_agent_start` hook — which
8
+ fires once per user prompt, before the agent loop — so the rules apply to every
9
+ turn of the run without rewriting Pi's default prompt.
10
+
11
+ ## Rules
12
+
13
+ - **Lead with the action** — the first line is the answer, command, path, or next
14
+ action; multi-step work is numbered; the reply ends with one next step.
15
+ - **Keep the prose short** — no preamble, no recap, no closing offer; five items
16
+ per list at most; one topic per reply.
17
+ - **Write Simplified Technical English (ASD-STE100)** — active voice, 20-word
18
+ sentences for instructions and 25-word sentences for description, verb over
19
+ noun, one word one meaning, literal words, no marketing adjectives, articles
20
+ kept.
21
+ - **Restate the context** — never cite a bead, ticket, phase, or stage by ID alone;
22
+ say what it is in plain words first, and restate state after a gap or topic
23
+ switch.
24
+
25
+ The rules shape the reply, never the work. When a rule would delete the answer,
26
+ the task wins.
27
+
28
+ ## Structure
29
+
30
+ ```
31
+ output-style/
32
+ ├── README.md # This file
33
+ ├── index.ts # Extension entry point — appends on before_agent_start
34
+ ├── output-style.ts # Loads the block + appendOutputStyle composition
35
+ ├── output-style.md # The block text (single source, also shipped to Claude)
36
+ ├── output-style.test.ts # Composition tests
37
+ ├── package.json
38
+ └── tsconfig.json
39
+ ```
40
+
41
+ ## Testing
42
+
43
+ ```bash
44
+ cd extensions/output-style
45
+ node --test --import tsx/esm output-style.test.ts
46
+ ```
47
+
48
+ ## Design notes
49
+
50
+ - The block is a stable constant appended after any chained system-prompt changes,
51
+ so the prompt-cache prefix is preserved.
52
+ - `appendOutputStyle` is idempotent — a prompt that already carries the marker is
53
+ returned unchanged, so a duplicate extension load cannot double the rules.
54
+ - Separate from `agent-guardrails` because it is a different failure: guardrails
55
+ govern *whether* work continues, this governs *how the result is reported*.
56
+ - The prose lives in `output-style.md`, not in the TypeScript, because there is a
57
+ second consumer: `agent-cortex install claude` wraps that file in the managed
58
+ `~/.claude/CLAUDE.md` section. One file, two harnesses, no drift.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Output Style Extension
3
+ *
4
+ * Appends the always-on reply shape to the PI system prompt on every turn: lead
5
+ * with the action, write in Simplified Technical English (ASD-STE100), restate the
6
+ * context the reader cannot remember (the second slice of
7
+ * docs/ideas/improve-pi-system-prompt.md).
8
+ *
9
+ * The block is stable text appended after any chained system-prompt changes, so it
10
+ * keeps the prompt-cache prefix intact while remaining authoritative for the model.
11
+ * See output-style.ts for the rules and appendOutputStyle for composition.
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { appendOutputStyle } from "./output-style.ts";
16
+
17
+ export { OUTPUT_STYLE, OUTPUT_STYLE_MARKER, appendOutputStyle } from "./output-style.ts";
18
+
19
+ export default function outputStyleExtension(pi: ExtensionAPI): void {
20
+ pi.on("before_agent_start", (event) => {
21
+ return { systemPrompt: appendOutputStyle(event.systemPrompt) };
22
+ });
23
+ }
@@ -0,0 +1,43 @@
1
+ ## Output style (agent-cortex)
2
+
3
+ How every reply is written. These rules shape the reply, never the work: do all
4
+ the analysis and tool calls the task needs, then report the result this way. If a
5
+ rule would delete the answer itself, the task wins; the shape stays.
6
+
7
+ ### Lead with the action
8
+
9
+ - The first line is the answer, the command, the path, or the next action. Not
10
+ context, not a plan, not "Let me…".
11
+ - Number multi-step work. One bounded action per step.
12
+ - End with one concrete next step. Omit it when the work is finished.
13
+
14
+ ### Keep the prose short
15
+
16
+ - No preamble ("Great question", "I'll now…"), no recap of work just completed,
17
+ no closing offer ("Let me know if…", "Hope this helps").
18
+ - Five items maximum per list. Rank them; group related items.
19
+ - One topic per reply. Name a tangent in one line at the end, then stop.
20
+
21
+ ### Write Simplified Technical English (ASD-STE100)
22
+
23
+ - Active voice. Imperative for instructions.
24
+ - Sentence limit: 20 words for instructions, 25 for descriptive text. One
25
+ instruction per sentence.
26
+ - Use the verb, not the noun: "check the file", not "perform a check of the file".
27
+ - One word, one meaning: pick the term for a thing, then reuse it.
28
+ - Literal words only. No idioms, metaphors, or figures of speech ("circle back",
29
+ "under the hood", "ballpark").
30
+ - No marketing adjectives or filler ("seamless", "robust", "comprehensive").
31
+ - Keep articles and subjects. Do not compress into fragments that lose meaning.
32
+ - Give times in concrete units ("about 10 minutes"), never "a bit of work".
33
+
34
+ ### Restate the context, never assume it
35
+
36
+ The reader cannot hold earlier turns in working memory.
37
+
38
+ - Never name a bead, ticket, phase, or pipeline stage by ID or number alone. Say
39
+ what it is in plain words first — "the login fix, verify step (bd-142)" — and
40
+ give the ID at most once.
41
+ - After a gap, a topic switch, or a long tool run, restate in one line: what is
42
+ done, what is next.
43
+ - Mark progress concretely: "Step 2 of 4 done: schema updated."
@@ -0,0 +1,50 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { OUTPUT_STYLE, OUTPUT_STYLE_MARKER, appendOutputStyle } from "./output-style.ts";
5
+
6
+ // ─── appendOutputStyle ────────────────────────────────────────────────────────
7
+
8
+ describe("appendOutputStyle", () => {
9
+ it("appends the output-style block after the base prompt", () => {
10
+ const result = appendOutputStyle("You are a helpful agent.");
11
+
12
+ assert.ok(result.startsWith("You are a helpful agent."));
13
+ assert.ok(result.includes(OUTPUT_STYLE));
14
+ });
15
+
16
+ it("preserves a chained base prompt from earlier handlers", () => {
17
+ const base = "Base prompt.\n\n---\nAgent mode instructions.";
18
+ const result = appendOutputStyle(base);
19
+
20
+ assert.ok(result.startsWith(base));
21
+ });
22
+
23
+ it("trims trailing whitespace before appending", () => {
24
+ const result = appendOutputStyle("Base prompt.\n\n");
25
+
26
+ assert.equal(result, `Base prompt.\n\n${OUTPUT_STYLE}`);
27
+ });
28
+
29
+ it("keeps a block appended by another extension", () => {
30
+ const withGuardrails = "Base prompt.\n\n## Operating discipline (agent-cortex)\n\nRules.\n";
31
+ const result = appendOutputStyle(withGuardrails);
32
+
33
+ assert.ok(result.includes("## Operating discipline (agent-cortex)"));
34
+ assert.ok(result.endsWith(OUTPUT_STYLE));
35
+ });
36
+
37
+ it("is idempotent when applied twice", () => {
38
+ const once = appendOutputStyle("Base prompt.");
39
+ const twice = appendOutputStyle(once);
40
+
41
+ assert.equal(twice, once);
42
+ assert.equal(twice.split(OUTPUT_STYLE_MARKER).length - 1, 1);
43
+ });
44
+
45
+ it("returns the prompt unchanged when the output style is already present", () => {
46
+ const existing = `Base prompt.\n\n${OUTPUT_STYLE}`;
47
+
48
+ assert.equal(appendOutputStyle(existing), existing);
49
+ });
50
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Output style for the PI system prompt.
3
+ *
4
+ * The second slice of docs/ideas/improve-pi-system-prompt.md: the reply shape, not
5
+ * the task discipline. Three concerns, in order — lead with the action, write in
6
+ * Simplified Technical English, restate the context the reader cannot remember.
7
+ *
8
+ * The prose lives in output-style.md so one file feeds both harnesses: this
9
+ * extension appends it to the PI system prompt, and the Claude installer wraps it
10
+ * in a managed CLAUDE.md section. It is read once at load, so the appended text is
11
+ * stable across turns and keeps the prompt-cache prefix intact. The marker doubles
12
+ * as an idempotency guard: a chained prompt that already carries the block is
13
+ * returned unchanged.
14
+ */
15
+
16
+ import { readFileSync } from "node:fs";
17
+
18
+ export const OUTPUT_STYLE_MARKER = "## Output style (agent-cortex)";
19
+
20
+ export const OUTPUT_STYLE = readFileSync(new URL("./output-style.md", import.meta.url), "utf-8");
21
+
22
+ /**
23
+ * Append the output-style block to a system prompt.
24
+ *
25
+ * The base prompt is preserved verbatim (minus trailing whitespace) so callers can
26
+ * chain this after other `before_agent_start` handlers. Idempotent: a prompt that
27
+ * already contains the marker is returned unchanged.
28
+ */
29
+ export function appendOutputStyle(systemPrompt: string): string {
30
+ if (systemPrompt.includes(OUTPUT_STYLE_MARKER)) return systemPrompt;
31
+ return `${systemPrompt.trimEnd()}\n\n${OUTPUT_STYLE}`;
32
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@agent-cortex/output-style",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "node --test --import tsx/esm output-style.test.ts",
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "devDependencies": {
11
+ "@earendil-works/pi-coding-agent": "^0.78.1",
12
+ "@types/node": "^22.0.0",
13
+ "tsx": "^4.0.0",
14
+ "typescript": "^5.0.0"
15
+ },
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-coding-agent": "^0.78.1"
18
+ }
19
+ }
@@ -20,6 +20,7 @@ for await (const chunk of process.stdin) {
20
20
  raw += chunk;
21
21
  }
22
22
 
23
+ /** @type {{ message?: unknown, title?: unknown, notification_type?: unknown }} */
23
24
  let payload = {};
24
25
  try {
25
26
  payload = raw.trim() ? JSON.parse(raw) : {};
@@ -36,7 +37,13 @@ const type = typeof payload.notification_type === "string" ? payload.notificatio
36
37
 
37
38
  process.stdout.write(JSON.stringify({ terminalSequence: pickSequence(title, message, type) }) + "\n");
38
39
 
39
- /** Pick an OSC notification escape sequence for the detected terminal. */
40
+ /**
41
+ * Pick an OSC notification escape sequence for the detected terminal.
42
+ * @param {string} title
43
+ * @param {string} message
44
+ * @param {string} type
45
+ * @returns {string}
46
+ */
40
47
  function pickSequence(title, message, type) {
41
48
  const body = message.replace(/\u001b/g, "").replace(/\n/g, " ").trim();
42
49
  const label = type ? `${type}: ${title}` : title;
package/lib/cli.mjs CHANGED
@@ -1,9 +1,28 @@
1
+ /**
2
+ * Result of parsing the CLI argv. Every branch returns `command`; the rest are
3
+ * populated only for the commands that accept them.
4
+ * @typedef {object} ParsedArgs
5
+ * @property {"help" | "version" | "install" | "migrate" | "unknown"} command
6
+ * @property {string | null} [harness]
7
+ * @property {string} [name]
8
+ * @property {string} [optionError]
9
+ * @property {boolean} [dryRun]
10
+ * @property {string} [output]
11
+ * @property {string} [pluginRoot]
12
+ * @property {boolean} [requireRegister]
13
+ * @property {boolean} [apply]
14
+ * @property {string} [dir]
15
+ */
16
+
17
+ /** @typedef {{ key: string, boolean?: boolean }} OptionSpec */
18
+
1
19
  const SUPPORTED_HARNESSES = ["claude", "pi"];
2
20
 
3
21
  // Options accepted by `install <harness>`. `--output`/`--dry-run` are shared by
4
22
  // the claude and pi installers; `--plugin-root` is pi-specific. Unknown or
5
23
  // malformed options surface as `optionError` on the parsed result; the CLI
6
24
  // prints them and exits 1.
25
+ /** @type {Record<string, OptionSpec>} */
7
26
  const INSTALL_OPTIONS = {
8
27
  "--dry-run": { key: "dryRun", boolean: true },
9
28
  "--output": { key: "output" },
@@ -11,11 +30,22 @@ const INSTALL_OPTIONS = {
11
30
  "--require-register": { key: "requireRegister", boolean: true },
12
31
  };
13
32
 
33
+ /**
34
+ * Parse the CLI argv.
35
+ * @param {string[]} argv
36
+ * @returns {ParsedArgs}
37
+ */
14
38
  export function parseArgs(argv) {
15
39
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
16
40
  return { command: "help" };
17
41
  }
18
42
 
43
+ // Like `--help`, the version flags are only recognised as the first argument,
44
+ // so they never shadow a command's own options.
45
+ if (argv[0] === "--version" || argv[0] === "-V") {
46
+ return { command: "version" };
47
+ }
48
+
19
49
  const [command, ...rest] = argv;
20
50
 
21
51
  if (command === "install") {
@@ -26,6 +56,7 @@ export function parseArgs(argv) {
26
56
  i = 1;
27
57
  }
28
58
 
59
+ /** @type {Record<string, string | true>} */
29
60
  const options = {};
30
61
  for (; i < rest.length; i++) {
31
62
  const arg = rest[i];
@@ -57,9 +88,54 @@ export function parseArgs(argv) {
57
88
  return { command: "install", harness, ...options };
58
89
  }
59
90
 
91
+ if (command === "migrate") {
92
+ // Dry run is the default, so `--apply` is the only state-changing flag;
93
+ // `--dry-run` stays accepted for scripts that pass it explicitly. `--dir`
94
+ // points the ideas step at a workspace that keeps its idea files elsewhere.
95
+ let sawApply = false;
96
+ let sawDryRun = false;
97
+ /** @type {string | undefined} */
98
+ let dir;
99
+ for (let i = 0; i < rest.length; i++) {
100
+ const arg = rest[i];
101
+ if (arg === "--help" || arg === "-h") return { command: "help" };
102
+ if (arg === "--apply") {
103
+ sawApply = true;
104
+ continue;
105
+ }
106
+ if (arg === "--dry-run") {
107
+ sawDryRun = true;
108
+ continue;
109
+ }
110
+ // Support both `--dir=<path>` and `--dir <path>`, like install's `--output`.
111
+ const inlineDir = arg.startsWith("--dir=") ? arg.slice("--dir=".length) : null;
112
+ if (inlineDir !== null && inlineDir !== "") {
113
+ dir = inlineDir;
114
+ continue;
115
+ }
116
+ if (arg === "--dir" || inlineDir !== null) {
117
+ const value = rest[i + 1];
118
+ if (inlineDir !== null || value == null || value.startsWith("--")) {
119
+ return { command: "migrate", optionError: `Missing value for "--dir". Run "agent-cortex --help" for usage.` };
120
+ }
121
+ dir = value;
122
+ i += 1;
123
+ continue;
124
+ }
125
+ return { command: "migrate", optionError: `Unknown option "${arg}". Run "agent-cortex --help" for usage.` };
126
+ }
127
+ if (sawApply && sawDryRun) {
128
+ return { command: "migrate", optionError: '"--apply" and "--dry-run" are mutually exclusive.' };
129
+ }
130
+ return dir === undefined ? { command: "migrate", apply: sawApply } : { command: "migrate", apply: sawApply, dir };
131
+ }
132
+
60
133
  return { command: "unknown", name: command };
61
134
  }
62
135
 
136
+ /**
137
+ * @returns {string}
138
+ */
63
139
  export function buildHelpText() {
64
140
  return `Usage: agent-cortex <command> [options]
65
141
 
@@ -125,11 +201,24 @@ Commands:
125
201
  cannot register the staged package
126
202
  (missing from PATH); default warns and
127
203
  prints the manual commands
128
- --help, -h Show this help text`;
204
+ migrate Migrate the current beads workspace to the latest
205
+ agent-cortex migration version, applying versioned steps
206
+ in order (repo-agnostic — works in any beads workspace)
207
+ Options:
208
+ (no option) Dry run — print the pending plan, write nothing
209
+ --apply Apply every pending step, verifying each
210
+ --dir <path> Read idea files from <path> instead of
211
+ docs/ideas (step v2's source)
212
+ --help, -h Show this help text
213
+ --version, -V Print the package version`;
129
214
  }
130
215
 
216
+ /**
217
+ * @param {string | null | undefined} harness
218
+ * @returns {{ ok: true } | { ok: false, error: string }}
219
+ */
131
220
  export function validateHarness(harness) {
132
- if (harness === null) {
221
+ if (harness === null || harness === undefined) {
133
222
  return {
134
223
  ok: false,
135
224
  error: `Missing harness argument. Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaybeeuu/agent-cortex",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Personal PI package with custom agents, skills, and extensions",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -13,12 +13,35 @@
13
13
  "bin": {
14
14
  "agent-cortex": "./bin/agent-cortex.mjs"
15
15
  },
16
+ "files": [
17
+ "bin/",
18
+ "lib/",
19
+ "scripts/",
20
+ "skills/",
21
+ "agents/",
22
+ "extensions/*.ts",
23
+ "extensions/lib/",
24
+ "extensions/*/*.ts",
25
+ "extensions/*/*.md",
26
+ "extensions/*/package.json",
27
+ "extensions/*/agents/",
28
+ "extensions/*/prompts/",
29
+ "hooks/",
30
+ "claude/",
31
+ "claude-extras/",
32
+ "token-map.json",
33
+ "token-map.README.md",
34
+ "README.md",
35
+ "CHANGELOG.md"
36
+ ],
16
37
  "publishConfig": {
17
38
  "access": "public"
18
39
  },
19
40
  "devDependencies": {
20
41
  "@changesets/cli": "^2.31.1",
42
+ "@types/node": "^22.20.1",
21
43
  "tsx": "^4.0.0",
44
+ "typescript": "^5.9.3",
22
45
  "yaml": "^2.0.0"
23
46
  },
24
47
  "pi": {
@@ -35,13 +58,15 @@
35
58
  },
36
59
  "scripts": {
37
60
  "build": "echo 'Build complete — no compilation needed for PI package'",
38
- "lint": "pnpm typecheck && pnpm check:sync",
61
+ "lint": "pnpm typecheck && pnpm check:sync && pnpm check:skills",
39
62
  "check:sync": "node scripts/check-no-sync.mjs",
63
+ "check:skills": "node scripts/check-skill-frontmatter.mjs",
40
64
  "test": "node --test test/*.test.mjs && pnpm -r test",
41
- "typecheck": "pnpm -r typecheck",
65
+ "typecheck": "tsc --noEmit -p tsconfig.json && pnpm -r typecheck",
66
+ "verify": "pnpm typecheck && pnpm test",
42
67
  "changeset": "changeset",
43
68
  "version-packages": "changeset version",
44
- "publish-package": "TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
69
+ "publish-package": "pnpm verify && TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
45
70
  "publish-noop": "echo 'No publish: the merged PR carries the no-publish label.'",
46
71
  "release": "changeset publish"
47
72
  }
@@ -10,6 +10,10 @@ async function readStdin() {
10
10
  return Buffer.concat(chunks).toString("utf8");
11
11
  }
12
12
 
13
+ /**
14
+ * @param {string} raw
15
+ * @returns {string[]}
16
+ */
13
17
  function readLabels(raw) {
14
18
  try {
15
19
  return JSON.parse(raw);
@@ -14,7 +14,11 @@ const SELF = "scripts/check-no-sync.mjs";
14
14
 
15
15
  const SYNC_CALL = /\b\w*Sync\s*\(/;
16
16
 
17
- /** Recursively collect files under dir, skipping this script and node_modules. */
17
+ /**
18
+ * Recursively collect files under dir, skipping this script and node_modules.
19
+ * @param {string} dir
20
+ * @returns {Promise<string[]>}
21
+ */
18
22
  async function collect(dir) {
19
23
  const out = [];
20
24
  const entries = await readdir(dir, { withFileTypes: true });
@@ -0,0 +1,21 @@
1
+ // Repo convention: every SKILL.md frontmatter must parse and declare a name and
2
+ // description, because PI silently drops a skill whose frontmatter throws. Runs
3
+ // via `pnpm lint` (CI) and locally. Scans the repository root, so both skills/
4
+ // and repo-local .agents/skills/ are covered.
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { checkSkills } from "./lib/skill-frontmatter.mjs";
8
+
9
+ const ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..");
10
+
11
+ const { checked, failures } = await checkSkills({ root: ROOT });
12
+
13
+ if (failures.length > 0) {
14
+ console.error("✗ Invalid SKILL.md frontmatter — PI would silently drop these skills:");
15
+ for (const { file, errors } of failures) {
16
+ for (const error of errors) console.error(` ${file}: ${error}`);
17
+ }
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log(`✓ SKILL.md frontmatter valid (${checked} files)`);
@@ -32,7 +32,49 @@
32
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). */
35
+ /** @typedef {{ base: string, relative: string }} PathSpec */
36
+
37
+ /**
38
+ * Parsed token-map.json. `tools` maps a canonical tool key to its per-harness
39
+ * name (null = the harness has no equivalent); `paths` maps a path key to its
40
+ * per-harness spec (a literal string, or a {base, relative} pair resolved
41
+ * recursively through the same table).
42
+ * @typedef {object} TokenMap
43
+ * @property {number} [version]
44
+ * @property {Record<string, Record<string, string | null>>} tools
45
+ * @property {Record<string, Record<string, string | PathSpec>>} paths
46
+ */
47
+
48
+ /**
49
+ * Options shared by the composer entry points.
50
+ * @typedef {object} ComposeOptions
51
+ * @property {boolean} [dropNullTools]
52
+ * @property {string} [pluginRoot]
53
+ * @property {boolean} [resolveRelativePaths]
54
+ * @property {(message: string) => void} [warn]
55
+ * @property {string} [context]
56
+ */
57
+
58
+ /**
59
+ * Parsed and schema-checked harness frontmatter.json.
60
+ * @typedef {object} AgentFrontmatter
61
+ * @property {string} name
62
+ * @property {string} description
63
+ * @property {string[]} tools
64
+ * @property {string} [model]
65
+ * @property {string} [argumentHint]
66
+ */
67
+
68
+ /**
69
+ * A composed agent: its frontmatter plus the substituted body.
70
+ * @typedef {AgentFrontmatter & { body: string }} ComposedAgent
71
+ */
72
+
73
+ /**
74
+ * True when a path exists (stat succeeds).
75
+ * @param {string} p
76
+ * @returns {Promise<boolean>}
77
+ */
36
78
  async function pathExists(p) {
37
79
  try {
38
80
  await stat(p);
@@ -47,6 +89,7 @@ async function pathExists(p) {
47
89
  const TOOL_RE = /\{\{TOOL:([^}]+)\}\}/g;
48
90
  const PATH_RE = /\{\{PATH:([^}]+)\}\}/g;
49
91
 
92
+ /** @returns {void} */
50
93
  function noop() {}
51
94
 
52
95
  /**
@@ -62,8 +105,8 @@ function noop() {}
62
105
  *
63
106
  * @param {string} content Source text containing {{TOOL:...}}/{{PATH:...}} tokens
64
107
  * @param {string} harness Harness id: "claude" | "pi"
65
- * @param {object} tokenMap Parsed token-map.json (from loadTokenMap)
66
- * @param {object} options { dropNullTools, pluginRoot, resolveRelativePaths, warn, context }
108
+ * @param {TokenMap} tokenMap Parsed token-map.json (from loadTokenMap)
109
+ * @param {ComposeOptions} [options] { dropNullTools, pluginRoot, resolveRelativePaths, warn, context }
67
110
  * @returns {string}
68
111
  */
69
112
  export function substituteTokens(content, harness, tokenMap, options = {}) {
@@ -100,7 +143,8 @@ export function substituteTokens(content, harness, tokenMap, options = {}) {
100
143
  * @param {string} root Package root (contains agents/ and token-map.json)
101
144
  * @param {string} name Agent directory name (e.g. "ralph", "plan")
102
145
  * @param {string} harness Harness id: "claude" | "pi"
103
- * @returns {{ name: string, description: string, tools: string[], model?: string, argumentHint?: string, body: string }}
146
+ * @param {ComposeOptions} [options]
147
+ * @returns {Promise<ComposedAgent>}
104
148
  */
105
149
  export async function composeAgent(root, name, harness, options = {}) {
106
150
  const { dropNullTools = false, pluginRoot, resolveRelativePaths = false, warn = noop } = options;
@@ -144,14 +188,21 @@ export async function composeAgent(root, name, harness, options = {}) {
144
188
  const REQUIRED_FIELDS = ["name", "description", "tools"];
145
189
  const KNOWN_OPTIONAL_FIELDS = ["model", "argumentHint"];
146
190
 
147
- /** Parse and schema-check a harness frontmatter.json (strict, per agents/README.md). */
191
+ /**
192
+ * Parse and schema-check a harness frontmatter.json (strict, per agents/README.md).
193
+ * @param {string} path
194
+ * @param {string} name
195
+ * @param {string} harness
196
+ * @returns {Promise<AgentFrontmatter>}
197
+ */
148
198
  async function parseFrontmatter(path, name, harness) {
149
199
  const raw = await readFile(path, "utf-8");
200
+ /** @type {Record<string, unknown>} */
150
201
  let fm;
151
202
  try {
152
203
  fm = JSON.parse(raw);
153
204
  } catch (err) {
154
- throw new Error(`agent "${name}": invalid JSON in ${path}: ${err.message}`);
205
+ throw new Error(`agent "${name}": invalid JSON in ${path}: ${/** @type {Error} */ (err).message}`);
155
206
  }
156
207
  if (typeof fm !== "object" || fm === null) {
157
208
  throw new Error(`agent "${name}": ${path} must be a JSON object`);
@@ -188,6 +239,11 @@ async function parseFrontmatter(path, name, harness) {
188
239
  * With the installer options (`pluginRoot`, `resolveRelativePaths`) the
189
240
  * plugin_root base and bare relative paths resolve against the actual install
190
241
  * layout instead.
242
+ * @param {string} arg
243
+ * @param {string} harness
244
+ * @param {TokenMap} tokenMap
245
+ * @param {ComposeOptions} [opts]
246
+ * @returns {string}
191
247
  */
192
248
  function resolvePath(arg, harness, tokenMap, opts = {}) {
193
249
  const entry = tokenMap.paths?.[arg];
@@ -217,9 +273,9 @@ function resolvePath(arg, harness, tokenMap, opts = {}) {
217
273
  * translation in extensions/agent-modes/discover.ts.
218
274
  *
219
275
  * @param {string[]} tools Canonical tool names from a harness frontmatter.json
220
- * @param {object} tokenMap Parsed token-map.json (from loadTokenMap)
276
+ * @param {TokenMap} tokenMap Parsed token-map.json (from loadTokenMap)
221
277
  * @param {string} harness Harness id: "claude" | "pi"
222
- * @param {object} options { warn }
278
+ * @param {ComposeOptions} [options] { warn }
223
279
  * @returns {string[]}
224
280
  */
225
281
  export function translateToolList(tools, tokenMap, harness, { warn = noop } = {}) {
@@ -242,7 +298,11 @@ export function translateToolList(tools, tokenMap, harness, { warn = noop } = {}
242
298
 
243
299
  // ─── Token map ───────────────────────────────────────────────────────────────
244
300
 
245
- /** Load token-map.json from the package root; missing/incomplete maps hard-fail. */
301
+ /**
302
+ * Load token-map.json from the package root; missing/incomplete maps hard-fail.
303
+ * @param {string} root
304
+ * @returns {Promise<TokenMap>}
305
+ */
246
306
  export async function loadTokenMap(root) {
247
307
  const path = join(root, "token-map.json");
248
308
  let raw;
@@ -12,7 +12,11 @@
12
12
 
13
13
  import { stat } from "node:fs/promises";
14
14
 
15
- /** True when `p` exists and is a regular file; false on any error. */
15
+ /**
16
+ * True when `p` exists and is a regular file; false on any error.
17
+ * @param {string} p
18
+ * @returns {Promise<boolean>}
19
+ */
16
20
  export async function isFile(p) {
17
21
  try {
18
22
  return (await stat(p)).isFile();
@@ -21,7 +25,11 @@ export async function isFile(p) {
21
25
  }
22
26
  }
23
27
 
24
- /** True when `p` exists and is a directory; false on any error. */
28
+ /**
29
+ * True when `p` exists and is a directory; false on any error.
30
+ * @param {string} p
31
+ * @returns {Promise<boolean>}
32
+ */
25
33
  export async function isDirectory(p) {
26
34
  try {
27
35
  return (await stat(p)).isDirectory();