@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.
- package/bin/maestro.mjs +6 -2
- package/docs/guides/front-door-session.md +86 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/global-setup-extras.mjs +44 -0
- package/lib/cli/global-setup-extras.test.mjs +95 -0
- package/lib/cli/session.mjs +11 -1
- package/lib/cli/session.test.mjs +17 -6
- package/lib/collective/global-config.mjs +5 -0
- package/lib/collective/global-config.test.mjs +5 -0
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/lib/telemetry/collect.mjs +357 -5
- package/lib/telemetry/collect.test.mjs +285 -0
- package/package.json +1 -1
- package/plugins/maestro-skills/plugin.json +4 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon.mjs +108 -0
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
- package/scripts/daemon/cadence-consumer.mjs +46 -22
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/local-triggers/autoupdate.test.mjs +33 -3
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- 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.
|
|
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
|
+
}
|