job-forge 2.14.45 → 2.14.47

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.
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ statSync,
9
+ } from 'node:fs';
10
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
11
+ import { fileURLToPath, pathToFileURL } from 'node:url';
12
+ import { PROJECT_DIR } from '../tracker-lib.mjs';
13
+ import {
14
+ companyRoleKey,
15
+ legacyCompanyRoleKey,
16
+ legacyUrlKey,
17
+ readJobForgeLedger,
18
+ slugPart,
19
+ urlKey,
20
+ } from '../lib/jobforge-ledger.mjs';
21
+ import { readJobForgeRedactConfig } from '../lib/jobforge-redact.mjs';
22
+
23
+ const RECEIPTS_DIR = '.jobforge-receipts';
24
+ const USAGE = `job-forge receipts - portable local evidence receipts
25
+
26
+ Usage:
27
+ job-forge receipts:create [--kind <kind>] [--out <receipt.agent.zip|dir>] [--subject <text>] [--run-id <id>]
28
+ [--url <url>] [--company <name> --role <role>] [--status <status>]
29
+ [--artifact <file> ...] [--geometra <file> ...] [--portal <file> ...]
30
+ [--include-ledger | --all-ledger] [--verdict <json|@file>] [--proof <json|@file>] [--redact] [--json]
31
+ job-forge receipts:capture [--out <receipt.agent.zip|dir>] [--subject <text>] [--run-id <id>] [--json] -- <command> [args...]
32
+ job-forge receipts:verify <receipt.agent.zip|dir> [--json]
33
+ job-forge receipts:inspect <receipt.agent.zip|dir> [--json]
34
+ job-forge receipts:redact <receipt.agent.zip|dir> --out <receipt.agent.zip|dir> [--json]
35
+ job-forge receipts:path
36
+
37
+ Use receipts at workflow boundaries: application submission, blocked-site
38
+ manual handoff, release, repro, or inter-agent handoff. Do not create receipts
39
+ for routine reads or ordinary local edits.`;
40
+
41
+ const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
42
+ const opts = parseArgs(rawArgs);
43
+
44
+ if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
45
+ console.log(USAGE);
46
+ process.exit(0);
47
+ }
48
+
49
+ try {
50
+ const receipts = await loadIsoReceipts();
51
+ if (cmd === 'path') {
52
+ console.log(receiptsDir());
53
+ } else if (cmd === 'create') {
54
+ await create(receipts, opts);
55
+ } else if (cmd === 'capture') {
56
+ await capture(receipts, opts);
57
+ } else if (cmd === 'verify') {
58
+ verify(receipts, opts);
59
+ } else if (cmd === 'inspect') {
60
+ inspect(receipts, opts);
61
+ } else if (cmd === 'redact') {
62
+ redact(receipts, opts);
63
+ } else {
64
+ console.error(`unknown receipts command "${cmd}"\n`);
65
+ console.error(USAGE);
66
+ process.exit(2);
67
+ }
68
+ } catch (error) {
69
+ console.error(error instanceof Error ? error.message : String(error));
70
+ process.exit(1);
71
+ }
72
+
73
+ function parseArgs(args) {
74
+ const opts = {
75
+ artifacts: [],
76
+ geometra: [],
77
+ portal: [],
78
+ command: [],
79
+ json: false,
80
+ help: false,
81
+ includeLedger: false,
82
+ allLedger: false,
83
+ redact: false,
84
+ };
85
+ let afterDoubleDash = false;
86
+ for (let i = 0; i < args.length; i++) {
87
+ const arg = args[i];
88
+ if (afterDoubleDash) {
89
+ opts.command.push(arg);
90
+ } else if (arg === '--') {
91
+ afterDoubleDash = true;
92
+ } else if (arg === '--json') {
93
+ opts.json = true;
94
+ } else if (arg === '--redact') {
95
+ opts.redact = true;
96
+ } else if (arg === '--include-ledger') {
97
+ opts.includeLedger = true;
98
+ } else if (arg === '--all-ledger') {
99
+ opts.allLedger = true;
100
+ } else if (arg === '--out' || arg === '-o') {
101
+ opts.out = valueAfter(args, ++i, arg);
102
+ } else if (arg.startsWith('--out=')) {
103
+ opts.out = arg.slice('--out='.length);
104
+ } else if (arg === '--kind') {
105
+ opts.kind = valueAfter(args, ++i, arg);
106
+ } else if (arg.startsWith('--kind=')) {
107
+ opts.kind = arg.slice('--kind='.length);
108
+ } else if (arg === '--subject') {
109
+ opts.subject = valueAfter(args, ++i, arg);
110
+ } else if (arg.startsWith('--subject=')) {
111
+ opts.subject = arg.slice('--subject='.length);
112
+ } else if (arg === '--run-id') {
113
+ opts.runId = valueAfter(args, ++i, arg);
114
+ } else if (arg.startsWith('--run-id=')) {
115
+ opts.runId = arg.slice('--run-id='.length);
116
+ } else if (arg === '--url') {
117
+ opts.url = valueAfter(args, ++i, arg);
118
+ } else if (arg.startsWith('--url=')) {
119
+ opts.url = arg.slice('--url='.length);
120
+ } else if (arg === '--company') {
121
+ opts.company = valueAfter(args, ++i, arg);
122
+ } else if (arg.startsWith('--company=')) {
123
+ opts.company = arg.slice('--company='.length);
124
+ } else if (arg === '--role') {
125
+ opts.role = valueAfter(args, ++i, arg);
126
+ } else if (arg.startsWith('--role=')) {
127
+ opts.role = arg.slice('--role='.length);
128
+ } else if (arg === '--status') {
129
+ opts.status = valueAfter(args, ++i, arg);
130
+ } else if (arg.startsWith('--status=')) {
131
+ opts.status = arg.slice('--status='.length);
132
+ } else if (arg === '--artifact') {
133
+ opts.artifacts.push(valueAfter(args, ++i, arg));
134
+ } else if (arg.startsWith('--artifact=')) {
135
+ opts.artifacts.push(arg.slice('--artifact='.length));
136
+ } else if (arg === '--geometra') {
137
+ opts.geometra.push(valueAfter(args, ++i, arg));
138
+ } else if (arg.startsWith('--geometra=')) {
139
+ opts.geometra.push(arg.slice('--geometra='.length));
140
+ } else if (arg === '--portal') {
141
+ opts.portal.push(valueAfter(args, ++i, arg));
142
+ } else if (arg.startsWith('--portal=')) {
143
+ opts.portal.push(arg.slice('--portal='.length));
144
+ } else if (arg === '--verdict') {
145
+ opts.verdict = readJsonArg(valueAfter(args, ++i, arg), arg);
146
+ } else if (arg.startsWith('--verdict=')) {
147
+ opts.verdict = readJsonArg(arg.slice('--verdict='.length), '--verdict');
148
+ } else if (arg === '--proof') {
149
+ opts.proof = readJsonArg(valueAfter(args, ++i, arg), arg);
150
+ } else if (arg.startsWith('--proof=')) {
151
+ opts.proof = readJsonArg(arg.slice('--proof='.length), '--proof');
152
+ } else if (arg === '--help' || arg === '-h') {
153
+ opts.help = true;
154
+ } else if (!arg.startsWith('--') && !opts.input) {
155
+ opts.input = arg;
156
+ } else {
157
+ throw new Error(`unknown argument "${arg}"`);
158
+ }
159
+ }
160
+ return opts;
161
+ }
162
+
163
+ function valueAfter(values, index, flag) {
164
+ const value = values[index];
165
+ if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
166
+ return value;
167
+ }
168
+
169
+ async function create(receipts, opts) {
170
+ const kind = opts.kind || 'application';
171
+ const artifacts = opts.artifacts.map((path) => fileInput(path, 'artifacts'));
172
+ const geometraReplay = [
173
+ ...opts.geometra.map((path) => fileInput(path, 'geometra-replay')),
174
+ ...opts.portal.map((path) => fileInput(path, 'geometra-replay')),
175
+ ];
176
+ const events = [
177
+ {
178
+ type: 'jobforge.receipt.created',
179
+ data: compactObject({
180
+ kind,
181
+ url: opts.url,
182
+ company: opts.company,
183
+ role: opts.role,
184
+ status: opts.status,
185
+ }),
186
+ meta: { source: 'job-forge' },
187
+ },
188
+ ];
189
+
190
+ if (opts.includeLedger || opts.allLedger) {
191
+ const ledger = selectLedgerEvents(opts);
192
+ artifacts.push({
193
+ path: 'artifacts/jobforge-ledger-events.jsonl',
194
+ content: `${ledger.map((event) => JSON.stringify(event)).join('\n')}${ledger.length ? '\n' : ''}`,
195
+ kind: 'artifact',
196
+ contentType: 'application/jsonl',
197
+ });
198
+ events.push({
199
+ type: 'jobforge.ledger.snapshot',
200
+ data: {
201
+ count: ledger.length,
202
+ all: Boolean(opts.allLedger),
203
+ filters: ledgerFilters(opts),
204
+ },
205
+ meta: { source: 'job-forge' },
206
+ });
207
+ }
208
+
209
+ let receipt = receipts.createReceipt({
210
+ subject: subjectFor(opts, kind),
211
+ runId: opts.runId,
212
+ generator: { name: 'job-forge', version: packageVersion() },
213
+ events,
214
+ artifacts,
215
+ geometraReplay,
216
+ verdict: opts.verdict,
217
+ proof: opts.proof,
218
+ extensions: {
219
+ jobforge: compactObject({
220
+ kind,
221
+ projectDir: PROJECT_DIR,
222
+ url: opts.url,
223
+ company: opts.company,
224
+ role: opts.role,
225
+ status: opts.status,
226
+ }),
227
+ },
228
+ });
229
+ if (opts.redact) receipt = receipts.redactReceipt(receipt, {
230
+ policy: readJobForgeRedactConfig(PROJECT_DIR),
231
+ policyName: 'templates/redact.json',
232
+ });
233
+
234
+ const out = resolveOutputPath(opts.out || defaultReceiptPath(kind, opts));
235
+ writeOutput(receipts, receipt, out);
236
+ const verifyResult = receipts.verifyReceipt(receipt);
237
+ output(opts, {
238
+ out,
239
+ receiptId: receipt.manifest.receiptId,
240
+ entries: receipt.manifest.entries.length,
241
+ verify: verifyResult,
242
+ }, () => {
243
+ console.log(`receipts: wrote ${relativePath(out)} (${receipt.manifest.receiptId})`);
244
+ console.log(receipts.formatReceiptVerifyResult(verifyResult));
245
+ });
246
+ }
247
+
248
+ async function capture(receipts, opts) {
249
+ if (opts.command.length === 0) throw new Error('capture requires a command after --');
250
+ const startedAt = new Date().toISOString();
251
+ const started = Date.now();
252
+ const result = spawnSync(opts.command[0], opts.command.slice(1), {
253
+ cwd: PROJECT_DIR,
254
+ encoding: 'buffer',
255
+ maxBuffer: 64 * 1024 * 1024,
256
+ });
257
+ const endedAt = new Date().toISOString();
258
+ const durationMs = Date.now() - started;
259
+ const exitCode = result.status ?? (result.error ? 127 : null);
260
+
261
+ const receipt = receipts.createReceipt({
262
+ subject: opts.subject || `jobforge:command:${opts.command[0]}`,
263
+ runId: opts.runId,
264
+ generator: { name: 'job-forge', version: packageVersion() },
265
+ command: {
266
+ argv: opts.command,
267
+ cwd: PROJECT_DIR,
268
+ exitCode,
269
+ signal: result.signal ?? null,
270
+ startedAt,
271
+ endedAt,
272
+ durationMs,
273
+ },
274
+ environment: {
275
+ platform: process.platform,
276
+ arch: process.arch,
277
+ node: process.version,
278
+ },
279
+ events: [
280
+ { type: 'jobforge.command.started', at: startedAt, data: { argv: opts.command, cwd: PROJECT_DIR } },
281
+ {
282
+ type: 'jobforge.command.exited',
283
+ at: endedAt,
284
+ data: compactObject({
285
+ exitCode,
286
+ signal: result.signal,
287
+ durationMs,
288
+ error: result.error?.message,
289
+ }),
290
+ },
291
+ ],
292
+ artifacts: [
293
+ { path: 'artifacts/stdout.txt', content: result.stdout || Buffer.alloc(0), contentType: 'text/plain' },
294
+ { path: 'artifacts/stderr.txt', content: result.stderr || Buffer.alloc(0), contentType: 'text/plain' },
295
+ ],
296
+ });
297
+
298
+ const out = resolveOutputPath(opts.out || defaultReceiptPath('command', opts));
299
+ writeOutput(receipts, receipt, out);
300
+ output(opts, {
301
+ out,
302
+ receiptId: receipt.manifest.receiptId,
303
+ exitCode,
304
+ }, () => {
305
+ console.log(`receipts: wrote ${relativePath(out)} (${receipt.manifest.receiptId}, command exit ${exitCode ?? 'signal'})`);
306
+ });
307
+ }
308
+
309
+ function verify(receipts, opts) {
310
+ if (!opts.input) throw new Error('verify requires a receipt path');
311
+ const result = receipts.verifyReceipt(resolveInputPath(opts.input));
312
+ output(opts, result, () => console.log(receipts.formatReceiptVerifyResult(result)));
313
+ if (!result.ok) process.exit(1);
314
+ }
315
+
316
+ function inspect(receipts, opts) {
317
+ if (!opts.input) throw new Error('inspect requires a receipt path');
318
+ const result = receipts.inspectReceipt(resolveInputPath(opts.input));
319
+ output(opts, result, () => console.log(receipts.formatReceiptInspectResult(result)));
320
+ }
321
+
322
+ function redact(receipts, opts) {
323
+ if (!opts.input) throw new Error('redact requires a receipt path');
324
+ if (!opts.out) throw new Error('redact requires --out <path>');
325
+ const redacted = receipts.redactReceipt(receipts.readReceipt(resolveInputPath(opts.input)), {
326
+ policy: readJobForgeRedactConfig(PROJECT_DIR),
327
+ policyName: 'templates/redact.json',
328
+ });
329
+ const out = resolveOutputPath(opts.out);
330
+ writeOutput(receipts, redacted, out);
331
+ output(opts, {
332
+ out,
333
+ receiptId: redacted.manifest.receiptId,
334
+ redaction: redacted.manifest.redaction,
335
+ }, () => {
336
+ console.log(`receipts: redacted ${redacted.manifest.receiptId} to ${relativePath(out)}`);
337
+ });
338
+ }
339
+
340
+ function fileInput(input, bucket) {
341
+ const path = resolveInputPath(input);
342
+ if (!existsSync(path)) throw new Error(`artifact not found: ${input}`);
343
+ if (!statSync(path).isFile()) throw new Error(`artifact is not a file: ${input}`);
344
+ return {
345
+ path: receiptArtifactPath(path, bucket),
346
+ content: readFileSync(path),
347
+ kind: bucket === 'geometra-replay' ? 'geometra-replay' : 'artifact',
348
+ };
349
+ }
350
+
351
+ function receiptArtifactPath(path, bucket) {
352
+ const rel = relative(PROJECT_DIR, path).replace(/\\/g, '/');
353
+ if (rel && !rel.startsWith('../') && rel !== '..' && !isAbsolute(rel)) return `${bucket}/${rel}`;
354
+ return `${bucket}/external/${basename(path)}`;
355
+ }
356
+
357
+ function selectLedgerEvents(opts) {
358
+ const events = readJobForgeLedger(PROJECT_DIR);
359
+ if (opts.allLedger) return statusFiltered(events, opts.status);
360
+ const keys = ledgerFilterKeys(opts);
361
+ if (keys.length === 0) {
362
+ throw new Error('--include-ledger requires --url or --company/--role; use --all-ledger to attach every ledger event');
363
+ }
364
+ return statusFiltered(events.filter((event) => keys.includes(event.key)), opts.status);
365
+ }
366
+
367
+ function ledgerFilterKeys(opts) {
368
+ const keys = [];
369
+ if (opts.url) keys.push(urlKey(opts.url, PROJECT_DIR), legacyUrlKey(opts.url));
370
+ if (opts.company || opts.role) {
371
+ if (!opts.company || !opts.role) throw new Error('--company and --role must be provided together');
372
+ keys.push(companyRoleKey(opts.company, opts.role, PROJECT_DIR), legacyCompanyRoleKey(opts.company, opts.role));
373
+ }
374
+ return [...new Set(keys)];
375
+ }
376
+
377
+ function statusFiltered(events, status) {
378
+ if (!status) return events;
379
+ return events.filter((event) => event.data?.status === status);
380
+ }
381
+
382
+ function ledgerFilters(opts) {
383
+ return compactObject({
384
+ url: opts.url,
385
+ company: opts.company,
386
+ role: opts.role,
387
+ status: opts.status,
388
+ keys: ledgerFilterKeys(opts),
389
+ });
390
+ }
391
+
392
+ function subjectFor(opts, kind) {
393
+ if (opts.subject) return opts.subject;
394
+ if (opts.company && opts.role) return `jobforge:application:${companyRoleKey(opts.company, opts.role, PROJECT_DIR)}`;
395
+ if (opts.url) return `jobforge:url:${urlKey(opts.url, PROJECT_DIR)}`;
396
+ if (opts.runId) return `jobforge:run:${opts.runId}`;
397
+ return `jobforge:${kind}`;
398
+ }
399
+
400
+ function defaultReceiptPath(kind, opts) {
401
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
402
+ const seed = opts.company || opts.role || opts.url || opts.subject || opts.runId || kind;
403
+ const slug = slugPart(seed).slice(0, 80) || kind;
404
+ return join(RECEIPTS_DIR, `${stamp}-${slug}.agent.zip`);
405
+ }
406
+
407
+ function writeOutput(receipts, receipt, out) {
408
+ mkdirSync(dirname(out), { recursive: true });
409
+ if (out.endsWith('.zip')) receipts.packReceipt(receipt, out);
410
+ else receipts.writeReceiptDirectory(receipt, out);
411
+ }
412
+
413
+ async function loadIsoReceipts() {
414
+ const explicit = process.env.JOB_FORGE_ISO_RECEIPTS_MODULE;
415
+ if (explicit) return import(pathToFileURL(resolve(explicit)).href);
416
+
417
+ try {
418
+ return await import('@agent-pattern-labs/iso-receipts');
419
+ } catch {
420
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
421
+ const sibling = resolve(scriptDir, '../../iso/packages/iso-receipts/dist/index.js');
422
+ if (existsSync(sibling)) return import(pathToFileURL(sibling).href);
423
+ throw new Error(
424
+ 'Could not load @agent-pattern-labs/iso-receipts. Install dependencies, ' +
425
+ 'or build the sibling iso repo so ../iso/packages/iso-receipts/dist/index.js exists.',
426
+ );
427
+ }
428
+ }
429
+
430
+ function packageVersion() {
431
+ try {
432
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
433
+ return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
434
+ } catch {
435
+ return undefined;
436
+ }
437
+ }
438
+
439
+ function readJsonArg(raw, flag) {
440
+ const text = raw.startsWith('@') ? readFileSync(resolveInputPath(raw.slice(1)), 'utf8') : raw;
441
+ try {
442
+ const parsed = JSON.parse(text);
443
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('expected object');
444
+ return parsed;
445
+ } catch (error) {
446
+ const detail = error instanceof Error ? error.message : String(error);
447
+ throw new Error(`${flag}: invalid JSON: ${detail}`);
448
+ }
449
+ }
450
+
451
+ function resolveInputPath(path) {
452
+ return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
453
+ }
454
+
455
+ function resolveOutputPath(path) {
456
+ return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
457
+ }
458
+
459
+ function receiptsDir() {
460
+ return join(PROJECT_DIR, RECEIPTS_DIR);
461
+ }
462
+
463
+ function relativePath(path) {
464
+ return relative(PROJECT_DIR, path) || '.';
465
+ }
466
+
467
+ function output(opts, jsonValue, textWriter) {
468
+ if (opts.json) console.log(JSON.stringify(jsonValue, null, 2));
469
+ else textWriter();
470
+ }
471
+
472
+ function compactObject(obj) {
473
+ const out = {};
474
+ for (const [key, value] of Object.entries(obj || {})) {
475
+ const clean = jsonValue(value);
476
+ if (clean !== undefined) out[key] = clean;
477
+ }
478
+ return out;
479
+ }
480
+
481
+ function jsonValue(value) {
482
+ if (value === undefined) return undefined;
483
+ if (value === null) return null;
484
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
485
+ if (Array.isArray(value)) return value.map(jsonValue).filter((item) => item !== undefined);
486
+ if (typeof value === 'object') return compactObject(value);
487
+ return String(value);
488
+ }
@@ -84,6 +84,12 @@
84
84
  "redact:verify": "job-forge redact:verify",
85
85
  "redact:apply": "job-forge redact:apply",
86
86
  "redact:explain": "job-forge redact:explain",
87
+ "receipts:create": "job-forge receipts:create",
88
+ "receipts:capture": "job-forge receipts:capture",
89
+ "receipts:verify": "job-forge receipts:verify",
90
+ "receipts:inspect": "job-forge receipts:inspect",
91
+ "receipts:redact": "job-forge receipts:redact",
92
+ "receipts:path": "job-forge receipts:path",
87
93
  "migrate:plan": "job-forge migrate:plan",
88
94
  "migrate:apply": "job-forge migrate:apply",
89
95
  "migrate:check": "job-forge migrate:check",
@@ -114,6 +120,7 @@
114
120
  ".jobforge-lineage.json",
115
121
  ".jobforge-mcp/",
116
122
  ".jobforge-redacted/",
123
+ ".jobforge-receipts/",
117
124
  "data/timeline-events.jsonl",
118
125
  "batch/preflight-candidates.json",
119
126
  "batch/preflight-plan.json",