@synap-core/workspace-templates 0.12.0 → 0.24.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/README.md +509 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +1496 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +494 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-DqoItRiR.d.ts +1370 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26447 -24075
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +27 -4
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +105 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +138 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.yaml +85 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +779 -91
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +349 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1873 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +246 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1563 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +146 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +78 -38
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +46 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +51 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +81 -0
- package/src/retired-goal-kinds.test.ts +52 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/stripe-capability.test.ts +192 -0
- package/src/task-intents.test.ts +293 -0
- package/src/template-documents.test.ts +158 -0
- package/src/templates.ts +20907 -21464
- package/src/types.ts +475 -61
- package/src/unipile-linkedin-find-profile.test.ts +183 -0
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1416 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/views/bookmark-views-pack.view.json +39 -0
- package/views/event-views-pack.view.json +45 -0
- package/views/task-views-pack.view.json +53 -0
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- package/src/the-arch.yaml +0 -201
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiVersion": "synap/v1",
|
|
3
|
+
"kind": "capability",
|
|
4
|
+
"meta": {
|
|
5
|
+
"slug": "claude-managed-agents",
|
|
6
|
+
"name": "Claude Managed Agents",
|
|
7
|
+
"description": "Hand a Synap session's work to a Claude Managed Agent that Anthropic runs. It reports back in the session room, asks you before pushing or merging, and shows its PR. Needs an Anthropic API key and a one-time agent setup.",
|
|
8
|
+
"tags": [
|
|
9
|
+
"capability",
|
|
10
|
+
"agents",
|
|
11
|
+
"anthropic",
|
|
12
|
+
"opt-in"
|
|
13
|
+
],
|
|
14
|
+
"isPublic": true,
|
|
15
|
+
"syncByDefault": false
|
|
16
|
+
},
|
|
17
|
+
"spec": {
|
|
18
|
+
"key": "claude-managed-agents",
|
|
19
|
+
"name": "Claude Managed Agents",
|
|
20
|
+
"description": "Hands Synap work to a Claude Managed Agent (Anthropic hosts the agent loop and its sandbox) — the first agent binding. Installs ONE external-agent tool (claude_managed_agents, kind external, executor external-agent) whose config.agentBinding names four verbs, all intent delegate_agent_task: <key>_dispatch (start: POST /v1/sessions with the fenced task in initial_events), <key>_send (an answer becomes user.message; a pending tool approval becomes user.tool_confirmation only for a decision naming its event id), <key>_cancel (user.interrupt) and <key>_get_status (READ ONLY, metadata.readOnly: session status + recent events -> running | needs_input | done | failed, with the Console URL and any PR link). The agent reports back through the Synap MCP server under ITS OWN Synap key; it closes a run by ending its final message with the line 'SYNAP_STATUS: done' (a turn that ends without it reads as needs_input). ONE-TIME SETUP (agents are created once, never per run): (1) Synap agent identity: mint a dedicated agent user + key with `synap mcp url --client claude-managed-agents` (prints the pod MCP URL https://<pod>/mcp and the key once). /mcp refuses a bare user key. (2) Anthropic side (Console or `ant apply` files): create an environment {name, config:{type:'cloud', networking:{type:'limited', allow_mcp_servers:true, allow_package_managers:true, allowed_hosts:['github.com','api.github.com']}}}; create the agent {name:'Synap coding agent', model:'claude-opus-5-5', system:<how you want it to work>, mcp_servers:[{type:'url', name:'synap', url:'https://<pod>/mcp'}, {type:'url', name:'github', url:'https://api.githubcopilot.com/mcp/'}], tools:[{type:'agent_toolset_20260401', default_config:{permission_policy:{type:'auto'}}, configs:[{name:'web_search', enabled:false},{name:'web_fetch', enabled:false}]}, {type:'mcp_toolset', mcp_server_name:'synap', default_config:{permission_policy:{type:'always_allow'}}}, {type:'mcp_toolset', mcp_server_name:'github', default_config:{permission_policy:{type:'auto'}}, configs:[{name:'push_files', permission_policy:{type:'always_ask'}},{name:'create_or_update_file', permission_policy:{type:'always_ask'}},{name:'delete_file', permission_policy:{type:'always_ask'}},{name:'merge_pull_request', permission_policy:{type:'always_ask'}}]}]}. Synap governs every write the agent makes through its MCP server, so that toolset is always_allow; pushes and merges are always_ask, which pauses the session (requires_action) and surfaces as needs_input in the session room, settled only by the person's approve / reject on the approval card naming that call (a plan or deploy decision, or any reply in words, never approves a push). Keep repository WRITES on the GitHub MCP tools: give the git-mount token Contents: Read only, so a bash `git push` cannot bypass always_ask. (3) Anthropic vault: create a vault, then add credentials {display_name:'Synap', auth:{type:'static_bearer', mcp_server_url:'https://<pod>/mcp', token:<the agent key from step 1>}} and {display_name:'GitHub', auth:{type:'static_bearer', mcp_server_url:'https://api.githubcopilot.com/mcp/', token:<GitHub PAT: contents + pull requests>}}. Note its vlt_ id. (4) Install this capability on the pod with: the Anthropic API key, the agent_ id, the env_ id, the vlt_ id(s), a read-only GitHub token, optional default repos and the Console workspace id. (5) Bind: `agentUsers.setBinding({ agentUserId: <agent user from step 1>, toolId: <the claude_managed_agents tool id> })` (a person only), approve the capability, and grant that agent auto-run on the dispatch/send/cancel verbs (otherwise every hand-off is a proposal). A verb's own calls to the claude_managed_agents tool are part of that one approved act and are not gated again. REPOS: the run's repos (else the default repos) are mounted as github_repository resources at /workspace/<name>, with the read-only GitHub token redeemed from THIS capability's own vault secret (the applier hands the start verb its vault://<id> at install). Redemption is grant-gated like every secret: grant the GitHub-token secret to whoever runs the verb, or the token reads as absent and the repos are named in the kickoff instead (NOT mounted) for the agent to clone through its GitHub access. PRECONDITION: an Anthropic API key with Managed Agents access (beta managed-agents-2026-04-01).",
|
|
21
|
+
"params": [
|
|
22
|
+
{
|
|
23
|
+
"name": "anthropicApiKey",
|
|
24
|
+
"label": "Anthropic API key",
|
|
25
|
+
"type": "password",
|
|
26
|
+
"required": true,
|
|
27
|
+
"description": "An Anthropic API key with Managed Agents access, in the workspace that owns the agent and environment. Stored server-encrypted in the vault; never persisted in the template."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "managedAgentId",
|
|
31
|
+
"label": "Managed agent id",
|
|
32
|
+
"type": "text",
|
|
33
|
+
"required": true,
|
|
34
|
+
"description": "The agent_... id of the managed agent created once in setup step 2. Its mcp_servers must include this pod's /mcp URL — the dispatch verb checks it before every start."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "environmentId",
|
|
38
|
+
"label": "Environment id",
|
|
39
|
+
"type": "text",
|
|
40
|
+
"required": true,
|
|
41
|
+
"description": "The env_... id of the environment the sessions run in (setup step 2)."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "anthropicVaultIds",
|
|
45
|
+
"label": "Anthropic vault ids",
|
|
46
|
+
"type": "text",
|
|
47
|
+
"required": false,
|
|
48
|
+
"default": "",
|
|
49
|
+
"description": "Comma-separated vlt_... ids attached to every session (setup step 3: the Synap MCP bearer + GitHub credentials). Without one the agent cannot reach Synap or GitHub."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "githubToken",
|
|
53
|
+
"label": "GitHub token (read-only)",
|
|
54
|
+
"type": "password",
|
|
55
|
+
"required": true,
|
|
56
|
+
"description": "A fine-grained GitHub token with Contents: Read on the repositories, for github_repository mounts. Read-only on purpose: writes go through the GitHub MCP tools, which ask first. Stored server-encrypted in the vault."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "defaultRepos",
|
|
60
|
+
"label": "Default repositories",
|
|
61
|
+
"type": "text",
|
|
62
|
+
"required": false,
|
|
63
|
+
"default": "",
|
|
64
|
+
"description": "Comma-separated owner/repo (or https://github.com/owner/repo) used when a run names no repos."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "consoleWorkspace",
|
|
68
|
+
"label": "Anthropic Console workspace id",
|
|
69
|
+
"type": "text",
|
|
70
|
+
"required": false,
|
|
71
|
+
"default": "default",
|
|
72
|
+
"description": "The workspace id in Console session links (https://platform.claude.com/workspaces/<id>/sessions/<session>). 'default' only for the org's Default workspace."
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"vault": [
|
|
76
|
+
{
|
|
77
|
+
"ref": "anthropicApiKeySecret",
|
|
78
|
+
"name": "{{name}} Anthropic API key",
|
|
79
|
+
"value": "{{anthropicApiKey}}",
|
|
80
|
+
"type": "api_key",
|
|
81
|
+
"service": "anthropic",
|
|
82
|
+
"description": "Anthropic API key for the claude_managed_agents binding tool (sent as x-api-key)."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"ref": "githubTokenSecret",
|
|
86
|
+
"name": "{{name}} GitHub token (read-only)",
|
|
87
|
+
"value": "{{githubToken}}",
|
|
88
|
+
"type": "api_key",
|
|
89
|
+
"service": "github",
|
|
90
|
+
"description": "Read-only GitHub token for github_repository mounts on Managed Agents sessions."
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"tools": [
|
|
94
|
+
{
|
|
95
|
+
"name": "claude_managed_agents",
|
|
96
|
+
"kind": "external",
|
|
97
|
+
"executor": "external-agent",
|
|
98
|
+
"description": "The dispatch binding to Claude Managed Agents. Synap never runs the agent: its verbs create, message, interrupt and read a Managed Agents session over https://api.anthropic.com. The vaulted Anthropic key is injected as x-api-key; the credentialRef is remapped to vault://<id> at apply time.",
|
|
99
|
+
"credentialRef": "anthropicApiKeySecret",
|
|
100
|
+
"config": {
|
|
101
|
+
"baseUrl": "https://api.anthropic.com",
|
|
102
|
+
"auth": {
|
|
103
|
+
"in": "header",
|
|
104
|
+
"name": "x-api-key",
|
|
105
|
+
"prefix": ""
|
|
106
|
+
},
|
|
107
|
+
"agentBinding": {
|
|
108
|
+
"protocol": "vendor:anthropic-managed-agents",
|
|
109
|
+
"provider": "claude-managed-agents",
|
|
110
|
+
"supports": {
|
|
111
|
+
"push": false,
|
|
112
|
+
"inputRequired": true,
|
|
113
|
+
"cancel": true
|
|
114
|
+
},
|
|
115
|
+
"verbs": {
|
|
116
|
+
"start": "{{key}}_dispatch",
|
|
117
|
+
"send": "{{key}}_send",
|
|
118
|
+
"cancel": "{{key}}_cancel",
|
|
119
|
+
"status": "{{key}}_get_status"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"skills": [
|
|
126
|
+
{
|
|
127
|
+
"name": "{{key}}_dispatch",
|
|
128
|
+
"intent": "delegate_agent_task",
|
|
129
|
+
"kind": "code",
|
|
130
|
+
"scope": "pod",
|
|
131
|
+
"description": "Starts a Claude Managed Agents session for a Synap task (the binding's start verb): checks the agent lists this pod's MCP server, creates the session with the vaults, any mounted repos and the fenced task as its first message. Returns { externalId (sesn_...), url (Console) }.",
|
|
132
|
+
"requires": [
|
|
133
|
+
"claude_managed_agents"
|
|
134
|
+
],
|
|
135
|
+
"executionMode": "sync",
|
|
136
|
+
"timeoutSeconds": 60,
|
|
137
|
+
"parameters": {
|
|
138
|
+
"task": "object",
|
|
139
|
+
"sessionId": "string",
|
|
140
|
+
"pod": "object",
|
|
141
|
+
"agent": "object",
|
|
142
|
+
"repos": "array?",
|
|
143
|
+
"branch": "string?",
|
|
144
|
+
"workBranch": "string?",
|
|
145
|
+
"runId": "string?",
|
|
146
|
+
"channelId": "string?"
|
|
147
|
+
},
|
|
148
|
+
"code": "// Claude Managed Agents binding verb. Statement body: the executor wraps it as\n// (async function(args, context){ <code> }). Request shapes follow the Managed\n// Agents API (beta header managed-agents-2026-04-01): POST /v1/sessions,\n// GET /v1/sessions/{id}, GET|POST /v1/sessions/{id}/events.\n// callProvider('claude_managed_agents', ...) resolves the binding tool by NAME,\n// injects the vaulted Anthropic key as `x-api-key` (tool config.auth) and pins\n// the host to https://api.anthropic.com (tool config.baseUrl).\nconst MA_HEADERS = {\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'managed-agents-2026-04-01',\n};\nasync function ma(method, path, body) {\n const res = await callProvider('claude_managed_agents', method, path, body, { headers: MA_HEADERS });\n if (res && res.proposed) {\n throw new Error(\n 'claude-managed-agents: the Anthropic call was queued for review (proposal ' + res.proposalId + ') instead of running. ' +\n 'Approve that proposal, or check the claude_managed_agents tool is enabled, then run again.'\n );\n }\n return res && res.body !== undefined ? res.body : res;\n}\nfunction sessionIdOf(raw) {\n const id = String(raw == null ? '' : raw).trim();\n if (!/^sesn_[A-Za-z0-9_-]+$/.test(id)) {\n throw new Error('claude-managed-agents: \"' + id + '\" is not a Managed Agents session id (sesn_...)');\n }\n return id;\n}\nasync function recentEvents(id) {\n // Newest first. The last 100 events always hold the latest status_idle and the\n // events it is blocked on (an idle is emitted after them).\n const page = await ma('GET', '/v1/sessions/' + id + '/events?order=desc&limit=100');\n if (!page || !Array.isArray(page.data)) {\n throw new Error('claude-managed-agents: unreadable event list for ' + id);\n }\n return page.data;\n}\nfunction latestIdle(events) {\n return events.find((e) => e && e.type === 'session.status_idle') || null;\n}\nfunction pendingConfirmations(events) {\n const idle = latestIdle(events);\n if (!idle || !idle.stop_reason || idle.stop_reason.type !== 'requires_action') return [];\n const ids = Array.isArray(idle.stop_reason.event_ids) ? idle.stop_reason.event_ids : [];\n return ids\n .map((eid) => events.find((e) => e && e.id === eid))\n .filter((e) => e && (e.type === 'agent.tool_use' || e.type === 'agent.mcp_tool_use') && e.evaluated_permission === 'ask');\n}\n\n// ── start: create a Managed Agents session for the task ─────────────────────\n// Install-time config, interpolated by the applier. These are IDs, not secrets\n// (the Anthropic key and the GitHub token live in the pod vault). Each is\n// validated before use: a blank or malformed value fails loudly.\nconst CFG = {\n agentId: '{{managedAgentId}}',\n environmentId: '{{environmentId}}',\n vaultIds: '{{anthropicVaultIds}}',\n defaultRepos: '{{defaultRepos}}',\n consoleWorkspace: '{{consoleWorkspace}}',\n // The pod vault ref of the GitHub token, for `github_repository` mounts: the\n // applier rewrites this placeholder to this template's OWN vault://<id> at\n // install. secrets.get redeems it only under a vault grant for the caller;\n // without one it reads null and the repos are named, NOT mounted. Never a raw\n // token here.\n githubTokenRef: '{{vault:githubTokenSecret}}',\n};\nconst ID_RE = /^[A-Za-z0-9_-]+$/;\nfunction configured(name, value, prefix) {\n const v = String(value || '').trim();\n if (!v || !ID_RE.test(v) || v.indexOf(prefix) !== 0) {\n throw new Error('claude-managed-agents: ' + name + ' is not configured (expected ' + prefix + '...). Re-apply the capability with it.');\n }\n return v;\n}\nconst agentId = configured('managedAgentId', CFG.agentId, 'agent_');\nconst environmentId = configured('environmentId', CFG.environmentId, 'env_');\nconst vaultIds = String(CFG.vaultIds || '')\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n .map((v) => configured('anthropicVaultIds', v, 'vlt_'));\nconst workspace = ID_RE.test(String(CFG.consoleWorkspace || '').trim()) ? String(CFG.consoleWorkspace).trim() : 'default';\n\nconst task = args.task && typeof args.task === 'object' ? args.task : null;\nif (!task || typeof task.goal !== 'string' || !task.goal.trim()) {\n throw new Error('claude-managed-agents: task.goal is required');\n}\nconst pod = args.pod && typeof args.pod === 'object' ? args.pod : {};\nconst mcpUrl = typeof pod.mcpUrl === 'string' ? pod.mcpUrl.trim() : '';\nif (!mcpUrl) {\n throw new Error('claude-managed-agents: the pod has no public MCP URL, so the agent could not report back. Set the pod public origin first.');\n}\n\n// The agent must reach Synap: its definition has to list the pod MCP server\n// (the vault static_bearer credential is matched to it by URL).\nfunction normUrl(u) {\n const m = /^(https?):\\/\\/([^/?#]+)([^?#]*)/i.exec(String(u || '').trim());\n if (!m) return null;\n const scheme = m[1].toLowerCase();\n let host = m[2].toLowerCase();\n if ((scheme === 'https' && /:443$/.test(host)) || (scheme === 'http' && /:80$/.test(host))) host = host.replace(/:\\d+$/, '');\n return scheme + '://' + host + m[3].replace(/\\/+$/, '');\n}\nconst agent = await ma('GET', '/v1/agents/' + agentId);\nconst servers = agent && Array.isArray(agent.mcp_servers) ? agent.mcp_servers : [];\nif (!servers.some((s) => s && normUrl(s.url) === normUrl(mcpUrl))) {\n throw new Error(\n 'claude-managed-agents: managed agent ' + agentId + ' does not list the Synap MCP server ' + mcpUrl +\n ' in mcp_servers, so it could not report back. Add it (setup step 2) and run again.'\n );\n}\n\n// Repositories: the run's, else the install default.\nfunction repoOf(raw) {\n const s = String(raw || '').trim().replace(/\\.git$/, '').replace(/\\/+$/, '');\n const m = /^https:\\/\\/github\\.com\\/([\\w.-]+)\\/([\\w.-]+)$/.exec(s) || /^([\\w.-]+)\\/([\\w.-]+)$/.exec(s);\n if (!m) throw new Error('claude-managed-agents: \"' + s + '\" is not a GitHub repository (owner/repo or https://github.com/owner/repo)');\n return { url: 'https://github.com/' + m[1] + '/' + m[2], full: m[1] + '/' + m[2], name: m[2] };\n}\nconst repoInputs = Array.isArray(args.repos) && args.repos.length\n ? args.repos\n : String(CFG.defaultRepos || '').split(',').map((s) => s.trim()).filter(Boolean);\nconst repos = repoInputs.map(repoOf);\n// `branch` = a branch the run NAMES: it exists, so the repos mount on it\n// (checkout). `workBranch` = the per-session branch Synap defaulted: it does NOT\n// exist yet, so it is never a checkout — the repos mount at their default\n// branch and the kickoff tells the agent to create it and push its work there.\nfunction branchName(v) {\n if (typeof v !== 'string' || !v.trim()) return null;\n const b = v.trim();\n if (!/^[\\w./-]{1,255}$/.test(b)) throw new Error('claude-managed-agents: \"' + b + '\" is not a branch name');\n return b;\n}\nconst branch = branchName(args.branch);\nconst workBranch = branch ? null : branchName(args.workBranch);\n\nlet githubToken = null;\nif (repos.length && /^vault:\\/\\//.test(CFG.githubTokenRef) && typeof secrets !== 'undefined') {\n githubToken = await secrets.get(CFG.githubTokenRef);\n}\nconst resources = githubToken\n ? repos.map((r) => {\n const res = { type: 'github_repository', url: r.url, authorization_token: githubToken };\n if (branch) res.checkout = { type: 'branch', name: branch };\n return res;\n })\n : [];\n\n// The kickoff. task.goal / task.feedback arrive ALREADY fenced as untrusted data.\nconst lines = [\n 'Synap handed you this task. Synap session ' + String(args.sessionId || '') + (args.runId ? ', run ' + args.runId : '') + '.',\n '',\n 'Report back through the Synap MCP server configured on you (' + mcpUrl + '), as yourself:',\n args.channelId\n ? '- post progress, questions and results in the session room with post_message, channelId ' + args.channelId + ';'\n : '- post progress, questions and results in the Synap session;',\n '- when you need a person (a question, a choice), ask it there, then end your turn with a final line `SYNAP_STATUS: needs_input`. Their answer arrives here as your next message;',\n '- when the task is complete, post the result (PR link, branch, preview URL) there and end your final message with the line `SYNAP_STATUS: done`.',\n '',\n];\n// Subject name / profile / stage are a person's words and sit OUTSIDE the\n// fenced task: one line each, capped, so they cannot open a new instruction\n// line (or forge a fence marker) in the kickoff.\nfunction oneLine(v, max) {\n return String(v == null ? '' : v).replace(/[\\r\\n\\u2028\\u2029]+/g, ' ').replace(/-{3,}/g, '-').trim().slice(0, max);\n}\nif (task.currentStage) lines.push('Current stage: ' + oneLine(task.currentStage, 120));\nif (task.subject && task.subject.id) {\n lines.push('Subject: ' + oneLine(task.subject.name || task.subject.id, 200) + (task.subject.profile ? ' (' + oneLine(task.subject.profile, 80) + ')' : '') + ', Synap id ' + oneLine(task.subject.id, 80));\n}\nif (repos.length) {\n lines.push(\n resources.length\n ? 'Repositories (mounted at /workspace/<name>): ' + repos.map((r) => r.full).join(', ') + (branch ? ' on branch ' + branch : '')\n : 'Repositories (NOT mounted; use your GitHub access): ' + repos.map((r) => r.url).join(', ') + (branch ? ' (branch ' + branch + ')' : '')\n );\n if (workBranch) {\n lines.push('Work branch: create ' + workBranch + ' from the default branch, push your work to it, and open the pull request from it. Never push to the default branch.');\n }\n}\nlines.push('', 'The task:', task.goal);\nif (typeof task.feedback === 'string' && task.feedback.trim()) lines.push('', 'Reviewer feedback on an earlier attempt:', task.feedback);\n\nconst metadata = {};\nif (args.sessionId) metadata.synap_session_id = String(args.sessionId).slice(0, 512);\nif (args.runId) metadata.synap_run_id = String(args.runId).slice(0, 512);\nif (args.channelId) metadata.synap_channel_id = String(args.channelId).slice(0, 512);\nif (args.agent && args.agent.agentUserId) metadata.synap_agent_user_id = String(args.agent.agentUserId).slice(0, 512);\n\nconst subjectName = task.subject && task.subject.name ? oneLine(task.subject.name, 180) : 'task';\nconst body = {\n agent: agentId,\n environment_id: environmentId,\n title: ('Synap: ' + subjectName).slice(0, 200),\n metadata,\n initial_events: [{ type: 'user.message', content: [{ type: 'text', text: lines.join('\\n') }] }],\n};\nif (vaultIds.length) body.vault_ids = vaultIds;\nif (resources.length) body.resources = resources;\n\nconst session = await ma('POST', '/v1/sessions', body);\nif (!session || typeof session.id !== 'string') {\n throw new Error('claude-managed-agents: the session create returned no id');\n}\nreturn {\n externalId: session.id,\n url: 'https://platform.claude.com/workspaces/' + workspace + '/sessions/' + session.id,\n status: session.status || null,\n repos: repos.map((r) => r.full),\n reposMounted: resources.length > 0,\n};\n"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "{{key}}_send",
|
|
152
|
+
"intent": "delegate_agent_task",
|
|
153
|
+
"kind": "code",
|
|
154
|
+
"scope": "pod",
|
|
155
|
+
"description": "Passes a person's answer or decision to the running session (the binding's send verb). A pending tool approval is settled with user.tool_confirmation ONLY by an approved / rejected decision naming its event id (message.confirmationId); every other answer or decision goes in as user.message.",
|
|
156
|
+
"requires": [
|
|
157
|
+
"claude_managed_agents"
|
|
158
|
+
],
|
|
159
|
+
"executionMode": "sync",
|
|
160
|
+
"timeoutSeconds": 60,
|
|
161
|
+
"parameters": {
|
|
162
|
+
"sessionId": "string",
|
|
163
|
+
"externalId": "string",
|
|
164
|
+
"message": "object",
|
|
165
|
+
"runId": "string?",
|
|
166
|
+
"channelId": "string?"
|
|
167
|
+
},
|
|
168
|
+
"code": "// Claude Managed Agents binding verb. Statement body: the executor wraps it as\n// (async function(args, context){ <code> }). Request shapes follow the Managed\n// Agents API (beta header managed-agents-2026-04-01): POST /v1/sessions,\n// GET /v1/sessions/{id}, GET|POST /v1/sessions/{id}/events.\n// callProvider('claude_managed_agents', ...) resolves the binding tool by NAME,\n// injects the vaulted Anthropic key as `x-api-key` (tool config.auth) and pins\n// the host to https://api.anthropic.com (tool config.baseUrl).\nconst MA_HEADERS = {\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'managed-agents-2026-04-01',\n};\nasync function ma(method, path, body) {\n const res = await callProvider('claude_managed_agents', method, path, body, { headers: MA_HEADERS });\n if (res && res.proposed) {\n throw new Error(\n 'claude-managed-agents: the Anthropic call was queued for review (proposal ' + res.proposalId + ') instead of running. ' +\n 'Approve that proposal, or check the claude_managed_agents tool is enabled, then run again.'\n );\n }\n return res && res.body !== undefined ? res.body : res;\n}\nfunction sessionIdOf(raw) {\n const id = String(raw == null ? '' : raw).trim();\n if (!/^sesn_[A-Za-z0-9_-]+$/.test(id)) {\n throw new Error('claude-managed-agents: \"' + id + '\" is not a Managed Agents session id (sesn_...)');\n }\n return id;\n}\nasync function recentEvents(id) {\n // Newest first. The last 100 events always hold the latest status_idle and the\n // events it is blocked on (an idle is emitted after them).\n const page = await ma('GET', '/v1/sessions/' + id + '/events?order=desc&limit=100');\n if (!page || !Array.isArray(page.data)) {\n throw new Error('claude-managed-agents: unreadable event list for ' + id);\n }\n return page.data;\n}\nfunction latestIdle(events) {\n return events.find((e) => e && e.type === 'session.status_idle') || null;\n}\nfunction pendingConfirmations(events) {\n const idle = latestIdle(events);\n if (!idle || !idle.stop_reason || idle.stop_reason.type !== 'requires_action') return [];\n const ids = Array.isArray(idle.stop_reason.event_ids) ? idle.stop_reason.event_ids : [];\n return ids\n .map((eid) => events.find((e) => e && e.id === eid))\n .filter((e) => e && (e.type === 'agent.tool_use' || e.type === 'agent.mcp_tool_use') && e.evaluated_permission === 'ask');\n}\n\n// ── send: a person's answer or decision reaches the running session ─────────\n// A pending tool confirmation (always_ask: pushes / merges) is settled ONLY by\n// a decision that NAMES it (message.confirmationId — the event id the status\n// verb reported, carried on the approval card Synap showed the person). Every\n// other message — an answer, a plan / deploy decision, any free text — goes\n// in as a user.message, never as an approval: approving a plan is not\n// approving a push.\nconst id = sessionIdOf(args.externalId);\nconst message = args.message && typeof args.message === 'object' ? args.message : null;\nif (!message || (message.kind !== 'answer' && message.kind !== 'decision')) {\n throw new Error('claude-managed-agents: message.kind must be \"answer\" or \"decision\"');\n}\nconst fenced = typeof message.text === 'string' ? message.text : '';\nconst confirmationId = message.confirmationId == null ? null : String(message.confirmationId).trim();\nif (confirmationId !== null) {\n if (message.kind !== 'decision' || (message.decision !== 'approved' && message.decision !== 'rejected')) {\n throw new Error('claude-managed-agents: a tool confirmation is settled only by an approved / rejected decision');\n }\n if (!/^[A-Za-z0-9_-]{1,200}$/.test(confirmationId)) {\n throw new Error('claude-managed-agents: \"' + confirmationId + '\" is not a Managed Agents event id');\n }\n}\n\nconst session = await ma('GET', '/v1/sessions/' + id);\nif (!session || typeof session.status !== 'string') {\n throw new Error('claude-managed-agents: unreadable session ' + id);\n}\nif (session.status === 'terminated') {\n throw new Error('claude-managed-agents: session ' + id + ' has ended; start a new run to continue');\n}\n\nlet events;\nif (confirmationId !== null) {\n const pending = session.status === 'idle' ? pendingConfirmations(await recentEvents(id)) : [];\n const target = pending.find((e) => e.id === confirmationId);\n if (!target) {\n throw new Error('claude-managed-agents: tool call ' + confirmationId + ' is no longer waiting for approval in session ' + id);\n }\n events = [\n message.decision === 'approved'\n ? { type: 'user.tool_confirmation', tool_use_id: target.id, result: 'allow' }\n : { type: 'user.tool_confirmation', tool_use_id: target.id, result: 'deny', deny_message: ('A person in Synap refused this call.\\n' + fenced).slice(0, 4000) },\n ];\n} else {\n // proposalId / decision / slotKey sit OUTSIDE the fence: one line each,\n // capped, so they cannot open a new instruction line (or forge a fence\n // marker). Same sanitizer as the start verb's subject line.\n const oneLine = (v, max) => String(v == null ? '' : v).replace(/[\\r\\n\\u2028\\u2029]+/g, ' ').replace(/-{3,}/g, '-').trim().slice(0, max);\n const head = message.kind === 'decision'\n ? 'A decision from Synap' + (message.proposalId ? ' on proposal ' + oneLine(message.proposalId, 200) : '') + ': ' + oneLine(message.decision || 'noted', 50) + '.'\n : 'A person answered in the Synap session' + (message.slotKey ? ' (about: ' + oneLine(message.slotKey, 200) + ')' : '') + '.';\n events = [{ type: 'user.message', content: [{ type: 'text', text: head + '\\n' + fenced }] }];\n}\nawait ma('POST', '/v1/sessions/' + id + '/events', { events });\nreturn { externalId: id, sent: events.map((e) => e.type), confirmed: confirmationId };\n"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "{{key}}_cancel",
|
|
172
|
+
"intent": "delegate_agent_task",
|
|
173
|
+
"kind": "code",
|
|
174
|
+
"scope": "pod",
|
|
175
|
+
"description": "Stops the running session with user.interrupt (the binding's cancel verb). An already-ended session reports alreadyEnded.",
|
|
176
|
+
"requires": [
|
|
177
|
+
"claude_managed_agents"
|
|
178
|
+
],
|
|
179
|
+
"executionMode": "sync",
|
|
180
|
+
"timeoutSeconds": 30,
|
|
181
|
+
"parameters": {
|
|
182
|
+
"sessionId": "string",
|
|
183
|
+
"externalId": "string",
|
|
184
|
+
"runId": "string?"
|
|
185
|
+
},
|
|
186
|
+
"code": "// Claude Managed Agents binding verb. Statement body: the executor wraps it as\n// (async function(args, context){ <code> }). Request shapes follow the Managed\n// Agents API (beta header managed-agents-2026-04-01): POST /v1/sessions,\n// GET /v1/sessions/{id}, GET|POST /v1/sessions/{id}/events.\n// callProvider('claude_managed_agents', ...) resolves the binding tool by NAME,\n// injects the vaulted Anthropic key as `x-api-key` (tool config.auth) and pins\n// the host to https://api.anthropic.com (tool config.baseUrl).\nconst MA_HEADERS = {\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'managed-agents-2026-04-01',\n};\nasync function ma(method, path, body) {\n const res = await callProvider('claude_managed_agents', method, path, body, { headers: MA_HEADERS });\n if (res && res.proposed) {\n throw new Error(\n 'claude-managed-agents: the Anthropic call was queued for review (proposal ' + res.proposalId + ') instead of running. ' +\n 'Approve that proposal, or check the claude_managed_agents tool is enabled, then run again.'\n );\n }\n return res && res.body !== undefined ? res.body : res;\n}\nfunction sessionIdOf(raw) {\n const id = String(raw == null ? '' : raw).trim();\n if (!/^sesn_[A-Za-z0-9_-]+$/.test(id)) {\n throw new Error('claude-managed-agents: \"' + id + '\" is not a Managed Agents session id (sesn_...)');\n }\n return id;\n}\nasync function recentEvents(id) {\n // Newest first. The last 100 events always hold the latest status_idle and the\n // events it is blocked on (an idle is emitted after them).\n const page = await ma('GET', '/v1/sessions/' + id + '/events?order=desc&limit=100');\n if (!page || !Array.isArray(page.data)) {\n throw new Error('claude-managed-agents: unreadable event list for ' + id);\n }\n return page.data;\n}\nfunction latestIdle(events) {\n return events.find((e) => e && e.type === 'session.status_idle') || null;\n}\nfunction pendingConfirmations(events) {\n const idle = latestIdle(events);\n if (!idle || !idle.stop_reason || idle.stop_reason.type !== 'requires_action') return [];\n const ids = Array.isArray(idle.stop_reason.event_ids) ? idle.stop_reason.event_ids : [];\n return ids\n .map((eid) => events.find((e) => e && e.id === eid))\n .filter((e) => e && (e.type === 'agent.tool_use' || e.type === 'agent.mcp_tool_use') && e.evaluated_permission === 'ask');\n}\n\n// ── cancel: interrupt the running session (user.interrupt jumps the queue and\n// forces it idle). An already-ended session is reported, not an error.\nconst id = sessionIdOf(args.externalId);\nconst session = await ma('GET', '/v1/sessions/' + id);\nif (!session || typeof session.status !== 'string') {\n throw new Error('claude-managed-agents: unreadable session ' + id);\n}\nif (session.status === 'terminated') {\n return { externalId: id, cancelled: true, alreadyEnded: true };\n}\nawait ma('POST', '/v1/sessions/' + id + '/events', { events: [{ type: 'user.interrupt' }] });\nreturn { externalId: id, cancelled: true, alreadyEnded: false };\n"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"name": "{{key}}_get_status",
|
|
190
|
+
"intent": "delegate_agent_task",
|
|
191
|
+
"kind": "code",
|
|
192
|
+
"scope": "pod",
|
|
193
|
+
"description": "Reads the session and its recent events and normalizes them (the binding's status verb, polled while a run is active). Read-only. Returns { state: running | needs_input | done | failed, url, prUrl?, summary?, confirmationId? } — confirmationId names the ONE tool call waiting for approval.",
|
|
194
|
+
"requires": [
|
|
195
|
+
"claude_managed_agents"
|
|
196
|
+
],
|
|
197
|
+
"executionMode": "sync",
|
|
198
|
+
"timeoutSeconds": 30,
|
|
199
|
+
"parameters": {
|
|
200
|
+
"sessionId": "string",
|
|
201
|
+
"externalId": "string",
|
|
202
|
+
"runId": "string?"
|
|
203
|
+
},
|
|
204
|
+
"code": "// Claude Managed Agents binding verb. Statement body: the executor wraps it as\n// (async function(args, context){ <code> }). Request shapes follow the Managed\n// Agents API (beta header managed-agents-2026-04-01): POST /v1/sessions,\n// GET /v1/sessions/{id}, GET|POST /v1/sessions/{id}/events.\n// callProvider('claude_managed_agents', ...) resolves the binding tool by NAME,\n// injects the vaulted Anthropic key as `x-api-key` (tool config.auth) and pins\n// the host to https://api.anthropic.com (tool config.baseUrl).\nconst MA_HEADERS = {\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'managed-agents-2026-04-01',\n};\nasync function ma(method, path, body) {\n const res = await callProvider('claude_managed_agents', method, path, body, { headers: MA_HEADERS });\n if (res && res.proposed) {\n throw new Error(\n 'claude-managed-agents: the Anthropic call was queued for review (proposal ' + res.proposalId + ') instead of running. ' +\n 'Approve that proposal, or check the claude_managed_agents tool is enabled, then run again.'\n );\n }\n return res && res.body !== undefined ? res.body : res;\n}\nfunction sessionIdOf(raw) {\n const id = String(raw == null ? '' : raw).trim();\n if (!/^sesn_[A-Za-z0-9_-]+$/.test(id)) {\n throw new Error('claude-managed-agents: \"' + id + '\" is not a Managed Agents session id (sesn_...)');\n }\n return id;\n}\nasync function recentEvents(id) {\n // Newest first. The last 100 events always hold the latest status_idle and the\n // events it is blocked on (an idle is emitted after them).\n const page = await ma('GET', '/v1/sessions/' + id + '/events?order=desc&limit=100');\n if (!page || !Array.isArray(page.data)) {\n throw new Error('claude-managed-agents: unreadable event list for ' + id);\n }\n return page.data;\n}\nfunction latestIdle(events) {\n return events.find((e) => e && e.type === 'session.status_idle') || null;\n}\nfunction pendingConfirmations(events) {\n const idle = latestIdle(events);\n if (!idle || !idle.stop_reason || idle.stop_reason.type !== 'requires_action') return [];\n const ids = Array.isArray(idle.stop_reason.event_ids) ? idle.stop_reason.event_ids : [];\n return ids\n .map((eid) => events.find((e) => e && e.id === eid))\n .filter((e) => e && (e.type === 'agent.tool_use' || e.type === 'agent.mcp_tool_use') && e.evaluated_permission === 'ask');\n}\n\n// ── status: READ ONLY. Session status + its recent events → the normalized\n// { state: running|needs_input|done|failed, url, prUrl?, summary? }.\n// Only GETs; declared metadata.readOnly so a poll never becomes a proposal.\nconst WS = '{{consoleWorkspace}}'.trim();\nconst CONSOLE_WORKSPACE = /^[A-Za-z0-9_-]+$/.test(WS) ? WS : 'default';\nconst id = sessionIdOf(args.externalId);\nconst session = await ma('GET', '/v1/sessions/' + id);\nif (!session || typeof session.status !== 'string') {\n throw new Error('claude-managed-agents: unreadable session ' + id);\n}\nconst events = await recentEvents(id);\n\nconst out = { url: 'https://platform.claude.com/workspaces/' + CONSOLE_WORKSPACE + '/sessions/' + id };\nfunction textOf(e) {\n return Array.isArray(e && e.content)\n ? e.content.filter((c) => c && c.type === 'text' && typeof c.text === 'string').map((c) => c.text).join('\\n')\n : '';\n}\n// The newest PR link the agent wrote or a GitHub tool returned.\nconst PR_RE = /https:\\/\\/github\\.com\\/[\\w.-]+\\/[\\w.-]+\\/pull\\/\\d+/;\nfor (const e of events) {\n if (!e || (e.type !== 'agent.message' && e.type !== 'agent.mcp_tool_result' && e.type !== 'agent.tool_result')) continue;\n const m = PR_RE.exec(JSON.stringify(e.content == null ? '' : e.content));\n if (m) { out.prUrl = m[0]; break; }\n}\nconst lastMessage = events.find((e) => e && e.type === 'agent.message' && textOf(e).trim());\nconst lastText = lastMessage ? textOf(lastMessage) : '';\nconst STATUS_RE = /^\\s*SYNAP_STATUS:\\s*(done|needs_input)\\s*$/im;\nconst declared = (STATUS_RE.exec(lastText) || [])[1] || null;\nconst said = lastText.replace(new RegExp(STATUS_RE.source, 'gim'), '').trim().slice(0, 1500);\nfunction errorText(predicate) {\n const err = events.find((e) => e && e.type === 'session.error' && (!predicate || predicate(e)));\n return err && err.error && typeof err.error.message === 'string' ? err.error.message : null;\n}\n\nswitch (session.status) {\n case 'running':\n case 'rescheduling':\n return Object.assign({ state: 'running' }, out);\n case 'terminated': {\n // Terminated is completion OR an unrecoverable error: a terminal-retry error says which.\n const fatal = errorText((e) => e.error && e.error.retry_status && e.error.retry_status.type === 'terminal');\n return fatal\n ? Object.assign({ state: 'failed', summary: fatal }, out)\n : Object.assign({ state: 'done' }, said ? { summary: said } : {}, out);\n }\n case 'idle': {\n const idle = latestIdle(events);\n const stop = idle && idle.stop_reason ? idle.stop_reason.type : null;\n if (stop === 'requires_action') {\n // ONE confirmation at a time: its event id rides on the approval card, and\n // only a decision naming it settles it (the send verb). The next poll\n // reports the next one, if several were waiting.\n const ask = pendingConfirmations(events)[0];\n const name = ask ? (ask.mcp_server_name ? ask.mcp_server_name + '.' : '') + String(ask.name || 'a tool') : null;\n return Object.assign(\n ask\n ? { state: 'needs_input', confirmationId: ask.id, summary: 'The agent is waiting for your approval to run ' + name + '.' }\n : { state: 'needs_input', summary: 'The agent is waiting on an action before it can continue.' },\n out\n );\n }\n if (stop === 'budget_reached') {\n return Object.assign({\n state: 'needs_input',\n summary: 'The agent paused at its spend budget. Raise or remove the session budget in the Anthropic Console to continue.',\n }, out);\n }\n if (stop === 'retries_exhausted') {\n return Object.assign({ state: 'failed', summary: errorText() || 'The agent stopped after repeated errors.' }, out);\n }\n if (stop === 'refusal') {\n return Object.assign({ state: 'failed', summary: 'The model declined to continue this task.' }, out);\n }\n if (stop === 'end_turn') {\n // Only the agent's own `SYNAP_STATUS: done` line closes the run. A turn that\n // ends without it (a question, an interrupt) waits on a person.\n if (declared === 'done') return Object.assign({ state: 'done' }, said ? { summary: said } : {}, out);\n return Object.assign({\n state: 'needs_input',\n summary: said || 'The agent stopped and is waiting for you.',\n }, out);\n }\n throw new Error('claude-managed-agents: session ' + id + ' is idle for an unrecognized reason (' + String(stop) + ')');\n }\n default:\n throw new Error('claude-managed-agents: unrecognized session status \"' + session.status + '\"');\n}\n",
|
|
205
|
+
"metadata": {
|
|
206
|
+
"readOnly": true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
"provides": [
|
|
211
|
+
"delegate_agent_task"
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
"dependencies": []
|
|
215
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiVersion": "synap/v1",
|
|
3
|
+
"kind": "capability",
|
|
4
|
+
"meta": {
|
|
5
|
+
"slug": "client-comms-report",
|
|
6
|
+
"name": "Client Comms Report",
|
|
7
|
+
"description": "Posts a daily summary of each client's recent messages to that client's internal team channel, never to the client. Includes an on-demand catch-up playbook. The daily report ships turned off; enable it when ready.",
|
|
8
|
+
"tags": [
|
|
9
|
+
"capability"
|
|
10
|
+
],
|
|
11
|
+
"isPublic": true,
|
|
12
|
+
"syncByDefault": true
|
|
13
|
+
},
|
|
14
|
+
"spec": {
|
|
15
|
+
"key": "client-comms-report",
|
|
16
|
+
"name": "Client Comms Report",
|
|
17
|
+
"description": "Portable daily client-comms digest. Install on any pod to post a per-client summary of recent inbound messages to each client's internal team channel (never client-facing), with an on-demand report playbook too. Message-first and schema-free — for teams that want per-client comms readouts without the full Operations delivery workspace. For durable, queryable client-briefing records over time, use the Operations workspace's built-in daily briefing.",
|
|
18
|
+
"params": [],
|
|
19
|
+
"vault": [],
|
|
20
|
+
"tools": [],
|
|
21
|
+
"skills": [
|
|
22
|
+
{
|
|
23
|
+
"name": "client-comms-report-method",
|
|
24
|
+
"kind": "instruction",
|
|
25
|
+
"scope": "pod",
|
|
26
|
+
"description": "How to digest a client's communication channel into a concise report for the internal team channel.",
|
|
27
|
+
"code": "# Client comms report — methodology\n\nWhen digesting a client's communication channel into a report for the internal team:\n\n- Surface only REAL signal: status changes, decisions, deals/opportunities, competitor mentions, new leads or contacts, risks/blockers, and any deadline or commitment. Skip small talk and noise.\n- Resolve identity BEFORE treating anything as new — match on strong signals (email, phone, url, handle) against existing entities.\n- Distinguish the CLIENT's people from OUR team.\n- Be skimmable: lead with what matters most, group by theme, keep it tight. If nothing crossed the bar, say so in one line.\n- FIREWALL (never bend this): the report is for the INTERNAL TEAM channel only. Never post the report — or any AI message — into the client-facing comms channel. Resolve the team channel via `channel.resolve` with `branchPurpose: \"team\"`, and post there and only there."
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"provides": [],
|
|
31
|
+
"playbooks": [
|
|
32
|
+
{
|
|
33
|
+
"name": "Client Comms Report",
|
|
34
|
+
"description": "On-demand \"catch me up on this client\": read the client's bound comms channel, extract the real signal, propose any CRM updates (governed), and post a concise report to the client's TEAM channel.",
|
|
35
|
+
"subjectProfile": {
|
|
36
|
+
"profileSlug": "company"
|
|
37
|
+
},
|
|
38
|
+
"params": [
|
|
39
|
+
{
|
|
40
|
+
"name": "client",
|
|
41
|
+
"label": "Client",
|
|
42
|
+
"type": "entity",
|
|
43
|
+
"required": true,
|
|
44
|
+
"description": "The client this report runs for (the session subject)."
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"channelSpec": {
|
|
48
|
+
"type": "THREAD",
|
|
49
|
+
"aiReactionMode": "only_mentioned"
|
|
50
|
+
},
|
|
51
|
+
"executor": "is-agent",
|
|
52
|
+
"status": "active",
|
|
53
|
+
"goalTemplate": "Produce a client-comms report for the subject client, following the `client-comms-report-method` methodology.\n\n1. Resolve this client's comms channel: run_capability channel.resolve { contextObjectType: \"entity\", contextObjectId: <the subject client's id>, branchPurpose: \"client-comms\" }. If it returns no channel, stop and say \"no comms channel is bound for this client yet\".\n2. Read the recent window: run_capability feed.read { channelId: <that channel id>, limit: 40 }.\n3. Extract the real signal (status, deals, competitors, new leads, risks, deadlines). Resolve identity before creating anything.\n4. PROPOSE any CRM updates through the normal governed doors (update_entity / attach_facet / capture). A \"proposed\" result is success, not failure — never force writes.\n5. Resolve the TEAM channel: run_capability channel.resolve { contextObjectType: \"entity\", contextObjectId: <the subject client's id>, branchPurpose: \"team\" }. If it returns no team channel, stop and say so (do NOT fall back to the client channel).\n6. Post a concise report: run_capability feed.post { channelId: <the team channel id>, content: <the report> }. FIREWALL: the team channel only, never the client channel."
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"automations": [
|
|
57
|
+
{
|
|
58
|
+
"name": "Client Comms — Daily Report",
|
|
59
|
+
"description": "Each morning, for every client, read their bound comms channel, and if there's real signal post a concise digest to the client's TEAM channel. Firewall-safe (team channel only). Seeded as draft — enable it after a manual dogfood run.",
|
|
60
|
+
"triggerType": "cron",
|
|
61
|
+
"triggerConfig": {
|
|
62
|
+
"expression": "0 8 * * *"
|
|
63
|
+
},
|
|
64
|
+
"status": "draft",
|
|
65
|
+
"_note": "KEPT AS DRAFT (decision 7 — 'keep both as different jobs'). Enrichment + the daily client read rode the ONE daily comms read in the (since-removed, D5 2026-09-25) Stellar grant capability's 'Stellar Grant — Client Sync' automation (its single `assess` pass over the same clients emits progress + contextSummary + need signal). Activating THIS standalone daily cron on the same pod would be a SECOND whole-fleet read + LLM pass + team-channel post over the same clients — redundant. ⚠️ THAT DELEGATION IS NOT LIVE TODAY (recorded 2026-09-07): 'Stellar Grant — Client Sync' is ITSELF seeded `status:\"draft\"` — the Stellar grant workspace is not currently operated — so while both stay draft the daily client-comms read happens NOWHERE. This is still the right call (nobody is doing the work), but the redundancy argument above only becomes true again once Client Sync is activated; if THIS automation is ever activated, the note over there must be revisited too. So this daily automation stays draft; the capability and its on-demand 'Client Comms Report' playbook ('catch me up on this client') remain fully intact and ACTIVE for portable, message-first installs that do NOT run the Stellar grant workspace. Do NOT delete this file from CAPABILITY_FILES (prune cascade would strand installed containers) — the quieting is a status flag, not a removal.",
|
|
66
|
+
"flowDefinition": {
|
|
67
|
+
"nodes": [
|
|
68
|
+
{
|
|
69
|
+
"id": "trigger",
|
|
70
|
+
"type": "trigger",
|
|
71
|
+
"position": {
|
|
72
|
+
"x": 0,
|
|
73
|
+
"y": 0
|
|
74
|
+
},
|
|
75
|
+
"data": {
|
|
76
|
+
"triggerType": "cron",
|
|
77
|
+
"label": "Daily at 08:00",
|
|
78
|
+
"config": {
|
|
79
|
+
"expression": "0 8 * * *"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "list-clients",
|
|
85
|
+
"type": "query",
|
|
86
|
+
"position": {
|
|
87
|
+
"x": 0,
|
|
88
|
+
"y": 120
|
|
89
|
+
},
|
|
90
|
+
"data": {
|
|
91
|
+
"label": "List client companies",
|
|
92
|
+
"profileSlug": "company",
|
|
93
|
+
"filter": "",
|
|
94
|
+
"limit": 100,
|
|
95
|
+
"_filterNote": "v1 loops ALL companies; only those with a bound client-comms channel AND real signal produce a post (the read + hasSignal switch make it self-limiting). To restrict up-front, set `filter` to a JSON object of property equalities."
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "client-loop",
|
|
100
|
+
"type": "loop",
|
|
101
|
+
"position": {
|
|
102
|
+
"x": 0,
|
|
103
|
+
"y": 240
|
|
104
|
+
},
|
|
105
|
+
"data": {
|
|
106
|
+
"label": "For each client",
|
|
107
|
+
"iteratorExpression": "steps.list-clients.output.entities",
|
|
108
|
+
"itemVariable": "client"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "read-comms",
|
|
113
|
+
"type": "messages_query",
|
|
114
|
+
"position": {
|
|
115
|
+
"x": 0,
|
|
116
|
+
"y": 360
|
|
117
|
+
},
|
|
118
|
+
"data": {
|
|
119
|
+
"label": "Read the client's comms channel",
|
|
120
|
+
"subjectEntityId": "{{loop.item.id}}",
|
|
121
|
+
"limit": 40,
|
|
122
|
+
"errorHandling": {
|
|
123
|
+
"continueOnError": true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "extract",
|
|
129
|
+
"type": "command",
|
|
130
|
+
"position": {
|
|
131
|
+
"x": 0,
|
|
132
|
+
"y": 480
|
|
133
|
+
},
|
|
134
|
+
"data": {
|
|
135
|
+
"label": "Extract the real signal → report",
|
|
136
|
+
"commandTitle": "Digest a client's recent comms into a team report",
|
|
137
|
+
"inputMapping": {
|
|
138
|
+
"messages": "{{steps.read-comms.output.messages}}",
|
|
139
|
+
"clientName": "{{loop.item.title}}"
|
|
140
|
+
},
|
|
141
|
+
"promptOverride": "You are digesting a client's recent communications into a concise report for the INTERNAL team channel, following the client-comms-report methodology. The client is {{loop.item.title}}. Their recent messages are provided as input (chronological).\n\nExtract ONLY real signal: status changes, decisions, deals/opportunities, competitor mentions, new leads/contacts, risks/blockers, deadlines/commitments. Distinguish the client's people from our team. Be skimmable — lead with what matters, group by theme, omit noise.\n\nRespond with STRICT JSON and nothing else: { \"hasSignal\": \"true\" or \"false\" (a STRING, not a boolean), \"report\": \"<a short markdown report for the team when hasSignal is 'true'; empty string when 'false'>\" }. Set hasSignal to \"false\" when there is no notable activity (e.g. no messages, or only chatter).",
|
|
142
|
+
"errorHandling": {
|
|
143
|
+
"continueOnError": true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "gate",
|
|
149
|
+
"type": "condition",
|
|
150
|
+
"position": {
|
|
151
|
+
"x": 0,
|
|
152
|
+
"y": 600
|
|
153
|
+
},
|
|
154
|
+
"data": {
|
|
155
|
+
"label": "Only post when there's real signal",
|
|
156
|
+
"expression": "{{steps.extract.output.hasSignal}}"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "resolve-team",
|
|
161
|
+
"type": "capability",
|
|
162
|
+
"position": {
|
|
163
|
+
"x": 0,
|
|
164
|
+
"y": 720
|
|
165
|
+
},
|
|
166
|
+
"data": {
|
|
167
|
+
"label": "Resolve the client's TEAM channel (FIREWALL: never the client channel)",
|
|
168
|
+
"verbId": "channel.resolve",
|
|
169
|
+
"inputMapping": {
|
|
170
|
+
"contextObjectType": "entity",
|
|
171
|
+
"contextObjectId": "{{loop.item.id}}",
|
|
172
|
+
"branchPurpose": "team"
|
|
173
|
+
},
|
|
174
|
+
"errorHandling": {
|
|
175
|
+
"continueOnError": true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"id": "post-report",
|
|
181
|
+
"type": "output",
|
|
182
|
+
"position": {
|
|
183
|
+
"x": 0,
|
|
184
|
+
"y": 840
|
|
185
|
+
},
|
|
186
|
+
"data": {
|
|
187
|
+
"label": "Post the report to the TEAM channel (FIREWALL: never the client channel)",
|
|
188
|
+
"outputType": "channel_message",
|
|
189
|
+
"config": {
|
|
190
|
+
"channelId": "{{steps.resolve-team.output.channelId}}",
|
|
191
|
+
"content": "{{steps.extract.output.report}}",
|
|
192
|
+
"metadata": {
|
|
193
|
+
"automationMessage": true,
|
|
194
|
+
"kind": "client-comms-report"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"errorHandling": {
|
|
198
|
+
"continueOnError": true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
"edges": [
|
|
204
|
+
{
|
|
205
|
+
"id": "e1",
|
|
206
|
+
"source": "trigger",
|
|
207
|
+
"target": "list-clients"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "e2",
|
|
211
|
+
"source": "list-clients",
|
|
212
|
+
"target": "client-loop"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"id": "e-loop",
|
|
216
|
+
"source": "client-loop",
|
|
217
|
+
"target": "read-comms"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": "e3",
|
|
221
|
+
"source": "read-comms",
|
|
222
|
+
"target": "extract"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"id": "e4",
|
|
226
|
+
"source": "extract",
|
|
227
|
+
"target": "gate"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"id": "e5",
|
|
231
|
+
"source": "gate",
|
|
232
|
+
"target": "resolve-team",
|
|
233
|
+
"sourceHandle": "yes"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"id": "e6",
|
|
237
|
+
"source": "resolve-team",
|
|
238
|
+
"target": "post-report"
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
},
|
|
245
|
+
"dependencies": []
|
|
246
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiVersion": "synap/v1",
|
|
3
|
+
"kind": "capability",
|
|
4
|
+
"meta": {
|
|
5
|
+
"slug": "content-idea-to-production",
|
|
6
|
+
"name": "Content Idea → Production",
|
|
7
|
+
"description": "Retired: this capability no longer adds anything. Produce Content and Plan Next Content now ship with the Content OS workspace template; install Content OS to get them. Spaces that already have these playbooks keep them.",
|
|
8
|
+
"tags": [
|
|
9
|
+
"capability",
|
|
10
|
+
"content",
|
|
11
|
+
"proactive"
|
|
12
|
+
],
|
|
13
|
+
"isPublic": true,
|
|
14
|
+
"syncByDefault": false
|
|
15
|
+
},
|
|
16
|
+
"spec": {
|
|
17
|
+
"key": "content-idea-to-production",
|
|
18
|
+
"name": "Content Idea → Production",
|
|
19
|
+
"description": "Retired: this capability no longer adds anything. Produce Content and Plan Next Content ship with the Content OS workspace template; install Content OS to get them. Spaces that already have these playbooks keep them.",
|
|
20
|
+
"params": [],
|
|
21
|
+
"vault": [],
|
|
22
|
+
"tools": [],
|
|
23
|
+
"skills": [],
|
|
24
|
+
"provides": [],
|
|
25
|
+
"playbooks": [],
|
|
26
|
+
"automations": []
|
|
27
|
+
},
|
|
28
|
+
"dependencies": []
|
|
29
|
+
}
|