wicked-crew 0.7.28 → 0.7.29

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 (96) hide show
  1. package/README.md +9 -1
  2. package/dist/api/chat-scope.d.ts +210 -0
  3. package/dist/api/chat-scope.d.ts.map +1 -0
  4. package/dist/api/chat-scope.js +724 -0
  5. package/dist/api/chat-scope.js.map +1 -0
  6. package/dist/api/diagnostics.d.ts +30 -2
  7. package/dist/api/diagnostics.d.ts.map +1 -1
  8. package/dist/api/diagnostics.js +73 -5
  9. package/dist/api/diagnostics.js.map +1 -1
  10. package/dist/api/governance-health.d.ts +163 -0
  11. package/dist/api/governance-health.d.ts.map +1 -0
  12. package/dist/api/governance-health.js +348 -0
  13. package/dist/api/governance-health.js.map +1 -0
  14. package/dist/api/open-path.d.ts.map +1 -1
  15. package/dist/api/open-path.js +2 -1
  16. package/dist/api/open-path.js.map +1 -1
  17. package/dist/api/post-hoc-deliver.d.ts +9 -5
  18. package/dist/api/post-hoc-deliver.d.ts.map +1 -1
  19. package/dist/api/post-hoc-deliver.js +21 -7
  20. package/dist/api/post-hoc-deliver.js.map +1 -1
  21. package/dist/api/routes.d.ts +48 -12
  22. package/dist/api/routes.d.ts.map +1 -1
  23. package/dist/api/routes.js +309 -48
  24. package/dist/api/routes.js.map +1 -1
  25. package/dist/api/seat-health.d.ts.map +1 -1
  26. package/dist/api/seat-health.js +25 -5
  27. package/dist/api/seat-health.js.map +1 -1
  28. package/dist/api/seat-signin.d.ts.map +1 -1
  29. package/dist/api/seat-signin.js +49 -23
  30. package/dist/api/seat-signin.js.map +1 -1
  31. package/dist/api/server.d.ts.map +1 -1
  32. package/dist/api/server.js +20 -0
  33. package/dist/api/server.js.map +1 -1
  34. package/dist/campaigns/routes.d.ts +4 -4
  35. package/dist/cli/governance.d.ts +131 -0
  36. package/dist/cli/governance.d.ts.map +1 -0
  37. package/dist/cli/governance.js +354 -0
  38. package/dist/cli/governance.js.map +1 -0
  39. package/dist/cli/index.js +69 -2
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/core/adapter.d.ts +89 -1
  42. package/dist/core/adapter.d.ts.map +1 -1
  43. package/dist/core/adapter.js +123 -7
  44. package/dist/core/adapter.js.map +1 -1
  45. package/dist/core/bridge-reaper.d.ts.map +1 -1
  46. package/dist/core/bridge-reaper.js +4 -1
  47. package/dist/core/bridge-reaper.js.map +1 -1
  48. package/dist/core/deliver-text.d.ts +184 -0
  49. package/dist/core/deliver-text.d.ts.map +1 -0
  50. package/dist/core/deliver-text.js +434 -0
  51. package/dist/core/deliver-text.js.map +1 -0
  52. package/dist/core/deliver-triage.d.ts +90 -0
  53. package/dist/core/deliver-triage.d.ts.map +1 -0
  54. package/dist/core/deliver-triage.js +106 -0
  55. package/dist/core/deliver-triage.js.map +1 -0
  56. package/dist/core/deliver.d.ts +103 -9
  57. package/dist/core/deliver.d.ts.map +1 -1
  58. package/dist/core/deliver.js +244 -46
  59. package/dist/core/deliver.js.map +1 -1
  60. package/dist/core/estate-mcp-client.d.ts.map +1 -1
  61. package/dist/core/estate-mcp-client.js +7 -2
  62. package/dist/core/estate-mcp-client.js.map +1 -1
  63. package/dist/core/exec.d.ts.map +1 -1
  64. package/dist/core/exec.js +11 -1
  65. package/dist/core/exec.js.map +1 -1
  66. package/dist/core/governance-store.d.ts +197 -0
  67. package/dist/core/governance-store.d.ts.map +1 -0
  68. package/dist/core/governance-store.js +283 -0
  69. package/dist/core/governance-store.js.map +1 -0
  70. package/dist/core/repoPaths.d.ts +36 -5
  71. package/dist/core/repoPaths.d.ts.map +1 -1
  72. package/dist/core/repoPaths.js +46 -4
  73. package/dist/core/repoPaths.js.map +1 -1
  74. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  75. package/dist/interactive/bridge-pool.js +9 -4
  76. package/dist/interactive/bridge-pool.js.map +1 -1
  77. package/dist/projects/graph-paths.d.ts +4 -2
  78. package/dist/projects/graph-paths.d.ts.map +1 -1
  79. package/dist/projects/graph-paths.js +4 -2
  80. package/dist/projects/graph-paths.js.map +1 -1
  81. package/dist/projects/graph.d.ts +5 -4
  82. package/dist/projects/graph.d.ts.map +1 -1
  83. package/dist/projects/graph.js +22 -5
  84. package/dist/projects/graph.js.map +1 -1
  85. package/dist/projects/routes.d.ts.map +1 -1
  86. package/dist/projects/routes.js +18 -9
  87. package/dist/projects/routes.js.map +1 -1
  88. package/dist/skills/plugin-source.d.ts.map +1 -1
  89. package/dist/skills/plugin-source.js +3 -0
  90. package/dist/skills/plugin-source.js.map +1 -1
  91. package/dist/studio/assets/index-CeJTutxP.js +540 -0
  92. package/dist/studio/index.html +1 -1
  93. package/dist/studio/testid-inventory.json +212 -6
  94. package/endpoint-manifest.json +60 -12
  95. package/package.json +3 -3
  96. package/dist/studio/assets/index-CdmCMh2f.js +0 -539
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `wicked-crew governance replay <outbox> [--governance-db <path> | --db <core db>] [--dry-run]`
3
+ * (crew#495, F-022): drain a dead-letter outbox back into the governance store.
4
+ *
5
+ * The engine spools every `wicked.*` event it could not store as one NDJSON line (`emit.rs`
6
+ * `spool_record`: the envelope plus `deadletter_reason`, and — on engines carrying crew#495's
7
+ * companion — `ts`, `pid`, `origin`). This command hands that file to the engine, which writes
8
+ * each record as the EVENT node it should have been, restoring the original `ts` where the record
9
+ * carries one so an id-ordered scan of the store is still chronological.
10
+ *
11
+ * Drain order matters, because the daemon may be appending to the SAME file while this runs:
12
+ * 1. RENAME the outbox to `<outbox>.replayed-<ISO>` first — atomic, so the daemon's next spool
13
+ * recreates a fresh file and no entry is lost between the read and the archive;
14
+ * 2. replay from the archived file;
15
+ * 3. APPEND the entries that failed to land back onto the live outbox — they stay dead letters,
16
+ * visible on `/diagnostics`, and the archive keeps the full record of what was attempted.
17
+ * `--dry-run` folds the outbox in place (count, types, reasons, timestamp range) and moves nothing.
18
+ *
19
+ * Target store resolution is `serve`'s (`core/governance-store.ts`): `--governance-db` ›
20
+ * `WICKED_CREW_GOVERNANCE_DB` › an inherited `WICKED_ESTATE_DB` › `<core db>.governance/
21
+ * governance.db`, with `--db` naming the core db (default: the state home's `core.db`).
22
+ *
23
+ * Exit codes: 0 every entry landed (or dry run); 1 some entries failed (they are back on the
24
+ * outbox); 2 the installed engine cannot replay (`Core.replayEmitOutbox` absent — upgrade
25
+ * wicked-core-ts), or the arguments are wrong.
26
+ */
27
+ import { Transform, type TransformCallback } from 'node:stream';
28
+ import { type GovernanceStoreLocation } from '../core/governance-store.js';
29
+ import { foldDeadletters, type DeadletterFold } from '../api/governance-health.js';
30
+ export declare const GOVERNANCE_USAGE: string;
31
+ /** crew's own package version, for the origin stamp the engine copies onto dead-letter records. */
32
+ export declare function crewPackageVersion(): string;
33
+ export declare class UsageError extends Error {
34
+ constructor(message: string);
35
+ }
36
+ /** The non-flag arguments, in order (`--dry-run` takes no value; `--db` / `--governance-db` do). */
37
+ export declare function positionalArgs(args: string[]): string[];
38
+ /** Resolve the target store for a replay — `serve`'s rule, over the core db named (or defaulted),
39
+ * with the cross-product bus resolved the way `serve` resolves it (`WICKED_BUS_DB` ›
40
+ * `WICKED_BUS_DATA_DIR` › `<core db>.bus/bus.db`) so the bus db is refused as a target here too. */
41
+ export declare function replayTarget(args: string[], env?: NodeJS.ProcessEnv): GovernanceStoreLocation;
42
+ /** The archive name a drained outbox is renamed to: timestamp + pid + a random nonce, so two replays
43
+ * of the same outbox in the same millisecond (or two processes) can never pick one name and have a
44
+ * later rename replace the earlier archive — the full recovery/audit copy. */
45
+ export declare function archiveNameFor(outbox: string, now?: Date, nonce?: string): string;
46
+ /**
47
+ * Put the lines that did not land back onto the live outbox — and if THAT write fails (permissions,
48
+ * a full disk), say exactly where they are: the archive is retained (nothing is lost on disk), but
49
+ * `/diagnostics` folds only the live path, so without this the operator would see fewer dead letters
50
+ * than exist.
51
+ */
52
+ export declare function appendFailedLines(outbox: string, archive: string, lines: string[]): void;
53
+ /**
54
+ * The caveat a replay that found entries `already_present` must state when the outbox holds
55
+ * UNTIMESTAMPED (pre-stamp) entries: replay ids are content-addressed, and with no stamp to tell
56
+ * them apart, byte-identical unstamped lines share one id — two dead letters of the same event land
57
+ * once. `null` when there is nothing to say.
58
+ */
59
+ export declare function conflationNote(alreadyPresent: number, fold: Pick<DeadletterFold, 'untimestamped'>): string | null;
60
+ /**
61
+ * Keep every NDJSON record whole while streaming one file onto another that a concurrent writer may
62
+ * also be appending to. Every chunk this transform emits is a whole number of records — it buffers
63
+ * up to the last `\n` it has seen and holds the partial tail until more arrives — so an `O_APPEND`
64
+ * write from the other writer landing between two of our writes can only ever sit BETWEEN records,
65
+ * never inside one (read-stream chunks are 64 KiB and not line-aligned; forwarding them raw would
66
+ * let a fresh record be spliced into the middle of an archived one). Two repairs ride INSIDE the
67
+ * data writes, never as separate writes a concurrent spool could slip in front of: a leading `\n`
68
+ * always opens the first emitted chunk (the live file may gain a torn tail between any look and
69
+ * our first write — a check-then-write cannot close that, an unconditional separator does; an
70
+ * empty NDJSON line is skipped by every reader), and a source whose last record lacks its newline
71
+ * gets one appended to that same final write.
72
+ */
73
+ export declare class LineBoundaryGuard extends Transform {
74
+ private readonly leading;
75
+ private carry;
76
+ private first;
77
+ constructor(leading: boolean);
78
+ /** Push one chunk of whole records, opening the very first with the leading separator. */
79
+ private pushRecords;
80
+ _transform(chunk: Buffer | string, _enc: BufferEncoding, cb: TransformCallback): void;
81
+ _flush(cb: TransformCallback): void;
82
+ }
83
+ /**
84
+ * Append NDJSON lines to a live outbox a daemon may be writing — ONE `write()`, ALWAYS behind a
85
+ * `\n`: the file may be absent, at a boundary, or mid-record at the instant of the write, and no
86
+ * look beforehand can know which (the engine writes a record and its newline as two syscalls), so
87
+ * an unconditional separator is the only race-free choice. At worst it is an empty line, which
88
+ * every reader — the fold, the engine's replay — skips.
89
+ */
90
+ export declare function appendLines(outbox: string, lines: string[]): void;
91
+ /**
92
+ * Put an archived outbox back after a replay that threw — APPEND-ONLY, never a rename over the
93
+ * live path: a daemon can create a fresh outbox at any instant, and an `exists`-then-`rename` would
94
+ * replace it and lose what it had just spooled. Appending (`O_APPEND`, so a concurrent spool is
95
+ * never clobbered — the file is created if absent) is loss-free in every interleaving; the cost is
96
+ * the two batches' relative order when the daemon did spool meanwhile (each entry carries its own
97
+ * `ts` on a stamping engine). Streamed, never the whole archive in memory, through
98
+ * {@link LineBoundaryGuard}: whole records per write, the live tail separated first, a torn last
99
+ * record terminated in its own write. The archive is removed only after its last byte is on the
100
+ * live outbox.
101
+ */
102
+ export declare function restoreOutbox(outbox: string, archive: string): Promise<void>;
103
+ export interface ReplayOutcome {
104
+ outbox: string;
105
+ store: {
106
+ path: string;
107
+ source: string;
108
+ };
109
+ archive: string | null;
110
+ read: number;
111
+ replayed: number;
112
+ /** Entries an earlier replay had already landed (a re-replay is a no-op); `null` on an engine that does not report it. */
113
+ alreadyPresent: number | null;
114
+ failed: number;
115
+ dryRun: boolean;
116
+ /** The conflation caveat (see `conflationNote`), or `null`. */
117
+ note: string | null;
118
+ fold?: Awaited<ReturnType<typeof foldDeadletters>>;
119
+ }
120
+ /**
121
+ * The command body, separated from process I/O so a test can drive it. Returns the outcome and the
122
+ * exit code; throws {@link UsageError} for bad arguments and {@link GovernanceReplayUnsupportedError}
123
+ * on an addon without the binding (the CLI maps both to exit 2).
124
+ */
125
+ export declare function replayOutbox(args: string[], env?: NodeJS.ProcessEnv): Promise<{
126
+ outcome: ReplayOutcome;
127
+ exitCode: 0 | 1;
128
+ }>;
129
+ /** The `wicked-crew governance …` entry: prints a JSON outcome and exits with the documented code. */
130
+ export declare function runGovernance(argv: string[]): Promise<void>;
131
+ //# sourceMappingURL=governance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governance.d.ts","sourceRoot":"","sources":["../../src/cli/governance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,354 @@
1
+ /**
2
+ * `wicked-crew governance replay <outbox> [--governance-db <path> | --db <core db>] [--dry-run]`
3
+ * (crew#495, F-022): drain a dead-letter outbox back into the governance store.
4
+ *
5
+ * The engine spools every `wicked.*` event it could not store as one NDJSON line (`emit.rs`
6
+ * `spool_record`: the envelope plus `deadletter_reason`, and — on engines carrying crew#495's
7
+ * companion — `ts`, `pid`, `origin`). This command hands that file to the engine, which writes
8
+ * each record as the EVENT node it should have been, restoring the original `ts` where the record
9
+ * carries one so an id-ordered scan of the store is still chronological.
10
+ *
11
+ * Drain order matters, because the daemon may be appending to the SAME file while this runs:
12
+ * 1. RENAME the outbox to `<outbox>.replayed-<ISO>` first — atomic, so the daemon's next spool
13
+ * recreates a fresh file and no entry is lost between the read and the archive;
14
+ * 2. replay from the archived file;
15
+ * 3. APPEND the entries that failed to land back onto the live outbox — they stay dead letters,
16
+ * visible on `/diagnostics`, and the archive keeps the full record of what was attempted.
17
+ * `--dry-run` folds the outbox in place (count, types, reasons, timestamp range) and moves nothing.
18
+ *
19
+ * Target store resolution is `serve`'s (`core/governance-store.ts`): `--governance-db` ›
20
+ * `WICKED_CREW_GOVERNANCE_DB` › an inherited `WICKED_ESTATE_DB` › `<core db>.governance/
21
+ * governance.db`, with `--db` naming the core db (default: the state home's `core.db`).
22
+ *
23
+ * Exit codes: 0 every entry landed (or dry run); 1 some entries failed (they are back on the
24
+ * outbox); 2 the installed engine cannot replay (`Core.replayEmitOutbox` absent — upgrade
25
+ * wicked-core-ts), or the arguments are wrong.
26
+ */
27
+ import { appendFileSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, } from 'node:fs';
28
+ import { dirname, join, resolve } from 'node:path';
29
+ import { Transform } from 'node:stream';
30
+ import { pipeline } from 'node:stream/promises';
31
+ import { fileURLToPath } from 'node:url';
32
+ import { randomBytes } from 'node:crypto';
33
+ import { CoreAdapter, GovernanceReplayUnsupportedError } from '../core/adapter.js';
34
+ import { CrewBusError, resolveCrewBus } from '../interactive/bus-location.js';
35
+ import { EMIT_DEADLETTER_ENGINE_ENV, ESTATE_DB_ENGINE_ENV, GOVERNANCE_DB_ENV, GOVERNANCE_DB_FLAG, GovernanceStoreError, isStoreSpec, resolveGovernanceStore, } from '../core/governance-store.js';
36
+ import { foldDeadletters } from '../api/governance-health.js';
37
+ import { crewStateHome } from '../projects/state-home.js';
38
+ export const GOVERNANCE_USAGE = 'Usage: wicked-crew governance replay <outbox.ndjson> [--governance-db <path> | --db <core db>] [--dry-run]\n' +
39
+ '\n' +
40
+ 'Replay a dead-letter outbox (the engine\'s NDJSON spool of governance events it could not store)\n' +
41
+ 'into the governance store. Resolution of the store matches `serve`:\n' +
42
+ ` ${GOVERNANCE_DB_FLAG} <path> explicit store (env: ${GOVERNANCE_DB_ENV}; an inherited ${ESTATE_DB_ENGINE_ENV} is honoured next)\n` +
43
+ ' --db <path> the core db whose sidecar store is the default target (<core db>.governance/governance.db;\n' +
44
+ ' default core db: ~/.wicked-crew/core.db)\n' +
45
+ ' --dry-run fold the outbox (count, types, reasons, timestamp range) and move nothing\n' +
46
+ '\n' +
47
+ 'The outbox is archived to <outbox>.replayed-<timestamp> BEFORE replay (so a running daemon\'s next\n' +
48
+ 'spool starts a fresh file); entries that fail to land are appended back onto the outbox.\n' +
49
+ `A daemon's own outbox is <core db>.governance/emit-outbox.ndjson (env: ${EMIT_DEADLETTER_ENGINE_ENV});\n` +
50
+ 'a pre-fix engine wrote ~/.something-wicked/wicked-apps/emit-outbox.ndjson.';
51
+ /** crew's own package version, for the origin stamp the engine copies onto dead-letter records. */
52
+ export function crewPackageVersion() {
53
+ try {
54
+ const raw = readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8');
55
+ return JSON.parse(raw).version ?? '0.0.0';
56
+ }
57
+ catch {
58
+ return '0.0.0';
59
+ }
60
+ }
61
+ function flag(args, name) {
62
+ const i = args.indexOf(name);
63
+ return i >= 0 ? args[i + 1] : undefined;
64
+ }
65
+ /** A flag that was given must carry a value, and the value must not be another flag. */
66
+ function flagValue(args, name) {
67
+ if (!args.includes(name))
68
+ return undefined;
69
+ const value = flag(args, name);
70
+ if (value === undefined || value.startsWith('-')) {
71
+ throw new UsageError(`${name} requires a value (got: ${value ?? '(missing)'})`);
72
+ }
73
+ return value;
74
+ }
75
+ export class UsageError extends Error {
76
+ constructor(message) {
77
+ super(message);
78
+ this.name = 'UsageError';
79
+ }
80
+ }
81
+ /** The flags that take a value — their value is never a positional. */
82
+ const VALUE_FLAGS = new Set(['--db', GOVERNANCE_DB_FLAG]);
83
+ /** The non-flag arguments, in order (`--dry-run` takes no value; `--db` / `--governance-db` do). */
84
+ export function positionalArgs(args) {
85
+ const out = [];
86
+ for (let i = 0; i < args.length; i += 1) {
87
+ const a = args[i];
88
+ if (VALUE_FLAGS.has(a)) {
89
+ i += 1; // skip the flag's value
90
+ continue;
91
+ }
92
+ if (a.startsWith('-'))
93
+ continue;
94
+ out.push(a);
95
+ }
96
+ return out;
97
+ }
98
+ /** Resolve the target store for a replay — `serve`'s rule, over the core db named (or defaulted),
99
+ * with the cross-product bus resolved the way `serve` resolves it (`WICKED_BUS_DB` ›
100
+ * `WICKED_BUS_DATA_DIR` › `<core db>.bus/bus.db`) so the bus db is refused as a target here too. */
101
+ export function replayTarget(args, env = process.env) {
102
+ const coreDbPath = flagValue(args, '--db') ?? join(crewStateHome(), 'core.db');
103
+ const bus = resolveCrewBus({ explicitDb: env['WICKED_BUS_DB'], envDataDir: env['WICKED_BUS_DATA_DIR'], coreDbPath });
104
+ return resolveGovernanceStore({
105
+ flagDb: flagValue(args, GOVERNANCE_DB_FLAG),
106
+ envCrewDb: env[GOVERNANCE_DB_ENV],
107
+ envEstateDb: env[ESTATE_DB_ENGINE_ENV],
108
+ coreDbPath,
109
+ busDbPath: bus.dbPath,
110
+ });
111
+ }
112
+ /** The archive name a drained outbox is renamed to: timestamp + pid + a random nonce, so two replays
113
+ * of the same outbox in the same millisecond (or two processes) can never pick one name and have a
114
+ * later rename replace the earlier archive — the full recovery/audit copy. */
115
+ export function archiveNameFor(outbox, now = new Date(), nonce = randomBytes(3).toString('hex')) {
116
+ return `${outbox}.replayed-${now.toISOString().replace(/[:.]/g, '-')}-${process.pid}-${nonce}`;
117
+ }
118
+ /** A fresh archive name that does not exist yet (the nonce makes a collision astronomically
119
+ * unlikely; the check makes it impossible to rename over an existing archive). */
120
+ function reserveArchiveName(outbox) {
121
+ for (;;) {
122
+ const candidate = archiveNameFor(outbox);
123
+ if (!existsSync(candidate))
124
+ return candidate;
125
+ }
126
+ }
127
+ /**
128
+ * Put the lines that did not land back onto the live outbox — and if THAT write fails (permissions,
129
+ * a full disk), say exactly where they are: the archive is retained (nothing is lost on disk), but
130
+ * `/diagnostics` folds only the live path, so without this the operator would see fewer dead letters
131
+ * than exist.
132
+ */
133
+ export function appendFailedLines(outbox, archive, lines) {
134
+ try {
135
+ appendLines(outbox, lines);
136
+ }
137
+ catch (err) {
138
+ const reason = err instanceof Error ? err.message : String(err);
139
+ throw new Error(`${lines.length} line(s) that failed to replay could not be written back to ${outbox} — they remain in the ` +
140
+ `retained archive ${archive} (nothing is lost; /diagnostics does not fold the archive): ${reason}`);
141
+ }
142
+ }
143
+ /**
144
+ * The caveat a replay that found entries `already_present` must state when the outbox holds
145
+ * UNTIMESTAMPED (pre-stamp) entries: replay ids are content-addressed, and with no stamp to tell
146
+ * them apart, byte-identical unstamped lines share one id — two dead letters of the same event land
147
+ * once. `null` when there is nothing to say.
148
+ */
149
+ export function conflationNote(alreadyPresent, fold) {
150
+ if (alreadyPresent <= 0 || fold.untimestamped <= 0)
151
+ return null;
152
+ return (`${alreadyPresent} entr${alreadyPresent === 1 ? 'y was' : 'ies were'} already on the store. This outbox holds ` +
153
+ `${fold.untimestamped} untimestamped (pre-stamp) entr${fold.untimestamped === 1 ? 'y' : 'ies'}: byte-identical ` +
154
+ 'unstamped lines share one replay id, so two dead letters of the same event land once (stamped lines never conflate ' +
155
+ 'unless their ts and content both match).');
156
+ }
157
+ const NL = 0x0a;
158
+ /**
159
+ * Keep every NDJSON record whole while streaming one file onto another that a concurrent writer may
160
+ * also be appending to. Every chunk this transform emits is a whole number of records — it buffers
161
+ * up to the last `\n` it has seen and holds the partial tail until more arrives — so an `O_APPEND`
162
+ * write from the other writer landing between two of our writes can only ever sit BETWEEN records,
163
+ * never inside one (read-stream chunks are 64 KiB and not line-aligned; forwarding them raw would
164
+ * let a fresh record be spliced into the middle of an archived one). Two repairs ride INSIDE the
165
+ * data writes, never as separate writes a concurrent spool could slip in front of: a leading `\n`
166
+ * always opens the first emitted chunk (the live file may gain a torn tail between any look and
167
+ * our first write — a check-then-write cannot close that, an unconditional separator does; an
168
+ * empty NDJSON line is skipped by every reader), and a source whose last record lacks its newline
169
+ * gets one appended to that same final write.
170
+ */
171
+ export class LineBoundaryGuard extends Transform {
172
+ leading;
173
+ carry = Buffer.alloc(0);
174
+ first = true;
175
+ constructor(leading) {
176
+ super();
177
+ this.leading = leading;
178
+ }
179
+ /** Push one chunk of whole records, opening the very first with the leading separator. */
180
+ pushRecords(data) {
181
+ let out = data;
182
+ if (this.first) {
183
+ this.first = false;
184
+ if (this.leading)
185
+ out = Buffer.concat([Buffer.from('\n'), out]);
186
+ }
187
+ this.push(out);
188
+ }
189
+ _transform(chunk, _enc, cb) {
190
+ const data = Buffer.concat([this.carry, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
191
+ const lastNl = data.lastIndexOf(NL);
192
+ if (lastNl < 0) {
193
+ this.carry = data; // no complete record yet — hold everything
194
+ }
195
+ else {
196
+ this.pushRecords(data.subarray(0, lastNl + 1)); // whole records only
197
+ this.carry = data.subarray(lastNl + 1); // the partial tail, if any
198
+ }
199
+ cb();
200
+ }
201
+ _flush(cb) {
202
+ if (this.carry.length > 0) {
203
+ this.pushRecords(Buffer.concat([this.carry, Buffer.from('\n')])); // the torn last record, terminated in the same write
204
+ this.carry = Buffer.alloc(0);
205
+ }
206
+ cb();
207
+ }
208
+ }
209
+ /**
210
+ * Append NDJSON lines to a live outbox a daemon may be writing — ONE `write()`, ALWAYS behind a
211
+ * `\n`: the file may be absent, at a boundary, or mid-record at the instant of the write, and no
212
+ * look beforehand can know which (the engine writes a record and its newline as two syscalls), so
213
+ * an unconditional separator is the only race-free choice. At worst it is an empty line, which
214
+ * every reader — the fold, the engine's replay — skips.
215
+ */
216
+ export function appendLines(outbox, lines) {
217
+ if (lines.length === 0)
218
+ return;
219
+ appendFileSync(outbox, `\n${lines.join('\n')}\n`, 'utf8');
220
+ }
221
+ /**
222
+ * Put an archived outbox back after a replay that threw — APPEND-ONLY, never a rename over the
223
+ * live path: a daemon can create a fresh outbox at any instant, and an `exists`-then-`rename` would
224
+ * replace it and lose what it had just spooled. Appending (`O_APPEND`, so a concurrent spool is
225
+ * never clobbered — the file is created if absent) is loss-free in every interleaving; the cost is
226
+ * the two batches' relative order when the daemon did spool meanwhile (each entry carries its own
227
+ * `ts` on a stamping engine). Streamed, never the whole archive in memory, through
228
+ * {@link LineBoundaryGuard}: whole records per write, the live tail separated first, a torn last
229
+ * record terminated in its own write. The archive is removed only after its last byte is on the
230
+ * live outbox.
231
+ */
232
+ export async function restoreOutbox(outbox, archive) {
233
+ if (statSync(archive).size > 0) {
234
+ await pipeline(createReadStream(archive), new LineBoundaryGuard(true), createWriteStream(outbox, { flags: 'a' }));
235
+ }
236
+ rmSync(archive);
237
+ }
238
+ /**
239
+ * The command body, separated from process I/O so a test can drive it. Returns the outcome and the
240
+ * exit code; throws {@link UsageError} for bad arguments and {@link GovernanceReplayUnsupportedError}
241
+ * on an addon without the binding (the CLI maps both to exit 2).
242
+ */
243
+ export async function replayOutbox(args, env = process.env) {
244
+ const outboxArg = positionalArgs(args)[0];
245
+ if (outboxArg === undefined)
246
+ throw new UsageError('missing <outbox.ndjson>');
247
+ const outbox = resolve(outboxArg);
248
+ if (!existsSync(outbox))
249
+ throw new UsageError(`outbox not found: ${outbox}`);
250
+ const dryRun = args.includes('--dry-run');
251
+ const store = replayTarget(args, env);
252
+ if (dryRun) {
253
+ // A dry run folds the FILE and touches no store — so the same target configuration a daemon
254
+ // runs with (`:memory:` included) is inspectable; the refusal below guards only a real replay.
255
+ const fold = await foldDeadletters(outbox);
256
+ return {
257
+ outcome: {
258
+ outbox,
259
+ store: { path: store.displayPath, source: store.source },
260
+ archive: null,
261
+ read: fold.count,
262
+ replayed: 0,
263
+ alreadyPresent: null,
264
+ failed: 0,
265
+ dryRun: true,
266
+ note: null,
267
+ fold,
268
+ },
269
+ exitCode: 0,
270
+ };
271
+ }
272
+ if (store.dbPath === ':memory:') {
273
+ throw new UsageError('refusing to replay into :memory: — name a durable store with --governance-db or --db');
274
+ }
275
+ if (!CoreAdapter.replayEmitOutboxSupported()) {
276
+ throw new GovernanceReplayUnsupportedError('Replaying a dead-letter outbox');
277
+ }
278
+ // The sidecar (or whatever directory the store lives in) must exist before the engine opens it
279
+ // — for a filesystem path, never for an engine spec (`postgres://…`; `:memory:` was refused above).
280
+ if (!isStoreSpec(store.dbPath))
281
+ mkdirSync(dirname(store.dbPath), { recursive: true });
282
+ // 1. Archive first — atomic, so nothing the daemon appends from here on is lost; the name is
283
+ // fresh (timestamp + pid + nonce, checked), so no earlier archive is ever renamed over.
284
+ const archive = reserveArchiveName(outbox);
285
+ renameSync(outbox, archive);
286
+ // 2. Replay from the archive. If the engine THROWS (a store it cannot open, an I/O error
287
+ // mid-file, a permission problem) the archive goes back where the daemon spools and
288
+ // `/diagnostics` looks — an exception must never turn "not replayed" into "0 dead letters".
289
+ // Restored WHOLE: the engine may have landed some records before it threw; a later replay of
290
+ // those is a no-op (replay ids are deterministic per spool line), so nothing is duplicated and
291
+ // nothing is lost.
292
+ let report;
293
+ try {
294
+ report = await CoreAdapter.replayEmitOutbox(archive, store.dbPath);
295
+ }
296
+ catch (err) {
297
+ await restoreOutbox(outbox, archive);
298
+ const reason = err instanceof Error ? err.message : String(err);
299
+ throw new Error(`replay failed and the outbox was restored to ${outbox} (nothing is lost): ${reason}`);
300
+ }
301
+ // 3. What did not land stays a dead letter on the live outbox (append: the daemon may have
302
+ // started a fresh file already — and may be mid-record on it, hence the boundary-safe append);
303
+ // a failed write-back names the archive the lines are still in.
304
+ if (report.failed.length > 0) {
305
+ appendFailedLines(outbox, archive, report.failed.map((f) => f.line));
306
+ }
307
+ const alreadyPresent = report.already_present ?? null;
308
+ const note = alreadyPresent !== null && alreadyPresent > 0 ? conflationNote(alreadyPresent, await foldDeadletters(archive)) : null;
309
+ return {
310
+ outcome: {
311
+ outbox,
312
+ store: { path: store.displayPath, source: store.source },
313
+ archive,
314
+ read: report.read,
315
+ replayed: report.replayed,
316
+ alreadyPresent,
317
+ failed: report.failed.length,
318
+ dryRun: false,
319
+ note,
320
+ },
321
+ exitCode: report.failed.length > 0 ? 1 : 0,
322
+ };
323
+ }
324
+ /** The `wicked-crew governance …` entry: prints a JSON outcome and exits with the documented code. */
325
+ export async function runGovernance(argv) {
326
+ const [sub, ...rest] = argv;
327
+ if (sub !== 'replay' || rest.includes('--help') || rest.includes('-h')) {
328
+ console.error(GOVERNANCE_USAGE);
329
+ process.exit(sub === 'replay' ? 0 : 2);
330
+ }
331
+ try {
332
+ const { outcome, exitCode } = await replayOutbox(rest);
333
+ console.log(JSON.stringify(outcome, null, 2));
334
+ if (outcome.note !== null)
335
+ console.error(`[governance] ${outcome.note}`);
336
+ if (!outcome.dryRun && outcome.failed > 0) {
337
+ console.error(`[governance] ${outcome.failed} entr${outcome.failed === 1 ? 'y' : 'ies'} did not land and were appended back onto ${outcome.outbox}`);
338
+ }
339
+ process.exit(exitCode);
340
+ }
341
+ catch (err) {
342
+ if (err instanceof UsageError || err instanceof GovernanceStoreError || err instanceof CrewBusError) {
343
+ console.error(`[governance] ${err.message}\n\n${GOVERNANCE_USAGE}`);
344
+ process.exit(2);
345
+ }
346
+ if (err instanceof GovernanceReplayUnsupportedError) {
347
+ console.error(`[governance] ${err.message}\n` +
348
+ 'Nothing was moved. Inspect the outbox with --dry-run; replay once the engine is upgraded.');
349
+ process.exit(2);
350
+ }
351
+ throw err;
352
+ }
353
+ }
354
+ //# sourceMappingURL=governance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governance.js","sourceRoot":"","sources":["../../src/cli/governance.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -11,6 +11,9 @@ import { startServer } from '../api/server.js';
11
11
  import { resolveAuthMode } from '../api/auth.js';
12
12
  import { crewStateHome, setCrewStateHome, stateHomeOfDb } from '../projects/state-home.js';
13
13
  import { CrewBusError, resolveCrewBus } from '../interactive/bus-location.js';
14
+ import { applyEmitOrigin, emitOrigin, EMIT_DEADLETTER_ENGINE_ENV, ESTATE_DB_ENGINE_ENV, GOVERNANCE_DB_ENV, GOVERNANCE_DB_FLAG, GovernanceStoreError, legacyHomeOutboxPath, resolveGovernanceStore, } from '../core/governance-store.js';
15
+ import { probeLegacyOutbox, replayCommand } from '../api/governance-health.js';
16
+ import { crewPackageVersion, runGovernance } from './governance.js';
14
17
  import { runMcpServer } from './mcp.js';
15
18
  const [, , command, ...argv] = process.argv;
16
19
  function flag(args, name) {
@@ -90,6 +93,39 @@ function parseBootstrap(args) {
90
93
  console.error(`[crew] ${err.message}`);
91
94
  process.exit(1);
92
95
  }
96
+ // The governance store (crew#495 / F-022). The engine's emit seam writes every `wicked.*`
97
+ // governance event — conformance claims, phase transitions, rule lifecycle — to the estate store
98
+ // named by WICKED_ESTATE_DB, and `serve` never set it: on every default install EVERY such event
99
+ // dead-lettered to an outbox under the operator's HOME, silently. Resolution: an explicit
100
+ // --governance-db / WICKED_CREW_GOVERNANCE_DB, else an inherited WICKED_ESTATE_DB (the engine's
101
+ // own variable, honoured), else the daemon's OWN `<core db>.governance/governance.db` — a sidecar
102
+ // for the same fence reason as the bus above. The dead-letter outbox lives in that sidecar too
103
+ // (an explicit WICKED_APPS_EMIT_DEADLETTER wins), never under HOME.
104
+ const governanceDbFlag = flag(args, GOVERNANCE_DB_FLAG);
105
+ if (hasFlag(args, GOVERNANCE_DB_FLAG) && (governanceDbFlag === undefined || governanceDbFlag.startsWith('-'))) {
106
+ console.error(`${GOVERNANCE_DB_FLAG} requires a value (got: ${governanceDbFlag ?? '(missing)'})`);
107
+ process.exit(1);
108
+ }
109
+ let governanceStore;
110
+ try {
111
+ governanceStore = resolveGovernanceStore({
112
+ flagDb: governanceDbFlag,
113
+ envCrewDb: process.env[GOVERNANCE_DB_ENV],
114
+ envEstateDb: process.env[ESTATE_DB_ENGINE_ENV],
115
+ envOutbox: process.env[EMIT_DEADLETTER_ENGINE_ENV],
116
+ coreDbPath: dbPath,
117
+ busDbPath: crewBus.dbPath,
118
+ });
119
+ }
120
+ catch (err) {
121
+ // A store the engine cannot honour (a URL spec on the SQLite-only emit seam) or must not share
122
+ // (the core db, the bus db) is a CONFIG error — refuse to boot rather than run a daemon that
123
+ // dead-letters every governance event or puts a second writer on the actor's store.
124
+ if (!(err instanceof GovernanceStoreError))
125
+ throw err;
126
+ console.error(`[crew] ${err.message}`);
127
+ process.exit(1);
128
+ }
93
129
  // DEFAULT ON (closes #261): answer wicked-interactive's `doc.created` (kind:source) with a
94
130
  // governed `interactive-draft` run. The bus is already required for the project bridge.
95
131
  // Project-bound docs launch FILED runs; unbound (Unfiled) docs launch unfiled governed runs
@@ -121,7 +157,7 @@ function parseBootstrap(args) {
121
157
  // Deterministic-worker override for harnesses (a JSON AgenticCli array); unset = the roster.
122
158
  const interactiveSeats = process.env['WICKED_INTERACTIVE_SEATS'];
123
159
  return {
124
- dbPath, port, stub, engineExec, busDbPath, qeGateEvents, qeBusDbPath, crewBus,
160
+ dbPath, port, stub, engineExec, busDbPath, qeGateEvents, qeBusDbPath, crewBus, governanceStore,
125
161
  interactiveDraftEvents, interactiveEditEvents, interactiveChatEvents, interactiveDemoEvents,
126
162
  interactiveSeats,
127
163
  };
@@ -145,11 +181,26 @@ async function bootstrap(opts) {
145
181
  // wicked-bus (better-sqlite3 underneath) does not create a missing parent: the sidecar dir —
146
182
  // or an explicit dir — must exist before the seams open the db, or every seam disables itself.
147
183
  mkdirSync(dirname(crewBus.dbPath), { recursive: true });
184
+ // The governance store (crew#495): say which rule won, stamp the origin the engine copies onto
185
+ // any dead letter it spools, and point at a pre-fix outbox under HOME if one is still sitting
186
+ // there — the adapter exports the store/outbox variables to the engine before it spawns.
187
+ const { governanceStore } = opts;
188
+ console.error(`[crew] governance store: ${governanceStore.displayPath} (${governanceStore.source}); ` +
189
+ `dead letters: ${governanceStore.outboxPath} (${governanceStore.outboxSource})`);
190
+ const crewVersion = crewPackageVersion();
191
+ applyEmitOrigin(emitOrigin({ version: crewVersion, pid: process.pid, coreDbPath: opts.dbPath }));
192
+ const legacyOutbox = await probeLegacyOutbox(legacyHomeOutboxPath());
193
+ if (legacyOutbox !== null && legacyOutbox.path !== governanceStore.outboxPath) {
194
+ console.warn(`[crew] a pre-fix dead-letter outbox exists under HOME at ${legacyOutbox.path} (${legacyOutbox.bytes} bytes) — ` +
195
+ `governance events earlier daemons could not store; inspect with ${replayCommand(legacyOutbox.path, governanceStore)} --dry-run, ` +
196
+ 'then replay it into this daemon\'s store with the same command');
197
+ }
148
198
  const adapter = new CoreAdapter({
149
199
  dbPath: opts.dbPath,
150
200
  stub: opts.stub,
151
201
  engineExec: opts.engineExec,
152
202
  busDbPath: opts.busDbPath,
203
+ governanceStore,
153
204
  });
154
205
  adapterRef = adapter;
155
206
  const serverOptions = {
@@ -209,6 +260,8 @@ async function bootstrap(opts) {
209
260
  interactiveBridge: { busDataDir: crewBus.dataDir },
210
261
  };
211
262
  const { port } = await startServer(adapter, opts.port, undefined, Object.keys(serverOptions).length > 0 ? serverOptions : undefined);
263
+ // Now that the port is known, complete the origin stamp (the engine reads it at emit time).
264
+ applyEmitOrigin(emitOrigin({ version: crewVersion, pid: process.pid, coreDbPath: opts.dbPath, port }));
212
265
  installShutdownHandlers();
213
266
  return { adapter, port };
214
267
  }
@@ -279,6 +332,14 @@ async function main() {
279
332
  ' --bus-db <path> Bus database path (env: WICKED_BUS_DB) for the interactive/project seams,\n' +
280
333
  ' the /ws relay and the bridge crew spawns (default: $WICKED_BUS_DATA_DIR/bus.db,\n' +
281
334
  ' else <core db>.bus/bus.db); --engine-exec defaults to <state home>/bus.db\n' +
335
+ ' --governance-db <path> Governance store the engine writes conformance claims, phase transitions and\n' +
336
+ ' rule-lifecycle events to (env: WICKED_CREW_GOVERNANCE_DB; an inherited\n' +
337
+ ' WICKED_ESTATE_DB is honoured next; default <core db>.governance/governance.db).\n' +
338
+ ' The emit seam stores to SQLite only: a URL-form WICKED_ESTATE_DB (postgres://…)\n' +
339
+ ' in the shell refuses boot until --governance-db names a SQLite file.\n' +
340
+ ' Dead letters spool to <core db>.governance/emit-outbox.ndjson by default — under the\n' +
341
+ ' state home, not HOME (an explicit WICKED_APPS_EMIT_DEADLETTER is honoured);\n' +
342
+ ' see `wicked-crew governance replay`\n' +
282
343
  ' --stub Use stub engine (env: WICKED_CORE_STUB=1)\n' +
283
344
  ' --engine-exec Arm event-driven execution seam (env: WICKED_BUS_EXEC)\n' +
284
345
  ' --qe-gate-events Consume QE gate bus events (env: WICKED_QE_GATE_EVENTS)\n' +
@@ -306,6 +367,9 @@ async function main() {
306
367
  mode: 'serve',
307
368
  port,
308
369
  db: opts.dbPath,
370
+ // Where the engine's governance events land (crew#495) — an evidence harness can open it
371
+ // (a URL spec's credentials redacted; the raw value went to the engine only).
372
+ governanceDb: opts.governanceStore.displayPath,
309
373
  stub: opts.stub,
310
374
  // The identity seam's resolved mode (task #88): `required` under
311
375
  // WICKED_RUNTIME=team / WICKED_CREW_AUTH=required, else `off` (local).
@@ -378,9 +442,12 @@ async function main() {
378
442
  }
379
443
  await runMcpServer(port);
380
444
  }
445
+ else if (command === 'governance') {
446
+ await runGovernance(argv);
447
+ }
381
448
  else {
382
449
  console.error(`Unknown command: ${command ?? '(none)'}`);
383
- console.error('Usage: wicked-crew serve|start|resume|gate|status|mcp');
450
+ console.error('Usage: wicked-crew serve|start|resume|gate|status|mcp|governance');
384
451
  process.exit(1);
385
452
  }
386
453
  }