tickmarkr 2.5.5 → 2.5.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 +3 -1
- package/dist/adapters/prompt.js +21 -1
- package/dist/cli/commands/approve.js +69 -8
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/fleet.js +4 -0
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/status.js +95 -34
- package/dist/cli/commands/verify.js +108 -85
- package/dist/config/config.d.ts +11 -0
- package/dist/config/config.js +21 -12
- package/dist/config/fleet-overlay.js +55 -17
- package/dist/drivers/index.js +2 -1
- package/dist/drivers/orca.d.ts +21 -1
- package/dist/drivers/orca.js +209 -27
- package/dist/gates/baseline.d.ts +21 -5
- package/dist/gates/baseline.js +67 -17
- package/dist/gates/cache.d.ts +14 -13
- package/dist/gates/cache.js +17 -5
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +118 -15
- package/dist/gates/run-gates.d.ts +10 -2
- package/dist/gates/run-gates.js +362 -108
- package/dist/gates/test-manifest.d.ts +33 -1
- package/dist/gates/test-manifest.js +132 -40
- package/dist/gates/test-reporter.js +20 -7
- package/dist/graph/graph.d.ts +4 -0
- package/dist/graph/graph.js +50 -1
- package/dist/run/activity.d.ts +28 -0
- package/dist/run/activity.js +194 -0
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +11 -0
- package/dist/run/daemon.js +663 -128
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +46 -1
- package/dist/run/git.js +149 -12
- package/dist/run/journal.d.ts +23 -5
- package/dist/run/journal.js +98 -25
- package/dist/run/lease.d.ts +44 -0
- package/dist/run/lease.js +226 -3
- package/dist/run/operator-page-summary.d.ts +56 -0
- package/dist/run/operator-page-summary.js +68 -0
- package/dist/run/operator-summary.d.ts +69 -0
- package/dist/run/operator-summary.js +77 -0
- package/dist/run/protocol.d.ts +71 -0
- package/dist/run/protocol.js +32 -0
- package/dist/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- package/dist/tui/cockpit/board.d.ts +9 -0
- package/dist/tui/cockpit/board.js +10 -0
- package/dist/tui/cockpit/derive.d.ts +35 -0
- package/dist/tui/cockpit/derive.js +152 -10
- package/dist/tui/cockpit/evidence-view.d.ts +2 -0
- package/dist/tui/cockpit/evidence-view.js +42 -12
- package/dist/tui/cockpit/run-cockpit.d.ts +8 -1
- package/dist/tui/cockpit/run-cockpit.js +95 -1
- package/dist/tui/cockpit/run-view.d.ts +37 -0
- package/dist/tui/cockpit/run-view.js +189 -2
- package/dist/tui/ink/fleet-app.d.ts +10 -2
- package/dist/tui/ink/fleet-app.js +33 -15
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +55 -3
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ExecutionBudgetExceeded extends Error {
|
|
2
|
+
readonly taskId: string;
|
|
3
|
+
constructor(taskId: string, reason?: string);
|
|
4
|
+
}
|
|
5
|
+
export interface ExecutionBudgetEvent {
|
|
6
|
+
event: string;
|
|
7
|
+
taskId?: string;
|
|
8
|
+
data: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface ExecutionBudgetOptions {
|
|
11
|
+
limitMs: number;
|
|
12
|
+
taskId: string;
|
|
13
|
+
readEvents: () => readonly ExecutionBudgetEvent[];
|
|
14
|
+
append: (event: string, taskId: string, data: Record<string, unknown>) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare const executionSignal: () => AbortSignal | undefined;
|
|
17
|
+
/** Cleanup only: the caller must bound cleanup and must not authorize task work here. */
|
|
18
|
+
export declare const withoutExecutionBudget: <T>(run: () => T) => T;
|
|
19
|
+
export declare const remainingExecutionMs: () => number | undefined;
|
|
20
|
+
/** One wall-clock interval around task execution, shared by parallel children. Cancellation is
|
|
21
|
+
* cooperative: callers must stop on executionSignal(), and this wrapper waits for their cleanup.
|
|
22
|
+
* Durable slices are bought before work: at least one minute or 1% of the task ceiling, capped
|
|
23
|
+
* by what remains. This avoids a per-second journal stream. An interrupted slice stays fully
|
|
24
|
+
* charged, conservatively consuming its unused portion; daemon-offline time is never charged. */
|
|
25
|
+
export declare function withExecutionBudget<T>(opts: ExecutionBudgetOptions, run: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
export class ExecutionBudgetExceeded extends Error {
|
|
4
|
+
taskId;
|
|
5
|
+
constructor(taskId, reason = "automated execution ceiling exhausted") {
|
|
6
|
+
super(`${taskId}: ${reason}`);
|
|
7
|
+
this.taskId = taskId;
|
|
8
|
+
this.name = "ExecutionBudgetExceeded";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const context = new AsyncLocalStorage();
|
|
12
|
+
const MIN_SLICE_MS = 60_000;
|
|
13
|
+
const finite = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
14
|
+
function consumed(opts) {
|
|
15
|
+
const reservations = new Map();
|
|
16
|
+
for (const e of opts.readEvents()) {
|
|
17
|
+
if (e.taskId !== opts.taskId || !["execution-budget-reserved", "execution-budget-settled"].includes(e.event))
|
|
18
|
+
continue;
|
|
19
|
+
const d = e.data;
|
|
20
|
+
if (!d || d.limitMs !== opts.limitMs || typeof d.id !== "string" || !d.id) {
|
|
21
|
+
throw new ExecutionBudgetExceeded(opts.taskId, "invalid execution budget accounting");
|
|
22
|
+
}
|
|
23
|
+
if (e.event === "execution-budget-reserved") {
|
|
24
|
+
if (!finite(d.reservedMs) || d.reservedMs === 0 || reservations.has(d.id)) {
|
|
25
|
+
throw new ExecutionBudgetExceeded(opts.taskId, "invalid execution reservation");
|
|
26
|
+
}
|
|
27
|
+
reservations.set(d.id, { reserved: d.reservedMs });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const prior = reservations.get(d.id);
|
|
31
|
+
if (!prior || prior.used !== undefined || !finite(d.usedMs) || d.usedMs > prior.reserved) {
|
|
32
|
+
throw new ExecutionBudgetExceeded(opts.taskId, "invalid execution settlement");
|
|
33
|
+
}
|
|
34
|
+
prior.used = d.usedMs;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const total = [...reservations.values()].reduce((sum, r) => sum + (r.used ?? r.reserved), 0);
|
|
38
|
+
if (!finite(total) || total > opts.limitMs)
|
|
39
|
+
throw new ExecutionBudgetExceeded(opts.taskId, "invalid execution total");
|
|
40
|
+
return total;
|
|
41
|
+
}
|
|
42
|
+
export const executionSignal = () => {
|
|
43
|
+
const budget = context.getStore();
|
|
44
|
+
budget?.check();
|
|
45
|
+
return budget?.signal;
|
|
46
|
+
};
|
|
47
|
+
/** Cleanup only: the caller must bound cleanup and must not authorize task work here. */
|
|
48
|
+
export const withoutExecutionBudget = (run) => context.exit(run);
|
|
49
|
+
export const remainingExecutionMs = () => context.getStore()?.remaining();
|
|
50
|
+
/** One wall-clock interval around task execution, shared by parallel children. Cancellation is
|
|
51
|
+
* cooperative: callers must stop on executionSignal(), and this wrapper waits for their cleanup.
|
|
52
|
+
* Durable slices are bought before work: at least one minute or 1% of the task ceiling, capped
|
|
53
|
+
* by what remains. This avoids a per-second journal stream. An interrupted slice stays fully
|
|
54
|
+
* charged, conservatively consuming its unused portion; daemon-offline time is never charged. */
|
|
55
|
+
export async function withExecutionBudget(opts, run) {
|
|
56
|
+
if (!finite(opts.limitMs) || opts.limitMs === 0 || !opts.taskId)
|
|
57
|
+
throw new ExecutionBudgetExceeded(opts.taskId, "invalid execution ceiling");
|
|
58
|
+
const parent = context.getStore();
|
|
59
|
+
if (parent) {
|
|
60
|
+
if (parent.taskId !== opts.taskId || parent.limitMs !== opts.limitMs)
|
|
61
|
+
throw new ExecutionBudgetExceeded(opts.taskId, "nested execution budget mismatch");
|
|
62
|
+
parent.check();
|
|
63
|
+
parent.signal.throwIfAborted();
|
|
64
|
+
return run();
|
|
65
|
+
}
|
|
66
|
+
const prior = consumed(opts);
|
|
67
|
+
const available = opts.limitMs - prior;
|
|
68
|
+
const sliceMs = Math.max(MIN_SLICE_MS, opts.limitMs / 100);
|
|
69
|
+
if (available <= 0)
|
|
70
|
+
throw new ExecutionBudgetExceeded(opts.taskId);
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const start = performance.now();
|
|
73
|
+
let reserved = 0;
|
|
74
|
+
let current;
|
|
75
|
+
let timer;
|
|
76
|
+
const elapsed = () => Math.max(0, performance.now() - start);
|
|
77
|
+
const remaining = () => Math.max(0, available - elapsed());
|
|
78
|
+
const stop = (error) => controller.abort(error instanceof Error ? error : new ExecutionBudgetExceeded(opts.taskId));
|
|
79
|
+
const buy = (amount) => {
|
|
80
|
+
if (amount <= 0)
|
|
81
|
+
return;
|
|
82
|
+
const next = { id: randomUUID(), before: reserved, amount };
|
|
83
|
+
opts.append("execution-budget-reserved", opts.taskId, { id: next.id, limitMs: opts.limitMs, reservedMs: amount });
|
|
84
|
+
current = next;
|
|
85
|
+
reserved += amount;
|
|
86
|
+
};
|
|
87
|
+
// JS timers cannot preempt a blocked event loop. Charge elapsed overruns before another
|
|
88
|
+
// observable operation or completion; expiration consumes the whole allowance across resume.
|
|
89
|
+
const check = () => {
|
|
90
|
+
try {
|
|
91
|
+
const used = Math.min(available, elapsed());
|
|
92
|
+
if (used > reserved)
|
|
93
|
+
buy(used - reserved);
|
|
94
|
+
if (used >= available)
|
|
95
|
+
stop(new ExecutionBudgetExceeded(opts.taskId));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
stop(error);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const reserve = () => {
|
|
102
|
+
if (elapsed() >= available)
|
|
103
|
+
check();
|
|
104
|
+
if (controller.signal.aborted)
|
|
105
|
+
return;
|
|
106
|
+
buy(Math.min(available - reserved, Math.max(sliceMs, elapsed() - reserved + sliceMs)));
|
|
107
|
+
timer = setTimeout(() => {
|
|
108
|
+
try {
|
|
109
|
+
reserve();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
stop(error);
|
|
113
|
+
}
|
|
114
|
+
}, Math.max(0, reserved - elapsed()));
|
|
115
|
+
};
|
|
116
|
+
reserve();
|
|
117
|
+
controller.signal.throwIfAborted();
|
|
118
|
+
const budget = { taskId: opts.taskId, limitMs: opts.limitMs, signal: controller.signal, remaining, check };
|
|
119
|
+
try {
|
|
120
|
+
return await context.run(budget, async () => {
|
|
121
|
+
controller.signal.throwIfAborted();
|
|
122
|
+
const result = await run();
|
|
123
|
+
check();
|
|
124
|
+
controller.signal.throwIfAborted();
|
|
125
|
+
return result;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
check();
|
|
130
|
+
controller.signal.throwIfAborted();
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
check();
|
|
136
|
+
if (current) {
|
|
137
|
+
const usedMs = Math.min(current.amount, Math.max(0, elapsed() - current.before));
|
|
138
|
+
opts.append("execution-budget-settled", opts.taskId, { id: current.id, limitMs: opts.limitMs, usedMs });
|
|
139
|
+
}
|
|
140
|
+
controller.signal.throwIfAborted();
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/run/git.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CommandReceiptAttribution, ShellReceipt } from "./protocol.js";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
3
4
|
export { ROUTING_ENV_SEAMS };
|
|
@@ -76,6 +77,46 @@ export declare function readCapacity(value: unknown): CapacityRead;
|
|
|
76
77
|
* different capacity, or a current capacity the caller could not state → no.
|
|
77
78
|
*/
|
|
78
79
|
export declare function sameCapacity(recorded: unknown, current: RunCapacity | undefined): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* R41: the verification PROTOCOL a verdict was produced under — which implementation of the manifest
|
|
82
|
+
* discovery/report contract judged it, and the npm lifecycle policy its runner children ran with.
|
|
83
|
+
* Two verdicts are comparable only when both match: a manifest built from module collection ("vl1.1",
|
|
84
|
+
* the pre-stamp implementation) and one built from file specifications ("vl1.2") certify different
|
|
85
|
+
* sets, and a suite whose `pretest` hook ran (ignore-scripts=false) is a different measurement from
|
|
86
|
+
* one where npm skipped it. Bump VERIFICATION_PROTOCOL whenever discovery or report semantics change.
|
|
87
|
+
*/
|
|
88
|
+
export declare const VERIFICATION_PROTOCOL = "vl1.2";
|
|
89
|
+
export type RunnerLifecycle = "hooks" | "ignore-scripts" | "unknown";
|
|
90
|
+
export interface VerificationProtocol {
|
|
91
|
+
protocol: string;
|
|
92
|
+
/** The EFFECTIVE npm lifecycle policy a runner child receives: `hooks` (pretest/prebuild run),
|
|
93
|
+
* `ignore-scripts` (npm skips them), or `unknown` (could not be measured — never comparable). */
|
|
94
|
+
lifecycle: RunnerLifecycle;
|
|
95
|
+
/** Provenance only, never part of compatibility: an explicit process-scoped env export, or npm's
|
|
96
|
+
* own resolved config for that checkout (user/project npmrc). */
|
|
97
|
+
source: "explicit" | "npm-config" | "unknown";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* R41: the lifecycle policy a runner child spawned from `env` in `cwd` receives. An explicit
|
|
101
|
+
* process-scoped `npm_config_ignore_scripts` export decides (npm reads env over every npmrc) and is
|
|
102
|
+
* recorded as such; otherwise the policy is MEASURED from npm's own resolved config for that checkout
|
|
103
|
+
* (`npm config get ignore-scripts` — user npmrc, project npmrc, env), so the identity binds what
|
|
104
|
+
* npm will actually do rather than a guess. A policy that cannot be measured is `unknown`, and an
|
|
105
|
+
* unknown policy is never compatible with anything, itself included. No env or npmrc is written here.
|
|
106
|
+
*/
|
|
107
|
+
export declare function runnerLifecycle(env?: NodeJS.ProcessEnv, cwd?: string): Pick<VerificationProtocol, "lifecycle" | "source">;
|
|
108
|
+
export declare function verificationProtocol(env?: NodeJS.ProcessEnv, cwd?: string): VerificationProtocol;
|
|
109
|
+
/**
|
|
110
|
+
* May a verdict recorded under `recorded` be reused — cached, replayed — by a session running
|
|
111
|
+
* `current`? Unlike capacity, ABSENT is NOT compatible: a row with no protocol stamp was produced by
|
|
112
|
+
* the pre-stamp implementation (module-collection discovery, lifecycle unstated), which is exactly
|
|
113
|
+
* the evidence a changed protocol must not inherit — positive or negative. Malformed → no. An
|
|
114
|
+
* `unknown` lifecycle on either side → no: two unmeasured policies are not the same policy.
|
|
115
|
+
* `source` is provenance and never enters the comparison — an explicit `false` and an npmrc `false`
|
|
116
|
+
* are the same effective policy.
|
|
117
|
+
*/
|
|
118
|
+
export declare function sameVerification(recorded: unknown, current: VerificationProtocol): boolean;
|
|
119
|
+
export declare const describeVerification: (value: unknown) => string;
|
|
79
120
|
export declare const describeCapacity: (value: unknown) => string;
|
|
80
121
|
/**
|
|
81
122
|
* The capacity a child spawned on THIS async context would receive: the same precedence `shell`
|
|
@@ -110,10 +151,14 @@ export interface ShellOptions {
|
|
|
110
151
|
signal?: AbortSignal;
|
|
111
152
|
onSpawn?: (pid: number | undefined) => void;
|
|
112
153
|
onTimeout?: () => void;
|
|
154
|
+
/** Independent of the legacy pid callback; started means the child emitted spawn. */
|
|
155
|
+
onReceipt?: (receipt: ShellReceipt) => void;
|
|
156
|
+
/** Called once per invocation (1-based), including each pre-spawn retry. */
|
|
157
|
+
receiptAttribution?: (invocation: number) => CommandReceiptAttribution;
|
|
113
158
|
}
|
|
114
159
|
/** Shared command seam, including invocation-bound manifested runners. */
|
|
115
160
|
export declare function shell(cmd: string, cwd: string, timeoutMs: number, login?: boolean, options?: ShellOptions): Promise<ShResult>;
|
|
116
|
-
export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
161
|
+
export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options?: ShellOptions): Promise<ShResult>;
|
|
117
162
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
118
163
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
|
119
164
|
export declare function shGitOk(cmd: string, cwd: string): Promise<string>;
|
package/dist/run/git.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { executionSignal } from "./execution-budget.js";
|
|
1
2
|
import { commandLeaseEnvironment, withCommandLease } from "./lease.js";
|
|
2
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { availableParallelism, tmpdir } from "node:os";
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { availableParallelism, homedir, tmpdir } from "node:os";
|
|
6
7
|
import { join, resolve } from "node:path";
|
|
7
8
|
import { StringDecoder } from "node:string_decoder";
|
|
8
9
|
import { shq } from "../adapters/types.js";
|
|
@@ -82,6 +83,86 @@ export function sameCapacity(recorded, current) {
|
|
|
82
83
|
return false;
|
|
83
84
|
return read.capacity.forkCap === current.forkCap && read.capacity.cores === current.cores;
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* R41: the verification PROTOCOL a verdict was produced under — which implementation of the manifest
|
|
88
|
+
* discovery/report contract judged it, and the npm lifecycle policy its runner children ran with.
|
|
89
|
+
* Two verdicts are comparable only when both match: a manifest built from module collection ("vl1.1",
|
|
90
|
+
* the pre-stamp implementation) and one built from file specifications ("vl1.2") certify different
|
|
91
|
+
* sets, and a suite whose `pretest` hook ran (ignore-scripts=false) is a different measurement from
|
|
92
|
+
* one where npm skipped it. Bump VERIFICATION_PROTOCOL whenever discovery or report semantics change.
|
|
93
|
+
*/
|
|
94
|
+
export const VERIFICATION_PROTOCOL = "vl1.2";
|
|
95
|
+
const lifecycleOf = (raw) => {
|
|
96
|
+
const v = raw?.trim().toLowerCase();
|
|
97
|
+
return v === "false" ? "hooks" : v === "true" ? "ignore-scripts" : undefined;
|
|
98
|
+
};
|
|
99
|
+
// Measured effective policy, keyed on everything npm resolves it from that this process can see:
|
|
100
|
+
// the checkout, its project npmrc and the user npmrc (path + mtime), and the explicit env. A
|
|
101
|
+
// rewritten npmrc changes the key, so a later battery in the same process re-measures.
|
|
102
|
+
const measuredLifecycle = new Map();
|
|
103
|
+
const mtimeOf = (p) => { try {
|
|
104
|
+
return String(statSync(p).mtimeMs);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return "-";
|
|
108
|
+
} };
|
|
109
|
+
/**
|
|
110
|
+
* R41: the lifecycle policy a runner child spawned from `env` in `cwd` receives. An explicit
|
|
111
|
+
* process-scoped `npm_config_ignore_scripts` export decides (npm reads env over every npmrc) and is
|
|
112
|
+
* recorded as such; otherwise the policy is MEASURED from npm's own resolved config for that checkout
|
|
113
|
+
* (`npm config get ignore-scripts` — user npmrc, project npmrc, env), so the identity binds what
|
|
114
|
+
* npm will actually do rather than a guess. A policy that cannot be measured is `unknown`, and an
|
|
115
|
+
* unknown policy is never compatible with anything, itself included. No env or npmrc is written here.
|
|
116
|
+
*/
|
|
117
|
+
export function runnerLifecycle(env = process.env, cwd = process.cwd()) {
|
|
118
|
+
const explicit = lifecycleOf(env.npm_config_ignore_scripts ?? env.NPM_CONFIG_IGNORE_SCRIPTS);
|
|
119
|
+
if (explicit)
|
|
120
|
+
return { lifecycle: explicit, source: "explicit" };
|
|
121
|
+
const userconfig = env.NPM_CONFIG_USERCONFIG ?? env.npm_config_userconfig ?? join(homedir(), ".npmrc");
|
|
122
|
+
const key = [cwd, mtimeOf(join(cwd, ".npmrc")), userconfig, mtimeOf(userconfig),
|
|
123
|
+
env.npm_config_ignore_scripts ?? env.NPM_CONFIG_IGNORE_SCRIPTS ?? ""].join("\0");
|
|
124
|
+
let lifecycle = measuredLifecycle.get(key);
|
|
125
|
+
if (lifecycle === undefined) {
|
|
126
|
+
try {
|
|
127
|
+
const out = execFileSync("npm", ["config", "get", "ignore-scripts"], {
|
|
128
|
+
cwd: existsSync(cwd) ? cwd : process.cwd(), env, encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "ignore"],
|
|
129
|
+
});
|
|
130
|
+
lifecycle = lifecycleOf(out) ?? "unknown";
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
lifecycle = "unknown";
|
|
134
|
+
}
|
|
135
|
+
measuredLifecycle.set(key, lifecycle);
|
|
136
|
+
}
|
|
137
|
+
return { lifecycle, source: lifecycle === "unknown" ? "unknown" : "npm-config" };
|
|
138
|
+
}
|
|
139
|
+
export function verificationProtocol(env = process.env, cwd = process.cwd()) {
|
|
140
|
+
return { protocol: VERIFICATION_PROTOCOL, ...runnerLifecycle(env, cwd) };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* May a verdict recorded under `recorded` be reused — cached, replayed — by a session running
|
|
144
|
+
* `current`? Unlike capacity, ABSENT is NOT compatible: a row with no protocol stamp was produced by
|
|
145
|
+
* the pre-stamp implementation (module-collection discovery, lifecycle unstated), which is exactly
|
|
146
|
+
* the evidence a changed protocol must not inherit — positive or negative. Malformed → no. An
|
|
147
|
+
* `unknown` lifecycle on either side → no: two unmeasured policies are not the same policy.
|
|
148
|
+
* `source` is provenance and never enters the comparison — an explicit `false` and an npmrc `false`
|
|
149
|
+
* are the same effective policy.
|
|
150
|
+
*/
|
|
151
|
+
export function sameVerification(recorded, current) {
|
|
152
|
+
if (recorded === null || typeof recorded !== "object")
|
|
153
|
+
return false;
|
|
154
|
+
const { protocol, lifecycle } = recorded;
|
|
155
|
+
if (lifecycle === "unknown" || current.lifecycle === "unknown")
|
|
156
|
+
return false;
|
|
157
|
+
return protocol === current.protocol && lifecycle === current.lifecycle;
|
|
158
|
+
}
|
|
159
|
+
export const describeVerification = (value) => {
|
|
160
|
+
if (value === null || typeof value !== "object")
|
|
161
|
+
return "an unrecorded verification protocol";
|
|
162
|
+
const { protocol, lifecycle, source } = value;
|
|
163
|
+
return typeof protocol === "string" && typeof lifecycle === "string"
|
|
164
|
+
? `protocol ${protocol}, lifecycle ${lifecycle}${typeof source === "string" ? ` (${source})` : ""}` : "a malformed verification protocol";
|
|
165
|
+
};
|
|
85
166
|
export const describeCapacity = (value) => {
|
|
86
167
|
const read = readCapacity(value);
|
|
87
168
|
return read.state === "present"
|
|
@@ -131,10 +212,45 @@ export const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
|
131
212
|
const SHELL_REAP_GRACE_MS = 2000;
|
|
132
213
|
/** Shared command seam, including invocation-bound manifested runners. */
|
|
133
214
|
export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
134
|
-
|
|
215
|
+
const inherited = executionSignal();
|
|
216
|
+
const signal = inherited && options.signal ? AbortSignal.any([inherited, options.signal]) : inherited ?? options.signal;
|
|
217
|
+
let attribution;
|
|
218
|
+
let confirmedStart = false;
|
|
219
|
+
let terminal = false;
|
|
220
|
+
const begin = (invocation) => {
|
|
221
|
+
attribution = options.receiptAttribution?.(invocation);
|
|
222
|
+
confirmedStart = false;
|
|
223
|
+
terminal = false;
|
|
224
|
+
};
|
|
225
|
+
const emit = (receipt) => {
|
|
226
|
+
if (terminal)
|
|
227
|
+
return;
|
|
228
|
+
if (receipt.outcome === "started")
|
|
229
|
+
confirmedStart = true;
|
|
230
|
+
else
|
|
231
|
+
terminal = true;
|
|
232
|
+
// Observational callbacks must not change process cleanup, retries or cancellation.
|
|
233
|
+
try {
|
|
234
|
+
options.onReceipt?.({ ...receipt, confirmedStart, ...(attribution ? { attribution: { ...attribution } } : {}) });
|
|
235
|
+
}
|
|
236
|
+
catch { /* receipt sinks are observational */ }
|
|
237
|
+
};
|
|
238
|
+
const checkAbort = () => {
|
|
239
|
+
if (signal?.aborted)
|
|
240
|
+
emit({ outcome: "cancelled", exitCode: null, signal: null });
|
|
241
|
+
signal?.throwIfAborted();
|
|
242
|
+
};
|
|
243
|
+
begin(1);
|
|
244
|
+
checkAbort();
|
|
245
|
+
return withCommandLease(cmd, () => executeShell(cmd, cwd, timeoutMs, login, { ...options, signal }, { begin, emit, checkAbort }))
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
if (signal?.aborted)
|
|
248
|
+
emit({ outcome: "cancelled", exitCode: null, signal: null });
|
|
249
|
+
throw error;
|
|
250
|
+
});
|
|
135
251
|
}
|
|
136
|
-
function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
137
|
-
|
|
252
|
+
function executeShell(cmd, cwd, timeoutMs, login, options, observation) {
|
|
253
|
+
observation.checkAbort();
|
|
138
254
|
// OBS-74: scrub tickmarkr's own routing env seams from every child — a daemon carrying
|
|
139
255
|
// TICKMARKR_QUALITY leaked it into baseline/gate/tip-verify children, turning a dogfood
|
|
140
256
|
// repo's route() tests red inside the gates. Scrub a copy at this one choke point so
|
|
@@ -161,13 +277,22 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
161
277
|
// detached: bash gets its own process group so a timeout can kill the whole tree —
|
|
162
278
|
// SIGKILLing bash alone orphans grandchildren (codex/pi) that hold the stdio pipes
|
|
163
279
|
// open, so "close" never fires and the promise wedges forever (v1.33.1 init hang).
|
|
164
|
-
|
|
280
|
+
let p;
|
|
281
|
+
try {
|
|
282
|
+
p = (spawnChild ?? spawn)("bash", [login ? "-lc" : "-c", cmd], { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(error) });
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
165
288
|
let stdout = "", stderr = "";
|
|
166
289
|
const stdoutDecoder = new StringDecoder("utf8");
|
|
167
290
|
const stderrDecoder = new StringDecoder("utf8");
|
|
168
291
|
let timedOut = false, reapedGroup = false, done = false, started = false, outputSeen = false;
|
|
169
292
|
let reapError, exitedCode;
|
|
170
293
|
let signalExit = false;
|
|
294
|
+
let exitSignal = null;
|
|
295
|
+
let spawnError;
|
|
171
296
|
let reapTimer;
|
|
172
297
|
let drainTimer;
|
|
173
298
|
const finish = (code, err) => {
|
|
@@ -180,6 +305,12 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
180
305
|
options.signal?.removeEventListener("abort", abort);
|
|
181
306
|
stdout += stdoutDecoder.end();
|
|
182
307
|
stderr += stderrDecoder.end();
|
|
308
|
+
observation.emit({
|
|
309
|
+
outcome: options.signal?.aborted ? "cancelled" : timedOut ? "timed-out" : spawnError ? "spawn-failed" : "completed",
|
|
310
|
+
pid: p.pid, exitCode: signalExit || spawnError ? null : code,
|
|
311
|
+
signal: exitSignal, ...(spawnError ? { error: spawnError } : {}),
|
|
312
|
+
durationMs: Date.now() - startedAt,
|
|
313
|
+
});
|
|
183
314
|
resolve({
|
|
184
315
|
code,
|
|
185
316
|
...(signalExit ? { signalExit: true } : {}),
|
|
@@ -232,7 +363,7 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
232
363
|
options.onSpawn?.(p.pid);
|
|
233
364
|
if (options.signal?.aborted)
|
|
234
365
|
abort();
|
|
235
|
-
p.on("spawn", () => { started = true; }); // the command exists from here on — never retryable past it
|
|
366
|
+
p.on("spawn", () => { started = true; observation.emit({ outcome: "started", pid: p.pid }); }); // the command exists from here on — never retryable past it
|
|
236
367
|
// OBS-716: one stateful decoder per stream carries an incomplete UTF-8 sequence into that
|
|
237
368
|
// stream's next pipe chunk; decoding each chunk through string concatenation corrupts bytes at
|
|
238
369
|
// kernel-chosen boundaries. A deterministic fixture proves this decoder correct rather than
|
|
@@ -249,21 +380,24 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
249
380
|
stderr += stderrDecoder.write(d);
|
|
250
381
|
});
|
|
251
382
|
p.on("error", (e) => {
|
|
383
|
+
spawnError = String(e);
|
|
252
384
|
if (!done && !started && !outputSeen && e.code === RETRYABLE_SPAWN_CODE) {
|
|
253
385
|
done = true;
|
|
254
386
|
clearTimeout(timer);
|
|
255
387
|
options.signal?.removeEventListener("abort", abort);
|
|
388
|
+
observation.emit({ outcome: "spawn-failed", exitCode: null, signal: null, error: String(e), durationMs: Date.now() - startedAt });
|
|
256
389
|
resolve({ refused: e });
|
|
257
390
|
return;
|
|
258
391
|
}
|
|
259
392
|
finish(127, String(e));
|
|
260
393
|
});
|
|
261
|
-
p.on("close", (code) => { signalExit = code === null; finish(code ?? 1); });
|
|
394
|
+
p.on("close", (code, signal) => { signalExit = code === null; exitSignal = signal; finish(code ?? 1); });
|
|
262
395
|
// "close" waits for stdio to drain. Once bash exits normally, give descendants a bounded grace
|
|
263
396
|
// to exit with it; a survivor still in bash's detached group is then reaped so its inherited pipe
|
|
264
397
|
// cannot hold this promise until the command ceiling. A real timeout wins first and is never
|
|
265
398
|
// reclassified as a grace reap.
|
|
266
|
-
p.on("exit", (code) => {
|
|
399
|
+
p.on("exit", (code, signal) => {
|
|
400
|
+
exitSignal = signal;
|
|
267
401
|
signalExit = code === null;
|
|
268
402
|
exitedCode = code ?? 1;
|
|
269
403
|
// Allow a short pipe drain after killing, bounded even for an escaped descendant.
|
|
@@ -289,6 +423,9 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
289
423
|
return (async () => {
|
|
290
424
|
const startedAt = Date.now();
|
|
291
425
|
for (let n = 1;; n++) {
|
|
426
|
+
if (n > 1)
|
|
427
|
+
observation.begin(n);
|
|
428
|
+
observation.checkAbort();
|
|
292
429
|
const r = await attempt();
|
|
293
430
|
if (!("refused" in r))
|
|
294
431
|
return r;
|
|
@@ -301,8 +438,8 @@ function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
|
301
438
|
}
|
|
302
439
|
})();
|
|
303
440
|
}
|
|
304
|
-
export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
|
305
|
-
return shell(cmd, cwd, timeoutMs, true);
|
|
441
|
+
export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, options = {}) {
|
|
442
|
+
return shell(cmd, cwd, timeoutMs, true, options);
|
|
306
443
|
}
|
|
307
444
|
// Git plumbing never needs an operator profile; skip login-shell startup and its side effects.
|
|
308
445
|
export function shGit(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export interface ConsultGuidanceCarry {
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function outstandingConsultGuidance(events: JournalEvent[], taskId: string): ConsultGuidanceCarry | undefined;
|
|
62
62
|
export declare const UNIDENTIFIED = "<unidentified>";
|
|
63
|
+
export declare function boundedSymbol(note: string): string;
|
|
63
64
|
/**
|
|
64
65
|
* Structured findings for a BLOCKING review/judge gate result, parsed from the details the gate
|
|
65
66
|
* already writes (D-03: no gate-module change, so an older gate's prose degrades to one unclassified
|
|
@@ -143,8 +144,11 @@ export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
|
143
144
|
* `task-dispatch`: everything between the two — worktree recreation, setup, prompt write, slot
|
|
144
145
|
* allocation, the launch itself — can still die with no worker having read a word, and clearing at
|
|
145
146
|
* task-dispatch meant `--retry-failed` after exactly that death rebuilt the prompt without the gate
|
|
146
|
-
* failures OR the delivery failure that preceded it.
|
|
147
|
-
*
|
|
147
|
+
* failures OR the delivery failure that preceded it. Of the approvals, only a WAIVE clears (the operator
|
|
148
|
+
* retired the findings by fiat — the uphold case re-derives its own brief separately). OBS-1074: a
|
|
149
|
+
* plain approve, a scope grant or a recheck re-funds an attempt that must still see why the last one
|
|
150
|
+
* parked, plus the operator's stated reason — v2.5.7's T11 looped four times on one hygiene oracle
|
|
151
|
+
* because every approval erased exactly the finding the fresh attempt was funded to fix.
|
|
148
152
|
*/
|
|
149
153
|
export declare function journaledFailureBrief(events: JournalEvent[], taskId: string): string[];
|
|
150
154
|
/**
|
|
@@ -192,7 +196,7 @@ export declare function pendingRepairFindings(events: JournalEvent[], taskId: st
|
|
|
192
196
|
* a channel it is no longer using, and a later unrelated failure is not parked under a stale reason.
|
|
193
197
|
*/
|
|
194
198
|
export declare function activeRetryBan(events: JournalEvent[], taskId: string, channel: string): string | undefined;
|
|
195
|
-
export declare const PARK_KINDS: readonly ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved", "infra", "dispatch", "authoring", "scope-request"];
|
|
199
|
+
export declare const PARK_KINDS: readonly ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved", "infra", "dispatch", "authoring", "scope-request", "diff-cap"];
|
|
196
200
|
export type ParkKind = (typeof PARK_KINDS)[number];
|
|
197
201
|
export declare const RETRY_MODES: readonly ["resume", "fresh", "repair"];
|
|
198
202
|
export type RetryMode = (typeof RETRY_MODES)[number];
|
|
@@ -244,6 +248,7 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
244
248
|
"tip-moved": "tip-moved";
|
|
245
249
|
authoring: "authoring";
|
|
246
250
|
"scope-request": "scope-request";
|
|
251
|
+
"diff-cap": "diff-cap";
|
|
247
252
|
}>>;
|
|
248
253
|
tokens: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
249
254
|
input: z.ZodNumber;
|
|
@@ -298,6 +303,13 @@ export declare function gateResultJournalData(gate: string, pass: boolean, detai
|
|
|
298
303
|
signalQuality: number;
|
|
299
304
|
} & Record<string, unknown>;
|
|
300
305
|
export declare function recordedGraphDefinitionHash(events: JournalEvent[]): string | undefined;
|
|
306
|
+
/**
|
|
307
|
+
* OBS-1073 residual: the `--graph-changed` release belongs to the ENGAGEMENT, not to the launch call. The
|
|
308
|
+
* daemon writes it on the run-resume row; every replay that runs inside that engagement from another
|
|
309
|
+
* process (the approve CLI) reads it back here. Without it the first in-run approval after a released
|
|
310
|
+
* launch re-ran the whole-graph asserts the launch had waived and killed the daemon (v2.5.7 run …152220).
|
|
311
|
+
*/
|
|
312
|
+
export declare function engagementReleased(events: JournalEvent[]): boolean;
|
|
301
313
|
/** An approval is the durable authority; graph.json is only its materialized projection. */
|
|
302
314
|
export declare const ScopeAmendmentSchema: z.ZodObject<{
|
|
303
315
|
from: z.ZodString;
|
|
@@ -305,10 +317,16 @@ export declare const ScopeAmendmentSchema: z.ZodObject<{
|
|
|
305
317
|
beforeFiles: z.ZodArray<z.ZodString>;
|
|
306
318
|
files: z.ZodArray<z.ZodString>;
|
|
307
319
|
parkLine: z.ZodNumber;
|
|
320
|
+
definition: z.ZodOptional<z.ZodString>;
|
|
308
321
|
}, z.core.$strip>;
|
|
309
|
-
|
|
322
|
+
/**
|
|
323
|
+
* `release` is the operator's audited `--graph-changed`. `approvedDefinitions` supplies, per amended task,
|
|
324
|
+
* the definition fingerprint the amendment was granted against when the row itself carries none (rows
|
|
325
|
+
* older than v2.5.7): the caller reads it from the run's materialized graph snapshot.
|
|
326
|
+
*/
|
|
327
|
+
export declare function replayScopeAmendments(graph: RunGraph, events: JournalEvent[], release?: boolean, approvedDefinitions?: ReadonlyMap<string, string>): RunGraph;
|
|
310
328
|
/** Publish/recover the audit before dispatch, even after a crash between approval and rehash. */
|
|
311
|
-
export declare function applyScopeAmendments(graph: RunGraph, journal: Journal, auditReplay?: boolean): RunGraph;
|
|
329
|
+
export declare function applyScopeAmendments(graph: RunGraph, journal: Journal, auditReplay?: boolean, release?: boolean): RunGraph;
|
|
312
330
|
export type EngagementCompare = {
|
|
313
331
|
comparable: true;
|
|
314
332
|
recorded: string;
|