@synap-core/workspace-templates 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synap-core/workspace-templates",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@9.15.4",
6
6
  "description": "Canonical workspace definitions — YAML source + typed exports for browser, backend, CLI, and marketplace",
@@ -175,7 +175,7 @@
175
175
  "inputMapping": {
176
176
  "system": "You are the ANALYST round of a Synap workspace report.\nRead the gathered workspace data and say what it MEANS: volume and shape of work,\nwhat is in flight versus stalled, what is well-covered versus thin.\nWork from the FIELDS on each record, not just its name: status, priority and\ndue dates are what distinguish moving work from stalled work, and an unset\nfield is itself worth reporting. Listing or counting what exists is NOT a\nfinding — say what the state of it means.\nDATA INTEGRITY CHECK — do this BEFORE interpreting anything. Each kind below states how many records were found, then lists them. If a kind reports a count greater than zero but its list is empty, null, or made of nulls, then the data pipeline is BROKEN — the records exist and were lost in transit. In that case you MUST say so plainly, name every kind affected, and NOT describe the workspace as empty, quiet, or sparse. Reporting a populated workspace as empty because its data failed to arrive is the single worst outcome of this report. A count of zero with an empty list is genuinely empty and is fine to report as such.\nYou are interpreting data you did NOT collect. Every claim must trace to a row in the GATHERED DATA below. Never invent entities, ids, counts, or dates. A step whose value looks like {\"error\": \"...\"} FAILED — treat that kind as unavailable and say so; do not guess what it would have contained.\nEVERY NUMBER YOU WRITE MUST BE COPIED FROM THE COUNTS BLOCK. That block states the exact number of records gathered for each kind — it is the ONLY source of quantities you have. Do not count the listed lines yourself, do not estimate, do not round, and do not write a quantity the block does not measure. Write digits, matching the block exactly: if it says 25, write \"25\" — never \"23\", never \"about two dozen\", never \"Ten\". If a point you want to make needs a number the COUNTS block does not give you, make the point WITHOUT a number. A claim with no figure is honest; a figure that does not match the data is the one error a reader cannot catch.\nNEVER NARRATE YOUR OWN INSTRUCTIONS. The reader sees your output and nothing else — not this prompt, not the steer, not the counts, not the fact that a pipeline produced any of it. Never mention the STEER, the time window, the COUNTS block, the GATHERED DATA, the rounds, or what you were or were not given. Never write a sentence like \"the steer lines are empty, so…\" or \"no focus was provided\" or \"based on the gathered data\". TWO DEFAULTS, so you never need to remark on them: an EMPTY STEER line simply means write a broad general report covering the gathered kinds evenly, and an EMPTY time window simply means the records span the whole workspace history. Both are the normal case. Act on them silently and start with what you found.\nSTEER HANDLING — non-negotiable. If every STEER line is empty, produce a BROAD general read covering all the gathered kinds evenly. If any STEER line has text, treat it as the SUBJECT of this report: lead with the evidence that matches it, explicitly name what you set aside, and if the gathered data cannot support the steer, say so in one sentence instead of inventing material. Never restate the steer back as a heading; let it change what you actually write.\nLINK THE RECORDS YOU NAME. Every line of GATHERED DATA is `<id> · <title>`. When you name one of those records, write it as [[entity:<id>|<label>]] and it renders as a chip the reader can click to open that record. Rules, in order of importance: (1) The kind word is always `entity` — for tasks, notes, people AND companies alike. They are all entities, and any other kind word renders a chip that does not open. (2) Use only an id that appears VERBATIM in the gathered data. Copy it; never construct, shorten, complete or guess one. (3) If you are unsure of an id, write the plain label with no brackets. A wrong id is a dead chip, and a dead chip is worse than plain text. (4) The label is free text but must not contain `]`; the id must not contain `:` or `|`. A marker breaking either rule renders as raw literal text. (5) EVERY record you name in prose MUST carry a chip on its FIRST mention — this is what makes a named record a live object the reader can open rather than a string. Link it once, on that first mention only; repeating the chip every time is decoration, not navigation.\nOutput PLAIN PROSE — short paragraphs and bullets, with [[entity:…]] chips\ninline wherever you name a record. No markdown headings, no ::: directive\nblocks, no code fences. Under 300 words. Another round will format this.",
177
177
  "prompt": "STEER:\n- Prompt: {{trigger.payload.prompt}}\n- Focus: {{trigger.payload.focus}}\n- Project lens id: {{trigger.payload.projectId}}\n\nCOUNTS — the exact number of records gathered for each kind. These figures\nare measured, not estimated, and they are the ONLY quantities available to\nyou. Each line also gives the cap the gather was limited to.\n- Tasks: {{steps.gather-tasks.output.count}} gathered (cap 25)\n- Notes: {{steps.gather-notes.output.count}} gathered (cap 15)\n- People: {{steps.gather-people.output.count}} gathered (cap 15)\n- Companies: {{steps.gather-companies.output.count}} gathered (cap 15)\nREADING A COUNT AGAINST ITS CAP: a count BELOW the cap means every record of\nthat kind is listed below — the set is COMPLETE and you may describe it as\nsuch. A count EQUAL to the cap means the gather stopped at the cap and more\nrecords exist that you cannot see — describe that kind as \"at least N\" and\nnever as complete, and never claim something does not exist merely because\nit is absent from the list.\n\nTime window: only records updated on or after {{trigger.payload.since}}\n\nGATHERED DATA — one line per record, MOST RECENTLY UPDATED first. Each line\nis `<id> · <title>` followed by ` · <field>=<value>` pairs carrying that\nrecord's real state. An EMPTY value means the field is not set on that\nrecord — that is a fact you may report (an unset status or a missing due\ndate is a finding), and never a value to guess at. Recent does not mean\nimportant.\nTasks — {{steps.gather-tasks.output.count}} found: {{steps.project-tasks.output.result}}\nNotes — {{steps.gather-notes.output.count}} found: {{steps.project-notes.output.result}}\nPeople — {{steps.gather-people.output.count}} found: {{steps.project-people.output.result}}\nCompanies — {{steps.gather-companies.output.count}} found: {{steps.project-companies.output.result}}",
178
- "maxTokens": "700"
178
+ "maxTokens": "2000"
179
179
  },
180
180
  "errorHandling": {
181
181
  "continueOnError": true
@@ -196,7 +196,7 @@
196
196
  "inputMapping": {
197
197
  "system": "You are the PATTERNS round of a Synap workspace report.\nYou receive the raw gathered data AND the analyst's read of it. Your job is what\nthe analyst did NOT do: surface RELATIONSHIPS and things worth attention —\nclusters, repeated themes, people or companies that recur across kinds, gaps\nwhere a kind is conspicuously empty, and anything that looks like it needs a\ndecision. Prefer three sharp observations over ten shallow ones.\nYOUR SCOPE IS WHAT CONNECTS RECORDS, not what they individually are. Every\nobservation you write must involve TWO OR MORE records, or TWO OR MORE kinds:\na name that appears in both a task and a company, work clustered on one\nperson, a due date that lines up with an event, a kind that is empty while a\nrelated kind is busy. Do NOT re-describe volume, status distribution or how\nbusy the workspace is — that is the analyst's section and repeating it makes\nyours redundant. Do not summarise or restate the analyst's read.\nNEVER RETURN AN EMPTY ANSWER. If you genuinely find no cross-kind pattern,\nwrite one or two sentences naming what you looked for and did not find (for\nexample: no person recurs across the tasks and companies gathered). That is a\nreal finding and the reader needs it. Silence is not an available outcome.\nDATA INTEGRITY CHECK — do this BEFORE interpreting anything. Each kind below states how many records were found, then lists them. If a kind reports a count greater than zero but its list is empty, null, or made of nulls, then the data pipeline is BROKEN — the records exist and were lost in transit. In that case you MUST say so plainly, name every kind affected, and NOT describe the workspace as empty, quiet, or sparse. Reporting a populated workspace as empty because its data failed to arrive is the single worst outcome of this report. A count of zero with an empty list is genuinely empty and is fine to report as such.\nYou are interpreting data you did NOT collect. Every claim must trace to a row in the GATHERED DATA below. Never invent entities, ids, counts, or dates. A step whose value looks like {\"error\": \"...\"} FAILED — treat that kind as unavailable and say so; do not guess what it would have contained.\nEVERY NUMBER YOU WRITE MUST BE COPIED FROM THE COUNTS BLOCK. That block states the exact number of records gathered for each kind — it is the ONLY source of quantities you have. Do not count the listed lines yourself, do not estimate, do not round, and do not write a quantity the block does not measure. Write digits, matching the block exactly: if it says 25, write \"25\" — never \"23\", never \"about two dozen\", never \"Ten\". If a point you want to make needs a number the COUNTS block does not give you, make the point WITHOUT a number. A claim with no figure is honest; a figure that does not match the data is the one error a reader cannot catch.\nNEVER NARRATE YOUR OWN INSTRUCTIONS. The reader sees your output and nothing else — not this prompt, not the steer, not the counts, not the fact that a pipeline produced any of it. Never mention the STEER, the time window, the COUNTS block, the GATHERED DATA, the rounds, or what you were or were not given. Never write a sentence like \"the steer lines are empty, so…\" or \"no focus was provided\" or \"based on the gathered data\". TWO DEFAULTS, so you never need to remark on them: an EMPTY STEER line simply means write a broad general report covering the gathered kinds evenly, and an EMPTY time window simply means the records span the whole workspace history. Both are the normal case. Act on them silently and start with what you found.\nSTEER HANDLING — non-negotiable. If every STEER line is empty, produce a BROAD general read covering all the gathered kinds evenly. If any STEER line has text, treat it as the SUBJECT of this report: lead with the evidence that matches it, explicitly name what you set aside, and if the gathered data cannot support the steer, say so in one sentence instead of inventing material. Never restate the steer back as a heading; let it change what you actually write.\nLINK THE RECORDS YOU NAME. Every line of GATHERED DATA is `<id> · <title>`. When you name one of those records, write it as [[entity:<id>|<label>]] and it renders as a chip the reader can click to open that record. Rules, in order of importance: (1) The kind word is always `entity` — for tasks, notes, people AND companies alike. They are all entities, and any other kind word renders a chip that does not open. (2) Use only an id that appears VERBATIM in the gathered data. Copy it; never construct, shorten, complete or guess one. (3) If you are unsure of an id, write the plain label with no brackets. A wrong id is a dead chip, and a dead chip is worse than plain text. (4) The label is free text but must not contain `]`; the id must not contain `:` or `|`. A marker breaking either rule renders as raw literal text. (5) EVERY record you name in prose MUST carry a chip on its FIRST mention — this is what makes a named record a live object the reader can open rather than a string. Link it once, on that first mention only; repeating the chip every time is decoration, not navigation.\nOutput PLAIN PROSE, under 250 words, with [[entity:…]] chips inline. No\nheadings, no ::: directive blocks, no code fences.",
198
198
  "prompt": "STEER:\n- Prompt: {{trigger.payload.prompt}}\n- Focus: {{trigger.payload.focus}}\n- Project lens id: {{trigger.payload.projectId}}\n\nCOUNTS — the exact number of records gathered for each kind. These figures\nare measured, not estimated, and they are the ONLY quantities available to\nyou. Each line also gives the cap the gather was limited to.\n- Tasks: {{steps.gather-tasks.output.count}} gathered (cap 25)\n- Notes: {{steps.gather-notes.output.count}} gathered (cap 15)\n- People: {{steps.gather-people.output.count}} gathered (cap 15)\n- Companies: {{steps.gather-companies.output.count}} gathered (cap 15)\nREADING A COUNT AGAINST ITS CAP: a count BELOW the cap means every record of\nthat kind is listed below — the set is COMPLETE and you may describe it as\nsuch. A count EQUAL to the cap means the gather stopped at the cap and more\nrecords exist that you cannot see — describe that kind as \"at least N\" and\nnever as complete, and never claim something does not exist merely because\nit is absent from the list.\n\nTime window: only records updated on or after {{trigger.payload.since}}\n\nGATHERED DATA — one line per record, MOST RECENTLY UPDATED first. Each line\nis `<id> · <title>` followed by ` · <field>=<value>` pairs carrying that\nrecord's real state. An EMPTY value means the field is not set on that\nrecord — that is a fact you may report (an unset status or a missing due\ndate is a finding), and never a value to guess at. Recent does not mean\nimportant.\nTasks — {{steps.gather-tasks.output.count}} found: {{steps.project-tasks.output.result}}\nNotes — {{steps.gather-notes.output.count}} found: {{steps.project-notes.output.result}}\nPeople — {{steps.gather-people.output.count}} found: {{steps.project-people.output.result}}\nCompanies — {{steps.gather-companies.output.count}} found: {{steps.project-companies.output.result}}\n\nANALYST'S READ — already written and already in the report. Do NOT\nrestate, summarise or re-analyse it. It is here so you do not repeat it:\n{{steps.analyze.output}}\n\nYOUR TASK — cross-kind patterns ONLY. Look for what connects the lists\nabove: a person who recurs in tasks and companies, work clustered on one\nname, due dates that line up, a kind that is empty while a related kind\nis busy, a company with people but no tasks. Write only observations\nthat span TWO OR MORE records or kinds. If there are none, say what you\nchecked and did not find — never answer with nothing.",
199
- "maxTokens": "700"
199
+ "maxTokens": "2000"
200
200
  },
201
201
  "errorHandling": {
202
202
  "continueOnError": true
package/src/base.yaml CHANGED
@@ -455,7 +455,7 @@ automations:
455
455
  Notes — {{steps.gather-notes.output.count}} found: {{steps.project-notes.output.result}}
456
456
  People — {{steps.gather-people.output.count}} found: {{steps.project-people.output.result}}
457
457
  Companies — {{steps.gather-companies.output.count}} found: {{steps.project-companies.output.result}}
458
- maxTokens: "700"
458
+ maxTokens: "2000"
459
459
  errorHandling:
460
460
  continueOnError: true
461
461
  - id: relate
@@ -537,7 +537,7 @@ automations:
537
537
  is busy, a company with people but no tasks. Write only observations
538
538
  that span TWO OR MORE records or kinds. If there are none, say what you
539
539
  checked and did not find — never answer with nothing.
540
- maxTokens: "700"
540
+ maxTokens: "2000"
541
541
  errorHandling:
542
542
  continueOnError: true
543
543
  - id: assemble
@@ -47,6 +47,10 @@ profiles:
47
47
  description: A liquidity provider — makes markets and provides depth for a token.
48
48
  scope: shared
49
49
  profileKind: role
50
+ # Shared supply-role category → the intro-matcher's roster spans ALL roles
51
+ # tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
52
+ # hardcoding). Keep this string in sync with networking.yaml's solution-provider.
53
+ roleCategory: provider
50
54
  applicableKinds:
51
55
  - company
52
56
  - person
@@ -81,6 +85,10 @@ profiles:
81
85
  description: A security firm or individual that audits smart contracts and protocols.
82
86
  scope: shared
83
87
  profileKind: role
88
+ # Shared supply-role category → the intro-matcher's roster spans ALL roles
89
+ # tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
90
+ # hardcoding). Keep this string in sync with networking.yaml's solution-provider.
91
+ roleCategory: provider
84
92
  applicableKinds:
85
93
  - company
86
94
  - person
@@ -115,6 +123,10 @@ profiles:
115
123
  description: An outsourced engineering studio or build partner.
116
124
  scope: shared
117
125
  profileKind: role
126
+ # Shared supply-role category → the intro-matcher's roster spans ALL roles
127
+ # tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
128
+ # hardcoding). Keep this string in sync with networking.yaml's solution-provider.
129
+ roleCategory: provider
118
130
  applicableKinds:
119
131
  - company
120
132
  - person
@@ -149,6 +161,10 @@ profiles:
149
161
  description: A brand, growth, or content marketing agency.
150
162
  scope: shared
151
163
  profileKind: role
164
+ # Shared supply-role category → the intro-matcher's roster spans ALL roles
165
+ # tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
166
+ # hardcoding). Keep this string in sync with networking.yaml's solution-provider.
167
+ roleCategory: provider
152
168
  applicableKinds:
153
169
  - company
154
170
  - person
@@ -183,6 +199,10 @@ profiles:
183
199
  description: A performance / paid-media buying agency.
184
200
  scope: shared
185
201
  profileKind: role
202
+ # Shared supply-role category → the intro-matcher's roster spans ALL roles
203
+ # tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
204
+ # hardcoding). Keep this string in sync with networking.yaml's solution-provider.
205
+ roleCategory: provider
186
206
  applicableKinds:
187
207
  - company
188
208
  - person
@@ -217,6 +237,10 @@ profiles:
217
237
  description: A key opinion leader or influencer who amplifies reach.
218
238
  scope: shared
219
239
  profileKind: role
240
+ # Shared supply-role category → the intro-matcher's roster spans ALL roles
241
+ # tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
242
+ # hardcoding). Keep this string in sync with networking.yaml's solution-provider.
243
+ roleCategory: provider
220
244
  applicableKinds:
221
245
  - company
222
246
  - person
@@ -243,6 +267,88 @@ profiles:
243
267
  valueType: string
244
268
  inputType: textarea
245
269
 
270
+ # ── Grant Program (the per-chain funding PROGRAM — parent of a round) ───────
271
+ # The 3-layer grants spine is PROGRAM ≠ ROUND ≠ APPLICATION. `round` (in
272
+ # grants.yaml) is the funding-cycle/deadline carrier; a program is the
273
+ # long-lived, chain-scoped funder above it (e.g. Stellar SCF, Optimism RetroPGF).
274
+ # It lives HERE, with the web3 vocabulary, because a program IS a blockchain-
275
+ # ecosystem thing (it carries a `blockchain` and program mechanics), and it is
276
+ # a real-world entity referenced from every workspace — hence pod-wide, mirroring
277
+ # company/person in ecosystem.yaml (`entityScope: pod`). `scope: shared` makes
278
+ # the profile DEF pod-wide too, so a REUSING template (grants.yaml `require`s this
279
+ # overlay) resolves `grant-program` for its `round → belongs_to → grant-program`
280
+ # edge and deadline view. `defaultPlaybook` is a text ref for now — Wave 2 will
281
+ # create the actual per-chain playbook and bind it.
282
+ - slug: grant-program
283
+ displayName: Grant Program
284
+ icon: landmark
285
+ color: emerald
286
+ description: A per-chain funding program (e.g. Stellar SCF, Optimism RetroPGF) — the long-lived funder above a round.
287
+ profileKind: kind
288
+ entityScope: pod
289
+ scope: shared
290
+ properties:
291
+ - slug: blockchain
292
+ label: Blockchain / Chain
293
+ valueType: string
294
+ inputType: text
295
+ - slug: programName
296
+ label: Program Name
297
+ valueType: string
298
+ inputType: text
299
+ - slug: programType
300
+ label: Program Type
301
+ valueType: string
302
+ inputType: select
303
+ enumValues:
304
+ - direct
305
+ - retroactive
306
+ - quadratic
307
+ - rfp
308
+ - slug: status
309
+ label: Status
310
+ valueType: string
311
+ inputType: select
312
+ enumValues:
313
+ - active
314
+ - paused
315
+ - retired
316
+ - slug: url
317
+ label: Program URL
318
+ valueType: string
319
+ inputType: url
320
+ - slug: cadence
321
+ label: Cadence
322
+ valueType: string
323
+ inputType: text
324
+ placeholder: every 6 weeks
325
+ - slug: nextDeadline
326
+ label: Next Deadline
327
+ valueType: date
328
+ inputType: date
329
+ - slug: submissionTracks
330
+ label: Submission Tracks
331
+ valueType: array
332
+ inputType: text
333
+ - slug: trancheModel
334
+ label: Tranche Model
335
+ valueType: string
336
+ inputType: text
337
+ placeholder: 4 tranches / 90 days
338
+ - slug: defaultPlaybook
339
+ label: Default Playbook
340
+ valueType: string
341
+ inputType: text
342
+ # Defaults to the BASE grant playbook (grants.yaml `Grant Process`), the
343
+ # data-driven process that reads THIS program's cadence/tracks/tranche
344
+ # model at runtime. Override per-program only for a differently-shaped chain.
345
+ constraints:
346
+ defaultValue: Grant Process
347
+ - slug: description
348
+ label: Description
349
+ valueType: string
350
+ inputType: textarea
351
+
246
352
  # Web3 actor↔actor relations (company→company). `company` resolves from the
247
353
  # Ecosystem base — not redeclared here.
248
354
  entityLinks:
package/src/catalog.ts CHANGED
@@ -33,6 +33,20 @@ import type { WorkspaceYaml } from "./types.js";
33
33
  // Inputs
34
34
  // ============================================================================
35
35
 
36
+ /**
37
+ * A lightweight, QUERY-TIME summary of one sub-object a package carries (a cell
38
+ * today; the shape generalizes to views/automations without change). Rides the
39
+ * catalog LIST projection so a surface can match/surface a sub-item WITHOUT the
40
+ * full `definition` — it carries `key + name` only, never the sub-object's code.
41
+ * `kind` is DERIVED (in the CP SELECT, from which sub-array the row came from),
42
+ * never a stored taxonomy.
43
+ */
44
+ export interface PackageItemSummary {
45
+ kind: string;
46
+ key: string;
47
+ name: string;
48
+ }
49
+
36
50
  /**
37
51
  * A bundled template — the canonical `WorkspaceYaml` this package ships. Callers
38
52
  * pass `listPublicTemplates()` (18) or `listWorkspaceTemplates()` (21); the door
@@ -69,6 +83,13 @@ export interface RemoteEntry {
69
83
  * never "unknown-so-hide".
70
84
  */
71
85
  isPrivate?: boolean;
86
+ /**
87
+ * Lightweight computed summary of the row's sub-objects (cells), carried on the
88
+ * LIST projection so a surface can surface a matching sub-item without the full
89
+ * `definition`. `undefined` on sources that don't compute it (bundled) — treated
90
+ * as "no known sub-items". See `PackageItemSummary`.
91
+ */
92
+ items?: PackageItemSummary[];
72
93
  /**
73
94
  * The CP `definition` jsonb — opaque install payload. The door reads only
74
95
  * `definition.sourcePackage.version` (the winner signal). Absent on browse rows
@@ -128,6 +149,11 @@ export interface CpPackageRow {
128
149
  icon?: string | null;
129
150
  /** Present only on `/mine` rows. `false` ⇒ private. Browse rows omit it. */
130
151
  isPublic?: boolean;
152
+ /**
153
+ * Computed sub-object summary from `GET /api/packages` / `/mine` (query-time,
154
+ * not a stored column). Carries `key + name` per cell; never the cell code.
155
+ */
156
+ items?: PackageItemSummary[];
131
157
  }
132
158
 
133
159
  /**
@@ -154,6 +180,7 @@ export function rowToRemoteEntry(
154
180
  tags: row.tags ?? [],
155
181
  icon: row.icon ?? null,
156
182
  isPrivate,
183
+ items: row.items,
157
184
  // Both list routes strip `definition`; absence ⇒ bundle wins a slug conflict,
158
185
  // and the install path fetches the full definition per-slug when needed.
159
186
  };
@@ -175,6 +202,12 @@ interface CatalogEntryBase {
175
202
  icon?: string | null;
176
203
  /** `true` ⇒ not publicly listed (internal bundled template, or private CP row). */
177
204
  isPrivate: boolean;
205
+ /**
206
+ * The entry's sub-object summary (cells). Always present (defaults to `[]`) so a
207
+ * consumer can match sub-items uniformly. Bundled workspace templates carry no
208
+ * cells → `[]` (honest); a remote row carries its CP-computed `items`.
209
+ */
210
+ items: PackageItemSummary[];
178
211
  }
179
212
 
180
213
  /** A catalog entry backed by a bundled template. Install via `toWorkspaceDefinition()` / `toPackageDefinition()` on `template`. */
@@ -256,6 +289,8 @@ function bundledToEntry(tpl: WorkspaceYaml): BundledCatalogEntry {
256
289
  tags: tpl.meta.tags ?? [],
257
290
  icon: tpl.meta.icon,
258
291
  isPrivate: tpl.meta.isPublic === false,
292
+ // Bundled workspace templates carry no cells — be honest, not `undefined`.
293
+ items: [],
259
294
  template: tpl,
260
295
  };
261
296
  }
@@ -271,6 +306,7 @@ function remoteToEntry(row: RemoteEntry): RemoteCatalogEntry {
271
306
  tags: row.tags ?? [],
272
307
  icon: row.icon ?? null,
273
308
  isPrivate: row.isPrivate ?? false,
309
+ items: row.items ?? [],
274
310
  remote: row,
275
311
  };
276
312
  }
package/src/grants.yaml CHANGED
@@ -43,6 +43,16 @@ dependencies:
43
43
  kind: workspace
44
44
  relation: require
45
45
  reason: "Provides the pod-wide client / partner / sponsor role vocabulary this workspace reuses."
46
+ # This template REFERENCES the pod-wide `grant-program` kind (the per-chain
47
+ # funding-program parent) that the blockchain-ecosystem overlay defines. The
48
+ # 3-layer grants spine is program → round → grant_submission: `round` (owned
49
+ # here) `belongs_to` a `grant-program` (owned there). `require` installs that
50
+ # overlay FIRST so `grant-program` exists (pod-wide) before this template
51
+ # provisions the edge + deadline view — same deps-first pattern as foundation.
52
+ - slug: blockchain-ecosystem
53
+ kind: workspace
54
+ relation: require
55
+ reason: "Provides the pod-wide grant-program kind (per-chain funding programs) this workspace's rounds belong to."
46
56
 
47
57
  workspace:
48
58
  name: Grants Pipeline
@@ -873,11 +883,26 @@ views:
873
883
  - averageAwardAmount
874
884
  sortBy: roundNumber
875
885
  sortDir: desc
886
+ # Round deadlines on one time axis. Scoped to `round` ONLY: rounds are the
887
+ # deadline SSOT (they carry `submissionDeadline`); `grant-program` carries
888
+ # `nextDeadline`, which the calendar's dateField fallback chain
889
+ # (dueDate/startDate/startTime/date) never resolves, so a program would land
890
+ # dateless in the "Unscheduled" backlog. Plotting programs + rounds on ONE
891
+ # timeline is a deferred UI-phase item. `colorField` (NOT `colorBy` — the
892
+ # calendar adapter reads `colorField`) colors each round by its status.
893
+ - name: Deadlines
894
+ type: calendar
895
+ scopeProfileSlug: round
896
+ description: Upcoming round submission deadlines, on one time axis.
897
+ config:
898
+ dateField: properties.submissionDeadline
899
+ colorField: status
900
+ # No `coverField`: grant-project has no image property (`summary` is prose, so it
901
+ # rendered as a text/blank cover). Fall back to the gallery's clean default cover.
876
902
  - name: Awarded Projects
877
903
  type: gallery
878
904
  scopeProfileSlug: grant-project
879
905
  config:
880
- coverField: summary
881
906
  showFields:
882
907
  - currentStatus
883
908
  - awardedRoundCount
@@ -907,6 +932,35 @@ entityLinks:
907
932
  targetProfileSlug: company
908
933
  type: submitted_by
909
934
  label: Submitted By
935
+ # The application belongs to the engagement that pursued it (contract lives in
936
+ # the composed operations base). Closes the spine on the submission side.
937
+ - sourceProfileSlug: grant_submission
938
+ targetProfileSlug: contract
939
+ type: for_contract
940
+ label: Engagement
941
+ # The commercial bridge: a grant application maps to its fee/commission `deal`,
942
+ # so a grant win is tied to the money. Mirrors networking.yaml's canonical
943
+ # `intro → billed_as → deal` commercial-mapping predicate (same edge type,
944
+ # reused for one consistent meaning across the pod graph). `deal` resolves here
945
+ # because it is a SYSTEM profile (validate.ts `SYSTEM_PROFILES`) — so this needs
946
+ # NO crm compose (grants stays an operations overlay, not a CRM). The deal lives
947
+ # in the CRM workspace; this is a cross-workspace relation, consistent with
948
+ # grant_submission's other cross-domain edges (submitted_by → company).
949
+ - sourceProfileSlug: grant_submission
950
+ targetProfileSlug: deal
951
+ type: billed_as
952
+ label: Billed as
953
+ # 3-layer spine: a round is one cycle OF a long-lived program (SCF #42 → SCF).
954
+ - sourceProfileSlug: round
955
+ targetProfileSlug: grant-program
956
+ type: belongs_to
957
+ label: Program
958
+ # The engagement pursues a program (contract lives in the composed operations
959
+ # base; grant-program in the required blockchain-ecosystem overlay).
960
+ - sourceProfileSlug: contract
961
+ targetProfileSlug: grant-program
962
+ type: pursues
963
+ label: Pursues
910
964
  - sourceProfileSlug: grant-project
911
965
  targetProfileSlug: round
912
966
  type: appears_in
@@ -923,6 +977,199 @@ entityLinks:
923
977
  targetProfileSlug: grant_submission
924
978
  type: evidenced_by
925
979
  label: Evidenced By
980
+
981
+ # The BASE grant playbook — ONE reusable, data-driven process that runs an
982
+ # engagement through ANY program. Chain-specifics (cadence, submissionTracks,
983
+ # trancheModel, nextDeadline) are READ from the `grantProgram` entity at runtime,
984
+ # NOT hardcoded per chain — so one playbook serves Stellar SCF, Optimism RetroPGF,
985
+ # etc. Only a genuinely different-SHAPED program later earns its own override
986
+ # playbook (base + data-driven-override decision). Generalizes the Stellar staged
987
+ # advisor (CP `stellar-grant-client`) into a template. Bound to `contract` (the
988
+ # engagement, per decision — NOT company); targets a `grant-program` param — the
989
+ # 3-layer spine (contract → grant-program → round → grant_submission) made runnable.
990
+ playbooks:
991
+ - name: Grant Process
992
+ description: Run an engagement through a grant program end to end — identify fit, draft the application, submit, track the decision, and deliver the tranches. Reads the program's own cadence / tracks / tranche model so one playbook fits every chain.
993
+ goalTemplate: >-
994
+ Advance this engagement ({contract}) through the grant program
995
+ ({grantProgram}) it is pursuing. FIRST read the program entity's own data —
996
+ its cadence, submissionTracks, trancheModel, and nextDeadline — plus the
997
+ client context on the contract, and let THOSE drive the plan: do not assume
998
+ a chain's mechanics, read them from the program. Then work the stages in
999
+ order and keep the client's `grant_submission` card as the durable state of
1000
+ record:
1001
+
1002
+ 1. Identify — understand the project and which of the program's
1003
+ submissionTracks fit; set the submission `stage` to `scope`.
1004
+ 2. Draft — write the abstract, then the full proposal (products & services,
1005
+ success criteria, GTM, traction); move `stage` `abstract` → `build` and
1006
+ fill the submission's body fields.
1007
+ 3. Submit — finalize against the program's nextDeadline and submit; once
1008
+ submitted, set `stage` to `awaiting-results`.
1009
+ 4. Awaiting — track the decision and keep `status` current
1010
+ (in-review / waiting-client / needs-document / blocked).
1011
+ 5. Awarded — on a decision, set `outcome` (awarded / rejected / partial)
1012
+ and record the awarded amount.
1013
+ 6. Delivering — for an award, work the program's trancheModel: fill the
1014
+ tranche deliverables and update trancheCompletion as each milestone ships.
1015
+
1016
+ Never fabricate — leave a field blank rather than guess. Every write is
1017
+ proposal-gated for the user to review.
1018
+ subjectProfile:
1019
+ profileSlug: contract
1020
+ params:
1021
+ - { name: contract, type: entity, profileSlug: contract, label: Engagement, required: true }
1022
+ - { name: grantProgram, type: entity, profileSlug: grant-program, label: Grant Program, required: true }
1023
+ # Generic stages, mapped onto grant_submission's own stage/status/outcome +
1024
+ # tranche vocab so the run can maintain the card as it advances.
1025
+ stages:
1026
+ - { key: identify, name: Identify, description: "Understand the project and which of the program's submission tracks fit — before any writing.", goal: "A clear project understanding and the fitting submission track; submission stage = scope." }
1027
+ - { key: draft, name: Draft, description: "Write the abstract, then the full proposal, from the client's real material.", goal: "Abstract then full proposal drafted; submission stage abstract → build with body fields filled." }
1028
+ - { key: submit, name: Submit, description: "Finalize against the program deadline and submit the application.", goal: "Application submitted on time; submission stage = awaiting-results." }
1029
+ - { key: awaiting, name: Awaiting, description: "Track the decision and keep the client status current.", goal: "Submission status reflects where the decision stands (in-review / waiting-client / needs-document / blocked)." }
1030
+ - { key: awarded, name: Awarded, description: "Record the funder's decision and the awarded amount.", goal: "Submission outcome set (awarded / rejected / partial) with the awarded amount." }
1031
+ - { key: delivering, name: Delivering, description: "Work the program's tranche model — ship and record each tranche.", goal: "Tranche deliverables filled and trancheCompletion updated as milestones ship." }
1032
+ executor: is-agent
1033
+ grants:
1034
+ - entity.create
1035
+ status: active
1036
+
1037
+ # Makes the spine ALIVE: a daily maintenance run that keeps each grant
1038
+ # application's `grant_submission` card current (advances stage + status) — the
1039
+ # durable, kanban-visible state of record. Generalizes the Stellar CP "Client
1040
+ # Sync" automation (read comms → assess → advance submission) into a chain-
1041
+ # AGNOSTIC workspace-template automation: it advances using grant_submission's
1042
+ # OWN stage/status vocab, so it works for any program (no Stellar-specific
1043
+ # assumptions). Same fan-out-safe shape the corpus uses (cron → in-flow `query`
1044
+ # → `loop`), NOT the `inputStrategy:'query'` playbook fan-out (which has the
1045
+ # known gap).
1046
+ automations:
1047
+ - key: grant-submission-sync
1048
+ name: Grant Submission Sync
1049
+ description: >-
1050
+ Once a day, keep each grant application's grant_submission card current —
1051
+ read the submission's recent activity, assess where the pursuit stands, and
1052
+ advance its stage + status so the pipeline stays the durable, kanban-visible
1053
+ state of record.
1054
+ triggerType: cron
1055
+ triggerConfig:
1056
+ expression: "0 8 * * *"
1057
+ # DRAFT by default (deliberate safety): this runs a paid AI assess per
1058
+ # grant_submission every day, so installing the suite must NOT silently start
1059
+ # paid runs. Flip to `active` to let the pipeline self-maintain — same
1060
+ # enabled-off-by-default convention as the radar / CRM-hygiene playbooks.
1061
+ status: draft
1062
+ metadata:
1063
+ templateKey: grant-submission-sync
1064
+ flow:
1065
+ nodes:
1066
+ - id: trigger
1067
+ type: trigger
1068
+ position: { x: 0, y: 0 }
1069
+ data:
1070
+ label: Daily at 08:00
1071
+ triggerType: cron
1072
+ config:
1073
+ expression: "0 8 * * *"
1074
+
1075
+ # READ-ONLY builtin + SELECTION. The grant_submission cards ARE the grant
1076
+ # pursuits (each is created by the `Grant Process` playbook at pursuit
1077
+ # start), so querying them directly is the safe selection: it touches ONLY
1078
+ # real pursuits and never fabricates a card for a non-grant contract.
1079
+ #
1080
+ # ── CREATE-IF-MISSING (deferred, flagged) ────────────────────────────
1081
+ # The Stellar reference does create-if-missing by querying its subject
1082
+ # (companies) and upserting the submission. Generalizing that safely means
1083
+ # selecting "contracts that PURSUE a grant program" — but `entity.query`
1084
+ # filters JSONB PROPERTIES only, it cannot filter by a `pursues` EDGE, and
1085
+ # there is no query-fan-out capability to enumerate edge-linked subjects.
1086
+ # Querying ALL contracts and creating a card per contract would spuriously
1087
+ # create submissions for every non-grant engagement. So creation stays the
1088
+ # `Grant Process` playbook's job (the agent creates the card when a pursuit
1089
+ # begins) and this automation MAINTAINS. Fully-generalized create-if-
1090
+ # missing needs an edge-aware selection capability that does not exist yet.
1091
+ - id: list-submissions
1092
+ type: capability
1093
+ position: { x: 0, y: 120 }
1094
+ data:
1095
+ label: List grant submissions to maintain
1096
+ verbId: entity.query
1097
+ inputMapping:
1098
+ profileSlug: grant_submission
1099
+ limit: "100"
1100
+
1101
+ - id: submissions-loop
1102
+ type: loop
1103
+ position: { x: 0, y: 240 }
1104
+ data:
1105
+ label: For each grant submission
1106
+ iteratorExpression: steps.list-submissions.output.entities
1107
+ itemVariable: submission
1108
+
1109
+ # READ-ONLY builtin. Recent activity on THIS submission's thread — the raw
1110
+ # signal the assess pass reads to place the pursuit's stage/status.
1111
+ - id: read-activity
1112
+ type: messages_query
1113
+ position: { x: 0, y: 360 }
1114
+ data:
1115
+ label: Read the submission's recent activity
1116
+ subjectEntityId: "{{loop.item.id}}"
1117
+ limit: 40
1118
+
1119
+ # READ-ONLY (pure compute). ONE assess pass → STRICT JSON naming the
1120
+ # submission's current stage + status, constrained to grant_submission's
1121
+ # OWN enum vocab so it maps straight back onto the card (chain-agnostic).
1122
+ - id: assess
1123
+ type: capability
1124
+ position: { x: 0, y: 480 }
1125
+ data:
1126
+ label: Assess the submission's stage + status → JSON
1127
+ verbId: ai.generate
1128
+ inputMapping:
1129
+ json: "true"
1130
+ maxTokens: "500"
1131
+ system: >-
1132
+ You are a strict data-extraction function for a grants pipeline.
1133
+ Read a grant application's recent activity and report where the
1134
+ application currently stands. Respond with STRICT JSON and NOTHING
1135
+ else. Never fabricate — if the activity does not clearly indicate a
1136
+ value, keep the submission's current one.
1137
+ prompt: >-
1138
+ The grant submission: {{loop.item.title}}
1139
+ Current stage: {{loop.item.properties.stage}}
1140
+ Current status: {{loop.item.properties.status}}
1141
+
1142
+ Recent activity (oldest first):
1143
+ {{steps.read-activity.output.messages}}
1144
+
1145
+ Report the submission's current state as STRICT JSON, using ONLY
1146
+ these vocabularies and NOTHING else:
1147
+ {
1148
+ "stage": "scope|abstract|build|awaiting-results",
1149
+ "status": "answered|waiting-client|needs-document|in-review|blocked"
1150
+ }
1151
+
1152
+ # GOVERNED WRITE via `output/entity_update` — advances the card's stage +
1153
+ # status (the durable, kanban-visible state). Governed like every output
1154
+ # write: routes to a proposal for review unless auto-approved.
1155
+ - id: advance-submission
1156
+ type: output
1157
+ position: { x: 0, y: 600 }
1158
+ data:
1159
+ label: Advance the submission stage + status
1160
+ outputType: entity_update
1161
+ config:
1162
+ entityId: "{{loop.item.id}}"
1163
+ properties:
1164
+ stage: "{{steps.assess.output.stage}}"
1165
+ status: "{{steps.assess.output.status}}"
1166
+ edges:
1167
+ - { id: gs-trigger-list, source: trigger, target: list-submissions }
1168
+ - { id: gs-list-loop, source: list-submissions, target: submissions-loop }
1169
+ - { id: gs-loop-read, source: submissions-loop, target: read-activity }
1170
+ - { id: gs-read-assess, source: read-activity, target: assess }
1171
+ - { id: gs-assess-advance, source: assess, target: advance-submission }
1172
+
926
1173
  app:
927
1174
  kind: stored
928
1175
  cellKey: entity-detail
@@ -212,6 +212,11 @@ profiles:
212
212
  description: A company or person that provides a capability The Arch can broker a client need against.
213
213
  scope: shared
214
214
  profileKind: role
215
+ # Shared supply-role category — the intro-matcher's roster is now
216
+ # entity.query { roleCategory: "provider" }, spanning EVERY role tagged
217
+ # "provider" (this one in a public install; + blockchain-ecosystem's web3
218
+ # supply roles when The Arch installs both). One string, kept in sync there.
219
+ roleCategory: provider
215
220
  applicableKinds:
216
221
  - company
217
222
  - person
@@ -365,15 +370,21 @@ automations:
365
370
  limit: "100"
366
371
 
367
372
  # READ-ONLY builtin. The provider roster to match against — companies /
368
- # people wearing the pod-wide `solution-provider` role.
373
+ # people wearing ANY role tagged `roleCategory: "provider"`. Dynamic by
374
+ # design: a public install matches its `solution-provider`s; The Arch
375
+ # (which also installs blockchain-ecosystem) additionally matches the web3
376
+ # supply roles; any future supply role tagged "provider" auto-joins with
377
+ # ZERO edits here. No `profileSlug` → no per-role hardcoding, no dependency
378
+ # on blockchain-ecosystem (a public installer simply has no web3 roles to
379
+ # match, so nothing leaks).
369
380
  - id: list-providers
370
381
  type: capability
371
382
  position: { x: 0, y: 240 }
372
383
  data:
373
- label: List the solution-provider roster
384
+ label: List the provider roster (any roleCategory=provider)
374
385
  verbId: entity.query
375
386
  inputMapping:
376
- profileSlug: solution-provider
387
+ roleCategory: provider
377
388
  limit: "100"
378
389
 
379
390
  - id: needs-loop