tickmarkr 2.5.9 → 2.6.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/claude-code.js +19 -6
- package/dist/adapters/prompt.d.ts +2 -1
- package/dist/adapters/prompt.js +11 -1
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +10 -0
- package/dist/cli/commands/fleet.js +26 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/report.js +11 -1
- package/dist/cli/commands/status.js +13 -3
- package/dist/cli/commands/verify.js +2 -0
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +2 -0
- package/dist/compile/native.js +39 -4
- package/dist/drivers/orca.d.ts +1 -1
- package/dist/drivers/orca.js +27 -5
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +9 -1
- package/dist/gates/cache.d.ts +11 -1
- package/dist/gates/cache.js +35 -15
- package/dist/gates/llm.js +4 -1
- package/dist/gates/review.d.ts +2 -3
- package/dist/gates/review.js +28 -35
- package/dist/gates/run-gates.d.ts +4 -0
- package/dist/gates/run-gates.js +17 -4
- package/dist/gates/test-manifest.d.ts +17 -0
- package/dist/gates/test-manifest.js +109 -9
- package/dist/gates/test-reporter.js +4 -0
- package/dist/graph/graph.d.ts +7 -3
- package/dist/graph/graph.js +21 -5
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +10 -39
- package/dist/route/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +9 -1
- package/dist/run/consult.js +5 -9
- package/dist/run/daemon.d.ts +16 -1
- package/dist/run/daemon.js +554 -77
- package/dist/run/git.d.ts +46 -1
- package/dist/run/git.js +138 -6
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.d.ts +1 -1
- package/dist/run/journal.js +51 -6
- package/dist/run/merge.d.ts +1 -1
- package/dist/run/merge.js +30 -6
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +27 -19
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +4 -0
- package/dist/tui/cockpit/live-runtime.d.ts +4 -0
- package/dist/tui/cockpit/live-runtime.js +37 -3
- package/dist/tui/cockpit/live-store.d.ts +3 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.js +2 -1
- package/dist/tui/cockpit/run-view.d.ts +2 -4
- package/dist/tui/cockpit/run-view.js +9 -8
- package/package.json +1 -1
- package/schema/rungraph.schema.json +7 -0
- package/skills/tickmarkr-overseer/SKILL.md +76 -14
|
@@ -5,6 +5,12 @@ const bucketNames = ["failed", "human", "blocked", "pending"];
|
|
|
5
5
|
const strings = (value) => Array.isArray(value) && value.every(v => typeof v === "string") ? [...value] : undefined;
|
|
6
6
|
const identity = ({ source, line, id, generation }) => ({ source, line, id, generation });
|
|
7
7
|
const emptyGates = () => Object.fromEntries(GATE_NAMES.map(g => [g, { state: "not-run" }]));
|
|
8
|
+
/** Author channels recorded on a task-done row. A row written before authors were recorded carries none and
|
|
9
|
+
* projects nothing (never the dispatch); unattributed preserved work arrives as an explicit "unknown" entry. */
|
|
10
|
+
export const doneAuthors = (data) => strings(data.authors);
|
|
11
|
+
/** The note a status row or board row shows beside the last-dispatched channel when the merged
|
|
12
|
+
* authors are not exactly that channel (a carry-only seat, a mixed subject, or legacy unknown). */
|
|
13
|
+
export const authorsNote = (authors, channel) => authors === undefined || (authors.length === 1 && authors[0] === channel) ? undefined : `authors ${authors.join("+")}`;
|
|
8
14
|
/** Incremental pure lifecycle fold. Retains facts per task/gate, never verdict bodies or event history. */
|
|
9
15
|
export class OperatorStateFold {
|
|
10
16
|
tasks = new Map();
|
|
@@ -75,6 +81,7 @@ export class OperatorStateFold {
|
|
|
75
81
|
t.dispatches++;
|
|
76
82
|
t.parkKind = undefined;
|
|
77
83
|
t.gates = emptyGates();
|
|
84
|
+
t.gateActivity = undefined;
|
|
78
85
|
t.attempt = typeof e.data.attempt === "number" ? e.data.attempt : undefined;
|
|
79
86
|
// These are recorded values only: no guessed directory or universal silence threshold.
|
|
80
87
|
t.path = typeof e.data.worktree === "string" ? e.data.worktree : typeof e.data.cwd === "string" ? e.data.cwd : undefined;
|
|
@@ -84,8 +91,10 @@ export class OperatorStateFold {
|
|
|
84
91
|
if (a && typeof a.adapter === "string" && typeof a.model === "string")
|
|
85
92
|
t.channel = `${a.adapter}:${a.model}`;
|
|
86
93
|
}
|
|
87
|
-
if (e.event === "task-done")
|
|
94
|
+
if (e.event === "task-done") {
|
|
88
95
|
t.state = "completed";
|
|
96
|
+
t.authors = doneAuthors(e.data);
|
|
97
|
+
}
|
|
89
98
|
if (e.event === "merge") {
|
|
90
99
|
t.state = "merged";
|
|
91
100
|
t.merged = true;
|
|
@@ -104,12 +113,39 @@ export class OperatorStateFold {
|
|
|
104
113
|
if (e.event === "task-approved") {
|
|
105
114
|
t.state = "pending";
|
|
106
115
|
t.parkKind = undefined;
|
|
116
|
+
t.gateActivity = undefined;
|
|
107
117
|
this.approved = !this.active;
|
|
118
|
+
if (e.data.release === "recheck")
|
|
119
|
+
t.gates = emptyGates();
|
|
120
|
+
}
|
|
121
|
+
if (e.event === "phase-start" && e.data.phase === "gates")
|
|
122
|
+
t.gateActivity = undefined;
|
|
123
|
+
if (e.event === "suite-wait" || e.event === "host-degraded") {
|
|
124
|
+
// Only this row's attribution is authority. Legacy task-only waits must not borrow a red gate.
|
|
125
|
+
const gate = typeof e.data.gate === "string" && GATE_NAMES.includes(e.data.gate) ? e.data.gate : undefined;
|
|
126
|
+
t.gateActivity = { state: "queued", gate, reason: e.event, evidence: ref,
|
|
127
|
+
...(typeof e.data.count === "number" ? { count: e.data.count } : {}) };
|
|
128
|
+
if (gate)
|
|
129
|
+
t.gates[gate] = { state: "queued", evidence: ref };
|
|
130
|
+
}
|
|
131
|
+
if (e.event === "suite-admitted")
|
|
132
|
+
t.gateActivity = undefined;
|
|
133
|
+
if (["task-done", "merge", "task-failed", "task-human", "task-blocked"].includes(e.event))
|
|
134
|
+
t.gateActivity = undefined;
|
|
135
|
+
const phaseGate = e.event === "phase-start" && typeof e.data.phase === "string"
|
|
136
|
+
? e.data.phase.startsWith("gate:") ? e.data.phase.slice(5) : e.data.phase === "judge" ? "acceptance" : e.data.phase === "review" ? "review" : undefined
|
|
137
|
+
: undefined;
|
|
138
|
+
const gate = e.data.gate ?? phaseGate;
|
|
139
|
+
if ((e.event === "phase-start" && (phaseGate !== undefined || e.data.gate !== undefined)) || e.event === "gate-start" || e.event === "gate-phase-start") {
|
|
140
|
+
t.gateActivity = { state: "running", evidence: ref };
|
|
108
141
|
}
|
|
109
|
-
const gate = e.data.gate;
|
|
110
142
|
if (typeof gate === "string" && GATE_NAMES.includes(gate)) {
|
|
111
|
-
if (e.event === "gate-start")
|
|
143
|
+
if (e.event === "gate-start" || e.event === "gate-phase-start" || e.event === "phase-start") {
|
|
112
144
|
t.gates[gate] = { state: "running", evidence: ref };
|
|
145
|
+
t.gateActivity = { state: "running", gate, evidence: ref };
|
|
146
|
+
}
|
|
147
|
+
if (e.event === "gate-result" && t.gateActivity?.gate === gate)
|
|
148
|
+
t.gateActivity = undefined;
|
|
113
149
|
if (e.event === "gate-result" && gate === "review")
|
|
114
150
|
t.reviewRounds++;
|
|
115
151
|
if (e.event === "gate-result")
|
|
@@ -126,7 +162,7 @@ export class OperatorStateFold {
|
|
|
126
162
|
}
|
|
127
163
|
return t;
|
|
128
164
|
}
|
|
129
|
-
snapshot({ graph, sequence = 0, observedAt = Date.now(), readable = true } = {}) {
|
|
165
|
+
snapshot({ graph, sequence = 0, observedAt = Date.now(), readable = true, graphAvailability } = {}) {
|
|
130
166
|
const hash = graph && graphDefinitionHash(graph);
|
|
131
167
|
const comparable = this.comparableTo(hash);
|
|
132
168
|
const ids = comparable ? [...graph.tasks.map(t => t.id), ...[...this.tasks.keys()].filter(id => !graph.tasks.some(t => t.id === id))] : [...this.tasks.keys()];
|
|
@@ -150,7 +186,7 @@ export class OperatorStateFold {
|
|
|
150
186
|
return {
|
|
151
187
|
sequence, observedAt, lastEventAt: this.lastEventAt, firstEventAt: this.firstEventAt, resumes: this.resumes, escalations: this.escalations, lifecycle,
|
|
152
188
|
label: this.approved ? "approved; resume required" : lifecycle,
|
|
153
|
-
green, currentTip, comparable, comparison: comparable ? "matching graph" : "not comparable",
|
|
189
|
+
green, currentTip, graphAvailability, comparable, comparison: comparable ? "matching graph" : "not comparable",
|
|
154
190
|
merged,
|
|
155
191
|
planned, tasks, buckets,
|
|
156
192
|
gatesRan: { passed: this.passed, total: this.total }, latestRunEnd: this.end?.evidence,
|
package/dist/run/stall.d.ts
CHANGED
|
@@ -13,7 +13,42 @@ export declare function resetHarvestCpuFlatMsForTests(): void;
|
|
|
13
13
|
/** The dispatch shell records its foreground group in an attempt-unique artifact. */
|
|
14
14
|
export declare function readOwnedProcessGroup(path: string): number | undefined;
|
|
15
15
|
/** Never infer ownership from a task name or scan another attempt's marker when reaping. */
|
|
16
|
-
export declare function reapOwnedProcessGroup(group: number | undefined, cwd: string): Promise<number[] | null>;
|
|
16
|
+
export declare function reapOwnedProcessGroup(group: number | undefined, cwd: string, ownership?: WorkerReapOwnership): Promise<number[] | null>;
|
|
17
|
+
export interface ReapProcess {
|
|
18
|
+
pid: number;
|
|
19
|
+
ppid: number;
|
|
20
|
+
group: number;
|
|
21
|
+
session: string;
|
|
22
|
+
identity: string;
|
|
23
|
+
command: string;
|
|
24
|
+
startedAt?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface WorkerReapOwnership {
|
|
27
|
+
marker: string;
|
|
28
|
+
/** Session recorded by the dispatch shell before its children can detach. */
|
|
29
|
+
session?: string;
|
|
30
|
+
parent?: {
|
|
31
|
+
pid: number;
|
|
32
|
+
startedAt: string;
|
|
33
|
+
};
|
|
34
|
+
identities?: Map<number, string>;
|
|
35
|
+
/** Descendants observed while the dispatch root was still alive. */
|
|
36
|
+
descendants?: Map<number, string>;
|
|
37
|
+
excludedGroups: number[];
|
|
38
|
+
excludedWorktrees: string[];
|
|
39
|
+
/** Discovery evidence is distinct from the final survivor census. */
|
|
40
|
+
strays: number[];
|
|
41
|
+
}
|
|
42
|
+
/** Bounded host boundary, also used to inject unreadability and pid reuse in regressions. */
|
|
43
|
+
export declare const workerReapHost: {
|
|
44
|
+
snapshot(): Promise<ReapProcess[] | undefined>;
|
|
45
|
+
inspect(pid: number): Promise<{
|
|
46
|
+
cwd: string;
|
|
47
|
+
suiteParent?: number;
|
|
48
|
+
} | undefined>;
|
|
49
|
+
identity: (pid: number) => Promise<string | null | undefined>;
|
|
50
|
+
kill(pid: number): void;
|
|
51
|
+
};
|
|
17
52
|
export declare function workerTreeCpuMs(marker: string, cwd: string): Promise<{
|
|
18
53
|
ms: number;
|
|
19
54
|
resolutionMs: number;
|
|
@@ -22,6 +57,7 @@ export declare class WorkerTreeCpuAccountant {
|
|
|
22
57
|
private marker;
|
|
23
58
|
private cwd;
|
|
24
59
|
private group?;
|
|
60
|
+
private identities?;
|
|
25
61
|
private active;
|
|
26
62
|
private loop;
|
|
27
63
|
private live;
|
|
@@ -29,7 +65,7 @@ export declare class WorkerTreeCpuAccountant {
|
|
|
29
65
|
private gaps;
|
|
30
66
|
private consecutiveGaps;
|
|
31
67
|
private latest;
|
|
32
|
-
constructor(marker: string, cwd: string, group?: (() => number | undefined) | undefined);
|
|
68
|
+
constructor(marker: string, cwd: string, group?: (() => number | undefined) | undefined, identities?: Map<number, string> | undefined);
|
|
33
69
|
private sample;
|
|
34
70
|
start(): Promise<void>;
|
|
35
71
|
read(): {
|
package/dist/run/stall.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
4
|
+
import { activeShellIdentities, processIdentity, resolveActiveShellIdentities, shGit, SUITE_PARENT_ENV } from "./git.js";
|
|
3
5
|
// OBS-82: normalize known presentation tokens before measuring transcript extent or filtering an
|
|
4
6
|
// LLM-bound transcript. This remains a closed allowlist — ANSI/VT escapes, braille-range spinner
|
|
5
7
|
// glyphs, and elapsed-time tokens bound to time-unit suffixes. Every other byte passes through
|
|
@@ -64,7 +66,9 @@ export function readOwnedProcessGroup(path) {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
/** Never infer ownership from a task name or scan another attempt's marker when reaping. */
|
|
67
|
-
export async function reapOwnedProcessGroup(group, cwd) {
|
|
69
|
+
export async function reapOwnedProcessGroup(group, cwd, ownership) {
|
|
70
|
+
if (ownership)
|
|
71
|
+
return reapWorkerProcesses(group, cwd, ownership);
|
|
68
72
|
if (group === undefined)
|
|
69
73
|
return null;
|
|
70
74
|
const own = await shGit(`ps -o pgid= -p ${process.pid}`, cwd, 5_000);
|
|
@@ -97,6 +101,259 @@ export async function reapOwnedProcessGroup(group, cwd) {
|
|
|
97
101
|
}
|
|
98
102
|
return survivors;
|
|
99
103
|
}
|
|
104
|
+
const below = (root, path) => {
|
|
105
|
+
const rel = relative(root, path);
|
|
106
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
107
|
+
};
|
|
108
|
+
// Every probe is async and bounded like the CPU accountant's ps: a loaded host is slow, not unreadable.
|
|
109
|
+
const probeExec = (command, args) => new Promise((resolve, reject) => execFile(command, args, { encoding: "utf8", timeout: 15_000, maxBuffer: 4 * 1024 * 1024 }, (error, stdout) => (error ? reject(error) : resolve(stdout))));
|
|
110
|
+
/** Bounded host boundary, also used to inject unreadability and pid reuse in regressions. */
|
|
111
|
+
export const workerReapHost = {
|
|
112
|
+
async snapshot() {
|
|
113
|
+
try {
|
|
114
|
+
const out = await probeExec("ps", ["-Awwo", "pid=,ppid=,pgid=,sess=,stat=,lstart=,command="]);
|
|
115
|
+
const rows = [];
|
|
116
|
+
const lines = out.split("\n").filter((line) => line.trim());
|
|
117
|
+
if (!lines.length || lines.length > 20_000)
|
|
118
|
+
return undefined;
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+\s+\S+\s+\d+\s+\S+\s+\d+)\s+(.*)$/.exec(line);
|
|
121
|
+
if (!m)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (m[5].startsWith("Z"))
|
|
124
|
+
continue;
|
|
125
|
+
const pid = Number(m[1]);
|
|
126
|
+
const identity = process.platform === "linux" ? await processIdentity(pid) : `${pid}:${m[6].trim().replace(/\s+/g, " ")}`;
|
|
127
|
+
// A process disappearing during ps is normal; a present, unreadable identity is not.
|
|
128
|
+
if (!identity) {
|
|
129
|
+
try {
|
|
130
|
+
process.kill(pid, 0);
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code !== "ESRCH")
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
rows.push({ pid, ppid: Number(m[2]), group: Number(m[3]), session: m[4], identity,
|
|
140
|
+
startedAt: m[6].trim().replace(/\s+/g, " "), command: m[7] });
|
|
141
|
+
}
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
async inspect(pid) {
|
|
149
|
+
try {
|
|
150
|
+
let cwd;
|
|
151
|
+
let env;
|
|
152
|
+
if (process.platform === "linux") {
|
|
153
|
+
cwd = realpathSync(`/proc/${pid}/cwd`);
|
|
154
|
+
env = readFileSync(`/proc/${pid}/environ`, "utf8").split("\0");
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const [open, command] = await Promise.all([
|
|
158
|
+
probeExec("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"]),
|
|
159
|
+
probeExec("ps", ["eww", "-p", String(pid), "-o", "command="]),
|
|
160
|
+
]);
|
|
161
|
+
const path = open.split("\n").find((line) => line.startsWith("n"))?.slice(1);
|
|
162
|
+
if (!path)
|
|
163
|
+
return undefined;
|
|
164
|
+
cwd = realpathSync(path);
|
|
165
|
+
env = command.split(/\s+/);
|
|
166
|
+
}
|
|
167
|
+
const parent = env.find((entry) => entry.startsWith(`${SUITE_PARENT_ENV}=`))?.slice(SUITE_PARENT_ENV.length + 1);
|
|
168
|
+
return { cwd, ...(parent && /^\d+$/.test(parent) ? { suiteParent: Number(parent) } : {}) };
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
// Lazy: a partial module mock of git.js must not fail at import time.
|
|
175
|
+
identity: (pid) => processIdentity(pid),
|
|
176
|
+
kill(pid) { process.kill(pid, "SIGKILL"); },
|
|
177
|
+
};
|
|
178
|
+
async function reapWorkerProcesses(group, cwd, ownership) {
|
|
179
|
+
let root;
|
|
180
|
+
let excludedPaths;
|
|
181
|
+
try {
|
|
182
|
+
root = realpathSync(cwd);
|
|
183
|
+
excludedPaths = ownership.excludedWorktrees.map((path) => realpathSync(path));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
let rows = await workerReapHost.snapshot();
|
|
189
|
+
if (!rows)
|
|
190
|
+
return null;
|
|
191
|
+
const roots = rows.filter((row) => row.command === `bash ${ownership.marker}` || row.command === `/bin/bash ${ownership.marker}`);
|
|
192
|
+
// Ownership evidence: the recorded group, a live dispatch root, or descendants observed while it
|
|
193
|
+
// lived. cwd alone is never authority, and neither is the session id (OBS-1170): on Linux a
|
|
194
|
+
// same-session SIBLING that never descended from the dispatch — an operator job in a kept pane
|
|
195
|
+
// whose cwd is the worktree — is not ours to signal. `ownership.session` stays recorded as
|
|
196
|
+
// evidence only. With none of these in a readable snapshot nothing owned is observable: the
|
|
197
|
+
// dispatch has retired, so the census is empty rather than unknown (a finished root leaves no row).
|
|
198
|
+
const groupRows = group === undefined ? [] : rows.filter((row) => row.group === group && row.pid !== process.pid);
|
|
199
|
+
if (roots.length === 0 && !ownership.descendants?.size && groupRows.length === 0)
|
|
200
|
+
return [];
|
|
201
|
+
const owned = new Map(ownership.descendants);
|
|
202
|
+
const protectedIds = new Map();
|
|
203
|
+
const recordedParent = rows.find((row) => row.pid === ownership.parent?.pid && row.startedAt === ownership.parent.startedAt);
|
|
204
|
+
// Never reap a pane's shell (an ancestor of the dispatch root), or the daemon.
|
|
205
|
+
for (const start of [...roots.map((row) => row.ppid), ...(recordedParent ? [recordedParent.pid] : []), process.pid]) {
|
|
206
|
+
const seen = new Set();
|
|
207
|
+
for (let pid = start; pid && !seen.has(pid);) {
|
|
208
|
+
seen.add(pid);
|
|
209
|
+
const row = rows.find((p) => p.pid === pid);
|
|
210
|
+
if (!row)
|
|
211
|
+
break;
|
|
212
|
+
protectedIds.set(pid, row.identity);
|
|
213
|
+
pid = row.ppid;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const targets = new Map();
|
|
217
|
+
const observed = ownership.identities ?? new Map();
|
|
218
|
+
for (const [pid, identity] of owned)
|
|
219
|
+
if (!observed.has(pid))
|
|
220
|
+
observed.set(pid, identity);
|
|
221
|
+
for (const row of rows)
|
|
222
|
+
if (!observed.has(row.pid))
|
|
223
|
+
observed.set(row.pid, row.identity);
|
|
224
|
+
let unknown = false;
|
|
225
|
+
// Bounds the whole sweep; each probe is bounded on its own (15 s) and never blocks the loop.
|
|
226
|
+
const deadline = Date.now() + 60_000;
|
|
227
|
+
// One cwd verdict per identity: a later round never re-probes a process it already classified.
|
|
228
|
+
const inspected = new Set();
|
|
229
|
+
const discover = async (snapshot) => {
|
|
230
|
+
const children = new Map();
|
|
231
|
+
for (const row of snapshot) {
|
|
232
|
+
const siblings = children.get(row.ppid) ?? [];
|
|
233
|
+
siblings.push(row.pid);
|
|
234
|
+
children.set(row.ppid, siblings);
|
|
235
|
+
}
|
|
236
|
+
const descendants = (seeds, stop = new Set()) => {
|
|
237
|
+
const queue = [...seeds];
|
|
238
|
+
for (let index = 0; index < queue.length; index++) {
|
|
239
|
+
for (const pid of children.get(queue[index]) ?? [])
|
|
240
|
+
if (!seeds.has(pid) && !stop.has(pid)) {
|
|
241
|
+
seeds.add(pid);
|
|
242
|
+
queue.push(pid);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const tree = new Set(snapshot.filter((row) => owned.get(row.pid) === row.identity || roots.some((r) => r.pid === row.pid && r.identity === row.identity)
|
|
247
|
+
|| (group !== undefined && row.group === group)).map((row) => row.pid));
|
|
248
|
+
descendants(tree);
|
|
249
|
+
await resolveActiveShellIdentities();
|
|
250
|
+
const receipts = activeShellIdentities();
|
|
251
|
+
for (const [pid, identity] of receipts)
|
|
252
|
+
if (identity === undefined && tree.has(pid))
|
|
253
|
+
unknown = true;
|
|
254
|
+
const excluded = new Set(snapshot.filter((row) => row.pid === process.pid
|
|
255
|
+
|| ownership.excludedGroups.includes(row.group)
|
|
256
|
+
|| (receipts.has(row.pid) && (receipts.get(row.pid) === undefined || receipts.get(row.pid) === row.identity))).map((row) => row.pid));
|
|
257
|
+
// A daemon ancestor is protected, but the explicitly owned dispatch branch is not.
|
|
258
|
+
descendants(excluded, new Set(snapshot.filter((row) => roots.some((r) => r.pid === row.pid && r.identity === row.identity)).map((row) => row.pid)));
|
|
259
|
+
for (const row of snapshot)
|
|
260
|
+
if (protectedIds.get(row.pid) === row.identity)
|
|
261
|
+
excluded.add(row.pid);
|
|
262
|
+
const candidates = [];
|
|
263
|
+
for (const row of snapshot) {
|
|
264
|
+
if (!tree.has(row.pid) || excluded.has(row.pid))
|
|
265
|
+
continue;
|
|
266
|
+
if (observed.has(row.pid) && observed.get(row.pid) !== row.identity) {
|
|
267
|
+
unknown = true;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
observed.set(row.pid, row.identity);
|
|
271
|
+
owned.set(row.pid, row.identity);
|
|
272
|
+
if (!inspected.has(row.identity)) {
|
|
273
|
+
inspected.add(row.identity);
|
|
274
|
+
candidates.push(row);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (candidates.length && Date.now() > deadline) {
|
|
278
|
+
unknown = true;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Probe concurrently, then record verdicts in snapshot order.
|
|
282
|
+
const verdicts = await Promise.all(candidates.map(async (row) => {
|
|
283
|
+
const details = await workerReapHost.inspect(row.pid);
|
|
284
|
+
// A child that exited between the snapshot and this probe is a normal race, not
|
|
285
|
+
// unreadable ownership; only a still-live identity with no readable cwd is unknown.
|
|
286
|
+
// OBS-1173 add.2: a FAILED recheck (null) is unknown too — it never reads as gone.
|
|
287
|
+
if (!details) {
|
|
288
|
+
const recheck = await workerReapHost.identity(row.pid);
|
|
289
|
+
return recheck === row.identity || recheck === null ? "unknown" : "spared";
|
|
290
|
+
}
|
|
291
|
+
if (details.suiteParent === process.pid || excludedPaths.some((path) => below(path, details.cwd)))
|
|
292
|
+
return "spared";
|
|
293
|
+
if (!below(root, details.cwd))
|
|
294
|
+
return "spared";
|
|
295
|
+
// OBS-1173: the same race after a readable cwd — a short-lived tool child that exits between
|
|
296
|
+
// inspect and this recheck is gone, not unreadable. Only a live, different identity — or a
|
|
297
|
+
// failed probe (null, OBS-1173 add.2), which is never gone — is unknown.
|
|
298
|
+
const current = await workerReapHost.identity(row.pid);
|
|
299
|
+
if (current === row.identity)
|
|
300
|
+
return "target";
|
|
301
|
+
return current === undefined ? "spared" : "unknown";
|
|
302
|
+
}));
|
|
303
|
+
candidates.forEach((row, index) => {
|
|
304
|
+
if (verdicts[index] === "unknown")
|
|
305
|
+
unknown = true;
|
|
306
|
+
if (verdicts[index] !== "target")
|
|
307
|
+
return;
|
|
308
|
+
targets.set(row.pid, row.identity);
|
|
309
|
+
if (row.group !== group && !ownership.strays.includes(row.pid))
|
|
310
|
+
ownership.strays.push(row.pid);
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
await discover(rows); // capture detached ancestry before killing its parents
|
|
314
|
+
// A protected/foreign group member rules out a group signal. Signal only proven identities.
|
|
315
|
+
const members = group === undefined ? [] : rows.filter((row) => row.group === group);
|
|
316
|
+
if (group !== undefined && members.length && members.every((row) => targets.get(row.pid) === row.identity)
|
|
317
|
+
&& (await Promise.all(members.map((row) => workerReapHost.identity(row.pid)))).every((identity, index) => identity === members[index].identity)) {
|
|
318
|
+
try {
|
|
319
|
+
workerReapHost.kill(-group);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error.code !== "ESRCH")
|
|
323
|
+
unknown = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
let survivors = [];
|
|
327
|
+
if (targets.size === 0)
|
|
328
|
+
return unknown ? null : [];
|
|
329
|
+
for (let probe = 0; probe < 5; probe++) {
|
|
330
|
+
// Revalidate immediately before each signal, including after any slow cwd probe.
|
|
331
|
+
await Promise.all([...targets].map(async ([pid, identity]) => {
|
|
332
|
+
const current = await workerReapHost.identity(pid);
|
|
333
|
+
if (current !== identity) {
|
|
334
|
+
if (current !== undefined)
|
|
335
|
+
unknown = true;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
workerReapHost.kill(pid);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (error.code !== "ESRCH")
|
|
343
|
+
unknown = true;
|
|
344
|
+
}
|
|
345
|
+
}));
|
|
346
|
+
await new Promise((resolve) => setTimeout(resolve, probe === 0 ? 10 : 50));
|
|
347
|
+
rows = await workerReapHost.snapshot();
|
|
348
|
+
if (!rows || Date.now() > deadline)
|
|
349
|
+
return null;
|
|
350
|
+
await discover(rows);
|
|
351
|
+
survivors = rows.filter((row) => targets.get(row.pid) === row.identity).map((row) => row.pid);
|
|
352
|
+
if (!survivors.length)
|
|
353
|
+
return unknown ? null : [];
|
|
354
|
+
}
|
|
355
|
+
return unknown ? null : survivors;
|
|
356
|
+
}
|
|
100
357
|
let linuxClockTickMs;
|
|
101
358
|
function linuxProcessCpuMs(pid, cwd) {
|
|
102
359
|
if (!existsSync("/proc/self/stat"))
|
|
@@ -126,7 +383,7 @@ function linuxProcessCpuMs(pid, cwd) {
|
|
|
126
383
|
// Every non-seeded worker process descends from its attempt-unique dispatch script. One ps snapshot
|
|
127
384
|
// finds that root and its descendants. An empty tree is measurable zero; a failed or unparseable
|
|
128
385
|
// snapshot is undefined because missing evidence can never prove inactivity.
|
|
129
|
-
async function workerTreeCpuSnapshot(marker, cwd, group) {
|
|
386
|
+
async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
|
|
130
387
|
const snapshot = await shGit("ps -Awwo pid=,ppid=,time=,command=", cwd, 15_000);
|
|
131
388
|
if (snapshot.code !== 0)
|
|
132
389
|
return undefined;
|
|
@@ -142,6 +399,8 @@ async function workerTreeCpuSnapshot(marker, cwd, group) {
|
|
|
142
399
|
if (rows.length === 0)
|
|
143
400
|
return undefined;
|
|
144
401
|
const tree = new Set(rows.filter((p) => p.cmd.includes(marker)).map((p) => p.pid));
|
|
402
|
+
// CPU presentation matching is intentionally broader than authority to signal a process.
|
|
403
|
+
const owned = new Set(rows.filter((p) => p.cmd === `bash ${marker}` || p.cmd === `/bin/bash ${marker}`).map((p) => p.pid));
|
|
145
404
|
if (group !== undefined) {
|
|
146
405
|
const groups = await shGit("ps -Awwo pid=,pgid=", cwd, 15_000);
|
|
147
406
|
if (groups.code !== 0)
|
|
@@ -156,6 +415,10 @@ async function workerTreeCpuSnapshot(marker, cwd, group) {
|
|
|
156
415
|
for (let grew = true; grew;) {
|
|
157
416
|
grew = false;
|
|
158
417
|
for (const p of rows) {
|
|
418
|
+
if (!owned.has(p.pid) && owned.has(p.ppid)) {
|
|
419
|
+
owned.add(p.pid);
|
|
420
|
+
grew = true;
|
|
421
|
+
}
|
|
159
422
|
if (!tree.has(p.pid) && tree.has(p.ppid)) {
|
|
160
423
|
tree.add(p.pid);
|
|
161
424
|
grew = true;
|
|
@@ -167,6 +430,11 @@ async function workerTreeCpuSnapshot(marker, cwd, group) {
|
|
|
167
430
|
for (const p of rows) {
|
|
168
431
|
if (!tree.has(p.pid))
|
|
169
432
|
continue;
|
|
433
|
+
if (identities && owned.has(p.pid) && !identities.has(Number(p.pid))) {
|
|
434
|
+
const identity = await processIdentity(Number(p.pid));
|
|
435
|
+
if (identity)
|
|
436
|
+
identities.set(Number(p.pid), identity);
|
|
437
|
+
}
|
|
170
438
|
const cpu = await linuxProcessCpuMs(p.pid, cwd);
|
|
171
439
|
precise.set(p.pid, cpu?.ms ?? p.cpuMs);
|
|
172
440
|
if (cpu !== undefined)
|
|
@@ -197,6 +465,7 @@ export class WorkerTreeCpuAccountant {
|
|
|
197
465
|
marker;
|
|
198
466
|
cwd;
|
|
199
467
|
group;
|
|
468
|
+
identities;
|
|
200
469
|
active = false;
|
|
201
470
|
loop;
|
|
202
471
|
live = new Map();
|
|
@@ -204,13 +473,14 @@ export class WorkerTreeCpuAccountant {
|
|
|
204
473
|
gaps = 0;
|
|
205
474
|
consecutiveGaps = 0;
|
|
206
475
|
latest;
|
|
207
|
-
constructor(marker, cwd, group) {
|
|
476
|
+
constructor(marker, cwd, group, identities) {
|
|
208
477
|
this.marker = marker;
|
|
209
478
|
this.cwd = cwd;
|
|
210
479
|
this.group = group;
|
|
480
|
+
this.identities = identities;
|
|
211
481
|
}
|
|
212
482
|
async sample() {
|
|
213
|
-
const snapshot = await workerTreeCpuSnapshot(this.marker, this.cwd, this.group?.());
|
|
483
|
+
const snapshot = await workerTreeCpuSnapshot(this.marker, this.cwd, this.group?.(), this.identities);
|
|
214
484
|
if (snapshot === undefined) {
|
|
215
485
|
this.gaps++;
|
|
216
486
|
this.live.clear();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RunGraph } from "../../graph/schema.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type OperatorSnapshot } from "../../run/operator-state.js";
|
|
3
3
|
import type { AttemptHarvest, TaskRow } from "./derive.js";
|
|
4
4
|
/** The prototype's palette (tasks-redesign.mjs:24-30): xterm-256 indices, stated once. */
|
|
5
5
|
export declare const BOARD_PALETTE: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
2
|
+
import { authorsNote } from "../../run/operator-state.js";
|
|
2
3
|
import { cellWidth, sliceCells } from "./width.js";
|
|
3
4
|
/* ------------------------------------------------------------------------ */
|
|
4
5
|
/* BD-1 — the approved task board (RULING-231-19 §2, OBS-965 add.2). */
|
|
@@ -113,14 +114,15 @@ export function boardFrame(input, width) {
|
|
|
113
114
|
// recompile) lends nothing — no titles, deps, declarations or denominator — so the rows are the
|
|
114
115
|
// journal's tasks alone and every gate reads as declared.
|
|
115
116
|
const graphTasks = input.graph?.tasks && s.comparable ? input.graph.tasks : s.tasks.map((t) => ({ id: t.id, title: t.title ?? "", deps: [], files: [], gates: undefined }));
|
|
116
|
-
const uncomparable =
|
|
117
|
+
const uncomparable = !s.comparable;
|
|
118
|
+
const graphStatus = s.graphAvailability?.status ?? (input.graph ? "readable" : "absent");
|
|
117
119
|
const doneIds = new Set(s.tasks.filter(isDone).map((t) => t.id));
|
|
118
120
|
const t0 = s.firstEventAt ? Date.parse(s.firstEventAt) : input.now;
|
|
119
121
|
const t1 = s.lastEventAt ? Date.parse(s.lastEventAt) : input.now;
|
|
120
122
|
const wide = W >= 118;
|
|
121
123
|
const top = [];
|
|
122
124
|
top.push(`${bg(16, 2)(" tickmarkr ")} ${bold(input.runId)} ${chrome("│")} ` +
|
|
123
|
-
`${pass(`${doneIds.size}/${graphTasks.length} done`)} ` +
|
|
125
|
+
`${pass(`${doneIds.size}/${s.comparable ? graphTasks.length : "?"} done`)} ` +
|
|
124
126
|
(wide
|
|
125
127
|
? `${chrome("│")} ${warn(`${s.resumes} restarts`)} ${chrome("│")} ${fail(`${s.escalations} escalations`)} `
|
|
126
128
|
: `${chrome("│")} ${fail(`${s.escalations} esc`)} `) +
|
|
@@ -145,7 +147,7 @@ export function boardFrame(input, width) {
|
|
|
145
147
|
top.push(` ${accent("▌")} ${bold("NOW")} ${now}`);
|
|
146
148
|
top.push("");
|
|
147
149
|
}
|
|
148
|
-
top.push(` ${accent("▌")} ${bold("TASKS")} ${mute(uncomparable ? `${doneIds.size}
|
|
150
|
+
top.push(` ${accent("▌")} ${bold("TASKS")} ${mute(uncomparable ? `${doneIds.size} journal tasks merged · ${graphStatus === "readable" ? "graph not comparable" : `graph ${graphStatus}${s.graphAvailability?.error ? `: ${s.graphAvailability.error}` : ""}`}` : `${doneIds.size} of ${graphTasks.length} merged in this graph`)}` +
|
|
149
151
|
(wide ? mute(" · rows in dispatch order · gates left→right in declaration order") : ""));
|
|
150
152
|
top.push("");
|
|
151
153
|
// ── aligned gate header: each label truncated to the cell it owns (prototype :382-394) ──
|
|
@@ -170,7 +172,7 @@ export function boardFrame(input, width) {
|
|
|
170
172
|
};
|
|
171
173
|
const strip = BOARD_GATES.map((k) => {
|
|
172
174
|
const v = cell(k);
|
|
173
|
-
return pad(v === true ? pass("✔") : v === false ? fail("✖") : !declared.has(k) ? faint("─") : faint("·"), BOARD_CELL);
|
|
175
|
+
return pad(v === true ? pass("✔") : v === false ? fail("✖") : t?.gates[k]?.state === "queued" ? warn("Q") : t?.gates[k]?.state === "running" ? accent("R") : t?.gates[k]?.state === "unknown" ? warn("?") : t?.gates[k]?.state === "disabled" ? faint("D") : !declared.has(k) ? faint("─") : faint("·"), BOARD_CELL);
|
|
174
176
|
}).join("");
|
|
175
177
|
const unrun = BOARD_GATES.filter((k) => declared.has(k) && cell(k) === undefined).length;
|
|
176
178
|
const failedGates = BOARD_GATES.filter((k) => cell(k) === false);
|
|
@@ -204,21 +206,25 @@ export function boardFrame(input, width) {
|
|
|
204
206
|
parks >= 3 ? park(`◍ parked ×${parks}`) : "",
|
|
205
207
|
reviews > 2 ? warn(`↻ ${reviews} review rounds`) : "",
|
|
206
208
|
failedGates.length ? fail(`✖ ${failedGates.join(",")}`) : "",
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
// Merged authors beside the last-dispatched channel; equal-to-channel says nothing new.
|
|
210
|
+
authorsNote(t?.authors, t?.channel) ? faint(authorsNote(t?.authors, t?.channel)) : "",
|
|
211
|
+
t?.gateActivity
|
|
212
|
+
? `${t.gateActivity.state} · ${t.gateActivity.gate ?? "gate unknown"}${t.gateActivity.reason ? ` · ${t.gateActivity.reason}` : ""}${t.gateActivity.count === undefined ? "" : ` (${t.gateActivity.count} suites)`}`
|
|
213
|
+
: !attempts
|
|
214
|
+
? (() => {
|
|
215
|
+
const blockers = depIds.filter((d) => !doneIds.has(d));
|
|
216
|
+
if (!blockers.length)
|
|
217
|
+
return faint("queued");
|
|
218
|
+
const room = Math.max(10, W - BOARD_FIXED - LBL - CHAN);
|
|
219
|
+
let shown = blockers, dropped = 0;
|
|
220
|
+
while (shown.length > 1 && `waiting on ${shown.join(", ")}`.length + (dropped ? 5 : 0) > room) {
|
|
221
|
+
shown = shown.slice(0, -1);
|
|
222
|
+
dropped = blockers.length - shown.length;
|
|
223
|
+
}
|
|
224
|
+
return faint(`waiting on ${shown.join(", ")}${dropped ? ` +${dropped}` : ""}`);
|
|
225
|
+
})()
|
|
226
|
+
: unrun === declared.size ? faint("in flight")
|
|
227
|
+
: unrun > 2 ? faint(`${declared.size - unrun}/${declared.size} gates run`) : "",
|
|
222
228
|
].filter(Boolean);
|
|
223
229
|
rows.push({
|
|
224
230
|
id: g.id,
|
|
@@ -237,6 +243,8 @@ export function boardFrame(input, width) {
|
|
|
237
243
|
bottom.push(` ${pad("", 8)}${fail("UNMAPPED")} ${mute([...unmappedPaths].slice(0, 3).join(" "))}${unmappedPaths.size > 3 ? mute(` +${unmappedPaths.size - 3}`) : ""}`);
|
|
238
244
|
bottom.push(` ${faint("gates")} ${BOARD_GATES.map((k) => `${body(k.slice(0, BOARD_CELL - 1))} ${mute(k)}`).join(" ")}`);
|
|
239
245
|
bottom.push(` ${faint(" ")} ${pass("✔")} ${mute("passed")} ${fail("✖")} ${mute("failed")} ${faint("·")} ${mute("pending")} ${faint("─")} ${mute("not declared (acceptance and review are the optional two)")}`);
|
|
246
|
+
if (s.tasks.some(t => Object.values(t.gates).some(cell => ["queued", "running", "unknown", "disabled"].includes(cell.state))))
|
|
247
|
+
bottom.push(` ${accent("R")} running ${warn("Q")} queued ${warn("?")} unknown ${faint("D")} disabled`);
|
|
240
248
|
bottom.push("");
|
|
241
249
|
// the gap is stated, never silently skipped — derived, so it cannot go stale
|
|
242
250
|
const retired = s.tasks.map((t) => t.id).filter((id) => !graphTasks.some((g) => g.id === id)).sort();
|
|
@@ -115,6 +115,8 @@ export type TaskRow = {
|
|
|
115
115
|
readonly state?: TaskState;
|
|
116
116
|
readonly attempts?: number;
|
|
117
117
|
readonly actor?: string;
|
|
118
|
+
/** Distinct merged-author channels from task-done; legacy rows say ["unknown"]. Not the actor. */
|
|
119
|
+
readonly authors?: readonly string[];
|
|
118
120
|
readonly lastEventTime?: string;
|
|
119
121
|
/** Complete UTC instant paired with `lastEventTime`; additive for existing consumers. */
|
|
120
122
|
readonly lastEventTimestamp?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Consolidated C1/C6 readers use the incremental lifecycle contract; legacy captures keep their API.
|
|
2
2
|
export { readOperatorState as deriveOperatorState } from "../../run/operator-state.js";
|
|
3
|
+
import { doneAuthors } from "../../run/operator-state.js";
|
|
3
4
|
import { channelKey } from "../../adapters/types.js";
|
|
4
5
|
import { newestPark, permittedDecisionVerbs } from "../../cli/commands/approve.js";
|
|
5
6
|
import { parseOwnedName } from "../../drivers/types.js";
|
|
@@ -276,6 +277,8 @@ function deriveTasks(events, interrupted) {
|
|
|
276
277
|
if (state) {
|
|
277
278
|
const fact = task(event.taskId, index, event.ts);
|
|
278
279
|
fact.state = state;
|
|
280
|
+
if (event.event === "task-done")
|
|
281
|
+
fact.authors = doneAuthors(event.data);
|
|
279
282
|
// The park kind travels with the park: `task-human` records it, and the
|
|
280
283
|
// next recorded state — approved, done, failed — retires it with the park
|
|
281
284
|
// it named. A resolved state must not leave its badge behind (OBS-256).
|
|
@@ -641,6 +644,7 @@ function taskRows(events, tasks, graph, runId) {
|
|
|
641
644
|
...(state === undefined ? {} : { state }),
|
|
642
645
|
...(attempts === 0 ? {} : { attempts }),
|
|
643
646
|
...(assignment === undefined ? {} : { actor: channelKey(assignment) }),
|
|
647
|
+
...(fact?.authors === undefined ? {} : { authors: fact.authors }),
|
|
644
648
|
...(lastTs === undefined ? {} : { lastEventTime: eventTime(lastTs) }),
|
|
645
649
|
...(lastTs === undefined ? {} : { lastEventTimestamp: lastTs }),
|
|
646
650
|
...(title === undefined ? {} : { title }),
|
|
@@ -22,6 +22,10 @@ export interface ShellDelivery {
|
|
|
22
22
|
diagnostics: LiveStore["diagnostics"];
|
|
23
23
|
stage: (edits: readonly string[]) => void;
|
|
24
24
|
geometry: () => ShellCommit | undefined;
|
|
25
|
+
/** Application render commits since mount (OBS-1132: an idle board adds none). */
|
|
26
|
+
frames: () => number;
|
|
27
|
+
/** Model derivations (the App body) since mount. */
|
|
28
|
+
derivations: () => number;
|
|
25
29
|
}
|
|
26
30
|
export interface ConsolidatedOptions {
|
|
27
31
|
input: NodeJS.ReadStream;
|