@jaybeeuu/agent-cortex 1.40.0 → 1.41.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 (69) hide show
  1. package/AGENTS.md +21 -8
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +78 -38
  4. package/agents/README.md +5 -3
  5. package/bin/agent-cortex.mjs +36 -4
  6. package/bin/installers/claude.mjs +171 -21
  7. package/bin/installers/copilot.mjs +2 -17
  8. package/bin/installers/pi.mjs +261 -22
  9. package/claude/settings.json +4 -0
  10. package/claude-extras/scripts/statusline-command.sh +32 -4
  11. package/docs/claude-hooks.md +12 -0
  12. package/docs/ideas/ban-content-assertion-tests.md +73 -0
  13. package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
  14. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
  15. package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
  16. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
  17. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
  18. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
  19. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
  20. package/docs/ideas/extension-manifests.md +55 -14
  21. package/docs/ideas/improve-pi-system-prompt.md +5 -2
  22. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
  23. package/docs/ideas/record-idea-implementation-questions.md +68 -0
  24. package/docs/ideas/self-flushing-session.md +72 -0
  25. package/docs/ideas/state-in-beads-not-md.md +69 -0
  26. package/docs/ideas/task-session-context.local.md +95 -0
  27. package/docs/ideas/test-data-creation-helpers.md +92 -0
  28. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
  29. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
  30. package/docs/technical-direction/harness-config.md +161 -0
  31. package/docs/technical-direction/migration-execution-labels.md +60 -23
  32. package/extensions/agent-guardrails/README.md +45 -0
  33. package/extensions/agent-guardrails/guardrails.test.ts +42 -0
  34. package/extensions/agent-guardrails/guardrails.ts +66 -0
  35. package/extensions/agent-guardrails/index.ts +22 -0
  36. package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
  37. package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
  38. package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
  39. package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
  40. package/extensions/agent-guardrails/package.json +19 -0
  41. package/extensions/agent-guardrails/tsconfig.json +12 -0
  42. package/extensions/agent-modes/README.md +9 -3
  43. package/extensions/agent-modes/index.test.ts +208 -0
  44. package/extensions/agent-modes/index.ts +23 -6
  45. package/extensions/agent-modes/package.json +1 -1
  46. package/lib/cli.mjs +8 -2
  47. package/lib/pi-packages.mjs +169 -0
  48. package/package.json +5 -1
  49. package/pi/settings.json +23 -12
  50. package/plugin.json +1 -1
  51. package/scripts/lib/fs.mjs +31 -0
  52. package/skills/planning/classify-bead/REFERENCE.md +85 -0
  53. package/skills/planning/classify-bead/SKILL.md +32 -36
  54. package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
  55. package/skills/planning/create-task/SKILL.md +19 -10
  56. package/skills/planning/write-a-prd/SKILL.md +2 -2
  57. package/skills/style/style-code/REFERENCE.md +32 -2
  58. package/skills/style/style-code/SKILL.md +5 -21
  59. package/skills/style/style-tests/SKILL.md +69 -17
  60. package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
  61. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
  62. package/test/classify-bead.test.mjs +277 -0
  63. package/test/cli.test.mjs +55 -11
  64. package/test/fs.test.mjs +76 -0
  65. package/test/installer.test.mjs +237 -4
  66. package/test/pi-installer.test.mjs +343 -4
  67. package/test/pi-packages.test.mjs +295 -0
  68. package/token-map.README.md +4 -2
  69. package/token-map.json +3 -3
@@ -0,0 +1,76 @@
1
+ // Unit tests for scripts/lib/fs.mjs — the shared async stat wrappers. The
2
+ // helpers are exercised through their public API only; each case creates its
3
+ // own throwaway fixture under os.tmpdir().
4
+
5
+ import { describe, it } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { isDirectory, isFile } from "../scripts/lib/fs.mjs";
11
+
12
+ async function makeTmp() {
13
+ return await mkdtemp(join(tmpdir(), "agent-cortex-fs-"));
14
+ }
15
+
16
+ describe("isFile", () => {
17
+ it("is true for an existing regular file", async () => {
18
+ const dir = await makeTmp();
19
+ try {
20
+ const file = join(dir, "file.txt");
21
+ await writeFile(file, "hello");
22
+ assert.equal(await isFile(file), true);
23
+ } finally {
24
+ await rm(dir, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ it("is false for a directory", async () => {
29
+ const dir = await makeTmp();
30
+ try {
31
+ assert.equal(await isFile(dir), false);
32
+ } finally {
33
+ await rm(dir, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ it("is false for a missing path", async () => {
38
+ const dir = await makeTmp();
39
+ try {
40
+ assert.equal(await isFile(join(dir, "nope.txt")), false);
41
+ } finally {
42
+ await rm(dir, { recursive: true, force: true });
43
+ }
44
+ });
45
+ });
46
+
47
+ describe("isDirectory", () => {
48
+ it("is true for an existing directory", async () => {
49
+ const dir = await makeTmp();
50
+ try {
51
+ assert.equal(await isDirectory(dir), true);
52
+ } finally {
53
+ await rm(dir, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ it("is false for a regular file", async () => {
58
+ const dir = await makeTmp();
59
+ try {
60
+ const file = join(dir, "file.txt");
61
+ await writeFile(file, "hello");
62
+ assert.equal(await isDirectory(file), false);
63
+ } finally {
64
+ await rm(dir, { recursive: true, force: true });
65
+ }
66
+ });
67
+
68
+ it("is false for a missing path", async () => {
69
+ const dir = await makeTmp();
70
+ try {
71
+ assert.equal(await isDirectory(join(dir, "nope")), false);
72
+ } finally {
73
+ await rm(dir, { recursive: true, force: true });
74
+ }
75
+ });
76
+ });
@@ -18,6 +18,7 @@ import {
18
18
  stat,
19
19
  lstat,
20
20
  readlink,
21
+ symlink,
21
22
  } from "node:fs/promises";
22
23
  import { tmpdir } from "node:os";
23
24
  import { fileURLToPath } from "node:url";
@@ -294,14 +295,15 @@ describe("install claude", () => {
294
295
  assert.equal(await pathExists(join(out, ".claude-plugin")), false);
295
296
  });
296
297
 
297
- it("leaves the canonical sources untouched (no committed claude/ output)", async () => {
298
+ it("leaves the canonical sources untouched (no generated claude/ output)", async () => {
298
299
  const out = await makeTmp();
299
300
  const beforeExtras = (await readdir(join(ROOT, "claude-extras"))).sort();
300
301
  const beforeExtrasScripts = (await readdir(join(ROOT, "claude-extras", "scripts"))).sort();
301
302
  await runCli(["install", "claude", "--output", out]);
302
- // no committed claude/ output remains, and installs never write into the
303
- // canonical sources the plugin materialises from
304
- assert.equal(await pathExists(join(ROOT, "claude")), false);
303
+ // the committed claude/ store carries only the settings template — the
304
+ // generated plugin subtree (agents/, skills/, .claude-plugin/, hooks.json)
305
+ // is never committed, and installs never write into the canonical sources
306
+ assert.deepStrictEqual((await readdir(join(ROOT, "claude"))).sort(), ["settings.json"]);
305
307
  assert.deepStrictEqual((await readdir(join(ROOT, "claude-extras"))).sort(), beforeExtras);
306
308
  assert.deepStrictEqual((await readdir(join(ROOT, "claude-extras", "scripts"))).sort(), beforeExtrasScripts);
307
309
  });
@@ -346,6 +348,11 @@ describe("install claude", () => {
346
348
  "plugin install agent-cortex@jaybeeuu -y",
347
349
  ]);
348
350
  assert.ok(stdout.includes("Registering plugin"), "CLI reports registration");
351
+
352
+ // the default install also materialises ~/.claude/settings.json from the
353
+ // committed template, owning only the two declared keys
354
+ const settings = JSON.parse(await readFile(join(fakeHome, ".claude", "settings.json"), "utf-8"));
355
+ assert.deepEqual(Object.keys(settings).sort(), ["enabledPlugins", "extraKnownMarketplaces"]);
349
356
  });
350
357
 
351
358
  it("re-running a plain install is the update path: no marketplace re-add, no double-install", async () => {
@@ -407,8 +414,24 @@ describe("install claude", () => {
407
414
  const { exitCode, stdout } = await runCli(["install", "claude", "--dry-run"], env);
408
415
  assert.equal(exitCode, 0);
409
416
  assert.ok(stdout.includes(join(fakeHome, ".agent-cortex", "claude")), "dry-run names the home target");
417
+ assert.ok(stdout.includes(join(fakeHome, ".claude", "settings.json")), "dry-run names the settings target");
410
418
  assert.ok(/copied/i.test(stdout), "dry-run describes copied skills");
411
419
  assert.equal(await pathExists(join(fakeHome, ".agent-cortex")), false, "nothing written in dry-run");
420
+ assert.equal(await pathExists(join(fakeHome, ".claude")), false, "no settings written in dry-run");
421
+ });
422
+
423
+ it("--output stays generate-only and never touches ~/.claude/settings.json", async () => {
424
+ const fakeHome = await makeTmp();
425
+ const out = await makeTmp();
426
+ try {
427
+ const { exitCode } = await runCli(["install", "claude", "--output", out], { HOME: fakeHome });
428
+ assert.equal(exitCode, 0);
429
+ assert.ok(await pathExists(join(out, ".claude-plugin", "plugin.json")), "plugin subtree still generated");
430
+ assert.equal(await pathExists(join(fakeHome, ".claude")), false, "no user settings written by --output");
431
+ } finally {
432
+ await rm(fakeHome, { recursive: true, force: true });
433
+ await rm(out, { recursive: true, force: true });
434
+ }
412
435
  });
413
436
 
414
437
  it("rejects token-map.json versions newer than the implemented contract", async () => {
@@ -428,4 +451,214 @@ describe("install claude", () => {
428
451
  await rm(fixture, { recursive: true, force: true });
429
452
  }
430
453
  });
454
+ });
455
+
456
+ // ─── installClaude — ~/.claude/settings.json ─────────────────────────────────
457
+
458
+ /** A claude/settings.json template carrying the two CLI-owned keys. */
459
+ function settingsTemplate(overrides = {}) {
460
+ return { enabledPlugins: {}, extraKnownMarketplaces: {}, ...overrides };
461
+ }
462
+
463
+ /** A realistic personal ~/.claude/settings.json (keys the installer must never own). */
464
+ function personalSettings(overrides = {}) {
465
+ return {
466
+ permissions: { allow: ["Bash(pnpm test:*)"] },
467
+ hooks: { SessionStart: [{ hooks: [{ type: "command", command: "echo personal" }] }] },
468
+ env: { PERSONAL_FLAG: "1" },
469
+ statusLine: { type: "command", command: "~/.claude/statusline.sh" },
470
+ ...overrides,
471
+ };
472
+ }
473
+
474
+ /** Minimal package root with a claude/settings.json template, enough for
475
+ * installClaude to materialise the plugin subtree into a throwaway output. */
476
+ async function makeSettingsFixture({ template = settingsTemplate(), settings } = {}) {
477
+ const root = await makeTmp();
478
+ await writeFile(
479
+ join(root, "package.json"),
480
+ JSON.stringify({ name: "agent-cortex", version: "1.2.3", license: "MIT" }),
481
+ );
482
+ await writeFile(
483
+ join(root, "token-map.json"),
484
+ JSON.stringify({ tools: {}, paths: { plugin_root: { claude: "PLUGIN_ROOT" } } }),
485
+ );
486
+ await mkdir(join(root, "agents"));
487
+ await mkdir(join(root, "skills"));
488
+ if (template !== null) {
489
+ await mkdir(join(root, "claude"), { recursive: true });
490
+ await writeFile(join(root, "claude", "settings.json"), JSON.stringify(template, null, 2) + "\n");
491
+ }
492
+ const output = join(root, "out");
493
+ const settingsPath = join(root, "claude-home", "settings.json");
494
+ if (settings !== undefined) {
495
+ await mkdir(dirname(settingsPath), { recursive: true });
496
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
497
+ }
498
+ return { root, output, settingsPath, cleanup: () => rm(root, { recursive: true, force: true }) };
499
+ }
500
+
501
+ describe("install claude — settings.json", () => {
502
+ it("materialises a real settings.json carrying the CLI-owned keys from the committed template", async () => {
503
+ const fx = await makeSettingsFixture({
504
+ template: settingsTemplate({
505
+ enabledPlugins: { "context7@claude-plugins-official": true },
506
+ extraKnownMarketplaces: {
507
+ "claude-plugins-official": { source: { source: "github", repo: "anthropics/claude-plugins" } },
508
+ },
509
+ }),
510
+ });
511
+ try {
512
+ const result = await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
513
+
514
+ assert.equal(result.settings.path, fx.settingsPath);
515
+ assert.equal(result.settings.action, "written");
516
+ assert.equal((await lstat(fx.settingsPath)).isSymbolicLink(), false, "settings.json is a real file");
517
+ const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
518
+ assert.deepEqual(settings.enabledPlugins, { "context7@claude-plugins-official": true });
519
+ assert.deepEqual(settings.extraKnownMarketplaces, {
520
+ "claude-plugins-official": { source: { source: "github", repo: "anthropics/claude-plugins" } },
521
+ });
522
+ assert.deepEqual(
523
+ Object.keys(settings).sort(),
524
+ ["enabledPlugins", "extraKnownMarketplaces"],
525
+ "only the two CLI-owned keys are written",
526
+ );
527
+ } finally {
528
+ await fx.cleanup();
529
+ }
530
+ });
531
+
532
+ it("preserves personal and unknown keys on re-install (merge-keep-personal)", async () => {
533
+ const fx = await makeSettingsFixture({
534
+ template: settingsTemplate({
535
+ theme: { name: "dark", accent: "blue" },
536
+ enabledPlugins: { "team@official": true },
537
+ }),
538
+ settings: personalSettings({
539
+ theme: { name: "light" },
540
+ personalOnly: { nested: true },
541
+ }),
542
+ });
543
+ try {
544
+ await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
545
+ const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
546
+
547
+ assert.deepEqual(settings.permissions, { allow: ["Bash(pnpm test:*)"] });
548
+ assert.deepEqual(settings.hooks, { SessionStart: [{ hooks: [{ type: "command", command: "echo personal" }] }] });
549
+ assert.deepEqual(settings.env, { PERSONAL_FLAG: "1" });
550
+ assert.deepEqual(settings.statusLine, { type: "command", command: "~/.claude/statusline.sh" });
551
+ assert.deepEqual(settings.personalOnly, { nested: true }, "unknown key preserved");
552
+ assert.equal(settings.theme.name, "light", "personal value wins over the template default");
553
+ assert.equal(settings.theme.accent, "blue", "template default fills the unset nested key");
554
+ } finally {
555
+ await fx.cleanup();
556
+ }
557
+ });
558
+
559
+ it("owns only enabledPlugins + extraKnownMarketplaces — personal entries under them are replaced by the template", async () => {
560
+ const fx = await makeSettingsFixture({
561
+ template: settingsTemplate({
562
+ enabledPlugins: { "team@official": true },
563
+ extraKnownMarketplaces: {
564
+ official: { source: { source: "github", repo: "anthropics/plugins" } },
565
+ },
566
+ }),
567
+ settings: personalSettings({
568
+ enabledPlugins: { "personal@mine": true },
569
+ extraKnownMarketplaces: { mine: { source: { source: "github", repo: "me/mine" } } },
570
+ }),
571
+ });
572
+ try {
573
+ await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
574
+ const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
575
+
576
+ assert.deepEqual(settings.enabledPlugins, { "team@official": true }, "template owns enabledPlugins");
577
+ assert.deepEqual(
578
+ settings.extraKnownMarketplaces,
579
+ { official: { source: { source: "github", repo: "anthropics/plugins" } } },
580
+ "template owns extraKnownMarketplaces",
581
+ );
582
+ assert.ok(!("personal@mine" in settings.enabledPlugins));
583
+ } finally {
584
+ await fx.cleanup();
585
+ }
586
+ });
587
+
588
+ it("dry-run plans the settings write without touching the filesystem", async () => {
589
+ const fx = await makeSettingsFixture({
590
+ template: settingsTemplate({ enabledPlugins: { "team@official": true } }),
591
+ });
592
+ try {
593
+ const result = await installClaude({
594
+ root: fx.root,
595
+ output: fx.output,
596
+ settingsPath: fx.settingsPath,
597
+ dryRun: true,
598
+ });
599
+
600
+ assert.equal(result.settings.action, "would-write");
601
+ assert.equal(await pathExists(fx.settingsPath), false, "nothing written in dry-run");
602
+ } finally {
603
+ await fx.cleanup();
604
+ }
605
+ });
606
+
607
+ it("re-running is idempotent — the second install leaves settings.json unchanged", async () => {
608
+ const fx = await makeSettingsFixture({
609
+ template: settingsTemplate({ enabledPlugins: { "team@official": true } }),
610
+ });
611
+ try {
612
+ await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
613
+ const first = await readFile(fx.settingsPath, "utf-8");
614
+
615
+ const second = await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
616
+ assert.equal(second.settings.action, "unchanged");
617
+ assert.equal(await readFile(fx.settingsPath, "utf-8"), first);
618
+ } finally {
619
+ await fx.cleanup();
620
+ }
621
+ });
622
+
623
+ it("replaces a legacy symlinked settings.json with a real file, never writing through the template", async () => {
624
+ const fx = await makeSettingsFixture({
625
+ template: settingsTemplate({ enabledPlugins: { "team@official": true } }),
626
+ });
627
+ try {
628
+ const templatePath = join(fx.root, "claude", "settings.json");
629
+ const templateBefore = await readFile(templatePath, "utf-8");
630
+ await mkdir(dirname(fx.settingsPath), { recursive: true });
631
+ await symlink(templatePath, fx.settingsPath);
632
+
633
+ const result = await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
634
+
635
+ assert.equal(result.settings.action, "removed-symlink");
636
+ assert.equal(result.settings.removedSymlink, true);
637
+ assert.equal((await lstat(fx.settingsPath)).isSymbolicLink(), false, "symlink replaced by a real file");
638
+ const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
639
+ assert.deepEqual(settings.enabledPlugins, { "team@official": true });
640
+ assert.equal(await readFile(templatePath, "utf-8"), templateBefore, "template never written through");
641
+ } finally {
642
+ await fx.cleanup();
643
+ }
644
+ });
645
+
646
+ it("warns and skips when the package ships no claude/settings.json template", async () => {
647
+ const fx = await makeSettingsFixture({ template: null });
648
+ try {
649
+ const warnings = [];
650
+ const result = await installClaude({
651
+ root: fx.root,
652
+ output: fx.output,
653
+ settingsPath: fx.settingsPath,
654
+ warn: (m) => warnings.push(m),
655
+ });
656
+
657
+ assert.equal(result.settings, null);
658
+ assert.ok(warnings.some((w) => w.includes("claude/settings.json")), "missing template is warned about");
659
+ assert.equal(await pathExists(fx.settingsPath), false, "nothing written without a template");
660
+ } finally {
661
+ await fx.cleanup();
662
+ }
663
+ });
431
664
  });