@synap-core/cli 1.12.2 → 1.13.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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -8
  3. package/dist/build-info.json +2 -2
  4. package/dist/commands/agents.d.ts +0 -1
  5. package/dist/commands/agents.js +1 -1
  6. package/dist/commands/agents.js.map +1 -1
  7. package/dist/commands/app.js +2 -1
  8. package/dist/commands/app.js.map +1 -1
  9. package/dist/commands/automation.js +46 -41
  10. package/dist/commands/automation.js.map +1 -1
  11. package/dist/commands/box.d.ts +42 -0
  12. package/dist/commands/box.js +89 -0
  13. package/dist/commands/box.js.map +1 -0
  14. package/dist/commands/bridge-setup.js +3 -1
  15. package/dist/commands/bridge-setup.js.map +1 -1
  16. package/dist/commands/capability.d.ts +1 -1
  17. package/dist/commands/capability.js +57 -34
  18. package/dist/commands/capability.js.map +1 -1
  19. package/dist/commands/close-questions.d.ts +27 -0
  20. package/dist/commands/close-questions.js +49 -0
  21. package/dist/commands/close-questions.js.map +1 -0
  22. package/dist/commands/close-questions.test.d.ts +1 -0
  23. package/dist/commands/close-questions.test.js +52 -0
  24. package/dist/commands/close-questions.test.js.map +1 -0
  25. package/dist/commands/connect.d.ts +1 -3
  26. package/dist/commands/connect.js +2 -2
  27. package/dist/commands/connect.js.map +1 -1
  28. package/dist/commands/context.js +72 -43
  29. package/dist/commands/context.js.map +1 -1
  30. package/dist/commands/data-extra.js +12 -4
  31. package/dist/commands/data-extra.js.map +1 -1
  32. package/dist/commands/data.js +2 -1
  33. package/dist/commands/data.js.map +1 -1
  34. package/dist/commands/doctor.d.ts +21 -0
  35. package/dist/commands/doctor.js +108 -13
  36. package/dist/commands/doctor.js.map +1 -1
  37. package/dist/commands/hooks.d.ts +19 -0
  38. package/dist/commands/hooks.js +70 -0
  39. package/dist/commands/hooks.js.map +1 -0
  40. package/dist/commands/import.d.ts +25 -0
  41. package/dist/commands/import.js +98 -12
  42. package/dist/commands/import.js.map +1 -1
  43. package/dist/commands/init.d.ts +14 -0
  44. package/dist/commands/init.js +59 -19
  45. package/dist/commands/init.js.map +1 -1
  46. package/dist/commands/knowledge.js +4 -2
  47. package/dist/commands/knowledge.js.map +1 -1
  48. package/dist/commands/lens.js +2 -1
  49. package/dist/commands/lens.js.map +1 -1
  50. package/dist/commands/market-authoring.js +1 -1
  51. package/dist/commands/market-authoring.js.map +1 -1
  52. package/dist/commands/mcp.js +2 -2
  53. package/dist/commands/mcp.js.map +1 -1
  54. package/dist/commands/observe.category.test.d.ts +1 -0
  55. package/dist/commands/observe.category.test.js +59 -0
  56. package/dist/commands/observe.category.test.js.map +1 -0
  57. package/dist/commands/observe.d.ts +3 -0
  58. package/dist/commands/observe.js +7 -5
  59. package/dist/commands/observe.js.map +1 -1
  60. package/dist/commands/observe.title.test.d.ts +1 -0
  61. package/dist/commands/observe.title.test.js +37 -0
  62. package/dist/commands/observe.title.test.js.map +1 -0
  63. package/dist/commands/playbook.js +9 -0
  64. package/dist/commands/playbook.js.map +1 -1
  65. package/dist/commands/pods.d.ts +6 -1
  66. package/dist/commands/pods.js +32 -15
  67. package/dist/commands/pods.js.map +1 -1
  68. package/dist/commands/proton-setup.js +3 -1
  69. package/dist/commands/proton-setup.js.map +1 -1
  70. package/dist/commands/session-feed-wait.d.ts +48 -0
  71. package/dist/commands/session-feed-wait.js +261 -0
  72. package/dist/commands/session-feed-wait.js.map +1 -0
  73. package/dist/commands/session-post.d.ts +40 -0
  74. package/dist/commands/session-post.js +96 -0
  75. package/dist/commands/session-post.js.map +1 -0
  76. package/dist/commands/session-wait.d.ts +9 -4
  77. package/dist/commands/session-wait.js +20 -5
  78. package/dist/commands/session-wait.js.map +1 -1
  79. package/dist/commands/sessions.d.ts +2 -0
  80. package/dist/commands/sessions.js +23 -0
  81. package/dist/commands/sessions.js.map +1 -1
  82. package/dist/commands/skills-commands.d.ts +2 -0
  83. package/dist/commands/skills-commands.js +9 -1
  84. package/dist/commands/skills-commands.js.map +1 -1
  85. package/dist/commands/skills-commands.test.d.ts +1 -0
  86. package/dist/commands/skills-commands.test.js +57 -0
  87. package/dist/commands/skills-commands.test.js.map +1 -0
  88. package/dist/commands/status.js +1 -1
  89. package/dist/commands/status.js.map +1 -1
  90. package/dist/commands/template.d.ts +42 -0
  91. package/dist/commands/template.js +191 -0
  92. package/dist/commands/template.js.map +1 -0
  93. package/dist/commands/tracks.d.ts +67 -6
  94. package/dist/commands/tracks.js +268 -16
  95. package/dist/commands/tracks.js.map +1 -1
  96. package/dist/commands/whoami.d.ts +7 -0
  97. package/dist/commands/whoami.js +17 -0
  98. package/dist/commands/whoami.js.map +1 -1
  99. package/dist/commands/worker-accounts.d.ts +149 -0
  100. package/dist/commands/worker-accounts.js +339 -0
  101. package/dist/commands/worker-accounts.js.map +1 -0
  102. package/dist/commands/worker-accounts.test.d.ts +1 -0
  103. package/dist/commands/worker-accounts.test.js +224 -0
  104. package/dist/commands/worker-accounts.test.js.map +1 -0
  105. package/dist/commands/worker.d.ts +219 -10
  106. package/dist/commands/worker.js +749 -117
  107. package/dist/commands/worker.js.map +1 -1
  108. package/dist/commands/worker.test.js +321 -10
  109. package/dist/commands/worker.test.js.map +1 -1
  110. package/dist/index.js +164 -27
  111. package/dist/index.js.map +1 -1
  112. package/dist/lib/agent-door.d.ts +1 -3
  113. package/dist/lib/agent-door.js +10 -7
  114. package/dist/lib/agent-door.js.map +1 -1
  115. package/dist/lib/auth.d.ts +3 -2
  116. package/dist/lib/auth.js +3 -3
  117. package/dist/lib/auth.js.map +1 -1
  118. package/dist/lib/box/egress.d.ts +82 -0
  119. package/dist/lib/box/egress.js +264 -0
  120. package/dist/lib/box/egress.js.map +1 -0
  121. package/dist/lib/box/evidence.d.ts +86 -0
  122. package/dist/lib/box/evidence.js +121 -0
  123. package/dist/lib/box/evidence.js.map +1 -0
  124. package/dist/lib/box/job-server.d.ts +72 -0
  125. package/dist/lib/box/job-server.js +246 -0
  126. package/dist/lib/box/job-server.js.map +1 -0
  127. package/dist/lib/box/provisioner.d.ts +83 -0
  128. package/dist/lib/box/provisioner.js +312 -0
  129. package/dist/lib/box/provisioner.js.map +1 -0
  130. package/dist/lib/box/proxmox.d.ts +112 -0
  131. package/dist/lib/box/proxmox.js +211 -0
  132. package/dist/lib/box/proxmox.js.map +1 -0
  133. package/dist/lib/box/wire.d.ts +98 -0
  134. package/dist/lib/box/wire.js +70 -0
  135. package/dist/lib/box/wire.js.map +1 -0
  136. package/dist/lib/browser-auth.d.ts +35 -0
  137. package/dist/lib/browser-auth.js +170 -0
  138. package/dist/lib/browser-auth.js.map +1 -0
  139. package/dist/lib/client-header.test.d.ts +1 -0
  140. package/dist/lib/client-header.test.js +85 -0
  141. package/dist/lib/client-header.test.js.map +1 -0
  142. package/dist/lib/conversation-headers.d.ts +24 -9
  143. package/dist/lib/conversation-headers.js +40 -3
  144. package/dist/lib/conversation-headers.js.map +1 -1
  145. package/dist/lib/credential-class.d.ts +28 -1
  146. package/dist/lib/credential-class.js +28 -6
  147. package/dist/lib/credential-class.js.map +1 -1
  148. package/dist/lib/exporter-coverage.d.ts +1 -1
  149. package/dist/lib/exporter-coverage.js +1 -1
  150. package/dist/lib/goal-words.d.ts +38 -0
  151. package/dist/lib/goal-words.js +112 -0
  152. package/dist/lib/goal-words.js.map +1 -0
  153. package/dist/lib/hub-client.d.ts +28 -7
  154. package/dist/lib/hub-client.js +75 -34
  155. package/dist/lib/hub-client.js.map +1 -1
  156. package/dist/lib/init-handoff.d.ts +8 -0
  157. package/dist/lib/init-handoff.js +9 -2
  158. package/dist/lib/init-handoff.js.map +1 -1
  159. package/dist/lib/key-source.d.ts +3 -1
  160. package/dist/lib/key-source.js +7 -4
  161. package/dist/lib/key-source.js.map +1 -1
  162. package/dist/lib/knowledge-title.d.ts +3 -2
  163. package/dist/lib/knowledge-title.js +3 -2
  164. package/dist/lib/knowledge-title.js.map +1 -1
  165. package/dist/lib/mcp-bridge.d.ts +7 -0
  166. package/dist/lib/mcp-bridge.js +34 -1
  167. package/dist/lib/mcp-bridge.js.map +1 -1
  168. package/dist/lib/mcp-bridge.test.js +31 -3
  169. package/dist/lib/mcp-bridge.test.js.map +1 -1
  170. package/dist/lib/openclaw.d.ts +32 -0
  171. package/dist/lib/openclaw.js +165 -0
  172. package/dist/lib/openclaw.js.map +1 -0
  173. package/dist/lib/pod-moved.d.ts +70 -0
  174. package/dist/lib/pod-moved.js +146 -0
  175. package/dist/lib/pod-moved.js.map +1 -0
  176. package/dist/lib/pod-reach.d.ts +39 -0
  177. package/dist/lib/pod-reach.js +117 -0
  178. package/dist/lib/pod-reach.js.map +1 -0
  179. package/dist/lib/pod-reach.test.d.ts +1 -0
  180. package/dist/lib/pod-reach.test.js +67 -0
  181. package/dist/lib/pod-reach.test.js.map +1 -0
  182. package/dist/lib/pod.d.ts +8 -0
  183. package/dist/lib/pod.js +12 -3
  184. package/dist/lib/pod.js.map +1 -1
  185. package/dist/lib/register-claude-code-mcp.test.d.ts +1 -0
  186. package/dist/lib/register-claude-code-mcp.test.js +78 -0
  187. package/dist/lib/register-claude-code-mcp.test.js.map +1 -0
  188. package/dist/lib/review-credential.js +3 -1
  189. package/dist/lib/review-credential.js.map +1 -1
  190. package/dist/lib/seed.d.ts +13 -0
  191. package/dist/lib/seed.js +135 -0
  192. package/dist/lib/seed.js.map +1 -0
  193. package/dist/lib/session-feed.d.ts +185 -0
  194. package/dist/lib/session-feed.js +257 -0
  195. package/dist/lib/session-feed.js.map +1 -0
  196. package/dist/lib/targets.d.ts +21 -3
  197. package/dist/lib/targets.js +52 -107
  198. package/dist/lib/targets.js.map +1 -1
  199. package/dist/lib/wake-hook-settings.d.ts +57 -0
  200. package/dist/lib/wake-hook-settings.js +153 -0
  201. package/dist/lib/wake-hook-settings.js.map +1 -0
  202. package/dist/lib/wake-state.d.ts +65 -0
  203. package/dist/lib/wake-state.js +122 -0
  204. package/dist/lib/wake-state.js.map +1 -0
  205. package/package.json +5 -2
  206. package/skills/README.md +1 -1
  207. package/skills/agent-os/SKILL.md +8 -8
  208. package/skills/synap/SKILL.md +141 -84
  209. package/skills/synap/code-skills.md +70 -0
  210. package/skills/synap/concepts.md +28 -20
  211. package/skills/synap/creative-loop.md +1 -1
  212. package/skills/synap/document-embeds.md +3 -1
  213. package/skills/synap/focus-sessions.md +16 -7
  214. package/skills/synap/from-intent.md +32 -16
  215. package/skills/synap/governance-rules.md +1 -1
  216. package/skills/synap/lenses.md +4 -4
  217. package/skills/synap/linking.md +2 -2
  218. package/skills/synap/mental-model.md +1 -1
  219. package/skills/synap/operating-mode-rules.md +1 -1
  220. package/skills/synap/quick-reference.md +2 -1
  221. package/skills/synap/reflexes.md +6 -6
  222. package/skills/synap/viewframe-cells.md +1 -1
  223. package/skills/synap/work-model.md +24 -10
  224. package/skills/synap/workspace-design.md +5 -5
  225. package/skills/synap/writes.md +16 -9
  226. package/skills/synap-app/SKILL.md +148 -66
  227. package/skills/synap-app/creating-the-base-app.md +12 -7
  228. package/skills/synap-app/governance.md +27 -16
  229. package/skills/synap-app/install-commands.md +18 -9
  230. package/skills/synap-app/overview.md +16 -9
  231. package/skills/synap-app/using-the-sdk.md +75 -25
  232. package/skills/synap-schema/SKILL.md +21 -21
  233. package/skills/synap-schema/intent-vocabulary-ssot.md +21 -21
  234. package/templates/box/runner.py +206 -0
@@ -8,16 +8,16 @@ A process is the life of a **thing**, not a script. A lead, a post or a feature
8
8
 
9
9
  ### The nouns
10
10
 
11
- | Noun | What it is | Not |
12
- | -------------------- | ----------------------------------------------------------------------------------- | ------------------------------------ |
13
- | **Project** | a commitment, and a lens across spaces | a method, a domain |
14
- | **Track** (optional) | a LASTING intent in one project: direction, instructions, goal; it turns workers on | a list of stages; it never completes |
15
- | **Thing** | any entity: one kind, maybe roles, one status per kind or role | a second entity for a hat |
16
- | **Session** | the unit of work that closes: goal, criteria, room; one thing or a batch | a "run" because it follows a method |
11
+ | Noun | What it is | Not |
12
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
13
+ | **Project** | a commitment, and a lens across spaces | a method, a domain |
14
+ | **Goal** (optional) | in one project (a _track_ in tools): direction, criteria, the workers it turns on; its judge closes it with a verdict | a list of stages |
15
+ | **Thing** | any entity: one kind, maybe roles, one status per kind or role | a second entity for a hat |
16
+ | **Session** | the unit of work that closes: goal, criteria, room; one thing or a batch | a "run" because it follows a method |
17
17
 
18
18
  Not nouns (nobody manages them as objects): a **pipeline** is a view of a lifecycle; a **worker** is configuration (a method + a trigger + a size); a **method** is a playbook a session may follow; a **check** is a recurring verification.
19
19
 
20
- How they relate: a project files things; each kind or role declares one **lifecycle** (its status list); its **pipeline** is computed (columns = the options in order); a status change fires **workers**, which start **sessions** carrying the project, the track and their **targets**; a session moves each thing's status through the door. **Hand-off is a role:** a won deal makes the party a client (attach the role); nothing is copied.
20
+ How they relate: a project files things; each kind or role declares one **lifecycle** (its status list); its **pipeline** is computed (columns = the options in order); a status change fires **workers**, which start **sessions** carrying the project, the goal and their **targets**; a session moves each thing's status through the door. **Hand-off is a role:** a won deal makes the party a client (attach the role); nothing is copied.
21
21
 
22
22
  ### Three tenses of a thing
23
23
 
@@ -49,11 +49,25 @@ Every status change — your call, a board drag, a method's step, a sync — goe
49
49
 
50
50
  ### Workers
51
51
 
52
- A worker is a playbook **activator**, declared on the method and owned by the lifecycle: `on` = `created` · `enters_status` (+ `status`) · `count` (+ `status`, `min`: once N things wait) · `schedule` (+ `cron`) · `manual`; `size` = `per_thing` · `per_moment` (a count or schedule is always per batch); `mode` = `propose` (default) · `run`; `maxPendingApprovals` throttles it. It compiles into a governed rule the owner can pause; it fires once per thing per transition. A track or project **turns it on** for its scope; installs turn it on space-wide. Authoring: `processes`.
52
+ A worker is a playbook **activator**, declared on the method and owned by the lifecycle: `on` = `created` · `enters_status` (+ `status`) · `count` (+ `status`, `min`: once N things wait) · `schedule` (+ `cron`) · `manual`; `size` = `per_thing` · `per_moment` (a count or schedule is always per batch); `mode` = `propose` (default) · `run`; `maxPendingApprovals` throttles it. It compiles into a governed rule the owner can pause; it fires once per thing per transition. A goal or project **turns it on** for its scope; installs turn it on space-wide. Authoring: `processes`.
53
53
 
54
- ### Tracks
54
+ ### Goals (tracks)
55
55
 
56
- A track holds an owner, a direction, instructions, the workers it turns on, and a goal (stated, or measured as transitions into `done`). It has no stages and never completes: pause or archive it. Work in it = `start_session` + `trackId`. A thing may count for two tracks. Solo users may never need one: workers can run space-wide. Legacy: `start_stage_session` and `add_track_stage` serve old tracks started from a method; never `advance_track` without the user.
56
+ A goal is a _track_ in every tool and field name (`start_track`, `trackId`) — say "goal" to the person; a session's one-line `goal` is its **Aim**. A goal holds an owner, a direction, instructions, the workers it turns on, and what done means (criteria, or a number measured as transitions into `done`). It has no stages. A goal can stay open as long as it matters, or finish: a person closes it with a verdict once its criteria are met (`set_track_status` `completed` + `verdict`; dropped = `archived`, with a `verdict` saying why). Your close is a proposal; a closed goal comes back only as a new version. Pause it while nobody should work on it. Work in it = `start_session` + `trackId`. A thing may count for two goals. Solo users may never need one: workers can run space-wide. Legacy: `start_stage_session` and `add_track_stage` serve old goals started from a method; never `advance_track` without the user.
57
+
58
+ **Keep the pod current (every agent, every session)** — three rules, so the next agent picks your step up from the pod alone:
59
+
60
+ 1. **Attach the session to its goal.** `orient`'s `startHere.openGoals` and the start reply's `offers.goals` name the open goals; start with `trackId`, or file an open session with `update_session` `trackId` (a proposal for an agent). No goal fits: say so in the room, never invent one silently.
61
+ 2. **Record your PR as an outcome** the moment you open it: `update_session` `outcomes: [{ key: "pr-<repo>-<n>", kind: "pull_request", label: "<repo>#<n>", ref: { url: "<PR url>" } }]`. With no `sessionId`, the pod files it on the one open session this conversation started, and refuses with the list when there are several.
62
+ 3. **Close when your outcomes are met.** When every outcome is met the `update_session` reply carries `nudges.readyToClose`: grade with `evaluate_session`, then `complete_session`. A merged PR is not always the end of the work: if more remains, declare it instead.
63
+
64
+ **The rest of the goal protocol** (`set_track_participant`, `reopen_track`, `convert_to_goal`, `update_track`):
65
+
66
+ - **A delegate's commitment.** The owner or judge asks someone (`add`, role `delegate`, with a `commitment`). The delegate answers only their own: `accept`, `counter` (text = what they would commit to instead), `decline`, or later `report_impossible` (text = why). A report is not the goal's verdict: the owner or judge `acknowledge`s it and re-plans. Never answer for a person; adding yourself as a delegate commits you.
67
+ - **Claims and the judge.** A session that passes a goal criterion CLAIMS it. Only the judge (the owner unless one is named) accepts or sends back a claim, and only the judge closes the goal. Never accept your own claim.
68
+ - **Reopen = a new version.** A closed or dropped goal never moves back: `reopen_track` starts a new version linked to it, with the same people asked again.
69
+ - **Make it a goal.** A goal, objective or OKR entity becomes a goal once with `convert_to_goal` (its key results become measured criteria). Offer it when the person treats such an entity as live work; never convert one twice.
70
+ - **The loop** is OFF by default. When it is on (`update_track` `loop: { on: true, perWeek? }`), the goal proposes its next session while a required criterion is unmet, and a person approves each one. Your switch-on is always a proposal: the person turns it on. If it stops after two failures on one criterion, ask the owner to re-plan; do not switch it on again yourself.
57
71
 
58
72
  ### The homes: what each space makes, what is moving
59
73
 
@@ -1,6 +1,6 @@
1
1
  ## Workspace design — is this concern a WORKSPACE, or something smaller?
2
2
 
3
- Before you create a workspace, run the decision rule. A workspace (an operational **domain**) is the heaviest structure in the pod — it owns kinds, confers roles, carries its own team and automations. Most new concerns are NOT domains; they are a **hat**, an **initiative**, a **lasting goal** (track), or a **status**. Creating a workspace for one of those is the anti-pattern that fragments the graph. Decide first, then create. What each word means: `concepts` (the one glossary).
3
+ Before you create a workspace, run the decision rule. A workspace (an operational **domain**) is the heaviest structure in the pod — it owns kinds, confers roles, carries its own team and automations. Most new concerns are NOT domains; they are a **hat**, an **initiative**, a **lasting goal** (a goal — `track` in tools), or a **status**. Creating a workspace for one of those is the anti-pattern that fragments the graph. Decide first, then create. What each word means: `concepts` (the one glossary).
4
4
 
5
5
  **One space per domain:** A space is a domain, installed once (Content, Brand, CRM…). A project is a filter across spaces. To make something “for project X”: file it into X inside the domain's space (`file_into_project`), and link X to the space (`project_use_workspace`). Never create a space named after a project, a brand or a client.
6
6
 
@@ -17,7 +17,7 @@ Before you create a workspace, run the decision rule. A workspace (an operationa
17
17
  | ----------------------------------------------------------- | ----------- | -------------------------------------- | ------------------------------------------- |
18
18
  | a **role/hat** an existing entity wears in a domain | **Facet** | `attach_facet` (`profileKind: "role"`) | `client`, `sponsor`, `prospect`, `investor` |
19
19
  | a **cross-cutting, time-bound initiative** spanning domains | **Project** | `create_project` (a lens) | a campaign, an engagement, a launch |
20
- | a **lasting goal** a project pursues (worked in sessions) | **Track** | `start_track` (no method needed) | grow the newsletter, win ten clients |
20
+ | a **lasting goal** a project pursues (worked in sessions) | **Goal** | `start_track` (no method needed) | grow the newsletter, win ten clients |
21
21
  | a **stage/filter WITHIN a domain** | **Status** | an option of the kind's lifecycle | pipeline stage, "active"/"archived" |
22
22
 
23
23
  ## The decision procedure (follow in order)
@@ -25,8 +25,8 @@ Before you create a workspace, run the decision rule. A workspace (an operationa
25
25
  1. **Name the source-of-truth noun.** What kind would this workspace _own_ that no existing workspace owns? Run `list_profiles` — if the noun already lives in another domain, you have a facet or a project, not a domain. STOP.
26
26
  2. **Test all four conditions.** Owns kinds AND own team AND native automations AND stable. Any one fails → fork below.
27
27
  3. **If it's a hat** (a status/role on an entity that already exists elsewhere) → resolve the entity, `attach_facet`. Never a workspace, never a second entity. One role per name, pod-wide: a role several workspaces use is ONE role, each workspace adding properties by overlay — never a per-workspace twin (`concepts`).
28
- 4. **If it's time-bound work across domains** → `create_project` and set it as the lens; the work files into it from whatever workspace holds the data. **A project is a COMMITMENT WITH GRAVITY** — a real initiative that ties work together (a campaign, an engagement, a client, a launch). Tasks, plans, repos, themes, and topics are **entities**, never projects. Before you create one: (a) **search existing projects first** (`synap orient` / `GET /api/hub/projects`) and prefer **linking into an existing project** via `belongs_to_project` — near-duplicate names are rejected with the existing candidates; (b) an agent-created project must cite **≥5 existing entities** that would belong to it as `evidenceEntityIds` — the backend rejects a project with no gravity and tells you to store it as an entity or reuse an existing project instead; (c) **never create a project for the pod owner's own company** — the company _is_ the pod, not a project inside it.
29
- 5. **If it's a lasting goal a project pursues** ("keep the content side of the launch going") → a **track**: `list_tracks`, else `start_track` (`name`, `direction`); work in it with `start_session` + `trackId`. Never a workspace, never a twin project.
28
+ 4. **If it's time-bound work across domains** → `create_project` and set it as the lens; the work files into it from whatever workspace holds the data. **A project is a COMMITMENT WITH GRAVITY** — a real initiative that ties work together (a campaign, an engagement, a client, a launch). Tasks, plans, repos, themes, and topics are **entities**, never projects. Before you create one: (a) **search existing projects first** (`synap orient` / `GET /api/hub/projects`) and prefer **linking into an existing project** via `belongs_to_project` — a near-duplicate name is filed for the person to review, showing the existing project; (b) an agent-created project must cite **≥5 existing entities** that would belong to it as `evidenceEntityIds` — the backend rejects a project with no gravity and tells you to store it as an entity or reuse an existing project instead; (c) **never create a project for the pod owner's own company** — the company _is_ the pod, not a project inside it.
29
+ 5. **If it's a lasting goal a project pursues** ("keep the content side of the launch going") → a **goal**: `list_tracks`, else `start_track` (`name`, `direction`); work in it with `start_session` + `trackId`. Never a workspace, never a twin project.
30
30
  6. **If it's a stage inside a domain** → it is an option of the kind's or role's declared lifecycle, never a role and never a space. Its pipeline is computed from the lifecycle: open it, don't build one. A kind with no lifecycle yet gets a `status` property def (`create_property_def`).
31
31
  7. **Only if all four held** → **template first** (escalation ladder L3):
32
32
  `market.search({query, kind: "template"})` and propose install of a matching
@@ -36,7 +36,7 @@ Before you create a workspace, run the decision rule. A workspace (an operationa
36
36
 
37
37
  ## The CRM corollary — the load-bearing example
38
38
 
39
- Operational state — **prospect → client → delivered** — is a **FLOW across domains**, expressed as **facets + an engagement project** (its delivery runs as a track), NEVER as workspaces and NEVER by bolting delivery onto the identity domain.
39
+ Operational state — **prospect → client → delivered** — is a **FLOW across domains**, expressed as **facets + an engagement project** (its delivery runs as a goal), NEVER as workspaces and NEVER by bolting delivery onto the identity domain.
40
40
 
41
41
  - CRM = **who** (owns `person`/`company`, confers the `lead`/`client` facets).
42
42
  - Operations = **what we do for them** (owns `engagement`/`contract`/`deliverable`).
@@ -14,10 +14,14 @@ several entities, or a graph — or **when in doubt** — use the capture door
14
14
  in the SAME call, never from picking a different tool or a second commit step.
15
15
 
16
16
  The difference that is NOT visible in either tool's description: `create_entity`
17
- writes one entity with per-entity governance and REJECTS a name collision unless
18
- `forceCreate`; capture-with-`entities[]` files ONE atomic proposal for the whole
19
- graph and DEDUP-MERGES instead of rejecting. Pick by whether you want a partial
20
- write to be possible, not by how structured your data feels.
17
+ writes one entity with per-entity governance; capture-with-`entities[]` files ONE
18
+ atomic proposal for the whole graph. Neither rejects a look-alike: a record (or
19
+ one of your own pending proposals) of the same kind with the same name or
20
+ identity makes the write a proposal carrying `duplicateCheck`, and the person
21
+ decides (keep both, use the existing one, fill its empty fields). Only a strong
22
+ identity (email, phone, url, handle, external id) links to the existing record
23
+ on its own. Pick by whether you want a partial write to be possible, not by how
24
+ structured your data feels.
21
25
 
22
26
  **Every title is linted.** Both doors return `titleLint` in the receipt when a
23
27
  title breaks the rubric (too long, cut off, status words, ids, a placeholder…);
@@ -98,6 +102,8 @@ the connection's lens). A kind another space needs: `synap_change_kind_scope`
98
102
  (`scope: "shared"` + `allowedWorkspaceIds`), never a twin kind. Records:
99
103
  `synap_move_entities`. All three are reviewed.
100
104
 
105
+ **Removing one: `synap_delete_entity`** (`entityId`, optional `reasoning`). For an agent it is always a proposal — nothing is deleted until the person approves, and no rule can widen that. An approved delete hides the entity (its body is kept) and the person can undo it from the proposal. Do not retry a `proposed` answer.
106
+
101
107
  ### Authored text is content, not a `file`
102
108
 
103
109
  Something **you** author — a pitch deck, a strategic plan, a note body — is
@@ -120,7 +126,7 @@ You CAN store content you **hold** — you send it inline. Pick by what you have
120
126
  | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
121
127
  | **Content in hand** — a report/CSV/image/PDF/etc. you generated or received (text or bytes) | **`synap_store_file`** (`content` for text, `contentBase64` for binary) + `mimeType`/`filename` | a `file` entity, content stored **as-is, never read** (≤10MB inline) |
122
128
  | Text you're **authoring as a note/idea** (not "a file") — a plan, a thought | `synap_create_entity` with `content` (a content kind) | body auto-becomes a document — NOT a `file` |
123
- | Only a **URL/link** (no bytes) — a Google Doc, a PDF url, an article | `synap_create_document` with **`url`**, or attach via `entityId` | an external **reference** document (no bytes) |
129
+ | Only a **URL/link** (no bytes) — a Google Doc, a PDF url, an article | `synap_create_document` with **`url`**; `entityId` only for a thing with no body yet | an external **reference** document (no bytes) |
124
130
  | A **large file** (over 10MB, up to 500MB video) | `synap_request_upload` → PUT the bytes to `uploadUrl` → `synap_store_file({ uploadToken })` | a `file` entity backed by stored bytes |
125
131
  | Only a file's **name**, nothing else | you **cannot** invent it — ask the human/client to provide the content or a link | — |
126
132
 
@@ -210,14 +216,15 @@ POST /api/hub/documents/{documentId}/patch
210
216
 
211
217
  Ops: `upsert_section {id, title, body}` (one section by id), `replace_text {old,
212
218
  new}` (`old` must match **exactly once**, else 400 with the count), `append
213
- {body}`, `replace_all {content}`. An agent's edit is filed as a proposal (a
219
+ {body}`, `replace_all {content}`, `set_title {title}` (renames the document, a
220
+ human name). An agent's edit is filed as a proposal (a
214
221
  governance rule may apply it directly); a person's section (`owner="human"`) is
215
222
  never changed and an embed is never dropped unless you pass
216
223
  `"allowRemovingEmbeds": true` (403 otherwise). A moved document answers 409 and
217
224
  nothing is written: read again. An applied or approved edit reaches open editors
218
225
  (`document:content-replaced`), so they reload instead of overwriting it. `PATCH /api/hub/documents/{id}` with `content` is the same door with
219
- one `replace_all` — it cannot rename (a `title` is refused with 400; to rename an
220
- entity's document, update the entity's title). MCP: `synap_update_document`;
226
+ one `replace_all` — it cannot rename (a `title` is refused with 400; to rename the
227
+ document, send a `set_title` op to `POST /api/hub/documents/{id}/patch`). MCP: `synap_update_document`;
221
228
  full syntax in `document-embeds.md`.
222
229
 
223
230
  The reverse lookup is `entities WHERE documentId = ?`. Always attach the document to a meaningful entity (the meeting event, the project, the person) — a floating document is another orphan.
@@ -273,7 +280,7 @@ POST /api/hub/entities
273
280
 
274
281
  This creates a first-class decision entity linked to Project Eve. It shows up in traversals, can be superseded later (`supersededBy: newDecisionId`), and survives governance. Memory can't do any of that.
275
282
 
276
- **A decision the PERSON must make** — create it with `decisionStatus: "proposed"` and the pod asks them for you: pass `decisionOptions` (≤8 of `{label, value?, description?}`) and `recommendedOption` (an option's `value`, else its `label`); set `sourceSessionId` to keep the question in your session. Their answer UPDATES that decision — never create a second one. Every answered `confirm`/`choose` ask already files its decision (`slot.decisionId`): don't create one for it. A person-owned slot blocked on a decision is ANSWERED by them (Yes/No or a choice) — it is never closed by marking it done. **Before you recommend**, recall the user's past decisions (`synap_ask`, or find `decision` entities) so your pick follows their past choices.
283
+ **A decision the PERSON must make** — create it with `decisionStatus: "proposed"` and the pod asks them for you: pass `decisionOptions` (≤8 of `{label, value?, description?}`) and `recommendedOption` (an option's `value`, else its `label`); set `sourceSessionId` to keep the question in your session. With no options the person sees its `title` with Accept / Reject, each saying what it does. So give it a short `title` (the decision itself, never "Decide: …") and a `summary` that says what the title does not. Only a decision YOU propose asks: a person's own proposed decision is their note, and the pod never asks them about it. Their answer UPDATES that decision — never create a second one. Every answered `confirm`/`choose` ask already files its decision (`slot.decisionId`): don't create one for it. A person-owned slot blocked on a decision is ANSWERED by them (Yes/No or a choice) — it is never closed by marking it done. **Before you recommend**, recall the user's past decisions (`synap_ask`, or find `decision` entities) so your pick follows their past choices.
277
284
 
278
285
  ### Post to the user's personal channel
279
286
 
@@ -32,8 +32,8 @@ pod, and **every write is governed** (see `governance.md`).
32
32
  | The user has… | Do this | Read |
33
33
  |---|---|---|
34
34
  | an existing app (any framework) | add the SDK | `using-the-sdk.md`, `install-commands.md` |
35
- | nothing yet, wants a full app fast | scaffold the base app | `creating-the-base-app.md`, `install-commands.md` |
36
- | a server / script / CLI (no browser) | SDK with a session token or API key | `using-the-sdk.md` |
35
+ | nothing yet, wants a full app fast | scaffold the base app (scaffolder not published yet) | `creating-the-base-app.md`, `install-commands.md` |
36
+ | a server / script / CLI (no browser) | SDK with a session token, or the REST client with an API key | `using-the-sdk.md` |
37
37
 
38
38
  Both paths use the same underlying packages; only the amount of scaffolding differs.
39
39
 
@@ -41,9 +41,11 @@ Both paths use the same underlying packages; only the amount of scaffolding diff
41
41
 
42
42
  - `@synap-core/sdk` — typed tRPC client. `createSynapClient({ podUrl, ... })`. **The one you almost always want.**
43
43
  - `@synap-core/react` — React/TanStack-Query bindings (`SynapProvider`, hooks).
44
- - `@synap-core/sdk-realtime` — Socket.IO presence/live-updates (`createRealtimeClient`).
45
- - `@synap-core/auth` — sign-in orchestration (Synap Cloud + pod handshake), framework-agnostic.
46
- - `@synap-core/auth-bootstrap` — zero-dep server helpers (`setupAgent`, `handshake`) for machine clients.
44
+ - `@synap-core/sdk-realtime` — Socket.IO "something changed" events (`createRealtimeClient`).
45
+ - `@synap-core/hub-rest-client` — zero-dep REST client for API-key holders (`HubRestClient`, `setupAgent`).
46
+
47
+ Do not install `@synap-core/auth`: that package was retired, and its last npm
48
+ release signs in through a pod route that no longer exists.
47
49
 
48
50
  Pin `@^1.0.0` on `sdk`/`react` so you never resolve an old, abandoned `0.x`.
49
51
 
@@ -53,13 +55,18 @@ Pin `@^1.0.0` on `sdk`/`react` so you never resolve an old, abandoned `0.x`.
53
55
  the change is queued for the pod owner's review (like a PR). Surface the review,
54
56
  don't retry. Full contract: `governance.md`.
55
57
  2. **The pod URL has no `/trpc` suffix and no trailing slash.** The SDK appends `/trpc`.
56
- 3. **Non-browser clients (server/CLI/mobile) authenticate with a token or API key**,
57
- never a cookie. Browser apps use the Synap Cloud sign-in flow (`@synap-core/auth`).
58
+ 3. **Non-browser clients (server/CLI/mobile) authenticate with a session token or
59
+ an API key**, never a cookie. An API key only works on the REST client and
60
+ realtime, not on the tRPC SDK. Browser apps use the Kratos session cookie.
58
61
 
59
62
  ## Get a pod to build against
60
63
 
61
- - **Self-host (free):** `git clone` the backend, `docker compose up -d` (run its
62
- `install.sh` to generate secrets). Pod answers on `:4000`.
64
+ - **Self-host (free):** run the backend's `install.sh` (it installs Docker if
65
+ needed, fetches the release bundle, checks it against the release manifest's
66
+ sha256 and runs `synap install`); see the backend's `deploy/README.md`. It
67
+ also checks the manifest's signature: by default
68
+ (`SYNAP_RELEASE_VERIFY=warn`) it only warns and carries on when the signature
69
+ is missing or does not verify; `SYNAP_RELEASE_VERIFY=enforce` refuses.
63
70
  - **Managed (paid):** sign up at synap.live and provision a pod.
64
71
 
65
72
  You can develop the base app with **no pod at all** using its mock mode
@@ -78,11 +85,17 @@ don't architect around a capability that isn't there.
78
85
 
79
86
  ## Scaffold a full app (recommended for a fresh start)
80
87
 
88
+ > **Not published yet.** The scaffolder (`@synap-core/create-synap-app`) is not
89
+ > on npm and the base template is not public, so `npx` returns 404 today. Use the
90
+ > SDK path below until a release exists.
91
+
92
+ Once published:
93
+
81
94
  ```bash
82
- npx create-synap-app my-app
95
+ npx @synap-core/create-synap-app my-app
83
96
  ```
84
97
 
85
- Creates a Next.js + HeroUI project from the base template, with the SDK, auth,
98
+ It creates a Next.js + HeroUI project from the base template, with the SDK, auth,
86
99
  app shell, command palette, realtime, and mock mode pre-wired. It also drops an
87
100
  `AGENTS.md` into the project so your AI coding tool knows how to work with it.
88
101
 
@@ -98,24 +111,27 @@ npm install @synap-core/sdk@^1.0.0 @synap-core/react@^1.0.0
98
111
  # realtime
99
112
  npm install @synap-core/sdk-realtime
100
113
 
101
- # auth (browser sign-in) or auth-bootstrap (server credential bootstrap)
102
- npm install @synap-core/auth # or @synap-core/auth-bootstrap
114
+ # REST client for API-key holders (agents, scripts); also has setupAgent()
115
+ npm install @synap-core/hub-rest-client
103
116
  ```
104
117
 
118
+ Do not install `@synap-core/auth` (retired; its last release calls a removed pod
119
+ route) or `@synap-core/auth-bootstrap` (not published).
120
+
105
121
  Pin `@^1.0.0` on `sdk`/`react`. **Do not** install unpinned `@synap-core/sdk` —
106
122
  older `0.x` versions on the registry are a different, abandoned package.
107
123
 
108
124
  ## Get this knowledge into your AI coding tool
109
125
 
110
126
  ```bash
111
- # add the app-creation skill (this skill) to a connected AI tool
112
- synap skills get synap-app
127
+ # print this skill from your pod's skill knowledge base
128
+ synap skill get synap-app
113
129
 
114
- # or wire a tool end-to-end (skills + MCP), any target
115
- synap connect --target=cursor # or claude, …
130
+ # or wire a tool end-to-end (skills + MCP)
131
+ synap connect --target=cursor # or claude-code, claude-desktop, codex, …
116
132
  ```
117
133
 
118
- Alternatively, the scaffolded project ships an `AGENTS.md` at its root — most AI
134
+ Once the scaffolder is published, the scaffolded project ships an `AGENTS.md` at its root — most AI
119
135
  coding tools read it automatically, so a freshly-scaffolded app teaches the AI
120
136
  how to build on it with zero extra setup.
121
137
 
@@ -141,33 +157,41 @@ import { createSynapClient } from "@synap-core/sdk";
141
157
 
142
158
  const synap = createSynapClient({
143
159
  podUrl: "https://pod.example.com", // NO trailing slash, NO /trpc suffix
144
- apiKey: process.env.SYNAP_API_KEY, // or sessionToken (see auth below)
160
+ sessionToken: process.env.SYNAP_SESSION_TOKEN, // see auth below
145
161
  workspaceId: process.env.SYNAP_WORKSPACE_ID ?? "",
162
+ onAuthError: () => {/* 401: sign in again */},
146
163
  });
147
164
 
148
165
  // It's a typed tRPC client — full autocomplete over every router.
149
166
  const me = await synap.users.me.query();
150
167
  const entities = await synap.entities.list.query({ /* filter */ });
151
168
  const result = await synap.entities.create.mutate({ /* ... */ });
152
- // result may be { status: "proposed", ... } — that is success. See governance.md.
169
+ // result.status may be "proposed" — that is success. See governance.md.
153
170
  ```
154
171
 
155
172
  ### Auth modes (pick ONE)
156
- - **`apiKey`** — a Bearer key generated in the workspace's developer settings.
157
- Best for servers, scripts, CLIs.
158
- - **`sessionToken`** — an Ory Kratos session token, sent as `X-Session-Token`.
159
- Best for non-browser clients that completed a sign-in.
160
- - **cookie fallback** — omit both; the client uses the ambient Kratos cookie.
161
- Browser, same-origin only.
162
-
163
- For machine clients that need to bootstrap a credential, use
164
- `@synap-core/auth-bootstrap` (`setupAgent(provisioningToken)` → API key,
165
- `handshake(jwt)` → session token). Zero-dependency, safe in edge/serverless.
173
+ - **`sessionToken`** — an Ory Kratos session token from a Kratos API (native)
174
+ login flow, sent as `X-Session-Token`. The normal choice for servers, scripts,
175
+ CLIs and mobile.
176
+ - **cookie fallback** — omit both; the client sends the ambient Kratos cookie
177
+ (`credentials: "include"`). Browser only.
178
+ - **`apiKey`** — only the local-mode token of a desktop pod, sent as
179
+ `Authorization: Bearer`. `/trpc` refuses hub and user API keys
180
+ (keys starting with synap_hub_ or synap_user_), and `createSynapClient` throws if you pass
181
+ one. For an API key, use `@synap-core/hub-rest-client` (below).
182
+
183
+ Other options: `extraHeaders`, and `onAuthError({ status, message })`, called on a
184
+ 401 (it also turns the pod's plain-JSON 401 body into a clean tRPC error).
185
+
186
+ To mint an API key for a machine client, `setupAgent(podUrl, provisioningToken,
187
+ agentType)` from `@synap-core/hub-rest-client` returns `{ hubApiKey, agentUserId,
188
+ workspaceId }`. That key works with the REST client and the realtime client, not
189
+ with `/trpc`.
166
190
 
167
191
  ## React — `@synap-core/react`
168
192
 
169
193
  ```tsx
170
- import { SynapProvider, useSynapContext } from "@synap-core/react";
194
+ import { SynapProvider, useSynap, useEntities } from "@synap-core/react";
171
195
 
172
196
  function App() {
173
197
  return (
@@ -176,31 +200,73 @@ function App() {
176
200
  </SynapProvider>
177
201
  );
178
202
  }
203
+
204
+ function Dashboard() {
205
+ const { data } = useEntities("note", { limit: 20 }); // thin data hook
206
+ const create = useSynap.entities.create.useMutation(); // raw tRPC hooks
207
+ // …
208
+ }
179
209
  ```
180
210
 
181
- `SynapProvider` honours every `createSynapClient` option, incl. `sessionToken`
182
- and `onAuthError` (a 401 handler). `react` is a peer of your app's React — it
183
- does not bundle its own.
211
+ `useSynap` is the tRPC React hook tree itself — write
212
+ `useSynap.entities.list.useQuery(…)`, **not** `useSynap().entities…`: calling it
213
+ returns `undefined` at runtime (some package docs still show the call form).
214
+
215
+ `SynapProvider` takes every `createSynapClient` option (incl. `sessionToken` and
216
+ `onAuthError`) plus an optional `queryClient` to share an existing TanStack Query
217
+ client. Exports: `SynapProvider`, `useSynapContext` (`{ podUrl, workspaceId }`),
218
+ `useSynap`, the data hooks `useEntity`, `useEntities`, `useLinks`, `useView`,
219
+ and the `RouterInputs` / `RouterOutputs` types. React 18 or 19 is a peer
220
+ dependency — the package does not bundle its own.
184
221
 
185
222
  ## Realtime — `@synap-core/sdk-realtime`
186
223
 
187
224
  ```ts
188
225
  import { createRealtimeClient } from "@synap-core/sdk-realtime";
189
226
 
190
- const rt = createRealtimeClient({ podUrl, sessionToken });
191
- const stop = rt.onInvalidate((target) => {
192
- // e.g. refetch the affected query
227
+ const rt = createRealtimeClient({
228
+ url: podUrl, // pod base URL; the client adds the /presence namespace
229
+ workspaceId,
230
+ sessionToken, // a Kratos session token…
231
+ userId, // …and the identity id it belongs to (both required)
232
+ // or, as a service account: apiKey (scope realtime:observe), no userId
233
+ onInvalidate: (event) => {
234
+ // entity/relation changed — refetch, e.g.
235
+ queryClient.invalidateQueries({ queryKey: ["entities"] });
236
+ },
193
237
  });
238
+
239
+ rt.connect(); // nothing connects until you call this
240
+ // … later
241
+ rt.disconnect();
194
242
  ```
195
243
 
196
- Websocket-only transport (credentials never ride a long-poll URL) and it refuses
197
- to send credentials over non-TLS unless you explicitly pass `allowInsecure`.
244
+ - Callbacks (all options, no subscribe methods): `onInvalidate` (the
245
+ entity/relation-changing subset), `onEvent` (every known event, `{ name, data }`),
246
+ `onConnect`, `onDisconnect(reason)`, `onError(error)`.
247
+ - The returned client has `connect()`, `disconnect()`, and read-only `connected`
248
+ and `state` (`"idle" | "connecting" | "connected" | "disconnected"`).
249
+ - `SYNAP_EVENT_NAMES` lists the forwarded events; `ENTITY_INVALIDATING_EVENTS` is
250
+ the subset behind `onInvalidate`.
251
+ - Websocket-only transport (credentials never ride a long-poll URL), and
252
+ `connect()` throws on a non-TLS URL unless you pass `allowInsecure: true`
253
+ (localhost dev only).
254
+
255
+ **Version note.** `@synap-core/sdk-realtime@0.1.0` (the current npm release) sends
256
+ every credential as `auth.token`. The pod reads `{ userId, token, workspaceId }`
257
+ for a user and `{ apiKey, workspaceId }` for a service account, so on 0.1.0 a
258
+ session token works only when you also pass `userId`, and an `apiKey` is always
259
+ refused. The fix — correct handshake keys, plus an up-front throw for no
260
+ credential, both credentials, or `sessionToken` without `userId` — lands in the
261
+ first release after 0.1.0. Until then, authenticate realtime with
262
+ `sessionToken` + `userId`.
198
263
 
199
264
  ## Two transports, on purpose
200
- - `@synap-core/sdk` (tRPC) — for apps. Typed, hooks, realtime.
201
- - `@synap-core/hub-rest-client` (REST, zero-dep) — for agents/CLIs/scripts. Carries
202
- agent verbs the tRPC surface has no first-class path for (`ask`, `orient`,
203
- `discover`). Use it when you want a dependency-free REST client, not a full app.
265
+ - `@synap-core/sdk` (tRPC) — for apps signed in as a user. Typed, React hooks.
266
+ - `@synap-core/hub-rest-client` (REST against `/api/hub/*`, zero-dep) — for
267
+ agents/CLIs/scripts holding an API key. `new HubRestClient({ podUrl, apiKey,
268
+ workspaceId })`. Carries agent verbs the tRPC surface has no first-class path
269
+ for (`ask`, `orient`, `discover`).
204
270
 
205
271
  ## Keep types fresh
206
272
  The SDK is typed by `@synap-core/api-types`, generated from the pod's live router.
@@ -217,11 +283,16 @@ mock mode — all pre-wired. You add your screens; the plumbing is done.
217
283
 
218
284
  ## Scaffold it
219
285
 
286
+ > **Not published yet.** `@synap-core/create-synap-app` is not on npm and the
287
+ > base template is not public, so this does not work today. Until a release
288
+ > exists, build on the SDK directly (`using-the-sdk.md`). The rest of this page
289
+ > describes the template as it will ship.
290
+
220
291
  ```bash
221
- npx create-synap-app my-app
222
- cd my-app
223
- cp .env.example .env.local # fill in your pod / control-plane URLs
224
- pnpm dev # http://localhost:3010
292
+ npx @synap-core/create-synap-app my-app
293
+ cd my-app # the scaffolder seeds .env.local from env.example;
294
+ # fill in your pod / control-plane URLs
295
+ pnpm install && pnpm dev # http://localhost:3010
225
296
  ```
226
297
 
227
298
  Develop with **no pod** by setting `NEXT_PUBLIC_BASE_MOCK_MODE=true`.
@@ -269,9 +340,9 @@ components/layout/
269
340
 
270
341
  ## Auth flow (already wired)
271
342
 
272
- `/login` → Synap Cloud sign-in (email + password or magic link) → optional TOTP →
273
- pod picker → handshake → session stored → `/home`. Self-hosted pods fall back to
274
- the pod's own Kratos at `/login?mode=kratos`. You don't build this; you theme it.
343
+ `/login` → Synap Cloud sign-in → optional TOTP → pod picker → session stored →
344
+ `/home`. A pod can also be signed into directly through its own Kratos (the
345
+ login screen's manual-login step). You don't build this; you theme it.
275
346
 
276
347
  ---
277
348
 
@@ -282,38 +353,49 @@ mutates a Synap pod.
282
353
 
283
354
  ## The contract
284
355
 
285
- Every mutation (`entities.create`, `entities.update`, `relations.create`, …)
286
- resolves to one of three outcomes, decided **by the pod**, not by your app:
356
+ Every governed mutation (`entities.create`, `entities.update`, `relations.create`, …)
357
+ has one of three outcomes, decided **by the pod**, not by your app:
287
358
 
288
- | Outcome | Meaning | What your code should do |
359
+ | Outcome | How it arrives | What your code should do |
289
360
  |---|---|---|
290
- | `approved` | applied immediately | proceed |
291
- | `proposed` | queued for the pod owner's review (like a PR) | **surface the review — do NOT treat as failure, do NOT retry** |
292
- | `denied` | the principal may not do this | show why; do not retry blindly |
361
+ | applied | resolves with the procedure's own status (`"created"`, `"updated"`, …) | proceed |
362
+ | proposed | resolves with `status: "proposed"`, `proposalId`, `reviewUrl`, `message` — queued for the pod owner's review (like a PR) | **surface the review — do NOT treat as failure, do NOT retry** |
363
+ | denied | **rejects** with a tRPC `FORBIDDEN` error whose message is the reason | show why; do not retry blindly |
293
364
 
294
365
  ## Why it matters for your code
295
366
 
296
367
  - **Never treat `proposed` as an error.** It is the normal, healthy path for any
297
368
  write that governance decides a human should see first. Retrying a `proposed`
298
369
  write creates duplicates.
299
- - **Never `try/catch` a `proposed` as if it threw.** It's a successful response
300
- with a status, not an exception.
370
+ - **A `proposed` result does not throw.** It is a successful response with a
371
+ status; only a denial throws.
301
372
  - **Surface the review link/id** so the user can approve it. An unreviewed
302
373
  proposal is not yet in the graph — it won't show up in reads until approved.
303
374
 
304
375
  ## Pattern
305
376
 
306
377
  ```ts
307
- const res = await synap.entities.create.mutate(input);
308
- switch (res.status) {
309
- case "approved": /* it's live */ break;
310
- case "proposed": /* tell the user: "queued for review" + show res.proposalId */ break;
311
- case "denied": /* show res.reason; don't retry */ break;
378
+ import { TRPCClientError } from "@trpc/client";
379
+
380
+ try {
381
+ const res = await synap.entities.create.mutate(input);
382
+ if (res.status === "proposed") {
383
+ // tell the user "queued for review"; link res.reviewUrl / res.proposalId.
384
+ // res.entity is null — nothing exists until the proposal is approved.
385
+ } else {
386
+ // "created": res.id / res.entity are live
387
+ }
388
+ } catch (err) {
389
+ if (err instanceof TRPCClientError && err.data?.code === "FORBIDDEN") {
390
+ // denied: show err.message; don't retry
391
+ } else throw err;
312
392
  }
313
393
  ```
314
394
 
315
395
  ## Idempotency
316
396
 
317
- If you must retry a create after a network error, pass an idempotency key where
318
- the API accepts one — a `proposed` write that you retry without one will
319
- duplicate on approval. When unsure, read back before re-creating.
397
+ `entities.create` takes no idempotency key. The pod dedups some repeats on its
398
+ own — an identical agent-authored retry within a short window, and creates that
399
+ match an existing entity by identity (a stable `externalId`, or identifying
400
+ properties) — but neither covers every case. Never resend a `proposed` write,
401
+ and when unsure, read back before re-creating.
@@ -7,11 +7,16 @@ mock mode — all pre-wired. You add your screens; the plumbing is done.
7
7
 
8
8
  ## Scaffold it
9
9
 
10
+ > **Not published yet.** `@synap-core/create-synap-app` is not on npm and the
11
+ > base template is not public, so this does not work today. Until a release
12
+ > exists, build on the SDK directly (`using-the-sdk.md`). The rest of this page
13
+ > describes the template as it will ship.
14
+
10
15
  ```bash
11
- npx create-synap-app my-app
12
- cd my-app
13
- cp .env.example .env.local # fill in your pod / control-plane URLs
14
- pnpm dev # http://localhost:3010
16
+ npx @synap-core/create-synap-app my-app
17
+ cd my-app # the scaffolder seeds .env.local from env.example;
18
+ # fill in your pod / control-plane URLs
19
+ pnpm install && pnpm dev # http://localhost:3010
15
20
  ```
16
21
 
17
22
  Develop with **no pod** by setting `NEXT_PUBLIC_BASE_MOCK_MODE=true`.
@@ -59,6 +64,6 @@ components/layout/
59
64
 
60
65
  ## Auth flow (already wired)
61
66
 
62
- `/login` → Synap Cloud sign-in (email + password or magic link) → optional TOTP →
63
- pod picker → handshake → session stored → `/home`. Self-hosted pods fall back to
64
- the pod's own Kratos at `/login?mode=kratos`. You don't build this; you theme it.
67
+ `/login` → Synap Cloud sign-in → optional TOTP → pod picker → session stored →
68
+ `/home`. A pod can also be signed into directly through its own Kratos (the
69
+ login screen's manual-login step). You don't build this; you theme it.
@@ -5,38 +5,49 @@ mutates a Synap pod.
5
5
 
6
6
  ## The contract
7
7
 
8
- Every mutation (`entities.create`, `entities.update`, `relations.create`, …)
9
- resolves to one of three outcomes, decided **by the pod**, not by your app:
8
+ Every governed mutation (`entities.create`, `entities.update`, `relations.create`, …)
9
+ has one of three outcomes, decided **by the pod**, not by your app:
10
10
 
11
- | Outcome | Meaning | What your code should do |
11
+ | Outcome | How it arrives | What your code should do |
12
12
  |---|---|---|
13
- | `approved` | applied immediately | proceed |
14
- | `proposed` | queued for the pod owner's review (like a PR) | **surface the review — do NOT treat as failure, do NOT retry** |
15
- | `denied` | the principal may not do this | show why; do not retry blindly |
13
+ | applied | resolves with the procedure's own status (`"created"`, `"updated"`, …) | proceed |
14
+ | proposed | resolves with `status: "proposed"`, `proposalId`, `reviewUrl`, `message` — queued for the pod owner's review (like a PR) | **surface the review — do NOT treat as failure, do NOT retry** |
15
+ | denied | **rejects** with a tRPC `FORBIDDEN` error whose message is the reason | show why; do not retry blindly |
16
16
 
17
17
  ## Why it matters for your code
18
18
 
19
19
  - **Never treat `proposed` as an error.** It is the normal, healthy path for any
20
20
  write that governance decides a human should see first. Retrying a `proposed`
21
21
  write creates duplicates.
22
- - **Never `try/catch` a `proposed` as if it threw.** It's a successful response
23
- with a status, not an exception.
22
+ - **A `proposed` result does not throw.** It is a successful response with a
23
+ status; only a denial throws.
24
24
  - **Surface the review link/id** so the user can approve it. An unreviewed
25
25
  proposal is not yet in the graph — it won't show up in reads until approved.
26
26
 
27
27
  ## Pattern
28
28
 
29
29
  ```ts
30
- const res = await synap.entities.create.mutate(input);
31
- switch (res.status) {
32
- case "approved": /* it's live */ break;
33
- case "proposed": /* tell the user: "queued for review" + show res.proposalId */ break;
34
- case "denied": /* show res.reason; don't retry */ break;
30
+ import { TRPCClientError } from "@trpc/client";
31
+
32
+ try {
33
+ const res = await synap.entities.create.mutate(input);
34
+ if (res.status === "proposed") {
35
+ // tell the user "queued for review"; link res.reviewUrl / res.proposalId.
36
+ // res.entity is null — nothing exists until the proposal is approved.
37
+ } else {
38
+ // "created": res.id / res.entity are live
39
+ }
40
+ } catch (err) {
41
+ if (err instanceof TRPCClientError && err.data?.code === "FORBIDDEN") {
42
+ // denied: show err.message; don't retry
43
+ } else throw err;
35
44
  }
36
45
  ```
37
46
 
38
47
  ## Idempotency
39
48
 
40
- If you must retry a create after a network error, pass an idempotency key where
41
- the API accepts one — a `proposed` write that you retry without one will
42
- duplicate on approval. When unsure, read back before re-creating.
49
+ `entities.create` takes no idempotency key. The pod dedups some repeats on its
50
+ own — an identical agent-authored retry within a short window, and creates that
51
+ match an existing entity by identity (a stable `externalId`, or identifying
52
+ properties) — but neither covers every case. Never resend a `proposed` write,
53
+ and when unsure, read back before re-creating.