humanish 0.49.0 → 0.51.0
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 +52 -3
- package/dist/concurrent-shared-world-lab.d.ts +5 -0
- package/dist/concurrent-shared-world-lab.js +28 -0
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +7 -0
- package/dist/cua-actor-lab.js +64 -3
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/e2b-terminal-lab.d.ts +7 -0
- package/dist/e2b-terminal-lab.js +54 -0
- package/dist/e2b-terminal-lab.js.map +1 -1
- package/dist/init-templates.js +7 -2
- package/dist/init-templates.js.map +1 -1
- package/dist/lab-engine.d.ts +11 -0
- package/dist/lab-engine.js +18 -0
- package/dist/lab-engine.js.map +1 -1
- package/dist/lab-summary.d.ts +36 -0
- package/dist/lab-summary.js +86 -0
- package/dist/lab-summary.js.map +1 -0
- package/dist/oss-lab.d.ts +3 -0
- package/dist/oss-lab.js.map +1 -1
- package/dist/oss-meta-lab.d.ts +3 -0
- package/dist/oss-meta-lab.js +13 -0
- package/dist/oss-meta-lab.js.map +1 -1
- package/dist/program.d.ts +14 -0
- package/dist/program.js +131 -9
- package/dist/program.js.map +1 -1
- package/dist/run-detail.d.ts +59 -0
- package/dist/run-detail.js +108 -0
- package/dist/run-detail.js.map +1 -0
- package/dist/run-index.d.ts +71 -0
- package/dist/run-index.js +206 -0
- package/dist/run-index.js.map +1 -0
- package/dist/run-paths.js +16 -1
- package/dist/run-paths.js.map +1 -1
- package/dist/run-projection.d.ts +182 -0
- package/dist/run-projection.js +349 -0
- package/dist/run-projection.js.map +1 -0
- package/dist/run-status.d.ts +139 -0
- package/dist/run-status.js +218 -0
- package/dist/run-status.js.map +1 -0
- package/dist/run.d.ts +15 -0
- package/dist/run.js +91 -6
- package/dist/run.js.map +1 -1
- package/dist/scripted-browser-lab.d.ts +5 -0
- package/dist/scripted-browser-lab.js +27 -0
- package/dist/scripted-browser-lab.js.map +1 -1
- package/dist/shared-world-lab.d.ts +5 -0
- package/dist/shared-world-lab.js +27 -0
- package/dist/shared-world-lab.js.map +1 -1
- package/dist/tui-app.js +402 -0
- package/dist/tui-contract.d.ts +84 -0
- package/dist/tui-contract.js +32 -0
- package/dist/tui-contract.js.map +1 -0
- package/dist/tui-launch.d.ts +48 -0
- package/dist/tui-launch.js +159 -0
- package/dist/tui-launch.js.map +1 -0
- package/dist/tui-project.d.ts +9 -0
- package/dist/tui-project.js +18 -0
- package/dist/tui-project.js.map +1 -0
- package/docs/contracts/run-bundle.md +8 -0
- package/docs/contracts/schemas.md +70 -1
- package/docs/goals/current.md +3 -2
- package/docs/ramp/README.md +1 -1
- package/package.json +8 -5
- package/skills/humanish/SKILL.md +19 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type PreparedOutputRoot } from "./selected-output-paths.js";
|
|
2
|
+
export declare const RUN_STATUS_SCHEMA = "humanish.run-status.v1";
|
|
3
|
+
/** The file, relative to the run directory. */
|
|
4
|
+
export declare const RUN_STATUS_FILE = "status.json";
|
|
5
|
+
/** How often a live run touches `updatedAt`. */
|
|
6
|
+
export declare const RUN_STATUS_TOUCH_MS = 5000;
|
|
7
|
+
/**
|
|
8
|
+
* A `running` record whose `updatedAt` is older than this is INTERRUPTED, not alive: the process
|
|
9
|
+
* died without finalizing (a dropped SSH, a killed terminal, a crash). Three touch intervals of
|
|
10
|
+
* slack so an ordinary scheduling hiccup or a slow disk never mislabels a healthy run.
|
|
11
|
+
*/
|
|
12
|
+
export declare const RUN_STATUS_STALE_MS: number;
|
|
13
|
+
/** Which manifest a run came from, when it came from one. */
|
|
14
|
+
export interface RunLabProvenance {
|
|
15
|
+
/** The lab id as declared in its manifest (`config.id`). */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Repo-relative manifest path, when the run came from a file on disk. */
|
|
18
|
+
path?: string;
|
|
19
|
+
/** `committed` = humanish/labs, `ignored` = a local overlay, `explicit` = a path the operator passed. */
|
|
20
|
+
origin?: "committed" | "ignored" | "explicit";
|
|
21
|
+
}
|
|
22
|
+
export type RunStatusState = "running" | "finished";
|
|
23
|
+
/** The outcome summary a finalized record carries. Derived from the bundle; never authoritative. */
|
|
24
|
+
export interface RunStatusOutcome {
|
|
25
|
+
/** `review.verdict` verbatim. */
|
|
26
|
+
verdict?: string;
|
|
27
|
+
/** True when the run's own envelope reported success. */
|
|
28
|
+
ok?: boolean;
|
|
29
|
+
/** `review.participants` counts, when the run recorded any. */
|
|
30
|
+
participants?: {
|
|
31
|
+
total: number;
|
|
32
|
+
reachedGoal: number;
|
|
33
|
+
reportedFriction?: number;
|
|
34
|
+
};
|
|
35
|
+
/** The run-level estimate, `null` when declared absent (never coerced to 0). */
|
|
36
|
+
estimatedCostUsd?: number | null;
|
|
37
|
+
durationMs?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface RunStatusRecord {
|
|
40
|
+
schema: typeof RUN_STATUS_SCHEMA;
|
|
41
|
+
runId: string;
|
|
42
|
+
state: RunStatusState;
|
|
43
|
+
mode: "dry-run" | "live";
|
|
44
|
+
/** Absent when the run did not come from a lab manifest (a library caller, a bare `run`). */
|
|
45
|
+
lab?: RunLabProvenance;
|
|
46
|
+
/** The pid that owns the run, for local liveness and (later) cancellation. */
|
|
47
|
+
pid: number;
|
|
48
|
+
startedAt: string;
|
|
49
|
+
/** Refreshed on a fixed cadence while the run is alive; the staleness signal. */
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
completedAt?: string;
|
|
52
|
+
outcome?: RunStatusOutcome;
|
|
53
|
+
}
|
|
54
|
+
export interface RunStatusHandle {
|
|
55
|
+
/** Resolves once the initial record has landed on disk. The write itself is fire-and-forget —
|
|
56
|
+
* starting a run must never block on its own index — but a caller that needs the record to
|
|
57
|
+
* exist before proceeding (a test, or a launcher that hands the run id to another process)
|
|
58
|
+
* can await this instead of polling. */
|
|
59
|
+
readonly started: Promise<void>;
|
|
60
|
+
/** Write `updatedAt` now. Called by the internal cadence; exposed for tests and for backends
|
|
61
|
+
* that want to mark a phase boundary. Never throws. */
|
|
62
|
+
touch(): Promise<void>;
|
|
63
|
+
/** Finalize: state `finished`, `completedAt`, and the derived outcome. Stops the cadence.
|
|
64
|
+
* Idempotent — a second call is a no-op, so a backend with several exit paths is safe. */
|
|
65
|
+
finish(outcome?: RunStatusOutcome): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Stop the cadence WITHOUT claiming an outcome. For a path that is abandoning the run: the record
|
|
68
|
+
* stays `running` and goes stale, which is the honest reading.
|
|
69
|
+
*
|
|
70
|
+
* Resolves when any IN-FLIGHT write has settled, so a caller that is about to delete the run
|
|
71
|
+
* directory can be sure nothing is still writing into it. Clearing the interval alone is not
|
|
72
|
+
* enough — a write started microseconds earlier is still on its way to disk.
|
|
73
|
+
*/
|
|
74
|
+
stop(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
export interface BeginRunStatusOptions {
|
|
77
|
+
runId: string;
|
|
78
|
+
mode: "dry-run" | "live";
|
|
79
|
+
lab?: RunLabProvenance;
|
|
80
|
+
/** Injectable clock (tests freeze it; the repo's `now()` convention). */
|
|
81
|
+
now?: () => number;
|
|
82
|
+
/** Injectable pid so a test never depends on the real process id. */
|
|
83
|
+
pid?: number;
|
|
84
|
+
/** Cadence override; 0 disables the interval entirely (tests drive `touch()` themselves). */
|
|
85
|
+
touchMs?: number;
|
|
86
|
+
}
|
|
87
|
+
/** A no-op handle, so a caller that cannot write status still has a uniform interface. */
|
|
88
|
+
export declare function inertRunStatus(): RunStatusHandle;
|
|
89
|
+
/**
|
|
90
|
+
* Bind a run's status records to the lifetime of the run itself.
|
|
91
|
+
*
|
|
92
|
+
* WHY THIS IS NOT A `finally` AT EACH BACKEND. A run function does not have one exit — the lab
|
|
93
|
+
* backends have 18 early `return`s between opening the record and finalizing it, every one of them
|
|
94
|
+
* a fail-closed path (bad subject, packing failure, missing key). Relying on each of those to
|
|
95
|
+
* remember the record is the same per-call-site discipline that already failed once on this
|
|
96
|
+
* contract, and the failure is silent: the run is over, the cadence keeps ticking, and the record
|
|
97
|
+
* keeps saying `running` — a listing surface then shows a dead run as alive for as long as the
|
|
98
|
+
* process lives. CI caught it as a deleted run directory racing a still-live writer.
|
|
99
|
+
*
|
|
100
|
+
* So the scope owns the lifetime. Control returning from the run function IS the run ending,
|
|
101
|
+
* whatever path it took, and any record still open at that moment is finalized with NO outcome:
|
|
102
|
+
* the run ended and we have no verdict to report. That is honest and it is different from both
|
|
103
|
+
* neighbours — a backend that finalized properly carries its real outcome, and a process that
|
|
104
|
+
* CRASHED never reaches here at all, leaving a `running` record to go stale and read as
|
|
105
|
+
* `interrupted`, which is exactly what happened.
|
|
106
|
+
*/
|
|
107
|
+
export declare function withRunStatusScope<T>(fn: () => Promise<T>): Promise<T>;
|
|
108
|
+
/**
|
|
109
|
+
* Start a run's status record and keep it fresh. Fire-and-forget by design: a status write that
|
|
110
|
+
* fails must never fail the run it describes, so every write swallows its error. The interval is
|
|
111
|
+
* `unref`'d — this file can never be the reason a process stays alive.
|
|
112
|
+
*/
|
|
113
|
+
export declare function beginRunStatus(runPaths: PreparedOutputRoot, options: BeginRunStatusOptions): RunStatusHandle;
|
|
114
|
+
/** The three ways a run reads from disk. `interrupted` is a `running` record gone stale. */
|
|
115
|
+
export type RunLiveness = "running" | "interrupted" | "finished";
|
|
116
|
+
/**
|
|
117
|
+
* Classify a status record. Pure, so the TUI, the CLI and tests share one definition of "alive".
|
|
118
|
+
* `nowMs` is passed in rather than read, so a classification is reproducible.
|
|
119
|
+
*/
|
|
120
|
+
export declare function classifyRunStatus(record: Pick<RunStatusRecord, "state" | "updatedAt">, nowMs: number, staleMs?: number): RunLiveness;
|
|
121
|
+
/** Shape guard for a record read off disk. Unknown extra fields are tolerated (additive contract). */
|
|
122
|
+
export declare function isRunStatusRecord(value: unknown): value is RunStatusRecord;
|
|
123
|
+
/**
|
|
124
|
+
* The legacy bridge: infer a lab id for a bundle written BEFORE this contract, where the only
|
|
125
|
+
* attribution was the `lab:<id>` convention on persona/scenario source strings. Deliberately
|
|
126
|
+
* conservative — it reads the convention and nothing else, and a `lab:` prefix with an empty
|
|
127
|
+
* remainder is not an id. Ids may contain colons (`oss:meta`), so only the FIRST segment is
|
|
128
|
+
* stripped. Returns undefined when the bundle carries no such marker.
|
|
129
|
+
*/
|
|
130
|
+
export declare function inferLegacyLabId(bundle: {
|
|
131
|
+
persona?: {
|
|
132
|
+
source?: string;
|
|
133
|
+
};
|
|
134
|
+
scenario?: {
|
|
135
|
+
source?: string;
|
|
136
|
+
};
|
|
137
|
+
}): string | undefined;
|
|
138
|
+
/** A monotonic elapsed-ms helper for callers that need a duration without trusting wall clocks. */
|
|
139
|
+
export declare function elapsedMsSince(startNs: bigint): number;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Run identity + liveness on disk (#455/#475): one small record per run that says WHICH LAB the
|
|
2
|
+
// run belongs to and WHETHER IT IS STILL ALIVE, written by every backend.
|
|
3
|
+
//
|
|
4
|
+
// Why it exists. Two questions could not be answered from the filesystem before this:
|
|
5
|
+
// 1. "which lab produced this run?" — the bundle carried no lab field; attribution rode a string
|
|
6
|
+
// convention (`persona.source = "lab:<id>"`) that is not universal.
|
|
7
|
+
// 2. "is this run still going?" — the mid-run bundle flush is gated on an interactive-observer
|
|
8
|
+
// callback, so a run launched by an agent (`lab run --json`) or detached wrote nothing at all
|
|
9
|
+
// until it completed. Anything watching the directory could not tell running from abandoned.
|
|
10
|
+
//
|
|
11
|
+
// EVIDENCE VS INDEX (the honesty rule that makes this safe). `run.json` remains the
|
|
12
|
+
// evidence-of-record; this file is a DERIVED INDEX + LIVENESS RECORD. `verify` never gates on it,
|
|
13
|
+
// nothing here is a claim about what a participant did, and when the two disagree `run.json` wins
|
|
14
|
+
// and this file is rebuildable from it. It exists so a reader can list and classify runs without
|
|
15
|
+
// parsing every bundle (a 25-run tree measured 152ms warm that way), and so a live run is
|
|
16
|
+
// recognizable while it is live.
|
|
17
|
+
//
|
|
18
|
+
// PUBLIC-SAFETY. Only public-safe fields: the run id, the lab id/path/origin (author-chosen names,
|
|
19
|
+
// the same strings `humanish lab list` already prints), the mode, a local pid, and timestamps.
|
|
20
|
+
// Deliberately NOT the hostname or any user/path identity — this file sits inside a run directory
|
|
21
|
+
// that an operator may share, so it must carry nothing a share-safety gate would have to strip.
|
|
22
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
|
+
import { hrtime } from "node:process";
|
|
24
|
+
import { writeContainedOutputFile } from "./selected-output-paths.js";
|
|
25
|
+
export const RUN_STATUS_SCHEMA = "humanish.run-status.v1";
|
|
26
|
+
/** The file, relative to the run directory. */
|
|
27
|
+
export const RUN_STATUS_FILE = "status.json";
|
|
28
|
+
/** How often a live run touches `updatedAt`. */
|
|
29
|
+
export const RUN_STATUS_TOUCH_MS = 5_000;
|
|
30
|
+
/**
|
|
31
|
+
* A `running` record whose `updatedAt` is older than this is INTERRUPTED, not alive: the process
|
|
32
|
+
* died without finalizing (a dropped SSH, a killed terminal, a crash). Three touch intervals of
|
|
33
|
+
* slack so an ordinary scheduling hiccup or a slow disk never mislabels a healthy run.
|
|
34
|
+
*/
|
|
35
|
+
export const RUN_STATUS_STALE_MS = RUN_STATUS_TOUCH_MS * 3;
|
|
36
|
+
/** A no-op handle, so a caller that cannot write status still has a uniform interface. */
|
|
37
|
+
export function inertRunStatus() {
|
|
38
|
+
return { started: Promise.resolve(), touch: async () => { }, finish: async () => { }, stop: async () => { } };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The set of handles opened inside the currently-running run, so the run's own return finalizes
|
|
42
|
+
* them. Scoped rather than global: labs can run concurrently in one process, and each must clean up
|
|
43
|
+
* only what it opened.
|
|
44
|
+
*/
|
|
45
|
+
const runStatusScope = new AsyncLocalStorage();
|
|
46
|
+
/**
|
|
47
|
+
* Bind a run's status records to the lifetime of the run itself.
|
|
48
|
+
*
|
|
49
|
+
* WHY THIS IS NOT A `finally` AT EACH BACKEND. A run function does not have one exit — the lab
|
|
50
|
+
* backends have 18 early `return`s between opening the record and finalizing it, every one of them
|
|
51
|
+
* a fail-closed path (bad subject, packing failure, missing key). Relying on each of those to
|
|
52
|
+
* remember the record is the same per-call-site discipline that already failed once on this
|
|
53
|
+
* contract, and the failure is silent: the run is over, the cadence keeps ticking, and the record
|
|
54
|
+
* keeps saying `running` — a listing surface then shows a dead run as alive for as long as the
|
|
55
|
+
* process lives. CI caught it as a deleted run directory racing a still-live writer.
|
|
56
|
+
*
|
|
57
|
+
* So the scope owns the lifetime. Control returning from the run function IS the run ending,
|
|
58
|
+
* whatever path it took, and any record still open at that moment is finalized with NO outcome:
|
|
59
|
+
* the run ended and we have no verdict to report. That is honest and it is different from both
|
|
60
|
+
* neighbours — a backend that finalized properly carries its real outcome, and a process that
|
|
61
|
+
* CRASHED never reaches here at all, leaving a `running` record to go stale and read as
|
|
62
|
+
* `interrupted`, which is exactly what happened.
|
|
63
|
+
*/
|
|
64
|
+
export async function withRunStatusScope(fn) {
|
|
65
|
+
const scope = new Set();
|
|
66
|
+
try {
|
|
67
|
+
return await runStatusScope.run(scope, fn);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
// `finish` swallows its own write errors and is idempotent, so this can neither throw over the
|
|
71
|
+
// run's own error nor overwrite an outcome a backend already recorded.
|
|
72
|
+
await Promise.all([...scope].map((handle) => handle.finish()));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Start a run's status record and keep it fresh. Fire-and-forget by design: a status write that
|
|
77
|
+
* fails must never fail the run it describes, so every write swallows its error. The interval is
|
|
78
|
+
* `unref`'d — this file can never be the reason a process stays alive.
|
|
79
|
+
*/
|
|
80
|
+
export function beginRunStatus(runPaths, options) {
|
|
81
|
+
const now = options.now ?? (() => Date.now());
|
|
82
|
+
const iso = () => new Date(now()).toISOString();
|
|
83
|
+
const startedAt = iso();
|
|
84
|
+
const base = {
|
|
85
|
+
schema: RUN_STATUS_SCHEMA,
|
|
86
|
+
runId: options.runId,
|
|
87
|
+
state: "running",
|
|
88
|
+
mode: options.mode,
|
|
89
|
+
...(options.lab === undefined ? {} : { lab: options.lab }),
|
|
90
|
+
pid: options.pid ?? process.pid,
|
|
91
|
+
startedAt,
|
|
92
|
+
updatedAt: startedAt
|
|
93
|
+
};
|
|
94
|
+
let finished = false;
|
|
95
|
+
let writing = Promise.resolve();
|
|
96
|
+
const write = (record) => {
|
|
97
|
+
// Serialized: two overlapping atomic writes of the same path would be a coin flip over which
|
|
98
|
+
// record survives, and a `running` record landing after a `finished` one would resurrect it.
|
|
99
|
+
writing = writing
|
|
100
|
+
.then(() => writeContainedOutputFile(runPaths, RUN_STATUS_FILE, `${JSON.stringify(record, null, 2)}\n`, "utf8"))
|
|
101
|
+
.catch(() => {
|
|
102
|
+
// Deliberately swallowed: the index is a convenience, the bundle is the evidence.
|
|
103
|
+
});
|
|
104
|
+
return writing;
|
|
105
|
+
};
|
|
106
|
+
const started = write(base);
|
|
107
|
+
const touchMs = options.touchMs ?? RUN_STATUS_TOUCH_MS;
|
|
108
|
+
let timer;
|
|
109
|
+
if (touchMs > 0) {
|
|
110
|
+
timer = setInterval(() => {
|
|
111
|
+
if (finished)
|
|
112
|
+
return;
|
|
113
|
+
void write({ ...base, updatedAt: iso() });
|
|
114
|
+
}, touchMs);
|
|
115
|
+
timer.unref?.();
|
|
116
|
+
}
|
|
117
|
+
const stop = () => {
|
|
118
|
+
if (timer !== undefined) {
|
|
119
|
+
clearInterval(timer);
|
|
120
|
+
timer = undefined;
|
|
121
|
+
}
|
|
122
|
+
// `writing` is the tail of the serialized write chain, so awaiting it awaits everything queued.
|
|
123
|
+
return writing.catch(() => undefined);
|
|
124
|
+
};
|
|
125
|
+
const scope = runStatusScope.getStore();
|
|
126
|
+
const handle = {
|
|
127
|
+
started,
|
|
128
|
+
async touch() {
|
|
129
|
+
if (finished)
|
|
130
|
+
return;
|
|
131
|
+
await write({ ...base, updatedAt: iso() });
|
|
132
|
+
},
|
|
133
|
+
async finish(outcome) {
|
|
134
|
+
if (finished)
|
|
135
|
+
return;
|
|
136
|
+
finished = true;
|
|
137
|
+
void stop();
|
|
138
|
+
scope?.delete(handle);
|
|
139
|
+
const completedAt = iso();
|
|
140
|
+
await write({
|
|
141
|
+
...base,
|
|
142
|
+
state: "finished",
|
|
143
|
+
updatedAt: completedAt,
|
|
144
|
+
completedAt,
|
|
145
|
+
...(outcome === undefined ? {} : { outcome })
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
stop() {
|
|
149
|
+
scope?.delete(handle);
|
|
150
|
+
return stop();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
// The enclosing run now owns this record's lifetime; see `withRunStatusScope`. A caller outside a
|
|
154
|
+
// scope (a direct library import) simply gets the old behavior.
|
|
155
|
+
scope?.add(handle);
|
|
156
|
+
return handle;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Classify a status record. Pure, so the TUI, the CLI and tests share one definition of "alive".
|
|
160
|
+
* `nowMs` is passed in rather than read, so a classification is reproducible.
|
|
161
|
+
*/
|
|
162
|
+
export function classifyRunStatus(record, nowMs, staleMs = RUN_STATUS_STALE_MS) {
|
|
163
|
+
if (record.state === "finished")
|
|
164
|
+
return "finished";
|
|
165
|
+
const updated = Date.parse(record.updatedAt);
|
|
166
|
+
if (!Number.isFinite(updated))
|
|
167
|
+
return "interrupted";
|
|
168
|
+
return nowMs - updated <= staleMs ? "running" : "interrupted";
|
|
169
|
+
}
|
|
170
|
+
/** Shape guard for a record read off disk. Unknown extra fields are tolerated (additive contract). */
|
|
171
|
+
export function isRunStatusRecord(value) {
|
|
172
|
+
if (value === null || typeof value !== "object")
|
|
173
|
+
return false;
|
|
174
|
+
const record = value;
|
|
175
|
+
if (record.schema !== RUN_STATUS_SCHEMA)
|
|
176
|
+
return false;
|
|
177
|
+
if (typeof record.runId !== "string" || record.runId === "")
|
|
178
|
+
return false;
|
|
179
|
+
if (record.state !== "running" && record.state !== "finished")
|
|
180
|
+
return false;
|
|
181
|
+
if (record.mode !== "dry-run" && record.mode !== "live")
|
|
182
|
+
return false;
|
|
183
|
+
if (typeof record.pid !== "number")
|
|
184
|
+
return false;
|
|
185
|
+
if (typeof record.startedAt !== "string" || typeof record.updatedAt !== "string")
|
|
186
|
+
return false;
|
|
187
|
+
if (record.lab !== undefined) {
|
|
188
|
+
if (record.lab === null || typeof record.lab !== "object")
|
|
189
|
+
return false;
|
|
190
|
+
if (typeof record.lab.id !== "string")
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The legacy bridge: infer a lab id for a bundle written BEFORE this contract, where the only
|
|
197
|
+
* attribution was the `lab:<id>` convention on persona/scenario source strings. Deliberately
|
|
198
|
+
* conservative — it reads the convention and nothing else, and a `lab:` prefix with an empty
|
|
199
|
+
* remainder is not an id. Ids may contain colons (`oss:meta`), so only the FIRST segment is
|
|
200
|
+
* stripped. Returns undefined when the bundle carries no such marker.
|
|
201
|
+
*/
|
|
202
|
+
export function inferLegacyLabId(bundle) {
|
|
203
|
+
for (const source of [bundle.persona?.source, bundle.scenario?.source]) {
|
|
204
|
+
if (typeof source !== "string")
|
|
205
|
+
continue;
|
|
206
|
+
if (!source.startsWith("lab:"))
|
|
207
|
+
continue;
|
|
208
|
+
const id = source.slice("lab:".length).trim();
|
|
209
|
+
if (id !== "")
|
|
210
|
+
return id;
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
/** A monotonic elapsed-ms helper for callers that need a duration without trusting wall clocks. */
|
|
215
|
+
export function elapsedMsSince(startNs) {
|
|
216
|
+
return Number((hrtime.bigint() - startNs) / 1000000n);
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=run-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-status.js","sourceRoot":"","sources":["../src/run-status.ts"],"names":[],"mappings":"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"}
|
package/dist/run.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { TaskFunnel } from "./tasks.js";
|
|
|
4
4
|
import { type CapturedGitState } from "./core/git-state.js";
|
|
5
5
|
import type { E2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
6
6
|
import { type PreparedRunArtifactPaths } from "./run-paths.js";
|
|
7
|
+
import { type RunLabProvenance } from "./run-status.js";
|
|
7
8
|
export declare const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v1";
|
|
8
9
|
export declare const SHARED_WORLD_SCHEMA = "humanish.shared-world.v1";
|
|
9
10
|
export declare const REVIEW_SCHEMA = "humanish.review.v1";
|
|
@@ -13,6 +14,8 @@ export declare const DOCTOR_SCHEMA = "humanish.doctor-result.v1";
|
|
|
13
14
|
export declare const CLEANUP_SCHEMA = "humanish.cleanup-result.v1";
|
|
14
15
|
export declare const PUBLIC_TARGET_CWD = "[target-cwd]";
|
|
15
16
|
export interface RunOptions {
|
|
17
|
+
/** Which manifest produced this run (#455). */
|
|
18
|
+
lab?: RunLabProvenance;
|
|
16
19
|
cwd: string;
|
|
17
20
|
actor?: string;
|
|
18
21
|
actorCommand?: string[];
|
|
@@ -744,6 +747,13 @@ export interface RunBundle {
|
|
|
744
747
|
* resource lease. Optional + additive; core never enumerates provider accounts.
|
|
745
748
|
*/
|
|
746
749
|
providerResources?: RunProviderResource[];
|
|
750
|
+
/**
|
|
751
|
+
* Which lab manifest produced this run (#455). Optional + additive: absent on every bundle
|
|
752
|
+
* written before this contract and on library callers who pass a LabConfig directly (the run is
|
|
753
|
+
* then honestly lab-less rather than guessed). For older bundles a reader may fall back to
|
|
754
|
+
* `inferLegacyLabId`, which reads only the historical `persona.source = "lab:<id>"` convention.
|
|
755
|
+
*/
|
|
756
|
+
lab?: RunLabProvenance;
|
|
747
757
|
/**
|
|
748
758
|
* OPTIONAL, ADDITIVE run-level cost ESTIMATE (humanish.run-cost-summary.v1): the sum of every
|
|
749
759
|
* lane's model-token estimate PLUS the E2B desktop-minute estimate, carrying the SAME
|
|
@@ -1037,6 +1047,11 @@ export interface DoctorResult {
|
|
|
1037
1047
|
message: string;
|
|
1038
1048
|
}>;
|
|
1039
1049
|
}
|
|
1050
|
+
/**
|
|
1051
|
+
* The synthetic/local backends. The body runs inside a status scope so that returning from it —
|
|
1052
|
+
* by any of its exits, including the fail-closed ones — finalizes whatever status records it
|
|
1053
|
+
* opened. See `withRunStatusScope`.
|
|
1054
|
+
*/
|
|
1040
1055
|
export declare function runDryRun(options: RunOptions): Promise<RunResult>;
|
|
1041
1056
|
type LocalActorTerminalStatus = Extract<RunSimulationStatus, "passed" | "failed" | "blocked" | "timed_out">;
|
|
1042
1057
|
/**
|
package/dist/run.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { lstat, readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { parse as parseYaml } from "yaml";
|
|
7
8
|
// The deterministic browser-persona driver lives in the scripted-browser-actor leaf module
|
|
@@ -22,6 +23,8 @@ import { round6 } from "./pricing.js";
|
|
|
22
23
|
import { containsSensitive, digestText, redactText, redactToSecretLabel, tailText } from "./redaction.js";
|
|
23
24
|
import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, prepareRunArtifactPaths, resolveExistingRunDirectory, resolveLatestRunDirectory, resolveRunsRoot, validatePreparedRunArtifactPaths } from "./run-paths.js";
|
|
24
25
|
import { probeKeySources } from "./key-resolution.js";
|
|
26
|
+
import { beginRunStatus, withRunStatusScope } from "./run-status.js";
|
|
27
|
+
import { TUI_MIN_NODE_MAJOR, nodeSupportsTui, tuiBundleUrl } from "./tui-contract.js";
|
|
25
28
|
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, bindExistingManagedHumanishOutputDirectory, prepareContainedOutputDirectory, prepareContainedOutputDirectoryRoot, prepareContainedOutputFile, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile, writePreparedRunLatestPointer } from "./selected-output-paths.js";
|
|
26
29
|
export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v1";
|
|
27
30
|
export const SHARED_WORLD_SCHEMA = "humanish.shared-world.v1";
|
|
@@ -165,7 +168,15 @@ const builtinScenario = {
|
|
|
165
168
|
source: "builtin:first-run-smoke",
|
|
166
169
|
sourceDigest: "builtin"
|
|
167
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* The synthetic/local backends. The body runs inside a status scope so that returning from it —
|
|
173
|
+
* by any of its exits, including the fail-closed ones — finalizes whatever status records it
|
|
174
|
+
* opened. See `withRunStatusScope`.
|
|
175
|
+
*/
|
|
168
176
|
export async function runDryRun(options) {
|
|
177
|
+
return withRunStatusScope(() => runDryRunInScope(options));
|
|
178
|
+
}
|
|
179
|
+
async function runDryRunInScope(options) {
|
|
169
180
|
const cwd = path.resolve(options.cwd);
|
|
170
181
|
const cwdError = await validateCwd(cwd);
|
|
171
182
|
const warnings = [];
|
|
@@ -262,6 +273,13 @@ export async function runDryRun(options) {
|
|
|
262
273
|
const selection = await loadDryRunSelection(projectRoot, humanishSource);
|
|
263
274
|
await assertPreparedSelectedOutputDirectory(projectRoot);
|
|
264
275
|
const runPaths = await prepareRunArtifactPaths(cwd, runId);
|
|
276
|
+
// Identity + liveness on disk (#455): uniform across every route, so a reader classifies any
|
|
277
|
+
// run from one small file instead of parsing bundles.
|
|
278
|
+
const runStatus = beginRunStatus(runPaths, {
|
|
279
|
+
runId,
|
|
280
|
+
mode: options.dryRun ? "dry-run" : "live",
|
|
281
|
+
...(options.lab === undefined ? {} : { lab: options.lab })
|
|
282
|
+
});
|
|
265
283
|
const artifactRoot = runPaths.relativeRunRoot;
|
|
266
284
|
if (humanishSource === "missing") {
|
|
267
285
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic dry-run defaults.");
|
|
@@ -281,6 +299,7 @@ export async function runDryRun(options) {
|
|
|
281
299
|
createdAt,
|
|
282
300
|
cwd,
|
|
283
301
|
artifactRoot,
|
|
302
|
+
...(options.lab === undefined ? {} : { lab: options.lab }),
|
|
284
303
|
source,
|
|
285
304
|
persona: selection.persona,
|
|
286
305
|
scenario: selection.scenario,
|
|
@@ -323,7 +342,7 @@ export async function runDryRun(options) {
|
|
|
323
342
|
review: createReviewSummary(),
|
|
324
343
|
feedbackCandidates: []
|
|
325
344
|
};
|
|
326
|
-
await writeRunBundleArtifacts(runPaths, bundle);
|
|
345
|
+
await writeRunBundleArtifacts(runPaths, bundle, runStatus);
|
|
327
346
|
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
328
347
|
schema: "humanish.latest-run.v1",
|
|
329
348
|
runId,
|
|
@@ -381,6 +400,13 @@ async function runBrowserAppProof(options) {
|
|
|
381
400
|
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
382
401
|
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
383
402
|
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
403
|
+
// Identity + liveness on disk (#455): uniform across every route, so a reader classifies any
|
|
404
|
+
// run from one small file instead of parsing bundles.
|
|
405
|
+
const runStatus = beginRunStatus(runPaths, {
|
|
406
|
+
runId,
|
|
407
|
+
mode: options.dryRun ? "dry-run" : "live",
|
|
408
|
+
...(options.lab === undefined ? {} : { lab: options.lab })
|
|
409
|
+
});
|
|
384
410
|
const artifactRoot = runPaths.relativeRunRoot;
|
|
385
411
|
if (selection.browserJourneyFailure) {
|
|
386
412
|
return {
|
|
@@ -426,6 +452,7 @@ async function runBrowserAppProof(options) {
|
|
|
426
452
|
createdAt,
|
|
427
453
|
cwd: options.cwd,
|
|
428
454
|
artifactRoot,
|
|
455
|
+
...(options.lab === undefined ? {} : { lab: options.lab }),
|
|
429
456
|
source,
|
|
430
457
|
persona: {
|
|
431
458
|
id: selection.persona.id,
|
|
@@ -549,7 +576,7 @@ async function runBrowserAppProof(options) {
|
|
|
549
576
|
review,
|
|
550
577
|
feedbackCandidates: []
|
|
551
578
|
};
|
|
552
|
-
await writeRunBundleArtifacts(runPaths, bundle);
|
|
579
|
+
await writeRunBundleArtifacts(runPaths, bundle, runStatus);
|
|
553
580
|
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
554
581
|
schema: "humanish.latest-run.v1",
|
|
555
582
|
runId,
|
|
@@ -702,6 +729,13 @@ async function runLocalCodexTui(options) {
|
|
|
702
729
|
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
703
730
|
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
704
731
|
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
732
|
+
// Identity + liveness on disk (#455): uniform across every route, so a reader classifies any
|
|
733
|
+
// run from one small file instead of parsing bundles.
|
|
734
|
+
const runStatus = beginRunStatus(runPaths, {
|
|
735
|
+
runId,
|
|
736
|
+
mode: options.dryRun ? "dry-run" : "live",
|
|
737
|
+
...(options.lab === undefined ? {} : { lab: options.lab })
|
|
738
|
+
});
|
|
705
739
|
const artifactRoot = runPaths.relativeRunRoot;
|
|
706
740
|
if (humanishSource === "missing") {
|
|
707
741
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
|
|
@@ -750,6 +784,7 @@ async function runLocalCodexTui(options) {
|
|
|
750
784
|
createdAt,
|
|
751
785
|
cwd: options.cwd,
|
|
752
786
|
artifactRoot,
|
|
787
|
+
...(options.lab === undefined ? {} : { lab: options.lab }),
|
|
753
788
|
source,
|
|
754
789
|
persona: selection.persona,
|
|
755
790
|
scenario: selection.scenario,
|
|
@@ -890,6 +925,7 @@ async function runLocalCodexTui(options) {
|
|
|
890
925
|
createdAt,
|
|
891
926
|
cwd: options.cwd,
|
|
892
927
|
artifactRoot,
|
|
928
|
+
...(options.lab === undefined ? {} : { lab: options.lab }),
|
|
893
929
|
source,
|
|
894
930
|
persona: selection.persona,
|
|
895
931
|
scenario: selection.scenario,
|
|
@@ -977,7 +1013,7 @@ async function runLocalCodexTui(options) {
|
|
|
977
1013
|
review: createLocalActorReviewSummary("Codex TUI", status, verdictReason),
|
|
978
1014
|
feedbackCandidates: []
|
|
979
1015
|
};
|
|
980
|
-
await writeRunBundleArtifacts(runPaths, bundle);
|
|
1016
|
+
await writeRunBundleArtifacts(runPaths, bundle, runStatus);
|
|
981
1017
|
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
982
1018
|
schema: "humanish.latest-run.v1",
|
|
983
1019
|
runId,
|
|
@@ -1117,6 +1153,13 @@ async function runLocalCodexExec(options) {
|
|
|
1117
1153
|
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
1118
1154
|
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
1119
1155
|
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
1156
|
+
// Identity + liveness on disk (#455): uniform across every route, so a reader classifies any
|
|
1157
|
+
// run from one small file instead of parsing bundles.
|
|
1158
|
+
const runStatus = beginRunStatus(runPaths, {
|
|
1159
|
+
runId,
|
|
1160
|
+
mode: options.dryRun ? "dry-run" : "live",
|
|
1161
|
+
...(options.lab === undefined ? {} : { lab: options.lab })
|
|
1162
|
+
});
|
|
1120
1163
|
const artifactRoot = runPaths.relativeRunRoot;
|
|
1121
1164
|
if (humanishSource === "missing") {
|
|
1122
1165
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
|
|
@@ -1316,7 +1359,7 @@ async function runLocalCodexExec(options) {
|
|
|
1316
1359
|
events,
|
|
1317
1360
|
review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout", status, verdictReason)
|
|
1318
1361
|
});
|
|
1319
|
-
await writeRunBundleArtifacts(runPaths, bundle);
|
|
1362
|
+
await writeRunBundleArtifacts(runPaths, bundle, runStatus);
|
|
1320
1363
|
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
1321
1364
|
schema: "humanish.latest-run.v1",
|
|
1322
1365
|
runId,
|
|
@@ -1470,6 +1513,13 @@ async function runLocalCodexAppServer(options) {
|
|
|
1470
1513
|
const selection = await loadDryRunSelection(options.projectRoot, humanishSource);
|
|
1471
1514
|
await assertPreparedSelectedOutputDirectory(options.projectRoot);
|
|
1472
1515
|
const runPaths = await prepareRunArtifactPaths(options.cwd, runId);
|
|
1516
|
+
// Identity + liveness on disk (#455): uniform across every route, so a reader classifies any
|
|
1517
|
+
// run from one small file instead of parsing bundles.
|
|
1518
|
+
const runStatus = beginRunStatus(runPaths, {
|
|
1519
|
+
runId,
|
|
1520
|
+
mode: options.dryRun ? "dry-run" : "live",
|
|
1521
|
+
...(options.lab === undefined ? {} : { lab: options.lab })
|
|
1522
|
+
});
|
|
1473
1523
|
const artifactRoot = runPaths.relativeRunRoot;
|
|
1474
1524
|
if (humanishSource === "missing") {
|
|
1475
1525
|
warnings.push("Committed humanish/ source was not found; using built-in synthetic Codex app-server actor defaults.");
|
|
@@ -1647,7 +1697,7 @@ async function runLocalCodexAppServer(options) {
|
|
|
1647
1697
|
events,
|
|
1648
1698
|
review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex app-server" : "Codex app-server fanout", status, verdictReason)
|
|
1649
1699
|
});
|
|
1650
|
-
await writeRunBundleArtifacts(runPaths, bundle);
|
|
1700
|
+
await writeRunBundleArtifacts(runPaths, bundle, runStatus);
|
|
1651
1701
|
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
1652
1702
|
schema: "humanish.latest-run.v1",
|
|
1653
1703
|
runId,
|
|
@@ -2981,6 +3031,23 @@ export async function doctor(cwdInput) {
|
|
|
2981
3031
|
: "optional peer @e2b/desktop is NOT installed — dry runs work, but any live desktop lane will fail closed. Install it with `npm i -D @e2b/desktop`."
|
|
2982
3032
|
};
|
|
2983
3033
|
})(),
|
|
3034
|
+
// The stakeholder surface (#455). Reported as capability, never as a gate: the TUI is optional,
|
|
3035
|
+
// and `doctor` is itself mostly run by agents through a pipe, where a TTY requirement says
|
|
3036
|
+
// nothing about whether the PROJECT is ready. So this row is always ok and its job is to tell a
|
|
3037
|
+
// human the surface exists and whether this machine can host it.
|
|
3038
|
+
(() => {
|
|
3039
|
+
const supported = nodeSupportsTui();
|
|
3040
|
+
const bundlePresent = existsSync(tuiBundleUrl(import.meta.url));
|
|
3041
|
+
return {
|
|
3042
|
+
name: "terminal surface",
|
|
3043
|
+
ok: true,
|
|
3044
|
+
message: !supported
|
|
3045
|
+
? `\`humanish tui\` needs Node ${TUI_MIN_NODE_MAJOR}+ (this is ${process.version}); every other command works here`
|
|
3046
|
+
: bundlePresent
|
|
3047
|
+
? "`humanish tui` is available in an interactive terminal"
|
|
3048
|
+
: "`humanish tui` bundle is not built in this checkout — run `pnpm build` (installed packages ship it prebuilt)"
|
|
3049
|
+
};
|
|
3050
|
+
})(),
|
|
2984
3051
|
// Provider-key discovery (#436): which source supplies each live-run key, through the same
|
|
2985
3052
|
// chain a live command resolves (env/--env-file, project overlay, vendor stores, the
|
|
2986
3053
|
// humanish user store). Values never appear; sources and fill commands do.
|
|
@@ -3398,12 +3465,30 @@ async function readTextIfExists(filePath) {
|
|
|
3398
3465
|
throw error;
|
|
3399
3466
|
}
|
|
3400
3467
|
}
|
|
3401
|
-
async function writeRunBundleArtifacts(runPaths, bundle
|
|
3468
|
+
async function writeRunBundleArtifacts(runPaths, bundle,
|
|
3469
|
+
/** Pass ONLY when this write is the run's final one: the shared writer is also used for
|
|
3470
|
+
* mid-run in-progress snapshots, and finalizing there would declare a live run finished (#455). */
|
|
3471
|
+
finalizeStatus) {
|
|
3402
3472
|
const publicBundle = {
|
|
3403
3473
|
...bundle,
|
|
3404
3474
|
cwd: PUBLIC_TARGET_CWD
|
|
3405
3475
|
};
|
|
3406
3476
|
await writeContainedOutputFile(runPaths, "run.json", `${JSON.stringify(publicBundle, null, 2)}\n`, "utf8");
|
|
3477
|
+
await finalizeStatus?.finish({
|
|
3478
|
+
...(publicBundle.review?.verdict === undefined ? {} : { verdict: publicBundle.review.verdict }),
|
|
3479
|
+
...(publicBundle.review?.participants === undefined
|
|
3480
|
+
? {}
|
|
3481
|
+
: {
|
|
3482
|
+
participants: {
|
|
3483
|
+
total: publicBundle.review.participants.total,
|
|
3484
|
+
reachedGoal: publicBundle.review.participants.reachedGoal,
|
|
3485
|
+
...(publicBundle.review.participants.reportedFriction === undefined
|
|
3486
|
+
? {}
|
|
3487
|
+
: { reportedFriction: publicBundle.review.participants.reportedFriction })
|
|
3488
|
+
}
|
|
3489
|
+
}),
|
|
3490
|
+
...(publicBundle.cost?.estimatedTotalUsd === undefined ? {} : { estimatedCostUsd: publicBundle.cost.estimatedTotalUsd })
|
|
3491
|
+
});
|
|
3407
3492
|
await writeContainedOutputFile(runPaths, "review.json", `${JSON.stringify(publicBundle.review, null, 2)}\n`, "utf8");
|
|
3408
3493
|
await writeContainedOutputFile(runPaths, "review.md", renderReviewMarkdown(publicBundle), "utf8");
|
|
3409
3494
|
await writeContainedOutputFile(runPaths, "events.ndjson", `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|