patchwork-os 1.2.0-beta.2.canary.646 → 1.2.0-beta.2.canary.648
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/butler/errandOutcomeGrader.d.ts +21 -0
- package/dist/butler/errandOutcomeGrader.js +6 -0
- package/dist/butler/errandOutcomeGrader.js.map +1 -1
- package/dist/butler/outcomeIngester.d.ts +95 -0
- package/dist/butler/outcomeIngester.js +117 -0
- package/dist/butler/outcomeIngester.js.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -1
- package/dist/recipesHttp.js +85 -22
- package/dist/recipesHttp.js.map +1 -1
- package/package.json +1 -1
|
@@ -66,6 +66,27 @@ export interface ObservedErrandArtifact {
|
|
|
66
66
|
deleted?: boolean;
|
|
67
67
|
/** When the errand created the artifact. */
|
|
68
68
|
createdAt?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Was the artifact's CURRENT state actually looked up?
|
|
71
|
+
*
|
|
72
|
+
* This exists because `stale-unactioned → junk` is only sound when a
|
|
73
|
+
* completion COULD have been seen. The rule converts silence into a
|
|
74
|
+
* negative, and silence is only evidence if somebody was listening.
|
|
75
|
+
*
|
|
76
|
+
* An ingester that derives artifacts from the local run log knows when each
|
|
77
|
+
* one was created and nothing whatever about what the operator later did
|
|
78
|
+
* with it. Feeding those to the grader without this flag would mark every
|
|
79
|
+
* errand older than the horizon `junk` — not because the operator ignored
|
|
80
|
+
* it, but because nobody ever asked. That is the trust-by-neglect defect
|
|
81
|
+
* (#1064, #1318/#1319, #1320, #1322) with its sign flipped: instead of
|
|
82
|
+
* flattering a worker for unexamined actions it slanders one, which is
|
|
83
|
+
* worse, because the worker cannot appeal a verdict nobody looked at.
|
|
84
|
+
*
|
|
85
|
+
* Absent or false ⇒ the staleness branch is skipped entirely and the result
|
|
86
|
+
* is `unknown` / `not-observed`. Only an observation channel that could
|
|
87
|
+
* have reported `completed: true` may set this.
|
|
88
|
+
*/
|
|
89
|
+
stateObserved?: boolean;
|
|
69
90
|
}
|
|
70
91
|
export interface GradeOptions {
|
|
71
92
|
/** Clock. Injected so grading is reproducible in replay. */
|
|
@@ -69,6 +69,12 @@ export function gradeErrandOutcome(observed, opts) {
|
|
|
69
69
|
// silence has become meaningful. Withhold.
|
|
70
70
|
return { disposition: "unknown", reason: "not-observed" };
|
|
71
71
|
}
|
|
72
|
+
if (observed.stateObserved !== true) {
|
|
73
|
+
// We know when it was created but never checked what became of it. Age
|
|
74
|
+
// alone cannot distinguish "the operator ignored it" from "nobody looked",
|
|
75
|
+
// and only the first is evidence. See `stateObserved`.
|
|
76
|
+
return { disposition: "unknown", reason: "not-observed" };
|
|
77
|
+
}
|
|
72
78
|
const staleAfter = opts.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
73
79
|
const age = opts.now - observed.createdAt;
|
|
74
80
|
if (age >= staleAfter) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errandOutcomeGrader.js","sourceRoot":"","sources":["../../src/butler/errandOutcomeGrader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAIH,0CAA0C;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"errandOutcomeGrader.js","sourceRoot":"","sources":["../../src/butler/errandOutcomeGrader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAIH,0CAA0C;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAoE/D;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAgC,EAChC,IAAkB;IAElB,yEAAyE;IACzE,6EAA6E;IAC7E,6EAA6E;IAC7E,yCAAyC;IACzC,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpD,CAAC;IACD,IAAI,QAAQ,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAChC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC3D,CAAC;IAED,yEAAyE;IACzE,mCAAmC;IACnC,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACrC,2EAA2E;QAC3E,2CAA2C;QAC3C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAC5D,CAAC;IAED,IAAI,QAAQ,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;QACpC,uEAAuE;QACvE,2EAA2E;QAC3E,uDAAuD;QACvD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAC5D,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,IAAI,sBAAsB,CAAC;IAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,QAAQ,CAAC,SAAS,CAAC;IAC1C,IAAI,GAAG,IAAI,UAAU,EAAE,CAAC;QACtB,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7D,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Butler errand outcome ingester — grade a batch of observations into the
|
|
3
|
+
* SHADOW ledger, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* ## What this is for
|
|
6
|
+
*
|
|
7
|
+
* `errandOutcomeGrader.ts` is a pure function and `outcomeShadowLog.ts` is an
|
|
8
|
+
* append-only file. Between them there was nothing: the grader was merged
|
|
9
|
+
* unwired, so no row had ever been written and the measurement the shadow
|
|
10
|
+
* phase exists to produce did not exist. This is the missing middle.
|
|
11
|
+
*
|
|
12
|
+
* ## Where it may be called from, and where it may not
|
|
13
|
+
*
|
|
14
|
+
* Operator paths ONLY — the Bearer-authenticated HTTP route and the CLI, the
|
|
15
|
+
* same shape as `outcomes confirm`. It is deliberately NOT registered in the
|
|
16
|
+
* recipe tool registry, and there is a test asserting that.
|
|
17
|
+
*
|
|
18
|
+
* The reason is the one that makes `outcomes confirm` a CLI verb rather than a
|
|
19
|
+
* recipe step: a worker must not be able to grade its own filings. A recipe
|
|
20
|
+
* step runs as the worker. If grading were reachable from one, a worker whose
|
|
21
|
+
* errands nobody ever looks at could emit `completed: true` for each of them
|
|
22
|
+
* and manufacture the evidence that raises its own trust dial. That is not a
|
|
23
|
+
* hypothetical class of bug in this subsystem — it is the same defect as
|
|
24
|
+
* #1064, #1318/#1319, #1320 and #1322, which is four times.
|
|
25
|
+
*
|
|
26
|
+
* ## No model in the loop
|
|
27
|
+
*
|
|
28
|
+
* Every disposition here comes from `gradeErrandOutcome`, a pure function of
|
|
29
|
+
* observed state and two timestamps. A prior LLM judge in this repo flipped
|
|
30
|
+
* verdicts between runs on identical inputs; a trust ledger you cannot replay
|
|
31
|
+
* is not evidence, it is an opinion with a timestamp.
|
|
32
|
+
*
|
|
33
|
+
* ## It cannot promote
|
|
34
|
+
*
|
|
35
|
+
* This module imports `appendShadowOutcome` and does not import `OutcomeStore`
|
|
36
|
+
* at all, so there is no code path from here into `outcome-log.jsonl`. That is
|
|
37
|
+
* structural, not a convention — promotion needs a measured before/after on
|
|
38
|
+
* the real log, exactly as #1319 required, and until somebody has READ these
|
|
39
|
+
* rows the grader has not earned the right to be evidence.
|
|
40
|
+
*/
|
|
41
|
+
import { type GradeOptions, type ObservedErrandArtifact } from "./errandOutcomeGrader.js";
|
|
42
|
+
import { type ShadowSummary } from "./outcomeShadowLog.js";
|
|
43
|
+
/** One artifact to grade, plus the key it joins on. */
|
|
44
|
+
export interface ErrandObservation extends ObservedErrandArtifact {
|
|
45
|
+
/**
|
|
46
|
+
* `canonicalActionRef` form (`"<tool>:<id>"`, or a URL for legacy rows).
|
|
47
|
+
*
|
|
48
|
+
* Required. A graded row under a key the fold could not resolve is a
|
|
49
|
+
* measurement of nothing — it would sit in the shadow ledger inflating the
|
|
50
|
+
* counts somebody reads before deciding to promote.
|
|
51
|
+
*/
|
|
52
|
+
ref: string;
|
|
53
|
+
/** The recipe that filed it, for attribution during review. */
|
|
54
|
+
recipe?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface IngestResult {
|
|
57
|
+
/** Observations accepted and written. */
|
|
58
|
+
graded: number;
|
|
59
|
+
/** Rejected before grading, with why. Never silently dropped. */
|
|
60
|
+
skipped: {
|
|
61
|
+
ref?: string;
|
|
62
|
+
reason: "missing-ref" | "duplicate-ref";
|
|
63
|
+
}[];
|
|
64
|
+
/** Per-disposition counts for THIS batch (not the whole ledger). */
|
|
65
|
+
batch: {
|
|
66
|
+
confirmed: number;
|
|
67
|
+
junk: number;
|
|
68
|
+
unknown: number;
|
|
69
|
+
};
|
|
70
|
+
/** The whole ledger after the write — what a reviewer actually acts on. */
|
|
71
|
+
ledger: ShadowSummary;
|
|
72
|
+
}
|
|
73
|
+
export interface IngestOptions extends GradeOptions {
|
|
74
|
+
/** Shadow-ledger directory override. Tests pass a temp dir. */
|
|
75
|
+
dir?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Grade every observation and append a shadow row per accepted one.
|
|
79
|
+
*
|
|
80
|
+
* Duplicate refs within a batch are skipped rather than written twice: the
|
|
81
|
+
* ledger is append-only and the summary counts rows, so a batch that repeated
|
|
82
|
+
* a ref would overstate the evidence available. Duplicates ACROSS batches are
|
|
83
|
+
* deliberately not deduped — successive observations of the same artifact over
|
|
84
|
+
* time are the point, and collapsing them would erase the history that shows
|
|
85
|
+
* an errand going from open to completed.
|
|
86
|
+
*/
|
|
87
|
+
export declare function ingestErrandOutcomes(observations: readonly ErrandObservation[], opts: IngestOptions): IngestResult;
|
|
88
|
+
/**
|
|
89
|
+
* Render the ledger for a human — the "READ the shadow rows" half.
|
|
90
|
+
*
|
|
91
|
+
* States the promotion bar in the output rather than in a doc nobody opens
|
|
92
|
+
* next to the number, because the number's whole purpose is to be weighed
|
|
93
|
+
* against it.
|
|
94
|
+
*/
|
|
95
|
+
export declare function formatShadowSummary(s: ShadowSummary): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Butler errand outcome ingester — grade a batch of observations into the
|
|
3
|
+
* SHADOW ledger, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* ## What this is for
|
|
6
|
+
*
|
|
7
|
+
* `errandOutcomeGrader.ts` is a pure function and `outcomeShadowLog.ts` is an
|
|
8
|
+
* append-only file. Between them there was nothing: the grader was merged
|
|
9
|
+
* unwired, so no row had ever been written and the measurement the shadow
|
|
10
|
+
* phase exists to produce did not exist. This is the missing middle.
|
|
11
|
+
*
|
|
12
|
+
* ## Where it may be called from, and where it may not
|
|
13
|
+
*
|
|
14
|
+
* Operator paths ONLY — the Bearer-authenticated HTTP route and the CLI, the
|
|
15
|
+
* same shape as `outcomes confirm`. It is deliberately NOT registered in the
|
|
16
|
+
* recipe tool registry, and there is a test asserting that.
|
|
17
|
+
*
|
|
18
|
+
* The reason is the one that makes `outcomes confirm` a CLI verb rather than a
|
|
19
|
+
* recipe step: a worker must not be able to grade its own filings. A recipe
|
|
20
|
+
* step runs as the worker. If grading were reachable from one, a worker whose
|
|
21
|
+
* errands nobody ever looks at could emit `completed: true` for each of them
|
|
22
|
+
* and manufacture the evidence that raises its own trust dial. That is not a
|
|
23
|
+
* hypothetical class of bug in this subsystem — it is the same defect as
|
|
24
|
+
* #1064, #1318/#1319, #1320 and #1322, which is four times.
|
|
25
|
+
*
|
|
26
|
+
* ## No model in the loop
|
|
27
|
+
*
|
|
28
|
+
* Every disposition here comes from `gradeErrandOutcome`, a pure function of
|
|
29
|
+
* observed state and two timestamps. A prior LLM judge in this repo flipped
|
|
30
|
+
* verdicts between runs on identical inputs; a trust ledger you cannot replay
|
|
31
|
+
* is not evidence, it is an opinion with a timestamp.
|
|
32
|
+
*
|
|
33
|
+
* ## It cannot promote
|
|
34
|
+
*
|
|
35
|
+
* This module imports `appendShadowOutcome` and does not import `OutcomeStore`
|
|
36
|
+
* at all, so there is no code path from here into `outcome-log.jsonl`. That is
|
|
37
|
+
* structural, not a convention — promotion needs a measured before/after on
|
|
38
|
+
* the real log, exactly as #1319 required, and until somebody has READ these
|
|
39
|
+
* rows the grader has not earned the right to be evidence.
|
|
40
|
+
*/
|
|
41
|
+
import { gradeErrandOutcome, } from "./errandOutcomeGrader.js";
|
|
42
|
+
import { appendShadowOutcome, summariseShadowLog, } from "./outcomeShadowLog.js";
|
|
43
|
+
/**
|
|
44
|
+
* Grade every observation and append a shadow row per accepted one.
|
|
45
|
+
*
|
|
46
|
+
* Duplicate refs within a batch are skipped rather than written twice: the
|
|
47
|
+
* ledger is append-only and the summary counts rows, so a batch that repeated
|
|
48
|
+
* a ref would overstate the evidence available. Duplicates ACROSS batches are
|
|
49
|
+
* deliberately not deduped — successive observations of the same artifact over
|
|
50
|
+
* time are the point, and collapsing them would erase the history that shows
|
|
51
|
+
* an errand going from open to completed.
|
|
52
|
+
*/
|
|
53
|
+
export function ingestErrandOutcomes(observations, opts) {
|
|
54
|
+
const skipped = [];
|
|
55
|
+
const batch = { confirmed: 0, junk: 0, unknown: 0 };
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
let graded = 0;
|
|
58
|
+
for (const obs of observations) {
|
|
59
|
+
const ref = typeof obs.ref === "string" ? obs.ref.trim() : "";
|
|
60
|
+
if (!ref) {
|
|
61
|
+
skipped.push({ reason: "missing-ref" });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (seen.has(ref)) {
|
|
65
|
+
skipped.push({ ref, reason: "duplicate-ref" });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
seen.add(ref);
|
|
69
|
+
const { disposition, reason } = gradeErrandOutcome(obs, opts);
|
|
70
|
+
appendShadowOutcome({
|
|
71
|
+
ref,
|
|
72
|
+
disposition,
|
|
73
|
+
reason,
|
|
74
|
+
gradedAt: opts.now,
|
|
75
|
+
...(obs.recipe ? { recipe: obs.recipe } : {}),
|
|
76
|
+
}, { dir: opts.dir });
|
|
77
|
+
batch[disposition]++;
|
|
78
|
+
graded++;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
graded,
|
|
82
|
+
skipped,
|
|
83
|
+
batch,
|
|
84
|
+
ledger: summariseShadowLog({ dir: opts.dir }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Render the ledger for a human — the "READ the shadow rows" half.
|
|
89
|
+
*
|
|
90
|
+
* States the promotion bar in the output rather than in a doc nobody opens
|
|
91
|
+
* next to the number, because the number's whole purpose is to be weighed
|
|
92
|
+
* against it.
|
|
93
|
+
*/
|
|
94
|
+
export function formatShadowSummary(s) {
|
|
95
|
+
if (s.total === 0) {
|
|
96
|
+
return [
|
|
97
|
+
"[butler-shadow] no graded rows yet.",
|
|
98
|
+
"",
|
|
99
|
+
" Nothing has been measured, so nothing may be promoted. Run the",
|
|
100
|
+
" ingester against real errand observations first.",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
const pct = (n) => `${((n / s.total) * 100).toFixed(1)}%`;
|
|
104
|
+
return [
|
|
105
|
+
`[butler-shadow] ${s.total} graded row(s):`,
|
|
106
|
+
` confirmed ${s.confirmed} (${pct(s.confirmed)})`,
|
|
107
|
+
` junk ${s.junk} (${pct(s.junk)})`,
|
|
108
|
+
` unknown ${s.unknown} (${pct(s.unknown)}) — withheld by the fold`,
|
|
109
|
+
"",
|
|
110
|
+
` ${s.wouldCount} row(s) (${pct(s.wouldCount)}) would have become evidence.`,
|
|
111
|
+
"",
|
|
112
|
+
" These rows moved nothing. Before promoting, check a sample against the",
|
|
113
|
+
" real errands they describe — a disposition that reads plausibly in",
|
|
114
|
+
" aggregate can still be wrong on every individual row.",
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=outcomeIngester.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcomeIngester.js","sourceRoot":"","sources":["../../src/butler/outcomeIngester.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -215,6 +215,7 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
215
215
|
"approvals",
|
|
216
216
|
"gate",
|
|
217
217
|
"outcomes",
|
|
218
|
+
"butler",
|
|
218
219
|
// Dispatched at `process.argv[2] === "tools"` (and `help`) but absent from
|
|
219
220
|
// this array until 2026-08. The comment below calls this "the dispatch
|
|
220
221
|
// source", and it was not: `patchwork tool` could never be corrected to
|
|
@@ -4230,6 +4231,88 @@ if (process.argv[2] === "gate") {
|
|
|
4230
4231
|
}
|
|
4231
4232
|
})();
|
|
4232
4233
|
}
|
|
4234
|
+
// Handle butler subcommand — SHADOW-only errand outcome grading.
|
|
4235
|
+
//
|
|
4236
|
+
// Operator path, like `outcomes` above and for the same reason: a recipe step
|
|
4237
|
+
// runs AS the worker, so grading reachable from one would let a worker
|
|
4238
|
+
// manufacture the evidence that raises its own trust dial. Nothing here writes
|
|
4239
|
+
// the trust ledger — rows go to the Butler shadow ledger and move nothing.
|
|
4240
|
+
if (process.argv[2] === "butler") {
|
|
4241
|
+
const args = process.argv.slice(3);
|
|
4242
|
+
(async () => {
|
|
4243
|
+
try {
|
|
4244
|
+
const { formatShadowSummary, ingestErrandOutcomes } = await import("./butler/outcomeIngester.js");
|
|
4245
|
+
const { summariseShadowLog } = await import("./butler/outcomeShadowLog.js");
|
|
4246
|
+
if (args[0] === "shadow") {
|
|
4247
|
+
const summary = summariseShadowLog();
|
|
4248
|
+
if (args.includes("--json")) {
|
|
4249
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
4250
|
+
}
|
|
4251
|
+
else {
|
|
4252
|
+
process.stdout.write(`${formatShadowSummary(summary)}\n`);
|
|
4253
|
+
}
|
|
4254
|
+
process.exit(0);
|
|
4255
|
+
}
|
|
4256
|
+
if (args[0] === "ingest") {
|
|
4257
|
+
// Observations come from a file or stdin as a JSON array. They are NOT
|
|
4258
|
+
// gathered here: reading the operator's trackers is a connector
|
|
4259
|
+
// concern, and inventing an observation the ingester did not actually
|
|
4260
|
+
// make is precisely what `stateObserved` exists to prevent.
|
|
4261
|
+
const fileIdx = args.indexOf("--file");
|
|
4262
|
+
const src = fileIdx !== -1 && args[fileIdx + 1] ? args[fileIdx + 1] : "-";
|
|
4263
|
+
const { readFileSync } = await import("node:fs");
|
|
4264
|
+
const raw = src === "-"
|
|
4265
|
+
? readFileSync(0, "utf-8")
|
|
4266
|
+
: readFileSync(src, "utf-8");
|
|
4267
|
+
let parsed;
|
|
4268
|
+
try {
|
|
4269
|
+
parsed = JSON.parse(raw);
|
|
4270
|
+
}
|
|
4271
|
+
catch (err) {
|
|
4272
|
+
process.stderr.write(`[butler] observations are not valid JSON: ${err.message}\n`);
|
|
4273
|
+
process.exit(2);
|
|
4274
|
+
}
|
|
4275
|
+
if (!Array.isArray(parsed)) {
|
|
4276
|
+
process.stderr.write("[butler] expected a JSON array\n");
|
|
4277
|
+
process.exit(2);
|
|
4278
|
+
}
|
|
4279
|
+
const daysIdx = args.indexOf("--stale-after-days");
|
|
4280
|
+
const days = daysIdx !== -1 && args[daysIdx + 1]
|
|
4281
|
+
? Number(args[daysIdx + 1])
|
|
4282
|
+
: undefined;
|
|
4283
|
+
const res = ingestErrandOutcomes(parsed, {
|
|
4284
|
+
now: Date.now(),
|
|
4285
|
+
...(days !== undefined && Number.isFinite(days)
|
|
4286
|
+
? { staleAfterMs: days * 24 * 60 * 60 * 1000 }
|
|
4287
|
+
: {}),
|
|
4288
|
+
});
|
|
4289
|
+
if (args.includes("--json")) {
|
|
4290
|
+
process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
4291
|
+
}
|
|
4292
|
+
else {
|
|
4293
|
+
process.stdout.write(`[butler] graded ${res.graded} observation(s): ` +
|
|
4294
|
+
`${res.batch.confirmed} confirmed, ${res.batch.junk} junk, ` +
|
|
4295
|
+
`${res.batch.unknown} unknown.\n`);
|
|
4296
|
+
for (const s of res.skipped) {
|
|
4297
|
+
process.stdout.write(`[butler] skipped ${s.ref ?? "(no ref)"}: ${s.reason}\n`);
|
|
4298
|
+
}
|
|
4299
|
+
process.stdout.write(`\n${formatShadowSummary(res.ledger)}\n`);
|
|
4300
|
+
}
|
|
4301
|
+
process.exit(0);
|
|
4302
|
+
}
|
|
4303
|
+
process.stderr.write("Usage: patchwork butler <shadow|ingest> [--json]\n\n" +
|
|
4304
|
+
" shadow summarise the graded shadow ledger\n" +
|
|
4305
|
+
" ingest [--file <path>|-] grade a JSON array of observations\n" +
|
|
4306
|
+
" [--stale-after-days N]\n\n" +
|
|
4307
|
+
" Shadow-only: nothing here writes the trust ledger.\n");
|
|
4308
|
+
process.exit(2);
|
|
4309
|
+
}
|
|
4310
|
+
catch (err) {
|
|
4311
|
+
process.stderr.write(`[butler] ${err.message}\n`);
|
|
4312
|
+
process.exit(2);
|
|
4313
|
+
}
|
|
4314
|
+
})();
|
|
4315
|
+
}
|
|
4233
4316
|
// Handle outcomes subcommand — operator positive-act confirmation of worker
|
|
4234
4317
|
// filings (confirm/reject/list). Writes to ~/.patchwork/outcome-log.jsonl, the
|
|
4235
4318
|
// same store the trust ramp reads; it is NOT a recipe step, so a worker cannot
|