dorfl 0.11.0 → 0.11.2

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 (72) hide show
  1. package/dist/arbiter-refs.d.ts +139 -0
  2. package/dist/arbiter-refs.d.ts.map +1 -0
  3. package/dist/arbiter-refs.js +114 -0
  4. package/dist/arbiter-refs.js.map +1 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +32 -18
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts +73 -12
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +28 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/do-config.d.ts +21 -1
  13. package/dist/do-config.d.ts.map +1 -1
  14. package/dist/do-config.js +19 -0
  15. package/dist/do-config.js.map +1 -1
  16. package/dist/do.d.ts +14 -2
  17. package/dist/do.d.ts.map +1 -1
  18. package/dist/do.js +108 -14
  19. package/dist/do.js.map +1 -1
  20. package/dist/env-config.d.ts.map +1 -1
  21. package/dist/env-config.js +3 -0
  22. package/dist/env-config.js.map +1 -1
  23. package/dist/harness.d.ts +29 -0
  24. package/dist/harness.d.ts.map +1 -1
  25. package/dist/harness.js.map +1 -1
  26. package/dist/ledger-write.d.ts.map +1 -1
  27. package/dist/ledger-write.js +38 -3
  28. package/dist/ledger-write.js.map +1 -1
  29. package/dist/needs-attention.d.ts +43 -0
  30. package/dist/needs-attention.d.ts.map +1 -1
  31. package/dist/needs-attention.js +211 -50
  32. package/dist/needs-attention.js.map +1 -1
  33. package/dist/pi-harness.d.ts.map +1 -1
  34. package/dist/pi-harness.js +109 -13
  35. package/dist/pi-harness.js.map +1 -1
  36. package/dist/protocol/WORK-CONTRACT.md +11 -2
  37. package/dist/protocol/spec-template.md +1 -1
  38. package/dist/protocol/task-template.md +1 -1
  39. package/dist/reap-agent-tree.d.ts +108 -0
  40. package/dist/reap-agent-tree.d.ts.map +1 -0
  41. package/dist/reap-agent-tree.js +173 -0
  42. package/dist/reap-agent-tree.js.map +1 -0
  43. package/dist/repo-config.d.ts +1 -1
  44. package/dist/repo-config.d.ts.map +1 -1
  45. package/dist/repo-config.js +19 -2
  46. package/dist/repo-config.js.map +1 -1
  47. package/dist/run.d.ts.map +1 -1
  48. package/dist/run.js +4 -3
  49. package/dist/run.js.map +1 -1
  50. package/dist/skills/drive-tasks/SKILL.md +20 -2
  51. package/dist/skills/setup/protocol/WORK-CONTRACT.md +11 -2
  52. package/dist/skills/setup/protocol/spec-template.md +1 -1
  53. package/dist/skills/setup/protocol/task-template.md +1 -1
  54. package/dist/worktree-writer-lock.d.ts +99 -0
  55. package/dist/worktree-writer-lock.d.ts.map +1 -0
  56. package/dist/worktree-writer-lock.js +158 -0
  57. package/dist/worktree-writer-lock.js.map +1 -0
  58. package/package.json +1 -1
  59. package/src/arbiter-refs.ts +222 -0
  60. package/src/cli.ts +57 -17
  61. package/src/config.ts +90 -12
  62. package/src/do-config.ts +38 -0
  63. package/src/do.ts +158 -19
  64. package/src/env-config.ts +3 -0
  65. package/src/harness.ts +30 -0
  66. package/src/ledger-write.ts +41 -5
  67. package/src/needs-attention.ts +282 -59
  68. package/src/pi-harness.ts +109 -15
  69. package/src/reap-agent-tree.ts +221 -0
  70. package/src/repo-config.ts +19 -1
  71. package/src/run.ts +4 -3
  72. package/src/worktree-writer-lock.ts +217 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-writer-lock.js","sourceRoot":"","sources":["../src/worktree-writer-lock.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dorfl",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/wighawag/dorfl.git",
@@ -0,0 +1,222 @@
1
+ import {runAsync} from './git.js';
2
+
3
+ /**
4
+ * **The ONE arbiter-ref refresh + authoritative-read seam** (observation
5
+ * `checkpoint-path-reports-its-own-write-as-absent`).
6
+ *
7
+ * Every "did my own write land?" / "is the branch on the arbiter?" question in
8
+ * the checkpoint + surface paths used to be answered the same wrong way: run a
9
+ * PLAIN `git fetch <arbiter>`, then `git rev-parse <arbiter>/<branch>` — i.e.
10
+ * read a REMOTE-TRACKING ref (`refs/remotes/<arbiter>/…`) and trust it. That is
11
+ * unsound in the configuration dorfl itself creates for `--isolated` runs, and
12
+ * it produced two field defects where dorfl reported its OWN successful write as
13
+ * absent:
14
+ *
15
+ * 1. A job worktree is `git worktree add`ed from the BARE HUB MIRROR
16
+ * (`workspace.ts` `createJob` → `repo-mirror.ts` `ensureMirror`), whose
17
+ * `origin` carries the MIRROR-style refspec `+refs/heads/*:refs/heads/*`.
18
+ * So a plain fetch there writes `refs/heads/main`, and **never populates**
19
+ * `refs/remotes/origin/main` at all. `rev-parse origin/main` then returns
20
+ * whatever a PRIOR explicit-refspec fetch happened to leave behind — a value
21
+ * that PREDATES the write being verified. A push that genuinely landed reads
22
+ * back as "not our commit ⇒ rejected".
23
+ * 2. Worse, in that same worktree a plain `git fetch origin` **fails outright**
24
+ * (`fatal: refusing to fetch into branch 'refs/heads/work/<slug>' checked out
25
+ * at …`), because the mirror refspec's destination IS the branch the worktree
26
+ * has checked out. So it refreshes NOTHING, and a follow-up
27
+ * `rev-parse <arbiter>/work/<slug>` fails against a ref that never existed —
28
+ * reported as "no work branch on <arbiter>" while the branch (and an hour of
29
+ * agent work) sits on the arbiter.
30
+ *
31
+ * Both call sites now route through this module, which fixes the class rather
32
+ * than the two instances:
33
+ *
34
+ * - {@link refreshArbiterRefs} prune-fetches with an EXPLICIT, per-branch
35
+ * refspec into the `refs/remotes/<arbiter>/…` namespace the readers actually
36
+ * read, tolerating the checked-out-branch refusal instead of being silently
37
+ * defeated by it.
38
+ * - {@link resolveArbiterBranch} answers the sha question from the ARBITER
39
+ * ITSELF (`git ls-remote`), so no local ref-namespace/refspec accident can
40
+ * make a landed write look absent. The local tracking ref is only a FALLBACK,
41
+ * used when the arbiter cannot be reached at all.
42
+ *
43
+ * The `ls-remote`-is-authoritative stance is not new — it is the same one
44
+ * `continue-branch.ts` (`branchAheadOfArbiter`), `workspace.ts`, `integrator.ts`
45
+ * and `reap-branches.ts` already take for continue-detection and branch reaping.
46
+ * This module makes it the SHARED default for the post-write verification too,
47
+ * instead of each site re-deciding.
48
+ */
49
+
50
+ /** How a {@link ResolvedArbiterBranch} sha was obtained — the read's PROVENANCE. */
51
+ export type ArbiterRefAuthority =
52
+ /** Read from the arbiter itself (`git ls-remote`): AUTHORITATIVE. */
53
+ | 'arbiter'
54
+ /**
55
+ * The arbiter could not be reached (offline / broken remote), so the local
56
+ * remote-tracking ref was used. Best-effort: it may be stale, so a caller
57
+ * deciding "did MY write land?" must NOT treat a mismatch here as proof of
58
+ * a loss (see {@link ResolvedArbiterBranch.trustworthy}).
59
+ */
60
+ | 'local-fallback'
61
+ /** Neither the arbiter nor any local ref has this branch. */
62
+ | 'absent';
63
+
64
+ /** The resolved state of ONE branch on the arbiter (a single, coherent read). */
65
+ export interface ResolvedArbiterBranch {
66
+ /** The unqualified branch name that was resolved (e.g. `main`, `work/task-x`). */
67
+ branch: string;
68
+ /** Its sha, or `undefined` when the branch exists nowhere we could look. */
69
+ sha?: string;
70
+ /** Where {@link sha} came from. */
71
+ authority: ArbiterRefAuthority;
72
+ /**
73
+ * True iff the arbiter answered (`authority` is `arbiter` or `absent` off a
74
+ * REACHABLE arbiter). When false the read is a stale-capable local fallback,
75
+ * so a mismatch proves nothing and callers must not report a loss from it.
76
+ */
77
+ trustworthy: boolean;
78
+ /** The `ls-remote` stderr when the arbiter could not be reached (diagnostics). */
79
+ unreachableDetail?: string;
80
+ }
81
+
82
+ /** The explicit refspec that maps an arbiter branch into the namespace we READ. */
83
+ function trackingRefspec(arbiter: string, branch: string): string {
84
+ return `+refs/heads/${branch}:refs/remotes/${arbiter}/${branch}`;
85
+ }
86
+
87
+ /**
88
+ * PRUNE-FETCH the named arbiter branches into `refs/remotes/<arbiter>/<branch>`
89
+ * — the namespace every reader in this codebase actually reads — using an
90
+ * EXPLICIT per-branch refspec.
91
+ *
92
+ * Three properties matter, and all three are the reason this is not just
93
+ * `git fetch <arbiter>`:
94
+ *
95
+ * - **Explicit refspec.** A bare-hub-mirror worktree's `origin` maps
96
+ * `+refs/heads/*:refs/heads/*`, so a plain fetch never writes
97
+ * `refs/remotes/<arbiter>/*`. Naming the destination makes the refresh work
98
+ * identically in a normal clone AND in a mirror worktree.
99
+ * - **`--prune`.** A branch DELETED on the arbiter (a `requeue --reset`, a
100
+ * merge-reap, a cross-machine `gc`) must disappear from our view too;
101
+ * otherwise a stale tracking ref answers a liveness question with a ghost.
102
+ * - **Per-branch and SOFT.** Fetching branch-at-a-time means the one refspec
103
+ * git refuses (the destination that is checked out in THIS worktree — see the
104
+ * module doc) cannot abort the refresh of the others, which is exactly how the
105
+ * single combined fetch silently refreshed nothing. Every failure is
106
+ * tolerated and reported rather than thrown: this is a REFRESH, and the
107
+ * authoritative answer comes from {@link resolveArbiterBranch} anyway.
108
+ *
109
+ * Returns the branches that could not be refreshed (for diagnostics only — a
110
+ * caller should not gate on it, because the authoritative read does not depend
111
+ * on the refresh succeeding).
112
+ */
113
+ export async function refreshArbiterRefs(params: {
114
+ cwd: string;
115
+ arbiter: string;
116
+ /** Unqualified branch names to refresh (e.g. `['main', 'work/task-x']`). */
117
+ branches: readonly string[];
118
+ env?: NodeJS.ProcessEnv;
119
+ }): Promise<{failed: string[]}> {
120
+ const {cwd, arbiter, branches, env} = params;
121
+ const failed: string[] = [];
122
+ for (const branch of branches) {
123
+ const fetched = await runAsync(
124
+ 'git',
125
+ [
126
+ 'fetch',
127
+ '--quiet',
128
+ '--prune',
129
+ arbiter,
130
+ trackingRefspec(arbiter, branch),
131
+ ],
132
+ cwd,
133
+ {env},
134
+ );
135
+ if (fetched.status !== 0) {
136
+ failed.push(branch);
137
+ }
138
+ }
139
+ return {failed};
140
+ }
141
+
142
+ /**
143
+ * Resolve ONE branch's sha on the arbiter, ARBITER-AUTHORITATIVELY.
144
+ *
145
+ * `git ls-remote --heads <arbiter> <branch>` asks the arbiter directly, so the
146
+ * answer cannot be defeated by a local refspec/namespace accident — which is the
147
+ * whole point: this is the read a post-write verification uses to decide whether
148
+ * its OWN push landed, and that decision must never be made from a view that
149
+ * predates the push.
150
+ *
151
+ * Decision order:
152
+ * - `ls-remote` exits 0 with a sha ⇒ `{sha, authority: 'arbiter'}` (trustworthy).
153
+ * - `ls-remote` exits 0 with EMPTY output ⇒ the arbiter genuinely does not have
154
+ * the branch ⇒ `{authority: 'absent'}` (trustworthy: a definite "no").
155
+ * - `ls-remote` exits non-zero (unreachable / no such remote) ⇒ fall back to the
156
+ * local `refs/remotes/<arbiter>/<branch>`, flagged `trustworthy: false` so a
157
+ * caller cannot mistake a stale local read for proof of anything.
158
+ *
159
+ * Always call {@link refreshArbiterRefs} first when the caller ALSO needs the
160
+ * objects locally (a CAS base, a `merge-base` / `rev-list` comparison): a sha
161
+ * from `ls-remote` names a commit this repo may not have yet.
162
+ */
163
+ export async function resolveArbiterBranch(params: {
164
+ cwd: string;
165
+ arbiter: string;
166
+ branch: string;
167
+ env?: NodeJS.ProcessEnv;
168
+ }): Promise<ResolvedArbiterBranch> {
169
+ const {cwd, arbiter, branch, env} = params;
170
+ const ls = await runAsync(
171
+ 'git',
172
+ ['ls-remote', '--heads', arbiter, branch],
173
+ cwd,
174
+ {env},
175
+ );
176
+ if (ls.status === 0) {
177
+ // `<sha>\t<ref>` lines. `--heads <branch>` can match several refs when the
178
+ // name is a glob-ish prefix, so take the line whose ref is EXACTLY ours.
179
+ const sha = ls.stdout
180
+ .split('\n')
181
+ .map((line) => line.trim())
182
+ .filter((line) => line !== '')
183
+ .map((line) => line.split(/\s+/))
184
+ .find(([, ref]) => ref === `refs/heads/${branch}`)?.[0];
185
+ if (sha !== undefined && sha !== '') {
186
+ return {branch, sha, authority: 'arbiter', trustworthy: true};
187
+ }
188
+ // Reachable arbiter that does NOT have the branch: a definite, trustworthy
189
+ // "absent" (a stale local ref must not be able to resurrect it).
190
+ return {branch, authority: 'absent', trustworthy: true};
191
+ }
192
+ // Unreachable arbiter: best-effort local read, explicitly NOT trustworthy.
193
+ const local = await runAsync(
194
+ 'git',
195
+ [
196
+ 'rev-parse',
197
+ '--verify',
198
+ '--quiet',
199
+ `refs/remotes/${arbiter}/${branch}^{commit}`,
200
+ ],
201
+ cwd,
202
+ {env},
203
+ );
204
+ const localSha = local.status === 0 ? local.stdout.trim() : '';
205
+ const unreachableDetail =
206
+ ls.stderr.trim() || `git ls-remote exit ${ls.status}`;
207
+ if (localSha !== '') {
208
+ return {
209
+ branch,
210
+ sha: localSha,
211
+ authority: 'local-fallback',
212
+ trustworthy: false,
213
+ unreachableDetail,
214
+ };
215
+ }
216
+ return {
217
+ branch,
218
+ authority: 'absent',
219
+ trustworthy: false,
220
+ unreachableDetail,
221
+ };
222
+ }
package/src/cli.ts CHANGED
@@ -99,6 +99,7 @@ import {
99
99
  freshWorktreeGateFlagOverrides,
100
100
  mergeRetriesFlagOverrides,
101
101
  noPRFlagOverrides,
102
+ triageModelFlagOverrides,
102
103
  } from './do-config.js';
103
104
  import {harnessReviewGate, harnessTaskAcceptanceGate} from './review-gate.js';
104
105
  import {harnessSurfaceGate} from './surface-gate.js';
@@ -107,7 +108,7 @@ import {harnessApplyDecider} from './apply-decide.js';
107
108
  import {harnessTaskReviewGate} from './tasker-review-loop.js';
108
109
  import {runVerify} from './verify.js';
109
110
  import {renderPrompt} from './prompt.js';
110
- import {resolvePromptGuidance} from './config.js';
111
+ import {resolvePromptGuidance, applyModelFallbacks} from './config.js';
111
112
  import {
112
113
  gc,
113
114
  resolveArbiterKeyFromCwd,
@@ -216,7 +217,9 @@ function resolveGlobalConfig(
216
217
  fileConfig: PartialConfig,
217
218
  flags: PartialConfig,
218
219
  ): Config {
219
- return mergeConfig({...fileConfig, ...envOverrides(), ...flags});
220
+ const config = mergeConfig({...fileConfig, ...envOverrides(), ...flags});
221
+ applyModelFallbacks(config);
222
+ return config;
220
223
  }
221
224
 
222
225
  /**
@@ -423,7 +426,7 @@ function buildRegistrySetAdvanceTick(options: {
423
426
  noPR: config.noPR,
424
427
  harness,
425
428
  agentCmd: config.agentCmd,
426
- model: config.model,
429
+ model: config.buildModel,
427
430
  sessionsDir: config.sessionsDir,
428
431
  review: config.review,
429
432
  reviewModel: config.reviewModel,
@@ -448,12 +451,12 @@ function buildRegistrySetAdvanceTick(options: {
448
451
  arbiter: arbiter ?? config.defaultArbiter,
449
452
  doOptions,
450
453
  surfaceGate: harnessSurfaceGate({harness, agentCmd: config.agentCmd}),
451
- surfaceModel: config.model,
454
+ surfaceModel: config.triageModel,
452
455
  applyDecide: harnessApplyDecider({harness, agentCmd: config.agentCmd}),
453
- applyModel: config.model,
456
+ applyModel: config.triageModel,
454
457
  observationTriage: config.observationTriage,
455
458
  triageGate: harnessTriageGate({harness, agentCmd: config.agentCmd}),
456
- triageModel: config.model,
459
+ triageModel: config.triageModel,
457
460
  // The ANSWERED-MERGE LAND DISPATCH context (task
458
461
  // `apply-rung-merge-disposition`, spec `land-time-reverify-and-parallel-
459
462
  // merge-ceiling`): the dispatcher cuts a per-job worktree via
@@ -551,6 +554,8 @@ interface RunFlags extends ScanFlags {
551
554
  pr?: boolean;
552
555
  agentCmd?: string;
553
556
  model?: string;
557
+ /** `--build-model <id>` — the builder-specific model override (falls back to `--model`). */
558
+ buildModel?: string;
554
559
  harness?: string;
555
560
  piBin?: string;
556
561
  sessionsDir?: string;
@@ -558,6 +563,8 @@ interface RunFlags extends ScanFlags {
558
563
  review?: boolean;
559
564
  reviewModel?: string;
560
565
  reviewMaxRounds?: string;
566
+ /** `--triage-model <id>` — the advance lifecycle gates' model (surface, apply, triage). */
567
+ triageModel?: string;
561
568
  /** `--fresh-worktree-gate` / `--no-fresh-worktree-gate` — gate the REBASED tip in a clean throwaway worktree (ON by default). */
562
569
  freshWorktreeGate?: boolean;
563
570
  /** `--merge-retries <n>` — the cross-job merge-serialiser CAS-retry cap (spec `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)). */
@@ -593,6 +600,8 @@ function runFlagOverrides(flags: RunFlags, command?: Commander): PartialConfig {
593
600
  // flag > env > per-repo > global > default — mirroring the `do` command (the
594
601
  // fleet inherits the review gate via the converged `performIntegration` core).
595
602
  Object.assign(overrides, reviewFlagOverrides(flags));
603
+ // `--triage-model` (the advance lifecycle gates' model) rides the SAME chain.
604
+ Object.assign(overrides, triageModelFlagOverrides(flags));
596
605
  // `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain: gate
597
606
  // the REBASED tip in a clean throwaway worktree (ON by default). The `run` fleet
598
607
  // caller additionally downgrades it to today's gate at `perRepoMax > 1` (the
@@ -769,6 +778,8 @@ interface DoFlags {
769
778
  allowBacklog?: boolean;
770
779
  agentCmd?: string;
771
780
  model?: string;
781
+ /** `--build-model <id>` — the builder-specific model override (falls back to `--model`). */
782
+ buildModel?: string;
772
783
  harness?: string;
773
784
  piBin?: string;
774
785
  sessionsDir?: string;
@@ -776,6 +787,8 @@ interface DoFlags {
776
787
  review?: boolean;
777
788
  reviewModel?: string;
778
789
  reviewMaxRounds?: string;
790
+ /** `--triage-model <id>` — the advance lifecycle gates' model (surface, apply, triage). */
791
+ triageModel?: string;
779
792
  /** `--tasker-loop` / `--no-tasker-loop` — the tasker improver loop on/off toggle (`do spec:` path). Resolves into the `taskerLoop` config key. */
780
793
  taskerLoop?: boolean;
781
794
  /** `--tasker-loop-max <n>` — the tasker improver loop's in-context convergence cap (`do spec:` path). Resolves into the `taskerLoopMax` config key. */
@@ -813,6 +826,8 @@ interface IntakeFlags {
813
826
  tasksLandIn?: string;
814
827
  agentCmd?: string;
815
828
  model?: string;
829
+ /** `--intake-model <id>` — the intake decision agent's model (falls back to `--model`). */
830
+ intakeModel?: string;
816
831
  harness?: string;
817
832
  piBin?: string;
818
833
  sessionsDir?: string;
@@ -1491,6 +1506,10 @@ export function buildProgram(): Command {
1491
1506
  '--model <id>',
1492
1507
  'model the agent runs on (routing intent; auth/keys stay the harness\u2019s job). pi: passed as --model; null/shell: substitutes a {model} placeholder in agentCmd. Resolved flag > env > per-repo > global > default (unset).',
1493
1508
  )
1509
+ .option(
1510
+ '--build-model <id>',
1511
+ 'model the BUILD agent runs on (the builder-specific override of --model). Unset at every level inherits --model. Resolved flag > env > per-repo > global > fallback to model > default (unset).',
1512
+ )
1494
1513
  .option(
1495
1514
  '--harness <adapter>',
1496
1515
  'harness adapter that launches the agent + reports liveness: null (default, shells out to agentCmd) or pi (the pi CLI)',
@@ -1516,6 +1535,10 @@ export function buildProgram(): Command {
1516
1535
  '--review-model <id>',
1517
1536
  'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.',
1518
1537
  )
1538
+ .option(
1539
+ '--triage-model <id>',
1540
+ 'model the advance lifecycle gates (surface, apply, triage) run on. Unset at every level inherits --model. Resolved flag > env > per-repo > global > fallback to model > default (unset).',
1541
+ )
1519
1542
  .option(
1520
1543
  '--review-max-rounds <n>',
1521
1544
  'bound the revise/review loop; on exhaustion force needs-attention (default 2)',
@@ -2323,6 +2346,10 @@ export function buildProgram(): Command {
2323
2346
  '--model <id>',
2324
2347
  'model the agent runs on (routing intent; resolved flag > env > per-repo > global > default)',
2325
2348
  )
2349
+ .option(
2350
+ '--build-model <id>',
2351
+ 'model the BUILD agent runs on (the builder-specific override of --model). Unset at every level inherits --model. Resolved flag > env > per-repo > global > fallback to model > default (unset).',
2352
+ )
2326
2353
  .option(
2327
2354
  '--harness <adapter>',
2328
2355
  'harness adapter that launches the agent: null (default, shells out to agentCmd) or pi (the pi CLI)',
@@ -2351,6 +2378,10 @@ export function buildProgram(): Command {
2351
2378
  '--review-model <id>',
2352
2379
  'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.',
2353
2380
  )
2381
+ .option(
2382
+ '--triage-model <id>',
2383
+ 'model the advance lifecycle gates (surface, apply, triage) run on. Unset at every level inherits --model. Resolved flag > env > per-repo > global > fallback to model > default (unset).',
2384
+ )
2354
2385
  .option(
2355
2386
  '--review-max-rounds <n>',
2356
2387
  'bound the revise/review loop; on exhaustion force needs-attention (default 2)',
@@ -2620,7 +2651,7 @@ export function buildProgram(): Command {
2620
2651
  noPR: remoteConfig.noPR,
2621
2652
  harness: remoteHarness,
2622
2653
  agentCmd: remoteConfig.agentCmd,
2623
- model: remoteConfig.model,
2654
+ model: remoteConfig.buildModel,
2624
2655
  sessionsDir: remoteConfig.sessionsDir,
2625
2656
  review: remoteConfig.review,
2626
2657
  reviewModel: remoteConfig.reviewModel,
@@ -2799,7 +2830,7 @@ export function buildProgram(): Command {
2799
2830
  noPR: config.noPR,
2800
2831
  harness,
2801
2832
  agentCmd: config.agentCmd,
2802
- model: config.model,
2833
+ model: config.buildModel,
2803
2834
  // The HOST-ONLY sessions root (resolved Config → DoOptions bridge, like
2804
2835
  // model/agentCmd): the path generator turns it into
2805
2836
  // `<sessionsDir>/<id>.jsonl` for `--session`. Without this map the key
@@ -3132,7 +3163,7 @@ export function buildProgram(): Command {
3132
3163
  noPR: remoteConfig.noPR,
3133
3164
  harness: isoHarness,
3134
3165
  agentCmd: remoteConfig.agentCmd,
3135
- model: remoteConfig.model,
3166
+ model: remoteConfig.buildModel,
3136
3167
  sessionsDir: remoteConfig.sessionsDir,
3137
3168
  review: remoteConfig.review,
3138
3169
  reviewModel: remoteConfig.reviewModel,
@@ -3174,18 +3205,18 @@ export function buildProgram(): Command {
3174
3205
  harness: isoHarness,
3175
3206
  agentCmd: remoteConfig.agentCmd,
3176
3207
  }),
3177
- surfaceModel: remoteConfig.model,
3208
+ surfaceModel: remoteConfig.triageModel,
3178
3209
  applyDecide: harnessApplyDecider({
3179
3210
  harness: isoHarness,
3180
3211
  agentCmd: remoteConfig.agentCmd,
3181
3212
  }),
3182
- applyModel: remoteConfig.model,
3213
+ applyModel: remoteConfig.triageModel,
3183
3214
  observationTriage: remoteConfig.observationTriage,
3184
3215
  triageGate: harnessTriageGate({
3185
3216
  harness: isoHarness,
3186
3217
  agentCmd: remoteConfig.agentCmd,
3187
3218
  }),
3188
- triageModel: remoteConfig.model,
3219
+ triageModel: remoteConfig.triageModel,
3189
3220
  note: (message) => console.error(`>> ${message}`),
3190
3221
  env: process.env,
3191
3222
  };
@@ -3276,7 +3307,7 @@ export function buildProgram(): Command {
3276
3307
  noPR: config.noPR,
3277
3308
  harness,
3278
3309
  agentCmd: config.agentCmd,
3279
- model: config.model,
3310
+ model: config.buildModel,
3280
3311
  sessionsDir: config.sessionsDir,
3281
3312
  review: config.review,
3282
3313
  reviewModel: config.reviewModel,
@@ -3312,12 +3343,12 @@ export function buildProgram(): Command {
3312
3343
  arbiter: flags.arbiter ?? config.defaultArbiter,
3313
3344
  doOptions,
3314
3345
  surfaceGate: harnessSurfaceGate({harness, agentCmd: config.agentCmd}),
3315
- surfaceModel: config.model,
3346
+ surfaceModel: config.triageModel,
3316
3347
  applyDecide: harnessApplyDecider({harness, agentCmd: config.agentCmd}),
3317
- applyModel: config.model,
3348
+ applyModel: config.triageModel,
3318
3349
  observationTriage: config.observationTriage,
3319
3350
  triageGate: harnessTriageGate({harness, agentCmd: config.agentCmd}),
3320
- triageModel: config.model,
3351
+ triageModel: config.triageModel,
3321
3352
  note: (message) => console.error(`>> ${message}`),
3322
3353
  };
3323
3354
 
@@ -4386,6 +4417,10 @@ export function buildProgram(): Command {
4386
4417
  '--model <id>',
4387
4418
  'model the decision agent runs on (routing intent; resolved flag > env > per-repo > global > default)',
4388
4419
  )
4420
+ .option(
4421
+ '--intake-model <id>',
4422
+ 'model the intake decision agent runs on (the builder-specific override of --model). Unset at every level inherits --model. Resolved flag > env > per-repo > global > fallback to model > default (unset).',
4423
+ )
4389
4424
  .option(
4390
4425
  '--harness <adapter>',
4391
4426
  'harness adapter that launches the decision agent: null (default) or pi',
@@ -4420,6 +4455,11 @@ export function buildProgram(): Command {
4420
4455
  ...harnessFlagOverrides(flags),
4421
4456
  // `--no-pr` (the PR-INTENT axis) rides the SAME chain.
4422
4457
  ...noPRFlagOverrides(flags),
4458
+ // `--intake-model` rides the SAME chain (falls back to `model`
4459
+ // when unset at all levels via `applyModelFallbacks`).
4460
+ ...(flags.intakeModel !== undefined
4461
+ ? {intakeModel: flags.intakeModel}
4462
+ : {}),
4423
4463
  },
4424
4464
  });
4425
4465
  if (resolved.message) {
@@ -4534,7 +4574,7 @@ export function buildProgram(): Command {
4534
4574
  explicitTasksLandIn,
4535
4575
  harness,
4536
4576
  agentCmd: config.agentCmd,
4537
- model: config.model,
4577
+ model: config.intakeModel,
4538
4578
  sessionsDir: config.sessionsDir,
4539
4579
  // Host-only runner IDENTITY — scopes intake's `gh`/git ops (not the
4540
4580
  // decision/review AGENT launches); absent ⇒ ambient.
package/src/config.ts CHANGED
@@ -528,9 +528,25 @@ export interface Config {
528
528
  * (`LaunchInput.model`); the ADAPTER decides HOW it reaches its tool — the pi
529
529
  * adapter passes `--model <model>` natively, the null/shell adapter substitutes
530
530
  * a `{model}` placeholder in `agentCmd`. Resolved per-repo like `integration`:
531
- * flag (`--model`) > env > per-repo > global > default (unset).
531
+ * flag (`--model`) > env > per-repo > global > default (unset). The fallback
532
+ * to this field from `buildModel` / `reviewModel` / `taskerLoopModel` is
533
+ * applied AFTER the full precedence chain resolves (in `applyModelFallbacks`),
534
+ * so a per-role knob set at ANY level (even global config) is NEVER reset by a
535
+ * higher-precedence `--model`.
532
536
  */
533
537
  model?: string;
538
+ /**
539
+ * The model the BUILD agent runs on (the builder-specific override of the
540
+ * general `model`). Optional with NO default so "unset" means "inherit the
541
+ * general `model`" (via `applyModelFallbacks`, applied at the resolution FINAL
542
+ * point — after the full flag > env > per-repo > global chain). Carried to the
543
+ * build launch through the same harness seam (`LaunchInput.model`). Resolved
544
+ * per-repo like `model`: flag (`--build-model`) > env (`DORFL_BUILD_MODEL`) >
545
+ * per-repo > global > fallback to `model` > default (unset). DISTINCT from
546
+ * `reviewModel` (Gate 2) and `taskerLoopModel` (the improver loop) — each has
547
+ * its OWN knob and each falls back to `model` independently.
548
+ */
549
+ buildModel?: string;
534
550
  /**
535
551
  * Which harness adapter launches + reports liveness for a job's agent (the
536
552
  * harness seam, ADR §5): `null` (default — shells out to `agentCmd`,
@@ -600,11 +616,14 @@ export interface Config {
600
616
  review: boolean;
601
617
  /**
602
618
  * The model the REVIEW agent runs on (de-correlation from the builder's
603
- * `model`). Optional with NO default so "unset" means "no forced review model"
604
- * (the harness's own default). Carried to the review-agent launch through the
605
- * EXISTING harness seam (`LaunchInput.model` / `substituteModel`) — NOT a new
606
- * mechanism. Resolved like `model`: flag (`--review-model`) > env > per-repo >
607
- * global > default (unset). Distinct from the builder's `model`.
619
+ * `buildModel` when explicitly set). Optional with NO default so "unset" means
620
+ * "inherit the general `model`" (via `applyModelFallbacks`, applied at the
621
+ * resolution FINAL point — after the full flag > env > per-repo > global chain).
622
+ * Carried to the review-agent launch through the EXISTING harness seam
623
+ * (`LaunchInput.model` / `substituteModel`) — NOT a new mechanism. Resolved like
624
+ * `model`: flag (`--review-model`) > env > per-repo > global > fallback to
625
+ * `model` > default (unset). Distinct from the builder's `buildModel` — each
626
+ * has its OWN knob and each falls back to `model` independently.
608
627
  */
609
628
  reviewModel?: string;
610
629
  /**
@@ -650,14 +669,44 @@ export interface Config {
650
669
  taskerLoopMax: number;
651
670
  /**
652
671
  * The model the tasker IMPROVER loop's review agent runs on (de-correlation
653
- * from the tasker). Optional with NO default so "unset" means "no forced model"
654
- * (the harness's own default). Carried to the review-agent launch through the
655
- * EXISTING harness seam (`LaunchInput.model` / `substituteModel`). Resolved like
656
- * `model`: flag (`--tasker-loop-model`) > env > per-repo > global > default
657
- * (unset). DISTINCT from the acceptance gate's `reviewModel` (build
658
- * `--review-model`).
672
+ * from the tasker when explicitly set). Optional with NO default so "unset"
673
+ * means "inherit the general `model`" (via `applyModelFallbacks`, applied at
674
+ * the resolution FINAL point — after the full flag > env > per-repo > global
675
+ * chain). Carried to the review-agent launch through the EXISTING harness seam
676
+ * (`LaunchInput.model` / `substituteModel`). Resolved like `model`: flag
677
+ * (`--tasker-loop-model`) > env > per-repo > global > fallback to `model` >
678
+ * default (unset). DISTINCT from the acceptance gate's `reviewModel` (build
679
+ * `--review-model`) and from `buildModel` — each has its OWN knob and each falls
680
+ * back to `model` independently.
659
681
  */
660
682
  taskerLoopModel?: string;
683
+ /**
684
+ * The model the ADVANCE LIFECYCLE GATES (surface, apply, triage) run on —
685
+ * the fresh-context judgement agents that surface questions from blocked work
686
+ * (`surface`), interpret answered questions and decide what to mint (`apply`),
687
+ * and auto-disposition no-question observations (`triage`). Optional with NO
688
+ * default so "unset" means "inherit the general `model`" (via
689
+ * `applyModelFallbacks`, applied at the resolution FINAL point). Carried to each
690
+ * gate's launch through the EXISTING harness seam (`LaunchInput.model`). Resolved
691
+ * like `model`: flag (`--triage-model`) > env (`DORFL_TRIAGE_MODEL`) > per-repo >
692
+ * global > fallback to `model` > default (unset). DISTINCT from `reviewModel`,
693
+ * `taskerLoopModel`, `buildModel`, and `intakeModel` — each has its OWN knob and
694
+ * each falls back to `model` independently.
695
+ */
696
+ triageModel?: string;
697
+ /**
698
+ * The model the INTAKE decision agent runs on — the front-door fresh-context
699
+ * agent that reads an issue/PR and decides whether to mint a task, mint a spec,
700
+ * or ask a follow-up question. Optional with NO default so "unset" means
701
+ * "inherit the general `model`" (via `applyModelFallbacks`, applied at the
702
+ * resolution FINAL point). Carried to the intake-agent launch through the
703
+ * EXISTING harness seam (`LaunchInput.model`). Resolved like `model`: flag
704
+ * (`--intake-model`) > env (`DORFL_INTAKE_MODEL`) > per-repo > global > fallback
705
+ * to `model` > default (unset). DISTINCT from the advance lifecycle gates'
706
+ * `triageModel` and from `buildModel`/`reviewModel`/`taskerLoopModel` — each has
707
+ * its OWN knob and each falls back to `model` independently.
708
+ */
709
+ intakeModel?: string;
661
710
  /**
662
711
  * **The fresh-worktree acceptance-gate toggle** (`--fresh-worktree-gate` /
663
712
  * `--no-fresh-worktree-gate`). When ON (the default), the acceptance gate
@@ -1078,6 +1127,35 @@ export function mergeConfig(overrides: PartialConfig): Config {
1078
1127
  return merged;
1079
1128
  }
1080
1129
 
1130
+ /**
1131
+ * Apply the per-role model fallbacks IN PLACE on a FULLY RESOLVED {@link Config}:
1132
+ * `buildModel`, `reviewModel`, and `taskerLoopModel` each inherit the general
1133
+ * `model` when they are unset (i.e. were not set at ANY precedence level — flag,
1134
+ * env, per-repo, or global). This is the single place the fallback lives; it is
1135
+ * called at the resolution FINAL points (`resolveRepoConfigFromLoaded` for the
1136
+ * per-repo chain; `resolveGlobalConfig` for the global-only chain) so the
1137
+ * fallback uses the FINAL resolved `model`, never an intermediate layer's. A
1138
+ * per-role knob set at ANY level is NEVER reset — `applyModelFallbacks` only
1139
+ * touches fields that are still `undefined` after the full chain resolved.
1140
+ */
1141
+ export function applyModelFallbacks(config: Config): void {
1142
+ if (config.buildModel === undefined) {
1143
+ config.buildModel = config.model;
1144
+ }
1145
+ if (config.reviewModel === undefined) {
1146
+ config.reviewModel = config.model;
1147
+ }
1148
+ if (config.taskerLoopModel === undefined) {
1149
+ config.taskerLoopModel = config.model;
1150
+ }
1151
+ if (config.triageModel === undefined) {
1152
+ config.triageModel = config.model;
1153
+ }
1154
+ if (config.intakeModel === undefined) {
1155
+ config.intakeModel = config.model;
1156
+ }
1157
+ }
1158
+
1081
1159
  /**
1082
1160
  * Persist `config` to `path` as pretty JSON (creating the parent dir). Used by
1083
1161
  * `work-on` to SAVE the prompted `humanWorktreesDir` on first use so the human is