dorfl 0.1.2 → 0.2.1

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/dist/advance.js CHANGED
@@ -18,6 +18,7 @@ import { APPLY_ALLOWED_OUTCOMES, buildApplyDecisionInput, harnessApplyDecider, }
18
18
  import { persistSurfacedQuestions, } from './surface-persist.js';
19
19
  import { applyAnsweredQuestions, } from './apply-persist.js';
20
20
  import { detectAnsweredMergeAction, performMergeAction, } from './apply-merge-action.js';
21
+ import { detectAnsweredStuckAction, performStuckAction, } from './apply-stuck-action.js';
21
22
  /**
22
23
  * The **`advance` verb SKELETON** (spec `advance-loop`, task
23
24
  * `advance-verb-resolver`, US #1/5/6/18). `advance` is the SIBLING top-level verb
@@ -223,7 +224,7 @@ function mapDoOutcome(result) {
223
224
  * answered sidecar back to not-all-answered (the persist owns that). An EMPTY
224
225
  * emit (the skill's honest "no open judgement") writes nothing and reports it.
225
226
  */
226
- async function surfaceRung(input) {
227
+ async function surfaceRung(input, surfaceOpts = {}) {
227
228
  const { item, context } = input;
228
229
  const note = context.note ?? (() => { });
229
230
  const cwd = context.cwd;
@@ -234,50 +235,80 @@ async function surfaceRung(input) {
234
235
  if (itemPath === undefined) {
235
236
  return vanishedSkip({ rung: 'surface', item });
236
237
  }
238
+ // DETERMINISTIC BASE QUESTIONS (the triage rung's "always ask" contract): the
239
+ // caller may pass engine-built questions that MUST be surfaced regardless of the
240
+ // agent. They are added ONLY on the FIRST pass (no sidecar yet) — on a re-surface
241
+ // the sidecar already carries them, and `appendQuestions` does NOT dedup, so
242
+ // re-adding would duplicate. When base questions are present the agent is
243
+ // ADDITIVE ONLY and its flake/empty is NON-FATAL (the base question still lands).
244
+ const baseQuestions = surfaceOpts.baseQuestions ?? [];
245
+ const hasBase = baseQuestions.length > 0;
246
+ const sidecarExists = existsSync(join(cwd, sidecarPathFor(item)));
247
+ const baseToAdd = hasBase && !sidecarExists ? baseQuestions : [];
237
248
  // 1. SPAWN the fresh-context `surface-questions` agent (the skill JUDGES). The
238
249
  // expensive model work is POST-lock (the lock is held by `performAdvance`).
250
+ //
251
+ // SHORT-CIRCUIT (primary/load-bearing half of task
252
+ // `surface-short-circuit-already-triaged-observations-and-harden-skill-empty-emit`):
253
+ // an OBSERVATION with provably no open judgement (frontmatter `needsAnswers` NOT
254
+ // true, no non-empty `## Open questions` section, no pending sidecar) — the
255
+ // typical shape of a decision-record / already-triaged note — deterministically
256
+ // yields `{questions: []}` WITHOUT round-tripping the flaky surface-questions
257
+ // agent (source observation
258
+ // `surface-questions-agent-still-emits-no-parseable-questions-on-decision-record-obs-2026-07-10`).
259
+ // The loud-error contract from
260
+ // `advance-surface-limbo-observation-loudly-instead-of-silent-no-op` is
261
+ // PRESERVED for the observations that DO reach the agent — this only spares the
262
+ // ones that provably have nothing to ask. Conservative: fires only on
263
+ // observations; tasks/specs still always ask.
239
264
  const gate = context.surfaceGate ?? harnessSurfaceGate();
240
265
  let emit;
241
- try {
242
- emit = await gate({
243
- item,
244
- cwd,
245
- surfaceModel: context.surfaceModel,
246
- });
266
+ if (isNothingToSurfaceObservation(cwd, input, itemPath, sidecarExists)) {
267
+ note(`surface ${item}: auto-triaged (no open questions, no sidecar) — skipped agent.`);
268
+ emit = { questions: [] };
247
269
  }
248
- catch (err) {
249
- const detail = err instanceof Error ? err.message : String(err);
250
- return {
251
- exitCode: 1,
252
- outcome: 'usage-error',
253
- message: `surface ${item}: the surface-questions agent produced no usable emit (${detail}).`,
254
- };
270
+ else {
271
+ try {
272
+ emit = await gate({
273
+ item,
274
+ cwd,
275
+ surfaceModel: context.surfaceModel,
276
+ });
277
+ }
278
+ catch (err) {
279
+ const detail = err instanceof Error ? err.message : String(err);
280
+ if (!hasBase) {
281
+ return {
282
+ exitCode: 1,
283
+ outcome: 'usage-error',
284
+ message: `surface ${item}: the surface-questions agent produced no usable emit (${detail}).`,
285
+ };
286
+ }
287
+ // The agent flaked but we have a deterministic base question to surface: the
288
+ // flake is NON-FATAL. Treat the agent's extras as empty and carry on (the
289
+ // base triage question still lands, so the human can triage via the sidecar).
290
+ note(`surface ${item}: the surface-questions agent produced no usable emit ` +
291
+ `(${detail}); surfacing the deterministic question(s) only.`);
292
+ emit = { questions: [] };
293
+ }
255
294
  }
256
295
  // 2. The ENGINE persists (the skill wrote nothing): append-or-create the sidecar
257
296
  // + set `needsAnswers:true` in ONE commit (CAS-atomic under the held lock).
297
+ // Base questions FIRST (q1…), then the agent's additive extras.
258
298
  const persist = context.surfacePersist ?? persistSurfacedQuestions;
259
299
  const result = persist({
260
300
  cwd,
261
301
  item,
262
302
  itemPath,
263
- questions: toNewQuestions(emit),
303
+ questions: [...baseToAdd, ...toNewQuestions(emit)],
264
304
  note,
265
305
  });
266
306
  if (result.outcome === 'nothing') {
267
- // LIMBO DETECTION (task `advance-surface-limbo-observation-loudly-instead-of-
268
- // silent-no-op`): an untriaged observation with NO sidecar whose surfacer had
269
- // nothing to ask is a TRAP the human's triage answer may have been recorded
270
- // in an in-BODY "Applied answers" block, but the engine reads triage-vs-
271
- // settled only from the `triaged:` frontmatter marker (`ledger-read.ts`) and
272
- // the promote path only from an answered `disposition: promote` SIDECAR
273
- // (`triage-persist.ts`). Without either, the item is untriaged (re-enumerated
274
- // every tick), un-surfaceable (nothing to ask), un-promotable (no sidecar) —
275
- // a silent exit-0 no-op forever. Surface it LOUDLY instead.
276
- const limbo = detectObservationLimbo(input);
277
- if (limbo !== undefined) {
278
- note(limbo);
279
- return { exitCode: 1, outcome: 'usage-error', message: limbo };
280
- }
307
+ // The agent had nothing to ask AND there was no base question to add (a
308
+ // task/spec surface, or a re-surface whose base question is already present).
309
+ // This is the calm "no open judgement" no-op no sidecar written. (There is
310
+ // no "limbo" any more: the triage rung ALWAYS passes a base question on the
311
+ // first pass, so an untriaged observation can never fall here empty-handed.)
281
312
  return {
282
313
  exitCode: 0,
283
314
  outcome: 'no-op',
@@ -292,42 +323,105 @@ async function surfaceRung(input) {
292
323
  };
293
324
  }
294
325
  /**
295
- * Detect the OBSERVATION LIMBO shape (task `advance-surface-limbo-observation-
296
- * loudly-instead-of-silent-no-op`): an observation whose triage answer was
297
- * (mis-)authored in an in-body "Applied answers" block instead of the sidecar/
298
- * frontmatter channels the engine reads. All four conditions must hold:
326
+ * Would this OBSERVATION provably surface `{questions: []}` — i.e. is there no
327
+ * open judgement anywhere on it? Cheap read-only predicate the surface rung uses
328
+ * to skip the flaky agent round-trip for already-triaged / decision-record notes
329
+ * (task
330
+ * `surface-short-circuit-already-triaged-observations-and-harden-skill-empty-emit`;
331
+ * source observation
332
+ * `surface-questions-agent-still-emits-no-parseable-questions-on-decision-record-obs-2026-07-10`).
299
333
  *
300
- * 1. the item is an OBSERVATION;
301
- * 2. its frontmatter has NO `triaged:` marker (untriaged per `ledger-read.ts`);
302
- * 3. there is NO active question sidecar at `work/questions/observation-<slug>.md`;
303
- * 4. the surfacer just returned empty (the caller {@link surfaceRung} — only
304
- * calls this on the persist's `nothing` branch).
334
+ * Fires ONLY when ALL of these hold (conservative — err on the side of STILL
335
+ * calling the agent if uncertain):
336
+ * - the item's namespace is `observation` (tasks/specs are out of scope here —
337
+ * they carry no engine-owned base question and a false-positive there would
338
+ * silently drop a real question the author asked for);
339
+ * - the item body's frontmatter does NOT set `needsAnswers: true` (an author
340
+ * who set the flag is explicitly asking for the agent's pass);
341
+ * - the body carries no non-empty `## Open questions` section (empty /
342
+ * whitespace-only sections don't count as judgement);
343
+ * - there is no pending open-question sidecar for the item (same signal the
344
+ * classifier's invariant-1 read uses — the sidecar path is identity-derived).
305
345
  *
306
- * Returns the loud diagnostic to emit (naming BOTH valid channels), or `undefined`
307
- * when the shape does not match (the surfacer's empty is then the normal calm no-op).
308
- * The engine does NOT (and will not) honour in-body disposition prose — one channel
309
- * (the sidecar + `triaged:` frontmatter) keeps the loop honest.
346
+ * The decision-record shape (a `Decision (…)` line and/or `## Alternatives
347
+ * considered` section) is a HINT, not required the four conditions above are
348
+ * load-bearing on their own.
310
349
  */
311
- function detectObservationLimbo(input) {
312
- if (input.namespace !== 'observation')
313
- return undefined;
314
- const { item, slug, context } = input;
315
- const cwd = context.cwd;
316
- const itemRel = findItemPath(cwd, input.namespace, slug);
317
- if (itemRel === undefined)
318
- return undefined;
319
- const fm = parseFrontmatter(readFileSync(join(cwd, itemRel), 'utf8'));
320
- if (fm.triaged !== undefined && fm.triaged !== '')
321
- return undefined;
322
- const sidecarRel = sidecarPathFor(item);
323
- if (existsSync(join(cwd, sidecarRel)))
324
- return undefined;
325
- return (`observation \`${slug}\` is in a limbo: no \`triaged:\` frontmatter marker ` +
326
- `AND no answered question sidecar at \`${sidecarRel}\`, but the surfacer has ` +
327
- `nothing to ask. If a human triage decision (promote-slice / keep / duplicate) ` +
328
- `has been recorded in the observation BODY, that channel is INVISIBLE to the ` +
329
- `runner author the sidecar, or set \`triaged:\` in frontmatter. The engine ` +
330
- `does not (and will not) honour in-body disposition prose.`);
350
+ function isNothingToSurfaceObservation(cwd, input, itemPath, sidecarExists) {
351
+ if (input.namespace !== 'observation') {
352
+ return false;
353
+ }
354
+ if (sidecarExists) {
355
+ return false;
356
+ }
357
+ let content;
358
+ try {
359
+ content = readFileSync(join(cwd, itemPath), 'utf8');
360
+ }
361
+ catch {
362
+ return false;
363
+ }
364
+ const fm = parseFrontmatter(content);
365
+ if (fm.needsAnswers === true) {
366
+ return false;
367
+ }
368
+ if (hasNonEmptyOpenQuestionsSection(content)) {
369
+ return false;
370
+ }
371
+ return true;
372
+ }
373
+ /**
374
+ * Does the body carry a non-empty `## Open questions` section? Tolerates the
375
+ * `## Open questions to NOT guess` variant the capture-signal skill writes (same
376
+ * pattern the triage split uses). Whitespace-only / "none" markers count as
377
+ * empty.
378
+ */
379
+ function hasNonEmptyOpenQuestionsSection(content) {
380
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
381
+ const startIdx = lines.findIndex((l) => /^##\s+Open questions\b/i.test(l));
382
+ if (startIdx === -1) {
383
+ return false;
384
+ }
385
+ let endIdx = lines.length;
386
+ for (let i = startIdx + 1; i < lines.length; i++) {
387
+ if (/^##\s+/.test(lines[i])) {
388
+ endIdx = i;
389
+ break;
390
+ }
391
+ }
392
+ const body = lines
393
+ .slice(startIdx + 1, endIdx)
394
+ .join('\n')
395
+ .trim();
396
+ if (body === '') {
397
+ return false;
398
+ }
399
+ // A single "none" / "n/a" marker line — treat as empty (author signalled "no
400
+ // open judgement" explicitly).
401
+ if (/^(?:none|n\/a|-|_+)\.?$/i.test(body)) {
402
+ return false;
403
+ }
404
+ return true;
405
+ }
406
+ /**
407
+ * The DETERMINISTIC triage question the triage rung ALWAYS surfaces for an
408
+ * untriaged observation (the "no limbo, ever" contract). It is engine-built (NOT
409
+ * LLM output), so it can never be zeroed out or flake: every untriaged observation
410
+ * gets exactly this question, and the human answers it via the sidecar — a record,
411
+ * a rationale note, and a fresh-bug signal are all treated identically (the human
412
+ * decides the disposition). The `surface-questions` agent runs ADDITIVELY on top,
413
+ * adding any extra pointed questions it extracts from the body.
414
+ */
415
+ export function buildTriageBaseQuestion() {
416
+ return {
417
+ question: 'What should become of this observation? Reply with a disposition and a ' +
418
+ 'reason: resolve (settle it, keep the note on record — say why), promote ' +
419
+ '(mint a task / spec / adr — say which and why), delete (redundant or ' +
420
+ 'obsolete — say why), or duplicate (maps onto an existing item — name it).',
421
+ context: 'The engine records your disposition from the answer (no token needed); an ' +
422
+ 'answered promote mints the artifact, resolve keeps the note settled, ' +
423
+ 'delete/duplicate discharge it.',
424
+ };
331
425
  }
332
426
  /**
333
427
  * The observation TRIAGE rung BODY (task `advance-rung-triage`, US #16/17/23):
@@ -362,6 +456,27 @@ async function triageRung(input) {
362
456
  const { item, context } = input;
363
457
  const note = context.note ?? (() => { });
364
458
  const cwd = context.cwd;
459
+ // SETTLED GUARD: a `triaged:` frontmatter marker means a human already
460
+ // dispositioned this observation, so it DROPS OUT of the triage pool. The pool
461
+ // enumeration already excludes it, but an EXPLICIT `obs:<slug>` bypasses the pool
462
+ // gate and still classifies as `triage-observation` — so re-check the marker here
463
+ // and no-op rather than surfacing a FRESH triage question on an already-settled
464
+ // note. (Replaces the old `detectObservationLimbo` special-case: there is no
465
+ // limbo any more — an UNtriaged observation always surfaces the deterministic
466
+ // question below; a SETTLED one is a calm no-op here.)
467
+ {
468
+ const itemRel = findItemPath(cwd, input.namespace, input.slug);
469
+ if (itemRel !== undefined) {
470
+ const fm = parseFrontmatter(readFileSync(join(cwd, itemRel), 'utf8'));
471
+ if (fm.triaged !== undefined && fm.triaged !== '') {
472
+ return {
473
+ exitCode: 0,
474
+ outcome: 'no-op',
475
+ message: `triage ${item}: already triaged (triaged:${fm.triaged}) — nothing to do.`,
476
+ };
477
+ }
478
+ }
479
+ }
365
480
  // The CONSERVATIVE auto-disposition EXCEPTION — ONLY under `observationTriage:
366
481
  // 'auto'`. Under `'ask'`/`'off'`/unset (including `off` + an EXPLICIT obs:<slug>
367
482
  // that bypassed the selection gate), EVERY untriaged observation surfaces the
@@ -410,7 +525,12 @@ async function triageRung(input) {
410
525
  // skill emits the triage question — NO disposition token any more, task
411
526
  // `agentic-apply-retire-disposition-vocabulary`); the AGENTIC apply decision
412
527
  // reads the human's answer + source and decides what to DO when it is answered.
413
- return surfaceRung(input);
528
+ //
529
+ // ALWAYS-ASK (the "no limbo" contract): pass the DETERMINISTIC triage question as
530
+ // the base question, so an untriaged observation ALWAYS surfaces it (q1) even if
531
+ // the `surface-questions` agent emits empty or flakes. The agent is ADDITIVE:
532
+ // any pointed questions it extracts from the body are appended after q1.
533
+ return surfaceRung(input, { baseQuestions: [buildTriageBaseQuestion()] });
414
534
  }
415
535
  /**
416
536
  * The APPLY rung BODY (task `advance-rung-apply`; AGENTIC apply, task
@@ -421,8 +541,9 @@ async function triageRung(input) {
421
541
  * apply rung is now AGENT-DRIVEN: it runs the shared `decide(input, allowedOutcomes)`
422
542
  * engine ({@link decide}) over `(the answered question(s) + the SOURCE item + its
423
543
  * type/context)` via the injected {@link ApplyDecider}, allowing the set
424
- * `{task | prd | adr | delete | ask}` (= `{mint-task | mint-spec | mint-adr |
425
- * delete-source | ask-follow-up}`; `adr` is now WIRED by task
544
+ * `{task | spec | adr | dispose | resolve | ask}` (= `{mint-task | mint-spec |
545
+ * mint-adr | dispose-source | resolve-no-mint | ask-follow-up}`; `adr` is now WIRED
546
+ * by task
426
547
  * `agentic-apply-mint-adr-route`, which added the {@link mintAdr} route). The
427
548
  * verdict ROUTES:
428
549
  * - `ask` → the EXISTING append/re-pause loop ({@link applyAnsweredQuestions}
@@ -431,11 +552,22 @@ async function triageRung(input) {
431
552
  * - `adr` → {@link mintAdr} (mint a SELF-CONTAINED ADR into `docs/adr/` + `git
432
553
  * rm` the source + sidecar in the SAME atomic commit; the SIBLING route for the
433
554
  * off-board target);
434
- * - `task` / `prd` → {@link promoteObservation} (mint a SELF-CONTAINED artifact +
555
+ * - `task` / `spec` → {@link promoteObservation} (mint a SELF-CONTAINED artifact +
435
556
  * `git rm` the source + sidecar in the SAME atomic commit); the artifact type
436
557
  * comes from the agent's VERDICT, NOT a human `promote-*` field;
437
- * - `delete` → {@link applyAnsweredQuestions} discharge-by-deletion (`git rm`
438
- * source + sidecar in one revertible commit, the reason in the commit message).
558
+ * - `resolve` → {@link applyAnsweredQuestions} resolve-fully (harvest answers
559
+ * into the body, clear `needsAnswers`, delete the sidecar; the note is
560
+ * RETAINED — the sibling of `dispose` that mints nothing but keeps the note);
561
+ * - `dispose` → {@link applyAnsweredQuestions} regime-polymorphic disposal
562
+ * (task `apply-disposition-delete-to-dispose-regime-polymorphic`, spec
563
+ * `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #5): an
564
+ * OBSERVATION is `git rm`-ed in one revertible commit (notes leave by
565
+ * deletion; reason in the message); a TASK is `git mv`-ed to
566
+ * `tasks/cancelled/` (RETAINED, `reason:` written into the moved body); a
567
+ * SPEC is `git mv`-ed to `specs/dropped/` (RETAINED). Making `dispose`
568
+ * polymorphic (rather than a literal `delete`) makes "a task cannot be
569
+ * hard-deleted by the apply rung, only disposed to its terminal" true BY
570
+ * CONSTRUCTION.
439
571
  *
440
572
  * For a TASK/SPEC (answering its OWN open questions) or a caller-supplied follow-up
441
573
  * batch, it delegates straight to {@link applyAnsweredQuestions} (resolve fully /
@@ -470,6 +602,25 @@ async function applyRung(input) {
470
602
  if (mergeRoute !== undefined) {
471
603
  return mergeRoute;
472
604
  }
605
+ // RUNNER-ACTION KIND-CHECK for `kind: 'stuck'` (task
606
+ // `apply-resolve-reset-flag-discards-work-branch`, spec
607
+ // `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #6): a
608
+ // bounced TASK surfaces as a `kind: 'stuck'` question; the human's plain
609
+ // `keep | reset | cancel` answer drives one of the three deterministic
610
+ // verbs. The DIRECT SIBLING of `maybeRunMergeAction` above (there is NO
611
+ // agentic decider on the TASK apply path — `runAgenticDecision` fires only
612
+ // for observations — so a bounced task's answer MUST be sourced from a
613
+ // deterministic parse+dispatch, not a widened decider). `keep` and `reset`
614
+ // fall through to the normal fall-through persist (with the branch
615
+ // pre-deleted on `reset` via the SHARED
616
+ // `deleteRemoteWorkBranchIfPresent`); `cancel` dispatches through
617
+ // `applyAnsweredQuestions`' `dispose` option (task →
618
+ // `tasks/cancelled/`); `refused` short-circuits with the sidecar left in
619
+ // place for a re-answer.
620
+ const stuckRoute = await maybeRunStuckAction(input, itemPath);
621
+ if (stuckRoute !== undefined) {
622
+ return stuckRoute;
623
+ }
473
624
  // AGENTIC APPLY for an answered OBSERVATION (the subsumed triage rung): run the
474
625
  // shared decision engine over the answer(s) + source, route the verdict. A
475
626
  // caller-supplied follow-up batch (`applyFollowups`) bypasses the decision and
@@ -595,8 +746,13 @@ async function maybeRunMergeAction(input, itemPath) {
595
746
  // bounce to needs-attention through its own shared seam). SHORT-CIRCUIT:
596
747
  // leave the sidecar so the open answer stays surfaced — the apply rung
597
748
  // MUST NOT also resolve it (the next surfacer / human will follow up).
749
+ // Outcome tag `merge-refused` (task `merge-action-nits-followup` nit 2):
750
+ // distinct from `usage-error` — which is reserved for genuine caller-usage
751
+ // errors (e.g. the workspacesDir-unset guard above) — so reviewers can
752
+ // grep the refusal-on-rebased-tip signal without confusing it with a
753
+ // misuse. `exitCode: 1` preserved.
598
754
  note(result.message);
599
- return { exitCode: 1, outcome: 'usage-error', message: result.message };
755
+ return { exitCode: 1, outcome: 'merge-refused', message: result.message };
600
756
  }
601
757
  if (result.outcome === 'restale') {
602
758
  // `strictMergeApproval` re-surface: append a follow-up question and
@@ -661,6 +817,105 @@ async function maybeRunMergeAction(input, itemPath) {
661
817
  };
662
818
  }
663
819
  }
820
+ /**
821
+ * Dispatch an answered STUCK-QUESTION (a sidecar entry stamped `kind: 'stuck'`
822
+ * by the bounce-surface path) through the deterministic
823
+ * {@link performStuckAction} sibling. The DIRECT SIBLING of
824
+ * {@link maybeRunMergeAction} for the `kind: 'stuck'` axis (task
825
+ * `apply-resolve-reset-flag-discards-work-branch`, spec
826
+ * `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #6):
827
+ *
828
+ * - `keep` -> fall through to today's normal `applyAnsweredQuestions`
829
+ * resolve (branch untouched; continue-from-WIP);
830
+ * - `reset` -> the SHARED `deleteRemoteWorkBranchIfPresent` primitive
831
+ * discards the remote `work/task-<slug>` FIRST, then the
832
+ * normal fall-through persist clears `needsAnswers`. Safely
833
+ * IDEMPOTENT when no branch exists (an item never built) —
834
+ * an `already-gone` push is tolerated as a no-op;
835
+ * - `cancel` -> fall through to the normal persist with the `dispose`
836
+ * option (`git mv -> tasks/cancelled/`), the answer text
837
+ * recorded as the human's reason;
838
+ * - `refused` -> a REAL push-delete failure aborted the discard;
839
+ * SHORT-CIRCUIT with the sidecar left in place so the human
840
+ * sees the failure and re-answers (matches the
841
+ * `requeue --reset` abort-on-failed-delete contract).
842
+ *
843
+ * Returns `undefined` when no answered `kind: 'stuck'` entry is present (the
844
+ * apply rung then proceeds to the existing path — an unrelated content
845
+ * question falls through to the normal `applyAnsweredQuestions` /
846
+ * agentic-decider path).
847
+ */
848
+ async function maybeRunStuckAction(input, itemPath) {
849
+ const { item, context } = input;
850
+ const cwd = context.cwd;
851
+ const note = context.note ?? (() => { });
852
+ const detected = detectAnsweredStuckAction(cwd, item);
853
+ if (detected === undefined)
854
+ return undefined;
855
+ const handler = context.stuckAction ?? performStuckAction;
856
+ let result;
857
+ try {
858
+ result = await handler({
859
+ action: detected,
860
+ item,
861
+ slug: input.slug,
862
+ cwd,
863
+ arbiter: context.arbiter ?? DEFAULT_ARBITER,
864
+ note,
865
+ });
866
+ }
867
+ catch (err) {
868
+ const detail = err instanceof Error ? err.message : String(err);
869
+ const message = `apply ${item}: answered stuck-question dispatch raised (${detail}); NOT ` +
870
+ `clearing needsAnswers; the sidecar stays surfaced.`;
871
+ note(message);
872
+ return { exitCode: 1, outcome: 'usage-error', message };
873
+ }
874
+ if (result.outcome === 'refused') {
875
+ // The arbiter delete FAILED (not `already-gone`); we MUST NOT clear
876
+ // needsAnswers, because that would leave the item claimable while still
877
+ // carrying the WIP branch we meant to discard — the very stale-continue
878
+ // trap the `requeue --reset` path guards against. Leave the sidecar in
879
+ // place for a re-answer.
880
+ note(result.message);
881
+ return { exitCode: 1, outcome: 'usage-error', message: result.message };
882
+ }
883
+ note(result.message);
884
+ const apply = context.applyPersist ?? applyAnsweredQuestions;
885
+ try {
886
+ const applyOptions = {
887
+ cwd,
888
+ item,
889
+ itemPath,
890
+ note,
891
+ };
892
+ if (result.outcome === 'cancel') {
893
+ // The human's answer text is the dispose reason (verbatim), so the
894
+ // terminal `tasks/cancelled/` body records WHY — the same reason
895
+ // contract the agentic `dispose` verdict uses.
896
+ applyOptions.dispose = { reason: detected.entry.answer.trim() };
897
+ }
898
+ const applied = apply(applyOptions);
899
+ const mapped = applied.outcome === 'repaused'
900
+ ? 'no-op'
901
+ : applied.outcome === 'vanished'
902
+ ? 'vanished'
903
+ : 'advanced';
904
+ return {
905
+ exitCode: 0,
906
+ outcome: mapped,
907
+ message: `${result.message} ${applied.message}`,
908
+ };
909
+ }
910
+ catch (err) {
911
+ const detail = err instanceof Error ? err.message : String(err);
912
+ return {
913
+ exitCode: 1,
914
+ outcome: 'usage-error',
915
+ message: `apply ${item}: ${detail}`,
916
+ };
917
+ }
918
+ }
664
919
  /**
665
920
  * The cross-tick-window BENIGN SKIP shared by all three rungs that need to
666
921
  * resolve an item file (surface / triage / apply). The lifecycle pool enumerated
@@ -706,15 +961,24 @@ function findItemPath(cwd, namespace, slug) {
706
961
  *
707
962
  * - `ask` → append the follow-up question(s) + re-pause (the EXISTING loop, via
708
963
  * {@link applyAnsweredQuestions}'s `appendQuestions`);
709
- * - `task` / `prd` → {@link promoteObservation} (mint self-contained + delete
964
+ * - `task` / `spec` → {@link promoteObservation} (mint self-contained + delete
710
965
  * source in the same atomic commit);
711
966
  * - `adr` → {@link mintAdr} (mint a self-contained ADR into `docs/adr/` + delete
712
967
  * source in the same atomic commit; the SIBLING route for the off-board target,
713
968
  * task `agentic-apply-mint-adr-route`);
714
- * - `delete` → {@link applyAnsweredQuestions}'s discharge-by-deletion (`git rm`
715
- * source + sidecar in one revertible commit, the reason in the message).
969
+ * - `resolve` → {@link applyAnsweredQuestions}'s resolve-fully path (harvest the
970
+ * answers into `## Applied answers`, strip the open-questions block, clear
971
+ * `needsAnswers`, DELETE the sidecar — the note is RETAINED). The sibling of
972
+ * `dispose` that KEEPS the note instead of moving/rm-ing it (task
973
+ * `apply-decide-resolve-verdict-mint-nothing`);
974
+ * - `dispose` → {@link applyAnsweredQuestions}'s regime-polymorphic disposal
975
+ * (task `apply-disposition-delete-to-dispose-regime-polymorphic`): an
976
+ * observation is `git rm`-ed in one revertible commit (reason in the
977
+ * message); a task is `git mv`-ed to `tasks/cancelled/` (reason written into
978
+ * the body); a spec is `git mv`-ed to `specs/dropped/`.
716
979
  *
717
- * The allowed set is `{task | prd | adr | delete | ask}`; a verdict outside it is
980
+ * The allowed set is `{task | spec | adr | dispose | resolve | ask}`; a verdict
981
+ * outside it is
718
982
  * rejected by the engine's allowed-outcome guard ({@link DisallowedOutcomeError})
719
983
  * and mapped onto a usage-error — never dispatched.
720
984
  */
@@ -812,11 +1076,13 @@ async function applyAgenticDecision(input, itemPath) {
812
1076
  exitCode: result.exitCode,
813
1077
  outcome: result.outcome === 'promoted'
814
1078
  ? 'advanced'
815
- : result.outcome === 'lost'
816
- ? 'lost'
817
- : result.outcome === 'contended'
818
- ? 'contended'
819
- : 'usage-error',
1079
+ : result.outcome === 'already-triaged'
1080
+ ? 'already-triaged'
1081
+ : result.outcome === 'lost'
1082
+ ? 'lost'
1083
+ : result.outcome === 'contended'
1084
+ ? 'contended'
1085
+ : 'usage-error',
820
1086
  message: result.message,
821
1087
  };
822
1088
  }
@@ -880,16 +1146,53 @@ async function applyAgenticDecision(input, itemPath) {
880
1146
  };
881
1147
  }
882
1148
  }
883
- // delete-source → discharge by deletion (DIRECT, no confirm — decision 12). The
884
- // human's answer is the source of truth; the deletion is a single revertible
885
- // commit with the reason in the message.
1149
+ if (verdict.outcome === 'resolve') {
1150
+ // resolve-no-mint the EXISTING resolve-fully path (task
1151
+ // `apply-decide-resolve-verdict-mint-nothing`). The answer SETTLES the item
1152
+ // with NOTHING to mint and the note RETAINED: call `applyAnsweredQuestions`
1153
+ // with NEITHER `appendQuestions` (re-pause) NOR `dispose` (drop/terminal), so it
1154
+ // takes the default resolve-fully branch — harvest the answers into `##
1155
+ // Applied answers`, strip the marker-fenced open-questions block, clear
1156
+ // `needsAnswers`, and DELETE the sidecar in ONE atomic commit. Invariant-clean
1157
+ // (`needsAnswers:false` ⟺ no active sidecar). The note file is KEPT (this is
1158
+ // the sibling of `dispose`, which git-rm's it or moves it to a terminal). `resolveReason` is advisory
1159
+ // context only; the durable disposition record is the harvested `## Applied
1160
+ // answers` block the resolve-fully path writes (NOT a separate convention).
1161
+ const apply = context.applyPersist ?? applyAnsweredQuestions;
1162
+ try {
1163
+ const result = apply({ cwd, item, itemPath, note });
1164
+ return {
1165
+ exitCode: 0,
1166
+ outcome: result.outcome === 'vanished' ? 'vanished' : 'advanced',
1167
+ message: result.message,
1168
+ };
1169
+ }
1170
+ catch (err) {
1171
+ const detail = err instanceof Error ? err.message : String(err);
1172
+ return {
1173
+ exitCode: 1,
1174
+ outcome: 'usage-error',
1175
+ message: `apply ${item}: ${detail}`,
1176
+ };
1177
+ }
1178
+ }
1179
+ // dispose-source → regime-polymorphic disposal (DIRECT, no confirm — decision
1180
+ // 12; task `apply-disposition-delete-to-dispose-regime-polymorphic`, spec
1181
+ // `surface-stuck-as-questions-and-retire-stuck-lock-state` decision #5). The
1182
+ // human's answer is the source of truth; the persist chooses the on-disk
1183
+ // effect by the source's regime: an OBSERVATION is `git rm`-ed in one
1184
+ // revertible commit (reason in the message, git history = archive); a TASK is
1185
+ // `git mv`-ed to `tasks/cancelled/` (RETAINED, `reason:` written into the
1186
+ // moved body); a SPEC is `git mv`-ed to `specs/dropped/` (RETAINED). A task
1187
+ // cannot be hard-deleted by the apply rung — dispose is the only path off the
1188
+ // board.
886
1189
  const apply = context.applyPersist ?? applyAnsweredQuestions;
887
1190
  try {
888
1191
  const result = apply({
889
1192
  cwd,
890
1193
  item,
891
1194
  itemPath,
892
- discharge: { reason: verdict.deleteReason ?? '' },
1195
+ dispose: { reason: verdict.disposeReason ?? '' },
893
1196
  note,
894
1197
  });
895
1198
  return {
@@ -1069,6 +1372,10 @@ export async function performAdvance(options) {
1069
1372
  verify: options.verify,
1070
1373
  strictMergeApproval: options.strictMergeApproval,
1071
1374
  mergeAction: options.mergeAction,
1375
+ // The answered-stuck-question dispatch context (task
1376
+ // `apply-resolve-reset-flag-discards-work-branch`): threaded into the
1377
+ // apply rung's kind-check for a `kind: 'stuck'` runner-action.
1378
+ stuckAction: options.stuckAction,
1072
1379
  note,
1073
1380
  },
1074
1381
  });