dorfl 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/dist/advance-drivers.d.ts +1 -1
  2. package/dist/advance-drivers.d.ts.map +1 -1
  3. package/dist/advance-drivers.js +15 -3
  4. package/dist/advance-drivers.js.map +1 -1
  5. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  6. package/dist/advance-lifecycle-template.js +76 -4
  7. package/dist/advance-lifecycle-template.js.map +1 -1
  8. package/dist/advance-treeless-publish.d.ts +23 -0
  9. package/dist/advance-treeless-publish.d.ts.map +1 -1
  10. package/dist/advance-treeless-publish.js +41 -0
  11. package/dist/advance-treeless-publish.js.map +1 -1
  12. package/dist/advance.d.ts +59 -7
  13. package/dist/advance.d.ts.map +1 -1
  14. package/dist/advance.js +389 -82
  15. package/dist/advance.js.map +1 -1
  16. package/dist/advancing-lock.d.ts +30 -2
  17. package/dist/advancing-lock.d.ts.map +1 -1
  18. package/dist/advancing-lock.js +50 -0
  19. package/dist/advancing-lock.js.map +1 -1
  20. package/dist/agent-launch.d.ts +12 -0
  21. package/dist/agent-launch.d.ts.map +1 -1
  22. package/dist/agent-launch.js +22 -12
  23. package/dist/agent-launch.js.map +1 -1
  24. package/dist/agent-stop.d.ts +40 -2
  25. package/dist/agent-stop.d.ts.map +1 -1
  26. package/dist/agent-stop.js +30 -2
  27. package/dist/agent-stop.js.map +1 -1
  28. package/dist/apply-decide.d.ts +17 -3
  29. package/dist/apply-decide.d.ts.map +1 -1
  30. package/dist/apply-decide.js +36 -7
  31. package/dist/apply-decide.js.map +1 -1
  32. package/dist/apply-merge-action.d.ts +17 -5
  33. package/dist/apply-merge-action.d.ts.map +1 -1
  34. package/dist/apply-merge-action.js +43 -8
  35. package/dist/apply-merge-action.js.map +1 -1
  36. package/dist/apply-persist.d.ts +57 -26
  37. package/dist/apply-persist.d.ts.map +1 -1
  38. package/dist/apply-persist.js +166 -37
  39. package/dist/apply-persist.js.map +1 -1
  40. package/dist/apply-stuck-action.d.ts +151 -0
  41. package/dist/apply-stuck-action.d.ts.map +1 -0
  42. package/dist/apply-stuck-action.js +125 -0
  43. package/dist/apply-stuck-action.js.map +1 -0
  44. package/dist/brand.d.ts +12 -1
  45. package/dist/brand.d.ts.map +1 -1
  46. package/dist/brand.js +2 -1
  47. package/dist/brand.js.map +1 -1
  48. package/dist/cli-spinner.d.ts +1 -1
  49. package/dist/cli-spinner.js +1 -1
  50. package/dist/cli.d.ts +10 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +320 -63
  53. package/dist/cli.js.map +1 -1
  54. package/dist/close-job-template.js +1 -1
  55. package/dist/complete.d.ts +6 -3
  56. package/dist/complete.d.ts.map +1 -1
  57. package/dist/complete.js +91 -37
  58. package/dist/complete.js.map +1 -1
  59. package/dist/config.d.ts +55 -9
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +56 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/continue-branch.d.ts.map +1 -1
  64. package/dist/continue-branch.js +22 -0
  65. package/dist/continue-branch.js.map +1 -1
  66. package/dist/decision-engine.d.ts +38 -10
  67. package/dist/decision-engine.d.ts.map +1 -1
  68. package/dist/decision-engine.js +10 -6
  69. package/dist/decision-engine.js.map +1 -1
  70. package/dist/do-autopick.d.ts.map +1 -1
  71. package/dist/do-autopick.js +11 -1
  72. package/dist/do-autopick.js.map +1 -1
  73. package/dist/do-config.d.ts +1 -1
  74. package/dist/do.d.ts +49 -56
  75. package/dist/do.d.ts.map +1 -1
  76. package/dist/do.js +419 -30
  77. package/dist/do.js.map +1 -1
  78. package/dist/env-config.d.ts.map +1 -1
  79. package/dist/env-config.js +12 -4
  80. package/dist/env-config.js.map +1 -1
  81. package/dist/failure-cause.d.ts +3 -2
  82. package/dist/failure-cause.d.ts.map +1 -1
  83. package/dist/failure-cause.js +28 -1
  84. package/dist/failure-cause.js.map +1 -1
  85. package/dist/format.d.ts +6 -6
  86. package/dist/format.d.ts.map +1 -1
  87. package/dist/format.js +10 -27
  88. package/dist/format.js.map +1 -1
  89. package/dist/frontmatter.d.ts +16 -4
  90. package/dist/frontmatter.d.ts.map +1 -1
  91. package/dist/frontmatter.js +27 -1
  92. package/dist/frontmatter.js.map +1 -1
  93. package/dist/gc.d.ts +65 -6
  94. package/dist/gc.d.ts.map +1 -1
  95. package/dist/gc.js +126 -9
  96. package/dist/gc.js.map +1 -1
  97. package/dist/github.d.ts +14 -0
  98. package/dist/github.d.ts.map +1 -1
  99. package/dist/github.js +73 -0
  100. package/dist/github.js.map +1 -1
  101. package/dist/harness.d.ts +25 -0
  102. package/dist/harness.d.ts.map +1 -1
  103. package/dist/harness.js.map +1 -1
  104. package/dist/index.d.ts +4 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +2 -1
  107. package/dist/index.js.map +1 -1
  108. package/dist/install-ci-branch-protection.d.ts +132 -38
  109. package/dist/install-ci-branch-protection.d.ts.map +1 -1
  110. package/dist/install-ci-branch-protection.js +190 -48
  111. package/dist/install-ci-branch-protection.js.map +1 -1
  112. package/dist/install-ci-core.d.ts +21 -1
  113. package/dist/install-ci-core.d.ts.map +1 -1
  114. package/dist/install-ci-core.js +10 -3
  115. package/dist/install-ci-core.js.map +1 -1
  116. package/dist/install-ci-github.d.ts +32 -0
  117. package/dist/install-ci-github.d.ts.map +1 -1
  118. package/dist/install-ci-github.js +73 -0
  119. package/dist/install-ci-github.js.map +1 -1
  120. package/dist/install-skills.d.ts +123 -0
  121. package/dist/install-skills.d.ts.map +1 -0
  122. package/dist/install-skills.js +100 -0
  123. package/dist/install-skills.js.map +1 -0
  124. package/dist/intake-trigger-template.js +1 -1
  125. package/dist/intake.d.ts +17 -4
  126. package/dist/intake.d.ts.map +1 -1
  127. package/dist/intake.js +23 -7
  128. package/dist/intake.js.map +1 -1
  129. package/dist/integration-core.d.ts +10 -7
  130. package/dist/integration-core.d.ts.map +1 -1
  131. package/dist/integration-core.js +174 -60
  132. package/dist/integration-core.js.map +1 -1
  133. package/dist/integrator.d.ts +1 -1
  134. package/dist/integrator.d.ts.map +1 -1
  135. package/dist/integrator.js +19 -3
  136. package/dist/integrator.js.map +1 -1
  137. package/dist/isolation.d.ts +2 -2
  138. package/dist/isolation.d.ts.map +1 -1
  139. package/dist/isolation.js +9 -0
  140. package/dist/isolation.js.map +1 -1
  141. package/dist/item-lock.d.ts +196 -129
  142. package/dist/item-lock.d.ts.map +1 -1
  143. package/dist/item-lock.js +329 -254
  144. package/dist/item-lock.js.map +1 -1
  145. package/dist/ledger-write.d.ts +31 -26
  146. package/dist/ledger-write.d.ts.map +1 -1
  147. package/dist/ledger-write.js +100 -122
  148. package/dist/ledger-write.js.map +1 -1
  149. package/dist/lifecycle-gather.d.ts +17 -0
  150. package/dist/lifecycle-gather.d.ts.map +1 -1
  151. package/dist/lifecycle-gather.js +7 -1
  152. package/dist/lifecycle-gather.js.map +1 -1
  153. package/dist/lifecycle-pools.d.ts +40 -3
  154. package/dist/lifecycle-pools.d.ts.map +1 -1
  155. package/dist/lifecycle-pools.js +26 -6
  156. package/dist/lifecycle-pools.js.map +1 -1
  157. package/dist/merge-question-surfacer.d.ts +15 -1
  158. package/dist/merge-question-surfacer.d.ts.map +1 -1
  159. package/dist/merge-question-surfacer.js +18 -3
  160. package/dist/merge-question-surfacer.js.map +1 -1
  161. package/dist/migrate-stuck-locks.d.ts +129 -0
  162. package/dist/migrate-stuck-locks.d.ts.map +1 -0
  163. package/dist/migrate-stuck-locks.js +355 -0
  164. package/dist/migrate-stuck-locks.js.map +1 -0
  165. package/dist/needs-attention.d.ts +293 -16
  166. package/dist/needs-attention.d.ts.map +1 -1
  167. package/dist/needs-attention.js +532 -57
  168. package/dist/needs-attention.js.map +1 -1
  169. package/dist/orphan-sidecar.d.ts +8 -4
  170. package/dist/orphan-sidecar.d.ts.map +1 -1
  171. package/dist/orphan-sidecar.js +35 -2
  172. package/dist/orphan-sidecar.js.map +1 -1
  173. package/dist/pi-harness.d.ts +16 -0
  174. package/dist/pi-harness.d.ts.map +1 -1
  175. package/dist/pi-harness.js +82 -2
  176. package/dist/pi-harness.js.map +1 -1
  177. package/dist/placement.d.ts +3 -3
  178. package/dist/placement.js +1 -1
  179. package/dist/prd-to-spec.d.ts.map +1 -1
  180. package/dist/prd-to-spec.js +9 -5
  181. package/dist/prd-to-spec.js.map +1 -1
  182. package/dist/protocol/CLAIM-PROTOCOL.md +17 -10
  183. package/dist/protocol/REVIEW-PROTOCOL.md +4 -1
  184. package/dist/protocol/SURFACE-PROTOCOL.md +16 -2
  185. package/dist/protocol/TASKING-PROTOCOL.md +3 -1
  186. package/dist/protocol/WORK-CONTRACT.md +22 -18
  187. package/dist/protocol/task-template.md +1 -1
  188. package/dist/reap-branches.d.ts +12 -9
  189. package/dist/reap-branches.d.ts.map +1 -1
  190. package/dist/reap-branches.js +25 -7
  191. package/dist/reap-branches.js.map +1 -1
  192. package/dist/recover-isolated.d.ts +12 -0
  193. package/dist/recover-isolated.d.ts.map +1 -1
  194. package/dist/recover-isolated.js +5 -0
  195. package/dist/recover-isolated.js.map +1 -1
  196. package/dist/repo-config.d.ts +23 -2
  197. package/dist/repo-config.d.ts.map +1 -1
  198. package/dist/repo-config.js +53 -7
  199. package/dist/repo-config.js.map +1 -1
  200. package/dist/repo-mirror.d.ts.map +1 -1
  201. package/dist/repo-mirror.js +18 -2
  202. package/dist/repo-mirror.js.map +1 -1
  203. package/dist/review-verdict.js +1 -1
  204. package/dist/review-verdict.js.map +1 -1
  205. package/dist/run.d.ts +1 -1
  206. package/dist/run.d.ts.map +1 -1
  207. package/dist/run.js +54 -19
  208. package/dist/run.js.map +1 -1
  209. package/dist/scan.d.ts +17 -8
  210. package/dist/scan.d.ts.map +1 -1
  211. package/dist/scan.js +22 -8
  212. package/dist/scan.js.map +1 -1
  213. package/dist/select-priority.d.ts +2 -2
  214. package/dist/select-priority.js +1 -1
  215. package/dist/select-priority.js.map +1 -1
  216. package/dist/sidecar-apply.js +1 -1
  217. package/dist/sidecar-apply.js.map +1 -1
  218. package/dist/sidecar.d.ts +45 -10
  219. package/dist/sidecar.d.ts.map +1 -1
  220. package/dist/sidecar.js +82 -2
  221. package/dist/sidecar.js.map +1 -1
  222. package/dist/skills/answer-questions/SKILL.md +89 -0
  223. package/dist/skills/capture-signal/SKILL.md +52 -0
  224. package/dist/skills/convert-from-prd-to-spec/SKILL.md +90 -0
  225. package/dist/skills/drive-tasks/SKILL.md +218 -0
  226. package/dist/skills/from-idea/SKILL.md +83 -0
  227. package/dist/skills/merge-prs/SKILL.md +70 -0
  228. package/dist/skills/orchestrate/SKILL.md +101 -0
  229. package/dist/skills/promote/SKILL.md +35 -0
  230. package/dist/skills/review/SKILL.md +16 -0
  231. package/dist/skills/setup/SKILL.md +258 -0
  232. package/dist/skills/setup/protocol/ADR-FORMAT.md +47 -0
  233. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +224 -0
  234. package/dist/skills/setup/protocol/REVIEW-PROTOCOL.md +122 -0
  235. package/dist/skills/setup/protocol/SURFACE-PROTOCOL.md +135 -0
  236. package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +124 -0
  237. package/dist/skills/setup/protocol/WORK-CONTRACT.md +280 -0
  238. package/dist/skills/setup/protocol/spec-template.md +71 -0
  239. package/dist/skills/setup/protocol/task-template.md +65 -0
  240. package/dist/skills/surface-questions/SKILL.md +16 -0
  241. package/dist/skills/to-spec/SKILL.md +34 -0
  242. package/dist/skills/to-task/SKILL.md +19 -0
  243. package/dist/skills/triage-observations/SKILL.md +78 -0
  244. package/dist/skills/work/SKILL.md +51 -0
  245. package/dist/slug-namespace.d.ts +5 -5
  246. package/dist/slug-namespace.js +5 -5
  247. package/dist/slug-namespace.js.map +1 -1
  248. package/dist/spec-complete.d.ts.map +1 -1
  249. package/dist/spec-complete.js +2 -3
  250. package/dist/spec-complete.js.map +1 -1
  251. package/dist/start.d.ts.map +1 -1
  252. package/dist/start.js +51 -57
  253. package/dist/start.js.map +1 -1
  254. package/dist/surface-gate.d.ts.map +1 -1
  255. package/dist/surface-gate.js +10 -3
  256. package/dist/surface-gate.js.map +1 -1
  257. package/dist/surface-persist.js +1 -1
  258. package/dist/surface-persist.js.map +1 -1
  259. package/dist/tasker-review-loop.d.ts +4 -3
  260. package/dist/tasker-review-loop.d.ts.map +1 -1
  261. package/dist/tasker-review-loop.js.map +1 -1
  262. package/dist/tasking-lock.d.ts +2 -2
  263. package/dist/tasking-lock.d.ts.map +1 -1
  264. package/dist/tasking-lock.js +53 -32
  265. package/dist/tasking-lock.js.map +1 -1
  266. package/dist/tasking.d.ts +40 -6
  267. package/dist/tasking.d.ts.map +1 -1
  268. package/dist/tasking.js +224 -12
  269. package/dist/tasking.js.map +1 -1
  270. package/dist/triage-persist.d.ts +10 -6
  271. package/dist/triage-persist.d.ts.map +1 -1
  272. package/dist/triage-persist.js +34 -4
  273. package/dist/triage-persist.js.map +1 -1
  274. package/dist/vendor/incur/agents.d.ts +58 -0
  275. package/dist/vendor/incur/agents.d.ts.map +1 -0
  276. package/dist/vendor/incur/agents.js +343 -0
  277. package/dist/vendor/incur/agents.js.map +1 -0
  278. package/dist/verify-workflow-template.js +1 -1
  279. package/dist/watch-session.d.ts +11 -3
  280. package/dist/watch-session.d.ts.map +1 -1
  281. package/dist/watch-session.js +94 -7
  282. package/dist/watch-session.js.map +1 -1
  283. package/dist/work-layout.d.ts +7 -3
  284. package/dist/work-layout.d.ts.map +1 -1
  285. package/dist/work-layout.js +6 -3
  286. package/dist/work-layout.js.map +1 -1
  287. package/dist/workspace.d.ts +1 -1
  288. package/package.json +2 -2
  289. package/src/advance-drivers.ts +16 -3
  290. package/src/advance-lifecycle-template.ts +89 -4
  291. package/src/advance-treeless-publish.ts +45 -0
  292. package/src/advance.ts +467 -87
  293. package/src/advancing-lock.ts +96 -1
  294. package/src/agent-launch.ts +37 -12
  295. package/src/agent-stop.ts +60 -2
  296. package/src/apply-decide.ts +36 -7
  297. package/src/apply-merge-action.ts +45 -7
  298. package/src/apply-persist.ts +228 -54
  299. package/src/apply-stuck-action.ts +260 -0
  300. package/src/brand.ts +14 -2
  301. package/src/cli-spinner.ts +1 -1
  302. package/src/cli.ts +401 -70
  303. package/src/close-job-template.ts +1 -1
  304. package/src/complete.ts +110 -52
  305. package/src/config.ts +107 -13
  306. package/src/continue-branch.ts +22 -0
  307. package/src/decision-engine.ts +55 -17
  308. package/src/do-autopick.ts +12 -1
  309. package/src/do-config.ts +1 -1
  310. package/src/do.ts +511 -38
  311. package/src/env-config.ts +12 -4
  312. package/src/failure-cause.ts +30 -1
  313. package/src/format.ts +10 -30
  314. package/src/frontmatter.ts +35 -5
  315. package/src/gc.ts +172 -9
  316. package/src/github.ts +78 -0
  317. package/src/harness.ts +25 -0
  318. package/src/index.ts +10 -0
  319. package/src/install-ci-branch-protection.ts +282 -57
  320. package/src/install-ci-core.ts +30 -3
  321. package/src/install-ci-github.ts +86 -0
  322. package/src/install-skills.ts +166 -0
  323. package/src/intake-trigger-template.ts +1 -1
  324. package/src/intake.ts +46 -11
  325. package/src/integration-core.ts +199 -65
  326. package/src/integrator.ts +22 -9
  327. package/src/isolation.ts +11 -2
  328. package/src/item-lock.ts +485 -341
  329. package/src/ledger-write.ts +135 -150
  330. package/src/lifecycle-gather.ts +29 -6
  331. package/src/lifecycle-pools.ts +67 -9
  332. package/src/merge-question-surfacer.ts +28 -4
  333. package/src/migrate-stuck-locks.ts +451 -0
  334. package/src/needs-attention.ts +857 -84
  335. package/src/orphan-sidecar.ts +47 -6
  336. package/src/pi-harness.ts +82 -2
  337. package/src/placement.ts +3 -3
  338. package/src/prd-to-spec.ts +13 -5
  339. package/src/reap-branches.ts +38 -14
  340. package/src/recover-isolated.ts +17 -0
  341. package/src/repo-config.ts +55 -6
  342. package/src/repo-mirror.ts +23 -1
  343. package/src/review-verdict.ts +1 -1
  344. package/src/run.ts +64 -18
  345. package/src/scan.ts +22 -8
  346. package/src/select-priority.ts +3 -3
  347. package/src/sidecar-apply.ts +1 -1
  348. package/src/sidecar.ts +138 -11
  349. package/src/slug-namespace.ts +7 -7
  350. package/src/spec-complete.ts +2 -3
  351. package/src/start.ts +53 -59
  352. package/src/surface-gate.ts +10 -3
  353. package/src/surface-persist.ts +1 -1
  354. package/src/tasker-review-loop.ts +4 -3
  355. package/src/tasking-lock.ts +52 -36
  356. package/src/tasking.ts +261 -17
  357. package/src/triage-persist.ts +56 -12
  358. package/src/vendor/incur/LICENSE +21 -0
  359. package/src/vendor/incur/README.md +19 -0
  360. package/src/vendor/incur/agents.ts +392 -0
  361. package/src/verify-workflow-template.ts +1 -1
  362. package/src/watch-session.ts +102 -7
  363. package/src/work-layout.ts +6 -3
  364. package/src/workspace.ts +1 -1
@@ -3,8 +3,44 @@ import {join} from 'node:path';
3
3
  import {run} from './git.js';
4
4
  import {resolveSidecarIdentity} from './sidecar.js';
5
5
  import {resolveItemPathByIdentity} from './item-path.js';
6
+ import {terminalMainPaths} from './item-lock.js';
6
7
  import {workFolderPath, workFolderRel} from './work-layout.js';
7
8
 
9
+ /**
10
+ * Resolve the source item's CURRENT on-disk path INCLUDING its TERMINAL resting
11
+ * records — the orphan-sweep existence predicate.
12
+ *
13
+ * This deliberately WIDENS {@link resolveItemPathByIdentity} (which scans the
14
+ * ACTIVE lifecycle folders + staging, and EXCLUDES terminals on purpose: for the
15
+ * apply rung a terminal-reached item is `vanished`). The orphan sweep asks a
16
+ * DIFFERENT question — "does a source item for this sidecar exist AT ALL?" — and a
17
+ * task/spec that reached its terminal (`tasks/cancelled/`, `specs/dropped/`) is a
18
+ * durable RESTING record that still EXISTS, so its sidecar is NOT an orphan.
19
+ * Treating "reached a terminal" as "gone" would let a routine `gc` delete a live
20
+ * sidecar (and any human answer it carries) — the 2026-07-12 false-positive.
21
+ *
22
+ * Reuses the shared {@link terminalMainPaths} (`item-lock.ts`, the SAME per-type
23
+ * terminal set the lock/branch reapers key off) so the "where do terminals live?"
24
+ * knowledge stays single-sourced. Returns the ACTIVE path when present, else the
25
+ * first existing TERMINAL path, else `undefined` (a genuine orphan).
26
+ */
27
+ function resolveSourceForOrphanSweep(
28
+ cwd: string,
29
+ item: string,
30
+ ): string | undefined {
31
+ const active = resolveItemPathByIdentity(cwd, item);
32
+ if (active !== undefined) {
33
+ return active;
34
+ }
35
+ const {type, slug} = resolveSidecarIdentity(item);
36
+ for (const rel of terminalMainPaths(type, slug)) {
37
+ if (existsSync(join(cwd, rel))) {
38
+ return rel;
39
+ }
40
+ }
41
+ return undefined;
42
+ }
43
+
8
44
  /**
9
45
  * The **orphan-sidecar reaper** — the WORKING-TREE counterpart of the job-worktree
10
46
  * and remote-branch reapers (`gc.ts` / `reap-branches.ts`), folded into `dorfl gc`
@@ -26,10 +62,14 @@ import {workFolderPath, workFolderRel} from './work-layout.js';
26
62
  *
27
63
  * The existence check REUSES the keystone's extracted `resolveItemPathByIdentity`
28
64
  * (`item-path.ts`, the neutral re-exported seam) — the same working-tree,
29
- * by-identity "does the source item exist?" resolver the apply rung uses — never a
30
- * forked second resolver. A sidecar whose source EXISTS is left untouched; a
31
- * sidecar whose source is ABSENT is `git rm`'d (deletion, not a `git mv`: the
32
- * sidecar leaves by deletion, recoverable from git history).
65
+ * by-identity "does the source item exist?" resolver the apply rung uses — WIDENED
66
+ * to also count a source that has reached a TERMINAL folder (`tasks/cancelled/`,
67
+ * `specs/dropped/`, via the shared `terminalMainPaths`) as EXISTING: a terminal is
68
+ * a durable resting record, not "gone", so its sidecar is NOT an orphan (see
69
+ * `resolveSourceForOrphanSweep`). A sidecar whose source EXISTS anywhere is left
70
+ * untouched; a sidecar whose source is ABSENT from every active AND terminal
71
+ * folder is `git rm`'d (deletion, not a `git mv`: the sidecar leaves by deletion,
72
+ * recoverable from git history).
33
73
  */
34
74
 
35
75
  /** A sidecar the orphan sweep reaped (its source item was absent). */
@@ -111,9 +151,10 @@ export function sweepOrphanSidecars(
111
151
  const {item} = resolveSidecarIdentity(stem.replace('-', ':'));
112
152
  const path = `${workFolderRel('questions')}/${entry}`;
113
153
 
114
- const sourcePath = resolveItemPathByIdentity(input.cwd, item);
154
+ const sourcePath = resolveSourceForOrphanSweep(input.cwd, item);
115
155
  if (sourcePath !== undefined) {
116
- // The source item still exists a LIVE sidecar leave it untouched.
156
+ // The source item still exists (active lifecycle OR a terminal resting
157
+ // record) ⇒ a LIVE sidecar ⇒ leave it untouched.
117
158
  retained.push({path, item, sourcePath});
118
159
  note(`Retained ${path}: source ${item} still exists (${sourcePath}).`);
119
160
  continue;
package/src/pi-harness.ts CHANGED
@@ -65,6 +65,15 @@ import type {HarnessAdapter} from './config.js';
65
65
  /** The default pi CLI binary name (resolved on `PATH`). */
66
66
  export const DEFAULT_PI_BIN = 'pi';
67
67
 
68
+ /**
69
+ * The grace period between a deadline SIGTERM and the follow-up SIGKILL in
70
+ * {@link PiHarness.launchAsync} (spec `graceful-pre-timeout-wip-checkpoint`).
71
+ * Ten seconds gives pi enough time to flush its session `.jsonl` and exit
72
+ * cleanly; a wedged/frozen child that ignores SIGTERM still gets reaped in a
73
+ * bounded time so the promise settles and the caller's checkpoint routing runs.
74
+ */
75
+ export const DEADLINE_SIGKILL_GRACE_MS = 10_000;
76
+
68
77
  export interface PiHarnessOptions {
69
78
  /**
70
79
  * The pi CLI binary (default `pi` on `PATH`). Tests inject a stub script here
@@ -239,11 +248,60 @@ export class PiHarness implements Harness {
239
248
  child.stdout?.on('data', () => {});
240
249
  // Guard so `exit` and a late `error` never double-settle the promise.
241
250
  let settled = false;
251
+ let timedOut = false;
252
+ // DEADLINE RACE (spec `graceful-pre-timeout-wip-checkpoint`): when the
253
+ // caller threads a `deadlineMs` (absolute wall-clock epoch-ms), arm a
254
+ // SOFT SIGTERM at that time, then a HARD SIGKILL after a ~10s grace. The
255
+ // child's own `exit` handler below still resolves the promise (with
256
+ // `timedOut: true`), so we preserve the settle-once + FD-release
257
+ // discipline and never double-settle. A run that finishes BEFORE the
258
+ // deadline clears both timers (see the `exit` handler) — byte-for-byte
259
+ // unchanged from the pre-task behaviour.
260
+ let softTimer: NodeJS.Timeout | undefined;
261
+ let hardTimer: NodeJS.Timeout | undefined;
262
+ const clearDeadlineTimers = (): void => {
263
+ if (softTimer !== undefined) {
264
+ clearTimeout(softTimer);
265
+ softTimer = undefined;
266
+ }
267
+ if (hardTimer !== undefined) {
268
+ clearTimeout(hardTimer);
269
+ hardTimer = undefined;
270
+ }
271
+ };
272
+ if (input.deadlineMs !== undefined) {
273
+ const softMs = Math.max(0, input.deadlineMs - Date.now());
274
+ softTimer = setTimeout(() => {
275
+ if (settled) {
276
+ return;
277
+ }
278
+ timedOut = true;
279
+ try {
280
+ child.kill('SIGTERM');
281
+ } catch {
282
+ // Best-effort: a already-exited child throws ESRCH; the `exit`
283
+ // handler will still settle the promise.
284
+ }
285
+ hardTimer = setTimeout(() => {
286
+ if (settled) {
287
+ return;
288
+ }
289
+ try {
290
+ child.kill('SIGKILL');
291
+ } catch {
292
+ // Best-effort: see above.
293
+ }
294
+ }, DEADLINE_SIGKILL_GRACE_MS);
295
+ hardTimer.unref?.();
296
+ }, softMs);
297
+ softTimer.unref?.();
298
+ }
242
299
  child.on('error', (err) => {
243
300
  if (settled) {
244
301
  return;
245
302
  }
246
303
  settled = true;
304
+ clearDeadlineTimers();
247
305
  reject(new Error(`failed to spawn pi (${this.piBin}): ${err.message}`));
248
306
  });
249
307
  // Resolve on `exit` (pi itself terminated), NOT `close`: `close` waits for
@@ -255,6 +313,7 @@ export class PiHarness implements Harness {
255
313
  return;
256
314
  }
257
315
  settled = true;
316
+ clearDeadlineTimers();
258
317
  // Release our end of the stdio pipes so a leaked grandchild's inherited
259
318
  // FDs stop keeping our streams referenced; `unref` the child handle too.
260
319
  child.stdout?.destroy();
@@ -263,9 +322,11 @@ export class PiHarness implements Harness {
263
322
  child.unref?.();
264
323
  const status = code ?? -1;
265
324
  resolve({
266
- ok: status === 0,
325
+ ok: status === 0 && !timedOut,
267
326
  record,
268
- detail: status === 0 ? undefined : stderr.trim(),
327
+ detail:
328
+ status === 0 && !timedOut ? undefined : stderr.trim() || undefined,
329
+ timedOut: timedOut ? true : undefined,
269
330
  // Read the agent's ANSWER from the `.jsonl` at `exit` — the same
270
331
  // last-assistant-text read `launch` does at return (task
271
332
  // `harness-agent-output`); the process has exited so the log is final.
@@ -354,6 +415,14 @@ export class PiHarness implements Harness {
354
415
  * pointer with an `existsSync` check. This is the pi-native "is there an audit
355
416
  * trail to look at?" signal — distinct from PID liveness, and STILL not mtime
356
417
  * (we check existence, never modification time).
418
+ *
419
+ * Studied (task `pi-harness-polish`, finding
420
+ * `work/notes/findings/pi-harness-channels.md`): the recorded `--session <path>`
421
+ * file remains the right audit pointer even against pi's other output surfaces
422
+ * (`--mode json` stdout stream, `--mode rpc`, in-process SDK) — those are
423
+ * transient stdio channels that do not survive the process, whereas the session
424
+ * file is the versioned, dashboard-visible activity trail pi itself treats as
425
+ * the audit artefact. PID stays the liveness anchor alongside it.
357
426
  */
358
427
  export function piSessionExists(record: HarnessRecord): boolean {
359
428
  return record.session !== undefined && existsSync(record.session);
@@ -368,6 +437,17 @@ export function piSessionExists(record: HarnessRecord): boolean {
368
437
  * It REUSES `watch-session.ts`'s {@link lastAssistantText} (one `.jsonl` parser,
369
438
  * not two). An absent file (pi never wrote it) yields `undefined`, as does a log
370
439
  * with no assistant text — a read error is never thrown back into the launch.
440
+ *
441
+ * Studied (task `pi-harness-polish`, finding
442
+ * `work/notes/findings/pi-harness-channels.md`, pinned against pi 0.73.1 +
443
+ * session format v3): the session `.jsonl` is the right channel for output
444
+ * because it is the only one that is BOTH post-mortem readable AND explicitly
445
+ * versioned + migrated (`docs/session-format.md`). The `--mode json` STDOUT
446
+ * event stream is not versioned and is transient; `--mode rpc` and the
447
+ * in-process SDK cost more without solving anything the file does not. The
448
+ * cross-harness `LaunchResult.output` seam (Option C) is a bare `string | undefined`,
449
+ * so a future stream/HTTP-shaped harness (opencode-style) still fits: the file
450
+ * shape lives BEHIND this reader and is not observable through the seam.
371
451
  */
372
452
  function readLastAssistantText(sessionFile: string): string | undefined {
373
453
  let jsonl: string;
package/src/placement.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * LIFECYCLE-GENERIC. The folder names + the configured-default value are
21
21
  * PARAMETERS (`slots`, `configuredDefault`), so the same resolver serves the
22
- * TASK lifecycle (`tasksLandIn`: `pre-backlog`/`ready` — the POOL value was
22
+ * TASK lifecycle (`tasksLandIn`: `backlog`/`ready` — the POOL value was
23
23
  * renamed `'backlog'` → `'todo'` → `'ready'`, ADR
24
24
  * `rename-task-pool-folder-todo-to-ready`) AND the SPEC-
25
25
  * placement lifecycle (`specsLandIn`: `pre-proposed`/`ready`) without forking. A future
@@ -31,7 +31,7 @@
31
31
  * Which side of the staging/pool split the runner chose. The CALLER maps this
32
32
  * onto the lifecycle's concrete folder name via {@link PlacementSlots}.
33
33
  *
34
- * - `'staging'` — land in the staging area (tasks: `pre-backlog/`; prds:
34
+ * - `'staging'` — land in the staging area (tasks: `tasks/backlog/`; prds:
35
35
  * `specs/proposed/`). Not in the agent pool; a human/runner promotion is needed to make
36
36
  * the item eligible. Review-without-PR review surface.
37
37
  * - `'pool'` — land directly in the agent-eligible pool (tasks: `tasks/ready/`;
@@ -42,7 +42,7 @@ export type PlacementSide = 'staging' | 'pool';
42
42
  /**
43
43
  * The two folder names a lifecycle uses for its staging/pool split. Supplied by
44
44
  * the caller so this resolver stays lifecycle-generic (the TASK caller passes
45
- * `{staging: 'pre-backlog', pool: 'tasks/ready'}`; the SPEC-placement caller passes
45
+ * `{staging: 'tasks/backlog', pool: 'tasks/ready'}`; the SPEC-placement caller passes
46
46
  * `{staging: 'specs/proposed', pool: 'specs/ready'}`).
47
47
  */
48
48
  export interface PlacementSlots {
@@ -8,10 +8,11 @@ import {
8
8
  mkdirSync,
9
9
  rmdirSync,
10
10
  } from 'node:fs';
11
- import {join, relative, dirname} from 'node:path';
11
+ import {join, relative, dirname, basename} from 'node:path';
12
12
  import {run, git, gitMv} from './git.js';
13
13
  import {resolveProtocolDoc} from './prompt.js';
14
14
  import {WORK_ROOT} from './work-layout.js';
15
+ import {repoConfigPath} from './repo-config.js';
15
16
 
16
17
  /**
17
18
  * The **`dorfl prd-to-spec` migration ENGINE** (spec
@@ -675,7 +676,9 @@ export function migrateConfig(
675
676
  migration: VocabularyMigration = MIGRATION,
676
677
  options: {dryRun?: boolean} = {},
677
678
  ): ConfigRewrite[] {
678
- const configAbs = join(repoPath, '.dorfl.json');
679
+ // Resolve the repo's actual config file (prefers `dorfl.json`, falls back to
680
+ // the legacy `.dorfl.json`), so the key rewrite finds it under either name.
681
+ const configAbs = repoConfigPath(repoPath);
679
682
  if (!existsSync(configAbs)) {
680
683
  return [];
681
684
  }
@@ -922,10 +925,15 @@ export function scanForLeaks(
922
925
  scanText(rel, readFileSync(join(repoPath, rel), 'utf8'), []);
923
926
  }
924
927
  }
925
- // The config file (the key layer).
926
- const configAbs = join(repoPath, '.dorfl.json');
928
+ // The config file (the key layer) — under either the preferred `dorfl.json`
929
+ // or the legacy `.dorfl.json`.
930
+ const configAbs = repoConfigPath(repoPath);
927
931
  if (existsSync(configAbs)) {
928
- scanText('.dorfl.json', readFileSync(configAbs, 'utf8'), configKeyPatterns);
932
+ scanText(
933
+ basename(configAbs),
934
+ readFileSync(configAbs, 'utf8'),
935
+ configKeyPatterns,
936
+ );
929
937
  }
930
938
  // The FOLDER TREE itself: a surviving `work/prds/` directory (empty ⇒ no
931
939
  // file, but the folder still leaks). Report it structurally.
@@ -1,6 +1,7 @@
1
1
  import {run} from './git.js';
2
- import {isAncestor} from './gc.js';
2
+ import {isProvablyMergedForReap} from './gc.js';
3
3
  import {parseWorkBranchRef} from './slug-namespace.js';
4
+ import type {SidecarType} from './sidecar.js';
4
5
 
5
6
  /**
6
7
  * The **remote merged-work-branch reaper** — the cross-machine COUNTERPART of the
@@ -9,13 +10,15 @@ import {parseWorkBranchRef} from './slug-namespace.js';
9
10
  * the arbiter after their propose-mode PR has MERGED.
10
11
  *
11
12
  * The safety lynchpin is the SAME provably-merged predicate the worktree reaper
12
- * uses (`gc.ts` `isAncestor`, REUSED here, never a second one): a remote
13
- * `work/<slug>` branch is reapable **iff** its tip is
14
- * `git merge-base --is-ancestor <tip> <arbiter>/main` — provably on `main`. A
15
- * merely-pushed-but-unmerged branch (an in-flight, kept-for-continue recovery
16
- * point) is NOT an ancestor of `main`, so the predicate AUTOMATICALLY excludes it
17
- * the never-delete-the-in-flight-branch invariant (ADR §4 deletion-safety)
18
- * stays absolute. Deletion of a fully-merged ref needs NO force, so this NEVER
13
+ * uses (`gc.ts` `isProvablyMergedForReap`, REUSED here, never a second one): a
14
+ * remote `work/<slug>` branch is reapable **iff** it is provably on
15
+ * `<arbiter>/main` — EITHER `git merge-base --is-ancestor <tip> <arbiter>/main`
16
+ * (fast path) OR the squash-aware fallback (durable done/dropped record on main
17
+ * AND branch carries nothing main lacks). A merely-pushed-but-unmerged branch
18
+ * (an in-flight, kept-for-continue recovery point) has no terminal record on
19
+ * main AND is not an ancestor, so the predicate AUTOMATICALLY excludes it — the
20
+ * never-delete-the-in-flight-branch invariant (ADR §4 deletion-safety) stays
21
+ * absolute. Deletion of a fully-merged ref needs NO force, so this NEVER
19
22
  * `--force`s.
20
23
  *
21
24
  * It is provider-AGNOSTIC plain git (`git ls-remote` to enumerate, `git push
@@ -101,8 +104,9 @@ export interface SweepRemoteBranchesResult {
101
104
  * 2. Fetch `<arbiter>/main` into a local tracking ref so the ancestor check
102
105
  * resolves the descendant LOCALLY; fetch each candidate tip object too (so
103
106
  * `merge-base` can read it) — best-effort, into a scratch ref.
104
- * 3. For each branch whose tip `--is-ancestor` of the fetched main delete via
105
- * `git push <arbiter> --delete <branch>`; else retain with a reason.
107
+ * 3. For each branch that is provably merged for reap (fast-path ancestor
108
+ * OR squash-aware fallback {@link isProvablyMergedForReap}) delete
109
+ * via `git push <arbiter> --delete <branch>`; else retain with a reason.
106
110
  */
107
111
  export function sweepRemoteMergedBranches(
108
112
  input: SweepRemoteBranchesInput,
@@ -126,12 +130,32 @@ export function sweepRemoteMergedBranches(
126
130
  retain(retained, note, branch, tip, 'main-unresolved');
127
131
  continue;
128
132
  }
129
- // Make the candidate tip OBJECT readable locally so `merge-base` can use it
130
- // (ls-remote gave us the sha, but the object may not be in the local repo).
133
+ // Make the candidate tip OBJECT readable locally so `merge-base` /
134
+ // `diff` / `cherry` can read it (ls-remote gave us the sha, but the
135
+ // object may not be in the local repo).
131
136
  fetchScratch(input.cwd, input.arbiter, branch, env);
132
137
 
133
- if (!isAncestor(input.cwd, tip, mainRef, env)) {
134
- // NOT an ancestor of main ⇒ still in-flight ⇒ NEVER delete (the invariant).
138
+ const parsed = parseWorkBranchRef(branch);
139
+ if (parsed === undefined) {
140
+ // Guarded above by `listRemoteWorkBranches` (which only enumerates
141
+ // parseable heads), but keep a defensive belt: without a slug the
142
+ // squash-aware fallback has no anchor ⇒ retain.
143
+ retain(retained, note, branch, tip, 'not-merged');
144
+ continue;
145
+ }
146
+
147
+ if (
148
+ !isProvablyMergedForReap({
149
+ cwd: input.cwd,
150
+ tip,
151
+ arbiterMain: mainRef,
152
+ namespace: parsed.namespace as SidecarType,
153
+ slug: parsed.slug,
154
+ env,
155
+ })
156
+ ) {
157
+ // NOT provably merged (neither ancestor nor squash-aware equivalent)
158
+ // ⇒ still in-flight ⇒ NEVER delete (the invariant).
135
159
  retain(retained, note, branch, tip, 'not-merged');
136
160
  continue;
137
161
  }
@@ -57,6 +57,18 @@ export interface RecoverIsolatedOptions {
57
57
  integration?: IntegrationMode;
58
58
  /** Suppress the PR on the propose path (push the branch, open no review request). */
59
59
  noPR?: boolean;
60
+ /**
61
+ * **The cross-job merge-serialiser CAS-retry cap** (config `mergeRetries`, spec
62
+ * `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1
63
+ * (a)). Threaded VERBATIM into {@link performIntegration} so a wide-matrix CI's
64
+ * raised cap actually reaches the cross-job land queue on the isolated-recovery
65
+ * path too (task `thread-merge-retries-cross-task-and-ratify-default`). Resolved
66
+ * ONCE at the CLI (the `complete --isolated` command) through the SAME
67
+ * gate-family precedence chain the plain `complete` path uses (flag > env >
68
+ * per-repo > global > default). Unset ⇒ the engine's `DEFAULT_MERGE_RETRIES =
69
+ * 1000` (byte-for-byte unchanged).
70
+ */
71
+ mergeRetries?: number;
60
72
  /** Optional fully-formed provider used verbatim (test/embedding seam). */
61
73
  providerInstance?: ReviewProvider;
62
74
  /** Optional injectable PR opener (legacy bridge); used in `propose` mode. */
@@ -177,6 +189,11 @@ export async function performRecoverIsolated(
177
189
  recovering: false,
178
190
  committedRecovery: true,
179
191
  mode: options.integration ?? 'propose',
192
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — the
193
+ // git-alone floor of the cross-job land queue, resolved through the gate-
194
+ // family precedence chain in the CLI and threaded here so the isolated-
195
+ // recovery land tail respects the per-repo cap.
196
+ mergeRetries: options.mergeRetries,
180
197
  noPR: options.noPR,
181
198
  providerInstance: options.providerInstance,
182
199
  openPr: options.openPr,
@@ -2,6 +2,7 @@ import {readFileSync, existsSync} from 'node:fs';
2
2
  import {join} from 'node:path';
3
3
  import {
4
4
  mergeConfig,
5
+ validateDeadlineConfig,
5
6
  warnDeprecatedConfigKeys,
6
7
  type Config,
7
8
  type PartialConfig,
@@ -67,17 +68,17 @@ export const REPO_ALLOWED_KEYS = [
67
68
  // still BUILDS each task as a reviewable PR is agreed by all collaborators +
68
69
  // travels with the repo. Resolved per-repo through the SAME chain as
69
70
  // `integration`, then falls back to `integration` when unset. DISTINCT from
70
- // intake's per-EMITTED-TYPE `{task, prd}` resolver (front door, author-trust):
71
+ // intake's per-EMITTED-TYPE `{task, spec}` resolver (front door, author-trust):
71
72
  // this is a per-lifecycle-transition knob, inside the boundary, config-resolved.
72
73
  'taskingIntegration',
73
74
  // `tasksLandIn` (the per-repo TASK-PLACEMENT default — staging vs pool, spec
74
75
  // `staging-pool-position-gate-and-trust-model` US #5) is a genuine repo property
75
76
  // exactly like `taskingIntegration`/`integration`: whether THIS repo's tasker
76
- // output lands STAGED (`pre-backlog/`, review-without-PR human-promote path) or
77
+ // output lands STAGED (`tasks/backlog/`, review-without-PR human-promote path) or
77
78
  // straight in the agent-eligible POOL (`ready/`, trusted fast-path) is agreed
78
79
  // by all collaborators + travels with the repo. Resolved per-repo through the
79
80
  // SAME chain as `taskingIntegration` (flag > env > per-repo > global > built-in
80
- // `pre-backlog`). DISTINCT from intake's per-emitted-type stamps (front door):
81
+ // `backlog`). DISTINCT from intake's per-emitted-type stamps (front door):
81
82
  // this is a per-lifecycle PLACEMENT knob, inside the trust boundary,
82
83
  // config-resolved. Fed into the shared placement resolver as the
83
84
  // configured-default rung (`src/placement.ts`).
@@ -91,7 +92,7 @@ export const REPO_ALLOWED_KEYS = [
91
92
  // through the SAME chain as `tasksLandIn` (flag `--specs-land-in` > env
92
93
  // `DORFL_SPECS_LAND_IN` > per-repo > global > built-in). Fed into the shared
93
94
  // placement resolver as the configured-default rung for the spec lifecycle
94
- // (`src/placement.ts`). The sole spec-placement key after the `prd` → `spec` HARD
95
+ // (`src/placement.ts`). The sole spec-placement key after the ''prd'' → `spec` HARD
95
96
  // CUTOVER (the legacy `prdsLandIn` key is GONE — clean break, no accepted alias).
96
97
  'specsLandIn',
97
98
  // `noPR` (the PR-INTENT axis — push the branch but deliberately skip the PR) is
@@ -207,6 +208,16 @@ export const REPO_ALLOWED_KEYS = [
207
208
  // default or shape. The re-surface vs. land branch is
208
209
  // `apply-rung-merge-disposition`'s consumer, never here.
209
210
  'strictMergeApproval',
211
+ // `agentDeadlineMinutes` / `checkpointHeadroomMinutes` / `maxAutoCheckpoints`
212
+ // (spec `graceful-pre-timeout-wip-checkpoint`) are genuine repo properties:
213
+ // how long an agent session runs before checkpointing, and how much head-room
214
+ // the GitHub backstop leaves above it, are agreed by all collaborators + travel
215
+ // with the repo. The dynamic GitHub `timeout-minutes` cap is rendered by the
216
+ // `enumerate` job at run time FROM this committed config, so an edit reflects
217
+ // EVERYWHERE (internal deadline AND GitHub cap) with no install-ci re-run.
218
+ 'agentDeadlineMinutes',
219
+ 'checkpointHeadroomMinutes',
220
+ 'maxAutoCheckpoints',
210
221
  // `promptGuidance` (the NUDGE namespace — prompt-text knobs that strengthen
211
222
  // the worker's in-band wrapper line, e.g. `testFirst`) is a genuine repo
212
223
  // property: "is this repo nudged toward test-first?" travels with the repo
@@ -257,9 +268,42 @@ export type RepoRejectedKey = (typeof REPO_REJECTED_KEYS)[number];
257
268
 
258
269
  const ALLOWED_SET = new Set<string>(REPO_ALLOWED_KEYS);
259
270
 
260
- /** The path to a repo's `.dorfl.json` (repo root + filename). */
271
+ /**
272
+ * The LEGACY per-repo config filename (`.dorfl.json`, the original dotfile form)
273
+ * still honoured on READ for backward compatibility. Reads prefer
274
+ * {@link REPO_CONFIG_FILENAME} (`dorfl.json`) and fall back to this; writes /
275
+ * `setup` use the preferred name. Derived from the single brand identity so a
276
+ * rename flips it in lockstep (see `brand.ts`).
277
+ */
278
+ export const REPO_CONFIG_FILENAME_LEGACY = brand.repoConfigFilenameLegacy;
279
+
280
+ /**
281
+ * Resolve which per-repo config file a repo USES: the PREFERRED
282
+ * `dorfl.json` if present, else the LEGACY `.dorfl.json` if present, else the
283
+ * preferred path (the location a missing file WOULD live / be created at). So a
284
+ * repo that already committed the legacy dotfile keeps working, while a fresh
285
+ * repo (and every write) uses the plain name. If BOTH exist, the preferred
286
+ * `dorfl.json` wins (a deliberate, documented precedence, not a merge).
287
+ */
288
+ export function resolveRepoConfigPath(repoPath: string): string {
289
+ const preferred = join(repoPath, REPO_CONFIG_FILENAME);
290
+ if (existsSync(preferred)) {
291
+ return preferred;
292
+ }
293
+ const legacy = join(repoPath, REPO_CONFIG_FILENAME_LEGACY);
294
+ if (existsSync(legacy)) {
295
+ return legacy;
296
+ }
297
+ return preferred;
298
+ }
299
+
300
+ /**
301
+ * The path to a repo's config file. Prefers the plain `dorfl.json`, falls back
302
+ * to the legacy `.dorfl.json` when only that exists (see
303
+ * {@link resolveRepoConfigPath}).
304
+ */
261
305
  export function repoConfigPath(repoPath: string): string {
262
- return join(repoPath, REPO_CONFIG_FILENAME);
306
+ return resolveRepoConfigPath(repoPath);
263
307
  }
264
308
 
265
309
  /** The result of reading (and filtering) a repo's `.dorfl.json`. */
@@ -542,6 +586,11 @@ export function resolveRepoConfigFromLoaded(
542
586
  ...envOverrides(env),
543
587
  ...(flags ?? {}),
544
588
  });
589
+ // Fail-loud range validation for the deadline / backstop / ceiling triple
590
+ // (spec `graceful-pre-timeout-wip-checkpoint`): a bad value from ANY layer
591
+ // (flag / env / per-repo / global) throws with a clear message naming the
592
+ // field + range — NEVER silently clamped.
593
+ validateDeadlineConfig(config);
545
594
  return {
546
595
  config,
547
596
  rejected: repo.rejected,
@@ -5,6 +5,7 @@ import type {Config, PartialConfig} from './config.js';
5
5
  import type {ConfigOverrideMap} from './config-override.js';
6
6
  import {
7
7
  REPO_CONFIG_FILENAME,
8
+ REPO_CONFIG_FILENAME_LEGACY,
8
9
  loadRepoConfigFromContent,
9
10
  resolveRepoConfigFromLoaded,
10
11
  type LoadedRepoConfig,
@@ -247,7 +248,28 @@ export function readRepoConfigFromMirrorMain(
247
248
  mirrorDir: string,
248
249
  env?: NodeJS.ProcessEnv,
249
250
  ): string | undefined {
250
- const spec = `main:${REPO_CONFIG_FILENAME}`;
251
+ // Prefer the plain `dorfl.json`; fall back to the legacy `.dorfl.json` when the
252
+ // preferred name is absent on `main` (a repo that committed the dotfile before
253
+ // dual-support). A genuine read fault on the FIRST attempt propagates; a
254
+ // path-missing on the first falls through to the legacy attempt.
255
+ const preferred = showConfigOnMain(mirrorDir, REPO_CONFIG_FILENAME, env);
256
+ if (preferred !== undefined) {
257
+ return preferred;
258
+ }
259
+ return showConfigOnMain(mirrorDir, REPO_CONFIG_FILENAME_LEGACY, env);
260
+ }
261
+
262
+ /**
263
+ * `git show main:<filename>` in a mirror, returning the bytes, or `undefined`
264
+ * when the PATH is absent on `main` (the expected config-less / wrong-name case).
265
+ * A genuine fault (unresolvable `main` / bad object) throws.
266
+ */
267
+ function showConfigOnMain(
268
+ mirrorDir: string,
269
+ filename: string,
270
+ env?: NodeJS.ProcessEnv,
271
+ ): string | undefined {
272
+ const spec = `main:${filename}`;
251
273
  const res = run('git', ['show', spec], mirrorDir, {env});
252
274
  if (res.status === 0) {
253
275
  return res.stdout;
@@ -397,7 +397,7 @@ export function reviewDisciplinePrompt(): string {
397
397
  '(the in-band, protocol-native review protocol every set-up repo carries;',
398
398
  'the human-facing pointer is `skills/review/SKILL.md`). Apply its lenses',
399
399
  'IN ORDER, ENDING in the destination check ("if this is built/merged',
400
- 'exactly as written, do we end up with the system the prd/task',
400
+ 'exactly as written, do we end up with the system the spec/task',
401
401
  'goal describes?"). Be ADVERSARIAL; verify against what ACTUALLY LANDED',
402
402
  '(the bytes on disk), not intent. Weight findings by REAL impact \u2014 the',
403
403
  'lenses find candidates, impact decides severity (a technically-true nit',