tickmarkr 2.4.2 → 2.5.1
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 +110 -24
- package/dist/adapters/fake.d.ts +1 -0
- package/dist/adapters/fake.js +2 -1
- package/dist/adapters/opencode.d.ts +1 -0
- package/dist/adapters/opencode.js +4 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.js +25 -0
- package/dist/cli/commands/approve.d.ts +42 -0
- package/dist/cli/commands/approve.js +80 -13
- package/dist/cli/commands/doctor.d.ts +234 -0
- package/dist/cli/commands/doctor.js +139 -5
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +45 -5
- package/dist/cli/commands/plan.js +58 -6
- package/dist/cli/commands/report.js +18 -45
- package/dist/cli/commands/run.js +3 -0
- package/dist/cli/commands/scope.js +36 -6
- package/dist/cli/commands/stats.d.ts +2 -0
- package/dist/cli/commands/stats.js +42 -23
- package/dist/cli/commands/status.js +20 -4
- package/dist/cli/commands/ui.js +42 -53
- package/dist/cli/commands/unlock.d.ts +1 -1
- package/dist/cli/commands/unlock.js +59 -9
- package/dist/cli/help.d.ts +219 -0
- package/dist/cli/help.js +212 -0
- package/dist/cli/index.d.ts +42 -2
- package/dist/cli/index.js +23 -8
- package/dist/drivers/herdr.d.ts +7 -2
- package/dist/drivers/herdr.js +78 -48
- package/dist/drivers/orca.d.ts +3 -2
- package/dist/drivers/orca.js +43 -4
- package/dist/drivers/subprocess.d.ts +1 -0
- package/dist/drivers/subprocess.js +3 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/gates/artifact-manifest.d.ts +50 -0
- package/dist/gates/artifact-manifest.js +23 -0
- package/dist/gates/llm.d.ts +5 -0
- package/dist/gates/llm.js +188 -3
- package/dist/gates/review.d.ts +6 -3
- package/dist/gates/review.js +79 -33
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +14 -4
- package/dist/plan/scope.d.ts +25 -0
- package/dist/plan/scope.js +92 -12
- package/dist/report/operator-record.d.ts +49 -0
- package/dist/report/operator-record.js +137 -0
- package/dist/run/daemon.d.ts +5 -5
- package/dist/run/daemon.js +166 -44
- package/dist/run/lock.d.ts +59 -2
- package/dist/run/lock.js +184 -26
- package/dist/run/operator-state.d.ts +86 -0
- package/dist/run/operator-state.js +165 -0
- package/dist/run/supervision.d.ts +32 -0
- package/dist/run/supervision.js +138 -17
- package/dist/tui/cockpit/capture.d.ts +19 -0
- package/dist/tui/cockpit/capture.js +89 -1
- package/dist/tui/cockpit/components.d.ts +15 -1
- package/dist/tui/cockpit/components.js +79 -9
- package/dist/tui/cockpit/decision-actions.d.ts +147 -0
- package/dist/tui/cockpit/decision-actions.js +315 -0
- package/dist/tui/cockpit/derive.d.ts +1 -1
- package/dist/tui/cockpit/derive.js +2 -0
- package/dist/tui/cockpit/evidence-view.d.ts +119 -0
- package/dist/tui/cockpit/evidence-view.js +210 -0
- package/dist/tui/cockpit/home-view.d.ts +88 -0
- package/dist/tui/cockpit/home-view.js +240 -0
- package/dist/tui/cockpit/keys.d.ts +125 -0
- package/dist/tui/cockpit/keys.js +31 -0
- package/dist/tui/cockpit/layout.d.ts +14 -0
- package/dist/tui/cockpit/layout.js +15 -0
- package/dist/tui/cockpit/live-runtime.d.ts +46 -0
- package/dist/tui/cockpit/live-runtime.js +683 -0
- package/dist/tui/cockpit/live-store.d.ts +289 -0
- package/dist/tui/cockpit/live-store.js +308 -0
- package/dist/tui/cockpit/live.d.ts +21 -1
- package/dist/tui/cockpit/live.js +12 -1
- package/dist/tui/cockpit/run-view.d.ts +100 -0
- package/dist/tui/cockpit/run-view.js +202 -0
- package/dist/tui/cockpit/shell.d.ts +50 -0
- package/dist/tui/cockpit/shell.js +74 -0
- package/dist/tui/cockpit/theme.d.ts +27 -0
- package/dist/tui/cockpit/theme.js +21 -0
- package/dist/tui/ink/init-app.d.ts +4 -0
- package/dist/tui/ink/init-app.js +18 -7
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -1
- package/skills/tickmarkr-loop/SKILL.md +72 -1
- package/skills/tickmarkr-overseer/SKILL.md +12 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +13 -2
package/dist/run/daemon.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { stringify } from "yaml";
|
|
9
9
|
import { classifyDeadChannel, NO_TRAILER_SUMMARY, trailerPattern, UNPARSEABLE_TRAILER_SUMMARY, writePrompt } from "../adapters/prompt.js";
|
|
10
10
|
import { allAdapters, getAdapter, probeAll, readDoctor, rolePools } from "../adapters/registry.js";
|
|
11
|
-
import { addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
|
|
11
|
+
import { SettledTrailerTracker, addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
|
|
12
12
|
import { bannerShell, paneDispatchCommand } from "../brand.js";
|
|
13
13
|
import { collateralHits } from "../compile/collateral.js";
|
|
14
14
|
import { DEFAULT_DIFF_CAP, globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
|
|
@@ -27,14 +27,25 @@ import { runEnvironment } from "./environment.js";
|
|
|
27
27
|
import { cleanupRunWorktrees, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, preserveWorktree, resolvedCapacity, runWithForkBudget, sameCapacity, sh, shGit, SUITE_PARENT_ENV, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
28
28
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
29
29
|
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingRechecks, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
30
|
-
import { isDiffCapPark } from "../gates/review.js";
|
|
30
|
+
import { isDiffCapPark, pickReviewer } from "../gates/review.js";
|
|
31
31
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
32
32
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
33
33
|
import { nextChannel, route } from "../route/router.js";
|
|
34
34
|
import { desiredPanes } from "./reconcile.js";
|
|
35
35
|
import { harvestCpuFlatWindowMs, NUDGEABLE_ADAPTERS, PANE_READ_ROWS, StallProgressTracker, stallSnapshotBannerRows, WorkerTreeCpuAccountant, } from "./stall.js";
|
|
36
|
-
//
|
|
37
|
-
|
|
36
|
+
// The live set is also the ownership claim shared by task cleanup and termination.
|
|
37
|
+
export async function closeLiveSlot(liveSlots, driver, slot) {
|
|
38
|
+
if (!liveSlots.delete(slot))
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
await driver.close(slot);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
liveSlots.add(slot);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Compatibility exports: gate dispatch can import stall.ts without importing the daemon.
|
|
38
49
|
export { harvestCpuFlatWindowMs, resetHarvestCpuFlatMsForTests, setHarvestCpuFlatMsForTests, workerTreeCpuMs, } from "./stall.js";
|
|
39
50
|
const MODE_RANK = { "staff-led": 0, "risk-based": 1, "partner-led": 2 };
|
|
40
51
|
// An override (flag/spec) re-resolves through loadConfigWithMode itself, via a synthesized repo overlay
|
|
@@ -134,12 +145,11 @@ export function formatSummary(s) {
|
|
|
134
145
|
: "";
|
|
135
146
|
return `done: ${s.done.length}, failed: ${s.failed.length}, human: ${s.human.length}, blocked: ${s.blocked.length}, pending: ${s.pending.length}\nintegration branch: ${s.branch}${tip}${outstanding}`;
|
|
136
147
|
}
|
|
137
|
-
/** The narrator
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* the wrong run is a recorded incident (skills/tickmarkr-overseer/SKILL.md). */
|
|
148
|
+
/** The narrator enters the production Run cockpit for this exact run. The
|
|
149
|
+
* completed static/growing four-hour records precede this default cutover;
|
|
150
|
+
* an explicit ID prevents a newer journal from redirecting the owned board. */
|
|
141
151
|
export const daemonEntrypoint = fileURLToPath(new URL("../cli/index.js", import.meta.url));
|
|
142
|
-
export const watchCommand = (runId) => `${shq(process.execPath)} ${shq(daemonEntrypoint)}
|
|
152
|
+
export const watchCommand = (runId) => `${shq(process.execPath)} ${shq(daemonEntrypoint)} ui ${shq(runId)} --view run`;
|
|
143
153
|
const MAX_ATTEMPTS = 10; // ponytail: hard cap so a pathological ladder can never loop forever
|
|
144
154
|
// v1.85 T3 (retry economics): two repairs per engagement, then the fresh ladder. A repair re-uses the
|
|
145
155
|
// findings and the landed diff instead of re-buying onboarding; when two of them have not closed the
|
|
@@ -1265,16 +1275,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1265
1275
|
// opens/closes keep the ledger exact. Termination then closes exactly what is still live — and a
|
|
1266
1276
|
// slot closed once (task-done, quota reroute, gate self-clean) can never be closed twice.
|
|
1267
1277
|
const liveSlots = new Set();
|
|
1268
|
-
const closeSlot =
|
|
1269
|
-
if (!liveSlots.delete(s))
|
|
1270
|
-
return; // already closed — never twice
|
|
1271
|
-
await driver.close(s);
|
|
1272
|
-
};
|
|
1278
|
+
const closeSlot = (s) => closeLiveSlot(liveSlots, driver, s);
|
|
1273
1279
|
const trackedDriver = {
|
|
1274
1280
|
id: driver.id,
|
|
1275
1281
|
interactive: driver.interactive,
|
|
1276
1282
|
...(driver.readSource ? { readSource: driver.readSource } : {}),
|
|
1277
1283
|
...(driver.describe ? { describe: driver.describe.bind(driver) } : {}),
|
|
1284
|
+
...(driver.focus ? { focus: driver.focus.bind(driver) } : {}),
|
|
1278
1285
|
slot: async (cwd, name, o) => { const s = await driver.slot(cwd, name, o); liveSlots.add(s); return s; },
|
|
1279
1286
|
run: (s, cmd) => driver.run(s, cmd),
|
|
1280
1287
|
waitOutput: (s, p, ms, o) => driver.waitOutput(s, p, ms, o),
|
|
@@ -1330,7 +1337,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1330
1337
|
// new attempt while this reaper is still closing the old ones.
|
|
1331
1338
|
abortRun(new Error(`terminated by ${sig}`));
|
|
1332
1339
|
if (cfg.visibility.keepPanes !== "forever") {
|
|
1333
|
-
for (const s of liveSlots) { //
|
|
1340
|
+
for (const s of Array.from(liveSlots)) { // snapshot: failed closes restore membership for the next sweep
|
|
1334
1341
|
try {
|
|
1335
1342
|
await closeSlot(s);
|
|
1336
1343
|
}
|
|
@@ -1349,6 +1356,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1349
1356
|
process.on("SIGINT", onTermination);
|
|
1350
1357
|
process.on("SIGTERM", onTermination);
|
|
1351
1358
|
journal = opts.resume ? Journal.open(repoRoot, runId, opts.narrate) : Journal.create(repoRoot, runId, opts.narrate);
|
|
1359
|
+
const demotedReviewers = new Set(journal.read()
|
|
1360
|
+
.filter((event) => event.event === "review-pool-demotion" && typeof event.data.reviewer === "string")
|
|
1361
|
+
.map((event) => event.data.reviewer));
|
|
1352
1362
|
const reviewHistory = journal.read()
|
|
1353
1363
|
.filter((event) => event.event === "gate-result" && event.data.gate === "review" && typeof event.data.reviewer === "string")
|
|
1354
1364
|
.map((event) => event.data.reviewer);
|
|
@@ -1392,7 +1402,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1392
1402
|
watchSlot = await trackedDriver.narrator(repoRoot, watchCommand(runId), runId);
|
|
1393
1403
|
}
|
|
1394
1404
|
catch (error) {
|
|
1395
|
-
|
|
1405
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1406
|
+
journal.append("watch-placement-failed", undefined, { error: reason });
|
|
1407
|
+
console.error(`tickmarkr: narrator not opened: ${reason}`);
|
|
1396
1408
|
}
|
|
1397
1409
|
};
|
|
1398
1410
|
let baseRef;
|
|
@@ -1550,12 +1562,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1550
1562
|
// (no run identity) is never this run's to sweep.
|
|
1551
1563
|
if (watchSlot && watchSlot.name === watchName && !desired.has(watchName)) {
|
|
1552
1564
|
const w = watchSlot;
|
|
1553
|
-
watchSlot = undefined;
|
|
1554
1565
|
await trackedDriver.close(w);
|
|
1566
|
+
watchSlot = undefined;
|
|
1555
1567
|
}
|
|
1556
1568
|
}
|
|
1557
|
-
catch {
|
|
1558
|
-
|
|
1569
|
+
catch (error) {
|
|
1570
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1571
|
+
journal.append("watch-cleanup-failed", undefined, { error: reason });
|
|
1572
|
+
console.error(`tickmarkr: ${reason}`);
|
|
1559
1573
|
}
|
|
1560
1574
|
};
|
|
1561
1575
|
// run start/resume boundary: nothing in flight, so the sweep takes this run's judge/review/consult
|
|
@@ -1811,6 +1825,35 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1811
1825
|
if (rs)
|
|
1812
1826
|
journal.append("resume-restore", t.id, { attempts: rs.attempts, tried: [...tried], assignment });
|
|
1813
1827
|
const badReviewers = []; // v1.1: reviewer channels that produced unparseable output for this task
|
|
1828
|
+
const noteReviewEvent = (e) => {
|
|
1829
|
+
journal.append(e.name, t.id, e.payload);
|
|
1830
|
+
if (e.name === "review-no-verdict" && typeof e.payload.reviewer === "string") {
|
|
1831
|
+
if (!badReviewers.includes(e.payload.reviewer))
|
|
1832
|
+
badReviewers.push(e.payload.reviewer);
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
// Keep the measured siblings, including a red judge, while replacing only a missing review.
|
|
1836
|
+
// Every dispatched seat is excluded for this task, so the loop is bounded by the eligible pool.
|
|
1837
|
+
const runReviewRecovery = async (task, ctx) => {
|
|
1838
|
+
const round = await runGates(task, ctx);
|
|
1839
|
+
let review = round.results.find((g) => g.gate === "review");
|
|
1840
|
+
while (review?.meta?.noVerdict === true) {
|
|
1841
|
+
const next = pickReviewer(ctx.author, ctx.channels, badReviewers, cfg.review.prefer ?? [], task.routingHints?.floor, reviewHistory, undefined, demotedReviewers);
|
|
1842
|
+
if (!next)
|
|
1843
|
+
break;
|
|
1844
|
+
journal.append("review-infra-retry", t.id, { reviewer: channelKey(next), cause: review.meta.cause });
|
|
1845
|
+
const retried = await runGates({ ...task, gates: ["review"] }, ctx);
|
|
1846
|
+
const replacement = retried.results.find((g) => g.gate === "review");
|
|
1847
|
+
// A dirty tree or another pre-dispatch refusal must still fail closed.
|
|
1848
|
+
if (!replacement) {
|
|
1849
|
+
round.results.push(...retried.results.filter((g) => !gateSatisfied(g)));
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
round.results = round.results.map((g) => g.gate === "review" ? replacement : g);
|
|
1853
|
+
review = replacement;
|
|
1854
|
+
}
|
|
1855
|
+
return round;
|
|
1856
|
+
};
|
|
1814
1857
|
// v1.70 T5 (review-convergence): failed review rounds this task has drawn, counted from the per-task
|
|
1815
1858
|
// review history already in the journal — the SAME review gate-result stream onGate reads to grow the
|
|
1816
1859
|
// reviewer-exclusion list (badReviewers), never a second parallel counter. OBS-189: scoped to the
|
|
@@ -1835,7 +1878,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1835
1878
|
// One helper, both onGate sites (satisfied-gate resume + main attempt loop).
|
|
1836
1879
|
let gateSubject;
|
|
1837
1880
|
const journalGateResult = (g) => {
|
|
1838
|
-
const blocking = gateFailed(g) && (g.gate === "review" || g.gate === "acceptance");
|
|
1881
|
+
const blocking = g.meta?.infra !== true && gateFailed(g) && (g.gate === "review" || g.gate === "acceptance");
|
|
1839
1882
|
// T2: a review that PASSED while DEFERRING a concern still recorded a defect — the prompt
|
|
1840
1883
|
// promises the deferral is recorded and never dropped, and a details string is not a record a
|
|
1841
1884
|
// later round can match. The blocking projection above is the only writer of structured
|
|
@@ -1856,12 +1899,17 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1856
1899
|
// The legacy baseline declines (a build command the repo never configured) have always written
|
|
1857
1900
|
// `pass: true` beside `skipped: true` and every consumer already reads them right, so their row
|
|
1858
1901
|
// is untouched: only a decline that would otherwise be recorded RED changes shape here.
|
|
1859
|
-
|
|
1902
|
+
// A no-verdict review is journaled `infra` + `skipped` with NO `pass`: the canonical reader
|
|
1903
|
+
// (outcome.ts) files it as infra regardless, the `pass === false` readers (rounds, briefs,
|
|
1904
|
+
// park kind) never charge it, and outstandingReviewFindings (journal.ts:816) skips a `skipped`
|
|
1905
|
+
// row instead of reading a missing `pass` as a PASS that settles every open blocking finding.
|
|
1906
|
+
const noVerdictReview = g.gate === "review" && g.meta?.noVerdict === true;
|
|
1907
|
+
const unverdicted = (g.meta?.skipped === true && !g.pass) || noVerdictReview;
|
|
1860
1908
|
journal.append("gate-result", t.id, {
|
|
1861
1909
|
gate: g.gate, ...(unverdicted ? {} : { pass: g.pass }), details: g.details,
|
|
1862
1910
|
...(gateSubject ? { commit: gateSubject.commit, attempt: gateSubject.attempt } : {}),
|
|
1863
1911
|
...(gateSubject?.replayMeasurement ? { replayMeasurement: true } : {}),
|
|
1864
|
-
...(g.meta?.skipped === true ? { skipped: true } : {}),
|
|
1912
|
+
...(g.meta?.skipped === true || noVerdictReview ? { skipped: true } : {}),
|
|
1865
1913
|
// T9: an infra-only exit is journaled AS one. The operator reading a red `test` row has to
|
|
1866
1914
|
// be able to tell "the suite found a defect" from "the runner never ran", and the merge
|
|
1867
1915
|
// predicate's reason for refusing has to be legible in the ledger it refused from.
|
|
@@ -1887,6 +1935,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1887
1935
|
...(g.meta?.reapedGroup === true ? { reapedGroup: true } : {}),
|
|
1888
1936
|
...(g.gate === "review" && typeof g.meta?.reviewer === "string" ? {
|
|
1889
1937
|
reviewer: g.meta.reviewer,
|
|
1938
|
+
...Object.fromEntries(["cause", "seatAuthoredBytes", "bytes", "rawPath", "briefPath", "timeoutMs", "unparseable", "noVerdict", "resolved", "reraised"]
|
|
1939
|
+
.filter((key) => g.meta?.[key] !== undefined).map((key) => [key, g.meta[key]])),
|
|
1890
1940
|
...(typeof g.meta.vendor === "string" ? { vendor: g.meta.vendor } : {}),
|
|
1891
1941
|
...(typeof g.meta.provider === "string" ? { provider: g.meta.provider } : {}),
|
|
1892
1942
|
...(typeof g.meta.rotationSeat === "number" ? { rotationSeat: g.meta.rotationSeat } : {}),
|
|
@@ -1896,7 +1946,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1896
1946
|
// where work is allowed, not where this verdict found the defect.
|
|
1897
1947
|
...(blocking ? {
|
|
1898
1948
|
taskContentDigest: contentDigest,
|
|
1899
|
-
findings: structuredFindings(g.gate, g.details),
|
|
1949
|
+
findings: Array.isArray(g.meta?.findings) ? g.meta.findings : structuredFindings(g.gate, g.details),
|
|
1900
1950
|
} : deferred.length > 0 ? {
|
|
1901
1951
|
taskContentDigest: contentDigest,
|
|
1902
1952
|
findings: deferred,
|
|
@@ -2262,7 +2312,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2262
2312
|
};
|
|
2263
2313
|
await trackedDriver.project?.(t.id, "in-review");
|
|
2264
2314
|
journal.phaseStart(t.id, "gates");
|
|
2265
|
-
const { results } = await withSuiteWindow(t.id, resumedTask.gates.includes("test") && commands.test !== undefined, () =>
|
|
2315
|
+
const { results } = await withSuiteWindow(t.id, resumedTask.gates.includes("test") && commands.test !== undefined, () => runReviewRecovery(resumedTask, {
|
|
2316
|
+
carriedFindings: outstandingReviewFindings(journal.read(), t.id),
|
|
2266
2317
|
worktree: wt, baseRef: taskBase, result: priorResult, author: gateAuthor,
|
|
2267
2318
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
|
|
2268
2319
|
collateral: collateral.get(t.id) ?? [],
|
|
@@ -2278,7 +2329,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2278
2329
|
}
|
|
2279
2330
|
: undefined,
|
|
2280
2331
|
excludeReviewers: badReviewers,
|
|
2281
|
-
reviewHistory,
|
|
2332
|
+
reviewHistory, demotedReviewers,
|
|
2282
2333
|
onGate: async (e) => {
|
|
2283
2334
|
if (e.phase === "start") {
|
|
2284
2335
|
notePhaseStart(e);
|
|
@@ -2286,7 +2337,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2286
2337
|
return;
|
|
2287
2338
|
}
|
|
2288
2339
|
if (e.phase === "note") {
|
|
2289
|
-
|
|
2340
|
+
noteReviewEvent(e);
|
|
2290
2341
|
return;
|
|
2291
2342
|
}
|
|
2292
2343
|
const g = e.result;
|
|
@@ -2313,6 +2364,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2313
2364
|
graph = addEvidence(graph, t.id, { commits: approvedCommits, gateResults: results });
|
|
2314
2365
|
saveGraph(repoRoot, graph);
|
|
2315
2366
|
if (!results.every(gateSatisfied)) {
|
|
2367
|
+
const infra = results.find((g) => g.meta?.infra === true);
|
|
2368
|
+
if (infra) {
|
|
2369
|
+
await park(t, `${infra.gate}: ${infra.details}`, "infra", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2316
2372
|
// OBS-547: disposition FIRST, and through the same helper the ordinary attempt path uses. A
|
|
2317
2373
|
// crash between a journaled scope red and its classification must not turn a predicted red
|
|
2318
2374
|
// into a charged one just because a resume is what observed it.
|
|
@@ -2725,6 +2781,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2725
2781
|
journal.append("worker-launch", t.id, {
|
|
2726
2782
|
attempt,
|
|
2727
2783
|
retryMode,
|
|
2784
|
+
driver: trackedDriver.id,
|
|
2785
|
+
slot: { ...slot },
|
|
2786
|
+
workspace: driver.id === "herdr" ? process.env.HERDR_WORKSPACE_ID : undefined,
|
|
2728
2787
|
...(trackedDriver.describe ? await trackedDriver.describe(slot) : {}),
|
|
2729
2788
|
});
|
|
2730
2789
|
};
|
|
@@ -2863,6 +2922,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2863
2922
|
let startupFailure = false;
|
|
2864
2923
|
let deadWorkerPark;
|
|
2865
2924
|
let settleParsed;
|
|
2925
|
+
const trailerFrames = interactive && adapter.busyFrameMarkers
|
|
2926
|
+
? new SettledTrailerTracker(adapter.busyFrameMarkers) : undefined;
|
|
2927
|
+
const sampleTrailer = (frame) => {
|
|
2928
|
+
const trailer = new RegExp(trailerPattern(nonce)).test(frame);
|
|
2929
|
+
return trailerFrames ? trailerFrames.sample(frame, trailer) : trailer;
|
|
2930
|
+
};
|
|
2866
2931
|
let seedResult;
|
|
2867
2932
|
// v1.22 T5 / OBS-19: auto-answer a fingerprint-matched trust dialog exactly once per slot.
|
|
2868
2933
|
// Any other blocked/idle dialog pages the operator (unlatched since T1 — see below).
|
|
@@ -3028,14 +3093,22 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3028
3093
|
// own source, where "TICKMARKR_EXIT:" is a string literal) must not end the wait. Only a
|
|
3029
3094
|
// parseable trailer or a digit-suffixed exit marker in the harvest is completion.
|
|
3030
3095
|
output = await driver.read(slot, PANE_READ_ROWS); // TUI transcripts carry chrome — read deeper than print's 500
|
|
3031
|
-
finished =
|
|
3096
|
+
finished = sampleTrailer(output);
|
|
3032
3097
|
const exit = exitRe.exec(output);
|
|
3033
|
-
if (finished || exit) {
|
|
3098
|
+
if (finished || (exit && !(trailerFrames && new RegExp(trailerPattern(nonce)).test(output)))) {
|
|
3034
3099
|
exitCode = exit ? Number(exit[1]) : null; // null ⇔ the TUI is still alive
|
|
3035
3100
|
processExited = exit !== null;
|
|
3036
3101
|
await sampleContext(); // final poll-seam sample before leaving the wait
|
|
3037
3102
|
break;
|
|
3038
3103
|
}
|
|
3104
|
+
if (trailerFrames && new RegExp(trailerPattern(nonce)).test(output)) {
|
|
3105
|
+
// A matching busy frame is still a live turn. Preserve the rolling progress budget.
|
|
3106
|
+
await sampleContext();
|
|
3107
|
+
if (stallProgress.observe({ paneText: output, contextTokens }))
|
|
3108
|
+
lastProgressAt = Date.now();
|
|
3109
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(250, remaining)));
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3039
3112
|
}
|
|
3040
3113
|
// A failed pane read is absence of evidence, never evidence of an absent pane. Keep the
|
|
3041
3114
|
// rolling taskTimeoutMinutes window as the backstop and name the held probe once.
|
|
@@ -3070,6 +3143,16 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3070
3143
|
await new Promise((r) => setTimeout(r, Math.min(slice - spent, 1_000)));
|
|
3071
3144
|
continue;
|
|
3072
3145
|
}
|
|
3146
|
+
// waitOutput is only a wake hint; a driver may miss a trailer already painted.
|
|
3147
|
+
if (trailerFrames && sampleTrailer(paneText)) {
|
|
3148
|
+
output = paneText;
|
|
3149
|
+
finished = true;
|
|
3150
|
+
const exit = exitRe.exec(output);
|
|
3151
|
+
exitCode = exit ? Number(exit[1]) : null;
|
|
3152
|
+
processExited = exit !== null;
|
|
3153
|
+
await sampleContext();
|
|
3154
|
+
break;
|
|
3155
|
+
}
|
|
3073
3156
|
if (paneText.length > 0)
|
|
3074
3157
|
everHadOutput = true;
|
|
3075
3158
|
if (startupFailureInWindow(paneText, workerLaunchedAt)) {
|
|
@@ -3421,9 +3504,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3421
3504
|
// un-answered nudge instead of the remainder of the window.
|
|
3422
3505
|
nudgeDeadline = undefined;
|
|
3423
3506
|
output = await driver.read(slot, PANE_READ_ROWS);
|
|
3424
|
-
finished =
|
|
3507
|
+
finished = sampleTrailer(output);
|
|
3425
3508
|
const exit = exitRe.exec(output);
|
|
3426
|
-
if (finished || exit) {
|
|
3509
|
+
if (finished || (exit && !(trailerFrames && new RegExp(trailerPattern(nonce)).test(output)))) {
|
|
3427
3510
|
exitCode = exit ? Number(exit[1]) : null;
|
|
3428
3511
|
processExited = exit !== null;
|
|
3429
3512
|
await sampleContext();
|
|
@@ -3468,12 +3551,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3468
3551
|
// The poll loop already recorded the unreadable pane. Retain the last readable bytes
|
|
3469
3552
|
// so this ambiguous path still reaches the ordinary timeout/consult backstop.
|
|
3470
3553
|
}
|
|
3471
|
-
finished = new RegExp(trailerPattern(nonce)).test(output);
|
|
3554
|
+
finished = trailerFrames ? trailerFrames.settled : new RegExp(trailerPattern(nonce)).test(output);
|
|
3472
3555
|
const exit = exitRe.exec(output);
|
|
3473
3556
|
exitCode = exit ? Number(exit[1]) : null;
|
|
3474
3557
|
processExited = exit !== null;
|
|
3475
3558
|
}
|
|
3476
|
-
if (finished) {
|
|
3559
|
+
if (finished && !trailerFrames) {
|
|
3477
3560
|
await driver.waitAgentStatus(slot, "idle", 5_000); // settle, then re-harvest the final render
|
|
3478
3561
|
output = await driver.read(slot, PANE_READ_ROWS);
|
|
3479
3562
|
}
|
|
@@ -3493,11 +3576,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3493
3576
|
break;
|
|
3494
3577
|
await new Promise((r) => setTimeout(r, Math.min(settleDelayMs, remaining)));
|
|
3495
3578
|
output = await driver.read(slot, PANE_READ_ROWS);
|
|
3579
|
+
trailerFrames?.sample(output, new RegExp(trailerPattern(nonce)).test(output));
|
|
3496
3580
|
settleParsed = adapter.parse(output, nonce);
|
|
3497
3581
|
settleTries++;
|
|
3498
3582
|
}
|
|
3499
3583
|
if (settleParsed.summary !== UNPARSEABLE_TRAILER_SUMMARY) {
|
|
3500
|
-
finished = settleParsed.summary !== NO_TRAILER_SUMMARY;
|
|
3584
|
+
finished = settleParsed.summary !== NO_TRAILER_SUMMARY && (!trailerFrames || trailerFrames.settled);
|
|
3501
3585
|
}
|
|
3502
3586
|
}
|
|
3503
3587
|
}
|
|
@@ -3605,13 +3689,18 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3605
3689
|
// encoded and therefore invisible to the raw wait regex. Keep `output` bounded: every text
|
|
3606
3690
|
// classifier below retains its pre-artifact tail surface.
|
|
3607
3691
|
let result = settleParsed ?? adapter.parse(capturedStream.output, nonce);
|
|
3692
|
+
const unsettledTrailer = trailerFrames && !trailerFrames.settled
|
|
3693
|
+
&& (trailerFrames.busyMarkersSeen.size > 0 || new RegExp(trailerPattern(nonce)).test(capturedStream.output));
|
|
3694
|
+
if (unsettledTrailer) {
|
|
3695
|
+
result = { ok: false, summary: `premature-idle: trailer did not settle; busy markers: ${[...trailerFrames.busyMarkersSeen].join(", ") || "awaiting two idle samples"}`, deviations: [], raw: output };
|
|
3696
|
+
}
|
|
3608
3697
|
const adapterCause = result.cause;
|
|
3609
3698
|
// Law 46: there is one completion fact. A classified adapter result with no parse-failure cause
|
|
3610
3699
|
// means its decoder found the nonce-bound trailer even when the raw JSON envelope escaped it.
|
|
3611
|
-
const workerFinished = finished || adapterCause === undefined;
|
|
3700
|
+
const workerFinished = !unsettledTrailer && (finished || adapterCause === undefined);
|
|
3612
3701
|
finished = workerFinished;
|
|
3613
3702
|
const adapterStartupFailure = adapterCause === "startup-failure";
|
|
3614
|
-
const workerCause = adapterStartupFailure
|
|
3703
|
+
const workerCause = unsettledTrailer ? "premature-idle" : adapterStartupFailure
|
|
3615
3704
|
? "startup-failure"
|
|
3616
3705
|
: startupFailure
|
|
3617
3706
|
? "startup-failure"
|
|
@@ -3700,9 +3789,24 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3700
3789
|
// never certifies the channel. Reading the synthesized `finished`/`ok` here reset the streak
|
|
3701
3790
|
// on every harvest, so a CLI that produces commits and swallows every trailer was immune to
|
|
3702
3791
|
// the two-window demotion and stayed first pick for the rest of the run.
|
|
3792
|
+
// Q-1 (OBS-926): BOTH branches read the same tail — a print stream tested whole failed a
|
|
3793
|
+
// pinned codex worker over for a diff line number (`429:`) 41 985 bytes before EOF and for
|
|
3794
|
+
// this repository's own fixture prose. The matched bytes and their stream offset ride the
|
|
3795
|
+
// row so a false positive is legible from the record alone.
|
|
3796
|
+
const quotaMatch = (interactive ? !workerFinished : exitCode !== 0)
|
|
3797
|
+
? QUOTA_RE.exec(stallSnapshotBannerRows(output))
|
|
3798
|
+
: null;
|
|
3799
|
+
// Q-1: a graph pin is an operator instruction — a quota match ALONE never overrides it; only a
|
|
3800
|
+
// channel-attributed error (auth/setup/outage/timeout, the typed dead-channel classes) may.
|
|
3801
|
+
const pin = t.routingHints?.pin;
|
|
3802
|
+
const onPin = !!pin && assignment.adapter === pin.via && assignment.model === pin.model;
|
|
3803
|
+
const channelError = quotaMatch && onPin ? classifyDeadChannel({ ...preHarvestResult, raw: output }) : undefined;
|
|
3804
|
+
const pinRefusesQuota = !!quotaMatch && onPin && !channelError;
|
|
3805
|
+
// a refused quota window is not a no-trailer verdict on the pinned channel: counting it would
|
|
3806
|
+
// demote the pin two tails in and the demotion re-dispatch would move the task off it.
|
|
3703
3807
|
if (preHarvestResult.ok && workerFinished)
|
|
3704
3808
|
noTrailerStreak.set(channelKey(assignment), 0);
|
|
3705
|
-
else if (!workerFinished && cause !== "provider-death") {
|
|
3809
|
+
else if (!workerFinished && cause !== "provider-death" && !pinRefusesQuota) {
|
|
3706
3810
|
const ck = channelKey(assignment);
|
|
3707
3811
|
const streak = (noTrailerStreak.get(ck) ?? 0) + 1;
|
|
3708
3812
|
noTrailerStreak.set(ck, streak);
|
|
@@ -3732,12 +3836,29 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3732
3836
|
// T2 review: the gate is `workerFinished`, not the harvest-synthesized `finished` — a
|
|
3733
3837
|
// committed-but-quota-walled attempt routes here FIRST (its commits ride the carry-forward
|
|
3734
3838
|
// into the next attempt's recreated worktree), it never buys a gate run on throttled work.
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
if (
|
|
3839
|
+
// Q-1 review: NO cap and NO fall-through to the consult ladder (failoverOrRecycle ignores the
|
|
3840
|
+
// pin) — the attempt is counted, so a pin that never clears parks at MAX_ATTEMPTS on its own
|
|
3841
|
+
// channel; only a channel-attributed error below may leave it.
|
|
3842
|
+
if (pinRefusesQuota) {
|
|
3843
|
+
journal.append("quota-failover-refused", t.id, {
|
|
3844
|
+
pin: `${pin.via}:${pin.model}`, channel: channelKey(assignment), matched: quotaMatch[0],
|
|
3845
|
+
reason: "pinned channel; no channel-attributed error", attempt,
|
|
3846
|
+
});
|
|
3847
|
+
await new Promise((r) => setTimeout(r, PROVIDER_DEATH_BACKOFF_MS));
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
if (quotaMatch && (!onPin || channelError)) {
|
|
3739
3851
|
const next = failover("quota-failover");
|
|
3740
|
-
|
|
3852
|
+
// byte offset inside the PERSISTED stream (the journaled `stream` file) — the capture keeps
|
|
3853
|
+
// only its last MAX_BUF bytes, so the file's origin may sit past the buffer's.
|
|
3854
|
+
const streamBytes = Buffer.byteLength(capturedStream.output);
|
|
3855
|
+
const idx = capturedStream.output.lastIndexOf(quotaMatch[0]);
|
|
3856
|
+
const offset = idx >= 0 ? Buffer.byteLength(capturedStream.output.slice(0, idx)) - Math.max(0, streamBytes - MAX_BUF) : null;
|
|
3857
|
+
journal.append("quota-failover", t.id, {
|
|
3858
|
+
from: channelKey(assignment), to: next ? channelKey(next) : null,
|
|
3859
|
+
matched: quotaMatch[0], offset, stream: capturedStream.path, streamBytes: Math.min(streamBytes, MAX_BUF),
|
|
3860
|
+
...(onPin ? { pin: `${pin.via}:${pin.model}`, channelError } : {}),
|
|
3861
|
+
});
|
|
3741
3862
|
if (next) {
|
|
3742
3863
|
await driver.notify(`tickmarkr ${runId}: ${t.id} quota failover`, { tier: "attention" });
|
|
3743
3864
|
// OBS-17 T2: the superseded slot's pane closes AT REROUTE TIME — it holds a throttled
|
|
@@ -3858,7 +3979,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3858
3979
|
return;
|
|
3859
3980
|
}
|
|
3860
3981
|
if (e.phase === "note") {
|
|
3861
|
-
|
|
3982
|
+
noteReviewEvent(e);
|
|
3862
3983
|
return;
|
|
3863
3984
|
}
|
|
3864
3985
|
const g = e.result;
|
|
@@ -3896,7 +4017,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3896
4017
|
gateSubject = { commit: await gateCommitSubject(taskBase, gated, wt), attempt };
|
|
3897
4018
|
await trackedDriver.project?.(t.id, "in-review");
|
|
3898
4019
|
journal.phaseStart(t.id, "gates");
|
|
3899
|
-
({ results, commits } = await withSuiteWindow(t.id, t.gates.includes("test") && commands.test !== undefined, () =>
|
|
4020
|
+
({ results, commits } = await withSuiteWindow(t.id, t.gates.includes("test") && commands.test !== undefined, () => runReviewRecovery(t, {
|
|
4021
|
+
carriedFindings: outstandingFindings,
|
|
3900
4022
|
worktree: wt, baseRef: taskBase, result, author: assignment,
|
|
3901
4023
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
|
|
3902
4024
|
collateral: collateral.get(t.id) ?? [],
|
|
@@ -3918,7 +4040,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3918
4040
|
}
|
|
3919
4041
|
: undefined,
|
|
3920
4042
|
excludeReviewers: badReviewers,
|
|
3921
|
-
reviewHistory,
|
|
4043
|
+
reviewHistory, demotedReviewers,
|
|
3922
4044
|
onGate,
|
|
3923
4045
|
})));
|
|
3924
4046
|
results.forEach(classifySignalOnlyTest);
|
package/dist/run/lock.d.ts
CHANGED
|
@@ -36,12 +36,69 @@ export declare function runLockOwner(repoRoot: string): {
|
|
|
36
36
|
export declare function runLockRunId(repoRoot: string): string | undefined;
|
|
37
37
|
export declare function runStatusLine(repoRoot: string, runId: string, events: readonly RunLineEvent[]): string | null;
|
|
38
38
|
export declare function isRunLockLive(repoRoot: string): boolean;
|
|
39
|
-
export
|
|
39
|
+
export interface LockSnapshot {
|
|
40
|
+
pid?: number;
|
|
41
|
+
runId?: string;
|
|
42
|
+
garbage: boolean;
|
|
43
|
+
unreadable: boolean;
|
|
44
|
+
dead: boolean;
|
|
45
|
+
ino: number;
|
|
46
|
+
mtimeMs: number;
|
|
47
|
+
raw: Buffer;
|
|
48
|
+
}
|
|
49
|
+
export type UnlockPreview = {
|
|
40
50
|
held: false;
|
|
41
51
|
} | {
|
|
42
52
|
held: true;
|
|
53
|
+
eligible: false;
|
|
54
|
+
reason: string;
|
|
55
|
+
pid?: number;
|
|
56
|
+
runId?: string;
|
|
57
|
+
} | {
|
|
58
|
+
held: true;
|
|
59
|
+
eligible: true;
|
|
60
|
+
pid: number;
|
|
61
|
+
runId: string;
|
|
62
|
+
ino: number;
|
|
63
|
+
mtimeMs: number;
|
|
64
|
+
raw: Buffer;
|
|
65
|
+
};
|
|
66
|
+
export declare function previewUnlock(repoRoot: string, runId: string): UnlockPreview;
|
|
67
|
+
export declare function commitUnlock(repoRoot: string, target: {
|
|
68
|
+
pid: number;
|
|
69
|
+
runId: string;
|
|
70
|
+
ino: number;
|
|
71
|
+
mtimeMs: number;
|
|
72
|
+
raw: Buffer;
|
|
73
|
+
}): {
|
|
43
74
|
removed: true;
|
|
75
|
+
pid: number;
|
|
76
|
+
runId: string;
|
|
77
|
+
} | {
|
|
78
|
+
removed: false;
|
|
79
|
+
reason: string;
|
|
80
|
+
};
|
|
81
|
+
export type UnlockGarbagePreview = {
|
|
82
|
+
held: false;
|
|
83
|
+
} | {
|
|
84
|
+
held: true;
|
|
85
|
+
eligible: false;
|
|
86
|
+
reason: string;
|
|
44
87
|
pid?: number;
|
|
45
88
|
runId?: string;
|
|
46
|
-
|
|
89
|
+
} | {
|
|
90
|
+
held: true;
|
|
91
|
+
eligible: true;
|
|
92
|
+
ino: number;
|
|
93
|
+
raw: Buffer;
|
|
94
|
+
};
|
|
95
|
+
export declare function previewGarbageUnlock(repoRoot: string): UnlockGarbagePreview;
|
|
96
|
+
export declare function commitGarbageUnlock(repoRoot: string, target: {
|
|
97
|
+
ino: number;
|
|
98
|
+
raw: Buffer;
|
|
99
|
+
}): {
|
|
100
|
+
removed: true;
|
|
101
|
+
} | {
|
|
102
|
+
removed: false;
|
|
103
|
+
reason: string;
|
|
47
104
|
};
|