harnery 0.37.0 → 0.38.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/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -2
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +48 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +349 -21
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +7 -3
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +268 -13
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +57 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +265 -10
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -0
- package/dist/core/diagnostics/bundle.d.ts +16 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +101 -7
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +9 -2
- package/dist/core/events/v3/producers/recorder.d.ts +23 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +264 -18
- package/dist/core/hooks/cli.js +120 -27
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +6 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/sampler.d.ts +7 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +106 -5
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +41 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +167 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +4 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +136 -1
- package/dist/lib/browser/qa-run.d.ts +100 -10
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +768 -169
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +5 -2
- package/package.json +8 -2
- package/src/commander.ts +50 -2
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +87 -1
- package/src/commands/artifacts.ts +97 -21
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +481 -20
- package/src/commands/fetch.ts +1 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +335 -16
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +370 -21
- package/src/core/config.ts +23 -0
- package/src/core/diagnostics/bundle.ts +119 -11
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/producers/intake.ts +9 -2
- package/src/core/events/v3/producers/recorder.ts +312 -18
- package/src/core/hooks/cli.ts +140 -32
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +7 -0
- package/src/core/resources/sampler.ts +138 -6
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +206 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +36 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +366 -3
- package/src/lib/browser/qa-run.ts +868 -190
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/templates.ts +5 -2
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Durable job records: a small on-disk representation of "a job that outlives
|
|
2
|
+
// the client that launched it". A supervisor process owns the record; a client
|
|
3
|
+
// only reads it. A bridge drop, a closed terminal, or a killed client
|
|
4
|
+
// therefore interrupts the *view* of the job, never the job itself.
|
|
5
|
+
//
|
|
6
|
+
// Layout, under a caller-supplied base directory:
|
|
7
|
+
//
|
|
8
|
+
// <base>/jobs/<job_id>/job.json immutable description (argv, cwd, resource)
|
|
9
|
+
// <base>/jobs/<job_id>/status.json mutable state, rewritten atomically
|
|
10
|
+
// <base>/jobs/<job_id>/job.log merged child stdout + stderr
|
|
11
|
+
//
|
|
12
|
+
// Classification is fail-closed, mirroring qa-status: liveness is proven by the
|
|
13
|
+
// PID, never assumed from the status document. A non-terminal state whose PID
|
|
14
|
+
// is dead classifies as "dead", never as "running".
|
|
15
|
+
//
|
|
16
|
+
// Toolkit tier: this module must not import src/core (layering check).
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
19
|
+
import { join, resolve } from "node:path";
|
|
20
|
+
export const DURABLE_JOB_SCHEMA_VERSION = 1;
|
|
21
|
+
/** How often the supervisor rewrites status.json while a job runs. */
|
|
22
|
+
export const DURABLE_JOB_HEARTBEAT_MS = 15_000;
|
|
23
|
+
/** A running job whose heartbeat is older than this earns a staleness warning,
|
|
24
|
+
* but stays "running" while its PID is alive. */
|
|
25
|
+
export const DURABLE_JOB_HEARTBEAT_STALE_MS = 120_000;
|
|
26
|
+
export const DURABLE_JOB_DOCUMENT_FILENAME = "job.json";
|
|
27
|
+
export const DURABLE_JOB_STATUS_FILENAME = "status.json";
|
|
28
|
+
export const DURABLE_JOB_LOG_FILENAME = "job.log";
|
|
29
|
+
const JOBS_DIRNAME = "jobs";
|
|
30
|
+
const WRITTEN_STATES = new Set(["launching", "queued", "running", "completed"]);
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
/** Default PID liveness probe. EPERM counts as alive; only ESRCH is dead. */
|
|
35
|
+
function defaultPidAlive(pid) {
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pid, 0);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return err.code !== "ESRCH";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Directory holding every job record under a base directory. */
|
|
45
|
+
export function jobsRoot(base) {
|
|
46
|
+
return join(resolve(base), JOBS_DIRNAME);
|
|
47
|
+
}
|
|
48
|
+
/** Directory of one job record. Does not create it. */
|
|
49
|
+
export function jobDirFor(base, jobId) {
|
|
50
|
+
return join(jobsRoot(base), jobId);
|
|
51
|
+
}
|
|
52
|
+
/** Create (idempotently) the directory for one job and return its path. */
|
|
53
|
+
export function createJobDir(base, jobId) {
|
|
54
|
+
const dir = jobDirFor(base, jobId);
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
export function jobLogPath(jobDir) {
|
|
59
|
+
return join(resolve(jobDir), DURABLE_JOB_LOG_FILENAME);
|
|
60
|
+
}
|
|
61
|
+
function writeAtomic(path, body) {
|
|
62
|
+
const tmp = `${path}.${randomUUID().slice(0, 8)}.tmp`;
|
|
63
|
+
writeFileSync(tmp, body);
|
|
64
|
+
renameSync(tmp, path);
|
|
65
|
+
}
|
|
66
|
+
/** Write the immutable job description. */
|
|
67
|
+
export function writeJobDocument(jobDir, document) {
|
|
68
|
+
writeAtomic(join(resolve(jobDir), DURABLE_JOB_DOCUMENT_FILENAME), `${JSON.stringify(document, null, 2)}\n`);
|
|
69
|
+
}
|
|
70
|
+
/** Replace the status document atomically, so a reader never sees a torn file. */
|
|
71
|
+
export function writeJobStatus(jobDir, status) {
|
|
72
|
+
writeAtomic(join(resolve(jobDir), DURABLE_JOB_STATUS_FILENAME), `${JSON.stringify(status, null, 2)}\n`);
|
|
73
|
+
}
|
|
74
|
+
function readJson(path) {
|
|
75
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
76
|
+
}
|
|
77
|
+
/** Read the job description, or null when it is missing or unusable. */
|
|
78
|
+
export function readJobDocument(jobDir) {
|
|
79
|
+
const path = join(resolve(jobDir), DURABLE_JOB_DOCUMENT_FILENAME);
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = readJson(path);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!isRecord(parsed))
|
|
88
|
+
return null;
|
|
89
|
+
if (typeof parsed.job_id !== "string")
|
|
90
|
+
return null;
|
|
91
|
+
return {
|
|
92
|
+
schema_version: typeof parsed.schema_version === "number"
|
|
93
|
+
? parsed.schema_version
|
|
94
|
+
: DURABLE_JOB_SCHEMA_VERSION,
|
|
95
|
+
job_id: parsed.job_id,
|
|
96
|
+
resource: typeof parsed.resource === "string" ? parsed.resource : "",
|
|
97
|
+
capacity: typeof parsed.capacity === "number" ? parsed.capacity : 1,
|
|
98
|
+
label: typeof parsed.label === "string" ? parsed.label : "",
|
|
99
|
+
argv: Array.isArray(parsed.argv)
|
|
100
|
+
? parsed.argv.filter((a) => typeof a === "string")
|
|
101
|
+
: [],
|
|
102
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : "",
|
|
103
|
+
created_at: typeof parsed.created_at === "string" ? parsed.created_at : "",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Read the status document, or null when it is missing or unusable. */
|
|
107
|
+
export function readJobStatus(jobDir) {
|
|
108
|
+
const path = join(resolve(jobDir), DURABLE_JOB_STATUS_FILENAME);
|
|
109
|
+
let parsed;
|
|
110
|
+
try {
|
|
111
|
+
parsed = readJson(path);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (!isRecord(parsed))
|
|
117
|
+
return null;
|
|
118
|
+
if (typeof parsed.job_id !== "string")
|
|
119
|
+
return null;
|
|
120
|
+
if (typeof parsed.state !== "string" || !WRITTEN_STATES.has(parsed.state))
|
|
121
|
+
return null;
|
|
122
|
+
const queue = parsed.queue;
|
|
123
|
+
return {
|
|
124
|
+
schema_version: typeof parsed.schema_version === "number"
|
|
125
|
+
? parsed.schema_version
|
|
126
|
+
: DURABLE_JOB_SCHEMA_VERSION,
|
|
127
|
+
job_id: parsed.job_id,
|
|
128
|
+
pid: typeof parsed.pid === "number" ? parsed.pid : 0,
|
|
129
|
+
state: parsed.state,
|
|
130
|
+
started_at: typeof parsed.started_at === "string" ? parsed.started_at : "",
|
|
131
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : "",
|
|
132
|
+
...(isRecord(queue) &&
|
|
133
|
+
typeof queue.resource === "string" &&
|
|
134
|
+
typeof queue.waiting_since === "string"
|
|
135
|
+
? { queue: { resource: queue.resource, waiting_since: queue.waiting_since } }
|
|
136
|
+
: {}),
|
|
137
|
+
...(typeof parsed.exit_code === "number" || parsed.exit_code === null
|
|
138
|
+
? { exit_code: parsed.exit_code }
|
|
139
|
+
: {}),
|
|
140
|
+
...(typeof parsed.signal === "string" || parsed.signal === null
|
|
141
|
+
? { signal: parsed.signal }
|
|
142
|
+
: {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Classify one job directory, fail-closed. "completed" is terminal and carries
|
|
147
|
+
* the wrapped command's exit code. Any other written state is trusted only
|
|
148
|
+
* while the supervisor PID is alive; otherwise the job is "dead" — the record
|
|
149
|
+
* outlived the process that owned it.
|
|
150
|
+
*/
|
|
151
|
+
export function classifyJob(jobDir, deps = {}) {
|
|
152
|
+
const isAlive = deps.pidAlive ?? defaultPidAlive;
|
|
153
|
+
const nowMs = deps.now ? deps.now() : Date.now();
|
|
154
|
+
const abs = resolve(jobDir);
|
|
155
|
+
if (!existsSync(abs))
|
|
156
|
+
return { ok: false, error: `no such job directory: ${abs}` };
|
|
157
|
+
const statusPath = join(abs, DURABLE_JOB_STATUS_FILENAME);
|
|
158
|
+
if (!existsSync(statusPath)) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
error: `${abs} carries no ${DURABLE_JOB_STATUS_FILENAME}: not a job directory`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const status = readJobStatus(abs);
|
|
165
|
+
if (!status) {
|
|
166
|
+
return { ok: false, error: `${statusPath} is not a readable job status document` };
|
|
167
|
+
}
|
|
168
|
+
const document = readJobDocument(abs);
|
|
169
|
+
let heartbeatAgeMs = null;
|
|
170
|
+
const updatedMs = Date.parse(status.updated_at);
|
|
171
|
+
if (!Number.isNaN(updatedMs))
|
|
172
|
+
heartbeatAgeMs = Math.max(0, nowMs - updatedMs);
|
|
173
|
+
const base = {
|
|
174
|
+
job_id: status.job_id,
|
|
175
|
+
pid: status.pid > 0 ? status.pid : null,
|
|
176
|
+
resource: document?.resource ?? null,
|
|
177
|
+
label: document?.label ?? null,
|
|
178
|
+
argv: document?.argv ?? null,
|
|
179
|
+
created_at: document?.created_at ?? null,
|
|
180
|
+
started_at: status.started_at || null,
|
|
181
|
+
updated_at: status.updated_at || null,
|
|
182
|
+
heartbeat_age_ms: heartbeatAgeMs,
|
|
183
|
+
job_dir: abs,
|
|
184
|
+
log_path: jobLogPath(abs),
|
|
185
|
+
};
|
|
186
|
+
if (status.state === "completed") {
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
report: {
|
|
190
|
+
...base,
|
|
191
|
+
state: "completed",
|
|
192
|
+
terminal: true,
|
|
193
|
+
exit_code: status.exit_code ?? null,
|
|
194
|
+
signal: status.signal ?? null,
|
|
195
|
+
warnings: [],
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (!(status.pid > 0 && isAlive(status.pid))) {
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
report: {
|
|
203
|
+
...base,
|
|
204
|
+
state: "dead",
|
|
205
|
+
terminal: false,
|
|
206
|
+
exit_code: null,
|
|
207
|
+
signal: null,
|
|
208
|
+
...(status.queue ? { queue: status.queue } : {}),
|
|
209
|
+
warnings: [],
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const warnings = [];
|
|
214
|
+
if (status.state === "running" &&
|
|
215
|
+
heartbeatAgeMs !== null &&
|
|
216
|
+
heartbeatAgeMs > DURABLE_JOB_HEARTBEAT_STALE_MS) {
|
|
217
|
+
warnings.push(`heartbeat stale: last update ${Math.round(heartbeatAgeMs / 1000)}s ago ` +
|
|
218
|
+
`(threshold ${DURABLE_JOB_HEARTBEAT_STALE_MS / 1000}s), but pid ${status.pid} is alive`);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
report: {
|
|
223
|
+
...base,
|
|
224
|
+
state: status.state,
|
|
225
|
+
terminal: false,
|
|
226
|
+
exit_code: null,
|
|
227
|
+
signal: null,
|
|
228
|
+
...(status.queue ? { queue: status.queue } : {}),
|
|
229
|
+
warnings,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Every job record under a base directory, newest first. Ordering keys on the
|
|
235
|
+
* description's created_at, falling back to the status document's started_at,
|
|
236
|
+
* then to the directory name, so a record missing its description still lists.
|
|
237
|
+
*/
|
|
238
|
+
export function listJobs(base, deps = {}) {
|
|
239
|
+
const root = jobsRoot(base);
|
|
240
|
+
let names;
|
|
241
|
+
try {
|
|
242
|
+
names = readdirSync(root, { withFileTypes: true })
|
|
243
|
+
.filter((entry) => entry.isDirectory())
|
|
244
|
+
.map((entry) => entry.name);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const entries = [];
|
|
250
|
+
for (const name of names) {
|
|
251
|
+
const dir = join(root, name);
|
|
252
|
+
const document = readJobDocument(dir);
|
|
253
|
+
const classified = classifyJob(dir, deps);
|
|
254
|
+
const createdAt = document?.created_at || (classified.ok ? classified.report.started_at : null) || null;
|
|
255
|
+
const parsed = createdAt !== null ? Date.parse(createdAt) : Number.NaN;
|
|
256
|
+
entries.push({
|
|
257
|
+
entry: {
|
|
258
|
+
job_id: document?.job_id ?? name,
|
|
259
|
+
job_dir: dir,
|
|
260
|
+
created_at: createdAt,
|
|
261
|
+
document,
|
|
262
|
+
report: classified.ok ? classified.report : null,
|
|
263
|
+
...(classified.ok ? {} : { error: classified.error }),
|
|
264
|
+
},
|
|
265
|
+
sortKey: Number.isNaN(parsed) ? 0 : parsed,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
entries.sort((a, b) => {
|
|
269
|
+
if (b.sortKey !== a.sortKey)
|
|
270
|
+
return b.sortKey - a.sortKey;
|
|
271
|
+
return b.entry.job_id.localeCompare(a.entry.job_id);
|
|
272
|
+
});
|
|
273
|
+
return entries.map((item) => item.entry);
|
|
274
|
+
}
|
|
275
|
+
/** Describe a job's outcome as a process exit code: the wrapped command's own
|
|
276
|
+
* code, 1 when it died on a signal, 4 when the record is dead. */
|
|
277
|
+
export function jobExitCode(report) {
|
|
278
|
+
if (report.state === "dead")
|
|
279
|
+
return 4;
|
|
280
|
+
if (!report.terminal)
|
|
281
|
+
return 5;
|
|
282
|
+
if (report.signal !== null)
|
|
283
|
+
return 1;
|
|
284
|
+
return report.exit_code ?? 1;
|
|
285
|
+
}
|
|
286
|
+
/** Format a duration the way the status commands do. */
|
|
287
|
+
export function formatJobAge(ms) {
|
|
288
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
289
|
+
if (totalSeconds < 60)
|
|
290
|
+
return `${totalSeconds}s`;
|
|
291
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
292
|
+
if (minutes < 60)
|
|
293
|
+
return `${minutes}m ${totalSeconds % 60}s`;
|
|
294
|
+
const hours = Math.floor(minutes / 60);
|
|
295
|
+
return `${hours}h ${minutes % 60}m`;
|
|
296
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CookieJar, type ExtraCookies } from "../cookies/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Simple cookie-aware HTTP client.
|
|
4
4
|
*
|
|
@@ -36,6 +36,12 @@ export interface FetchOptions<TResponseType extends FetchResponseType = "text">
|
|
|
36
36
|
* only land when the key isn't already set.
|
|
37
37
|
*/
|
|
38
38
|
extraHeaders?: (url: string) => Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* Optional callback that returns extra cookies to persist into `jar`
|
|
41
|
+
* before the Cookie header is built. Ignored when no jar is passed
|
|
42
|
+
* (`--no-cookies`). Must stay synchronous.
|
|
43
|
+
*/
|
|
44
|
+
extraCookies?: ExtraCookies;
|
|
39
45
|
/** Response body representation. Default `text`; use `bytes` for lossless file output. */
|
|
40
46
|
responseType?: TResponseType;
|
|
41
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM;IAC5E,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED,KAAK,SAAS,CAAC,aAAa,SAAS,iBAAiB,IAAI,aAAa,SAAS,OAAO,GACnF,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM,EACjF,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAY,CAAC,aAAa,CAAM,GACrC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CA+EhD"}
|
package/dist/lib/http/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyExtraCookies, } from "../cookies/index.js";
|
|
1
2
|
/**
|
|
2
3
|
* Fetch a URL with optional cookie-jar attach + persist.
|
|
3
4
|
*
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
export async function fetchWithJar(url, opts = {}) {
|
|
10
11
|
const headers = { ...(opts.headers ?? {}) };
|
|
11
12
|
if (opts.jar) {
|
|
13
|
+
applyExtraCookies(url, opts.jar, opts.extraCookies);
|
|
12
14
|
const cookieHeader = opts.jar.header(url);
|
|
13
15
|
if (cookieHeader && !headers.Cookie && !headers.cookie) {
|
|
14
16
|
headers.Cookie = cookieHeader;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,sEAAsE;AACtE,eAAO,MAAM,aAAa,WAAW,CAAC;AACtC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,GAAG,EAAE,OAAO,CAAC;IACb,4CAA4C;IAC5C,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,WAAoE,GAC3E,MAAM,CAgHR;AAID,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,sEAAsE;AACtE,eAAO,MAAM,aAAa,WAAW,CAAC;AACtC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,GAAG,EAAE,OAAO,CAAC;IACb,4CAA4C;IAC5C,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,WAAoE,GAC3E,MAAM,CAgHR;AAID,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAsVD,eAAO,MAAM,MAAM,EAAE,aAAa,EAiDjC,CAAC"}
|
|
@@ -248,10 +248,13 @@ auto-route into contribute.
|
|
|
248
248
|
## Create (\`create <objective>\`)
|
|
249
249
|
|
|
250
250
|
The web UI is the member + steward picker; don't create from the CLI (that skips
|
|
251
|
-
the steward choice).
|
|
251
|
+
the steward choice). Read the dashboard base URL from the active Harnery web
|
|
252
|
+
status or start output, which resolves the project's configured \`web.port\`.
|
|
253
|
+
Append the URL-encoded objective to this path; never assume a port or reuse a
|
|
254
|
+
dashboard URL from another project:
|
|
252
255
|
|
|
253
256
|
\`\`\`
|
|
254
|
-
|
|
257
|
+
<configured-dashboard-base-url>/councils/new?objective=<encoded>
|
|
255
258
|
\`\`\`
|
|
256
259
|
|
|
257
260
|
If the dev server isn't up, start it with \`${b} web up\`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harnery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryan Kelly",
|
|
@@ -152,6 +152,11 @@
|
|
|
152
152
|
"types": "./dist/core/policy/index.d.ts",
|
|
153
153
|
"import": "./dist/core/policy/index.js"
|
|
154
154
|
},
|
|
155
|
+
"./lib/admission": {
|
|
156
|
+
"bun": "./src/lib/admission.ts",
|
|
157
|
+
"types": "./dist/lib/admission.d.ts",
|
|
158
|
+
"import": "./dist/lib/admission.js"
|
|
159
|
+
},
|
|
155
160
|
"./lib/agent-browser": {
|
|
156
161
|
"bun": "./src/lib/agent-browser/index.ts",
|
|
157
162
|
"types": "./dist/lib/agent-browser/index.d.ts",
|
|
@@ -230,7 +235,7 @@
|
|
|
230
235
|
"access": "public",
|
|
231
236
|
"provenance": true
|
|
232
237
|
},
|
|
233
|
-
"packageManager": "bun@1.
|
|
238
|
+
"packageManager": "bun@1.4.0",
|
|
234
239
|
"scripts": {
|
|
235
240
|
"build": "npm run clean && tsc -p tsconfig.build.json && node scripts/rewrite-dts-extensions.mjs",
|
|
236
241
|
"clean": "rm -rf dist",
|
|
@@ -244,6 +249,7 @@
|
|
|
244
249
|
"lint:fix": "bun x @biomejs/biome check --write src",
|
|
245
250
|
"format": "bun x @biomejs/biome format --write src",
|
|
246
251
|
"test": "bun run scripts/test.mjs",
|
|
252
|
+
"test:list": "bun run scripts/test.mjs --list",
|
|
247
253
|
"test:web": "cd web && bun install && bun test",
|
|
248
254
|
"test:integration": "bash tests/integration/run.sh",
|
|
249
255
|
"smoke": "node scripts/smoke-test.mjs",
|
package/src/commander.ts
CHANGED
|
@@ -121,6 +121,15 @@ export interface HarneryProgramContext {
|
|
|
121
121
|
* zones behind a WAF). harn standalone skips the callback entirely.
|
|
122
122
|
*/
|
|
123
123
|
extraHeaders?: (url: string) => Record<string, string>;
|
|
124
|
+
/**
|
|
125
|
+
* Optional callback that returns extra cookies to persist into the shared
|
|
126
|
+
* jar before `fetch` / `browse` attach it. Runs only when a jar is in
|
|
127
|
+
* play (`--no-cookies` skips it). Use this for host-minted session
|
|
128
|
+
* cookies; `extraHeaders` cannot merge a `Cookie` header once the jar
|
|
129
|
+
* has already set one. Must stay synchronous, same as `extraHeaders`.
|
|
130
|
+
* harn standalone skips the callback entirely.
|
|
131
|
+
*/
|
|
132
|
+
extraCookies?: import("./lib/cookies/index.ts").ExtraCookies;
|
|
124
133
|
/**
|
|
125
134
|
* Host-injected vision-model call for `browse --check-critique`. harnery
|
|
126
135
|
* ships no model client or API key; a consumer wires this to its own
|
|
@@ -443,9 +452,48 @@ function harneryCommandBundles({
|
|
|
443
452
|
lazy(
|
|
444
453
|
"qa-run <target>",
|
|
445
454
|
`Run the whole page-QA matrix in one command (planner, deterministic gates per context, interactions, critique, snapshot) via child \`${binName} browse\` processes; writes page-qa-result.json with a fail-closed verdict.`,
|
|
446
|
-
async (program) =>
|
|
455
|
+
async (program) =>
|
|
456
|
+
(await import("./commands/qa-run.ts")).registerQaRunCommand(program, emit, context),
|
|
457
|
+
{ hasOptions: true },
|
|
458
|
+
),
|
|
459
|
+
lazy(
|
|
460
|
+
"review-pack",
|
|
461
|
+
"Page review packs: capture a page's rendering contexts (tiles, DOM, signature) into a directory an agent can review without a browser, and judge a pack from disk through one bounded pool of vision calls.",
|
|
462
|
+
async (program) =>
|
|
463
|
+
(await import("./commands/review-pack.ts")).registerReviewPackCommand(
|
|
464
|
+
program,
|
|
465
|
+
emit,
|
|
466
|
+
context,
|
|
467
|
+
),
|
|
468
|
+
{ hasOptions: true },
|
|
469
|
+
),
|
|
470
|
+
lazy(
|
|
471
|
+
"qa-verify <path>",
|
|
472
|
+
"Verify a qa-run result is fresh evidence for a specific invocation: match its run identity (run_id, job digest, tested revision, age, recorded output directory) against expectations. Fail-closed on missing identity.",
|
|
473
|
+
async (program) =>
|
|
474
|
+
(await import("./commands/qa-verify.ts")).registerQaVerifyCommand(program, emit),
|
|
447
475
|
{ hasOptions: true },
|
|
448
476
|
),
|
|
477
|
+
lazy(
|
|
478
|
+
"qa-record <target>",
|
|
479
|
+
"Record hand-performed page QA as a standard result document: same schema, run identity, stage timings, and freshness guarantees as qa-run, marked evidence_source manual so it can report a defect but never a pass.",
|
|
480
|
+
async (program) =>
|
|
481
|
+
(await import("./commands/qa-record.ts")).registerQaRecordCommand(program, emit, context),
|
|
482
|
+
{ hasOptions: true },
|
|
483
|
+
),
|
|
484
|
+
lazy(
|
|
485
|
+
"qa-status [path]",
|
|
486
|
+
"Report a qa-run's live state (queued/launching/running/completed/dead) from its run-status.json, PID liveness, and result document; --wait reconnects to a detached run and exits with its verdict; --queue shows the machine-wide browser-qa admission queue.",
|
|
487
|
+
async (program) =>
|
|
488
|
+
(await import("./commands/qa-status.ts")).registerQaStatusCommand(program, emit, context),
|
|
489
|
+
{ hasOptions: true },
|
|
490
|
+
),
|
|
491
|
+
lazy(
|
|
492
|
+
"admission",
|
|
493
|
+
"Machine-wide admission control for heavy jobs: `admission run --resource <name> -- <command…>` waits for a FIFO slot then runs the command; `admission status` lists holders and waiters per resource.",
|
|
494
|
+
async (program) =>
|
|
495
|
+
(await import("./commands/admission.ts")).registerAdmissionCommand(program, emit),
|
|
496
|
+
),
|
|
449
497
|
lazy(
|
|
450
498
|
"browse-session",
|
|
451
499
|
"Control an opted-in headed browse session through an owner-only descriptor",
|
|
@@ -456,7 +504,7 @@ function harneryCommandBundles({
|
|
|
456
504
|
"browse-ai <url>",
|
|
457
505
|
`agent-browser (Vercel Labs) wrapper: accessibility-tree snapshots with element refs (@e1, @e2) for LLM consumption. Daemon-mode: successive calls reuse the same browser instance. Sister to ${binName} browse (Playwright).`,
|
|
458
506
|
async (program) =>
|
|
459
|
-
(await import("./commands/browse-ai.ts")).registerBrowseAiCommand(program, emit),
|
|
507
|
+
(await import("./commands/browse-ai.ts")).registerBrowseAiCommand(program, emit, context),
|
|
460
508
|
{ hasOptions: true },
|
|
461
509
|
),
|
|
462
510
|
{
|