@mikenguyen69/harness 0.1.0-beta.2 → 0.1.0-beta.3
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/cli.js +31 -9
- package/dist/help.js +80 -1
- package/dist/internal/orchestration/analyze/index.js +4 -1
- package/dist/internal/orchestration/harness/cli.js +11 -1
- package/dist/internal/orchestration/loop/index.js +114 -5
- package/dist/internal/orchestration/prompts/index.js +23 -7
- package/dist/internal/orchestration/runners/telemetry.js +2 -1
- package/dist/internal/system/agentops/scope.js +25 -2
- package/dist/internal/system/cli.js +13 -0
- package/dist/internal/system/doors/cli.js +158 -20
- package/dist/internal/system/doors/index.js +70 -15
- package/dist/internal/system/provenance/cli.js +70 -0
- package/dist/internal/system/provenance/index.js +295 -3
- package/dist/internal/system/runner/index.js +16 -5
- package/dist/internal/system/sequencer/cli.js +21 -2
- package/dist/internal/system/sequencer/index.js +46 -1
- package/dist/internal/system/status/cli.js +41 -0
- package/dist/internal/system/status/index.js +212 -0
- package/dist/internal/system/units/index.js +1 -0
- package/dist/internal/system/verify/cli.js +28 -1
- package/dist/internal/system/verify/index.js +42 -5
- package/dist/routes.js +1 -3
- package/package.json +1 -1
- package/schema/attestation.schema.json +134 -0
- package/schema/envelope.schema.json +12 -1
- package/schema/ledger-event.schema.json +1 -0
package/dist/cli.js
CHANGED
|
@@ -8,19 +8,45 @@
|
|
|
8
8
|
* hook-install policy lives here.
|
|
9
9
|
*/
|
|
10
10
|
import { buildDispatchRequest, dispatch } from "./dispatch.js";
|
|
11
|
-
import { USAGE } from "./help.js";
|
|
11
|
+
import { COMMAND_HELP, SESSION_HELP, SESSION_PHASES, USAGE, wantsHelp, } from "./help.js";
|
|
12
12
|
import { resolveInternalClis } from "./paths.js";
|
|
13
13
|
import { buildInternalArgv, resolveRoute } from "./routes.js";
|
|
14
14
|
import { formatVersion } from "./version.js";
|
|
15
15
|
export async function main(argv) {
|
|
16
|
-
if (argv.includes("--help") || argv.includes("-h")) {
|
|
17
|
-
process.stdout.write(USAGE);
|
|
18
|
-
return 0;
|
|
19
|
-
}
|
|
20
16
|
if (argv.includes("--version") || argv.includes("-V")) {
|
|
21
17
|
process.stdout.write(formatVersion());
|
|
22
18
|
return 0;
|
|
23
19
|
}
|
|
20
|
+
const first = argv[0];
|
|
21
|
+
const helpRequested = wantsHelp(argv);
|
|
22
|
+
// Bare `harness`, `harness --help`, `harness -h` — the operator-journey text.
|
|
23
|
+
// A help flag that is not the leading token belongs to a specific command.
|
|
24
|
+
if (first === undefined || first === "--help" || first === "-h") {
|
|
25
|
+
process.stdout.write(USAGE);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
// `harness session [<phase>]` — enumerate the phases, and classify an
|
|
29
|
+
// unrecognised one as an unknown *phase*, not an unknown command. Runs before
|
|
30
|
+
// the generic `<noun> --help` path so a bare `harness session` prints them too.
|
|
31
|
+
if (first === "session") {
|
|
32
|
+
const phase = argv[1];
|
|
33
|
+
if (phase === undefined || phase === "--help" || phase === "-h") {
|
|
34
|
+
process.stdout.write(SESSION_HELP + "\n");
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (!SESSION_PHASES.includes(phase)) {
|
|
38
|
+
process.stderr.write(`harness session ${phase}: unknown phase — expected one of ${SESSION_PHASES.join(", ")}\n`);
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// `harness <noun> --help` / `harness <noun> -h` — that command's own usage.
|
|
43
|
+
if (helpRequested && (argv[1] === "--help" || argv[1] === "-h")) {
|
|
44
|
+
const synopsis = COMMAND_HELP[first];
|
|
45
|
+
if (synopsis !== undefined) {
|
|
46
|
+
process.stdout.write(synopsis + "\n");
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
24
50
|
const route = resolveRoute(argv);
|
|
25
51
|
if (route === null) {
|
|
26
52
|
process.stderr.write(argv.length === 0
|
|
@@ -28,10 +54,6 @@ export async function main(argv) {
|
|
|
28
54
|
: `harness ${argv.join(" ")}: unknown command\nRun harness --help\n`);
|
|
29
55
|
return argv.length === 0 ? 0 : 2;
|
|
30
56
|
}
|
|
31
|
-
if (route === "status-skeleton") {
|
|
32
|
-
process.stderr.write("harness status: not implemented (skeleton)\n");
|
|
33
|
-
return 1;
|
|
34
|
-
}
|
|
35
57
|
const clis = resolveInternalClis();
|
|
36
58
|
const internalArgv = buildInternalArgv(route, argv);
|
|
37
59
|
const request = buildDispatchRequest(route.product, internalArgv, {
|
package/dist/help.js
CHANGED
|
@@ -5,7 +5,7 @@ Operator journey:
|
|
|
5
5
|
harness doctor read-only preflight before a run
|
|
6
6
|
harness run drive the spec DAG with an agent fleet
|
|
7
7
|
harness resume continue after a signed door approval
|
|
8
|
-
harness status
|
|
8
|
+
harness status read-only unit lanes from the ledger (+ run overlay)
|
|
9
9
|
harness report run render the end-of-run HTML report
|
|
10
10
|
harness report calibration aggregate verification envelopes (calibration input)
|
|
11
11
|
|
|
@@ -41,7 +41,86 @@ Integration (machine-facing, same binary):
|
|
|
41
41
|
|
|
42
42
|
Usage:
|
|
43
43
|
harness --help this text
|
|
44
|
+
harness <command> --help usage for one command
|
|
44
45
|
harness --version facade version and internal build identity
|
|
45
46
|
harness <command> [args…] dispatch to the owning internal CLI
|
|
46
47
|
`;
|
|
48
|
+
/**
|
|
49
|
+
* The phases `harness session <phase>` accepts. The facade routes each to
|
|
50
|
+
* `agent session <phase>`; anything else is an unknown phase, not an unknown
|
|
51
|
+
* command.
|
|
52
|
+
*/
|
|
53
|
+
export const SESSION_PHASES = [
|
|
54
|
+
"started",
|
|
55
|
+
"heartbeat",
|
|
56
|
+
"gate_fired",
|
|
57
|
+
"escalated",
|
|
58
|
+
"ended",
|
|
59
|
+
];
|
|
60
|
+
export const SESSION_HELP = `harness session <phase> append session.<phase> evidence (event JSON on stdin)
|
|
61
|
+
|
|
62
|
+
phases: ${SESSION_PHASES.join(", ")}
|
|
63
|
+
|
|
64
|
+
e.g. harness session started < event.json`;
|
|
65
|
+
/**
|
|
66
|
+
* One synopsis per facade command noun, shown for `harness <noun> --help` /
|
|
67
|
+
* `harness <noun> -h` so subcommand help never falls through to the
|
|
68
|
+
* operator-journey text above. Keyed by the leading facade token.
|
|
69
|
+
*/
|
|
70
|
+
export const COMMAND_HELP = {
|
|
71
|
+
init: `harness init scaffold component.toml, hooks, CI workflow, generated adapters
|
|
72
|
+
--audit read-only brownfield inventory (writes nothing)
|
|
73
|
+
--mode ratchet brownfield adoption against a frozen debt baseline`,
|
|
74
|
+
doctor: `harness doctor read-only preflight before a run
|
|
75
|
+
--repo <dir> --routes <routes.toml> [--spool <dir>] [--json]`,
|
|
76
|
+
run: `harness run drive the spec DAG with an agent fleet
|
|
77
|
+
--repo <dir> --config <orchestration.json> --routes <routes.toml> [--spool <dir>] [--report <html>]`,
|
|
78
|
+
resume: `harness resume continue a run after a signed door approval
|
|
79
|
+
--unit <id> [--repo <dir>] [--routes <routes.toml>]`,
|
|
80
|
+
status: `harness status read-only unit lanes from the ledger, with a run-status spool overlay
|
|
81
|
+
[--ledger <dir>] [--spool <dir>] [--json]`,
|
|
82
|
+
report: `harness report <run|calibration>
|
|
83
|
+
report run --state <state.json> end-of-run HTML report (orchestration)
|
|
84
|
+
report calibration aggregate verification envelopes (system)`,
|
|
85
|
+
verify: `harness verify run all bound gates (cheap first, fail fast) and emit the signed envelope`,
|
|
86
|
+
gate: `harness gate --pre --file <path> ring-0 cheap gates before tool use`,
|
|
87
|
+
spec: `harness spec <check|materialize|project>
|
|
88
|
+
check lint specs, dep DAG, consumes/produces [--specs <dir>] [--system <system.toml>]
|
|
89
|
+
materialize write idempotent unit.planned facts --specs <dir> --system <system.toml> [--ledger <dir>] [--json]
|
|
90
|
+
project emit unit YAML from OpenSpec + projection --change <dir> [--openspec <dir>] [--out <dir>] [--json]`,
|
|
91
|
+
next: `harness next list units ready to claim (deps merged, unclaimed or stale)
|
|
92
|
+
[--only <id-prefix>] [--all | --in-review | --merged | --escalated] [--json]`,
|
|
93
|
+
claim: `harness claim <unit-id> --session <id> atomically take a ready unit
|
|
94
|
+
[--specs <dir>] [--system <path>] [--ledger <dir>] [--json] [--resume | --resume-after-door]`,
|
|
95
|
+
heartbeat: `harness heartbeat <unit-id> --session <id> keep a claim alive [--ledger <dir>]`,
|
|
96
|
+
merge: `harness merge <unit-id> <--check | --landed <sha>> authorize or record a landed merge
|
|
97
|
+
[--system <system.toml>] [facts…]`,
|
|
98
|
+
door: `harness door <list|show|approve|reject> open-decision surface
|
|
99
|
+
list [--sweep]
|
|
100
|
+
show <id>
|
|
101
|
+
approve <id> --reason "<who>: <why>" [--who <w>]
|
|
102
|
+
reject <id> --reason <why>`,
|
|
103
|
+
ledger: `harness ledger <append|read|verify-chain> the hash-chained record plane [--dir <path>]
|
|
104
|
+
append full event JSON document on stdin
|
|
105
|
+
read print the chain
|
|
106
|
+
verify-chain check hash linkage`,
|
|
107
|
+
board: `harness board <build|serve> local live dashboard
|
|
108
|
+
build [--ledger <dir>] [--spool <dir>] [--out board.html]
|
|
109
|
+
serve [--root <dir>]… [--port 4317] [--refresh 2000]`,
|
|
110
|
+
attest: `harness attest write/verify provenance attestations for a merge
|
|
111
|
+
write --unit <id> --landed <sha> [--ledger <dir>] [--out <dir>]
|
|
112
|
+
verify --unit <id> [--ledger <dir>] [--dir <dir>]`,
|
|
113
|
+
calibrate: `harness calibrate calibration jobs`,
|
|
114
|
+
digest: `harness digest digest surfaces`,
|
|
115
|
+
route: `harness route print the resolved route as JSON`,
|
|
116
|
+
escalate: `harness escalate raise session.escalated for the current unit`,
|
|
117
|
+
hook: `harness hook pre-tool-use agent PreToolUse handler (payload on stdin)`,
|
|
118
|
+
session: SESSION_HELP,
|
|
119
|
+
};
|
|
120
|
+
/** True when a `-h`/`--help` token appears before any bare `--` separator. */
|
|
121
|
+
export function wantsHelp(argv) {
|
|
122
|
+
const sep = argv.indexOf("--");
|
|
123
|
+
const scan = sep === -1 ? argv : argv.slice(0, sep);
|
|
124
|
+
return scan.includes("--help") || scan.includes("-h");
|
|
125
|
+
}
|
|
47
126
|
//# sourceMappingURL=help.js.map
|
|
@@ -186,7 +186,10 @@ export function findExploreBeforeEdit(unitEvents) {
|
|
|
186
186
|
export function findVerifyHeavierThanImplement(unitEvents) {
|
|
187
187
|
const implementTurns = unitEvents.filter((e) => IMPLEMENT_STEPS.has(e.step) && (e.kind === "tool" || e.kind === "text")).length;
|
|
188
188
|
const reviewTurns = unitEvents.filter((e) => REVIEW_STEPS.has(e.step) && (e.kind === "tool" || e.kind === "text")).length;
|
|
189
|
-
|
|
189
|
+
// implementTurns === 0 is structural (a resume starting mid-pipeline, no
|
|
190
|
+
// implement step at all) — there is nothing to compare review against, not
|
|
191
|
+
// an imbalance. Symmetric with the reviewTurns === 0 guard below.
|
|
192
|
+
if (reviewTurns <= implementTurns || reviewTurns === 0 || implementTurns === 0)
|
|
190
193
|
return [];
|
|
191
194
|
return [
|
|
192
195
|
{
|
|
@@ -238,6 +238,7 @@ export class CliHarness {
|
|
|
238
238
|
system,
|
|
239
239
|
actor: { kind: event.session ? "agent" : "harness", id: actorId },
|
|
240
240
|
type: event.type,
|
|
241
|
+
...(event.ts ? { ts: event.ts } : {}),
|
|
241
242
|
body,
|
|
242
243
|
});
|
|
243
244
|
await this.callMutation(["ledger", "append", "--dir", this.opts.ledgerDir ?? join(this.opts.cwd, ".harness", "ledger")], payload);
|
|
@@ -260,6 +261,14 @@ export class CliHarness {
|
|
|
260
261
|
...(this.opts.specsDir ? ["--specs", workspacePath(this.opts.specsDir, workspace)] : []),
|
|
261
262
|
...(this.opts.profilesDir ? ["--profiles", workspacePath(this.opts.profilesDir, workspace)] : []),
|
|
262
263
|
...(this.opts.systemId ? ["--system-id", this.opts.systemId] : []),
|
|
264
|
+
// an agent-driven run must be distinguishable from an operator typing
|
|
265
|
+
// `harness verify` by hand (issue #50) — record this dispatched
|
|
266
|
+
// session's real identity rather than relying on verify's human/cli
|
|
267
|
+
// default.
|
|
268
|
+
"--agent-kind",
|
|
269
|
+
"agent",
|
|
270
|
+
"--agent-id",
|
|
271
|
+
session,
|
|
263
272
|
"--json",
|
|
264
273
|
...this.base(),
|
|
265
274
|
]);
|
|
@@ -370,9 +379,10 @@ export function parseEnvelopeHash(stdout) {
|
|
|
370
379
|
* `⛔ merge blocked — door door-core-a-schema opened. Decide with: …`
|
|
371
380
|
* `⛔ merge blocked — door door-core-a-schema was REJECTED: …`
|
|
372
381
|
* `⛔ merge blocked — door door-core-a-schema awaits a decision`
|
|
382
|
+
* `⛔ merge blocked — door door-core-a-schema is approved but <reason>`
|
|
373
383
|
*/
|
|
374
384
|
export function parseDoorId(stderr) {
|
|
375
|
-
const m = stderr.match(/door\s+(\S+?)\s+(?:opened|was REJECTED|awaits)\b/);
|
|
385
|
+
const m = stderr.match(/door\s+(\S+?)\s+(?:opened|was REJECTED|awaits|is approved)\b/);
|
|
376
386
|
return m ? m[1] : undefined;
|
|
377
387
|
}
|
|
378
388
|
//# sourceMappingURL=cli.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { reconstructSessionEvents } from "../runners/telemetry.js";
|
|
2
2
|
import { NULL_PUBLISHER } from "../runstate/publish.js";
|
|
3
3
|
import picomatch from "picomatch";
|
|
4
|
+
import { isUnparseableVerdict } from "../prompts/index.js";
|
|
5
|
+
export const MAX_VERIFIER_REASKS = 2;
|
|
4
6
|
function isRemoteTrackingRef(ref) {
|
|
5
7
|
return ref.startsWith("refs/remotes/") || ref.startsWith("origin/");
|
|
6
8
|
}
|
|
@@ -155,6 +157,7 @@ const needsYouKindFor = {
|
|
|
155
157
|
"infrastructure-failure": "infrastructure-failure",
|
|
156
158
|
"config-error": "budget",
|
|
157
159
|
budget: "budget",
|
|
160
|
+
"unparseable-verdict": "escalation",
|
|
158
161
|
};
|
|
159
162
|
function recordHalt(log, halt) {
|
|
160
163
|
log.append({
|
|
@@ -427,11 +430,21 @@ export async function step(unit, policy, deps) {
|
|
|
427
430
|
catch (error) {
|
|
428
431
|
return mutationFailure(unit.id, "heartbeat mutation", error, log);
|
|
429
432
|
}
|
|
433
|
+
const implStartedAt = new Date().toISOString();
|
|
430
434
|
const impl = await runner.run(briefing, policy, "implementer", workspace, onActivity);
|
|
435
|
+
const implEndedAt = new Date().toISOString();
|
|
431
436
|
log.append({ t: "lane.cost", unitId: unit.id, delta: impl.cost });
|
|
432
437
|
if (impl.telemetryError)
|
|
433
438
|
return mutationFailure(unit.id, "implementer session telemetry", impl.telemetryError, log);
|
|
434
|
-
await emitReconstructedTelemetry(deps, {
|
|
439
|
+
await emitReconstructedTelemetry(deps, {
|
|
440
|
+
unitId: unit.id,
|
|
441
|
+
session,
|
|
442
|
+
runner: runner.name,
|
|
443
|
+
model: policy.model,
|
|
444
|
+
kind: "implementer",
|
|
445
|
+
startedAt: implStartedAt,
|
|
446
|
+
endedAt: implEndedAt,
|
|
447
|
+
}, runner, impl);
|
|
435
448
|
if (impl.escalation) {
|
|
436
449
|
log.append({
|
|
437
450
|
t: "unit.escalated",
|
|
@@ -530,11 +543,21 @@ export async function step(unit, policy, deps) {
|
|
|
530
543
|
}
|
|
531
544
|
// --- review (independent verifier session, read-only) -----------
|
|
532
545
|
setStep("review");
|
|
533
|
-
|
|
546
|
+
let reviewStartedAt = new Date().toISOString();
|
|
547
|
+
let review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
|
|
548
|
+
let reviewEndedAt = new Date().toISOString();
|
|
534
549
|
log.append({ t: "lane.cost", unitId: unit.id, delta: review.cost });
|
|
535
550
|
if (review.telemetryError)
|
|
536
551
|
return mutationFailure(unit.id, "verifier session telemetry", review.telemetryError, log);
|
|
537
|
-
await emitReconstructedTelemetry(deps, {
|
|
552
|
+
await emitReconstructedTelemetry(deps, {
|
|
553
|
+
unitId: unit.id,
|
|
554
|
+
session,
|
|
555
|
+
runner: runner.name,
|
|
556
|
+
model: policy.model,
|
|
557
|
+
kind: "verifier",
|
|
558
|
+
startedAt: reviewStartedAt,
|
|
559
|
+
endedAt: reviewEndedAt,
|
|
560
|
+
}, runner, review);
|
|
538
561
|
// a verifier that touches a file voids its verdict (M2 backstop). A
|
|
539
562
|
// ClaudeRunner verifier structurally cannot write (no write tools); this
|
|
540
563
|
// catches a runner that reports one anyway. Isolating a degraded-tier
|
|
@@ -546,6 +569,47 @@ export async function step(unit, policy, deps) {
|
|
|
546
569
|
continue;
|
|
547
570
|
return exhausted(unit.id, config.retryBudget, log);
|
|
548
571
|
}
|
|
572
|
+
let verifierReasks = 0;
|
|
573
|
+
while (isUnparseableVerdict(review.verdict)) {
|
|
574
|
+
if (verifierReasks >= MAX_VERIFIER_REASKS) {
|
|
575
|
+
const reasons = review.verdict?.reasons?.join("; ") || "no VERDICT line in the verifier output";
|
|
576
|
+
const halt = {
|
|
577
|
+
kind: "unparseable-verdict",
|
|
578
|
+
unitId: unit.id,
|
|
579
|
+
message: `${unit.id}: verifier verdict unparseable after ${verifierReasks + 1} attempts (${reasons})`,
|
|
580
|
+
unblock: `inspect the verifier transcript, check model output, and re-run`,
|
|
581
|
+
};
|
|
582
|
+
recordHalt(log, halt);
|
|
583
|
+
return { outcome: "halted", halt };
|
|
584
|
+
}
|
|
585
|
+
verifierReasks++;
|
|
586
|
+
log.append({
|
|
587
|
+
t: "lane.note",
|
|
588
|
+
unitId: unit.id,
|
|
589
|
+
note: `verifier verdict unparseable (${review.verdict?.reasons?.join("; ") || "no VERDICT line"}) — re-asking verifier session (re-ask ${verifierReasks}/${MAX_VERIFIER_REASKS})`,
|
|
590
|
+
});
|
|
591
|
+
reviewStartedAt = new Date().toISOString();
|
|
592
|
+
review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
|
|
593
|
+
reviewEndedAt = new Date().toISOString();
|
|
594
|
+
log.append({ t: "lane.cost", unitId: unit.id, delta: review.cost });
|
|
595
|
+
if (review.telemetryError)
|
|
596
|
+
return mutationFailure(unit.id, "verifier session telemetry", review.telemetryError, log);
|
|
597
|
+
await emitReconstructedTelemetry(deps, {
|
|
598
|
+
unitId: unit.id,
|
|
599
|
+
session,
|
|
600
|
+
runner: runner.name,
|
|
601
|
+
model: policy.model,
|
|
602
|
+
kind: "verifier",
|
|
603
|
+
startedAt: reviewStartedAt,
|
|
604
|
+
endedAt: reviewEndedAt,
|
|
605
|
+
}, runner, review);
|
|
606
|
+
if (review.filesTouched.length > 0) {
|
|
607
|
+
const retry = registerRetryOrExhaust(unit.id, attempt, config.retryBudget, log, `verifier session wrote files (${review.filesTouched.join(", ")}) — verdict voided`);
|
|
608
|
+
if (retry)
|
|
609
|
+
continue;
|
|
610
|
+
return exhausted(unit.id, config.retryBudget, log);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
549
613
|
if (review.verdict && !review.verdict.approved) {
|
|
550
614
|
const retry = registerRetryOrExhaust(unit.id, attempt, config.retryBudget, log, `verifier rejected: ${review.verdict.reasons.join("; ")}`);
|
|
551
615
|
if (retry)
|
|
@@ -786,11 +850,56 @@ export async function resumeAfterDoor(unitId, deps) {
|
|
|
786
850
|
return exhausted(unitId, 0, log, ring2.evidence);
|
|
787
851
|
}
|
|
788
852
|
setStep("review");
|
|
789
|
-
|
|
853
|
+
let reviewStartedAt = new Date().toISOString();
|
|
854
|
+
let review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
|
|
855
|
+
let reviewEndedAt = new Date().toISOString();
|
|
790
856
|
log.append({ t: "lane.cost", unitId, delta: review.cost });
|
|
791
857
|
if (review.telemetryError)
|
|
792
858
|
return mutationFailure(unitId, "door-resume verifier telemetry", review.telemetryError, log);
|
|
793
|
-
await emitReconstructedTelemetry(deps, {
|
|
859
|
+
await emitReconstructedTelemetry(deps, {
|
|
860
|
+
unitId,
|
|
861
|
+
session,
|
|
862
|
+
runner: runner.name,
|
|
863
|
+
model: policy.model,
|
|
864
|
+
kind: "verifier",
|
|
865
|
+
startedAt: reviewStartedAt,
|
|
866
|
+
endedAt: reviewEndedAt,
|
|
867
|
+
}, runner, review);
|
|
868
|
+
let verifierReasks = 0;
|
|
869
|
+
while (isUnparseableVerdict(review.verdict)) {
|
|
870
|
+
if (verifierReasks >= MAX_VERIFIER_REASKS) {
|
|
871
|
+
const reasons = review.verdict?.reasons?.join("; ") || "no VERDICT line in the verifier output";
|
|
872
|
+
const halt = {
|
|
873
|
+
kind: "unparseable-verdict",
|
|
874
|
+
unitId,
|
|
875
|
+
message: `${unitId}: verifier verdict unparseable during door resume after ${verifierReasks + 1} attempts (${reasons})`,
|
|
876
|
+
unblock: `inspect the verifier transcript and re-run orchestrate resume --unit ${unitId}`,
|
|
877
|
+
};
|
|
878
|
+
recordHalt(log, halt);
|
|
879
|
+
return { outcome: "halted", halt };
|
|
880
|
+
}
|
|
881
|
+
verifierReasks++;
|
|
882
|
+
log.append({
|
|
883
|
+
t: "lane.note",
|
|
884
|
+
unitId,
|
|
885
|
+
note: `verifier verdict unparseable during door resume (${review.verdict?.reasons?.join("; ") || "no VERDICT line"}) — re-asking verifier session (re-ask ${verifierReasks}/${MAX_VERIFIER_REASKS})`,
|
|
886
|
+
});
|
|
887
|
+
reviewStartedAt = new Date().toISOString();
|
|
888
|
+
review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
|
|
889
|
+
reviewEndedAt = new Date().toISOString();
|
|
890
|
+
log.append({ t: "lane.cost", unitId, delta: review.cost });
|
|
891
|
+
if (review.telemetryError)
|
|
892
|
+
return mutationFailure(unitId, "door-resume verifier telemetry", review.telemetryError, log);
|
|
893
|
+
await emitReconstructedTelemetry(deps, {
|
|
894
|
+
unitId,
|
|
895
|
+
session,
|
|
896
|
+
runner: runner.name,
|
|
897
|
+
model: policy.model,
|
|
898
|
+
kind: "verifier",
|
|
899
|
+
startedAt: reviewStartedAt,
|
|
900
|
+
endedAt: reviewEndedAt,
|
|
901
|
+
}, runner, review);
|
|
902
|
+
}
|
|
794
903
|
if (!review.ok || review.filesTouched.length || review.verdict?.approved === false) {
|
|
795
904
|
const evidence = review.filesTouched.length
|
|
796
905
|
? `verifier wrote files: ${review.filesTouched.join(", ")}`
|
|
@@ -170,7 +170,12 @@ ${diff.trim() || "(empty diff)"}
|
|
|
170
170
|
\`\`\`
|
|
171
171
|
`;
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
export function isUnparseableVerdict(verdict) {
|
|
174
|
+
if (!verdict)
|
|
175
|
+
return true;
|
|
176
|
+
return verdict.unparseable === true;
|
|
177
|
+
}
|
|
178
|
+
/** Parse the verifier's final `VERDICT:` line. Absent / malformed → unparseable. */
|
|
174
179
|
export function parseVerdict(text) {
|
|
175
180
|
// Strip leading markdown emphasis / quote / list / heading markers and
|
|
176
181
|
// surrounding `**`/`__`/backticks — agents routinely bold the verdict line
|
|
@@ -182,18 +187,29 @@ export function parseVerdict(text) {
|
|
|
182
187
|
.filter((l) => l.toUpperCase().startsWith("VERDICT:"))
|
|
183
188
|
.at(-1);
|
|
184
189
|
if (!line) {
|
|
185
|
-
return {
|
|
190
|
+
return {
|
|
191
|
+
approved: false,
|
|
192
|
+
reasons: ["no VERDICT line in the verifier output"],
|
|
193
|
+
unparseable: true,
|
|
194
|
+
};
|
|
186
195
|
}
|
|
187
196
|
const body = line.slice(line.indexOf(":") + 1).trim();
|
|
188
197
|
if (/^APPROVE\b/i.test(body))
|
|
189
198
|
return { approved: true, reasons: [] };
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
if (/^REJECT\b/i.test(body)) {
|
|
200
|
+
const dash = body.search(/[—-]/);
|
|
201
|
+
const reasonText = dash >= 0 ? body.slice(dash + 1).trim() : "";
|
|
202
|
+
return {
|
|
203
|
+
approved: false,
|
|
204
|
+
reasons: reasonText
|
|
205
|
+
? reasonText.split(",").map((r) => r.trim()).filter(Boolean)
|
|
206
|
+
: ["verifier rejected without stated reasons"],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
192
209
|
return {
|
|
193
210
|
approved: false,
|
|
194
|
-
reasons:
|
|
195
|
-
|
|
196
|
-
: ["verifier rejected without stated reasons"],
|
|
211
|
+
reasons: [`unparseable VERDICT line: "${line}"`],
|
|
212
|
+
unparseable: true,
|
|
197
213
|
};
|
|
198
214
|
}
|
|
199
215
|
//# sourceMappingURL=index.js.map
|
|
@@ -16,11 +16,12 @@ export function reconstructSessionEvents(p) {
|
|
|
16
16
|
? "ok"
|
|
17
17
|
: "failed";
|
|
18
18
|
return [
|
|
19
|
-
{ type: "session.started", unitId: p.unitId, ...common },
|
|
19
|
+
{ type: "session.started", unitId: p.unitId, ts: p.startedAt, ...common },
|
|
20
20
|
{
|
|
21
21
|
type: "session.ended",
|
|
22
22
|
unitId: p.unitId,
|
|
23
23
|
session: p.session,
|
|
24
|
+
ts: p.endedAt,
|
|
24
25
|
body: {
|
|
25
26
|
...common.body,
|
|
26
27
|
outcome,
|
|
@@ -10,6 +10,23 @@ export function checkScope(changedFiles, effectiveScope) {
|
|
|
10
10
|
const matches = picomatch(effectiveScope.map(norm), { dot: true });
|
|
11
11
|
return changedFiles.map(norm).filter((path) => !matches(path));
|
|
12
12
|
}
|
|
13
|
+
/** Path-like tokens in a gate's `run =` command — e.g. `node packages/x/gates.mjs size` → `["packages/x/gates.mjs"]`. */
|
|
14
|
+
function pathTokensIn(run) {
|
|
15
|
+
return run
|
|
16
|
+
.split(/\s+/)
|
|
17
|
+
.filter((tok) => tok.includes("/") || tok.includes("\\"))
|
|
18
|
+
.map(norm);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Which of the resolved profile's own bound-gate script paths this diff
|
|
22
|
+
* touches — a unit editing the referee that judges it (issue #55). Visibility
|
|
23
|
+
* only: this never changes the verdict by itself.
|
|
24
|
+
*/
|
|
25
|
+
export function gateFilesTouched(changedFiles, boundGates) {
|
|
26
|
+
const scriptPaths = new Set(boundGates.flatMap((g) => pathTokensIn(g.run)));
|
|
27
|
+
const changed = new Set(changedFiles.map(norm));
|
|
28
|
+
return [...scriptPaths].filter((p) => changed.has(p)).sort();
|
|
29
|
+
}
|
|
13
30
|
/** Derive both sides of the scope assertion from git + the accepted spec. */
|
|
14
31
|
export function attestScope(opts) {
|
|
15
32
|
if (!opts.baseRef || !opts.specsDir) {
|
|
@@ -35,7 +52,11 @@ export function attestScope(opts) {
|
|
|
35
52
|
reason: `no accepted spec found for unit "${unitId}" under ${opts.specsDir}`,
|
|
36
53
|
};
|
|
37
54
|
}
|
|
38
|
-
const stdout = execFileSync("git", ["diff", "--name-only", `${opts.baseRef}...HEAD`, "--"], {
|
|
55
|
+
const stdout = execFileSync("git", ["diff", "--name-only", `${opts.baseRef}...HEAD`, "--"], {
|
|
56
|
+
cwd: opts.componentDir,
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
});
|
|
39
60
|
const changedFiles = stdout
|
|
40
61
|
.split(/\r?\n/)
|
|
41
62
|
.map((path) => norm(path.trim()))
|
|
@@ -49,7 +70,9 @@ export function attestScope(opts) {
|
|
|
49
70
|
effectiveScope,
|
|
50
71
|
violations,
|
|
51
72
|
...(violations.length > 0
|
|
52
|
-
? {
|
|
73
|
+
? {
|
|
74
|
+
reason: `changed paths outside declared scope: ${violations.join(", ")}`,
|
|
75
|
+
}
|
|
53
76
|
: {}),
|
|
54
77
|
};
|
|
55
78
|
}
|
|
@@ -26,6 +26,8 @@ import { initCommand } from "./init/index.js";
|
|
|
26
26
|
import { claimCommand, heartbeatCommand, nextCommand, } from "./sequencer/cli.js";
|
|
27
27
|
import { specCommand } from "./spec/cli.js";
|
|
28
28
|
import { verifyCommand } from "./verify/cli.js";
|
|
29
|
+
import { attestCommand } from "./provenance/cli.js";
|
|
30
|
+
import { statusCommand } from "./status/cli.js";
|
|
29
31
|
const USAGE = `harness — deterministic scaffolding around nondeterministic agents
|
|
30
32
|
|
|
31
33
|
Usage:
|
|
@@ -45,7 +47,11 @@ Usage:
|
|
|
45
47
|
project emit harness unit YAML from OpenSpec + harness.projection.yaml
|
|
46
48
|
--change <dir> [--openspec <dir>] [--out <dir>] [--json]
|
|
47
49
|
harness next list units ready to claim (deps merged, unclaimed or stale)
|
|
50
|
+
harness status read-only unit lanes from the ledger, with a run-status
|
|
51
|
+
spool overlay [--ledger <dir>] [--spool <dir>] [--json]
|
|
48
52
|
harness claim <unit-id> --session <id> atomically take a ready unit
|
|
53
|
+
--release hand an executing claim straight back
|
|
54
|
+
(refused unless --session holds it)
|
|
49
55
|
harness heartbeat <unit-id> --session <id> keep a claim alive
|
|
50
56
|
harness door list [--sweep] | show <id> | approve <id> --reason … | reject …
|
|
51
57
|
harness merge <unit-id> <--check | --landed <sha>> [facts…] authorize/record
|
|
@@ -65,6 +71,7 @@ const COMMANDS = [
|
|
|
65
71
|
"gate",
|
|
66
72
|
"spec",
|
|
67
73
|
"next",
|
|
74
|
+
"status",
|
|
68
75
|
"claim",
|
|
69
76
|
"heartbeat",
|
|
70
77
|
"door",
|
|
@@ -103,6 +110,9 @@ if (cmd === "spec") {
|
|
|
103
110
|
if (cmd === "next") {
|
|
104
111
|
process.exit(nextCommand(process.argv.slice(3)));
|
|
105
112
|
}
|
|
113
|
+
if (cmd === "status") {
|
|
114
|
+
process.exit(statusCommand(process.argv.slice(3)));
|
|
115
|
+
}
|
|
106
116
|
if (cmd === "door") {
|
|
107
117
|
process.exit(doorCommand(process.argv.slice(3)));
|
|
108
118
|
}
|
|
@@ -169,6 +179,9 @@ if (cmd === "gate") {
|
|
|
169
179
|
process.exit(code);
|
|
170
180
|
// fall through: a bare `harness gate <name>` (not --pre) is still a skeleton
|
|
171
181
|
}
|
|
182
|
+
if (cmd === "attest") {
|
|
183
|
+
process.exit(attestCommand(process.argv.slice(3)));
|
|
184
|
+
}
|
|
172
185
|
process.stderr.write(`harness ${cmd}: not implemented (skeleton)\n`);
|
|
173
186
|
process.exit(1);
|
|
174
187
|
//# sourceMappingURL=cli.js.map
|