@cohortapp/agent-sdk 2.12.0 → 2.14.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 (129) hide show
  1. package/bin/maestro.mjs +6 -2
  2. package/docs/guides/front-door-session.md +86 -0
  3. package/lib/cli/design.mjs +185 -0
  4. package/lib/cli/design.test.mjs +270 -0
  5. package/lib/cli/global-setup-extras.mjs +44 -0
  6. package/lib/cli/global-setup-extras.test.mjs +95 -0
  7. package/lib/cli/session.mjs +11 -1
  8. package/lib/cli/session.test.mjs +17 -6
  9. package/lib/collective/global-config.mjs +5 -0
  10. package/lib/collective/global-config.test.mjs +5 -0
  11. package/lib/collective/vendor-skills.mjs +305 -0
  12. package/lib/collective/vendor-skills.test.mjs +306 -0
  13. package/lib/design/design-md.mjs +793 -0
  14. package/lib/design/design-md.test.mjs +318 -0
  15. package/lib/design/fixtures/DESIGN.golden.md +238 -0
  16. package/lib/design/fixtures/PRODUCT.golden.md +67 -0
  17. package/lib/design/fixtures/foundation.json +133 -0
  18. package/lib/design/refresh-gate.mjs +154 -0
  19. package/lib/design/refresh-gate.test.mjs +144 -0
  20. package/lib/design/write.mjs +275 -0
  21. package/lib/design/write.test.mjs +241 -0
  22. package/lib/prompts/parallelism.mjs +79 -0
  23. package/lib/prompts/parallelism.test.mjs +177 -0
  24. package/lib/telemetry/collect.mjs +357 -5
  25. package/lib/telemetry/collect.test.mjs +285 -0
  26. package/package.json +1 -1
  27. package/plugins/maestro-skills/plugin.json +4 -0
  28. package/plugins/maestro-skills/skills/cohort-design.md +153 -0
  29. package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
  30. package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
  31. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
  32. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
  33. package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
  34. package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
  35. package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
  36. package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
  37. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
  38. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
  39. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
  40. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
  41. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
  42. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
  43. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
  44. package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
  45. package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
  46. package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
  47. package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
  48. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
  49. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
  50. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
  51. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
  52. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
  53. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
  54. package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
  55. package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
  56. package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
  57. package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
  58. package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
  59. package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
  60. package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
  61. package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
  62. package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
  63. package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
  64. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
  65. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
  66. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
  67. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  68. package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
  69. package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
  70. package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
  71. package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
  72. package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
  73. package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
  74. package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
  75. package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
  76. package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
  77. package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
  78. package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
  79. package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
  80. package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
  81. package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
  82. package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
  83. package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
  84. package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
  85. package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
  86. package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
  87. package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
  88. package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
  89. package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
  90. package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
  91. package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
  92. package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
  93. package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
  94. package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
  95. package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
  96. package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
  97. package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
  98. package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
  99. package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
  100. package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
  101. package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
  102. package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
  103. package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
  104. package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
  105. package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
  106. package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
  107. package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
  108. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
  109. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
  110. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  111. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
  112. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
  113. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
  114. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
  115. package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
  116. package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
  117. package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
  118. package/scripts/ci/check-skill-packs.mjs +388 -0
  119. package/scripts/ci/check-skill-packs.test.mjs +495 -0
  120. package/scripts/ci/check.mjs +3 -0
  121. package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
  122. package/scripts/daemon/agent-daemon.mjs +108 -0
  123. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
  124. package/scripts/daemon/cadence-consumer.mjs +46 -22
  125. package/scripts/daemon/prompt-builder.mjs +19 -3
  126. package/scripts/local-triggers/autoupdate.test.mjs +33 -3
  127. package/scripts/vendor/skill-packs.mjs +354 -0
  128. package/scripts/vendor/sync-skill-packs.mjs +242 -0
  129. package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
@@ -933,3 +933,288 @@ test("collectStatus: machine.upgrade rides the beat when state/autoupdate/last.j
933
933
  assert.equal(injected.machine.upgrade.ok, false);
934
934
  } finally { rmSync(root, { recursive: true, force: true }); }
935
935
  });
936
+
937
+ // ---------------------------------------------------------------------------
938
+ // machine.sessionNote (WP-M8) — WHY the front door is not live
939
+ // ---------------------------------------------------------------------------
940
+
941
+ const M8_NOW = Date.parse("2026-09-11T12:00:00Z");
942
+ /** The record scripts/session/supervisor.mjs writes via first-run#attentionRecord. */
943
+ const attention = (over = {}) => ({
944
+ reason: "no-heartbeat",
945
+ since: "2026-09-11T11:57:00.000Z",
946
+ runMs: 180_000,
947
+ attach: "tmux attach -t =maestro-maya",
948
+ hint: "The session has been up 180 s without a heartbeat — it is probably waiting on a first-run dialog or a permission prompt. Attach with `tmux attach -t =maestro-maya` and answer it.",
949
+ ...over,
950
+ });
951
+
952
+ test("sessionNote: a live front door says nothing at all", () => {
953
+ const { sessionNote } = _internals;
954
+ assert.equal(sessionNote({ heartbeat: { ts: M8_NOW - 1_000 }, now: M8_NOW }), null);
955
+ assert.equal(sessionNote({ heartbeat: { ts: M8_NOW - 89_000 }, jobInstalled: false, attention: attention(), now: M8_NOW }), null,
956
+ "liveness wins over every other input — the note exists only to explain a door that is NOT open");
957
+ // A shorter staleness bound makes the same beat stale, and then it does speak.
958
+ assert.equal(sessionNote({ heartbeat: { ts: M8_NOW - 30_000 }, jobInstalled: true, now: M8_NOW, staleMs: 10_000 }).reason, "heartbeat-stale");
959
+ });
960
+
961
+ test("sessionNote: job-absent — no plist for this seat, and only ever from a hard false", () => {
962
+ const { sessionNote } = _internals;
963
+ const n = sessionNote({ jobInstalled: false, label: "ai.maestro.maya-session", now: M8_NOW });
964
+ assert.equal(n.reason, "job-absent");
965
+ assert.match(n.detail, /ai\.maestro\.maya-session/);
966
+ assert.equal(n.since, undefined, "there is no moment to date — the job was never there");
967
+ // It outranks a stale attention record left behind by a previous install.
968
+ assert.equal(sessionNote({ jobInstalled: false, attention: attention(), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "job-absent");
969
+ // UNKNOWN is not absent: a home we could not read must never accuse the seat.
970
+ assert.equal(sessionNote({ jobInstalled: null, now: M8_NOW }).reason, "never-beaten");
971
+ assert.equal(sessionNote({ now: M8_NOW }).reason, "never-beaten");
972
+ // No label still yields a usable line.
973
+ assert.match(sessionNote({ jobInstalled: false, now: M8_NOW }).detail, /main-session launchd job/);
974
+ // …and it does not call a deliberate daemon-front-door seat broken: this seat
975
+ // reports job-absent on EVERY beat forever, so the line states a configuration.
976
+ assert.match(n.detail, /the daemon --print lane is the front door/);
977
+ assert.match(n.detail, /by configuration, or the session job was never installed/);
978
+ });
979
+
980
+ test("sessionNote: awaiting-input passes the supervisor's own reason, since and hint through", () => {
981
+ const { sessionNote } = _internals;
982
+ const n = sessionNote({ jobInstalled: true, attention: attention(), now: M8_NOW });
983
+ assert.equal(n.reason, "awaiting-input", "the beat's vocabulary — the record's own reason rides in detail");
984
+ assert.equal(n.since, "2026-09-11T11:57:00.000Z", "attentionRecord.since, normalised to canonical ISO");
985
+ assert.match(n.detail, /^up, never beaten: /, "the record's own reason survives, rendered out of the watchdog's vocabulary");
986
+ assert.match(n.detail, /tmux attach -t =maestro-maya/, "the attach command is the actionable part");
987
+ assert.ok(n.detail.length <= 200);
988
+ // screen seats say screen.
989
+ assert.match(sessionNote({ attention: attention({ attach: "screen -r maestro-isla", hint: "… Attach with `screen -r maestro-isla` and answer it." }), now: M8_NOW }).detail, /screen -r maestro-isla/);
990
+ // It outranks a stale heartbeat: "waiting on a human" is the actionable fact.
991
+ assert.equal(sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "awaiting-input");
992
+ });
993
+
994
+ test("sessionNote: a heartbeat LATER than the attention record supersedes it", () => {
995
+ const { sessionNote } = _internals;
996
+ // supervisor.mjs clears attention.json only at the START of the next launch —
997
+ // never when the human answers the dialog — so the record outlives its truth.
998
+ // Answered at 11:57, beat until 3 days later, then the session died.
999
+ const beat = M8_NOW + 3 * 86_400_000;
1000
+ const n = sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { ts: beat }, now: beat + 86_400_000 });
1001
+ assert.equal(n.reason, "heartbeat-stale", "a days-old attach command for a mux session that ended is worse than no note");
1002
+ assert.equal(n.since, new Date(beat).toISOString());
1003
+ // A record with no orderable `since` loses to a heartbeat that has one.
1004
+ assert.equal(sessionNote({ jobInstalled: true, attention: attention({ since: "not a date" }), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "heartbeat-stale");
1005
+ // But the record still wins while it is the LATER fact — the ordinary case:
1006
+ // the session came up, never beat, and the watchdog flagged it.
1007
+ assert.equal(sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "awaiting-input");
1008
+ // …and with no heartbeat at all there is nothing to supersede it.
1009
+ assert.equal(sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { pid: 7 }, now: M8_NOW }).reason, "awaiting-input");
1010
+ });
1011
+
1012
+ test("sessionNote: `since` is re-formatted from a parsed instant, never passed through", () => {
1013
+ const { sessionNote } = _internals;
1014
+ // V8's legacy date parser accepts trailing prose, so a bare Date.parse check
1015
+ // let a home path and a token-shaped run ride the one field the detail
1016
+ // sanitiser does not cover. `since` is canonical ISO or it is absent.
1017
+ const evil = "Thu, 01 Jan 2026 00:00:00 GMT (token sk-ant-supersecretvalue1234567890 /Users/maya/x)";
1018
+ assert.ok(Number.isFinite(Date.parse(evil)), "the hostile string really does parse — that is the hazard");
1019
+ const n = sessionNote({ jobInstalled: true, attention: attention({ since: evil }), now: M8_NOW });
1020
+ assert.equal(n.since, "2026-01-01T00:00:00.000Z");
1021
+ assert.ok(!/Users|sk-ant/.test(n.since));
1022
+ // Non-ISO spellings are normalised rather than echoed.
1023
+ assert.equal(sessionNote({ jobInstalled: true, attention: attention({ since: "2026-09-11T11:57:00+00:00" }), now: M8_NOW }).since, "2026-09-11T11:57:00.000Z");
1024
+ });
1025
+
1026
+ test("sessionNote: the attention record's own vocabulary is rendered, not collided with", () => {
1027
+ const { sessionNote } = _internals;
1028
+ // first-run#heartbeatSilence's "no-heartbeat" means "up, never beaten" — the
1029
+ // opposite of what a beat reason of nearly that spelling would mean. Rendering
1030
+ // it keeps `{reason, detail}` from reading as a contradiction side by side.
1031
+ assert.match(sessionNote({ jobInstalled: true, attention: attention(), now: M8_NOW }).detail, /^up, never beaten: /);
1032
+ assert.match(sessionNote({ jobInstalled: true, attention: attention({ reason: "stale-heartbeat" }), now: M8_NOW }).detail, /^no beat since this launch: /);
1033
+ // A word we do not know passes through verbatim rather than being invented.
1034
+ assert.match(sessionNote({ jobInstalled: true, attention: attention({ reason: "wedged-mcp" }), now: M8_NOW }).detail, /^wedged-mcp: /);
1035
+ // And no beat reason collides with a watchdog reason any more.
1036
+ const beatReasons = new Set(["job-absent", "awaiting-input", "heartbeat-stale", "heartbeat-unreadable", "never-beaten"]);
1037
+ for (const r of ["no-heartbeat", "stale-heartbeat", "within-grace", "beating"]) assert.ok(!beatReasons.has(r), `${r} is the watchdog's word, not the beat's`);
1038
+ });
1039
+
1040
+ test("sessionNote: heartbeat-stale carries the last beat and its age", () => {
1041
+ const { sessionNote } = _internals;
1042
+ const n = sessionNote({ jobInstalled: true, heartbeat: { ts: M8_NOW - 600_000, pid: 4242 }, now: M8_NOW });
1043
+ assert.deepEqual(n, { reason: "heartbeat-stale", since: "2026-09-11T11:50:00.000Z", detail: "no beat for 600 s" });
1044
+ // ISO timestamps are the shape the feed actually writes.
1045
+ assert.equal(sessionNote({ jobInstalled: true, heartbeat: { ts: "2026-09-11T11:50:00Z" }, now: M8_NOW }).since, "2026-09-11T11:50:00.000Z");
1046
+ // A beat from the future is clock skew, not a negative age.
1047
+ assert.equal(sessionNote({ jobInstalled: true, heartbeat: { ts: M8_NOW + 600_000 }, now: M8_NOW }).detail, "last beat is in the future (clock skew)");
1048
+ });
1049
+
1050
+ test("sessionNote: every malformed input degrades to a coarser note or to null, and nothing throws", () => {
1051
+ const { sessionNote } = _internals;
1052
+ // No `now` at all → no note (a note with no clock behind it would be a guess).
1053
+ assert.equal(sessionNote({ heartbeat: { ts: 1 } }), null);
1054
+ assert.equal(sessionNote({ now: NaN, jobInstalled: false }), null);
1055
+ assert.equal(sessionNote(), null);
1056
+ assert.equal(sessionNote(null), null);
1057
+ assert.equal(sessionNote("nonsense"), null);
1058
+ // Absent files (the reader's null) → the installed-but-silent note.
1059
+ assert.deepEqual(sessionNote({ heartbeat: null, attention: null, jobInstalled: true, now: M8_NOW }),
1060
+ { reason: "never-beaten", detail: "the session job is installed but has never written a heartbeat" });
1061
+ // …and when job presence was never established, the note says THAT rather
1062
+ // than affirming an installation nobody checked.
1063
+ assert.deepEqual(sessionNote({ jobInstalled: null, now: M8_NOW }),
1064
+ { reason: "never-beaten", detail: "no heartbeat from the session job, and its presence on this seat could not be checked" });
1065
+ assert.equal(sessionNote({ now: M8_NOW }).detail, "no heartbeat from the session job, and its presence on this seat could not be checked");
1066
+ // A parsed-but-wrong SHAPE must not throw.
1067
+ assert.equal(sessionNote({ heartbeat: "{}", attention: "{}", now: M8_NOW }).reason, "never-beaten");
1068
+ assert.equal(sessionNote({ heartbeat: [], attention: [], jobInstalled: true, now: M8_NOW }).reason, "never-beaten", "arrays are not records");
1069
+ // A heartbeat file that is THERE but says nothing usable is its own fault —
1070
+ // the job may well have beaten, so it is never called "never beaten".
1071
+ assert.deepEqual(sessionNote({ heartbeat: { pid: 7 }, jobInstalled: true, now: M8_NOW }),
1072
+ { reason: "heartbeat-unreadable", detail: "the heartbeat file is present but carries no readable timestamp" });
1073
+ assert.equal(sessionNote({ heartbeat: { ts: "not a date" }, jobInstalled: true, now: M8_NOW }).reason, "heartbeat-unreadable");
1074
+ assert.deepEqual(sessionNote({ heartbeatUnreadable: true, jobInstalled: true, now: M8_NOW }),
1075
+ { reason: "heartbeat-unreadable", detail: "the heartbeat file is present but could not be parsed" });
1076
+ // An attention record missing every field is still the right REASON — it is
1077
+ // the file's existence that says "the supervisor flagged this seat".
1078
+ assert.deepEqual(sessionNote({ attention: {}, jobInstalled: true, now: M8_NOW }), { reason: "awaiting-input" });
1079
+ assert.deepEqual(sessionNote({ attention: { since: 12345, reason: 7, hint: null }, jobInstalled: true, now: M8_NOW }), { reason: "awaiting-input" });
1080
+ assert.equal(sessionNote({ attention: { since: "not a date", reason: "no-heartbeat" }, jobInstalled: true, now: M8_NOW }).since, undefined);
1081
+ });
1082
+
1083
+ test("sanitizeNoteDetail: no home paths, no ids, no control characters, never past the cap", () => {
1084
+ const { sanitizeNoteDetail } = _internals;
1085
+ assert.equal(sanitizeNoteDetail("attach with `tmux attach -t =maestro-maya`"), "attach with `tmux attach -t =maestro-maya`");
1086
+ // Home directories name a person and leak the seat's layout.
1087
+ assert.equal(sanitizeNoteDetail("blocked in /Users/maya/maya-ai/state"), "blocked in <path>");
1088
+ // A home directory with a space in it is ordinary on macOS, and its tail
1089
+ // carries the person's surname — the match may not stop at the whitespace.
1090
+ assert.equal(sanitizeNoteDetail("blocked in /Users/olivia chen/maya-ai/state/session"), "blocked in <path>");
1091
+ assert.equal(sanitizeNoteDetail("blocked in /Users/Olivia Chen/Library/Application Support/x"), "blocked in <path>");
1092
+ // …but ordinary prose after a path is not swallowed with it.
1093
+ assert.equal(sanitizeNoteDetail("blocked in /Users/maya and then retry"), "blocked in <path> and then retry");
1094
+ assert.equal(sanitizeNoteDetail("attach to /Users/maya/x and answer it"), "attach to <path> and answer it");
1095
+ assert.equal(sanitizeNoteDetail("blocked in ~/maya-ai and /home/maya/x and /private/var/folders/t/x"), "blocked in <path> and <path> and <path>");
1096
+ // Session ids and token-shaped runs.
1097
+ assert.equal(sanitizeNoteDetail("resume 3f2a1c94-7b0e-4a11-9c3d-0b7e2f8a6d51 failed"), "resume <redacted> failed");
1098
+ assert.equal(sanitizeNoteDetail("token sk-ant-0123456789abcdef rejected"), "token <redacted> rejected");
1099
+ // Newlines and control characters collapse; the note is one line.
1100
+ assert.equal(sanitizeNoteDetail("a\nb\tc\r\nd"), "a b c d");
1101
+ // The cap holds, with an ellipsis so a reader knows it was cut.
1102
+ const long = sanitizeNoteDetail("lorem ipsum ".repeat(50));
1103
+ assert.equal(long.length, 200);
1104
+ assert.ok(long.endsWith("…"));
1105
+ assert.ok(sanitizeNoteDetail("ab ".repeat(20), 10).length <= 10, "the cap is a ceiling, never exceeded (a word boundary can land under it)");
1106
+ // Nothing to say → "", and the caller then omits the field entirely.
1107
+ assert.equal(sanitizeNoteDetail(""), "");
1108
+ assert.equal(sanitizeNoteDetail(null), "");
1109
+ assert.equal(sanitizeNoteDetail(undefined), "");
1110
+ assert.equal(sanitizeNoteDetail(42), "");
1111
+ assert.equal(sanitizeNoteDetail(" \n "), "");
1112
+ // A hint long enough to overflow is truncated, not dropped.
1113
+ const hint = sanitizeNoteDetail(`no-heartbeat: The session has been up 1800 s without a heartbeat — it is probably waiting on a first-run dialog or a permission prompt. Attach with \`tmux attach -t =maestro-maya\` and answer it. ${"tail ".repeat(20)}`);
1114
+ assert.ok(hint.length <= 200 && hint.endsWith("…"), "an over-long hint is truncated, not dropped");
1115
+ });
1116
+
1117
+ test("sessionJobInstalled: a readdir of ~/Library/LaunchAgents, and 'unknown' whenever it cannot be read", () => {
1118
+ const { sessionJobInstalled, sessionJobLabel } = _internals;
1119
+ const label = "ai.maestro.maya-session";
1120
+ assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya.plist", "ai.maestro.maya-session.plist"] }, label), true);
1121
+ assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya-session"] }, label), true, "bare labels count too");
1122
+ assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya.plist"] }, label), false);
1123
+ assert.equal(sessionJobInstalled({ launchAgents: [] }, label), false);
1124
+ // Unknowns: no label, an unreadable home, a home that is not there.
1125
+ assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya-session.plist"] }, ""), null);
1126
+ assert.equal(sessionJobInstalled({ os: { homedir: () => "" } }, label), null);
1127
+ assert.equal(sessionJobInstalled({ os: { homedir: () => { throw new Error("nope"); } } }, label), null);
1128
+ assert.equal(sessionJobInstalled({ os: { homedir: () => join(tmpdir(), "no-such-home-xyz") } }, label), null);
1129
+ // An explicit override wins (the daemon may already know).
1130
+ assert.equal(sessionJobInstalled({ sessionJobInstalled: true, launchAgents: [] }, label), true);
1131
+ // A case difference is not an absence: agentFirstName leaves its directory
1132
+ // fallback as-is (`~/Maya-ai` -> `Maya`), and the volume is case-insensitive.
1133
+ assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.Maya-session.plist"] }, "ai.maestro.maya-session"), true);
1134
+ assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya-session.plist"] }, "ai.maestro.Maya-session"), true);
1135
+ // The label matches what generate-plists.sh and `maestro session` spell —
1136
+ // including its casing, which the generator does not lower-case either.
1137
+ assert.equal(sessionJobLabel("/x/maya-ai", "maya"), "ai.maestro.maya-session");
1138
+ assert.equal(sessionJobLabel("/x/Maya-ai", "Maya"), "ai.maestro.Maya-session");
1139
+ assert.equal(sessionJobLabel("", ""), "");
1140
+ });
1141
+
1142
+ test("collectStatus: machine.sessionNote appears ONLY when the front door is down, and rides machine (never session)", async () => {
1143
+ const root = mkdtempSync(join(tmpdir(), "session-note-"));
1144
+ try {
1145
+ const now = Date.parse("2026-09-11T12:00:00Z");
1146
+ const osImpl = fakeOs({});
1147
+ osImpl.hostname = () => "seat.local";
1148
+ const base = {
1149
+ agentRoot: root, now, os: osImpl, execFile: fakeExecFile({}), subAgentsRunning: 0,
1150
+ powermetrics: false, disk: false, spend: false, agentFirst: "maya", launchAgents: [],
1151
+ };
1152
+ const sess = join(root, "state", "session");
1153
+ mkdirSync(sess, { recursive: true });
1154
+
1155
+ // 1. No job on the seat — the case A010/A016 could not tell apart.
1156
+ const absent = await collectStatus(base);
1157
+ assert.equal(absent.machine.sessionLive, false);
1158
+ assert.equal(absent.machine.sessionNote.reason, "job-absent");
1159
+ assert.match(absent.machine.sessionNote.detail, /ai\.maestro\.maya-session/);
1160
+ assert.equal(absent.session, null, "the note NEVER touches session — hq validates that object strictly");
1161
+ assert.ok(!("sessionNote" in (absent.session || {})));
1162
+
1163
+ // 2. Job installed, session up, waiting on a first-run dialog.
1164
+ const installed = { ...base, launchAgents: ["ai.maestro.maya-session.plist"] };
1165
+ writeFileSync(join(sess, "attention.json"), JSON.stringify(attention()));
1166
+ const waiting = await collectStatus(installed);
1167
+ assert.equal(waiting.machine.sessionNote.reason, "awaiting-input");
1168
+ assert.equal(waiting.machine.sessionNote.since, "2026-09-11T11:57:00.000Z");
1169
+ assert.match(waiting.machine.sessionNote.detail, /tmux attach -t =maestro-maya/);
1170
+
1171
+ // 3. The supervisor cleared attention.json and the beat then went stale.
1172
+ rmSync(join(sess, "attention.json"));
1173
+ writeFileSync(join(sess, "heartbeat.json"), JSON.stringify({ pid: 4242, ts: "2026-09-11T11:50:00Z" }));
1174
+ const stale = await collectStatus(installed);
1175
+ assert.equal(stale.machine.frontDoor, "session");
1176
+ assert.deepEqual(stale.machine.sessionNote, { reason: "heartbeat-stale", since: "2026-09-11T11:50:00.000Z", detail: "no beat for 600 s" });
1177
+
1178
+ // 4. A live front door carries NO note — the key is absent, not null.
1179
+ writeFileSync(join(sess, "heartbeat.json"), JSON.stringify({ pid: 4242, ts: "2026-09-11T11:59:50Z" }));
1180
+ const live = await collectStatus(installed);
1181
+ assert.equal(live.machine.sessionLive, true);
1182
+ assert.ok(!("sessionNote" in live.machine), "a healthy seat must not pay for a field it has nothing to say in");
1183
+
1184
+ // 5. Corrupt state files never break the beat — and a heartbeat file that
1185
+ // is THERE and will not parse is reported as such, not as "never beaten".
1186
+ writeFileSync(join(sess, "heartbeat.json"), "{not json");
1187
+ writeFileSync(join(sess, "attention.json"), "{not json");
1188
+ const corrupt = await collectStatus(installed);
1189
+ assert.equal(corrupt.machine.sessionLive, false);
1190
+ assert.deepEqual(corrupt.machine.sessionNote,
1191
+ { reason: "heartbeat-unreadable", detail: "the heartbeat file is present but could not be parsed" });
1192
+ assert.equal(typeof corrupt.ts, "string", "the snapshot is intact");
1193
+
1194
+ // 5b. No heartbeat file at all, job installed → never-beaten.
1195
+ rmSync(join(sess, "heartbeat.json"));
1196
+ rmSync(join(sess, "attention.json"));
1197
+ const silent = await collectStatus(installed);
1198
+ assert.deepEqual(silent.machine.sessionNote,
1199
+ { reason: "never-beaten", detail: "the session job is installed but has never written a heartbeat" });
1200
+
1201
+ // 6. The whole note is a best-effort extra: a throwing os.homedir drops the
1202
+ // launchd read, never the beat — and an unknown job presence is reported
1203
+ // as neither an absence nor a presence.
1204
+ const hostile = await collectStatus({ ...base, launchAgents: undefined, os: { ...osImpl, homedir: () => { throw new Error("no home"); } } });
1205
+ assert.equal(hostile.machine.sessionNote.reason, "never-beaten", "unknown job presence is not an absence");
1206
+ assert.equal(hostile.machine.sessionNote.detail, "no heartbeat from the session job, and its presence on this seat could not be checked",
1207
+ "…and it is not reported as a presence either");
1208
+ assert.equal(hostile.machine.sessionLive, false);
1209
+
1210
+ // 7. The emitted shape is exactly {reason, since?, detail?} — nothing else,
1211
+ // and nothing unbounded.
1212
+ for (const s of [absent, waiting, stale, corrupt, silent, hostile]) {
1213
+ const note = s.machine.sessionNote;
1214
+ assert.equal(typeof note.reason, "string");
1215
+ for (const k of Object.keys(note)) assert.ok(["reason", "since", "detail"].includes(k), `sessionNote.${k} is not in the contract`);
1216
+ if (note.since !== undefined) assert.ok(Number.isFinite(Date.parse(note.since)), "since is ISO 8601");
1217
+ if (note.detail !== undefined) assert.ok(note.detail.length <= 200 && !/\/Users\//.test(note.detail));
1218
+ }
1219
+ } finally { rmSync(root, { recursive: true, force: true }); }
1220
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -155,6 +155,10 @@
155
155
  "name": "peer-sessions",
156
156
  "description": "Spawn, find and talk to the agent's own peer sessions with ListAgents/SendMessage and `maestro session spawn|peers`, and relay their status to a human in the agent's own voice. Use for parallel work, when a human asks how something is going, or when a peer reports back."
157
157
  },
158
+ {
159
+ "name": "cohort-design",
160
+ "description": "Harmonise the installed design skills with Cohort's own design system — DESIGN.md, PRODUCT.md and the design_* tools outrank every skill's defaults; impeccable for product-UI craft, the motion skills for animation, the taste skills for marketing pages only, unlazy for completion gates. Use before any design, redesign, polish, critique, audit, layout, typography, colour or motion work."
161
+ },
158
162
  {
159
163
  "name": "persona-discipline",
160
164
  "description": "Be one persona to every human — never name Claude Code, sessions, sub-sessions, subagents, workflows or models in anything a person could read; how to rewrite when the pre-send audit blocks a message. Use before any outbound message or when the send hook blocks."
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: cohort-design
3
+ description: Harmonise the installed design skills with Cohort's own design system for any interface work — design, redesign, layout, typography, colour, polish, critique, audit, accessibility, motion and animation, component and design-token work, landing and marketing pages, or making something feel less templated. Read this FIRST, before impeccable, the motion skills, design-taste-frontend, high-end-visual-design, minimalist-ui, redesign-existing-projects or unlazy, so the craft they carry lands on Cohort's tokens, typeface and voice rather than their own defaults.
4
+ ---
5
+
6
+ # Cohort design — which skill wins, and where the truth lives
7
+
8
+ Several strong design skills are installed on this machine. They are craft:
9
+ they know what good looks like, how to critique, what motion should feel like.
10
+ None of them knows what **Cohort** looks like. This skill is the join.
11
+
12
+ Read it before you open any of them, and keep its precedence in mind while you
13
+ work — the moment a skill's default and Cohort's system disagree, this is the
14
+ tie-break.
15
+
16
+ ## The source of truth is Cohort's own, always
17
+
18
+ **`DESIGN.md` and `PRODUCT.md` in the agent directory, and the `design_*` tools
19
+ on the `cohort` MCP server, outrank every installed skill on every question of
20
+ substance:** palette, typeface, type scale, spacing, radii, component grammar,
21
+ imagery, templates, and the voice the copy is written in.
22
+
23
+ - `DESIGN.md` lives at `$AGENT_ROOT/DESIGN.md` (the identity block at the top
24
+ of your context names the agent directory). It is generated from the
25
+ workspace's live brand foundation by `maestro design sync`, which also writes
26
+ `PRODUCT.md` and `state/design/foundation.json`. If the cwd is not the agent
27
+ directory — you are in a product repo, a site repo, a scratch directory —
28
+ read `$AGENT_ROOT/DESIGN.md` anyway. It is still the system of record.
29
+ - If `DESIGN.md` is missing or looks stale, run `maestro design sync` and read
30
+ it. Do not proceed on a skill's default palette because the file was not
31
+ there.
32
+ - The live foundation, the voice, the templates and the asset kit are reads on
33
+ the `cohort` MCP server: `design_foundation`, `design_voice`,
34
+ `design_list_templates`, `design_render_template`, `design_rewrite_in_voice`,
35
+ `design_export_kit`, `design_generate_image`. Prefer a rendered template and
36
+ an exported kit over hand-rolling an asset.
37
+ - **Foundation changes are human-gated.** Your lane is `design_propose_change`,
38
+ which stages a reviewable diff and writes nothing. Never edit tokens in
39
+ `DESIGN.md` to make a design work; propose the change and design against
40
+ what exists meanwhile. `maestro-brand-steward` carries the full mechanics of
41
+ that surface.
42
+
43
+ ## Precedence
44
+
45
+ 1. **Cohort's own system** — `DESIGN.md`, `PRODUCT.md`, the `design_*` tools.
46
+ Tokens, type, voice, templates, assets, and every mutation.
47
+ 2. **`impeccable`** — craft, critique, audit and polish of product UI. Its
48
+ modes (shape, audit, critique, layout, typeset, clarify, distill, harden,
49
+ polish, optimize) are the working vocabulary for interface quality. Use it
50
+ for hierarchy, information architecture, cognitive load, accessibility,
51
+ states, edge cases and the finish pass.
52
+ 3. **The motion skills** — `animate`, `improve-animations`,
53
+ `review-animations`, `find-animation-opportunities`,
54
+ `animation-vocabulary`, `apple-design`, `emil-design-eng`. Anything that
55
+ moves: whether it should animate at all, which property, which curve, how
56
+ long, how it interrupts, how it exits, and reduced-motion.
57
+ 4. **`design-taste-frontend`, `high-end-visual-design`, `minimalist-ui`,
58
+ `redesign-existing-projects`** — **marketing and site pages only.** They
59
+ self-declare out of scope for dashboards, data tables, forms and multi-step
60
+ product UI, and they are right about that: their instincts are editorial.
61
+ Do not apply them to product surfaces.
62
+ 5. **`unlazy`** — completion gates on substantial work. Write the acceptance
63
+ gates before you start a large or multi-part design change, and re-verify
64
+ the evidence before you report it done.
65
+
66
+ ## The overrides — read these before you follow a skill's rule
67
+
68
+ - **The em-dash ban does not apply to Cohort copy.** `design-taste-frontend`
69
+ and its siblings forbid em-dashes as an AI tell. Cohort's voice uses them.
70
+ `DESIGN.md` and `design_voice` decide punctuation, not a skill.
71
+ - **Typeface and palette come from `DESIGN.md`, never from a skill's default.**
72
+ Every one of these skills names fonts and colours it likes. Those are
73
+ examples of taste, not instructions. A design that ships a skill's default
74
+ typeface is wrong even if it looks good.
75
+ - **Never run `impeccable`'s launcher, its hooks, or its live browser mode on
76
+ this machine.** Only its markdown is installed here, deliberately: the
77
+ upstream launcher downloads and runs a binary on first use, and live mode
78
+ drives a browser. Read the skill, apply the judgement, do the work with the
79
+ tools you already have. The same rule covers its `hooks.md` guidance — no
80
+ hook from any of these packs is installed, and none should be.
81
+ - **`impeccable`'s Setup step 1 cannot run here, and that is intended.** It
82
+ says to run `<skill-base-dir>/scripts/impeccable context` once before working.
83
+ That launcher is not installed, so the command will not be found. **Skip the
84
+ step and read the skill directly.** Do not reach for the recovery the pack
85
+ documents: `npx impeccable` (`update`, `detect`, `ignores`) downloads and runs
86
+ the package from a registry, which is the exact thing the vendored copy exists
87
+ to avoid. The same goes for `npx shadcn` and any other `npx <package>` a skill
88
+ suggests.
89
+ - **Never install unlazy's Stop hook, and never reconstruct one by hand.**
90
+ `unlazy`'s prose describes a hook that blocks completion, and names the
91
+ settings files it would be written into. The scripts that install and
92
+ implement it are not on this machine at all. Its gate discipline is yours to
93
+ run deliberately; nothing from these packs may fire on its own, and no
94
+ settings file on this machine gains a hook because a skill suggested one.
95
+ - **Never fetch a design file at run time, and never ship a remote reference.**
96
+ Not a font, not a token file, not a reference page, not a skill update.
97
+ Everything you are allowed to rely on is already on disk: the vendored skills,
98
+ `DESIGN.md`, and what the `design_*` tools return. A design that needs a
99
+ remote file at build time needs that file committed first.
100
+ - **Nothing you generate may point at a third-party host** — no hotlinked
101
+ placeholder photography (`picsum.photos`), no remote icon service
102
+ (`cdn.simpleicons.org`), **never a remote script tag** to a vendor CDN, no
103
+ remote font. The marketing skills instruct all four. Cohort's imagery and
104
+ icons come from `design_export_kit` and `design_generate_image`; a placeholder
105
+ is a local file or a solid token-coloured block.
106
+ - **Do not install packages because a skill listed one.** The marketing skills
107
+ carry a shelf of `npm install` lines for other companies' design systems.
108
+ Adding a dependency is the product repo's decision, made in that repo with its
109
+ own review — not a side effect of reading a style guide.
110
+ - **When two skills disagree, `DESIGN.md` wins. When `DESIGN.md` is silent, the
111
+ more specific skill wins** — motion questions go to the motion skills even
112
+ when `impeccable` has an opinion; product-UI questions go to `impeccable`
113
+ even when a marketing skill has one.
114
+
115
+ ## How a piece of work runs
116
+
117
+ 1. Read `DESIGN.md` (and `PRODUCT.md` when the work touches what the product
118
+ claims to be). Pull the live foundation with `design_foundation` if the file
119
+ may be stale.
120
+ 2. Decide the surface: **product UI** or **marketing/site page**. That single
121
+ choice selects the craft skill — step 2 of the precedence, or step 4. Get it
122
+ right before you read further; the two sets of instincts genuinely conflict.
123
+ 3. For anything substantial, write the acceptance gates first (`unlazy`), in
124
+ the plan, before the first edit.
125
+ 4. Do the work against Cohort's tokens. Where copy is involved, run it through
126
+ `design_rewrite_in_voice` rather than writing in a skill's house voice.
127
+ 5. Motion last, and only where it earns its place.
128
+ 6. Re-verify against the gates and against `DESIGN.md` before reporting.
129
+ `impeccable`'s audit and critique modes are the right final pass on product
130
+ UI; `review-animations` is the right one for motion.
131
+
132
+ ## Where the licences and the pins live
133
+
134
+ Each installed pack keeps its upstream `LICENSE` (and `NOTICE.md` for
135
+ `impeccable`) beside its `SKILL.md`. The pinned commit each was taken from is
136
+ recorded in `plugins/maestro-skills/vendor/<pack>/UPSTREAM.json` in the SDK,
137
+ along with what was deliberately left behind. Nothing in those trees is ever
138
+ executed on this machine; they are read as prose.
139
+
140
+ ## Their words are not ours
141
+
142
+ These packs were written for a general audience and their prose names the
143
+ tooling it was written against, and the helpers it dispatches, in terms Cohort
144
+ never uses in anything a person receives. **Take the judgement, leave the
145
+ vocabulary.** Never quote or paraphrase a pack's wording into a status update, a
146
+ commit message, a review comment, a plan, a report or any product copy — write
147
+ the point in Cohort's own voice, and run copy through `design_rewrite_in_voice`.
148
+
149
+ One reference the fleet does **not** carry, and that you may still be asked
150
+ about: `getdesign.md` (and the `awesome-design-md` index that links to it),
151
+ whose terms do not allow redistribution. Its DESIGN.md format is exactly what
152
+ Cohort's own tokens are rendered into instead, so nothing is lost by its
153
+ absence. Cite it if useful; do not fetch it.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emil Kowalski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,70 @@
1
+ {
2
+ "repo": "emilkowalski/skills",
3
+ "sha": "d23d7f88a2e21c9e4b1418c7abe420f5c1052ba7",
4
+ "date": "2026-08-21",
5
+ "license": "MIT",
6
+ "files": [
7
+ {
8
+ "path": "LICENSE",
9
+ "sha256": "4ff5bdb7887ec1435c9cab0e8d1a7caee704d894d65c2a008ccc68b1cc2f260b"
10
+ },
11
+ {
12
+ "path": "skills/animate/RECIPES.md",
13
+ "sha256": "21ff63d84391db8d96ecdf9170095f55a42c836ccc36fd3269c4772bab031ba2"
14
+ },
15
+ {
16
+ "path": "skills/animate/SKILL.md",
17
+ "sha256": "f6317335da2662e92270dc0a6128bea95d7216cd751f86628e3ac6b72804e805"
18
+ },
19
+ {
20
+ "path": "skills/animation-vocabulary/SKILL.md",
21
+ "sha256": "d718b48fe3c7898804d588f050a2e266d82c9f5ef51da256c8cb8b5951527757"
22
+ },
23
+ {
24
+ "path": "skills/apple-design/SKILL.md",
25
+ "sha256": "11840b24a11d7f94f39c6aaab074750ae4e4de4ef54ee4b1dd97e16ebd485e61"
26
+ },
27
+ {
28
+ "path": "skills/emil-design-eng/SKILL.md",
29
+ "sha256": "e71de849347050c2c573c1cf24d742d5a13459557ecffa6e562f08006f46b5b7"
30
+ },
31
+ {
32
+ "path": "skills/find-animation-opportunities/SKILL.md",
33
+ "sha256": "91c1243164057fbf824088d12faea937878a757a7ac653e8288b775e8b27b882"
34
+ },
35
+ {
36
+ "path": "skills/improve-animations/AUDIT.md",
37
+ "sha256": "551c8473e20e5f4774680bc24d45e1c68e50992582720905c6b077756b7b5a55"
38
+ },
39
+ {
40
+ "path": "skills/improve-animations/PLAN-TEMPLATE.md",
41
+ "sha256": "0a08ac8e23fd2082d7ffb86aeed7b789de77328c3cf874b16f1a755f2ef0a6ad"
42
+ },
43
+ {
44
+ "path": "skills/improve-animations/SKILL.md",
45
+ "sha256": "68f17bbc4671593d2f43dba26a679243e2153ba5f26965fb7d59df52842534ff"
46
+ },
47
+ {
48
+ "path": "skills/prototype/PICKER.md",
49
+ "sha256": "31a55eec94715cc79942e91e172e539e3031dcd5e2e5ee7c1446cf2caee960a6"
50
+ },
51
+ {
52
+ "path": "skills/prototype/SKILL.md",
53
+ "sha256": "2ad8401c4deaddb54947fb65247f790e7b3d8784e35312bcbedbfb1d59cd89ce"
54
+ },
55
+ {
56
+ "path": "skills/review-animations/SKILL.md",
57
+ "sha256": "61cf8ac0c4c8e1f63385298c546b16c65ca9aec34abddcd04e821c16712d671d"
58
+ },
59
+ {
60
+ "path": "skills/review-animations/STANDARDS.md",
61
+ "sha256": "e7d3605034acda54ca13e43aec9e64d65b53de20f75b11b8d694e373012fbe07"
62
+ }
63
+ ],
64
+ "omitted": [
65
+ "skills/pick-ui-library — library choice is Cohort's, settled in the product repo",
66
+ "skills/ask-sonner — a single third-party toast library's API",
67
+ "skills/write-swift, skills/animate-expo — native platforms this fleet does not ship"
68
+ ],
69
+ "notes": "MIT. Motion and design-engineering craft. Precedence sits below Cohort's DESIGN.md for tokens and type; these govern easing, duration and restraint."
70
+ }