@repo-os/repoos 0.5.69 → 0.5.71

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 (159) hide show
  1. package/dist/.build-info.json +2 -2
  2. package/dist/.build-stamp.json +1 -1
  3. package/dist/cli/help.js +5 -5
  4. package/dist/cli/task-flags.js +16 -0
  5. package/dist/commands/control-api.js +42 -9
  6. package/dist/commands/init.js +19 -0
  7. package/dist/commands/screenshot-flag.d.ts +36 -0
  8. package/dist/commands/screenshot-flag.js +99 -0
  9. package/dist/commands/shot.d.ts +10 -0
  10. package/dist/commands/shot.js +129 -32
  11. package/dist/commands/tasks.js +118 -5
  12. package/dist/core/budget-allowlist.d.ts +43 -0
  13. package/dist/core/budget-allowlist.js +83 -0
  14. package/dist/core/config.d.ts +8 -1
  15. package/dist/core/config.js +176 -1
  16. package/dist/core/review-passes.d.ts +1 -1
  17. package/dist/core/review-passes.js +7 -1
  18. package/dist/core/review-verification-config.d.ts +36 -0
  19. package/dist/core/review-verification-config.js +36 -0
  20. package/dist/core/review-verification.d.ts +59 -1
  21. package/dist/core/review-verification.js +132 -19
  22. package/dist/core/review-verification.test.js +95 -0
  23. package/dist/core/shot-command.d.ts +72 -0
  24. package/dist/core/shot-command.js +265 -0
  25. package/dist/core/shot-fixtures.d.ts +4 -2
  26. package/dist/core/shot-fixtures.js +5 -0
  27. package/dist/core/shot-page.d.ts +3 -1
  28. package/dist/core/shot-plan.d.ts +52 -3
  29. package/dist/core/shot-plan.js +133 -18
  30. package/dist/core/shot-targets.d.ts +35 -2
  31. package/dist/core/shot-targets.js +118 -6
  32. package/dist/core/types.d.ts +129 -2
  33. package/dist/core/types.js +11 -1
  34. package/dist/server/agent-model-allowlist.d.ts +30 -0
  35. package/dist/server/agent-model-allowlist.js +35 -0
  36. package/dist/server/agents.d.ts +9 -0
  37. package/dist/server/agents.js +28 -0
  38. package/dist/server/local-token.d.ts +11 -0
  39. package/dist/server/local-token.js +22 -0
  40. package/dist/server/review-verification-gather.js +44 -15
  41. package/dist/server/review.d.ts +16 -0
  42. package/dist/server/review.js +140 -29
  43. package/dist/server/routes/config.js +43 -1
  44. package/dist/server/routes/tasks.js +88 -2
  45. package/dist/server/server.js +2 -1
  46. package/dist/server/shot-capture.d.ts +67 -1
  47. package/dist/server/shot-capture.js +190 -15
  48. package/dist/server/shot-context.d.ts +1 -1
  49. package/dist/server/shot-context.js +7 -2
  50. package/dist/server/shots.d.ts +8 -0
  51. package/dist/server/shots.js +3 -0
  52. package/dist/server/ui-handoff-gate.d.ts +27 -2
  53. package/dist/server/ui-handoff-gate.js +102 -18
  54. package/dist/ui/assets/AgentModelControl-D_Idhaon.js +1 -0
  55. package/dist/ui/assets/{AgentModelModal-C8GhSyIa.js → AgentModelModal-qiRXdw6a.js} +1 -1
  56. package/dist/ui/assets/AgentsView-Bsp_kUpY.js +1 -0
  57. package/dist/ui/assets/AgentsView-BvMAuoAh.css +1 -0
  58. package/dist/ui/assets/{ChecksView-CU7ikl8l.js → ChecksView-vW_M6sZs.js} +0 -0
  59. package/dist/ui/assets/ContextView-BsRhYLEM.js +4 -0
  60. package/dist/ui/assets/DashboardView-DLHWXZvB.js +1 -0
  61. package/dist/ui/assets/{DevView-omNNz-ZQ.js → DevView-DZb2LbMn.js} +1 -1
  62. package/dist/ui/assets/{DiffView-CKMjBvIa.js → DiffView-CgUV2isa.js} +1 -1
  63. package/dist/ui/assets/{FailedTestsList-3_oexlME.js → FailedTestsList-CdlcNhbe.js} +1 -1
  64. package/dist/ui/assets/InputsView-q5EU9qzR.js +1 -0
  65. package/dist/ui/assets/{ReleasesView-D3qIgMaL.js → ReleasesView-B7yJdjLp.js} +1 -1
  66. package/dist/ui/assets/SearchOverlay-BgKVgkJE.js +1 -0
  67. package/dist/ui/assets/{SettingsView-gqVC3E7s.js → SettingsView-DiypqAsv.js} +2 -2
  68. package/dist/ui/assets/StoriesView-DM6B6MJf.js +1 -0
  69. package/dist/ui/assets/VoiceDictate-CS-9Kmsa.css +1 -0
  70. package/dist/ui/assets/{VoiceDictate-JU6yFzDl.js → VoiceDictate-CbJnmeNg.js} +2 -2
  71. package/dist/ui/assets/{WorkView-qq7xNw0b.js → WorkView-D6PNGvIY.js} +1 -1
  72. package/dist/ui/assets/{abnfDiagram-VCTEODGH-KzOgBc6g.js → abnfDiagram-VCTEODGH-Bd-cBM60.js} +1 -1
  73. package/dist/ui/assets/{architectureDiagram-5GKGNRK7-CP4J_21k.js → architectureDiagram-5GKGNRK7-CipYgUsO.js} +1 -1
  74. package/dist/ui/assets/{blockDiagram-I7D4REHJ-B5q-JPzd.js → blockDiagram-I7D4REHJ-f_cW_0BB.js} +1 -1
  75. package/dist/ui/assets/{c4Diagram-7LVT6UL2-C8gg_KAI.js → c4Diagram-7LVT6UL2-Br35UBf0.js} +1 -1
  76. package/dist/ui/assets/channel-HBsTnkcF.js +1 -0
  77. package/dist/ui/assets/{chunk-2E4U76K2-Bgho8hyQ.js → chunk-2E4U76K2-DkDkPJbl.js} +1 -1
  78. package/dist/ui/assets/{chunk-4HAMMTFA-DQYpRm2O.js → chunk-4HAMMTFA-B5OLaaS5.js} +1 -1
  79. package/dist/ui/assets/{chunk-75Z2AOVW-DlvTKtcP.js → chunk-75Z2AOVW-BiOKSVCI.js} +1 -1
  80. package/dist/ui/assets/{chunk-CLGD4ZFX-xhgIfmhG.js → chunk-CLGD4ZFX-BurZYOsg.js} +1 -1
  81. package/dist/ui/assets/{chunk-DU6HZSFF-Dkcc-d_4.js → chunk-DU6HZSFF-BGDoa0O8.js} +1 -1
  82. package/dist/ui/assets/{chunk-F27PBJKO-gD-r9Rww.js → chunk-F27PBJKO-DxIrKenk.js} +1 -1
  83. package/dist/ui/assets/{chunk-GMAD6QVW-rvBxuygZ.js → chunk-GMAD6QVW-COGgFsG-.js} +1 -1
  84. package/dist/ui/assets/{chunk-GVQU2GXP-Cq3-rF-u.js → chunk-GVQU2GXP-ChFWBbq9.js} +1 -1
  85. package/dist/ui/assets/{chunk-IMKFNOWR-BHREb6T1.js → chunk-IMKFNOWR-wsEQiYt4.js} +1 -1
  86. package/dist/ui/assets/{chunk-L3NEJ4N5-D-R9OvAs.js → chunk-L3NEJ4N5-Cr1dKnZJ.js} +1 -1
  87. package/dist/ui/assets/{chunk-OSK3NFVY-D0WDbtxw.js → chunk-OSK3NFVY-cSWMlBxc.js} +1 -1
  88. package/dist/ui/assets/{chunk-P2QGCYS3-DLFLbH4p.js → chunk-P2QGCYS3-D1sKUill.js} +1 -1
  89. package/dist/ui/assets/{chunk-POPQ4Y6H-Cw6X8QGG.js → chunk-POPQ4Y6H-DluqWxSK.js} +1 -1
  90. package/dist/ui/assets/{chunk-PWAF6VOD-Ds6RWXzu.js → chunk-PWAF6VOD-kdXLYxnu.js} +1 -1
  91. package/dist/ui/assets/{chunk-SHT3W25Y-DvJuv0Ft.js → chunk-SHT3W25Y-Dm81UtEL.js} +1 -1
  92. package/dist/ui/assets/{chunk-SVP7TREG-DMasMVeC.js → chunk-SVP7TREG-1RGCR-KK.js} +1 -1
  93. package/dist/ui/assets/{chunk-TICWLB2K-yWH6tp94.js → chunk-TICWLB2K-BwkWIbAw.js} +1 -1
  94. package/dist/ui/assets/chunk-TLUHSLCS-CCcqBXqp.js +2 -0
  95. package/dist/ui/assets/classDiagram-ZZMXUADV-D_W2hGoN.js +1 -0
  96. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-D_W2hGoN.js +1 -0
  97. package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-DAETHLCh.js → cynefinDiagram-5FMLGOSQ-BDPeRvBt.js} +1 -1
  98. package/dist/ui/assets/{dagre-GXQ25YYZ-BoTD9ES5.js → dagre-GXQ25YYZ-BqmYPMiI.js} +1 -1
  99. package/dist/ui/assets/{diagram-S7CK7UJ4-etZnEmAz.js → diagram-S7CK7UJ4-DXQbSart.js} +1 -1
  100. package/dist/ui/assets/{diagram-UQ7AKVKN-CywmEzAf.js → diagram-UQ7AKVKN-8Iu6RcsI.js} +1 -1
  101. package/dist/ui/assets/{diagram-VSXAHHWV-Dp_aDmgC.js → diagram-VSXAHHWV-D1vUZYDf.js} +1 -1
  102. package/dist/ui/assets/{diagram-VX7I27RA-Bxt7i5Mf.js → diagram-VX7I27RA-D8vq2Cmg.js} +1 -1
  103. package/dist/ui/assets/{diagram-Z3DM3KII-BReedDkK.js → diagram-Z3DM3KII-w6p5EbNr.js} +1 -1
  104. package/dist/ui/assets/{ebnfDiagram-PWID7BFC-Dz7S0T6B.js → ebnfDiagram-PWID7BFC-BYjT4y_m.js} +1 -1
  105. package/dist/ui/assets/{erDiagram-RLTQ6QDP-C2OhjIoP.js → erDiagram-RLTQ6QDP-CEELItZa.js} +1 -1
  106. package/dist/ui/assets/flowDiagram-HODETNUW-nKF0KGmt.js +1 -0
  107. package/dist/ui/assets/format-B31EJAKm.js +1 -0
  108. package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-X63oIy-b.js → ganttDiagram-EL5Y4UJY-j3mvcMyd.js} +1 -1
  109. package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-CjRtFyZs.js → gitGraphDiagram-WWUBYQGX-BfEOsCSC.js} +1 -1
  110. package/dist/ui/assets/index-CDFBWswc.css +1 -0
  111. package/dist/ui/assets/index-DKMEICXY.js +57 -0
  112. package/dist/ui/assets/{infoDiagram-27XIBGKW-D_WjlHCS.js → infoDiagram-27XIBGKW-CoKHZI4g.js} +1 -1
  113. package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-CcSnDibc.js → ishikawaDiagram-5VMMS53U-BzLGZ78I.js} +1 -1
  114. package/dist/ui/assets/{journeyDiagram-3NMN7TZE-C_cOiv3U.js → journeyDiagram-3NMN7TZE-DasBgTnF.js} +1 -1
  115. package/dist/ui/assets/{kanban-definition-UXKFOSKX-BE6bTzJ_.js → kanban-definition-UXKFOSKX-Bi2G6162.js} +1 -1
  116. package/dist/ui/assets/{line-B0YzR-ai.js → line-BD89T94A.js} +1 -1
  117. package/dist/ui/assets/{mermaid-parser.core-D-C4q-gD.js → mermaid-parser.core-CmPHHcqL.js} +1 -1
  118. package/dist/ui/assets/mermaid.core-B22Uko0X.js +11 -0
  119. package/dist/ui/assets/{mindmap-definition-YA3MSWOX-H_1t8eO8.js → mindmap-definition-YA3MSWOX-CmkalisH.js} +1 -1
  120. package/dist/ui/assets/{pegDiagram-XKGWAZYB-DEzpGr4J.js → pegDiagram-XKGWAZYB-B7n3kqND.js} +1 -1
  121. package/dist/ui/assets/{pieDiagram-E7YTZNPT-DzBWBOML.js → pieDiagram-E7YTZNPT-Dc8m07AW.js} +1 -1
  122. package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-BJNb82FM.js → quadrantDiagram-AXDQQJYC-C_-Mk5Ep.js} +1 -1
  123. package/dist/ui/assets/{railroadDiagram-O6MQD6OU-dHphbrs7.js → railroadDiagram-O6MQD6OU-DmT5wkuj.js} +1 -1
  124. package/dist/ui/assets/{repo-Rgx38OJH.js → repo-3D9Lh1aB.js} +1 -1
  125. package/dist/ui/assets/{requirementDiagram-BXWQKSXE-BpZeR6MN.js → requirementDiagram-BXWQKSXE-BERE-gY9.js} +1 -1
  126. package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-pfWwS_Ol.js → sankeyDiagram-P5KCCOFB-C51xkN9v.js} +1 -1
  127. package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-jX0ZWILZ.js → sequenceDiagram-WJ2MYXX4-DJYgfPQJ.js} +1 -1
  128. package/dist/ui/assets/{stateDiagram-D77RDMKH-cx9TocBZ.js → stateDiagram-D77RDMKH-BdMTpabl.js} +1 -1
  129. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BWp5c8Bl.js +1 -0
  130. package/dist/ui/assets/{swimlanes-42K2YHIH-D0HO4IaA.js → swimlanes-42K2YHIH-By3v_NQb.js} +1 -1
  131. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-D4WKOkfS.js +8 -0
  132. package/dist/ui/assets/text-insertion-sXVs8rKF.js +1 -0
  133. package/dist/ui/assets/{timeline-definition-24CTP7MA-CU20d-G7.js → timeline-definition-24CTP7MA-DgqMOYEl.js} +1 -1
  134. package/dist/ui/assets/useChatScroll-BiQjE1tB.js +1 -0
  135. package/dist/ui/assets/{vennDiagram-4TSXK5OY-Du2dqf4k.js → vennDiagram-4TSXK5OY-Cfy1llHd.js} +1 -1
  136. package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-CsiwW_49.js → wardleyDiagram-VM6X3IG4-Z5UvJKtg.js} +1 -1
  137. package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-8lEaXvDi.js → xychartDiagram-S5SC5T6Z-D4EkuQPv.js} +1 -1
  138. package/dist/ui/index.html +11 -8
  139. package/dist/ui/sw.js +2 -2
  140. package/package.json +1 -1
  141. package/dist/ui/assets/AgentsView-0r3QjL0C.css +0 -1
  142. package/dist/ui/assets/AgentsView-ChOMboOE.js +0 -1
  143. package/dist/ui/assets/ContextView-Dx2siGFv.js +0 -4
  144. package/dist/ui/assets/DashboardView-DP81OKcA.js +0 -1
  145. package/dist/ui/assets/InputsView-B79I4jwu.js +0 -1
  146. package/dist/ui/assets/SearchOverlay-0rM6zfNm.js +0 -1
  147. package/dist/ui/assets/StoriesView-BazXnyDz.js +0 -1
  148. package/dist/ui/assets/VoiceDictate-Bc00yWSj.css +0 -1
  149. package/dist/ui/assets/channel-CKK9ZV4r.js +0 -1
  150. package/dist/ui/assets/chunk-TLUHSLCS-CzTkKr54.js +0 -2
  151. package/dist/ui/assets/classDiagram-ZZMXUADV-AoAfBymA.js +0 -1
  152. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-AoAfBymA.js +0 -1
  153. package/dist/ui/assets/flowDiagram-HODETNUW-DWZFA4j8.js +0 -1
  154. package/dist/ui/assets/index-BBJ0SQAG.css +0 -1
  155. package/dist/ui/assets/index-DpAh1097.js +0 -57
  156. package/dist/ui/assets/mermaid.core-BinXQHOy.js +0 -11
  157. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-C8aOH2zy.js +0 -1
  158. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-TUNEXRA4.js +0 -8
  159. package/dist/ui/assets/useChatScroll-BD10j3cq.js +0 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "hash": "b009708e237c666064aee174b838afdb790721e2fdef67d53c2317781afd5073",
3
- "version": "0.5.69",
2
+ "hash": "eacca03dfc8be35b1f9d1238a9ac18cbdaf50c0d39cab898568df9fc589d514a",
3
+ "version": "0.5.71",
4
4
  "devUi": false
5
5
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "generatedAt": "2026-10-11T01:57:56.517Z"
2
+ "generatedAt": "2026-10-13T19:22:09.840Z"
3
3
  }
package/dist/cli/help.js CHANGED
@@ -12,7 +12,7 @@ import { c } from "./colors.js";
12
12
  import { table, termWidth, visibleWidth, wrap } from "./layout.js";
13
13
  import { NEW_FLAG_HELP, UPDATE_FLAG_HELP, flagSummary, flagUsageTokens } from "./task-flags.js";
14
14
  /** Commands whose own argument parser already handles `--help`. */
15
- export const SELF_HELP_COMMANDS = new Set(["support", "tunnel", "shot", "uninstall"]);
15
+ export const SELF_HELP_COMMANDS = new Set(["support", "tunnel", "shot", "uninstall", "override"]);
16
16
  const GROUPS = [
17
17
  {
18
18
  name: "Setup",
@@ -88,9 +88,9 @@ const GROUPS = [
88
88
  },
89
89
  {
90
90
  name: "message",
91
- usage: 'message <id> "<text>" [--json] [--port N]',
92
- desc: "Send a follow-up to the task agent (POST /api/tasks/:id/message)",
93
- flags: "--json · --port",
91
+ usage: 'message <id> "<text>" [--now] [--json] [--port N]',
92
+ desc: "Send a follow-up to the task agent; queues while busy, --now interrupts the turn and delivers immediately (POST /api/tasks/:id/message)",
93
+ flags: "--now · --json · --port",
94
94
  },
95
95
  {
96
96
  name: "review",
@@ -107,7 +107,7 @@ const GROUPS = [
107
107
  {
108
108
  name: "override",
109
109
  usage: "override <id> [--cli <name>] [--model <model>] [--json] [--port N]",
110
- desc: "Set per-task engineer overrides (PATCH /api/tasks/:id)",
110
+ desc: "Set per-task engineer overrides (PATCH /api/tasks/:id). --help prints the agent-driver budget allowlist",
111
111
  flags: "--cli · --model · --json · --port",
112
112
  },
113
113
  {
@@ -33,6 +33,14 @@ export const NEW_FLAG_HELP = {
33
33
  paths: { value: "a,b", desc: "Files the task touches, e.g. src/a.ts,src/b.ts" },
34
34
  body: { value: '"..."|-', desc: "Task body; `-` reads it from stdin" },
35
35
  shots: { value: '"<JSON list>"|-', desc: "Declare the ## Shots section (validated JSON)" },
36
+ screenshot: {
37
+ value: "path.png",
38
+ desc: "Attach a local image to ## Screenshots (repeatable)",
39
+ },
40
+ caption: {
41
+ value: '"..."',
42
+ desc: "Alt text for a single --screenshot image",
43
+ },
36
44
  };
37
45
  /** Every flag `repoos update` accepts, in help order. */
38
46
  export const UPDATE_FLAG_HELP = {
@@ -52,6 +60,14 @@ export const UPDATE_FLAG_HELP = {
52
60
  questions: { value: '"Q1\\nQ2"', desc: "Open questions for the human (newline or JSON list)" },
53
61
  "clear-questions": { desc: "Clear the recorded questions" },
54
62
  shots: { value: '"<JSON list>"|-', desc: "Declare the ## Shots section (validated JSON)" },
63
+ screenshot: {
64
+ value: "path.png",
65
+ desc: "Attach a local image to ## Screenshots (repeatable)",
66
+ },
67
+ caption: {
68
+ value: '"..."',
69
+ desc: "Alt text for a single --screenshot image",
70
+ },
55
71
  section: { value: '"heading"', desc: "Replace/create one ## section (pair with --section-body)" },
56
72
  "section-body": { value: '"..."|-', desc: "Body for --section; `-` reads it from stdin" },
57
73
  force: { desc: "Allow a --body replace that drops spec headings" },
@@ -6,7 +6,7 @@ import { pollUntil, RepoOsApi, RepoOsApiError, resolveControlPlaneBase, } from "
6
6
  import { effectiveEngineerFromApi } from "../cli/effective-from-api.js";
7
7
  import { HANDOFF_DEADLINE_MS } from "../server/handoff.js";
8
8
  function parseCommonFlags(args) {
9
- const opts = { json: false, wait: false };
9
+ const opts = { json: false, wait: false, now: false };
10
10
  const rest = [];
11
11
  for (let i = 0; i < args.length; i++) {
12
12
  const a = args[i];
@@ -14,6 +14,8 @@ function parseCommonFlags(args) {
14
14
  opts.json = true;
15
15
  else if (a === "--wait")
16
16
  opts.wait = true;
17
+ else if (a === "--now")
18
+ opts.now = true;
17
19
  else if (a === "--port") {
18
20
  const p = args[++i];
19
21
  const n = Number(p);
@@ -29,6 +31,21 @@ function parseCommonFlags(args) {
29
31
  function printJson(data) {
30
32
  console.log(JSON.stringify(data, null, 2));
31
33
  }
34
+ /**
35
+ * Print the agent-driver budget allowlist so a driver can choose an allowed
36
+ * pair before running `repoos override` (#0764). Reads the local config file —
37
+ * no server round-trip.
38
+ */
39
+ function printBudgetAllowlist(config) {
40
+ const rows = config.budget?.allowlist ?? [];
41
+ if (!rows.length) {
42
+ console.log(c.dim(" Budget allowlist: (empty — agent drivers are not restricted to any agent+model pair)"));
43
+ return;
44
+ }
45
+ console.log(c.dim(" Budget allowlist for agent drivers — allowed agent + model pairs:"));
46
+ for (const row of rows)
47
+ console.log(` · ${row.cli} + ${row.models.join(", ")}`);
48
+ }
32
49
  function printErr(message) {
33
50
  console.error(c.red(" ✗ ") + message);
34
51
  }
@@ -133,18 +150,30 @@ export async function cmdMessage(args) {
133
150
  const id = rest[0];
134
151
  const text = rest.slice(1).join(" ").trim();
135
152
  if (!id || !text) {
136
- printErr('Usage: repoos message <id> "<text>" [--json] [--port N]');
153
+ printErr('Usage: repoos message <id> "<text>" [--now] [--json] [--port N]');
137
154
  return 1;
138
155
  }
139
156
  const api = new RepoOsApi({ port: opts.port });
140
157
  try {
141
158
  await ensureServer(api);
142
- const data = await api.requestOk("POST", `/api/tasks/${id}/message`, { text });
159
+ const data = await api.requestOk("POST", `/api/tasks/${id}/message`, {
160
+ text,
161
+ ...(opts.now ? { now: true } : {}),
162
+ });
143
163
  if (opts.json)
144
164
  printJson(data);
165
+ else if (data?.queued) {
166
+ const why = data.now
167
+ ? (data.reason ?? "agent still busy after the interrupt")
168
+ : "agent busy — delivers when the turn ends";
169
+ console.log(c.green(" ✓ ") + c.dim(`message queued to #${id} (${why})`));
170
+ }
171
+ else if (data?.deliveredNow) {
172
+ const how = data.interrupted ? " (interrupted the running turn)" : " (agent was idle)";
173
+ console.log(c.green(" ✓ ") + c.dim(`message delivered now to #${id}${how}`));
174
+ }
145
175
  else {
146
- const status = data?.queued ? "queued" : "sent";
147
- console.log(c.green(" ✓ ") + c.dim(`message ${status} to #${id}`));
176
+ console.log(c.green(" ✓ ") + c.dim(`message sent to #${id}`));
148
177
  }
149
178
  return 0;
150
179
  }
@@ -290,10 +319,14 @@ export async function cmdOverride(args) {
290
319
  printErr(error);
291
320
  return 1;
292
321
  }
293
- const id = rest[0];
294
- if (!id) {
295
- printErr("Usage: repoos override <id> [--cli <name>] [--model <model>] [--json] [--port N]");
296
- return 1;
322
+ const wantsHelp = rest.includes("--help") || rest.includes("-h");
323
+ const id = rest.find((a) => !a.startsWith("-"));
324
+ if (!id || wantsHelp) {
325
+ const { config } = resolveControlPlaneBase({ port: opts.port });
326
+ console.log(c.cyan("Usage:") +
327
+ " repoos override <id> [--cli <name>] [--model <model>] [--json] [--port N]");
328
+ printBudgetAllowlist(config);
329
+ return wantsHelp ? 0 : 1;
297
330
  }
298
331
  let cli;
299
332
  let model;
@@ -253,6 +253,15 @@ a candidate merge. Do not erase a task's branch metadata to force this path.
253
253
  must use those same RepoOS CLI commands or HTTP API endpoints for task body,
254
254
  metadata, and status changes; it must never edit task Markdown directly.
255
255
  - **Never** deploy to production without human sign-off.
256
+ - **Attach the human's screenshots to the task.** When a task comes from a
257
+ request that included images, pass every one with
258
+ \`repoos new "<title>" --screenshot path.png\` (repeat \`--screenshot\` per
259
+ image) or \`repoos update <id> --screenshot path.png\` (\`--caption "..."\`
260
+ labels a single image). The command stores the files under the gitignored
261
+ \`${workDir}/.attachments/<id>/\` and writes the task's \`## Screenshots\`
262
+ section itself — never \`git add\` the pixels, never hand-copy them, never
263
+ edit that section by hand. Say in your reply which images you attached.
264
+ An image the human gave you is an image that lands on the task.
256
265
  - Keep frontmatter tidy; \`repoos\` will normalize key order on write.
257
266
  - One task = one focused worktree.
258
267
  - **Never invent evidence.** Do not fabricate device sessions, live API results,
@@ -452,6 +461,16 @@ function previewTargetRows(areas) {
452
461
  `# areas = ${areaList}`,
453
462
  `# command = "bun run dev --port {port} --host {host}"`,
454
463
  `# readyTimeoutMs = 240000 # raise for a command that also builds`,
464
+ "",
465
+ "# No dev server (native macOS, iOS/Android, desktop)? Declare a command",
466
+ "# capture target instead — the command renders image files into",
467
+ "# REPOOS_SHOT_DIR and RepoOS collects them as the task's evidence shots.",
468
+ "# [[shot.targets]]",
469
+ `# name = "macOS app"`,
470
+ `# areas = ${areaList}`,
471
+ `# paths = ["Sources/**"] # changed files that make this the target`,
472
+ `# command = "swift test --filter RenderShots"`,
473
+ `# outputGlob = ["*.png"]`,
455
474
  ].join("\n");
456
475
  }
457
476
  const ENV_EXAMPLE = `# Copy to .env and fill in what you need — .env is gitignored, this file is
@@ -0,0 +1,36 @@
1
+ import type { RepoOSConfig, Task } from "../core/types.js";
2
+ import { type ScreenshotMeta } from "../server/attachments.js";
3
+ /** One local image read from disk, ready for `saveScreenshot`. */
4
+ export interface ScreenshotInput {
5
+ name: string;
6
+ mime: string;
7
+ /** Base64-encoded file bytes. */
8
+ data: string;
9
+ }
10
+ /**
11
+ * Read and validate one local image file. Validation mirrors the upload route:
12
+ * the extension must map to an allowed MIME type, the file must exist and be a
13
+ * regular file, be non-empty, and be within the size limit. Never throws —
14
+ * problems come back as `{ error }` with the offending path named.
15
+ */
16
+ export declare function readScreenshotFile(filePath: string): ScreenshotInput | {
17
+ error: string;
18
+ };
19
+ /** Read every `--screenshot` path, failing fast on the first bad one. */
20
+ export declare function readScreenshotFiles(paths: string[]): {
21
+ inputs: ScreenshotInput[];
22
+ } | {
23
+ error: string;
24
+ };
25
+ /**
26
+ * Store every local image on a task and extend its `## Screenshots` section in
27
+ * one patch (numbering continues from whatever the task already has). The
28
+ * images are gitignored storage under `work/.attachments/<id>/`; only the
29
+ * markdown links land in the task file. Reports the first failure rather than
30
+ * leaving a partial attach silent, and never half-writes the body.
31
+ */
32
+ export declare function attachScreenshots(config: RepoOSConfig, task: Task, paths: string[], caption?: string): {
33
+ metas: ScreenshotMeta[];
34
+ } | {
35
+ error: string;
36
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `--screenshot` support for `repoos new` / `repoos update` (#0763).
3
+ *
4
+ * An agent that is handed images in chat needs a way to attach them when it
5
+ * creates or updates the task, without hand-copying files into
6
+ * `work/.attachments/<id>/` and guessing at the `## Screenshots` markdown. This
7
+ * module is the one place that turns local image paths into stored attachments:
8
+ * it validates each file against the SAME allowlist and size limit the HTTP
9
+ * upload route uses ({@link saveScreenshot}), stores them through the same
10
+ * storage helper, and extends the task body through the same
11
+ * `patchTaskFile({ addScreenshots })` path the API route uses. No storage logic
12
+ * is duplicated here.
13
+ *
14
+ * The write is local, exactly like the rest of `repoos new` / `repoos update`:
15
+ * it works with no server running, and equally against a running server (which
16
+ * picks the file change up through its work-dir watcher). A caller that wants
17
+ * the server to have the task in the same request can post `attachments` to
18
+ * `POST /api/tasks` instead — both paths land in the same storage.
19
+ */
20
+ import { existsSync, readFileSync, statSync } from "node:fs";
21
+ import { extname } from "node:path";
22
+ import { MAX_SCREENSHOT_BYTES, mimeForExtension, saveScreenshot, } from "../server/attachments.js";
23
+ import { patchTaskFile } from "../server/write.js";
24
+ /** How many bytes are too many, as human words for the error message. */
25
+ function maxMb() {
26
+ return Math.round(MAX_SCREENSHOT_BYTES / 1024 / 1024);
27
+ }
28
+ /**
29
+ * Read and validate one local image file. Validation mirrors the upload route:
30
+ * the extension must map to an allowed MIME type, the file must exist and be a
31
+ * regular file, be non-empty, and be within the size limit. Never throws —
32
+ * problems come back as `{ error }` with the offending path named.
33
+ */
34
+ export function readScreenshotFile(filePath) {
35
+ if (!filePath || filePath.trim() === "") {
36
+ return { error: "empty --screenshot path" };
37
+ }
38
+ if (!existsSync(filePath)) {
39
+ return { error: `image not found: ${filePath}` };
40
+ }
41
+ const stat = statSync(filePath);
42
+ if (!stat.isFile()) {
43
+ return { error: `not a file: ${filePath}` };
44
+ }
45
+ const mime = mimeForExtension(filePath);
46
+ if (!mime) {
47
+ return {
48
+ error: `unsupported image type "${extname(filePath) || filePath}" — supported: .png, .jpg, .jpeg, .gif, .webp, .avif, .bmp`,
49
+ };
50
+ }
51
+ const bytes = readFileSync(filePath);
52
+ if (bytes.length === 0) {
53
+ return { error: `image is empty: ${filePath}` };
54
+ }
55
+ if (bytes.length > MAX_SCREENSHOT_BYTES) {
56
+ return { error: `image is too large (max ${maxMb()} MB): ${filePath}` };
57
+ }
58
+ return { name: filePath, mime, data: bytes.toString("base64") };
59
+ }
60
+ /** Read every `--screenshot` path, failing fast on the first bad one. */
61
+ export function readScreenshotFiles(paths) {
62
+ const inputs = [];
63
+ for (const path of paths) {
64
+ const result = readScreenshotFile(path);
65
+ if ("error" in result)
66
+ return { error: result.error };
67
+ inputs.push(result);
68
+ }
69
+ return { inputs };
70
+ }
71
+ /** Trim a caption into safe markdown alt text, or null when blank. */
72
+ function normalizeCaption(caption) {
73
+ const trimmed = (caption ?? "").replace(/[[\]]/g, "").replace(/\s+/g, " ").trim();
74
+ return trimmed || null;
75
+ }
76
+ /**
77
+ * Store every local image on a task and extend its `## Screenshots` section in
78
+ * one patch (numbering continues from whatever the task already has). The
79
+ * images are gitignored storage under `work/.attachments/<id>/`; only the
80
+ * markdown links land in the task file. Reports the first failure rather than
81
+ * leaving a partial attach silent, and never half-writes the body.
82
+ */
83
+ export function attachScreenshots(config, task, paths, caption) {
84
+ const read = readScreenshotFiles(paths);
85
+ if ("error" in read)
86
+ return { error: read.error };
87
+ const label = normalizeCaption(caption);
88
+ const metas = [];
89
+ for (const input of read.inputs) {
90
+ const saved = saveScreenshot(config, task, input);
91
+ if ("error" in saved)
92
+ return { error: saved.error };
93
+ metas.push(label ? { ...saved, name: label } : saved);
94
+ }
95
+ if (metas.length === 0)
96
+ return { metas };
97
+ patchTaskFile(config, task.absPath, { addScreenshots: metas });
98
+ return { metas };
99
+ }
@@ -11,6 +11,10 @@ interface CmdShotOptions {
11
11
  height: number;
12
12
  };
13
13
  fullPage: boolean;
14
+ /** Extra env for a command capture target (#0769), from repeated `--env KEY=VAL`. */
15
+ env?: Record<string, string>;
16
+ /** Per-entry output glob for a command capture target (#0769), from `--glob`. */
17
+ glob?: string;
14
18
  /** Set when an argument is unknown or malformed; cmdShot prints and exits. */
15
19
  error?: string;
16
20
  /** Set for `-h`/`--help`; cmdShot prints usage and exits 0. */
@@ -38,6 +42,12 @@ export interface CliShotPlanInput {
38
42
  matchedGlobs: ReadonlyMap<string, string[]>;
39
43
  /** Targets whose matched files are docs content only (#0603). */
40
44
  docsContentOnly: ReadonlySet<string>;
45
+ /** Command capture target names (#0761) — no route, run a command instead. */
46
+ commandTargets?: ReadonlySet<string>;
47
+ /** Extra env for a command capture target (#0769), from `--env KEY=VAL`. */
48
+ env?: Record<string, string>;
49
+ /** Per-entry output glob for a command capture target (#0769), from `--glob`. */
50
+ glob?: string;
41
51
  }
42
52
  /** The plan plus the visible notes `cmdShot` prints alongside it. */
43
53
  export interface CliShotPlan {
@@ -15,15 +15,18 @@
15
15
  * overrides; area resolution is the fallback. Missing Playwright is a clean
16
16
  * skip with install advice, not a crash.
17
17
  */
18
- import { basename } from "node:path";
18
+ import { basename, extname, join } from "node:path";
19
+ import { rmSync } from "node:fs";
19
20
  import { c } from "../cli/colors.js";
20
21
  import { findRepoRoot, loadConfig, mainCheckoutRoot, resolveServePort } from "../core/config.js";
21
22
  import { changedPathsVsBase, currentBranch } from "../core/git.js";
22
23
  import { createRepoOS } from "../core/repoos.js";
23
24
  import { shotCapturePageUrl } from "../core/shot-fixtures.js";
24
25
  import { captureShotPage } from "../core/shot-page.js";
26
+ import { runShotCommand, slugName } from "../core/shot-command.js";
27
+ import { shotsDir } from "../server/shots.js";
25
28
  import { buildCapturePlan, parseShotPlan, provenanceCaption, } from "../core/shot-plan.js";
26
- import { describeTargetPathMatches, formatTargetList, resolveShotTargets, } from "../core/shot-targets.js";
29
+ import { describeShotTargetPathMatches, describeTargetPathMatches, formatTargetList, resolveShotTargets, shotTargetByName, } from "../core/shot-targets.js";
27
30
  import { readServeLocks } from "./status.js";
28
31
  import { readLocalCliToken } from "../server/local-token.js";
29
32
  import { launchWebkit } from "./ui-harness.js";
@@ -74,6 +77,24 @@ export function parseShotArgs(args) {
74
77
  return opts;
75
78
  opts.selector = v;
76
79
  }
80
+ else if (a === "--env") {
81
+ const v = value(a);
82
+ if (v === undefined)
83
+ return opts;
84
+ const eq = v.indexOf("=");
85
+ const key = eq > 0 ? v.slice(0, eq) : "";
86
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
87
+ opts.error = `--env expects KEY=VALUE with a valid variable name, got "${v}"`;
88
+ return opts;
89
+ }
90
+ opts.env = { ...opts.env, [key]: v.slice(eq + 1) };
91
+ }
92
+ else if (a === "--glob") {
93
+ const v = value(a);
94
+ if (v === undefined)
95
+ return opts;
96
+ opts.glob = v;
97
+ }
77
98
  else if (a === "--task") {
78
99
  const v = value(a);
79
100
  if (v === undefined)
@@ -160,8 +181,11 @@ export function buildCliShotPlan(input) {
160
181
  const built = buildCapturePlan(input.targets, declared.shots, {
161
182
  matchedGlobs: input.matchedGlobs,
162
183
  docsContentOnly: input.docsContentOnly,
184
+ ...(input.commandTargets ? { commandTargets: input.commandTargets } : {}),
163
185
  route: input.route,
164
186
  selector: input.selector,
187
+ ...(input.env ? { env: input.env } : {}),
188
+ ...(input.glob ? { glob: input.glob } : {}),
165
189
  });
166
190
  return {
167
191
  entries: built.entries,
@@ -239,11 +263,12 @@ async function uploadShot(port, taskId, token, input) {
239
263
  headers: cliHeaders(token, true),
240
264
  body: JSON.stringify({
241
265
  target: input.target,
242
- route: input.route,
266
+ ...(input.route ? { route: input.route } : {}),
267
+ ...(input.command ? { command: input.command } : {}),
243
268
  ...(input.label ? { label: input.label } : {}),
244
269
  ...(input.provenance ? { provenance: input.provenance } : {}),
245
- name: input.target,
246
- mime: "image/png",
270
+ name: input.name ?? input.target,
271
+ mime: input.mime ?? "image/png",
247
272
  data: input.data,
248
273
  ...(input.warnings?.length ? { warnings: input.warnings } : {}),
249
274
  }),
@@ -287,6 +312,14 @@ export async function cmdShot(args) {
287
312
  target's paths; diffs touching just tests or task notes stand down with a
288
313
  visible "shots: skipped — no UI change to capture" note instead.
289
314
 
315
+ A repo with no dev server (a native macOS, iOS/Android or desktop app) can
316
+ declare a command capture target in repoos.toml as a [[shot.targets]] row —
317
+ a command that renders image files into REPOOS_SHOT_DIR. --target <name>
318
+ selects one, and its images are stored like any other shot; no browser is
319
+ needed. --env KEY=VALUE and --glob narrow what such a target renders and which
320
+ produced files are kept (a declared ## Shots entry can carry the same as
321
+ "env"/"args"/"glob"). See user-docs/configuration.md.
322
+
290
323
  An explicit <route> or --selector replaces the declared list outright, and
291
324
  applies to every resolved target: declared entries carry steps, a highlight
292
325
  and a label written for their own route, so they are never re-pointed at a
@@ -300,6 +333,9 @@ export async function cmdShot(args) {
300
333
  Flags:
301
334
  --target <name> Preview target to capture (overrides path/area resolution)
302
335
  --selector <css> Capture one element instead of the whole page
336
+ --env KEY=VAL (command target) pass an env var to the capture command;
337
+ repeat per variable, e.g. --env DIVOT_SNAPSHOT_ONLY=worldclock
338
+ --glob <glob> (command target) collect only produced files matching this glob
303
339
  --task <id> Task to shoot (defaults to the current worktree's branch)
304
340
  --base <ref> Base branch for changed-path resolution (default "main")
305
341
  --wait <ms> Settle time after load before capturing (default 900)
@@ -309,7 +345,7 @@ export async function cmdShot(args) {
309
345
  }
310
346
  if (opts.error) {
311
347
  console.error(c.red(" ✗ ") + opts.error);
312
- console.error(c.dim(" usage: repoos shot [<route|url>] [--target <name>] [--selector <css>]"));
348
+ console.error(c.dim(" usage: repoos shot [<route|url>] [--target <name>] [--selector <css>] [--env K=V] [--glob <glob>]"));
313
349
  return 1;
314
350
  }
315
351
  const cwdRoot = findRepoRoot();
@@ -355,7 +391,7 @@ export async function cmdShot(args) {
355
391
  console.error(c.red(" ✗ ") + `Base ref "${opts.base}" is not a commit — cannot resolve changed paths.`);
356
392
  return 1;
357
393
  }
358
- const resolution = resolveShotTargets(worktreeConfig.preview, task.area, changed, opts.target);
394
+ const resolution = resolveShotTargets(worktreeConfig.preview, task.area, changed, opts.target, worktreeConfig.shot);
359
395
  if (resolution.names.length === 0) {
360
396
  console.error(c.red(" ✗ ") + (resolution.reason ?? "No preview target resolved."));
361
397
  if (resolution.detected.length > 0) {
@@ -386,6 +422,12 @@ export async function cmdShot(args) {
386
422
  if (match.contentOnly)
387
423
  docsContentOnly.add(match.target);
388
424
  }
425
+ for (const match of describeShotTargetPathMatches(worktreeConfig.shot, changed)) {
426
+ matchedGlobs.set(match.target, match.globs);
427
+ if (match.contentOnly)
428
+ docsContentOnly.add(match.target);
429
+ }
430
+ const commandTargets = new Set((worktreeConfig.shot?.targets ?? []).map((t) => t.name));
389
431
  // #0610: the requested route/selector is part of the plan, not a separate
390
432
  // candidate that lost to the fallback's `/`.
391
433
  const built = buildCliShotPlan({
@@ -395,6 +437,9 @@ export async function cmdShot(args) {
395
437
  targets,
396
438
  matchedGlobs,
397
439
  docsContentOnly,
440
+ commandTargets,
441
+ ...(opts.env ? { env: opts.env } : {}),
442
+ ...(opts.glob ? { glob: opts.glob } : {}),
398
443
  });
399
444
  for (const error of built.declaredErrors) {
400
445
  console.error(c.yellow(" · ") + `declared shot list: ${error}`);
@@ -431,29 +476,34 @@ export async function cmdShot(args) {
431
476
  `RepoOS server is not reachable on ${baseUrl}. Start it with \`repoos serve\`.`);
432
477
  return 1;
433
478
  }
479
+ const commandTargets = new Set((worktreeConfig.shot?.targets ?? []).map((t) => t.name));
480
+ const webPlan = plan.filter((e) => !commandTargets.has(e.target));
481
+ const commandPlan = plan.filter((e) => commandTargets.has(e.target));
434
482
  let browser;
435
483
  let context;
436
484
  const saved = [];
437
485
  try {
438
- try {
439
- browser = await launchWebkit();
440
- context = await browser.newContext({ serviceWorkers: "block" });
441
- }
442
- catch (err) {
443
- const msg = err.message;
444
- if (isShotCaptureUnavailable(err)) {
445
- console.error(c.yellow(" · ") + "Screenshot capture skipped — Playwright/WebKit unavailable");
446
- console.error(c.dim(` ${msg.split("\n")[0]}`));
447
- console.error(c.dim(` ${INSTALL_ADVICE}`));
486
+ if (webPlan.length > 0) {
487
+ try {
488
+ browser = await launchWebkit();
489
+ context = await browser.newContext({ serviceWorkers: "block" });
448
490
  }
449
- else {
450
- console.error(c.red(" ✗ ") + `could not launch the browser: ${msg.split("\n")[0]}`);
491
+ catch (err) {
492
+ const msg = err.message;
493
+ if (isShotCaptureUnavailable(err)) {
494
+ console.error(c.yellow(" · ") + "Screenshot capture skipped — Playwright/WebKit unavailable");
495
+ console.error(c.dim(` ${msg.split("\n")[0]}`));
496
+ console.error(c.dim(` ${INSTALL_ADVICE}`));
497
+ }
498
+ else {
499
+ console.error(c.red(" ✗ ") + `could not launch the browser: ${msg.split("\n")[0]}`);
500
+ }
501
+ return 1;
451
502
  }
452
- return 1;
453
503
  }
454
504
  let currentTarget;
455
505
  let currentPreviewUrl = "";
456
- for (const entry of plan) {
506
+ for (const entry of webPlan) {
457
507
  // Only one preview runs per task, so a different target must replace
458
508
  // the running one before it can start (#0379/#0271). Same-target
459
509
  // entries reuse the running preview — no restart, no churn.
@@ -467,26 +517,27 @@ export async function cmdShot(args) {
467
517
  currentPreviewUrl = started.url.replace(/\/$/, "");
468
518
  currentTarget = entry.target;
469
519
  }
470
- const pageUrl = absolute ? entry.route : shotCapturePageUrl(currentPreviewUrl, entry);
520
+ const route = entry.route ?? "/";
521
+ const pageUrl = absolute ? route : shotCapturePageUrl(currentPreviewUrl, { ...entry, route });
471
522
  const page = (await context.newPage());
472
523
  let png;
473
524
  const warnings = [];
474
525
  try {
475
526
  await page.setViewportSize(opts.viewport);
476
527
  png = await captureShotPage(page, pageUrl, entry, { waitMs: opts.waitMs, fullPage: opts.fullPage }, {
477
- onHighlightMiss: (selector, route) => {
478
- warnings.push(`highlight ${selector} matched nothing on ${route}`);
528
+ onHighlightMiss: (selector, r) => {
529
+ warnings.push(`highlight ${selector} matched nothing on ${r}`);
479
530
  console.error(c.yellow(" · ") +
480
- `highlight "${selector}" matched nothing on ${route} — capture went ahead unhighlighted`);
531
+ `highlight "${selector}" matched nothing on ${r} — capture went ahead unhighlighted`);
481
532
  },
482
- onSelectorMiss: (selector, route) => {
483
- warnings.push(`selector ${selector} matched nothing on ${route}`);
533
+ onSelectorMiss: (selector, r) => {
534
+ warnings.push(`selector ${selector} matched nothing on ${r}`);
484
535
  console.error(c.yellow(" · ") +
485
- `selector "${selector}" matched nothing on ${route} — capture used the whole window`);
536
+ `selector "${selector}" matched nothing on ${r} — capture used the whole window`);
486
537
  },
487
- onStepMiss: (message, route) => {
488
- warnings.push(`${message} on ${route}`);
489
- console.error(c.yellow(" · ") + `${message} on ${route} — capture went ahead`);
538
+ onStepMiss: (message, r) => {
539
+ warnings.push(`${message} on ${r}`);
540
+ console.error(c.yellow(" · ") + `${message} on ${r} — capture went ahead`);
490
541
  },
491
542
  });
492
543
  }
@@ -499,7 +550,7 @@ export async function cmdShot(args) {
499
550
  await page.close();
500
551
  const uploaded = await uploadShot(port, task.id, token, {
501
552
  target: entry.target,
502
- route: entry.route.startsWith("/") ? normalizedRoute(entry.route) : entry.route,
553
+ route: route.startsWith("/") ? normalizedRoute(route) : route,
503
554
  ...(entry.label ? { label: entry.label } : {}),
504
555
  provenance: provenanceCaption(entry.provenance),
505
556
  data: png.toString("base64"),
@@ -519,6 +570,52 @@ export async function cmdShot(args) {
519
570
  console.log(c.green(" ✔ ") +
520
571
  `${entry.target}${entry.highlight ? " · highlighted" : ""}${entry.label ? ` – ${entry.label}` : ""} → ${c.cyan(uploaded.shot.path)} ${c.dim(`(${Math.round(png.length / 1024)} KB)`)}`);
521
572
  }
573
+ for (const entry of commandPlan) {
574
+ const target = shotTargetByName(worktreeConfig.shot, entry.target);
575
+ if (!target) {
576
+ console.error(c.red(" ✗ ") + `command capture target "${entry.target}" is not configured`);
577
+ return 1;
578
+ }
579
+ const outputDir = join(shotsDir(worktreeRoot, worktreeConfig.workDir, task.id), "cmd", slugName(entry.target));
580
+ const outcome = runShotCommand(target, worktreeRoot, outputDir, {
581
+ ...(entry.env ? { env: entry.env } : {}),
582
+ ...(entry.args ? { args: entry.args } : {}),
583
+ ...(entry.glob ? { outputGlob: [entry.glob] } : {}),
584
+ });
585
+ if (outcome.status !== "captured") {
586
+ console.error(c.yellow(" · ") + `shots: ${outcome.status} — ${outcome.reason}`);
587
+ continue;
588
+ }
589
+ for (const msg of outcome.warnings) {
590
+ console.error(c.yellow(" · ") + msg);
591
+ }
592
+ for (const file of outcome.files) {
593
+ const uploaded = await uploadShot(port, task.id, token, {
594
+ target: entry.target,
595
+ command: "command",
596
+ ...(entry.label ? { label: entry.label } : {}),
597
+ provenance: provenanceCaption(entry.provenance),
598
+ name: `${entry.target}-${basename(file.name, extname(file.name))}`,
599
+ mime: file.mime,
600
+ data: file.data.toString("base64"),
601
+ });
602
+ if (!uploaded.ok) {
603
+ console.error(c.red(" ✗ ") + `storing the shot failed: ${uploaded.error}`);
604
+ return 1;
605
+ }
606
+ saved.push({
607
+ target: entry.target,
608
+ ...(entry.label ? { label: entry.label } : {}),
609
+ file: uploaded.shot.name,
610
+ path: uploaded.shot.path,
611
+ url: uploaded.shot.url,
612
+ });
613
+ console.log(c.green(" ✔ ") +
614
+ `${entry.target}${entry.label ? ` – ${entry.label}` : ""} → ${c.cyan(uploaded.shot.path)} ${c.dim(`(${Math.round(file.data.length / 1024)} KB)`)}`);
615
+ }
616
+ // The raw command output is transient — the server now owns the bytes.
617
+ rmSync(outputDir, { recursive: true, force: true });
618
+ }
522
619
  if (saved.length === 0) {
523
620
  console.error(c.yellow(" · ") + "No shots captured.");
524
621
  return 1;