job-forge 2.14.46 → 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.
- package/.cursor/rules/main.mdc +3 -3
- package/AGENTS.md +3 -3
- package/CLAUDE.md +3 -3
- package/README.md +2 -1
- package/bin/create-job-forge.mjs +8 -0
- package/bin/job-forge.mjs +32 -0
- package/docs/ARCHITECTURE.md +3 -0
- package/docs/CUSTOMIZATION.md +4 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +2 -0
- package/iso/instructions.md +3 -3
- package/modes/reference-local-helpers.md +3 -0
- package/package.json +8 -1
- package/scripts/check-helper-integration.mjs +1 -0
- package/scripts/receipts.mjs +488 -0
- package/templates/migrations.json +7 -0
package/.cursor/rules/main.mdc
CHANGED
|
@@ -30,7 +30,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
30
30
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
31
31
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
32
32
|
|
|
33
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`,
|
|
33
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, lineage records returned by `npx job-forge lineage:explain ...`, and verified `.jobforge-receipts/*.agent.zip` paths checked with `npx job-forge receipts:verify ...`.
|
|
34
34
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
35
35
|
|
|
36
36
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true`, `browserMode: \"stock\"`, `blockDetection: true`, and `blockedSitePolicy: \"manual-handoff\"` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -62,7 +62,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
62
62
|
- [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@agent-pattern-labs/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
|
|
63
63
|
why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
|
|
64
64
|
|
|
65
|
-
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
65
|
+
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, receipts, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
66
66
|
why: the helper ecosystem is now broad enough that repeating every command in the shared prefix wastes cache budget; the reference keeps operational details on demand while `npm run lint:helpers` enforces integration drift in code
|
|
67
67
|
|
|
68
68
|
## Procedure
|
|
@@ -76,7 +76,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
76
76
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
77
77
|
8. Apply score gate [D4, D8].
|
|
78
78
|
9. Merge contract-validated TSV outcomes [H6, D8].
|
|
79
|
-
10. Verify tracker and run postflight check before ending [H6, D8].
|
|
79
|
+
10. Verify tracker and run postflight check before ending [H6, D8]. For irreversible or trust-sensitive boundaries only (application submitted, blocked-site manual handoff, release, repro, inter-agent handoff), create and verify a receipt from the relevant artifacts with `npx job-forge receipts:create ...` and `npx job-forge receipts:verify ...` [D8].
|
|
80
80
|
|
|
81
81
|
## Routing
|
|
82
82
|
|
package/AGENTS.md
CHANGED
|
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
25
25
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
26
26
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
27
27
|
|
|
28
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`,
|
|
28
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, lineage records returned by `npx job-forge lineage:explain ...`, and verified `.jobforge-receipts/*.agent.zip` paths checked with `npx job-forge receipts:verify ...`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true`, `browserMode: \"stock\"`, `blockDetection: true`, and `blockedSitePolicy: \"manual-handoff\"` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -57,7 +57,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
57
57
|
- [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@agent-pattern-labs/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
|
|
58
58
|
why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
|
|
59
59
|
|
|
60
|
-
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
60
|
+
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, receipts, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
61
61
|
why: the helper ecosystem is now broad enough that repeating every command in the shared prefix wastes cache budget; the reference keeps operational details on demand while `npm run lint:helpers` enforces integration drift in code
|
|
62
62
|
|
|
63
63
|
## Procedure
|
|
@@ -71,7 +71,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
71
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
72
72
|
8. Apply score gate [D4, D8].
|
|
73
73
|
9. Merge contract-validated TSV outcomes [H6, D8].
|
|
74
|
-
10. Verify tracker and run postflight check before ending [H6, D8].
|
|
74
|
+
10. Verify tracker and run postflight check before ending [H6, D8]. For irreversible or trust-sensitive boundaries only (application submitted, blocked-site manual handoff, release, repro, inter-agent handoff), create and verify a receipt from the relevant artifacts with `npx job-forge receipts:create ...` and `npx job-forge receipts:verify ...` [D8].
|
|
75
75
|
|
|
76
76
|
## Routing
|
|
77
77
|
|
package/CLAUDE.md
CHANGED
|
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
25
25
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
26
26
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
27
27
|
|
|
28
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`,
|
|
28
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, lineage records returned by `npx job-forge lineage:explain ...`, and verified `.jobforge-receipts/*.agent.zip` paths checked with `npx job-forge receipts:verify ...`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true`, `browserMode: \"stock\"`, `blockDetection: true`, and `blockedSitePolicy: \"manual-handoff\"` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -57,7 +57,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
57
57
|
- [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@agent-pattern-labs/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
|
|
58
58
|
why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
|
|
59
59
|
|
|
60
|
-
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
60
|
+
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, receipts, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
61
61
|
why: the helper ecosystem is now broad enough that repeating every command in the shared prefix wastes cache budget; the reference keeps operational details on demand while `npm run lint:helpers` enforces integration drift in code
|
|
62
62
|
|
|
63
63
|
## Procedure
|
|
@@ -71,7 +71,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
71
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
72
72
|
8. Apply score gate [D4, D8].
|
|
73
73
|
9. Merge contract-validated TSV outcomes [H6, D8].
|
|
74
|
-
10. Verify tracker and run postflight check before ending [H6, D8].
|
|
74
|
+
10. Verify tracker and run postflight check before ending [H6, D8]. For irreversible or trust-sensitive boundaries only (application submitted, blocked-site manual handoff, release, repro, inter-agent handoff), create and verify a receipt from the relevant artifacts with `npx job-forge receipts:create ...` and `npx job-forge receipts:verify ...` [D8].
|
|
75
75
|
|
|
76
76
|
## Routing
|
|
77
77
|
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ JobForge is built for selective, high-fit applications. It is not intended for s
|
|
|
65
65
|
- Scans configured company portals and job boards.
|
|
66
66
|
- Tracks applications, follow-ups, rejections, offers, reports, and PDFs.
|
|
67
67
|
- Supports batch evaluation and application work through bounded subagents.
|
|
68
|
-
- Uses local helper CLIs for dedupe, scoring, lineage, preflight, postflight, and tracker integrity.
|
|
68
|
+
- Uses local helper CLIs for dedupe, scoring, lineage, preflight, postflight, receipts, and tracker integrity.
|
|
69
69
|
|
|
70
70
|
## Common Commands
|
|
71
71
|
|
|
@@ -78,6 +78,7 @@ Run these from your personal project root after `npm install`.
|
|
|
78
78
|
| Merge batch tracker additions | `npx job-forge merge` |
|
|
79
79
|
| Generate a CV PDF from the current project | `npx job-forge pdf` |
|
|
80
80
|
| Show token usage | `npx job-forge tokens --days 1` |
|
|
81
|
+
| Create a verifiable evidence receipt | `npx job-forge receipts:create --artifact <file>` |
|
|
81
82
|
| Rebuild harness symlinks | `npx job-forge sync` |
|
|
82
83
|
| Upgrade the harness | `npm run update-harness` |
|
|
83
84
|
|
package/bin/create-job-forge.mjs
CHANGED
|
@@ -198,6 +198,12 @@ const consumerPkg = {
|
|
|
198
198
|
'redact:verify': 'job-forge redact:verify',
|
|
199
199
|
'redact:apply': 'job-forge redact:apply',
|
|
200
200
|
'redact:explain': 'job-forge redact:explain',
|
|
201
|
+
'receipts:create': 'job-forge receipts:create',
|
|
202
|
+
'receipts:capture': 'job-forge receipts:capture',
|
|
203
|
+
'receipts:verify': 'job-forge receipts:verify',
|
|
204
|
+
'receipts:inspect': 'job-forge receipts:inspect',
|
|
205
|
+
'receipts:redact': 'job-forge receipts:redact',
|
|
206
|
+
'receipts:path': 'job-forge receipts:path',
|
|
201
207
|
'migrate:plan': 'job-forge migrate:plan',
|
|
202
208
|
'migrate:apply': 'job-forge migrate:apply',
|
|
203
209
|
'migrate:check': 'job-forge migrate:check',
|
|
@@ -317,6 +323,7 @@ Before doing any work, remember where things live in *this* project:
|
|
|
317
323
|
| Dispatch preflight policy | \`templates/preflight.json\` | Safe apply rounds/gates; use \`job-forge preflight:*\` |
|
|
318
324
|
| Dispatch postflight policy | \`templates/postflight.json\` | Safe apply settlement; use \`job-forge postflight:*\` |
|
|
319
325
|
| Consumer migrations | \`templates/migrations.json\` | Safe script/gitignore upgrades; use \`job-forge migrate:*\` |
|
|
326
|
+
| Evidence receipts | \`.jobforge-receipts/\` | Portable verifiable work receipts; use \`job-forge receipts:*\` |
|
|
320
327
|
| Scanner config | \`portals.yml\` (project root) | Company configs |
|
|
321
328
|
| Profile / identity | \`config/profile.yml\` | Candidate name, email, target roles |
|
|
322
329
|
| CV | \`cv.md\` (project root) | Markdown, source of truth |
|
|
@@ -417,6 +424,7 @@ data/timeline-events.jsonl
|
|
|
417
424
|
.jobforge-mcp/
|
|
418
425
|
.jobforge-runs/
|
|
419
426
|
.jobforge-redacted/
|
|
427
|
+
.jobforge-receipts/
|
|
420
428
|
reports/
|
|
421
429
|
!reports/.gitkeep
|
|
422
430
|
batch/batch-state.tsv
|
package/bin/job-forge.mjs
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* prioritize:* Rank local next-action queues via iso-prioritize
|
|
37
37
|
* lineage:* Record artifact lineage and stale outputs via iso-lineage
|
|
38
38
|
* redact:* Sanitize local exports via iso-redact
|
|
39
|
+
* receipts:* Create/verify/redact portable JobForge evidence receipts
|
|
39
40
|
* migrate:* Apply deterministic consumer-project migrations via iso-migrate
|
|
40
41
|
* sync Re-run the harness symlink sync (bin/sync.mjs)
|
|
41
42
|
* help, --help Show this message
|
|
@@ -230,6 +231,15 @@ const redactAliases = {
|
|
|
230
231
|
'redact:path': 'path',
|
|
231
232
|
};
|
|
232
233
|
|
|
234
|
+
const receiptsAliases = {
|
|
235
|
+
'receipts:create': 'create',
|
|
236
|
+
'receipts:capture': 'capture',
|
|
237
|
+
'receipts:verify': 'verify',
|
|
238
|
+
'receipts:inspect': 'inspect',
|
|
239
|
+
'receipts:redact': 'redact',
|
|
240
|
+
'receipts:path': 'path',
|
|
241
|
+
};
|
|
242
|
+
|
|
233
243
|
const migrateAliases = {
|
|
234
244
|
'migrate:plan': 'plan',
|
|
235
245
|
'migrate:apply': 'apply',
|
|
@@ -332,6 +342,11 @@ Commands:
|
|
|
332
342
|
redact:verify Fail if local text still contains sensitive values
|
|
333
343
|
redact:apply Write a sanitized copy of local text
|
|
334
344
|
redact:explain Show the active redaction policy
|
|
345
|
+
receipts:create Create a portable receipt from JobForge artifacts
|
|
346
|
+
receipts:capture Capture a command's stdout/stderr into a receipt
|
|
347
|
+
receipts:verify Verify a receipt manifest and artifact hashes
|
|
348
|
+
receipts:inspect Summarize a receipt without unpacking it
|
|
349
|
+
receipts:redact Redact a receipt with templates/redact.json
|
|
335
350
|
migrate:plan Preview deterministic consumer-project migrations
|
|
336
351
|
migrate:apply Apply deterministic consumer-project migrations
|
|
337
352
|
migrate:check Fail if migrations are pending
|
|
@@ -394,6 +409,8 @@ Pass --help after a command to see its own flags, e.g.:
|
|
|
394
409
|
job-forge lineage:check --artifact reports/123-acme-2026-04-27.md
|
|
395
410
|
job-forge redact:scan --input raw-session.jsonl
|
|
396
411
|
job-forge redact:apply --input raw-session.jsonl --output .jobforge-redacted/session.jsonl
|
|
412
|
+
job-forge receipts:create --company "Acme" --role "Staff Engineer" --artifact batch/tracker-additions/123-acme.tsv --include-ledger
|
|
413
|
+
job-forge receipts:verify .jobforge-receipts/receipt.agent.zip
|
|
397
414
|
job-forge migrate:check
|
|
398
415
|
job-forge migrate:apply
|
|
399
416
|
|
|
@@ -675,6 +692,21 @@ if (cmd === 'redact' || redactAliases[cmd]) {
|
|
|
675
692
|
process.exit(result.status ?? 1);
|
|
676
693
|
}
|
|
677
694
|
|
|
695
|
+
if (cmd === 'receipts' || receiptsAliases[cmd]) {
|
|
696
|
+
const receiptsArgs = cmd === 'receipts'
|
|
697
|
+
? (rest.length === 0 ? ['help'] : rest)
|
|
698
|
+
: [receiptsAliases[cmd], ...rest];
|
|
699
|
+
|
|
700
|
+
const scriptPath = join(PKG_ROOT, 'scripts/receipts.mjs');
|
|
701
|
+
const result = spawnSync(process.execPath, [scriptPath, ...receiptsArgs], {
|
|
702
|
+
stdio: 'inherit',
|
|
703
|
+
cwd: PROJECT_DIR,
|
|
704
|
+
env: process.env,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
process.exit(result.status ?? 1);
|
|
708
|
+
}
|
|
709
|
+
|
|
678
710
|
if (cmd === 'migrate' || migrateAliases[cmd]) {
|
|
679
711
|
const migrateArgs = cmd === 'migrate'
|
|
680
712
|
? (rest.length === 0 ? ['help'] : rest)
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -169,6 +169,7 @@ data/pipeline.md → Pending URLs and `local:jds/...` inbox (see modes/p
|
|
|
169
169
|
.jobforge-timeline.json → Deterministic follow-up action plan built from templates/timeline.json
|
|
170
170
|
.jobforge-prioritize.json → Deterministic next-action priority queue built from templates/prioritize.json
|
|
171
171
|
.jobforge-lineage.json → Artifact lineage graph for stale report/PDF checks
|
|
172
|
+
.jobforge-receipts/ → Portable evidence receipts for side-effect, handoff, release, blocked-site, and repro boundaries
|
|
172
173
|
jds/*.md → Saved job descriptions referenced from the pipeline (`local:jds/{file}`)
|
|
173
174
|
templates/states.yml → Canonical status values
|
|
174
175
|
templates/canon.json → Canonical URL/company/role identity keys
|
|
@@ -197,6 +198,7 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
|
|
|
197
198
|
- Timeline: `.jobforge-timeline.json` (created on demand by `job-forge timeline:*`; gitignored local next-action state)
|
|
198
199
|
- Prioritize: `.jobforge-prioritize.json` and `.jobforge-prioritize-items.json` (created on demand by `job-forge prioritize:*`; gitignored local ranking state)
|
|
199
200
|
- Lineage: `.jobforge-lineage.json` (created by `job-forge lineage:record`; gitignored local stale-output state)
|
|
201
|
+
- Receipts: `.jobforge-receipts/*.agent.zip` (created by `job-forge receipts:create` / `receipts:capture`; gitignored local evidence bundles)
|
|
200
202
|
- Canon: `templates/canon.json` (identity rules inspected with `job-forge canon:*`)
|
|
201
203
|
- Score: `templates/score.json` (weighted rubric and gates inspected with `job-forge score:*`)
|
|
202
204
|
- Timeline policy: `templates/timeline.json` (follow-up windows inspected with `job-forge timeline:*`)
|
|
@@ -266,6 +268,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
|
|
|
266
268
|
| `scripts/preflight.mjs` | `npx job-forge preflight:plan` / `preflight:check` / `preflight:explain` | Deterministic `@agent-pattern-labs/iso-preflight` dispatch planning for file-backed candidate facts and gates |
|
|
267
269
|
| `scripts/postflight.mjs` | `npx job-forge postflight:status` / `postflight:check` / `postflight:explain` | Deterministic `@agent-pattern-labs/iso-postflight` settlement for dispatch outcomes, required tracker TSV artifacts, and merge/verify post-steps |
|
|
268
270
|
| `scripts/redact.mjs` | `npx job-forge redact:scan` / `redact:apply` / `redact:verify` | Deterministic `@agent-pattern-labs/iso-redact` safe-export scanning and sanitization for traces, prompts, reports, and fixtures |
|
|
271
|
+
| `scripts/receipts.mjs` | `npx job-forge receipts:create` / `receipts:verify` / `receipts:redact` | Portable JobForge evidence receipts for side-effect, release, blocked-site, repro, and inter-agent handoff boundaries |
|
|
269
272
|
| `scripts/migrate.mjs` | `npx job-forge migrate:plan` / `migrate:apply` / `migrate:check` | Deterministic `@agent-pattern-labs/iso-migrate` consumer-project upgrades for scripts and generated-artifact ignores |
|
|
270
273
|
| `scripts/check-helper-integration.mjs` | `npm run lint:helpers` | Integration lint that keeps helper packages, scripts, scaffolder defaults, migrations, generated ignores, docs, and `modes/reference-local-helpers.md` aligned |
|
|
271
274
|
| `tracker-lib.mjs` | _(library)_ | Shared helpers for reading/writing day-based tracker files — imported by merge/dedup/verify/normalize |
|
package/docs/CUSTOMIZATION.md
CHANGED
|
@@ -190,6 +190,10 @@ Application settlement policy lives in `templates/postflight.json` and is checke
|
|
|
190
190
|
|
|
191
191
|
Safe-export redaction rules live in `templates/redact.json` and are enforced locally by `@agent-pattern-labs/iso-redact`. Use `job-forge redact:scan --input <file>` before sharing traces, prompts, reports, or fixtures outside the project, `job-forge redact:apply --input <file> --output .jobforge-redacted/<file>` to write a sanitized copy, and `job-forge redact:verify --input <file>` to fail if secrets or configured PII remain. Findings never print matched values. This is not an MCP and does not add prompt or tool-schema tokens.
|
|
192
192
|
|
|
193
|
+
## JobForge evidence receipts
|
|
194
|
+
|
|
195
|
+
Evidence receipts live under `.jobforge-receipts/` and are created locally by `job-forge receipts:*`. Use them at side-effect boundaries such as application submission, blocked-site manual handoff, release, repro, or inter-agent handoff. `receipts:create` can attach tracker TSVs, reports, portal snapshots/form schemas, Geometra replay files, filtered ledger events, proof payloads, and verdicts; `receipts:verify` checks manifest hashes; `receipts:redact` applies `templates/redact.json` before sharing. Routine reads and ordinary local edits do not need receipts.
|
|
196
|
+
|
|
193
197
|
## JobForge consumer migrations
|
|
194
198
|
|
|
195
199
|
Consumer-project migrations live in `templates/migrations.json` and are applied locally by `@agent-pattern-labs/iso-migrate`. `job-forge sync` applies safe migrations automatically after refreshing symlinks; use `JOB_FORGE_SKIP_MIGRATIONS=1` to opt out. Use `job-forge migrate:plan`, `job-forge migrate:apply`, and `job-forge migrate:check` to inspect or enforce script/gitignore drift explicitly. This is not an MCP and does not add prompt or tool-schema tokens.
|
package/docs/README.md
CHANGED
|
@@ -31,7 +31,7 @@ The harness exposes a single CLI (`job-forge`) installed as a `bin` entry. In a
|
|
|
31
31
|
|
|
32
32
|
| What you need | Where to read |
|
|
33
33
|
|---------------|---------------|
|
|
34
|
-
| Full command list (`verify`, `merge`, `dedup`, `normalize`, `pdf`, `sync-check`, `tokens`, `trace`, `telemetry`, `guard`, `ledger`, `capabilities`, `context`, `cache`, `index`, `facts`, `score`, `canon`, `timeline`, `prioritize`, `lineage`, `preflight`, `postflight`, `redact`, `migrate`, `sync`). | [SETUP.md — Tracker and scripts (terminal)](SETUP.md#tracker-and-scripts-terminal). |
|
|
34
|
+
| Full command list (`verify`, `merge`, `dedup`, `normalize`, `pdf`, `sync-check`, `tokens`, `trace`, `telemetry`, `guard`, `ledger`, `capabilities`, `context`, `cache`, `index`, `facts`, `score`, `canon`, `timeline`, `prioritize`, `lineage`, `preflight`, `postflight`, `redact`, `receipts`, `migrate`, `sync`). | [SETUP.md — Tracker and scripts (terminal)](SETUP.md#tracker-and-scripts-terminal). |
|
|
35
35
|
| What each harness `.mjs` script does. | [ARCHITECTURE.md — Pipeline integrity](ARCHITECTURE.md#pipeline-integrity) and the scripts table underneath. |
|
|
36
36
|
| Batch runner, TSV layout, and `batch/tracker-additions/` merge flow. | [batch/README.md](../batch/README.md). |
|
|
37
37
|
| PR gate for harness contributions (`npm run verify` + `npm run smoke:iso` + `npm run build:dashboard`). | [CONTRIBUTING.md — Development](../CONTRIBUTING.md#development). |
|
package/docs/SETUP.md
CHANGED
|
@@ -145,6 +145,8 @@ From your project root, these commands maintain the tracker and pipeline checks.
|
|
|
145
145
|
| Fail unless dispatch outcomes and post-steps are complete | `npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json` | `npm run postflight:check -- --plan ... --outcomes ...` |
|
|
146
146
|
| Sanitize local text before exporting it | `npx job-forge redact:apply --input raw-session.jsonl --output .jobforge-redacted/session.jsonl` | `npm run redact:apply -- --input ... --output ...` |
|
|
147
147
|
| Verify local text is safe to export | `npx job-forge redact:verify --input .jobforge-redacted/session.jsonl` | `npm run redact:verify -- --input ...` |
|
|
148
|
+
| Create a verifiable work receipt | `npx job-forge receipts:create --company "Acme" --role "Staff Engineer" --artifact batch/tracker-additions/example.tsv --include-ledger` | `npm run receipts:create -- --company ... --role ... --artifact ... --include-ledger` |
|
|
149
|
+
| Verify or redact a receipt | `npx job-forge receipts:verify .jobforge-receipts/example.agent.zip` | `npm run receipts:verify -- .jobforge-receipts/example.agent.zip` |
|
|
148
150
|
| Inspect pending consumer migrations | `npx job-forge migrate:plan` | `npm run migrate:plan` |
|
|
149
151
|
| Map status column to canonical labels | `npx job-forge normalize` | `npm run normalize` |
|
|
150
152
|
| Merge duplicate company/role rows | `npx job-forge dedup` | `npm run dedup` |
|
package/iso/instructions.md
CHANGED
|
@@ -25,7 +25,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
25
25
|
- [H6] Application outcomes flow through `batch/tracker-additions/*.tsv`, not `data/pipeline.md`. After any multi-apply run, the orchestrator MUST run `npx job-forge merge` then `npx job-forge verify` before ending the session.
|
|
26
26
|
why: `pipeline.md` is the URL inbox (`[ ]` pending → `[x]` processed); `data/applications/YYYY-MM-DD.md` is the outcome log; the TSV pathway is the only safe bridge because `merge` handles column order and duplicate detection
|
|
27
27
|
|
|
28
|
-
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`,
|
|
28
|
+
- [H7] Load-bearing facts passed to downstream subagents must originate from a file, not from prior subagent prose. Authoritative sources: `data/pipeline.md`, `data/scan-history.tsv`, `batch/scan-output-*.md`, `reports/{num}-*.md` with `**URL:**` / `**Score:**` headers, emitted score JSON validated by `npx job-forge score:check --input ...`, `batch/tracker-additions/*.tsv`, cached JD content returned by `npx job-forge cache:get --url ...`, source path/line pointers returned by `npx job-forge index:query ...`, materialized fact records returned by `npx job-forge facts:query ...`, selected next actions returned by `npx job-forge prioritize:select ...`, lineage records returned by `npx job-forge lineage:explain ...`, and verified `.jobforge-receipts/*.agent.zip` paths checked with `npx job-forge receipts:verify ...`.
|
|
29
29
|
why: 2026-04-18 scan subagent returned 30 fabricated Greenhouse IDs in prose (plausible-looking, non-existent); orchestrator dispatched 30 downstream subagents that all 404'd. Subagents can hallucinate IDs, scores, and confirmation text — round-trip through a file or don't trust the value
|
|
30
30
|
|
|
31
31
|
- [H8] Never paste proxy values from `config/profile.yml` into `task` prompts, status text, or summaries. If a proxy is configured, tell the subagent exactly: "Proxy is configured; read `config/profile.yml` and pass its top-level `proxy:` object plus `headless: true`, `browserMode: \"stock\"`, `blockDetection: true`, and `blockedSitePolicy: \"manual-handoff\"` to every `geometra_connect` call and every Geometra auto-connect call that passes `pageUrl` or `url`." Do not transcribe `server`, `username`, `password`, or `bypass`, even if you just read them from disk.
|
|
@@ -57,7 +57,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
57
57
|
- [D7] For standalone `batch` runs, prefer `batch/batch-runner.sh` instead of hand-rolling the loop. It delegates to `@agent-pattern-labs/iso-orchestrator`, persists workflow records in `.jobforge-runs/`, caps bundle fan-out, and mutexes state/report-number writes. Use `JOBFORGE_LEGACY_BATCH_RUNNER=1` only as a fallback.
|
|
58
58
|
why: the old Bash loop encoded resumability and parallelism manually; the iso-orchestrator path makes the durable control state inspectable and prevents report-number collisions under parallel bundles
|
|
59
59
|
|
|
60
|
-
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
60
|
+
- [D8] Use deterministic local helpers instead of prose when they can answer or validate state, identity, policy, scoring, timing, dispatch, priority, lineage, migration, receipts, or safe-export questions. Read `modes/reference-local-helpers.md` when choosing a helper or changing helper wiring.
|
|
61
61
|
why: the helper ecosystem is now broad enough that repeating every command in the shared prefix wastes cache budget; the reference keeps operational details on demand while `npm run lint:helpers` enforces integration drift in code
|
|
62
62
|
|
|
63
63
|
## Procedure
|
|
@@ -71,7 +71,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
71
71
|
7. Cross-check subagent facts against authoritative files [H7].
|
|
72
72
|
8. Apply score gate [D4, D8].
|
|
73
73
|
9. Merge contract-validated TSV outcomes [H6, D8].
|
|
74
|
-
10. Verify tracker and run postflight check before ending [H6, D8].
|
|
74
|
+
10. Verify tracker and run postflight check before ending [H6, D8]. For irreversible or trust-sensitive boundaries only (application submitted, blocked-site manual handoff, release, repro, inter-agent handoff), create and verify a receipt from the relevant artifacts with `npx job-forge receipts:create ...` and `npx job-forge receipts:verify ...` [D8].
|
|
75
75
|
|
|
76
76
|
## Routing
|
|
77
77
|
|
|
@@ -13,6 +13,7 @@ Prefer a local helper when the workflow needs:
|
|
|
13
13
|
- One-shot rendered browser snapshots or form schemas.
|
|
14
14
|
- Scoring, timing, priority, or lineage decisions.
|
|
15
15
|
- Safe export checks.
|
|
16
|
+
- Evidence bundles at side-effect, release, handoff, blocked-site, or repro boundaries.
|
|
16
17
|
|
|
17
18
|
Do not paste whole helper outputs into prompts unless the downstream agent needs that exact file-backed result. Prefer passing paths, ids, keys, and short summaries.
|
|
18
19
|
|
|
@@ -40,6 +41,7 @@ Do not paste whole helper outputs into prompts unless the downstream agent needs
|
|
|
40
41
|
| Follow-up timing | `templates/timeline.json` | `npx job-forge timeline:*` |
|
|
41
42
|
| Next-action ranking | `templates/prioritize.json` | `npx job-forge prioritize:*` |
|
|
42
43
|
| Artifact lineage | `.jobforge-lineage.json` | `npx job-forge lineage:*` |
|
|
44
|
+
| Evidence receipts | `.jobforge-receipts/` | `npx job-forge receipts:*` |
|
|
43
45
|
|
|
44
46
|
## Mandatory Uses
|
|
45
47
|
|
|
@@ -50,6 +52,7 @@ Do not paste whole helper outputs into prompts unless the downstream agent needs
|
|
|
50
52
|
- For next-action or replacement-candidate selection, run `prioritize:build` or `prioritize:select --limit N`.
|
|
51
53
|
- For generated reports or PDFs reused after input changes, run `lineage:check --artifact <file>` if lineage exists; after creating derived artifacts, record them with `lineage:record --artifact <file> --input <source>...`.
|
|
52
54
|
- Before exporting traces, prompts, reports, or fixtures outside the project, run `redact:scan`, `redact:apply`, or `redact:verify`.
|
|
55
|
+
- After irreversible or trust-sensitive actions, create a receipt with `receipts:create` from the relevant tracker TSV, report, portal snapshot/form schema, and filtered ledger events. Use `receipts:verify` before treating a receipt as a workflow gate, and `receipts:redact` before sharing it outside the project.
|
|
53
56
|
- When diagnosing consumer harness drift, run `migrate:plan` or `migrate:check`; `job-forge sync` applies safe migrations automatically unless `JOB_FORGE_SKIP_MIGRATIONS=1` is set.
|
|
54
57
|
- When you only need a rendered page model, compact snapshot, or form schema from one URL, prefer `portal:snapshot` / `portal:form-schema` over Geometra MCP tool calls. Use MCP for interactive multi-step browser sessions.
|
|
55
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.47",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -106,6 +106,12 @@
|
|
|
106
106
|
"redact:verify": "node bin/job-forge.mjs redact:verify",
|
|
107
107
|
"redact:apply": "node bin/job-forge.mjs redact:apply",
|
|
108
108
|
"redact:explain": "node bin/job-forge.mjs redact:explain",
|
|
109
|
+
"receipts:create": "node bin/job-forge.mjs receipts:create",
|
|
110
|
+
"receipts:capture": "node bin/job-forge.mjs receipts:capture",
|
|
111
|
+
"receipts:verify": "node bin/job-forge.mjs receipts:verify",
|
|
112
|
+
"receipts:inspect": "node bin/job-forge.mjs receipts:inspect",
|
|
113
|
+
"receipts:redact": "node bin/job-forge.mjs receipts:redact",
|
|
114
|
+
"receipts:path": "node bin/job-forge.mjs receipts:path",
|
|
109
115
|
"migrate:plan": "node bin/job-forge.mjs migrate:plan",
|
|
110
116
|
"migrate:apply": "node bin/job-forge.mjs migrate:apply",
|
|
111
117
|
"migrate:check": "node bin/job-forge.mjs migrate:check",
|
|
@@ -195,6 +201,7 @@
|
|
|
195
201
|
"@agent-pattern-labs/iso-postflight": "^0.1.1",
|
|
196
202
|
"@agent-pattern-labs/iso-preflight": "^0.1.1",
|
|
197
203
|
"@agent-pattern-labs/iso-prioritize": "^0.1.1",
|
|
204
|
+
"@agent-pattern-labs/iso-receipts": "^0.1.0",
|
|
198
205
|
"@agent-pattern-labs/iso-redact": "^0.1.1",
|
|
199
206
|
"@agent-pattern-labs/iso-score": "^0.1.1",
|
|
200
207
|
"@agent-pattern-labs/iso-timeline": "^0.1.1",
|
|
@@ -35,6 +35,7 @@ const groups = [
|
|
|
35
35
|
helper('prioritize', '@agent-pattern-labs/iso-prioritize', ['status', 'items', 'build', 'rank', 'select', 'check', 'verify', 'explain'], { template: 'templates/prioritize.json', artifacts: ['.jobforge-prioritize.json', '.jobforge-prioritize-items.json'], migrated: true }),
|
|
36
36
|
helper('lineage', '@agent-pattern-labs/iso-lineage', ['status', 'record', 'check', 'stale', 'verify', 'explain'], { artifacts: ['.jobforge-lineage.json'], migrated: true }),
|
|
37
37
|
helper('redact', '@agent-pattern-labs/iso-redact', ['scan', 'verify', 'apply', 'explain'], { template: 'templates/redact.json', artifacts: ['.jobforge-redacted/'], migrated: true }),
|
|
38
|
+
helper('receipts', '', ['create', 'capture', 'verify', 'inspect', 'redact', 'path'], { artifacts: ['.jobforge-receipts/'], migrated: true }),
|
|
38
39
|
helper('migrate', '@agent-pattern-labs/iso-migrate', ['plan', 'apply', 'check', 'explain'], { template: 'templates/migrations.json', migrated: true }),
|
|
39
40
|
];
|
|
40
41
|
|
|
@@ -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",
|