dorfl 0.1.2 → 0.2.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 (364) hide show
  1. package/dist/advance-drivers.d.ts +1 -1
  2. package/dist/advance-drivers.d.ts.map +1 -1
  3. package/dist/advance-drivers.js +15 -3
  4. package/dist/advance-drivers.js.map +1 -1
  5. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  6. package/dist/advance-lifecycle-template.js +76 -4
  7. package/dist/advance-lifecycle-template.js.map +1 -1
  8. package/dist/advance-treeless-publish.d.ts +23 -0
  9. package/dist/advance-treeless-publish.d.ts.map +1 -1
  10. package/dist/advance-treeless-publish.js +41 -0
  11. package/dist/advance-treeless-publish.js.map +1 -1
  12. package/dist/advance.d.ts +59 -7
  13. package/dist/advance.d.ts.map +1 -1
  14. package/dist/advance.js +389 -82
  15. package/dist/advance.js.map +1 -1
  16. package/dist/advancing-lock.d.ts +30 -2
  17. package/dist/advancing-lock.d.ts.map +1 -1
  18. package/dist/advancing-lock.js +50 -0
  19. package/dist/advancing-lock.js.map +1 -1
  20. package/dist/agent-launch.d.ts +12 -0
  21. package/dist/agent-launch.d.ts.map +1 -1
  22. package/dist/agent-launch.js +22 -12
  23. package/dist/agent-launch.js.map +1 -1
  24. package/dist/agent-stop.d.ts +40 -2
  25. package/dist/agent-stop.d.ts.map +1 -1
  26. package/dist/agent-stop.js +30 -2
  27. package/dist/agent-stop.js.map +1 -1
  28. package/dist/apply-decide.d.ts +17 -3
  29. package/dist/apply-decide.d.ts.map +1 -1
  30. package/dist/apply-decide.js +36 -7
  31. package/dist/apply-decide.js.map +1 -1
  32. package/dist/apply-merge-action.d.ts +17 -5
  33. package/dist/apply-merge-action.d.ts.map +1 -1
  34. package/dist/apply-merge-action.js +43 -8
  35. package/dist/apply-merge-action.js.map +1 -1
  36. package/dist/apply-persist.d.ts +57 -26
  37. package/dist/apply-persist.d.ts.map +1 -1
  38. package/dist/apply-persist.js +166 -37
  39. package/dist/apply-persist.js.map +1 -1
  40. package/dist/apply-stuck-action.d.ts +151 -0
  41. package/dist/apply-stuck-action.d.ts.map +1 -0
  42. package/dist/apply-stuck-action.js +125 -0
  43. package/dist/apply-stuck-action.js.map +1 -0
  44. package/dist/brand.d.ts +12 -1
  45. package/dist/brand.d.ts.map +1 -1
  46. package/dist/brand.js +2 -1
  47. package/dist/brand.js.map +1 -1
  48. package/dist/cli-spinner.d.ts +1 -1
  49. package/dist/cli-spinner.js +1 -1
  50. package/dist/cli.d.ts +10 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +320 -63
  53. package/dist/cli.js.map +1 -1
  54. package/dist/close-job-template.js +1 -1
  55. package/dist/complete.d.ts +6 -3
  56. package/dist/complete.d.ts.map +1 -1
  57. package/dist/complete.js +91 -37
  58. package/dist/complete.js.map +1 -1
  59. package/dist/config.d.ts +55 -9
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +56 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/continue-branch.d.ts.map +1 -1
  64. package/dist/continue-branch.js +22 -0
  65. package/dist/continue-branch.js.map +1 -1
  66. package/dist/decision-engine.d.ts +38 -10
  67. package/dist/decision-engine.d.ts.map +1 -1
  68. package/dist/decision-engine.js +10 -6
  69. package/dist/decision-engine.js.map +1 -1
  70. package/dist/do-autopick.d.ts.map +1 -1
  71. package/dist/do-autopick.js +11 -1
  72. package/dist/do-autopick.js.map +1 -1
  73. package/dist/do-config.d.ts +1 -1
  74. package/dist/do.d.ts +49 -56
  75. package/dist/do.d.ts.map +1 -1
  76. package/dist/do.js +419 -30
  77. package/dist/do.js.map +1 -1
  78. package/dist/env-config.d.ts.map +1 -1
  79. package/dist/env-config.js +12 -4
  80. package/dist/env-config.js.map +1 -1
  81. package/dist/failure-cause.d.ts +3 -2
  82. package/dist/failure-cause.d.ts.map +1 -1
  83. package/dist/failure-cause.js +28 -1
  84. package/dist/failure-cause.js.map +1 -1
  85. package/dist/format.d.ts +6 -6
  86. package/dist/format.d.ts.map +1 -1
  87. package/dist/format.js +10 -27
  88. package/dist/format.js.map +1 -1
  89. package/dist/frontmatter.d.ts +16 -4
  90. package/dist/frontmatter.d.ts.map +1 -1
  91. package/dist/frontmatter.js +27 -1
  92. package/dist/frontmatter.js.map +1 -1
  93. package/dist/gc.d.ts +65 -6
  94. package/dist/gc.d.ts.map +1 -1
  95. package/dist/gc.js +126 -9
  96. package/dist/gc.js.map +1 -1
  97. package/dist/github.d.ts +14 -0
  98. package/dist/github.d.ts.map +1 -1
  99. package/dist/github.js +73 -0
  100. package/dist/github.js.map +1 -1
  101. package/dist/harness.d.ts +25 -0
  102. package/dist/harness.d.ts.map +1 -1
  103. package/dist/harness.js.map +1 -1
  104. package/dist/index.d.ts +4 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +2 -1
  107. package/dist/index.js.map +1 -1
  108. package/dist/install-ci-branch-protection.d.ts +132 -38
  109. package/dist/install-ci-branch-protection.d.ts.map +1 -1
  110. package/dist/install-ci-branch-protection.js +190 -48
  111. package/dist/install-ci-branch-protection.js.map +1 -1
  112. package/dist/install-ci-core.d.ts +21 -1
  113. package/dist/install-ci-core.d.ts.map +1 -1
  114. package/dist/install-ci-core.js +10 -3
  115. package/dist/install-ci-core.js.map +1 -1
  116. package/dist/install-ci-github.d.ts +32 -0
  117. package/dist/install-ci-github.d.ts.map +1 -1
  118. package/dist/install-ci-github.js +73 -0
  119. package/dist/install-ci-github.js.map +1 -1
  120. package/dist/install-skills.d.ts +123 -0
  121. package/dist/install-skills.d.ts.map +1 -0
  122. package/dist/install-skills.js +100 -0
  123. package/dist/install-skills.js.map +1 -0
  124. package/dist/intake-trigger-template.js +1 -1
  125. package/dist/intake.d.ts +17 -4
  126. package/dist/intake.d.ts.map +1 -1
  127. package/dist/intake.js +23 -7
  128. package/dist/intake.js.map +1 -1
  129. package/dist/integration-core.d.ts +10 -7
  130. package/dist/integration-core.d.ts.map +1 -1
  131. package/dist/integration-core.js +174 -60
  132. package/dist/integration-core.js.map +1 -1
  133. package/dist/integrator.d.ts +1 -1
  134. package/dist/integrator.d.ts.map +1 -1
  135. package/dist/integrator.js +19 -3
  136. package/dist/integrator.js.map +1 -1
  137. package/dist/isolation.d.ts +2 -2
  138. package/dist/isolation.d.ts.map +1 -1
  139. package/dist/isolation.js +9 -0
  140. package/dist/isolation.js.map +1 -1
  141. package/dist/item-lock.d.ts +196 -129
  142. package/dist/item-lock.d.ts.map +1 -1
  143. package/dist/item-lock.js +329 -254
  144. package/dist/item-lock.js.map +1 -1
  145. package/dist/ledger-write.d.ts +31 -26
  146. package/dist/ledger-write.d.ts.map +1 -1
  147. package/dist/ledger-write.js +100 -122
  148. package/dist/ledger-write.js.map +1 -1
  149. package/dist/lifecycle-gather.d.ts +17 -0
  150. package/dist/lifecycle-gather.d.ts.map +1 -1
  151. package/dist/lifecycle-gather.js +7 -1
  152. package/dist/lifecycle-gather.js.map +1 -1
  153. package/dist/lifecycle-pools.d.ts +40 -3
  154. package/dist/lifecycle-pools.d.ts.map +1 -1
  155. package/dist/lifecycle-pools.js +26 -6
  156. package/dist/lifecycle-pools.js.map +1 -1
  157. package/dist/merge-question-surfacer.d.ts +15 -1
  158. package/dist/merge-question-surfacer.d.ts.map +1 -1
  159. package/dist/merge-question-surfacer.js +18 -3
  160. package/dist/merge-question-surfacer.js.map +1 -1
  161. package/dist/migrate-stuck-locks.d.ts +129 -0
  162. package/dist/migrate-stuck-locks.d.ts.map +1 -0
  163. package/dist/migrate-stuck-locks.js +355 -0
  164. package/dist/migrate-stuck-locks.js.map +1 -0
  165. package/dist/needs-attention.d.ts +293 -16
  166. package/dist/needs-attention.d.ts.map +1 -1
  167. package/dist/needs-attention.js +532 -57
  168. package/dist/needs-attention.js.map +1 -1
  169. package/dist/orphan-sidecar.d.ts +8 -4
  170. package/dist/orphan-sidecar.d.ts.map +1 -1
  171. package/dist/orphan-sidecar.js +35 -2
  172. package/dist/orphan-sidecar.js.map +1 -1
  173. package/dist/pi-harness.d.ts +16 -0
  174. package/dist/pi-harness.d.ts.map +1 -1
  175. package/dist/pi-harness.js +82 -2
  176. package/dist/pi-harness.js.map +1 -1
  177. package/dist/placement.d.ts +3 -3
  178. package/dist/placement.js +1 -1
  179. package/dist/prd-to-spec.d.ts.map +1 -1
  180. package/dist/prd-to-spec.js +9 -5
  181. package/dist/prd-to-spec.js.map +1 -1
  182. package/dist/protocol/CLAIM-PROTOCOL.md +17 -10
  183. package/dist/protocol/REVIEW-PROTOCOL.md +4 -1
  184. package/dist/protocol/SURFACE-PROTOCOL.md +16 -2
  185. package/dist/protocol/TASKING-PROTOCOL.md +3 -1
  186. package/dist/protocol/WORK-CONTRACT.md +22 -18
  187. package/dist/protocol/task-template.md +1 -1
  188. package/dist/reap-branches.d.ts +12 -9
  189. package/dist/reap-branches.d.ts.map +1 -1
  190. package/dist/reap-branches.js +25 -7
  191. package/dist/reap-branches.js.map +1 -1
  192. package/dist/recover-isolated.d.ts +12 -0
  193. package/dist/recover-isolated.d.ts.map +1 -1
  194. package/dist/recover-isolated.js +5 -0
  195. package/dist/recover-isolated.js.map +1 -1
  196. package/dist/repo-config.d.ts +23 -2
  197. package/dist/repo-config.d.ts.map +1 -1
  198. package/dist/repo-config.js +53 -7
  199. package/dist/repo-config.js.map +1 -1
  200. package/dist/repo-mirror.d.ts.map +1 -1
  201. package/dist/repo-mirror.js +18 -2
  202. package/dist/repo-mirror.js.map +1 -1
  203. package/dist/review-verdict.js +1 -1
  204. package/dist/review-verdict.js.map +1 -1
  205. package/dist/run.d.ts +1 -1
  206. package/dist/run.d.ts.map +1 -1
  207. package/dist/run.js +54 -19
  208. package/dist/run.js.map +1 -1
  209. package/dist/scan.d.ts +17 -8
  210. package/dist/scan.d.ts.map +1 -1
  211. package/dist/scan.js +22 -8
  212. package/dist/scan.js.map +1 -1
  213. package/dist/select-priority.d.ts +2 -2
  214. package/dist/select-priority.js +1 -1
  215. package/dist/select-priority.js.map +1 -1
  216. package/dist/sidecar-apply.js +1 -1
  217. package/dist/sidecar-apply.js.map +1 -1
  218. package/dist/sidecar.d.ts +45 -10
  219. package/dist/sidecar.d.ts.map +1 -1
  220. package/dist/sidecar.js +82 -2
  221. package/dist/sidecar.js.map +1 -1
  222. package/dist/skills/answer-questions/SKILL.md +89 -0
  223. package/dist/skills/capture-signal/SKILL.md +52 -0
  224. package/dist/skills/convert-from-prd-to-spec/SKILL.md +90 -0
  225. package/dist/skills/drive-tasks/SKILL.md +218 -0
  226. package/dist/skills/from-idea/SKILL.md +83 -0
  227. package/dist/skills/merge-prs/SKILL.md +70 -0
  228. package/dist/skills/orchestrate/SKILL.md +101 -0
  229. package/dist/skills/promote/SKILL.md +35 -0
  230. package/dist/skills/review/SKILL.md +16 -0
  231. package/dist/skills/setup/SKILL.md +258 -0
  232. package/dist/skills/setup/protocol/ADR-FORMAT.md +47 -0
  233. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +224 -0
  234. package/dist/skills/setup/protocol/REVIEW-PROTOCOL.md +122 -0
  235. package/dist/skills/setup/protocol/SURFACE-PROTOCOL.md +135 -0
  236. package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +124 -0
  237. package/dist/skills/setup/protocol/WORK-CONTRACT.md +280 -0
  238. package/dist/skills/setup/protocol/spec-template.md +71 -0
  239. package/dist/skills/setup/protocol/task-template.md +65 -0
  240. package/dist/skills/surface-questions/SKILL.md +16 -0
  241. package/dist/skills/to-spec/SKILL.md +34 -0
  242. package/dist/skills/to-task/SKILL.md +19 -0
  243. package/dist/skills/triage-observations/SKILL.md +78 -0
  244. package/dist/skills/work/SKILL.md +51 -0
  245. package/dist/slug-namespace.d.ts +5 -5
  246. package/dist/slug-namespace.js +5 -5
  247. package/dist/slug-namespace.js.map +1 -1
  248. package/dist/spec-complete.d.ts.map +1 -1
  249. package/dist/spec-complete.js +2 -3
  250. package/dist/spec-complete.js.map +1 -1
  251. package/dist/start.d.ts.map +1 -1
  252. package/dist/start.js +51 -57
  253. package/dist/start.js.map +1 -1
  254. package/dist/surface-gate.d.ts.map +1 -1
  255. package/dist/surface-gate.js +10 -3
  256. package/dist/surface-gate.js.map +1 -1
  257. package/dist/surface-persist.js +1 -1
  258. package/dist/surface-persist.js.map +1 -1
  259. package/dist/tasker-review-loop.d.ts +4 -3
  260. package/dist/tasker-review-loop.d.ts.map +1 -1
  261. package/dist/tasker-review-loop.js.map +1 -1
  262. package/dist/tasking-lock.d.ts +2 -2
  263. package/dist/tasking-lock.d.ts.map +1 -1
  264. package/dist/tasking-lock.js +53 -32
  265. package/dist/tasking-lock.js.map +1 -1
  266. package/dist/tasking.d.ts +40 -6
  267. package/dist/tasking.d.ts.map +1 -1
  268. package/dist/tasking.js +224 -12
  269. package/dist/tasking.js.map +1 -1
  270. package/dist/triage-persist.d.ts +10 -6
  271. package/dist/triage-persist.d.ts.map +1 -1
  272. package/dist/triage-persist.js +34 -4
  273. package/dist/triage-persist.js.map +1 -1
  274. package/dist/vendor/incur/agents.d.ts +58 -0
  275. package/dist/vendor/incur/agents.d.ts.map +1 -0
  276. package/dist/vendor/incur/agents.js +343 -0
  277. package/dist/vendor/incur/agents.js.map +1 -0
  278. package/dist/verify-workflow-template.js +1 -1
  279. package/dist/watch-session.d.ts +11 -3
  280. package/dist/watch-session.d.ts.map +1 -1
  281. package/dist/watch-session.js +94 -7
  282. package/dist/watch-session.js.map +1 -1
  283. package/dist/work-layout.d.ts +7 -3
  284. package/dist/work-layout.d.ts.map +1 -1
  285. package/dist/work-layout.js +6 -3
  286. package/dist/work-layout.js.map +1 -1
  287. package/dist/workspace.d.ts +1 -1
  288. package/package.json +2 -2
  289. package/src/advance-drivers.ts +16 -3
  290. package/src/advance-lifecycle-template.ts +89 -4
  291. package/src/advance-treeless-publish.ts +45 -0
  292. package/src/advance.ts +467 -87
  293. package/src/advancing-lock.ts +96 -1
  294. package/src/agent-launch.ts +37 -12
  295. package/src/agent-stop.ts +60 -2
  296. package/src/apply-decide.ts +36 -7
  297. package/src/apply-merge-action.ts +45 -7
  298. package/src/apply-persist.ts +228 -54
  299. package/src/apply-stuck-action.ts +260 -0
  300. package/src/brand.ts +14 -2
  301. package/src/cli-spinner.ts +1 -1
  302. package/src/cli.ts +401 -70
  303. package/src/close-job-template.ts +1 -1
  304. package/src/complete.ts +110 -52
  305. package/src/config.ts +107 -13
  306. package/src/continue-branch.ts +22 -0
  307. package/src/decision-engine.ts +55 -17
  308. package/src/do-autopick.ts +12 -1
  309. package/src/do-config.ts +1 -1
  310. package/src/do.ts +511 -38
  311. package/src/env-config.ts +12 -4
  312. package/src/failure-cause.ts +30 -1
  313. package/src/format.ts +10 -30
  314. package/src/frontmatter.ts +35 -5
  315. package/src/gc.ts +172 -9
  316. package/src/github.ts +78 -0
  317. package/src/harness.ts +25 -0
  318. package/src/index.ts +10 -0
  319. package/src/install-ci-branch-protection.ts +282 -57
  320. package/src/install-ci-core.ts +30 -3
  321. package/src/install-ci-github.ts +86 -0
  322. package/src/install-skills.ts +166 -0
  323. package/src/intake-trigger-template.ts +1 -1
  324. package/src/intake.ts +46 -11
  325. package/src/integration-core.ts +199 -65
  326. package/src/integrator.ts +22 -9
  327. package/src/isolation.ts +11 -2
  328. package/src/item-lock.ts +485 -341
  329. package/src/ledger-write.ts +135 -150
  330. package/src/lifecycle-gather.ts +29 -6
  331. package/src/lifecycle-pools.ts +67 -9
  332. package/src/merge-question-surfacer.ts +28 -4
  333. package/src/migrate-stuck-locks.ts +451 -0
  334. package/src/needs-attention.ts +857 -84
  335. package/src/orphan-sidecar.ts +47 -6
  336. package/src/pi-harness.ts +82 -2
  337. package/src/placement.ts +3 -3
  338. package/src/prd-to-spec.ts +13 -5
  339. package/src/reap-branches.ts +38 -14
  340. package/src/recover-isolated.ts +17 -0
  341. package/src/repo-config.ts +55 -6
  342. package/src/repo-mirror.ts +23 -1
  343. package/src/review-verdict.ts +1 -1
  344. package/src/run.ts +64 -18
  345. package/src/scan.ts +22 -8
  346. package/src/select-priority.ts +3 -3
  347. package/src/sidecar-apply.ts +1 -1
  348. package/src/sidecar.ts +138 -11
  349. package/src/slug-namespace.ts +7 -7
  350. package/src/spec-complete.ts +2 -3
  351. package/src/start.ts +53 -59
  352. package/src/surface-gate.ts +10 -3
  353. package/src/surface-persist.ts +1 -1
  354. package/src/tasker-review-loop.ts +4 -3
  355. package/src/tasking-lock.ts +52 -36
  356. package/src/tasking.ts +261 -17
  357. package/src/triage-persist.ts +56 -12
  358. package/src/vendor/incur/LICENSE +21 -0
  359. package/src/vendor/incur/README.md +19 -0
  360. package/src/vendor/incur/agents.ts +392 -0
  361. package/src/verify-workflow-template.ts +1 -1
  362. package/src/watch-session.ts +102 -7
  363. package/src/work-layout.ts +6 -3
  364. package/src/workspace.ts +1 -1
@@ -3,6 +3,7 @@ import {dirname, join} from 'node:path';
3
3
  import {runAsync, type RunResult} from './git.js';
4
4
  import {ledgerWrite} from './ledger-write.js';
5
5
  import {resolveSidecarIdentity} from './sidecar.js';
6
+ import {readFrontmatterField} from './frontmatter.js';
6
7
  import {acquireItemLock, releaseItemLock} from './item-lock.js';
7
8
  import {realSleep, type Sleep} from './retry-backoff.js';
8
9
 
@@ -545,11 +546,25 @@ export function nextCasContentionDelayMs(input: {
545
546
  return Math.max(base, Math.min(cap, Math.floor(base + r * (upper - base))));
546
547
  }
547
548
 
548
- /** A semantic label for the new-item creation outcome. */
549
+ /**
550
+ * A semantic label for the new-item creation outcome.
551
+ *
552
+ * `already-exists-from-source` (task
553
+ * `observation-triage-already-triaged-benign-skip`): the target path already
554
+ * exists on `<arbiter>/main` AND that existing item is PROVABLY the one this same
555
+ * `sourceItem` already minted (its `promotedFrom:` back-reference equals
556
+ * `sourceItem`). This is a TERMINAL idempotency fact — the source was already
557
+ * triaged in a prior run and a retry can NEVER succeed — so it is a BENIGN SKIP
558
+ * (`exitCode: 0`), NOT the loud `lost` (exit 2) a genuine concurrent-create race
559
+ * gets. Distinguishable from `created`/`lost` so a caller (and CI) can grep it out.
560
+ * Only ever produced when the caller passes `sourceItem`; without it the
561
+ * path-exists case stays `lost` exactly as before (zero behaviour change).
562
+ */
549
563
  export type CreateItemOutcome =
550
564
  | 'created'
551
565
  | 'usage-error'
552
566
  | 'lost'
567
+ | 'already-exists-from-source'
553
568
  | 'contended';
554
569
 
555
570
  /** Maps onto the claim-CAS exit codes (identical semantics). */
@@ -574,6 +589,21 @@ export interface CreateItemThroughCasOptions {
574
589
  * A listed path that is absent on the create branch is skipped (best-effort rm).
575
590
  */
576
591
  deletePaths?: string[];
592
+ /**
593
+ * OPTIONAL provenance disambiguator for the LOST-race case (task
594
+ * `observation-triage-already-triaged-benign-skip`). When set, this is the
595
+ * namespaced identity of the SOURCE item this create was minted FROM (e.g.
596
+ * `observation:<slug>`). If the target `path` already exists on `<arbiter>/main`
597
+ * AND that existing file's `promotedFrom:` back-reference equals THIS `sourceItem`,
598
+ * the create is TERMINAL-by-existence (the source was already triaged in a prior
599
+ * run) — a BENIGN SKIP (`already-exists-from-source`, exit 0), NOT the loud `lost`
600
+ * (exit 2) a genuine concurrent-create race with an UNRELATED same-path item gets.
601
+ * The match is on the explicit back-reference, NOT the slug, so an unrelated task
602
+ * that merely shares the slug does NOT false-positive into a benign skip. Absent
603
+ * (or the existing file carries no matching `promotedFrom:`) ⇒ the path-exists case
604
+ * stays `lost` exactly as before (zero behaviour change for non-provenance callers).
605
+ */
606
+ sourceItem?: string;
577
607
  /** Working clone/worktree the creation runs in. */
578
608
  cwd: string;
579
609
  /** Name of the arbiter remote (`--arbiter`). Defaults to `origin`. */
@@ -688,6 +718,36 @@ async function runCreate(
688
718
  );
689
719
  }
690
720
 
721
+ // TERMINAL 'already triaged' PRE-CHECK (case A; task
722
+ // `observation-triage-already-triaged-benign-skip`). When the caller passes
723
+ // `sourceItem`, check ONCE UP FRONT whether the target already exists on the
724
+ // arbiter AND was PROVABLY minted from THIS same source in a PRIOR run (its
725
+ // `promotedFrom:` back-reference matches). If so this is an idempotency fact, not
726
+ // a race: a BENIGN SKIP (exit 0), never a retry. Doing this BEFORE the contention
727
+ // loop is what keeps it distinct from case B (a LIVE concurrent-create race): a
728
+ // race winner's file appears DURING the loop (the target is absent at this
729
+ // pre-check), so the loser still funnels through the loop to the loud `lost`
730
+ // (exit 2) the existing same-NEW-task race semantics require. Only a task that
731
+ // ALREADY existed before this leg began resolves here.
732
+ if (options.sourceItem !== undefined && options.sourceItem !== '') {
733
+ await gitHard(['fetch', '--quiet', arbiter], cwd, env);
734
+ const skip = await alreadyMintedFromSource(
735
+ options.sourceItem,
736
+ path,
737
+ arbiter,
738
+ cwd,
739
+ env,
740
+ );
741
+ if (skip !== undefined) {
742
+ note(skip);
743
+ return {
744
+ exitCode: 0,
745
+ outcome: 'already-exists-from-source',
746
+ message: skip,
747
+ };
748
+ }
749
+ }
750
+
691
751
  // A throwaway create branch, slugified off the new item's path (so concurrent
692
752
  // creators of DIFFERENT paths don't collide on the branch name either).
693
753
  const createBranch = `create/${path.replace(/[^A-Za-z0-9._-]+/g, '-')}`;
@@ -911,6 +971,41 @@ async function catFileExists(
911
971
  return (await gitSoft(['cat-file', '-e', object], cwd, env)).status === 0;
912
972
  }
913
973
 
974
+ /**
975
+ * The LOST-race case-A disambiguator (task
976
+ * `observation-triage-already-triaged-benign-skip`): is the item ALREADY at `path`
977
+ * on `<arbiter>/main` PROVABLY the one `sourceItem` already minted? Reads that
978
+ * file's `promotedFrom:` back-reference and returns a benign-skip MESSAGE iff it
979
+ * equals `sourceItem`, else `undefined` (⇒ the caller keeps the loud `lost`).
980
+ *
981
+ * The match is on the EXPLICIT `promotedFrom:` back-reference, NOT the slug, so an
982
+ * unrelated task that merely shares the slug (or a slug PREFIX) never
983
+ * false-positives — only an item this exact source stamped resolves to the skip. A
984
+ * plumbing failure reading the existing file degrades to `undefined` (stay loud),
985
+ * never a false benign skip.
986
+ */
987
+ async function alreadyMintedFromSource(
988
+ sourceItem: string,
989
+ path: string,
990
+ arbiter: string,
991
+ cwd: string,
992
+ env: NodeJS.ProcessEnv | undefined,
993
+ ): Promise<string | undefined> {
994
+ const show = await gitSoft(['show', `${arbiter}/main:${path}`], cwd, env);
995
+ if (show.status !== 0) {
996
+ return undefined;
997
+ }
998
+ const promotedFrom = readFrontmatterField(show.stdout, 'promotedFrom');
999
+ if (promotedFrom !== sourceItem) {
1000
+ return undefined;
1001
+ }
1002
+ return (
1003
+ `'${path}' already exists on ${arbiter}/main and was PROVABLY minted from ` +
1004
+ `${sourceItem} (its promotedFrom: back-reference matches) — already triaged in ` +
1005
+ `a prior run. Terminal by existence; a retry can never succeed. Benign skip.`
1006
+ );
1007
+ }
1008
+
914
1009
  /** Run git, returning the raw result (no throw) — for soft checks. */
915
1010
  function gitSoft(
916
1011
  args: string[],
@@ -66,6 +66,18 @@ export interface LaunchWithOptionalWatchInput {
66
66
  sessionsDir?: string;
67
67
  /** Tail the session `.jsonl` live (pi harness only). */
68
68
  watch?: boolean;
69
+ /**
70
+ * **Optional agent-session DEADLINE** (absolute epoch-ms wall-clock timestamp,
71
+ * spec `graceful-pre-timeout-wip-checkpoint`) threaded through to
72
+ * {@link LaunchInput.deadlineMs}. Forces the ASYNC {@link
73
+ * PiHarness.launchAsync} path (even without `watch`), because the deadline
74
+ * race only works there — `spawnSync` cannot be interrupted from the same
75
+ * event loop. A non-pi harness with a deadline set silently degrades to a
76
+ * plain `harness.launch` (no race is possible), the same safe fall-through
77
+ * used for `watch` on a non-pi harness. Absent ⇒ no deadline; a run that
78
+ * finishes normally is byte-for-byte unchanged.
79
+ */
80
+ deadlineMs?: number;
69
81
  /** Where the tailed high-signal lines are written (defaults to stderr). */
70
82
  watchSink?: (line: string) => void;
71
83
  /** Emit ANSI colour in the tailed lines (the caller's TTY/`NO_COLOR` decision). */
@@ -97,17 +109,27 @@ export async function launchWithOptionalWatch(
97
109
  id: input.sessionId,
98
110
  });
99
111
 
100
- // `--watch` (pi only): launch async + tail the KNOWN session .jsonl path
101
- // concurrently. The caller is responsible for rejecting `--watch` on a non-pi
102
- // harness up-front (as `do` does); a non-pi harness here falls through to the
103
- // sync launch below rather than tailing a log that will never exist.
104
- if (input.watch === true && harness instanceof PiHarness) {
105
- const tailer = new SessionTailer({
106
- sessionFile: session,
107
- color: input.color ?? false,
108
- sink: input.watchSink,
109
- });
110
- tailer.start();
112
+ const wantsAsync =
113
+ (input.watch === true || input.deadlineMs !== undefined) &&
114
+ harness instanceof PiHarness;
115
+
116
+ // `--watch` OR a threaded deadline (spec
117
+ // `graceful-pre-timeout-wip-checkpoint`) forces the ASYNC path: the deadline
118
+ // race is only expressible against `launchAsync` (a spawnSync child cannot be
119
+ // SIGTERMed from the same event loop), and `--watch` needs it to tail the
120
+ // session log concurrently. Both are pi-only — a non-pi harness here falls
121
+ // through to the sync launch below (safe: watch would have no log to tail,
122
+ // and the deadline was already opt-in).
123
+ if (wantsAsync) {
124
+ const tailer =
125
+ input.watch === true
126
+ ? new SessionTailer({
127
+ sessionFile: session,
128
+ color: input.color ?? false,
129
+ sink: input.watchSink,
130
+ })
131
+ : undefined;
132
+ tailer?.start();
111
133
  try {
112
134
  return await harness.launchAsync({
113
135
  dir: input.dir,
@@ -116,12 +138,15 @@ export async function launchWithOptionalWatch(
116
138
  prompt: input.prompt,
117
139
  model: input.model,
118
140
  session,
141
+ deadlineMs: input.deadlineMs,
119
142
  env: input.env,
120
143
  });
121
144
  } finally {
122
145
  // Always release the tailer (one final drain) — even on a launch error —
123
146
  // so the observer never outlives the run or leaks a handle.
124
- await tailer.stop();
147
+ if (tailer) {
148
+ await tailer.stop();
149
+ }
125
150
  }
126
151
  }
127
152
 
package/src/agent-stop.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {runAsync} from './git.js';
2
2
  import {JOB_RECORD_FILENAME} from './workspace.js';
3
+ import type {NewQuestion} from './sidecar.js';
3
4
 
4
5
  /**
5
6
  * **The build-agent → runner REPORTING CHANNEL** (task `agent-stop-signal`).
@@ -11,8 +12,10 @@ import {JOB_RECORD_FILENAME} from './workspace.js';
11
12
  *
12
13
  * 1. A HARD **STOP** sentinel ({@link STOP_SENTINEL_OPEN} … {@link
13
14
  * STOP_SENTINEL_CLOSE}): the task DRIFTED / is ambiguous / rests on a stale
14
- * premise, so the agent could not build it. The runner routes the item to
15
- * `work/needs-attention/` (the agent's reason VERBATIM) and SKIPS the
15
+ * premise, so the agent could not build it. The runner marks the item stuck
16
+ * on its per-item lock (post lock-cutover `state: stuck` with the agent's
17
+ * reason VERBATIM on the lock entry, no `work/needs-attention/` folder
18
+ * write) and SKIPS the
16
19
  * acceptance gate + the Gate-2 review (a clean STOP is not a build that
17
20
  * changed nothing). See {@link parseStopSentinel}.
18
21
  * 2. A SOFT **`## Decisions`** block ({@link extractDecisionsBlock}): the agent
@@ -359,3 +362,58 @@ export function emptyDiffStopReason(slug: string): string {
359
362
  'arbiter main); treating as a no-op/stop — re-scope or re-claim.'
360
363
  );
361
364
  }
365
+
366
+ /**
367
+ * The DISCRIMINATED classification of a `saveAgentStop` reason (spec
368
+ * `surface-stuck-as-questions-and-retire-stuck-lock-state` resolved decision #2,
369
+ * task `empty-diff-bounce-surfaces-dispose-defaulted-question`). The two kinds
370
+ * route DIFFERENTLY at the bounce seam:
371
+ *
372
+ * - `'sentinel'` — the agent raised a hard STOP with a specific drift report
373
+ * (the principled case). The runner surfaces the reason VERBATIM through the
374
+ * existing lock-stuck bounce path.
375
+ * - `'empty-diff'` — the agent produced no source change (the deterministic
376
+ * backstop). Because "nothing to do" is a NON-DETERMINISTIC LLM judgement, a
377
+ * blind requeue would infinite-loop; instead the runner surfaces a sidecar
378
+ * with an engine-authored DISPOSE-DEFAULTED disposition question. The engine
379
+ * owns the envelope + the safe default; the LLM owns the prose.
380
+ */
381
+ export type AgentStopKind = 'sentinel' | 'empty-diff';
382
+
383
+ /** A classified STOP verdict + the needs-attention reason prose. */
384
+ export interface AgentStopClass {
385
+ kind: AgentStopKind;
386
+ reason: string;
387
+ }
388
+
389
+ /**
390
+ * The DISPOSE-DEFAULTED disposition question the empty-diff bounce path surfaces
391
+ * as its engine-authored envelope entry (spec resolved decision #2). The engine
392
+ * GUARANTEES this envelope exists on every empty-diff bounce even when the
393
+ * agent surfaced no questions of its own; the LLM/human owns the prose that
394
+ * follows. The `default: 'dispose'` hint routes an answered "cancel" to the
395
+ * regime-polymorphic `dispose` outcome (task → `git mv → tasks/cancelled/`,
396
+ * RETAINED; NOT a `git rm`, NOT a requeue) via the shared apply decider
397
+ * (`apply-decide.ts`).
398
+ */
399
+ export function emptyDiffDisposeEnvelope(params: {
400
+ item: string;
401
+ reason: string;
402
+ }): NewQuestion {
403
+ const {item, reason} = params;
404
+ return {
405
+ question:
406
+ `'${item}': the agent produced no change (${reason}). ` +
407
+ 'Cancel this item? [default: yes]',
408
+ context:
409
+ '"Nothing to do" is a non-deterministic LLM judgement, so this bounce is ' +
410
+ 'SURFACED (not blindly requeued) to break any infinite "re-run → re-judge ' +
411
+ 'nothing-to-do → re-bounce" loop. Answering "cancel" (or accepting the ' +
412
+ 'default) disposes this task to its terminal (`git mv → ' +
413
+ '`work/tasks/cancelled/`, retained) via the regime-polymorphic `dispose` ' +
414
+ 'outcome — the task is NOT hard-deleted. Answer with a REQUEUE / RESET ' +
415
+ 'directive if you disagree with the agent and want the loop to try again.',
416
+ default: 'dispose (cancel this task → work/tasks/cancelled/, retained)',
417
+ kind: 'stuck',
418
+ };
419
+ }
@@ -29,8 +29,19 @@ import {
29
29
  * from the agent's VERDICT, NOT a human `promote-*` field.
30
30
  *
31
31
  * **The allowed set** (the SUBSET the apply rung passes to `decide`):
32
- * `{task | spec | adr | delete | ask}` \u2014 i.e. `{mint-task | mint-spec | mint-adr |
33
- * delete-source | ask-follow-up}`. `adr` was DEFERRED at the keystone launch (no
32
+ * `{task | spec | adr | dispose | resolve | ask}` \u2014 i.e. `{mint-task | mint-spec |
33
+ * mint-adr | dispose-source | resolve-no-mint | ask-follow-up}`. The `dispose`
34
+ * verdict replaced the older `delete` token (task
35
+ * `apply-disposition-delete-to-dispose-regime-polymorphic`, spec
36
+ * `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #5): the
37
+ * disposal is now REGIME-POLYMORPHIC (observation \u2192 `git rm`, task \u2192
38
+ * `git mv` to `tasks/cancelled/`, spec \u2192 `git mv` to `specs/dropped/`), so a
39
+ * task can no longer be hard-deleted by the apply rung. `resolve` was added
40
+ * by the task `apply-decide-resolve-verdict-mint-nothing` so the decider can
41
+ * honestly handle "the human answered, keep the note on record, mint nothing"
42
+ * (previously it had no valid verdict for that case and looped on `ask`,
43
+ * re-surfacing an already-answered question every tick). `adr` was DEFERRED at the
44
+ * keystone launch (no
34
45
  * ADR-mint path existed yet) and is now WIRED by the follow-on task
35
46
  * `agentic-apply-mint-adr-route`, which added the {@link
36
47
  * import('./mint-adr.js').mintAdr} route and widened this set. Intake's set is
@@ -54,7 +65,8 @@ export const APPLY_ALLOWED_OUTCOMES: readonly DecisionOutcome[] = [
54
65
  'task',
55
66
  'spec',
56
67
  'adr',
57
- 'delete',
68
+ 'dispose',
69
+ 'resolve',
58
70
  'ask',
59
71
  ];
60
72
 
@@ -136,7 +148,10 @@ export function buildApplyDecisionInput(opts: {
136
148
  * the human's recorded ANSWER(S) + the SOURCE item and emit a single
137
149
  * `{outcome, \u2026}` verdict ({@link parseDecisionVerdict} reads it). The agent decides
138
150
  * what to DO with the answered signal \u2014 mint a self-contained task, mint a SPEC,
139
- * mint an ADR, delete the source, or ask one BATCH of follow-up questions \u2014
151
+ * mint an ADR, DISPOSE the source (regime-polymorphic: an observation is
152
+ * git-rm-ed, a task is `git mv`-ed to `tasks/cancelled/`, a spec to
153
+ * `specs/dropped/`), resolve it (settle the loop, mint nothing, KEEP the note),
154
+ * or ask one BATCH of follow-up questions \u2014
140
155
  * grounded in the source's full context. It writes NOTHING (the engine acts on
141
156
  * the verdict).
142
157
  *
@@ -178,9 +193,23 @@ export function buildApplyDecisionPrompt(input: ApplyDecisionInput): string {
178
193
  ` a self-contained ADR (docs/adr/, the context/decision/why shape) carrying`,
179
194
  ` the WHY from the answer(s). Emit {"outcome":"adr","adrSlug":"\u2026",`,
180
195
  ` "adrTitle":"\u2026","adrBody":"\u2026 (markdown AFTER the frontmatter)"}.`,
181
- ` - "delete": the answer means this signal should be DROPPED. Emit`,
182
- ` {"outcome":"delete","deleteReason":"\u2026"} (a single revertible deletion;`,
183
- ` the reason rides the commit message, git history is the archive).`,
196
+ ` - "dispose": the answer means this signal should be DISPOSED. Emit`,
197
+ ` {"outcome":"dispose","disposeReason":"\u2026"}. REGIME-POLYMORPHIC on the`,
198
+ ` SOURCE type: an OBSERVATION is git-rm-ed in a single revertible commit`,
199
+ ` (reason rides the message, git history is the archive); a TASK is git-mv-ed`,
200
+ ` to \`tasks/cancelled/\` (RETAINED, reason recorded in the moved body's`,
201
+ ` \`reason:\` frontmatter); a SPEC is git-mv-ed to \`specs/dropped/\` (RETAINED).`,
202
+ ` A task can NEVER be hard-deleted by the apply rung \u2014 dispose is the`,
203
+ ` only path off the board.`,
204
+ ` - "resolve": the answer SETTLES this item and there is NOTHING to mint (no`,
205
+ ` task/spec/adr) \u2014 the correct move is to CLOSE the question-loop while`,
206
+ ` KEEPING the note on record (e.g. an evidence/watch-item observation whose`,
207
+ ` answer is "acknowledged, keep this on record, no artifact"). The answers are`,
208
+ ` harvested into the item body and the loop is cleared; the note is RETAINED`,
209
+ ` (this is the sibling of "dispose", which DROPS the observation-note or`,
210
+ ` moves a task/spec to its terminal \u2014 pick "resolve" when the observation`,
211
+ ` should SURVIVE in place, "dispose" when it should not). Emit`,
212
+ ` {"outcome":"resolve","resolveReason":"\u2026"}.`,
184
213
  ` - "ask": you need more from the human before acting. Emit`,
185
214
  ` {"outcome":"ask","question":"\u2026"} \u2014 ask everything you still need as ONE`,
186
215
  ` batch (never a drip); the engine appends it and re-pauses.`,
@@ -1,9 +1,9 @@
1
1
  import {existsSync, readFileSync} from 'node:fs';
2
2
  import {join} from 'node:path';
3
3
  import {
4
- allAnswered,
5
4
  isEntryAnswered,
6
5
  parseSidecar,
6
+ resolveSidecarIdentity,
7
7
  sidecarPathFor,
8
8
  type SidecarEntry,
9
9
  type SidecarModel,
@@ -116,11 +116,23 @@ export function parseMergeAnswer(text: string): MergeActionVerb | undefined {
116
116
  }
117
117
 
118
118
  /**
119
- * Detect the FIRST answered `kind: merge` entry's deterministic action verb in
120
- * a fully-answered sidecar (the apply rung's pre-decider kind-check). Returns
121
- * `undefined` when the sidecar carries no answered `kind: merge` entry (the
122
- * apply rung then proceeds to the existing path — agentic for observations,
123
- * normal apply for task/spec content questions).
119
+ * Detect the `kind: merge` action verb that should drive the next apply run.
120
+ * The apply rung's pre-decider kind-check; returns `undefined` when there is
121
+ * nothing for the dispatcher to act on.
122
+ *
123
+ * ORDERING (task `merge-action-nits-followup` nit 1). The re-stale re-surface
124
+ * path (`advance.ts` `maybeRunMergeAction` restale branch) APPENDS a new
125
+ * `kind: merge` follow-up via `appendQuestions` rather than clearing the prior
126
+ * `answer=merge` on the original entry — that append-and-keep-history shape is
127
+ * canonical, but a naive FIRST-match lookup would re-fire against the STALE
128
+ * prior answer instead of the human's fresh follow-up. So:
129
+ *
130
+ * 1. If ANY `kind: merge` entry is UNANSWERED, return `undefined` — that is
131
+ * the re-paused state (a freshly-appended follow-up awaiting an answer),
132
+ * and the apply MUST NOT fire against a stale prior sibling.
133
+ * 2. Otherwise return the LATEST answered `kind: merge` entry — a fresh
134
+ * follow-up answer wins over the stale one, and a plain single-entry
135
+ * merge-question sidecar behaves as before.
124
136
  *
125
137
  * The sidecar is read OFF DISK keyed off the namespaced item identity; the
126
138
  * model is parsed via the SAME `parseSidecar` the rest of the engine uses.
@@ -139,8 +151,13 @@ export function detectAnsweredMergeAction(
139
151
  } catch {
140
152
  return undefined;
141
153
  }
142
- if (!allAnswered(model)) return undefined;
154
+ // (1) Unanswered `kind: merge` follow-up ⇒ re-paused; the apply must NOT fire.
143
155
  for (const entry of model.entries) {
156
+ if (entry.kind === 'merge' && !isEntryAnswered(entry)) return undefined;
157
+ }
158
+ // (2) LATEST answered `kind: merge` entry — a fresh follow-up beats a stale one.
159
+ for (let i = model.entries.length - 1; i >= 0; i--) {
160
+ const entry = model.entries[i];
144
161
  if (entry.kind !== 'merge') continue;
145
162
  if (!isEntryAnswered(entry)) continue;
146
163
  const verb = parseMergeAnswer(entry.answer);
@@ -336,6 +353,27 @@ export async function performMergeAction(
336
353
  const note = input.note ?? (() => {});
337
354
  const {verb} = input.action;
338
355
 
356
+ // INVARIANT (task `merge-action-nits-followup` nit 4). The merge-question
357
+ // surfacer only emits `kind: merge` sidecars for TASKS, so the hard-coded
358
+ // `type: 'task'` on the `createJob` call below is correct in practice — but
359
+ // if a future surfacer change ever stamps a `kind: merge` sidecar on a
360
+ // non-task item (e.g. a spec-level unmerged branch), the branch name
361
+ // `work/task-<slug>` would silently mis-target. Fail LOUDLY here so the
362
+ // invariant is asserted at the dispatcher's entry rather than manifesting
363
+ // as a mysterious rebase/push mis-target downstream.
364
+ const identity = resolveSidecarIdentity(input.item);
365
+ if (identity.type !== 'task') {
366
+ throw new Error(
367
+ `performMergeAction: sidecar source item \`${input.item}\` has type ` +
368
+ `\`${identity.type}\`, but the answered-merge dispatcher only supports ` +
369
+ `\`task\` items (the surfacer only stamps \`kind: merge\` on tasks; the ` +
370
+ `branch name \`work/task-<slug>\` would silently mis-target otherwise). ` +
371
+ `This is the entry-invariant asserted by nit 4 of task ` +
372
+ `\`merge-action-nits-followup\` — reconcile the surfacer or extend the ` +
373
+ `dispatcher before landing a non-task source item.`,
374
+ );
375
+ }
376
+
339
377
  if (verb === 'hold') {
340
378
  return {
341
379
  outcome: 'hold',