@synap-core/workspace-templates 0.12.0 → 0.24.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 (237) hide show
  1. package/README.md +509 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +1496 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +494 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-DqoItRiR.d.ts +1370 -0
  56. package/dist/index.d.ts +174 -900
  57. package/dist/index.js +26447 -24075
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +27 -4
  63. package/retired.yaml +136 -0
  64. package/skills/app-factory-build-slice/SKILL.md +70 -0
  65. package/skills/app-factory-build-slice.catalog.json +17 -0
  66. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  67. package/skills/app-factory-design-canvas.catalog.json +17 -0
  68. package/skills/app-factory-design-review/SKILL.md +105 -0
  69. package/skills/app-factory-design-review.catalog.json +18 -0
  70. package/skills/app-factory-research/SKILL.md +123 -0
  71. package/skills/app-factory-research.catalog.json +17 -0
  72. package/skills/bookmark-enrichment/SKILL.md +40 -0
  73. package/skills/carousel-format/carousel-format.skill.json +18 -0
  74. package/skills/collage-broll/collage-broll.skill.json +20 -0
  75. package/skills/content-taste/content-taste.skill.json +18 -0
  76. package/skills/creative-director/creative-director.skill.json +18 -0
  77. package/skills/design-critique/SKILL.md +138 -0
  78. package/skills/design-critique.catalog.json +12 -0
  79. package/skills/designing-with-real-components/SKILL.md +140 -0
  80. package/skills/designing-with-real-components.catalog.json +12 -0
  81. package/skills/edit-loop/edit-loop.skill.json +25 -0
  82. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  83. package/skills/mapping-the-server/SKILL.md +54 -0
  84. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  85. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  86. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  87. package/skills/reading-client-progress/SKILL.md +65 -0
  88. package/skills/routing-to-channels/SKILL.md +54 -0
  89. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  90. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  91. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  92. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  93. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  94. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  95. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  96. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  97. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  98. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  99. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  100. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  101. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  102. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  103. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  104. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  105. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  106. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  107. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  108. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  109. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  110. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  111. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  112. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  113. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  114. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  115. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  116. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  117. package/skills/video-direction/video-direction.skill.json +20 -0
  118. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  119. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  120. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  121. package/src/agency-os.yaml +122 -0
  122. package/src/agent-os-skill-slugs.test.ts +65 -0
  123. package/src/apify-social-reader.test.ts +317 -0
  124. package/src/base.yaml +85 -39
  125. package/src/blockchain-ecosystem.yaml +1 -0
  126. package/src/brand-library.yaml +525 -14
  127. package/src/builder-dev-fold.template.test.ts +113 -0
  128. package/src/builder.yaml +779 -91
  129. package/src/business-model-catalog-description.test.ts +47 -0
  130. package/src/business-model-grp.template.test.ts +214 -0
  131. package/src/business-model.yaml +296 -49
  132. package/src/catalog-safety.test.ts +198 -0
  133. package/src/catalog.template-items.test.ts +75 -0
  134. package/src/catalog.ts +66 -2
  135. package/src/concept-guard.test.ts +28 -8
  136. package/src/concept-guard.ts +2 -2
  137. package/src/content-creation.template.test.ts +52 -0
  138. package/src/content-creation.yaml +192 -0
  139. package/src/content-os.edit-loop.template.test.ts +210 -0
  140. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  141. package/src/content-os.template.test.ts +349 -0
  142. package/src/content-os.video-takes.template.test.ts +338 -0
  143. package/src/content-os.work-a-take.template.test.ts +125 -0
  144. package/src/content-os.yaml +1873 -39
  145. package/src/content-studio-retired.template.test.ts +82 -0
  146. package/src/corpus-flow-validity.test.ts +4 -5
  147. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  148. package/src/crm-build-plan.template.test.ts +128 -0
  149. package/src/crm-lists.template.test.ts +96 -0
  150. package/src/crm-messages-cadence.template.test.ts +246 -0
  151. package/src/crm-one-send-door.template.test.ts +65 -0
  152. package/src/crm.yaml +1563 -181
  153. package/src/decisions-wave.template.test.ts +186 -0
  154. package/src/define.ts +146 -17
  155. package/src/design-to-implementation.template.test.ts +94 -0
  156. package/src/domain-template-type.test.ts +75 -0
  157. package/src/ecosystem.yaml +27 -66
  158. package/src/enterprise-os.yaml +10 -134
  159. package/src/facecam-edit.audio-studio.test.ts +188 -0
  160. package/src/fal-generate-image.test.ts +200 -0
  161. package/src/fal-media-lab-pod-files.test.ts +180 -0
  162. package/src/finance.yaml +254 -1
  163. package/src/foundation.yaml +78 -38
  164. package/src/frame-host-modules.d.mts +6 -0
  165. package/src/frame-host-modules.mjs +58 -0
  166. package/src/generate.mjs +9 -3
  167. package/src/github-merge.test.ts +210 -0
  168. package/src/grants.yaml +46 -17
  169. package/src/hr.yaml +39 -6
  170. package/src/index.ts +36 -1
  171. package/src/intent.test.ts +161 -0
  172. package/src/internal-runbook.yaml +19 -1
  173. package/src/jev.test.ts +356 -0
  174. package/src/legal.yaml +33 -1
  175. package/src/life-os.yaml +51 -240
  176. package/src/lifecycle-coverage.test.ts +124 -0
  177. package/src/lifecycle-declarations.test.ts +252 -0
  178. package/src/lifecycle-presets.test.ts +468 -0
  179. package/src/lifecycle-presets.ts +494 -0
  180. package/src/lifecycle-stuck.test.ts +46 -0
  181. package/src/lifecycle-waits.parity.test.ts +319 -0
  182. package/src/marketing.yaml +108 -4
  183. package/src/meetings-knowledge.yaml +18 -0
  184. package/src/networking.yaml +23 -2
  185. package/src/operations.yaml +304 -4
  186. package/src/pipeline-views.template.test.ts +88 -0
  187. package/src/playbook-goal-placeholders.test.ts +57 -0
  188. package/src/postiz-contract.test.ts +134 -0
  189. package/src/presentation-declarations.test.ts +174 -0
  190. package/src/process-contract.validate.test.ts +352 -0
  191. package/src/project-management.yaml +39 -740
  192. package/src/publish-body.d.mts +29 -0
  193. package/src/publish-body.mjs +74 -0
  194. package/src/publish-body.test.ts +74 -0
  195. package/src/publish-official-lib.test.ts +176 -0
  196. package/src/replicate-rvm.test.ts +504 -0
  197. package/src/research-base.yaml +119 -20
  198. package/src/research-decision.yaml +60 -3
  199. package/src/reserved-profile-slugs.test.ts +81 -0
  200. package/src/retired-goal-kinds.test.ts +52 -0
  201. package/src/role-reference-and-step-domain.test.ts +192 -0
  202. package/src/seed-obligation.test.ts +1 -6
  203. package/src/send-message-maps.test.ts +227 -0
  204. package/src/shared-role-ssot.test.ts +40 -2
  205. package/src/ship-a-change.template.test.ts +177 -0
  206. package/src/skills.test.ts +170 -0
  207. package/src/social.yaml +85 -27
  208. package/src/space-packages.freshness.test.ts +97 -0
  209. package/src/space-packages.generated.ts +40 -0
  210. package/src/space-packages.test.ts +125 -0
  211. package/src/space-packages.ts +73 -0
  212. package/src/start.test.ts +188 -0
  213. package/src/stripe-capability.test.ts +192 -0
  214. package/src/task-intents.test.ts +293 -0
  215. package/src/template-documents.test.ts +158 -0
  216. package/src/templates.ts +20907 -21464
  217. package/src/types.ts +475 -61
  218. package/src/unipile-linkedin-find-profile.test.ts +183 -0
  219. package/src/unipile-publish-post.test.ts +162 -0
  220. package/src/validate-flow.drift.test.ts +2 -3
  221. package/src/validate-flow.ts +22 -2
  222. package/src/validate-onboarding.test.ts +59 -0
  223. package/src/validate.test.ts +71 -4
  224. package/src/validate.ts +1416 -7
  225. package/src/wire-core.ts +231 -0
  226. package/src/wire.test.ts +159 -0
  227. package/src/wire.ts +107 -0
  228. package/views/bookmark-views-pack.view.json +39 -0
  229. package/views/event-views-pack.view.json +45 -0
  230. package/views/task-views-pack.view.json +53 -0
  231. package/src/agent-fleet.yaml +0 -1645
  232. package/src/business-developer.template.test.ts +0 -351
  233. package/src/business-developer.yaml +0 -860
  234. package/src/content-studio.yaml +0 -361
  235. package/src/dev-dashboard.yaml +0 -990
  236. package/src/outreach-comms.yaml +0 -486
  237. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The founder's decisions of 2026-10-12 (pod file 00c1b2fe-ef98-41fa-86cf-801dfab12cd1, decisions wave),
3
+ * pinned against the REAL YAML corpus (`readYamlTemplates`, the source the
4
+ * build and the publish both read) and the real validator.
5
+ *
6
+ * 1. Business Developer folds into CRM: "Qualify a lead" runs on the `lead`
7
+ * ROLE's leadStage; "Advance a commercial proposal" is gone; the Synap-fee
8
+ * and commission model went to the private arch-sales, which was retired
9
+ * the same day with the-arch (The Arch is no longer in use);
10
+ * business-developer
11
+ * is a tombstone pointing at crm, and nothing installs it any more.
12
+ * 4. "Deliver a deliverable" starts when a deliverable ENTERS in-progress.
13
+ * 5. Billing: per-engagement milestone invoices, and a monthly recurring
14
+ * process on revenue-stream whose schedule the pod can actually create.
15
+ * 6. A new brand voice guide is a draft.
16
+ *
17
+ * Does NOT cover: runtime behaviour on a pod (a role activator firing on
18
+ * attach, the facet status write) — that is the backend lane's (R1).
19
+ */
20
+
21
+ import { describe, expect, it } from "vitest";
22
+ import { readYamlTemplates } from "./generate.mjs";
23
+ import type { WorkspaceYaml } from "./types.js";
24
+ import { validateTemplate } from "./validate.js";
25
+ import { loadRetired } from "../scripts/catalog-lib.mjs";
26
+
27
+ const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
28
+ const tpl = (slug: string, c: WorkspaceYaml[] = corpus) => {
29
+ const t = c.find((x) => x.meta.slug === slug);
30
+ if (!t) throw new Error(`template ${slug} not in the corpus`);
31
+ return t;
32
+ };
33
+ const profileOf = (t: WorkspaceYaml, slug: string) => t.profiles.find((p) => p.slug === slug);
34
+ const propOf = (t: WorkspaceYaml, profile: string, prop: string) =>
35
+ profileOf(t, profile)?.properties.find((p) => p.slug === prop);
36
+ const playbookOf = (t: WorkspaceYaml, name: string) => t.playbooks?.find((p) => p.name === name);
37
+ const errorsOf = (t: WorkspaceYaml, c: WorkspaceYaml[] = corpus) =>
38
+ validateTemplate(t, { corpus: c }).errors.map((e) => `${e.rule} ${e.path}`);
39
+
40
+ describe("decision 1 — Business Developer folds into CRM", () => {
41
+ it("non-vacuity: the corpus is the real one", () => {
42
+ expect(corpus.length).toBeGreaterThanOrEqual(25);
43
+ expect(corpus.some((t) => t.meta.slug === "crm")).toBe(true);
44
+ });
45
+
46
+ it("business-developer is a tombstone replaced by crm, and no live template depends on it", async () => {
47
+ expect(corpus.some((t) => t.meta.slug === "business-developer")).toBe(false);
48
+ const { retired } = await loadRetired();
49
+ expect(retired.find((r) => r.slug === "business-developer")).toMatchObject({ kind: "workspace", replacedBy: "crm" });
50
+ const dependents = corpus.filter((t) => (t.dependencies ?? []).some((d) => d.slug === "business-developer"));
51
+ expect(dependents.map((t) => t.meta.slug)).toEqual([]);
52
+ });
53
+
54
+ it("'Qualify a lead' is a process on the lead ROLE's leadStage, started by a person", () => {
55
+ const crm = tpl("crm");
56
+ const pb = playbookOf(crm, "Qualify a lead");
57
+ expect(pb?.subjectProfile).toMatchObject({ profileSlug: "lead", statusProperty: "leadStage" });
58
+ // Manual only since 2026-10-27 (founder): a `created` worker ran once per
59
+ // lead and burned the budget; "Score a list" scores a whole list in one run.
60
+ expect(pb?.activators).toEqual([{ on: "manual", mode: "propose" }]);
61
+ // The subject is a ROLE, declared pod-wide by foundation.
62
+ expect(profileOf(tpl("foundation"), "lead")).toMatchObject({ profileKind: "role", scope: "shared" });
63
+ // Every leadStage value is a stage's status or the person's.
64
+ const owned = new Set([...(pb?.stages ?? []).map((s) => s.subjectStatus), ...(pb?.humanOnlyStatuses ?? [])]);
65
+ expect([...owned].filter(Boolean).sort()).toEqual([...(propOf(tpl("foundation"), "lead", "leadStage")?.enumValues ?? [])].sort());
66
+ // Only the last stage, reached when the person chose pursue, moves the lead
67
+ // to qualified. It opens no deal (CRM build plan A4, 2026-10-22): a deal is
68
+ // opened when a person records a booked meeting, which converts the lead.
69
+ expect(pb?.stages?.at(-1)).toMatchObject({ key: "pursue", subjectStatus: "qualified" });
70
+ expect(pb?.stages?.flatMap((s) => s.expectedOutputs ?? [])).toEqual([]);
71
+ expect(pb?.expectedOutputs).toBeUndefined();
72
+ });
73
+
74
+ it("the validator reads a role subject's statuses from the role (a wrong one is refused)", () => {
75
+ expect(errorsOf(tpl("crm"))).toEqual([]);
76
+ const c = structuredClone(corpus);
77
+ const pb = playbookOf(tpl("crm", c), "Qualify a lead")!;
78
+ pb.stages![0]!.subjectStatus = "won"; // a deal status, not a leadStage one
79
+ expect(errorsOf(tpl("crm", c), c)).toContain('stage-subject-status-unknown playbooks."Qualify a lead".stages.research.subjectStatus');
80
+ });
81
+
82
+ it("'Advance a commercial proposal' is gone everywhere; CRM's 'Win a deal' remains", () => {
83
+ const names = corpus.flatMap((t) => (t.playbooks ?? []).map((p) => p.name));
84
+ expect(names).not.toContain("Advance a commercial proposal");
85
+ expect(names).not.toContain("Qualify a CRM lead");
86
+ expect(playbookOf(tpl("crm"), "Win a deal")).toBeDefined();
87
+ });
88
+
89
+ it("CRM carries the moved lead/client fields, the campaign kind and member_of", () => {
90
+ const crm = tpl("crm");
91
+ for (const p of ["icpScore", "qualificationNotes", "nextAction", "outreachChannels", "convertedAt", "disqualificationReason"])
92
+ expect(propOf(crm, "lead", p), `lead.${p}`).toBeDefined();
93
+ for (const p of ["becameClientAt", "handoffStatus", "handoffDestination", "handoffNotes"])
94
+ expect(propOf(crm, "client", p), `client.${p}`).toBeDefined();
95
+ expect(profileOf(crm, "campaign")?.semanticSlug).toBe("marketing.campaign");
96
+ expect(crm.entityLinks?.filter((l) => l.type === "member_of").map((l) => l.sourceProfileSlug)).toEqual(["person", "company"]);
97
+ });
98
+
99
+ it("the Synap-fee and commission model left the catalog with arch-sales and the-arch, both tombstones", async () => {
100
+ const FEE = ["synapFeeModel", "synapFixedFee", "synapSuccessFeeRate", "synapFeeAmount", "sourcedByUserId", "commissionEntitlementId", "commissionStatus"];
101
+ const { retired } = await loadRetired();
102
+ for (const slug of ["arch-sales", "the-arch"]) {
103
+ expect(corpus.some((t) => t.meta.slug === slug), `${slug} is live`).toBe(false);
104
+ const stone = retired.find((r) => r.slug === slug);
105
+ expect(stone, `${slug} tombstone`).toMatchObject({ kind: "workspace", since: "2026-10-12" });
106
+ expect(stone?.replacedBy, `${slug} has no replacement`).toBeUndefined();
107
+ const dependents = corpus.filter((t) => (t.dependencies ?? []).some((d) => d.slug === slug));
108
+ expect(dependents.map((t) => t.meta.slug), `dependents of ${slug}`).toEqual([]);
109
+ }
110
+ // No live template's deal carries the fee model any more (networking's
111
+ // `intro` keeps its own copy of the fee vocabulary: a separate decision).
112
+ for (const p of FEE) {
113
+ const holders = corpus.filter((t) => !!propOf(t, "deal", p)).map((t) => t.meta.slug);
114
+ expect(holders, `deal.${p}`).toEqual([]);
115
+ }
116
+ const holders = (pred: (t: WorkspaceYaml) => boolean) => corpus.filter(pred).map((t) => t.meta.slug);
117
+ expect(holders((t) => !!profileOf(t, "crm-commission-entitlement"))).toEqual([]);
118
+ expect(holders((t) => (t.automations ?? []).some((a) => a.name === "Win and hand off"))).toEqual([]);
119
+ });
120
+ });
121
+
122
+ describe("decision 4 — 'Deliver a deliverable' starts when the deliverable enters in-progress", () => {
123
+ const pb = playbookOf(tpl("operations"), "Deliver a deliverable")!;
124
+
125
+ it("its one activator is entering in-progress", () => {
126
+ expect(pb.activators).toEqual([{ on: "enters_status", status: "in-progress", mode: "propose" }]);
127
+ });
128
+
129
+ it("no stage writes not-started (a run started at in-progress never moves the deliverable back)", () => {
130
+ expect(pb.stages?.[0]?.subjectStatus).toBeUndefined();
131
+ expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("not-started");
132
+ // `cancelled` (0.20.0, plan aac47c4a W5) is a person's call too: no stage writes it.
133
+ expect(pb.humanOnlyStatuses).toEqual(["not-started", "cancelled"]);
134
+ expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("cancelled");
135
+ expect(errorsOf(tpl("operations"))).toEqual([]);
136
+ });
137
+ });
138
+
139
+ describe("decision 5 — billing", () => {
140
+ it("an engagement chooses at-close (default) or per-deliverable billing", () => {
141
+ expect(propOf(tpl("operations"), "engagement", "engagement-billing-mode")).toMatchObject({
142
+ inputType: "select",
143
+ enumValues: ["at-close", "per-deliverable"],
144
+ constraints: { defaultValue: "at-close" },
145
+ });
146
+ });
147
+
148
+ it("an accepted deliverable may produce its milestone invoice (an optional slot)", () => {
149
+ const accepted = playbookOf(tpl("operations"), "Deliver a deliverable")!.stages!.find((s) => s.key === "accepted");
150
+ expect(accepted?.expectedOutputs?.map((o) => o.kind)).toEqual(["invoice"]);
151
+ // Owed by nobody (synap-backend 30a9ab85): an at-close run never shows it pending.
152
+ expect(accepted?.expectedOutputs?.[0]?.optional).toBe(true);
153
+ expect(accepted?.goal).toMatch(/per deliverable/);
154
+ });
155
+
156
+ it("'Bill a recurring fee' runs monthly over active monthly revenue streams, with a filter the pod keeps", () => {
157
+ const finance = tpl("finance");
158
+ const pb = playbookOf(finance, "Bill a recurring fee")!;
159
+ expect(pb.subjectProfile?.profileSlug).toBe("revenue-stream");
160
+ expect(pb.schedule).toMatchObject({ cron: "0 8 1 * *", enabled: true });
161
+ expect(pb.metadata).toMatchObject({ governance: { forceProposeWrites: true } });
162
+ // The pod's cron door keeps ONLY a string filter (readSubjectProfile), and
163
+ // refuses an unfiltered AI fan-out, so an object here = no schedule at all.
164
+ const filter = pb.subjectProfile?.filter;
165
+ expect(typeof filter).toBe("string");
166
+ const parsed = JSON.parse(filter as string) as Record<string, string>;
167
+ expect(Object.keys(parsed).length).toBeGreaterThan(0);
168
+ // Reachability: every filtered key is a property of revenue-stream and every value one of its options.
169
+ for (const [key, value] of Object.entries(parsed)) {
170
+ const prop = propOf(finance, "revenue-stream", key.replace(/^properties\./, ""));
171
+ expect(prop?.enumValues, key).toContain(value);
172
+ }
173
+ expect(pb.expectedOutputs?.map((o) => o.kind)).toEqual(["invoice"]);
174
+ expect(errorsOf(finance)).toEqual([]);
175
+ });
176
+
177
+ it("the drafted invoice is taken over by 'Bill a client' (its created activator)", () => {
178
+ expect(playbookOf(tpl("finance"), "Bill a client")?.activators).toContainEqual({ on: "created", mode: "propose" });
179
+ });
180
+ });
181
+
182
+ describe("settled item — a new brand voice guide is a draft", () => {
183
+ it("voice-status defaults to draft", () => {
184
+ expect(propOf(tpl("brand-library"), "brand-voice-guide", "voice-status")?.constraints).toEqual({ defaultValue: "draft" });
185
+ });
186
+ });
package/src/define.ts CHANGED
@@ -16,6 +16,8 @@ import type {
16
16
  WorkspacePrimarySurface,
17
17
  WorkspaceYaml, } from './types.js';
18
18
  import { getWorkspaceTemplate } from './templates.js';
19
+ import { resolveLifecyclePresets } from './lifecycle-presets.js';
20
+ import { spacePackageCells, type SpacePackageCellWire } from './space-packages.js';
19
21
 
20
22
  /**
21
23
  * A `TemplateAutomation` as the apply door expects it: `flowDefinition`, not
@@ -33,7 +35,8 @@ export interface PackageAutomationWire {
33
35
  status?: TemplateAutomation['status'];
34
36
  }
35
37
 
36
- function toAutomationWire(
38
+ /** The ONE automation projection (authored `flow` → wire `flowDefinition`), shared by workspaces and standalone automation packages. */
39
+ export function toAutomationWire(
37
40
  automations: TemplateAutomation[] | undefined
38
41
  ): PackageAutomationWire[] | undefined {
39
42
  return automations?.map((a) => ({
@@ -211,8 +214,9 @@ export function normalizeTemplateLayout(
211
214
  * `kind` is INFERRED from field presence when absent. `TemplateBentoBlock.kind`
212
215
  * is a required field in TypeScript, but the generated `templates.ts` casts the
213
216
  * parsed YAML `as unknown as Record<string, WorkspaceYaml>` — so the compiler
214
- * never checks the authored data against the type, and `agent-fleet.yaml`
215
- * authored 8 widget blocks with no `kind:` at all (bare `widgetType`). Routing
217
+ * never checks the authored data against the type, and the (since retired,
218
+ * R7 2026-09-27) `agent-fleet.yaml` authored 8 widget blocks with no `kind:`
219
+ * at all (bare `widgetType`). Routing
216
220
  * strictly on `kind` would silently DROP those — the exact failure mode this
217
221
  * function exists to end. Inferring by field mirrors what the pod itself does
218
222
  * (it splits by field and never reads `kind`), so the seam is total: a block
@@ -268,6 +272,8 @@ export interface WorkspaceDefinitionInput {
268
272
  proposalId?: string;
269
273
  profiles: WorkspaceYaml['profiles'];
270
274
  views: NonNullable<WorkspaceYaml['views']>;
275
+ /** Template documents — created by the pod with the views (`utils/template-documents.ts`). */
276
+ documents?: WorkspaceYaml['documents'];
271
277
  entityLinks: NonNullable<WorkspaceYaml['entityLinks']>;
272
278
  seedEntities: NonNullable<WorkspaceYaml['seedEntities']>;
273
279
  suggestedRelations?: WorkspaceYaml['suggestedRelations'];
@@ -330,6 +336,20 @@ export interface WorkspaceDefinitionInput {
330
336
  workspaceType?: NonNullable<WorkspaceYaml['workspace']>['workspaceType'];
331
337
  workspaceVisibility?: string;
332
338
  workspaceCapabilities?: string[];
339
+ /**
340
+ * THE DECLARED INTENT SINK (Phase 3). `taskIntents` is authored at the TOP
341
+ * level of the template, not inside `workspace`, and is forwarded verbatim to
342
+ * `workspaces.createFromDefinition`, which persists it to
343
+ * `settings.taskIntents` — where the intent resolver joins it against what is
344
+ * actually installed. No shape transform: the authored form IS the wire form.
345
+ */
346
+ taskIntents?: WorkspaceYaml['taskIntents'];
347
+ /**
348
+ * What the space PRODUCES (`meta.intent`), forwarded verbatim; the pod
349
+ * projects it into `settings.intent`. Same top-level field on the package
350
+ * door.
351
+ */
352
+ intent?: WorkspaceYaml['meta']['intent'];
333
353
  sourceRoles?: NonNullable<WorkspaceYaml['workspace']>['sourceRoles'];
334
354
  defaultSources?: NonNullable<WorkspaceYaml['workspace']>['defaultSources'];
335
355
  /** Per-workspace onboarding context — written to settings.onboarding. */
@@ -378,6 +398,30 @@ function toWorkspacePlaybooks(
378
398
  }));
379
399
  }
380
400
 
401
+ /**
402
+ * A template by slug (the bundled corpus, already compiled) or as authored
403
+ * YAML, with its lifecycle presets compiled (`adopts:` → flat options,
404
+ * generated `enumValues`; lifecycle-presets.ts), so both install doors carry
405
+ * the flat declaration the pod stores. An overlay that does not resolve throws,
406
+ * like an unknown slug: `validateTemplate` names it first on every catalog path.
407
+ */
408
+ function compiledTemplate(slugOrYaml: string | WorkspaceYaml): WorkspaceYaml {
409
+ const tpl: WorkspaceYaml | undefined =
410
+ typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
411
+ if (!tpl) {
412
+ throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
413
+ }
414
+ const { template, problems } = resolveLifecyclePresets(tpl);
415
+ if (problems.length > 0) {
416
+ throw new Error(
417
+ `Template "${String(tpl.meta?.slug)}": lifecycle presets do not resolve — ${problems
418
+ .map((p) => `${p.path}: ${p.message}`)
419
+ .join('; ')}`
420
+ );
421
+ }
422
+ return template;
423
+ }
424
+
381
425
  /**
382
426
  * Resolve a template (by slug or YAML object) into the createFromDefinition
383
427
  * payload. Pass `nameOverride` to rename the workspace at creation time.
@@ -386,12 +430,7 @@ export function toWorkspaceDefinition(
386
430
  slugOrYaml: string | WorkspaceYaml,
387
431
  nameOverride?: string
388
432
  ): ToDefinitionResult {
389
- const tpl: WorkspaceYaml | undefined =
390
- typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
391
-
392
- if (!tpl) {
393
- throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
394
- }
433
+ const tpl = compiledTemplate(slugOrYaml);
395
434
 
396
435
  const ws = tpl.workspace;
397
436
  const name = nameOverride ?? ws.name;
@@ -407,6 +446,9 @@ export function toWorkspaceDefinition(
407
446
  proposalId,
408
447
  profiles: tpl.profiles,
409
448
  views: tpl.views ?? [],
449
+ // Absent when the template has none, so a template without documents
450
+ // compiles to the same definition (and content hash) as before.
451
+ ...(tpl.documents?.length ? { documents: tpl.documents } : {}),
410
452
  entityLinks: tpl.entityLinks ?? [],
411
453
  seedEntities: tpl.seedEntities ?? [],
412
454
  suggestedRelations: tpl.suggestedRelations,
@@ -432,6 +474,11 @@ export function toWorkspaceDefinition(
432
474
  workspaceType: ws.workspaceType,
433
475
  workspaceVisibility: ws.visibility,
434
476
  workspaceCapabilities: ws.capabilities,
477
+ // Forwarded VERBATIM from the template's TOP level (not `workspace`) — the
478
+ // pod persists it to `settings.taskIntents` and reports any gap against what
479
+ // the installed capabilities actually provide. Authored form IS wire form.
480
+ taskIntents: tpl.taskIntents,
481
+ intent: tpl.meta.intent,
435
482
  sourceRoles: ws.sourceRoles,
436
483
  defaultSources: ws.defaultSources,
437
484
  // Workspace creation persists its own compact playbook parameter shape.
@@ -486,6 +533,7 @@ type WorkspaceDefinitionForwarded =
486
533
  | 'workspace' // flattened into workspaceName/description/workspaceType/…
487
534
  | 'profiles'
488
535
  | 'views'
536
+ | 'documents' // → carried verbatim; created with the views by the pod
489
537
  | 'entityLinks'
490
538
  | 'seedEntities'
491
539
  | 'suggestedRelations'
@@ -500,7 +548,11 @@ type WorkspaceDefinitionForwarded =
500
548
  | 'actionPlacements' // → settings.actionPlacements via applyPackagePostWorkspace
501
549
  | 'automations' // → flowAutomations → shared applyPackagePostWorkspace door
502
550
  | 'onboarding'
503
- | 'dependencies';
551
+ | 'dependencies'
552
+ // Phase 3 gave the declaration a sink: `createWorkspaceFromDefinition`
553
+ // accepts `taskIntents` and persists it verbatim to `settings.taskIntents`,
554
+ // where the intent resolver joins it against what is actually installed.
555
+ | 'taskIntents';
504
556
 
505
557
  /**
506
558
  * Keys deliberately NOT forwarded by `toWorkspaceDefinition`. Each needs a
@@ -508,8 +560,21 @@ type WorkspaceDefinitionForwarded =
508
560
  * else is a real gap and should say so.
509
561
  */
510
562
  type WorkspaceDefinitionDropped =
563
+ // A POST-workspace layer (rule rows via the pod's rule door), carried on the
564
+ // PACKAGE definition (`toPackageDefinition`), not the workspace door's input.
565
+ // The workspace door has no rules field; the rule reconcile converges them.
566
+ | 'rules'
567
+ // Same layer as `rules`: the skill is LINKED to the space by
568
+ // `applyPackagePostWorkspace` (`skills.workspaceId` + `scope:"workspace"`),
569
+ // which owns the row. The workspace door has no skills field, so forwarding
570
+ // it here would be a no-op — it is carried on the PACKAGE definition instead.
571
+ | 'skills'
511
572
  // Normalized into `layoutConfig.primarySurface`.
512
573
  | 'app'
574
+ // Same layer as `skills`: built into frame cells that `applyPackagePostWorkspace`
575
+ // installs in the space. The workspace door has no cells field; carried on
576
+ // the PACKAGE definition as `cells[]` instead.
577
+ | 'spacePackage'
513
578
  // The door DOES accept `extends`, but no template declares it today (0/21).
514
579
  // Forwarding is a no-op until one does — wire it then.
515
580
  | 'extends';
@@ -543,6 +608,12 @@ void _workspaceDefinitionLedger;
543
608
  export interface PackageDefinitionOutput {
544
609
  _meta: {
545
610
  slug: string;
611
+ /** The PACKAGE's own name (meta.name). For an overlay this differs from
612
+ * `workspaceName`, which names the workspace it composes onto. */
613
+ name?: string;
614
+ /** The PACKAGE's own description (meta.description) — what the catalog
615
+ * lists. Top-level `description` stays the workspace's (install copy). */
616
+ description?: string;
546
617
  icon?: string;
547
618
  color?: string;
548
619
  domain?: string;
@@ -556,10 +627,37 @@ export interface PackageDefinitionOutput {
556
627
  workspaceSubtype?: string;
557
628
  workspaceVisibility?: string;
558
629
  workspaceCapabilities?: string[];
630
+ /**
631
+ * The DECLARED intents, carried to the marketplace/CLI install path too.
632
+ * Without it here, a workspace installed from the marketplace would LOSE its
633
+ * declaration while a bundled one kept it — the same field behaving
634
+ * differently per install route, which is precisely the fork this package
635
+ * exists to prevent.
636
+ */
637
+ taskIntents?: WorkspaceYaml['taskIntents'];
638
+ /**
639
+ * What the space PRODUCES (`meta.intent`), TOP-LEVEL like `taskIntents` —
640
+ * never in `_meta`, which the CP rebuilds from its row columns on serve
641
+ * (`normalizePublishedDefinition`), so a key there never reaches a pod.
642
+ */
643
+ intent?: WorkspaceYaml['meta']['intent'];
644
+ /**
645
+ * The onboarding start (`meta.start`), TOP-LEVEL for the same reason as
646
+ * `intent`. Package door only: the pod and the CP read starts off the package.
647
+ */
648
+ start?: WorkspaceYaml['meta']['start'];
559
649
  sourceRoles?: WorkspaceYaml['workspace']['sourceRoles'];
560
650
  defaultSources?: WorkspaceYaml['workspace']['defaultSources'];
561
651
  profiles: WorkspaceYaml['profiles'];
562
652
  views: NonNullable<WorkspaceYaml['views']>;
653
+ /**
654
+ * Template documents — created by the pod with the views
655
+ * (`utils/template-documents.ts`); the Hub `PackageApplySchema` slots them.
656
+ * The CP publish door does NOT yet (it strips the field and refuses a
657
+ * `kind: "document"` sidebar item), so no official template may declare
658
+ * documents until it does.
659
+ */
660
+ documents?: WorkspaceYaml['documents'];
563
661
  suggestedEntities: NonNullable<WorkspaceYaml['seedEntities']>;
564
662
  suggestedRelations?: WorkspaceYaml['suggestedRelations'];
565
663
  displayTemplates?: WorkspaceYaml['displayTemplates'];
@@ -596,6 +694,24 @@ export interface PackageDefinitionOutput {
596
694
  */
597
695
  actionPlacements?: WorkspaceYaml['actionPlacements'];
598
696
  onboarding?: WorkspaceYaml['onboarding'];
697
+ /**
698
+ * Rules the template installs in its space — applied by the pod's
699
+ * `applyPackagePostWorkspace` through the ONE rule door and converged by the
700
+ * template-rule reconcile (three-way stamp on `metadata.rule.seed`).
701
+ */
702
+ rules?: WorkspaceYaml['rules'];
703
+ /**
704
+ * Skills the template declares for its space — linked to the new space by the
705
+ * pod's `applyPackagePostWorkspace` (approved skills only), which is why they
706
+ * travel on the PACKAGE definition and not the workspace door's input.
707
+ */
708
+ skills?: WorkspaceYaml['skills'];
709
+ /**
710
+ * The space package's built modules (`spacePackage` → `space-packages.ts`),
711
+ * installed by the pod's `applyPackagePostWorkspace` as sandboxed frame cells
712
+ * `cell:<_meta.slug>:<key>` — what the template's `app: {kind: stored}` opens.
713
+ */
714
+ cells?: SpacePackageCellWire[];
599
715
  /** Template-composition dependencies — resolved by the apply endpoint. */
600
716
  dependencies?: WorkspaceYaml['dependencies'];
601
717
  }
@@ -609,20 +725,18 @@ export interface PackageDefinitionOutput {
609
725
  export function toPackageDefinition(
610
726
  slugOrYaml: string | WorkspaceYaml
611
727
  ): PackageDefinitionOutput {
612
- const tpl: WorkspaceYaml | undefined =
613
- typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
614
-
615
- if (!tpl) {
616
- throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
617
- }
728
+ const tpl = compiledTemplate(slugOrYaml);
618
729
 
619
730
  const ws = tpl.workspace;
620
731
  // Same routing as the tRPC door — the union is split by `kind`, never dumped
621
732
  // whole into one field. See `splitBentoBlocks`.
622
733
  const bento = splitBentoBlocks(tpl.bento?.blocks);
734
+ const cells = spacePackageCells(tpl);
623
735
  return {
624
736
  _meta: {
625
737
  slug: tpl.meta.slug,
738
+ name: tpl.meta.name,
739
+ description: tpl.meta.description,
626
740
  icon: tpl.meta.icon,
627
741
  color: tpl.meta.color,
628
742
  domain: tpl.meta.domain,
@@ -635,10 +749,16 @@ export function toPackageDefinition(
635
749
  workspaceSubtype: ws.subtype,
636
750
  workspaceVisibility: ws.visibility,
637
751
  workspaceCapabilities: ws.capabilities,
752
+ // Same declaration, same wire field, on the marketplace path — see the
753
+ // `taskIntents` note on the PackageDefinition type above.
754
+ taskIntents: tpl.taskIntents,
755
+ ...(tpl.meta.intent ? { intent: tpl.meta.intent } : {}),
756
+ ...(tpl.meta.start ? { start: tpl.meta.start } : {}),
638
757
  sourceRoles: ws.sourceRoles,
639
758
  defaultSources: ws.defaultSources,
640
759
  profiles: tpl.profiles,
641
760
  views: tpl.views ?? [],
761
+ ...(tpl.documents?.length ? { documents: tpl.documents } : {}),
642
762
  suggestedEntities: tpl.seedEntities ?? [],
643
763
  suggestedRelations: tpl.suggestedRelations,
644
764
  displayTemplates: tpl.displayTemplates,
@@ -655,6 +775,9 @@ export function toPackageDefinition(
655
775
  automations: toAutomationWire(tpl.automations),
656
776
  actionPlacements: tpl.actionPlacements,
657
777
  onboarding: tpl.onboarding,
778
+ ...(tpl.rules?.length ? { rules: tpl.rules } : {}),
779
+ ...(tpl.skills?.length ? { skills: tpl.skills } : {}),
780
+ ...(cells ? { cells } : {}),
658
781
  dependencies: tpl.dependencies,
659
782
  };
660
783
  }
@@ -672,6 +795,7 @@ type PackageDefinitionForwarded =
672
795
  | 'workspace' // flattened into workspaceName/description/workspaceType/…
673
796
  | 'profiles'
674
797
  | 'views'
798
+ | 'documents' // → carried verbatim (Hub PackageApplySchema slots it)
675
799
  | 'seedEntities' // → suggestedEntities
676
800
  | 'suggestedRelations'
677
801
  | 'displayTemplates'
@@ -686,6 +810,9 @@ type PackageDefinitionForwarded =
686
810
  | 'automations' // → mapped to the apply door's flowDefinition wire shape
687
811
  | 'actionPlacements' // → carried verbatim (Hub inbound schema declares it — parity closed)
688
812
  | 'onboarding'
813
+ | 'rules' // → carried verbatim (applied post-workspace via the rule door)
814
+ | 'skills' // → carried verbatim (linked to the space post-workspace)
815
+ | 'spacePackage' // → cells (the built frame modules, installed post-workspace)
689
816
  | 'dependencies';
690
817
 
691
818
  /** Keys deliberately NOT forwarded by `toPackageDefinition`. */
@@ -696,7 +823,9 @@ type PackageDefinitionDropped =
696
823
  // (package-definition.ts:262), but `PackageDefinitionOutput` omits it. No
697
824
  // template declares it today (0/21), so this is latent, not live — wire it
698
825
  // when a template first uses one.
699
- | 'extends';
826
+ | 'extends'
827
+ // Phase 1 declaration with no sink yet — see `WorkspaceDefinitionDropped`.
828
+ | 'taskIntents';
700
829
 
701
830
  type UnclassifiedPackageDefinitionKey = Exclude<
702
831
  keyof WorkspaceYaml,
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The design loop on `ui-design` (Builder, "Design to implementation"):
3
+ * Generate writes 3 sibling variants, Critique judges them with the
4
+ * design-critique rubric before a person reviews.
5
+ *
6
+ * What this guards:
7
+ * 1. REACHABILITY: every property an `evidence` criterion of the process reads
8
+ * is a property `ui-design` declares (derived from the stages, not listed)
9
+ * — an evidence key with no property is a criterion that can never pass;
10
+ * 2. Critique sits between Generate and Review, writes no status (like
11
+ * Verify), is a check gate and loops to Generate;
12
+ * 3. CORPUS-WIDE: no process that PRODUCES things of its own subject kind
13
+ * starts `on: created` — each produced thing would propose another run of
14
+ * the same process (the variants of a design are ui-designs). The
15
+ * activator grammar has no filter to tell a request from its product.
16
+ * 4. the skills the goal tells the agent to load exist in the catalog.
17
+ *
18
+ * Not covered: the wording of the rubric, and whether the pod honours a
19
+ * `manual` worker — only the catalog side.
20
+ */
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { describe, expect, it } from "vitest";
24
+ import { WORKSPACE_TEMPLATES, getWorkspaceTemplate } from "./templates.js";
25
+
26
+ const builder = getWorkspaceTemplate("builder-workspace")!;
27
+ const pb = builder.playbooks!.find(
28
+ (p) => p.name === "Design to implementation"
29
+ )!;
30
+ const kind = builder.profiles.find((p) => p.slug === "ui-design")!;
31
+ type Stage = {
32
+ key: string;
33
+ subjectStatus?: string;
34
+ gate?: { kind: string };
35
+ onFail?: { toStage: string };
36
+ criteria?: { check?: { kind: string; evidenceKey?: string } }[];
37
+ expectedOutputs?: { kind: string }[];
38
+ };
39
+ const stages = (pb as unknown as { stages: Stage[] }).stages;
40
+
41
+ describe("Design to implementation — the design loop", () => {
42
+ it("every evidence key the process reads is a property ui-design declares", () => {
43
+ const keys = stages.flatMap((s) =>
44
+ (s.criteria ?? []).map((c) => c.check?.evidenceKey).filter(Boolean)
45
+ );
46
+ expect(keys.length).toBeGreaterThanOrEqual(4); // non-vacuity: design-json, critique, implementation-pr, implemented-story-id
47
+ const declared = new Set(kind.properties.map((p) => p.slug));
48
+ expect(keys.filter((k) => !declared.has(k!))).toEqual([]);
49
+ });
50
+
51
+ it("Critique sits between Generate and Review: no status, a check gate, rework to Generate", () => {
52
+ expect(stages.map((s) => s.key).slice(0, 3)).toEqual([
53
+ "generate",
54
+ "critique",
55
+ "review",
56
+ ]);
57
+ const critique = stages.find((s) => s.key === "critique")!;
58
+ expect(critique.subjectStatus).toBeUndefined();
59
+ expect(critique.gate).toEqual({ kind: "check" });
60
+ expect(critique.onFail).toEqual({ toStage: "generate" });
61
+ });
62
+
63
+ it("no process that produces its own subject kind starts on `created` (corpus-wide)", () => {
64
+ const selfProducing: string[] = [];
65
+ const offenders: string[] = [];
66
+ for (const t of Object.values(WORKSPACE_TEMPLATES))
67
+ for (const p of t.playbooks ?? []) {
68
+ const subject = p.subjectProfile?.profileSlug;
69
+ const ss = (p as unknown as { stages?: Stage[] }).stages ?? [];
70
+ const outs = [
71
+ ...ss.flatMap((s) => s.expectedOutputs ?? []),
72
+ ...((p as unknown as { expectedOutputs?: { kind: string }[] })
73
+ .expectedOutputs ?? []),
74
+ ];
75
+ if (!subject || !outs.some((o) => o.kind === subject)) continue;
76
+ selfProducing.push(`${t.meta.slug}:${p.name}`);
77
+ if ((p.activators ?? []).some((a) => a.on === "created"))
78
+ offenders.push(`${t.meta.slug}:${p.name}`);
79
+ }
80
+ expect(selfProducing).toContain(
81
+ "builder-workspace:Design to implementation"
82
+ ); // non-vacuity
83
+ expect(offenders).toEqual([]);
84
+ });
85
+
86
+ it("the skills the goal loads exist in the catalog", () => {
87
+ for (const slug of ["designing-with-real-components", "design-critique"]) {
88
+ expect(pb.goalTemplate).toContain(slug);
89
+ expect(
90
+ existsSync(join(__dirname, "..", "skills", slug, "SKILL.md"))
91
+ ).toBe(true);
92
+ }
93
+ });
94
+ });