crewhaus 0.1.8 → 0.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.
Files changed (136) hide show
  1. package/README.md +10 -3
  2. package/dist/advice-apply.d.ts +182 -0
  3. package/dist/advice-apply.js +286 -0
  4. package/dist/advise-rules.d.ts +348 -0
  5. package/dist/advise-rules.js +905 -0
  6. package/dist/alert-sink.d.ts +48 -0
  7. package/dist/alert-sink.js +86 -0
  8. package/dist/approval-gate.d.ts +127 -0
  9. package/dist/approval-gate.js +254 -0
  10. package/dist/audit-verify.d.ts +69 -0
  11. package/dist/audit-verify.js +97 -0
  12. package/dist/autodistill.d.ts +113 -0
  13. package/dist/autodistill.js +256 -0
  14. package/dist/channel-provision.d.ts +360 -0
  15. package/dist/channel-provision.js +881 -0
  16. package/dist/ci-scaffold.d.ts +31 -0
  17. package/dist/ci-scaffold.js +343 -0
  18. package/dist/compile-check.d.ts +90 -0
  19. package/dist/compile-check.js +285 -0
  20. package/dist/compliance-schedule.d.ts +35 -0
  21. package/dist/compliance-schedule.js +36 -0
  22. package/dist/context-pressure.d.ts +80 -0
  23. package/dist/context-pressure.js +166 -0
  24. package/dist/dataset-mine.d.ts +172 -0
  25. package/dist/dataset-mine.js +403 -0
  26. package/dist/datasets.d.ts +124 -0
  27. package/dist/datasets.js +260 -0
  28. package/dist/deploy-canary.d.ts +83 -0
  29. package/dist/deploy-canary.js +87 -0
  30. package/dist/doctor-checks.d.ts +33 -0
  31. package/dist/doctor-checks.js +92 -0
  32. package/dist/doctor-detect.d.ts +108 -0
  33. package/dist/doctor-detect.js +214 -0
  34. package/dist/doctor-fix.d.ts +81 -0
  35. package/dist/doctor-fix.js +164 -0
  36. package/dist/egress-triage.d.ts +121 -0
  37. package/dist/egress-triage.js +261 -0
  38. package/dist/eval-bridge.d.ts +114 -0
  39. package/dist/eval-bridge.js +158 -0
  40. package/dist/eval-coverage.d.ts +140 -0
  41. package/dist/eval-coverage.js +428 -0
  42. package/dist/eval-history.d.ts +48 -0
  43. package/dist/eval-history.js +157 -0
  44. package/dist/eval-matrix.d.ts +80 -0
  45. package/dist/eval-matrix.js +182 -0
  46. package/dist/eval-sentinel.d.ts +65 -0
  47. package/dist/eval-sentinel.js +132 -0
  48. package/dist/faq.d.ts +68 -0
  49. package/dist/faq.js +168 -0
  50. package/dist/feedback.d.ts +9 -2
  51. package/dist/feedback.js +17 -7
  52. package/dist/fewshot.d.ts +83 -0
  53. package/dist/fewshot.js +158 -0
  54. package/dist/fleet.d.ts +207 -0
  55. package/dist/fleet.js +488 -0
  56. package/dist/flywheel.d.ts +193 -0
  57. package/dist/flywheel.js +519 -0
  58. package/dist/graders-suggest.d.ts +186 -0
  59. package/dist/graders-suggest.js +658 -0
  60. package/dist/incident.d.ts +99 -0
  61. package/dist/incident.js +217 -0
  62. package/dist/index.d.ts +9 -1
  63. package/dist/index.js +11601 -964
  64. package/dist/init-interactive.d.ts +105 -0
  65. package/dist/init-interactive.js +208 -0
  66. package/dist/intents.d.ts +105 -0
  67. package/dist/intents.js +292 -0
  68. package/dist/judge-calibrate.d.ts +137 -0
  69. package/dist/judge-calibrate.js +247 -0
  70. package/dist/justification-calibrate.d.ts +150 -0
  71. package/dist/justification-calibrate.js +262 -0
  72. package/dist/justification-gate.d.ts +27 -6
  73. package/dist/justification-gate.js +30 -6
  74. package/dist/knowledge-sync.d.ts +179 -0
  75. package/dist/knowledge-sync.js +551 -0
  76. package/dist/lessons.d.ts +87 -0
  77. package/dist/lessons.js +207 -0
  78. package/dist/lint.d.ts +127 -0
  79. package/dist/lint.js +226 -0
  80. package/dist/loadtest.d.ts +114 -0
  81. package/dist/loadtest.js +196 -0
  82. package/dist/marketplace-cli.d.ts +110 -0
  83. package/dist/marketplace-cli.js +250 -0
  84. package/dist/mcp-doctor.d.ts +121 -0
  85. package/dist/mcp-doctor.js +249 -0
  86. package/dist/model-scan.d.ts +116 -0
  87. package/dist/model-scan.js +226 -0
  88. package/dist/onchain-tune.d.ts +164 -0
  89. package/dist/onchain-tune.js +346 -0
  90. package/dist/permissions-suggest.d.ts +126 -0
  91. package/dist/permissions-suggest.js +333 -0
  92. package/dist/pii-tune.d.ts +107 -0
  93. package/dist/pii-tune.js +122 -0
  94. package/dist/propose.d.ts +117 -0
  95. package/dist/propose.js +184 -0
  96. package/dist/refresh-goldens.d.ts +82 -0
  97. package/dist/refresh-goldens.js +221 -0
  98. package/dist/regression-pin.d.ts +160 -0
  99. package/dist/regression-pin.js +281 -0
  100. package/dist/retention.d.ts +193 -0
  101. package/dist/retention.js +607 -0
  102. package/dist/retire.d.ts +118 -0
  103. package/dist/retire.js +291 -0
  104. package/dist/right-size.d.ts +100 -0
  105. package/dist/right-size.js +123 -0
  106. package/dist/scaffold-evals.d.ts +138 -0
  107. package/dist/scaffold-evals.js +410 -0
  108. package/dist/scope-audit-drift.d.ts +139 -0
  109. package/dist/scope-audit-drift.js +260 -0
  110. package/dist/security-corpus.d.ts +237 -0
  111. package/dist/security-corpus.js +516 -0
  112. package/dist/security-digest.d.ts +173 -0
  113. package/dist/security-digest.js +650 -0
  114. package/dist/sessions-index.d.ts +27 -0
  115. package/dist/sessions-index.js +51 -0
  116. package/dist/slo-doctor.d.ts +67 -0
  117. package/dist/slo-doctor.js +119 -0
  118. package/dist/slo-sink.d.ts +96 -0
  119. package/dist/slo-sink.js +107 -0
  120. package/dist/spec-changelog.d.ts +102 -0
  121. package/dist/spec-changelog.js +237 -0
  122. package/dist/state-backup.d.ts +223 -0
  123. package/dist/state-backup.js +648 -0
  124. package/dist/tools-cli.d.ts +170 -0
  125. package/dist/tools-cli.js +298 -0
  126. package/dist/triage.d.ts +202 -0
  127. package/dist/triage.js +403 -0
  128. package/dist/upgrade.d.ts +57 -0
  129. package/dist/upgrade.js +113 -0
  130. package/dist/version.d.ts +6 -0
  131. package/dist/version.js +27 -0
  132. package/dist/voice-eval.d.ts +138 -0
  133. package/dist/voice-eval.js +309 -0
  134. package/dist/watch.d.ts +58 -0
  135. package/dist/watch.js +97 -0
  136. package/package.json +89 -65
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Item 46 — auto-register spec versions with changelogs distilled from
3
+ * patch history. Shared by `crewhaus compile` (post-emission), `crewhaus
4
+ * optimize --write-back` (post-write-back) and `crewhaus spec put`:
5
+ *
6
+ * - `autoRegisterSpecVersion` content-hashes the spec and, when the
7
+ * local registry has no version with that content, `put`s the next
8
+ * `vN` — so registry state tracks working files without a manual
9
+ * `crewhaus spec put`. Recompiling an unchanged spec is a no-op.
10
+ * - every auto/manual put appends a distilled entry to a per-spec
11
+ * `CHANGELOG.md` stored BESIDE the registry manifest
12
+ * (`.crewhaus/specs/<name>/CHANGELOG.md` — a NEW file; the registry's
13
+ * own on-disk format is untouched): version, date, the field-level
14
+ * YAML diff vs the previous version, and — when the content carries a
15
+ * `formatWriteBackHeader` stamp — the optimizer's runId/mutator/score
16
+ * plus the SpecPatch `rationale` from that run's `patch.json`.
17
+ *
18
+ * Kept in a side-effect-free module (nothing runs on import) so the logic
19
+ * is unit-testable — the CLI entry file runs an argv switch on import.
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { diffSpecYaml, parseWriteBackHeader, } from "@crewhaus/spec-patch";
25
+ /** The per-spec changelog file, beside the registry's `manifest.json`. */
26
+ export const CHANGELOG_FILE = "CHANGELOG.md";
27
+ // The registry's own name floor (spec-registry NAME_REGEX). Spec names may
28
+ // additionally contain spaces and ':' (the spec `safeName` grammar), so
29
+ // auto-registration maps a display name onto this grammar deterministically.
30
+ const REGISTRY_NAME_REGEX = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/;
31
+ // A write-back runId is read from a COMMENT in the spec being registered;
32
+ // refuse to path-join anything that could escape the optimize root.
33
+ const SAFE_RUN_ID_REGEX = /^[A-Za-z0-9_-]+$/;
34
+ /**
35
+ * Map a spec's display name (spec `safeName`: letters, digits, spaces,
36
+ * `_ . - :`) onto the registry's stricter name grammar: runs of characters
37
+ * outside `[A-Za-z0-9_.-]` collapse to a single `-`, leading dots are
38
+ * stripped (the registry hides dot-dirs), and an empty result falls back to
39
+ * `"spec"`. Deterministic, so the same spec always lands in the same slot.
40
+ */
41
+ export function registrySpecName(specName) {
42
+ const cleaned = specName.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^\.+/, "");
43
+ const name = cleaned.length > 0 ? cleaned : "spec";
44
+ // Defensive: the transform above always satisfies the registry grammar,
45
+ // but verify so a future grammar drift fails loudly here, not deep in fs.
46
+ if (!REGISTRY_NAME_REGEX.test(name))
47
+ return "spec";
48
+ return name;
49
+ }
50
+ /** SHA-256 hex of the exact YAML text (the registry stores bytes verbatim). */
51
+ export function contentHash(yaml) {
52
+ return createHash("sha256").update(yaml).digest("hex");
53
+ }
54
+ /**
55
+ * Next auto-version: `v<max+1>` over the existing `v<N>` versions (manifest
56
+ * order, numeric — `v10` beats `v2`). Manually-put versions that don't match
57
+ * `v<N>` (e.g. `2.0`, `release-1`) are ignored for numbering.
58
+ */
59
+ export function nextVersion(existing) {
60
+ let max = 0;
61
+ for (const v of existing) {
62
+ const m = /^v(\d+)$/.exec(v);
63
+ if (m?.[1] === undefined)
64
+ continue;
65
+ const n = Number.parseInt(m[1], 10);
66
+ if (n > max)
67
+ max = n;
68
+ }
69
+ return `v${max + 1}`;
70
+ }
71
+ /**
72
+ * When the YAML carries a `formatWriteBackHeader` stamp, recover the run
73
+ * metadata and — if that run's `patch.json` is readable — its `rationale`.
74
+ * Returns `undefined` for content that was never written back. Never throws:
75
+ * provenance is best-effort garnish on the changelog, not a gate.
76
+ */
77
+ export function extractOptimizeMetadata(opts) {
78
+ const header = parseWriteBackHeader(opts.yaml);
79
+ if (header === undefined)
80
+ return undefined;
81
+ let patchJsonPath = opts.patchJsonPath;
82
+ if (patchJsonPath === undefined &&
83
+ opts.optimizeRootDir !== undefined &&
84
+ SAFE_RUN_ID_REGEX.test(header.runId)) {
85
+ patchJsonPath = join(opts.optimizeRootDir, header.runId, "patch.json");
86
+ }
87
+ let rationale;
88
+ if (patchJsonPath !== undefined && existsSync(patchJsonPath)) {
89
+ try {
90
+ const parsed = JSON.parse(readFileSync(patchJsonPath, "utf-8"));
91
+ if (typeof parsed.rationale === "string" && parsed.rationale.length > 0) {
92
+ rationale = parsed.rationale;
93
+ }
94
+ }
95
+ catch {
96
+ // Unreadable/malformed patch.json — keep the header fields alone.
97
+ }
98
+ }
99
+ return { ...header, ...(rationale !== undefined ? { rationale } : {}) };
100
+ }
101
+ /**
102
+ * Render one changelog entry (pure): `## <version> — <date>` followed by
103
+ * the field-level diff bullets vs the previous version (or `initial
104
+ * version`) and, when present, the optimizer provenance + rationale.
105
+ */
106
+ export function renderChangelogEntry(opts) {
107
+ const date = (opts.now ?? new Date()).toISOString().slice(0, 10);
108
+ const lines = [`## ${opts.version} — ${date}`, ""];
109
+ if (opts.previousYaml === undefined) {
110
+ lines.push("- initial version");
111
+ }
112
+ else {
113
+ let diff;
114
+ try {
115
+ diff = diffSpecYaml(opts.previousYaml, opts.yaml);
116
+ }
117
+ catch (err) {
118
+ lines.push(`- diff unavailable (${err.message})`);
119
+ }
120
+ if (diff !== undefined) {
121
+ if (diff.length === 0) {
122
+ lines.push("- no structural changes (comments/formatting only)");
123
+ }
124
+ else {
125
+ for (const d of diff)
126
+ lines.push(formatDiffLine(d));
127
+ }
128
+ }
129
+ }
130
+ const meta = extractOptimizeMetadata(opts);
131
+ if (meta !== undefined) {
132
+ const details = [];
133
+ if (meta.mutator !== undefined)
134
+ details.push(`mutator ${meta.mutator}`);
135
+ if (meta.iterations !== undefined)
136
+ details.push(`${meta.iterations} iteration(s)`);
137
+ if (meta.scoreBefore !== undefined && meta.scoreAfter !== undefined) {
138
+ details.push(`score ${meta.scoreBefore.toFixed(3)} → ${meta.scoreAfter.toFixed(3)}`);
139
+ }
140
+ const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
141
+ lines.push(`- optimizer: runId ${meta.runId}${suffix}`);
142
+ if (meta.rationale !== undefined)
143
+ lines.push(`- rationale: ${meta.rationale}`);
144
+ }
145
+ lines.push("");
146
+ return lines.join("\n");
147
+ }
148
+ function formatDiffLine(d) {
149
+ switch (d.kind) {
150
+ case "added":
151
+ return `- added \`${d.path}\`: ${d.after ?? ""}`;
152
+ case "removed":
153
+ return `- removed \`${d.path}\`: ${d.before ?? ""}`;
154
+ case "changed":
155
+ return `- changed \`${d.path}\`: ${d.before ?? ""} → ${d.after ?? ""}`;
156
+ }
157
+ }
158
+ /**
159
+ * Insert an entry into a changelog text, NEWEST FIRST (pure): the new entry
160
+ * lands above the previous top `## ` heading, under the `# Changelog — …`
161
+ * title. `crewhaus spec log` can therefore print the file verbatim and read
162
+ * most-recent-first.
163
+ */
164
+ export function insertNewestFirst(existing, name, entry) {
165
+ if (existing.trim() === "") {
166
+ return `# Changelog — ${name}\n\n${entry}`;
167
+ }
168
+ const firstHeading = /^## /m.exec(existing);
169
+ if (firstHeading === null) {
170
+ // A title-only (or hand-edited) file: append below whatever is there.
171
+ return `${existing.trimEnd()}\n\n${entry}`;
172
+ }
173
+ return `${existing.slice(0, firstHeading.index)}${entry}\n${existing.slice(firstHeading.index)}`;
174
+ }
175
+ /**
176
+ * Append (newest-first) one entry to `<registryRoot>/<name>/CHANGELOG.md`,
177
+ * creating the file with its title on first use. Returns the file path.
178
+ */
179
+ export function appendChangelogEntry(opts) {
180
+ const dir = join(opts.registryRootDir, opts.name);
181
+ mkdirSync(dir, { recursive: true });
182
+ const path = join(dir, CHANGELOG_FILE);
183
+ const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
184
+ const entry = renderChangelogEntry(opts);
185
+ writeFileSync(path, insertNewestFirst(existing, opts.name, entry), { mode: 0o600 });
186
+ return path;
187
+ }
188
+ /**
189
+ * Content-hash gated auto-put: when some stored version already carries this
190
+ * exact content, report `unchanged` (with that version); otherwise `put` the
191
+ * next `vN` and append the distilled changelog entry. The previous-latest
192
+ * version (manifest order) is the diff baseline.
193
+ */
194
+ export async function autoRegisterSpecVersion(opts) {
195
+ const name = registrySpecName(opts.specName);
196
+ const hash = contentHash(opts.yaml);
197
+ const manifest = await opts.registry.manifest(name);
198
+ // Scan ALL stored versions for this content — re-registering any past
199
+ // version (e.g. a rollback restored on disk) is also a no-op. Later
200
+ // matches win so the reported version is the most recent occurrence.
201
+ let matched;
202
+ for (const v of manifest.versions) {
203
+ try {
204
+ if (contentHash(await opts.registry.get(name, v)) === hash)
205
+ matched = v;
206
+ }
207
+ catch {
208
+ // A manifest-listed version whose file vanished: ignore for matching.
209
+ }
210
+ }
211
+ if (matched !== undefined) {
212
+ return { status: "unchanged", name, version: matched };
213
+ }
214
+ const previousVersion = manifest.versions[manifest.versions.length - 1];
215
+ let previousYaml;
216
+ if (previousVersion !== undefined) {
217
+ try {
218
+ previousYaml = await opts.registry.get(name, previousVersion);
219
+ }
220
+ catch {
221
+ // Baseline unavailable — the entry renders as diff-less.
222
+ }
223
+ }
224
+ const version = nextVersion(manifest.versions);
225
+ await opts.registry.put(name, version, opts.yaml);
226
+ appendChangelogEntry({
227
+ registryRootDir: opts.registryRootDir,
228
+ name,
229
+ version,
230
+ yaml: opts.yaml,
231
+ ...(previousYaml !== undefined ? { previousYaml } : {}),
232
+ ...(opts.optimizeRootDir !== undefined ? { optimizeRootDir: opts.optimizeRootDir } : {}),
233
+ ...(opts.patchJsonPath !== undefined ? { patchJsonPath: opts.patchJsonPath } : {}),
234
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
235
+ });
236
+ return { status: "registered", name, version };
237
+ }
@@ -0,0 +1,223 @@
1
+ import { type FeedbackRecord } from "./feedback";
2
+ /** Manifest file name at the archive root (NOT restored into `.crewhaus`). */
3
+ export declare const MANIFEST_FILENAME = "backup-manifest.json";
4
+ /** Thrown for every operational failure (missing dir, tar failure, bad
5
+ * archive). The CLI entry file catches it and routes the message through
6
+ * `die()`; tests assert on `.message` without the process exiting. The
7
+ * `name` stays a plain string so `StateRestoreRefusedError` can narrow it. */
8
+ export declare class StateBackupError extends Error {
9
+ readonly name: string;
10
+ }
11
+ /** Thrown when `restore` (without --force/--merge) would overwrite an
12
+ * existing non-empty `.crewhaus`. Carries a preformatted message listing
13
+ * what is there and how to proceed. */
14
+ export declare class StateRestoreRefusedError extends StateBackupError {
15
+ readonly stateDir: string;
16
+ readonly name = "StateRestoreRefusedError";
17
+ constructor(stateDir: string, entries: ReadonlyArray<{
18
+ name: string;
19
+ kind: "dir" | "file";
20
+ files: number;
21
+ }>);
22
+ }
23
+ export type SubdirStat = {
24
+ files: number;
25
+ bytes: number;
26
+ };
27
+ export type BackupManifest = {
28
+ schemaVersion: 1;
29
+ /** ISO 8601 creation timestamp. */
30
+ createdAt: string;
31
+ /** Absolute path of the `.crewhaus` dir that was archived. */
32
+ sourceDir: string;
33
+ /** CLI version that produced the backup. */
34
+ crewhausVersion: string;
35
+ totals: SubdirStat;
36
+ /** Per-top-level-subdir file counts + byte sizes; "." = state-dir root files. */
37
+ subdirs: Record<string, SubdirStat>;
38
+ /** false when any sqlite file could not be snapshotted consistently and was
39
+ * byte-copied raw instead (a live writer may have torn the copy). */
40
+ sqliteConsistent: boolean;
41
+ sqlite: {
42
+ /** rel paths archived as a consistent read-only `serialize()` snapshot. */
43
+ snapshotted: string[];
44
+ /** rel paths byte-copied raw because the snapshot failed. */
45
+ copiedRaw: string[];
46
+ /** `-wal`/`-shm`/`-journal` sidecars omitted because their main db was
47
+ * snapshotted (the snapshot folds WAL content in). */
48
+ foldedSidecars: string[];
49
+ };
50
+ };
51
+ /** One file as it lands in the archive (post-snapshot sizes). */
52
+ export type ArchivedFile = {
53
+ relPath: string;
54
+ bytes: number;
55
+ };
56
+ /** Pure manifest assembly from the archived file list + backup metadata. */
57
+ export declare function buildManifest(files: ReadonlyArray<ArchivedFile>, meta: {
58
+ createdAt: string;
59
+ sourceDir: string;
60
+ crewhausVersion: string;
61
+ sqliteConsistent: boolean;
62
+ sqlite: BackupManifest["sqlite"];
63
+ }): BackupManifest;
64
+ /** Narrow a parsed JSON value to a BackupManifest (tolerant structural check —
65
+ * a hand-made or future-versioned manifest should degrade to a warning, not
66
+ * a crash). */
67
+ export declare function isBackupManifest(value: unknown): value is BackupManifest;
68
+ /** Collapse a harness name / dir basename to a filename-safe label. */
69
+ export declare function sanitizeBackupLabel(label: string): string;
70
+ /** Default tarball name: `crewhaus-state-<label>-<ISO date>.tar.gz`. */
71
+ export declare function defaultBackupFileName(label: string, now: Date): string;
72
+ /** Split a comma-separated `--exclude` value into trimmed non-empty globs. */
73
+ export declare function parseExcludeGlobs(raw: string | undefined): string[];
74
+ /**
75
+ * Whether a state-dir-relative path (always "/"-separated) matches any
76
+ * exclude glob. A glob is matched against the full relative path; a bare
77
+ * pattern (no "/") additionally matches any single path segment, mirroring
78
+ * tar's basename `--exclude` ergonomics — so `sessions` drops the whole
79
+ * subdir and `*.sqlite` drops sqlite files anywhere in the tree.
80
+ */
81
+ export declare function isExcluded(relPath: string, globs: ReadonlyArray<string>): boolean;
82
+ /** Whether `bytes` starts with the 16-byte SQLite main-db magic header. */
83
+ export declare function isSqliteHeader(bytes: Uint8Array): boolean;
84
+ /** For a `-wal`/`-shm`/`-journal` sidecar path, the main db path it belongs
85
+ * to; undefined for everything else. */
86
+ export declare function sqliteSidecarBase(relPath: string): string | undefined;
87
+ export type AdditiveMergePlan = {
88
+ /** Archive files absent locally — safe to copy in. */
89
+ copy: string[];
90
+ /** Archive files that already exist locally — skipped, never overwritten. */
91
+ skip: string[];
92
+ };
93
+ /** Plan a `--merge all` additive copy: only files that don't exist locally
94
+ * are copied; existing local files always win. Pure via the injected
95
+ * existence predicate. */
96
+ export declare function planAdditiveMerge(archiveFiles: ReadonlyArray<string>, existsLocally: (relPath: string) => boolean): AdditiveMergePlan;
97
+ export type FeedbackMergePlan = {
98
+ /** Folded records to append to the local feedback store. */
99
+ toImport: FeedbackRecord[];
100
+ /** Archived (session, turn) keys with no local record at all. */
101
+ added: number;
102
+ /** Keys where folding the archive in changed the local fold. */
103
+ updated: number;
104
+ /** Archived keys already fully represented locally — deduped away. */
105
+ unchanged: number;
106
+ };
107
+ /**
108
+ * Plan a `--merge feedback` fold using the exact dedupe semantics `crewhaus
109
+ * distill` applies: `mergeFeedback` collapses records to one per
110
+ * (sessionId, turnNumber), newest field wins, and a later comment-only
111
+ * record never erases an earlier rating. Local and archived records are
112
+ * each folded first, then each archived fold is folded AGAINST the local
113
+ * fold for its key — imported only when the archive actually contributes
114
+ * something (new key, or a fold that differs from the local one). Running
115
+ * the same merge twice is therefore a no-op.
116
+ */
117
+ export declare function planFeedbackMerge(local: ReadonlyArray<FeedbackRecord>, archived: ReadonlyArray<FeedbackRecord>): FeedbackMergePlan;
118
+ export type BackupOptions = {
119
+ /** The `.crewhaus` directory to archive. */
120
+ stateDir: string;
121
+ /** Absolute output tarball path. */
122
+ outFile: string;
123
+ excludeGlobs?: ReadonlyArray<string>;
124
+ /** Stamped into the manifest. */
125
+ crewhausVersion: string;
126
+ /** Test seam: clock. */
127
+ now?: () => Date;
128
+ };
129
+ export type BackupResult = {
130
+ manifest: BackupManifest;
131
+ outFile: string;
132
+ /** Files dropped by --exclude. */
133
+ excluded: string[];
134
+ /** Non-fatal warnings (skipped symlinks, raw-copied sqlite files). */
135
+ warnings: string[];
136
+ };
137
+ /**
138
+ * Archive `stateDir` to a gzipped tarball. Every regular file is staged as
139
+ * an exact byte copy (sqlite files as a consistent read-only snapshot — see
140
+ * the file header), a manifest is written at the archive root, and the
141
+ * system `tar` packs the staging dir. Source files are never modified.
142
+ */
143
+ export declare function createStateBackup(opts: BackupOptions): Promise<BackupResult>;
144
+ export type StateDirEntry = {
145
+ name: string;
146
+ kind: "dir" | "file";
147
+ files: number;
148
+ };
149
+ export type StateDirInspection = {
150
+ state: "absent";
151
+ } | {
152
+ state: "empty";
153
+ } | {
154
+ state: "occupied";
155
+ entries: StateDirEntry[];
156
+ };
157
+ /** What (if anything) currently occupies the target `.crewhaus` dir. */
158
+ export declare function inspectStateDir(stateDir: string): StateDirInspection;
159
+ export type RestoreOptions = {
160
+ archiveFile: string;
161
+ /** Directory that receives the `.crewhaus` dir (default: cwd — resolved by
162
+ * the caller). */
163
+ intoDir: string;
164
+ /** Full replace: move an existing non-empty `.crewhaus` aside to
165
+ * `.crewhaus.bak-<ts>` (never deleted) before restoring. */
166
+ force: boolean;
167
+ };
168
+ export type RestoreResult = {
169
+ stateDir: string;
170
+ /** Where the pre-existing state dir went (only set when one was moved). */
171
+ movedAsideTo?: string;
172
+ manifest?: BackupManifest;
173
+ filesRestored: number;
174
+ warnings: string[];
175
+ };
176
+ /**
177
+ * Full restore of a backup into `<intoDir>/.crewhaus`. Refuses (throws
178
+ * `StateRestoreRefusedError`) when the target is non-empty and `force` is
179
+ * not set. Extraction happens in a sibling temp dir which is renamed into
180
+ * place, so a half-extracted archive can never corrupt existing state, and
181
+ * restored file bytes are exactly the archived bytes (tar preserves them;
182
+ * nothing is rewritten).
183
+ */
184
+ export declare function restoreStateArchive(opts: RestoreOptions): Promise<RestoreResult>;
185
+ export type MergeFeedbackOptions = {
186
+ archiveFile: string;
187
+ intoDir: string;
188
+ /** Test seam: clock (names the imported jsonl file). */
189
+ now?: () => Date;
190
+ };
191
+ export type MergeFeedbackResult = FeedbackMergePlan & {
192
+ /** Distinct feedback records found in the archive (pre-fold). */
193
+ archivedRecords: number;
194
+ /** The `feedback/*.jsonl` file the imports were appended to (unset when
195
+ * the archive contributed nothing new). */
196
+ wroteFile?: string;
197
+ };
198
+ /**
199
+ * `--merge feedback`: fold ONLY the archive's feedback records into the
200
+ * local store. Imported folds land as bare records in a new
201
+ * `feedback/restored-<ts>.jsonl` — the exact sink `crewhaus distill`
202
+ * already reads — so local session transcripts are never rewritten and the
203
+ * next distill sees local + imported records merged by `mergeFeedback`.
204
+ */
205
+ export declare function mergeFeedbackFromArchive(opts: MergeFeedbackOptions): Promise<MergeFeedbackResult>;
206
+ export type MergeAllOptions = {
207
+ archiveFile: string;
208
+ intoDir: string;
209
+ };
210
+ export type MergeAllResult = {
211
+ stateDir: string;
212
+ /** Archive files copied in (absent locally). */
213
+ copied: string[];
214
+ /** Archive files skipped (already exist locally — local always wins). */
215
+ skipped: string[];
216
+ };
217
+ /**
218
+ * `--merge all`: per-subdir additive copy. Only archive files that do NOT
219
+ * exist locally are written; an existing local file is never overwritten
220
+ * (the skips are reported). The archive manifest is metadata, not state,
221
+ * and is never copied.
222
+ */
223
+ export declare function mergeAllFromArchive(opts: MergeAllOptions): Promise<MergeAllResult>;