tickmarkr 2.6.5 → 2.6.7
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/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +16 -4
- package/dist/adapters/codex-commit-check.js +150 -26
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +20 -8
- package/dist/adapters/types.d.ts +4 -1
- package/dist/cli/commands/beat.d.ts +6 -1
- package/dist/cli/commands/beat.js +123 -76
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/fleet.js +368 -85
- package/dist/cli/commands/init.js +23 -9
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/verify.js +37 -9
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +10 -5
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/config/fleet-overlay.d.ts +14 -0
- package/dist/config/fleet-overlay.js +154 -25
- package/dist/drivers/orca.d.ts +17 -3
- package/dist/drivers/orca.js +64 -20
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +14 -3
- package/dist/gates/cache.js +1 -1
- package/dist/gates/review.js +15 -0
- package/dist/gates/run-gates.d.ts +20 -7
- package/dist/gates/run-gates.js +345 -210
- package/dist/gates/test-manifest.d.ts +11 -0
- package/dist/gates/test-manifest.js +151 -111
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/beat-lifecycle.d.ts +154 -0
- package/dist/run/beat-lifecycle.js +825 -0
- package/dist/run/daemon.js +202 -126
- package/dist/run/git-trust.d.ts +27 -0
- package/dist/run/git-trust.js +178 -0
- package/dist/run/git.js +34 -14
- package/dist/run/journal.js +15 -3
- package/dist/run/lease.d.ts +23 -0
- package/dist/run/lease.js +232 -38
- package/dist/run/merge.js +2 -2
- package/dist/run/stall.js +20 -1
- package/dist/run/supervision.d.ts +8 -0
- package/dist/run/supervision.js +24 -4
- package/dist/run/verification-job.d.ts +51 -0
- package/dist/run/verification-job.js +137 -0
- package/dist/tui/ink/fleet-app.d.ts +35 -5
- package/dist/tui/ink/fleet-app.js +244 -124
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +19 -10
- package/skills/tickmarkr-loop/SKILL.md +19 -10
- package/skills/tickmarkr-overseer/SKILL.md +25 -16
- 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,7 +33,8 @@ 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";
|
|
@@ -374,9 +376,6 @@ 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
|
-
const TIMEOUT_SHAPED_RE = /\b(?:Test|Hook) timed out in (?:\d+|#)\s*ms\b|\bexpected (?:\d+|#)(?:\.(?:\d+|#))? to be (?:less than|below)(?: or equal to)? (?:\d+|#)/i;
|
|
378
|
-
const RUNNER_INFRA_DIAGNOSTIC_RE = /\bnever-started (?:[1-9]\d*|#)(?![\d#])|\[vitest-worker\]: Timeout calling\b/;
|
|
379
|
-
const timeoutShaped = (details) => TIMEOUT_SHAPED_RE.test(readableExcerpt(details));
|
|
380
379
|
/** C (E1): the one no-verdict that is the host's slowness, not the seat's — the same predicate run-gates
|
|
381
380
|
* applies to its run-scoped tally, read here for the live task strike and the resume reconstruction. */
|
|
382
381
|
const proofTimeoutNote = (data) => data.cause === "seat-launch-failed" && data.launchCause === "checkout-proof-timeout";
|
|
@@ -2690,7 +2689,29 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2690
2689
|
to: loadedHash,
|
|
2691
2690
|
});
|
|
2692
2691
|
}
|
|
2693
|
-
|
|
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
|
+
}
|
|
2694
2715
|
const replayEvents = journal.read();
|
|
2695
2716
|
// journal.replayStatuses predates typed releases and re-pends even inert rows. Keep that
|
|
2696
2717
|
// legacy reader intact; scheduling accepts only the fold's recognised authorities.
|
|
@@ -2739,8 +2760,47 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2739
2760
|
});
|
|
2740
2761
|
runStarted = true;
|
|
2741
2762
|
await placeBoard();
|
|
2742
|
-
|
|
2743
|
-
|
|
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.
|
|
2744
2804
|
baselineCapture = initializeHost();
|
|
2745
2805
|
void baselineCapture.catch(() => { baselineFailed = true; });
|
|
2746
2806
|
}
|
|
@@ -3709,6 +3769,81 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3709
3769
|
let testGateFailed = journal.read().some((e) => e.event === "gate-result" && e.taskId === t.id && e.data.gate === "test" && e.data.pass === false
|
|
3710
3770
|
&& !(cfg.executionPolicy?.boundedInfrastructure && e.data.disposition === "infrastructure"));
|
|
3711
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
|
+
};
|
|
3712
3847
|
let lastContextTokens; // v1.23 reset signal, including stalled/quota attempts
|
|
3713
3848
|
// v1.29: only a gate-failed attempt can seed same-session retry. The next attempt consumes this
|
|
3714
3849
|
// once; a changed channel, unknown context, or missing resumeCommand falls back to fresh.
|
|
@@ -4306,23 +4441,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4306
4441
|
const approvedCommits = await commitsAheadOf(taskBase, wt);
|
|
4307
4442
|
graph = addEvidence(graph, t.id, { commits: approvedCommits, gateResults: results });
|
|
4308
4443
|
saveGraph(repoRoot, graph);
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
}
|
|
4315
|
-
// OBS-1106: same predicate as classification — an infra replay is parked, never repaired.
|
|
4316
|
-
const infra = results.find((g) => gateFailed(g) && isInfraResult(g));
|
|
4317
|
-
if (infra) {
|
|
4318
|
-
await park(t, `${infra.gate}: ${infra.details}`, "infra", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4319
|
-
return;
|
|
4320
|
-
}
|
|
4321
|
-
const capTrip = results.find(isDiffCapPark);
|
|
4322
|
-
if (capTrip) {
|
|
4323
|
-
await park(t, `${capTrip.gate}: ${capTrip.details}`, "diff-cap", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
4324
|
-
return;
|
|
4325
|
-
}
|
|
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: {},
|
|
4326
4449
|
// OBS-547: disposition FIRST, and through the same helper the ordinary attempt path uses. A
|
|
4327
4450
|
// crash between a journaled scope red and its classification must not turn a predicted red
|
|
4328
4451
|
// into a charged one just because a resume is what observed it.
|
|
@@ -4334,15 +4457,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4334
4457
|
// erase an EARLIER chargeable attempt and attribute the park to the assignment the rewind
|
|
4335
4458
|
// restored. Ask the journal which state this is, and take the classified dispatch's own
|
|
4336
4459
|
// assignment back with it.
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
const classified = journal.classifiedDispatch(t.id);
|
|
4344
|
-
if (await dispositionScopeRed(t, results, classified?.assignment ?? gateAuthor, classified ? (rs?.attempts ?? 0) : Math.max(0, (rs?.attempts ?? 0) - 1), startMs, gateFails, consults, tokens, metered, retryMode, await treeOrDiffPaths(taskBase, wt)))
|
|
4345
|
-
return;
|
|
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") {
|
|
4346
4466
|
gateFails++;
|
|
4347
4467
|
// Observed green gates are only measurements. If the resumed suffix is red, preserve that
|
|
4348
4468
|
// result in the journal and return to the ordinary attempt/consult ladder, which rebuilds
|
|
@@ -4385,32 +4505,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4385
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);
|
|
4386
4506
|
return;
|
|
4387
4507
|
}
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
if (tipMoves++ === 0)
|
|
4392
|
-
continue gateLoop;
|
|
4393
|
-
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")
|
|
4394
4511
|
return;
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
journal.append("merge-conflict", t.id, { conflict: m.conflict });
|
|
4398
|
-
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);
|
|
4399
4514
|
return;
|
|
4400
4515
|
}
|
|
4401
|
-
graph = setStatus(graph, t.id, "done");
|
|
4402
|
-
saveGraph(repoRoot, graph);
|
|
4403
|
-
journal.append("task-done", t.id, {
|
|
4404
|
-
attempts: rs?.attempts ?? 0, assignment: gateAuthor, taskContentDigest: contentDigest,
|
|
4405
|
-
authors: mergedAuthors(await subjectAuthors(journal.read(), t.id, wt, taskBase)),
|
|
4406
|
-
});
|
|
4407
|
-
journal.append("merge", t.id, { branch: taskBranch, commit: await integrationHead(intWt) });
|
|
4408
|
-
await trackedDriver.project?.(t.id, "completed");
|
|
4409
|
-
journal.telemetry({
|
|
4410
|
-
taskId: t.id, shape: t.shape, adapter: gateAuthor.adapter, model: gateAuthor.model,
|
|
4411
|
-
channel: gateAuthor.channel, attempts: rs?.attempts ?? 0, outcome: "done",
|
|
4412
|
-
durationMs: Date.now() - startMs, firstAttemptOk: false, gateFails, consults, retryMode,
|
|
4413
|
-
});
|
|
4414
4516
|
await reconcile({ spareLiveLlm: true });
|
|
4415
4517
|
return;
|
|
4416
4518
|
}
|
|
@@ -5940,13 +6042,23 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5940
6042
|
reapFailure = error instanceof Error ? error.message : String(error);
|
|
5941
6043
|
}
|
|
5942
6044
|
}
|
|
5943
|
-
else if (keepOpen && (workerFinished || processExited || driver.id !== "subprocess")) {
|
|
5944
|
-
await reapWorker(slot);
|
|
5945
|
-
keptSlots.push(slot);
|
|
5946
|
-
supersededWorkerSlot = slot;
|
|
5947
|
-
}
|
|
5948
6045
|
else {
|
|
5949
|
-
|
|
6046
|
+
// A worker that exited cleanly can still leave a census the reap cannot read. That is the
|
|
6047
|
+
// same stall disposition as above, never a silent task failure; a kept slot is recorded
|
|
6048
|
+
// first so run end still closes it (public CI 37324617042, D-1278).
|
|
6049
|
+
try {
|
|
6050
|
+
if (keepOpen && (workerFinished || processExited || driver.id !== "subprocess")) {
|
|
6051
|
+
keptSlots.push(slot);
|
|
6052
|
+
supersededWorkerSlot = slot;
|
|
6053
|
+
await reapWorker(slot);
|
|
6054
|
+
}
|
|
6055
|
+
else {
|
|
6056
|
+
await closeSlot(slot);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
catch (error) {
|
|
6060
|
+
reapFailure = error instanceof Error ? error.message : String(error);
|
|
6061
|
+
}
|
|
5950
6062
|
}
|
|
5951
6063
|
if (retryMode === "resume") {
|
|
5952
6064
|
const transcript = readResumeTranscript();
|
|
@@ -5970,7 +6082,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5970
6082
|
tokens = addUsage(tokens, attemptUsage);
|
|
5971
6083
|
metered++;
|
|
5972
6084
|
}
|
|
5973
|
-
|
|
6085
|
+
// An unreadable cleanup outranks a dead-worker park: only the reapFailure park re-verifies the census.
|
|
6086
|
+
if (deadWorkerPark && !reapFailure) {
|
|
5974
6087
|
await park(t, deadWorkerPark.reason, "stall", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode, { ref: deadWorkerPark.ref });
|
|
5975
6088
|
return;
|
|
5976
6089
|
}
|
|
@@ -6455,7 +6568,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6455
6568
|
if (results.some((g) => g.gate === "test" && !g.pass
|
|
6456
6569
|
&& !(cfg.executionPolicy?.boundedInfrastructure && failureDisposition(g) === "infrastructure")))
|
|
6457
6570
|
testGateFailed = true;
|
|
6458
|
-
|
|
6571
|
+
const live = assignment;
|
|
6572
|
+
const settled = await settleResults(results, {
|
|
6573
|
+
wt, taskBase, taskBranch, gated, commit: gateSubject.commit, outsideScopeForced,
|
|
6574
|
+
author: live, attempts: attempt + 1,
|
|
6575
|
+
infraReason: (g) => `${g.gate}: ${g.details}${g.meta?.recoveryBlocked ? ` — ${g.meta.recoveryBlocked}` : ""}`,
|
|
6576
|
+
reobserve: { attempt, replayed: gateSubject.replayedFromAttempt !== undefined },
|
|
6577
|
+
disposition: () => ({ author: live, attempt }),
|
|
6578
|
+
firstAttemptOk: attempt === 0, metering: { tokens, meteredAttempts: tokens ? metered : undefined },
|
|
6459
6579
|
// T6: every gate — the review included — is satisfied on this commit, so the failure brief
|
|
6460
6580
|
// this loop is still holding describes nothing outstanding. It is dropped HERE, before the
|
|
6461
6581
|
// merge, because a conflict below sends the task around the attempt loop again: a brief kept
|
|
@@ -6464,35 +6584,23 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6464
6584
|
// leaving that dispatch's row indistinguishable from an amnesiac one. Rebuilt exactly as the
|
|
6465
6585
|
// gate-fail brief is, so prior-RUN evidence (retired by its own rule, not by this reviewer)
|
|
6466
6586
|
// survives and only this run's settled findings go.
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
}
|
|
6485
|
-
graph = setStatus(graph, t.id, "done");
|
|
6486
|
-
saveGraph(repoRoot, graph);
|
|
6487
|
-
journal.append("task-done", t.id, {
|
|
6488
|
-
attempts: attempt + 1, assignment, taskContentDigest: contentDigest,
|
|
6489
|
-
authors: mergedAuthors(await subjectAuthors(journal.read(), t.id, wt, taskBase)),
|
|
6490
|
-
});
|
|
6491
|
-
journal.append("merge", t.id, { branch: taskBranch, commit: await integrationHead(intWt) });
|
|
6492
|
-
await trackedDriver.project?.(t.id, "completed");
|
|
6493
|
-
// firstAttemptOk/gateFails/consults are recorded FACTS, not policy — a parkKind:"stall" row is
|
|
6494
|
-
// recorded but NOT quality-negative in v1.6; Phase 12 owns reward policy, so flipping it later needs zero data migration.
|
|
6495
|
-
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 });
|
|
6587
|
+
beforeMerge: () => {
|
|
6588
|
+
feedback = withCarriedEvidence("");
|
|
6589
|
+
fatalStop.signal.throwIfAborted();
|
|
6590
|
+
executionSignal()?.throwIfAborted();
|
|
6591
|
+
},
|
|
6592
|
+
});
|
|
6593
|
+
if (settled.kind === "regate")
|
|
6594
|
+
continue gateLoop;
|
|
6595
|
+
if (settled.kind === "parked")
|
|
6596
|
+
return;
|
|
6597
|
+
if (settled.kind === "conflict") {
|
|
6598
|
+
const v = await runConsult("merge-conflict", output, settled.conflict ?? "", results);
|
|
6599
|
+
if (await applyVerdict(v, attempt + 1, "merge-conflict"))
|
|
6600
|
+
continue attempts;
|
|
6601
|
+
return;
|
|
6602
|
+
}
|
|
6603
|
+
if (settled.kind === "merged") {
|
|
6496
6604
|
// D-07 done means gone (merged-P42-01-worker incident): a merged task's worker pane closes on
|
|
6497
6605
|
// the task-done path, not at run end. Only THIS successful attempt's `slot` is in scope — prior
|
|
6498
6606
|
// failed attempts' slots stay in keptSlots governed by keepPanes (they hold failure context the
|
|
@@ -6511,38 +6619,6 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
6511
6619
|
}
|
|
6512
6620
|
break gateLoop;
|
|
6513
6621
|
}
|
|
6514
|
-
// OBS-540: infra is a fail-closed NON-verdict, not quality degradation. Park with the blocker
|
|
6515
|
-
// already journaled by onGate, before gateFails and before any ladder selection can fund an
|
|
6516
|
-
// identical retry in the same environment. Parsed judge refusals never carry infra and keep
|
|
6517
|
-
// flowing through the chargeable quality path below.
|
|
6518
|
-
const unavailableReview = results.find((g) => gateFailed(g) && g.meta?.noEligibleReviewer === true);
|
|
6519
|
-
if (unavailableReview) {
|
|
6520
|
-
await park(t, gateFailApprovalReason(t.id, unavailableReview.details, true), "gate-fail", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
6521
|
-
return;
|
|
6522
|
-
}
|
|
6523
|
-
const infraFailure = results.find((g) => gateFailed(g) && isInfraResult(g));
|
|
6524
|
-
if (infraFailure) {
|
|
6525
|
-
await park(t, `${infraFailure.gate}: ${infraFailure.details}${infraFailure.meta?.recoveryBlocked ? ` — ${infraFailure.meta.recoveryBlocked}` : ""}`, "infra", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
6526
|
-
return;
|
|
6527
|
-
}
|
|
6528
|
-
// OBS-1007: a cap trip is a typed park of its own — the diff cannot shrink by retrying and the
|
|
6529
|
-
// only honest verb is a recheck under a raised cap. Decided BEFORE the authoring classifier, which
|
|
6530
|
-
// used to lift `gates.diffCap` out of the cap prose as a files[] hint.
|
|
6531
|
-
const capTrip = results.find(isDiffCapPark);
|
|
6532
|
-
if (capTrip) {
|
|
6533
|
-
await park(t, `${capTrip.gate}: ${capTrip.details}`, "diff-cap", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
6534
|
-
return;
|
|
6535
|
-
}
|
|
6536
|
-
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)))
|
|
6537
|
-
return;
|
|
6538
|
-
if (outsideScopeForced && freshOutsideScopeRed(t, gateSubject.commit)) {
|
|
6539
|
-
await park(t, outsideScopePark(t), "gate-fail", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
6540
|
-
return;
|
|
6541
|
-
}
|
|
6542
|
-
// OBS-547: who pays for this red is decided by the run's collateral prediction (see
|
|
6543
|
-
// dispositionScopeRed) — an authoring defect parks unchargeable before any accounting below.
|
|
6544
|
-
if (await dispositionScopeRed(t, results, assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode, await treeOrDiffPaths(taskBase, wt)))
|
|
6545
|
-
return;
|
|
6546
6622
|
gateFails++; // this attempt's gates failed — the one place quality degradation is verified (never inferred from attempts)
|
|
6547
6623
|
// v1.53 T3: prefer the CLI's own session id captured from this attempt's output (kimi's resume
|
|
6548
6624
|
// trailer) over the harness slot name; absent hook or no capture keeps today's slot-name id.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** A covered git invocation refused before spawn; `path` names the offending metadata. */
|
|
2
|
+
export declare class GitTrustRefusal extends Error {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
constructor(path: string, detail: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The canonical, independently expected common git directory for the checkout enclosing `cwd` — never what
|
|
8
|
+
* a mutable commondir says. Throws GitTrustRefusal naming the authority path when it cannot be established.
|
|
9
|
+
*/
|
|
10
|
+
export declare function trustedCommonDir(cwd: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* The uncached pre-spawn check for a covered git invocation at `cwd`. Outside any checkout it is a no-op
|
|
13
|
+
* (unrelated shell commands stay compatible). Inside one it refuses, naming the file, when the commondir git
|
|
14
|
+
* would follow resolves anywhere but the expected common directory, or when the trusted common config enables
|
|
15
|
+
* extensions.worktreeConfig and the linked gitdir holds a config.worktree.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assertGitTrust(cwd: string): void;
|
|
18
|
+
/** The inert hooks directory every protected git child receives (no hook can exist beneath it). */
|
|
19
|
+
export declare const INERT_HOOKS_PATH = "/dev/null";
|
|
20
|
+
/**
|
|
21
|
+
* A copy of `env` whose git children run with core.fsmonitor=false and an inert core.hooksPath, despite repo
|
|
22
|
+
* config and inherited GIT_CONFIG_COUNT/GIT_CONFIG_PARAMETERS: git applies GIT_CONFIG_PARAMETERS after
|
|
23
|
+
* GIT_CONFIG_COUNT and the last value wins, so the forced pair is appended last. Every other value survives.
|
|
24
|
+
* Named keys only — this does not neutralize arbitrary executable drivers. The pair is inherited by every
|
|
25
|
+
* descendant of the protected child (a suite that needs real hooks must clear it for its own git).
|
|
26
|
+
*/
|
|
27
|
+
export declare function protectedGitEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|