@synap-core/workspace-templates 0.30.0 → 0.32.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 (45) hide show
  1. package/README.md +13 -5
  2. package/agents/hermes.agent.yaml +3 -0
  3. package/dist/{define-DqoItRiR.d.ts → define-DHDi8C33.d.ts} +19 -18
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +428 -239
  6. package/dist/index.js.map +1 -1
  7. package/dist/wire.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/brand-library.yaml +0 -6
  10. package/src/builder-dev-fold.template.test.ts +2 -3
  11. package/src/builder-dev-playbooks.template.test.ts +7 -5
  12. package/src/builder.yaml +60 -42
  13. package/src/business-model.yaml +20 -1
  14. package/src/civic-france.yaml +9 -0
  15. package/src/content-creation.yaml +1 -1
  16. package/src/content-os.yaml +22 -18
  17. package/src/crm-absorbs-outreach.template.test.ts +1 -1
  18. package/src/crm-build-plan.template.test.ts +1 -1
  19. package/src/crm.yaml +5 -14
  20. package/src/decisions-wave.template.test.ts +7 -5
  21. package/src/define.primary-surface.test.ts +9 -0
  22. package/src/define.ts +4 -1
  23. package/src/finance.yaml +10 -6
  24. package/src/foundation.yaml +1 -2
  25. package/src/hr.yaml +3 -2
  26. package/src/intent.test.ts +43 -2
  27. package/src/internal-runbook.yaml +0 -1
  28. package/src/kanban-grouping.test.ts +72 -0
  29. package/src/legal.yaml +4 -4
  30. package/src/lifecycle-coverage.test.ts +7 -64
  31. package/src/lifecycle-declarations.test.ts +70 -74
  32. package/src/marketing.yaml +4 -3
  33. package/src/meetings-knowledge.yaml +1 -2
  34. package/src/operations.yaml +13 -9
  35. package/src/process-contract.validate.test.ts +40 -55
  36. package/src/project-management.yaml +2 -2
  37. package/src/publish-official-lib.test.ts +3 -2
  38. package/src/research-base.yaml +12 -5
  39. package/src/research-decision.yaml +18 -2
  40. package/src/ship-a-change.template.test.ts +3 -2
  41. package/src/social.yaml +4 -1
  42. package/src/start.test.ts +22 -0
  43. package/src/templates.ts +324 -183
  44. package/src/types.ts +28 -18
  45. package/src/validate.ts +143 -77
package/dist/wire.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as PackageDefinitionOutput, X as WorkspaceDefinitionInput } from './define-DqoItRiR.js';
1
+ import { g as PackageDefinitionOutput, X as WorkspaceDefinitionInput } from './define-DHDi8C33.js';
2
2
  import '@synap-core/types/presentation';
3
3
  import '@synap-core/types/space-brief';
4
4
  import '@synap-core/types/capability-intents';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synap-core/workspace-templates",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "type": "module",
5
5
  "description": "Canonical workspace definitions — YAML source + typed exports for browser, backend, CLI, and marketplace",
6
6
  "repository": {
@@ -1171,8 +1171,6 @@ playbooks:
1171
1171
  Deprecating an asset is a person's call.
1172
1172
  subjectProfile:
1173
1173
  profileSlug: brand-asset
1174
- statusProperty: asset-status
1175
- humanOnlyStatuses: [deprecated]
1176
1174
  params:
1177
1175
  - { name: asset, type: entity, label: Brand asset, required: true }
1178
1176
  executor: is-agent
@@ -1212,8 +1210,6 @@ playbooks:
1212
1210
  is checked against. Retiring a guide is a person's call.
1213
1211
  subjectProfile:
1214
1212
  profileSlug: brand-voice-guide
1215
- statusProperty: voice-status
1216
- humanOnlyStatuses: [deprecated]
1217
1213
  params:
1218
1214
  - { name: guide, type: entity, label: Brand voice guide, required: true }
1219
1215
  executor: is-agent
@@ -1249,8 +1245,6 @@ playbooks:
1249
1245
  guideline is a person's call.
1250
1246
  subjectProfile:
1251
1247
  profileSlug: content-guideline
1252
- statusProperty: guideline-status
1253
- humanOnlyStatuses: [deprecated]
1254
1248
  params:
1255
1249
  - { name: guideline, type: entity, label: Content guideline, required: true }
1256
1250
  executor: is-agent
@@ -45,9 +45,8 @@ const INTRINSIC_COLUMNS = new Set(["title"]);
45
45
  type View = {
46
46
  name: string;
47
47
  scopeProfileSlug?: string;
48
- groupBy?: string;
49
48
  sortBy?: string;
50
- config?: { columns?: unknown[] };
49
+ config?: { columns?: unknown[]; groupByField?: string };
51
50
  };
52
51
 
53
52
  const def = toWorkspaceDefinition("builder-workspace");
@@ -70,7 +69,7 @@ describe("builder-workspace — dev-dashboard fold (R7)", () => {
70
69
  expect(props.size, `${name} scopes "${scope}" which declares no properties`).toBeGreaterThan(0);
71
70
 
72
71
  const named = [
73
- view!.groupBy,
72
+ view!.config?.groupByField,
74
73
  view!.sortBy,
75
74
  ...((view!.config?.columns as string[] | undefined) ?? []),
76
75
  ].filter((s): s is string => typeof s === "string");
@@ -35,16 +35,18 @@ describe("builder dev playbooks", () => {
35
35
  expect(pb?.subjectProfile?.activators).toBeUndefined();
36
36
  });
37
37
 
38
- it.each(DEV_PLAYBOOKS)("%s names a statusProperty wherever a stage moves a status", (name) => {
39
- const pb = find(name);
40
- const moves = (pb?.stages ?? []).some((s) => s.subjectStatus);
41
- if (moves) expect(pb?.subjectProfile?.statusProperty).toBeTruthy();
38
+ // One model decision 4 (7 Nov 2026): the process follows the kind's declared
39
+ // lifecycle — no statusProperty, and no end move (decision of 6 Nov).
40
+ it.each(DEV_PLAYBOOKS)("%s follows its kind's lifecycle and declares no end move", (name) => {
41
+ const pb = find(name) as Record<string, any> | undefined;
42
+ expect(pb?.subjectProfile?.statusProperty).toBeUndefined();
43
+ expect(pb?.subjectProfile?.onEnd).toBeUndefined();
44
+ expect(pb?.humanOnlyStatuses).toBeUndefined();
42
45
  });
43
46
 
44
47
  it("Fix a bug works the core `issue` kind through its declared lifecycle", () => {
45
48
  const pb = find("Fix a bug");
46
49
  expect(pb?.subjectProfile?.profileSlug).toBe("issue");
47
- expect(pb?.subjectProfile?.statusProperty).toBe("issueStatus");
48
50
  const issue = (getWorkspaceTemplate("builder-workspace")?.profiles ?? []).find((p) => p.slug === "issue") as
49
51
  | { lifecycle?: { property: string; options: { key: string; category: string }[] } }
50
52
  | undefined;
package/src/builder.yaml CHANGED
@@ -46,6 +46,8 @@ meta:
46
46
  # What this space PRODUCES (homes): the purpose word, then its kinds/roles.
47
47
  intent:
48
48
  label: Product
49
+ # The ONE primary output (One model decision 3): the feature is what the dev loop ships (Build a change works one).
50
+ primary: devplane_feature
49
51
  produces:
50
52
  - profile: devplane_feature
51
53
  - profile: devplane_story
@@ -720,6 +722,12 @@ profiles:
720
722
  icon: alert-triangle
721
723
  color: red
722
724
  description: A production incident with severity, status, and root cause tracking
725
+ lifecycle:
726
+ property: incidentStatus
727
+ options:
728
+ - { key: open, category: open }
729
+ - { key: investigating, category: active }
730
+ - { key: resolved, category: done }
723
731
  properties:
724
732
  - slug: severity
725
733
  label: Severity
@@ -1033,10 +1041,10 @@ profiles:
1033
1041
  # `issue` is a CORE kind: the pod seeds it on every pod (ensure-system-profiles.ts,
1034
1042
  # "an open bug or problem" → issue) with its ONE lifecycle `issueStatus`
1035
1043
  # (ensure-system-lifecycles.ts). Declared here to match the seed exactly, the way
1036
- # research-base declares `research`, so the Fix a bug playbook can name its
1037
- # statusProperty. Pod-local extras (severity, night-shift-run) are not core: not here.
1044
+ # research-base declares `research`, so the Fix a bug playbook follows its
1045
+ # lifecycle. Pod-local extras (severity, night-shift-run) are not core: not here.
1038
1046
  # ONE deviation from the seed: `wont_fix` is humanOnly (a person decides an issue
1039
- # is not worth fixing), which the playbook's humanOnlyStatuses below requires.
1047
+ # is not worth fixing).
1040
1048
  - slug: issue
1041
1049
  displayName: Issue
1042
1050
  icon: bug
@@ -1297,6 +1305,13 @@ profiles:
1297
1305
  icon: workflow
1298
1306
  color: blue
1299
1307
  description: A user flow or business process — a sequence of steps through system components
1308
+ lifecycle:
1309
+ property: workflowStatus
1310
+ options:
1311
+ - { key: documented, category: open }
1312
+ - { key: in-progress, category: active }
1313
+ - { key: needs-review, category: active }
1314
+ - { key: complete, category: done }
1300
1315
  properties:
1301
1316
  - slug: workflowStatus
1302
1317
  label: Status
@@ -1517,9 +1532,40 @@ views:
1517
1532
  slug: task-board
1518
1533
  type: kanban
1519
1534
  scopeProfileSlug: task
1520
- groupBy: task-status
1535
+ config:
1536
+ groupByField: task-status
1521
1537
  defaultView: true
1522
1538
  description: "All delegated tasks by status: Backlog → Queued → Running → Review → Done."
1539
+ # The flow boards (One model §8 item 3): each produced kind on a board grouped
1540
+ # by its lifecycle — columns are its states, a drag is the governed status move.
1541
+ - name: Feature Board
1542
+ slug: feature-board
1543
+ type: kanban
1544
+ scopeProfileSlug: devplane_feature
1545
+ description: Features by stage, Idea to Shipped.
1546
+ config:
1547
+ groupByField: featureStatus
1548
+ - name: Story Board
1549
+ slug: story-board
1550
+ type: kanban
1551
+ scopeProfileSlug: devplane_story
1552
+ description: User stories by status, Draft to Done.
1553
+ config:
1554
+ groupByField: storyStatus
1555
+ - name: Issue Board
1556
+ slug: issue-board
1557
+ type: kanban
1558
+ scopeProfileSlug: issue
1559
+ description: Bugs and problems by status, Open to Fixed.
1560
+ config:
1561
+ groupByField: issueStatus
1562
+ - name: Design Board
1563
+ slug: design-board
1564
+ type: kanban
1565
+ scopeProfileSlug: ui-design
1566
+ description: Designs by status, from draft to implemented.
1567
+ config:
1568
+ groupByField: design-status
1523
1569
  - name: Deployments
1524
1570
  type: calendar
1525
1571
  scopeProfileSlug: devplane_deployment
@@ -1547,13 +1593,15 @@ views:
1547
1593
  slug: service-health
1548
1594
  type: kanban
1549
1595
  scopeProfileSlug: devplane_service
1550
- groupBy: serviceStatus
1596
+ config:
1597
+ groupByField: serviceStatus
1551
1598
  description: Every service by health — healthy, degraded, down, maintenance.
1552
1599
  - name: Services by Type
1553
1600
  slug: services-by-type
1554
1601
  type: kanban
1555
1602
  scopeProfileSlug: devplane_service
1556
- groupBy: serviceType
1603
+ config:
1604
+ groupByField: serviceType
1557
1605
  description: Services grouped by what they are — API, worker, cron, database, cache, queue.
1558
1606
  - name: Deploy Timeline
1559
1607
  slug: deploy-timeline
@@ -1688,10 +1736,8 @@ playbooks:
1688
1736
  (update_entity with expectedStatus); never invent a status.
1689
1737
  subjectProfile:
1690
1738
  profileSlug: ui-design
1691
- statusProperty: design-status
1692
1739
  # No stage owns these: `approved` and `archived` are a person's call
1693
1740
  # (humanOnly on the lifecycle).
1694
- humanOnlyStatuses: [approved, archived]
1695
1741
  activators:
1696
1742
  - { on: manual, mode: propose }
1697
1743
  params:
@@ -1999,8 +2045,7 @@ playbooks:
1999
2045
  # THE DEV LOOP AS STORED CONFIG (plan v5 T2, 2026-10-27). One change = one
2000
2046
  # session on its feature: shaped → building → PR opened → checks green →
2001
2047
  # merged → deployed to lab → dogfooded → criteria verified. It FOLLOWS the
2002
- # feature's declared lifecycle (`featureStatus`, named as statusProperty, no
2003
- # second status): Shape writes spec, Build in_progress, PR opened in_review,
2048
+ # feature's declared lifecycle (`featureStatus`, no second status): Shape writes spec, Build in_progress, PR opened in_review,
2004
2049
  # Verify done (an agent's move into done proposes). The stages after PR
2005
2050
  # opened carry no status — the feature stays In review until it ships.
2006
2051
  #
@@ -2045,10 +2090,8 @@ playbooks:
2045
2090
  NEEDS-DOGFOOD.
2046
2091
  subjectProfile:
2047
2092
  profileSlug: devplane_feature
2048
- statusProperty: featureStatus
2049
2093
  # No stage owns these: Idea is before the change is shaped, Dropped is a
2050
2094
  # person's call (humanOnly on the lifecycle).
2051
- humanOnlyStatuses: [backlog, cancelled]
2052
2095
  activators:
2053
2096
  - { on: enters_status, status: spec, mode: propose, maxPendingApprovals: 5 }
2054
2097
  - { on: manual, mode: propose }
@@ -2192,24 +2235,18 @@ playbooks:
2192
2235
  # THE SIX DEV PLAYBOOKS (decision d05, founder instruction 2026-11-05): Build a
2193
2236
  # change, Fix a bug, Investigate, Review a change, Maintain, Fix main red.
2194
2237
  # Mirrored field by field from the six draft playbook proposals filed on the Builder
2195
- # pod (stored shape; humanOnlyStatuses lifted to the top-level wire spelling).
2238
+ # pod (stored shape). No end moves (decision of 6 Nov 2026): a cancelled or failed
2239
+ # session leaves its subject where it is.
2196
2240
  # ALL `status: draft` and NO activators: a pod keeps its own draft/active
2197
2241
  # choice on later template updates, and a person turns each on.
2198
2242
 
2199
2243
  # --- Build a change ---
2200
2244
  - name: Build a change
2201
- description: 'Dev set (decision d05), key: build. Works one feature (devplane_feature) from shaped criteria to a verified change on main; its stages follow the feature''s lifecycle (spec, in_progress, in_review, done), and a cancelled or failed session puts the feature back to spec so nothing is left half-claimed. One feature per session, never two subjects (founder default, 2026-11-05). A quick change is this playbook with size small (no plan); a night shift run is this playbook with unattended true. No activators until binding is turned on; the shape would be {on: enters_status, status: spec, size: per_thing, mode: propose}, the trigger Ship a change uses today. Reuses Ship a change (2c5aa553) stage text and evidence keys; absorbs AI Dev Session and Quick change once they are re-pointed. Draft until the founder activates it.'
2245
+ description: 'Dev set (decision d05), key: build. Works one feature (devplane_feature) from shaped criteria to a verified change on main; its stages follow the feature''s lifecycle (spec, in_progress, in_review, done); a cancelled or failed session leaves the feature where it is. One feature per session, never two subjects (founder default, 2026-11-05). A quick change is this playbook with size small (no plan); a night shift run is this playbook with unattended true. No activators until binding is turned on; the shape would be {on: enters_status, status: spec, size: per_thing, mode: propose}, the trigger Ship a change uses today. Reuses Ship a change (2c5aa553) stage text and evidence keys; absorbs AI Dev Session and Quick change once they are re-pointed. Draft until the founder activates it.'
2202
2246
  goalTemplate: 'Build {feature} through one session and one PR per repo (or gated commits on main in a private repo), size {size}. SHAPE: write the end state and 2-5 binary criteria on this session (plan skill, skipped when size is small). BUILD: branch <type>/<slug> from origin/main in your own worktree, commit in verified slices, run the smallest gate that answers with its negative control, quote the output. LAND: follow the land stages (Change proposed, Checks green, Landed on main, Deployed to lab, Dogfooded, Criteria verified). Unattended: {unattended}. When unattended is true, ask no person, stop at a draft PR or a gated branch, never merge, push main or deploy, and post one "Look at first:" paragraph in the room.'
2203
2247
  scope: session
2204
2248
  subjectProfile:
2205
- onEnd:
2206
- failed: spec
2207
- cancelled: spec
2208
2249
  profileSlug: devplane_feature
2209
- statusProperty: featureStatus
2210
- humanOnlyStatuses:
2211
- - backlog
2212
- - cancelled
2213
2250
  params:
2214
2251
  - name: feature
2215
2252
  type: entity
@@ -2364,21 +2401,11 @@ playbooks:
2364
2401
 
2365
2402
  # --- Fix a bug ---
2366
2403
  - name: Fix a bug
2367
- description: 'Dev set (decision d05), key: fix. Works an issue at its root: reproduce it with a failing test, fix the canonical path, a clean-context review, then the shared land stages. Its stages follow the issue''s lifecycle (open, investigating, fixed); a cancelled or failed session puts the issue back to open, as Fix a finding does. Small fixes may be batched (founder default, 2026-11-05): several issues in one session, one as the subject and the rest as session targets; today a stage moves only the subject, and the session end moves the subject and every target still open. No activators until binding is turned on; a batch would be {on: count, status: open, min: 3} or {on: schedule, cron, status: open} (both one session per moment). A night shift run is this playbook with unattended true. Reuses Bugfix (8c335e2e), Night shift: build an issue (c101dd75) and Ship a change (2c5aa553). Draft until the founder activates it.'
2404
+ description: 'Dev set (decision d05), key: fix. Works an issue at its root: reproduce it with a failing test, fix the canonical path, a clean-context review, then the shared land stages. Its stages follow the issue''s lifecycle (open, investigating, fixed); a cancelled or failed session leaves the issue where it is, as Fix a finding does. Small fixes may be batched (founder default, 2026-11-05): several issues in one session, one as the subject and the rest as session targets; today a stage moves only the subject. No activators until binding is turned on; a batch would be {on: count, status: open, min: 3} or {on: schedule, cron, status: open} (both one session per moment). A night shift run is this playbook with unattended true. Reuses Bugfix (8c335e2e), Night shift: build an issue (c101dd75) and Ship a change (2c5aa553). Draft until the founder activates it.'
2368
2405
  goalTemplate: 'Fix the issue {issue} at its root and prove it with a test that fails before the fix and passes after. REPRODUCE (recall first, smallest reproduction, root cause file:line), FIX on the canonical path in your own worktree, a clean-context REVIEW, then LAND through the shared land stages (Change proposed, Checks green, Landed on main, Deployed to lab, Dogfooded, Criteria verified). Unattended: {unattended}. When unattended is true, ask no person, stop at a draft PR or a gated branch, never merge, push main or deploy; if no code fix is right, write the root cause as a pod document; post one "Look at first:" paragraph in the room.'
2369
2406
  scope: session
2370
2407
  subjectProfile:
2371
- onEnd:
2372
- failed: open
2373
- cancelled: open
2374
2408
  profileSlug: issue
2375
- statusProperty: issueStatus
2376
- # Added to the pod draft (it has none; the catalog's lifecycle-coverage check
2377
- # needs every option owned): `open` is where an issue starts and a failed or
2378
- # cancelled session returns it (onEnd), `wont_fix` is a person's call.
2379
- humanOnlyStatuses:
2380
- - open
2381
- - wont_fix
2382
2409
  params:
2383
2410
  - name: issue
2384
2411
  type: entity
@@ -2926,7 +2953,7 @@ playbooks:
2926
2953
 
2927
2954
  # Founder's pod playbook 18abd1ea, as it runs since 6 Nov 2026: no onEnd
2928
2955
  # (decision f06dc507: a cancelled or failed session leaves its finding where it
2929
- # is) and no statusProperty (it follows the kind's lifecycle, backend#853).
2956
+ # is); it follows the kind's lifecycle (backend#853).
2930
2957
  # The two pod automations that move it (PR merged → merged, deployed →
2931
2958
  # verified) are not shipped: their session_update step scopes on
2932
2959
  # `whenPlaybookId`, a pod uuid, and there is no catalog-stable reference yet.
@@ -2936,15 +2963,6 @@ playbooks:
2936
2963
  scope: session
2937
2964
  subjectProfile:
2938
2965
  profileSlug: finding
2939
- statusProperty: status
2940
- # The options no stage owns (catalog lifecycle coverage): `open` is where a
2941
- # finding starts, `wont_fix` and `superseded` are a call made outside the fix.
2942
- # With no onEnd nothing moves a finding into them, and a finding in them has
2943
- # no stage to bind a session to anyway.
2944
- humanOnlyStatuses:
2945
- - open
2946
- - wont_fix
2947
- - superseded
2948
2966
  params: []
2949
2967
  executor: is-agent
2950
2968
  status: active
@@ -76,6 +76,15 @@ profiles:
76
76
  displayName: Question
77
77
  icon: help-circle
78
78
  profileKind: kind
79
+ # The pod seeds this lifecycle on every pod (ensure-system-lifecycles.ts);
80
+ # declared here to match the seed exactly, so a playbook can follow it.
81
+ lifecycle:
82
+ property: questionStatus
83
+ options:
84
+ - { key: open, category: open }
85
+ - { key: exploring, category: active }
86
+ - { key: answered, category: done }
87
+ - { key: abandoned, category: lost }
79
88
  properties:
80
89
  - slug: questionStatus
81
90
  label: Status
@@ -91,6 +100,15 @@ profiles:
91
100
  displayName: Decision
92
101
  icon: gavel
93
102
  profileKind: kind
103
+ # The pod seeds this lifecycle on every pod (ensure-system-lifecycles.ts);
104
+ # declared here to match the seed exactly, so a playbook can follow it.
105
+ lifecycle:
106
+ property: decisionStatus
107
+ options:
108
+ - { key: proposed, category: open }
109
+ - { key: accepted, category: done }
110
+ - { key: superseded, category: lost }
111
+ - { key: rejected, category: lost }
94
112
  properties:
95
113
  - slug: decisionStatus
96
114
  label: Status
@@ -312,7 +330,8 @@ views:
312
330
  slug: business-model-open-questions
313
331
  type: kanban
314
332
  scopeProfileSlug: question
315
- groupBy: questionStatus
333
+ config:
334
+ groupByField: questionStatus
316
335
  description: The nine component questions and everything they spawned, by status.
317
336
 
318
337
  # ── Seeds: the interrogation, not fake data ────────────────────────────────
@@ -39,6 +39,15 @@ profiles:
39
39
  icon: help-circle
40
40
  color: blue
41
41
  description: An open question driving a civic-data investigation.
42
+ # The pod seeds this lifecycle on every pod (ensure-system-lifecycles.ts);
43
+ # declared here to match the seed exactly, so a playbook can follow it.
44
+ lifecycle:
45
+ property: questionStatus
46
+ options:
47
+ - { key: open, category: open }
48
+ - { key: exploring, category: active }
49
+ - { key: answered, category: done }
50
+ - { key: abandoned, category: lost }
42
51
  properties:
43
52
  - slug: questionStatus
44
53
  label: Status
@@ -115,7 +115,7 @@ views:
115
115
  scopeProfileSlug: content-program
116
116
  description: Programs grouped by status — a running scoreboard for the content operation.
117
117
  config:
118
- groupBy: status
118
+ groupByField: status
119
119
 
120
120
  onboarding:
121
121
  goal: >
@@ -28,7 +28,6 @@ meta:
28
28
  label: Content
29
29
  promise: Every post from idea to published, on one calendar.
30
30
  suggestFor: [writer, creator, marketer, musician, photographer]
31
- profile: post
32
31
  views: [content-posts, content-calendar]
33
32
  example:
34
33
  - { title: "Why we left spreadsheets", values: { post-status: Idea, post-format: Article } }
@@ -46,6 +45,8 @@ meta:
46
45
  # What this space PRODUCES (homes): the purpose word, then its kinds/roles.
47
46
  intent:
48
47
  label: Content
48
+ # The ONE primary output (One model decision 3): a published post is what the editorial flow produces (Produce Content).
49
+ primary: post
49
50
  produces:
50
51
  - profile: post
51
52
  - profile: newsletter
@@ -713,6 +714,13 @@ profiles:
713
714
  color: "#0EA5E9"
714
715
  description: "An ordered run of posts that teaches one thing end to end — the curriculum axis of the content engine."
715
716
  scope: WORKSPACE
717
+ lifecycle:
718
+ property: series-status
719
+ options:
720
+ - { key: Planned, category: open }
721
+ - { key: Running, category: active }
722
+ - { key: Complete, category: done }
723
+ - { key: Abandoned, category: lost }
716
724
  properties:
717
725
  - slug: series-status
718
726
  label: Status
@@ -1551,7 +1559,8 @@ views:
1551
1559
  slug: content-posts
1552
1560
  type: kanban
1553
1561
  scopeProfileSlug: post
1554
- groupBy: post-status
1562
+ config:
1563
+ groupByField: post-status
1555
1564
  description: Content posts by status.
1556
1565
  # The editorial calendar — the "it lands on a calendar" promise. Posts render
1557
1566
  # on a month grid keyed on their Publish Date. The calendar renderer resolves
@@ -1600,7 +1609,8 @@ views:
1600
1609
  slug: content-newsletters
1601
1610
  type: kanban
1602
1611
  scopeProfileSlug: newsletter
1603
- groupBy: newsletter-status
1612
+ config:
1613
+ groupByField: newsletter-status
1604
1614
  description: Newsletter issues by status.
1605
1615
  # ⚠️ NO `Journey Board` (matrix) view here — DELIBERATE, do not "restore" it.
1606
1616
  # A coverage grid is the right idea and this is the wrong substrate today:
@@ -1806,15 +1816,15 @@ entityLinks:
1806
1816
  # `subjectProfile` binds a playbook to a kind so the Capture → Session matcher
1807
1817
  # (playbooks.subject_profile->>'profileSlug') can offer it when one is captured.
1808
1818
  #
1809
- # THE PROCESS CONTRACT (lifecycle-bound playbooks). `subjectProfile.statusProperty`
1810
- # names the kind's lifecycle select (`post-status`); each stage's `subjectStatus`
1811
- # is the value that stage IS — advancing into the stage writes it to the subject
1812
- # (governed), and the subject entering it advances an open run to the stage.
1813
- # `humanOnlyStatuses` lists the values no stage owns because a person sets them.
1819
+ # THE PROCESS CONTRACT (lifecycle-bound playbooks). A playbook FOLLOWS the kind's
1820
+ # declared lifecycle (`post-status`); each stage's `subjectStatus` is the value
1821
+ # that stage IS — advancing into the stage writes it to the subject (governed),
1822
+ # and the subject entering it advances an open run to the stage. A value no stage
1823
+ # owns is derived by the pod; a person-only value is `humanOnly` on the lifecycle.
1814
1824
  # `activators` are DECLARATIONS the pod compiles into governed propose-mode rules
1815
1825
  # at install — never hand-written `automations:`. `relationToSubject` is the edge
1816
1826
  # written output → subject when a slot is satisfied. Validated by validate.ts
1817
- # (`checkProcessContract`) and lifecycle-coverage.test.ts.
1827
+ # (`checkProcessContract`).
1818
1828
  playbooks:
1819
1829
  - name: Produce Content
1820
1830
  # ONE playbook for every format (plan W3.3): text / visual / video are
@@ -1868,10 +1878,8 @@ playbooks:
1868
1878
  by a person at Publish.
1869
1879
  subjectProfile:
1870
1880
  profileSlug: post
1871
- statusProperty: post-status
1872
1881
  # Idea is where a capture lands and Archived is a person's call — no stage
1873
1882
  # owns either. The activator below starts the run once the post is Briefed.
1874
- humanOnlyStatuses: [Idea, Archived]
1875
1883
  activators:
1876
1884
  - { on: enters_status, status: Briefed, mode: propose }
1877
1885
  params:
@@ -2158,8 +2166,7 @@ playbooks:
2158
2166
 
2159
2167
  # One take, several videos (D2, D3, 29 Oct). The take is a content-asset of
2160
2168
  # Kind recording; it has no lifecycle (its state is derived from what was
2161
- # made from it), so this process declares no statusProperty and its stages
2162
- # move no status. The person decides at Keep, through one question per
2169
+ # made from it), so its stages move no status. The person decides at Keep, through one question per
2163
2170
  # proposed piece (a slot ask: the door Content Studio and Relay answer), not
2164
2171
  # a stage gate: keeping a piece commits nothing outside the pod, and the raw
2165
2172
  # take is never touched. The per-piece slots are declared by the agent as
@@ -2321,8 +2328,8 @@ playbooks:
2321
2328
  # never one run per post (a subject-bound playbook's `schedule:` field fans
2322
2329
  # out per entity; an activator's `schedule` and a per_moment `manual` do
2323
2330
  # not: api `run-playbook.ts` runWorkerBatch). The `subjectProfile` names the
2324
- # kind those workers read; there are no stages and no `statusProperty`, so it
2325
- # never moves a post's status.
2331
+ # kind those workers read; there are no stages, so it never moves a post's
2332
+ # status.
2326
2333
  # Run by hand ("Re-plan" on Content Studio's calendar, or the pod's button)
2327
2334
  # and once a week (founder, report 22fec5ee §6, 3 Nov 2026). The weekly one
2328
2335
  # compiles to a governed rule the owner can pause on the pod; both propose
@@ -2455,7 +2462,6 @@ playbooks:
2455
2462
  and link it derived_from the original. Never republish the original as is.
2456
2463
  subjectProfile:
2457
2464
  profileSlug: post
2458
- statusProperty: post-status
2459
2465
  activators:
2460
2466
  # A copy or repost sent for another account (post-copy-of) is not repurposed: its original is.
2461
2467
  - { on: enters_status, status: Published, mode: propose, unless: { property: post-copy-of, present: true } }
@@ -2480,8 +2486,6 @@ playbooks:
2480
2486
  a person confirms it went out.
2481
2487
  subjectProfile:
2482
2488
  profileSlug: newsletter
2483
- statusProperty: newsletter-status
2484
- humanOnlyStatuses: [Archived]
2485
2489
  params:
2486
2490
  - { name: newsletter, type: entity, label: Newsletter issue, required: true }
2487
2491
  executor: is-agent
@@ -114,7 +114,7 @@ describe("CRM absorbs Outreach & Comms", () => {
114
114
  expect((reply.stages ?? []).map((s) => s.subjectStatus).filter(Boolean)).toEqual(["replied", "bounced", "no_response"]);
115
115
 
116
116
  const outreach = playbookOf("Personalized Outreach");
117
- expect(outreach.subjectProfile).toMatchObject({ profileSlug: "campaign", statusProperty: "campaign-status" });
117
+ expect(outreach.subjectProfile).toEqual({ profileSlug: "campaign" });
118
118
  expect(outputsOf(outreach)).toEqual([
119
119
  { kind: "messaging", label: "Proposed first messages", relationToSubject: "none" },
120
120
  ]);
@@ -88,7 +88,7 @@ describe("CRM build plan — catalog changes", () => {
88
88
 
89
89
  it("A6: one follow-up process drafts proposed messages on weekdays; no outreach process opens a deal", () => {
90
90
  const pb = playbookOf(crm, "Follow up on leads in outreach");
91
- expect(pb.subjectProfile).toMatchObject({ profileSlug: "lead", statusProperty: "leadStage" });
91
+ expect(pb.subjectProfile).toEqual({ profileSlug: "lead" });
92
92
  expect(pb.activators).toContainEqual({ on: "schedule", cron: "0 7 * * 1-5", status: "outreach", mode: "propose" });
93
93
  expect(outputsOf(pb).map((o) => o.kind)).toEqual(["messaging"]);
94
94
  expect(pb.stages?.at(-1)).toMatchObject({ key: "send", gate: { kind: "human" } });
package/src/crm.yaml CHANGED
@@ -51,7 +51,6 @@ meta:
51
51
  label: Sales
52
52
  promise: Leads and deals, each with a dated next step.
53
53
  suggestFor: [small-business, founder]
54
- profile: deal
55
54
  views: [deal-pipeline, deals-table, deals-list]
56
55
  example:
57
56
  - { title: "Acme renewal", values: { commercialStage: negotiating, value: 12000, nextStep: "Send revised quote Thu" } }
@@ -69,6 +68,8 @@ meta:
69
68
  # What this space PRODUCES (homes): the purpose word, then its kinds/roles.
70
69
  intent:
71
70
  label: Sales
71
+ # The ONE primary output (One model decision 3): the won deal; the lead-becomes-client role flow is not modeled yet (S8, decision 10).
72
+ primary: deal
72
73
  produces:
73
74
  - profile: lead
74
75
  - profile: client
@@ -829,10 +830,9 @@ views:
829
830
  slug: outreach-touches
830
831
  type: kanban
831
832
  scopeProfileSlug: outreach-touch
832
- groupBy: status
833
833
  description: Every outreach message, grouped by where it stands — drafted for a person to send, through replied.
834
834
  config:
835
- groupBy: status
835
+ groupByField: status
836
836
  cardFields: [channel, touchType, sentDate]
837
837
 
838
838
  onboarding:
@@ -971,8 +971,6 @@ playbooks:
971
971
  step converts the lead. Never mark the lead a client.
972
972
  subjectProfile:
973
973
  profileSlug: lead
974
- statusProperty: leadStage
975
- humanOnlyStatuses: [new, outreach, engaged, nurturing, converted, disqualified]
976
974
  # Manual only (founder decision 2026-10-27): the `created` worker started
977
975
  # one run per lead and burned the budget; a list is scored in ONE run by
978
976
  # "Score a list" instead, and a person qualifies the leads worth it.
@@ -1096,8 +1094,6 @@ playbooks:
1096
1094
  person's call — record why it was lost.
1097
1095
  subjectProfile:
1098
1096
  profileSlug: deal
1099
- statusProperty: commercialStage
1100
- humanOnlyStatuses: [lost]
1101
1097
  activators:
1102
1098
  - { on: created, mode: propose }
1103
1099
  params:
@@ -1696,8 +1692,8 @@ playbooks:
1696
1692
  # next step, and a deal is never opened. "Read the reply" keeps only the
1697
1693
  # touches marked sent before the channels. A lifecycle READER of leadStage:
1698
1694
  # no stage moves the lead; nurturing after the second follow-up is proposed
1699
- # through the door once the last date passes in silence. `statusProperty`
1700
- # is named because the schedule worker reads `outreach`.
1695
+ # through the door once the last date passes in silence. Its schedule worker
1696
+ # reads `outreach`, a value of the lead role's lifecycle (foundation).
1701
1697
  # Next action (CRM review 2026-10-27): a drafted message becomes the lead's
1702
1698
  # `nextAction` ("Send the drafted message to <name>", `nextActionAt` today),
1703
1699
  # and a message marked sent sets `nextActionAt` by the CADENCE (founder
@@ -1783,7 +1779,6 @@ playbooks:
1783
1779
  reads it and proposes the next step.
1784
1780
  subjectProfile:
1785
1781
  profileSlug: lead
1786
- statusProperty: leadStage
1787
1782
  activators:
1788
1783
  # Weekdays 07:00 UTC, one batch over the leads in outreach.
1789
1784
  - { on: schedule, cron: "0 7 * * 1-5", status: outreach, mode: propose }
@@ -1906,8 +1901,6 @@ playbooks:
1906
1901
  meeting. Pausing or cancelling is a person's call.
1907
1902
  subjectProfile:
1908
1903
  profileSlug: campaign
1909
- statusProperty: campaign-status
1910
- humanOnlyStatuses: [paused, cancelled]
1911
1904
  activators:
1912
1905
  - { on: created, mode: propose }
1913
1906
  params:
@@ -2004,11 +1997,9 @@ playbooks:
2004
1997
  person does. Opened is recorded by a person.
2005
1998
  subjectProfile:
2006
1999
  profileSlug: outreach-touch
2007
- statusProperty: status
2008
2000
  # No stage of this process owns these: drafted and sent come before it
2009
2001
  # starts, opened is a person's record, replaced closes a draft moved into
2010
2002
  # the person's channel.
2011
- humanOnlyStatuses: [drafted, sent, opened, replaced]
2012
2003
  activators:
2013
2004
  - { on: enters_status, status: sent, mode: run }
2014
2005
  params:
@@ -54,15 +54,18 @@ describe("decision 1 — Business Developer folds into CRM", () => {
54
54
  it("'Qualify a lead' is a process on the lead ROLE's leadStage, started by a person", () => {
55
55
  const crm = tpl("crm");
56
56
  const pb = playbookOf(crm, "Qualify a lead");
57
- expect(pb?.subjectProfile).toMatchObject({ profileSlug: "lead", statusProperty: "leadStage" });
57
+ expect(pb?.subjectProfile).toEqual({ profileSlug: "lead" });
58
58
  // Manual only since 2026-10-27 (founder): a `created` worker ran once per
59
59
  // lead and burned the budget; "Score a list" scores a whole list in one run.
60
60
  expect(pb?.activators).toEqual([{ on: "manual", mode: "propose" }]);
61
61
  // The subject is a ROLE, declared pod-wide by foundation.
62
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());
63
+ // It owns researching and qualified; the other leadStage values belong to a
64
+ // person or another flow — derived by the pod, never listed (One model
65
+ // decision 4, 7 Nov 2026; conflict C2 on `outreach` closed).
66
+ const owned = (pb?.stages ?? []).map((s) => s.subjectStatus).filter(Boolean);
67
+ expect(owned).toEqual(["researching", "qualified"]);
68
+ for (const s of owned) expect(propOf(tpl("foundation"), "lead", "leadStage")?.enumValues).toContain(s);
66
69
  // Only the last stage, reached when the person chose pursue, moves the lead
67
70
  // to qualified. It opens no deal (CRM build plan A4, 2026-10-22): a deal is
68
71
  // opened when a person records a booked meeting, which converts the lead.
@@ -130,7 +133,6 @@ describe("decision 4 — 'Deliver a deliverable' starts when the deliverable ent
130
133
  expect(pb.stages?.[0]?.subjectStatus).toBeUndefined();
131
134
  expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("not-started");
132
135
  // `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
136
  expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("cancelled");
135
137
  expect(errorsOf(tpl("operations"))).toEqual([]);
136
138
  });
@@ -90,6 +90,7 @@ describe('workspace primary-surface conversion', () => {
90
90
  slug: 'pipeline',
91
91
  type: 'kanban',
92
92
  scopeProfileSlug: 'thing',
93
+ config: { groupByField: 'status' },
93
94
  },
94
95
  ],
95
96
  layout: {
@@ -100,6 +101,14 @@ describe('workspace primary-surface conversion', () => {
100
101
  },
101
102
  },
102
103
  });
104
+ // The kanban groups by a real select of its kind (view-kanban-group-*).
105
+ source.profiles = [
106
+ {
107
+ slug: 'thing',
108
+ displayName: 'Thing',
109
+ properties: [{ slug: 'status', label: 'Status', valueType: 'string', inputType: 'select', enumValues: ['open', 'done'] }],
110
+ },
111
+ ] as typeof source.profiles;
103
112
 
104
113
  expect(toWorkspaceDefinition(source).definition.layoutConfig).toMatchObject(
105
114
  {