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
package/src/advance.ts CHANGED
@@ -66,6 +66,12 @@ import {
66
66
  type MergeActionHandler,
67
67
  type MergeActionResult,
68
68
  } from './apply-merge-action.js';
69
+ import {
70
+ detectAnsweredStuckAction,
71
+ performStuckAction,
72
+ type StuckActionHandler,
73
+ type StuckActionResult,
74
+ } from './apply-stuck-action.js';
69
75
  import type {VerifyConfig} from './verify.js';
70
76
  import type {NewQuestion} from './sidecar.js';
71
77
 
@@ -131,11 +137,34 @@ export type AdvanceOutcome =
131
137
  | 'advanced'
132
138
  | 'no-op'
133
139
  | 'vanished'
140
+ /**
141
+ * The triage/apply promote leg found the task it would mint ALREADY EXISTS on
142
+ * the arbiter AND was PROVABLY minted from THIS observation in a prior run (its
143
+ * `promotedFrom:` back-reference matches) — an idempotency fact, so the source was
144
+ * already triaged. `exitCode: 0` (the matrix tolerates it, so it does NOT red CI),
145
+ * DISTINGUISHABLE from `vanished` (item file gone) and `no-op` (calm classify) so
146
+ * reviewers can grep it. The LOUD `lost` (exit 2) is reserved for a genuine
147
+ * concurrent-create race with an UNRELATED same-path item, where a retry helps.
148
+ * (task `observation-triage-already-triaged-benign-skip`; sibling of the
149
+ * stale-snapshot / held-lock CI-noise fixes.)
150
+ */
151
+ | 'already-triaged'
134
152
  | 'usage-error'
135
153
  | 'lost'
136
154
  | 'contended'
137
155
  | 'not-implemented'
138
- | 'invariant-violation';
156
+ | 'invariant-violation'
157
+ /**
158
+ * The answered-merge dispatcher REFUSED the land on the rebased tip (RED
159
+ * re-verify, rebase conflict, or a pre-checkout failure). Distinct from
160
+ * `usage-error` (which is reserved for genuine caller-usage errors — e.g.
161
+ * the workspacesDir-unset guard below). `performIntegration` has already
162
+ * routed the item to needs-attention via its shared seam, so `main` never
163
+ * received a failing tree; the sidecar is LEFT IN PLACE so the open answer
164
+ * stays surfaced for a human follow-up. `exitCode: 1`.
165
+ * (task `merge-action-nits-followup` nit 2.)
166
+ */
167
+ | 'merge-refused';
139
168
 
140
169
  /** Maps onto the claim-CAS exit codes (identical semantics). */
141
170
  export type AdvanceExitCode = 0 | 1 | 2 | 3;
@@ -166,7 +195,7 @@ export interface RungExecutor {
166
195
  export interface RungExecInput {
167
196
  /** The canonical namespaced identity (`task:<slug>` / `spec:<slug>` / `observation:<slug>`). */
168
197
  item: string;
169
- /** The resolved namespace (`task` / `prd` / `observation`). */
198
+ /** The resolved namespace (`task` / `spec` / `observation`). */
170
199
  namespace: SlugNamespace;
171
200
  /** The bare slug. */
172
201
  slug: string;
@@ -250,8 +279,8 @@ export interface AdvanceContext {
250
279
  * The AGENTIC apply DECISION seam (task
251
280
  * `agentic-apply-retire-disposition-vocabulary`): the fresh-context decision
252
281
  * agent the apply rung runs on a fully-answered OBSERVATION to choose what to DO
253
- * with the signal (`mint-task | mint-spec | mint-adr | delete-source |
254
- * ask-follow-up`),
282
+ * with the signal (`mint-task | mint-spec | mint-adr | dispose-source |
283
+ * resolve-no-mint | ask-follow-up`),
255
284
  * grounded in the source's full context. It is the injected
256
285
  * {@link ApplyDecider} the shared `decide(input, allowedOutcomes)` engine runs;
257
286
  * tests inject a CANNED verdict (no model). `undefined` ⇒ the apply rung defaults
@@ -322,9 +351,13 @@ export interface AdvanceContext {
322
351
  * The execution working area (`workspacesDir`, default `~/.dorfl`) the
323
352
  * answered-merge LAND uses to cut a per-job worktree from the hub mirror
324
353
  * (via `workspace.ts` `createJob`). Unset ⇒ no `workspacesDir` is available
325
- * to the dispatcher, so an answered `kind: merge` entry is REFUSED (clean
326
- * surfacing); the answer stays for a follow-up. The registry-set advance
327
- * driver threads the resolved `workspacesDir` here.
354
+ * to the dispatcher, so an answered `kind: merge` entry is REFUSED as a
355
+ * genuine caller-usage error (outcome `usage-error`, `exitCode: 1`) and the
356
+ * answer stays surfaced for a follow-up — threading `workspacesDir` is the
357
+ * caller's contract, and forgetting it gets this documented clean refusal
358
+ * rather than a mysterious downstream failure (task
359
+ * `merge-action-nits-followup` nit 3). The registry-set advance driver
360
+ * threads the resolved `workspacesDir` here.
328
361
  * (spec `land-time-reverify-and-parallel-merge-ceiling`, task
329
362
  * `apply-rung-merge-disposition`)
330
363
  */
@@ -370,6 +403,20 @@ export interface AdvanceContext {
370
403
  * WITHOUT spinning up a hub mirror or running real verify.
371
404
  */
372
405
  mergeAction?: MergeActionHandler;
406
+ /**
407
+ * The stuck-action dispatch SEAM (task
408
+ * `apply-resolve-reset-flag-discards-work-branch`, spec
409
+ * `surface-stuck-as-questions-and-retire-stuck-lock-state`): the
410
+ * deterministic answer-driven runner-action handler the apply rung invokes
411
+ * BEFORE the fall-through persist when an answered `kind: 'stuck'` entry
412
+ * (the shape the bounce-surface path stamps) is detected. Production wires
413
+ * {@link performStuckAction} (drives the shared
414
+ * `deleteRemoteWorkBranchIfPresent` primitive on `reset`); tests inject a
415
+ * stub so they assert on the apply-rung's `keep | reset | refused | cancel`
416
+ * routing WITHOUT touching a real arbiter. The deterministic sibling of
417
+ * {@link mergeAction} for the `kind: 'stuck'` axis.
418
+ */
419
+ stuckAction?: StuckActionHandler;
373
420
  /** Sink for human-readable progress notes. */
374
421
  note?: (message: string) => void;
375
422
  }
@@ -602,7 +649,10 @@ function mapDoOutcome(result: DoResult): AdvanceOutcome {
602
649
  * answered sidecar back to not-all-answered (the persist owns that). An EMPTY
603
650
  * emit (the skill's honest "no open judgement") writes nothing and reports it.
604
651
  */
605
- async function surfaceRung(input: RungExecInput): Promise<RungExecResult> {
652
+ async function surfaceRung(
653
+ input: RungExecInput,
654
+ surfaceOpts: {baseQuestions?: NewQuestion[]} = {},
655
+ ): Promise<RungExecResult> {
606
656
  const {item, context} = input;
607
657
  const note = context.note ?? (() => {});
608
658
  const cwd = context.cwd;
@@ -615,50 +665,84 @@ async function surfaceRung(input: RungExecInput): Promise<RungExecResult> {
615
665
  return vanishedSkip({rung: 'surface', item});
616
666
  }
617
667
 
668
+ // DETERMINISTIC BASE QUESTIONS (the triage rung's "always ask" contract): the
669
+ // caller may pass engine-built questions that MUST be surfaced regardless of the
670
+ // agent. They are added ONLY on the FIRST pass (no sidecar yet) — on a re-surface
671
+ // the sidecar already carries them, and `appendQuestions` does NOT dedup, so
672
+ // re-adding would duplicate. When base questions are present the agent is
673
+ // ADDITIVE ONLY and its flake/empty is NON-FATAL (the base question still lands).
674
+ const baseQuestions = surfaceOpts.baseQuestions ?? [];
675
+ const hasBase = baseQuestions.length > 0;
676
+ const sidecarExists = existsSync(join(cwd, sidecarPathFor(item)));
677
+ const baseToAdd = hasBase && !sidecarExists ? baseQuestions : [];
678
+
618
679
  // 1. SPAWN the fresh-context `surface-questions` agent (the skill JUDGES). The
619
680
  // expensive model work is POST-lock (the lock is held by `performAdvance`).
681
+ //
682
+ // SHORT-CIRCUIT (primary/load-bearing half of task
683
+ // `surface-short-circuit-already-triaged-observations-and-harden-skill-empty-emit`):
684
+ // an OBSERVATION with provably no open judgement (frontmatter `needsAnswers` NOT
685
+ // true, no non-empty `## Open questions` section, no pending sidecar) — the
686
+ // typical shape of a decision-record / already-triaged note — deterministically
687
+ // yields `{questions: []}` WITHOUT round-tripping the flaky surface-questions
688
+ // agent (source observation
689
+ // `surface-questions-agent-still-emits-no-parseable-questions-on-decision-record-obs-2026-07-10`).
690
+ // The loud-error contract from
691
+ // `advance-surface-limbo-observation-loudly-instead-of-silent-no-op` is
692
+ // PRESERVED for the observations that DO reach the agent — this only spares the
693
+ // ones that provably have nothing to ask. Conservative: fires only on
694
+ // observations; tasks/specs still always ask.
620
695
  const gate = context.surfaceGate ?? harnessSurfaceGate();
621
696
  let emit;
622
- try {
623
- emit = await gate({
624
- item,
625
- cwd,
626
- surfaceModel: context.surfaceModel,
627
- });
628
- } catch (err) {
629
- const detail = err instanceof Error ? err.message : String(err);
630
- return {
631
- exitCode: 1,
632
- outcome: 'usage-error',
633
- message: `surface ${item}: the surface-questions agent produced no usable emit (${detail}).`,
634
- };
697
+ if (isNothingToSurfaceObservation(cwd, input, itemPath, sidecarExists)) {
698
+ note(
699
+ `surface ${item}: auto-triaged (no open questions, no sidecar) — skipped agent.`,
700
+ );
701
+ emit = {questions: []};
702
+ } else {
703
+ try {
704
+ emit = await gate({
705
+ item,
706
+ cwd,
707
+ surfaceModel: context.surfaceModel,
708
+ });
709
+ } catch (err) {
710
+ const detail = err instanceof Error ? err.message : String(err);
711
+ if (!hasBase) {
712
+ return {
713
+ exitCode: 1,
714
+ outcome: 'usage-error',
715
+ message: `surface ${item}: the surface-questions agent produced no usable emit (${detail}).`,
716
+ };
717
+ }
718
+ // The agent flaked but we have a deterministic base question to surface: the
719
+ // flake is NON-FATAL. Treat the agent's extras as empty and carry on (the
720
+ // base triage question still lands, so the human can triage via the sidecar).
721
+ note(
722
+ `surface ${item}: the surface-questions agent produced no usable emit ` +
723
+ `(${detail}); surfacing the deterministic question(s) only.`,
724
+ );
725
+ emit = {questions: []};
726
+ }
635
727
  }
636
728
 
637
729
  // 2. The ENGINE persists (the skill wrote nothing): append-or-create the sidecar
638
730
  // + set `needsAnswers:true` in ONE commit (CAS-atomic under the held lock).
731
+ // Base questions FIRST (q1…), then the agent's additive extras.
639
732
  const persist = context.surfacePersist ?? persistSurfacedQuestions;
640
733
  const result = persist({
641
734
  cwd,
642
735
  item,
643
736
  itemPath,
644
- questions: toNewQuestions(emit),
737
+ questions: [...baseToAdd, ...toNewQuestions(emit)],
645
738
  note,
646
739
  });
647
740
  if (result.outcome === 'nothing') {
648
- // LIMBO DETECTION (task `advance-surface-limbo-observation-loudly-instead-of-
649
- // silent-no-op`): an untriaged observation with NO sidecar whose surfacer had
650
- // nothing to ask is a TRAP the human's triage answer may have been recorded
651
- // in an in-BODY "Applied answers" block, but the engine reads triage-vs-
652
- // settled only from the `triaged:` frontmatter marker (`ledger-read.ts`) and
653
- // the promote path only from an answered `disposition: promote` SIDECAR
654
- // (`triage-persist.ts`). Without either, the item is untriaged (re-enumerated
655
- // every tick), un-surfaceable (nothing to ask), un-promotable (no sidecar) —
656
- // a silent exit-0 no-op forever. Surface it LOUDLY instead.
657
- const limbo = detectObservationLimbo(input);
658
- if (limbo !== undefined) {
659
- note(limbo);
660
- return {exitCode: 1, outcome: 'usage-error', message: limbo};
661
- }
741
+ // The agent had nothing to ask AND there was no base question to add (a
742
+ // task/spec surface, or a re-surface whose base question is already present).
743
+ // This is the calm "no open judgement" no-op no sidecar written. (There is
744
+ // no "limbo" any more: the triage rung ALWAYS passes a base question on the
745
+ // first pass, so an untriaged observation can never fall here empty-handed.)
662
746
  return {
663
747
  exitCode: 0,
664
748
  outcome: 'no-op',
@@ -675,40 +759,113 @@ async function surfaceRung(input: RungExecInput): Promise<RungExecResult> {
675
759
  }
676
760
 
677
761
  /**
678
- * Detect the OBSERVATION LIMBO shape (task `advance-surface-limbo-observation-
679
- * loudly-instead-of-silent-no-op`): an observation whose triage answer was
680
- * (mis-)authored in an in-body "Applied answers" block instead of the sidecar/
681
- * frontmatter channels the engine reads. All four conditions must hold:
762
+ * Would this OBSERVATION provably surface `{questions: []}` — i.e. is there no
763
+ * open judgement anywhere on it? Cheap read-only predicate the surface rung uses
764
+ * to skip the flaky agent round-trip for already-triaged / decision-record notes
765
+ * (task
766
+ * `surface-short-circuit-already-triaged-observations-and-harden-skill-empty-emit`;
767
+ * source observation
768
+ * `surface-questions-agent-still-emits-no-parseable-questions-on-decision-record-obs-2026-07-10`).
682
769
  *
683
- * 1. the item is an OBSERVATION;
684
- * 2. its frontmatter has NO `triaged:` marker (untriaged per `ledger-read.ts`);
685
- * 3. there is NO active question sidecar at `work/questions/observation-<slug>.md`;
686
- * 4. the surfacer just returned empty (the caller {@link surfaceRung} — only
687
- * calls this on the persist's `nothing` branch).
770
+ * Fires ONLY when ALL of these hold (conservative — err on the side of STILL
771
+ * calling the agent if uncertain):
772
+ * - the item's namespace is `observation` (tasks/specs are out of scope here —
773
+ * they carry no engine-owned base question and a false-positive there would
774
+ * silently drop a real question the author asked for);
775
+ * - the item body's frontmatter does NOT set `needsAnswers: true` (an author
776
+ * who set the flag is explicitly asking for the agent's pass);
777
+ * - the body carries no non-empty `## Open questions` section (empty /
778
+ * whitespace-only sections don't count as judgement);
779
+ * - there is no pending open-question sidecar for the item (same signal the
780
+ * classifier's invariant-1 read uses — the sidecar path is identity-derived).
688
781
  *
689
- * Returns the loud diagnostic to emit (naming BOTH valid channels), or `undefined`
690
- * when the shape does not match (the surfacer's empty is then the normal calm no-op).
691
- * The engine does NOT (and will not) honour in-body disposition prose — one channel
692
- * (the sidecar + `triaged:` frontmatter) keeps the loop honest.
782
+ * The decision-record shape (a `Decision (…)` line and/or `## Alternatives
783
+ * considered` section) is a HINT, not required the four conditions above are
784
+ * load-bearing on their own.
693
785
  */
694
- function detectObservationLimbo(input: RungExecInput): string | undefined {
695
- if (input.namespace !== 'observation') return undefined;
696
- const {item, slug, context} = input;
697
- const cwd = context.cwd;
698
- const itemRel = findItemPath(cwd, input.namespace, slug);
699
- if (itemRel === undefined) return undefined;
700
- const fm = parseFrontmatter(readFileSync(join(cwd, itemRel), 'utf8'));
701
- if (fm.triaged !== undefined && fm.triaged !== '') return undefined;
702
- const sidecarRel = sidecarPathFor(item);
703
- if (existsSync(join(cwd, sidecarRel))) return undefined;
704
- return (
705
- `observation \`${slug}\` is in a limbo: no \`triaged:\` frontmatter marker ` +
706
- `AND no answered question sidecar at \`${sidecarRel}\`, but the surfacer has ` +
707
- `nothing to ask. If a human triage decision (promote-slice / keep / duplicate) ` +
708
- `has been recorded in the observation BODY, that channel is INVISIBLE to the ` +
709
- `runner author the sidecar, or set \`triaged:\` in frontmatter. The engine ` +
710
- `does not (and will not) honour in-body disposition prose.`
711
- );
786
+ function isNothingToSurfaceObservation(
787
+ cwd: string,
788
+ input: RungExecInput,
789
+ itemPath: string,
790
+ sidecarExists: boolean,
791
+ ): boolean {
792
+ if (input.namespace !== 'observation') {
793
+ return false;
794
+ }
795
+ if (sidecarExists) {
796
+ return false;
797
+ }
798
+ let content: string;
799
+ try {
800
+ content = readFileSync(join(cwd, itemPath), 'utf8');
801
+ } catch {
802
+ return false;
803
+ }
804
+ const fm = parseFrontmatter(content);
805
+ if (fm.needsAnswers === true) {
806
+ return false;
807
+ }
808
+ if (hasNonEmptyOpenQuestionsSection(content)) {
809
+ return false;
810
+ }
811
+ return true;
812
+ }
813
+
814
+ /**
815
+ * Does the body carry a non-empty `## Open questions` section? Tolerates the
816
+ * `## Open questions to NOT guess` variant the capture-signal skill writes (same
817
+ * pattern the triage split uses). Whitespace-only / "none" markers count as
818
+ * empty.
819
+ */
820
+ function hasNonEmptyOpenQuestionsSection(content: string): boolean {
821
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
822
+ const startIdx = lines.findIndex((l) => /^##\s+Open questions\b/i.test(l));
823
+ if (startIdx === -1) {
824
+ return false;
825
+ }
826
+ let endIdx = lines.length;
827
+ for (let i = startIdx + 1; i < lines.length; i++) {
828
+ if (/^##\s+/.test(lines[i])) {
829
+ endIdx = i;
830
+ break;
831
+ }
832
+ }
833
+ const body = lines
834
+ .slice(startIdx + 1, endIdx)
835
+ .join('\n')
836
+ .trim();
837
+ if (body === '') {
838
+ return false;
839
+ }
840
+ // A single "none" / "n/a" marker line — treat as empty (author signalled "no
841
+ // open judgement" explicitly).
842
+ if (/^(?:none|n\/a|-|_+)\.?$/i.test(body)) {
843
+ return false;
844
+ }
845
+ return true;
846
+ }
847
+
848
+ /**
849
+ * The DETERMINISTIC triage question the triage rung ALWAYS surfaces for an
850
+ * untriaged observation (the "no limbo, ever" contract). It is engine-built (NOT
851
+ * LLM output), so it can never be zeroed out or flake: every untriaged observation
852
+ * gets exactly this question, and the human answers it via the sidecar — a record,
853
+ * a rationale note, and a fresh-bug signal are all treated identically (the human
854
+ * decides the disposition). The `surface-questions` agent runs ADDITIVELY on top,
855
+ * adding any extra pointed questions it extracts from the body.
856
+ */
857
+ export function buildTriageBaseQuestion(): NewQuestion {
858
+ return {
859
+ question:
860
+ 'What should become of this observation? Reply with a disposition and a ' +
861
+ 'reason: resolve (settle it, keep the note on record — say why), promote ' +
862
+ '(mint a task / spec / adr — say which and why), delete (redundant or ' +
863
+ 'obsolete — say why), or duplicate (maps onto an existing item — name it).',
864
+ context:
865
+ 'The engine records your disposition from the answer (no token needed); an ' +
866
+ 'answered promote mints the artifact, resolve keeps the note settled, ' +
867
+ 'delete/duplicate discharge it.',
868
+ };
712
869
  }
713
870
 
714
871
  /**
@@ -745,6 +902,28 @@ async function triageRung(input: RungExecInput): Promise<RungExecResult> {
745
902
  const note = context.note ?? (() => {});
746
903
  const cwd = context.cwd;
747
904
 
905
+ // SETTLED GUARD: a `triaged:` frontmatter marker means a human already
906
+ // dispositioned this observation, so it DROPS OUT of the triage pool. The pool
907
+ // enumeration already excludes it, but an EXPLICIT `obs:<slug>` bypasses the pool
908
+ // gate and still classifies as `triage-observation` — so re-check the marker here
909
+ // and no-op rather than surfacing a FRESH triage question on an already-settled
910
+ // note. (Replaces the old `detectObservationLimbo` special-case: there is no
911
+ // limbo any more — an UNtriaged observation always surfaces the deterministic
912
+ // question below; a SETTLED one is a calm no-op here.)
913
+ {
914
+ const itemRel = findItemPath(cwd, input.namespace, input.slug);
915
+ if (itemRel !== undefined) {
916
+ const fm = parseFrontmatter(readFileSync(join(cwd, itemRel), 'utf8'));
917
+ if (fm.triaged !== undefined && fm.triaged !== '') {
918
+ return {
919
+ exitCode: 0,
920
+ outcome: 'no-op',
921
+ message: `triage ${item}: already triaged (triaged:${fm.triaged}) — nothing to do.`,
922
+ };
923
+ }
924
+ }
925
+ }
926
+
748
927
  // The CONSERVATIVE auto-disposition EXCEPTION — ONLY under `observationTriage:
749
928
  // 'auto'`. Under `'ask'`/`'off'`/unset (including `off` + an EXPLICIT obs:<slug>
750
929
  // that bypassed the selection gate), EVERY untriaged observation surfaces the
@@ -795,7 +974,12 @@ async function triageRung(input: RungExecInput): Promise<RungExecResult> {
795
974
  // skill emits the triage question — NO disposition token any more, task
796
975
  // `agentic-apply-retire-disposition-vocabulary`); the AGENTIC apply decision
797
976
  // reads the human's answer + source and decides what to DO when it is answered.
798
- return surfaceRung(input);
977
+ //
978
+ // ALWAYS-ASK (the "no limbo" contract): pass the DETERMINISTIC triage question as
979
+ // the base question, so an untriaged observation ALWAYS surfaces it (q1) even if
980
+ // the `surface-questions` agent emits empty or flakes. The agent is ADDITIVE:
981
+ // any pointed questions it extracts from the body are appended after q1.
982
+ return surfaceRung(input, {baseQuestions: [buildTriageBaseQuestion()]});
799
983
  }
800
984
 
801
985
  /**
@@ -807,8 +991,9 @@ async function triageRung(input: RungExecInput): Promise<RungExecResult> {
807
991
  * apply rung is now AGENT-DRIVEN: it runs the shared `decide(input, allowedOutcomes)`
808
992
  * engine ({@link decide}) over `(the answered question(s) + the SOURCE item + its
809
993
  * type/context)` via the injected {@link ApplyDecider}, allowing the set
810
- * `{task | prd | adr | delete | ask}` (= `{mint-task | mint-spec | mint-adr |
811
- * delete-source | ask-follow-up}`; `adr` is now WIRED by task
994
+ * `{task | spec | adr | dispose | resolve | ask}` (= `{mint-task | mint-spec |
995
+ * mint-adr | dispose-source | resolve-no-mint | ask-follow-up}`; `adr` is now WIRED
996
+ * by task
812
997
  * `agentic-apply-mint-adr-route`, which added the {@link mintAdr} route). The
813
998
  * verdict ROUTES:
814
999
  * - `ask` → the EXISTING append/re-pause loop ({@link applyAnsweredQuestions}
@@ -817,11 +1002,22 @@ async function triageRung(input: RungExecInput): Promise<RungExecResult> {
817
1002
  * - `adr` → {@link mintAdr} (mint a SELF-CONTAINED ADR into `docs/adr/` + `git
818
1003
  * rm` the source + sidecar in the SAME atomic commit; the SIBLING route for the
819
1004
  * off-board target);
820
- * - `task` / `prd` → {@link promoteObservation} (mint a SELF-CONTAINED artifact +
1005
+ * - `task` / `spec` → {@link promoteObservation} (mint a SELF-CONTAINED artifact +
821
1006
  * `git rm` the source + sidecar in the SAME atomic commit); the artifact type
822
1007
  * comes from the agent's VERDICT, NOT a human `promote-*` field;
823
- * - `delete` → {@link applyAnsweredQuestions} discharge-by-deletion (`git rm`
824
- * source + sidecar in one revertible commit, the reason in the commit message).
1008
+ * - `resolve` → {@link applyAnsweredQuestions} resolve-fully (harvest answers
1009
+ * into the body, clear `needsAnswers`, delete the sidecar; the note is
1010
+ * RETAINED — the sibling of `dispose` that mints nothing but keeps the note);
1011
+ * - `dispose` → {@link applyAnsweredQuestions} regime-polymorphic disposal
1012
+ * (task `apply-disposition-delete-to-dispose-regime-polymorphic`, spec
1013
+ * `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #5): an
1014
+ * OBSERVATION is `git rm`-ed in one revertible commit (notes leave by
1015
+ * deletion; reason in the message); a TASK is `git mv`-ed to
1016
+ * `tasks/cancelled/` (RETAINED, `reason:` written into the moved body); a
1017
+ * SPEC is `git mv`-ed to `specs/dropped/` (RETAINED). Making `dispose`
1018
+ * polymorphic (rather than a literal `delete`) makes "a task cannot be
1019
+ * hard-deleted by the apply rung, only disposed to its terminal" true BY
1020
+ * CONSTRUCTION.
825
1021
  *
826
1022
  * For a TASK/SPEC (answering its OWN open questions) or a caller-supplied follow-up
827
1023
  * batch, it delegates straight to {@link applyAnsweredQuestions} (resolve fully /
@@ -859,6 +1055,26 @@ async function applyRung(input: RungExecInput): Promise<RungExecResult> {
859
1055
  return mergeRoute;
860
1056
  }
861
1057
 
1058
+ // RUNNER-ACTION KIND-CHECK for `kind: 'stuck'` (task
1059
+ // `apply-resolve-reset-flag-discards-work-branch`, spec
1060
+ // `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #6): a
1061
+ // bounced TASK surfaces as a `kind: 'stuck'` question; the human's plain
1062
+ // `keep | reset | cancel` answer drives one of the three deterministic
1063
+ // verbs. The DIRECT SIBLING of `maybeRunMergeAction` above (there is NO
1064
+ // agentic decider on the TASK apply path — `runAgenticDecision` fires only
1065
+ // for observations — so a bounced task's answer MUST be sourced from a
1066
+ // deterministic parse+dispatch, not a widened decider). `keep` and `reset`
1067
+ // fall through to the normal fall-through persist (with the branch
1068
+ // pre-deleted on `reset` via the SHARED
1069
+ // `deleteRemoteWorkBranchIfPresent`); `cancel` dispatches through
1070
+ // `applyAnsweredQuestions`' `dispose` option (task →
1071
+ // `tasks/cancelled/`); `refused` short-circuits with the sidecar left in
1072
+ // place for a re-answer.
1073
+ const stuckRoute = await maybeRunStuckAction(input, itemPath);
1074
+ if (stuckRoute !== undefined) {
1075
+ return stuckRoute;
1076
+ }
1077
+
862
1078
  // AGENTIC APPLY for an answered OBSERVATION (the subsumed triage rung): run the
863
1079
  // shared decision engine over the answer(s) + source, route the verdict. A
864
1080
  // caller-supplied follow-up batch (`applyFollowups`) bypasses the decision and
@@ -995,8 +1211,13 @@ async function maybeRunMergeAction(
995
1211
  // bounce to needs-attention through its own shared seam). SHORT-CIRCUIT:
996
1212
  // leave the sidecar so the open answer stays surfaced — the apply rung
997
1213
  // MUST NOT also resolve it (the next surfacer / human will follow up).
1214
+ // Outcome tag `merge-refused` (task `merge-action-nits-followup` nit 2):
1215
+ // distinct from `usage-error` — which is reserved for genuine caller-usage
1216
+ // errors (e.g. the workspacesDir-unset guard above) — so reviewers can
1217
+ // grep the refusal-on-rebased-tip signal without confusing it with a
1218
+ // misuse. `exitCode: 1` preserved.
998
1219
  note(result.message);
999
- return {exitCode: 1, outcome: 'usage-error', message: result.message};
1220
+ return {exitCode: 1, outcome: 'merge-refused', message: result.message};
1000
1221
  }
1001
1222
 
1002
1223
  if (result.outcome === 'restale') {
@@ -1064,6 +1285,113 @@ async function maybeRunMergeAction(
1064
1285
  }
1065
1286
  }
1066
1287
 
1288
+ /**
1289
+ * Dispatch an answered STUCK-QUESTION (a sidecar entry stamped `kind: 'stuck'`
1290
+ * by the bounce-surface path) through the deterministic
1291
+ * {@link performStuckAction} sibling. The DIRECT SIBLING of
1292
+ * {@link maybeRunMergeAction} for the `kind: 'stuck'` axis (task
1293
+ * `apply-resolve-reset-flag-discards-work-branch`, spec
1294
+ * `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #6):
1295
+ *
1296
+ * - `keep` -> fall through to today's normal `applyAnsweredQuestions`
1297
+ * resolve (branch untouched; continue-from-WIP);
1298
+ * - `reset` -> the SHARED `deleteRemoteWorkBranchIfPresent` primitive
1299
+ * discards the remote `work/task-<slug>` FIRST, then the
1300
+ * normal fall-through persist clears `needsAnswers`. Safely
1301
+ * IDEMPOTENT when no branch exists (an item never built) —
1302
+ * an `already-gone` push is tolerated as a no-op;
1303
+ * - `cancel` -> fall through to the normal persist with the `dispose`
1304
+ * option (`git mv -> tasks/cancelled/`), the answer text
1305
+ * recorded as the human's reason;
1306
+ * - `refused` -> a REAL push-delete failure aborted the discard;
1307
+ * SHORT-CIRCUIT with the sidecar left in place so the human
1308
+ * sees the failure and re-answers (matches the
1309
+ * `requeue --reset` abort-on-failed-delete contract).
1310
+ *
1311
+ * Returns `undefined` when no answered `kind: 'stuck'` entry is present (the
1312
+ * apply rung then proceeds to the existing path — an unrelated content
1313
+ * question falls through to the normal `applyAnsweredQuestions` /
1314
+ * agentic-decider path).
1315
+ */
1316
+ async function maybeRunStuckAction(
1317
+ input: RungExecInput,
1318
+ itemPath: string,
1319
+ ): Promise<RungExecResult | undefined> {
1320
+ const {item, context} = input;
1321
+ const cwd = context.cwd;
1322
+ const note = context.note ?? (() => {});
1323
+
1324
+ const detected = detectAnsweredStuckAction(cwd, item);
1325
+ if (detected === undefined) return undefined;
1326
+
1327
+ const handler = context.stuckAction ?? performStuckAction;
1328
+
1329
+ let result: StuckActionResult;
1330
+ try {
1331
+ result = await handler({
1332
+ action: detected,
1333
+ item,
1334
+ slug: input.slug,
1335
+ cwd,
1336
+ arbiter: context.arbiter ?? DEFAULT_ARBITER,
1337
+ note,
1338
+ });
1339
+ } catch (err) {
1340
+ const detail = err instanceof Error ? err.message : String(err);
1341
+ const message =
1342
+ `apply ${item}: answered stuck-question dispatch raised (${detail}); NOT ` +
1343
+ `clearing needsAnswers; the sidecar stays surfaced.`;
1344
+ note(message);
1345
+ return {exitCode: 1, outcome: 'usage-error', message};
1346
+ }
1347
+
1348
+ if (result.outcome === 'refused') {
1349
+ // The arbiter delete FAILED (not `already-gone`); we MUST NOT clear
1350
+ // needsAnswers, because that would leave the item claimable while still
1351
+ // carrying the WIP branch we meant to discard — the very stale-continue
1352
+ // trap the `requeue --reset` path guards against. Leave the sidecar in
1353
+ // place for a re-answer.
1354
+ note(result.message);
1355
+ return {exitCode: 1, outcome: 'usage-error', message: result.message};
1356
+ }
1357
+
1358
+ note(result.message);
1359
+ const apply = context.applyPersist ?? applyAnsweredQuestions;
1360
+ try {
1361
+ const applyOptions: ApplyAnsweredQuestionsOptions = {
1362
+ cwd,
1363
+ item,
1364
+ itemPath,
1365
+ note,
1366
+ };
1367
+ if (result.outcome === 'cancel') {
1368
+ // The human's answer text is the dispose reason (verbatim), so the
1369
+ // terminal `tasks/cancelled/` body records WHY — the same reason
1370
+ // contract the agentic `dispose` verdict uses.
1371
+ applyOptions.dispose = {reason: detected.entry.answer.trim()};
1372
+ }
1373
+ const applied = apply(applyOptions);
1374
+ const mapped: AdvanceOutcome =
1375
+ applied.outcome === 'repaused'
1376
+ ? 'no-op'
1377
+ : applied.outcome === 'vanished'
1378
+ ? 'vanished'
1379
+ : 'advanced';
1380
+ return {
1381
+ exitCode: 0,
1382
+ outcome: mapped,
1383
+ message: `${result.message} ${applied.message}`,
1384
+ };
1385
+ } catch (err) {
1386
+ const detail = err instanceof Error ? err.message : String(err);
1387
+ return {
1388
+ exitCode: 1,
1389
+ outcome: 'usage-error',
1390
+ message: `apply ${item}: ${detail}`,
1391
+ };
1392
+ }
1393
+ }
1394
+
1067
1395
  /**
1068
1396
  * The cross-tick-window BENIGN SKIP shared by all three rungs that need to
1069
1397
  * resolve an item file (surface / triage / apply). The lifecycle pool enumerated
@@ -1119,15 +1447,24 @@ function findItemPath(
1119
1447
  *
1120
1448
  * - `ask` → append the follow-up question(s) + re-pause (the EXISTING loop, via
1121
1449
  * {@link applyAnsweredQuestions}'s `appendQuestions`);
1122
- * - `task` / `prd` → {@link promoteObservation} (mint self-contained + delete
1450
+ * - `task` / `spec` → {@link promoteObservation} (mint self-contained + delete
1123
1451
  * source in the same atomic commit);
1124
1452
  * - `adr` → {@link mintAdr} (mint a self-contained ADR into `docs/adr/` + delete
1125
1453
  * source in the same atomic commit; the SIBLING route for the off-board target,
1126
1454
  * task `agentic-apply-mint-adr-route`);
1127
- * - `delete` → {@link applyAnsweredQuestions}'s discharge-by-deletion (`git rm`
1128
- * source + sidecar in one revertible commit, the reason in the message).
1455
+ * - `resolve` → {@link applyAnsweredQuestions}'s resolve-fully path (harvest the
1456
+ * answers into `## Applied answers`, strip the open-questions block, clear
1457
+ * `needsAnswers`, DELETE the sidecar — the note is RETAINED). The sibling of
1458
+ * `dispose` that KEEPS the note instead of moving/rm-ing it (task
1459
+ * `apply-decide-resolve-verdict-mint-nothing`);
1460
+ * - `dispose` → {@link applyAnsweredQuestions}'s regime-polymorphic disposal
1461
+ * (task `apply-disposition-delete-to-dispose-regime-polymorphic`): an
1462
+ * observation is `git rm`-ed in one revertible commit (reason in the
1463
+ * message); a task is `git mv`-ed to `tasks/cancelled/` (reason written into
1464
+ * the body); a spec is `git mv`-ed to `specs/dropped/`.
1129
1465
  *
1130
- * The allowed set is `{task | prd | adr | delete | ask}`; a verdict outside it is
1466
+ * The allowed set is `{task | spec | adr | dispose | resolve | ask}`; a verdict
1467
+ * outside it is
1131
1468
  * rejected by the engine's allowed-outcome guard ({@link DisallowedOutcomeError})
1132
1469
  * and mapped onto a usage-error — never dispatched.
1133
1470
  */
@@ -1234,11 +1571,13 @@ async function applyAgenticDecision(
1234
1571
  outcome:
1235
1572
  result.outcome === 'promoted'
1236
1573
  ? 'advanced'
1237
- : result.outcome === 'lost'
1238
- ? 'lost'
1239
- : result.outcome === 'contended'
1240
- ? 'contended'
1241
- : 'usage-error',
1574
+ : result.outcome === 'already-triaged'
1575
+ ? 'already-triaged'
1576
+ : result.outcome === 'lost'
1577
+ ? 'lost'
1578
+ : result.outcome === 'contended'
1579
+ ? 'contended'
1580
+ : 'usage-error',
1242
1581
  message: result.message,
1243
1582
  };
1244
1583
  } catch (err) {
@@ -1303,16 +1642,53 @@ async function applyAgenticDecision(
1303
1642
  }
1304
1643
  }
1305
1644
 
1306
- // delete-source → discharge by deletion (DIRECT, no confirm — decision 12). The
1307
- // human's answer is the source of truth; the deletion is a single revertible
1308
- // commit with the reason in the message.
1645
+ if (verdict.outcome === 'resolve') {
1646
+ // resolve-no-mint the EXISTING resolve-fully path (task
1647
+ // `apply-decide-resolve-verdict-mint-nothing`). The answer SETTLES the item
1648
+ // with NOTHING to mint and the note RETAINED: call `applyAnsweredQuestions`
1649
+ // with NEITHER `appendQuestions` (re-pause) NOR `dispose` (drop/terminal), so it
1650
+ // takes the default resolve-fully branch — harvest the answers into `##
1651
+ // Applied answers`, strip the marker-fenced open-questions block, clear
1652
+ // `needsAnswers`, and DELETE the sidecar in ONE atomic commit. Invariant-clean
1653
+ // (`needsAnswers:false` ⟺ no active sidecar). The note file is KEPT (this is
1654
+ // the sibling of `dispose`, which git-rm's it or moves it to a terminal). `resolveReason` is advisory
1655
+ // context only; the durable disposition record is the harvested `## Applied
1656
+ // answers` block the resolve-fully path writes (NOT a separate convention).
1657
+ const apply = context.applyPersist ?? applyAnsweredQuestions;
1658
+ try {
1659
+ const result = apply({cwd, item, itemPath, note});
1660
+ return {
1661
+ exitCode: 0,
1662
+ outcome: result.outcome === 'vanished' ? 'vanished' : 'advanced',
1663
+ message: result.message,
1664
+ };
1665
+ } catch (err) {
1666
+ const detail = err instanceof Error ? err.message : String(err);
1667
+ return {
1668
+ exitCode: 1,
1669
+ outcome: 'usage-error',
1670
+ message: `apply ${item}: ${detail}`,
1671
+ };
1672
+ }
1673
+ }
1674
+
1675
+ // dispose-source → regime-polymorphic disposal (DIRECT, no confirm — decision
1676
+ // 12; task `apply-disposition-delete-to-dispose-regime-polymorphic`, spec
1677
+ // `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #5). The
1678
+ // human's answer is the source of truth; the persist chooses the on-disk
1679
+ // effect by the source's regime: an OBSERVATION is `git rm`-ed in one
1680
+ // revertible commit (reason in the message, git history = archive); a TASK is
1681
+ // `git mv`-ed to `tasks/cancelled/` (RETAINED, `reason:` written into the
1682
+ // moved body); a SPEC is `git mv`-ed to `specs/dropped/` (RETAINED). A task
1683
+ // cannot be hard-deleted by the apply rung — dispose is the only path off the
1684
+ // board.
1309
1685
  const apply = context.applyPersist ?? applyAnsweredQuestions;
1310
1686
  try {
1311
1687
  const result = apply({
1312
1688
  cwd,
1313
1689
  item,
1314
1690
  itemPath,
1315
- discharge: {reason: verdict.deleteReason ?? ''},
1691
+ dispose: {reason: verdict.disposeReason ?? ''},
1316
1692
  note,
1317
1693
  });
1318
1694
  return {
@@ -1506,6 +1882,10 @@ export async function performAdvance(
1506
1882
  verify: options.verify,
1507
1883
  strictMergeApproval: options.strictMergeApproval,
1508
1884
  mergeAction: options.mergeAction,
1885
+ // The answered-stuck-question dispatch context (task
1886
+ // `apply-resolve-reset-flag-discards-work-branch`): threaded into the
1887
+ // apply rung's kind-check for a `kind: 'stuck'` runner-action.
1888
+ stuckAction: options.stuckAction,
1509
1889
  note,
1510
1890
  },
1511
1891
  });