tickmarkr 2.6.4 → 2.6.6
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/README.md +7 -3
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +36 -0
- package/dist/adapters/codex-commit-check.js +205 -0
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +12 -6
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +10 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +17 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +363 -106
- package/dist/cli/commands/init.js +10 -3
- package/dist/cli/commands/plan.js +76 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +65 -15
- package/dist/config/fleet-overlay.d.ts +16 -1
- package/dist/config/fleet-overlay.js +230 -28
- package/dist/drivers/orca.d.ts +34 -2
- package/dist/drivers/orca.js +206 -32
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +5 -3
- package/dist/gates/run-gates.js +112 -56
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +1 -1
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/daemon.js +326 -130
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +37 -10
- package/dist/tui/ink/fleet-app.js +65 -21
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
package/dist/run/daemon.js
CHANGED
|
@@ -4,7 +4,7 @@ import { COMMAND_LEASE_TOKEN_ENV, commandLeaseEnvironment, CommandLeases, curren
|
|
|
4
4
|
import { execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { shq } from "../adapters/types.js";
|
|
7
|
-
import { appendFileSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { appendFileSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
@@ -23,6 +23,7 @@ import { formatOwnedName } from "../drivers/types.js";
|
|
|
23
23
|
import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
|
|
24
24
|
import { reobserveTestFiles, runGates } from "../gates/run-gates.js";
|
|
25
25
|
import { isInfraResult } from "../gates/cache.js";
|
|
26
|
+
import { RUNNER_INFRA_DIAGNOSTIC_RE, timeoutShaped } from "../gates/timeout-shaped.js";
|
|
26
27
|
import { filesGlob } from "../graph/files-glob.js";
|
|
27
28
|
import { addEvidence, attributeBlocked, batteryPriority, blockedTasks, getTask, graphDefinitionHash, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
|
|
28
29
|
import { GATE_NAMES } from "../graph/schema.js";
|
|
@@ -32,11 +33,12 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
|
|
|
32
33
|
import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
|
|
33
34
|
import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
34
35
|
import { runEnvironment } from "./environment.js";
|
|
35
|
-
import { changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
36
|
+
import { changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
37
|
+
import { MASK } from "./redact.js";
|
|
36
38
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
37
39
|
import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
|
|
38
40
|
import { applyScopeAmendments, activeRetryBan, interruptedAttempt, RESUME_HARVEST_SOURCE, resumeHarvestAuthor, approvalAction, APPROVAL_REFUSED, bindingToken, effectiveEvents, foldDecisions, physicalLine, staleApprovals, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, readableExcerpt, receiptOrigin, outstandingReviewFindings, pendingApprovalActions, pendingRechecks, pendingRepairFindings, phaseForGate, priorJudgments, readPriorRunEvidence, recordedTaskFailureKind, reraisedReviewChains, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, standingRulings, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
39
|
-
import { gateReviewerFloor, isDiffCapPark, isGarbageReview, pickReviewer } from "../gates/review.js";
|
|
41
|
+
import { carriedAuthorVendors, gateReviewerFloor, isDiffCapPark, isGarbageReview, pickReviewer } from "../gates/review.js";
|
|
40
42
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
41
43
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, reusedTipEvidence, verifyIntegrationTip } from "./merge.js";
|
|
42
44
|
import { channelExclusion, climbChannel, inTaskPool, marginalCostRank, nextChannel, poolExhaustion, route, taskPool } from "../route/router.js";
|
|
@@ -374,9 +376,49 @@ function classifyInfraResult(g) {
|
|
|
374
376
|
// diagnostics (never-started files, a worker RPC timeout) that make such a red infrastructure-SHAPED.
|
|
375
377
|
// Shape alone never parks anything: it only buys one isolated re-observation before a charge. A
|
|
376
378
|
// fresh-fingerprint section masks digits to `#`, so both spellings of a number are read.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
379
|
+
/** C (E1): the one no-verdict that is the host's slowness, not the seat's — the same predicate run-gates
|
|
380
|
+
* applies to its run-scoped tally, read here for the live task strike and the resume reconstruction. */
|
|
381
|
+
const proofTimeoutNote = (data) => data.cause === "seat-launch-failed" && data.launchCause === "checkout-proof-timeout";
|
|
382
|
+
/** C: the gate-fresh-forced reason that bounds the out-of-scope re-execution, one per journaled subject. */
|
|
383
|
+
const OUTSIDE_SCOPE_RED = "no-commit-out-of-scope-red";
|
|
384
|
+
const NAMED_FAILURE_FILE_RE = /^\s*(\S+\.(?:test|spec)\.[cm]?[jt]sx?)\s+>/gm;
|
|
385
|
+
/** D-854: the runner certificate the vitest manifest path appends to a red (test-manifest.ts). */
|
|
386
|
+
const RUNNER_CERTIFICATE_RE = /runner-level diagnostic: never-started ([^;\s]+); reporter errors ([^;\s]+)/g;
|
|
387
|
+
/** C: a journaled test red whose failing-file attribution is nonempty, trustworthy and complete (repair
|
|
388
|
+
* selection's own rule: a behavioral red on a real subject from a full suite or a screen that names
|
|
389
|
+
* every failure), POSITIVELY certified complete by its runner, uncontradicted by the files its own text
|
|
390
|
+
* names, and wholly outside a declared files[]. Empty files[] is unrestricted and never qualifies;
|
|
391
|
+
* every other shape keeps its replay. */
|
|
392
|
+
function outsideScopeRed(t, taskId, data, commit) {
|
|
393
|
+
if (!t.files.length || data.gate !== "test" || data.pass !== false || data.skipped === true || data.commit !== commit
|
|
394
|
+
// a failed producer — an infra row, a dirty-tree refusal, a recovery the runner could not finish —
|
|
395
|
+
// never attributes anything, whatever files it lists
|
|
396
|
+
|| data.infra === true || data.dirtyWorktree === true || data.recoveryBlocked !== undefined)
|
|
397
|
+
return false;
|
|
398
|
+
const attribution = repairSelectionDecision([{ event: "gate-result", taskId, data }], taskId, true);
|
|
399
|
+
if (attribution.reason !== "known-failing-files" || attribution.requiredFiles.length === 0)
|
|
400
|
+
return false;
|
|
401
|
+
const text = readableExcerpt(String(data.details ?? ""));
|
|
402
|
+
// D-854: validate, never filter. A collection failure has no module record, so failingFiles omits it
|
|
403
|
+
// and it lives only in the certificate's counts; any never-started file or reporter error, an unknown
|
|
404
|
+
// count, or no certificate at all leaves the attribution unproven.
|
|
405
|
+
// ponytail: a nonzero count never qualifies, even when each diagnostic names an outside file — the
|
|
406
|
+
// details interleave diagnostics with the stdout tail; map them only if outside-scope collection reds need it.
|
|
407
|
+
const certificates = [...text.matchAll(RUNNER_CERTIFICATE_RE)];
|
|
408
|
+
if (!certificates.length || certificates.some((m) => m[1] !== "0" || m[2] !== "0"))
|
|
409
|
+
return false;
|
|
410
|
+
const named = [...text.matchAll(ATTRIBUTED_FILE_RE), ...text.matchAll(NAMED_FAILURE_FILE_RE)].map((m) => m[1].replace(/^\.\//, ""));
|
|
411
|
+
if (named.some((file) => !attribution.requiredFiles.includes(file)))
|
|
412
|
+
return false; // contradictory
|
|
413
|
+
const inScope = filesGlob(t.files);
|
|
414
|
+
return attribution.requiredFiles.every((file) => !inScope(file));
|
|
415
|
+
}
|
|
416
|
+
/** C: every test red a round journaled on `commit` is an out-of-scope red (none means none). */
|
|
417
|
+
const allOutsideScopeReds = (t, rows, commit) => {
|
|
418
|
+
const reds = rows.filter((e) => e.event === "gate-result" && e.data.gate === "test" && e.data.pass === false
|
|
419
|
+
&& e.data.skipped !== true && e.data.commit === commit);
|
|
420
|
+
return reds.length > 0 && reds.every((e) => outsideScopeRed(t, e.taskId ?? t.id, e.data, commit));
|
|
421
|
+
};
|
|
380
422
|
const ATTRIBUTED_FILE_RE = /^\s*(?:FAIL|×|✗)\s+(?:\|[^|\n]*\|\s+)?(\S+\.(?:test|spec)\.[cm]?[jt]sx?)\b/gm;
|
|
381
423
|
/** The failing files a red attributes to itself: the runner's own list, else its FAIL headlines. */
|
|
382
424
|
const attributedFailingFiles = (g) => [...new Set(Array.isArray(g.meta?.failingFiles) && g.meta.failingFiles.length
|
|
@@ -2203,10 +2245,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2203
2245
|
// is what bounds a seat that flakes once per task (or once per session) across the run. Seeded from the
|
|
2204
2246
|
// journal's review-no-verdict rows — the same notes noteReviewEvent appends — so a resumed daemon keeps
|
|
2205
2247
|
// the count; run-gates appends to it in place, exactly as it does demotedReviewers.
|
|
2248
|
+
// C (E1): a checkout-proof timeout never struck the live tally (run-gates), so a resume never seeds one.
|
|
2206
2249
|
const reviewNoVerdicts = new Map();
|
|
2207
2250
|
for (const event of journal.read()) {
|
|
2208
2251
|
if (event.event !== "review-no-verdict" || event.data.noVerdict !== true || typeof event.data.reviewer !== "string")
|
|
2209
2252
|
continue;
|
|
2253
|
+
if (proofTimeoutNote(event.data))
|
|
2254
|
+
continue;
|
|
2210
2255
|
reviewNoVerdicts.set(event.data.reviewer, [...(reviewNoVerdicts.get(event.data.reviewer) ?? []), String(event.data.cause)]);
|
|
2211
2256
|
}
|
|
2212
2257
|
const reviewHistory = journal.read()
|
|
@@ -2644,7 +2689,29 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2644
2689
|
to: loadedHash,
|
|
2645
2690
|
});
|
|
2646
2691
|
}
|
|
2647
|
-
|
|
2692
|
+
// B (D-812): a daemon interrupted inside its capture leaves run-start without baseline.json. Only
|
|
2693
|
+
// that absence recaptures; existing bytes that do not parse fail closed and are never written over.
|
|
2694
|
+
const baselinePath = join(journal.dir, "baseline.json");
|
|
2695
|
+
let recapture = false;
|
|
2696
|
+
try {
|
|
2697
|
+
baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
|
|
2698
|
+
}
|
|
2699
|
+
catch (error) {
|
|
2700
|
+
if (error.code !== "ENOENT") {
|
|
2701
|
+
throw new Error(`refusing to resume ${runId}: existing baseline.json is unreadable (${error instanceof Error ? error.message : String(error)}) — it is never recaptured over`);
|
|
2702
|
+
}
|
|
2703
|
+
// The recapture measures run-start's commands; current config is no authority for that base.
|
|
2704
|
+
// Only unmasked bytes prove equality: redaction maps a changed credential onto the same mask, so a
|
|
2705
|
+
// journaled command carrying the mask cannot be proven unchanged and is refused like a change.
|
|
2706
|
+
const recorded = (typeof start.data.commands === "object" && start.data.commands !== null ? start.data.commands : {});
|
|
2707
|
+
const effective = commands;
|
|
2708
|
+
const changed = [...new Set([...Object.keys(recorded), ...Object.keys(effective)])]
|
|
2709
|
+
.filter((name) => recorded[name] !== effective[name] || String(recorded[name]).includes(MASK)).sort();
|
|
2710
|
+
if (changed.length > 0) {
|
|
2711
|
+
throw new Error(`refusing to resume ${runId}: baseline recapture refused — effective commands differ from (or are masked in) run-start's journaled commands: ${changed.join(", ")}`);
|
|
2712
|
+
}
|
|
2713
|
+
recapture = true;
|
|
2714
|
+
}
|
|
2648
2715
|
const replayEvents = journal.read();
|
|
2649
2716
|
// journal.replayStatuses predates typed releases and re-pends even inert rows. Keep that
|
|
2650
2717
|
// legacy reader intact; scheduling accepts only the fold's recognised authorities.
|
|
@@ -2693,8 +2760,47 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2693
2760
|
});
|
|
2694
2761
|
runStarted = true;
|
|
2695
2762
|
await placeBoard();
|
|
2696
|
-
|
|
2697
|
-
|
|
2763
|
+
if (recapture) {
|
|
2764
|
+
// Moved HEAD is never baseline authority: measure the journaled base in a detached checkout this
|
|
2765
|
+
// daemon owns, and publish by rename so any interruption leaves no baseline.json or a whole one.
|
|
2766
|
+
journal.append("baseline-recapture-start", undefined, { baseRef, commands, capacity: resolvedCapacity() });
|
|
2767
|
+
baselinePending = true;
|
|
2768
|
+
baselineCapture = withCommandContext(undefined, async () => {
|
|
2769
|
+
const parent = join(tickmarkrDir(repoRoot), WORKTREES_DIR);
|
|
2770
|
+
mkdirSync(parent, { recursive: true });
|
|
2771
|
+
// A killed daemon never reaches the finally below; sweep checkouts whose owner is dead.
|
|
2772
|
+
for (const entry of readdirSync(parent)) {
|
|
2773
|
+
const owner = /^baseline-recapture-(\d+)-\w{6}$/.exec(entry)?.[1];
|
|
2774
|
+
if (owner && !isPidLive(Number(owner)))
|
|
2775
|
+
await removeWorktree(repoRoot, join(parent, entry));
|
|
2776
|
+
}
|
|
2777
|
+
const pristine = mkdtempSync(join(parent, `baseline-recapture-${process.pid}-`));
|
|
2778
|
+
const staged = `${baselinePath}.${process.pid}.tmp`;
|
|
2779
|
+
try {
|
|
2780
|
+
const added = await shGit(`git worktree add --detach ${shq(pristine)} ${shq(baseRef)}`, repoRoot);
|
|
2781
|
+
if (added.code !== 0)
|
|
2782
|
+
throw new Error(`baseline recapture: journaled base ${baseRef} cannot be checked out: ${added.stderr.trim()}`);
|
|
2783
|
+
if (!linkNodeModules(repoRoot, pristine, { force: true }))
|
|
2784
|
+
throw new Error("baseline recapture: pristine node_modules link could not be provisioned");
|
|
2785
|
+
await initializeHost();
|
|
2786
|
+
const captured = { ...await captureBaseline(pristine, commands),
|
|
2787
|
+
provenance: { baseRef, capturedAt: new Date().toISOString() } };
|
|
2788
|
+
writeFileSync(staged, JSON.stringify(captured, null, 2));
|
|
2789
|
+
renameSync(staged, baselinePath);
|
|
2790
|
+
baseline = captured;
|
|
2791
|
+
journal.append("baseline-recapture-complete", undefined, { provenance: captured.provenance });
|
|
2792
|
+
for (const warning of captured.warnings ?? [])
|
|
2793
|
+
journal.append("baseline-warning", undefined, { ...warning });
|
|
2794
|
+
}
|
|
2795
|
+
finally {
|
|
2796
|
+
rmSync(staged, { force: true });
|
|
2797
|
+
await removeWorktree(repoRoot, pristine);
|
|
2798
|
+
}
|
|
2799
|
+
}).finally(() => { baselinePending = false; });
|
|
2800
|
+
void baselineCapture.catch(() => { baselineFailed = true; });
|
|
2801
|
+
}
|
|
2802
|
+
else if (Object.keys(commands).length > 0 || graph.tasks.some(t => ["pending", "running", "gated"].includes(t.status))) {
|
|
2803
|
+
// A terminal resume with no commands has no execution to admit.
|
|
2698
2804
|
baselineCapture = initializeHost();
|
|
2699
2805
|
void baselineCapture.catch(() => { baselineFailed = true; });
|
|
2700
2806
|
}
|
|
@@ -2930,6 +3036,18 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2930
3036
|
return red !== undefined
|
|
2931
3037
|
&& identicalGateFailures(journal.read(), t.id, "test", normalizeGateFailure(String(red.data.details))) < GATE_FINGERPRINT_CAP;
|
|
2932
3038
|
};
|
|
3039
|
+
// C: an unchanged cached test red attributed wholly outside files[] is re-executed ONCE per journaled
|
|
3040
|
+
// subject — the worker cannot repair those files, so a copy of the red would only buy another round.
|
|
3041
|
+
const owesOutsideScopeRefresh = (t, priorRows, commit) => allOutsideScopeReds(t, priorRows, commit)
|
|
3042
|
+
&& !journal.read().some((e) => e.taskId === t.id && e.event === "gate-fresh-forced"
|
|
3043
|
+
&& e.data.reason === OUTSIDE_SCOPE_RED && e.data.commit === commit);
|
|
3044
|
+
// C: the forced re-execution's own round failed again only outside files[]: independent evidence the
|
|
3045
|
+
// worker cannot act on. It keeps its red row and parks gate-fail before any cap, consult or repair move.
|
|
3046
|
+
const freshOutsideScopeRed = (t, commit) => {
|
|
3047
|
+
const own = journal.read().filter((e) => e.taskId === t.id);
|
|
3048
|
+
return allOutsideScopeReds(t, own.slice(own.map((e) => e.event === "phase-start" && e.data.phase === "gates").lastIndexOf(true) + 1), commit);
|
|
3049
|
+
};
|
|
3050
|
+
const outsideScopePark = (t) => gateFailApprovalReason(t.id, "test: a fresh re-execution of the unchanged subject failed again only in files outside files[] — no repair can reach them");
|
|
2933
3051
|
// OBS-1106 residual: before a timeout or wall-budget red accompanied by runner infra diagnostics is
|
|
2934
3052
|
// charged, its attributed failing files are re-run ONCE, on the same checkout, under the suite
|
|
2935
3053
|
// lease. Only a reproduced assertion is charged. A pass, an infra rerun, a rerun naming none of the
|
|
@@ -3297,11 +3415,15 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3297
3415
|
}
|
|
3298
3416
|
// Keep one live list: recovery retries must see exclusions added by onGate during the round.
|
|
3299
3417
|
const badReviewers = [...replayedReviewerExclusions];
|
|
3418
|
+
// C (E1): a checkout-proof timeout is the host being slow, not the seat failing — it is excluded for
|
|
3419
|
+
// the CURRENT round only (so an always-timeout pool still ends the round), never struck task-wide.
|
|
3420
|
+
let roundTimeouts = [];
|
|
3300
3421
|
const noteReviewEvent = (e) => {
|
|
3301
3422
|
journal.append(e.name, t.id, e.payload);
|
|
3302
3423
|
if (e.name === "review-no-verdict" && typeof e.payload.reviewer === "string") {
|
|
3303
|
-
|
|
3304
|
-
|
|
3424
|
+
const strikes = proofTimeoutNote(e.payload) ? roundTimeouts : badReviewers;
|
|
3425
|
+
if (!strikes.includes(e.payload.reviewer))
|
|
3426
|
+
strikes.push(e.payload.reviewer);
|
|
3305
3427
|
}
|
|
3306
3428
|
// OBS-1196: a seat whose same-subject re-emission delivered a valid verdict was a delivery flake,
|
|
3307
3429
|
// not a bad reviewer — it stays seatable for this task's later rounds.
|
|
@@ -3337,17 +3459,31 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3337
3459
|
gateRound: journal.read().filter((row) => row.taskId === task.id
|
|
3338
3460
|
&& row.event === "phase-start" && row.data.phase === "gates").length,
|
|
3339
3461
|
};
|
|
3340
|
-
|
|
3462
|
+
roundTimeouts = [];
|
|
3463
|
+
// C: every gate-round boundary — a returned or a THROWN round — publishes the one row held behind
|
|
3464
|
+
// a still-pending sibling and forgets the pending set, before any park or task-failed row lands.
|
|
3465
|
+
const gateRound = async (subject) => {
|
|
3466
|
+
try {
|
|
3467
|
+
return await runGates(subject, { ...ctx, excludeReviewers: [...badReviewers, ...roundTimeouts] });
|
|
3468
|
+
}
|
|
3469
|
+
finally {
|
|
3470
|
+
settleParallelRound();
|
|
3471
|
+
}
|
|
3472
|
+
};
|
|
3473
|
+
const round = await gateRound(task);
|
|
3341
3474
|
let review = round.results.find((g) => g.gate === "review");
|
|
3475
|
+
// The recovery pick carries every preserved author, exactly as reviewGate's own pick does.
|
|
3476
|
+
const authors = ctx.carriedAuthors?.length ? [...ctx.carriedAuthors] : [channelKey(ctx.author)];
|
|
3342
3477
|
while (review?.meta?.noVerdict === true) {
|
|
3343
3478
|
// Recovery must honor the gate's floor, including the seats that just failed to
|
|
3344
|
-
// return a verdict; a below-floor alternative cannot replace the infra result.
|
|
3345
|
-
|
|
3346
|
-
const
|
|
3479
|
+
// return a verdict; a below-floor alternative cannot replace the infra result. This round's
|
|
3480
|
+
// proof timeouts count too — runGates folds every excluded seat into its own floor.
|
|
3481
|
+
const { floor } = gateReviewerFloor(task, ctx.cfg, ctx.author, ctx.channels, [...(ctx.priorReviewers ?? []), ...badReviewers, ...roundTimeouts]);
|
|
3482
|
+
const next = pickReviewer(ctx.author, ctx.channels, [...badReviewers, ...roundTimeouts], cfg.review.prefer ?? [], floor, reviewHistory, undefined, demotedReviewers, carriedAuthorVendors(ctx.channels, authors), authors);
|
|
3347
3483
|
if (!next)
|
|
3348
3484
|
break;
|
|
3349
3485
|
journal.append("review-infra-retry", t.id, { reviewer: channelKey(next), cause: review.meta.cause });
|
|
3350
|
-
const retried = await
|
|
3486
|
+
const retried = await gateRound({ ...task, gates: ["review"] });
|
|
3351
3487
|
const replacement = retried.results.find((g) => g.gate === "review");
|
|
3352
3488
|
// A dirty tree or another pre-dispatch refusal must still fail closed.
|
|
3353
3489
|
if (!replacement) {
|
|
@@ -3374,13 +3510,16 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3374
3510
|
gate: "review", flaked: rr.flaked, retried: rr.retried,
|
|
3375
3511
|
...(g.meta?.unparseable === true ? { secondUnparseable: true } : {}),
|
|
3376
3512
|
});
|
|
3377
|
-
|
|
3513
|
+
// C (E1): a seat re-routed after a checkout-proof timeout is excluded for this round only.
|
|
3514
|
+
if (!roundTimeouts.includes(rr.flaked))
|
|
3515
|
+
badReviewers.push(rr.flaked);
|
|
3378
3516
|
}
|
|
3379
3517
|
};
|
|
3380
3518
|
// v1.85 T3 (ruling R4): every BLOCKING review/judge result lands its findings in the journal
|
|
3381
3519
|
// structured — class + canonical path + stable symbol — so a retry, a consult or an auto-uphold
|
|
3382
3520
|
// decision reads identity instead of re-parsing prose, and line-number churn is not a new finding.
|
|
3383
3521
|
// One helper, both onGate sites (satisfied-gate resume + main attempt loop).
|
|
3522
|
+
// `journaledGates` narrows replayMeasurement to the gates an interrupted round already journaled (C).
|
|
3384
3523
|
let gateSubject;
|
|
3385
3524
|
const journalGateResult = (g) => {
|
|
3386
3525
|
const blocking = g.meta?.infra !== true && gateFailed(g) && (g.gate === "review" || g.gate === "acceptance");
|
|
@@ -3416,7 +3555,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3416
3555
|
// for the worktree it ran in; absent only for a row that never went through the battery.
|
|
3417
3556
|
...(g.meta?.verification !== undefined ? { verification: g.meta.verification } : {}),
|
|
3418
3557
|
...(gateSubject ? { commit: gateSubject.commit, attempt: gateSubject.attempt } : {}),
|
|
3419
|
-
...(gateSubject?.replayMeasurement ? { replayMeasurement: true } : {}),
|
|
3558
|
+
...(gateSubject?.replayMeasurement && (gateSubject.journaledGates?.includes(g.gate) ?? true) ? { replayMeasurement: true } : {}),
|
|
3420
3559
|
...(gateSubject?.replayedFromAttempt !== undefined ? { replayedFromAttempt: gateSubject.replayedFromAttempt } : {}),
|
|
3421
3560
|
...(g.meta?.skipped === true || noVerdictReview ? { skipped: true } : {}),
|
|
3422
3561
|
// T9: an infra-only exit is journaled AS one. The operator reading a red `test` row has to
|
|
@@ -3530,10 +3669,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3530
3669
|
// ever held, and only while an earlier sibling is still in flight — a review that finishes first
|
|
3531
3670
|
// waits for acceptance, never the reverse. That keeps T4's durability where it pays (the first
|
|
3532
3671
|
// verdict to land is still published immediately) and bounds the exposure to one row for the
|
|
3533
|
-
// remainder of one already-running gate. A gate that THROWS
|
|
3534
|
-
// row held behind it
|
|
3672
|
+
// remainder of one already-running gate. A gate that THROWS still ends its round: C publishes the
|
|
3673
|
+
// row held behind it at that boundary (settleParallelRound), so it is never lost or carried over.
|
|
3535
3674
|
const parallelPending = new Set();
|
|
3536
3675
|
let heldParallel;
|
|
3676
|
+
const settleParallelRound = () => {
|
|
3677
|
+
parallelPending.clear();
|
|
3678
|
+
const held = heldParallel;
|
|
3679
|
+
heldParallel = undefined;
|
|
3680
|
+
if (liveEngagement())
|
|
3681
|
+
held?.();
|
|
3682
|
+
};
|
|
3537
3683
|
const notePhaseStart = (e) => {
|
|
3538
3684
|
const active = activeGatePhases.get(t.id) ?? new Set();
|
|
3539
3685
|
active.add(e.gate);
|
|
@@ -3623,6 +3769,81 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3623
3769
|
let testGateFailed = journal.read().some((e) => e.event === "gate-result" && e.taskId === t.id && e.data.gate === "test" && e.data.pass === false
|
|
3624
3770
|
&& !(cfg.executionPolicy?.boundedInfrastructure && e.data.disposition === "infrastructure"));
|
|
3625
3771
|
let retryMode = "fresh";
|
|
3772
|
+
// v2.6.6 A: the ONE post-result settlement both gate loops call. Order is the contract:
|
|
3773
|
+
// satisfied → merge (tip-moved once regates, twice parks; a conflict goes back to the caller), else
|
|
3774
|
+
// noEligible → infra → diff-cap → adjudication → outside-scope → disposition, and whatever survives
|
|
3775
|
+
// is the caller's "red". Every declared resume/live difference arrives as a parameter; nothing here
|
|
3776
|
+
// normalizes one. Preparation, replay, conflict handling, repair funding, feedback and cleanup stay
|
|
3777
|
+
// in the callers.
|
|
3778
|
+
const settleResults = async (results, p) => {
|
|
3779
|
+
const parkAs = (reason, kind) => park(t, reason, kind, p.author, p.attempts, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
3780
|
+
if (results.every(gateSatisfied)) {
|
|
3781
|
+
p.beforeMerge?.();
|
|
3782
|
+
const m = await mergeSerial(p.taskBranch, t, p.gated);
|
|
3783
|
+
if (m.tipMoved) {
|
|
3784
|
+
journal.append("tip-moved", t.id, m.tipMoved);
|
|
3785
|
+
if (tipMoves++ === 0)
|
|
3786
|
+
return { kind: "regate" };
|
|
3787
|
+
await parkAs("task branch tip moved twice after gating", "tip-moved");
|
|
3788
|
+
return { kind: "parked" };
|
|
3789
|
+
}
|
|
3790
|
+
if (!m.ok) {
|
|
3791
|
+
journal.append("merge-conflict", t.id, { conflict: m.conflict });
|
|
3792
|
+
return { kind: "conflict", conflict: m.conflict };
|
|
3793
|
+
}
|
|
3794
|
+
graph = setStatus(graph, t.id, "done");
|
|
3795
|
+
saveGraph(repoRoot, graph);
|
|
3796
|
+
journal.append("task-done", t.id, {
|
|
3797
|
+
attempts: p.attempts, assignment: p.author, taskContentDigest: contentDigest,
|
|
3798
|
+
authors: mergedAuthors(await subjectAuthors(journal.read(), t.id, p.wt, p.taskBase)),
|
|
3799
|
+
});
|
|
3800
|
+
journal.append("merge", t.id, { branch: p.taskBranch, commit: await integrationHead(intWt) });
|
|
3801
|
+
await trackedDriver.project?.(t.id, "completed");
|
|
3802
|
+
// firstAttemptOk/gateFails/consults are recorded FACTS, not policy — a parkKind:"stall" row is
|
|
3803
|
+
// recorded but NOT quality-negative in v1.6; Phase 12 owns reward policy, so flipping it later needs zero data migration.
|
|
3804
|
+
journal.telemetry({
|
|
3805
|
+
taskId: t.id, shape: t.shape, adapter: p.author.adapter, model: p.author.model,
|
|
3806
|
+
channel: p.author.channel, attempts: p.attempts, outcome: "done",
|
|
3807
|
+
durationMs: Date.now() - startMs, firstAttemptOk: p.firstAttemptOk, gateFails, consults, ...p.metering, retryMode,
|
|
3808
|
+
});
|
|
3809
|
+
return { kind: "merged" };
|
|
3810
|
+
}
|
|
3811
|
+
// OBS-540: infra is a fail-closed NON-verdict, not quality degradation. Park with the blocker
|
|
3812
|
+
// already journaled by onGate, before gateFails and before any ladder selection can fund an
|
|
3813
|
+
// identical retry in the same environment. Parsed judge refusals never carry infra and keep
|
|
3814
|
+
// flowing through the chargeable quality path below.
|
|
3815
|
+
const unavailableReview = results.find((g) => gateFailed(g) && g.meta?.noEligibleReviewer === true);
|
|
3816
|
+
if (unavailableReview) {
|
|
3817
|
+
await parkAs(gateFailApprovalReason(t.id, unavailableReview.details, true), "gate-fail");
|
|
3818
|
+
return { kind: "parked" };
|
|
3819
|
+
}
|
|
3820
|
+
// OBS-1106: same predicate as classification — an infra replay is parked, never repaired.
|
|
3821
|
+
const infra = results.find((g) => gateFailed(g) && isInfraResult(g));
|
|
3822
|
+
if (infra) {
|
|
3823
|
+
await parkAs(p.infraReason(infra), "infra");
|
|
3824
|
+
return { kind: "parked" };
|
|
3825
|
+
}
|
|
3826
|
+
// OBS-1007: a cap trip is a typed park of its own — the diff cannot shrink by retrying and the
|
|
3827
|
+
// only honest verb is a recheck under a raised cap. Decided BEFORE the authoring classifier, which
|
|
3828
|
+
// used to lift `gates.diffCap` out of the cap prose as a files[] hint.
|
|
3829
|
+
const capTrip = results.find(isDiffCapPark);
|
|
3830
|
+
if (capTrip) {
|
|
3831
|
+
await parkAs(`${capTrip.gate}: ${capTrip.details}`, "diff-cap");
|
|
3832
|
+
return { kind: "parked" };
|
|
3833
|
+
}
|
|
3834
|
+
if (await adjudicateInfraShapedRed(t, results, p.wt, p.commit, p.reobserve.attempt, p.reobserve.replayed, (reason) => parkAs(reason, "infra")))
|
|
3835
|
+
return { kind: "parked" };
|
|
3836
|
+
if (p.outsideScopeForced && freshOutsideScopeRed(t, p.commit)) {
|
|
3837
|
+
await parkAs(outsideScopePark(t), "gate-fail");
|
|
3838
|
+
return { kind: "parked" };
|
|
3839
|
+
}
|
|
3840
|
+
// OBS-547: who pays for this red is decided by the run's collateral prediction (see
|
|
3841
|
+
// dispositionScopeRed) — an authoring defect parks unchargeable before any accounting below.
|
|
3842
|
+
const d = p.disposition();
|
|
3843
|
+
if (await dispositionScopeRed(t, results, d.author, d.attempt, startMs, gateFails, consults, tokens, metered, retryMode, await treeOrDiffPaths(p.taskBase, p.wt)))
|
|
3844
|
+
return { kind: "parked" };
|
|
3845
|
+
return { kind: "red" };
|
|
3846
|
+
};
|
|
3626
3847
|
let lastContextTokens; // v1.23 reset signal, including stalled/quota attempts
|
|
3627
3848
|
// v1.29: only a gate-failed attempt can seed same-session retry. The next attempt consumes this
|
|
3628
3849
|
// once; a changed channel, unknown context, or missing resumeCommand falls back to fresh.
|
|
@@ -4096,6 +4317,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4096
4317
|
journal.append("gate-provisioned", t.id, provisionedRow);
|
|
4097
4318
|
const resumedTask = { ...t, gates: remainingGates };
|
|
4098
4319
|
const operatorContext = approvalReviewContext(journal.read(), t.id);
|
|
4320
|
+
let outsideScopeForced = false;
|
|
4099
4321
|
gateLoop: while (true) {
|
|
4100
4322
|
fatalStop.signal.throwIfAborted();
|
|
4101
4323
|
executionSignal()?.throwIfAborted();
|
|
@@ -4124,13 +4346,42 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4124
4346
|
const own = journal.read().filter((e) => e.taskId === t.id);
|
|
4125
4347
|
return own.slice(own.map((e) => e.event === "phase-start" && e.data.phase === "gates").lastIndexOf(true) + 1);
|
|
4126
4348
|
})();
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4349
|
+
// C: a passing selected-test screen is never a forced round's conclusive test result (the
|
|
4350
|
+
// journal's own replay rule): its full suite is still owed, fresh, under the round's bypass.
|
|
4351
|
+
const measured = lastRound.filter((e) => e.event === "gate-result" && !(e.data.gate === "test"
|
|
4352
|
+
&& Array.isArray(e.data.selectedTests) && e.data.fullSuite !== true && e.data.pass !== false));
|
|
4353
|
+
const forcedUnmeasured = (outside) => lastRound.some((e) => e.event === "gate-fresh-forced"
|
|
4354
|
+
&& e.data.commit === gateSubject.commit && (e.data.reason === OUTSIDE_SCOPE_RED) === outside)
|
|
4355
|
+
&& !measured.some((e) => e.data.gate === "test");
|
|
4356
|
+
// C: a crash after the forced re-execution journaled its red but before its park still owes that
|
|
4357
|
+
// park — restored from the journaled subject and result, never re-measured, replayed or repaired.
|
|
4358
|
+
// Infra adjudication stays authoritative, exactly as it is ahead of the live park.
|
|
4359
|
+
if (!recheck && !satisfiedGate && lastRound.some((e) => e.event === "gate-fresh-forced"
|
|
4360
|
+
&& e.data.reason === OUTSIDE_SCOPE_RED && e.data.commit === gateSubject.commit)
|
|
4361
|
+
&& allOutsideScopeReds(t, lastRound, gateSubject.commit)) {
|
|
4362
|
+
const restored = lastRound.filter((e) => e.event === "gate-result" && e.data.gate === "test" && e.data.pass === false)
|
|
4363
|
+
.map((e) => ({ gate: "test", pass: false, details: String(e.data.details ?? ""), meta: e.data }));
|
|
4364
|
+
if (await adjudicateInfraShapedRed(t, restored, wt, gateSubject.commit, rs?.attempts ?? 0, true, (reason) => park(t, reason, "infra", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode)))
|
|
4365
|
+
return;
|
|
4366
|
+
await park(t, outsideScopePark(t), "gate-fail", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
// C: the same out-of-scope re-execution the live loop owes, and a crash inside it still owes it. It
|
|
4370
|
+
// takes precedence over a timeout refresh: a qualifying red keeps its own bypass and park.
|
|
4371
|
+
outsideScopeForced = !recheck && !satisfiedGate && ((harvestAuthor !== undefined && owesOutsideScopeRefresh(t, lastRound, gateSubject.commit)) || forcedUnmeasured(true));
|
|
4372
|
+
const freshTimeout = !recheck && !satisfiedGate && !outsideScopeForced && ((harvestAuthor !== undefined && owesTimeoutRefresh(t, lastRound, gateSubject.commit)) || forcedUnmeasured(false));
|
|
4373
|
+
// C: a measurement this forced round EXECUTES fresh — its forced test above all — is independent
|
|
4374
|
+
// evidence, never a replay measurement; only a gate the interrupted round already journaled is.
|
|
4375
|
+
if (outsideScopeForced) {
|
|
4376
|
+
gateSubject = { ...gateSubject, journaledGates: measured.map((e) => String(e.data.gate)) };
|
|
4377
|
+
}
|
|
4130
4378
|
journal.phaseStart(t.id, "gates");
|
|
4131
4379
|
if (freshTimeout) {
|
|
4132
4380
|
journal.append("gate-fresh-forced", t.id, { gate: "test", attempt: gateSubject.attempt, commit: gateSubject.commit, reason: "no-commit-timeout-red", resumed: true });
|
|
4133
4381
|
}
|
|
4382
|
+
if (outsideScopeForced) {
|
|
4383
|
+
journal.append("gate-fresh-forced", t.id, { gate: "test", attempt: gateSubject.attempt, commit: gateSubject.commit, reason: OUTSIDE_SCOPE_RED, resumed: true });
|
|
4384
|
+
}
|
|
4134
4385
|
const { results } = await withCommandContext(t.id, async () => runReviewRecovery(resumedTask, {
|
|
4135
4386
|
carriedAuthors: await subjectAuthors(journal.read(), t.id, wt, taskBase),
|
|
4136
4387
|
producer: producerNow(),
|
|
@@ -4155,7 +4406,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4155
4406
|
// flaking seat is re-asked on every task.
|
|
4156
4407
|
reviewNoVerdicts,
|
|
4157
4408
|
recheck, // OBS-1055: a recheck discards cached reds — the battery re-measures what the operator questioned
|
|
4158
|
-
cachedRedBypass: freshTimeout ? "timeout-fresh" : undefined,
|
|
4409
|
+
cachedRedBypass: freshTimeout ? "timeout-fresh" : outsideScopeForced ? "out-of-scope-fresh" : undefined,
|
|
4159
4410
|
onGate: async (e) => {
|
|
4160
4411
|
if (!liveEngagement())
|
|
4161
4412
|
return;
|
|
@@ -4190,23 +4441,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4190
4441
|
const approvedCommits = await commitsAheadOf(taskBase, wt);
|
|
4191
4442
|
graph = addEvidence(graph, t.id, { commits: approvedCommits, gateResults: results });
|
|
4192
4443
|
saveGraph(repoRoot, graph);
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
}
|
|
4199
|
-
// OBS-1106: same predicate as classification — an infra replay is parked, never repaired.
|
|
4200
|
-
const infra = results.find((g) => gateFailed(g) && isInfraResult(g));
|
|
4201
|
-
if (infra) {
|
|
4202
|
-
await park(t, `${infra.gate}: ${infra.details}`, "infra", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4203
|
-
return;
|
|
4204
|
-
}
|
|
4205
|
-
const capTrip = results.find(isDiffCapPark);
|
|
4206
|
-
if (capTrip) {
|
|
4207
|
-
await park(t, `${capTrip.gate}: ${capTrip.details}`, "diff-cap", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4208
|
-
return;
|
|
4209
|
-
}
|
|
4444
|
+
const n = rs?.attempts ?? 0;
|
|
4445
|
+
const settled = await settleResults(results, {
|
|
4446
|
+
wt, taskBase, taskBranch, gated, commit: gateSubject.commit, outsideScopeForced,
|
|
4447
|
+
author: gateAuthor, attempts: n, infraReason: (g) => `${g.gate}: ${g.details}`,
|
|
4448
|
+
reobserve: { attempt: n, replayed: false }, firstAttemptOk: false, metering: {},
|
|
4210
4449
|
// OBS-547: disposition FIRST, and through the same helper the ordinary attempt path uses. A
|
|
4211
4450
|
// crash between a journaled scope red and its classification must not turn a predicted red
|
|
4212
4451
|
// into a charged one just because a resume is what observed it.
|
|
@@ -4218,11 +4457,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4218
4457
|
// erase an EARLIER chargeable attempt and attribute the park to the assignment the rewind
|
|
4219
4458
|
// restored. Ask the journal which state this is, and take the classified dispatch's own
|
|
4220
4459
|
// assignment back with it.
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4460
|
+
disposition: () => {
|
|
4461
|
+
const classified = journal.classifiedDispatch(t.id);
|
|
4462
|
+
return { author: classified?.assignment ?? gateAuthor, attempt: classified ? n : Math.max(0, n - 1) };
|
|
4463
|
+
},
|
|
4464
|
+
});
|
|
4465
|
+
if (settled.kind === "red") {
|
|
4226
4466
|
gateFails++;
|
|
4227
4467
|
// Observed green gates are only measurements. If the resumed suffix is red, preserve that
|
|
4228
4468
|
// result in the journal and return to the ordinary attempt/consult ladder, which rebuilds
|
|
@@ -4265,32 +4505,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4265
4505
|
await park(t, gateFailApprovalReason(t.id, "post-approval gate failed", results.some((g) => g.gate === "review" && gateFailed(g))), "gate-fail", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4266
4506
|
return;
|
|
4267
4507
|
}
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
if (tipMoves++ === 0)
|
|
4272
|
-
continue gateLoop;
|
|
4273
|
-
await park(t, "task branch tip moved twice after gating", "tip-moved", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4508
|
+
if (settled.kind === "regate")
|
|
4509
|
+
continue gateLoop;
|
|
4510
|
+
if (settled.kind === "parked")
|
|
4274
4511
|
return;
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
journal.append("merge-conflict", t.id, { conflict: m.conflict });
|
|
4278
|
-
await park(t, `merge conflict after ${resumeReason}: ${m.conflict ?? "unknown conflict"}`, "merge-conflict", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4512
|
+
if (settled.kind === "conflict") {
|
|
4513
|
+
await park(t, `merge conflict after ${resumeReason}: ${settled.conflict ?? "unknown conflict"}`, "merge-conflict", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4279
4514
|
return;
|
|
4280
4515
|
}
|
|
4281
|
-
graph = setStatus(graph, t.id, "done");
|
|
4282
|
-
saveGraph(repoRoot, graph);
|
|
4283
|
-
journal.append("task-done", t.id, {
|
|
4284
|
-
attempts: rs?.attempts ?? 0, assignment: gateAuthor, taskContentDigest: contentDigest,
|
|
4285
|
-
authors: mergedAuthors(await subjectAuthors(journal.read(), t.id, wt, taskBase)),
|
|
4286
|
-
});
|
|
4287
|
-
journal.append("merge", t.id, { branch: taskBranch, commit: await integrationHead(intWt) });
|
|
4288
|
-
await trackedDriver.project?.(t.id, "completed");
|
|
4289
|
-
journal.telemetry({
|
|
4290
|
-
taskId: t.id, shape: t.shape, adapter: gateAuthor.adapter, model: gateAuthor.model,
|
|
4291
|
-
channel: gateAuthor.channel, attempts: rs?.attempts ?? 0, outcome: "done",
|
|
4292
|
-
durationMs: Date.now() - startMs, firstAttemptOk: false, gateFails, consults, retryMode,
|
|
4293
|
-
});
|
|
4294
4516
|
await reconcile({ spareLiveLlm: true });
|
|
4295
4517
|
return;
|
|
4296
4518
|
}
|
|
@@ -6246,6 +6468,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6246
6468
|
};
|
|
6247
6469
|
let results = [];
|
|
6248
6470
|
let commits = [];
|
|
6471
|
+
let outsideScopeForced = false;
|
|
6249
6472
|
gateLoop: while (true) {
|
|
6250
6473
|
fatalStop.signal.throwIfAborted();
|
|
6251
6474
|
executionSignal()?.throwIfAborted();
|
|
@@ -6261,12 +6484,18 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6261
6484
|
&& e.data.attempt === attempt - 1);
|
|
6262
6485
|
const previousRows = retryMode === "repair" ? lastRoundRows : [];
|
|
6263
6486
|
// OBS-1106 residual: see owesTimeoutRefresh — the replay below and the verdict cache are both refused.
|
|
6264
|
-
|
|
6487
|
+
// C: an unchanged red wholly outside files[] is re-executed once instead of copied (journal or cache);
|
|
6488
|
+
// it outranks the timeout refresh, so a timeout-shaped qualifying red keeps its own bypass and park.
|
|
6489
|
+
outsideScopeForced = !explicitlyFundedAttempt && owesOutsideScopeRefresh(t, lastRoundRows, gateSubject.commit);
|
|
6490
|
+
const freshTimeout = !explicitlyFundedAttempt && !outsideScopeForced && owesTimeoutRefresh(t, lastRoundRows, gateSubject.commit);
|
|
6265
6491
|
journal.phaseStart(t.id, "gates");
|
|
6266
6492
|
if (freshTimeout) {
|
|
6267
6493
|
journal.append("gate-fresh-forced", t.id, { gate: "test", attempt, priorAttempt: attempt - 1, commit: gateSubject.commit, reason: "no-commit-timeout-red" });
|
|
6268
6494
|
}
|
|
6269
|
-
|
|
6495
|
+
if (outsideScopeForced) {
|
|
6496
|
+
journal.append("gate-fresh-forced", t.id, { gate: "test", attempt, priorAttempt: attempt - 1, commit: gateSubject.commit, reason: OUTSIDE_SCOPE_RED });
|
|
6497
|
+
}
|
|
6498
|
+
const replay = !explicitlyFundedAttempt && !freshTimeout && !outsideScopeForced && previousRows.length > 0
|
|
6270
6499
|
&& previousRows.every((e) => e.data.commit === gateSubject.commit)
|
|
6271
6500
|
&& previousRows.some((e) => e.data.pass === false && e.data.skipped !== true);
|
|
6272
6501
|
if (replay) {
|
|
@@ -6299,7 +6528,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6299
6528
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, health, adapters, cfg, artifactDir: journal.dir,
|
|
6300
6529
|
collateral: collateral.get(t.id) ?? [],
|
|
6301
6530
|
selectTests: !testGateFailed,
|
|
6302
|
-
cachedRedBypass: explicitlyFundedAttempt ? "operator-rerun" : freshTimeout ? "timeout-fresh" : undefined,
|
|
6531
|
+
cachedRedBypass: explicitlyFundedAttempt ? "operator-rerun" : freshTimeout ? "timeout-fresh" : outsideScopeForced ? "out-of-scope-fresh" : undefined,
|
|
6303
6532
|
via: cfg.visibility.llm === "pane"
|
|
6304
6533
|
? {
|
|
6305
6534
|
driver: trackedDriver,
|
|
@@ -6328,7 +6557,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6328
6557
|
if (results.some((g) => g.gate === "test" && !g.pass
|
|
6329
6558
|
&& !(cfg.executionPolicy?.boundedInfrastructure && failureDisposition(g) === "infrastructure")))
|
|
6330
6559
|
testGateFailed = true;
|
|
6331
|
-
|
|
6560
|
+
const live = assignment;
|
|
6561
|
+
const settled = await settleResults(results, {
|
|
6562
|
+
wt, taskBase, taskBranch, gated, commit: gateSubject.commit, outsideScopeForced,
|
|
6563
|
+
author: live, attempts: attempt + 1,
|
|
6564
|
+
infraReason: (g) => `${g.gate}: ${g.details}${g.meta?.recoveryBlocked ? ` — ${g.meta.recoveryBlocked}` : ""}`,
|
|
6565
|
+
reobserve: { attempt, replayed: gateSubject.replayedFromAttempt !== undefined },
|
|
6566
|
+
disposition: () => ({ author: live, attempt }),
|
|
6567
|
+
firstAttemptOk: attempt === 0, metering: { tokens, meteredAttempts: tokens ? metered : undefined },
|
|
6332
6568
|
// T6: every gate — the review included — is satisfied on this commit, so the failure brief
|
|
6333
6569
|
// this loop is still holding describes nothing outstanding. It is dropped HERE, before the
|
|
6334
6570
|
// merge, because a conflict below sends the task around the attempt loop again: a brief kept
|
|
@@ -6337,35 +6573,23 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6337
6573
|
// leaving that dispatch's row indistinguishable from an amnesiac one. Rebuilt exactly as the
|
|
6338
6574
|
// gate-fail brief is, so prior-RUN evidence (retired by its own rule, not by this reviewer)
|
|
6339
6575
|
// survives and only this run's settled findings go.
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
}
|
|
6358
|
-
graph = setStatus(graph, t.id, "done");
|
|
6359
|
-
saveGraph(repoRoot, graph);
|
|
6360
|
-
journal.append("task-done", t.id, {
|
|
6361
|
-
attempts: attempt + 1, assignment, taskContentDigest: contentDigest,
|
|
6362
|
-
authors: mergedAuthors(await subjectAuthors(journal.read(), t.id, wt, taskBase)),
|
|
6363
|
-
});
|
|
6364
|
-
journal.append("merge", t.id, { branch: taskBranch, commit: await integrationHead(intWt) });
|
|
6365
|
-
await trackedDriver.project?.(t.id, "completed");
|
|
6366
|
-
// firstAttemptOk/gateFails/consults are recorded FACTS, not policy — a parkKind:"stall" row is
|
|
6367
|
-
// recorded but NOT quality-negative in v1.6; Phase 12 owns reward policy, so flipping it later needs zero data migration.
|
|
6368
|
-
journal.telemetry({ taskId: t.id, shape: t.shape, adapter: assignment.adapter, model: assignment.model, channel: assignment.channel, attempts: attempt + 1, outcome: "done", durationMs: Date.now() - startMs, firstAttemptOk: attempt === 0, gateFails, consults, tokens, meteredAttempts: tokens ? metered : undefined, retryMode });
|
|
6576
|
+
beforeMerge: () => {
|
|
6577
|
+
feedback = withCarriedEvidence("");
|
|
6578
|
+
fatalStop.signal.throwIfAborted();
|
|
6579
|
+
executionSignal()?.throwIfAborted();
|
|
6580
|
+
},
|
|
6581
|
+
});
|
|
6582
|
+
if (settled.kind === "regate")
|
|
6583
|
+
continue gateLoop;
|
|
6584
|
+
if (settled.kind === "parked")
|
|
6585
|
+
return;
|
|
6586
|
+
if (settled.kind === "conflict") {
|
|
6587
|
+
const v = await runConsult("merge-conflict", output, settled.conflict ?? "", results);
|
|
6588
|
+
if (await applyVerdict(v, attempt + 1, "merge-conflict"))
|
|
6589
|
+
continue attempts;
|
|
6590
|
+
return;
|
|
6591
|
+
}
|
|
6592
|
+
if (settled.kind === "merged") {
|
|
6369
6593
|
// D-07 done means gone (merged-P42-01-worker incident): a merged task's worker pane closes on
|
|
6370
6594
|
// the task-done path, not at run end. Only THIS successful attempt's `slot` is in scope — prior
|
|
6371
6595
|
// failed attempts' slots stay in keptSlots governed by keepPanes (they hold failure context the
|
|
@@ -6384,34 +6608,6 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6384
6608
|
}
|
|
6385
6609
|
break gateLoop;
|
|
6386
6610
|
}
|
|
6387
|
-
// OBS-540: infra is a fail-closed NON-verdict, not quality degradation. Park with the blocker
|
|
6388
|
-
// already journaled by onGate, before gateFails and before any ladder selection can fund an
|
|
6389
|
-
// identical retry in the same environment. Parsed judge refusals never carry infra and keep
|
|
6390
|
-
// flowing through the chargeable quality path below.
|
|
6391
|
-
const unavailableReview = results.find((g) => gateFailed(g) && g.meta?.noEligibleReviewer === true);
|
|
6392
|
-
if (unavailableReview) {
|
|
6393
|
-
await park(t, gateFailApprovalReason(t.id, unavailableReview.details, true), "gate-fail", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
6394
|
-
return;
|
|
6395
|
-
}
|
|
6396
|
-
const infraFailure = results.find((g) => gateFailed(g) && isInfraResult(g));
|
|
6397
|
-
if (infraFailure) {
|
|
6398
|
-
await park(t, `${infraFailure.gate}: ${infraFailure.details}${infraFailure.meta?.recoveryBlocked ? ` — ${infraFailure.meta.recoveryBlocked}` : ""}`, "infra", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
6399
|
-
return;
|
|
6400
|
-
}
|
|
6401
|
-
// OBS-1007: a cap trip is a typed park of its own — the diff cannot shrink by retrying and the
|
|
6402
|
-
// only honest verb is a recheck under a raised cap. Decided BEFORE the authoring classifier, which
|
|
6403
|
-
// used to lift `gates.diffCap` out of the cap prose as a files[] hint.
|
|
6404
|
-
const capTrip = results.find(isDiffCapPark);
|
|
6405
|
-
if (capTrip) {
|
|
6406
|
-
await park(t, `${capTrip.gate}: ${capTrip.details}`, "diff-cap", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
6407
|
-
return;
|
|
6408
|
-
}
|
|
6409
|
-
if (await adjudicateInfraShapedRed(t, results, wt, gateSubject.commit, attempt, gateSubject.replayedFromAttempt !== undefined, (reason) => park(t, reason, "infra", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode)))
|
|
6410
|
-
return;
|
|
6411
|
-
// OBS-547: who pays for this red is decided by the run's collateral prediction (see
|
|
6412
|
-
// dispositionScopeRed) — an authoring defect parks unchargeable before any accounting below.
|
|
6413
|
-
if (await dispositionScopeRed(t, results, assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode, await treeOrDiffPaths(taskBase, wt)))
|
|
6414
|
-
return;
|
|
6415
6611
|
gateFails++; // this attempt's gates failed — the one place quality degradation is verified (never inferred from attempts)
|
|
6416
6612
|
// v1.53 T3: prefer the CLI's own session id captured from this attempt's output (kimi's resume
|
|
6417
6613
|
// trailer) over the harness slot name; absent hook or no capture keeps today's slot-name id.
|