patchwork-os 1.2.0-beta.3.canary.929 → 1.2.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/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/maintenance/cronHaltGate.d.ts +93 -0
- package/dist/maintenance/cronHaltGate.js +185 -0
- package/dist/maintenance/cronHaltGate.js.map +1 -0
- package/dist/maintenance/sweepCollect.d.ts +13 -1
- package/dist/maintenance/sweepCollect.js +25 -1
- package/dist/maintenance/sweepCollect.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The third `sweep` gate: a scheduled recipe that used to work and now fails.
|
|
3
|
+
*
|
|
4
|
+
* On 2026-10-14 the hourly privacy heartbeat on the reference install began
|
|
5
|
+
* failing every run (the local model it named had been removed) and nothing
|
|
6
|
+
* surfaced it for 30 hours. `halts` could see it, but `halts` answers a
|
|
7
|
+
* question somebody has to ask; `sweep` is the verb that runs unasked. Every
|
|
8
|
+
* one of the 24 runs on the following day was `error`.
|
|
9
|
+
*
|
|
10
|
+
* ## The rule, decided and deliberately narrow
|
|
11
|
+
*
|
|
12
|
+
* A cron-triggered recipe whose runs were `done` for at least seven
|
|
13
|
+
* consecutive days and whose most recent six or more runs are all `error`
|
|
14
|
+
* is FAILING. The gate is healthy when no recipe is in that state. A recipe
|
|
15
|
+
* with fewer than seven days of history is not eligible — it has no "used to
|
|
16
|
+
* work" to flip from, and a baseline is not a flip, the same distinction the
|
|
17
|
+
* sweep itself draws on its first run.
|
|
18
|
+
*
|
|
19
|
+
* Why both halves. Six errors alone would fire on a brand-new recipe that
|
|
20
|
+
* never worked; that is a defect, but it is not THIS defect, and it is already
|
|
21
|
+
* visible on the run page the moment anyone installs it. Seven clean days
|
|
22
|
+
* alone would fire on one bad hour. Together they describe the failure that
|
|
23
|
+
* went unnoticed: something that had earned the right to be ignored, and then
|
|
24
|
+
* quietly stopped.
|
|
25
|
+
*
|
|
26
|
+
* ## Counts in the snapshot, names in the inbox
|
|
27
|
+
*
|
|
28
|
+
* The snapshot row is counts only — how many recipes are failing, how many
|
|
29
|
+
* were eligible — because `sweep_snapshots.jsonl` must never accumulate
|
|
30
|
+
* operator data. The recipe name and the first failing run time go to ONE
|
|
31
|
+
* inbox note, written when the gate flips and not again while it stays
|
|
32
|
+
* failing, because a note per sweep is a note nobody reads.
|
|
33
|
+
*/
|
|
34
|
+
import { type RecipeRun } from "../runLog.js";
|
|
35
|
+
import type { SweepDelta } from "./sweep.js";
|
|
36
|
+
/** Days of consecutive `done` runs a recipe needs before it can flip. */
|
|
37
|
+
export declare const CRON_HALT_MIN_CLEAN_DAYS = 7;
|
|
38
|
+
/** Consecutive `error` runs, most recent first, that constitute the flip. */
|
|
39
|
+
export declare const CRON_HALT_MIN_ERROR_STREAK = 6;
|
|
40
|
+
/** Gate key in the sweep reading. */
|
|
41
|
+
export declare const CRON_HALT_GATE = "cronHalts";
|
|
42
|
+
/** The slice of a run row this gate reads. */
|
|
43
|
+
export type CronRunRow = Pick<RecipeRun, "taskId" | "recipeName" | "trigger" | "status" | "createdAt" | "doneAt">;
|
|
44
|
+
export interface FailingRecipe {
|
|
45
|
+
recipeName: string;
|
|
46
|
+
/** `createdAt` of the first run in the trailing error streak. */
|
|
47
|
+
firstFailureAt: number;
|
|
48
|
+
/** Length of the trailing error streak. */
|
|
49
|
+
errorStreak: number;
|
|
50
|
+
/** Span of the `done` runs immediately before the streak, in ms. */
|
|
51
|
+
cleanSpanMs: number;
|
|
52
|
+
}
|
|
53
|
+
export interface CronHaltAssessment {
|
|
54
|
+
/** Cron recipes with at least `CRON_HALT_MIN_CLEAN_DAYS` of clean history. */
|
|
55
|
+
eligible: number;
|
|
56
|
+
failing: FailingRecipe[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pure: decide which cron recipes are in the failing state. Rows may arrive in
|
|
60
|
+
* any order and may include non-cron or non-terminal rows; both are ignored.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assessCronHalts(rows: CronRunRow[], _opts: {
|
|
63
|
+
now: number;
|
|
64
|
+
}): CronHaltAssessment;
|
|
65
|
+
/**
|
|
66
|
+
* Terminal cron runs from the run log and its rotation archive, through the
|
|
67
|
+
* same reader `workers shadow` uses: `RecipeRunLog` dedups by `taskId`
|
|
68
|
+
* (never `seq`, #1324) and the archive is merged the same way, so a run that
|
|
69
|
+
* rotated out is still history and a run written twice is still one run.
|
|
70
|
+
* Replay rows are not runs the scheduler started.
|
|
71
|
+
*
|
|
72
|
+
* Returns an empty list when there is no log to read; the caller treats that
|
|
73
|
+
* as a reading it could not take, not as a healthy gate.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readCronRuns(dir: string): CronRunRow[];
|
|
76
|
+
/** The log exists (live or archive), so a reading can be taken. */
|
|
77
|
+
export declare function cronRunLogPresent(dir: string): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Write the inbox note for a flip of the cron-halt gate, and only for a flip:
|
|
80
|
+
* the previous snapshot carried the gate as healthy and this reading does not.
|
|
81
|
+
* A first sweep, a gate the previous snapshot never had, and a gate that is
|
|
82
|
+
* still failing all write nothing. Returns the path written, or undefined.
|
|
83
|
+
*
|
|
84
|
+
* The note carries the same run header a recipe's `file.write` to the inbox
|
|
85
|
+
* gets (`recipe` / `trigger` / `deliveredAt`), so the inbox lists it like any
|
|
86
|
+
* other delivery. There is no `runSeq`: a sweep is not a run.
|
|
87
|
+
*/
|
|
88
|
+
export declare function noteCronHaltFlip(opts: {
|
|
89
|
+
delta: SweepDelta;
|
|
90
|
+
assessment: CronHaltAssessment;
|
|
91
|
+
inbox: string;
|
|
92
|
+
now: number;
|
|
93
|
+
}): string | undefined;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The third `sweep` gate: a scheduled recipe that used to work and now fails.
|
|
3
|
+
*
|
|
4
|
+
* On 2026-10-14 the hourly privacy heartbeat on the reference install began
|
|
5
|
+
* failing every run (the local model it named had been removed) and nothing
|
|
6
|
+
* surfaced it for 30 hours. `halts` could see it, but `halts` answers a
|
|
7
|
+
* question somebody has to ask; `sweep` is the verb that runs unasked. Every
|
|
8
|
+
* one of the 24 runs on the following day was `error`.
|
|
9
|
+
*
|
|
10
|
+
* ## The rule, decided and deliberately narrow
|
|
11
|
+
*
|
|
12
|
+
* A cron-triggered recipe whose runs were `done` for at least seven
|
|
13
|
+
* consecutive days and whose most recent six or more runs are all `error`
|
|
14
|
+
* is FAILING. The gate is healthy when no recipe is in that state. A recipe
|
|
15
|
+
* with fewer than seven days of history is not eligible — it has no "used to
|
|
16
|
+
* work" to flip from, and a baseline is not a flip, the same distinction the
|
|
17
|
+
* sweep itself draws on its first run.
|
|
18
|
+
*
|
|
19
|
+
* Why both halves. Six errors alone would fire on a brand-new recipe that
|
|
20
|
+
* never worked; that is a defect, but it is not THIS defect, and it is already
|
|
21
|
+
* visible on the run page the moment anyone installs it. Seven clean days
|
|
22
|
+
* alone would fire on one bad hour. Together they describe the failure that
|
|
23
|
+
* went unnoticed: something that had earned the right to be ignored, and then
|
|
24
|
+
* quietly stopped.
|
|
25
|
+
*
|
|
26
|
+
* ## Counts in the snapshot, names in the inbox
|
|
27
|
+
*
|
|
28
|
+
* The snapshot row is counts only — how many recipes are failing, how many
|
|
29
|
+
* were eligible — because `sweep_snapshots.jsonl` must never accumulate
|
|
30
|
+
* operator data. The recipe name and the first failing run time go to ONE
|
|
31
|
+
* inbox note, written when the gate flips and not again while it stays
|
|
32
|
+
* failing, because a note per sweep is a note nobody reads.
|
|
33
|
+
*/
|
|
34
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
import { isReplayRun, RecipeRunLog } from "../runLog.js";
|
|
37
|
+
/** Days of consecutive `done` runs a recipe needs before it can flip. */
|
|
38
|
+
export const CRON_HALT_MIN_CLEAN_DAYS = 7;
|
|
39
|
+
/** Consecutive `error` runs, most recent first, that constitute the flip. */
|
|
40
|
+
export const CRON_HALT_MIN_ERROR_STREAK = 6;
|
|
41
|
+
/** Gate key in the sweep reading. */
|
|
42
|
+
export const CRON_HALT_GATE = "cronHalts";
|
|
43
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
44
|
+
/**
|
|
45
|
+
* Pure: decide which cron recipes are in the failing state. Rows may arrive in
|
|
46
|
+
* any order and may include non-cron or non-terminal rows; both are ignored.
|
|
47
|
+
*/
|
|
48
|
+
export function assessCronHalts(rows, _opts) {
|
|
49
|
+
const byRecipe = new Map();
|
|
50
|
+
for (const r of rows) {
|
|
51
|
+
if (r.trigger !== "cron")
|
|
52
|
+
continue;
|
|
53
|
+
if (r.status !== "done" && r.status !== "error")
|
|
54
|
+
continue;
|
|
55
|
+
const list = byRecipe.get(r.recipeName) ?? [];
|
|
56
|
+
list.push(r);
|
|
57
|
+
byRecipe.set(r.recipeName, list);
|
|
58
|
+
}
|
|
59
|
+
let eligible = 0;
|
|
60
|
+
const failing = [];
|
|
61
|
+
for (const [recipeName, list] of byRecipe) {
|
|
62
|
+
list.sort((a, b) => a.createdAt - b.createdAt);
|
|
63
|
+
// Trailing error streak, newest backwards.
|
|
64
|
+
let i = list.length - 1;
|
|
65
|
+
let errorStreak = 0;
|
|
66
|
+
while (i >= 0 && list[i]?.status === "error") {
|
|
67
|
+
errorStreak++;
|
|
68
|
+
i--;
|
|
69
|
+
}
|
|
70
|
+
// The `done` run immediately before the streak, walking back while done.
|
|
71
|
+
const lastClean = i;
|
|
72
|
+
while (i >= 0 && list[i]?.status === "done")
|
|
73
|
+
i--;
|
|
74
|
+
const firstClean = i + 1;
|
|
75
|
+
const cleanSpanMs = lastClean >= firstClean
|
|
76
|
+
? list[lastClean].createdAt -
|
|
77
|
+
list[firstClean].createdAt
|
|
78
|
+
: 0;
|
|
79
|
+
if (cleanSpanMs < CRON_HALT_MIN_CLEAN_DAYS * DAY_MS)
|
|
80
|
+
continue;
|
|
81
|
+
eligible++;
|
|
82
|
+
if (errorStreak < CRON_HALT_MIN_ERROR_STREAK)
|
|
83
|
+
continue;
|
|
84
|
+
failing.push({
|
|
85
|
+
recipeName,
|
|
86
|
+
firstFailureAt: list[lastClean + 1].createdAt,
|
|
87
|
+
errorStreak,
|
|
88
|
+
cleanSpanMs,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
failing.sort((a, b) => a.recipeName.localeCompare(b.recipeName));
|
|
92
|
+
return { eligible, failing };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Terminal cron runs from the run log and its rotation archive, through the
|
|
96
|
+
* same reader `workers shadow` uses: `RecipeRunLog` dedups by `taskId`
|
|
97
|
+
* (never `seq`, #1324) and the archive is merged the same way, so a run that
|
|
98
|
+
* rotated out is still history and a run written twice is still one run.
|
|
99
|
+
* Replay rows are not runs the scheduler started.
|
|
100
|
+
*
|
|
101
|
+
* Returns an empty list when there is no log to read; the caller treats that
|
|
102
|
+
* as a reading it could not take, not as a healthy gate.
|
|
103
|
+
*/
|
|
104
|
+
export function readCronRuns(dir) {
|
|
105
|
+
if (!existsSync(path.join(dir, "runs.jsonl")) &&
|
|
106
|
+
!existsSync(path.join(dir, "runs.jsonl.1"))) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
// A cap large enough that one busy recipe cannot evict another's history
|
|
110
|
+
// before it is read; the default (500) is sized for a dashboard ring.
|
|
111
|
+
const log = new RecipeRunLog({ dir, memoryCap: 50_000 });
|
|
112
|
+
const merged = new Map();
|
|
113
|
+
for (const r of [...log.readArchive(), ...log.query({ limit: 500 })]) {
|
|
114
|
+
merged.set(r.taskId ? `task:${r.taskId}` : `seq:${r.seq}`, r);
|
|
115
|
+
}
|
|
116
|
+
// `query` clamps at 500 rows overall; re-query per cron recipe so a busy
|
|
117
|
+
// install does not truncate the recipe this gate is watching.
|
|
118
|
+
const names = new Set();
|
|
119
|
+
for (const r of merged.values())
|
|
120
|
+
if (r.trigger === "cron")
|
|
121
|
+
names.add(r.recipeName);
|
|
122
|
+
for (const recipe of names) {
|
|
123
|
+
for (const r of log.query({ recipe, trigger: "cron", limit: 500 })) {
|
|
124
|
+
merged.set(r.taskId ? `task:${r.taskId}` : `seq:${r.seq}`, r);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const r of merged.values()) {
|
|
129
|
+
if (r.trigger !== "cron" || isReplayRun(r))
|
|
130
|
+
continue;
|
|
131
|
+
if (r.status !== "done" && r.status !== "error")
|
|
132
|
+
continue;
|
|
133
|
+
out.push({
|
|
134
|
+
taskId: r.taskId,
|
|
135
|
+
recipeName: r.recipeName,
|
|
136
|
+
trigger: r.trigger,
|
|
137
|
+
status: r.status,
|
|
138
|
+
createdAt: r.createdAt,
|
|
139
|
+
doneAt: r.doneAt,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/** The log exists (live or archive), so a reading can be taken. */
|
|
145
|
+
export function cronRunLogPresent(dir) {
|
|
146
|
+
return (existsSync(path.join(dir, "runs.jsonl")) ||
|
|
147
|
+
existsSync(path.join(dir, "runs.jsonl.1")));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Write the inbox note for a flip of the cron-halt gate, and only for a flip:
|
|
151
|
+
* the previous snapshot carried the gate as healthy and this reading does not.
|
|
152
|
+
* A first sweep, a gate the previous snapshot never had, and a gate that is
|
|
153
|
+
* still failing all write nothing. Returns the path written, or undefined.
|
|
154
|
+
*
|
|
155
|
+
* The note carries the same run header a recipe's `file.write` to the inbox
|
|
156
|
+
* gets (`recipe` / `trigger` / `deliveredAt`), so the inbox lists it like any
|
|
157
|
+
* other delivery. There is no `runSeq`: a sweep is not a run.
|
|
158
|
+
*/
|
|
159
|
+
export function noteCronHaltFlip(opts) {
|
|
160
|
+
const flipped = opts.delta.gates.some((g) => g.key === CRON_HALT_GATE && g.regression);
|
|
161
|
+
if (!flipped || opts.assessment.failing.length === 0)
|
|
162
|
+
return undefined;
|
|
163
|
+
const stamp = new Date(opts.now).toISOString();
|
|
164
|
+
const lines = [
|
|
165
|
+
"---",
|
|
166
|
+
"recipe: sweep",
|
|
167
|
+
"trigger: manual",
|
|
168
|
+
`deliveredAt: ${stamp}`,
|
|
169
|
+
"---",
|
|
170
|
+
"",
|
|
171
|
+
"# A scheduled recipe used to work and is now failing",
|
|
172
|
+
"",
|
|
173
|
+
`\`patchwork sweep\` found ${opts.assessment.failing.length} cron recipe(s) that ran \`done\` for at least ${CRON_HALT_MIN_CLEAN_DAYS} days and whose latest ${CRON_HALT_MIN_ERROR_STREAK}+ runs are all \`error\`.`,
|
|
174
|
+
"",
|
|
175
|
+
];
|
|
176
|
+
for (const f of opts.assessment.failing) {
|
|
177
|
+
lines.push(`- **${f.recipeName}** — first failing run ${new Date(f.firstFailureAt).toISOString()}, ${f.errorStreak} consecutive errors after ${Math.floor(f.cleanSpanMs / DAY_MS)} clean days.`);
|
|
178
|
+
}
|
|
179
|
+
lines.push("", "Run `patchwork halts --window 7d --recipe <name>` for the error messages, then `patchwork recipe doctor`.", "");
|
|
180
|
+
mkdirSync(opts.inbox, { recursive: true, mode: 0o700 });
|
|
181
|
+
const file = path.join(opts.inbox, `sweep-cron-halt-${stamp.replace(/[:.]/g, "-")}.md`);
|
|
182
|
+
writeFileSync(file, lines.join("\n"), { mode: 0o600 });
|
|
183
|
+
return file;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=cronHaltGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cronHaltGate.js","sourceRoot":"","sources":["../../src/maintenance/cronHaltGate.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Collect one `SweepReading` from the five read-only verbs.
|
|
2
|
+
* Collect one `SweepReading` from the five read-only verbs plus the run log.
|
|
3
3
|
*
|
|
4
4
|
* Kept apart from `sweep.ts` so the diff stays a pure function a test can drive.
|
|
5
5
|
* This half touches the filesystem and every subsystem; that half decides what a
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* is no version of the snapshot that ever held them. A privacy rule enforced by
|
|
14
14
|
* a downstream formatter is one refactor away from being lost.
|
|
15
15
|
*/
|
|
16
|
+
import { type CronHaltAssessment } from "./cronHaltGate.js";
|
|
16
17
|
import { type SweepReading } from "./sweep.js";
|
|
17
18
|
export interface CollectOptions {
|
|
18
19
|
/** Ledger directory. Defaults to `$PATCHWORK_HOME`. */
|
|
@@ -21,7 +22,18 @@ export interface CollectOptions {
|
|
|
21
22
|
expectRunning?: number;
|
|
22
23
|
/** ms epoch for the reading. Injected so a test is not at the mercy of a clock. */
|
|
23
24
|
now?: number;
|
|
25
|
+
/**
|
|
26
|
+
* A cron-halt assessment already taken (the dispatcher needs the recipe
|
|
27
|
+
* names for the inbox note, which the reading must not carry). Absent ⇒
|
|
28
|
+
* taken here.
|
|
29
|
+
*/
|
|
30
|
+
cronHalts?: CronHaltAssessment;
|
|
24
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Take the cron-halt reading, or undefined when there is no run log to read —
|
|
34
|
+
* an absent log is a reading that could not be taken, not a healthy gate.
|
|
35
|
+
*/
|
|
36
|
+
export declare function collectCronHalts(dir: string, now: number): CronHaltAssessment | undefined;
|
|
25
37
|
/**
|
|
26
38
|
* Readings that could not be taken are OMITTED, never recorded as zero.
|
|
27
39
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Collect one `SweepReading` from the five read-only verbs.
|
|
2
|
+
* Collect one `SweepReading` from the five read-only verbs plus the run log.
|
|
3
3
|
*
|
|
4
4
|
* Kept apart from `sweep.ts` so the diff stays a pure function a test can drive.
|
|
5
5
|
* This half touches the filesystem and every subsystem; that half decides what a
|
|
@@ -19,8 +19,23 @@ import { evidenceCoverage } from "../evidenceCoverage.js";
|
|
|
19
19
|
import { patchworkHome } from "../patchworkHome.js";
|
|
20
20
|
import { scanRecipeDir } from "../privacy/undeclaredSteps.js";
|
|
21
21
|
import { defaultRecipesDir, defaultWorkersDir, validateWorkers, } from "../workers/workersCli.js";
|
|
22
|
+
import { assessCronHalts, CRON_HALT_GATE, cronRunLogPresent, readCronRuns, } from "./cronHaltGate.js";
|
|
22
23
|
import { readObservations, summarise } from "./prOutcomeLedger.js";
|
|
23
24
|
import { SWEEP_RV } from "./sweep.js";
|
|
25
|
+
/**
|
|
26
|
+
* Take the cron-halt reading, or undefined when there is no run log to read —
|
|
27
|
+
* an absent log is a reading that could not be taken, not a healthy gate.
|
|
28
|
+
*/
|
|
29
|
+
export function collectCronHalts(dir, now) {
|
|
30
|
+
if (!cronRunLogPresent(dir))
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
return assessCronHalts(readCronRuns(dir), { now });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
24
39
|
/**
|
|
25
40
|
* Readings that could not be taken are OMITTED, never recorded as zero.
|
|
26
41
|
*
|
|
@@ -70,6 +85,15 @@ export function collectSweep(opts = {}) {
|
|
|
70
85
|
catch {
|
|
71
86
|
// No workers directory is a legitimate install. Omit rather than zero.
|
|
72
87
|
}
|
|
88
|
+
// --- gate 3: did a scheduled recipe that used to work stop working? ---
|
|
89
|
+
// Counts only. The names go to an inbox note written by the dispatcher on
|
|
90
|
+
// the FLIP, never into this row (see cronHaltGate.ts).
|
|
91
|
+
const halts = opts.cronHalts ?? collectCronHalts(dir, opts.now ?? Date.now());
|
|
92
|
+
if (halts !== undefined) {
|
|
93
|
+
gates[CRON_HALT_GATE] = halts.failing.length === 0;
|
|
94
|
+
counts["cronHalts.eligibleRecipes"] = halts.eligible;
|
|
95
|
+
counts["cronHalts.failing"] = halts.failing.length;
|
|
96
|
+
}
|
|
73
97
|
// --- drift: evidence spine ---
|
|
74
98
|
const ev = evidenceCoverage(dir);
|
|
75
99
|
for (const l of ev.ledgers) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sweepCollect.js","sourceRoot":"","sources":["../../src/maintenance/sweepCollect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,yBAAyB,EACzB,aAAa,EACb,oBAAoB,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAqB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"sweepCollect.js","sourceRoot":"","sources":["../../src/maintenance/sweepCollect.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchwork-os",
|
|
3
|
-
"version": "1.2.0
|
|
4
|
-
"description": "Your personal AI runtime, local-first. Patchwork OS gives any AI model a consistent set of tools, YAML recipes, a delegation policy with approval queue, and a durable trace memory
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Your personal AI runtime, local-first. Patchwork OS gives any AI model a consistent set of tools, YAML recipes, a delegation policy with approval queue, and a durable trace memory \u2014 all on your machine, all under your policy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|