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.
- package/dist/arbiter-refs.d.ts +139 -0
- package/dist/arbiter-refs.d.ts.map +1 -0
- package/dist/arbiter-refs.js +114 -0
- package/dist/arbiter-refs.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +32 -18
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +73 -12
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -1
- package/dist/do-config.d.ts +21 -1
- package/dist/do-config.d.ts.map +1 -1
- package/dist/do-config.js +19 -0
- package/dist/do-config.js.map +1 -1
- package/dist/do.d.ts +14 -2
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +108 -14
- package/dist/do.js.map +1 -1
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +3 -0
- package/dist/env-config.js.map +1 -1
- package/dist/harness.d.ts +29 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js.map +1 -1
- package/dist/ledger-write.d.ts.map +1 -1
- package/dist/ledger-write.js +38 -3
- package/dist/ledger-write.js.map +1 -1
- package/dist/needs-attention.d.ts +43 -0
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +211 -50
- package/dist/needs-attention.js.map +1 -1
- package/dist/pi-harness.d.ts.map +1 -1
- package/dist/pi-harness.js +109 -13
- package/dist/pi-harness.js.map +1 -1
- package/dist/protocol/WORK-CONTRACT.md +11 -2
- package/dist/protocol/spec-template.md +1 -1
- package/dist/protocol/task-template.md +1 -1
- package/dist/reap-agent-tree.d.ts +108 -0
- package/dist/reap-agent-tree.d.ts.map +1 -0
- package/dist/reap-agent-tree.js +173 -0
- package/dist/reap-agent-tree.js.map +1 -0
- package/dist/repo-config.d.ts +1 -1
- package/dist/repo-config.d.ts.map +1 -1
- package/dist/repo-config.js +19 -2
- package/dist/repo-config.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +4 -3
- package/dist/run.js.map +1 -1
- package/dist/skills/drive-tasks/SKILL.md +20 -2
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +11 -2
- package/dist/skills/setup/protocol/spec-template.md +1 -1
- package/dist/skills/setup/protocol/task-template.md +1 -1
- package/dist/worktree-writer-lock.d.ts +99 -0
- package/dist/worktree-writer-lock.d.ts.map +1 -0
- package/dist/worktree-writer-lock.js +158 -0
- package/dist/worktree-writer-lock.js.map +1 -0
- package/package.json +1 -1
- package/src/arbiter-refs.ts +222 -0
- package/src/cli.ts +57 -17
- package/src/config.ts +90 -12
- package/src/do-config.ts +38 -0
- package/src/do.ts +158 -19
- package/src/env-config.ts +3 -0
- package/src/harness.ts +30 -0
- package/src/ledger-write.ts +41 -5
- package/src/needs-attention.ts +282 -59
- package/src/pi-harness.ts +109 -15
- package/src/reap-agent-tree.ts +221 -0
- package/src/repo-config.ts +19 -1
- package/src/run.ts +4 -3
- package/src/worktree-writer-lock.ts +217 -0
package/src/do-config.ts
CHANGED
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
export interface HarnessFlags {
|
|
18
18
|
agentCmd?: string;
|
|
19
19
|
model?: string;
|
|
20
|
+
/** `--build-model <id>` — the builder-specific model override (falls back to `--model`). */
|
|
21
|
+
buildModel?: string;
|
|
20
22
|
harness?: string;
|
|
21
23
|
piBin?: string;
|
|
22
24
|
/**
|
|
@@ -43,6 +45,9 @@ export function harnessFlagOverrides(flags: HarnessFlags): PartialConfig {
|
|
|
43
45
|
if (flags.model !== undefined) {
|
|
44
46
|
overrides.model = flags.model;
|
|
45
47
|
}
|
|
48
|
+
if (flags.buildModel !== undefined) {
|
|
49
|
+
overrides.buildModel = flags.buildModel;
|
|
50
|
+
}
|
|
46
51
|
if (flags.harness === 'null' || flags.harness === 'pi') {
|
|
47
52
|
overrides.harness = flags.harness;
|
|
48
53
|
}
|
|
@@ -69,6 +74,7 @@ export function doFlagOverrides(
|
|
|
69
74
|
flags: HarnessFlags &
|
|
70
75
|
ReviewFlags &
|
|
71
76
|
TaskerLoopFlags &
|
|
77
|
+
TriageModelFlags &
|
|
72
78
|
FreshWorktreeGateFlags &
|
|
73
79
|
SelectionOrderFlags &
|
|
74
80
|
ObservationTriageFlags &
|
|
@@ -115,6 +121,10 @@ export function doFlagOverrides(
|
|
|
115
121
|
// family from the gate's `--review*`, never sharing a flag/key/field name.
|
|
116
122
|
// The flag fields bridge onto the `taskerLoop*` config keys.
|
|
117
123
|
...taskerLoopFlagOverrides(flags),
|
|
124
|
+
// `--triage-model <id>` (the advance lifecycle gates' model — surface, apply,
|
|
125
|
+
// triage) rides the SAME chain. DISTINCT from `--review-model`/
|
|
126
|
+
// `--tasker-loop-model`; falls back to `model` when unset at all levels.
|
|
127
|
+
...triageModelFlagOverrides(flags),
|
|
118
128
|
// `--fresh-worktree-gate`/`--no-fresh-worktree-gate` (run the acceptance gate
|
|
119
129
|
// against the REBASED tip in a clean throwaway worktree, ON by default) rides
|
|
120
130
|
// the SAME chain — a DISTINCT family from `--review*`/`--tasker-loop*`.
|
|
@@ -232,6 +242,34 @@ export function taskerLoopFlagOverrides(flags: TaskerLoopFlags): PartialConfig {
|
|
|
232
242
|
return overrides;
|
|
233
243
|
}
|
|
234
244
|
|
|
245
|
+
/**
|
|
246
|
+
* The `--triage-model` flag — the model the advance lifecycle gates (surface,
|
|
247
|
+
* apply, triage) run on. A single string flag, distinct from the harness
|
|
248
|
+
* `--model`/`--build-model` family and from `--review-model`/
|
|
249
|
+
* `--tasker-loop-model`. Resolved through the SAME `flag > env > per-repo >
|
|
250
|
+
* global > default` chain; falls back to `model` when unset at all levels (via
|
|
251
|
+
* `applyModelFallbacks`).
|
|
252
|
+
*/
|
|
253
|
+
export interface TriageModelFlags {
|
|
254
|
+
/** `--triage-model <id>` — the advance lifecycle gates' model (routing intent). */
|
|
255
|
+
triageModel?: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Map the `--triage-model` flag into a {@link PartialConfig} override. Only
|
|
260
|
+
* contributes when the flag is actually present (absent flag => absent key), so
|
|
261
|
+
* the override layer never clobbers a lower-precedence source with `undefined`.
|
|
262
|
+
*/
|
|
263
|
+
export function triageModelFlagOverrides(
|
|
264
|
+
flags: TriageModelFlags,
|
|
265
|
+
): PartialConfig {
|
|
266
|
+
const overrides: PartialConfig = {};
|
|
267
|
+
if (flags.triageModel !== undefined) {
|
|
268
|
+
overrides.triageModel = flags.triageModel;
|
|
269
|
+
}
|
|
270
|
+
return overrides;
|
|
271
|
+
}
|
|
272
|
+
|
|
235
273
|
/**
|
|
236
274
|
* The fresh-worktree acceptance-gate CLI flag (`do`/`complete`/`run`):
|
|
237
275
|
* `--fresh-worktree-gate` / `--no-fresh-worktree-gate`, the POSITIVE boolean
|
package/src/do.ts
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
resolvePromptGuidanceForItem,
|
|
18
18
|
PromptError,
|
|
19
19
|
} from './prompt.js';
|
|
20
|
-
import {NullHarness, type Harness} from './harness.js';
|
|
20
|
+
import {NullHarness, type AgentTreeReap, type Harness} from './harness.js';
|
|
21
|
+
import {acquireWorktreeWriterLock} from './worktree-writer-lock.js';
|
|
21
22
|
import {PiHarness} from './pi-harness.js';
|
|
22
23
|
import {launchWithOptionalWatch} from './agent-launch.js';
|
|
23
24
|
import {ledgerRead, type LedgerReadStrategy} from './ledger-read.js';
|
|
@@ -155,10 +156,27 @@ function deadlineAutoContinueReason(params: {
|
|
|
155
156
|
}
|
|
156
157
|
function deadlineSurfaceReason(params: {
|
|
157
158
|
slug: string;
|
|
158
|
-
kind: 'no-progress' | 'ceiling';
|
|
159
|
+
kind: 'no-progress' | 'ceiling' | 'unreaped';
|
|
159
160
|
count?: number;
|
|
160
161
|
max?: number;
|
|
162
|
+
/** The harness's loud reap detail (the `unreaped` kind only). */
|
|
163
|
+
detail?: string;
|
|
161
164
|
}): string {
|
|
165
|
+
if (params.kind === 'unreaped') {
|
|
166
|
+
// The auto-continue path is BLOCKED, not merely skipped: releasing the lock
|
|
167
|
+
// would let a successor onboard into a worktree a live predecessor may still
|
|
168
|
+
// be writing to (observation
|
|
169
|
+
// `checkpoint-releases-lock-while-predecessor-agent-still-writes`). The work
|
|
170
|
+
// is saved and pushed; only the hand-off is withheld.
|
|
171
|
+
return (
|
|
172
|
+
`deadline checkpoint (agent NOT verifiably stopped): '${params.slug}' hit ` +
|
|
173
|
+
'the dorfl-internal deadline and its WIP was saved + pushed, but the agent ' +
|
|
174
|
+
'process tree could not be confirmed dead, so the lock was NOT released and ' +
|
|
175
|
+
'no successor was dispatched — a second agent in the same working tree can ' +
|
|
176
|
+
'clobber or silently absorb the first one\u2019s edits. ' +
|
|
177
|
+
`${params.detail ?? ''}`.trim()
|
|
178
|
+
);
|
|
179
|
+
}
|
|
162
180
|
if (params.kind === 'no-progress') {
|
|
163
181
|
return (
|
|
164
182
|
`deadline checkpoint (no progress / ceiling): '${params.slug}' hit ` +
|
|
@@ -329,6 +347,16 @@ export type DoDorfl = (input: {
|
|
|
329
347
|
* injected path — the real deadline race lives in `PiHarness.launchAsync`.
|
|
330
348
|
*/
|
|
331
349
|
timedOut?: boolean;
|
|
350
|
+
/**
|
|
351
|
+
* **The simulated process-tree REAP verdict** for a {@link timedOut} stop
|
|
352
|
+
* (observation `checkpoint-releases-lock-while-predecessor-agent-still-writes`).
|
|
353
|
+
* Test-only signal, the sibling of `timedOut`, so an injected agent can drive
|
|
354
|
+
* the "predecessor could not be verified dead" routing — where the checkpoint
|
|
355
|
+
* must NOT release the item lock, because the next tick would then onboard a
|
|
356
|
+
* successor into a working tree the predecessor may still be writing to.
|
|
357
|
+
* Absent ⇒ nothing was left running (the pre-existing behaviour).
|
|
358
|
+
*/
|
|
359
|
+
reap?: AgentTreeReap;
|
|
332
360
|
};
|
|
333
361
|
|
|
334
362
|
export interface DoOptions {
|
|
@@ -549,7 +577,9 @@ export interface DoOptions {
|
|
|
549
577
|
watch?: boolean;
|
|
550
578
|
/** The configured agent command the harness shells out to (null adapter). */
|
|
551
579
|
agentCmd?: string;
|
|
552
|
-
/** The model routing intent forwarded to the harness (ADR §13).
|
|
580
|
+
/** The model routing intent forwarded to the harness (ADR §13). Sourced
|
|
581
|
+
* from `config.buildModel` (which inherits the general `config.model` when
|
|
582
|
+
* unset via `applyModelFallbacks`). */
|
|
553
583
|
model?: string;
|
|
554
584
|
/**
|
|
555
585
|
* The HOST-ONLY root folder under which this run's pi session FILE is generated
|
|
@@ -1304,6 +1334,7 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
|
|
|
1304
1334
|
detail?: string;
|
|
1305
1335
|
output?: string;
|
|
1306
1336
|
timedOut?: boolean;
|
|
1337
|
+
reap?: AgentTreeReap;
|
|
1307
1338
|
};
|
|
1308
1339
|
try {
|
|
1309
1340
|
agent = await runDoAgent(options, tree.dir, prompt, slug);
|
|
@@ -1332,6 +1363,7 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
|
|
|
1332
1363
|
cwd: tree.dir,
|
|
1333
1364
|
arbiter: tree.arbiterRemote,
|
|
1334
1365
|
maxAutoCheckpoints: options.maxAutoCheckpoints ?? 5,
|
|
1366
|
+
reap: agent.reap,
|
|
1335
1367
|
env,
|
|
1336
1368
|
note,
|
|
1337
1369
|
});
|
|
@@ -1950,11 +1982,58 @@ async function runDoAgent(
|
|
|
1950
1982
|
detail?: string;
|
|
1951
1983
|
output?: string;
|
|
1952
1984
|
timedOut?: boolean;
|
|
1985
|
+
reap?: AgentTreeReap;
|
|
1953
1986
|
}> {
|
|
1954
1987
|
if (options.dorfl) {
|
|
1955
1988
|
return options.dorfl({cwd, prompt, slug, env: options.env});
|
|
1956
1989
|
}
|
|
1957
1990
|
const harness = options.harness ?? new NullHarness();
|
|
1991
|
+
|
|
1992
|
+
// WORKING-TREE SENTINEL (observation
|
|
1993
|
+
// `checkpoint-releases-lock-while-predecessor-agent-still-writes`): the item
|
|
1994
|
+
// lock guards the ITEM; this guards the TREE. A deadline checkpoint releases the
|
|
1995
|
+
// item lock so the next tick can continue the task in the SAME worktree, so the
|
|
1996
|
+
// item lock alone cannot keep a successor out while a predecessor is still
|
|
1997
|
+
// alive. Refuse to launch a second agent into a tree that already has a LIVE
|
|
1998
|
+
// writer, independently of the reap. A dead holder's sentinel is stale and taken
|
|
1999
|
+
// over, so a crashed runner never poisons the worktree.
|
|
2000
|
+
const writer = acquireWorktreeWriterLock({dir: cwd, slug, env: options.env});
|
|
2001
|
+
if (!writer.acquired) {
|
|
2002
|
+
return {ok: false, detail: writer.reason};
|
|
2003
|
+
}
|
|
2004
|
+
try {
|
|
2005
|
+
return await launchAgentUnderWriterLock({
|
|
2006
|
+
options,
|
|
2007
|
+
cwd,
|
|
2008
|
+
prompt,
|
|
2009
|
+
slug,
|
|
2010
|
+
harness,
|
|
2011
|
+
});
|
|
2012
|
+
} finally {
|
|
2013
|
+
writer.release();
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* The actual harness launch, run while this process holds the worktree writer
|
|
2019
|
+
* sentinel (see {@link runDoAgent}). Split out purely so the sentinel's
|
|
2020
|
+
* acquire/release brackets the launch in a `try/finally` without indenting the
|
|
2021
|
+
* whole body.
|
|
2022
|
+
*/
|
|
2023
|
+
async function launchAgentUnderWriterLock(params: {
|
|
2024
|
+
options: DoAgentLaunchOptions;
|
|
2025
|
+
cwd: string;
|
|
2026
|
+
prompt: string;
|
|
2027
|
+
slug: string;
|
|
2028
|
+
harness: Harness;
|
|
2029
|
+
}): Promise<{
|
|
2030
|
+
ok: boolean;
|
|
2031
|
+
detail?: string;
|
|
2032
|
+
output?: string;
|
|
2033
|
+
timedOut?: boolean;
|
|
2034
|
+
reap?: AgentTreeReap;
|
|
2035
|
+
}> {
|
|
2036
|
+
const {options, cwd, prompt, slug, harness} = params;
|
|
1958
2037
|
// Convert the dorfl-internal deadline (minutes) into a wall-clock epoch-ms so
|
|
1959
2038
|
// the harness (`launchAsync`) can race the child against it (spec
|
|
1960
2039
|
// `graceful-pre-timeout-wip-checkpoint`). Absent ⇒ no deadline; a run that
|
|
@@ -1989,6 +2068,10 @@ async function runDoAgent(
|
|
|
1989
2068
|
detail: launched.detail,
|
|
1990
2069
|
output: launched.output,
|
|
1991
2070
|
timedOut: launched.timedOut,
|
|
2071
|
+
// The harness's PROOF that a deadline-stopped agent's process tree is gone.
|
|
2072
|
+
// Threaded to {@link routeDeadlineCheckpoint}, which refuses to release the
|
|
2073
|
+
// item lock without it.
|
|
2074
|
+
reap: launched.reap,
|
|
1992
2075
|
};
|
|
1993
2076
|
}
|
|
1994
2077
|
|
|
@@ -2006,10 +2089,39 @@ async function routeDeadlineCheckpoint(params: {
|
|
|
2006
2089
|
cwd: string;
|
|
2007
2090
|
arbiter: string;
|
|
2008
2091
|
maxAutoCheckpoints: number;
|
|
2092
|
+
/**
|
|
2093
|
+
* The harness's VERIFIED reap of the checkpointed agent's process tree. The
|
|
2094
|
+
* auto-continue branch releases the lock and lets the next tick dispatch a
|
|
2095
|
+
* SUCCESSOR into this same worktree, so it may only run once the predecessor
|
|
2096
|
+
* is proven gone (observation
|
|
2097
|
+
* `checkpoint-releases-lock-while-predecessor-agent-still-writes`). Absent ⮕
|
|
2098
|
+
* the harness spawned no killable group (test doubles, the null adapter), which
|
|
2099
|
+
* is treated as "nothing was left running".
|
|
2100
|
+
*/
|
|
2101
|
+
reap?: AgentTreeReap;
|
|
2009
2102
|
env: NodeJS.ProcessEnv | undefined;
|
|
2010
2103
|
note: (message: string) => void;
|
|
2011
2104
|
}): Promise<DoResult> {
|
|
2012
|
-
const {slug, branch, cwd, arbiter, maxAutoCheckpoints, env, note} =
|
|
2105
|
+
const {slug, branch, cwd, arbiter, maxAutoCheckpoints, reap, env, note} =
|
|
2106
|
+
params;
|
|
2107
|
+
|
|
2108
|
+
// 0. THE PREDECESSOR MUST BE DEAD BEFORE THE LOCK CAN MOVE.
|
|
2109
|
+
//
|
|
2110
|
+
// The item lock guards the ITEM; nothing guards the WORKING TREE. So if we
|
|
2111
|
+
// released the lock while the checkpointed agent's tree were still alive, the
|
|
2112
|
+
// next tick would onboard a successor into the very worktree the predecessor is
|
|
2113
|
+
// still writing to — one lock, one working tree, two live writers. That was
|
|
2114
|
+
// observed in the field: a predecessor kept writing for four minutes into its
|
|
2115
|
+
// successor's run, and its last write landed on a file the successor had already
|
|
2116
|
+
// read as clean in its opening `git status`.
|
|
2117
|
+
//
|
|
2118
|
+
// A reap we could not VERIFY is therefore a hard stop for the auto-continue
|
|
2119
|
+
// path. We still SAVE the work below (never lose work) and still surface the
|
|
2120
|
+
// item, but we do not hand the tree to anybody else.
|
|
2121
|
+
const predecessorGone = reap === undefined || reap.reaped;
|
|
2122
|
+
if (reap !== undefined && reap.escalatedToSigkill && reap.reaped) {
|
|
2123
|
+
note(`Deadline checkpoint for '${slug}': ${reap.detail}`);
|
|
2124
|
+
}
|
|
2013
2125
|
|
|
2014
2126
|
// 1. ALWAYS save the WIP first: commit any residue + push the work branch.
|
|
2015
2127
|
const savedReason = `deadline checkpoint save for '${slug}'`;
|
|
@@ -2038,7 +2150,11 @@ async function routeDeadlineCheckpoint(params: {
|
|
|
2038
2150
|
env,
|
|
2039
2151
|
});
|
|
2040
2152
|
|
|
2041
|
-
if (
|
|
2153
|
+
if (
|
|
2154
|
+
predecessorGone &&
|
|
2155
|
+
madeProgressThisSession &&
|
|
2156
|
+
checkpointCount <= maxAutoCheckpoints
|
|
2157
|
+
) {
|
|
2042
2158
|
// AUTO-CONTINUE: release the lock via the SAME default keep+continue path
|
|
2043
2159
|
// `requeue` uses (no --reset, no --reconcile, no sidecar). The branch is
|
|
2044
2160
|
// KEPT on the arbiter so the next claim continues from its tip.
|
|
@@ -2055,10 +2171,27 @@ async function routeDeadlineCheckpoint(params: {
|
|
|
2055
2171
|
note,
|
|
2056
2172
|
});
|
|
2057
2173
|
if (returned.moved) {
|
|
2174
|
+
// Derive this line from the SINGLE state `returnToBacklog` resolved, never
|
|
2175
|
+
// from an independent assumption. It used to assert "the next tick continues
|
|
2176
|
+
// from <branch>" unconditionally, which contradicted the requeue's own
|
|
2177
|
+
// "'<slug>' has no work branch on <arbiter> — nothing to continue from" line
|
|
2178
|
+
// emitted two lines earlier. Both cannot be true, and acting on the wrong one
|
|
2179
|
+
// re-drives the task from scratch and discards the saved work (observation
|
|
2180
|
+
// `checkpoint-path-reports-its-own-write-as-absent`). One resolved state, one
|
|
2181
|
+
// story: mutually contradictory lines are worse than emitting nothing.
|
|
2182
|
+
const continueFrom = returned.continueBranch;
|
|
2183
|
+
const continuation =
|
|
2184
|
+
continueFrom === undefined || continueFrom.aheadOfMain
|
|
2185
|
+
? `lock released so the next tick continues from ${continueFrom?.branch ?? branch}`
|
|
2186
|
+
: continueFrom.trustworthy
|
|
2187
|
+
? 'lock released; the arbiter has no work to continue from, so the next ' +
|
|
2188
|
+
'tick starts this item fresh'
|
|
2189
|
+
: `lock released; the arbiter could not be read to confirm ${continueFrom.branch}, ` +
|
|
2190
|
+
'so do NOT assume the work is gone — check the branch before re-driving';
|
|
2058
2191
|
const message =
|
|
2059
2192
|
`Auto-continued '${slug}' at the dorfl-internal deadline (checkpoint ` +
|
|
2060
2193
|
`${checkpointCount}/${maxAutoCheckpoints}): WIP saved + branch pushed, ` +
|
|
2061
|
-
|
|
2194
|
+
`${continuation}. ${reason}`;
|
|
2062
2195
|
note(message);
|
|
2063
2196
|
return {
|
|
2064
2197
|
exitCode: 0,
|
|
@@ -2078,14 +2211,16 @@ async function routeDeadlineCheckpoint(params: {
|
|
|
2078
2211
|
// SURFACE: mark the lock stuck via the whole applyNeedsAttentionTransition
|
|
2079
2212
|
// (save + push + stuck). The WIP was already saved above; a second save is
|
|
2080
2213
|
// idempotent (empty commit is skipped inside routeToNeedsAttention).
|
|
2081
|
-
const surfaceReason =
|
|
2082
|
-
? deadlineSurfaceReason({
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2214
|
+
const surfaceReason = !predecessorGone
|
|
2215
|
+
? deadlineSurfaceReason({slug, kind: 'unreaped', detail: reap?.detail})
|
|
2216
|
+
: madeProgressThisSession
|
|
2217
|
+
? deadlineSurfaceReason({
|
|
2218
|
+
slug,
|
|
2219
|
+
kind: 'ceiling',
|
|
2220
|
+
count: checkpointCount,
|
|
2221
|
+
max: maxAutoCheckpoints,
|
|
2222
|
+
})
|
|
2223
|
+
: deadlineSurfaceReason({slug, kind: 'no-progress'});
|
|
2089
2224
|
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
2090
2225
|
cwd,
|
|
2091
2226
|
slug,
|
|
@@ -2095,12 +2230,14 @@ async function routeDeadlineCheckpoint(params: {
|
|
|
2095
2230
|
note,
|
|
2096
2231
|
});
|
|
2097
2232
|
const report = routed.moved ? routeReport(routed, branch) : undefined;
|
|
2233
|
+
const why = !predecessorGone
|
|
2234
|
+
? 'the checkpointed agent could not be verified dead'
|
|
2235
|
+
: madeProgressThisSession
|
|
2236
|
+
? `ceiling ${checkpointCount}/${maxAutoCheckpoints}`
|
|
2237
|
+
: 'no progress this session';
|
|
2098
2238
|
const message = routed.moved
|
|
2099
|
-
? `Surfaced '${slug}' at the deadline checkpoint (${
|
|
2100
|
-
|
|
2101
|
-
? `ceiling ${checkpointCount}/${maxAutoCheckpoints}`
|
|
2102
|
-
: 'no progress this session'
|
|
2103
|
-
}); ${report!.fragment}. ${surfaceReason}`
|
|
2239
|
+
? `Surfaced '${slug}' at the deadline checkpoint (${why}); ` +
|
|
2240
|
+
`${report!.fragment}. ${surfaceReason}`
|
|
2104
2241
|
: `Could not surface '${slug}' at the deadline checkpoint ` +
|
|
2105
2242
|
`(${routed.reasonNotMoved ?? 'unknown'}). ${surfaceReason}`;
|
|
2106
2243
|
note(message);
|
|
@@ -2744,6 +2881,7 @@ async function runRemotePipeline(
|
|
|
2744
2881
|
detail?: string;
|
|
2745
2882
|
output?: string;
|
|
2746
2883
|
timedOut?: boolean;
|
|
2884
|
+
reap?: AgentTreeReap;
|
|
2747
2885
|
};
|
|
2748
2886
|
try {
|
|
2749
2887
|
agent = await runDoAgent(options, cwd, prompt, slug);
|
|
@@ -2768,6 +2906,7 @@ async function runRemotePipeline(
|
|
|
2768
2906
|
cwd,
|
|
2769
2907
|
arbiter: arbiterRemote,
|
|
2770
2908
|
maxAutoCheckpoints: options.maxAutoCheckpoints ?? 5,
|
|
2909
|
+
reap: agent.reap,
|
|
2771
2910
|
env,
|
|
2772
2911
|
note,
|
|
2773
2912
|
});
|
package/src/env-config.ts
CHANGED
|
@@ -152,6 +152,9 @@ const KEY_COERCIONS: {[K in keyof Config]?: Coercion} = {
|
|
|
152
152
|
// A non-string / empty value is normalised at load (`validateDorflCmdConfig`).
|
|
153
153
|
dorflCmd: 'string',
|
|
154
154
|
model: 'string',
|
|
155
|
+
buildModel: 'string',
|
|
156
|
+
triageModel: 'string',
|
|
157
|
+
intakeModel: 'string',
|
|
155
158
|
harness: {enum: ['null', 'pi']},
|
|
156
159
|
piBin: 'string',
|
|
157
160
|
sessionsDir: 'string',
|
package/src/harness.ts
CHANGED
|
@@ -210,6 +210,36 @@ export interface LaunchResult {
|
|
|
210
210
|
* part from its `--format json` stream — the SAME `output` field.
|
|
211
211
|
*/
|
|
212
212
|
output?: string;
|
|
213
|
+
/**
|
|
214
|
+
* **The verified outcome of reaping the agent's process TREE** after a deadline
|
|
215
|
+
* stop (observation
|
|
216
|
+
* `checkpoint-releases-lock-while-predecessor-agent-still-writes`).
|
|
217
|
+
*
|
|
218
|
+
* Only present when {@link timedOut} is set — i.e. when the harness itself
|
|
219
|
+
* signalled the agent and therefore owes the caller PROOF that it is gone
|
|
220
|
+
* rather than merely signalled. `reaped: false` means a descendant may still be
|
|
221
|
+
* writing to the worktree, so the caller MUST NOT release the item lock or let
|
|
222
|
+
* a successor agent onboard there (see `do.ts`'s deadline routing).
|
|
223
|
+
*
|
|
224
|
+
* Absent on a normal exit (nothing was signalled, so there is nothing to prove)
|
|
225
|
+
* and on adapters that do not spawn a killable process group.
|
|
226
|
+
*/
|
|
227
|
+
reap?: AgentTreeReap;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The harness-reported result of reaping a stopped agent's process tree — the
|
|
232
|
+
* adapter-agnostic projection of `reap-agent-tree.ts`'s `ReapResult`.
|
|
233
|
+
*/
|
|
234
|
+
export interface AgentTreeReap {
|
|
235
|
+
/** True iff the tree is VERIFIED gone (observed, not merely signalled). */
|
|
236
|
+
reaped: boolean;
|
|
237
|
+
/** The process group that was reaped (the agent's group-leader pid). */
|
|
238
|
+
pgid?: number;
|
|
239
|
+
/** True iff SIGKILL was needed because the tree ignored SIGTERM. */
|
|
240
|
+
escalatedToSigkill?: boolean;
|
|
241
|
+
/** Human-readable account — the LOUD text when `reaped` is false. */
|
|
242
|
+
detail: string;
|
|
213
243
|
}
|
|
214
244
|
|
|
215
245
|
/**
|
package/src/ledger-write.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {randomUUID} from 'node:crypto';
|
|
2
2
|
import {runAsync, type RunResult} from './git.js';
|
|
3
|
+
import {refreshArbiterRefs, resolveArbiterBranch} from './arbiter-refs.js';
|
|
3
4
|
import {
|
|
4
5
|
Integrator,
|
|
5
6
|
type IntegrateResult,
|
|
@@ -560,17 +561,52 @@ export const currentLedgerWrite: LedgerWriteStrategy = {
|
|
|
560
561
|
// making" no-op, which is a LOSS. The nonce makes the two naturally
|
|
561
562
|
// distinguishable: `arbiterHead === nonced` iff WE won. So an up-to-date
|
|
562
563
|
// no-op can never satisfy this and is classified REJECTED, never published.
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
564
|
+
//
|
|
565
|
+
// The read MUST be ARBITER-AUTHORITATIVE, and used to not be: it was a
|
|
566
|
+
// plain `git fetch <arbiter>` + `rev-parse <arbiter>/main`. In the
|
|
567
|
+
// bare-hub-mirror job worktree `--isolated` runs in, that fetch does not
|
|
568
|
+
// populate `refs/remotes/<arbiter>/main` at all (the mirror refspec maps
|
|
569
|
+
// `+refs/heads/*:refs/heads/*`) and can even fail outright, so the verify
|
|
570
|
+
// compared our fresh sha against a view PREDATING our own push and declared
|
|
571
|
+
// a landed transition "not our commit ⇒ rejected" — five times per bounce,
|
|
572
|
+
// landing five identical commits and then reporting "did not land"
|
|
573
|
+
// (observation `checkpoint-path-reports-its-own-write-as-absent`). We now
|
|
574
|
+
// prune-fetch with the EXPLICIT refspec (so the objects/refs are local for
|
|
575
|
+
// any follow-up comparison) and then ask the ARBITER for the sha via
|
|
576
|
+
// `ls-remote`, which no local refspec accident can defeat.
|
|
577
|
+
await refreshArbiterRefs({cwd, arbiter, branches: ['main'], env});
|
|
578
|
+
const resolved = await resolveArbiterBranch({
|
|
579
|
+
cwd,
|
|
580
|
+
arbiter,
|
|
581
|
+
branch: 'main',
|
|
582
|
+
env,
|
|
583
|
+
});
|
|
584
|
+
if (resolved.sha === nonced) {
|
|
568
585
|
return {
|
|
569
586
|
kind: 'published',
|
|
570
587
|
message: 'transition published',
|
|
571
588
|
publishedHead: nonced,
|
|
572
589
|
};
|
|
573
590
|
}
|
|
591
|
+
if (!resolved.trustworthy) {
|
|
592
|
+
// The arbiter could not be reached, so we CANNOT tell a lost CAS from an
|
|
593
|
+
// unreadable view — and our push exited 0, which is evidence FOR landing.
|
|
594
|
+
// Reporting "rejected" here is precisely the defect (a successful write
|
|
595
|
+
// described as absent), and it also drives a retry that would duplicate
|
|
596
|
+
// the commit. Trust the green push: report published, and say why.
|
|
597
|
+
emit(
|
|
598
|
+
`push to ${arbiter}/main succeeded but the arbiter could not be re-read ` +
|
|
599
|
+
`to confirm it (${resolved.unreachableDetail ?? 'arbiter unreachable'}) ` +
|
|
600
|
+
'— trusting the successful push rather than reporting a landed write as ' +
|
|
601
|
+
'absent.',
|
|
602
|
+
);
|
|
603
|
+
return {
|
|
604
|
+
kind: 'published',
|
|
605
|
+
message:
|
|
606
|
+
'transition published (push succeeded; arbiter re-read unavailable)',
|
|
607
|
+
publishedHead: nonced,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
574
610
|
emit(
|
|
575
611
|
`push reported up-to-date / no change of our making — ${arbiter}/main is not our commit — treating as rejected.`,
|
|
576
612
|
);
|