@desplega.ai/agent-swarm 2.5.2 → 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/dist/{acp-adapter-csh0zz3b.js → acp-adapter-ek38cwsv.js} +5 -5
  2. package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
  3. package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
  4. package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
  5. package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
  6. package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
  7. package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
  8. package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
  9. package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
  10. package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
  11. package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
  12. package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
  13. package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
  14. package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
  15. package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
  16. package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
  17. package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
  18. package/dist/cli-2655k16q.js +140 -0
  19. package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
  20. package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
  21. package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
  22. package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
  23. package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
  24. package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
  25. package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
  26. package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
  27. package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
  28. package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
  29. package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
  30. package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
  31. package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
  32. package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
  33. package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
  34. package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
  35. package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
  36. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  37. package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
  38. package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
  39. package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
  40. package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
  41. package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
  42. package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
  43. package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
  44. package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
  45. package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
  46. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  47. package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
  48. package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
  49. package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
  50. package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
  51. package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
  52. package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
  53. package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
  54. package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
  55. package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
  56. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  57. package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
  58. package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
  59. package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
  60. package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
  61. package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
  62. package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
  63. package/dist/cli.js +20 -20
  64. package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
  65. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  66. package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
  67. package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
  68. package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
  69. package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
  70. package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
  71. package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
  72. package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
  73. package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
  74. package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
  75. package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
  76. package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
  77. package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
  78. package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
  79. package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
  80. package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
  81. package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
  82. package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
  83. package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
  84. package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
  85. package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
  86. package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
  87. package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
  88. package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
  89. package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
  90. package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
  91. package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
  92. package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
  93. package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
  94. package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
  95. package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
  96. package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
  97. package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
  98. package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
  99. package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
  100. package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
  101. package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
  102. package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
  103. package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
  104. package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
  105. package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
  106. package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
  107. package/openapi.json +3078 -2424
  108. package/package.json +7 -7
  109. package/plugin/commands/create-pr.md +2 -2
  110. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  111. package/src/be/approval-config-question.ts +389 -0
  112. package/src/be/automation-preflight.ts +37 -3
  113. package/src/be/db/approvals.ts +48 -0
  114. package/src/be/db/session-tree.ts +73 -15
  115. package/src/be/db/skill-search.ts +87 -0
  116. package/src/be/db/tasks/read.ts +29 -2
  117. package/src/be/db/tasks/write.ts +15 -5
  118. package/src/be/db/workflow-idempotency.ts +183 -0
  119. package/src/be/db/workflow-runs.ts +366 -8
  120. package/src/be/db.ts +92 -22
  121. package/src/be/migrations/239_skills_fts.sql +40 -0
  122. package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
  123. package/src/be/migrations/241_skills_meta_fts.sql +39 -0
  124. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  125. package/src/be/sealed-json.ts +1 -0
  126. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  127. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  128. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  129. package/src/be/sql-connections.ts +7 -1
  130. package/src/be/swarm-config-guard.ts +25 -0
  131. package/src/be/users.ts +9 -0
  132. package/src/commands/controller.ts +33 -0
  133. package/src/commands/runner.ts +6 -2
  134. package/src/extensions/apply-task-create.ts +57 -11
  135. package/src/extensions/catalog.generated.json +4 -4
  136. package/src/extensions/contract-types.generated.ts +1 -1
  137. package/src/extensions/contract.ts +25 -0
  138. package/src/extensions/dispatcher.ts +4 -0
  139. package/src/extensions/post-bridge.ts +8 -0
  140. package/src/hooks/context-bound-note.ts +196 -0
  141. package/src/hooks/hook.ts +12 -0
  142. package/src/http/approval-requests.ts +209 -38
  143. package/src/http/config.ts +70 -53
  144. package/src/http/idempotency-key.ts +117 -0
  145. package/src/http/sessions.ts +27 -2
  146. package/src/http/workflows.ts +129 -24
  147. package/src/integrations/chat-sdk/inbound.ts +5 -4
  148. package/src/prompts/context-bound-note.ts +48 -0
  149. package/src/prompts/session-templates.ts +25 -1
  150. package/src/providers/amp-adapter.ts +30 -8
  151. package/src/providers/claude-adapter.ts +29 -4
  152. package/src/providers/claude-managed-adapter.ts +26 -0
  153. package/src/providers/claude-sdk-session.ts +10 -0
  154. package/src/providers/claude-session-events.ts +13 -0
  155. package/src/providers/codex-adapter.ts +12 -0
  156. package/src/providers/dsh-adapter.ts +1 -1
  157. package/src/providers/opencode-adapter.ts +18 -0
  158. package/src/providers/pi-mono-adapter.ts +35 -0
  159. package/src/runtime-providers/docker.ts +6 -2
  160. package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
  161. package/src/tests/amp-adapter.test.ts +97 -0
  162. package/src/tests/approval-config-question.test.ts +702 -0
  163. package/src/tests/approval-requests.test.ts +10 -7
  164. package/src/tests/approval-sweeps.test.ts +12 -6
  165. package/src/tests/base-prompt.test.ts +1 -1
  166. package/src/tests/chat-sdk-inbound.test.ts +20 -0
  167. package/src/tests/claude-hook-output.test.ts +79 -1
  168. package/src/tests/claude-managed-adapter.test.ts +275 -0
  169. package/src/tests/codex-adapter.test.ts +163 -0
  170. package/src/tests/context-bound-note.test.ts +269 -0
  171. package/src/tests/dsh-adapter.test.ts +1 -1
  172. package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
  173. package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
  174. package/src/tests/extensions-pre-task.test.ts +273 -0
  175. package/src/tests/fetch-resolved-env.test.ts +17 -0
  176. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  177. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  178. package/src/tests/fixtures/fake-amp.ts +22 -8
  179. package/src/tests/fixtures/held-template-render.ts +42 -0
  180. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  181. package/src/tests/list-endpoint-slimming.test.ts +33 -0
  182. package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
  183. package/src/tests/openapi-route-headers.test.ts +2 -0
  184. package/src/tests/opencode-adapter.test.ts +134 -0
  185. package/src/tests/pi-mono-adapter.test.ts +105 -0
  186. package/src/tests/prompt-template-session.test.ts +4 -3
  187. package/src/tests/runner-push-delivery.test.ts +25 -0
  188. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  189. package/src/tests/runtime-providers-docker.test.ts +2 -0
  190. package/src/tests/script-connections.test.ts +9 -0
  191. package/src/tests/script-run-config-scope.test.ts +91 -3
  192. package/src/tests/session-logs.test.ts +70 -0
  193. package/src/tests/sessions-tree.test.ts +205 -1
  194. package/src/tests/skill-search-long-content.test.ts +193 -0
  195. package/src/tests/skill-search.test.ts +173 -22
  196. package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
  197. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  198. package/src/tests/workflow-idempotency.test.ts +915 -0
  199. package/src/tests/workflow-sub-workflow.test.ts +538 -1
  200. package/src/tools/request-human-input.ts +27 -5
  201. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  202. package/src/types.ts +7 -0
  203. package/src/utils/context-window.ts +8 -6
  204. package/src/workflows/approval-cancel.ts +15 -13
  205. package/src/workflows/approval-notifications.ts +15 -4
  206. package/src/workflows/checkpoint.ts +72 -35
  207. package/src/workflows/engine.ts +262 -93
  208. package/src/workflows/executors/base.ts +6 -0
  209. package/src/workflows/executors/foreach.ts +2 -2
  210. package/src/workflows/executors/human-in-the-loop.ts +25 -1
  211. package/src/workflows/executors/sub-workflow.ts +45 -16
  212. package/src/workflows/foreach-driver.ts +10 -3
  213. package/src/workflows/foreach-join.ts +29 -6
  214. package/src/workflows/recovery.ts +68 -1
  215. package/src/workflows/resume.ts +228 -77
  216. package/src/workflows/retry-poller.ts +74 -7
  217. package/src/workflows/task-step-routing.ts +29 -7
  218. package/templates/ai-toolbox.manifest.json +8 -8
  219. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  220. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  221. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  222. package/templates/extensions/comms-adherence-gate/README.md +38 -13
  223. package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
  224. package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
  225. package/templates/skills/agent-browser/SKILL.md +4 -4
  226. package/templates/skills/agent-browser/content.md +4 -4
  227. package/templates/skills/code-quality/config.json +1 -1
  228. package/templates/skills/code-quality/content.md +2 -2
  229. package/templates/skills/delegate-work/content.md +29 -25
  230. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  231. package/templates/skills/qa/content.md +2 -2
  232. package/templates/skills/researching/content.md +2 -0
  233. package/templates/skills/turso-interaction/content.md +1 -1
  234. package/templates/skills/work-on-task/SKILL.md +1 -1
  235. package/templates/skills/work-on-task/content.md +1 -1
  236. package/templates/skills/workflow-iterate/content.md +8 -1
@@ -5,22 +5,30 @@ import { z } from "zod";
5
5
  * comms-adherence-gate
6
6
  *
7
7
  * When an agent calls `store-progress` with status "completed" on a task that
8
- * reaches a human in Slack, judge the `output` against the requesting user's
9
- * stored comms preferences (users.metadata.$.comms) with Jev, and against what
10
- * that user actually asked for in their latest message. An explicit request
11
- * ("tell me a long joke", "explain in detail") outranks the stored default, so
12
- * a requested long answer is not a verbosity failure.
8
+ * reaches a human in Slack, or posts to Slack itself (`slack-reply`,
9
+ * `slack-post`, `slack-start-thread`, `slack-update`), judge the text with Jev:
10
+ * against the requesting user's stored comms preferences
11
+ * (users.metadata.$.comms) and what that user actually asked for in their
12
+ * latest message, and against a plain-language rubric that applies to every
13
+ * message whatever preferences exist. An explicit request ("tell me a long
14
+ * joke", "explain in detail") outranks the stored default, so a requested long
15
+ * answer is not a verbosity failure.
16
+ *
17
+ * Threads with several people: a message that @-mentions someone is judged
18
+ * against the first mentioned person with stored preferences, skipping a
19
+ * requester without preferences; otherwise against the requester. `plain` applies to everyone.
13
20
  *
14
21
  * Transport (config.provider): Jev is served by TypeSafe directly and by
15
22
  * OpenRouter's Decisions endpoint. Both take the same {model, state, questions}
16
23
  * body and return the same `answers` shape. `auto` uses TYPESAFE_API_KEY when
17
24
  * set, else OPENROUTER_API_KEY.
18
25
  *
19
- * modes (config.mode):
26
+ * modes (config.mode for the preference dimensions, config.plainMode for `plain`):
20
27
  * off -> never evaluate
21
28
  * shadow -> evaluate + record, never block (default)
22
- * enforce -> record, and block the completion up to maxBlocksPerTask times
23
- * per task with actionable feedback
29
+ * enforce -> record, and block the call up to maxBlocksPerTask times per task
30
+ * with actionable feedback. The budget is shared by both modes and
31
+ * by every gated tool.
24
32
  *
25
33
  * Fails OPEN on every error: a judge outage must never wall the swarm.
26
34
  * Every verdict lands in KV namespace `ext:comms-gate`.
@@ -28,8 +36,15 @@ import { z } from "zod";
28
36
 
29
37
  export const config = z.object({
30
38
  mode: z.enum(["off", "shadow", "enforce"]).default("shadow"),
31
- /** A dimension scoring below this fails. */
39
+ /** A preference dimension scoring below this fails. */
32
40
  threshold: z.number().min(0).max(1).default(0.5),
41
+ /** The `plain` dimension, judged on every message. Independent of `mode`. */
42
+ plainMode: z.enum(["off", "shadow", "enforce"]).default("shadow"),
43
+ /**
44
+ * `plain` scoring below this fails. Jev scores dense status text low and
45
+ * plain text high with little in between, so this sits far below `threshold`.
46
+ */
47
+ plainThreshold: z.number().min(0).max(1).default(0.25),
33
48
  timeoutMs: z.number().int().positive().default(4000),
34
49
  maxBlocksPerTask: z.number().int().min(0).default(1),
35
50
  provider: z.enum(["auto", "typesafe", "openrouter"]).default("auto"),
@@ -48,6 +63,13 @@ const NS = "ext:comms-gate";
48
63
  const TYPESAFE_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
49
64
  const OPENROUTER_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
50
65
  const UUID_RE = /^[0-9a-fA-F-]{36}$/;
66
+ /** MCP tools that post agent-authored text to Slack, all with a `message` arg. */
67
+ export const SLACK_TOOLS = new Set([
68
+ "slack-reply",
69
+ "slack-post",
70
+ "slack-start-thread",
71
+ "slack-update",
72
+ ]);
51
73
  // The dispatcher cancels a handler after 5 s; leave room for the DB read and KV writes.
52
74
  const MAX_TIMEOUT_MS = 4000;
53
75
 
@@ -96,6 +118,39 @@ const JUDGING_RULES = [
96
118
 
97
119
  type Prefs = { verbosity?: unknown; tone?: unknown; language?: unknown } & Record<string, unknown>;
98
120
 
121
+ /**
122
+ * The `plain` rubric, from the `comms` skill (Simpler and Precise modes). Jev
123
+ * separates dense from plain text reliably only with contrasting examples; a
124
+ * prose-only rubric scored a plain rewrite as low as the dense original.
125
+ */
126
+ export const PLAIN_RULES = [
127
+ "Examples of NOT plain: 'p50 Δ +120ms vs baseline (n=37)', 'error rate 2.1% → 0.4% (cohort B, ±0.3 at 95%)', 'cost $-42/day', 'first-call hit ratio 0.61', 'drift unmeasured; 2 flaky'.",
128
+ "Examples of plain: 'Pages load 120 ms slower than last week, across 37 runs', 'Errors dropped from 2 in 100 requests to 4 in 1,000', 'We spend $42 a day less', 'PR #123 keeps the migration reversible', 'Lead saves about $75 a week'.",
129
+ "Code identifiers, task ids, PR numbers, links, file paths and version numbers are fine as they are.",
130
+ ];
131
+
132
+ export const PLAIN_QUESTION = {
133
+ type: "noul",
134
+ instructions:
135
+ "Is agent_message written like the plain examples in plain_language_rules rather than like the NOT plain ones? Ignore length and tone.",
136
+ criteria: {
137
+ true: "Written like the plain examples: everyday words and numbers with units and direction.",
138
+ false:
139
+ "Written like the NOT plain examples: statistics shorthand, abbreviations, signed or cryptic numbers.",
140
+ },
141
+ };
142
+
143
+ /** Structured output (an outputSchema answer) is for machines; `plain` skips it. */
144
+ export const looksLikeJson = (text: string): boolean => {
145
+ if (!/^[[{]/.test(text)) return false;
146
+ try {
147
+ JSON.parse(text);
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ };
153
+
99
154
  /** One `noul` question per stated preference the gate knows how to judge. */
100
155
  export const buildQuestions = (prefs: Prefs): Record<string, unknown> => {
101
156
  const questions: Record<string, unknown> = {};
@@ -246,27 +301,41 @@ const SELFTEST_BAD =
246
301
  "Great question! Let me walk you through what I did here. First I looked at the task, then I reviewed the relevant files, and after careful consideration I decided to go with option B. I hope this helps - let me know if you would like me to elaborate further on any of the above!";
247
302
  const SELFTEST_GOOD =
248
303
  "Went with option B: it is the only one that keeps the migration reversible. PR #123.";
304
+ // A real dense monitor post a requester had to ask to have rewritten, and that rewrite.
305
+ const SELFTEST_PLAIN_BAD =
306
+ "Prompt cache monitor, day 7 verdict for v2.0.0: partial. Lead first-call read median 12.4k (baseline v1.173.0, n 410) → 19.1k tokens (n 2234; expected 25.0k-40.0k). First-call saving about $75/week on Lead vs v1.173.0 (expected $129; sampling noise ±$24 at 95%, one baseline version, day-to-day drift not measured); session $ change $-167/week; fleet first-call saving $-7/week.";
307
+ const SELFTEST_PLAIN_GOOD =
308
+ "Cheaper, but half what we hoped: Lead saves about $75 a week, not $129. Whole Lead tasks still cost $167 a week more, probably from other releases and heavier work, not this change.";
309
+
310
+ const below = (bad: unknown, good: unknown, threshold: number): boolean =>
311
+ typeof bad === "number" && typeof good === "number" && bad < threshold && good >= threshold;
249
312
 
250
313
  /**
251
- * Run the judge on a known-bad (positive control, must score below threshold) and a
252
- * known-good (negative control, must score at or above it) message and record both
253
- * under `selftest:last`. Proves key, egress and KV from inside the API runtime
254
- * without touching real traffic.
314
+ * Run the judge on known-bad (positive control, must score below threshold) and
315
+ * known-good (negative control, must score at or above it) messages and record
316
+ * them under `selftest:last`: one pair for verbosity, one for `plain`. Proves key,
317
+ * egress and KV from inside the API runtime without touching real traffic.
255
318
  */
256
319
  const selfTest = async (swarm: Swarm, cfg: Cfg, signal?: AbortSignal): Promise<void> => {
257
320
  const started = Date.now();
258
321
  try {
259
322
  const transport = resolveTransport(cfg);
260
- const questions = { verbosity: buildQuestions(SELFTEST_PREFS).verbosity };
261
- const judge = (message: string) =>
323
+ const judge = (message: string, dim: "verbosity" | "plain") =>
262
324
  askJev(
263
- { requester_preferences: SELFTEST_PREFS, agent_message: message },
264
- questions,
325
+ dim === "plain"
326
+ ? { plain_language_rules: PLAIN_RULES, agent_message: message }
327
+ : { requester_preferences: SELFTEST_PREFS, agent_message: message },
328
+ { [dim]: dim === "plain" ? PLAIN_QUESTION : buildQuestions(SELFTEST_PREFS).verbosity },
265
329
  transport,
266
330
  cfg,
267
331
  signal,
268
- ).then((r) => r.scores.verbosity ?? null);
269
- const [bad, good] = await Promise.all([judge(SELFTEST_BAD), judge(SELFTEST_GOOD)]);
332
+ ).then((r) => r.scores[dim] ?? null);
333
+ const [bad, good, plainBad, plainGood] = await Promise.all([
334
+ judge(SELFTEST_BAD, "verbosity"),
335
+ judge(SELFTEST_GOOD, "verbosity"),
336
+ judge(SELFTEST_PLAIN_BAD, "plain"),
337
+ judge(SELFTEST_PLAIN_GOOD, "plain"),
338
+ ]);
270
339
  await swarm.kv_set({
271
340
  namespace: NS,
272
341
  key: "selftest:last",
@@ -276,11 +345,11 @@ const selfTest = async (swarm: Swarm, cfg: Cfg, signal?: AbortSignal): Promise<v
276
345
  threshold: cfg.threshold,
277
346
  positiveControlBadScore: bad,
278
347
  negativeControlGoodScore: good,
279
- controlsOk:
280
- typeof bad === "number" &&
281
- typeof good === "number" &&
282
- bad < cfg.threshold &&
283
- good >= cfg.threshold,
348
+ controlsOk: below(bad, good, cfg.threshold),
349
+ plainThreshold: cfg.plainThreshold,
350
+ plainPositiveControlBadScore: plainBad,
351
+ plainNegativeControlGoodScore: plainGood,
352
+ plainControlsOk: below(plainBad, plainGood, cfg.plainThreshold),
284
353
  latencyMs: Date.now() - started,
285
354
  },
286
355
  });
@@ -302,13 +371,77 @@ const selfTest = async (swarm: Swarm, cfg: Cfg, signal?: AbortSignal): Promise<v
302
371
  const TASK_SQL = `SELECT t.source, t.slackThreadTs, t.agentId, t.requestedByUserId,
303
372
  json_extract(u.metadata, '$.comms') AS comms, u.name,
304
373
  substr(t.task, 1, 400) AS askHead,
305
- substr(t.task, -1400) AS askTail
374
+ substr(t.task, -1400) AS askTail,
375
+ t.creatorAgentId
306
376
  FROM agent_tasks t
307
377
  LEFT JOIN users u ON u.id = t.requestedByUserId
308
378
  WHERE t.id = ?`;
309
379
 
310
380
  const LEAD_SQL = "SELECT isLead FROM agents WHERE id = ?";
311
381
 
382
+ /** Slack user mentions in agent text: `<@U123>` or `<@U123|name>`. */
383
+ const MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^>]*)?>/g;
384
+ const MAX_MENTIONS = 5;
385
+
386
+ /** Mentioned Slack user ids in order of first appearance, capped. */
387
+ export const mentionedSlackIds = (text: string): string[] => {
388
+ const ids: string[] = [];
389
+ for (const m of text.matchAll(MENTION_RE)) {
390
+ if (!ids.includes(m[1]!)) ids.push(m[1]!);
391
+ if (ids.length >= MAX_MENTIONS) break;
392
+ }
393
+ return ids;
394
+ };
395
+
396
+ const addresseeSql = (n: number) => `SELECT x.externalId, u.id, u.name,
397
+ json_extract(u.metadata, '$.comms') AS comms
398
+ FROM user_external_ids x
399
+ JOIN users u ON u.id = x.userId
400
+ WHERE x.kind = 'slack' AND x.externalId IN (${Array(n).fill("?").join(", ")})`;
401
+
402
+ const parsePrefs = (raw: unknown): Prefs | null => {
403
+ if (!raw) return null;
404
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
405
+ return parsed && typeof parsed === "object" && Object.keys(parsed).length > 0
406
+ ? (parsed as Prefs)
407
+ : null;
408
+ };
409
+
410
+ type Audience = { id: string; name: string; prefs: Prefs; via: "mention" | "requester" };
411
+
412
+ /**
413
+ * Whose preferences a message is judged against. In a thread with several
414
+ * people, the message is for whoever it @-mentions: the first mentioned person
415
+ * with stored preferences wins, including the requester only if they have prefs.
416
+ * Without such a mention, the task requester (the
417
+ * person whose message created this task). One SQL read, only when the text
418
+ * mentions someone; no judge call.
419
+ */
420
+ const resolveAudience = async (
421
+ swarm: Swarm,
422
+ text: string,
423
+ requester: Audience,
424
+ ): Promise<Audience> => {
425
+ const ids = mentionedSlackIds(text);
426
+ if (ids.length === 0) return requester;
427
+ const q = (await swarm.db_query({ sql: addresseeSql(ids.length), params: ids })) as Envelope;
428
+ const rows = q?.data?.rows ?? q?.rows ?? [];
429
+ for (const slackId of ids) {
430
+ const row = rows.find((r) => String(cell(r, 0, "externalId")) === slackId);
431
+ if (!row) continue;
432
+ const userId = String(cell(row, 1, "id") ?? "");
433
+ // A requester with stored preferences keeps their own task context.
434
+ if (userId && userId === requester.id) {
435
+ if (Object.keys(requester.prefs).length > 0) return requester;
436
+ continue;
437
+ }
438
+ const prefs = parsePrefs(cell(row, 3, "comms"));
439
+ if (!prefs) continue;
440
+ return { id: userId, name: String(cell(row, 2, "name") ?? userId), prefs, via: "mention" };
441
+ }
442
+ return requester;
443
+ };
444
+
312
445
  /**
313
446
  * Mirrors the `task.progress.write` rule store-progress enforces
314
447
  * (src/rbac/legacy-policy.ts): the caller owns the task, the task is
@@ -327,29 +460,56 @@ const mayWriteProgress = async (
327
460
  return Number(cell(row, 0, "isLead") ?? 0) === 1;
328
461
  };
329
462
 
463
+ /** The text a gated call would put in front of a human, and the task it belongs to. */
464
+ const gatedText = (
465
+ tool: string,
466
+ args: Record<string, unknown>,
467
+ sourceTaskId: string | undefined,
468
+ ): { text: string; taskId: string } | null => {
469
+ if (tool === "store-progress") {
470
+ if (args.status !== "completed") return null;
471
+ return {
472
+ text: typeof args.output === "string" ? args.output.trim() : "",
473
+ taskId: String(args.taskId ?? sourceTaskId ?? ""),
474
+ };
475
+ }
476
+ if (SLACK_TOOLS.has(tool)) {
477
+ // slack-reply names its task; the other tools post from the caller's task context.
478
+ const named = tool === "slack-reply" ? args.taskId : undefined;
479
+ return {
480
+ text: typeof args.message === "string" ? args.message.trim() : "",
481
+ taskId: String(named ?? sourceTaskId ?? ""),
482
+ };
483
+ }
484
+ return null;
485
+ };
486
+
330
487
  const extension: SwarmExtension = (api) => {
331
488
  api.on("pre.tool.call", async (event, ctx) => {
332
- if (event.tool !== "store-progress") return;
333
- if (event.requestInfo?.callOrigin === "extension") return;
489
+ if (event.tool !== "store-progress" && !SLACK_TOOLS.has(event.tool)) return;
490
+ const origin = event.requestInfo?.callOrigin;
491
+ if (origin === "extension") return;
334
492
 
335
493
  const cfg = cfgOf(ctx.config);
336
- if (cfg.mode === "off") return;
494
+ if (cfg.mode === "off" && cfg.plainMode === "off") return;
337
495
 
338
496
  const args = (event.args ?? {}) as Record<string, unknown>;
339
497
 
340
498
  // Self-test hatch: a progress call whose text starts with the sentinel runs
341
499
  // the controls and records them under `selftest:last`.
342
- if (args.status !== "completed") {
343
- if (typeof args.progress === "string" && args.progress.startsWith(SELFTEST)) {
344
- await selfTest(ctx.swarm, cfg, ctx.signal);
345
- }
500
+ if (
501
+ event.tool === "store-progress" &&
502
+ args.status !== "completed" &&
503
+ typeof args.progress === "string" &&
504
+ args.progress.startsWith(SELFTEST)
505
+ ) {
506
+ await selfTest(ctx.swarm, cfg, ctx.signal);
346
507
  return;
347
508
  }
348
509
 
349
- const output = typeof args.output === "string" ? args.output.trim() : "";
350
- if (!output) return;
351
-
352
- const taskId = String(args.taskId ?? event.requestInfo?.sourceTaskId ?? "");
510
+ const gated = gatedText(event.tool, args, event.requestInfo?.sourceTaskId);
511
+ if (!gated?.text) return;
512
+ const { text, taskId } = gated;
353
513
  if (!UUID_RE.test(taskId)) return;
354
514
 
355
515
  try {
@@ -362,33 +522,54 @@ const extension: SwarmExtension = (api) => {
362
522
  const agentId = String(cell(row, 2, "agentId") ?? "");
363
523
  const requesterId = String(cell(row, 3, "requestedByUserId") ?? "");
364
524
  const commsRaw = cell(row, 4, "comms");
365
- const requesterName = String(cell(row, 5, "name") ?? requesterId);
525
+ const requesterName = String(cell(row, 5, "name") ?? (requesterId || "the requester"));
366
526
  const threadOpening = String(cell(row, 6, "askHead") ?? "");
367
527
  const ask = latestAsk(String(cell(row, 7, "askTail") ?? ""));
368
528
 
369
- // Judge only a completion store-progress will accept, so no agent can ship
370
- // another task's thread and preferences to the external judge.
371
- if (!(await mayWriteProgress(ctx.swarm, event.requestInfo?.agentId, agentId))) return;
372
-
373
- // Only where the text actually reaches a human in Slack.
374
- if (source !== "slack" && !slackThreadTs) return;
375
- if (!commsRaw) return; // no stated preferences -> nothing to judge against
376
- const prefs = (typeof commsRaw === "string" ? JSON.parse(commsRaw) : commsRaw) as Prefs;
377
- if (!prefs || typeof prefs !== "object") return;
378
-
379
- const questions = buildQuestions(prefs);
529
+ // Judge only a call the tool itself will accept, so no agent can ship another
530
+ // task's thread and preferences to the external judge. store-progress follows
531
+ // its write rule; the Slack tools follow slack-reply's: the caller is the
532
+ // task's assignee or its creator.
533
+ const callerId = event.requestInfo?.agentId;
534
+ const creatorAgentId = String(cell(row, 8, "creatorAgentId") ?? "");
535
+ const allowed =
536
+ event.tool === "store-progress"
537
+ ? await mayWriteProgress(ctx.swarm, callerId, agentId)
538
+ : Boolean(callerId) && (callerId === agentId || callerId === creatorAgentId);
539
+ if (!allowed) return;
540
+
541
+ // A completion reaches a human only on a Slack task; a Slack tool always does.
542
+ if (event.tool === "store-progress" && source !== "slack" && !slackThreadTs) return;
543
+
544
+ const requester: Audience = {
545
+ id: requesterId,
546
+ name: requesterName,
547
+ prefs: parsePrefs(commsRaw) ?? {},
548
+ via: "requester",
549
+ };
550
+ // `plain` alone judges no one's preferences, so it skips the lookup.
551
+ const audience =
552
+ cfg.mode === "off" ? requester : await resolveAudience(ctx.swarm, text, requester);
553
+ const { prefs, name: audienceName } = audience;
554
+
555
+ const questions: Record<string, unknown> = cfg.mode === "off" ? {} : buildQuestions(prefs);
556
+ // Only a store-progress outputSchema answer is machine JSON; a Slack post is
557
+ // always read by a human, JSON or not.
558
+ if (cfg.plainMode !== "off" && (event.tool !== "store-progress" || !looksLikeJson(text)))
559
+ questions.plain = PLAIN_QUESTION;
380
560
  if (Object.keys(questions).length === 0) return;
381
561
 
382
562
  const transport = resolveTransport(cfg);
383
563
  const { scores, latencyMs } = await askJev(
384
564
  {
385
- role: "You audit an AI agent's final message to the human who requested the work.",
386
- requester: requesterName,
565
+ role: "You audit an AI agent's message to the human who requested the work.",
566
+ requester: audienceName,
387
567
  requester_preferences: prefs,
388
568
  latest_request: ask,
389
569
  thread_opening: threadOpening,
390
570
  judging_rules: JUDGING_RULES,
391
- agent_message: output,
571
+ plain_language_rules: PLAIN_RULES,
572
+ agent_message: text,
392
573
  },
393
574
  questions,
394
575
  transport,
@@ -396,26 +577,38 @@ const extension: SwarmExtension = (api) => {
396
577
  ctx.signal,
397
578
  );
398
579
 
580
+ const limitOf = (dim: string) => (dim === "plain" ? cfg.plainThreshold : cfg.threshold);
399
581
  const failed = Object.entries(scores)
400
- .filter(([, v]) => v < cfg.threshold)
582
+ .filter(([k, v]) => v < limitOf(k))
401
583
  .map(([k]) => k);
584
+ // Script SDK calls never reach pre.tool.call today; if one does, record it only.
585
+ const blocking =
586
+ origin === "script-sdk"
587
+ ? []
588
+ : failed.filter((d) => (d === "plain" ? cfg.plainMode : cfg.mode) === "enforce");
402
589
 
403
590
  const verdict = {
404
591
  at: new Date().toISOString(),
592
+ tool: event.tool,
405
593
  taskId,
406
594
  agentId,
407
595
  source,
408
596
  requester: requesterName,
409
597
  requesterId,
598
+ judgedFor: audience.via,
599
+ audienceId: audience.id,
600
+ audience: audienceName,
410
601
  provider: transport.provider,
411
602
  scores,
412
603
  failed,
413
604
  threshold: cfg.threshold,
605
+ plainThreshold: cfg.plainThreshold,
414
606
  mode: cfg.mode,
607
+ plainMode: cfg.plainMode,
415
608
  latencyMs,
416
609
  preferences: prefs,
417
610
  askPreview: ask.slice(0, 400),
418
- outputPreview: output.slice(0, 700),
611
+ outputPreview: text.slice(0, 700),
419
612
  };
420
613
 
421
614
  await ctx.swarm.kv_incr({ namespace: NS, key: "total" });
@@ -435,7 +628,7 @@ const extension: SwarmExtension = (api) => {
435
628
  // kv_get answers {success, data:{value}}, and {success:false} when absent.
436
629
  const prior = (await ctx.swarm.kv_get({ namespace: NS, key: blocksKey })) as Envelope;
437
630
  const priorBlocks = Number(prior?.data?.value ?? prior?.value ?? 0) || 0;
438
- const willBlock = cfg.mode === "enforce" && priorBlocks < cfg.maxBlocksPerTask;
631
+ const willBlock = blocking.length > 0 && priorBlocks < cfg.maxBlocksPerTask;
439
632
 
440
633
  await ctx.swarm.kv_set({
441
634
  namespace: NS,
@@ -446,7 +639,7 @@ const extension: SwarmExtension = (api) => {
446
639
  if (!willBlock) {
447
640
  await ctx.swarm.kv_incr({
448
641
  namespace: NS,
449
- key: cfg.mode === "enforce" ? "escaped" : "shadowed",
642
+ key: blocking.length > 0 ? "escaped" : "shadowed",
450
643
  });
451
644
  return;
452
645
  }
@@ -454,20 +647,25 @@ const extension: SwarmExtension = (api) => {
454
647
  await ctx.swarm.kv_incr({ namespace: NS, key: "blocked" });
455
648
  await ctx.swarm.kv_incr({ namespace: NS, key: blocksKey });
456
649
 
457
- const detail = failed
458
- .map(
459
- (d) =>
460
- `${d} (${scores[d]!.toFixed(2)} vs ${cfg.threshold}) — requested: ${String(prefs[d] ?? "").slice(0, 220)}`,
461
- )
462
- .join("\n");
463
-
464
- return block(
465
- [
466
- `Comms gate: this output does not match ${requesterName}'s stated preferences.`,
467
- detail,
468
- "Rewrite the output to match and call store-progress again. Do not change the facts, only how they are said.",
469
- ].join("\n"),
650
+ const lines = [
651
+ blocking.includes("plain")
652
+ ? `Comms gate: this message is not in plain words for ${audienceName}.`
653
+ : `Comms gate: this output does not match ${audienceName}'s stated preferences.`,
654
+ ];
655
+ for (const d of blocking) {
656
+ const score = `${d} (${scores[d]!.toFixed(2)} vs ${limitOf(d)})`;
657
+ lines.push(
658
+ d === "plain"
659
+ ? `${score}: a reader outside the swarm cannot follow it on first read.`
660
+ : `${score}: requested ${String(prefs[d] ?? "").slice(0, 220)}`,
661
+ );
662
+ }
663
+ lines.push(
664
+ blocking.includes("plain")
665
+ ? `Rewrite the message with the \`comms\` skill (Simpler mode): common words, one idea per sentence, every number with a unit and a direction word, shorthand explained or replaced. Keep every number, name, link and hedge verbatim, then call ${event.tool} again.`
666
+ : `Rewrite the message to match and call ${event.tool} again. Do not change the facts, only how they are said.`,
470
667
  );
668
+ return block(lines.join("\n"));
471
669
  } catch (err) {
472
670
  // Fail open, always.
473
671
  try {
@@ -475,7 +673,12 @@ const extension: SwarmExtension = (api) => {
475
673
  await ctx.swarm.kv_set({
476
674
  namespace: NS,
477
675
  key: "error:last",
478
- value: { at: new Date().toISOString(), taskId, error: String(err).slice(0, 400) },
676
+ value: {
677
+ at: new Date().toISOString(),
678
+ tool: event.tool,
679
+ taskId,
680
+ error: String(err).slice(0, 400),
681
+ },
479
682
  });
480
683
  } catch {
481
684
  /* ignore */
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "../manifest.schema.json",
3
3
  "name": "comms-adherence-gate",
4
- "description": "Judges every Slack-facing store-progress completion against the requester's comms preferences and latest request with Jev (TypeSafe or OpenRouter), and in enforce blocks a non-adherent completion once per task",
5
- "version": "1.5.0",
4
+ "description": "Judges every Slack-facing store-progress completion and agent Slack post against the comms preferences of the requester, or of the person it @-mentions, their latest request and a plain-language rubric with Jev (TypeSafe or OpenRouter), and in enforce blocks a failing call once per task",
5
+ "version": "1.7.0",
6
6
  "runtime": "api",
7
7
  "assets": { "hooks": "hooks.ts" }
8
8
  }
@@ -70,18 +70,18 @@ Add `--contact-sheet` to the start command for a timestamped PNG summary of visu
70
70
 
71
71
  ## Step 3: share the screenshot through agent-fs
72
72
 
73
- Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. Use `share-create` when a person needs to open the file. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
73
+ Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. A person opens it through the live app link `${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>`. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
74
74
 
75
75
  ```bash
76
76
  agent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \
77
77
  --file /tmp/<name>.png -m "<what it shows>"
78
78
  agent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0
79
- agent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json
80
79
  ```
81
80
 
82
81
  - **GitHub PR body or comment**: upload the PNG, GIF or mp4 as a GitHub user attachment with the `github-attach` skill and embed `![caption](https://github.com/user-attachments/assets/<uuid>)`. Attachments never expire and mp4s play inline. A UI PR puts a before (main) and after pair in the `## Before / after` table. Fallback for images only, when the GitHub token is an App token (`ghu_`/`ghs_`): `agent-fs signed-url <path> --inline --json` (expires in 7 days). Never a presigned URL for an mp4 or a doc: it downloads.
83
- - **Linear comment or Slack message**: paste the `share-create` link.
84
- - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.
82
+ - **Linear comment or Slack message**: paste the live app link.
83
+ - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the live app link for people who need to open the file.
84
+ - Make an `agent-fs share-create <path> --json` link only when the requester asks for one or the reader has no agent-fs login (customer, public post, email, named outsider). Never make one in case the file gets forwarded.
85
85
  - If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.
86
86
 
87
87
  The `agent-fs` skill holds the full agent-fs recipe.
@@ -65,18 +65,18 @@ Add `--contact-sheet` to the start command for a timestamped PNG summary of visu
65
65
 
66
66
  ## Step 3: share the screenshot through agent-fs
67
67
 
68
- Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. Use `share-create` when a person needs to open the file. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
68
+ Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. A person opens it through the live app link `${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>`. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
69
69
 
70
70
  ```bash
71
71
  agent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \
72
72
  --file /tmp/<name>.png -m "<what it shows>"
73
73
  agent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0
74
- agent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json
75
74
  ```
76
75
 
77
76
  - **GitHub PR body or comment**: upload the PNG, GIF or mp4 as a GitHub user attachment with the `github-attach` skill and embed `![caption](https://github.com/user-attachments/assets/<uuid>)`. Attachments never expire and mp4s play inline. A UI PR puts a before (main) and after pair in the `## Before / after` table. Fallback for images only, when the GitHub token is an App token (`ghu_`/`ghs_`): `agent-fs signed-url <path> --inline --json` (expires in 7 days). Never a presigned URL for an mp4 or a doc: it downloads.
78
- - **Linear comment or Slack message**: paste the `share-create` link.
79
- - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.
77
+ - **Linear comment or Slack message**: paste the live app link.
78
+ - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the live app link for people who need to open the file.
79
+ - Make an `agent-fs share-create <path> --json` link only when the requester asks for one or the reader has no agent-fs login (customer, public post, email, named outsider). Never make one in case the file gets forwarded.
80
80
  - If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.
81
81
 
82
82
  The `agent-fs` skill holds the full agent-fs recipe.
@@ -5,7 +5,7 @@
5
5
  "slug": "code-quality",
6
6
  "title": "Code Quality",
7
7
  "description": "Push, PR, MR, merge, review, CI, gh, glab: the checks to run before you push or open a pull request, how to review one, the merge policy, and the GitHub review-reply provenance rule. Use before you push, open a PR or MR, or review one.",
8
- "version": "1.0.0",
8
+ "version": "1.0.1",
9
9
  "category": "skills",
10
10
  "placeholders": [],
11
11
  "runAllSeedersCandidate": true,
@@ -51,8 +51,8 @@ The Repository Guidelines carry `allowMerge` and `mergeChecks`.
51
51
 
52
52
  1. CI status first, including the fork checks below before treating CI as evidence. Failing CI is blocking. Name the failing checks.
53
53
  2. Detect a GitHub fork PR with `gh pr view <number> --json isCrossRepository --jq .isCrossRepository`.
54
- 3. For a GitHub fork PR, inspect held runs with `SHA=$(gh pr view <number> --json headRefOid --jq .headRefOid); gh api --paginate "repos/<owner>/<repo>/actions/runs?head_sha=$SHA" --jq '.workflow_runs[] | "\(.conclusion // .status)|\(.name)"'`; `gh pr checks` and `statusCheckRollup` can omit them.
55
- 4. Treat any `action_required` run or no substantive run executed on that SHA as a CI blocker. Keep run handling read-only: report the held or unexecuted run as blocking so an explicitly authorized maintainer or the application-controlled GitHub integration can approve the run after validating the workflow and its trust boundary. Do not approve on omitted runs.
54
+ 3. For a GitHub fork PR, inspect held runs with `SHA=$(gh pr view <number> --json headRefOid --jq .headRefOid); gh api --paginate "repos/<owner>/<repo>/actions/runs?head_sha=$SHA" --jq '.workflow_runs[] | "\(.conclusion // .status)|\(.path)|\(.name)"'`; `gh pr checks` and `statusCheckRollup` can omit them.
55
+ 4. Treat no substantive run executed on that SHA, or an `action_required` run without a completed `success` run with the same workflow `path` on that SHA, as a CI blocker. A name match alone never qualifies; a different or missing `path` stays blocking. When the same `path` already succeeded on that SHA, name the held run on the `Non-blocking:` line as waiting on maintainer approval. Keep run handling read-only: report blocking held or unexecuted runs so an explicitly authorized maintainer or the application-controlled GitHub integration can approve the run after validating the workflow and its trust boundary. Do not approve on omitted runs.
56
56
  5. Tests. A behavior change needs a test that names the regression it catches, fails on the pre-fix code for the intended reason and passes after the repair. Apply the test rules in the repo's `REVIEW.md`.
57
57
  6. Apply the "Review Guidance" entries from the Repository Guidelines.
58
58
  7. Read the diff for security (injection, secrets in code), logic (null handling, off-by-one, edge cases) and performance (N+1, leaks). Run the test suite and the type check locally when you can.