@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.
- package/LICENSE +21 -0
- package/README.md +25 -8
- package/dist/build-info.json +2 -2
- package/dist/commands/agents.d.ts +0 -1
- package/dist/commands/agents.js +1 -1
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/app.js +2 -1
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/automation.js +46 -41
- package/dist/commands/automation.js.map +1 -1
- package/dist/commands/box.d.ts +42 -0
- package/dist/commands/box.js +89 -0
- package/dist/commands/box.js.map +1 -0
- package/dist/commands/bridge-setup.js +3 -1
- package/dist/commands/bridge-setup.js.map +1 -1
- package/dist/commands/capability.d.ts +1 -1
- package/dist/commands/capability.js +57 -34
- package/dist/commands/capability.js.map +1 -1
- package/dist/commands/close-questions.d.ts +27 -0
- package/dist/commands/close-questions.js +49 -0
- package/dist/commands/close-questions.js.map +1 -0
- package/dist/commands/close-questions.test.d.ts +1 -0
- package/dist/commands/close-questions.test.js +52 -0
- package/dist/commands/close-questions.test.js.map +1 -0
- package/dist/commands/connect.d.ts +1 -3
- package/dist/commands/connect.js +2 -2
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/context.js +72 -43
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/data-extra.js +12 -4
- package/dist/commands/data-extra.js.map +1 -1
- package/dist/commands/data.js +2 -1
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.js +108 -13
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hooks.d.ts +19 -0
- package/dist/commands/hooks.js +70 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/import.d.ts +25 -0
- package/dist/commands/import.js +98 -12
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +59 -19
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/knowledge.js +4 -2
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/lens.js +2 -1
- package/dist/commands/lens.js.map +1 -1
- package/dist/commands/market-authoring.js +1 -1
- package/dist/commands/market-authoring.js.map +1 -1
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/observe.category.test.d.ts +1 -0
- package/dist/commands/observe.category.test.js +59 -0
- package/dist/commands/observe.category.test.js.map +1 -0
- package/dist/commands/observe.d.ts +3 -0
- package/dist/commands/observe.js +7 -5
- package/dist/commands/observe.js.map +1 -1
- package/dist/commands/observe.title.test.d.ts +1 -0
- package/dist/commands/observe.title.test.js +37 -0
- package/dist/commands/observe.title.test.js.map +1 -0
- package/dist/commands/playbook.js +9 -0
- package/dist/commands/playbook.js.map +1 -1
- package/dist/commands/pods.d.ts +6 -1
- package/dist/commands/pods.js +32 -15
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proton-setup.js +3 -1
- package/dist/commands/proton-setup.js.map +1 -1
- package/dist/commands/session-feed-wait.d.ts +48 -0
- package/dist/commands/session-feed-wait.js +261 -0
- package/dist/commands/session-feed-wait.js.map +1 -0
- package/dist/commands/session-post.d.ts +40 -0
- package/dist/commands/session-post.js +96 -0
- package/dist/commands/session-post.js.map +1 -0
- package/dist/commands/session-wait.d.ts +9 -4
- package/dist/commands/session-wait.js +20 -5
- package/dist/commands/session-wait.js.map +1 -1
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +23 -0
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/skills-commands.d.ts +2 -0
- package/dist/commands/skills-commands.js +9 -1
- package/dist/commands/skills-commands.js.map +1 -1
- package/dist/commands/skills-commands.test.d.ts +1 -0
- package/dist/commands/skills-commands.test.js +57 -0
- package/dist/commands/skills-commands.test.js.map +1 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/template.d.ts +42 -0
- package/dist/commands/template.js +191 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/tracks.d.ts +67 -6
- package/dist/commands/tracks.js +268 -16
- package/dist/commands/tracks.js.map +1 -1
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/worker-accounts.d.ts +149 -0
- package/dist/commands/worker-accounts.js +339 -0
- package/dist/commands/worker-accounts.js.map +1 -0
- package/dist/commands/worker-accounts.test.d.ts +1 -0
- package/dist/commands/worker-accounts.test.js +224 -0
- package/dist/commands/worker-accounts.test.js.map +1 -0
- package/dist/commands/worker.d.ts +219 -10
- package/dist/commands/worker.js +749 -117
- package/dist/commands/worker.js.map +1 -1
- package/dist/commands/worker.test.js +321 -10
- package/dist/commands/worker.test.js.map +1 -1
- package/dist/index.js +164 -27
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-door.d.ts +1 -3
- package/dist/lib/agent-door.js +10 -7
- package/dist/lib/agent-door.js.map +1 -1
- package/dist/lib/auth.d.ts +3 -2
- package/dist/lib/auth.js +3 -3
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/box/egress.d.ts +82 -0
- package/dist/lib/box/egress.js +264 -0
- package/dist/lib/box/egress.js.map +1 -0
- package/dist/lib/box/evidence.d.ts +86 -0
- package/dist/lib/box/evidence.js +121 -0
- package/dist/lib/box/evidence.js.map +1 -0
- package/dist/lib/box/job-server.d.ts +72 -0
- package/dist/lib/box/job-server.js +246 -0
- package/dist/lib/box/job-server.js.map +1 -0
- package/dist/lib/box/provisioner.d.ts +83 -0
- package/dist/lib/box/provisioner.js +312 -0
- package/dist/lib/box/provisioner.js.map +1 -0
- package/dist/lib/box/proxmox.d.ts +112 -0
- package/dist/lib/box/proxmox.js +211 -0
- package/dist/lib/box/proxmox.js.map +1 -0
- package/dist/lib/box/wire.d.ts +98 -0
- package/dist/lib/box/wire.js +70 -0
- package/dist/lib/box/wire.js.map +1 -0
- package/dist/lib/browser-auth.d.ts +35 -0
- package/dist/lib/browser-auth.js +170 -0
- package/dist/lib/browser-auth.js.map +1 -0
- package/dist/lib/client-header.test.d.ts +1 -0
- package/dist/lib/client-header.test.js +85 -0
- package/dist/lib/client-header.test.js.map +1 -0
- package/dist/lib/conversation-headers.d.ts +24 -9
- package/dist/lib/conversation-headers.js +40 -3
- package/dist/lib/conversation-headers.js.map +1 -1
- package/dist/lib/credential-class.d.ts +28 -1
- package/dist/lib/credential-class.js +28 -6
- package/dist/lib/credential-class.js.map +1 -1
- package/dist/lib/exporter-coverage.d.ts +1 -1
- package/dist/lib/exporter-coverage.js +1 -1
- package/dist/lib/goal-words.d.ts +38 -0
- package/dist/lib/goal-words.js +112 -0
- package/dist/lib/goal-words.js.map +1 -0
- package/dist/lib/hub-client.d.ts +28 -7
- package/dist/lib/hub-client.js +75 -34
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/init-handoff.d.ts +8 -0
- package/dist/lib/init-handoff.js +9 -2
- package/dist/lib/init-handoff.js.map +1 -1
- package/dist/lib/key-source.d.ts +3 -1
- package/dist/lib/key-source.js +7 -4
- package/dist/lib/key-source.js.map +1 -1
- package/dist/lib/knowledge-title.d.ts +3 -2
- package/dist/lib/knowledge-title.js +3 -2
- package/dist/lib/knowledge-title.js.map +1 -1
- package/dist/lib/mcp-bridge.d.ts +7 -0
- package/dist/lib/mcp-bridge.js +34 -1
- package/dist/lib/mcp-bridge.js.map +1 -1
- package/dist/lib/mcp-bridge.test.js +31 -3
- package/dist/lib/mcp-bridge.test.js.map +1 -1
- package/dist/lib/openclaw.d.ts +32 -0
- package/dist/lib/openclaw.js +165 -0
- package/dist/lib/openclaw.js.map +1 -0
- package/dist/lib/pod-moved.d.ts +70 -0
- package/dist/lib/pod-moved.js +146 -0
- package/dist/lib/pod-moved.js.map +1 -0
- package/dist/lib/pod-reach.d.ts +39 -0
- package/dist/lib/pod-reach.js +117 -0
- package/dist/lib/pod-reach.js.map +1 -0
- package/dist/lib/pod-reach.test.d.ts +1 -0
- package/dist/lib/pod-reach.test.js +67 -0
- package/dist/lib/pod-reach.test.js.map +1 -0
- package/dist/lib/pod.d.ts +8 -0
- package/dist/lib/pod.js +12 -3
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/register-claude-code-mcp.test.d.ts +1 -0
- package/dist/lib/register-claude-code-mcp.test.js +78 -0
- package/dist/lib/register-claude-code-mcp.test.js.map +1 -0
- package/dist/lib/review-credential.js +3 -1
- package/dist/lib/review-credential.js.map +1 -1
- package/dist/lib/seed.d.ts +13 -0
- package/dist/lib/seed.js +135 -0
- package/dist/lib/seed.js.map +1 -0
- package/dist/lib/session-feed.d.ts +185 -0
- package/dist/lib/session-feed.js +257 -0
- package/dist/lib/session-feed.js.map +1 -0
- package/dist/lib/targets.d.ts +21 -3
- package/dist/lib/targets.js +52 -107
- package/dist/lib/targets.js.map +1 -1
- package/dist/lib/wake-hook-settings.d.ts +57 -0
- package/dist/lib/wake-hook-settings.js +153 -0
- package/dist/lib/wake-hook-settings.js.map +1 -0
- package/dist/lib/wake-state.d.ts +65 -0
- package/dist/lib/wake-state.js +122 -0
- package/dist/lib/wake-state.js.map +1 -0
- package/package.json +5 -2
- package/skills/README.md +1 -1
- package/skills/agent-os/SKILL.md +8 -8
- package/skills/synap/SKILL.md +141 -84
- package/skills/synap/code-skills.md +70 -0
- package/skills/synap/concepts.md +28 -20
- package/skills/synap/creative-loop.md +1 -1
- package/skills/synap/document-embeds.md +3 -1
- package/skills/synap/focus-sessions.md +16 -7
- package/skills/synap/from-intent.md +32 -16
- package/skills/synap/governance-rules.md +1 -1
- package/skills/synap/lenses.md +4 -4
- package/skills/synap/linking.md +2 -2
- package/skills/synap/mental-model.md +1 -1
- package/skills/synap/operating-mode-rules.md +1 -1
- package/skills/synap/quick-reference.md +2 -1
- package/skills/synap/reflexes.md +6 -6
- package/skills/synap/viewframe-cells.md +1 -1
- package/skills/synap/work-model.md +24 -10
- package/skills/synap/workspace-design.md +5 -5
- package/skills/synap/writes.md +16 -9
- package/skills/synap-app/SKILL.md +148 -66
- package/skills/synap-app/creating-the-base-app.md +12 -7
- package/skills/synap-app/governance.md +27 -16
- package/skills/synap-app/install-commands.md +18 -9
- package/skills/synap-app/overview.md +16 -9
- package/skills/synap-app/using-the-sdk.md +75 -25
- package/skills/synap-schema/SKILL.md +21 -21
- package/skills/synap-schema/intent-vocabulary-ssot.md +21 -21
- 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
|
|
12
|
-
|
|
|
13
|
-
| **Project**
|
|
14
|
-
| **
|
|
15
|
-
| **Thing**
|
|
16
|
-
| **Session**
|
|
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
|
|
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
|
|
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
|
-
###
|
|
54
|
+
### Goals (tracks)
|
|
55
55
|
|
|
56
|
-
A
|
|
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) | **
|
|
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
|
|
29
|
-
5. **If it's a lasting goal a project pursues** ("keep the content side of the launch going") → a **
|
|
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
|
|
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`).
|
package/skills/synap/writes.md
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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}
|
|
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
|
|
220
|
-
|
|
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
|
|
45
|
-
- `@synap-core/
|
|
46
|
-
|
|
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
|
|
57
|
-
never a cookie.
|
|
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):**
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
102
|
-
npm install @synap-core/
|
|
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
|
-
#
|
|
112
|
-
synap
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
169
|
+
// result.status may be "proposed" — that is success. See governance.md.
|
|
153
170
|
```
|
|
154
171
|
|
|
155
172
|
### Auth modes (pick ONE)
|
|
156
|
-
- **`
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
`@synap-core/
|
|
165
|
-
|
|
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,
|
|
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
|
-
`
|
|
182
|
-
|
|
183
|
-
|
|
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({
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
|
201
|
-
- `@synap-core/hub-rest-client` (REST
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
224
|
-
pnpm dev
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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 |
|
|
359
|
+
| Outcome | How it arrives | What your code should do |
|
|
289
360
|
|---|---|---|
|
|
290
|
-
|
|
|
291
|
-
| `proposed`
|
|
292
|
-
|
|
|
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
|
-
- **
|
|
300
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
14
|
-
pnpm dev
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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 |
|
|
11
|
+
| Outcome | How it arrives | What your code should do |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
|
|
|
14
|
-
| `proposed`
|
|
15
|
-
|
|
|
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
|
-
- **
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|