job-forge 2.14.32 → 2.14.33
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 +5 -2
- package/.opencode/skills/job-forge.md +4 -0
- package/AGENTS.md +5 -2
- package/CLAUDE.md +5 -2
- package/README.md +7 -5
- package/bin/job-forge.mjs +34 -0
- package/docs/ARCHITECTURE.md +9 -3
- package/docs/CUSTOMIZATION.md +4 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +3 -0
- package/iso/commands/job-forge.md +4 -0
- package/iso/instructions.md +5 -2
- package/lib/jobforge-timeline.mjs +294 -0
- package/modes/followup.md +6 -6
- package/package.json +9 -1
- package/scripts/check-iso-smoke.mjs +1 -0
- package/scripts/timeline.mjs +237 -0
- package/templates/migrations.json +10 -0
- package/templates/timeline.json +86 -0
- package/verify-pipeline.mjs +20 -0
package/.cursor/rules/main.mdc
CHANGED
|
@@ -24,7 +24,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
24
24
|
- [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
|
|
25
25
|
why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
|
|
26
26
|
|
|
27
|
-
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
27
|
+
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, `.jobforge-timeline.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
28
28
|
why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
|
|
29
29
|
|
|
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.
|
|
@@ -98,11 +98,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
98
98
|
- [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
|
|
99
99
|
why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
|
|
100
100
|
|
|
101
|
+
- [D20] Treat `templates/timeline.json` as the source of truth for follow-up and next-action timing. For follow-up triage, run `npx job-forge timeline:due` before reading tracker files; use `npx job-forge timeline:check --fail-on overdue` when a workflow must fail only on stale actions. Use `timeline:build` when a durable `.jobforge-timeline.json` artifact is useful.
|
|
102
|
+
why: `iso-timeline` is not an MCP and adds no prompt/tool-schema tokens; it turns timing windows over tracker/pipeline sources into executable local policy instead of repeated date math in model context
|
|
103
|
+
|
|
101
104
|
## Procedure
|
|
102
105
|
|
|
103
106
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
104
107
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
105
|
-
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
108
|
+
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use timeline due/check commands for follow-up timing [D20]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
106
109
|
4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
|
|
107
110
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
|
|
108
111
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
@@ -90,6 +90,10 @@ Postflight dispatch settlement (terminal, outside opencode):
|
|
|
90
90
|
npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
|
|
91
91
|
npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
|
|
92
92
|
|
|
93
|
+
Follow-up timeline (terminal, outside opencode):
|
|
94
|
+
npx job-forge timeline:due
|
|
95
|
+
npx job-forge timeline:check --fail-on overdue
|
|
96
|
+
|
|
93
97
|
Consumer migrations (terminal, outside opencode):
|
|
94
98
|
npx job-forge migrate:plan # preview package.json/.gitignore drift
|
|
95
99
|
npx job-forge migrate:apply # apply safe harness upgrade migrations
|
package/AGENTS.md
CHANGED
|
@@ -19,7 +19,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
19
19
|
- [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
|
|
20
20
|
why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
|
|
21
21
|
|
|
22
|
-
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
22
|
+
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, `.jobforge-timeline.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
23
23
|
why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
|
|
24
24
|
|
|
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.
|
|
@@ -93,11 +93,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
93
93
|
- [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
|
|
94
94
|
why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
|
|
95
95
|
|
|
96
|
+
- [D20] Treat `templates/timeline.json` as the source of truth for follow-up and next-action timing. For follow-up triage, run `npx job-forge timeline:due` before reading tracker files; use `npx job-forge timeline:check --fail-on overdue` when a workflow must fail only on stale actions. Use `timeline:build` when a durable `.jobforge-timeline.json` artifact is useful.
|
|
97
|
+
why: `iso-timeline` is not an MCP and adds no prompt/tool-schema tokens; it turns timing windows over tracker/pipeline sources into executable local policy instead of repeated date math in model context
|
|
98
|
+
|
|
96
99
|
## Procedure
|
|
97
100
|
|
|
98
101
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
99
102
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
100
|
-
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
103
|
+
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use timeline due/check commands for follow-up timing [D20]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
101
104
|
4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
|
|
102
105
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
|
|
103
106
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
package/CLAUDE.md
CHANGED
|
@@ -19,7 +19,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
19
19
|
- [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
|
|
20
20
|
why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
|
|
21
21
|
|
|
22
|
-
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
22
|
+
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, `.jobforge-timeline.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
23
23
|
why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
|
|
24
24
|
|
|
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.
|
|
@@ -93,11 +93,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
93
93
|
- [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
|
|
94
94
|
why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
|
|
95
95
|
|
|
96
|
+
- [D20] Treat `templates/timeline.json` as the source of truth for follow-up and next-action timing. For follow-up triage, run `npx job-forge timeline:due` before reading tracker files; use `npx job-forge timeline:check --fail-on overdue` when a workflow must fail only on stale actions. Use `timeline:build` when a durable `.jobforge-timeline.json` artifact is useful.
|
|
97
|
+
why: `iso-timeline` is not an MCP and adds no prompt/tool-schema tokens; it turns timing windows over tracker/pipeline sources into executable local policy instead of repeated date math in model context
|
|
98
|
+
|
|
96
99
|
## Procedure
|
|
97
100
|
|
|
98
101
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
99
102
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
100
|
-
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
103
|
+
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use timeline due/check commands for follow-up timing [D20]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
101
104
|
4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
|
|
102
105
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
|
|
103
106
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ The scaffolded `opencode.json` already has three MCPs wired up — they launch a
|
|
|
31
31
|
- **Gmail** — reads replies from recruiters
|
|
32
32
|
- **state-trace** — typed working memory for cross-session context (resumed batches, recent decisions, repeated portal quirks). Install once with `python3 -m pip install "state-trace[mcp]"`; the MCP command is `state-trace-mcp`.
|
|
33
33
|
|
|
34
|
-
JobForge also keeps MCP-free local workflow state and policy: `templates/canon.json` defines URL/company/role identity keys via `@razroo/iso-canon`, `templates/contracts.json` defines tracker/apply artifact shapes via `@razroo/iso-contract`, `templates/score.json` defines weighted scoring and gates via `@razroo/iso-score`, `templates/capabilities.json` defines role capability boundaries via `@razroo/iso-capabilities`, `templates/context.json` defines deterministic mode/reference bundles via `@razroo/iso-context`, `templates/preflight.json` defines safe dispatch rounds/gates via `@razroo/iso-preflight`, `templates/postflight.json` defines safe dispatch settlement via `@razroo/iso-postflight`, `templates/redact.json` defines safe-export redaction rules via `@razroo/iso-redact`, `templates/migrations.json` defines safe consumer-project upgrades via `@razroo/iso-migrate`, `templates/facts.json` defines source-backed fact extraction via `@razroo/iso-facts`, `.jobforge-ledger/events.jsonl` records duplicate/status events via `@razroo/iso-ledger`, `.jobforge-cache/` stores reusable JD/artifact content via `@razroo/iso-cache`, `.jobforge-index.json` indexes artifact source pointers via `@razroo/iso-index`,
|
|
34
|
+
JobForge also keeps MCP-free local workflow state and policy: `templates/canon.json` defines URL/company/role identity keys via `@razroo/iso-canon`, `templates/contracts.json` defines tracker/apply artifact shapes via `@razroo/iso-contract`, `templates/score.json` defines weighted scoring and gates via `@razroo/iso-score`, `templates/timeline.json` defines follow-up and next-action windows via `@razroo/iso-timeline`, `templates/capabilities.json` defines role capability boundaries via `@razroo/iso-capabilities`, `templates/context.json` defines deterministic mode/reference bundles via `@razroo/iso-context`, `templates/preflight.json` defines safe dispatch rounds/gates via `@razroo/iso-preflight`, `templates/postflight.json` defines safe dispatch settlement via `@razroo/iso-postflight`, `templates/redact.json` defines safe-export redaction rules via `@razroo/iso-redact`, `templates/migrations.json` defines safe consumer-project upgrades via `@razroo/iso-migrate`, `templates/facts.json` defines source-backed fact extraction via `@razroo/iso-facts`, `.jobforge-ledger/events.jsonl` records duplicate/status events via `@razroo/iso-ledger`, `.jobforge-cache/` stores reusable JD/artifact content via `@razroo/iso-cache`, `.jobforge-index.json` indexes artifact source pointers via `@razroo/iso-index`, `.jobforge-facts.json` materializes queryable facts with provenance, and `.jobforge-timeline.json` materializes due/overdue follow-up actions. None of these add always-on prompt or tool-schema tokens.
|
|
35
35
|
|
|
36
36
|
`npm install` also materializes symlinks for every supported agent harness — OpenCode, Cursor, Claude Code, and Codex — so you can run `opencode`, `cursor`, `claude`, or `codex` in the same project and each picks up the shared MCP config and instructions.
|
|
37
37
|
|
|
@@ -67,7 +67,7 @@ JobForge turns opencode into a full job search command center. Instead of manual
|
|
|
67
67
|
| **Anti-AI-Detection CVs** | Writing rules that avoid ATS filters on Indeed, LinkedIn, Workday |
|
|
68
68
|
| **6-Block Evaluation** | Role summary, CV match, level strategy, comp research, personalization, interview prep (STAR+R) |
|
|
69
69
|
| **Interview Story Bank** | Curated bank of 10-12 stories with match counts, archetype tags, and automatic pruning |
|
|
70
|
-
| **Follow-Up System** |
|
|
70
|
+
| **Follow-Up System** | `job-forge timeline:*` computes timing-based nudges from local tracker/pipeline sources: Applied 7+ days ago, Contacted 5+ days ago, Interview 1-day thank-you, and 7-day interview nudge. |
|
|
71
71
|
| **Gmail Integration** | MCP server configured to retrieve emails for interview callbacks, offer responses, and application status updates |
|
|
72
72
|
| **Rejection Analysis** | Captures stage + reason, surfaces patterns (archetype gaps, scoring miscalibration) |
|
|
73
73
|
| **Offer Negotiation** | Total comp breakdown, equity valuation, leverage from pipeline, counter-offer scripts |
|
|
@@ -78,7 +78,7 @@ JobForge turns opencode into a full job search command center. Instead of manual
|
|
|
78
78
|
| **Durable Batch Orchestration** | `batch-runner.sh` uses `@razroo/iso-orchestrator` for resumable bundle execution, bounded fan-out, mutexed state writes, and workflow records in `.jobforge-runs/`. |
|
|
79
79
|
| **Pipeline Integrity** | Automated merge, dedup, status normalization, health checks |
|
|
80
80
|
| **Cost-Aware Agent Routing** | Three subagents (`@general-free`, `@general-paid`, `@glm-minimal`) with per-task tool surfaces. On OpenCode, JobForge pins all tiers to `opencode-go/deepseek-v4-flash` so application runs avoid overloaded free-model pools. See [Subagent Routing in AGENTS.md](AGENTS.md) for the task-to-agent mapping. |
|
|
81
|
-
| **Trace + Telemetry + Guard + Contract + Score + Canon + Ledger + Capabilities + Context + Cache + Index + Facts + Preflight + Postflight + Redact + Migrate** | `job-forge trace:*` exposes local OpenCode transcripts, `job-forge telemetry:*` summarizes runs, `job-forge guard:*` audits deterministic policy rules, `templates/contracts.json` enforces artifact shape with `iso-contract`, `job-forge score:*` computes/checks weighted offer scores, `job-forge canon:*` derives stable URL/company/role identity keys, `job-forge ledger:*` queries append-only workflow state, `job-forge capabilities:*` checks role boundaries, `job-forge context:*` plans mode/reference context bundles, `job-forge cache:*` reuses fetched JD/artifact content, `job-forge index:*` queries compact source pointers, `job-forge facts:*` materializes source-backed job/application/candidate facts, `job-forge preflight:*` plans bounded apply dispatch rounds from file-backed candidate facts, `job-forge postflight:*` settles dispatch outcomes/artifacts/post-steps, `job-forge redact:*` sanitizes local exports, and `job-forge migrate:*` applies safe consumer-project upgrades without MCP/tool-schema overhead. |
|
|
81
|
+
| **Trace + Telemetry + Guard + Contract + Score + Canon + Ledger + Capabilities + Context + Cache + Index + Facts + Timeline + Preflight + Postflight + Redact + Migrate** | `job-forge trace:*` exposes local OpenCode transcripts, `job-forge telemetry:*` summarizes runs, `job-forge guard:*` audits deterministic policy rules, `templates/contracts.json` enforces artifact shape with `iso-contract`, `job-forge score:*` computes/checks weighted offer scores, `job-forge canon:*` derives stable URL/company/role identity keys, `job-forge ledger:*` queries append-only workflow state, `job-forge capabilities:*` checks role boundaries, `job-forge context:*` plans mode/reference context bundles, `job-forge cache:*` reuses fetched JD/artifact content, `job-forge index:*` queries compact source pointers, `job-forge facts:*` materializes source-backed job/application/candidate facts, `job-forge timeline:*` computes due/overdue follow-up actions, `job-forge preflight:*` plans bounded apply dispatch rounds from file-backed candidate facts, `job-forge postflight:*` settles dispatch outcomes/artifacts/post-steps, `job-forge redact:*` sanitizes local exports, and `job-forge migrate:*` applies safe consumer-project upgrades without MCP/tool-schema overhead. |
|
|
82
82
|
| **Token Cost Visibility** | `job-forge tokens --days 1` for per-session breakdown; `job-forge session-report --since-minutes 60 --log` to flag sessions over budget and append history to `data/token-usage.tsv`. Auto-logged after every batch run. |
|
|
83
83
|
|
|
84
84
|
## Usage
|
|
@@ -149,6 +149,7 @@ my-search/
|
|
|
149
149
|
├── .jobforge-cache/ # content-addressed local JD/artifact cache (personal, gitignored)
|
|
150
150
|
├── .jobforge-index.json # deterministic artifact lookup index (generated, gitignored)
|
|
151
151
|
├── .jobforge-facts.json # deterministic fact set with provenance (generated, gitignored)
|
|
152
|
+
├── .jobforge-timeline.json # deterministic follow-up action plan (generated, gitignored)
|
|
152
153
|
├── .jobforge-redacted/ # sanitized local exports (generated, gitignored)
|
|
153
154
|
├── reports/ # generated evaluation reports (personal, gitignored)
|
|
154
155
|
├── batch/{batch-input,batch-state}.tsv, tracker-additions/, logs/ # personal
|
|
@@ -166,7 +167,7 @@ my-search/
|
|
|
166
167
|
├── .opencode/skills/job-forge.md # → skill router
|
|
167
168
|
├── .opencode/agents/ # → @general-free, @general-paid, @glm-minimal
|
|
168
169
|
├── modes/ # → _shared.md + skill modes
|
|
169
|
-
├── templates/ # → states.yml, portals.example.yml, cv-template.html, canon.json, score.json, capabilities.json, context.json, index.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
|
|
170
|
+
├── templates/ # → states.yml, portals.example.yml, cv-template.html, canon.json, score.json, timeline.json, capabilities.json, context.json, index.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
|
|
170
171
|
├── batch/batch-prompt.md # → batch worker prompt
|
|
171
172
|
├── batch/batch-runner.sh # → parallel orchestrator
|
|
172
173
|
│
|
|
@@ -192,7 +193,7 @@ JobForge/
|
|
|
192
193
|
│ ├── sync.mjs # postinstall: creates symlinks in consumer project
|
|
193
194
|
│ └── create-job-forge.mjs # scaffolder
|
|
194
195
|
├── modes/ # _shared.md + 16 skill modes
|
|
195
|
-
├── templates/ # cv-template.html, portals.example.yml, states.yml, canon.json, score.json, capabilities.json, context.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
|
|
196
|
+
├── templates/ # cv-template.html, portals.example.yml, states.yml, canon.json, score.json, timeline.json, capabilities.json, context.json, facts.json, preflight.json, postflight.json, redact.json, migrations.json
|
|
196
197
|
├── config/profile.example.yml # template for consumer's profile.yml
|
|
197
198
|
├── batch/{batch-prompt.md,batch-runner.sh} # batch orchestrator
|
|
198
199
|
├── scripts/
|
|
@@ -204,6 +205,7 @@ JobForge/
|
|
|
204
205
|
│ ├── cache.mjs # iso-cache-backed local artifact cache CLI
|
|
205
206
|
│ ├── index.mjs # iso-index-backed artifact lookup CLI
|
|
206
207
|
│ ├── facts.mjs # iso-facts-backed local fact materialization
|
|
208
|
+
│ ├── timeline.mjs # iso-timeline-backed follow-up planning CLI
|
|
207
209
|
│ ├── score.mjs # iso-score-backed offer scoring CLI
|
|
208
210
|
│ ├── canon.mjs # iso-canon-backed identity normalization CLI
|
|
209
211
|
│ ├── preflight.mjs # iso-preflight-backed dispatch planning CLI
|
package/bin/job-forge.mjs
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* canon:* Compute deterministic identity keys via iso-canon
|
|
31
31
|
* preflight:* Plan safe dispatch rounds via iso-preflight
|
|
32
32
|
* postflight:* Settle dispatch outcomes via iso-postflight
|
|
33
|
+
* timeline:* Plan follow-up/next-action windows via iso-timeline
|
|
33
34
|
* redact:* Sanitize local exports via iso-redact
|
|
34
35
|
* migrate:* Apply deterministic consumer-project migrations via iso-migrate
|
|
35
36
|
* sync Re-run the harness symlink sync (bin/sync.mjs)
|
|
@@ -177,6 +178,17 @@ const postflightAliases = {
|
|
|
177
178
|
'postflight:path': 'path',
|
|
178
179
|
};
|
|
179
180
|
|
|
181
|
+
const timelineAliases = {
|
|
182
|
+
'timeline:status': 'status',
|
|
183
|
+
'timeline:build': 'build',
|
|
184
|
+
'timeline:plan': 'plan',
|
|
185
|
+
'timeline:due': 'due',
|
|
186
|
+
'timeline:check': 'check',
|
|
187
|
+
'timeline:verify': 'verify',
|
|
188
|
+
'timeline:explain': 'explain',
|
|
189
|
+
'timeline:path': 'path',
|
|
190
|
+
};
|
|
191
|
+
|
|
180
192
|
const redactAliases = {
|
|
181
193
|
'redact:scan': 'scan',
|
|
182
194
|
'redact:verify': 'verify',
|
|
@@ -263,6 +275,11 @@ Commands:
|
|
|
263
275
|
postflight:status Reconcile dispatch plan, outcomes, artifacts, and post-steps
|
|
264
276
|
postflight:check Fail unless a dispatched workflow is fully settled
|
|
265
277
|
postflight:explain Show the active postflight workflow policy
|
|
278
|
+
timeline:status Show local follow-up/next-action timeline status
|
|
279
|
+
timeline:build Build .jobforge-timeline.json from tracker/pipeline sources
|
|
280
|
+
timeline:due Show currently due/overdue follow-up actions
|
|
281
|
+
timeline:check Fail on due/overdue timeline actions per policy
|
|
282
|
+
timeline:explain Show the active timeline policy
|
|
266
283
|
redact:scan Scan local text for sensitive values before export
|
|
267
284
|
redact:verify Fail if local text still contains sensitive values
|
|
268
285
|
redact:apply Write a sanitized copy of local text
|
|
@@ -319,6 +336,8 @@ Pass --help after a command to see its own flags, e.g.:
|
|
|
319
336
|
job-forge preflight:check --candidates batch/preflight-candidates.json
|
|
320
337
|
job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
|
|
321
338
|
job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
|
|
339
|
+
job-forge timeline:due
|
|
340
|
+
job-forge timeline:check --fail-on overdue
|
|
322
341
|
job-forge redact:scan --input raw-session.jsonl
|
|
323
342
|
job-forge redact:apply --input raw-session.jsonl --output .jobforge-redacted/session.jsonl
|
|
324
343
|
job-forge migrate:check
|
|
@@ -527,6 +546,21 @@ if (cmd === 'postflight' || postflightAliases[cmd]) {
|
|
|
527
546
|
process.exit(result.status ?? 1);
|
|
528
547
|
}
|
|
529
548
|
|
|
549
|
+
if (cmd === 'timeline' || timelineAliases[cmd]) {
|
|
550
|
+
const timelineArgs = cmd === 'timeline'
|
|
551
|
+
? (rest.length === 0 ? ['help'] : rest)
|
|
552
|
+
: [timelineAliases[cmd], ...rest];
|
|
553
|
+
|
|
554
|
+
const scriptPath = join(PKG_ROOT, 'scripts/timeline.mjs');
|
|
555
|
+
const result = spawnSync(process.execPath, [scriptPath, ...timelineArgs], {
|
|
556
|
+
stdio: 'inherit',
|
|
557
|
+
cwd: PROJECT_DIR,
|
|
558
|
+
env: process.env,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
process.exit(result.status ?? 1);
|
|
562
|
+
}
|
|
563
|
+
|
|
530
564
|
if (cmd === 'redact' || redactAliases[cmd]) {
|
|
531
565
|
const redactArgs = cmd === 'redact'
|
|
532
566
|
? (rest.length === 0 ? ['help'] : rest)
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -32,7 +32,7 @@ my-search/
|
|
|
32
32
|
├── .opencode/skills/job-forge.md # → skill router
|
|
33
33
|
├── .opencode/agents/ # → @general-free, @general-paid, @glm-minimal
|
|
34
34
|
├── modes/ # → mode files
|
|
35
|
-
├── templates/ # → states.yml, portals.example.yml, cv-template.html, score.json, preflight.json, postflight.json
|
|
35
|
+
├── templates/ # → states.yml, portals.example.yml, cv-template.html, score.json, timeline.json, preflight.json, postflight.json
|
|
36
36
|
├── batch/batch-prompt.md # → batch worker prompt
|
|
37
37
|
├── batch/batch-runner.sh # → parallel orchestrator
|
|
38
38
|
└── node_modules/job-forge/ # harness, installed from npm
|
|
@@ -163,12 +163,14 @@ data/pipeline.md → Pending URLs and `local:jds/...` inbox (see modes/p
|
|
|
163
163
|
.jobforge-ledger/events.jsonl → Append-only workflow events for cheap local duplicate/status checks
|
|
164
164
|
.jobforge-index.json → Deterministic artifact lookup index built from templates/index.json
|
|
165
165
|
.jobforge-facts.json → Deterministic fact set built from templates/facts.json
|
|
166
|
+
.jobforge-timeline.json → Deterministic follow-up action plan built from templates/timeline.json
|
|
166
167
|
jds/*.md → Saved job descriptions referenced from the pipeline (`local:jds/{file}`)
|
|
167
168
|
templates/states.yml → Canonical status values
|
|
168
169
|
templates/canon.json → Canonical URL/company/role identity keys
|
|
169
170
|
templates/score.json → Canonical weighted scoring rubric and gates
|
|
170
171
|
templates/context.json → Deterministic mode/reference context bundle policy
|
|
171
172
|
templates/facts.json → Source-backed fact extraction policy
|
|
173
|
+
templates/timeline.json → Follow-up and next-action timing policy
|
|
172
174
|
templates/preflight.json → Safe apply dispatch rounds/gates policy
|
|
173
175
|
templates/postflight.json → Safe apply dispatch settlement policy
|
|
174
176
|
templates/migrations.json → Safe consumer-project upgrade policy
|
|
@@ -186,8 +188,10 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
|
|
|
186
188
|
- Ledger: `.jobforge-ledger/events.jsonl` (created by `job-forge ledger:rebuild`, `tracker-line --write`, or `merge`; gitignored personal state)
|
|
187
189
|
- Index: `.jobforge-index.json` (created on demand by `job-forge index:*`; gitignored local lookup state)
|
|
188
190
|
- Facts: `.jobforge-facts.json` (created on demand by `job-forge facts:*`; gitignored local fact state)
|
|
191
|
+
- Timeline: `.jobforge-timeline.json` (created on demand by `job-forge timeline:*`; gitignored local next-action state)
|
|
189
192
|
- Canon: `templates/canon.json` (identity rules inspected with `job-forge canon:*`)
|
|
190
193
|
- Score: `templates/score.json` (weighted rubric and gates inspected with `job-forge score:*`)
|
|
194
|
+
- Timeline policy: `templates/timeline.json` (follow-up windows inspected with `job-forge timeline:*`)
|
|
191
195
|
- Preflight: `templates/preflight.json` (dispatch rounds/gates inspected with `job-forge preflight:*`)
|
|
192
196
|
- Postflight: `templates/postflight.json` (dispatch outcomes/artifacts/post-steps inspected with `job-forge postflight:*`)
|
|
193
197
|
- Migrations: `templates/migrations.json` (applied by `job-forge sync` and inspectable with `job-forge migrate:*`)
|
|
@@ -196,7 +200,7 @@ Create `data/pipeline.md` when you start using the URL inbox (`/job-forge pipeli
|
|
|
196
200
|
|
|
197
201
|
## Pipeline Integrity
|
|
198
202
|
|
|
199
|
-
From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify-pipeline.mjs`. When a tracker file exists, it validates canonical statuses (using `templates/states.yml` when that file is present and parseable), validates every tracker row against `templates/contracts.json`, warns on probable duplicate company/role rows, checks that report column markdown links resolve to files in the repo, validates score column format (`X.X/5`, `N/A`, or `DUP`), rejects table rows with too few columns, flags markdown bold inside the score column, and warns if any `batch/tracker-additions/*.tsv` files are still waiting to be merged. If `.jobforge-ledger/events.jsonl` exists, verify also validates the append-only ledger. If `.jobforge-index.json` exists, verify validates the artifact index. If `.jobforge-facts.json` exists, verify validates the materialized fact set. It also compares state ids from `templates/states.yml` to an internal fallback list and warns when the two sets drift. **Fresh clone:** the command exits successfully when neither `data/applications.md` nor root `applications.md` exists yet; pending-TSV and states-drift checks still run so contributors see unmerged batch output early. Optional setup validation after you add `cv.md` and `config/profile.yml`: `npm run sync-check` (`cv-sync-check.mjs`).
|
|
203
|
+
From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify-pipeline.mjs`. When a tracker file exists, it validates canonical statuses (using `templates/states.yml` when that file is present and parseable), validates every tracker row against `templates/contracts.json`, warns on probable duplicate company/role rows, checks that report column markdown links resolve to files in the repo, validates score column format (`X.X/5`, `N/A`, or `DUP`), rejects table rows with too few columns, flags markdown bold inside the score column, and warns if any `batch/tracker-additions/*.tsv` files are still waiting to be merged. If `.jobforge-ledger/events.jsonl` exists, verify also validates the append-only ledger. If `.jobforge-index.json` exists, verify validates the artifact index. If `.jobforge-facts.json` exists, verify validates the materialized fact set. If `.jobforge-timeline.json` exists, verify validates the follow-up timeline. It also compares state ids from `templates/states.yml` to an internal fallback list and warns when the two sets drift. **Fresh clone:** the command exits successfully when neither `data/applications.md` nor root `applications.md` exists yet; pending-TSV and states-drift checks still run so contributors see unmerged batch output early. Optional setup validation after you add `cv.md` and `config/profile.yml`: `npm run sync-check` (`cv-sync-check.mjs`).
|
|
200
204
|
|
|
201
205
|
**`verify-pipeline.mjs` checks (same order as the script header):**
|
|
202
206
|
|
|
@@ -212,8 +216,9 @@ From the project root, `npx job-forge verify` (or `npm run verify`) runs `verify
|
|
|
212
216
|
10. Validate `.jobforge-ledger/events.jsonl` when present.
|
|
213
217
|
11. Validate `.jobforge-index.json` when present.
|
|
214
218
|
12. Validate `.jobforge-facts.json` when present.
|
|
219
|
+
13. Validate `.jobforge-timeline.json` when present.
|
|
215
220
|
|
|
216
|
-
When the tracker file is missing, checks 1-6 and 8 are skipped; checks 7, 9, 10, and
|
|
221
|
+
When the tracker file is missing, checks 1-6 and 8 are skipped; checks 7, 9, 10, 11, 12, and 13 still run when applicable.
|
|
217
222
|
|
|
218
223
|
## Contributing touchpoints
|
|
219
224
|
|
|
@@ -238,6 +243,7 @@ Scripts maintain data consistency. In a consumer project they're invoked via the
|
|
|
238
243
|
| `scripts/ledger.mjs` | `npx job-forge ledger:status` / `ledger:has` / `ledger:rebuild` | Deterministic `@razroo/iso-ledger` state over tracker, TSV, and pipeline files |
|
|
239
244
|
| `scripts/index.mjs` | `npx job-forge index:status` / `index:has` / `index:query` | Deterministic `@razroo/iso-index` lookup over reports, tracker rows, TSVs, pipeline, scan history, and ledger events |
|
|
240
245
|
| `scripts/facts.mjs` | `npx job-forge facts:status` / `facts:has` / `facts:query` | Deterministic `@razroo/iso-facts` materialization over job URLs, scores, application statuses, preflight candidates, scan history, and ledger events |
|
|
246
|
+
| `scripts/timeline.mjs` | `npx job-forge timeline:due` / `timeline:check` / `timeline:build` | Deterministic `@razroo/iso-timeline` follow-up and next-action planning over tracker rows and dated pipeline items |
|
|
241
247
|
| `scripts/score.mjs` | `npx job-forge score:check` / `score:gate` / `score:explain` | Deterministic `@razroo/iso-score` checks for weighted offer scores, threshold booleans, recommendations, and score gates |
|
|
242
248
|
| `scripts/canon.mjs` | `npx job-forge canon:normalize` / `canon:key` / `canon:compare` | Deterministic `@razroo/iso-canon` identity normalization for URLs, companies, roles, and company+role pairs |
|
|
243
249
|
| `scripts/context.mjs` | `npx job-forge context:list` / `context:plan` / `context:check` / `context:render` | Deterministic `@razroo/iso-context` mode/reference context bundle planning and rendering |
|
package/docs/CUSTOMIZATION.md
CHANGED
|
@@ -158,6 +158,10 @@ Artifact lookup policy lives in `templates/index.json` and is built locally by `
|
|
|
158
158
|
|
|
159
159
|
Fact extraction policy lives in `templates/facts.json` and is built locally by `@razroo/iso-facts`. Use `job-forge facts:query --fact job.url`, `job-forge facts:has --fact application.status --key "company-role:acme:staff-engineer"`, and `job-forge facts:verify` to work with compact source-backed facts instead of rereading reports, tracker day files, TSVs, candidate JSON, scan history, and ledger events. Query, has, verify, and check rebuild `.jobforge-facts.json` on demand, so scaffolded projects need no setup. JobForge canonicalizes company/role and URL fact keys through `templates/canon.json` before writing the fact set. This is not an MCP and does not add prompt or tool-schema tokens.
|
|
160
160
|
|
|
161
|
+
## JobForge timeline policy
|
|
162
|
+
|
|
163
|
+
Follow-up and next-action policy lives in `templates/timeline.json` and is planned locally by `@razroo/iso-timeline`. Use `job-forge timeline:due` to list due/overdue nudges, `job-forge timeline:check --fail-on overdue` to fail only when action is stale, and `job-forge timeline:build` to materialize `.jobforge-timeline-events.jsonl` plus `.jobforge-timeline.json` for inspection. It reads tracker rows and dated pipeline items from local files, so scaffolded projects need no setup. This is not an MCP and does not add prompt or tool-schema tokens.
|
|
164
|
+
|
|
161
165
|
## JobForge scoring policy
|
|
162
166
|
|
|
163
167
|
Weighted scoring policy lives in `templates/score.json` and is enforced locally by `@razroo/iso-score`. Use `job-forge score:check --input <score.json>` to validate emitted report score JSON, `job-forge score:gate --input <score.json> --gate apply` to check an application threshold, and `job-forge score:explain` to inspect the active dimensions, weights, bands, and gates. Custom forks can change weights or thresholds in `templates/score.json`, but keep the dimension ids aligned with `modes/_shared.md` and report rendering.
|
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`, `canon`, `context`, `index`, `facts`, `preflight`, `postflight`, `redact`, `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`, `canon`, `context`, `index`, `facts`, `timeline`, `preflight`, `postflight`, `redact`, `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 build:dashboard`). | [CONTRIBUTING.md — Development](../CONTRIBUTING.md#development). |
|
package/docs/SETUP.md
CHANGED
|
@@ -135,6 +135,8 @@ From your project root, these commands maintain the tracker and pipeline checks.
|
|
|
135
135
|
| Inspect local JD/artifact cache | `npx job-forge cache:status` | `npm run cache:status` |
|
|
136
136
|
| Inspect local artifact index | `npx job-forge index:status` | `npm run index:status` |
|
|
137
137
|
| Inspect local materialized facts | `npx job-forge facts:status` | `npm run facts:status` |
|
|
138
|
+
| Show due follow-up actions | `npx job-forge timeline:due` | `npm run timeline:due` |
|
|
139
|
+
| Fail on stale follow-up actions | `npx job-forge timeline:check --fail-on overdue` | `npm run timeline:check -- --fail-on overdue` |
|
|
138
140
|
| Plan safe application dispatch rounds | `npx job-forge preflight:plan --candidates batch/preflight-candidates.json` | `npm run preflight:plan -- --candidates ...` |
|
|
139
141
|
| Fail on blocked preflight candidates | `npx job-forge preflight:check --candidates batch/preflight-candidates.json` | `npm run preflight:check -- --candidates ...` |
|
|
140
142
|
| Settle dispatch outcomes after a round | `npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json` | `npm run postflight:status -- --plan ... --outcomes ...` |
|
|
@@ -161,6 +163,7 @@ From your project root, these commands maintain the tracker and pipeline checks.
|
|
|
161
163
|
| Check/reuse cached JD content | `npx job-forge cache:has --url <url>` / `npx job-forge cache:get --url <url>` | `npm run cache:has -- --url ...` |
|
|
162
164
|
| Query local artifact pointers | `npx job-forge index:query "Acme"` / `npx job-forge index:has --key company-role:acme:staff-engineer` | `npm run index:query -- Acme` |
|
|
163
165
|
| Query local source-backed facts | `npx job-forge facts:query --fact job.url` / `npx job-forge facts:has --fact application.status --key company-role:acme:staff-engineer` | `npm run facts:query -- --fact job.url` |
|
|
166
|
+
| Materialize follow-up timeline | `npx job-forge timeline:build` | `npm run timeline:build` |
|
|
164
167
|
| Apply safe consumer migrations | `npx job-forge migrate:apply` | `npm run migrate:apply` |
|
|
165
168
|
| Re-create harness symlinks | `npx job-forge sync` | `npm run sync` |
|
|
166
169
|
| Build optional dashboard TUI (Go on `PATH`) | `(cd node_modules/job-forge/dashboard && go build .)` | `npm run build:dashboard` (harness repo only) |
|
|
@@ -93,6 +93,10 @@ Postflight dispatch settlement (terminal, outside opencode):
|
|
|
93
93
|
npx job-forge postflight:status --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
|
|
94
94
|
npx job-forge postflight:check --plan batch/preflight-plan.json --outcomes batch/postflight-outcomes.json
|
|
95
95
|
|
|
96
|
+
Follow-up timeline (terminal, outside opencode):
|
|
97
|
+
npx job-forge timeline:due
|
|
98
|
+
npx job-forge timeline:check --fail-on overdue
|
|
99
|
+
|
|
96
100
|
Consumer migrations (terminal, outside opencode):
|
|
97
101
|
npx job-forge migrate:plan # preview package.json/.gitignore drift
|
|
98
102
|
npx job-forge migrate:apply # apply safe harness upgrade migrations
|
package/iso/instructions.md
CHANGED
|
@@ -19,7 +19,7 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
19
19
|
- [H5] Re-dispatch the same company only AFTER the previous subagent returns. Never fire the same `task` twice while the first is still in flight.
|
|
20
20
|
why: two in-flight subagents for the same URL race on Geometra sessions and on tracker TSV writes, corrupting state and sometimes double-submitting
|
|
21
21
|
|
|
22
|
-
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
22
|
+
- [H5b] Do not use `task` to poll task status. If OpenCode returns a task/session id without a final result, record the id, stop dispatching new rounds, and tell the user the round is still in flight. When the user asks to check later, inspect authoritative files (`batch/tracker-additions/*.tsv`, `batch/tracker-additions/merged/*.tsv`, day files, `.jobforge-ledger/events.jsonl`, `.jobforge-index.json`, `.jobforge-facts.json`, `.jobforge-timeline.json`, or `iso-trace`) rather than spawning a "check task status" subagent.
|
|
23
23
|
why: OpenCode status prompts can be delivered into the target subagent as a new user message; a 2026-04-25 trace caused a subagent to call `task` recursively instead of finishing the application
|
|
24
24
|
|
|
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.
|
|
@@ -93,11 +93,14 @@ AI-powered job search pipeline: scans portals, evaluates offers, generates CVs v
|
|
|
93
93
|
- [D19] Treat `templates/score.json` as the source of truth for offer scoring weights, bands, and gates. After emitting a report score JSON that will drive PDF/application/batch decisions, run `npx job-forge score:check --input <file>`; for apply decisions run `npx job-forge score:gate --input <file> --gate apply`. Do not recalculate weighted totals or thresholds manually when the local helper can check them.
|
|
94
94
|
why: `iso-score` is not an MCP and adds no prompt/tool-schema tokens; it makes scoring math, recommendation bands, and threshold booleans executable local policy instead of repeated model prose
|
|
95
95
|
|
|
96
|
+
- [D20] Treat `templates/timeline.json` as the source of truth for follow-up and next-action timing. For follow-up triage, run `npx job-forge timeline:due` before reading tracker files; use `npx job-forge timeline:check --fail-on overdue` when a workflow must fail only on stale actions. Use `timeline:build` when a durable `.jobforge-timeline.json` artifact is useful.
|
|
97
|
+
why: `iso-timeline` is not an MCP and adds no prompt/tool-schema tokens; it turns timing windows over tracker/pipeline sources into executable local policy instead of repeated date math in model context
|
|
98
|
+
|
|
96
99
|
## Procedure
|
|
97
100
|
|
|
98
101
|
1. Check `cv.md`, `profile.yml`, and `portals.yml`; onboard if any file is missing.
|
|
99
102
|
2. Pick and name the mode from **Routing** [D6]. No match → ask; do not guess.
|
|
100
|
-
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
103
|
+
3. Read the active mode file [D3]. Use context bundle checks when changing context loads [D11]. Check cached artifacts before URL/JD refetches [D12]. Use artifact index lookups before broad file reads when they can answer the question [D13]. Use materialized facts when a fact query can answer the question [D13b]. Use canonical identity keys for duplicate checks [D15]. Use score checks/gates for scoring decisions [D19]. Use timeline due/check commands for follow-up timing [D20]. Use migration checks for harness drift [D14]. Use redaction checks before exporting local artifacts [D18]. Decide inline vs delegated work [D1].
|
|
101
104
|
4. Prepare Geometra dispatches: cleanup [H3], canon/index/facts/ledger prefilter when useful [D8, D13, D13b, D15], dedupe [H2], location filter [D5], materialize candidate facts/gates and run preflight plan/check [D16], routing [D2, D10], proxy prompt hygiene [H8].
|
|
102
105
|
5. Dispatch at most 2 tasks per round [H1]; wait for final outcomes, not just task ids [H5b], then settle the round with postflight status [D17].
|
|
103
106
|
6. Keep multi-job form-filling out of the orchestrator [H4].
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
checkTimeline,
|
|
5
|
+
filterTimelineResult,
|
|
6
|
+
loadTimelineConfig,
|
|
7
|
+
parseJson,
|
|
8
|
+
parseJsonLines,
|
|
9
|
+
planTimeline,
|
|
10
|
+
verifyTimelineResult,
|
|
11
|
+
} from '@razroo/iso-timeline';
|
|
12
|
+
import { DATA_APPS_DIR, PROJECT_DIR, readAllEntries } from '../tracker-lib.mjs';
|
|
13
|
+
import { jobForgeCompanyRoleKey, jobForgeUrlKey, legacyCompanyRoleKey, legacyUrlKey } from './jobforge-canon.mjs';
|
|
14
|
+
|
|
15
|
+
export const TIMELINE_CONFIG_FILE = 'templates/timeline.json';
|
|
16
|
+
export const TIMELINE_FILE = '.jobforge-timeline.json';
|
|
17
|
+
export const TIMELINE_EVENTS_FILE = '.jobforge-timeline-events.jsonl';
|
|
18
|
+
export const USER_TIMELINE_EVENTS_FILE = 'data/timeline-events.jsonl';
|
|
19
|
+
|
|
20
|
+
export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
|
|
21
|
+
return projectDir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function jobForgeTimelineConfigPath(projectDir = resolveProjectDir()) {
|
|
25
|
+
return process.env.JOB_FORGE_TIMELINE_CONFIG || join(projectDir, TIMELINE_CONFIG_FILE);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function jobForgeTimelinePath(projectDir = resolveProjectDir()) {
|
|
29
|
+
return process.env.JOB_FORGE_TIMELINE || join(projectDir, TIMELINE_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function jobForgeTimelineEventsPath(projectDir = resolveProjectDir()) {
|
|
33
|
+
return process.env.JOB_FORGE_TIMELINE_EVENTS || join(projectDir, TIMELINE_EVENTS_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readJobForgeTimelineConfig(projectDir = resolveProjectDir()) {
|
|
37
|
+
const path = jobForgeTimelineConfigPath(projectDir);
|
|
38
|
+
return loadTimelineConfig(parseJson(readFileSync(path, 'utf8'), path));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function timelineExists(projectDir = resolveProjectDir()) {
|
|
42
|
+
return existsSync(jobForgeTimelinePath(projectDir));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function timelineEventsExist(projectDir = resolveProjectDir()) {
|
|
46
|
+
return existsSync(jobForgeTimelineEventsPath(projectDir));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readJobForgeTimeline(projectDir = resolveProjectDir()) {
|
|
50
|
+
const path = jobForgeTimelinePath(projectDir);
|
|
51
|
+
return parseJson(readFileSync(path, 'utf8'), path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildJobForgeTimelineEvents(projectDir = resolveProjectDir()) {
|
|
55
|
+
const events = [
|
|
56
|
+
...applicationEvents(projectDir),
|
|
57
|
+
...pipelineEvents(projectDir),
|
|
58
|
+
...userEvents(projectDir),
|
|
59
|
+
];
|
|
60
|
+
events.sort(compareEvents);
|
|
61
|
+
return events;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function writeJobForgeTimelineEvents(events, options = {}, projectDir = resolveProjectDir()) {
|
|
65
|
+
const out = options.out || jobForgeTimelineEventsPath(projectDir);
|
|
66
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
67
|
+
const content = events.map((event) => JSON.stringify(event)).join('\n');
|
|
68
|
+
writeFileSync(out, `${content}${content ? '\n' : ''}`, 'utf8');
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function planJobForgeTimeline(options = {}, projectDir = resolveProjectDir()) {
|
|
73
|
+
const config = readJobForgeTimelineConfig(projectDir);
|
|
74
|
+
const events = options.events || buildJobForgeTimelineEvents(projectDir);
|
|
75
|
+
return planTimeline(config, events, { now: options.now });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function dueJobForgeTimeline(options = {}, projectDir = resolveProjectDir()) {
|
|
79
|
+
return filterTimelineResult(planJobForgeTimeline(options, projectDir), ['overdue', 'due']);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function checkJobForgeTimeline(options = {}, projectDir = resolveProjectDir()) {
|
|
83
|
+
const config = readJobForgeTimelineConfig(projectDir);
|
|
84
|
+
const events = options.events || buildJobForgeTimelineEvents(projectDir);
|
|
85
|
+
return checkTimeline(config, events, { now: options.now, failOn: options.failOn });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildJobForgeTimeline(options = {}, projectDir = resolveProjectDir()) {
|
|
89
|
+
const events = buildJobForgeTimelineEvents(projectDir);
|
|
90
|
+
const result = planJobForgeTimeline({ now: options.now, events }, projectDir);
|
|
91
|
+
const eventsOut = writeJobForgeTimelineEvents(events, { out: options.eventsOut }, projectDir);
|
|
92
|
+
const out = options.out || jobForgeTimelinePath(projectDir);
|
|
93
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
94
|
+
writeFileSync(out, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
95
|
+
return { result, events, out, eventsOut };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function verifyJobForgeTimeline(options = {}, projectDir = resolveProjectDir()) {
|
|
99
|
+
const result = options.result || readJobForgeTimeline(projectDir);
|
|
100
|
+
return verifyTimelineResult(result);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function jobForgeTimelineSummary(projectDir = resolveProjectDir()) {
|
|
104
|
+
if (!timelineExists(projectDir)) {
|
|
105
|
+
return {
|
|
106
|
+
path: jobForgeTimelinePath(projectDir),
|
|
107
|
+
eventsPath: jobForgeTimelineEventsPath(projectDir),
|
|
108
|
+
config: jobForgeTimelineConfigPath(projectDir),
|
|
109
|
+
exists: false,
|
|
110
|
+
eventsExists: timelineEventsExist(projectDir),
|
|
111
|
+
items: 0,
|
|
112
|
+
due: 0,
|
|
113
|
+
overdue: 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const result = readJobForgeTimeline(projectDir);
|
|
117
|
+
return {
|
|
118
|
+
path: jobForgeTimelinePath(projectDir),
|
|
119
|
+
eventsPath: jobForgeTimelineEventsPath(projectDir),
|
|
120
|
+
config: jobForgeTimelineConfigPath(projectDir),
|
|
121
|
+
exists: true,
|
|
122
|
+
eventsExists: timelineEventsExist(projectDir),
|
|
123
|
+
items: result.stats?.total || 0,
|
|
124
|
+
due: result.stats?.due || 0,
|
|
125
|
+
overdue: result.stats?.overdue || 0,
|
|
126
|
+
generatedAt: result.generatedAt,
|
|
127
|
+
id: result.id,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applicationEvents(projectDir) {
|
|
132
|
+
const { entries } = readAllEntries();
|
|
133
|
+
return entries
|
|
134
|
+
.map((entry) => applicationEvent(entry, projectDir))
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function applicationEvent(entry, projectDir) {
|
|
139
|
+
const at = dateToIso(entry.date);
|
|
140
|
+
if (!at || !entry.company || !entry.role || !entry.status) return null;
|
|
141
|
+
const status = canonicalStatus(entry.status);
|
|
142
|
+
const key = safeCompanyRoleKey(entry.company, entry.role, projectDir);
|
|
143
|
+
return {
|
|
144
|
+
id: `jobforge:application-status:${entry.num}:${key}:${at}`,
|
|
145
|
+
key,
|
|
146
|
+
type: 'application.status',
|
|
147
|
+
at,
|
|
148
|
+
data: compactObject({
|
|
149
|
+
num: entry.num,
|
|
150
|
+
date: entry.date,
|
|
151
|
+
company: entry.company,
|
|
152
|
+
role: entry.role,
|
|
153
|
+
score: entry.score,
|
|
154
|
+
status,
|
|
155
|
+
pdf: entry.pdf,
|
|
156
|
+
report: entry.report,
|
|
157
|
+
notes: entry.notes,
|
|
158
|
+
}),
|
|
159
|
+
source: sourceForApplication(entry, projectDir),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pipelineEvents(projectDir) {
|
|
164
|
+
const pipelinePath = join(projectDir, 'data', 'pipeline.md');
|
|
165
|
+
if (!existsSync(pipelinePath)) return [];
|
|
166
|
+
const scanDates = scanHistoryDates(projectDir);
|
|
167
|
+
const lines = readFileSync(pipelinePath, 'utf8').split('\n');
|
|
168
|
+
const events = [];
|
|
169
|
+
lines.forEach((line, index) => {
|
|
170
|
+
const match = line.match(/^\s*-\s*\[([ xX])\]\s+(https?:\/\/[^|\s#]+)(.*)$/);
|
|
171
|
+
if (!match) return;
|
|
172
|
+
const url = match[2].trim();
|
|
173
|
+
const at = dateToIso(scanDates.get(url) || firstDateInText(line));
|
|
174
|
+
if (!at) return;
|
|
175
|
+
const fields = (match[3] || '').split('|').map((field) => field.trim()).filter(Boolean);
|
|
176
|
+
const status = match[1].toLowerCase() === 'x' ? 'processed' : 'pending';
|
|
177
|
+
const key = safeUrlKey(url, projectDir);
|
|
178
|
+
events.push({
|
|
179
|
+
id: `jobforge:pipeline:${status}:${key}:${at}`,
|
|
180
|
+
key,
|
|
181
|
+
type: status === 'processed' ? 'pipeline.processed' : 'pipeline.item',
|
|
182
|
+
at,
|
|
183
|
+
data: compactObject({
|
|
184
|
+
status,
|
|
185
|
+
url,
|
|
186
|
+
company: fields[0],
|
|
187
|
+
role: fields[1],
|
|
188
|
+
}),
|
|
189
|
+
source: {
|
|
190
|
+
path: 'data/pipeline.md',
|
|
191
|
+
line: index + 1,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
return events;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function userEvents(projectDir) {
|
|
199
|
+
const path = join(projectDir, USER_TIMELINE_EVENTS_FILE);
|
|
200
|
+
if (!existsSync(path)) return [];
|
|
201
|
+
return parseJsonLines(readFileSync(path, 'utf8'), path);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function scanHistoryDates(projectDir) {
|
|
205
|
+
const path = join(projectDir, 'data', 'scan-history.tsv');
|
|
206
|
+
const dates = new Map();
|
|
207
|
+
if (!existsSync(path)) return dates;
|
|
208
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
if (!line.trim()) continue;
|
|
211
|
+
const parts = line.split('\t');
|
|
212
|
+
if (parts.length < 4) continue;
|
|
213
|
+
const date = parts[0]?.trim();
|
|
214
|
+
const url = parts[3]?.trim();
|
|
215
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(date) && /^https?:\/\//.test(url)) dates.set(url, date);
|
|
216
|
+
}
|
|
217
|
+
return dates;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sourceForApplication(entry, projectDir) {
|
|
221
|
+
const raw = String(entry._sourceFile || '');
|
|
222
|
+
if (/^\d{4}-\d{2}-\d{2}\.md$/.test(raw)) {
|
|
223
|
+
return { path: relativePath(projectDir, join(DATA_APPS_DIR, raw)) };
|
|
224
|
+
}
|
|
225
|
+
if (raw) return { path: relativePath(projectDir, raw) };
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function dateToIso(value) {
|
|
230
|
+
const text = String(value || '').trim();
|
|
231
|
+
if (!text) return null;
|
|
232
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return `${text}T12:00:00.000Z`;
|
|
233
|
+
const parsed = new Date(text);
|
|
234
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function firstDateInText(value) {
|
|
238
|
+
return String(value || '').match(/\b\d{4}-\d{2}-\d{2}\b/)?.[0] || '';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function canonicalStatus(value) {
|
|
242
|
+
const text = String(value || '').trim();
|
|
243
|
+
const lower = text.toLowerCase();
|
|
244
|
+
const map = new Map([
|
|
245
|
+
['evaluated', 'Evaluated'],
|
|
246
|
+
['applied', 'Applied'],
|
|
247
|
+
['responded', 'Responded'],
|
|
248
|
+
['contacted', 'Contacted'],
|
|
249
|
+
['interview', 'Interview'],
|
|
250
|
+
['offer', 'Offer'],
|
|
251
|
+
['rejected', 'Rejected'],
|
|
252
|
+
['discarded', 'Discarded'],
|
|
253
|
+
['failed', 'Failed'],
|
|
254
|
+
['skip', 'SKIP'],
|
|
255
|
+
]);
|
|
256
|
+
return map.get(lower) || text;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function safeCompanyRoleKey(company, role, projectDir) {
|
|
260
|
+
try {
|
|
261
|
+
return jobForgeCompanyRoleKey(company, role, projectDir);
|
|
262
|
+
} catch {
|
|
263
|
+
return legacyCompanyRoleKey(company, role);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function safeUrlKey(url, projectDir) {
|
|
268
|
+
try {
|
|
269
|
+
return jobForgeUrlKey(url, projectDir);
|
|
270
|
+
} catch {
|
|
271
|
+
return legacyUrlKey(url);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function compactObject(obj) {
|
|
276
|
+
const out = {};
|
|
277
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
278
|
+
if (value === undefined || value === null || value === '') continue;
|
|
279
|
+
out[key] = value;
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function compareEvents(a, b) {
|
|
285
|
+
return `${a.at}\0${a.key}\0${a.type}\0${a.id || ''}`.localeCompare(`${b.at}\0${b.key}\0${b.type}\0${b.id || ''}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function relativePath(projectDir, value) {
|
|
289
|
+
const text = String(value || '');
|
|
290
|
+
if (!text) return '';
|
|
291
|
+
const rel = relative(projectDir, text);
|
|
292
|
+
if (rel && !rel.startsWith('..') && !isAbsolute(rel)) return rel.replace(/\\/g, '/');
|
|
293
|
+
return text.replace(/\\/g, '/');
|
|
294
|
+
}
|
package/modes/followup.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Mode: followup — Follow-Up Timing & Nudge System
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Uses `job-forge timeline:*` to scan local tracker and dated pipeline sources for entries that need follow-up action based on their current state and how long they've been in that state.
|
|
4
4
|
|
|
5
5
|
**This mode is read-only on existing pipeline logic.** It reads the tracker and suggests actions — it never changes scores, reports, or pipeline behavior.
|
|
6
6
|
|
|
@@ -19,11 +19,11 @@ Scans all day files in `data/applications/` for entries that need follow-up acti
|
|
|
19
19
|
|
|
20
20
|
## Run This Workflow
|
|
21
21
|
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
26
|
-
5.
|
|
22
|
+
1. Run `npx job-forge timeline:due` first. It rebuilds the due queue from local tracker/pipeline sources without loading growing files into prompt context.
|
|
23
|
+
2. If the user wants a persistent artifact, run `npx job-forge timeline:build`.
|
|
24
|
+
3. Use `npx job-forge timeline:check --fail-on overdue` when the workflow should fail only on stale actions.
|
|
25
|
+
4. Present the action list grouped by `OVERDUE`, `DUE`, and upcoming manual context if needed.
|
|
26
|
+
5. Only read individual tracker/report files after the user selects an action that needs message drafting.
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
## Follow-Up Actions — {today's date}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.33",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -78,6 +78,13 @@
|
|
|
78
78
|
"postflight:status": "node bin/job-forge.mjs postflight:status",
|
|
79
79
|
"postflight:check": "node bin/job-forge.mjs postflight:check",
|
|
80
80
|
"postflight:explain": "node bin/job-forge.mjs postflight:explain",
|
|
81
|
+
"timeline:status": "node bin/job-forge.mjs timeline:status",
|
|
82
|
+
"timeline:build": "node bin/job-forge.mjs timeline:build",
|
|
83
|
+
"timeline:plan": "node bin/job-forge.mjs timeline:plan",
|
|
84
|
+
"timeline:due": "node bin/job-forge.mjs timeline:due",
|
|
85
|
+
"timeline:check": "node bin/job-forge.mjs timeline:check",
|
|
86
|
+
"timeline:verify": "node bin/job-forge.mjs timeline:verify",
|
|
87
|
+
"timeline:explain": "node bin/job-forge.mjs timeline:explain",
|
|
81
88
|
"redact:scan": "node bin/job-forge.mjs redact:scan",
|
|
82
89
|
"redact:verify": "node bin/job-forge.mjs redact:verify",
|
|
83
90
|
"redact:apply": "node bin/job-forge.mjs redact:apply",
|
|
@@ -167,6 +174,7 @@
|
|
|
167
174
|
"@razroo/iso-preflight": "^0.1.0",
|
|
168
175
|
"@razroo/iso-redact": "^0.1.0",
|
|
169
176
|
"@razroo/iso-score": "^0.1.0",
|
|
177
|
+
"@razroo/iso-timeline": "^0.1.0",
|
|
170
178
|
"@razroo/iso-trace": "^0.4.0",
|
|
171
179
|
"playwright": "^1.58.1"
|
|
172
180
|
},
|
|
@@ -20,6 +20,7 @@ const checks = [
|
|
|
20
20
|
["H6 requires merge and verify", () => every(files.instructions, ["batch/tracker-additions/*.tsv", "npx job-forge merge", "npx job-forge verify"])],
|
|
21
21
|
["H7 distrusts subagent prose", () => every(files.instructions, ["must originate from a file", "not from prior subagent prose"])],
|
|
22
22
|
["score policy points to local helper", () => every(files.instructions, ["[D19]", "templates/score.json", "npx job-forge score:check", "npx job-forge score:gate"])],
|
|
23
|
+
["timeline policy points to local helper", () => every(files.instructions, ["[D20]", "templates/timeline.json", "npx job-forge timeline:due", "npx job-forge timeline:check --fail-on overdue"])],
|
|
23
24
|
["shared prompt points to on-demand references", () => every(files.instructions, ["modes/{mode}.md", "modes/reference-setup.md", "modes/reference-portals.md", "modes/reference-geometra.md"])],
|
|
24
25
|
["apply mode owns high-stakes upgrade", () => every(files.apply, ["[D8]", "@general-paid", "4.0/5", "high-stakes"])],
|
|
25
26
|
["apply mode blocks provider auto-downgrade", () => every(files.apply, ["[D9]", "do not auto-downgrade", "inspect telemetry before retrying"])],
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { writeFileSync } from 'fs';
|
|
4
|
+
import { isAbsolute, relative, resolve } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
formatCheckResult,
|
|
7
|
+
formatConfigSummary,
|
|
8
|
+
formatTimelineResult,
|
|
9
|
+
formatVerifyResult,
|
|
10
|
+
} from '@razroo/iso-timeline';
|
|
11
|
+
import { PROJECT_DIR } from '../tracker-lib.mjs';
|
|
12
|
+
import {
|
|
13
|
+
buildJobForgeTimeline,
|
|
14
|
+
buildJobForgeTimelineEvents,
|
|
15
|
+
checkJobForgeTimeline,
|
|
16
|
+
dueJobForgeTimeline,
|
|
17
|
+
jobForgeTimelineConfigPath,
|
|
18
|
+
jobForgeTimelineEventsPath,
|
|
19
|
+
jobForgeTimelinePath,
|
|
20
|
+
jobForgeTimelineSummary,
|
|
21
|
+
planJobForgeTimeline,
|
|
22
|
+
readJobForgeTimelineConfig,
|
|
23
|
+
timelineExists,
|
|
24
|
+
verifyJobForgeTimeline,
|
|
25
|
+
} from '../lib/jobforge-timeline.mjs';
|
|
26
|
+
|
|
27
|
+
const USAGE = `job-forge timeline - deterministic follow-up and next-action planning
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
job-forge timeline:status [--json]
|
|
31
|
+
job-forge timeline:build [--now <iso>] [--json]
|
|
32
|
+
job-forge timeline:plan [--now <iso>] [--out <file>] [--json]
|
|
33
|
+
job-forge timeline:due [--now <iso>] [--json]
|
|
34
|
+
job-forge timeline:check [--now <iso>] [--fail-on overdue|due|none] [--json]
|
|
35
|
+
job-forge timeline:verify [--json]
|
|
36
|
+
job-forge timeline:explain [--json]
|
|
37
|
+
job-forge timeline:path [--config|--events]
|
|
38
|
+
|
|
39
|
+
Default policy is templates/timeline.json. Tracker day files and dated pipeline
|
|
40
|
+
items are converted into .jobforge-timeline-events.jsonl; the plan is written
|
|
41
|
+
to .jobforge-timeline.json by timeline:build. This is local project state, not
|
|
42
|
+
an MCP and not prompt context.`;
|
|
43
|
+
|
|
44
|
+
const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
|
|
45
|
+
const opts = parseArgs(rawArgs);
|
|
46
|
+
|
|
47
|
+
if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
48
|
+
console.log(USAGE);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (cmd === 'path') {
|
|
54
|
+
path(opts);
|
|
55
|
+
} else if (cmd === 'status') {
|
|
56
|
+
status(opts);
|
|
57
|
+
} else if (cmd === 'build') {
|
|
58
|
+
build(opts);
|
|
59
|
+
} else if (cmd === 'plan') {
|
|
60
|
+
plan(opts);
|
|
61
|
+
} else if (cmd === 'due') {
|
|
62
|
+
due(opts);
|
|
63
|
+
} else if (cmd === 'check') {
|
|
64
|
+
check(opts);
|
|
65
|
+
} else if (cmd === 'verify') {
|
|
66
|
+
verify(opts);
|
|
67
|
+
} else if (cmd === 'explain') {
|
|
68
|
+
explain(opts);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(`unknown timeline command "${cmd}"\n`);
|
|
71
|
+
console.error(USAGE);
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseArgs(args) {
|
|
80
|
+
const opts = {
|
|
81
|
+
json: false,
|
|
82
|
+
help: false,
|
|
83
|
+
failOn: undefined,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const arg = args[i];
|
|
88
|
+
if (arg === '--json') {
|
|
89
|
+
opts.json = true;
|
|
90
|
+
} else if (arg === '--now') {
|
|
91
|
+
opts.now = valueAfter(args, ++i, '--now');
|
|
92
|
+
} else if (arg.startsWith('--now=')) {
|
|
93
|
+
opts.now = arg.slice('--now='.length);
|
|
94
|
+
} else if (arg === '--fail-on') {
|
|
95
|
+
opts.failOn = valueAfter(args, ++i, '--fail-on');
|
|
96
|
+
} else if (arg.startsWith('--fail-on=')) {
|
|
97
|
+
opts.failOn = arg.slice('--fail-on='.length);
|
|
98
|
+
} else if (arg === '--out') {
|
|
99
|
+
opts.out = valueAfter(args, ++i, '--out');
|
|
100
|
+
} else if (arg.startsWith('--out=')) {
|
|
101
|
+
opts.out = arg.slice('--out='.length);
|
|
102
|
+
} else if (arg === '--config') {
|
|
103
|
+
opts.configPath = true;
|
|
104
|
+
} else if (arg === '--events') {
|
|
105
|
+
opts.eventsPath = true;
|
|
106
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
107
|
+
opts.help = true;
|
|
108
|
+
} else {
|
|
109
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return opts;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function valueAfter(values, index, flag) {
|
|
117
|
+
const value = values[index];
|
|
118
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function path(opts) {
|
|
123
|
+
if (opts.configPath) {
|
|
124
|
+
console.log(jobForgeTimelineConfigPath(PROJECT_DIR));
|
|
125
|
+
} else if (opts.eventsPath) {
|
|
126
|
+
console.log(jobForgeTimelineEventsPath(PROJECT_DIR));
|
|
127
|
+
} else {
|
|
128
|
+
console.log(jobForgeTimelinePath(PROJECT_DIR));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function status(opts) {
|
|
133
|
+
const summary = jobForgeTimelineSummary(PROJECT_DIR);
|
|
134
|
+
if (opts.json) {
|
|
135
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!summary.exists) {
|
|
139
|
+
console.log(`timeline: missing (${relativePath(summary.path)})`);
|
|
140
|
+
console.log('run: job-forge timeline:build');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const verifyResult = verifyJobForgeTimeline({}, PROJECT_DIR);
|
|
144
|
+
console.log(`timeline: ${relativePath(summary.path)}`);
|
|
145
|
+
console.log(`events: ${relativePath(summary.eventsPath)} (${summary.eventsExists ? 'present' : 'missing'})`);
|
|
146
|
+
console.log(`items: ${summary.items}`);
|
|
147
|
+
console.log(`due: ${summary.due}`);
|
|
148
|
+
console.log(`overdue: ${summary.overdue}`);
|
|
149
|
+
console.log(`verify: ${verifyResult.ok ? 'PASS' : 'FAIL'} (${verifyResult.errors} errors, ${verifyResult.warnings} warnings)`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function build(opts) {
|
|
153
|
+
const result = buildJobForgeTimeline({ now: opts.now }, PROJECT_DIR);
|
|
154
|
+
if (opts.json) {
|
|
155
|
+
console.log(JSON.stringify({
|
|
156
|
+
out: result.out,
|
|
157
|
+
eventsOut: result.eventsOut,
|
|
158
|
+
events: result.events.length,
|
|
159
|
+
stats: result.result.stats,
|
|
160
|
+
}, null, 2));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
console.log(`timeline: wrote ${relativePath(result.out)}`);
|
|
164
|
+
console.log(`events: wrote ${relativePath(result.eventsOut)} (${result.events.length} event(s))`);
|
|
165
|
+
console.log(formatTimelineResult(result.result));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function plan(opts) {
|
|
169
|
+
const result = planJobForgeTimeline({ now: opts.now }, PROJECT_DIR);
|
|
170
|
+
if (opts.out) writePlan(resolveInputPath(opts.out), result);
|
|
171
|
+
if (opts.json) {
|
|
172
|
+
console.log(JSON.stringify(result, null, 2));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log(formatTimelineResult(result));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function due(opts) {
|
|
179
|
+
const result = dueJobForgeTimeline({ now: opts.now }, PROJECT_DIR);
|
|
180
|
+
if (opts.json) {
|
|
181
|
+
console.log(JSON.stringify(result, null, 2));
|
|
182
|
+
} else {
|
|
183
|
+
console.log(formatTimelineResult(result));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function check(opts) {
|
|
188
|
+
const result = checkJobForgeTimeline({ now: opts.now, failOn: opts.failOn }, PROJECT_DIR);
|
|
189
|
+
if (opts.json) {
|
|
190
|
+
console.log(JSON.stringify(result, null, 2));
|
|
191
|
+
} else {
|
|
192
|
+
console.log(formatCheckResult(result));
|
|
193
|
+
}
|
|
194
|
+
process.exit(result.ok ? 0 : 1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function verify(opts) {
|
|
198
|
+
if (!timelineExists(PROJECT_DIR)) {
|
|
199
|
+
if (opts.json) {
|
|
200
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeTimelinePath(PROJECT_DIR) }, null, 2));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(`timeline: missing (${relativePath(jobForgeTimelinePath(PROJECT_DIR))})`);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const result = verifyJobForgeTimeline({}, PROJECT_DIR);
|
|
207
|
+
if (opts.json) {
|
|
208
|
+
console.log(JSON.stringify(result, null, 2));
|
|
209
|
+
} else {
|
|
210
|
+
console.log(formatVerifyResult(result));
|
|
211
|
+
}
|
|
212
|
+
process.exit(result.ok ? 0 : 1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function explain(opts) {
|
|
216
|
+
const config = readJobForgeTimelineConfig(PROJECT_DIR);
|
|
217
|
+
if (opts.json) {
|
|
218
|
+
console.log(JSON.stringify(config, null, 2));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
console.log(`config: ${relativePath(jobForgeTimelineConfigPath(PROJECT_DIR))}`);
|
|
222
|
+
console.log(formatConfigSummary(config));
|
|
223
|
+
const events = buildJobForgeTimelineEvents(PROJECT_DIR);
|
|
224
|
+
console.log(`events available now: ${events.length}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveInputPath(path) {
|
|
228
|
+
return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function relativePath(path) {
|
|
232
|
+
return relative(PROJECT_DIR, path) || '.';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function writePlan(path, result) {
|
|
236
|
+
writeFileSync(path, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
237
|
+
}
|
|
@@ -56,6 +56,13 @@
|
|
|
56
56
|
"postflight:status": "job-forge postflight:status",
|
|
57
57
|
"postflight:check": "job-forge postflight:check",
|
|
58
58
|
"postflight:explain": "job-forge postflight:explain",
|
|
59
|
+
"timeline:status": "job-forge timeline:status",
|
|
60
|
+
"timeline:build": "job-forge timeline:build",
|
|
61
|
+
"timeline:plan": "job-forge timeline:plan",
|
|
62
|
+
"timeline:due": "job-forge timeline:due",
|
|
63
|
+
"timeline:check": "job-forge timeline:check",
|
|
64
|
+
"timeline:verify": "job-forge timeline:verify",
|
|
65
|
+
"timeline:explain": "job-forge timeline:explain",
|
|
59
66
|
"redact:scan": "job-forge redact:scan",
|
|
60
67
|
"redact:verify": "job-forge redact:verify",
|
|
61
68
|
"redact:apply": "job-forge redact:apply",
|
|
@@ -83,7 +90,10 @@
|
|
|
83
90
|
".jobforge-cache/",
|
|
84
91
|
".jobforge-index.json",
|
|
85
92
|
".jobforge-facts.json",
|
|
93
|
+
".jobforge-timeline.json",
|
|
94
|
+
".jobforge-timeline-events.jsonl",
|
|
86
95
|
".jobforge-redacted/",
|
|
96
|
+
"data/timeline-events.jsonl",
|
|
87
97
|
"batch/preflight-candidates.json",
|
|
88
98
|
"batch/preflight-plan.json",
|
|
89
99
|
"batch/postflight-outcomes.json"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"defaults": {
|
|
4
|
+
"overdueAfter": "7d",
|
|
5
|
+
"latestOnly": true
|
|
6
|
+
},
|
|
7
|
+
"rules": [
|
|
8
|
+
{
|
|
9
|
+
"id": "applied-follow-up",
|
|
10
|
+
"label": "Applied follow-up",
|
|
11
|
+
"description": "Applications in Applied state should receive a follow-up after one week.",
|
|
12
|
+
"action": "send_follow_up",
|
|
13
|
+
"match": {
|
|
14
|
+
"type": "application.status",
|
|
15
|
+
"where": {
|
|
16
|
+
"data.status": "Applied"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"after": "7d"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "contacted-follow-up",
|
|
23
|
+
"label": "Contacted follow-up",
|
|
24
|
+
"description": "Outreach/contacted rows should receive a shorter follow-up after five days.",
|
|
25
|
+
"action": "send_contact_follow_up",
|
|
26
|
+
"match": {
|
|
27
|
+
"type": "application.status",
|
|
28
|
+
"where": {
|
|
29
|
+
"data.status": "Contacted"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"after": "5d",
|
|
33
|
+
"overdueAfter": "7d"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "interview-thank-you",
|
|
37
|
+
"label": "Interview thank-you",
|
|
38
|
+
"description": "Interview rows should prompt a thank-you note the next day.",
|
|
39
|
+
"action": "send_thank_you",
|
|
40
|
+
"match": {
|
|
41
|
+
"type": "application.status",
|
|
42
|
+
"where": {
|
|
43
|
+
"data.status": "Interview"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"after": "1d",
|
|
47
|
+
"overdueAfter": "2d"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "interview-follow-up",
|
|
51
|
+
"label": "Interview follow-up",
|
|
52
|
+
"description": "Interview rows without a later status should receive a recruiter nudge after one week.",
|
|
53
|
+
"action": "follow_up_after_interview",
|
|
54
|
+
"match": {
|
|
55
|
+
"type": "application.status",
|
|
56
|
+
"where": {
|
|
57
|
+
"data.status": "Interview"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"after": "7d",
|
|
61
|
+
"overdueAfter": "10d"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "stale-pipeline-item",
|
|
65
|
+
"label": "Stale pending pipeline item",
|
|
66
|
+
"description": "Pending pipeline URLs with a known source date should be processed or discarded after three days.",
|
|
67
|
+
"action": "process_or_discard_pipeline_item",
|
|
68
|
+
"match": {
|
|
69
|
+
"type": "pipeline.item",
|
|
70
|
+
"where": {
|
|
71
|
+
"data.status": "pending"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"after": "3d",
|
|
75
|
+
"overdueAfter": "4d",
|
|
76
|
+
"suppressWhen": [
|
|
77
|
+
{
|
|
78
|
+
"type": [
|
|
79
|
+
"pipeline.processed",
|
|
80
|
+
"application.status"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
package/verify-pipeline.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* 10. Ledger file verifies if .jobforge-ledger/events.jsonl exists
|
|
20
20
|
* 11. Artifact index verifies if .jobforge-index.json exists
|
|
21
21
|
* 12. Fact set verifies if .jobforge-facts.json exists
|
|
22
|
+
* 13. Timeline verifies if .jobforge-timeline.json exists
|
|
22
23
|
*
|
|
23
24
|
* Run: node verify-pipeline.mjs (from repo root; same as npm run verify)
|
|
24
25
|
*/
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
import { jobForgeLedgerPath, ledgerExists, verifyJobForgeLedger } from './lib/jobforge-ledger.mjs';
|
|
34
35
|
import { indexExists, jobForgeIndexPath, verifyJobForgeIndex } from './lib/jobforge-index.mjs';
|
|
35
36
|
import { factsExist, jobForgeFactsPath, verifyJobForgeFacts } from './lib/jobforge-facts.mjs';
|
|
37
|
+
import { jobForgeTimelinePath, timelineExists, verifyJobForgeTimeline } from './lib/jobforge-timeline.mjs';
|
|
36
38
|
import {
|
|
37
39
|
canonicalStatusValues,
|
|
38
40
|
formatContractIssues,
|
|
@@ -189,6 +191,22 @@ function verifyFactsIfPresent() {
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
|
|
194
|
+
function verifyTimelineIfPresent() {
|
|
195
|
+
if (!timelineExists(PROJECT_DIR)) {
|
|
196
|
+
ok('Timeline not initialized');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const result = verifyJobForgeTimeline({}, PROJECT_DIR);
|
|
200
|
+
for (const issue of result.issues) {
|
|
201
|
+
const msg = `timeline: ${issue.code}: ${issue.message}`;
|
|
202
|
+
if (issue.severity === 'error') error(msg);
|
|
203
|
+
else warn(msg);
|
|
204
|
+
}
|
|
205
|
+
if (result.ok) {
|
|
206
|
+
ok(`Timeline valid (${relative(PROJECT_DIR, jobForgeTimelinePath(PROJECT_DIR))})`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
192
210
|
// --- Read entries ---
|
|
193
211
|
const { entries, source } = readAllEntries();
|
|
194
212
|
|
|
@@ -200,6 +218,7 @@ if (entries.length === 0) {
|
|
|
200
218
|
verifyLedgerIfPresent();
|
|
201
219
|
verifyIndexIfPresent();
|
|
202
220
|
verifyFactsIfPresent();
|
|
221
|
+
verifyTimelineIfPresent();
|
|
203
222
|
console.log('\n' + '='.repeat(50));
|
|
204
223
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|
|
205
224
|
if (errors === 0 && warnings === 0) console.log('🟢 Pipeline is clean!');
|
|
@@ -337,6 +356,7 @@ verifyStatesYamlDrift();
|
|
|
337
356
|
verifyLedgerIfPresent();
|
|
338
357
|
verifyIndexIfPresent();
|
|
339
358
|
verifyFactsIfPresent();
|
|
359
|
+
verifyTimelineIfPresent();
|
|
340
360
|
|
|
341
361
|
console.log('\n' + '='.repeat(50));
|
|
342
362
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|