crewly 1.20.250 → 1.20.252

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 (176) hide show
  1. package/config/skills/agent/core/app-comments/SKILL.md +11 -1
  2. package/config/skills/agent/core/app-data/SKILL.md +65 -3
  3. package/config/skills/agent/core/app-data/execute.sh +51 -3
  4. package/config/skills/agent/core/app-data/execute.test.sh +42 -0
  5. package/config/skills/agent/core/complete-task/SKILL.md +5 -1
  6. package/config/skills/agent/core/complete-task/execute.sh +30 -15
  7. package/config/skills/agent/core/complete-task/execute.test.sh +45 -1
  8. package/config/skills/agent/core/publish-app/SKILL.md +5 -1
  9. package/config/skills/agent/core/publish-app/execute.sh +26 -3
  10. package/config/skills/agent/core/publish-app/execute.test.sh +18 -0
  11. package/dist/backend/backend/src/constants.d.ts +66 -0
  12. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  13. package/dist/backend/backend/src/constants.js +66 -0
  14. package/dist/backend/backend/src/constants.js.map +1 -1
  15. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +17 -0
  16. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  17. package/dist/backend/backend/src/controllers/apps/apps.controller.js +31 -0
  18. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  19. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +7 -0
  20. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  21. package/dist/backend/backend/src/controllers/apps/apps.routes.js +29 -2
  22. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  23. package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
  24. package/dist/backend/backend/src/controllers/team/team.controller.js +43 -4
  25. package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
  26. package/dist/backend/backend/src/index.d.ts.map +1 -1
  27. package/dist/backend/backend/src/index.js +13 -0
  28. package/dist/backend/backend/src/index.js.map +1 -1
  29. package/dist/backend/backend/src/services/agent/agent-registration.service.js +2 -2
  30. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  31. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +13 -0
  32. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  33. package/dist/backend/backend/src/services/agent/resource-mode.service.js +36 -0
  34. package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
  35. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  36. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  37. package/dist/backend/backend/src/services/apps/app-collaborators.service.d.ts +103 -0
  38. package/dist/backend/backend/src/services/apps/app-collaborators.service.d.ts.map +1 -0
  39. package/dist/backend/backend/src/services/apps/app-collaborators.service.js +161 -0
  40. package/dist/backend/backend/src/services/apps/app-collaborators.service.js.map +1 -0
  41. package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts +149 -0
  42. package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts.map +1 -0
  43. package/dist/backend/backend/src/services/apps/app-comment-room.service.js +316 -0
  44. package/dist/backend/backend/src/services/apps/app-comment-room.service.js.map +1 -0
  45. package/dist/backend/backend/src/services/apps/app-comments-slack.service.d.ts +38 -1
  46. package/dist/backend/backend/src/services/apps/app-comments-slack.service.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/apps/app-comments-slack.service.js +58 -3
  48. package/dist/backend/backend/src/services/apps/app-comments-slack.service.js.map +1 -1
  49. package/dist/backend/backend/src/services/apps/app-roster.service.d.ts +37 -1
  50. package/dist/backend/backend/src/services/apps/app-roster.service.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/apps/app-roster.service.js +62 -7
  52. package/dist/backend/backend/src/services/apps/app-roster.service.js.map +1 -1
  53. package/dist/backend/backend/src/services/apps/app-wake-message.d.ts +6 -0
  54. package/dist/backend/backend/src/services/apps/app-wake-message.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/apps/app-wake-message.js +12 -10
  56. package/dist/backend/backend/src/services/apps/app-wake-message.js.map +1 -1
  57. package/dist/backend/backend/src/services/apps/app-wake.service.d.ts +30 -0
  58. package/dist/backend/backend/src/services/apps/app-wake.service.d.ts.map +1 -1
  59. package/dist/backend/backend/src/services/apps/app-wake.service.js +90 -16
  60. package/dist/backend/backend/src/services/apps/app-wake.service.js.map +1 -1
  61. package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts +6 -0
  62. package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts.map +1 -1
  63. package/dist/backend/backend/src/services/apps/apps-cloud.client.js +5 -4
  64. package/dist/backend/backend/src/services/apps/apps-cloud.client.js.map +1 -1
  65. package/dist/backend/backend/src/services/apps/apps.service.d.ts +115 -2
  66. package/dist/backend/backend/src/services/apps/apps.service.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/apps/apps.service.js +181 -9
  68. package/dist/backend/backend/src/services/apps/apps.service.js.map +1 -1
  69. package/dist/backend/backend/src/services/apps/apps.wiring.d.ts +57 -1
  70. package/dist/backend/backend/src/services/apps/apps.wiring.d.ts.map +1 -1
  71. package/dist/backend/backend/src/services/apps/apps.wiring.js +233 -4
  72. package/dist/backend/backend/src/services/apps/apps.wiring.js.map +1 -1
  73. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +1 -1
  74. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +3 -0
  76. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  77. package/dist/backend/backend/src/services/decisions/decision.service.d.ts +2 -0
  78. package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/decisions/decision.service.js +1 -0
  80. package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
  81. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +24 -1
  82. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +44 -1
  84. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.d.ts +48 -0
  86. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js +46 -0
  88. package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -1
  89. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts +43 -1
  90. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js +34 -0
  92. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-decision.js.map +1 -1
  93. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.d.ts.map +1 -1
  94. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.js +1 -0
  95. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-stats.js.map +1 -1
  96. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
  97. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +23 -7
  98. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
  99. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +5 -0
  100. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  101. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  102. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +18 -2
  103. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  104. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +67 -8
  105. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  106. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +117 -3
  107. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  108. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +304 -19
  109. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  110. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.d.ts +66 -0
  111. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.d.ts.map +1 -0
  112. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.js +107 -0
  113. package/dist/backend/backend/src/services/reconciler/stalled-agent-recovery.js.map +1 -0
  114. package/dist/backend/backend/src/services/trace/trace-metrics.d.ts.map +1 -1
  115. package/dist/backend/backend/src/services/trace/trace-metrics.js +2 -1
  116. package/dist/backend/backend/src/services/trace/trace-metrics.js.map +1 -1
  117. package/dist/backend/backend/src/services/trace/trace.types.d.ts +1 -1
  118. package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -1
  119. package/dist/backend/backend/src/services/trace/trace.types.js +2 -0
  120. package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -1
  121. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts +15 -0
  122. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
  123. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +47 -0
  124. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
  125. package/dist/backend/backend/src/types/decision.types.d.ts +27 -2
  126. package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -1
  127. package/dist/backend/backend/src/types/v2/index.d.ts +1 -1
  128. package/dist/backend/backend/src/types/v2/index.d.ts.map +1 -1
  129. package/dist/backend/backend/src/types/v2/index.js.map +1 -1
  130. package/dist/backend/backend/src/types/v2/reconcile.types.d.ts +20 -0
  131. package/dist/backend/backend/src/types/v2/reconcile.types.d.ts.map +1 -1
  132. package/dist/backend/backend/src/types/v2/reconcile.types.js.map +1 -1
  133. package/dist/backend/build-info.json +2 -2
  134. package/dist/cli/backend/src/constants.d.ts +66 -0
  135. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  136. package/dist/cli/backend/src/constants.js +66 -0
  137. package/dist/cli/backend/src/constants.js.map +1 -1
  138. package/dist/cli/backend/src/services/agent/agent-registration.service.js +2 -2
  139. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  140. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +13 -0
  141. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  142. package/dist/cli/backend/src/services/agent/resource-mode.service.js +36 -0
  143. package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
  144. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  145. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  146. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +1 -1
  147. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  148. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +3 -0
  149. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  150. package/dist/cli/backend/src/services/decisions/decision.service.d.ts +2 -0
  151. package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  152. package/dist/cli/backend/src/services/decisions/decision.service.js +1 -0
  153. package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
  154. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +24 -1
  155. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  156. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +44 -1
  157. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  158. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts +48 -0
  159. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -1
  160. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js +46 -0
  161. package/dist/cli/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -1
  162. package/dist/cli/backend/src/services/trace/trace.types.d.ts +1 -1
  163. package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -1
  164. package/dist/cli/backend/src/services/trace/trace.types.js +2 -0
  165. package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -1
  166. package/dist/cli/backend/src/types/decision.types.d.ts +27 -2
  167. package/dist/cli/backend/src/types/decision.types.d.ts.map +1 -1
  168. package/dist/cli/backend/src/types/v2/index.d.ts +1 -1
  169. package/dist/cli/backend/src/types/v2/index.d.ts.map +1 -1
  170. package/dist/cli/backend/src/types/v2/index.js.map +1 -1
  171. package/dist/cli/backend/src/types/v2/reconcile.types.d.ts +20 -0
  172. package/dist/cli/backend/src/types/v2/reconcile.types.d.ts.map +1 -1
  173. package/dist/cli/backend/src/types/v2/reconcile.types.js.map +1 -1
  174. package/frontend/dist/assets/{index-2db17f1e.js → index-83188f0b.js} +2 -2
  175. package/frontend/dist/index.html +1 -1
  176. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: App Comments
3
3
  description: The owner's comments on a Crewly App — each one points at an element they tapped in the app (data-crewly-id, CSS selector, text, outerHTML, position). List them, reply in the thread, resolve after you addressed one, reopen. Use it when an [APP CHANGES] message says the owner commented on your app or @mentioned you in a comment.
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  category: productivity
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -64,6 +64,16 @@ Output (`--list`):
64
64
  "comment":"Make this green","at":"…","replies":[],"resolvedBy":null}]}
65
65
  ```
66
66
 
67
+ ## Apps owned by a team or a channel
68
+
69
+ If the app's owner is a team or a Crewly channel (`app-data --owner`), each
70
+ comment arrives as a thread in that room (and its Slack channel), not as an
71
+ `[APP CHANGES]` message: "💬 Comment on Daily brief (#3) — Heading “Today”: …".
72
+ The room's rules decide who answers. Reply **in that thread** (as for any room
73
+ message): your reply is added to the comment in the app, so do not also run
74
+ `--reply`. Use `--resolve <id>` when it is done; that shows in the thread too.
75
+ The owner's replies in the thread (in Slack or in the app) reach the room once.
76
+
67
77
  ## Finding the element
68
78
 
69
79
  `on` is what the app's page reported for the tapped element:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: App Data
3
3
  description: Read and write the data of a Crewly App you published (the same collections the app's page sees through crewly.db) — list, get, set, update, add, delete. Use it to fill an app with content, act on what the owner entered, or answer an [APP CHANGES] message.
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  category: productivity
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -44,6 +44,11 @@ bash execute.sh --app 28au74d9cj --set meta settings --data '{"title":"Weekly sh
44
44
  bash execute.sh --app 28au74d9cj --update items 1kx2-ab --data '{"done":true}' --if-rev 3
45
45
  bash execute.sh --app 28au74d9cj --add items --data '{"name":"eggs","done":false}'
46
46
  bash execute.sh --app 28au74d9cj --delete items 1kx2-ab
47
+ bash execute.sh --app 28au74d9cj --request-access --reason "write the morning briefing" # ask the owner, see "Collaborators"
48
+ bash execute.sh --app 28au74d9cj --collaborators # who the owner let in
49
+ bash execute.sh --app 28au74d9cj --owner # who the app's comments go to
50
+ bash execute.sh --app 28au74d9cj --set-owner 'channel:#daily-brief' # see "Owner"
51
+ bash execute.sh --app 28au74d9cj --add-collaborator Kai # owner agents only
47
52
  ```
48
53
 
49
54
  Output:
@@ -53,8 +58,9 @@ Output:
53
58
  {"success":true,"id":"settings","data":{"title":"Weekly shop"},"rev":1,"updatedAt":"…"}
54
59
  ```
55
60
 
56
- - You can use the data of apps you published or a teammate published
57
- (`not_your_app` otherwise).
61
+ - You can use the data of apps you published, that a teammate published, or
62
+ that the owner added you (or your team) to as a collaborator (`not_your_app`
63
+ otherwise).
58
64
  - `--data-file <path>` reads the JSON from a regular file inside your project
59
65
  directory (not a symlink, not under `~/.crewly`, at most 1 MB).
60
66
  - `--list` pages with `--after <next>` until `next` is null.
@@ -64,6 +70,62 @@ Output:
64
70
  - The app's page sees your writes at once (its subscriptions fire). Your
65
71
  writes never wake you; the owner's do (`[APP CHANGES]`).
66
72
 
73
+ ## Collaborators: working in an app another team published
74
+
75
+ If the app is another team's, you get `not_your_app`. Do not publish a copy.
76
+ Ask the owner:
77
+
78
+ ```bash
79
+ bash execute.sh --app <appId> --request-access --reason "what you need to write"
80
+ ```
81
+
82
+ - The owner gets one card, **Allow / Do not allow**. Only their tap grants
83
+ anything; you cannot add yourself, and nothing you send changes who is added
84
+ (it is your own team, or only you with `--scope agent`).
85
+ - You are told when they answer. With **Allow** your team (and teammates who join
86
+ later) can use that app's **data and comments** with this skill, using the app
87
+ id. You get the app id from the owner or the publisher; collaborators do not
88
+ show up in `publish-app`'s list.
89
+ - It is **data only**: you cannot republish, roll back, transfer or change who
90
+ can open the app (`publish-app --app <id>` is refused). To change the page
91
+ itself, ask the publisher.
92
+ - The owner can take the access away at any time; your next call then returns
93
+ `not_your_app`.
94
+ - `--list` shows who wrote each document: documents written by an agent carry
95
+ `by` (its session), e.g. `crewly-marketing-ella-…`.
96
+
97
+ ## Owner: who the app's comments go to
98
+
99
+ Every app has an **owner**: one agent, a team, or a Crewly channel (a
100
+ cross-team room, see `list-channels`). By default it is the agent that
101
+ published it. The owner's comments in the app go to the owner:
102
+
103
+ - agent → that agent, mirrored to its Slack DM with the owner (as before);
104
+ - team → the team's room (its Slack team channel); the room's rules decide
105
+ who answers (an @mention in the comment → that agent; otherwise whoever is
106
+ awake reads it, and the lead when nobody is);
107
+ - channel → that channel, so its members from every team see the owner's
108
+ comments and any of them can pick one up.
109
+
110
+ ```bash
111
+ bash execute.sh --app <appId> --owner # {"owner":{"kind":"agent","name":"Ella","default":true}}
112
+ bash execute.sh --app <appId> --set-owner 'channel:#daily-brief' # or 'team:Dev', 'agent:Kai', default
113
+ bash execute.sh --app <appId> --add-collaborator Kai # let another agent of this machine use the data
114
+ ```
115
+
116
+ - Only the app's **owner agents** may change the owner or add a collaborator:
117
+ the owning agent, or a member of the owning team / channel (by default the
118
+ publisher). Anyone else gets `forbidden`; ask the owner, or use
119
+ `--request-access` for yourself.
120
+ - Members of the owning team or channel can use the app's data and comments
121
+ without being added. A collaborator gets data and comments only, never
122
+ publishing (that stays with the publisher's team).
123
+ - In a room, answer in the comment's thread: your reply there is added to the
124
+ comment in the app (do not also use `app-comments --reply`). Resolve with
125
+ `app-comments --resolve <id>` when it is done.
126
+ - The channel or team must exist on this machine (a channel made a moment ago
127
+ is fine).
128
+
67
129
  ## What you read is a sanitised display copy
68
130
 
69
131
  Every string you read here — values **and** object keys, at any depth — is
@@ -19,6 +19,11 @@ Usage:
19
19
  bash execute.sh --app <appId> --update <collection> <docId> --data '{"done":true}' [--if-rev 4]
20
20
  bash execute.sh --app <appId> --add <collection> --data '{"name":"milk"}'
21
21
  bash execute.sh --app <appId> --delete <collection> <docId>
22
+ bash execute.sh --app <appId> --request-access [--scope team|agent] [--reason "why"]
23
+ bash execute.sh --app <appId> --collaborators
24
+ bash execute.sh --app <appId> --owner
25
+ bash execute.sh --app <appId> --set-owner 'channel:#daily-brief' # or 'team:<name>', 'agent:<name>', default
26
+ bash execute.sh --app <appId> --add-collaborator <agent name or session>
22
27
 
23
28
  Options:
24
29
  --app App id (from publish-app)
@@ -26,6 +31,13 @@ Options:
26
31
  --if-rev Only update when the doc is still at this rev (409 conflict otherwise)
27
32
  --limit Page size for --list (1-500, default 100)
28
33
  --after Continue a --list after this doc id (the previous page's "next")
34
+ --request-access Ask the owner to let your team (default) or only you work in an app another team published; a card goes to the owner, nothing is granted until they tap Allow
35
+ --collaborators Who the owner let work in the app
36
+ --scope team (your team, default) or agent (only you), with --request-access
37
+ --reason One line for the owner, with --request-access
38
+ --owner Show who owns the app's comments (an agent, a team or a channel)
39
+ --set-owner Change it: channel:#<name>, team:<name>, agent:<name> or default (the publishing agent). Only the app's owner agents (and the owner) may
40
+ --add-collaborator An owner agent lets another agent of this machine work in the app's data
29
41
  --help | -h Show this help
30
42
  EOF_USAGE
31
43
  }
@@ -69,7 +81,7 @@ process.stdout.write(fs.readFileSync(r, 'utf8'));
69
81
  NODE
70
82
  }
71
83
 
72
- APP=""; OP=""; COLL=""; DOC=""; DATA=""; IF_REV=""; LIMIT=""; AFTER=""
84
+ APP=""; OP=""; COLL=""; DOC=""; DATA=""; IF_REV=""; LIMIT=""; AFTER=""; SCOPE=""; REASON=""; SET_OWNER=""; WHO=""
73
85
  while [[ $# -gt 0 ]]; do
74
86
  case "$1" in
75
87
  --app) [ $# -ge 2 ] || error_exit "--app requires a value"; APP="$2"; shift 2 ;;
@@ -84,18 +96,54 @@ while [[ $# -gt 0 ]]; do
84
96
  --if-rev) [ $# -ge 2 ] || error_exit "--if-rev requires a value"; IF_REV="$2"; shift 2 ;;
85
97
  --limit) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
86
98
  --after) [ $# -ge 2 ] || error_exit "--after requires a value"; AFTER="$2"; shift 2 ;;
99
+ --request-access) OP="request-access"; shift ;;
100
+ --collaborators) OP="collaborators"; shift ;;
101
+ --owner) OP="owner"; shift ;;
102
+ --set-owner) [ $# -ge 2 ] || error_exit "--set-owner requires an owner (channel:#name, team:<name>, agent:<name> or default)"; OP="owner"; SET_OWNER="$2"; shift 2 ;;
103
+ --add-collaborator) [ $# -ge 2 ] || error_exit "--add-collaborator requires an agent name or session"; OP="add-collaborator"; WHO="$2"; shift 2 ;;
104
+ --scope) [ $# -ge 2 ] || error_exit "--scope requires a value"; SCOPE="$2"; shift 2 ;;
105
+ --reason) [ $# -ge 2 ] || error_exit "--reason requires a value"; REASON="$2"; shift 2 ;;
87
106
  --help|-h) print_usage; exit 0 ;;
88
107
  *) error_exit "Unknown option: $1" ;;
89
108
  esac
90
109
  done
91
110
 
92
111
  [ -n "$APP" ] || error_exit "--app is required"
93
- [ -n "$OP" ] || error_exit "one of --list / --get / --set / --update / --add / --delete is required"
112
+ [ -n "$OP" ] || error_exit "one of --list / --get / --set / --update / --add / --delete / --request-access / --collaborators / --owner / --add-collaborator is required"
94
113
  [[ "$APP" =~ ^[a-z0-9]{10}$ ]] || error_exit "--app must be a 10-character app id"
95
114
  case "$COLL" in .|..) error_exit "collection cannot be . or .." ;; esac
96
115
  case "$DOC" in .|..) error_exit "doc id cannot be . or .." ;; esac
97
116
  BASE="/apps/${APP}/data/$(uri "$COLL")"
98
117
 
118
+ case "$OP" in
119
+ request-access)
120
+ case "$SCOPE" in ""|team|agent) ;; *) error_exit "--scope is team or agent" ;; esac
121
+ BODY=$(jq -cn --arg s "$SCOPE" --arg r "$REASON" '{} + (if $s != "" then {scope: $s} else {} end) + (if $r != "" then {reason: $r} else {} end)')
122
+ RESPONSE=$(call POST "/apps/${APP}/collaborators/request" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
123
+ printf '%s' "$RESPONSE" | jq -c '{success: true, requested: .data.requested, for: .data.for, note: "The owner got a card. You are told here when they answer; until then you still cannot use this app."}'
124
+ exit 0
125
+ ;;
126
+ collaborators)
127
+ RESPONSE=$(call GET "/apps/${APP}/collaborators") || { printf '%s\n' "$RESPONSE"; exit 1; }
128
+ printf '%s' "$RESPONSE" | jq -c '{success: true, collaborators: [.data.collaborators[]? | {id, kind, name, who}]}'
129
+ exit 0
130
+ ;;
131
+ owner)
132
+ if [ -n "$SET_OWNER" ]; then
133
+ RESPONSE=$(call PUT "/apps/${APP}/owner" "$(jq -cn --arg o "$SET_OWNER" '{owner: $o}')") || { printf '%s\n' "$RESPONSE"; exit 1; }
134
+ else
135
+ RESPONSE=$(call GET "/apps/${APP}/owner") || { printf '%s\n' "$RESPONSE"; exit 1; }
136
+ fi
137
+ printf '%s' "$RESPONSE" | jq -c '{success: true, owner: (.data.owner | if . == null then null else {kind, name, default: (.explicit | not)} + (if .members then {members: .members} else {} end) end)}'
138
+ exit 0
139
+ ;;
140
+ add-collaborator)
141
+ RESPONSE=$(call POST "/apps/${APP}/collaborators/agents" "$(jq -cn --arg a "$WHO" '{agent: $a}')") || { printf '%s\n' "$RESPONSE"; exit 1; }
142
+ printf '%s' "$RESPONSE" | jq -c '{success: true, collaborators: [.data.collaborators[]? | {id, kind, name, who}]}'
143
+ exit 0
144
+ ;;
145
+ esac
146
+
99
147
  need_data() {
100
148
  [ -n "$DATA" ] || error_exit "--data is required for --$OP"
101
149
  printf '%s' "$DATA" | jq -e 'type == "object"' >/dev/null 2>&1 || error_exit "--data must be a JSON object"
@@ -107,7 +155,7 @@ case "$OP" in
107
155
  [ -n "$LIMIT" ] && Q="limit=$(uri "$LIMIT")"
108
156
  [ -n "$AFTER" ] && Q="${Q:+$Q&}after=$(uri "$AFTER")"
109
157
  RESPONSE=$(call GET "${BASE}${Q:+?$Q}") || { printf '%s\n' "$RESPONSE"; exit 1; }
110
- printf '%s' "$RESPONSE" | jq -c '{success: true, docs: [.data.docs[]? | {id, data, rev, updatedAt, updatedBy: (.updatedBy.kind // null)}], next: .data.next}'
158
+ printf '%s' "$RESPONSE" | jq -c '{success: true, docs: [.data.docs[]? | {id, data, rev, updatedAt, updatedBy: (.updatedBy.kind // null)} + (if .updatedBy.kind == "agent" then {by: .updatedBy.id} else {} end)], next: .data.next}'
111
159
  ;;
112
160
  get)
113
161
  RESPONSE=$(call GET "${BASE}/$(uri "$DOC")") || { printf '%s\n' "$RESPONSE"; exit 1; }
@@ -26,6 +26,18 @@ class H(BaseHTTPRequestHandler):
26
26
  self.wfile.write(json.dumps(obj).encode())
27
27
  if self.path.endswith('/missing'):
28
28
  return send(404, {'success': False, 'error': 'not_found', 'message': 'Document not found.', 'hint': 'No such app, document or version for this account.'})
29
+ if self.path.endswith('/collaborators') and method == 'GET':
30
+ return send(200, {'success': True, 'data': {'collaborators': [{'id': 'e1', 'kind': 'team', 'who': 'Marketing', 'name': 'Marketing', 'instanceId': 'i'}]}})
31
+ if self.path.endswith('/owner'):
32
+ if method == 'PUT':
33
+ return send(200, {'success': True, 'data': {'appId': 'x', 'owner': {'kind': 'channel', 'explicit': True, 'channelId': 'h1', 'name': '#daily-brief', 'instanceId': 'i', 'members': ['a', 'b']}, 'previous': None}})
34
+ return send(200, {'success': True, 'data': {'appId': 'x', 'owner': {'kind': 'agent', 'explicit': False, 'session': 'dev-ella', 'name': 'Ella', 'instanceId': 'i'}}})
35
+ if self.path.endswith('/collaborators/agents'):
36
+ return send(200, {'success': True, 'data': {'collaborators': [{'id': 'e2', 'kind': 'agent', 'who': 'crewly-dev-kai-1', 'name': 'Kai', 'instanceId': 'i'}]}})
37
+ if self.path.endswith('/collaborators/request'):
38
+ return send(200, {'success': True, 'data': {'requested': True, 'decisionId': 'D-1', 'for': 'the Marketing team'}})
39
+ if self.path.endswith('/byagent'):
40
+ return send(200, {'success': True, 'data': {'docs': [{'id': 'a', 'data': {}, 'rev': 1, 'updatedAt': 't', 'updatedBy': {'kind': 'agent', 'id': 'crewly-marketing-ella-1'}}], 'next': None}})
29
41
  if (data or {}).get('ifRev') == 1:
30
42
  return send(409, {'success': False, 'error': 'conflict', 'message': 'rev mismatch'})
31
43
  if method == 'GET' and '?' in self.path or self.path.endswith('/items'):
@@ -104,5 +116,35 @@ check "data-file in Crewly home refused" "$(printf '%s' "$OUT" | grep -c "inside
104
116
  OUT=$(run_err --app $A --get items .. || true)
105
117
  check "doc id .. refused" "$(printf '%s' "$OUT" | grep -c 'cannot be')" "1"
106
118
 
119
+ # A write by an agent shows who (the session); an owner write does not carry an id.
120
+ OUT=$(run --app $A --list byagent)
121
+ check "list: agent write shows its session" "$(printf '%s' "$OUT" | jq -c '.docs[0] | {updatedBy, by}')" '{"updatedBy":"agent","by":"crewly-marketing-ella-1"}'
122
+
123
+ # Asking for access: POST .../collaborators/request with the scope and reason only.
124
+ OUT=$(run --app $A --request-access --scope agent --reason "write the briefing")
125
+ check "request-access: path" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "POST /api/apps/$A/collaborators/request"
126
+ check "request-access: body" "$(jq -c .body "$STUB_LOG")" '{"scope":"agent","reason":"write the briefing"}'
127
+ check "request-access: output says requested" "$(printf '%s' "$OUT" | jq -c '{success, requested, for}')" '{"success":true,"requested":true,"for":"the Marketing team"}'
128
+ OUT=$(run_err --app $A --request-access --scope everyone || true)
129
+ check "request-access: bad scope refused" "$(printf '%s' "$OUT" | grep -c 'team or agent')" "1"
130
+ OUT=$(run --app $A --collaborators)
131
+ check "collaborators: output" "$OUT" '{"success":true,"collaborators":[{"id":"e1","kind":"team","name":"Marketing","who":"Marketing"}]}'
132
+
133
+ # App owner (crewly-services apps/SPEC.md §15).
134
+ OUT=$(run --app $A --owner)
135
+ check "owner: shows the default owner" "$OUT" '{"success":true,"owner":{"kind":"agent","name":"Ella","default":true}}'
136
+ check "owner: GET path" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "GET /api/apps/$A/owner"
137
+ OUT=$(run --app $A --set-owner 'channel:#daily-brief')
138
+ check "set-owner: request" "$(jq -c '{method, path, body}' "$STUB_LOG")" "{\"method\":\"PUT\",\"path\":\"/api/apps/$A/owner\",\"body\":{\"owner\":\"channel:#daily-brief\"}}"
139
+ check "set-owner: output" "$OUT" '{"success":true,"owner":{"kind":"channel","name":"#daily-brief","default":false,"members":["a","b"]}}'
140
+ OUT=$(run --app $A --add-collaborator Kai)
141
+ check "add-collaborator: request" "$(jq -c '{method, path, body}' "$STUB_LOG")" "{\"method\":\"POST\",\"path\":\"/api/apps/$A/collaborators/agents\",\"body\":{\"agent\":\"Kai\"}}"
142
+ check "add-collaborator: output" "$(printf '%s' "$OUT" | jq -c '.collaborators[0].name')" '"Kai"'
143
+ OUT=$(run_err --app $A --set-owner || true)
144
+ check "set-owner needs a value" "$(printf '%s' "$OUT" | grep -c 'requires an owner')" "1"
145
+ # --set still means a data write.
146
+ run --app $A --set items milk --data '{"done":true}' >/dev/null
147
+ check "--set is still a data write" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "PUT /api/apps/$A/data/items/milk"
148
+
107
149
  echo "app-data: $PASS passed, $FAIL failed"
108
150
  [ "$FAIL" -eq 0 ]
@@ -37,7 +37,7 @@ execution:
37
37
 
38
38
  # Complete Task
39
39
 
40
- Mark a task as complete with a summary of the work done. If the task has an output schema, provide structured output that will be validated against the schema. Optionally skip quality gates if they have already been verified separately.
40
+ Mark a task as complete with a summary of the work done. If the task has an output schema, provide structured output that will be validated against the schema. Unknown fields are refused, never silently dropped.
41
41
 
42
42
  ## Parameters
43
43
 
@@ -51,6 +51,10 @@ Mark a task as complete with a summary of the work done. If the task has an outp
51
51
  | `output` | No | Structured output object (required if task has an output schema) |
52
52
  | ~~`skipGates`~~ | **Rejected** | **Not supported.** This endpoint runs no quality gates, so there is nothing to skip. Passing it is a hard error. Use the `check-quality-gates` skill for gates. |
53
53
 
54
+ **Any other field is an error.** The skill accepts only the fields in this table plus `verdict`/`feedback` (reviews) and `taskId`/`artifacts`/`testResults`/`structured` (verification requests). An unknown field (for example `projectPath`, `skipGates`, `force`) stops the call before anything is sent, and the error names the field. Put extra results inside `output`.
55
+
56
+ On success the skill prints **one JSON line** that always names the `workItemId`, plus the server's reply (`success`, `message`, any `warning`).
57
+
54
58
  ### Which WorkItem gets completed
55
59
 
56
60
  Resolution order:
@@ -19,27 +19,31 @@ INPUT=$(read_json_input "${1:-}")
19
19
  # /api/task-pool/complete by hand. Same silent-friction class as the
20
20
  # create-task `{workItem: ...}` wrapper: the skill rejected a request that
21
21
  # was, by its own contract, correct.
22
+ if [ "$(printf '%s' "$INPUT" | jq -r 'type' 2>/dev/null || echo invalid)" != "object" ]; then
23
+ error_exit "complete-task input must be a JSON object, e.g. {\"workItemId\":\"wi-abc123\",\"sessionName\":\"dev-1\",\"summary\":\"...\"}"
24
+ fi
22
25
  WORK_ITEM_ID=$(printf '%s' "$INPUT" | jq -r '.workItemId // empty')
23
26
  ABSOLUTE_TASK_PATH=$(printf '%s' "$INPUT" | jq -r '.absoluteTaskPath // empty')
24
27
  SESSION_NAME=$(printf '%s' "$INPUT" | jq -r '.sessionName // empty')
25
28
  SUMMARY=$(printf '%s' "$INPUT" | jq -r '.summary // empty')
26
- # `skipGates` is REJECTED, not ignored.
27
- #
28
- # It was previously parsed here and forwarded in the request body. Nothing ever
29
- # read it: `completeItem` destructures only `{agentId, tokenUsage, result}`
30
- # (task-pool.controller.ts), and `POST /task-pool/complete/:id` runs no quality
31
- # gates at all — gates live behind `POST /quality-gates/check`, reached via the
32
- # `check-quality-gates` skill. So the field promised to skip something this
33
- # endpoint never runs.
29
+ # Unknown top-level fields are REJECTED, not ignored (CREW-267).
34
30
  #
35
- # Accepting-and-ignoring is the worst of the three dispositions: the caller
36
- # believes gates were skipped, the belief is unfalsifiable, and nothing ever
37
- # fails. Honouring it is incoherent (there is nothing here to skip). So it
38
- # fails loudly instead. Safe to hard-error: no caller in the repo passes it.
39
- SKIP_GATES=$(printf '%s' "$INPUT" | jq -r '.skipGates // empty')
40
- if [ -n "$SKIP_GATES" ]; then
31
+ # `skipGates` was once parsed and forwarded, and nothing ever read it:
32
+ # `POST /task-pool/complete/:id` runs no quality gates (they live behind the
33
+ # `check-quality-gates` skill). On 2026-10-06 seven completions passed it,
34
+ # printed a bare `{}`, and looked closed when they were not. Accepting and
35
+ # ignoring a field is the worst disposition: the caller believes it did
36
+ # something, and nothing ever fails. So every field this skill does not use
37
+ # fails loudly, with its name, before any request is sent.
38
+ ALLOWED_FIELDS='["workItemId","absoluteTaskPath","sessionName","summary","output","evidence","verdict","feedback","taskId","artifacts","testResults","structured"]'
39
+ SKIP_GATES=$(printf '%s' "$INPUT" | jq -r 'has("skipGates")')
40
+ if [ "$SKIP_GATES" = "true" ]; then
41
41
  error_exit "skipGates is not supported by complete-task and never was — it was accepted and silently discarded. POST /task-pool/complete runs no quality gates, so there is nothing here to skip. Quality gates live behind the 'check-quality-gates' skill (POST /quality-gates/check); run or skip them there. Remove skipGates from this call."
42
42
  fi
43
+ UNKNOWN_FIELDS=$(printf '%s' "$INPUT" | jq -r --argjson allowed "$ALLOWED_FIELDS" '[keys[] | select(. as $k | $allowed | index($k) | not)] | join(", ")')
44
+ if [ -n "$UNKNOWN_FIELDS" ]; then
45
+ error_exit "complete-task does not accept: ${UNKNOWN_FIELDS}. Nothing was completed. Remove the field(s) and call again. Accepted fields: $(printf '%s' "$ALLOWED_FIELDS" | jq -r 'join(", ")'). Put structured results inside \"output\" and proof inside \"evidence\"."
46
+ fi
43
47
  OUTPUT_JSON=$(printf '%s' "$INPUT" | jq -c '.output // empty')
44
48
  # Evidence contract (#873): "done" needs evidence. Each entry is one of
45
49
  # {"type":"artifact","path":"<existing file, relative to the project/worktree, or https URL>"}
@@ -168,7 +172,18 @@ BODY=$(jq -n \
168
172
  }')
169
173
 
170
174
  COMPLETE_RESPONSE=$(api_call POST "/task-pool/complete/${WORK_ITEM_ID}" "$BODY")
171
- printf '%s\n' "$COMPLETE_RESPONSE"
175
+ # Always one result line naming the WorkItem (CREW-267): a bare `{}` read as
176
+ # "closed" when nothing said so. A non-empty object is passed through with
177
+ # the id added; an empty or non-JSON body gets an explicit line instead.
178
+ RESULT_LINE=$(printf '%s' "$COMPLETE_RESPONSE" | jq -c --arg id "$WORK_ITEM_ID" '
179
+ if type == "object" and length > 0 then {workItemId: $id} + .
180
+ else {success: true, workItemId: $id, message: ("WorkItem " + $id + " completion was accepted, but the server sent no details. Check it with GET /api/task-pool/items/" + $id + ".")}
181
+ end' 2>/dev/null || true)
182
+ if [ -z "$RESULT_LINE" ]; then
183
+ RESULT_LINE=$(jq -nc --arg id "$WORK_ITEM_ID" --arg raw "$COMPLETE_RESPONSE" \
184
+ '{success: true, workItemId: $id, message: ("WorkItem " + $id + " completion was accepted; the server reply was not JSON."), raw: $raw}')
185
+ fi
186
+ printf '%s\n' "$RESULT_LINE"
172
187
  # Warn-mode rollout (#873): the server accepts a completion without evidence
173
188
  # for one release but says so. Repeat it on stderr so it is not missed.
174
189
  EVIDENCE_WARNING=$(printf '%s' "$COMPLETE_RESPONSE" | jq -r '.warning // empty' 2>/dev/null || true)
@@ -233,7 +233,7 @@ echo "--- Finished-task summary stays out of long-term memory (#833) ---"
233
233
 
234
234
  # The summary lives on the WorkItem and in task-history.json. Saving it as a
235
235
  # project decision crowded real decisions out of recall.
236
- run_skill '{"workItemId":"wi-833","sessionName":"dev-1","summary":"Shipped the importer","projectPath":"/proj"}'
236
+ run_skill '{"workItemId":"wi-833","sessionName":"dev-1","summary":"Shipped the importer"}'
237
237
  assert_contains "completion still reaches the task pool" \
238
238
  "POST http://stub.invalid/api/task-pool/complete/wi-833" "$REQUESTS"
239
239
  assert_not_contains "no /memory/remember call on completion" "/memory/remember" "$REQUESTS"
@@ -267,6 +267,50 @@ STUB_COMPLETE_RESPONSE='{"success":true,"message":"WorkItem wi-plain completed",
267
267
  run_skill '{"workItemId":"wi-plain","sessionName":"dev-1","summary":"Report written"}'
268
268
  assert_contains "server warning is printed" '"warning": "Completed WITHOUT evidence' "$OUT"
269
269
 
270
+ echo ""
271
+ echo "--- Unknown fields fail loudly, before any request (CREW-267) ---"
272
+
273
+ run_skill '{"workItemId":"wi-u","sessionName":"dev-1","summary":"done","projectPath":"/proj"}'
274
+ assert_contains "an unknown field is refused" '"error"' "$OUT"
275
+ assert_contains "the refusal names the field" "projectPath" "$OUT"
276
+ assert_contains "the refusal says nothing was completed" "Nothing was completed" "$OUT"
277
+ assert_not_contains "no request is sent for an unknown field" "/task-pool/complete" "$REQUESTS"
278
+
279
+ run_skill '{"workItemId":"wi-u","sessionName":"dev-1","summary":"done","fallback":true,"force":1}'
280
+ assert_contains "every unknown field is named (1)" "fallback" "$OUT"
281
+ assert_contains "every unknown field is named (2)" "force" "$OUT"
282
+ assert_not_contains "no request is sent for several unknown fields" "/task-pool/complete" "$REQUESTS"
283
+
284
+ run_skill '{"workItemId":"wi-u","sessionName":"dev-1","summary":"done","skipGates":false}'
285
+ assert_contains "skipGates is refused even when false" "skipGates is not supported" "$OUT"
286
+ assert_not_contains "no request is sent for skipGates" "/task-pool/complete" "$REQUESTS"
287
+
288
+ run_skill '["not","an","object"]'
289
+ assert_contains "a non-object input is refused" "must be a JSON object" "$OUT"
290
+
291
+ run_skill '{"workItemId":"wi-ok","sessionName":"dev-1","summary":"done","output":{"x":1},"evidence":[],"verdict":"verified","taskId":"t","artifacts":[],"testResults":"ok","structured":false,"absoluteTaskPath":"/x"}'
292
+ assert_not_contains "every documented field is still accepted" "does not accept" "$OUT"
293
+ assert_contains "a call with every documented field completes" \
294
+ "POST http://stub.invalid/api/task-pool/complete/wi-ok" "$REQUESTS"
295
+
296
+ echo ""
297
+ echo "--- Success always prints one result line naming the WorkItem (CREW-267) ---"
298
+
299
+ STUB_COMPLETE_RESPONSE='{}' run_skill '{"workItemId":"wi-empty","sessionName":"dev-1","summary":"done"}'
300
+ BARE_LINES="$(printf '%s\n' "$OUT" | grep -cx '{}' || true)"
301
+ assert_contains "an empty server reply is not echoed as a bare {}" "0" "$BARE_LINES"
302
+ LAST_LINE="$(printf '%s' "$OUT" | tail -1)"
303
+ assert_contains "the result line names the WorkItem" '"workItemId":"wi-empty"' "$LAST_LINE"
304
+ assert_contains "the result line says the server sent no details" "no details" "$LAST_LINE"
305
+
306
+ STUB_COMPLETE_RESPONSE='{"success":true,"message":"WorkItem wi-full completed"}' \
307
+ run_skill '{"workItemId":"wi-full","sessionName":"dev-1","summary":"done"}'
308
+ LAST_LINE="$(printf '%s' "$OUT" | tail -1)"
309
+ assert_contains "the server reply is kept" '"message":"WorkItem wi-full completed"' "$LAST_LINE"
310
+ assert_contains "the server reply gains the WorkItem id" '"workItemId":"wi-full"' "$LAST_LINE"
311
+ LINE_COUNT="$(printf '%s\n' "$OUT" | grep -c '"workItemId":"wi-full"' || true)"
312
+ assert_contains "the result is a single line" "1" "$LINE_COUNT"
313
+
270
314
  rm -rf "$STUB_DIR"
271
315
 
272
316
  echo ""
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Publish App
3
3
  description: Publish a small web app (HTML/JS) you wrote to https://apps.crewlyai.com/<appId> so the owner can open it on their phone. Creates the app the first time, uploads a new version each time after (same app for the same directory), supports rollback, and can post an "Open app" card to the owner (a one-tap signed link, in your DM with them). Can ask the owner to make an app public (only the owner can approve it). The owner's edits, comments on elements, and anonymous visitors' submissions on a public app, come back to you as an [APP CHANGES] message.
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  category: productivity
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -48,6 +48,8 @@ bash execute.sh --html ./timer.html --name "Timer"
48
48
  bash execute.sh --app 28au74d9cj --rollback 2 # back to version 2
49
49
  bash execute.sh --app 28au74d9cj --versions
50
50
  bash execute.sh --app 28au74d9cj --transfer-to edu-game-milo-13e8d3ca # hand the app to another agent / team
51
+ bash execute.sh --dir ./daily-brief --owner 'channel:#daily-brief' # publish, and send the owner's comments to a channel
52
+ bash execute.sh --app 28au74d9cj --owner 'team:Dev' # change only who gets the comments
51
53
  bash execute.sh --list # apps published from this machine
52
54
  bash execute.sh --app 28au74d9cj --share # send the owner a fresh one-tap link (no publish)
53
55
  bash execute.sh --app 28au74d9cj --links # its open-links; --revoke-link <id> / --revoke-links
@@ -132,6 +134,8 @@ bash execute.sh --app 28au74d9cj --private # pr
132
134
  shared keep working. So test a new version before publishing it to a public
133
135
  app. Widening what is public (more `--public-read` / `--public-submit`
134
136
  collections) still needs a new request the owner approves.
137
+ - Collaborators: an app another team published is not yours to republish. `--app <id>` from a team that did not publish it is refused (`not_your_app`), including when the owner added your team as a collaborator: collaborators get the app's **data and comments** only (use `app-data`; ask with `app-data --app <id> --request-access`). To change the page itself, ask the publisher or the owner to transfer the app (`--transfer-to`).
138
+ - Owner: `--owner channel:#<name> | team:<name> | agent:<name> | default` sets who the owner's **comments** go to (a channel's members from every team see them; see `app-data` → Owner). It does not change who publishes. Only the app's owner agents may change it (by default the publisher); with a publish the owner is set right after it, and an `ownerError` in the output means only that part failed.
135
139
  - Transfer: `--app <appId> --transfer-to <session>` hands the app to another agent, e.g. when the owner moves the work to a new team. Only the app's publisher, the lead of its team, the orchestrator or the owner may do it, and the target must be a member of an active team on this machine. Afterwards the new publisher publishes with `--app <appId> --dir <its directory>`, comments and changes go to it, and the old team can no longer publish, roll back or use the app's data. Both agents get a short note.
136
140
  - Thumbnails: the owner's portal list shows a small screenshot of each app. Publishing (and rollback) takes it automatically in the background when this machine has Chrome or Chromium; `bash execute.sh --app <appId> --refresh-thumbnail` re-takes it now (answers `captured:false` with a reason such as `no_browser` when it cannot).
137
141
  - `--private` is always allowed and takes effect at once. Use it if anything
@@ -29,6 +29,7 @@ Usage:
29
29
  bash execute.sh --app <appId> --rollback <version> # make an earlier version current
30
30
  bash execute.sh --app <appId> --versions # list versions
31
31
  bash execute.sh --app <appId> --transfer-to <session> # hand the app to another agent / team
32
+ bash execute.sh --app <appId> --owner 'channel:#daily-brief' # who gets the owner's comments (also with a publish)
32
33
  bash execute.sh --list # apps published from this machine
33
34
  bash execute.sh --app <appId> --share [--ttl-days 7] # fresh one-tap link card to the owner (no publish)
34
35
  bash execute.sh --app <appId> --links # open-links of the app (no secrets)
@@ -63,6 +64,8 @@ Options:
63
64
  --rollback Version number to make current (needs --app)
64
65
  --versions List versions (needs --app)
65
66
  --transfer-to Hand the app to another agent (its session name; it must be on a team on this machine).
67
+ --owner Who owns the app's comments: channel:#<name>, team:<name>, agent:<name> or default (the publishing agent).
68
+ With a publish it is set after the publish; alone it needs --app. Only the app's owner agents may change it.
66
69
  Allowed for the app's publisher, the lead of its team, the orchestrator or the owner.
67
70
  The new publisher publishes with --app <appId> --dir <its directory>; the old team loses access.
68
71
  --list List apps published from this machine
@@ -84,7 +87,7 @@ call() {
84
87
  }
85
88
 
86
89
  DIR=""; FILE=""; NAME=""; APP=""; ENTRY=""; NOTE=""; NOTIFY=false; ROLLBACK=""; TRANSFER_TO=""; VERSIONS=false; LIST=false
87
- SHARE=false; TTL_DAYS=""; LINKS=false; REVOKE_LINK=""; REVOKE_LINKS=false
90
+ SHARE=false; TTL_DAYS=""; LINKS=false; REVOKE_LINK=""; REVOKE_LINKS=false; OWNER_SPEC=""
88
91
  PUBLIC=false; PUBLIC_READ=""; PUBLIC_SUBMIT=""; PUBLIC_NOTE=""; CANCEL_PUBLIC=false; PRIVATE=false; REFRESH_THUMB=false
89
92
  while [[ $# -gt 0 ]]; do
90
93
  case "$1" in
@@ -110,6 +113,7 @@ while [[ $# -gt 0 ]]; do
110
113
  --refresh-thumbnail) REFRESH_THUMB=true; shift ;;
111
114
  --versions) VERSIONS=true; shift ;;
112
115
  --transfer-to) [ $# -ge 2 ] || error_exit "--transfer-to requires a session name"; TRANSFER_TO="$2"; shift 2 ;;
116
+ --owner) [ $# -ge 2 ] || error_exit "--owner requires channel:#<name>, team:<name>, agent:<name> or default"; OWNER_SPEC="$2"; shift 2 ;;
113
117
  --list) LIST=true; shift ;;
114
118
  --help|-h) print_usage; exit 0 ;;
115
119
  *) error_exit "Unknown option: $1" ;;
@@ -201,6 +205,15 @@ if [ -z "$DIR" ] && [ -z "$FILE" ]; then
201
205
  fi
202
206
  fi
203
207
 
208
+ # The app's owner (crewly-services apps/SPEC.md §15): who its comments go to.
209
+ OWNER_JQ='(.data.owner | if . == null then null else {kind, name, default: (.explicit | not)} end)'
210
+ if [ -n "$OWNER_SPEC" ] && [ -z "$DIR" ] && [ -z "$FILE" ]; then
211
+ [ -n "$APP" ] || error_exit "--app is required with --owner (or publish with --dir / --html)"
212
+ RESPONSE=$(call PUT "/apps/${APP}/owner" "$(jq -cn --arg o "$OWNER_SPEC" '{owner: $o}')") || { printf '%s\n' "$RESPONSE"; exit 1; }
213
+ printf '%s' "$RESPONSE" | jq -c '{success: true, appId: .data.appId, owner: '"$OWNER_JQ"'}'
214
+ exit 0
215
+ fi
216
+
204
217
  if [ -n "$TRANSFER_TO" ]; then
205
218
  [ -n "$APP" ] || error_exit "--app is required with --transfer-to"
206
219
  BODY=$(jq -cn --arg s "$TRANSFER_TO" '{toSession: $s}')
@@ -311,9 +324,19 @@ OUT=$(curl -s -w '\n%{http_code}' -X POST -H "Content-Type: application/json" ${
311
324
  CODE=$(printf '%s\n' "$OUT" | tail -n 1)
312
325
  RESP=$(printf '%s\n' "$OUT" | sed '$d')
313
326
  if [ "$CODE" -ge 200 ] 2>/dev/null && [ "$CODE" -lt 300 ] 2>/dev/null; then
314
- printf '%s' "$RESP" | jq -c --arg msg "$PUBLIC_MSG" '.data | {success: true, appId, name, url: ("https://apps.crewlyai.com/" + .appId), version, created, notified} + '"$CARD_JQ"'
327
+ OWNER_OUT='{}'
328
+ if [ -n "$OWNER_SPEC" ]; then
329
+ NEW_APP=$(printf '%s' "$RESP" | jq -r '.data.appId // empty')
330
+ if OWNER_RESP=$(call PUT "/apps/${NEW_APP}/owner" "$(jq -cn --arg o "$OWNER_SPEC" '{owner: $o}')"); then
331
+ OWNER_OUT=$(printf '%s' "$OWNER_RESP" | jq -c '{owner: '"$OWNER_JQ"'}')
332
+ else
333
+ # The publish went through; only the owner change failed.
334
+ OWNER_OUT=$(printf '%s' "$OWNER_RESP" | jq -c '{ownerError: (.message // .reason // "failed")}' 2>/dev/null || echo '{"ownerError":"failed"}')
335
+ fi
336
+ fi
337
+ printf '%s' "$RESP" | jq -c --arg msg "$PUBLIC_MSG" --argjson own "$OWNER_OUT" '.data | {success: true, appId, name, url: ("https://apps.crewlyai.com/" + .appId), version, created, notified} + '"$CARD_JQ"'
315
338
  + (if .publicRequested == true then {publicRequested: true, message: $msg} elif .publicRequested == false then {publicRequested: false, publicError} else {} end)
316
- + (if .publicPaused == true then {publicPaused: true, publicPausedMessage} else {} end)'
339
+ + (if .publicPaused == true then {publicPaused: true, publicPausedMessage} else {} end) + $own'
317
340
  else
318
341
  printf '%s' "$RESP" | jq -c --arg code "$CODE" '{success: false, status: ($code|tonumber), reason: (.error // "unknown"), message: (.message // ""), hint: (.hint // "")}' 2>/dev/null \
319
342
  || jq -cn --arg code "$CODE" '{success: false, status: ($code|tonumber? // 0), reason: "http_error"}'
@@ -63,6 +63,10 @@ class H(BaseHTTPRequestHandler):
63
63
  return send(200, {'success': True, 'data': {'appId': 'bcdfghjkmn', 'captured': False, 'reason': 'no_browser', 'message': 'No Chrome'}})
64
64
  if self.path == '/api/apps/28au74d9cj/make-private':
65
65
  return send(200, {'success': True, 'data': {'appId': '28au74d9cj', 'visibility': 'private'}})
66
+ if self.path.endswith('/owner') and method == 'PUT':
67
+ if data.get('owner') == 'channel:#nope':
68
+ return send(400, {'success': False, 'error': 'validation', 'message': 'No channel "#nope" on this machine.'})
69
+ return send(200, {'success': True, 'data': {'appId': '28au74d9cj', 'owner': {'kind': 'channel', 'explicit': True, 'name': '#daily-brief'}, 'previous': None}})
66
70
  if self.path == '/api/apps/28au74d9cj/transfer':
67
71
  return send(200, {'success': True, 'data': {'appId': '28au74d9cj', 'name': 'G', 'publisher': data['toSession'], 'previous': 'dev-ella', 'changed': True, 'notified': [data['toSession']]}})
68
72
  if self.path.endswith('/rollback'):
@@ -79,6 +83,9 @@ class H(BaseHTTPRequestHandler):
79
83
  def do_POST(self):
80
84
  n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
81
85
  self._reply('POST', json.loads(body or '{}'))
86
+ def do_PUT(self):
87
+ n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
88
+ self._reply('PUT', json.loads(body or '{}'))
82
89
  def log_message(self, *a, **k): pass
83
90
  HTTPServer(('127.0.0.1', PORT), H).serve_forever()
84
91
  PY
@@ -220,5 +227,16 @@ check "refresh-thumbnail without a browser" "$OUT" '{"success":false,"appId":"bc
220
227
  OUT=$(run_err --dir "$APPDIR" --refresh-thumbnail || true)
221
228
  check "--refresh-thumbnail is not a publish option" "$(printf '%s' "$OUT" | grep -c 'act on an app')" "1"
222
229
 
230
+ # --owner (crewly-services apps/SPEC.md §15)
231
+ OUT=$(run --app 28au74d9cj --owner 'channel:#daily-brief')
232
+ check "owner alone: output" "$OUT" '{"success":true,"appId":"28au74d9cj","owner":{"kind":"channel","name":"#daily-brief","default":false}}'
233
+ check "owner alone: request" "$(jq -c '{method, path, body}' "$STUB_LOG")" '{"method":"PUT","path":"/api/apps/28au74d9cj/owner","body":{"owner":"channel:#daily-brief"}}'
234
+ check "owner alone needs --app" "$(run_err --owner 'team:dev' | grep -c -- '--app is required with --owner')" "1"
235
+ OUT=$(run --dir "$APPDIR" --owner 'channel:#daily-brief')
236
+ check "publish + owner: set after the publish" "$(printf '%s' "$OUT" | jq -c '{success, owner}')" '{"success":true,"owner":{"kind":"channel","name":"#daily-brief","default":false}}'
237
+ check "publish + owner: last request is the owner change" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "PUT /api/apps/28au74d9cj/owner"
238
+ OUT=$(run --dir "$APPDIR" --owner 'channel:#nope')
239
+ check "publish + bad owner: publish still succeeds, ownerError says why" "$(printf '%s' "$OUT" | jq -c '{success, ownerError}')" '{"success":true,"ownerError":"No channel \"#nope\" on this machine."}'
240
+
223
241
  echo "publish-app: $PASS passed, $FAIL failed"
224
242
  [ "$FAIL" -eq 0 ]