tickmarkr 2.5.4 → 2.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/adapters/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +118 -9
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +199 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +102 -81
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +25 -8
- package/dist/config/config.js +41 -29
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +114 -19
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +39 -10
- package/dist/drivers/orca.d.ts +119 -10
- package/dist/drivers/orca.js +781 -110
- package/dist/gates/baseline.d.ts +17 -3
- package/dist/gates/baseline.js +63 -15
- package/dist/gates/cache.d.ts +101 -0
- package/dist/gates/cache.js +401 -0
- 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 +106 -14
- package/dist/gates/run-gates.d.ts +9 -1
- package/dist/gates/run-gates.js +407 -102
- package/dist/gates/test-manifest.d.ts +128 -0
- package/dist/gates/test-manifest.js +463 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +57 -0
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +45 -1
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.js +31 -6
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +15 -0
- package/dist/run/daemon.js +638 -167
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +50 -1
- package/dist/run/git.js +131 -12
- package/dist/run/journal.d.ts +16 -2
- package/dist/run/journal.js +115 -25
- package/dist/run/lease.d.ts +58 -0
- package/dist/run/lease.js +310 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- 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 +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +71 -21
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +49 -27
- package/dist/tui/ink/fleet-app.js +229 -38
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
- 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
|
@@ -4,7 +4,7 @@ export { ROUTING_ENV_SEAMS };
|
|
|
4
4
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
5
5
|
export declare const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
6
6
|
export declare const DEFAULT_FORK_CAP = "6";
|
|
7
|
-
/** Verification
|
|
7
|
+
/** Verification uses command leases; workers keep their concurrency budget. */
|
|
8
8
|
export declare const runWithVerificationBudget: <T>(capacity: RunCapacity, fn: () => Promise<T>) => Promise<T>;
|
|
9
9
|
/**
|
|
10
10
|
* OBS-618: how many PROCESSES one vitest fork can hold at its peak — the fork, a daemon it spawns,
|
|
@@ -76,6 +76,46 @@ export declare function readCapacity(value: unknown): CapacityRead;
|
|
|
76
76
|
* different capacity, or a current capacity the caller could not state → no.
|
|
77
77
|
*/
|
|
78
78
|
export declare function sameCapacity(recorded: unknown, current: RunCapacity | undefined): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* R41: the verification PROTOCOL a verdict was produced under — which implementation of the manifest
|
|
81
|
+
* discovery/report contract judged it, and the npm lifecycle policy its runner children ran with.
|
|
82
|
+
* Two verdicts are comparable only when both match: a manifest built from module collection ("vl1.1",
|
|
83
|
+
* the pre-stamp implementation) and one built from file specifications ("vl1.2") certify different
|
|
84
|
+
* sets, and a suite whose `pretest` hook ran (ignore-scripts=false) is a different measurement from
|
|
85
|
+
* one where npm skipped it. Bump VERIFICATION_PROTOCOL whenever discovery or report semantics change.
|
|
86
|
+
*/
|
|
87
|
+
export declare const VERIFICATION_PROTOCOL = "vl1.2";
|
|
88
|
+
export type RunnerLifecycle = "hooks" | "ignore-scripts" | "unknown";
|
|
89
|
+
export interface VerificationProtocol {
|
|
90
|
+
protocol: string;
|
|
91
|
+
/** The EFFECTIVE npm lifecycle policy a runner child receives: `hooks` (pretest/prebuild run),
|
|
92
|
+
* `ignore-scripts` (npm skips them), or `unknown` (could not be measured — never comparable). */
|
|
93
|
+
lifecycle: RunnerLifecycle;
|
|
94
|
+
/** Provenance only, never part of compatibility: an explicit process-scoped env export, or npm's
|
|
95
|
+
* own resolved config for that checkout (user/project npmrc). */
|
|
96
|
+
source: "explicit" | "npm-config" | "unknown";
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* R41: the lifecycle policy a runner child spawned from `env` in `cwd` receives. An explicit
|
|
100
|
+
* process-scoped `npm_config_ignore_scripts` export decides (npm reads env over every npmrc) and is
|
|
101
|
+
* recorded as such; otherwise the policy is MEASURED from npm's own resolved config for that checkout
|
|
102
|
+
* (`npm config get ignore-scripts` — user npmrc, project npmrc, env), so the identity binds what
|
|
103
|
+
* npm will actually do rather than a guess. A policy that cannot be measured is `unknown`, and an
|
|
104
|
+
* unknown policy is never compatible with anything, itself included. No env or npmrc is written here.
|
|
105
|
+
*/
|
|
106
|
+
export declare function runnerLifecycle(env?: NodeJS.ProcessEnv, cwd?: string): Pick<VerificationProtocol, "lifecycle" | "source">;
|
|
107
|
+
export declare function verificationProtocol(env?: NodeJS.ProcessEnv, cwd?: string): VerificationProtocol;
|
|
108
|
+
/**
|
|
109
|
+
* May a verdict recorded under `recorded` be reused — cached, replayed — by a session running
|
|
110
|
+
* `current`? Unlike capacity, ABSENT is NOT compatible: a row with no protocol stamp was produced by
|
|
111
|
+
* the pre-stamp implementation (module-collection discovery, lifecycle unstated), which is exactly
|
|
112
|
+
* the evidence a changed protocol must not inherit — positive or negative. Malformed → no. An
|
|
113
|
+
* `unknown` lifecycle on either side → no: two unmeasured policies are not the same policy.
|
|
114
|
+
* `source` is provenance and never enters the comparison — an explicit `false` and an npmrc `false`
|
|
115
|
+
* are the same effective policy.
|
|
116
|
+
*/
|
|
117
|
+
export declare function sameVerification(recorded: unknown, current: VerificationProtocol): boolean;
|
|
118
|
+
export declare const describeVerification: (value: unknown) => string;
|
|
79
119
|
export declare const describeCapacity: (value: unknown) => string;
|
|
80
120
|
/**
|
|
81
121
|
* The capacity a child spawned on THIS async context would receive: the same precedence `shell`
|
|
@@ -89,6 +129,7 @@ export declare const resolvedCapacity: () => RunCapacity;
|
|
|
89
129
|
export declare const DEFAULT_SHELL_TIMEOUT_MS = 600000;
|
|
90
130
|
export interface ShResult {
|
|
91
131
|
code: number;
|
|
132
|
+
signalExit?: boolean;
|
|
92
133
|
stdout: string;
|
|
93
134
|
stderr: string;
|
|
94
135
|
timedOut?: boolean;
|
|
@@ -104,6 +145,14 @@ export declare const setSpawnForTests: (fn: typeof spawn) => void;
|
|
|
104
145
|
export declare const resetSpawnForTests: () => void;
|
|
105
146
|
export declare const SPAWN_ATTEMPT_LIMIT = 4;
|
|
106
147
|
export declare const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
148
|
+
export interface ShellOptions {
|
|
149
|
+
env?: NodeJS.ProcessEnv;
|
|
150
|
+
signal?: AbortSignal;
|
|
151
|
+
onSpawn?: (pid: number | undefined) => void;
|
|
152
|
+
onTimeout?: () => void;
|
|
153
|
+
}
|
|
154
|
+
/** Shared command seam, including invocation-bound manifested runners. */
|
|
155
|
+
export declare function shell(cmd: string, cwd: string, timeoutMs: number, login?: boolean, options?: ShellOptions): Promise<ShResult>;
|
|
107
156
|
export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
108
157
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
109
158
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
package/dist/run/git.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { executionSignal } from "./execution-budget.js";
|
|
2
|
+
import { commandLeaseEnvironment, withCommandLease } from "./lease.js";
|
|
1
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
-
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";
|
|
5
7
|
import { join, resolve } from "node:path";
|
|
6
8
|
import { StringDecoder } from "node:string_decoder";
|
|
7
9
|
import { shq } from "../adapters/types.js";
|
|
@@ -18,7 +20,7 @@ export const DEFAULT_FORK_CAP = "6";
|
|
|
18
20
|
/** Worker concurrency is resolved once per run and isolated from other async runs. */
|
|
19
21
|
const forkBudget = new AsyncLocalStorage();
|
|
20
22
|
const verificationBudget = new AsyncLocalStorage();
|
|
21
|
-
/** Verification
|
|
23
|
+
/** Verification uses command leases; workers keep their concurrency budget. */
|
|
22
24
|
export const runWithVerificationBudget = (capacity, fn) => verificationBudget.run(capacity, fn);
|
|
23
25
|
/**
|
|
24
26
|
* OBS-618: how many PROCESSES one vitest fork can hold at its peak — the fork, a daemon it spawns,
|
|
@@ -81,6 +83,86 @@ export function sameCapacity(recorded, current) {
|
|
|
81
83
|
return false;
|
|
82
84
|
return read.capacity.forkCap === current.forkCap && read.capacity.cores === current.cores;
|
|
83
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
|
+
};
|
|
84
166
|
export const describeCapacity = (value) => {
|
|
85
167
|
const read = readCapacity(value);
|
|
86
168
|
return read.state === "present"
|
|
@@ -128,14 +210,20 @@ export const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
|
128
210
|
// RULING-P99-28: SHIP — every gate user can hit the inherited-pipe hang at this product seam.
|
|
129
211
|
/** Give a normally-exited shell's descendants this long to close their inherited pipes themselves. */
|
|
130
212
|
const SHELL_REAP_GRACE_MS = 2000;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
213
|
+
/** Shared command seam, including invocation-bound manifested runners. */
|
|
214
|
+
export function shell(cmd, cwd, timeoutMs, login = false, options = {}) {
|
|
215
|
+
const inherited = executionSignal();
|
|
216
|
+
const signal = inherited && options.signal ? AbortSignal.any([inherited, options.signal]) : inherited ?? options.signal;
|
|
217
|
+
signal?.throwIfAborted();
|
|
218
|
+
return withCommandLease(cmd, () => executeShell(cmd, cwd, timeoutMs, login, { ...options, signal }));
|
|
219
|
+
}
|
|
220
|
+
function executeShell(cmd, cwd, timeoutMs, login, options) {
|
|
221
|
+
options.signal?.throwIfAborted();
|
|
134
222
|
// OBS-74: scrub tickmarkr's own routing env seams from every child — a daemon carrying
|
|
135
223
|
// TICKMARKR_QUALITY leaked it into baseline/gate/tip-verify children, turning a dogfood
|
|
136
224
|
// repo's route() tests red inside the gates. Scrub a copy at this one choke point so
|
|
137
225
|
// children are hermetic by construction; the daemon's own process.env stays unchanged.
|
|
138
|
-
const env =
|
|
226
|
+
const env = commandLeaseEnvironment(options.env ?? process.env);
|
|
139
227
|
for (const k of ROUTING_ENV_SEAMS)
|
|
140
228
|
delete env[k];
|
|
141
229
|
// A run freezes the operator override and cores at startup; admission can lower the round cap.
|
|
@@ -163,17 +251,22 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
163
251
|
const stderrDecoder = new StringDecoder("utf8");
|
|
164
252
|
let timedOut = false, reapedGroup = false, done = false, started = false, outputSeen = false;
|
|
165
253
|
let reapError, exitedCode;
|
|
254
|
+
let signalExit = false;
|
|
166
255
|
let reapTimer;
|
|
256
|
+
let drainTimer;
|
|
167
257
|
const finish = (code, err) => {
|
|
168
258
|
if (done)
|
|
169
259
|
return;
|
|
170
260
|
done = true;
|
|
171
261
|
clearTimeout(timer);
|
|
172
262
|
clearTimeout(reapTimer);
|
|
263
|
+
clearTimeout(drainTimer);
|
|
264
|
+
options.signal?.removeEventListener("abort", abort);
|
|
173
265
|
stdout += stdoutDecoder.end();
|
|
174
266
|
stderr += stderrDecoder.end();
|
|
175
267
|
resolve({
|
|
176
268
|
code,
|
|
269
|
+
...(signalExit ? { signalExit: true } : {}),
|
|
177
270
|
stdout,
|
|
178
271
|
stderr: err ?? stderr,
|
|
179
272
|
timedOut,
|
|
@@ -183,8 +276,18 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
183
276
|
...(reapError ? { reapError } : {}),
|
|
184
277
|
});
|
|
185
278
|
};
|
|
279
|
+
// Escaped descendants can retain pipes even after the shell and its group are dead.
|
|
280
|
+
const boundDrain = () => {
|
|
281
|
+
if (!done && !drainTimer)
|
|
282
|
+
drainTimer = setTimeout(() => {
|
|
283
|
+
p.stdout?.destroy();
|
|
284
|
+
p.stderr?.destroy();
|
|
285
|
+
finish(exitedCode ?? 1);
|
|
286
|
+
}, 100);
|
|
287
|
+
};
|
|
186
288
|
const timer = setTimeout(() => {
|
|
187
289
|
timedOut = true;
|
|
290
|
+
options.onTimeout?.();
|
|
188
291
|
try {
|
|
189
292
|
process.kill(-p.pid, "SIGKILL");
|
|
190
293
|
}
|
|
@@ -198,7 +301,21 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
198
301
|
if (code !== "ESRCH")
|
|
199
302
|
finish(exitedCode ?? 1);
|
|
200
303
|
}
|
|
304
|
+
boundDrain();
|
|
201
305
|
}, timeoutMs);
|
|
306
|
+
const abort = () => {
|
|
307
|
+
try {
|
|
308
|
+
process.kill(-p.pid, "SIGKILL");
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
p.kill("SIGKILL");
|
|
312
|
+
}
|
|
313
|
+
boundDrain();
|
|
314
|
+
};
|
|
315
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
316
|
+
options.onSpawn?.(p.pid);
|
|
317
|
+
if (options.signal?.aborted)
|
|
318
|
+
abort();
|
|
202
319
|
p.on("spawn", () => { started = true; }); // the command exists from here on — never retryable past it
|
|
203
320
|
// OBS-716: one stateful decoder per stream carries an incomplete UTF-8 sequence into that
|
|
204
321
|
// stream's next pipe chunk; decoding each chunk through string concatenation corrupts bytes at
|
|
@@ -219,22 +336,23 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
219
336
|
if (!done && !started && !outputSeen && e.code === RETRYABLE_SPAWN_CODE) {
|
|
220
337
|
done = true;
|
|
221
338
|
clearTimeout(timer);
|
|
339
|
+
options.signal?.removeEventListener("abort", abort);
|
|
222
340
|
resolve({ refused: e });
|
|
223
341
|
return;
|
|
224
342
|
}
|
|
225
343
|
finish(127, String(e));
|
|
226
344
|
});
|
|
227
|
-
p.on("close", (code) => finish(code ?? 1));
|
|
345
|
+
p.on("close", (code) => { signalExit = code === null; finish(code ?? 1); });
|
|
228
346
|
// "close" waits for stdio to drain. Once bash exits normally, give descendants a bounded grace
|
|
229
347
|
// to exit with it; a survivor still in bash's detached group is then reaped so its inherited pipe
|
|
230
348
|
// cannot hold this promise until the command ceiling. A real timeout wins first and is never
|
|
231
349
|
// reclassified as a grace reap.
|
|
232
350
|
p.on("exit", (code) => {
|
|
351
|
+
signalExit = code === null;
|
|
233
352
|
exitedCode = code ?? 1;
|
|
234
|
-
|
|
235
|
-
|
|
353
|
+
// Allow a short pipe drain after killing, bounded even for an escaped descendant.
|
|
354
|
+
if (timedOut)
|
|
236
355
|
return;
|
|
237
|
-
}
|
|
238
356
|
reapTimer = setTimeout(() => {
|
|
239
357
|
if (done)
|
|
240
358
|
return;
|
|
@@ -255,6 +373,7 @@ function shell(cmd, cwd, timeoutMs, login) {
|
|
|
255
373
|
return (async () => {
|
|
256
374
|
const startedAt = Date.now();
|
|
257
375
|
for (let n = 1;; n++) {
|
|
376
|
+
options.signal?.throwIfAborted();
|
|
258
377
|
const r = await attempt();
|
|
259
378
|
if (!("refused" in r))
|
|
260
379
|
return r;
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type Assignment } from "../adapters/types.js";
|
|
3
3
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
4
|
-
import { type GateName, type Task, type TaskStatus } from "../graph/schema.js";
|
|
4
|
+
import { type GateName, type RunGraph, type Task, type TaskStatus } from "../graph/schema.js";
|
|
5
5
|
import { type ProfileDiscount, type RoutingProfile } from "../route/profile.js";
|
|
6
6
|
import { type DecisionEventWrite, type TrackedJournalRow } from "./protocol.js";
|
|
7
7
|
export interface JournalEvent {
|
|
@@ -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
|
|
@@ -192,7 +193,7 @@ export declare function pendingRepairFindings(events: JournalEvent[], taskId: st
|
|
|
192
193
|
* a channel it is no longer using, and a later unrelated failure is not parked under a stale reason.
|
|
193
194
|
*/
|
|
194
195
|
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"];
|
|
196
|
+
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
197
|
export type ParkKind = (typeof PARK_KINDS)[number];
|
|
197
198
|
export declare const RETRY_MODES: readonly ["resume", "fresh", "repair"];
|
|
198
199
|
export type RetryMode = (typeof RETRY_MODES)[number];
|
|
@@ -243,6 +244,8 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
243
244
|
"merge-conflict": "merge-conflict";
|
|
244
245
|
"tip-moved": "tip-moved";
|
|
245
246
|
authoring: "authoring";
|
|
247
|
+
"scope-request": "scope-request";
|
|
248
|
+
"diff-cap": "diff-cap";
|
|
246
249
|
}>>;
|
|
247
250
|
tokens: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
248
251
|
input: z.ZodNumber;
|
|
@@ -297,6 +300,17 @@ export declare function gateResultJournalData(gate: string, pass: boolean, detai
|
|
|
297
300
|
signalQuality: number;
|
|
298
301
|
} & Record<string, unknown>;
|
|
299
302
|
export declare function recordedGraphDefinitionHash(events: JournalEvent[]): string | undefined;
|
|
303
|
+
/** An approval is the durable authority; graph.json is only its materialized projection. */
|
|
304
|
+
export declare const ScopeAmendmentSchema: z.ZodObject<{
|
|
305
|
+
from: z.ZodString;
|
|
306
|
+
to: z.ZodString;
|
|
307
|
+
beforeFiles: z.ZodArray<z.ZodString>;
|
|
308
|
+
files: z.ZodArray<z.ZodString>;
|
|
309
|
+
parkLine: z.ZodNumber;
|
|
310
|
+
}, z.core.$strip>;
|
|
311
|
+
export declare function replayScopeAmendments(graph: RunGraph, events: JournalEvent[]): RunGraph;
|
|
312
|
+
/** Publish/recover the audit before dispatch, even after a crash between approval and rehash. */
|
|
313
|
+
export declare function applyScopeAmendments(graph: RunGraph, journal: Journal, auditReplay?: boolean): RunGraph;
|
|
300
314
|
export type EngagementCompare = {
|
|
301
315
|
comparable: true;
|
|
302
316
|
recorded: string;
|