tickmarkr 2.1.9 → 2.2.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/dist/adapters/model-lints.js +9 -0
- package/dist/adapters/pi.d.ts +1 -0
- package/dist/adapters/pi.js +15 -1
- package/dist/adapters/prompt.js +11 -3
- package/dist/adapters/registry.js +13 -4
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/commands/approve.d.ts +20 -0
- package/dist/cli/commands/approve.js +127 -35
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +14 -4
- package/dist/cli/commands/plan.js +4 -1
- package/dist/cli/commands/verify.js +2 -0
- package/dist/cli/commands/version.d.ts +2 -1
- package/dist/cli/commands/version.js +25 -4
- package/dist/compile/collateral.js +149 -4
- package/dist/compile/native.js +2 -0
- package/dist/drivers/index.d.ts +1 -0
- package/dist/drivers/index.js +25 -10
- package/dist/drivers/orca.d.ts +19 -0
- package/dist/drivers/orca.js +45 -9
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +49 -14
- package/dist/gates/llm.d.ts +1 -1
- package/dist/gates/llm.js +41 -26
- package/dist/gates/review.js +2 -0
- package/dist/gates/run-gates.d.ts +6 -0
- package/dist/gates/run-gates.js +9 -1
- package/dist/run/consult.d.ts +7 -0
- package/dist/run/consult.js +16 -2
- package/dist/run/daemon.d.ts +12 -6
- package/dist/run/daemon.js +306 -24
- package/dist/run/journal.d.ts +16 -0
- package/dist/run/journal.js +51 -6
- package/dist/tui/cockpit/setup-cockpit.d.ts +8 -10
- package/dist/tui/cockpit/setup-cockpit.js +92 -53
- package/package.json +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -0
- package/skills/tickmarkr-overseer/SKILL.md +17 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +2 -2
package/dist/gates/run-gates.js
CHANGED
|
@@ -583,6 +583,14 @@ export async function runGates(task, ctx) {
|
|
|
583
583
|
// eligible seat keeps the ORIGINAL result so the recorded cause stays truthful (OBS-196).
|
|
584
584
|
if (rv.meta?.unparseable === true && typeof rv.meta.reviewer === "string") {
|
|
585
585
|
const flaked = rv.meta.reviewer;
|
|
586
|
+
const emptyOutput = rv.meta.cause === "empty-output";
|
|
587
|
+
if (emptyOutput) {
|
|
588
|
+
await ctx.onGate?.({
|
|
589
|
+
phase: "note", gate: "review", name: "reviewer-empty-output",
|
|
590
|
+
payload: { reviewer: flaked, bytes: typeof rv.meta.bytes === "number" ? rv.meta.bytes : 0 },
|
|
591
|
+
result: { ...rv, meta: { ...rv.meta, skipped: true } },
|
|
592
|
+
});
|
|
593
|
+
}
|
|
586
594
|
const retryVia = ctx.via
|
|
587
595
|
? { ...ctx.via, nameFor: (role, adapter) => ctx.via.nameFor(role, adapter) + "-r1" }
|
|
588
596
|
: undefined;
|
|
@@ -593,7 +601,7 @@ export async function runGates(task, ctx) {
|
|
|
593
601
|
...second,
|
|
594
602
|
// `details` is lifted onto the journal's gate-result row; meta.reviewRetry is not. Keep the
|
|
595
603
|
// re-route visible in the result text a reader actually opens, including on a red retry.
|
|
596
|
-
details: `review re-route: ${flaked} produced no parseable verdict; replaced by ${retried}\n${second.details}`,
|
|
604
|
+
details: `review re-route: ${flaked} produced ${emptyOutput ? "EMPTY output" : "no parseable verdict"}; replaced by ${retried}\n${second.details}`,
|
|
597
605
|
meta: { ...second.meta, reviewRetry: { flaked, retried } },
|
|
598
606
|
};
|
|
599
607
|
}
|
package/dist/run/consult.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export interface ConsultVerdict {
|
|
|
9
9
|
reason?: string;
|
|
10
10
|
guidance?: string;
|
|
11
11
|
excludeAdapter?: string;
|
|
12
|
+
adapter?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
vendor?: string;
|
|
12
15
|
}
|
|
13
16
|
export declare function renderRetryGuidance(v: ConsultVerdict): string;
|
|
14
17
|
export declare function augmentRetryBrief(feedback: string, opts: {
|
|
@@ -36,5 +39,9 @@ export declare function consult(d: Dossier, cfg: TickmarkrConfig, adapters: Work
|
|
|
36
39
|
runId?: string;
|
|
37
40
|
channels?: Array<{
|
|
38
41
|
adapter: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
vendor?: string;
|
|
44
|
+
channel?: "sub" | "api";
|
|
45
|
+
tier?: string;
|
|
39
46
|
}>;
|
|
40
47
|
}): Promise<ConsultVerdict>;
|
package/dist/run/consult.js
CHANGED
|
@@ -205,6 +205,17 @@ opts = {}) {
|
|
|
205
205
|
// the same rule as every prefer entry — and disallowedBy carries the full deny grammar (adapter,
|
|
206
206
|
// model, or adapter:model), so a model-scoped deny cannot slip past an adapter-id-only read.
|
|
207
207
|
const allowedSeats = seats.filter((s) => disallowedBy(s, cfg.routing, "consult") === null);
|
|
208
|
+
const seatIdentity = (seat) => {
|
|
209
|
+
let vendor = opts.channels?.find((candidate) => candidate.adapter === seat.adapter && candidate.model === seat.model)?.vendor;
|
|
210
|
+
if (!vendor) {
|
|
211
|
+
try {
|
|
212
|
+
const adapter = getAdapter(seat.adapter, adapters);
|
|
213
|
+
vendor = adapter.channels(cfg).find((channel) => channel.model === seat.model)?.vendor ?? adapter.vendor;
|
|
214
|
+
}
|
|
215
|
+
catch { /* an unknown adapter still gets explicit unknown provenance */ }
|
|
216
|
+
}
|
|
217
|
+
return { adapter: seat.adapter, model: seat.model, vendor: vendor ?? "unknown" };
|
|
218
|
+
};
|
|
208
219
|
if (!allowedSeats.length) {
|
|
209
220
|
const d = disallowedBy(seats[seats.length - 1], cfg.routing, "consult");
|
|
210
221
|
return {
|
|
@@ -216,11 +227,14 @@ opts = {}) {
|
|
|
216
227
|
try {
|
|
217
228
|
const parsed = await invokeSeat(seat.adapter, seat.model, i);
|
|
218
229
|
if (parsed.verdict)
|
|
219
|
-
return parsed.verdict;
|
|
230
|
+
return { ...parsed.verdict, ...seatIdentity(seat) };
|
|
220
231
|
}
|
|
221
232
|
catch {
|
|
222
233
|
// failed seat (unknown adapter, dead driver/pane, shell error) — fall to the next entry
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
action: "human",
|
|
238
|
+
notes: "consult verdict unparseable — failing safe to human",
|
|
239
|
+
};
|
|
226
240
|
}
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type WorkerAdapter } from "../adapters/types.js";
|
|
2
2
|
import { type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../config/config.js";
|
|
3
|
+
import { type DriverChoice } from "../drivers/index.js";
|
|
3
4
|
import { type ExecutorDriver } from "../drivers/types.js";
|
|
4
5
|
import { type Baseline } from "../gates/baseline.js";
|
|
5
6
|
import type { GateResult } from "../gates/types.js";
|
|
@@ -13,6 +14,7 @@ export interface RunOptions {
|
|
|
13
14
|
retryFailed?: boolean;
|
|
14
15
|
concurrency?: number;
|
|
15
16
|
driver?: ExecutorDriver;
|
|
17
|
+
driverOverride?: DriverChoice;
|
|
16
18
|
adapters?: WorkerAdapter[];
|
|
17
19
|
globalDir?: string;
|
|
18
20
|
mode?: RoutingMode;
|
|
@@ -49,12 +51,16 @@ export interface RunSummary {
|
|
|
49
51
|
outstandingApprovals?: string[];
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
|
-
* T14: approvals the run accepted and never acted on. `approved` above is
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* T14, amended by v2.2 T3: approvals the run accepted and never acted on. `approved` above is still
|
|
55
|
+
* built ONCE at startup — replay determinism depends on it — but a live approval is no longer inert:
|
|
56
|
+
* the boundary sweep in the task loop releases what lands while the daemon runs, so an approval
|
|
57
|
+
* written mid-run is normally enacted by this run. ONE window survives, and it is the reason this
|
|
58
|
+
* fold still exists: an approval accepted after the task loop exits — during tip verify, before the
|
|
59
|
+
* run-end sample below — meets no further boundary, so nothing can release it before this run ends.
|
|
60
|
+
* Without this the run-end record stated only buckets and tipVerify, both accurate, over a milestone
|
|
61
|
+
* that was silently incomplete: run …230 ended tipVerify "passed" with two upheld approvals and zero
|
|
62
|
+
* subsequent dispatches. Scored per task on its NEWEST approval: a later approval is the live
|
|
63
|
+
* decision, and the events that answer it are the ones after it.
|
|
58
64
|
*/
|
|
59
65
|
export declare function outstandingApprovals(events: JournalEvent[]): string[];
|
|
60
66
|
export declare function formatSummary(s: RunSummary): string;
|
package/dist/run/daemon.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
3
|
import { shq } from "../adapters/types.js";
|
|
3
|
-
import { appendFileSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
6
7
|
import { stringify } from "yaml";
|
|
7
8
|
import { classifyDeadChannel, NO_TRAILER_SUMMARY, trailerPattern, UNPARSEABLE_TRAILER_SUMMARY, writePrompt } from "../adapters/prompt.js";
|
|
8
9
|
import { allAdapters, getAdapter, probeAll, readDoctor, rolePools } from "../adapters/registry.js";
|
|
@@ -11,17 +12,20 @@ import { bannerShell, paneDispatchCommand } from "../brand.js";
|
|
|
11
12
|
import { collateralHits } from "../compile/collateral.js";
|
|
12
13
|
import { DEFAULT_DIFF_CAP, globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
|
|
13
14
|
import { DeliveryReadinessError } from "../drivers/herdr.js";
|
|
15
|
+
import { driverEvidence } from "../drivers/index.js";
|
|
14
16
|
import { herdrSealShellPrefix, SubprocessDriver } from "../drivers/subprocess.js";
|
|
15
17
|
import { formatOwnedName } from "../drivers/types.js";
|
|
16
18
|
import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
|
|
17
19
|
import { runGates } from "../gates/run-gates.js";
|
|
20
|
+
import { filesGlob } from "../graph/files-glob.js";
|
|
18
21
|
import { addEvidence, attributeBlocked, blockedTasks, getTask, graphDefinitionHash, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
|
|
19
22
|
import { GATE_NAMES } from "../graph/schema.js";
|
|
23
|
+
import { distFingerprint } from "../cli/commands/version.js";
|
|
20
24
|
import { augmentRetryBrief, consult, renderRetryGuidance } from "./consult.js";
|
|
21
25
|
import { runEnvironment } from "./environment.js";
|
|
22
26
|
import { cleanupRunWorktrees, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, preserveWorktree, resolvedCapacity, runWithForkBudget, sameCapacity, sh, shGit, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
23
27
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
24
|
-
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingReviewFindings, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, renderStructuredReviewFinding, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
28
|
+
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, renderStructuredReviewFinding, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
25
29
|
import { isDiffCapPark } from "../gates/review.js";
|
|
26
30
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
27
31
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
@@ -76,12 +80,16 @@ const DISPATCH_ENACTMENT = new Set(["task-dispatch", "repair-dispatch"]);
|
|
|
76
80
|
// for the task is worktree-recreation. That event is causal for this path, not incidental.
|
|
77
81
|
const GATE_SATISFIED_ENACTMENT = "worktree-recreation";
|
|
78
82
|
/**
|
|
79
|
-
* T14: approvals the run accepted and never acted on. `approved` above is
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
* T14, amended by v2.2 T3: approvals the run accepted and never acted on. `approved` above is still
|
|
84
|
+
* built ONCE at startup — replay determinism depends on it — but a live approval is no longer inert:
|
|
85
|
+
* the boundary sweep in the task loop releases what lands while the daemon runs, so an approval
|
|
86
|
+
* written mid-run is normally enacted by this run. ONE window survives, and it is the reason this
|
|
87
|
+
* fold still exists: an approval accepted after the task loop exits — during tip verify, before the
|
|
88
|
+
* run-end sample below — meets no further boundary, so nothing can release it before this run ends.
|
|
89
|
+
* Without this the run-end record stated only buckets and tipVerify, both accurate, over a milestone
|
|
90
|
+
* that was silently incomplete: run …230 ended tipVerify "passed" with two upheld approvals and zero
|
|
91
|
+
* subsequent dispatches. Scored per task on its NEWEST approval: a later approval is the live
|
|
92
|
+
* decision, and the events that answer it are the ones after it.
|
|
85
93
|
*/
|
|
86
94
|
export function outstandingApprovals(events) {
|
|
87
95
|
const newest = new Map();
|
|
@@ -687,6 +695,202 @@ function compareWorktrees(before, after) {
|
|
|
687
695
|
return "unreadable";
|
|
688
696
|
return before.signature === after.signature ? "unchanged" : "changed";
|
|
689
697
|
}
|
|
698
|
+
const CONTEXT_GLOB_CHARS = /[*?{[]/;
|
|
699
|
+
const MATERIALIZED_CONTEXT_BYTES = 256 * 1024 * 1024;
|
|
700
|
+
function containedIn(root, path) {
|
|
701
|
+
const fromRoot = relative(resolve(root), resolve(path));
|
|
702
|
+
return fromRoot === "" || (fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot));
|
|
703
|
+
}
|
|
704
|
+
function normalizeContextPath(entry) {
|
|
705
|
+
if (entry.includes("\0"))
|
|
706
|
+
return undefined;
|
|
707
|
+
const repositoryPath = entry.replace(/\\/g, "/");
|
|
708
|
+
if (posix.isAbsolute(repositoryPath) || repositoryPath.split("/").includes(".."))
|
|
709
|
+
return undefined;
|
|
710
|
+
const normalized = posix.normalize(repositoryPath).replace(/\/+$/, "");
|
|
711
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../"))
|
|
712
|
+
return undefined;
|
|
713
|
+
return normalized;
|
|
714
|
+
}
|
|
715
|
+
function workerFilesystemEntries(worktree) {
|
|
716
|
+
const entries = [];
|
|
717
|
+
const visit = (dir, prefix) => {
|
|
718
|
+
let dirents;
|
|
719
|
+
try {
|
|
720
|
+
dirents = readdirSync(dir, { withFileTypes: true });
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
const rel = relative(worktree, dir).split(sep).join(posix.sep) || ".";
|
|
724
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
725
|
+
throw new Error(`context filesystem observation failed at ${rel}: ${message}`);
|
|
726
|
+
}
|
|
727
|
+
for (const dirent of dirents) {
|
|
728
|
+
if (dirent.name === ".git")
|
|
729
|
+
continue;
|
|
730
|
+
const rel = prefix ? `${prefix}/${dirent.name}` : dirent.name;
|
|
731
|
+
entries.push(rel);
|
|
732
|
+
if (dirent.isDirectory())
|
|
733
|
+
visit(join(dir, dirent.name), rel);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
visit(worktree, "");
|
|
737
|
+
return entries;
|
|
738
|
+
}
|
|
739
|
+
function exactContextResolvesInWorktree(entry, normalized, worktree) {
|
|
740
|
+
const fullPath = join(worktree, normalized);
|
|
741
|
+
try {
|
|
742
|
+
const stat = lstatSync(fullPath);
|
|
743
|
+
if (stat.isSymbolicLink())
|
|
744
|
+
return false;
|
|
745
|
+
if (stat.isDirectory())
|
|
746
|
+
return true;
|
|
747
|
+
return !/\/+$/.test(entry) && stat.isFile() && worktreeHeadHasRegularBlob(worktree, normalized);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
if (["EACCES", "ELOOP", "ENOENT", "ENOTDIR", "EPERM"].includes(error.code ?? ""))
|
|
751
|
+
return false;
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function patternContextResolvesInWorktree(normalized, worktree) {
|
|
756
|
+
const match = filesGlob(normalized);
|
|
757
|
+
return workerFilesystemEntries(worktree).some((path) => match(path));
|
|
758
|
+
}
|
|
759
|
+
function regularBlobObjectId(repoRoot, ref, normalized) {
|
|
760
|
+
let tree = "";
|
|
761
|
+
try {
|
|
762
|
+
tree = execFileSync("git", ["-C", repoRoot, "ls-tree", "-z", ref, "--", normalized], {
|
|
763
|
+
encoding: "utf8",
|
|
764
|
+
maxBuffer: 1024 * 1024,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return undefined;
|
|
769
|
+
}
|
|
770
|
+
const row = tree.split("\0")
|
|
771
|
+
.filter(Boolean)
|
|
772
|
+
.map((line) => /^(\d{6}) (\w+) ([0-9a-f]+)\t([\s\S]*)$/.exec(line))
|
|
773
|
+
.find((match) => match?.[4] === normalized);
|
|
774
|
+
if (!row || row[2] !== "blob" || (row[1] !== "100644" && row[1] !== "100755"))
|
|
775
|
+
return undefined;
|
|
776
|
+
return row[3];
|
|
777
|
+
}
|
|
778
|
+
function worktreeHeadHasRegularBlob(worktree, normalized) {
|
|
779
|
+
return regularBlobObjectId(worktree, "HEAD", normalized) !== undefined;
|
|
780
|
+
}
|
|
781
|
+
function committedRegularBlob(repoRoot, normalized) {
|
|
782
|
+
const objectId = regularBlobObjectId(repoRoot, "HEAD", normalized);
|
|
783
|
+
if (!objectId)
|
|
784
|
+
return undefined;
|
|
785
|
+
try {
|
|
786
|
+
const bytes = execFileSync("git", ["-C", repoRoot, "cat-file", "blob", objectId], {
|
|
787
|
+
encoding: "buffer",
|
|
788
|
+
maxBuffer: MATERIALIZED_CONTEXT_BYTES,
|
|
789
|
+
});
|
|
790
|
+
return { bytes };
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function materializedContextDestination(root, task, attempt, normalized) {
|
|
797
|
+
const key = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
798
|
+
const leaf = basename(normalized).replace(/[^A-Za-z0-9._-]/g, "_") || "context";
|
|
799
|
+
const destination = join(root, task.id, `a${attempt}`, `${key}-${leaf}`);
|
|
800
|
+
if (!containedIn(root, destination))
|
|
801
|
+
throw new Error(`materialized context destination escapes context subtree: ${normalized}`);
|
|
802
|
+
return destination;
|
|
803
|
+
}
|
|
804
|
+
function ensureDirectoryWithoutSymlink(path) {
|
|
805
|
+
let stat;
|
|
806
|
+
try {
|
|
807
|
+
stat = lstatSync(path);
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
if (error.code !== "ENOENT")
|
|
811
|
+
throw error;
|
|
812
|
+
mkdirSync(path, { mode: 0o700 });
|
|
813
|
+
stat = lstatSync(path);
|
|
814
|
+
}
|
|
815
|
+
if (stat.isSymbolicLink())
|
|
816
|
+
throw new Error(`materialized context destination ancestor is a symlink: ${path}`);
|
|
817
|
+
if (!stat.isDirectory())
|
|
818
|
+
throw new Error(`materialized context destination ancestor is not a directory: ${path}`);
|
|
819
|
+
}
|
|
820
|
+
function ensureContainedContextParent(root, destination) {
|
|
821
|
+
const rootAbs = resolve(root);
|
|
822
|
+
const destAbs = resolve(destination);
|
|
823
|
+
if (!containedIn(rootAbs, destAbs))
|
|
824
|
+
throw new Error("materialized context destination escapes context subtree");
|
|
825
|
+
const parent = dirname(destAbs);
|
|
826
|
+
ensureDirectoryWithoutSymlink(rootAbs);
|
|
827
|
+
const relParent = relative(rootAbs, parent);
|
|
828
|
+
let current = rootAbs;
|
|
829
|
+
for (const segment of relParent.split(sep).filter(Boolean)) {
|
|
830
|
+
current = join(current, segment);
|
|
831
|
+
ensureDirectoryWithoutSymlink(current);
|
|
832
|
+
}
|
|
833
|
+
if (!containedIn(realpathSync(rootAbs), realpathSync(parent))) {
|
|
834
|
+
throw new Error("materialized context destination realpath escapes context subtree");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function writeContainedContextFile(root, destination, bytes) {
|
|
838
|
+
if (!containedIn(root, destination))
|
|
839
|
+
throw new Error("materialized context destination escapes context subtree");
|
|
840
|
+
ensureContainedContextParent(root, destination);
|
|
841
|
+
const fd = openSync(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o600);
|
|
842
|
+
try {
|
|
843
|
+
writeFileSync(fd, bytes);
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
closeSync(fd);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function materializeContextEntry(repoRoot, worktree, contextRoot, task, attempt, entry) {
|
|
850
|
+
const normalized = normalizeContextPath(entry);
|
|
851
|
+
if (!normalized)
|
|
852
|
+
return { kind: "missing", path: entry, reason: "invalid-path" };
|
|
853
|
+
if (exactContextResolvesInWorktree(entry, normalized, worktree))
|
|
854
|
+
return { kind: "keep", entry };
|
|
855
|
+
const blob = committedRegularBlob(repoRoot, normalized);
|
|
856
|
+
if (blob) {
|
|
857
|
+
const destination = materializedContextDestination(contextRoot, task, attempt, normalized);
|
|
858
|
+
writeContainedContextFile(contextRoot, destination, blob.bytes);
|
|
859
|
+
return { kind: "materialized", entry: destination, normalized, destination, bytes: blob.bytes.length };
|
|
860
|
+
}
|
|
861
|
+
if (CONTEXT_GLOB_CHARS.test(entry)) {
|
|
862
|
+
if (patternContextResolvesInWorktree(normalized, worktree))
|
|
863
|
+
return { kind: "keep", entry };
|
|
864
|
+
return { kind: "missing", path: entry, reason: "unresolved-pattern" };
|
|
865
|
+
}
|
|
866
|
+
if (/\/+$/.test(entry))
|
|
867
|
+
return { kind: "missing", path: entry, reason: "unresolved-pattern" };
|
|
868
|
+
return { kind: "missing", path: entry, reason: "no-committed-regular-blob" };
|
|
869
|
+
}
|
|
870
|
+
async function taskWithMaterializedContext(repoRoot, journal, worktree, task, attempt) {
|
|
871
|
+
if (task.context.length === 0)
|
|
872
|
+
return task;
|
|
873
|
+
const contextRoot = resolve(journal.dir, "context");
|
|
874
|
+
const context = [];
|
|
875
|
+
for (const entry of task.context) {
|
|
876
|
+
const prepared = await materializeContextEntry(repoRoot, worktree, contextRoot, task, attempt, entry);
|
|
877
|
+
if (prepared.kind === "missing") {
|
|
878
|
+
journal.append("context-missing", task.id, { path: prepared.path, reason: prepared.reason });
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
context.push(prepared.entry);
|
|
882
|
+
if (prepared.kind === "materialized") {
|
|
883
|
+
journal.append("context-materialized", task.id, {
|
|
884
|
+
path: entry,
|
|
885
|
+
normalized: prepared.normalized,
|
|
886
|
+
destination: prepared.destination,
|
|
887
|
+
bytes: prepared.bytes,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return { ...task, context };
|
|
893
|
+
}
|
|
690
894
|
async function cherryPickCommits(wt, commits) {
|
|
691
895
|
const carried = [];
|
|
692
896
|
for (const hash of commits) {
|
|
@@ -975,10 +1179,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
975
1179
|
journal.append("lock-reclaimed", undefined, lock.reclaimed); // HARD-02 audit trail
|
|
976
1180
|
}
|
|
977
1181
|
// GATE-08 (v1.12): the humanGate guard consults this run's journaled approvals, not just the compiled
|
|
978
|
-
// flag.
|
|
979
|
-
//
|
|
980
|
-
|
|
981
|
-
const approved = new Set(
|
|
1182
|
+
// flag. Startup approvals seed the first scheduling pass; live approvals are folded at task
|
|
1183
|
+
// boundaries below so a sibling can release parked work without waiting for run-end + resume.
|
|
1184
|
+
const approvalStartupEvents = journal.read();
|
|
1185
|
+
const approved = new Set(approvalStartupEvents.filter((e) => e.event === "task-approved" && e.taskId).map((e) => e.taskId));
|
|
1186
|
+
let approvalSweepCursor = approvalStartupEvents.length;
|
|
982
1187
|
const commands = detectGateCommands(repoRoot, cfg);
|
|
983
1188
|
let baseRef;
|
|
984
1189
|
let baseline;
|
|
@@ -986,9 +1191,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
986
1191
|
// Empty Map on fresh runs — every seed below conditions on resume.get(t.id), never on opts.resume (the
|
|
987
1192
|
// GATE-08 lesson at the humanGate guard: condition on the data, not the code path). Dead-code
|
|
988
1193
|
// equivalence to the router.ts:194 profile⇒undefined pattern: no map entry ⇒ today's literal.
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1194
|
+
let resume = opts.resume ? journal.replayResumeState() : new Map();
|
|
1195
|
+
let satisfiedGates = opts.resume ? journal.replaySatisfiedGates() : new Map();
|
|
1196
|
+
let replayedGateResults = resumeLifecycleOpen
|
|
992
1197
|
? journal.replayCurrentAttemptGateResults()
|
|
993
1198
|
: new Map();
|
|
994
1199
|
// T7: the capacity THIS session resolved — read inside the run's fork budget, so it is the number
|
|
@@ -1052,11 +1257,25 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1052
1257
|
baseRef = await gitHead(repoRoot);
|
|
1053
1258
|
baseline = await captureBaseline(repoRoot, commands);
|
|
1054
1259
|
writeFileSync(join(journal.dir, "baseline.json"), JSON.stringify(baseline, null, 2));
|
|
1260
|
+
writeFileSync(join(journal.dir, "graph.json"), readFileSync(join(tickmarkrDir(repoRoot), "graph.json")));
|
|
1055
1261
|
// v1.70 T2: environment identity beside the graph/branch identity — running tickmarkr version,
|
|
1056
1262
|
// loaded-config hash, and the probed CLI version of each adapter holding a channel in the run,
|
|
1057
1263
|
// gathered through the existing probe/config-load paths (no second mechanism).
|
|
1058
1264
|
const environment = runEnvironment(cfg, channels, health);
|
|
1059
|
-
journal.append("run-start", undefined, {
|
|
1265
|
+
journal.append("run-start", undefined, {
|
|
1266
|
+
pid: process.pid, baseRef, commands, channels: channels.map(channelKey),
|
|
1267
|
+
channelsByRole: {
|
|
1268
|
+
worker: pools.worker.map(channelKey),
|
|
1269
|
+
judge: pools.judge.map(channelKey),
|
|
1270
|
+
review: pools.review.map(channelKey),
|
|
1271
|
+
consult: pools.consult.map(channelKey),
|
|
1272
|
+
},
|
|
1273
|
+
driver: driver.id,
|
|
1274
|
+
driverEvidence: driverEvidence(cfg, driver, opts.driverOverride),
|
|
1275
|
+
distFingerprint: distFingerprint(),
|
|
1276
|
+
branch, graphDefinitionHash: graphDefinitionHash(graph), mode: rm.mode.mode, modeSource: rm.source,
|
|
1277
|
+
environment, ...(prior ? { supersedes: prior.runId } : {}),
|
|
1278
|
+
}); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2; supersedes: v1.53 T5
|
|
1060
1279
|
runStarted = true;
|
|
1061
1280
|
// v1.53 T5: mark the prior run AFTER this run's run-start exists, so the prior journal never
|
|
1062
1281
|
// names a successor that has no journal. Append-only — the prior journal is never rewritten.
|
|
@@ -1093,8 +1312,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1093
1312
|
try {
|
|
1094
1313
|
watchSlot = await driver.narrator?.(repoRoot, watchCommand(runId), runId);
|
|
1095
1314
|
}
|
|
1096
|
-
catch {
|
|
1097
|
-
|
|
1315
|
+
catch (error) {
|
|
1316
|
+
journal.append("watch-placement-failed", undefined, { error: error instanceof Error ? error.message : String(error) });
|
|
1098
1317
|
}
|
|
1099
1318
|
const intWt = await ensureIntegration(repoRoot, branch, baseRef);
|
|
1100
1319
|
// v1.1 visibility: role-named slots; panes persist per keepPanes (attempt = v1 close-after-harvest)
|
|
@@ -1169,6 +1388,14 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1169
1388
|
};
|
|
1170
1389
|
// gateFails/consults are execTask-scoped counters passed in so a park row is a rich verified-failure
|
|
1171
1390
|
// observation (e.g. ladder-exhausted + gateFails:4); every task-human row has a closed kind, never prose alone.
|
|
1391
|
+
const gateFailApprovalReason = (taskId, identity, includeUphold = false) => {
|
|
1392
|
+
const commands = [
|
|
1393
|
+
`tickmarkr approve ${runId} ${taskId} --waive`,
|
|
1394
|
+
`tickmarkr approve ${runId} ${taskId} --recheck`,
|
|
1395
|
+
...(includeUphold ? [`tickmarkr approve ${runId} ${taskId} --uphold`] : []),
|
|
1396
|
+
];
|
|
1397
|
+
return `${identity} — release with ${commands.map((command) => `\`${command}\``).join(" or ")}`;
|
|
1398
|
+
};
|
|
1172
1399
|
const park = async (t, reason, kind, assignment, attempts, startMs, gateFails = 0, consults = 0, tokens, metered = 0, retryMode = "fresh", details = {}) => {
|
|
1173
1400
|
graph = setStatus(graph, t.id, "human");
|
|
1174
1401
|
saveGraph(repoRoot, graph);
|
|
@@ -1228,6 +1455,38 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1228
1455
|
}
|
|
1229
1456
|
return false;
|
|
1230
1457
|
};
|
|
1458
|
+
const sweepLiveApprovals = () => {
|
|
1459
|
+
const events = journal.read();
|
|
1460
|
+
const approvals = events.slice(approvalSweepCursor)
|
|
1461
|
+
.filter((e) => e.event === "task-approved" && e.taskId);
|
|
1462
|
+
approvalSweepCursor = events.length;
|
|
1463
|
+
if (approvals.length === 0)
|
|
1464
|
+
return;
|
|
1465
|
+
resume = journal.replayResumeState();
|
|
1466
|
+
satisfiedGates = journal.replaySatisfiedGates();
|
|
1467
|
+
replayedGateResults = resumeLifecycleOpen
|
|
1468
|
+
? journal.replayCurrentAttemptGateResults()
|
|
1469
|
+
: new Map();
|
|
1470
|
+
let changed = false;
|
|
1471
|
+
for (const e of approvals) {
|
|
1472
|
+
const taskId = e.taskId;
|
|
1473
|
+
approved.add(taskId);
|
|
1474
|
+
if (e.data.release !== GATE_SATISFIED_RELEASE)
|
|
1475
|
+
replayedGateResults.delete(taskId);
|
|
1476
|
+
try {
|
|
1477
|
+
const task = getTask(graph, taskId);
|
|
1478
|
+
if (task.status === "human" || task.status === "failed") {
|
|
1479
|
+
graph = setStatus(graph, taskId, "pending");
|
|
1480
|
+
changed = true;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
// Unknown task in a stale journal: approval replay is inert, like the other journal folds.
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (changed)
|
|
1488
|
+
saveGraph(repoRoot, graph);
|
|
1489
|
+
};
|
|
1231
1490
|
const execTask = async (t) => {
|
|
1232
1491
|
const startMs = Date.now();
|
|
1233
1492
|
const taskTimeoutMinutes = t.timeoutMinutes ?? cfg.taskTimeoutMinutes;
|
|
@@ -1543,6 +1802,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1543
1802
|
const applyVerdict = async (v, attempts, trigger) => {
|
|
1544
1803
|
journal.append("consult-verdict", t.id, {
|
|
1545
1804
|
action: v.action, notes: v.notes,
|
|
1805
|
+
adapter: v.adapter ?? "unknown", model: v.model ?? "unknown", vendor: v.vendor ?? "unknown",
|
|
1546
1806
|
...(v.reason ? { reason: v.reason } : {}),
|
|
1547
1807
|
...(v.guidance ? { guidance: v.guidance } : {}),
|
|
1548
1808
|
...(v.excludeAdapter ? { excludeAdapter: v.excludeAdapter } : {}),
|
|
@@ -1766,6 +2026,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1766
2026
|
journal.phaseStart(t.id, phaseForGate(e.gate), { gate: e.gate, index: e.index, total: e.total, ...(e.parentAt === undefined ? {} : { parallel: true }) });
|
|
1767
2027
|
return;
|
|
1768
2028
|
}
|
|
2029
|
+
if (e.phase === "note") {
|
|
2030
|
+
journal.append(e.name, t.id, e.payload);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
1769
2033
|
const g = e.result;
|
|
1770
2034
|
inParallelOrder(g.gate, () => {
|
|
1771
2035
|
journalGateResult(g);
|
|
@@ -1801,7 +2065,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1801
2065
|
// feedback from those rows. Only an operator-authorized gate release parks on a new red.
|
|
1802
2066
|
if (!satisfiedGate)
|
|
1803
2067
|
break gateLoop;
|
|
1804
|
-
await park(t, "post-approval gate failed", "gate-fail", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
2068
|
+
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);
|
|
1805
2069
|
return;
|
|
1806
2070
|
}
|
|
1807
2071
|
const m = await mergeSerial(taskBranch, t, gated);
|
|
@@ -1859,7 +2123,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1859
2123
|
// survives the reset and governs only those further rounds. Absence preserves the module default.
|
|
1860
2124
|
const reviewRoundCap = approvedReviewRoundCeiling(journal.read(), t.id) ?? REVIEW_ROUND_CAP;
|
|
1861
2125
|
if (attempt > 0 && reviewRoundsDrawn() >= reviewRoundCap) {
|
|
1862
|
-
await park(t, `review round cap (${reviewRoundCap}) reached this engagement
|
|
2126
|
+
await park(t, gateFailApprovalReason(t.id, `review round cap (${reviewRoundCap}) reached this engagement`, true), "gate-fail", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
1863
2127
|
return;
|
|
1864
2128
|
}
|
|
1865
2129
|
// OBS-57: a demoted channel must not be re-dispatched on consult retry or provider requeue.
|
|
@@ -1895,7 +2159,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1895
2159
|
gate: banned, from: channelKey(assignment), to: next ? channelKey(next) : null,
|
|
1896
2160
|
});
|
|
1897
2161
|
if (!next) {
|
|
1898
|
-
await park(t, `identical ${banned} failure twice this engagement — an identical retry is banned and no untried channel is left
|
|
2162
|
+
await park(t, gateFailApprovalReason(t.id, `identical ${banned} failure twice this engagement — an identical retry is banned and no untried channel is left`), "gate-fail", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
1899
2163
|
return;
|
|
1900
2164
|
}
|
|
1901
2165
|
assignment = next;
|
|
@@ -1964,6 +2228,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1964
2228
|
})].join("\n");
|
|
1965
2229
|
feedback = feedback ? `${feedback}\n\n${brief}` : brief;
|
|
1966
2230
|
}
|
|
2231
|
+
const carriedConsultGuidance = outstandingConsultGuidance(journaledSoFar, t.id);
|
|
2232
|
+
if (carriedConsultGuidance) {
|
|
2233
|
+
const consultBrief = renderRetryGuidance({ ...carriedConsultGuidance, notes: "" });
|
|
2234
|
+
if (consultBrief && !feedback.includes(consultBrief)) {
|
|
2235
|
+
feedback = feedback ? `${feedback}\n\n${consultBrief}` : consultBrief;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
1967
2238
|
retryMode = repairFindings
|
|
1968
2239
|
? "repair"
|
|
1969
2240
|
: priorSession
|
|
@@ -1992,6 +2263,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1992
2263
|
journal.append("task-dispatch", t.id, {
|
|
1993
2264
|
assignment, attempt, provenance: dispatchProvenance(r.provenance), retryMode,
|
|
1994
2265
|
...(outstandingFindings.length > 0 ? { carriedFindings: outstandingFindings } : {}),
|
|
2266
|
+
...(carriedConsultGuidance ? { carriedConsultGuidance } : {}),
|
|
1995
2267
|
});
|
|
1996
2268
|
journal.phaseStart(t.id, "worker", { attempt, assignment });
|
|
1997
2269
|
const taskBase = await integrationHead(intWt); // deps are merged → visible to this task
|
|
@@ -2091,7 +2363,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2091
2363
|
return;
|
|
2092
2364
|
}
|
|
2093
2365
|
}
|
|
2094
|
-
const
|
|
2366
|
+
const promptTask = await taskWithMaterializedContext(repoRoot, journal, wt, t, attempt);
|
|
2367
|
+
const promptFile = writePrompt(journal.dir, promptTask, attempt, feedback, nonce);
|
|
2095
2368
|
// OBS-56: state the non-interactive, one-pass finish contract and the OBS-54 stall budget in every
|
|
2096
2369
|
// worker prompt, not only consult retry guidance. Prepended so prompt.ts's completion trailer stays last.
|
|
2097
2370
|
const workerContract = `## Harness contract\n- This harness is non-interactive: make one continuous pass; do not stop for questions or follow-up input.\n- You have a ${taskTimeoutMinutes} minute stall window. The gates run the full suite for you — never spend this window on one; commit your work and emit the completion trailer inside it.\n- Each test: acceptance criterion must exist as a vitest test whose OWN title (the leaf, not counting enclosing describe titles) is the criterion string verbatim — never shortened, never decorated. Nesting under describe() is allowed.`; // OBS-64; OBS-511: leaf-title rule stated where the worker reads it; OBS-548: the suite is the GATES' job — this repo's own suite outlasts the output-silence windows the daemon polices, so a worker obeying "budget the full suite" was killed by construction
|
|
@@ -3200,6 +3473,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3200
3473
|
journal.phaseStart(t.id, phaseForGate(e.gate), { gate: e.gate, index: e.index, total: e.total, ...(e.parentAt === undefined ? {} : { parallel: true }) });
|
|
3201
3474
|
return;
|
|
3202
3475
|
}
|
|
3476
|
+
if (e.phase === "note") {
|
|
3477
|
+
journal.append(e.name, t.id, e.payload);
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3203
3480
|
const g = e.result;
|
|
3204
3481
|
inParallelOrder(g.gate, () => {
|
|
3205
3482
|
// GATE-09 (ROADMAP SC-4): journal every judge retry as an attributable event — which gate flaked,
|
|
@@ -3402,7 +3679,11 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3402
3679
|
continue;
|
|
3403
3680
|
return;
|
|
3404
3681
|
}
|
|
3405
|
-
journal.append("consult-verdict", t.id, {
|
|
3682
|
+
journal.append("consult-verdict", t.id, {
|
|
3683
|
+
action: v.action, notes: v.notes,
|
|
3684
|
+
adapter: v.adapter ?? "unknown", model: v.model ?? "unknown", vendor: v.vendor ?? "unknown",
|
|
3685
|
+
capAdvisory: true,
|
|
3686
|
+
});
|
|
3406
3687
|
}
|
|
3407
3688
|
// v1.85 T3: a narrow battery over fully carried commits earns a REPAIR (decided above) — the
|
|
3408
3689
|
// next dispatch carries the findings verbatim and the diff content instead of re-onboarding a
|
|
@@ -3465,6 +3746,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3465
3746
|
// must still stop the run before it can dispatch more work or write run-end.
|
|
3466
3747
|
if (termSignal)
|
|
3467
3748
|
throw new Error(`terminated by ${termSignal}`);
|
|
3749
|
+
sweepLiveApprovals();
|
|
3468
3750
|
const ready = readyTasks(graph)
|
|
3469
3751
|
.filter((t) => !inflight.has(t.id))
|
|
3470
3752
|
.slice(0, Math.max(0, concurrency - inflight.size));
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -44,6 +44,21 @@ export interface StructuredFinding {
|
|
|
44
44
|
rationale?: string;
|
|
45
45
|
fingerprint: string;
|
|
46
46
|
}
|
|
47
|
+
declare const CONSULT_ACTIONS: readonly ["retry", "reroute", "decompose", "human"];
|
|
48
|
+
export type ConsultGuidanceAction = (typeof CONSULT_ACTIONS)[number];
|
|
49
|
+
export interface ConsultGuidanceCarry {
|
|
50
|
+
action: ConsultGuidanceAction;
|
|
51
|
+
reason?: string;
|
|
52
|
+
guidance?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The newest structured consult prescription still open for this task. A consult park is only a
|
|
56
|
+
* pause: graph-changed revival can dispatch a fresh worker without a live `applyVerdict()` frame, so
|
|
57
|
+
* prompt-build re-derives the worker-facing bullets from the journal. Notes are deliberately excluded:
|
|
58
|
+
* they are audit prose, and OBS-37a says workers get only structured reason/guidance rendered by
|
|
59
|
+
* `renderRetryGuidance()`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function outstandingConsultGuidance(events: JournalEvent[], taskId: string): ConsultGuidanceCarry | undefined;
|
|
47
62
|
export declare const UNIDENTIFIED = "<unidentified>";
|
|
48
63
|
/**
|
|
49
64
|
* Structured findings for a BLOCKING review/judge gate result, parsed from the details the gate
|
|
@@ -335,3 +350,4 @@ export declare class Journal {
|
|
|
335
350
|
readTelemetry(): TelemetryRow[];
|
|
336
351
|
readJudgeTelemetry(): TelemetryRow[];
|
|
337
352
|
}
|
|
353
|
+
export {};
|