@intentic/sandbox-contract 1.316.0 → 1.318.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/README.md +12 -4
  2. package/dist/chores/chores.js +1 -1
  3. package/dist/chores/chores.js.map +1 -1
  4. package/dist/chores/probes.js +1 -1
  5. package/dist/chores/probes.js.map +1 -1
  6. package/dist/contracts/accounts.contract.d.ts +75 -0
  7. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  8. package/dist/contracts/accounts.contract.js +10 -1
  9. package/dist/contracts/accounts.contract.js.map +1 -1
  10. package/dist/contracts/agent.contract.d.ts +616 -1
  11. package/dist/contracts/agent.contract.d.ts.map +1 -1
  12. package/dist/contracts/agents.contract.d.ts +1134 -29
  13. package/dist/contracts/agents.contract.d.ts.map +1 -1
  14. package/dist/contracts/agents.contract.js +12 -12
  15. package/dist/contracts/agents.contract.js.map +1 -1
  16. package/dist/contracts/capabilities.contract.d.ts +16 -0
  17. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  18. package/dist/contracts/capabilities.contract.js +10 -1
  19. package/dist/contracts/capabilities.contract.js.map +1 -1
  20. package/dist/contracts/ci.contract.d.ts +22 -4
  21. package/dist/contracts/ci.contract.d.ts.map +1 -1
  22. package/dist/contracts/device.contract.d.ts +39 -0
  23. package/dist/contracts/device.contract.d.ts.map +1 -1
  24. package/dist/contracts/device.contract.js +2 -0
  25. package/dist/contracts/device.contract.js.map +1 -1
  26. package/dist/contracts/endpoints.contract.d.ts +23 -0
  27. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  28. package/dist/contracts/endpoints.contract.js +6 -1
  29. package/dist/contracts/endpoints.contract.js.map +1 -1
  30. package/dist/contracts/extensions.contract.d.ts +12 -0
  31. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  32. package/dist/contracts/needs.contract.d.ts +534 -0
  33. package/dist/contracts/needs.contract.d.ts.map +1 -0
  34. package/dist/contracts/needs.contract.js +80 -0
  35. package/dist/contracts/needs.contract.js.map +1 -0
  36. package/dist/contracts/providers.contract.d.ts +4 -0
  37. package/dist/contracts/providers.contract.d.ts.map +1 -1
  38. package/dist/contracts/runner.contract.d.ts +193 -2
  39. package/dist/contracts/runner.contract.d.ts.map +1 -1
  40. package/dist/contracts/secrets.contract.d.ts +53 -0
  41. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  42. package/dist/contracts/secrets.contract.js +31 -2
  43. package/dist/contracts/secrets.contract.js.map +1 -1
  44. package/dist/contracts/sessions.contract.d.ts +83 -0
  45. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  46. package/dist/contracts/settings.contract.d.ts +33 -2
  47. package/dist/contracts/settings.contract.d.ts.map +1 -1
  48. package/dist/contracts/settings.contract.js +20 -1
  49. package/dist/contracts/settings.contract.js.map +1 -1
  50. package/dist/contracts/system.contract.d.ts +177 -356
  51. package/dist/contracts/system.contract.d.ts.map +1 -1
  52. package/dist/contracts/system.contract.js +13 -12
  53. package/dist/contracts/system.contract.js.map +1 -1
  54. package/dist/contracts/translator.contract.d.ts +7 -0
  55. package/dist/contracts/translator.contract.d.ts.map +1 -1
  56. package/dist/contracts/translator.contract.js.map +1 -1
  57. package/dist/contracts/webext.contract.d.ts +38 -0
  58. package/dist/contracts/webext.contract.d.ts.map +1 -1
  59. package/dist/contracts/webext.contract.js +3 -0
  60. package/dist/contracts/webext.contract.js.map +1 -1
  61. package/dist/contracts/workspace.contract.d.ts +1 -80
  62. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  63. package/dist/contracts/workspace.contract.js +0 -27
  64. package/dist/contracts/workspace.contract.js.map +1 -1
  65. package/dist/events/agent-events.d.ts +330 -0
  66. package/dist/events/agent-events.d.ts.map +1 -1
  67. package/dist/events/agent-events.js +13 -0
  68. package/dist/events/agent-events.js.map +1 -1
  69. package/dist/events/agent-words.d.ts +1 -0
  70. package/dist/events/agent-words.d.ts.map +1 -1
  71. package/dist/events/agent-words.js +3 -2
  72. package/dist/events/agent-words.js.map +1 -1
  73. package/dist/events/land-breakage.d.ts.map +1 -1
  74. package/dist/events/land-breakage.js.map +1 -1
  75. package/dist/events/need-wake.d.ts +17 -0
  76. package/dist/events/need-wake.d.ts.map +1 -0
  77. package/dist/events/need-wake.js +34 -0
  78. package/dist/events/need-wake.js.map +1 -0
  79. package/dist/events/resume.d.ts +6 -1
  80. package/dist/events/resume.d.ts.map +1 -1
  81. package/dist/events/resume.js +17 -10
  82. package/dist/events/resume.js.map +1 -1
  83. package/dist/events/sandbox-notice.d.ts +183 -0
  84. package/dist/events/sandbox-notice.d.ts.map +1 -0
  85. package/dist/events/sandbox-notice.js +56 -0
  86. package/dist/events/sandbox-notice.js.map +1 -0
  87. package/dist/events/system-events.d.ts +84 -0
  88. package/dist/events/system-events.d.ts.map +1 -1
  89. package/dist/events/system-events.js +2 -0
  90. package/dist/events/system-events.js.map +1 -1
  91. package/dist/events/transcript.d.ts +508 -0
  92. package/dist/events/transcript.d.ts.map +1 -1
  93. package/dist/events/transcript.js +12 -1
  94. package/dist/events/transcript.js.map +1 -1
  95. package/dist/ids/conversation-ids.d.ts +1 -3
  96. package/dist/ids/conversation-ids.d.ts.map +1 -1
  97. package/dist/ids/conversation-ids.js +1 -3
  98. package/dist/ids/conversation-ids.js.map +1 -1
  99. package/dist/ids/project-dir.d.ts +6 -0
  100. package/dist/ids/project-dir.d.ts.map +1 -0
  101. package/dist/ids/project-dir.js +43 -0
  102. package/dist/ids/project-dir.js.map +1 -0
  103. package/dist/index.d.ts +3474 -1163
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +13 -4
  106. package/dist/index.js.map +1 -1
  107. package/dist/models/agent-runtimes.d.ts.map +1 -1
  108. package/dist/models/agent-runtimes.js +2 -2
  109. package/dist/models/agent-runtimes.js.map +1 -1
  110. package/dist/models/model-roles.d.ts +1 -9
  111. package/dist/models/model-roles.d.ts.map +1 -1
  112. package/dist/models/model-roles.js +1 -9
  113. package/dist/models/model-roles.js.map +1 -1
  114. package/dist/policy/command-classes.d.ts.map +1 -1
  115. package/dist/policy/command-classes.js +52 -3
  116. package/dist/policy/command-classes.js.map +1 -1
  117. package/dist/policy/safety-policy.d.ts +7 -1
  118. package/dist/policy/safety-policy.d.ts.map +1 -1
  119. package/dist/policy/safety-policy.js +2 -0
  120. package/dist/policy/safety-policy.js.map +1 -1
  121. package/dist/policy/secret-hosts.d.ts +13 -0
  122. package/dist/policy/secret-hosts.d.ts.map +1 -0
  123. package/dist/policy/secret-hosts.js +41 -0
  124. package/dist/policy/secret-hosts.js.map +1 -0
  125. package/dist/policy/turned-away.d.ts.map +1 -1
  126. package/dist/policy/turned-away.js +1 -0
  127. package/dist/policy/turned-away.js.map +1 -1
  128. package/dist/protocol/raw-routes.d.ts +7 -5
  129. package/dist/protocol/raw-routes.d.ts.map +1 -1
  130. package/dist/protocol/raw-routes.js +4 -1
  131. package/dist/protocol/raw-routes.js.map +1 -1
  132. package/dist/protocol/routes.d.ts +2 -6
  133. package/dist/protocol/routes.d.ts.map +1 -1
  134. package/dist/protocol/routes.js +2 -3
  135. package/dist/protocol/routes.js.map +1 -1
  136. package/dist/protocol/stream-of.d.ts +6 -0
  137. package/dist/protocol/stream-of.d.ts.map +1 -0
  138. package/dist/protocol/stream-of.js +4 -0
  139. package/dist/protocol/stream-of.js.map +1 -0
  140. package/dist/schemas/activity.js +1 -1
  141. package/dist/schemas/activity.js.map +1 -1
  142. package/dist/schemas/agent.d.ts +19 -2
  143. package/dist/schemas/agent.d.ts.map +1 -1
  144. package/dist/schemas/agent.js +2 -1
  145. package/dist/schemas/agent.js.map +1 -1
  146. package/dist/schemas/agents.d.ts +119 -0
  147. package/dist/schemas/agents.d.ts.map +1 -1
  148. package/dist/schemas/agents.js +45 -0
  149. package/dist/schemas/agents.js.map +1 -1
  150. package/dist/schemas/automations.d.ts +37 -0
  151. package/dist/schemas/automations.d.ts.map +1 -1
  152. package/dist/schemas/capabilities.d.ts +21 -0
  153. package/dist/schemas/capabilities.d.ts.map +1 -1
  154. package/dist/schemas/capabilities.js +50 -4
  155. package/dist/schemas/capabilities.js.map +1 -1
  156. package/dist/schemas/ci.d.ts +69 -9
  157. package/dist/schemas/ci.d.ts.map +1 -1
  158. package/dist/schemas/ci.js +29 -8
  159. package/dist/schemas/ci.js.map +1 -1
  160. package/dist/schemas/context-trim.d.ts +5 -1
  161. package/dist/schemas/context-trim.d.ts.map +1 -1
  162. package/dist/schemas/context-trim.js +8 -4
  163. package/dist/schemas/context-trim.js.map +1 -1
  164. package/dist/schemas/devices.d.ts +91 -0
  165. package/dist/schemas/devices.d.ts.map +1 -1
  166. package/dist/schemas/devices.js +23 -1
  167. package/dist/schemas/devices.js.map +1 -1
  168. package/dist/schemas/environment.d.ts +48 -0
  169. package/dist/schemas/environment.d.ts.map +1 -1
  170. package/dist/schemas/environment.js +15 -0
  171. package/dist/schemas/environment.js.map +1 -1
  172. package/dist/schemas/extension-updates.d.ts +24 -0
  173. package/dist/schemas/extension-updates.d.ts.map +1 -1
  174. package/dist/schemas/git/git.d.ts +13 -0
  175. package/dist/schemas/git/git.d.ts.map +1 -1
  176. package/dist/schemas/git/git.js +11 -1
  177. package/dist/schemas/git/git.js.map +1 -1
  178. package/dist/schemas/hosts.d.ts +4 -0
  179. package/dist/schemas/hosts.d.ts.map +1 -1
  180. package/dist/schemas/hosts.js +3 -1
  181. package/dist/schemas/hosts.js.map +1 -1
  182. package/dist/schemas/keep-warm.d.ts +4 -0
  183. package/dist/schemas/keep-warm.d.ts.map +1 -1
  184. package/dist/schemas/keep-warm.js +15 -5
  185. package/dist/schemas/keep-warm.js.map +1 -1
  186. package/dist/schemas/loopback-catch.d.ts +33 -0
  187. package/dist/schemas/loopback-catch.d.ts.map +1 -0
  188. package/dist/schemas/loopback-catch.js +20 -0
  189. package/dist/schemas/loopback-catch.js.map +1 -0
  190. package/dist/schemas/maintenance.js +1 -1
  191. package/dist/schemas/maintenance.js.map +1 -1
  192. package/dist/schemas/needs.d.ts +664 -0
  193. package/dist/schemas/needs.d.ts.map +1 -0
  194. package/dist/schemas/needs.js +203 -0
  195. package/dist/schemas/needs.js.map +1 -0
  196. package/dist/schemas/personas.d.ts.map +1 -1
  197. package/dist/schemas/personas.js +6 -3
  198. package/dist/schemas/personas.js.map +1 -1
  199. package/dist/schemas/providers/provider-oauth.d.ts +87 -0
  200. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  201. package/dist/schemas/providers/provider-oauth.js +20 -0
  202. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  203. package/dist/schemas/secrets.d.ts +84 -0
  204. package/dist/schemas/secrets.d.ts.map +1 -1
  205. package/dist/schemas/secrets.js +70 -0
  206. package/dist/schemas/secrets.js.map +1 -1
  207. package/dist/schemas/settings-history.d.ts +1 -1
  208. package/dist/schemas/settings-history.d.ts.map +1 -1
  209. package/dist/schemas/settings-history.js +1 -0
  210. package/dist/schemas/settings-history.js.map +1 -1
  211. package/dist/schemas/settings.d.ts +44 -2
  212. package/dist/schemas/settings.d.ts.map +1 -1
  213. package/dist/schemas/settings.js +30 -4
  214. package/dist/schemas/settings.js.map +1 -1
  215. package/dist/schemas/state-plan.d.ts +1 -0
  216. package/dist/schemas/state-plan.d.ts.map +1 -1
  217. package/dist/schemas/state-plan.js +3 -1
  218. package/dist/schemas/state-plan.js.map +1 -1
  219. package/dist/schemas/system.d.ts +20 -0
  220. package/dist/schemas/system.d.ts.map +1 -1
  221. package/dist/schemas/system.js +7 -0
  222. package/dist/schemas/system.js.map +1 -1
  223. package/dist/schemas/terminal.d.ts +2 -3
  224. package/dist/schemas/terminal.d.ts.map +1 -1
  225. package/dist/schemas/terminal.js +12 -4
  226. package/dist/schemas/terminal.js.map +1 -1
  227. package/dist/schemas/updates.d.ts +39 -0
  228. package/dist/schemas/updates.d.ts.map +1 -0
  229. package/dist/schemas/updates.js +31 -0
  230. package/dist/schemas/updates.js.map +1 -0
  231. package/dist/schemas/webext.d.ts +3 -0
  232. package/dist/schemas/webext.d.ts.map +1 -1
  233. package/dist/schemas/webext.js +1 -0
  234. package/dist/schemas/webext.js.map +1 -1
  235. package/dist/schemas/workflows.js +3 -3
  236. package/dist/schemas/workflows.js.map +1 -1
  237. package/dist/schemas/workspace/workspace-tree.d.ts +2 -0
  238. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  239. package/dist/schemas/workspace/workspace-tree.js +4 -0
  240. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  241. package/dist/state/definition.d.ts +28 -4
  242. package/dist/state/definition.d.ts.map +1 -1
  243. package/dist/state/history-state.d.ts.map +1 -1
  244. package/dist/state/history-state.js +10 -0
  245. package/dist/state/history-state.js.map +1 -1
  246. package/dist/state/runtime-state.d.ts +0 -3
  247. package/dist/state/runtime-state.d.ts.map +1 -1
  248. package/dist/state/runtime-state.js +0 -1
  249. package/dist/state/runtime-state.js.map +1 -1
  250. package/dist/state/workspace-state.d.ts +4 -5
  251. package/dist/state/workspace-state.d.ts.map +1 -1
  252. package/dist/state/workspace-state.js +1 -6
  253. package/dist/state/workspace-state.js.map +1 -1
  254. package/dist/text/documents.d.ts +3 -0
  255. package/dist/text/documents.d.ts.map +1 -1
  256. package/dist/text/documents.js +14 -0
  257. package/dist/text/documents.js.map +1 -1
  258. package/dist/text/transcript-fold.d.ts +4 -3
  259. package/dist/text/transcript-fold.d.ts.map +1 -1
  260. package/dist/text/transcript-fold.js +140 -31
  261. package/dist/text/transcript-fold.js.map +1 -1
  262. package/dist/webext/index.d.ts +1 -0
  263. package/dist/webext/index.d.ts.map +1 -1
  264. package/dist/webext/index.js +1 -0
  265. package/dist/webext/index.js.map +1 -1
  266. package/package.json +5 -5
  267. package/src/chores/chores.ts +2 -2
  268. package/src/chores/probes.ts +1 -1
  269. package/src/contracts/accounts.contract.ts +12 -0
  270. package/src/contracts/agents.contract.ts +13 -13
  271. package/src/contracts/capabilities.contract.ts +14 -1
  272. package/src/contracts/device.contract.ts +5 -0
  273. package/src/contracts/endpoints.contract.ts +20 -4
  274. package/src/contracts/needs.contract.ts +103 -0
  275. package/src/contracts/secrets.contract.ts +45 -3
  276. package/src/contracts/settings.contract.ts +29 -0
  277. package/src/contracts/system.contract.ts +19 -14
  278. package/src/contracts/translator.contract.ts +2 -1
  279. package/src/contracts/webext.contract.ts +7 -1
  280. package/src/contracts/workspace.contract.ts +0 -37
  281. package/src/events/agent-events.ts +22 -1
  282. package/src/events/agent-words.ts +6 -2
  283. package/src/events/land-breakage.ts +3 -4
  284. package/src/events/need-wake.test.ts +55 -0
  285. package/src/events/need-wake.ts +71 -0
  286. package/src/events/resume.test.ts +13 -2
  287. package/src/events/resume.ts +25 -12
  288. package/src/events/sandbox-notice.ts +92 -0
  289. package/src/events/system-events.ts +7 -0
  290. package/src/events/transcript.ts +23 -1
  291. package/src/events/verify-nudge.ts +3 -3
  292. package/src/ids/conversation-ids.test.ts +2 -26
  293. package/src/ids/conversation-ids.ts +6 -12
  294. package/src/ids/project-dir.fixture.json +65 -0
  295. package/src/ids/project-dir.test.ts +60 -0
  296. package/src/ids/project-dir.ts +70 -0
  297. package/src/index.ts +18 -4
  298. package/src/models/agent-catalog.test.ts +7 -1
  299. package/src/models/agent-runtimes.ts +7 -5
  300. package/src/models/model-roles.ts +1 -9
  301. package/src/policy/command-classes.test.ts +39 -0
  302. package/src/policy/command-classes.ts +70 -3
  303. package/src/policy/safety-policy.ts +10 -0
  304. package/src/policy/secret-hosts.test.ts +89 -0
  305. package/src/policy/secret-hosts.ts +73 -0
  306. package/src/policy/turned-away.ts +1 -0
  307. package/src/protocol/raw-routes.ts +6 -3
  308. package/src/protocol/routes.test.ts +6 -6
  309. package/src/protocol/routes.ts +2 -9
  310. package/src/protocol/stream-of.ts +13 -0
  311. package/src/schemas/activity.ts +1 -1
  312. package/src/schemas/agent.ts +4 -1
  313. package/src/schemas/agents.ts +72 -5
  314. package/src/schemas/automations.ts +2 -2
  315. package/src/schemas/capabilities.ts +72 -7
  316. package/src/schemas/ci.ts +56 -11
  317. package/src/schemas/context-trim.ts +11 -6
  318. package/src/schemas/devices.ts +55 -2
  319. package/src/schemas/environment.ts +39 -0
  320. package/src/schemas/git/git.ts +20 -2
  321. package/src/schemas/hosts.ts +7 -1
  322. package/src/schemas/keep-warm.ts +19 -6
  323. package/src/schemas/loopback-catch.ts +45 -0
  324. package/src/schemas/maintenance.ts +1 -1
  325. package/src/schemas/needs.ts +301 -0
  326. package/src/schemas/personas.ts +8 -3
  327. package/src/schemas/providers/provider-oauth.ts +35 -0
  328. package/src/schemas/secrets.ts +93 -0
  329. package/src/schemas/settings-history.ts +2 -0
  330. package/src/schemas/settings.test.ts +19 -1
  331. package/src/schemas/settings.ts +66 -9
  332. package/src/schemas/speaker.ts +5 -5
  333. package/src/schemas/state-plan.ts +8 -1
  334. package/src/schemas/system.ts +15 -0
  335. package/src/schemas/terminal.ts +19 -6
  336. package/src/schemas/updates.ts +56 -0
  337. package/src/schemas/webext.ts +3 -0
  338. package/src/schemas/workflows.ts +5 -5
  339. package/src/schemas/workspace/workspace-tree.ts +7 -0
  340. package/src/state/fix-attempt-plan.test.ts +1 -1
  341. package/src/state/fix-stance.ts +1 -1
  342. package/src/state/history-state.ts +19 -0
  343. package/src/state/runtime-state.ts +0 -3
  344. package/src/state/workspace-state.ts +4 -6
  345. package/src/text/documents.test.ts +25 -1
  346. package/src/text/documents.ts +24 -0
  347. package/src/text/title.ts +1 -1
  348. package/src/text/transcript-fold.test.ts +218 -25
  349. package/src/text/transcript-fold.ts +170 -45
  350. package/src/webext/index.ts +1 -0
  351. package/dist/schemas/workspace/push-checks.d.ts +0 -222
  352. package/dist/schemas/workspace/push-checks.d.ts.map +0 -1
  353. package/dist/schemas/workspace/push-checks.js +0 -97
  354. package/dist/schemas/workspace/push-checks.js.map +0 -1
  355. package/dist/state/red-streak.d.ts +0 -7
  356. package/dist/state/red-streak.d.ts.map +0 -1
  357. package/dist/state/red-streak.js +0 -11
  358. package/dist/state/red-streak.js.map +0 -1
  359. package/src/schemas/workspace/push-checks.test.ts +0 -28
  360. package/src/schemas/workspace/push-checks.ts +0 -155
  361. package/src/state/red-streak.test.ts +0 -28
  362. package/src/state/red-streak.ts +0 -28
@@ -1,5 +1,6 @@
1
1
  import type { SubagentVerification } from "../schemas/terminal.js";
2
2
  import type { TranscriptAgentWords, TranscriptRow } from "./transcript.js";
3
+ import { needWakeRow } from "./need-wake.js";
3
4
  import { watchWakeRow } from "./watch-wake.js";
4
5
 
5
6
  // Composer and parser of another agent's prompt (a peer's message, a child's report) are one piece of knowledge.
@@ -43,6 +44,9 @@ export interface ChildReportFields {
43
44
  readonly report: string;
44
45
  // Undefined when nothing of the child's work was seen, which is not a verdict.
45
46
  readonly verification: SubagentVerification | undefined;
47
+ // Where its work went, in the daemon's words: into the parent's own checkout, or held off it by a clash. Undefined
48
+ // for a child whose work lands as any conversation's does.
49
+ readonly landing?: string | undefined;
46
50
  }
47
51
 
48
52
  // The wait tool's `verification` field, in words.
@@ -72,7 +76,7 @@ export const childReportPrompt = (fields: ChildReportFields): string => {
72
76
  "",
73
77
  fields.report.trim() === "" ? "(It ended without a closing report.)" : fields.report.trim(),
74
78
  "",
75
- "Its changes are in its own worktree and land the way any agent's do. Its account of its own work is a claim, not " +
79
+ `${fields.landing ?? "Its changes are in its own worktree and land the way any agent's do."} Its account of its own work is a claim, not ` +
76
80
  "a result: check what matters before you build on it, and follow up with the subagents tools as you would mid-turn.",
77
81
  ].join("\n");
78
82
  };
@@ -124,4 +128,4 @@ export const agentWordsRow = (prompt: string): TranscriptRow | undefined => {
124
128
  };
125
129
 
126
130
  /** The row for a prompt nobody at the composer typed; every transcript reader asks this one function. */
127
- export const unspokenPromptRow = (prompt: string): TranscriptRow | undefined => watchWakeRow(prompt) ?? agentWordsRow(prompt);
131
+ export const unspokenPromptRow = (prompt: string): TranscriptRow | undefined => watchWakeRow(prompt) ?? needWakeRow(prompt) ?? agentWordsRow(prompt);
@@ -1,8 +1,7 @@
1
- // The follow-ups the daemon USED to send when the check it ran after every land went red: back to the conversation
1
+ // The follow-ups the daemon USED to send when the check it ran after every land failed: back to the conversation
2
2
  // that landed the work, or to a fresh conversation started on it. Nothing sends either any more: nothing checks work
3
- // after it lands, CI checks what the owner pushes (schemas/workspace/push-checks.ts). They stay on the wire because
4
- // records already hold them, and the chat has to go on recognising them: a prompt nobody typed must not reach a reader
5
- // as their own words.
3
+ // after it lands, CI checks what the owner pushes (schemas/ci.ts). They stay on the wire because records already hold
4
+ // them, and the chat has to go on recognising them: a prompt nobody typed must not reach a reader as their own words.
6
5
 
7
6
  // Anchored on by the chat, so it must stay unique and stable across releases.
8
7
  export const LAND_BREAKAGE_OPENING =
@@ -0,0 +1,55 @@
1
+ import { unspokenPromptRow } from "./agent-words.js";
2
+ import { needRowText, needWakeOf, needWakePrompt, needWakeRow, type NeedWakeFields, type NeedWakeOutcome } from "./need-wake.js";
3
+
4
+ const fields = (over: Partial<NeedWakeFields> = {}): NeedWakeFields => ({
5
+ outcome: "met",
6
+ id: "need-7k2q",
7
+ title: "Connect GitHub",
8
+ why: "to open the pull request for the fix",
9
+ result: 'GitHub is connected as "github".',
10
+ use: ["In this turn, prefix a command with GH_TOKEN={{secret:github/token}}."],
11
+ ...over,
12
+ });
13
+
14
+ const OUTCOMES: NeedWakeOutcome[] = ["met", "declined"];
15
+
16
+ describe("need wake", () => {
17
+ // The composer and the parser are the same piece of knowledge; this holds them together when either is reworded.
18
+ it.each(OUTCOMES)("round-trips a %s wake back to its fields", (outcome) => {
19
+ const prompt = needWakePrompt(fields({ outcome }));
20
+ expect(needWakeOf(prompt)).toEqual({ outcome, title: "Connect GitHub", id: "need-7k2q", sent: prompt });
21
+ });
22
+
23
+ it("tells a met need to carry on and a declined one not to wait", () => {
24
+ expect(needWakePrompt(fields()).split("\n").at(-1)).toBe("Continue the task that needed it.");
25
+ expect(needWakePrompt(fields({ outcome: "declined", use: [] }))).not.toContain("Continue the task");
26
+ });
27
+
28
+ it("leaves the agent's reason out when it gave none, rather than printing an empty label", () => {
29
+ expect(needWakePrompt(fields({ why: undefined }))).not.toContain("You asked because:");
30
+ expect(needWakePrompt(fields())).toContain("You asked because: to open the pull request for the fix");
31
+ });
32
+
33
+ it("is not a wake once its labelled lines are gone", () => {
34
+ const prompt = needWakePrompt(fields());
35
+ expect(needWakeOf(prompt.replace("Need id: need-7k2q\n", ""))).toBeUndefined();
36
+ expect(needWakeOf(prompt.replace("Need: Connect GitHub\n", ""))).toBeUndefined();
37
+ expect(needWakeOf("please connect github")).toBeUndefined();
38
+ });
39
+
40
+ it("becomes a notice row that says what happened, and every transcript reader finds it", () => {
41
+ const prompt = needWakePrompt(fields());
42
+ const row = needWakeRow(prompt);
43
+ expect(row).toEqual({
44
+ role: "notice",
45
+ text: "Connect GitHub: done, and the agent carries on.",
46
+ needWake: { outcome: "met", title: "Connect GitHub", id: "need-7k2q", sent: prompt },
47
+ });
48
+ expect(unspokenPromptRow(prompt)).toEqual(row);
49
+ expect(needWakeRow(needWakePrompt(fields({ outcome: "declined" })))?.text).toBe("Connect GitHub: declined, and the agent was told.");
50
+ });
51
+
52
+ it("words a raised need's row for a reader that draws no card", () => {
53
+ expect(needRowText({ title: "The OPENAI_API_KEY secret" })).toBe("Needs a person: The OPENAI_API_KEY secret");
54
+ });
55
+ });
@@ -0,0 +1,71 @@
1
+ import type { TranscriptNeedWake, TranscriptRow } from "./transcript.js";
2
+
3
+ // What a need says when its answer reaches a conversation, and how that reads to a person. Delivered as an ordinary
4
+ // turn prompt (a steer into a live turn, or a turn of its own), so composing it and recognising it stay one piece of
5
+ // knowledge, exactly as a condition watch's wake does (watch-wake.ts): the daemon's record, a steered live turn and a
6
+ // provider's session store all turn this prompt back into the same row.
7
+
8
+ export type NeedWakeOutcome = TranscriptNeedWake["outcome"];
9
+
10
+ // Per-outcome opening sentence, which is also what the parser anchors on: keep each unique and stable across releases,
11
+ // or a reworded opening un-recognises wakes already in a record and they read as the user's own words.
12
+ const OPENINGS: Record<NeedWakeOutcome, string> = {
13
+ met: "Need met: a person gave you what you asked for.",
14
+ declined: "Need declined: a person said no to what you asked for. Carry on without it, and say plainly what it would have enabled.",
15
+ };
16
+
17
+ // Labels the prompt writes and the parser reads back. `Need` and `Need id` are load-bearing on both sides.
18
+ const NEED = "Need: ";
19
+ const NEED_ID = "Need id: ";
20
+ const ASKED = "You asked because: ";
21
+
22
+ export interface NeedWakeFields {
23
+ readonly outcome: NeedWakeOutcome;
24
+ readonly id: string;
25
+ readonly title: string;
26
+ readonly why: string | undefined;
27
+ // The daemon's sentence on how it ended ("Connected as "github".", "Stored under OPENAI_API_KEY.").
28
+ readonly result: string;
29
+ // What the agent can do with it now, and what arrives on its next turn; empty for a decline.
30
+ readonly use: readonly string[];
31
+ }
32
+
33
+ export const needWakePrompt = (fields: NeedWakeFields): string =>
34
+ [
35
+ OPENINGS[fields.outcome],
36
+ `${NEED}${fields.title}`,
37
+ `${NEED_ID}${fields.id}`,
38
+ ...(fields.why === undefined || fields.why === "" ? [] : [`${ASKED}${fields.why}`]),
39
+ fields.result,
40
+ ...fields.use,
41
+ ...(fields.outcome === "met" ? ["Continue the task that needed it."] : []),
42
+ ].join("\n");
43
+
44
+ const headline = (outcome: NeedWakeOutcome, title: string): string =>
45
+ outcome === "met" ? `${title}: done, and the agent carries on.` : `${title}: declined, and the agent was told.`;
46
+
47
+ const valueOn = (lines: readonly string[], label: string): string | undefined => lines.find((line) => line.startsWith(label))?.slice(label.length);
48
+
49
+ // The words a raised need's row carries for a reader that draws no card (search, an export, an older client); the card
50
+ // itself is drawn from the need.
51
+ export const needRowText = (need: { readonly title: string }): string => `Needs a person: ${need.title}`;
52
+
53
+ // Which need wake a stored prompt is, if any; undefined for every prompt that isn't one, so any reader can ask without
54
+ // checking first. One that opens as a wake but lost its labelled lines is not one.
55
+ export const needWakeOf = (prompt: string): TranscriptNeedWake | undefined => {
56
+ const outcome = (Object.keys(OPENINGS) as NeedWakeOutcome[]).find((key) => prompt.startsWith(OPENINGS[key]));
57
+ if (outcome === undefined) {
58
+ return undefined;
59
+ }
60
+ const lines = prompt.split("\n");
61
+ const title = valueOn(lines, NEED);
62
+ const id = valueOn(lines, NEED_ID);
63
+ return title === undefined || id === undefined ? undefined : { outcome, title, id, sent: prompt };
64
+ };
65
+
66
+ // The row a wake becomes: a notice, since a need being answered happened to the conversation rather than being said by
67
+ // either side. Undefined when the prompt is not a need wake at all.
68
+ export const needWakeRow = (prompt: string): TranscriptRow | undefined => {
69
+ const wake = needWakeOf(prompt);
70
+ return wake === undefined ? undefined : { role: "notice", text: headline(wake.outcome, wake.title), needWake: wake };
71
+ };
@@ -1,4 +1,4 @@
1
- import { RESUME_NOTES, resumeDisclosure, withResumeNote, withoutResumeNote } from "./resume.js";
1
+ import { RESUME_NOTES, resumeDisclosure, resumeNoticeRow, withResumeNote, withoutResumeNote } from "./resume.js";
2
2
 
3
3
  // Wrapping then unwrapping a resume note must return the exact original prompt; a mismatch leaks machine narration into
4
4
  // the user's own words on screen.
@@ -46,7 +46,7 @@ test("the spent-allowance notes do not disclose as each other", () => {
46
46
  test("a turn the door turned away discloses as its own notice, apart from the allowance's refusal", () => {
47
47
  const door = resumeDisclosure(withResumeNote("ship the parser", RESUME_NOTES.door));
48
48
  const refused = resumeDisclosure(withResumeNote("ship the parser", RESUME_NOTES.refused));
49
- expect(door).toEqual({ kind: "notice", text: expect.stringContaining("turned away") });
49
+ expect(door).toEqual({ kind: "notice", text: expect.stringContaining("turned away"), reason: "door" });
50
50
  expect(door).not.toEqual(refused);
51
51
  });
52
52
 
@@ -60,5 +60,16 @@ test("a re-run after the window overflowed discloses as its own notice", () => {
60
60
  expect(resumeDisclosure(withResumeNote("ship the parser", RESUME_NOTES.overflow))).toEqual({
61
61
  kind: "notice",
62
62
  text: "Sent again in a fresh session after the last one outgrew the model's context window.",
63
+ reason: "overflow",
64
+ });
65
+ });
66
+
67
+ // The row keeps the English line, and names the re-run so an app can say it in its reader's language.
68
+ test("a re-run's notice row names its reason beside its words", () => {
69
+ const disclosure = resumeDisclosure(withResumeNote("ship the parser", RESUME_NOTES.restart));
70
+ expect(disclosure?.kind === "notice" ? resumeNoticeRow(disclosure) : undefined).toEqual({
71
+ role: "notice",
72
+ text: "The sandbox came back, this turn picked up where it left off.",
73
+ noticeCode: { code: "resumed", params: { reason: "restart" } },
63
74
  });
64
75
  });
@@ -1,4 +1,5 @@
1
- import type { TurnNote } from "./transcript.js";
1
+ import { noticeCode, type ResumeNoticeReason } from "./sandbox-notice.js";
2
+ import type { TranscriptRow, TurnNote } from "./transcript.js";
2
3
 
3
4
  // What a resumed turn's prompt says happened; lives on the wire since the client must recognise it too.
4
5
 
@@ -46,23 +47,35 @@ export type ResumeReason = keyof typeof RESUME_NOTES;
46
47
 
47
48
  // How a resumed turn's interruption reads to a person: `notice` for the whole-turn re-runs, a muted line with the
48
49
  // repeat dropped; `note` for the answered case, the user's real answer disclosed normally.
49
- export type ResumeDisclosure = { readonly kind: "notice"; readonly text: string } | { readonly kind: "note"; readonly note: TurnNote };
50
+ // A notice names its reason too, so the row it becomes can say it in the reader's language (sandbox-notice.ts).
51
+ export type ResumeDisclosure =
52
+ | { readonly kind: "notice"; readonly text: string; readonly reason: ResumeNoticeReason }
53
+ | { readonly kind: "note"; readonly note: TurnNote };
54
+
55
+ const notice = (reason: ResumeNoticeReason, text: string): ResumeDisclosure => ({ kind: "notice", text, reason });
50
56
 
51
57
  const RESUME_DISCLOSURES: Record<ResumeReason, ResumeDisclosure> = {
52
- auth: { kind: "notice", text: "Claude sign-in renewed, this turn picked up where it left off." },
53
- outage: { kind: "notice", text: "The model provider came back, this turn picked up where it left off." },
54
- restart: { kind: "notice", text: "The sandbox came back, this turn picked up where it left off." },
55
- stopped: { kind: "notice", text: "Sent again after the turn stopped short, picking up where it left off." },
58
+ auth: notice("auth", "Claude sign-in renewed, this turn picked up where it left off."),
59
+ outage: notice("outage", "The model provider came back, this turn picked up where it left off."),
60
+ restart: notice("restart", "The sandbox came back, this turn picked up where it left off."),
61
+ stopped: notice("stopped", "Sent again after the turn stopped short, picking up where it left off."),
56
62
  // None of these four auto-resumed, a person pressed Continue; `switched`/`carried` name the differing account.
57
- limit: { kind: "notice", text: "Sent again after the allowance ran out mid-turn, picking up where it left off." },
58
- switched: { kind: "notice", text: "Sent again on the switched account after the allowance ran out mid-turn, in a fresh session." },
59
- carried: { kind: "notice", text: "Sent again on the switched account after the allowance ran out mid-turn, carrying the session with it." },
60
- refused: { kind: "notice", text: "Sent again after the allowance refused it: nothing had run." },
61
- door: { kind: "notice", text: "Sent again: the first attempt was turned away before anything ran." },
62
- overflow: { kind: "notice", text: "Sent again in a fresh session after the last one outgrew the model's context window." },
63
+ limit: notice("limit", "Sent again after the allowance ran out mid-turn, picking up where it left off."),
64
+ switched: notice("switched", "Sent again on the switched account after the allowance ran out mid-turn, in a fresh session."),
65
+ carried: notice("carried", "Sent again on the switched account after the allowance ran out mid-turn, carrying the session with it."),
66
+ refused: notice("refused", "Sent again after the allowance refused it: nothing had run."),
67
+ door: notice("door", "Sent again: the first attempt was turned away before anything ran."),
68
+ overflow: notice("overflow", "Sent again in a fresh session after the last one outgrew the model's context window."),
63
69
  answered: { kind: "note", note: { title: "Picked back up after a sandbox restart", text: RESUME_NOTES.answered } },
64
70
  };
65
71
 
72
+ /** The row a whole-turn re-run stands in for its repeated words with, wherever a reader restores one. */
73
+ export const resumeNoticeRow = (resume: Extract<ResumeDisclosure, { kind: "notice" }>): TranscriptRow => ({
74
+ role: "notice",
75
+ text: resume.text,
76
+ noticeCode: noticeCode({ code: "resumed", params: { reason: resume.reason } }),
77
+ });
78
+
66
79
  // What a stored prompt's resume note should be shown as; undefined when the prompt isn't a resume, so any reader can
67
80
  // ask without checking first.
68
81
  export const resumeDisclosure = (prompt: string): ResumeDisclosure | undefined => {
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+
3
+ // The sandbox's own notice rows (a land, a hold, a renewal, a stop), as a code and the facts they were worded from. The
4
+ // row's `text` stays the English sentence, which older apps, stored transcripts and agents reading their own history
5
+ // go on reading; `noticeCode` beside it is what lets an app say the same thing in the reader's language and words.
6
+
7
+ // On the wire a code is any string and its facts a flat record: a transcript page carrying a code newer than its reader
8
+ // still parses, and that reader draws `text`. The codes this build writes, with their facts typed, are
9
+ // SandboxNoticeSchema below.
10
+ export const NoticeCodeSchema = z.object({
11
+ code: z.string().describe("Which of the sandbox's notices this row is, by name. A reader that does not know the name shows the row's text."),
12
+ params: z
13
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
14
+ .optional()
15
+ .describe("The facts the notice was worded from, by name: counts, names, and the provider's own sentence where the notice quotes one."),
16
+ });
17
+ export type NoticeCode = z.infer<typeof NoticeCodeSchema>;
18
+
19
+ const count = z.number().int().nonnegative();
20
+
21
+ // A failure's own sentence, which the notice's clause follows: the provider's words, or the sandbox's under `error`, its
22
+ // failure code, for a reader that words a failure it knows itself.
23
+ const failure = {
24
+ message: z.string(),
25
+ error: z.string().optional(),
26
+ };
27
+
28
+ // The re-runs a resumed turn's row can name (events/resume.ts); a reason a reader does not know draws the row's text.
29
+ const ResumeNoticeReasonSchema = z.enum(["auth", "outage", "restart", "stopped", "limit", "switched", "carried", "refused", "door", "overflow"]);
30
+ export type ResumeNoticeReason = z.infer<typeof ResumeNoticeReasonSchema>;
31
+
32
+ // Every code this build writes, one per sentence shape, each with the facts that shape needs.
33
+ export const SandboxNoticeSchema = z.discriminatedUnion("code", [
34
+ z.object({ code: z.literal("compacted") }),
35
+ z.object({ code: z.literal("stopped") }),
36
+ // The branch was rebased onto `commits` new commits, and/or could not be in the repos named in `blocked`.
37
+ z.object({ code: z.literal("synced"), params: z.object({ commits: count, blocked: z.string().optional() }) }),
38
+ z.object({ code: z.literal("intoParent") }),
39
+ z.object({ code: z.literal("intoParentClash"), params: z.object({ files: count }) }),
40
+ z.object({ code: z.literal("landHeld") }),
41
+ z.object({ code: z.literal("landConflict"), params: z.object({ files: count, repos: z.string() }) }),
42
+ // `deps` dependencies the turn added or changed, installing now, or `queued` behind any other install.
43
+ z.object({ code: z.literal("landed"), params: z.object({ deps: count.optional(), queued: z.boolean().optional() }).optional() }),
44
+ z.object({ code: z.literal("retrying"), params: z.object({ ...failure, attempt: count, of: count }) }),
45
+ z.object({ code: z.literal("retried"), params: z.object({ ...failure, made: count, of: count }) }),
46
+ z.object({ code: z.literal("outageWaiting"), params: z.object(failure) }),
47
+ z.object({ code: z.literal("renewing"), params: z.object(failure) }),
48
+ z.object({ code: z.literal("renewalWithdrawn"), params: z.object(failure) }),
49
+ z.object({ code: z.literal("reconnect"), params: z.object(failure) }),
50
+ z.object({ code: z.literal("undelivered"), params: z.object({ ...failure, unattended: z.boolean().optional() }) }),
51
+ // A turn the sandbox kept whole, its message above; `memory` when a memory hold kept it.
52
+ z.object({ code: z.literal("kept"), params: z.object({ ...failure, memory: z.boolean().optional() }) }),
53
+ z.object({ code: z.literal("memoryHeld"), params: z.object(failure) }),
54
+ // A failure the sandbox names by code and adds nothing to.
55
+ z.object({ code: z.literal("failed"), params: z.object(failure) }),
56
+ z.object({ code: z.literal("questionDismissed") }),
57
+ z.object({ code: z.literal("planApproved") }),
58
+ z.object({ code: z.literal("keptPlanning") }),
59
+ // `note` is the agent's own words; `every` the interval, already worded ("5m").
60
+ z.object({ code: z.literal("watching"), params: z.object({ note: z.string(), every: z.string() }) }),
61
+ z.object({ code: z.literal("restartInterrupted") }),
62
+ // A turn that picked up a conversation the sandbox kept warm: `tokens` read from the cache ("305k"), or for
63
+ // keptCold sent again, after `span` ("2h 5m") and `refreshes` refreshes.
64
+ z.object({ code: z.literal("keptWarm"), params: z.object({ tokens: z.string(), span: z.string(), refreshes: count }) }),
65
+ z.object({ code: z.literal("keptCold"), params: z.object({ tokens: z.string(), span: z.string(), refreshes: count }) }),
66
+ // A turn sent thin for a small window ("16k"): what was left out, and whether the base instructions were swapped too.
67
+ z.object({ code: z.literal("contextTrim"), params: z.object({ window: z.string(), omitted: z.string().optional(), base: z.boolean() }) }),
68
+ z.object({ code: z.literal("resumed"), params: z.object({ reason: ResumeNoticeReasonSchema }) }),
69
+ // An agent's own dependency install starting: into its conversation's own copy (`ownCopy`) or the main tree; the
70
+ // workspace root (`root`) and up to three project folders named (`projects`, each with its slash, already joined),
71
+ // and how many `more` were not named.
72
+ z.object({
73
+ code: z.literal("installing"),
74
+ params: z.object({ ownCopy: z.boolean(), root: z.boolean().optional(), projects: z.string().optional(), more: count.optional() }),
75
+ }),
76
+ ]);
77
+ export type SandboxNotice = z.infer<typeof SandboxNoticeSchema>;
78
+
79
+ /** The wire form of one of this build's notices, facts nobody gave left off rather than sent as nothing. */
80
+ export const noticeCode = (notice: SandboxNotice): NoticeCode => {
81
+ if (!("params" in notice) || notice.params === undefined) {
82
+ return { code: notice.code };
83
+ }
84
+ const params = Object.fromEntries(
85
+ Object.entries(notice.params).filter((entry): entry is [string, string | number | boolean] => entry[1] !== undefined),
86
+ );
87
+ return Object.keys(params).length === 0 ? { code: notice.code } : { code: notice.code, params };
88
+ };
89
+
90
+ /** One of this build's notices, read off a row; undefined for a row without one, or with a code or facts this build does not know. */
91
+ export const sandboxNoticeOf = (row: { readonly noticeCode?: NoticeCode | undefined }): SandboxNotice | undefined =>
92
+ row.noticeCode === undefined ? undefined : SandboxNoticeSchema.safeParse(row.noticeCode).data;
@@ -53,6 +53,13 @@ export const HelloSchema = z.object({
53
53
  shapes: z.record(z.string(), z.string()).optional(),
54
54
  build: z.string().optional(),
55
55
  boot: BootProgressSchema.optional(),
56
+ // A project sandbox's folder, workspace-relative (`my-app` for /work/my-app): the owner's own folder, synced live
57
+ // with their computer, which the browser scopes the workspace to on arrival. Absent on every other sandbox.
58
+ projectDir: z.string().optional(),
59
+ // What answers: a sandbox's daemon, or the desktop app's sidecar serving a folder on the user's own computer
60
+ // (_devices/local-files), whose `routes` are the few a folder has rather than a daemon's missing ones. Absent reads
61
+ // as a sandbox, which every daemon before this field was.
62
+ surface: z.enum(["sandbox", "folder"]).optional(),
56
63
  });
57
64
  export type Hello = z.infer<typeof HelloSchema>;
58
65
 
@@ -3,7 +3,9 @@ import { AgentHarnessSchema, AgentProviderSchema } from "../schemas/agent.js";
3
3
  import { ShareDetailSchema } from "../schemas/share.js";
4
4
  import { TurnErrandSchema, TurnSpeakerSchema } from "../schemas/speaker.js";
5
5
  import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js";
6
+ import { NeedSchema } from "../schemas/needs.js";
6
7
  import { RetryLadderSchema } from "../schemas/turn-break.js";
8
+ import { NoticeCodeSchema } from "./sandbox-notice.js";
7
9
  import type { ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind} from "./requests.js";
8
10
  import { browserHelpRequest, capabilityOfferRequest, CapabilityOutcomeSchema, credentialOfferRequest, CredentialReceiptSchema, paymentOfferRequest, PaymentReceiptSchema, PermissionAskSchema, permissionRequest, planRequest, questionRequest, terminalHelpRequest, TodoItemSchema, ToolCallContentSchema, ToolCallLocationSchema, ToolCallStatusSchema, ToolKindSchema } from "./requests.js";
9
11
 
@@ -104,7 +106,7 @@ export const TranscriptSubagentSchema = z.object({
104
106
  .string()
105
107
  .optional()
106
108
  .describe(
107
- "Its own id, where that is not the card's: a spawned subagent is named by its own conversation, which is what the roster, `wait` and its page call it. Absent, the card's id is its id, as it is for one the runtime started in-process.",
109
+ "Its own id, where that is not the card's: a spawned subagent is named by its own conversation, which is what the roster, `wait` and its own chat call it. Absent, the card's id is its id, as it is for one the runtime started in-process.",
108
110
  ),
109
111
  kind: SubagentKindSchema,
110
112
  agentType: z.string().optional(),
@@ -155,6 +157,15 @@ export const TranscriptWatchWakeSchema = z.object({
155
157
  });
156
158
  export type TranscriptWatchWake = z.infer<typeof TranscriptWatchWakeSchema>;
157
159
 
160
+ // A need's answer that reached this conversation as a prompt (need-wake.ts); the row keeps the prompt and names the need.
161
+ export const TranscriptNeedWakeSchema = z.object({
162
+ outcome: z.enum(["met", "declined"]).describe("How the need ended: a person gave it, or said no."),
163
+ title: z.string().describe("The need, as its card leads with it."),
164
+ id: z.string().describe("The need's handle, which its live card is keyed by."),
165
+ sent: z.string().describe("The whole prompt the model received, disclosed under the row."),
166
+ });
167
+ export type TranscriptNeedWake = z.infer<typeof TranscriptNeedWakeSchema>;
168
+
158
169
  // Another agent's words that reached this conversation as a prompt; the row keeps the prompt and names the sender.
159
170
  export const TranscriptAgentWordsSchema = z.object({
160
171
  kind: z.enum(["peer", "child"]).describe("Who sent it: another conversation in the workspace, or a subagent this one started."),
@@ -290,6 +301,11 @@ export const TranscriptRowSchema = z.object({
290
301
  .string()
291
302
  .optional()
292
303
  .describe("Which instance of the wait this notice names, for a kind that can have several running at once."),
304
+ // The sandbox's own notice by name, beside its English `text` (sandbox-notice.ts); absent on every other row, and
305
+ // on the notices written before rows carried it, which a reader draws from `text` as it always did.
306
+ noticeCode: NoticeCodeSchema.optional().describe(
307
+ "Which of the sandbox's own notices this row is, and the facts it was worded from, so a reader can say it in the reader's own language. The text stays the English sentence.",
308
+ ),
293
309
  // At most one card per row; a card closes its bubble. One field per kind, so a reader reaches it by name.
294
310
  plan: TranscriptPlanSchema.optional().describe("The plan this row asked approval for, and the answer."),
295
311
  question: TranscriptQuestionSchema.optional().describe("The questions this row asked, and the picks that answered them."),
@@ -299,6 +315,12 @@ export const TranscriptRowSchema = z.object({
299
315
  capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
300
316
  paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
301
317
  watchWake: TranscriptWatchWakeSchema.optional().describe("The condition watch that woke this conversation, and the prompt it was woken with."),
318
+ // A need is not a parked card: it holds no turn open, outlives the one that raised it, and its live state is the
319
+ // needs store's, keyed by `need.id`. What rides here is the need as it was raised, for a reader with no store.
320
+ need: NeedSchema.optional().describe(
321
+ "Something the agent asked a person for, as it was when raised. Its live state (answered, met) is read by its id, since it outlives the turn.",
322
+ ),
323
+ needWake: TranscriptNeedWakeSchema.optional().describe("The answered need that reached this conversation, and the prompt it came as."),
302
324
  agentWords: TranscriptAgentWordsSchema.optional().describe("Another agent's words that reached this conversation, whose they are, and the prompt they came as."),
303
325
  backgroundJob: TranscriptBackgroundJobSchema.optional().describe("The background job this row marks the start of."),
304
326
  credentialOffer: TranscriptCredentialOfferSchema.optional().describe(
@@ -1,7 +1,7 @@
1
1
  // The follow-up the daemon USED to send when a turn ended with work it could not confirm. Nothing sends it any more:
2
- // nothing checks inside a conversation, CI checks what the owner pushes (schemas/workspace/push-checks.ts). It stays on
3
- // the wire because records already hold it, and the chat has to go on recognising it: a prompt nobody typed must not
4
- // reach a reader as their own words.
2
+ // nothing checks inside a conversation, CI checks what the owner pushes (schemas/ci.ts). It stays on the wire because
3
+ // records already hold it, and the chat has to go on recognising it: a prompt nobody typed must not reach a reader as
4
+ // their own words.
5
5
 
6
6
  // Anchored on by the reader, so it must stay unique and stable across releases — a reworded opening un-recognises every
7
7
  // nudge already in a record, and they read as the user's own typing again.
@@ -7,8 +7,6 @@ import {
7
7
  latestFixAttempt,
8
8
  newConversationId,
9
9
  nextFixAttemptId,
10
- PUSH_FIX_PREFIX,
11
- pushFixConversationId,
12
10
  } from "./conversation-ids.js";
13
11
  import { ConversationIdSchema } from "../schemas/agent.js";
14
12
 
@@ -90,28 +88,6 @@ test("the prefix is carried by every fix id", () => {
90
88
  expect(ciFixConversationId(`ci-fix`, 7)).toBe(`ci-fix-ci-fix-7`);
91
89
  });
92
90
 
93
- test("a push fix id is the same string every time the same failure derives it", () => {
94
- expect(pushFixConversationId(`intentic`, `checkout gates,lint`)).toBe(pushFixConversationId(`intentic`, `checkout gates,lint`));
95
- expect(pushFixConversationId(`intentic`, `checkout gates,lint`)).toMatch(/^push-fix-intentic-[0-9a-z]{7}$/);
96
- });
97
-
98
- test("a push fix id separates failures and scopes", () => {
99
- expect(pushFixConversationId(`intentic`, `lint`)).not.toBe(pushFixConversationId(`intentic`, `checkout gates`));
100
- expect(pushFixConversationId(`web`, `lint`)).not.toBe(pushFixConversationId(`api`, `lint`));
101
- });
102
-
103
- // Signatures cover raw gate output: control characters, unicode, path-like strings, and pathological lengths.
104
- test("every derived push fix id passes the conversation-id guard", () => {
105
- const signatures = [``, `checkout gates`, `✗ lint · pnpm lint`, `a`.repeat(4_000), `../../etc/passwd`, `a\nb\tc`, `résumé`];
106
- for (const scope of [`root`, `apps/web`, `my repo`, `___`, `x`.repeat(80)]) {
107
- for (const signature of signatures) {
108
- const id = pushFixConversationId(scope, signature);
109
- expect(ConversationIdSchema.safeParse(id).success).toBe(true);
110
- expect(id.startsWith(PUSH_FIX_PREFIX)).toBe(true);
111
- }
112
- }
113
- });
114
-
115
91
  // Attempt 1 is the bare derived id, so every fix conversation minted before attempts existed reads as attempt 1.
116
92
  test("the first attempt wears the failure's own id, later ones carry their number", () => {
117
93
  const base = ciFixConversationId(`web`, 41);
@@ -136,8 +112,8 @@ test("another failure's id is not an attempt at this one", () => {
136
112
  });
137
113
 
138
114
  test("attempts are read off the roster, earliest first, and the newest is the live answer", () => {
139
- const base = pushFixConversationId(`intentic`, `lint`);
140
- const roster = [{ id: `swift-otter-k9m2` }, { id: fixAttemptId(base, 3) }, { id: base }, { id: pushFixConversationId(`intentic`, `typecheck`) }];
115
+ const base = ciFixConversationId(`intentic`, 7);
116
+ const roster = [{ id: `swift-otter-k9m2` }, { id: fixAttemptId(base, 3) }, { id: base }, { id: ciFixConversationId(`intentic`, 8) }];
141
117
  expect(fixAttemptsOf(base, roster).map((entry) => entry.attempt)).toEqual([1, 3]);
142
118
  expect(latestFixAttempt(base, roster)?.agent.id).toBe(fixAttemptId(base, 3));
143
119
  expect(latestFixAttempt(base, [])).toBeUndefined();
@@ -160,13 +160,9 @@ const repoSlug = (repo: string): string =>
160
160
  .replace(/^-+|-+$/g, "") || "repo";
161
161
  export const ciFixConversationId = (repo: string, runId: number): string => `${CI_FIX_PREFIX}${repoSlug(repo)}-${runId}`;
162
162
 
163
- // Keyed by when the project's push red began (pushFixBase, `red:<since>`), not a run id (a push has none): a refused push
164
- // is owed by that red too (push-checks-store.ts). Hashed rather than spelled into the branch name; the fix agent's first
165
- // message names what it is for.
166
- export const PUSH_FIX_PREFIX = "push-fix-";
167
-
168
163
  // FNV-1a, not a cryptographic hash: nothing here is secret, the only requirement is the same failure yields the same
169
- // seven characters in every browser and in node. The one spelling: a push finding's id (push-checks-store.ts) is this too.
164
+ // seven characters in every browser and in node. The one spelling: what a failing main owes is named by it too (a
165
+ // failed job's id, ci/main-fixer.ts).
170
166
  const FNV_OFFSET = 0x811c_9dc5;
171
167
  const FNV_PRIME = 0x0100_0193;
172
168
  export const fnvDigest = (text: string): string => {
@@ -177,14 +173,12 @@ export const fnvDigest = (text: string): string => {
177
173
  return (hash >>> 0).toString(36).padStart(7, "0");
178
174
  };
179
175
 
180
- export const pushFixConversationId = (scope: string, signature: string): string => `${PUSH_FIX_PREFIX}${repoSlug(scope)}-${fnvDigest(signature)}`;
181
-
182
- // A red main-line check that no conversation still holding the work could take: keyed by the project and the moment its
183
- // red streak began, so every attempt at one streak shares a base and a later streak in the same project starts over.
176
+ // A failing main-line check that no conversation still holding the work could take: keyed by the project and the moment
177
+ // it began failing, so every attempt at one streak shares a base and a later streak in the same project starts over.
184
178
  // Seconds in base36 keep it short; the workspace root is spelled `workspace`, since its project folder is empty.
185
179
  export const LAND_FIX_PREFIX = "land-fix-";
186
- export const landFixConversationId = (project: string, redSince: number): string =>
187
- `${LAND_FIX_PREFIX}${repoSlug(project === "" ? "workspace" : project)}-${Math.floor(redSince / 1000).toString(36)}`;
180
+ export const landFixConversationId = (project: string, failingSince: number): string =>
181
+ `${LAND_FIX_PREFIX}${repoSlug(project === "" ? "workspace" : project)}-${Math.floor(failingSince / 1000).toString(36)}`;
188
182
 
189
183
  // ONE FAILURE, MANY ATTEMPTS, ONE LIVE ANSWER. The derived id above names the FAILURE; an attempt at it is a
190
184
  // conversation of its own, so starting over (another model, a clean worktree) never rewrites a record some other window
@@ -0,0 +1,65 @@
1
+ {
2
+ "names": [
3
+ { "name": "my-app", "valid": true },
4
+ { "name": "My_App.v2", "valid": true },
5
+ { "name": "a", "valid": true },
6
+ { "name": "0", "valid": true },
7
+ { "name": "Public", "valid": true },
8
+ { "name": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "valid": true },
9
+ { "name": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "valid": false },
10
+ { "name": "public", "valid": false },
11
+ { "name": "refs", "valid": false },
12
+ { "name": "site", "valid": false },
13
+ { "name": "root", "valid": false },
14
+ { "name": "intent", "valid": false },
15
+ { "name": "desired-state", "valid": false },
16
+ { "name": "app", "valid": false },
17
+ { "name": "AGENTS.md", "valid": false },
18
+ { "name": "node_modules", "valid": false },
19
+ { "name": "dist", "valid": false },
20
+ { "name": "venv", "valid": false },
21
+ { "name": "claude.json", "valid": false },
22
+ { "name": "__pycache__", "valid": false },
23
+ { "name": ".intentic", "valid": false },
24
+ { "name": ".hidden", "valid": false },
25
+ { "name": "-flag", "valid": false },
26
+ { "name": "_under", "valid": false },
27
+ { "name": "", "valid": false },
28
+ { "name": "a/b", "valid": false },
29
+ { "name": "..", "valid": false },
30
+ { "name": "has space", "valid": false },
31
+ { "name": "café", "valid": false },
32
+ { "name": "my-app/", "valid": false }
33
+ ],
34
+ "folders": [
35
+ { "folder": "my-app", "dirName": "my-app" },
36
+ { "folder": "My App", "dirName": "My-App" },
37
+ { "folder": "My App!!", "dirName": "My-App" },
38
+ { "folder": "Café Menu", "dirName": "Cafe-Menu" },
39
+ { "folder": "日本語", "dirName": "project" },
40
+ { "folder": "", "dirName": "project" },
41
+ { "folder": "...", "dirName": "project" },
42
+ { "folder": ".config", "dirName": "config" },
43
+ { "folder": "--flag--", "dirName": "flag" },
44
+ { "folder": "_private", "dirName": "private" },
45
+ { "folder": "public", "dirName": "public-project" },
46
+ { "folder": "site", "dirName": "site-project" },
47
+ { "folder": "app", "dirName": "app-project" },
48
+ { "folder": "AGENTS.md", "dirName": "AGENTS.md-project" },
49
+ { "folder": "dist", "dirName": "dist-project" },
50
+ { "folder": "node_modules", "dirName": "node_modules-project" },
51
+ { "folder": "__pycache__", "dirName": "pycache__" },
52
+ { "folder": "a--b", "dirName": "a-b" },
53
+ { "folder": "report (final) v2", "dirName": "report-final-v2" },
54
+ {
55
+ "folder": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
56
+ "dirName": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
57
+ },
58
+ {
59
+ "folder": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b",
60
+ "dirName": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
61
+ },
62
+ { "folder": "C++ Project", "dirName": "C-Project" },
63
+ { "folder": "release.2024.", "dirName": "release.2024" }
64
+ ]
65
+ }