@synap-core/workspace-templates 0.9.4 → 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/dist/index.d.ts +154 -45
- package/dist/index.js +2462 -543
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/default-relation-defs.json +208 -0
- package/src/__fixtures__/report-automation-flow.json +393 -0
- package/src/agent-fleet.yaml +6 -1
- package/src/base.template.test.ts +160 -0
- package/src/base.yaml +839 -0
- package/src/blockchain-ecosystem.yaml +106 -0
- package/src/business-developer.template.test.ts +79 -10
- package/src/business-developer.yaml +61 -39
- package/src/catalog.ts +36 -0
- package/src/concept-guard.test.ts +47 -31
- package/src/concept-guard.ts +23 -19
- package/src/content-os.yaml +22 -0
- package/src/corpus-flow-validity.test.ts +130 -0
- package/src/crm.yaml +59 -8
- package/src/define.primary-surface.test.ts +2 -4
- package/src/define.ts +28 -0
- package/src/ecosystem.yaml +14 -0
- package/src/enterprise-os.yaml +1 -1
- package/src/foundation.yaml +8 -0
- package/src/grants.yaml +256 -1
- package/src/life-os.yaml +6 -1
- package/src/networking.yaml +14 -3
- package/src/operations.yaml +270 -0
- package/src/outreach-comms.yaml +196 -0
- package/src/project-management.yaml +6 -1
- package/src/templates.ts +1770 -129
- package/src/the-arch.yaml +4 -0
- package/src/types.ts +98 -34
- package/src/validate-flow.ts +186 -7
- package/src/validate.ts +26 -2
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
|
|
@@ -730,6 +740,14 @@ profiles:
|
|
|
730
740
|
- healthy
|
|
731
741
|
- at_risk
|
|
732
742
|
- critical
|
|
743
|
+
- slug: driveLink
|
|
744
|
+
label: Drive folder
|
|
745
|
+
valueType: string
|
|
746
|
+
inputType: url
|
|
747
|
+
- slug: driveFolderId
|
|
748
|
+
label: Drive folder ID
|
|
749
|
+
valueType: string
|
|
750
|
+
inputType: text
|
|
733
751
|
# ── Partner (shared ecosystem role — SSOT is foundation.yaml) ─────────────
|
|
734
752
|
- slug: partner
|
|
735
753
|
displayName: Partner
|
|
@@ -865,11 +883,26 @@ views:
|
|
|
865
883
|
- averageAwardAmount
|
|
866
884
|
sortBy: roundNumber
|
|
867
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.
|
|
868
902
|
- name: Awarded Projects
|
|
869
903
|
type: gallery
|
|
870
904
|
scopeProfileSlug: grant-project
|
|
871
905
|
config:
|
|
872
|
-
coverField: summary
|
|
873
906
|
showFields:
|
|
874
907
|
- currentStatus
|
|
875
908
|
- awardedRoundCount
|
|
@@ -899,6 +932,35 @@ entityLinks:
|
|
|
899
932
|
targetProfileSlug: company
|
|
900
933
|
type: submitted_by
|
|
901
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
|
|
902
964
|
- sourceProfileSlug: grant-project
|
|
903
965
|
targetProfileSlug: round
|
|
904
966
|
type: appears_in
|
|
@@ -915,6 +977,199 @@ entityLinks:
|
|
|
915
977
|
targetProfileSlug: grant_submission
|
|
916
978
|
type: evidenced_by
|
|
917
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
|
+
|
|
918
1173
|
app:
|
|
919
1174
|
kind: stored
|
|
920
1175
|
cellKey: entity-detail
|
package/src/life-os.yaml
CHANGED
|
@@ -207,7 +207,12 @@ profiles:
|
|
|
207
207
|
icon: User
|
|
208
208
|
color: "#06B6D4"
|
|
209
209
|
description: People in your intellectual and professional world.
|
|
210
|
-
|
|
210
|
+
# Pod-wide identity: `person` is ONE pod-wide SYSTEM kind, not a life-os-local
|
|
211
|
+
# copy. `entityScope: pod` lands the ROWS with `workspace_id = NULL` (visible
|
|
212
|
+
# from every workspace); `scope` omitted (no claim) so the applier reuses the
|
|
213
|
+
# system row and these life-os props land as this workspace's OVERLAY.
|
|
214
|
+
# (Was `scope: WORKSPACE`, which contradicted the pod-wide model.)
|
|
215
|
+
entityScope: pod
|
|
211
216
|
properties:
|
|
212
217
|
- slug: name
|
|
213
218
|
label: Name
|
package/src/networking.yaml
CHANGED
|
@@ -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
|
|
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
|
|
384
|
+
label: List the provider roster (any roleCategory=provider)
|
|
374
385
|
verbId: entity.query
|
|
375
386
|
inputMapping:
|
|
376
|
-
|
|
387
|
+
roleCategory: provider
|
|
377
388
|
limit: "100"
|
|
378
389
|
|
|
379
390
|
- id: needs-loop
|
package/src/operations.yaml
CHANGED
|
@@ -212,6 +212,22 @@ entityLinks:
|
|
|
212
212
|
targetProfileSlug: company
|
|
213
213
|
type: briefing_about_client
|
|
214
214
|
label: Client
|
|
215
|
+
# The CRM → Operations handoff edge. A `client` is a ROLE facet on a company
|
|
216
|
+
# OR a person (foundation's `applicableKinds`), so both endpoint pairs are
|
|
217
|
+
# declared — a solo-trader client is a person, and the pickup automation must
|
|
218
|
+
# be able to link its engagement either way.
|
|
219
|
+
- sourceProfileSlug: engagement
|
|
220
|
+
targetProfileSlug: company
|
|
221
|
+
type: engagement_for_client
|
|
222
|
+
label: Client
|
|
223
|
+
- sourceProfileSlug: engagement
|
|
224
|
+
targetProfileSlug: person
|
|
225
|
+
type: engagement_for_client
|
|
226
|
+
label: Client
|
|
227
|
+
- sourceProfileSlug: contract
|
|
228
|
+
targetProfileSlug: engagement
|
|
229
|
+
type: contract_for_engagement
|
|
230
|
+
label: Engagement
|
|
215
231
|
|
|
216
232
|
# CURATED views — the delivery motion, not one per profile
|
|
217
233
|
views:
|
|
@@ -571,3 +587,257 @@ automations:
|
|
|
571
587
|
- { id: e-write-link, source: write-briefing, target: link-briefing }
|
|
572
588
|
- { id: e-link-resolve, source: link-briefing, target: resolve-team }
|
|
573
589
|
- { id: e-resolve-post, source: resolve-team, target: post-recap }
|
|
590
|
+
|
|
591
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
592
|
+
# CRM → Operations HANDOFF PICKUP
|
|
593
|
+
#
|
|
594
|
+
# The other half of business-developer's "Win and hand off". That flow ends by
|
|
595
|
+
# attaching the pod-wide `client` role with `handoffStatus`; nothing on the
|
|
596
|
+
# delivery side ever picked it up, so a won deal became a client and then
|
|
597
|
+
# nothing happened. This automation is that pickup: once a day it finds
|
|
598
|
+
# clients marked ready/handed_off that have NO engagement yet, and opens the
|
|
599
|
+
# delivery shell — a `proposed` engagement plus a `draft` contract.
|
|
600
|
+
#
|
|
601
|
+
# WHY THE SHARED `client` SLUG MATTERS: this reads foundation's pod-wide
|
|
602
|
+
# `client` role. When the CRM wrote a workspace-scoped `crm-client` instead,
|
|
603
|
+
# this query could not have seen a single handed-off party — the two halves of
|
|
604
|
+
# the handoff were spelling the same concept differently. Collapsing the fork
|
|
605
|
+
# is what makes this automation possible at all.
|
|
606
|
+
#
|
|
607
|
+
# WHY EVERY NODE TYPE IS WHAT IT IS (loop-body grammar, automation-executor.ts
|
|
608
|
+
# `LOOP_BODY_NODE_TYPES`): the per-client body may only contain
|
|
609
|
+
# command / output / condition / skill / capability / playbook_run /
|
|
610
|
+
# messages_query / query / fetch / transform. `switch`, `entity_read` and
|
|
611
|
+
# `related_entities` are BOUNDARY types — putting one in the chain would end
|
|
612
|
+
# the loop body there and silently run the rest ONCE for the whole run. That
|
|
613
|
+
# is why the two gates are `condition` nodes (per-item continue-semantics) and
|
|
614
|
+
# why the "does an engagement already exist?" lookup is a `capability`
|
|
615
|
+
# entity.query rather than a `related_entities` node.
|
|
616
|
+
#
|
|
617
|
+
# IDEMPOTENCY is per-client and does not rely on the gate alone:
|
|
618
|
+
# `dedupeBy: engagement-client` makes a second run for the same client a
|
|
619
|
+
# no-op SKIP before the governance gate is even reached, and the contract
|
|
620
|
+
# dedupes on `contract-engagement`. Both keys are real property defs on the
|
|
621
|
+
# profiles above, not invented ones.
|
|
622
|
+
#
|
|
623
|
+
# ── GOVERNANCE, HONESTLY ──────────────────────────────────────────────────
|
|
624
|
+
# `metadata.governance.forceProposeWrites` is deliberately NOT set here, and
|
|
625
|
+
# that is not an oversight. That marker is read in exactly ONE place —
|
|
626
|
+
# `deriveSessionForceProposeGovernance` (synap-backend
|
|
627
|
+
# `packages/api/src/utils/permission-check.ts`), off a FOCUS SESSION stamped
|
|
628
|
+
# by `executePlaybookRun`. It is a PLAYBOOK mechanism. A flow automation's
|
|
629
|
+
# `output/entity_create` goes through `checkAutomationWriteOrPropose`
|
|
630
|
+
# (packages/jobs/src/utils/automation-governance.ts), which never looks at
|
|
631
|
+
# automation metadata, so the field would be inert decoration. Worse: the
|
|
632
|
+
# automation propose path is currently BROKEN end-to-end (proposeAutomationWrite
|
|
633
|
+
# persists `data` FLAT while the approve executor reads a nested envelope —
|
|
634
|
+
# documented at automation-executor.ts, "KNOWN PRE-EXISTING BUG"), so forcing
|
|
635
|
+
# proposals here would produce reviews that throw on approve.
|
|
636
|
+
#
|
|
637
|
+
# The reviewability is carried by STATE instead: the engagement is created
|
|
638
|
+
# `proposed` and the contract `draft`. Nothing is active until a human moves
|
|
639
|
+
# it. If genuine proposal-gating is wanted, the canonical door is a PLAYBOOK
|
|
640
|
+
# with `metadata.governance.forceProposeWrites: true` + a `schedule` (the CRM
|
|
641
|
+
# Hygiene precedent in crm.yaml) — a different, agent-authored design.
|
|
642
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
643
|
+
- key: operations-crm-handoff-pickup
|
|
644
|
+
name: Pick up handed-off clients
|
|
645
|
+
description: >-
|
|
646
|
+
Daily. Finds pod-wide clients the CRM marked ready or handed off that have
|
|
647
|
+
no engagement yet, and opens the delivery shell for them — a proposed
|
|
648
|
+
engagement plus a draft contract, both linked to the client. Deduped per
|
|
649
|
+
client, so it is safe to re-run.
|
|
650
|
+
triggerType: cron
|
|
651
|
+
triggerConfig:
|
|
652
|
+
expression: "0 7 * * *"
|
|
653
|
+
status: active
|
|
654
|
+
metadata:
|
|
655
|
+
templateKey: operations-crm-handoff-pickup
|
|
656
|
+
# Same pod-wide dependency as the briefing automation: `client` entities
|
|
657
|
+
# are pod-wide, and `scope: pod` is the explicit opt-in that must be
|
|
658
|
+
# DEPLOYED for this query to return anything.
|
|
659
|
+
requiresPodWideEntityQueryLens: true
|
|
660
|
+
flow:
|
|
661
|
+
nodes:
|
|
662
|
+
- id: trigger
|
|
663
|
+
type: trigger
|
|
664
|
+
position: { x: 0, y: 0 }
|
|
665
|
+
data:
|
|
666
|
+
label: Daily at 07:00
|
|
667
|
+
triggerType: cron
|
|
668
|
+
config:
|
|
669
|
+
expression: "0 7 * * *"
|
|
670
|
+
|
|
671
|
+
# READ-ONLY builtin. Enumerates parties carrying the pod-wide `client`
|
|
672
|
+
# ROLE. `scope: pod` is required — see the long note on the briefing
|
|
673
|
+
# automation's `list-clients` node.
|
|
674
|
+
- id: list-clients
|
|
675
|
+
type: capability
|
|
676
|
+
position: { x: 0, y: 120 }
|
|
677
|
+
data:
|
|
678
|
+
label: List clients (pod-wide `client` role)
|
|
679
|
+
verbId: entity.query
|
|
680
|
+
inputMapping:
|
|
681
|
+
profileSlug: client
|
|
682
|
+
scope: pod
|
|
683
|
+
limit: "100"
|
|
684
|
+
|
|
685
|
+
- id: handoff-loop
|
|
686
|
+
type: loop
|
|
687
|
+
position: { x: 0, y: 240 }
|
|
688
|
+
data:
|
|
689
|
+
label: For each client
|
|
690
|
+
iteratorExpression: steps.list-clients.output.entities
|
|
691
|
+
itemVariable: client
|
|
692
|
+
|
|
693
|
+
# READ-ONLY builtin. `handoffStatus` lives on the client FACET row, not
|
|
694
|
+
# on the entity — `entity.query`'s `filter` reads `entities.properties`
|
|
695
|
+
# and therefore cannot see it. This is the only door that returns facet
|
|
696
|
+
# overlay properties.
|
|
697
|
+
- id: read-client-facets
|
|
698
|
+
type: capability
|
|
699
|
+
position: { x: 0, y: 360 }
|
|
700
|
+
data:
|
|
701
|
+
label: Read this party's role facets
|
|
702
|
+
verbId: entity_facet.list
|
|
703
|
+
inputMapping:
|
|
704
|
+
entityId: "{{loop.item.id}}"
|
|
705
|
+
|
|
706
|
+
# Pull the CLIENT facet's handoffStatus out BY SLUG. Positional
|
|
707
|
+
# indexing (`facets.0.properties…`) would be a latent bug: facet order
|
|
708
|
+
# is not guaranteed, so on a party that is also a partner it would read
|
|
709
|
+
# the wrong row. `filter` + `map` resolves it by identity.
|
|
710
|
+
- id: client-handoff-status
|
|
711
|
+
type: transform
|
|
712
|
+
position: { x: 0, y: 480 }
|
|
713
|
+
data:
|
|
714
|
+
label: Read the client facet's handoff status
|
|
715
|
+
expression: "{{steps.read-client-facets.output.facets}} | filter:item.profileSlug === client | map:{{item.properties.handoffStatus}}"
|
|
716
|
+
|
|
717
|
+
# PER-ITEM FILTER (continue-semantics). `pending` means the CRM has not
|
|
718
|
+
# finished with this client yet — only ready/handed_off are ours.
|
|
719
|
+
- id: handoff-gate
|
|
720
|
+
type: condition
|
|
721
|
+
position: { x: 0, y: 600 }
|
|
722
|
+
data:
|
|
723
|
+
label: Only clients the CRM marked ready or handed off
|
|
724
|
+
expression: "steps.client-handoff-status.output.result contains-any ready,handed_off"
|
|
725
|
+
|
|
726
|
+
# Pure read, workspace-lensed (engagements are Operations-local, so no
|
|
727
|
+
# `scope: pod`). `engagement-client` is a real property def above, so
|
|
728
|
+
# this equality filter hits `entities.properties`.
|
|
729
|
+
#
|
|
730
|
+
# `filter` is authored as a JSON *STRING*, not a mapping, and that is
|
|
731
|
+
# load-bearing: `parseQueryFilterConditions` (automation-executor.ts)
|
|
732
|
+
# template-resolves the filter ONLY when it is a string. Written as a
|
|
733
|
+
# YAML object, `{{loop.item.id}}` would reach SQL as that literal text,
|
|
734
|
+
# match nothing, and this gate would then open for EVERY client.
|
|
735
|
+
- id: existing-engagements
|
|
736
|
+
type: query
|
|
737
|
+
position: { x: 0, y: 720 }
|
|
738
|
+
data:
|
|
739
|
+
label: Does this client already have an engagement?
|
|
740
|
+
profileSlug: engagement
|
|
741
|
+
filter: '{"engagement-client": "{{loop.item.id}}"}'
|
|
742
|
+
limit: 1
|
|
743
|
+
|
|
744
|
+
- id: engagement-gate
|
|
745
|
+
type: condition
|
|
746
|
+
position: { x: 0, y: 840 }
|
|
747
|
+
data:
|
|
748
|
+
label: Only clients with no engagement yet
|
|
749
|
+
expression: "steps.existing-engagements.output.count === 0"
|
|
750
|
+
|
|
751
|
+
# Cron trigger carries no payload timestamp, so `now` falls back to the
|
|
752
|
+
# actual run time — stamping the handoff moment onto both records
|
|
753
|
+
# below so they plot on the calendar the day they're handed off.
|
|
754
|
+
# `engagement-end`/`start`-adjacent dates the owner still sets by hand
|
|
755
|
+
# stay unset.
|
|
756
|
+
- id: handoff-time
|
|
757
|
+
type: compute
|
|
758
|
+
position: { x: 0, y: 900 }
|
|
759
|
+
data:
|
|
760
|
+
label: Record handoff time
|
|
761
|
+
operation: now
|
|
762
|
+
|
|
763
|
+
# GOVERNED WRITE via an `output` node. Created `proposed`, never active:
|
|
764
|
+
# the delivery shell exists so the work is visible, but a human decides
|
|
765
|
+
# when it starts. `dedupeBy: engagement-client` is the durable
|
|
766
|
+
# per-client idempotency key — a re-run SKIPS before the governance gate.
|
|
767
|
+
- id: create-engagement
|
|
768
|
+
type: output
|
|
769
|
+
position: { x: 0, y: 960 }
|
|
770
|
+
data:
|
|
771
|
+
label: Open a proposed engagement
|
|
772
|
+
outputType: entity_create
|
|
773
|
+
config:
|
|
774
|
+
profileSlug: engagement
|
|
775
|
+
title: "Engagement — {{loop.item.title}}"
|
|
776
|
+
dedupeBy: engagement-client
|
|
777
|
+
properties:
|
|
778
|
+
engagement-status: proposed
|
|
779
|
+
engagement-client: "{{loop.item.id}}"
|
|
780
|
+
engagement-start: "{{steps.handoff-time.output.result}}"
|
|
781
|
+
engagement-notes: "Opened automatically from the CRM handoff. Confirm scope, dates, and owner before activating."
|
|
782
|
+
|
|
783
|
+
- id: link-engagement
|
|
784
|
+
type: output
|
|
785
|
+
position: { x: 0, y: 1080 }
|
|
786
|
+
data:
|
|
787
|
+
label: Link engagement → client
|
|
788
|
+
outputType: relation_create
|
|
789
|
+
config:
|
|
790
|
+
fromEntityId: "{{steps.create-engagement.output.entityId}}"
|
|
791
|
+
toEntityId: "{{loop.item.id}}"
|
|
792
|
+
relationType: engagement_for_client
|
|
793
|
+
|
|
794
|
+
# Draft contract, deduped on the engagement it governs. A skipped
|
|
795
|
+
# (duplicate) engagement create still returns the existing entityId, so
|
|
796
|
+
# this stays correctly anchored on a partial re-run.
|
|
797
|
+
- id: create-contract
|
|
798
|
+
type: output
|
|
799
|
+
position: { x: 0, y: 1200 }
|
|
800
|
+
data:
|
|
801
|
+
label: Open a draft contract
|
|
802
|
+
outputType: entity_create
|
|
803
|
+
config:
|
|
804
|
+
profileSlug: contract
|
|
805
|
+
title: "Contract — {{loop.item.title}}"
|
|
806
|
+
dedupeBy: contract-engagement
|
|
807
|
+
properties:
|
|
808
|
+
contract-status: draft
|
|
809
|
+
contract-engagement: "{{steps.create-engagement.output.entityId}}"
|
|
810
|
+
start-date: "{{steps.handoff-time.output.result}}"
|
|
811
|
+
contract-notes: "Drafted automatically from the CRM handoff. Attach the signed document and set the term."
|
|
812
|
+
|
|
813
|
+
- id: link-contract
|
|
814
|
+
type: output
|
|
815
|
+
position: { x: 0, y: 1320 }
|
|
816
|
+
data:
|
|
817
|
+
label: Link contract → engagement
|
|
818
|
+
outputType: relation_create
|
|
819
|
+
config:
|
|
820
|
+
fromEntityId: "{{steps.create-contract.output.entityId}}"
|
|
821
|
+
toEntityId: "{{steps.create-engagement.output.entityId}}"
|
|
822
|
+
relationType: contract_for_engagement
|
|
823
|
+
edges:
|
|
824
|
+
- { id: h-trigger-list, source: trigger, target: list-clients }
|
|
825
|
+
# `handoff-time` is a `compute` node, and `compute` is NOT a per-item
|
|
826
|
+
# loop-body type — the executor's body walk STOPS at it. Sitting inside
|
|
827
|
+
# the loop chain it severed the body, orphaning create-engagement /
|
|
828
|
+
# link-engagement / create-contract / link-contract into the main pass
|
|
829
|
+
# where `{{loop.item.id}}` resolved to undefined (empty-id entities and
|
|
830
|
+
# relations). It is a RUN-level timestamp, identical for every item, so
|
|
831
|
+
# it belongs before the loop; `{{steps.handoff-time.output.result}}`
|
|
832
|
+
# still resolves from inside the body because step outputs are global.
|
|
833
|
+
- { id: h-list-time, source: list-clients, target: handoff-time }
|
|
834
|
+
- { id: h-time-loop, source: handoff-time, target: handoff-loop }
|
|
835
|
+
- { id: h-loop-facets, source: handoff-loop, target: read-client-facets }
|
|
836
|
+
- { id: h-facets-status, source: read-client-facets, target: client-handoff-status }
|
|
837
|
+
- { id: h-status-gate, source: client-handoff-status, target: handoff-gate }
|
|
838
|
+
- { id: h-gate-existing, source: handoff-gate, target: existing-engagements }
|
|
839
|
+
- { id: h-existing-gate, source: existing-engagements, target: engagement-gate }
|
|
840
|
+
- { id: h-gate-engagement, source: engagement-gate, target: create-engagement }
|
|
841
|
+
- { id: h-engagement-link, source: create-engagement, target: link-engagement }
|
|
842
|
+
- { id: h-link-contract, source: link-engagement, target: create-contract }
|
|
843
|
+
- { id: h-contract-link, source: create-contract, target: link-contract }
|