@synap-core/workspace-templates 0.29.0 → 0.31.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 (44) hide show
  1. package/README.md +12 -4
  2. package/dist/{define-DqoItRiR.d.ts → define-DHDi8C33.d.ts} +19 -18
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +594 -241
  5. package/dist/index.js.map +1 -1
  6. package/dist/wire.d.ts +1 -1
  7. package/package.json +1 -1
  8. package/src/brand-library.yaml +0 -6
  9. package/src/builder-dev-fold.template.test.ts +2 -3
  10. package/src/builder-dev-playbooks.template.test.ts +7 -5
  11. package/src/builder.yaml +162 -32
  12. package/src/business-model.yaml +20 -1
  13. package/src/civic-france.yaml +9 -0
  14. package/src/content-creation.yaml +1 -1
  15. package/src/content-os.yaml +22 -18
  16. package/src/crm-absorbs-outreach.template.test.ts +1 -1
  17. package/src/crm-build-plan.template.test.ts +1 -1
  18. package/src/crm.yaml +5 -14
  19. package/src/decisions-wave.template.test.ts +7 -5
  20. package/src/define.primary-surface.test.ts +9 -0
  21. package/src/define.ts +4 -1
  22. package/src/finance.yaml +10 -6
  23. package/src/foundation.yaml +1 -2
  24. package/src/hr.yaml +3 -2
  25. package/src/intent.test.ts +44 -3
  26. package/src/internal-runbook.yaml +0 -1
  27. package/src/kanban-grouping.test.ts +72 -0
  28. package/src/legal.yaml +4 -4
  29. package/src/lifecycle-coverage.test.ts +7 -64
  30. package/src/lifecycle-declarations.test.ts +70 -74
  31. package/src/marketing.yaml +4 -3
  32. package/src/meetings-knowledge.yaml +1 -2
  33. package/src/operations.yaml +13 -9
  34. package/src/process-contract.validate.test.ts +40 -55
  35. package/src/project-management.yaml +2 -2
  36. package/src/publish-official-lib.test.ts +3 -2
  37. package/src/research-base.yaml +22 -15
  38. package/src/research-decision.yaml +18 -2
  39. package/src/ship-a-change.template.test.ts +3 -2
  40. package/src/social.yaml +4 -1
  41. package/src/start.test.ts +22 -0
  42. package/src/templates.ts +490 -185
  43. package/src/types.ts +28 -18
  44. package/src/validate.ts +143 -77
package/dist/index.js CHANGED
@@ -61,6 +61,9 @@ var PACKAGE_DEPENDENCY_KINDS = [
61
61
  /** @deprecated Legacy alias for `workflow` (CP 0049). Still accepted on the wire. */
62
62
  "automation"
63
63
  ];
64
+ function startProfileOf(meta) {
65
+ return meta.start?.profile ?? meta.intent?.primary;
66
+ }
64
67
  var START_VIEWS_MAX = 3;
65
68
  var START_EXAMPLE_RANGE = [4, 6];
66
69
  var START_STARTERS_RANGE = [3, 6];
@@ -3350,12 +3353,8 @@ var DATA = {
3350
3353
  "description": "Turn a request for a logo, image, icon or template into a draft built from the approved brand, and approve it with a person before anything uses it.",
3351
3354
  "goalTemplate": "Make this brand asset ({asset}); each stage moves its status. DRAFT: read the request and the approved brand only \u2014 approved colours, fonts, references and the active token set; never a draft \u2014 and load the creative-director skill to pick the recipe. Make the draft (or up to three variants, each its own asset derived from this one) and record the generator, prompt and parameters on it. APPROVE: a person approves one draft as final; with variants, the approval asks them to `choose` one, your recommendation first, and that choice is the approval. Until then nothing may use it. Deprecating an asset is a person's call.",
3352
3355
  "subjectProfile": {
3353
- "profileSlug": "brand-asset",
3354
- "statusProperty": "asset-status"
3356
+ "profileSlug": "brand-asset"
3355
3357
  },
3356
- "humanOnlyStatuses": [
3357
- "deprecated"
3358
- ],
3359
3358
  "params": [
3360
3359
  {
3361
3360
  "name": "asset",
@@ -3420,12 +3419,8 @@ var DATA = {
3420
3419
  "description": "Draft or revise the brand voice guide from what the brand already publishes, and approve it with a person.",
3421
3420
  "goalTemplate": "Write this brand voice guide ({guide}); each stage moves its status. DRAFT: read the brand identity, the approved assets and the last posts and emails the brand published; write the voice in a few traits, each with a do and a don't taken from real examples, and the words to use and avoid. APPROVE: a person approves the guide; then it is what every draft is checked against. Retiring a guide is a person's call.",
3422
3421
  "subjectProfile": {
3423
- "profileSlug": "brand-voice-guide",
3424
- "statusProperty": "voice-status"
3422
+ "profileSlug": "brand-voice-guide"
3425
3423
  },
3426
- "humanOnlyStatuses": [
3427
- "deprecated"
3428
- ],
3429
3424
  "params": [
3430
3425
  {
3431
3426
  "name": "guide",
@@ -3473,12 +3468,8 @@ var DATA = {
3473
3468
  "description": "Distil approved references into a content guideline (base, format or the frame for one goal), and approve it with a person.",
3474
3469
  "goalTemplate": "Write this content guideline ({guideline}); each stage moves its status. DRAFT: read the brand identity, the approved voice guide and the approved references; load the content-taste skill. Write what to take and what to avoid as rules a maker can follow, link the guideline to every reference it was distilled from, and fill the summary. For a frame guideline, set the goal and give the structure in order. APPROVE: a person approves the guideline; then content is made and checked against it. Retiring a guideline is a person's call.",
3475
3470
  "subjectProfile": {
3476
- "profileSlug": "content-guideline",
3477
- "statusProperty": "guideline-status"
3471
+ "profileSlug": "content-guideline"
3478
3472
  },
3479
- "humanOnlyStatuses": [
3480
- "deprecated"
3481
- ],
3482
3473
  "params": [
3483
3474
  {
3484
3475
  "name": "guideline",
@@ -3609,6 +3600,7 @@ var DATA = {
3609
3600
  },
3610
3601
  "intent": {
3611
3602
  "label": "Product",
3603
+ "primary": "devplane_feature",
3612
3604
  "produces": [
3613
3605
  {
3614
3606
  "profile": "devplane_feature"
@@ -4725,6 +4717,23 @@ var DATA = {
4725
4717
  "icon": "alert-triangle",
4726
4718
  "color": "red",
4727
4719
  "description": "A production incident with severity, status, and root cause tracking",
4720
+ "lifecycle": {
4721
+ "property": "incidentStatus",
4722
+ "options": [
4723
+ {
4724
+ "key": "open",
4725
+ "category": "open"
4726
+ },
4727
+ {
4728
+ "key": "investigating",
4729
+ "category": "active"
4730
+ },
4731
+ {
4732
+ "key": "resolved",
4733
+ "category": "done"
4734
+ }
4735
+ ]
4736
+ },
4728
4737
  "properties": [
4729
4738
  {
4730
4739
  "slug": "severity",
@@ -5276,6 +5285,129 @@ var DATA = {
5276
5285
  }
5277
5286
  ]
5278
5287
  },
5288
+ {
5289
+ "slug": "finding",
5290
+ "displayName": "Finding",
5291
+ "icon": "bug",
5292
+ "color": "#F97316",
5293
+ "description": "A problem report or feedback item, with a severity, raised against our own product or process. Moves open \u2192 fixing \u2192 fixed \u2192 verified, or won't fix / superseded.",
5294
+ "scope": "shared",
5295
+ "entityScope": "pod",
5296
+ "profileKind": "kind",
5297
+ "lifecycle": {
5298
+ "property": "status",
5299
+ "options": [
5300
+ {
5301
+ "key": "open",
5302
+ "category": "open"
5303
+ },
5304
+ {
5305
+ "key": "fixing",
5306
+ "category": "active"
5307
+ },
5308
+ {
5309
+ "key": "fixed",
5310
+ "category": "done"
5311
+ },
5312
+ {
5313
+ "key": "verified",
5314
+ "category": "done"
5315
+ },
5316
+ {
5317
+ "key": "wont_fix",
5318
+ "category": "lost",
5319
+ "humanOnly": true
5320
+ },
5321
+ {
5322
+ "key": "superseded",
5323
+ "category": "lost",
5324
+ "humanOnly": true
5325
+ }
5326
+ ]
5327
+ },
5328
+ "properties": [
5329
+ {
5330
+ "slug": "status",
5331
+ "label": "Status",
5332
+ "valueType": "string",
5333
+ "inputType": "select",
5334
+ "enumValues": [
5335
+ "open",
5336
+ "fixing",
5337
+ "fixed",
5338
+ "verified",
5339
+ "wont_fix",
5340
+ "superseded"
5341
+ ],
5342
+ "constraints": {
5343
+ "defaultValue": "open"
5344
+ }
5345
+ },
5346
+ {
5347
+ "slug": "severity",
5348
+ "label": "Severity",
5349
+ "valueType": "string",
5350
+ "inputType": "select",
5351
+ "enumValues": [
5352
+ "low",
5353
+ "medium",
5354
+ "high",
5355
+ "critical"
5356
+ ]
5357
+ },
5358
+ {
5359
+ "slug": "area",
5360
+ "label": "Product area",
5361
+ "valueType": "string",
5362
+ "inputType": "text",
5363
+ "description": "Where it hurts: pod, mcp, bridge, cli, relay, browser, pod-admin, control-plane, governance, infra, process."
5364
+ },
5365
+ {
5366
+ "slug": "evidence",
5367
+ "label": "Evidence",
5368
+ "valueType": "string",
5369
+ "inputType": "textarea",
5370
+ "constraints": {
5371
+ "maxLength": 2e3
5372
+ },
5373
+ "description": "Proposal, session, file:line, log line or URL that shows it."
5374
+ },
5375
+ {
5376
+ "slug": "summary",
5377
+ "label": "Summary",
5378
+ "valueType": "string",
5379
+ "inputType": "text",
5380
+ "constraints": {
5381
+ "maxLength": 500
5382
+ }
5383
+ },
5384
+ {
5385
+ "slug": "description",
5386
+ "label": "Description",
5387
+ "valueType": "string",
5388
+ "inputType": "textarea",
5389
+ "constraints": {
5390
+ "maxLength": 5e3
5391
+ }
5392
+ },
5393
+ {
5394
+ "slug": "content",
5395
+ "label": "Content",
5396
+ "valueType": "string",
5397
+ "inputType": "textarea",
5398
+ "constraints": {
5399
+ "maxLength": 5e4
5400
+ }
5401
+ },
5402
+ {
5403
+ "slug": "supersededby",
5404
+ "label": "Superseded by",
5405
+ "valueType": "entity_id",
5406
+ "inputType": "entity_select",
5407
+ "description": "The record that replaced this finding: the canonical finding it duplicates, or the correct diagnosis when this one was wrong. Recall leaves a superseded finding out."
5408
+ }
5409
+ ]
5410
+ },
5279
5411
  {
5280
5412
  "slug": "skill",
5281
5413
  "displayName": "Skill",
@@ -5535,6 +5667,27 @@ var DATA = {
5535
5667
  "icon": "workflow",
5536
5668
  "color": "blue",
5537
5669
  "description": "A user flow or business process \u2014 a sequence of steps through system components",
5670
+ "lifecycle": {
5671
+ "property": "workflowStatus",
5672
+ "options": [
5673
+ {
5674
+ "key": "documented",
5675
+ "category": "open"
5676
+ },
5677
+ {
5678
+ "key": "in-progress",
5679
+ "category": "active"
5680
+ },
5681
+ {
5682
+ "key": "needs-review",
5683
+ "category": "active"
5684
+ },
5685
+ {
5686
+ "key": "complete",
5687
+ "category": "done"
5688
+ }
5689
+ ]
5690
+ },
5538
5691
  "properties": [
5539
5692
  {
5540
5693
  "slug": "workflowStatus",
@@ -5875,10 +6028,52 @@ var DATA = {
5875
6028
  "slug": "task-board",
5876
6029
  "type": "kanban",
5877
6030
  "scopeProfileSlug": "task",
5878
- "groupBy": "task-status",
6031
+ "config": {
6032
+ "groupByField": "task-status"
6033
+ },
5879
6034
  "defaultView": true,
5880
6035
  "description": "All delegated tasks by status: Backlog \u2192 Queued \u2192 Running \u2192 Review \u2192 Done."
5881
6036
  },
6037
+ {
6038
+ "name": "Feature Board",
6039
+ "slug": "feature-board",
6040
+ "type": "kanban",
6041
+ "scopeProfileSlug": "devplane_feature",
6042
+ "description": "Features by stage, Idea to Shipped.",
6043
+ "config": {
6044
+ "groupByField": "featureStatus"
6045
+ }
6046
+ },
6047
+ {
6048
+ "name": "Story Board",
6049
+ "slug": "story-board",
6050
+ "type": "kanban",
6051
+ "scopeProfileSlug": "devplane_story",
6052
+ "description": "User stories by status, Draft to Done.",
6053
+ "config": {
6054
+ "groupByField": "storyStatus"
6055
+ }
6056
+ },
6057
+ {
6058
+ "name": "Issue Board",
6059
+ "slug": "issue-board",
6060
+ "type": "kanban",
6061
+ "scopeProfileSlug": "issue",
6062
+ "description": "Bugs and problems by status, Open to Fixed.",
6063
+ "config": {
6064
+ "groupByField": "issueStatus"
6065
+ }
6066
+ },
6067
+ {
6068
+ "name": "Design Board",
6069
+ "slug": "design-board",
6070
+ "type": "kanban",
6071
+ "scopeProfileSlug": "ui-design",
6072
+ "description": "Designs by status, from draft to implemented.",
6073
+ "config": {
6074
+ "groupByField": "design-status"
6075
+ }
6076
+ },
5882
6077
  {
5883
6078
  "name": "Deployments",
5884
6079
  "type": "calendar",
@@ -5922,7 +6117,9 @@ var DATA = {
5922
6117
  "slug": "service-health",
5923
6118
  "type": "kanban",
5924
6119
  "scopeProfileSlug": "devplane_service",
5925
- "groupBy": "serviceStatus",
6120
+ "config": {
6121
+ "groupByField": "serviceStatus"
6122
+ },
5926
6123
  "description": "Every service by health \u2014 healthy, degraded, down, maintenance."
5927
6124
  },
5928
6125
  {
@@ -5930,7 +6127,9 @@ var DATA = {
5930
6127
  "slug": "services-by-type",
5931
6128
  "type": "kanban",
5932
6129
  "scopeProfileSlug": "devplane_service",
5933
- "groupBy": "serviceType",
6130
+ "config": {
6131
+ "groupByField": "serviceType"
6132
+ },
5934
6133
  "description": "Services grouped by what they are \u2014 API, worker, cron, database, cache, queue."
5935
6134
  },
5936
6135
  {
@@ -6070,13 +6269,8 @@ var DATA = {
6070
6269
  "description": "Take a UI design request from three variants built only of real Storybook components, through a rubric critique and a person's review on the canvas, to a pull request verified by the gates, a before/after compare and a walk in the running app.",
6071
6270
  "goalTemplate": "Take this UI design ({design}) for {surface} from draft to implemented. Load the designing-with-real-components skill first, and design-critique for the Critique stage. GENERATE: read the Storybook component manifest (/manifests/components.json, or the Storybook MCP), the design's goal and scope and the entities it references, and write THREE variants as Puck JSON \u2014 this design and two sibling ui-designs sharing its variant-group \u2014 using ONLY components the manifest lists, with props each one declares, design tokens and named motion presets only; each variant lays out every state the screen has (empty, loading, failed, full, long content); fill components-used from the JSON and set schema-version. CRITIQUE: judge each variant with the design-critique rubric and write the findings into its critique; a checked or pattern blocker sends the run back to Generate; opinions are never applied. REVIEW: a person compares the variants on the canvas and approves one; nothing is implemented from an unapproved design, and only a person sets approved. IMPLEMENT: read design-json and the Storybook MCP, build the screen from those same real components, open a pull request and record its URL in implementation-pr and the built screen's story in implemented-story-id. VERIFY: typecheck, story coverage, interaction and accessibility tests, visual regression and the token guards are green, the implemented story was captured and compared with the design and the current story, and the screen was walked in the running app. Move the design's status only through its lifecycle (update_entity with expectedStatus); never invent a status.",
6072
6271
  "subjectProfile": {
6073
- "profileSlug": "ui-design",
6074
- "statusProperty": "design-status"
6272
+ "profileSlug": "ui-design"
6075
6273
  },
6076
- "humanOnlyStatuses": [
6077
- "approved",
6078
- "archived"
6079
- ],
6080
6274
  "activators": [
6081
6275
  {
6082
6276
  "on": "manual",
@@ -6593,13 +6787,8 @@ var DATA = {
6593
6787
  "description": "Take a shaped change from branch to pull request, green checks, merge, lab and a dogfood walk, until its criteria are verified. The facts (PR opened, CI finished, merged, deployed) move the step; the person approves only the lab deploy.",
6594
6788
  "goalTemplate": `Ship this change ({feature}) through one session, one branch and its pull requests, then the merge, the lab and a dogfood walk, until its criteria are verified. The step follows the work; move the feature's status only through its lifecycle (update_entity with expectedStatus), never invent one. SHAPE: write the end state and 2-5 binary criteria on this session, set the feature's change type (feature, refactor or chore), and name the repos it touches. BUILD: branch <type>/<slug> from origin/main in your own worktree, commit in verified slices, and run the smallest gate that answers for each finished slice, with its negative control; quote the output. PR OPENED: open a draft pull request whose body carries the line "Pod session: <this session id>", and record it on this session as the outcome pr-<repo>-<number> (kind pull_request, ref url); one outcome per pull request, and for each a session criterion keyed the same, "<repo>#<number> is merged", checked by the capability github_check_pr_merged, so the pod grades the merge itself. CHECKS: read the checks on the pull request's head with github_pr_checks (the github-read capability, on the person's own token) or wait for the ci.finished event; a red check sends the work back to Build. MERGE: a person merges; never merge and never push to main. The merge marks the outcome met. DEPLOY: ask the person for the lab deploy (synap_request_deploy_approval); the deploy is their call. DOGFOOD: walk the change in the running lab as a person would and report WORKS, BROKEN or CANNOT-TELL with what was seen. VERIFY: grade every criterion with its evidence, and separate VERIFIED (gate or check output quoted) from NEEDS-DOGFOOD.`,
6595
6789
  "subjectProfile": {
6596
- "profileSlug": "devplane_feature",
6597
- "statusProperty": "featureStatus"
6790
+ "profileSlug": "devplane_feature"
6598
6791
  },
6599
- "humanOnlyStatuses": [
6600
- "backlog",
6601
- "cancelled"
6602
- ],
6603
6792
  "activators": [
6604
6793
  {
6605
6794
  "on": "enters_status",
@@ -6848,21 +7037,12 @@ var DATA = {
6848
7037
  },
6849
7038
  {
6850
7039
  "name": "Build a change",
6851
- "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.",
7040
+ "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.",
6852
7041
  "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.',
6853
7042
  "scope": "session",
6854
7043
  "subjectProfile": {
6855
- "onEnd": {
6856
- "failed": "spec",
6857
- "cancelled": "spec"
6858
- },
6859
- "profileSlug": "devplane_feature",
6860
- "statusProperty": "featureStatus"
7044
+ "profileSlug": "devplane_feature"
6861
7045
  },
6862
- "humanOnlyStatuses": [
6863
- "backlog",
6864
- "cancelled"
6865
- ],
6866
7046
  "params": [
6867
7047
  {
6868
7048
  "name": "feature",
@@ -7101,21 +7281,12 @@ var DATA = {
7101
7281
  },
7102
7282
  {
7103
7283
  "name": "Fix a bug",
7104
- "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.",
7284
+ "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.",
7105
7285
  "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.',
7106
7286
  "scope": "session",
7107
7287
  "subjectProfile": {
7108
- "onEnd": {
7109
- "failed": "open",
7110
- "cancelled": "open"
7111
- },
7112
- "profileSlug": "issue",
7113
- "statusProperty": "issueStatus"
7288
+ "profileSlug": "issue"
7114
7289
  },
7115
- "humanOnlyStatuses": [
7116
- "open",
7117
- "wont_fix"
7118
- ],
7119
7290
  "params": [
7120
7291
  {
7121
7292
  "name": "issue",
@@ -7954,6 +8125,41 @@ var DATA = {
7954
8125
  "description": "Shared land stage. Never report unverified work as verified. File the lesson, decision or finding the work produced (engineering-memory skill, rule dogfood-journal). Close what you opened: asks answered or withdrawn, worktree removed once merged, then synap_complete_session. Tools: synap_update_session, synap_capture, synap_complete_session. Gate: none. Model tier: quick (advisory until a tier field exists)."
7955
8126
  }
7956
8127
  ]
8128
+ },
8129
+ {
8130
+ "name": "Fix a finding",
8131
+ "description": "Work one finding (a problem with Synap itself) until its fix is live. The session's stages move the finding: fixing while the work runs, fixed when the fix is merged, verified once it runs on the pod. A cancelled or failed session leaves the finding where it is.",
8132
+ "goalTemplate": "Fix the finding and see the fix live",
8133
+ "scope": "session",
8134
+ "subjectProfile": {
8135
+ "profileSlug": "finding"
8136
+ },
8137
+ "params": [],
8138
+ "executor": "is-agent",
8139
+ "status": "active",
8140
+ "stages": [
8141
+ {
8142
+ "key": "fix",
8143
+ "name": "Fix it",
8144
+ "goal": "Find the root cause and ship the fix as a gated PR or commit",
8145
+ "category": "started",
8146
+ "subjectStatus": "fixing"
8147
+ },
8148
+ {
8149
+ "key": "merged",
8150
+ "name": "Fix merged",
8151
+ "goal": "The fix is on main (a merged PR or a gated commit)",
8152
+ "category": "started",
8153
+ "subjectStatus": "fixed"
8154
+ },
8155
+ {
8156
+ "key": "verified",
8157
+ "name": "Live and verified",
8158
+ "goal": "The fix runs on the pod (its build contains the fix) and the finding no longer happens",
8159
+ "category": "completed",
8160
+ "subjectStatus": "verified"
8161
+ }
8162
+ ]
7957
8163
  }
7958
8164
  ],
7959
8165
  "onboarding": {
@@ -8204,6 +8410,27 @@ var DATA = {
8204
8410
  "displayName": "Question",
8205
8411
  "icon": "help-circle",
8206
8412
  "profileKind": "kind",
8413
+ "lifecycle": {
8414
+ "property": "questionStatus",
8415
+ "options": [
8416
+ {
8417
+ "key": "open",
8418
+ "category": "open"
8419
+ },
8420
+ {
8421
+ "key": "exploring",
8422
+ "category": "active"
8423
+ },
8424
+ {
8425
+ "key": "answered",
8426
+ "category": "done"
8427
+ },
8428
+ {
8429
+ "key": "abandoned",
8430
+ "category": "lost"
8431
+ }
8432
+ ]
8433
+ },
8207
8434
  "properties": [
8208
8435
  {
8209
8436
  "slug": "questionStatus",
@@ -8224,6 +8451,27 @@ var DATA = {
8224
8451
  "displayName": "Decision",
8225
8452
  "icon": "gavel",
8226
8453
  "profileKind": "kind",
8454
+ "lifecycle": {
8455
+ "property": "decisionStatus",
8456
+ "options": [
8457
+ {
8458
+ "key": "proposed",
8459
+ "category": "open"
8460
+ },
8461
+ {
8462
+ "key": "accepted",
8463
+ "category": "done"
8464
+ },
8465
+ {
8466
+ "key": "superseded",
8467
+ "category": "lost"
8468
+ },
8469
+ {
8470
+ "key": "rejected",
8471
+ "category": "lost"
8472
+ }
8473
+ ]
8474
+ },
8227
8475
  "properties": [
8228
8476
  {
8229
8477
  "slug": "decisionStatus",
@@ -8545,7 +8793,9 @@ var DATA = {
8545
8793
  "slug": "business-model-open-questions",
8546
8794
  "type": "kanban",
8547
8795
  "scopeProfileSlug": "question",
8548
- "groupBy": "questionStatus",
8796
+ "config": {
8797
+ "groupByField": "questionStatus"
8798
+ },
8549
8799
  "description": "The nine component questions and everything they spawned, by status."
8550
8800
  }
8551
8801
  ],
@@ -8929,6 +9179,27 @@ var DATA = {
8929
9179
  "icon": "help-circle",
8930
9180
  "color": "blue",
8931
9181
  "description": "An open question driving a civic-data investigation.",
9182
+ "lifecycle": {
9183
+ "property": "questionStatus",
9184
+ "options": [
9185
+ {
9186
+ "key": "open",
9187
+ "category": "open"
9188
+ },
9189
+ {
9190
+ "key": "exploring",
9191
+ "category": "active"
9192
+ },
9193
+ {
9194
+ "key": "answered",
9195
+ "category": "done"
9196
+ },
9197
+ {
9198
+ "key": "abandoned",
9199
+ "category": "lost"
9200
+ }
9201
+ ]
9202
+ },
8932
9203
  "properties": [
8933
9204
  {
8934
9205
  "slug": "questionStatus",
@@ -9502,7 +9773,7 @@ var DATA = {
9502
9773
  "scopeProfileSlug": "content-program",
9503
9774
  "description": "Programs grouped by status \u2014 a running scoreboard for the content operation.",
9504
9775
  "config": {
9505
- "groupBy": "status"
9776
+ "groupByField": "status"
9506
9777
  }
9507
9778
  }
9508
9779
  ],
@@ -9623,7 +9894,6 @@ var DATA = {
9623
9894
  "musician",
9624
9895
  "photographer"
9625
9896
  ],
9626
- "profile": "post",
9627
9897
  "views": [
9628
9898
  "content-posts",
9629
9899
  "content-calendar"
@@ -9677,6 +9947,7 @@ var DATA = {
9677
9947
  },
9678
9948
  "intent": {
9679
9949
  "label": "Content",
9950
+ "primary": "post",
9680
9951
  "produces": [
9681
9952
  {
9682
9953
  "profile": "post"
@@ -10391,6 +10662,27 @@ var DATA = {
10391
10662
  "color": "#0EA5E9",
10392
10663
  "description": "An ordered run of posts that teaches one thing end to end \u2014 the curriculum axis of the content engine.",
10393
10664
  "scope": "WORKSPACE",
10665
+ "lifecycle": {
10666
+ "property": "series-status",
10667
+ "options": [
10668
+ {
10669
+ "key": "Planned",
10670
+ "category": "open"
10671
+ },
10672
+ {
10673
+ "key": "Running",
10674
+ "category": "active"
10675
+ },
10676
+ {
10677
+ "key": "Complete",
10678
+ "category": "done"
10679
+ },
10680
+ {
10681
+ "key": "Abandoned",
10682
+ "category": "lost"
10683
+ }
10684
+ ]
10685
+ },
10394
10686
  "properties": [
10395
10687
  {
10396
10688
  "slug": "series-status",
@@ -11554,7 +11846,9 @@ var DATA = {
11554
11846
  "slug": "content-posts",
11555
11847
  "type": "kanban",
11556
11848
  "scopeProfileSlug": "post",
11557
- "groupBy": "post-status",
11849
+ "config": {
11850
+ "groupByField": "post-status"
11851
+ },
11558
11852
  "description": "Content posts by status."
11559
11853
  },
11560
11854
  {
@@ -11598,7 +11892,9 @@ var DATA = {
11598
11892
  "slug": "content-newsletters",
11599
11893
  "type": "kanban",
11600
11894
  "scopeProfileSlug": "newsletter",
11601
- "groupBy": "newsletter-status",
11895
+ "config": {
11896
+ "groupByField": "newsletter-status"
11897
+ },
11602
11898
  "description": "Newsletter issues by status."
11603
11899
  },
11604
11900
  {
@@ -11894,13 +12190,8 @@ var DATA = {
11894
12190
  "description": "Take a briefed post from brief to script to published in any format \u2014 text, visual or video (HyperFrames) \u2014 from the pod's brand kit, one stage per post status, with a person approving the script before anything is generated.",
11895
12191
  "goalTemplate": "Produce content for {platform} from this idea ({post}) in the {format} format. Read the brand first from the pod's brand kit (GET /api/hub/brand/kit) \u2014 never re-derive it from a website capture \u2014 and load the creative-director skill (linked to this space) to pick the approved brand-template (layout and recipe), references and assets for this format and style; drafts never feed generation. Work the stages in order; each stage moves the post's status. SCRIPT before anything is made: write the script or outline for the format (carousel slide outline, deck outline, video script and storyboard, or the text draft) by filling the narrative frame for the post's goal \u2014 the approved content-guideline of type frame for that goal, else the default frame (sell: story, example, lesson, link; hot-take: claim, tension, reason, invitation; tutorial: promise, steps, result, next; product-demo: problem, real screen, outcome, try it). A person approves the script before the draft; nothing is generated from an unapproved script. At the draft, run the creative-director readiness check first (approved references, an approved base guideline, an approved brand-template for this format); if anything is missing, stop and route to the content-taste skill or brand onboarding instead of improvising. Then branch on the format. TEXT (article, thread, newsletter section, caption): draft it with a scroll-stopping hook and a clear CTA. VISUAL: build it pass by pass through the edit-loop skill, a person answering each pass \u2014 a carousel, LinkedIn PDF or image post as a layered composition saved on the post (carousel-format), a deck, PDF document or presentation in Open Design (open-design-edit). VIDEO (short or long video): first pick the route \u2014 FROM SCRATCH (script, storyboard, frames), FROM A VOICE (an existing voice-over or talk: transcribe it, cut on its beats, build the frames under it) or RE-EDIT (existing footage plus a voice: cut, caption, add music and motion); takes ({takes}) mean RE-EDIT, and the Draft stage says how takes, transcripts and passes are handled. Then run the HyperFrames workflow on your own agent with HYPERFRAMES_NO_TELEMETRY=1, from the kit's frame.md (format=frame-md), with GSAP vendored locally \u2014 never `hyperframes publish`, never a cloud render. For a visual or video format, record a content-composition made for this post (for a video: the brief and storyboard documents, the project zip, the MP4, key-frame stills and the brand-kit hash). Keep every format on the post's pillar and the brand voice. A human approves the script before the draft, each pass as it is built, and the draft on the preview at Review. At Schedule, a post whose account is a Postiz channel is queued through the capability that serves publish_post (postiz_schedule_post); that queueing is itself a proposal a person approves. Nothing publishes itself, and the post reads Published only once it went live: reported by Postiz, or confirmed by a person at Publish.",
11896
12192
  "subjectProfile": {
11897
- "profileSlug": "post",
11898
- "statusProperty": "post-status"
12193
+ "profileSlug": "post"
11899
12194
  },
11900
- "humanOnlyStatuses": [
11901
- "Idea",
11902
- "Archived"
11903
- ],
11904
12195
  "activators": [
11905
12196
  {
11906
12197
  "on": "enters_status",
@@ -12338,8 +12629,7 @@ var DATA = {
12338
12629
  "description": "Turn a published post into new posts for other formats and platforms, each linked to the original.",
12339
12630
  "goalTemplate": "Repurpose this published post ({post}). Read what it says and how it performed, then propose one to three new pieces in other formats or on other platforms where the same idea travels (a thread from an article, a short video from a thread, a newsletter section from a series). Create each as a new post at the Idea status, on the same pillar, with its own hook, and link it derived_from the original. Never republish the original as is.",
12340
12631
  "subjectProfile": {
12341
- "profileSlug": "post",
12342
- "statusProperty": "post-status"
12632
+ "profileSlug": "post"
12343
12633
  },
12344
12634
  "activators": [
12345
12635
  {
@@ -12375,12 +12665,8 @@ var DATA = {
12375
12665
  "description": "Take a newsletter issue from idea to sent \u2014 angle, draft, edit, schedule \u2014 with a person approving before it goes out.",
12376
12666
  "goalTemplate": "Produce this newsletter issue ({newsletter}). Read the brand voice from the pod's brand kit, then work the stages in order: settle the issue's angle and the posts it draws on, draft it, edit it down with a subject line and preview text, and schedule the send. A person approves the final issue before it is sent, nothing sends itself, and the issue reads Sent only once a person confirms it went out.",
12377
12667
  "subjectProfile": {
12378
- "profileSlug": "newsletter",
12379
- "statusProperty": "newsletter-status"
12668
+ "profileSlug": "newsletter"
12380
12669
  },
12381
- "humanOnlyStatuses": [
12382
- "Archived"
12383
- ],
12384
12670
  "params": [
12385
12671
  {
12386
12672
  "name": "newsletter",
@@ -13116,7 +13402,6 @@ var DATA = {
13116
13402
  "small-business",
13117
13403
  "founder"
13118
13404
  ],
13119
- "profile": "deal",
13120
13405
  "views": [
13121
13406
  "deal-pipeline",
13122
13407
  "deals-table",
@@ -13175,6 +13460,7 @@ var DATA = {
13175
13460
  },
13176
13461
  "intent": {
13177
13462
  "label": "Sales",
13463
+ "primary": "deal",
13178
13464
  "produces": [
13179
13465
  {
13180
13466
  "profile": "lead"
@@ -14148,10 +14434,9 @@ var DATA = {
14148
14434
  "slug": "outreach-touches",
14149
14435
  "type": "kanban",
14150
14436
  "scopeProfileSlug": "outreach-touch",
14151
- "groupBy": "status",
14152
14437
  "description": "Every outreach message, grouped by where it stands \u2014 drafted for a person to send, through replied.",
14153
14438
  "config": {
14154
- "groupBy": "status",
14439
+ "groupByField": "status",
14155
14440
  "cardFields": [
14156
14441
  "channel",
14157
14442
  "touchType",
@@ -14251,17 +14536,8 @@ var DATA = {
14251
14536
  "description": 'Research one lead, decide with a person whether it is worth pursuing, and set a pursued lead qualified with "book a meeting" as its next action.',
14252
14537
  "goalTemplate": 'Qualify this lead ({lead}). RESEARCH: verify and enrich only facts you can support \u2014 company, role, size, recent signal \u2014 and record the source of each. SCORE: assess fit against the offers and the ideal client, set the ICP score, qualification notes and outreach channels, and write the step you recommend as the lead\'s next action: `nextAction` one imperative line (e.g. "Send the first message on LinkedIn") and `nextActionAt` its date. DECIDE: ask the person to `choose` pursue, nurture or drop, your recommendation first; that answer is the decision. NURTURE: set the lead to nurturing, with `nextAction` to check in again and `nextActionAt` the date they gave (three months out when they gave none). DROP: write their reason in `disqualificationReason` and move the lead to disqualified; that move is the person\'s (disqualified is human-only), so it comes back proposed for them to approve, which is success; never write it another way. PURSUE: only when they chose pursue, set the lead to qualified with `nextAction` "Book a meeting" (one line naming the channel, e.g. "Ask for a meeting on LinkedIn") and `nextActionAt` its date. Never create a deal: a deal is opened when a person records a booked meeting, and that same step converts the lead. Never mark the lead a client.',
14253
14538
  "subjectProfile": {
14254
- "profileSlug": "lead",
14255
- "statusProperty": "leadStage"
14256
- },
14257
- "humanOnlyStatuses": [
14258
- "new",
14259
- "outreach",
14260
- "engaged",
14261
- "nurturing",
14262
- "converted",
14263
- "disqualified"
14264
- ],
14539
+ "profileSlug": "lead"
14540
+ },
14265
14541
  "activators": [
14266
14542
  {
14267
14543
  "on": "manual",
@@ -14385,12 +14661,8 @@ var DATA = {
14385
14661
  "description": "Take a deal from first conversation to won or lost \u2014 qualify it, write and send the proposal, negotiate, and hand a won deal to delivery, with a person deciding every commitment.",
14386
14662
  "goalTemplate": `Work this deal ({deal}) through its stages; each stage moves the deal's commercial stage. QUALIFY: from the pod (the company, its people, every past message and meeting) and public sources, establish the four facts a proposal needs \u2014 the problem the buyer wants solved and what it costs them, a budget range, who signs, and when they need it \u2014 and link the people involved to the deal. Do not guess a fact: when one is missing, ask the person who owns the relationship. WRITE: draft the proposal document (scope, deliverables, timeline, price, terms) from the agency's offers and past won proposals, and set the deal's value and close date. SEND: a person approves the proposal before it goes to the client; nothing is sent without that approval. AWAIT: wait for the client's answer and record it on the deal. NEGOTIATE: its check reads that answer; log every objection and counter-offer and propose the next step. WON: once a person confirms the client said yes, reuse the engagement Operations already has for this company; only when there is none, open one at proposed with its client set to the company. Then attach the client role to the company with handoff status handed_off. The daily "Pick up handed-off clients" automation skips any client that already has an engagement, so no second one is opened. A lost deal is a person's call \u2014 record why it was lost.`,
14387
14663
  "subjectProfile": {
14388
- "profileSlug": "deal",
14389
- "statusProperty": "commercialStage"
14664
+ "profileSlug": "deal"
14390
14665
  },
14391
- "humanOnlyStatuses": [
14392
- "lost"
14393
- ],
14394
14666
  "activators": [
14395
14667
  {
14396
14668
  "on": "created",
@@ -15098,8 +15370,7 @@ var DATA = {
15098
15370
  "description": "Each weekday, read the channels of the leads in outreach; summarise a reply and propose the next step, or, when the follow-up is due with no reply, draft the next message in the angle of its outreach campaign as a proposed message for a person to approve; the agent never sends.",
15099
15371
  "goalTemplate": `Follow up on the leads in outreach. For each, read its channels (synap_get_channel, mode person: for a company lead, the channels of the person who decides there) and its notes. A REPLY is an inbound message after our last outbound one in a channel. With a reply, draft nothing: summarise it for a person; when they show interest, propose the next step (prepare the free estimate, "Prepare a pre-sale estimate", or book a meeting) and propose moving the lead to engaged; when they decline, propose nurturing or disqualified with their reason (in disqualificationReason). Unless you propose dropping them, write the next step on the lead as nextAction, one imperative line, and nextActionAt, the date they named or today. Never open a deal and never answer the reply yourself; a person does. With no reply, and only when the lead's next action date is today or past and no proposed message to it is still waiting, draft the next message, to the lead (for a company, the person who decides there) and in the angle of the lead's outreach campaign: follow its value proposition and call to action, short and in the lead's language, offering the next useful step, never pressure. For a lead with no campaign, ask the person which one. Draft at most two follow-ups per lead: a lead already sent two follow-ups gets no draft, never a third follow-up; when its last date passes in silence, propose moving it to nurturing through the status door, with its expected status. DRAFT RULES: each draft is one proposed message in the channel of the person it is written to, filed through the pod's send door: run messaging.send with to = that person's entity id (for a company lead, its decider: when that person is not in the pod, create them through the entity door with only their own fields, such as name, role and LinkedIn URL, never the company's website, domain or email), text = the message, and provider linkedin or email. Read their channel first (synap_get_channel, mode person) for what was already said. The pod files it for a person to review in that channel; it goes out only once they approve it. Never call a tool's own send verb (Unipile, Gmail), never create an outreach touch, a note or a task holding the message, and never use synap_capture for it. Never send. A LinkedIn message is drafted whatever their reach: the pod holds a draft to someone who is not a connection yet, shows it with the next step and lets it go out once they connect, so never hold a draft back for want of a connection. Draft it, then, in this order: (1) a person with no linkedinUrl: find their profile with linkedin_find_profile (their full name and company); on a match, propose its linkedinUrl and linkedinProviderId on the person for a person to approve; on none or several, ask the person to choose among its candidates (the process "Find their LinkedIn" does this for one lead). (2) Read their linkedinConnection, which only the pod writes (never set it or linkedinInvitedAt): connected, nothing more; none or not set, ask to connect with linkedin_send_invite (it refuses someone already connected or already invited: then nothing more); pending, the draft waits for them; ignored, draft by email instead when they have an email address. While the draft waits for them to connect, set the lead's nextAction to "Wait for <name> to accept the LinkedIn request" and its nextActionAt 7 days later. An email message needs none of this. For each draft, set the lead's \`nextAction\` to send it (e.g. "Send the drafted message to Ana on LinkedIn") and \`nextActionAt\` to today. The person reviews each proposed message in its channel, edits it if needed and approves it; the agent never sends. CADENCE: when a message is sent (a person approved its proposed message, so it is an outbound message in the channel), set its lead's lastContactedAt to the sent day, count the messages sent to the lead (the outbound messages in its channels, read with synap_get_channel mode person: for a company lead, the channels of the person written to there), and set the lead's next action: after the first message, nextAction "Follow up with <name> if no reply" and nextActionAt the sent day + 3 days; after the first follow-up, nextAction "Follow up with <name> a last time if no reply" and nextActionAt the sent day + 7 days; after the second follow-up, nextAction "No reply after two follow-ups: nurture or drop <name>" and nextActionAt the sent day + 7 days, and when that date passes with no reply, "Follow up on leads in outreach" proposes nurturing. Never a third follow-up. Propose moving a lead at new or researching to outreach through the status door, with its expected status. A reply stops the cadence: a reply is an inbound message after our last outbound one in the channel, and "Follow up on leads in outreach" reads it and proposes the next step.`,
15100
15372
  "subjectProfile": {
15101
- "profileSlug": "lead",
15102
- "statusProperty": "leadStage"
15373
+ "profileSlug": "lead"
15103
15374
  },
15104
15375
  "activators": [
15105
15376
  {
@@ -15185,13 +15456,8 @@ var DATA = {
15185
15456
  "description": "Run one outreach campaign \u2014 write the angle and its offer, pick the people, draft one personal first message each as a proposed message in their channel; a person approves the list, then approves each message, which the pod sends.",
15186
15457
  "goalTemplate": `Run this outreach campaign ({campaign}) on {channel}; each stage moves its status. If its type is not outreach, stop and say so. PREPARE: write the target audience, the value proposition and the call to action. Pick the people to reach from the pod (and public sources when the pod has too few), resolving each one before creating anything, link each as a member of the campaign with the lead role (lead source cold-outreach when it has none), and for each draft one first message to that person in this campaign's angle, specific to them \u2014 their company, role and any past message \u2014 never a generic template. DRAFT RULES: each draft is one proposed message in the channel of the person it is written to, filed through the pod's send door: run messaging.send with to = that person's entity id (for a company lead, its decider: when that person is not in the pod, create them through the entity door with only their own fields, such as name, role and LinkedIn URL, never the company's website, domain or email), text = the message, and provider linkedin or email. Read their channel first (synap_get_channel, mode person) for what was already said. The pod files it for a person to review in that channel; it goes out only once they approve it. Never call a tool's own send verb (Unipile, Gmail), never create an outreach touch, a note or a task holding the message, and never use synap_capture for it. Never send. A LinkedIn message is drafted whatever their reach: the pod holds a draft to someone who is not a connection yet, shows it with the next step and lets it go out once they connect, so never hold a draft back for want of a connection. Draft it, then, in this order: (1) a person with no linkedinUrl: find their profile with linkedin_find_profile (their full name and company); on a match, propose its linkedinUrl and linkedinProviderId on the person for a person to approve; on none or several, ask the person to choose among its candidates (the process "Find their LinkedIn" does this for one lead). (2) Read their linkedinConnection, which only the pod writes (never set it or linkedinInvitedAt): connected, nothing more; none or not set, ask to connect with linkedin_send_invite (it refuses someone already connected or already invited: then nothing more); pending, the draft waits for them; ignored, draft by email instead when they have an email address. While the draft waits for them to connect, set the lead's nextAction to "Wait for <name> to accept the LinkedIn request" and its nextActionAt 7 days later. An email message needs none of this. START: a person approves the audience, the offer and the list, then reviews each proposed message in its channel and approves it; nothing goes out before they approve it, and the agent never sends. Once a message is sent, follow the cadence, so "Follow up on leads in outreach" drafts the next one. CADENCE: when a message is sent (a person approved its proposed message, so it is an outbound message in the channel), set its lead's lastContactedAt to the sent day, count the messages sent to the lead (the outbound messages in its channels, read with synap_get_channel mode person: for a company lead, the channels of the person written to there), and set the lead's next action: after the first message, nextAction "Follow up with <name> if no reply" and nextActionAt the sent day + 3 days; after the first follow-up, nextAction "Follow up with <name> a last time if no reply" and nextActionAt the sent day + 7 days; after the second follow-up, nextAction "No reply after two follow-ups: nurture or drop <name>" and nextActionAt the sent day + 7 days, and when that date passes with no reply, "Follow up on leads in outreach" proposes nurturing. Never a third follow-up. Propose moving a lead at new or researching to outreach through the status door, with its expected status. A reply stops the cadence: a reply is an inbound message after our last outbound one in the channel, and "Follow up on leads in outreach" reads it and proposes the next step. FINISH: when every person written to has replied or their follow-ups ended in silence, record in the campaign notes how many messages were sent (the outbound messages in their channels), how many got a reply and how many led to a booked meeting. Pausing or cancelling is a person's call.`,
15187
15458
  "subjectProfile": {
15188
- "profileSlug": "campaign",
15189
- "statusProperty": "campaign-status"
15459
+ "profileSlug": "campaign"
15190
15460
  },
15191
- "humanOnlyStatuses": [
15192
- "paused",
15193
- "cancelled"
15194
- ],
15195
15461
  "activators": [
15196
15462
  {
15197
15463
  "on": "created",
@@ -15293,15 +15559,8 @@ var DATA = {
15293
15559
  "description": "Once a person has marked an outreach touch sent (messages recorded before the channels), watch for the reply; summarise it and propose the next step \u2014 the free estimate or a meeting \u2014 and never open a deal.",
15294
15560
  "goalTemplate": 'Watch this sent outreach message ({touch}) for a reply; each stage moves its status. WAIT: watch its conversation. Set it bounced when it did not reach the person, or no response when its lead\'s next action date passes with no reply; that date is the cadence\'s (the sent day + 3 days after the first message, the sent day + 7 days after each follow-up), so when the lead has none, count from this message\'s sent date. The next message is drafted by "Follow up on leads in outreach", not here. When this message is the second follow-up (the third message sent to the lead) and its date passes with no reply, mark it no response and propose moving the lead to nurturing through the status door, with its expected status; there is never a third follow-up. REPLIED: its check reads the reply. Summarise it for a person. When they show interest, propose the next step \u2014 prepare the free estimate ("Prepare a pre-sale estimate") or book a meeting \u2014 and propose moving the lead to engaged; when they decline, propose nurturing or disqualified with their reason (in `disqualificationReason`). Unless you propose dropping them, write the next step you propose on the lead as `nextAction`, one imperative line (e.g. "Book a meeting with Ana"; "Check in again" for nurturing), and `nextActionAt`, the date they named or today; never leave it only in the summary. BOUNCED: set the lead\'s `nextAction` to find another way to reach them and `nextActionAt` to today. Never open a deal: a deal is opened only when a person records a booked meeting. Never answer the reply yourself; a person does. Opened is recorded by a person.',
15295
15561
  "subjectProfile": {
15296
- "profileSlug": "outreach-touch",
15297
- "statusProperty": "status"
15298
- },
15299
- "humanOnlyStatuses": [
15300
- "drafted",
15301
- "sent",
15302
- "opened",
15303
- "replaced"
15304
- ],
15562
+ "profileSlug": "outreach-touch"
15563
+ },
15305
15564
  "activators": [
15306
15565
  {
15307
15566
  "on": "enters_status",
@@ -16298,6 +16557,7 @@ var DATA = {
16298
16557
  "isPublic": true,
16299
16558
  "intent": {
16300
16559
  "label": "Finance",
16560
+ "primary": "invoice",
16301
16561
  "produces": [
16302
16562
  {
16303
16563
  "profile": "invoice"
@@ -16893,15 +17153,24 @@ var DATA = {
16893
17153
  }
16894
17154
  }
16895
17155
  },
17156
+ {
17157
+ "name": "Invoice Board",
17158
+ "slug": "invoice-board",
17159
+ "type": "kanban",
17160
+ "scopeProfileSlug": "invoice",
17161
+ "description": "Invoices by status, draft to paid \u2014 the flow board of Bill a client.",
17162
+ "config": {
17163
+ "groupByField": "status"
17164
+ }
17165
+ },
16896
17166
  {
16897
17167
  "name": "Expenses",
16898
17168
  "slug": "expenses",
16899
17169
  "type": "kanban",
16900
17170
  "scopeProfileSlug": "expense",
16901
- "groupBy": "status",
16902
17171
  "description": "Accounts payable and spend, grouped by approval/payment status.",
16903
17172
  "config": {
16904
- "groupBy": "status",
17173
+ "groupByField": "status",
16905
17174
  "cardFields": [
16906
17175
  "category",
16907
17176
  "amount",
@@ -17208,12 +17477,8 @@ var DATA = {
17208
17477
  "description": "Take an invoice from draft to paid \u2014 prepare it, send it with a person's approval, chase it when late, and confirm the payment.",
17209
17478
  "goalTemplate": "Bill this invoice ({invoice}); each stage moves its status. PREPARE: set the invoice number, customer, amount, currency, issue date and due date from the engagement or deal it bills, and check the amount against what was agreed. SEND: a person approves the invoice before it goes to the client; record the issue date. DUE: wait for the payment; look for it in the bank or payment provider (match the amount and the reference), and move on once it is found or the due date has passed. PAID: its check reads the payment; set the paid date and link the account it was deposited to. When the due date passed unpaid, the check sends the run to CHASE instead: the invoice becomes overdue and you draft a polite reminder for a person to send; once the payment is found, move the run back to Paid. Voiding an invoice is a person's call.",
17210
17479
  "subjectProfile": {
17211
- "profileSlug": "invoice",
17212
- "statusProperty": "status"
17480
+ "profileSlug": "invoice"
17213
17481
  },
17214
- "humanOnlyStatuses": [
17215
- "void"
17216
- ],
17217
17482
  "activators": [
17218
17483
  {
17219
17484
  "on": "created",
@@ -17371,13 +17636,8 @@ var DATA = {
17371
17636
  "description": "Review an expense against its budget, get a person's approval, and record the payment.",
17372
17637
  "goalTemplate": "Handle this expense ({expense}); each stage moves its status. REVIEW: check the vendor, amount, date, category and receipt, link the budget it counts against and say how much of that budget is left after it. APPROVE: a person approves or rejects the spend. PAY: a person confirms it was paid; link the account it was paid from. Reimbursement and rejection are a person's call.",
17373
17638
  "subjectProfile": {
17374
- "profileSlug": "expense",
17375
- "statusProperty": "status"
17639
+ "profileSlug": "expense"
17376
17640
  },
17377
- "humanOnlyStatuses": [
17378
- "reimbursed",
17379
- "rejected"
17380
- ],
17381
17641
  "activators": [
17382
17642
  {
17383
17643
  "on": "created",
@@ -18388,9 +18648,8 @@ var DATA = {
18388
18648
  "slug": "pain-points",
18389
18649
  "type": "kanban",
18390
18650
  "scopeProfileSlug": "pain-point",
18391
- "groupBy": "severity",
18392
18651
  "config": {
18393
- "groupBy": "severity",
18652
+ "groupByField": "severity",
18394
18653
  "cardFields": [
18395
18654
  "description",
18396
18655
  "audience",
@@ -20442,6 +20701,7 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
20442
20701
  "isPublic": true,
20443
20702
  "intent": {
20444
20703
  "label": "Team",
20704
+ "primary": "candidate",
20445
20705
  "produces": [
20446
20706
  {
20447
20707
  "profile": "candidate"
@@ -21009,10 +21269,9 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
21009
21269
  "slug": "reviews",
21010
21270
  "type": "kanban",
21011
21271
  "scopeProfileSlug": "review",
21012
- "groupBy": "status",
21013
21272
  "description": "Performance reviews across the cycle, by status.",
21014
21273
  "config": {
21015
- "groupBy": "status",
21274
+ "groupByField": "status",
21016
21275
  "cardFields": [
21017
21276
  "cycle",
21018
21277
  "rating",
@@ -21821,7 +22080,6 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
21821
22080
  "slug": "workflows",
21822
22081
  "type": "kanban",
21823
22082
  "scopeProfileSlug": "workflow",
21824
- "groupBy": "workflow-status",
21825
22083
  "description": "Recurring processes on a board by status.",
21826
22084
  "config": {
21827
22085
  "groupByField": "workflow-status",
@@ -22206,6 +22464,7 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
22206
22464
  "isPublic": true,
22207
22465
  "intent": {
22208
22466
  "label": "Legal",
22467
+ "primary": "legal-contract",
22209
22468
  "produces": [
22210
22469
  {
22211
22470
  "profile": "legal-contract"
@@ -22813,10 +23072,9 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
22813
23072
  "slug": "renewals",
22814
23073
  "type": "kanban",
22815
23074
  "scopeProfileSlug": "legal-contract",
22816
- "groupBy": "status",
22817
23075
  "description": "Contracts by lifecycle stage \u2014 catch renewals and expirations before they bite.",
22818
23076
  "config": {
22819
- "groupBy": "status",
23077
+ "groupByField": "status",
22820
23078
  "cardFields": [
22821
23079
  "counterparty",
22822
23080
  "renewalType",
@@ -22831,10 +23089,9 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
22831
23089
  "slug": "compliance",
22832
23090
  "type": "kanban",
22833
23091
  "scopeProfileSlug": "compliance-item",
22834
- "groupBy": "status",
22835
23092
  "description": "Obligations by status \u2014 nothing goes overdue.",
22836
23093
  "config": {
22837
- "groupBy": "status",
23094
+ "groupByField": "status",
22838
23095
  "cardFields": [
22839
23096
  "obligationType",
22840
23097
  "jurisdiction",
@@ -25609,6 +25866,7 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
25609
25866
  "isPublic": true,
25610
25867
  "intent": {
25611
25868
  "label": "Marketing",
25869
+ "primary": "campaign",
25612
25870
  "produces": [
25613
25871
  {
25614
25872
  "profile": "campaign"
@@ -25957,7 +26215,9 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
25957
26215
  "slug": "campaigns",
25958
26216
  "type": "kanban",
25959
26217
  "scopeProfileSlug": "campaign",
25960
- "groupBy": "campaign-status",
26218
+ "config": {
26219
+ "groupByField": "campaign-status"
26220
+ },
25961
26221
  "description": "Campaign kanban board by status."
25962
26222
  },
25963
26223
  {
@@ -26098,13 +26358,8 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
26098
26358
  "description": "Take a campaign from brief to results \u2014 goal, audience, plan, content, launch with a person's approval, and a report on the leads it brought.",
26099
26359
  "goalTemplate": "Run this campaign ({campaign}); each stage moves its status. BRIEF: write the campaign's goal as a number, its target audience, channel, budget, start and end dates, from the brand kit and the agency's offers. PLAN: list the pieces to make and the outreach to send, each with a date. MAKE: create each piece as a post at the Idea status in Content OS and each outreach sequence as an outreach campaign, so their own processes produce them. LAUNCH: a person approves the launch; nothing is published, sent or paid for before that. REPORT: after the end date, record the lead count and conversion rate, link every new lead the campaign brought, and write what worked and what to change. Pausing or cancelling is a person's call.",
26100
26360
  "subjectProfile": {
26101
- "profileSlug": "campaign",
26102
- "statusProperty": "campaign-status"
26361
+ "profileSlug": "campaign"
26103
26362
  },
26104
- "humanOnlyStatuses": [
26105
- "paused",
26106
- "cancelled"
26107
- ],
26108
26363
  "activators": [
26109
26364
  {
26110
26365
  "on": "created",
@@ -26467,10 +26722,9 @@ Report the submission's current state as STRICT JSON, using ONLY these vocabular
26467
26722
  "slug": "meeting-action-items",
26468
26723
  "type": "kanban",
26469
26724
  "scopeProfileSlug": "task",
26470
- "groupBy": "status",
26471
26725
  "description": "Tasks generated from meetings, by status.",
26472
26726
  "config": {
26473
- "groupBy": "status"
26727
+ "groupByField": "status"
26474
26728
  }
26475
26729
  }
26476
26730
  ],
@@ -27419,7 +27673,6 @@ Pick the SINGLE best provider for this need, or none if nothing fits well. Respo
27419
27673
  "suggestFor": [
27420
27674
  "consultant"
27421
27675
  ],
27422
- "profile": "engagement",
27423
27676
  "views": [
27424
27677
  "engagements"
27425
27678
  ],
@@ -27465,6 +27718,7 @@ Pick the SINGLE best provider for this need, or none if nothing fits well. Respo
27465
27718
  },
27466
27719
  "intent": {
27467
27720
  "label": "Delivery",
27721
+ "primary": "engagement",
27468
27722
  "produces": [
27469
27723
  {
27470
27724
  "profile": "engagement"
@@ -27880,7 +28134,9 @@ Pick the SINGLE best provider for this need, or none if nothing fits well. Respo
27880
28134
  "slug": "engagements",
27881
28135
  "type": "kanban",
27882
28136
  "scopeProfileSlug": "engagement",
27883
- "groupBy": "engagement-status",
28137
+ "config": {
28138
+ "groupByField": "engagement-status"
28139
+ },
27884
28140
  "description": "Client engagements on a board by status."
27885
28141
  },
27886
28142
  {
@@ -27888,7 +28144,9 @@ Pick the SINGLE best provider for this need, or none if nothing fits well. Respo
27888
28144
  "slug": "deliverables",
27889
28145
  "type": "kanban",
27890
28146
  "scopeProfileSlug": "deliverable",
27891
- "groupBy": "deliverable-status",
28147
+ "config": {
28148
+ "groupByField": "deliverable-status"
28149
+ },
27892
28150
  "description": "Deliverables on a board by status."
27893
28151
  },
27894
28152
  {
@@ -27898,6 +28156,16 @@ Pick the SINGLE best provider for this need, or none if nothing fits well. Respo
27898
28156
  "scopeProfileSlug": "contract",
27899
28157
  "description": "Contracts with their status, value, and term."
27900
28158
  },
28159
+ {
28160
+ "name": "Contract Board",
28161
+ "slug": "contract-board",
28162
+ "type": "kanban",
28163
+ "scopeProfileSlug": "contract",
28164
+ "description": "Contracts by status \u2014 the flow board of Get a contract signed.",
28165
+ "config": {
28166
+ "groupByField": "contract-status"
28167
+ }
28168
+ },
27901
28169
  {
27902
28170
  "name": "Client Briefings",
27903
28171
  "slug": "client-briefings",
@@ -28531,13 +28799,8 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
28531
28799
  "description": "Run one client engagement from kick-off to close \u2014 scope, contract, deliverables, final review and the closing invoice \u2014 with a person approving the start and the close.",
28532
28800
  "goalTemplate": "Run this client engagement ({engagement}); each stage moves its status. KICK-OFF: read the won deal, the proposal and every message with the client; write the scope, owner, start and end dates and value on the engagement; create one deliverable per promised output (each with a due date and an owner); reuse the draft contract already linked to the engagement (the daily client pick-up opens one), and create a draft contract only when none is linked. START: a person confirms the contract is signed and the work may start. DELIVER: track every deliverable to accepted (or cancelled by a person), post a short weekly status in this session's room, and flag any deliverable past its due date. CLOSE: once every deliverable is accepted or cancelled, write the closing review (what was delivered, what slipped, what the client said), create the closing invoice in Finance at the draft status for the engagement value still owed (when the engagement bills per deliverable, subtract every milestone invoice already created for it), and propose the renewal or next engagement. A person approves the close. Pausing or cancelling an engagement is a person's call.",
28533
28801
  "subjectProfile": {
28534
- "profileSlug": "engagement",
28535
- "statusProperty": "engagement-status"
28802
+ "profileSlug": "engagement"
28536
28803
  },
28537
- "humanOnlyStatuses": [
28538
- "on-hold",
28539
- "cancelled"
28540
- ],
28541
28804
  "activators": [
28542
28805
  {
28543
28806
  "on": "created",
@@ -28671,13 +28934,8 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
28671
28934
  "description": "Take one deliverable from in progress to accepted by the client \u2014 plan, produce, review internally, send, get the client's sign-off, and bill it when the engagement bills per deliverable.",
28672
28935
  "goalTemplate": "Deliver this deliverable ({deliverable}); each stage moves its status. PLAN: read the engagement's scope and the client's brief, and write what done looks like for this deliverable. PRODUCE: make it, or break it into tasks for the people who will. REVIEW: check it against what done looks like before anyone outside sees it. SEND: a person approves before it goes to the client; record the date it was delivered. ACCEPTED: a person confirms the client accepted it; if the client asked for changes, go back to producing. Then read the engagement's billing: only when it bills per deliverable, create this deliverable's invoice in Finance at the draft status for its share of the engagement value, linked to the deliverable. When it bills at close, create no invoice; the engagement's close bills it.",
28673
28936
  "subjectProfile": {
28674
- "profileSlug": "deliverable",
28675
- "statusProperty": "deliverable-status"
28937
+ "profileSlug": "deliverable"
28676
28938
  },
28677
- "humanOnlyStatuses": [
28678
- "not-started",
28679
- "cancelled"
28680
- ],
28681
28939
  "activators": [
28682
28940
  {
28683
28941
  "on": "enters_status",
@@ -28806,13 +29064,8 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
28806
29064
  "description": "Take a client contract from draft to signed and active, with a person approving what is sent and confirming the signature.",
28807
29065
  "goalTemplate": "Get this contract ({contract}) signed; each stage moves its status. DRAFT: fill the contract from the engagement's scope, value, dates and the agency's standard terms, and list every term that differs from the standard. SEND: a person approves the contract before it goes to the client. SIGNED: ask the person to `provide` the signed file; their approval confirms both sides signed. ACTIVE: set the start and end dates and link the signed document. Expiry and termination are a person's call.",
28808
29066
  "subjectProfile": {
28809
- "profileSlug": "contract",
28810
- "statusProperty": "contract-status"
29067
+ "profileSlug": "contract"
28811
29068
  },
28812
- "humanOnlyStatuses": [
28813
- "expired",
28814
- "terminated"
28815
- ],
28816
29069
  "activators": [
28817
29070
  {
28818
29071
  "on": "created",
@@ -28925,6 +29178,7 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
28925
29178
  "isPublic": true,
28926
29179
  "intent": {
28927
29180
  "label": "Planning",
29181
+ "primary": "sprint",
28928
29182
  "produces": [
28929
29183
  {
28930
29184
  "profile": "sprint"
@@ -29213,7 +29467,6 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
29213
29467
  "slug": "sprint-board",
29214
29468
  "type": "kanban",
29215
29469
  "scopeProfileSlug": "task",
29216
- "groupBy": "task-status",
29217
29470
  "description": "Active sprint tasks in Backlog / Todo / In Progress / Review / Done lanes",
29218
29471
  "config": {
29219
29472
  "groupByField": "task-status",
@@ -29329,7 +29582,6 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
29329
29582
  "slug": "retrospective-board",
29330
29583
  "type": "kanban",
29331
29584
  "scopeProfileSlug": "sprint",
29332
- "groupBy": "sprint-status",
29333
29585
  "description": "Sprint retrospectives \u2014 completed sprints with retrospective notes attached as documents",
29334
29586
  "config": {
29335
29587
  "groupByField": "sprint-status"
@@ -30191,12 +30443,13 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30191
30443
  "isPublic": true,
30192
30444
  "intent": {
30193
30445
  "label": "Research",
30446
+ "primary": "research-finding",
30194
30447
  "produces": [
30195
30448
  {
30196
30449
  "profile": "hypothesis"
30197
30450
  },
30198
30451
  {
30199
- "profile": "finding"
30452
+ "profile": "research-finding"
30200
30453
  }
30201
30454
  ]
30202
30455
  }
@@ -30221,6 +30474,27 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30221
30474
  "icon": "help-circle",
30222
30475
  "color": "blue",
30223
30476
  "description": "An open question driving an investigation \u2014 the thing research answers.",
30477
+ "lifecycle": {
30478
+ "property": "questionStatus",
30479
+ "options": [
30480
+ {
30481
+ "key": "open",
30482
+ "category": "open"
30483
+ },
30484
+ {
30485
+ "key": "exploring",
30486
+ "category": "active"
30487
+ },
30488
+ {
30489
+ "key": "answered",
30490
+ "category": "done"
30491
+ },
30492
+ {
30493
+ "key": "abandoned",
30494
+ "category": "lost"
30495
+ }
30496
+ ]
30497
+ },
30224
30498
  "properties": [
30225
30499
  {
30226
30500
  "slug": "questionStatus",
@@ -30492,7 +30766,7 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30492
30766
  ]
30493
30767
  },
30494
30768
  {
30495
- "slug": "finding",
30769
+ "slug": "research-finding",
30496
30770
  "displayName": "Finding",
30497
30771
  "icon": "check-circle",
30498
30772
  "color": "#10B981",
@@ -30562,7 +30836,7 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30562
30836
  "scopeProfileSlugs": [
30563
30837
  "field",
30564
30838
  "concept",
30565
- "finding"
30839
+ "research-finding"
30566
30840
  ],
30567
30841
  "description": "The cartography \u2014 fields and concepts, their overlaps and prerequisites, and the findings attached to them.",
30568
30842
  "defaultView": true,
@@ -30606,17 +30880,16 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30606
30880
  "slug": "research-hypotheses",
30607
30881
  "type": "kanban",
30608
30882
  "scopeProfileSlug": "hypothesis",
30609
- "groupBy": "stance",
30610
30883
  "description": "Candidate theories, grouped by where the evidence has left them.",
30611
30884
  "config": {
30612
- "groupBy": "stance"
30885
+ "groupByField": "stance"
30613
30886
  }
30614
30887
  },
30615
30888
  {
30616
30889
  "name": "Findings",
30617
30890
  "slug": "research-findings",
30618
30891
  "type": "table",
30619
- "scopeProfileSlug": "finding",
30892
+ "scopeProfileSlug": "research-finding",
30620
30893
  "description": "What's been established, with its confidence.",
30621
30894
  "config": {
30622
30895
  "columns": [
@@ -30686,7 +30959,7 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30686
30959
  },
30687
30960
  {
30688
30961
  "sourceProfileSlug": "research",
30689
- "targetProfileSlug": "finding",
30962
+ "targetProfileSlug": "research-finding",
30690
30963
  "type": "concluded",
30691
30964
  "label": "Concluded"
30692
30965
  },
@@ -30709,19 +30982,19 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30709
30982
  "label": "Cites"
30710
30983
  },
30711
30984
  {
30712
- "sourceProfileSlug": "finding",
30985
+ "sourceProfileSlug": "research-finding",
30713
30986
  "targetProfileSlug": "research",
30714
30987
  "type": "derived_from",
30715
30988
  "label": "Derived from"
30716
30989
  },
30717
30990
  {
30718
- "sourceProfileSlug": "finding",
30991
+ "sourceProfileSlug": "research-finding",
30719
30992
  "targetProfileSlug": "source",
30720
30993
  "type": "supported_by",
30721
30994
  "label": "Supported by"
30722
30995
  },
30723
30996
  {
30724
- "sourceProfileSlug": "finding",
30997
+ "sourceProfileSlug": "research-finding",
30725
30998
  "targetProfileSlug": "concept",
30726
30999
  "type": "about",
30727
31000
  "label": "About"
@@ -30731,14 +31004,10 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30731
31004
  {
30732
31005
  "name": "Research a Question",
30733
31006
  "description": "Investigate a question end-to-end \u2014 gather and rank sources (keeping the rejected ones), weigh hypotheses, emit findings with calibrated confidence, and map them onto the knowledge graph.",
30734
- "goalTemplate": "Investigate the question {question} and record the WHOLE investigation, not just the answer; each stage moves the question's status. FRAME: restate the question so it can be answered, and say what evidence would answer it. GATHER: use the source-triage skill to gather sources widely via web search, classify each by type and credibility, and create a `source` entity for every one \u2014 including the ones you rejected, marked rejected with a reason (never silently drop a source). WEIGH: form the candidate answers as `hypothesis` entities that answer the question; link every source that bears on a hypothesis with `supports` or `contradicts`; use the evidence-synthesis skill to emit `finding` entities with honestly calibrated confidence, each `supported_by` its sources and `derived_from` this research; use the concept-cartography skill to place the findings onto the knowledge map. ANSWER: create the `research` entity that `investigates` the question and `explored` the sources, write its conclusion and confidence, and set the question to answered. Abandoning a question is a person's call. Every write is proposal-gated and reviewable.",
31007
+ "goalTemplate": "Investigate the question {question} and record the WHOLE investigation, not just the answer; each stage moves the question's status. FRAME: restate the question so it can be answered, and say what evidence would answer it. GATHER: use the source-triage skill to gather sources widely via web search, classify each by type and credibility, and create a `source` entity for every one \u2014 including the ones you rejected, marked rejected with a reason (never silently drop a source). WEIGH: form the candidate answers as `hypothesis` entities that answer the question; link every source that bears on a hypothesis with `supports` or `contradicts`; use the evidence-synthesis skill to emit `research-finding` entities with honestly calibrated confidence, each `supported_by` its sources and `derived_from` this research; use the concept-cartography skill to place the findings onto the knowledge map. ANSWER: create the `research` entity that `investigates` the question and `explored` the sources, write its conclusion and confidence, and set the question to answered. Abandoning a question is a person's call. Every write is proposal-gated and reviewable.",
30735
31008
  "subjectProfile": {
30736
- "profileSlug": "question",
30737
- "statusProperty": "questionStatus"
31009
+ "profileSlug": "question"
30738
31010
  },
30739
- "humanOnlyStatuses": [
30740
- "abandoned"
30741
- ],
30742
31011
  "activators": [
30743
31012
  {
30744
31013
  "on": "created",
@@ -30852,8 +31121,7 @@ Recent conversation (oldest first, JSON): {{steps.read-comms.output.messages}}`
30852
31121
  "description": "When a finding is validated, propose the posts that would share it, each linked to the finding.",
30853
31122
  "goalTemplate": "Turn this validated finding ({finding}) into content. Read the finding, its sources and the research it came from. Propose one to three posts that share it with the audience \u2014 each created at the Idea status, on the pillar it fits, with its hook and the reason it is worth making \u2014 and link each one derived_from the finding. Quote only what the sources support.",
30854
31123
  "subjectProfile": {
30855
- "profileSlug": "finding",
30856
- "statusProperty": "status"
31124
+ "profileSlug": "research-finding"
30857
31125
  },
30858
31126
  "activators": [
30859
31127
  {
@@ -31061,6 +31329,27 @@ counter-evidence: research that only kept what agreed with it isn't research.
31061
31329
  "icon": "help-circle",
31062
31330
  "color": "blue",
31063
31331
  "description": "The open question being decided.",
31332
+ "lifecycle": {
31333
+ "property": "questionStatus",
31334
+ "options": [
31335
+ {
31336
+ "key": "open",
31337
+ "category": "open"
31338
+ },
31339
+ {
31340
+ "key": "exploring",
31341
+ "category": "active"
31342
+ },
31343
+ {
31344
+ "key": "answered",
31345
+ "category": "done"
31346
+ },
31347
+ {
31348
+ "key": "abandoned",
31349
+ "category": "lost"
31350
+ }
31351
+ ]
31352
+ },
31064
31353
  "properties": [
31065
31354
  {
31066
31355
  "slug": "questionStatus",
@@ -31135,6 +31424,27 @@ counter-evidence: research that only kept what agreed with it isn't research.
31135
31424
  "icon": "scale",
31136
31425
  "color": "amber",
31137
31426
  "description": "A resolved decision \u2014 the recommendation, why it won, and the alternatives that lost (and why).",
31427
+ "lifecycle": {
31428
+ "property": "decisionStatus",
31429
+ "options": [
31430
+ {
31431
+ "key": "proposed",
31432
+ "category": "open"
31433
+ },
31434
+ {
31435
+ "key": "accepted",
31436
+ "category": "done"
31437
+ },
31438
+ {
31439
+ "key": "superseded",
31440
+ "category": "lost"
31441
+ },
31442
+ {
31443
+ "key": "rejected",
31444
+ "category": "lost"
31445
+ }
31446
+ ]
31447
+ },
31138
31448
  "properties": [
31139
31449
  {
31140
31450
  "slug": "summary",
@@ -31239,13 +31549,8 @@ counter-evidence: research that only kept what agreed with it isn't research.
31239
31549
  "description": "Put a proposed decision in front of a person with its options and evidence, and record what they chose.",
31240
31550
  "goalTemplate": "Bring this decision ({decision}) to a person. Summarise the question it resolves, the options that were considered, the evidence for and against each, and the recommendation, in one screen. A person accepts or rejects it. When a later decision replaces it, the person marks it superseded.",
31241
31551
  "subjectProfile": {
31242
- "profileSlug": "decision",
31243
- "statusProperty": "decisionStatus"
31552
+ "profileSlug": "decision"
31244
31553
  },
31245
- "humanOnlyStatuses": [
31246
- "superseded",
31247
- "rejected"
31248
- ],
31249
31554
  "activators": [
31250
31555
  {
31251
31556
  "on": "created",
@@ -31333,6 +31638,7 @@ counter-evidence: research that only kept what agreed with it isn't research.
31333
31638
  "isPublic": true,
31334
31639
  "intent": {
31335
31640
  "label": "Distribution",
31641
+ "primary": "post",
31336
31642
  "produces": [
31337
31643
  {
31338
31644
  "profile": "post"
@@ -31717,7 +32023,9 @@ counter-evidence: research that only kept what agreed with it isn't research.
31717
32023
  "slug": "content-calendar",
31718
32024
  "type": "kanban",
31719
32025
  "scopeProfileSlug": "post",
31720
- "groupBy": "post-status",
32026
+ "config": {
32027
+ "groupByField": "post-status"
32028
+ },
31721
32029
  "description": "Posts on a board by status \u2014 idea to published."
31722
32030
  },
31723
32031
  {
@@ -35042,48 +35350,46 @@ function checkProcessContract(tpl, resolveDep, corpus, add) {
35042
35350
  for (const pb of tpl.playbooks || []) {
35043
35351
  const at = `playbooks."${pb.name}"`;
35044
35352
  const kind = pb.subjectProfile?.profileSlug;
35045
- const statusProperty = pb.subjectProfile?.statusProperty;
35046
- let statuses = null;
35047
- if (statusProperty !== void 0) {
35048
- if (!kind) {
35353
+ const raw = pb;
35354
+ const subjectRaw = pb.subjectProfile ?? {};
35355
+ for (const [field, present] of [
35356
+ ["subjectProfile.statusProperty", subjectRaw.statusProperty !== void 0],
35357
+ ["subjectProfile.onEnd", subjectRaw.onEnd !== void 0],
35358
+ ["onEnd", raw.onEnd !== void 0],
35359
+ ["humanOnlyStatuses", raw.humanOnlyStatuses !== void 0],
35360
+ ["subjectProfile.humanOnlyStatuses", subjectRaw.humanOnlyStatuses !== void 0]
35361
+ ])
35362
+ if (present)
35049
35363
  add(
35050
- "playbook-status-property-no-subject",
35051
- `${at}.subjectProfile.statusProperty`,
35052
- `playbook "${pb.name}" declares statusProperty "${statusProperty}" without a subjectProfile.profileSlug`
35364
+ "playbook-field-retired",
35365
+ `${at}.${field}`,
35366
+ `playbook "${pb.name}" declares ${field} \u2014 retired: the process follows the kind's declared lifecycle (a person-only status is \`humanOnly\` on that lifecycle, a status no stage owns is derived, and a session end moves nothing)`
35053
35367
  );
35054
- } else {
35055
- const decls = profileDeclarations(kind, tpl, resolveDep, corpus);
35056
- const prop = decls.flatMap((p) => p.properties ?? []).find((pr) => pr.slug === statusProperty);
35057
- if (!prop) {
35058
- if (decls.length > 0 || hasCorpus(resolveDep))
35059
- add(
35060
- "playbook-status-property-unknown",
35061
- `${at}.subjectProfile.statusProperty`,
35062
- `playbook "${pb.name}" statusProperty "${statusProperty}" is not a property of kind "${kind}"`
35063
- );
35064
- } else if (prop.inputType !== "select" || !Array.isArray(prop.enumValues) || prop.enumValues.length === 0) {
35065
- add(
35066
- "playbook-status-property-not-select",
35067
- `${at}.subjectProfile.statusProperty`,
35068
- `playbook "${pb.name}" statusProperty "${kind}.${statusProperty}" must be a select property with enumValues (found inputType "${String(prop.inputType)}")`
35069
- );
35070
- } else {
35071
- statuses = new Set(prop.enumValues);
35072
- }
35368
+ let statuses = null;
35369
+ let lifecycleProperty;
35370
+ let judgeable = hasCorpus(resolveDep);
35371
+ if (kind) {
35372
+ const decls = profileDeclarations(kind, tpl, resolveDep, corpus);
35373
+ judgeable ||= decls.length > 0;
35374
+ const declared = decls.find((p) => p.lifecycle !== void 0)?.lifecycle;
35375
+ if (declared) {
35376
+ lifecycleProperty = declared.property;
35377
+ statuses = new Set(declared.options.map((o) => o.key));
35073
35378
  }
35074
35379
  }
35075
35380
  const checkStatus = (rule, path, value) => {
35076
- if (statusProperty === void 0) {
35381
+ if (lifecycleProperty === void 0) {
35382
+ if (!judgeable) return;
35077
35383
  add(
35078
- `${rule}-no-property`,
35384
+ `${rule}-no-lifecycle`,
35079
35385
  path,
35080
- `${path} = "${String(value)}" needs subjectProfile.statusProperty \u2014 a status is a value OF that property`
35386
+ `${path} = "${String(value)}" needs "${String(kind)}" to declare a lifecycle \u2014 a status is a value of the kind's lifecycle property`
35081
35387
  );
35082
35388
  } else if (statuses && !statuses.has(value)) {
35083
35389
  add(
35084
35390
  rule,
35085
35391
  path,
35086
- `${path} = "${String(value)}" is not a value of ${kind}.${statusProperty} (${[...statuses].join(", ")})`
35392
+ `${path} = "${String(value)}" is not a value of ${kind}.${lifecycleProperty} (${[...statuses].join(", ")})`
35087
35393
  );
35088
35394
  }
35089
35395
  };
@@ -35128,9 +35434,6 @@ function checkProcessContract(tpl, resolveDep, corpus, add) {
35128
35434
  add("activator-status-unexpected", `${ap}.status`, `${ap}.status is not read when on = "${a.on}"`);
35129
35435
  }
35130
35436
  });
35131
- (pb.humanOnlyStatuses ?? []).forEach(
35132
- (s, i) => checkStatus("human-only-status-unknown", `${at}.humanOnlyStatuses[${i}]`, s)
35133
- );
35134
35437
  const outputs = [
35135
35438
  ...(pb.expectedOutputs ?? []).map((o, i) => ({
35136
35439
  at: `${at}.expectedOutputs[${i}]`,
@@ -35313,18 +35616,6 @@ function checkLifecycleDeclarations(tpl, resolveDep, corpus, add) {
35313
35616
  `"${profile.slug}" declares "${other.property}" here ${profile.lifecycle ? "with a different lifecycle than" : "without the lifecycle"} another template declares \u2014 the pod keeps whichever installs first, so every declaration must be identical`
35314
35617
  );
35315
35618
  }
35316
- for (const pb of tpl.playbooks ?? []) {
35317
- const kind = pb.subjectProfile?.profileSlug;
35318
- const statusProperty = pb.subjectProfile?.statusProperty;
35319
- if (!kind || statusProperty === void 0) continue;
35320
- const lc = profileDeclarations(kind, tpl, resolveDep, corpus).map((p) => p.lifecycle).find((l) => l !== void 0);
35321
- if (lc && lc.property !== statusProperty)
35322
- add(
35323
- "lifecycle-status-property",
35324
- `playbooks."${pb.name}".subjectProfile.statusProperty`,
35325
- `playbook "${pb.name}" moves "${kind}.${statusProperty}" but "${kind}"'s lifecycle is "${lc.property}" \u2014 a kind has ONE lifecycle`
35326
- );
35327
- }
35328
35619
  }
35329
35620
  function checkPresentationDeclarations(tpl, resolveDep, corpus, add) {
35330
35621
  for (const profile of tpl.profiles ?? []) {
@@ -35381,6 +35672,36 @@ function checkPresentationDeclarations(tpl, resolveDep, corpus, add) {
35381
35672
  );
35382
35673
  }
35383
35674
  }
35675
+ function checkKanbanGrouping(tpl, resolveDep, corpus, add) {
35676
+ for (const v of tpl.views ?? []) {
35677
+ if (String(v.type || "").toLowerCase() !== "kanban") continue;
35678
+ const at = `views."${v.name}"`;
35679
+ const dead = v.groupBy !== void 0 ? "groupBy" : v.config?.groupBy !== void 0 ? "config.groupBy" : null;
35680
+ if (dead)
35681
+ add(
35682
+ "view-kanban-group-dead",
35683
+ `${at}.${dead}`,
35684
+ `kanban "${v.name}" declares ${dead} \u2014 the board never reads it (it groups by status); write config.groupByField instead`
35685
+ );
35686
+ const field = v.config?.groupByField;
35687
+ if (typeof field !== "string" || !field.trim()) {
35688
+ add("view-kanban-group-missing", `${at}.config.groupByField`, `kanban "${v.name}" names no config.groupByField \u2014 its columns would be the values of "status"`);
35689
+ continue;
35690
+ }
35691
+ const kind = v.scopeProfileSlug;
35692
+ if (!kind) continue;
35693
+ const slug = field.includes(".") ? field.split(".").pop() : field;
35694
+ const decls = profileDeclarations(kind, tpl, resolveDep, corpus);
35695
+ if (decls.length === 0) continue;
35696
+ const prop = decls.flatMap((p) => p.properties ?? []).find((p) => p.slug === slug);
35697
+ if (!prop || prop.inputType !== "select" || !Array.isArray(prop.enumValues) || prop.enumValues.length === 0)
35698
+ add(
35699
+ "view-kanban-group-unknown",
35700
+ `${at}.config.groupByField`,
35701
+ `kanban "${v.name}" groups by "${slug}", which is not a select property of "${kind}" with enumValues`
35702
+ );
35703
+ }
35704
+ }
35384
35705
  function checkIntent(tpl, resolveDep, add) {
35385
35706
  const intent = tpl.meta?.intent;
35386
35707
  if (intent === void 0) return;
@@ -35394,16 +35715,37 @@ function checkIntent(tpl, resolveDep, add) {
35394
35715
  return;
35395
35716
  }
35396
35717
  if (!intent || typeof intent !== "object" || Array.isArray(intent)) {
35397
- add("intent-shape", "meta.intent", "meta.intent must be { label, produces?: [{ profile }] }");
35718
+ add("intent-shape", "meta.intent", "meta.intent must be { label, primary?, produces?: [{ profile }] }");
35398
35719
  return;
35399
35720
  }
35400
- const { label, produces } = intent;
35721
+ const { label, produces, primary } = intent;
35401
35722
  if (typeof label !== "string" || !label.trim() || label.trim().length > INTENT_LABEL_MAX)
35402
35723
  add(
35403
35724
  "intent-label",
35404
35725
  "meta.intent.label",
35405
35726
  `meta.intent.label must be a non-empty string of at most ${INTENT_LABEL_MAX} characters (got ${JSON.stringify(label)})`
35406
35727
  );
35728
+ const producedSlugs = Array.isArray(produces) ? produces.map((e) => e?.profile).filter((s) => typeof s === "string") : [];
35729
+ if (primary !== void 0 && (typeof primary !== "string" || !primary.trim()))
35730
+ add("intent-shape", "meta.intent.primary", `meta.intent.primary must be a profile slug (got ${JSON.stringify(primary)})`);
35731
+ else if (primary === void 0 && producedSlugs.length > 0)
35732
+ add(
35733
+ "intent-primary-missing",
35734
+ "meta.intent.primary",
35735
+ `"${tpl.meta.slug}" produces ${producedSlugs.join(", ")} but names no meta.intent.primary \u2014 name the ONE output its main flow produces`
35736
+ );
35737
+ else if (primary !== void 0 && producedSlugs.length === 0)
35738
+ add(
35739
+ "intent-primary-library",
35740
+ "meta.intent.primary",
35741
+ `meta.intent.primary "${primary}" on a library \u2014 a space with no produces has no flow; list "${primary}" in produces or drop primary`
35742
+ );
35743
+ else if (primary !== void 0 && !producedSlugs.includes(primary))
35744
+ add(
35745
+ "intent-primary-not-produced",
35746
+ "meta.intent.primary",
35747
+ `meta.intent.primary "${primary}" is not in meta.intent.produces (${producedSlugs.join(", ")}) \u2014 the primary is one of what the space produces`
35748
+ );
35407
35749
  if (produces === void 0) return;
35408
35750
  if (!Array.isArray(produces)) {
35409
35751
  add("intent-shape", "meta.intent.produces", "meta.intent.produces must be a list of { profile }");
@@ -35452,12 +35794,20 @@ function checkStart(tpl, resolveDep, corpus, add) {
35452
35794
  const strList = (v) => Array.isArray(v) && v.every(isStr);
35453
35795
  const st = raw;
35454
35796
  const fs = st?.firstSession;
35455
- const shapeOk = !!st && typeof st === "object" && !Array.isArray(st) && Number.isInteger(st.order) && st.order > 0 && isStr(st.label) && isStr(st.promise) && strList(st.suggestFor) && isStr(st.profile) && strList(st.views) && strList(st.starters) && Array.isArray(st.example) && st.example.every((e) => e && isStr(e.title) && (e.values === void 0 || typeof e.values === "object" && e.values !== null && !Array.isArray(e.values))) && !!fs && isStr(fs.title) && isStr(fs.goal);
35797
+ const shapeOk = !!st && typeof st === "object" && !Array.isArray(st) && Number.isInteger(st.order) && st.order > 0 && isStr(st.label) && isStr(st.promise) && strList(st.suggestFor) && (st.profile === void 0 || isStr(st.profile)) && strList(st.views) && strList(st.starters) && Array.isArray(st.example) && st.example.every((e) => e && isStr(e.title) && (e.values === void 0 || typeof e.values === "object" && e.values !== null && !Array.isArray(e.values))) && !!fs && isStr(fs.title) && isStr(fs.goal);
35456
35798
  if (!shapeOk) {
35457
- add("start-shape", "meta.start", "meta.start must be { order (int \u2265 1), label, promise, suggestFor[], profile, views[], example[{ title, values? }], firstSession{ title, goal }, starters[] }");
35799
+ add("start-shape", "meta.start", "meta.start must be { order (int \u2265 1), label, promise, suggestFor[], profile?, views[], example[{ title, values? }], firstSession{ title, goal }, starters[] }");
35458
35800
  return;
35459
35801
  }
35460
35802
  const start = st;
35803
+ const primary = tpl.meta.intent?.primary;
35804
+ if (start.profile !== void 0 && start.profile === primary)
35805
+ add("start-profile-redundant", "meta.start.profile", `meta.start.profile repeats meta.intent.primary "${primary}" \u2014 omit it (the start defaults to the primary output)`);
35806
+ const profile = startProfileOf(tpl.meta);
35807
+ if (!profile) {
35808
+ add("start-profile-missing", "meta.profile", `meta.start names no kind and "${tpl.meta.slug}" declares no meta.intent.primary to default to`);
35809
+ return;
35810
+ }
35461
35811
  if (start.label.trim().length > INTENT_LABEL_MAX)
35462
35812
  add("start-label", "meta.start.label", `meta.start.label must be at most ${INTENT_LABEL_MAX} characters (got ${JSON.stringify(start.label)})`);
35463
35813
  const [exMin, exMax] = START_EXAMPLE_RANGE;
@@ -35476,24 +35826,24 @@ function checkStart(tpl, resolveDep, corpus, add) {
35476
35826
  return;
35477
35827
  }
35478
35828
  const scope = [view.scopeProfileSlug, ...view.scopeProfileSlugs ?? []].filter(Boolean);
35479
- if (!scope.includes(start.profile))
35480
- add("start-views-scope", at, `view "${slug}" is not scoped to "${start.profile}" (scope: ${scope.join(", ") || "none"})`);
35829
+ if (!scope.includes(profile))
35830
+ add("start-views-scope", at, `view "${slug}" is not scoped to "${profile}" (scope: ${scope.join(", ") || "none"})`);
35481
35831
  });
35482
35832
  const closure = [...collectDependencyClosure(tpl, resolveDep)];
35483
35833
  const sources = [tpl, ...closure.map(resolveDep).filter((t) => !!t)];
35484
- const decls = sources.flatMap((t) => (t.profiles ?? []).filter((p) => p.slug === start.profile));
35834
+ const decls = sources.flatMap((t) => (t.profiles ?? []).filter((p) => p.slug === profile));
35485
35835
  if (decls.length === 0) {
35486
35836
  if (!closure.some((d) => !resolveDep(d)))
35487
- add("start-profile-unknown", "meta.start.profile", `"${start.profile}" is neither declared by "${tpl.meta.slug}" nor by a template it depends on`);
35837
+ add("start-profile-unknown", "meta.profile", `"${profile}" is neither declared by "${tpl.meta.slug}" nor by a template it depends on`);
35488
35838
  } else {
35489
35839
  const props = new Map(decls.flatMap((p) => p.properties ?? []).map((p) => [p.slug, p]));
35490
35840
  start.example.forEach((row, i) => {
35491
35841
  for (const [key, value] of Object.entries(row.values ?? {})) {
35492
35842
  const at = `meta.start.example[${i}].values.${key}`;
35493
35843
  const prop = props.get(key);
35494
- if (!prop) add("start-example-property", at, `"${key}" is not a property of "${start.profile}"`);
35844
+ if (!prop) add("start-example-property", at, `"${key}" is not a property of "${profile}"`);
35495
35845
  else if (prop.enumValues?.length && !prop.enumValues.includes(value))
35496
- add("start-example-enum", at, `${JSON.stringify(value)} is not one of ${start.profile}.${key}: ${prop.enumValues.join(", ")}`);
35846
+ add("start-example-enum", at, `${JSON.stringify(value)} is not one of ${profile}.${key}: ${prop.enumValues.join(", ")}`);
35497
35847
  }
35498
35848
  });
35499
35849
  }
@@ -36117,6 +36467,7 @@ function validateTemplate(authored, authoredOpts = {}) {
36117
36467
  checkSharedRoleReferences(tpl, resolveDepFrom(opts), add);
36118
36468
  checkStageDomains(tpl, resolveDepFrom(opts), add);
36119
36469
  checkProcessContract(tpl, resolveDepFrom(opts), opts.corpus, add);
36470
+ checkKanbanGrouping(tpl, resolveDepFrom(opts), opts.corpus, add);
36120
36471
  checkLifecycleDeclarations(tpl, resolveDepFrom(opts), opts.corpus, add);
36121
36472
  checkPresentationDeclarations(tpl, resolveDepFrom(opts), opts.corpus, add);
36122
36473
  checkIntent(tpl, resolveDepFrom(opts), add);
@@ -36487,7 +36838,9 @@ function toPackageDefinition(slugOrYaml) {
36487
36838
  // `taskIntents` note on the PackageDefinition type above.
36488
36839
  taskIntents: tpl.taskIntents,
36489
36840
  ...tpl.meta.intent ? { intent: tpl.meta.intent } : {},
36490
- ...tpl.meta.start ? { start: tpl.meta.start } : {},
36841
+ // The start's kind defaults to the primary output (one source); the wire
36842
+ // always carries it, so the pod, the CP and the landing read `start.profile`.
36843
+ ...tpl.meta.start ? { start: { ...tpl.meta.start, profile: startProfileOf(tpl.meta) } } : {},
36491
36844
  sourceRoles: ws.sourceRoles,
36492
36845
  defaultSources: ws.defaultSources,
36493
36846
  profiles: tpl.profiles,