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,139 @@
1
+ /**
2
+ * **The ONE arbiter-ref refresh + authoritative-read seam** (observation
3
+ * `checkpoint-path-reports-its-own-write-as-absent`).
4
+ *
5
+ * Every "did my own write land?" / "is the branch on the arbiter?" question in
6
+ * the checkpoint + surface paths used to be answered the same wrong way: run a
7
+ * PLAIN `git fetch <arbiter>`, then `git rev-parse <arbiter>/<branch>` — i.e.
8
+ * read a REMOTE-TRACKING ref (`refs/remotes/<arbiter>/…`) and trust it. That is
9
+ * unsound in the configuration dorfl itself creates for `--isolated` runs, and
10
+ * it produced two field defects where dorfl reported its OWN successful write as
11
+ * absent:
12
+ *
13
+ * 1. A job worktree is `git worktree add`ed from the BARE HUB MIRROR
14
+ * (`workspace.ts` `createJob` → `repo-mirror.ts` `ensureMirror`), whose
15
+ * `origin` carries the MIRROR-style refspec `+refs/heads/*:refs/heads/*`.
16
+ * So a plain fetch there writes `refs/heads/main`, and **never populates**
17
+ * `refs/remotes/origin/main` at all. `rev-parse origin/main` then returns
18
+ * whatever a PRIOR explicit-refspec fetch happened to leave behind — a value
19
+ * that PREDATES the write being verified. A push that genuinely landed reads
20
+ * back as "not our commit ⇒ rejected".
21
+ * 2. Worse, in that same worktree a plain `git fetch origin` **fails outright**
22
+ * (`fatal: refusing to fetch into branch 'refs/heads/work/<slug>' checked out
23
+ * at …`), because the mirror refspec's destination IS the branch the worktree
24
+ * has checked out. So it refreshes NOTHING, and a follow-up
25
+ * `rev-parse <arbiter>/work/<slug>` fails against a ref that never existed —
26
+ * reported as "no work branch on <arbiter>" while the branch (and an hour of
27
+ * agent work) sits on the arbiter.
28
+ *
29
+ * Both call sites now route through this module, which fixes the class rather
30
+ * than the two instances:
31
+ *
32
+ * - {@link refreshArbiterRefs} prune-fetches with an EXPLICIT, per-branch
33
+ * refspec into the `refs/remotes/<arbiter>/…` namespace the readers actually
34
+ * read, tolerating the checked-out-branch refusal instead of being silently
35
+ * defeated by it.
36
+ * - {@link resolveArbiterBranch} answers the sha question from the ARBITER
37
+ * ITSELF (`git ls-remote`), so no local ref-namespace/refspec accident can
38
+ * make a landed write look absent. The local tracking ref is only a FALLBACK,
39
+ * used when the arbiter cannot be reached at all.
40
+ *
41
+ * The `ls-remote`-is-authoritative stance is not new — it is the same one
42
+ * `continue-branch.ts` (`branchAheadOfArbiter`), `workspace.ts`, `integrator.ts`
43
+ * and `reap-branches.ts` already take for continue-detection and branch reaping.
44
+ * This module makes it the SHARED default for the post-write verification too,
45
+ * instead of each site re-deciding.
46
+ */
47
+ /** How a {@link ResolvedArbiterBranch} sha was obtained — the read's PROVENANCE. */
48
+ export type ArbiterRefAuthority =
49
+ /** Read from the arbiter itself (`git ls-remote`): AUTHORITATIVE. */
50
+ 'arbiter'
51
+ /**
52
+ * The arbiter could not be reached (offline / broken remote), so the local
53
+ * remote-tracking ref was used. Best-effort: it may be stale, so a caller
54
+ * deciding "did MY write land?" must NOT treat a mismatch here as proof of
55
+ * a loss (see {@link ResolvedArbiterBranch.trustworthy}).
56
+ */
57
+ | 'local-fallback'
58
+ /** Neither the arbiter nor any local ref has this branch. */
59
+ | 'absent';
60
+ /** The resolved state of ONE branch on the arbiter (a single, coherent read). */
61
+ export interface ResolvedArbiterBranch {
62
+ /** The unqualified branch name that was resolved (e.g. `main`, `work/task-x`). */
63
+ branch: string;
64
+ /** Its sha, or `undefined` when the branch exists nowhere we could look. */
65
+ sha?: string;
66
+ /** Where {@link sha} came from. */
67
+ authority: ArbiterRefAuthority;
68
+ /**
69
+ * True iff the arbiter answered (`authority` is `arbiter` or `absent` off a
70
+ * REACHABLE arbiter). When false the read is a stale-capable local fallback,
71
+ * so a mismatch proves nothing and callers must not report a loss from it.
72
+ */
73
+ trustworthy: boolean;
74
+ /** The `ls-remote` stderr when the arbiter could not be reached (diagnostics). */
75
+ unreachableDetail?: string;
76
+ }
77
+ /**
78
+ * PRUNE-FETCH the named arbiter branches into `refs/remotes/<arbiter>/<branch>`
79
+ * — the namespace every reader in this codebase actually reads — using an
80
+ * EXPLICIT per-branch refspec.
81
+ *
82
+ * Three properties matter, and all three are the reason this is not just
83
+ * `git fetch <arbiter>`:
84
+ *
85
+ * - **Explicit refspec.** A bare-hub-mirror worktree's `origin` maps
86
+ * `+refs/heads/*:refs/heads/*`, so a plain fetch never writes
87
+ * `refs/remotes/<arbiter>/*`. Naming the destination makes the refresh work
88
+ * identically in a normal clone AND in a mirror worktree.
89
+ * - **`--prune`.** A branch DELETED on the arbiter (a `requeue --reset`, a
90
+ * merge-reap, a cross-machine `gc`) must disappear from our view too;
91
+ * otherwise a stale tracking ref answers a liveness question with a ghost.
92
+ * - **Per-branch and SOFT.** Fetching branch-at-a-time means the one refspec
93
+ * git refuses (the destination that is checked out in THIS worktree — see the
94
+ * module doc) cannot abort the refresh of the others, which is exactly how the
95
+ * single combined fetch silently refreshed nothing. Every failure is
96
+ * tolerated and reported rather than thrown: this is a REFRESH, and the
97
+ * authoritative answer comes from {@link resolveArbiterBranch} anyway.
98
+ *
99
+ * Returns the branches that could not be refreshed (for diagnostics only — a
100
+ * caller should not gate on it, because the authoritative read does not depend
101
+ * on the refresh succeeding).
102
+ */
103
+ export declare function refreshArbiterRefs(params: {
104
+ cwd: string;
105
+ arbiter: string;
106
+ /** Unqualified branch names to refresh (e.g. `['main', 'work/task-x']`). */
107
+ branches: readonly string[];
108
+ env?: NodeJS.ProcessEnv;
109
+ }): Promise<{
110
+ failed: string[];
111
+ }>;
112
+ /**
113
+ * Resolve ONE branch's sha on the arbiter, ARBITER-AUTHORITATIVELY.
114
+ *
115
+ * `git ls-remote --heads <arbiter> <branch>` asks the arbiter directly, so the
116
+ * answer cannot be defeated by a local refspec/namespace accident — which is the
117
+ * whole point: this is the read a post-write verification uses to decide whether
118
+ * its OWN push landed, and that decision must never be made from a view that
119
+ * predates the push.
120
+ *
121
+ * Decision order:
122
+ * - `ls-remote` exits 0 with a sha ⇒ `{sha, authority: 'arbiter'}` (trustworthy).
123
+ * - `ls-remote` exits 0 with EMPTY output ⇒ the arbiter genuinely does not have
124
+ * the branch ⇒ `{authority: 'absent'}` (trustworthy: a definite "no").
125
+ * - `ls-remote` exits non-zero (unreachable / no such remote) ⇒ fall back to the
126
+ * local `refs/remotes/<arbiter>/<branch>`, flagged `trustworthy: false` so a
127
+ * caller cannot mistake a stale local read for proof of anything.
128
+ *
129
+ * Always call {@link refreshArbiterRefs} first when the caller ALSO needs the
130
+ * objects locally (a CAS base, a `merge-base` / `rev-list` comparison): a sha
131
+ * from `ls-remote` names a commit this repo may not have yet.
132
+ */
133
+ export declare function resolveArbiterBranch(params: {
134
+ cwd: string;
135
+ arbiter: string;
136
+ branch: string;
137
+ env?: NodeJS.ProcessEnv;
138
+ }): Promise<ResolvedArbiterBranch>;
139
+ //# sourceMappingURL=arbiter-refs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arbiter-refs.d.ts","sourceRoot":"","sources":["../src/arbiter-refs.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,oFAAoF;AACpF,MAAM,MAAM,mBAAmB;AAC9B,qEAAqE;AACnE,SAAS;AACX;;;;;GAKG;GACD,gBAAgB;AAClB,6DAA6D;GAC3D,QAAQ,CAAC;AAEZ,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACrC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,mBAAmB,CAAC;IAC/B;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC,CAqB9B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAsDjC"}
@@ -0,0 +1,114 @@
1
+ import { runAsync } from './git.js';
2
+ /** The explicit refspec that maps an arbiter branch into the namespace we READ. */
3
+ function trackingRefspec(arbiter, branch) {
4
+ return `+refs/heads/${branch}:refs/remotes/${arbiter}/${branch}`;
5
+ }
6
+ /**
7
+ * PRUNE-FETCH the named arbiter branches into `refs/remotes/<arbiter>/<branch>`
8
+ * — the namespace every reader in this codebase actually reads — using an
9
+ * EXPLICIT per-branch refspec.
10
+ *
11
+ * Three properties matter, and all three are the reason this is not just
12
+ * `git fetch <arbiter>`:
13
+ *
14
+ * - **Explicit refspec.** A bare-hub-mirror worktree's `origin` maps
15
+ * `+refs/heads/*:refs/heads/*`, so a plain fetch never writes
16
+ * `refs/remotes/<arbiter>/*`. Naming the destination makes the refresh work
17
+ * identically in a normal clone AND in a mirror worktree.
18
+ * - **`--prune`.** A branch DELETED on the arbiter (a `requeue --reset`, a
19
+ * merge-reap, a cross-machine `gc`) must disappear from our view too;
20
+ * otherwise a stale tracking ref answers a liveness question with a ghost.
21
+ * - **Per-branch and SOFT.** Fetching branch-at-a-time means the one refspec
22
+ * git refuses (the destination that is checked out in THIS worktree — see the
23
+ * module doc) cannot abort the refresh of the others, which is exactly how the
24
+ * single combined fetch silently refreshed nothing. Every failure is
25
+ * tolerated and reported rather than thrown: this is a REFRESH, and the
26
+ * authoritative answer comes from {@link resolveArbiterBranch} anyway.
27
+ *
28
+ * Returns the branches that could not be refreshed (for diagnostics only — a
29
+ * caller should not gate on it, because the authoritative read does not depend
30
+ * on the refresh succeeding).
31
+ */
32
+ export async function refreshArbiterRefs(params) {
33
+ const { cwd, arbiter, branches, env } = params;
34
+ const failed = [];
35
+ for (const branch of branches) {
36
+ const fetched = await runAsync('git', [
37
+ 'fetch',
38
+ '--quiet',
39
+ '--prune',
40
+ arbiter,
41
+ trackingRefspec(arbiter, branch),
42
+ ], cwd, { env });
43
+ if (fetched.status !== 0) {
44
+ failed.push(branch);
45
+ }
46
+ }
47
+ return { failed };
48
+ }
49
+ /**
50
+ * Resolve ONE branch's sha on the arbiter, ARBITER-AUTHORITATIVELY.
51
+ *
52
+ * `git ls-remote --heads <arbiter> <branch>` asks the arbiter directly, so the
53
+ * answer cannot be defeated by a local refspec/namespace accident — which is the
54
+ * whole point: this is the read a post-write verification uses to decide whether
55
+ * its OWN push landed, and that decision must never be made from a view that
56
+ * predates the push.
57
+ *
58
+ * Decision order:
59
+ * - `ls-remote` exits 0 with a sha ⇒ `{sha, authority: 'arbiter'}` (trustworthy).
60
+ * - `ls-remote` exits 0 with EMPTY output ⇒ the arbiter genuinely does not have
61
+ * the branch ⇒ `{authority: 'absent'}` (trustworthy: a definite "no").
62
+ * - `ls-remote` exits non-zero (unreachable / no such remote) ⇒ fall back to the
63
+ * local `refs/remotes/<arbiter>/<branch>`, flagged `trustworthy: false` so a
64
+ * caller cannot mistake a stale local read for proof of anything.
65
+ *
66
+ * Always call {@link refreshArbiterRefs} first when the caller ALSO needs the
67
+ * objects locally (a CAS base, a `merge-base` / `rev-list` comparison): a sha
68
+ * from `ls-remote` names a commit this repo may not have yet.
69
+ */
70
+ export async function resolveArbiterBranch(params) {
71
+ const { cwd, arbiter, branch, env } = params;
72
+ const ls = await runAsync('git', ['ls-remote', '--heads', arbiter, branch], cwd, { env });
73
+ if (ls.status === 0) {
74
+ // `<sha>\t<ref>` lines. `--heads <branch>` can match several refs when the
75
+ // name is a glob-ish prefix, so take the line whose ref is EXACTLY ours.
76
+ const sha = ls.stdout
77
+ .split('\n')
78
+ .map((line) => line.trim())
79
+ .filter((line) => line !== '')
80
+ .map((line) => line.split(/\s+/))
81
+ .find(([, ref]) => ref === `refs/heads/${branch}`)?.[0];
82
+ if (sha !== undefined && sha !== '') {
83
+ return { branch, sha, authority: 'arbiter', trustworthy: true };
84
+ }
85
+ // Reachable arbiter that does NOT have the branch: a definite, trustworthy
86
+ // "absent" (a stale local ref must not be able to resurrect it).
87
+ return { branch, authority: 'absent', trustworthy: true };
88
+ }
89
+ // Unreachable arbiter: best-effort local read, explicitly NOT trustworthy.
90
+ const local = await runAsync('git', [
91
+ 'rev-parse',
92
+ '--verify',
93
+ '--quiet',
94
+ `refs/remotes/${arbiter}/${branch}^{commit}`,
95
+ ], cwd, { env });
96
+ const localSha = local.status === 0 ? local.stdout.trim() : '';
97
+ const unreachableDetail = ls.stderr.trim() || `git ls-remote exit ${ls.status}`;
98
+ if (localSha !== '') {
99
+ return {
100
+ branch,
101
+ sha: localSha,
102
+ authority: 'local-fallback',
103
+ trustworthy: false,
104
+ unreachableDetail,
105
+ };
106
+ }
107
+ return {
108
+ branch,
109
+ authority: 'absent',
110
+ trustworthy: false,
111
+ unreachableDetail,
112
+ };
113
+ }
114
+ //# sourceMappingURL=arbiter-refs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arbiter-refs.js","sourceRoot":"","sources":["../src/arbiter-refs.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAC,OAAO,EAAS,MAAM,WAAW,CAAC;AAwH1C,OAAO,EAAiB,KAAK,YAAY,EAAC,MAAM,sBAAsB,CAAC;AAwCvE,OAAO,EAAgB,KAAK,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAgyB5E;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,OAAO,GACb,MAAM,CAoCR;AA6RD,wBAAgB,YAAY,IAAI,OAAO,CAwgHtC;AAaD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,OAAO,GACZ,MAAM,CAyBR;AA+FD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBzE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAC,OAAO,EAAS,MAAM,WAAW,CAAC;AAyH1C,OAAO,EAAiB,KAAK,YAAY,EAAC,MAAM,sBAAsB,CAAC;AAwCvE,OAAO,EAAgB,KAAK,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AA8yB5E;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,OAAO,GACb,MAAM,CAoCR;AA6RD,wBAAgB,YAAY,IAAI,OAAO,CAiiHtC;AAaD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,mBAAmB,EAC3B,KAAK,EAAE,OAAO,GACZ,MAAM,CAyBR;AA+FD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBzE"}
package/dist/cli.js CHANGED
@@ -35,7 +35,7 @@ import { shouldUseColor } from './output.js';
35
35
  import { resolveRepoConfig, resolveRepoConfigFromLoaded, loadRepoConfigFromContent, REPO_CONFIG_FILENAME, } from './repo-config.js';
36
36
  import { ensureMirrorMain, readRepoConfigFromMirrorMain, encodeRepoKey, } from './repo-mirror.js';
37
37
  import { identityEnv } from './identity.js';
38
- import { harnessFlagOverrides, doFlagOverrides, doNeedsAgentCmd, NO_AGENT_CMD_MESSAGE, reviewFlagOverrides, freshWorktreeGateFlagOverrides, mergeRetriesFlagOverrides, noPRFlagOverrides, } from './do-config.js';
38
+ import { harnessFlagOverrides, doFlagOverrides, doNeedsAgentCmd, NO_AGENT_CMD_MESSAGE, reviewFlagOverrides, freshWorktreeGateFlagOverrides, mergeRetriesFlagOverrides, noPRFlagOverrides, triageModelFlagOverrides, } from './do-config.js';
39
39
  import { harnessReviewGate, harnessTaskAcceptanceGate } from './review-gate.js';
40
40
  import { harnessSurfaceGate } from './surface-gate.js';
41
41
  import { harnessTriageGate } from './triage-gate.js';
@@ -43,7 +43,7 @@ import { harnessApplyDecider } from './apply-decide.js';
43
43
  import { harnessTaskReviewGate } from './tasker-review-loop.js';
44
44
  import { runVerify } from './verify.js';
45
45
  import { renderPrompt } from './prompt.js';
46
- import { resolvePromptGuidance } from './config.js';
46
+ import { resolvePromptGuidance, applyModelFallbacks } from './config.js';
47
47
  import { gc, resolveArbiterKeyFromCwd, enumerateWorkspaceArbiters, RETAIN_REASON_TEXT, } from './gc.js';
48
48
  import { runPrdToSpec, } from './prd-to-spec.js';
49
49
  import { resyncProtocol } from './resync-protocol.js';
@@ -107,7 +107,9 @@ function flagOverrides(flags, command) {
107
107
  * `resolveRepoConfig` instead.
108
108
  */
109
109
  function resolveGlobalConfig(fileConfig, flags) {
110
- return mergeConfig({ ...fileConfig, ...envOverrides(), ...flags });
110
+ const config = mergeConfig({ ...fileConfig, ...envOverrides(), ...flags });
111
+ applyModelFallbacks(config);
112
+ return config;
111
113
  }
112
114
  /**
113
115
  * Load BOTH the global config file AND the per-machine `config.override.json`
@@ -274,7 +276,7 @@ function buildRegistrySetAdvanceTick(options) {
274
276
  noPR: config.noPR,
275
277
  harness,
276
278
  agentCmd: config.agentCmd,
277
- model: config.model,
279
+ model: config.buildModel,
278
280
  sessionsDir: config.sessionsDir,
279
281
  review: config.review,
280
282
  reviewModel: config.reviewModel,
@@ -299,12 +301,12 @@ function buildRegistrySetAdvanceTick(options) {
299
301
  arbiter: arbiter ?? config.defaultArbiter,
300
302
  doOptions,
301
303
  surfaceGate: harnessSurfaceGate({ harness, agentCmd: config.agentCmd }),
302
- surfaceModel: config.model,
304
+ surfaceModel: config.triageModel,
303
305
  applyDecide: harnessApplyDecider({ harness, agentCmd: config.agentCmd }),
304
- applyModel: config.model,
306
+ applyModel: config.triageModel,
305
307
  observationTriage: config.observationTriage,
306
308
  triageGate: harnessTriageGate({ harness, agentCmd: config.agentCmd }),
307
- triageModel: config.model,
309
+ triageModel: config.triageModel,
308
310
  // The ANSWERED-MERGE LAND DISPATCH context (task
309
311
  // `apply-rung-merge-disposition`, spec `land-time-reverify-and-parallel-
310
312
  // merge-ceiling`): the dispatcher cuts a per-job worktree via
@@ -396,6 +398,8 @@ function runFlagOverrides(flags, command) {
396
398
  // flag > env > per-repo > global > default — mirroring the `do` command (the
397
399
  // fleet inherits the review gate via the converged `performIntegration` core).
398
400
  Object.assign(overrides, reviewFlagOverrides(flags));
401
+ // `--triage-model` (the advance lifecycle gates' model) rides the SAME chain.
402
+ Object.assign(overrides, triageModelFlagOverrides(flags));
399
403
  // `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain: gate
400
404
  // the REBASED tip in a clean throwaway worktree (ON by default). The `run` fleet
401
405
  // caller additionally downgrades it to today's gate at `perRepoMax > 1` (the
@@ -872,6 +876,7 @@ export function buildProgram() {
872
876
  .option('--no-pr', 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.')
873
877
  .option('--agent-cmd <cmd>', 'command to run one agent on a task prompt')
874
878
  .option('--model <id>', '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).')
879
+ .option('--build-model <id>', '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).')
875
880
  .option('--harness <adapter>', 'harness adapter that launches the agent + reports liveness: null (default, shells out to agentCmd) or pi (the pi CLI)')
876
881
  .option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
877
882
  .option('--sessions-dir <dir>', 'HOST-ONLY root folder under which pi session files are generated (--session <dir>/<id>.jsonl). Default: pi per-cwd folder under ~/.pi/agent/sessions. Resolved flag > env > global > default (no per-repo).')
@@ -879,6 +884,7 @@ export function buildProgram() {
879
884
  .option('--review', 'run Gate 2 (PR/code review) after verify, before the done-move, on every item (overrides config). Resolved flag > env > per-repo > global > default off.')
880
885
  .option('--no-review', 'do NOT run Gate 2 this tick (overrides config)')
881
886
  .option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
887
+ .option('--triage-model <id>', '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).')
882
888
  .option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
883
889
  .option('--fresh-worktree-gate', 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that integrates). ON by default; the `run` fleet uses it only when same-repo concurrency is off (perRepoMax=1), else today\u2019s in-build-worktree gate.')
884
890
  .option('--no-fresh-worktree-gate', 'run the acceptance gate in the build worktree (the pre-rebase tree) — the opt-out for when the per-gate install cost is too high')
@@ -1420,6 +1426,7 @@ export function buildProgram() {
1420
1426
  .option('--allow-backlog', 'do task:<slug> ONLY: also FIND, CLAIM, and COMPLETE a task that lives in tasks/backlog/ (staging), driving it in place WITHOUT promoting it to the pool (so no advance leg / run daemon can claim it out from under you). The done-move goes tasks/backlog/ -> tasks/done/ directly (your explicit drive IS the promotion). EXPLICIT-INVOCATION-ONLY: default off, never set by run/auto-pick/advance or config/env.')
1421
1427
  .option('--agent-cmd <cmd>', 'command to run the agent on the task prompt')
1422
1428
  .option('--model <id>', 'model the agent runs on (routing intent; resolved flag > env > per-repo > global > default)')
1429
+ .option('--build-model <id>', '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).')
1423
1430
  .option('--harness <adapter>', 'harness adapter that launches the agent: null (default, shells out to agentCmd) or pi (the pi CLI)')
1424
1431
  .option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
1425
1432
  .option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the pi session file is generated (--session <dir>/<id>.jsonl). Default: pi per-cwd folder under ~/.pi/agent/sessions. Resolved flag > env > global > default (no per-repo).')
@@ -1427,6 +1434,7 @@ export function buildProgram() {
1427
1434
  .option('--review', 'run Gate 2 (PR/code review) after verify, before the done-move (overrides config). Resolved flag > env > per-repo > global > default off.')
1428
1435
  .option('--no-review', 'do NOT run Gate 2 this invocation (overrides config)')
1429
1436
  .option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
1437
+ .option('--triage-model <id>', '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).')
1430
1438
  .option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
1431
1439
  .option('--tasker-loop', 'run the tasker IMPROVER loop on `do spec:<slug>` (review→edit→converge over the produced task set). ON by default; --no-tasker-loop skips it. DISTINCT from the acceptance gate (--review).')
1432
1440
  .option('--no-tasker-loop', 'skip the tasker improver loop on `do spec:<slug>`')
@@ -1649,7 +1657,7 @@ export function buildProgram() {
1649
1657
  noPR: remoteConfig.noPR,
1650
1658
  harness: remoteHarness,
1651
1659
  agentCmd: remoteConfig.agentCmd,
1652
- model: remoteConfig.model,
1660
+ model: remoteConfig.buildModel,
1653
1661
  sessionsDir: remoteConfig.sessionsDir,
1654
1662
  review: remoteConfig.review,
1655
1663
  reviewModel: remoteConfig.reviewModel,
@@ -1820,7 +1828,7 @@ export function buildProgram() {
1820
1828
  noPR: config.noPR,
1821
1829
  harness,
1822
1830
  agentCmd: config.agentCmd,
1823
- model: config.model,
1831
+ model: config.buildModel,
1824
1832
  // The HOST-ONLY sessions root (resolved Config → DoOptions bridge, like
1825
1833
  // model/agentCmd): the path generator turns it into
1826
1834
  // `<sessionsDir>/<id>.jsonl` for `--session`. Without this map the key
@@ -2082,7 +2090,7 @@ export function buildProgram() {
2082
2090
  noPR: remoteConfig.noPR,
2083
2091
  harness: isoHarness,
2084
2092
  agentCmd: remoteConfig.agentCmd,
2085
- model: remoteConfig.model,
2093
+ model: remoteConfig.buildModel,
2086
2094
  sessionsDir: remoteConfig.sessionsDir,
2087
2095
  review: remoteConfig.review,
2088
2096
  reviewModel: remoteConfig.reviewModel,
@@ -2122,18 +2130,18 @@ export function buildProgram() {
2122
2130
  harness: isoHarness,
2123
2131
  agentCmd: remoteConfig.agentCmd,
2124
2132
  }),
2125
- surfaceModel: remoteConfig.model,
2133
+ surfaceModel: remoteConfig.triageModel,
2126
2134
  applyDecide: harnessApplyDecider({
2127
2135
  harness: isoHarness,
2128
2136
  agentCmd: remoteConfig.agentCmd,
2129
2137
  }),
2130
- applyModel: remoteConfig.model,
2138
+ applyModel: remoteConfig.triageModel,
2131
2139
  observationTriage: remoteConfig.observationTriage,
2132
2140
  triageGate: harnessTriageGate({
2133
2141
  harness: isoHarness,
2134
2142
  agentCmd: remoteConfig.agentCmd,
2135
2143
  }),
2136
- triageModel: remoteConfig.model,
2144
+ triageModel: remoteConfig.triageModel,
2137
2145
  note: (message) => console.error(`>> ${message}`),
2138
2146
  env: process.env,
2139
2147
  };
@@ -2222,7 +2230,7 @@ export function buildProgram() {
2222
2230
  noPR: config.noPR,
2223
2231
  harness,
2224
2232
  agentCmd: config.agentCmd,
2225
- model: config.model,
2233
+ model: config.buildModel,
2226
2234
  sessionsDir: config.sessionsDir,
2227
2235
  review: config.review,
2228
2236
  reviewModel: config.reviewModel,
@@ -2258,12 +2266,12 @@ export function buildProgram() {
2258
2266
  arbiter: flags.arbiter ?? config.defaultArbiter,
2259
2267
  doOptions,
2260
2268
  surfaceGate: harnessSurfaceGate({ harness, agentCmd: config.agentCmd }),
2261
- surfaceModel: config.model,
2269
+ surfaceModel: config.triageModel,
2262
2270
  applyDecide: harnessApplyDecider({ harness, agentCmd: config.agentCmd }),
2263
- applyModel: config.model,
2271
+ applyModel: config.triageModel,
2264
2272
  observationTriage: config.observationTriage,
2265
2273
  triageGate: harnessTriageGate({ harness, agentCmd: config.agentCmd }),
2266
- triageModel: config.model,
2274
+ triageModel: config.triageModel,
2267
2275
  note: (message) => console.error(`>> ${message}`),
2268
2276
  };
2269
2277
  // `--watch` tails ONE pi session, so it only fits the single-named-item form
@@ -3076,6 +3084,7 @@ export function buildProgram() {
3076
3084
  .option('--tasks-land-in <where>', 'where an intake-authored TASK (emitted DIRECTLY from an issue) lands: `backlog` (staged, not agent-eligible) or `ready` (the agent POOL). The task twin of --specs-land-in: the EXPLICIT operator override at the top of the placement precedence (explicit flag > configured default > built-in staging; the caller selects the trusted-vs-untrusted default from the origin-trust stamp). Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
3077
3085
  .option('--agent-cmd <cmd>', 'command to run the decision agent')
3078
3086
  .option('--model <id>', 'model the decision agent runs on (routing intent; resolved flag > env > per-repo > global > default)')
3087
+ .option('--intake-model <id>', '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).')
3079
3088
  .option('--harness <adapter>', 'harness adapter that launches the decision agent: null (default) or pi')
3080
3089
  .option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
3081
3090
  .option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the pi session file is generated')
@@ -3097,6 +3106,11 @@ export function buildProgram() {
3097
3106
  ...harnessFlagOverrides(flags),
3098
3107
  // `--no-pr` (the PR-INTENT axis) rides the SAME chain.
3099
3108
  ...noPRFlagOverrides(flags),
3109
+ // `--intake-model` rides the SAME chain (falls back to `model`
3110
+ // when unset at all levels via `applyModelFallbacks`).
3111
+ ...(flags.intakeModel !== undefined
3112
+ ? { intakeModel: flags.intakeModel }
3113
+ : {}),
3100
3114
  },
3101
3115
  });
3102
3116
  if (resolved.message) {
@@ -3201,7 +3215,7 @@ export function buildProgram() {
3201
3215
  explicitTasksLandIn,
3202
3216
  harness,
3203
3217
  agentCmd: config.agentCmd,
3204
- model: config.model,
3218
+ model: config.intakeModel,
3205
3219
  sessionsDir: config.sessionsDir,
3206
3220
  // Host-only runner IDENTITY — scopes intake's `gh`/git ops (not the
3207
3221
  // decision/review AGENT launches); absent ⇒ ambient.