job-forge 2.14.33 → 2.14.35
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 +11 -49
- package/AGENTS.md +11 -49
- package/CLAUDE.md +11 -49
- package/README.md +8 -4
- package/bin/create-job-forge.mjs +42 -0
- package/bin/job-forge.mjs +69 -0
- package/docs/ARCHITECTURE.md +18 -3
- package/docs/CUSTOMIZATION.md +8 -0
- package/docs/README.md +2 -2
- package/docs/SETUP.md +4 -0
- package/iso/instructions.md +11 -49
- package/lib/jobforge-lineage.mjs +122 -0
- package/lib/jobforge-prioritize.mjs +294 -0
- package/modes/README.md +2 -0
- package/modes/reference-local-helpers.md +55 -0
- package/package.json +19 -2
- package/scripts/check-helper-integration.mjs +143 -0
- package/scripts/check-iso-smoke.mjs +4 -2
- package/scripts/lineage.mjs +247 -0
- package/scripts/prioritize.mjs +323 -0
- package/templates/migrations.json +17 -0
- package/templates/prioritize.json +125 -0
- package/verify-pipeline.mjs +48 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
checkPrioritize,
|
|
5
|
+
loadPrioritizeConfig,
|
|
6
|
+
parseJson,
|
|
7
|
+
prioritize,
|
|
8
|
+
selectPrioritized,
|
|
9
|
+
verifyPrioritizeResult,
|
|
10
|
+
} from '@razroo/iso-prioritize';
|
|
11
|
+
import { ensureJobForgeFacts } from './jobforge-facts.mjs';
|
|
12
|
+
import { dueJobForgeTimeline } from './jobforge-timeline.mjs';
|
|
13
|
+
|
|
14
|
+
export const PRIORITIZE_CONFIG_FILE = 'templates/prioritize.json';
|
|
15
|
+
export const PRIORITIZE_FILE = '.jobforge-prioritize.json';
|
|
16
|
+
export const PRIORITIZE_ITEMS_FILE = '.jobforge-prioritize-items.json';
|
|
17
|
+
|
|
18
|
+
export function resolveProjectDir(projectDir = process.env.JOB_FORGE_PROJECT || process.cwd()) {
|
|
19
|
+
return projectDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function jobForgePrioritizeConfigPath(projectDir = resolveProjectDir()) {
|
|
23
|
+
return process.env.JOB_FORGE_PRIORITIZE_CONFIG || join(projectDir, PRIORITIZE_CONFIG_FILE);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function jobForgePrioritizePath(projectDir = resolveProjectDir()) {
|
|
27
|
+
return process.env.JOB_FORGE_PRIORITIZE || join(projectDir, PRIORITIZE_FILE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function jobForgePrioritizeItemsPath(projectDir = resolveProjectDir()) {
|
|
31
|
+
return process.env.JOB_FORGE_PRIORITIZE_ITEMS || join(projectDir, PRIORITIZE_ITEMS_FILE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function prioritizeExists(projectDir = resolveProjectDir()) {
|
|
35
|
+
return existsSync(jobForgePrioritizePath(projectDir));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readJobForgePrioritizeConfig(projectDir = resolveProjectDir()) {
|
|
39
|
+
const path = jobForgePrioritizeConfigPath(projectDir);
|
|
40
|
+
return loadPrioritizeConfig(parseJson(readFileSync(path, 'utf8'), path));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readJobForgePrioritize(projectDir = resolveProjectDir()) {
|
|
44
|
+
const path = jobForgePrioritizePath(projectDir);
|
|
45
|
+
return parseJson(readFileSync(path, 'utf8'), path);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function readJobForgePrioritizeItems(projectDir = resolveProjectDir()) {
|
|
49
|
+
const path = jobForgePrioritizeItemsPath(projectDir);
|
|
50
|
+
return parseJson(readFileSync(path, 'utf8'), path);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildJobForgePrioritizeItems(options = {}, projectDir = resolveProjectDir()) {
|
|
54
|
+
const factSet = options.facts || ensureJobForgeFacts({ rebuild: options.rebuild !== false }, projectDir);
|
|
55
|
+
const items = [];
|
|
56
|
+
|
|
57
|
+
for (const fact of factSet.facts || []) {
|
|
58
|
+
const candidate = candidateItem(fact);
|
|
59
|
+
if (candidate) items.push(candidate);
|
|
60
|
+
|
|
61
|
+
const evaluated = evaluatedTrackerItem(fact);
|
|
62
|
+
if (evaluated) items.push(evaluated);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const item of dueJobForgeTimeline({ now: options.now }, projectDir).items || []) {
|
|
66
|
+
const followup = timelineItem(item);
|
|
67
|
+
if (followup) items.push(followup);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { items: dedupeItems(items) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function rankJobForgePrioritize(options = {}, projectDir = resolveProjectDir()) {
|
|
74
|
+
const config = options.config || readJobForgePrioritizeConfig(projectDir);
|
|
75
|
+
const items = options.items || buildJobForgePrioritizeItems(options, projectDir);
|
|
76
|
+
return prioritize(config, items, { profile: options.profile, limit: options.limit });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function selectJobForgePrioritize(options = {}, projectDir = resolveProjectDir()) {
|
|
80
|
+
return selectPrioritized(rankJobForgePrioritize(options, projectDir));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function checkJobForgePrioritize(options = {}, projectDir = resolveProjectDir()) {
|
|
84
|
+
const config = options.config || readJobForgePrioritizeConfig(projectDir);
|
|
85
|
+
const items = options.items || buildJobForgePrioritizeItems(options, projectDir);
|
|
86
|
+
return checkPrioritize(config, items, {
|
|
87
|
+
profile: options.profile,
|
|
88
|
+
limit: options.limit,
|
|
89
|
+
minSelected: options.minSelected,
|
|
90
|
+
failOn: options.failOn,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function writeJobForgePrioritize(result, options = {}, projectDir = resolveProjectDir()) {
|
|
95
|
+
const out = options.out || jobForgePrioritizePath(projectDir);
|
|
96
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
97
|
+
writeFileSync(out, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function writeJobForgePrioritizeItems(items, options = {}, projectDir = resolveProjectDir()) {
|
|
102
|
+
const out = options.out || jobForgePrioritizeItemsPath(projectDir);
|
|
103
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
104
|
+
writeFileSync(out, `${JSON.stringify(items, null, 2)}\n`, 'utf8');
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function verifyJobForgePrioritize(options = {}, projectDir = resolveProjectDir()) {
|
|
109
|
+
const result = options.result || readJobForgePrioritize(projectDir);
|
|
110
|
+
return verifyPrioritizeResult(result);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function jobForgePrioritizeSummary(projectDir = resolveProjectDir()) {
|
|
114
|
+
if (!prioritizeExists(projectDir)) {
|
|
115
|
+
return {
|
|
116
|
+
path: jobForgePrioritizePath(projectDir),
|
|
117
|
+
itemsPath: jobForgePrioritizeItemsPath(projectDir),
|
|
118
|
+
config: jobForgePrioritizeConfigPath(projectDir),
|
|
119
|
+
exists: false,
|
|
120
|
+
items: 0,
|
|
121
|
+
selected: 0,
|
|
122
|
+
candidate: 0,
|
|
123
|
+
skipped: 0,
|
|
124
|
+
blocked: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = readJobForgePrioritize(projectDir);
|
|
129
|
+
return {
|
|
130
|
+
path: jobForgePrioritizePath(projectDir),
|
|
131
|
+
itemsPath: jobForgePrioritizeItemsPath(projectDir),
|
|
132
|
+
config: jobForgePrioritizeConfigPath(projectDir),
|
|
133
|
+
exists: true,
|
|
134
|
+
items: result.stats?.total || 0,
|
|
135
|
+
selected: result.stats?.selected || 0,
|
|
136
|
+
candidate: result.stats?.candidate || 0,
|
|
137
|
+
skipped: result.stats?.skipped || 0,
|
|
138
|
+
blocked: result.stats?.blocked || 0,
|
|
139
|
+
id: result.id,
|
|
140
|
+
profile: result.profile,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function candidateItem(fact) {
|
|
145
|
+
if (fact.fact !== 'candidate.ready') return null;
|
|
146
|
+
const fields = fact.fields || {};
|
|
147
|
+
const company = stringField(fields.company);
|
|
148
|
+
const role = stringField(fields.role);
|
|
149
|
+
const score = parseScore(fields.score);
|
|
150
|
+
|
|
151
|
+
return compactItem({
|
|
152
|
+
id: `candidate-${slug(fields.id || fact.key || fact.id)}`,
|
|
153
|
+
key: fact.key,
|
|
154
|
+
type: 'apply',
|
|
155
|
+
title: title(company, role, 'apply'),
|
|
156
|
+
tags: ['apply', 'candidate'],
|
|
157
|
+
data: compactObject({
|
|
158
|
+
company,
|
|
159
|
+
role,
|
|
160
|
+
score,
|
|
161
|
+
urgency: score >= 4 ? 8 : 6,
|
|
162
|
+
ageDays: 0,
|
|
163
|
+
sourceQuality: sourceQuality(fact, score),
|
|
164
|
+
status: fields.gateStatus || 'Evaluated',
|
|
165
|
+
gateStatus: fields.gateStatus,
|
|
166
|
+
locationStatus: fields.locationStatus,
|
|
167
|
+
url: fields.url,
|
|
168
|
+
}),
|
|
169
|
+
source: fact.source,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function evaluatedTrackerItem(fact) {
|
|
174
|
+
if (fact.fact !== 'application.status') return null;
|
|
175
|
+
const fields = fact.fields || {};
|
|
176
|
+
const status = stringField(fields.status || fact.value);
|
|
177
|
+
if (status !== 'Evaluated') return null;
|
|
178
|
+
const company = stringField(fields.company);
|
|
179
|
+
const role = stringField(fields.role);
|
|
180
|
+
const score = parseScore(fields.score);
|
|
181
|
+
|
|
182
|
+
return compactItem({
|
|
183
|
+
id: `evaluated-${slug(fields.num || fact.key || fact.id)}`,
|
|
184
|
+
key: fact.key,
|
|
185
|
+
type: 'apply',
|
|
186
|
+
title: title(company, role, 'apply'),
|
|
187
|
+
tags: ['apply', 'tracker'],
|
|
188
|
+
data: compactObject({
|
|
189
|
+
company,
|
|
190
|
+
role,
|
|
191
|
+
score,
|
|
192
|
+
urgency: score >= 4 ? 8 : 5,
|
|
193
|
+
ageDays: ageDays(fields.date),
|
|
194
|
+
sourceQuality: sourceQuality(fact, score),
|
|
195
|
+
status,
|
|
196
|
+
report: fields.report,
|
|
197
|
+
pdf: fields.pdf,
|
|
198
|
+
}),
|
|
199
|
+
source: fact.source,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function timelineItem(item) {
|
|
204
|
+
const data = item.event?.data || {};
|
|
205
|
+
const company = stringField(data.company);
|
|
206
|
+
const role = stringField(data.role);
|
|
207
|
+
const score = parseScore(data.score);
|
|
208
|
+
|
|
209
|
+
return compactItem({
|
|
210
|
+
id: `timeline-${slug(item.id)}`,
|
|
211
|
+
key: item.key,
|
|
212
|
+
type: 'followup',
|
|
213
|
+
title: title(company, role, item.action || 'follow-up'),
|
|
214
|
+
tags: ['followup', item.state],
|
|
215
|
+
data: compactObject({
|
|
216
|
+
company,
|
|
217
|
+
role,
|
|
218
|
+
score,
|
|
219
|
+
urgency: item.state === 'overdue' ? 10 : 8,
|
|
220
|
+
ageDays: ageDays(item.event?.at),
|
|
221
|
+
sourceQuality: sourceQuality(item.event, score),
|
|
222
|
+
status: data.status,
|
|
223
|
+
timelineState: item.state,
|
|
224
|
+
action: item.action,
|
|
225
|
+
rule: item.rule,
|
|
226
|
+
}),
|
|
227
|
+
source: item.event?.source,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function dedupeItems(items) {
|
|
232
|
+
const byId = new Map();
|
|
233
|
+
for (const item of items) {
|
|
234
|
+
const existing = byId.get(item.id);
|
|
235
|
+
if (!existing || priorityValue(item) > priorityValue(existing)) byId.set(item.id, item);
|
|
236
|
+
}
|
|
237
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function priorityValue(item) {
|
|
241
|
+
return Number(item.data?.score || 0) * 100 +
|
|
242
|
+
Number(item.data?.urgency || 0) * 10 +
|
|
243
|
+
Number(item.data?.sourceQuality || 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseScore(value) {
|
|
247
|
+
const match = String(value || '').match(/([0-9]+(?:\.[0-9]+)?)(?:\s*\/\s*5)?/);
|
|
248
|
+
if (!match) return 0;
|
|
249
|
+
return Math.max(0, Math.min(5, Number(match[1])));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function ageDays(value) {
|
|
253
|
+
const text = String(value || '').trim();
|
|
254
|
+
if (!text) return 0;
|
|
255
|
+
const parsed = new Date(text);
|
|
256
|
+
if (Number.isNaN(parsed.getTime())) return 0;
|
|
257
|
+
return Math.max(0, Math.floor((Date.now() - parsed.getTime()) / 86400000));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function sourceQuality(sourceLike, score) {
|
|
261
|
+
if (sourceLike?.source?.path && score > 0) return 1;
|
|
262
|
+
return score > 0 ? 0.8 : 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function stringField(value) {
|
|
266
|
+
return value === undefined || value === null ? '' : String(value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function title(company, role, suffix) {
|
|
270
|
+
const base = [company, role].filter(Boolean).join(' - ');
|
|
271
|
+
return suffix && base ? `${base} ${suffix}` : base || suffix || 'JobForge item';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function slug(value) {
|
|
275
|
+
return String(value || 'item')
|
|
276
|
+
.toLowerCase()
|
|
277
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
278
|
+
.replace(/^-+|-+$/g, '')
|
|
279
|
+
.slice(0, 80) || 'item';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function compactItem(item) {
|
|
283
|
+
return {
|
|
284
|
+
...item,
|
|
285
|
+
tags: (item.tags || []).filter(Boolean),
|
|
286
|
+
...(item.source ? { source: item.source } : {}),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function compactObject(input) {
|
|
291
|
+
return Object.fromEntries(
|
|
292
|
+
Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ''),
|
|
293
|
+
);
|
|
294
|
+
}
|
package/modes/README.md
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
Markdown prompts used together with the root [`AGENTS.md`](../AGENTS.md). Each file aligns with a `/job-forge …` entry point or shared behavior described there.
|
|
4
4
|
|
|
5
5
|
- **`_shared.md`** — Archetypes, scoring dimensions, negotiation scaffolding. Edit this first when you change how offers are classified or weighted.
|
|
6
|
+
- **`reference-local-helpers.md`** — On-demand map of deterministic local helpers and when they replace prose, broad file reads, or manual policy checks.
|
|
6
7
|
- **Per-command files** — Each `*.md` here pairs with a `/job-forge …` entry in [`AGENTS.md`](../AGENTS.md). How modes connect to batch, tracker, and scripts is spelled out in [**Architecture — Modes**](../docs/ARCHITECTURE.md#modes-modes).
|
|
7
8
|
|
|
8
9
|
| File | Role |
|
|
9
10
|
|------|------|
|
|
10
11
|
| [`_shared.md`](_shared.md) | Shared archetypes, scoring, negotiation scaffolding |
|
|
12
|
+
| [`reference-local-helpers.md`](reference-local-helpers.md) | Deterministic helper selection, mandatory uses, and integration enforcement |
|
|
11
13
|
| [`auto-pipeline.md`](auto-pipeline.md) | Default path when the user pastes a JD or URL — full evaluate → report → PDF → tracker flow |
|
|
12
14
|
| [`offer.md`](offer.md) | Explicit full evaluation (blocks A–F) for a single offer |
|
|
13
15
|
| [`compare.md`](compare.md) | Side-by-side comparison of multiple offers |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Reference: Local Helpers
|
|
2
|
+
|
|
3
|
+
JobForge's architectural helpers are local CLIs and JSON policies, not MCPs. They add no always-on prompt or tool-schema tokens. Use them when they can answer a question or validate an artifact more deterministically than prose.
|
|
4
|
+
|
|
5
|
+
## Selection Rule
|
|
6
|
+
|
|
7
|
+
Prefer a local helper when the workflow needs:
|
|
8
|
+
|
|
9
|
+
- Identity, duplicate, or status truth.
|
|
10
|
+
- Machine-readable artifact validation.
|
|
11
|
+
- Context, capability, or migration policy.
|
|
12
|
+
- Dispatch planning or settlement.
|
|
13
|
+
- Scoring, timing, priority, or lineage decisions.
|
|
14
|
+
- Safe export checks.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
## Helper Map
|
|
19
|
+
|
|
20
|
+
| Need | Source / state | Command |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Trace inspection | Local OpenCode transcripts | `npx job-forge trace:*` |
|
|
23
|
+
| Run telemetry | Local traces + tracker TSV state | `npx job-forge telemetry:*` |
|
|
24
|
+
| Guard audits | `templates/guards/jobforge-baseline.yaml` | `npx job-forge guard:*` |
|
|
25
|
+
| Workflow state | `.jobforge-ledger/events.jsonl` | `npx job-forge ledger:*` |
|
|
26
|
+
| Artifact contracts | `templates/contracts.json` | `npx job-forge tracker-line ... --write`; `npx job-forge verify` |
|
|
27
|
+
| Role capability policy | `templates/capabilities.json` | `npx job-forge capabilities:*` |
|
|
28
|
+
| Context bundle policy | `templates/context.json` | `npx job-forge context:*` |
|
|
29
|
+
| JD/artifact reuse | `.jobforge-cache/` | `npx job-forge cache:*` |
|
|
30
|
+
| Artifact lookup | `.jobforge-index.json` from `templates/index.json` | `npx job-forge index:*` |
|
|
31
|
+
| Source-backed facts | `.jobforge-facts.json` from `templates/facts.json` | `npx job-forge facts:*` |
|
|
32
|
+
| Consumer upgrades | `templates/migrations.json` | `npx job-forge migrate:*` |
|
|
33
|
+
| Identity keys | `templates/canon.json` | `npx job-forge canon:*` |
|
|
34
|
+
| Apply dispatch safety | `templates/preflight.json` | `npx job-forge preflight:*` |
|
|
35
|
+
| Dispatch settlement | `templates/postflight.json` | `npx job-forge postflight:*` |
|
|
36
|
+
| Safe export | `templates/redact.json` | `npx job-forge redact:*` |
|
|
37
|
+
| Offer scoring | `templates/score.json` | `npx job-forge score:*` |
|
|
38
|
+
| Follow-up timing | `templates/timeline.json` | `npx job-forge timeline:*` |
|
|
39
|
+
| Next-action ranking | `templates/prioritize.json` | `npx job-forge prioritize:*` |
|
|
40
|
+
| Artifact lineage | `.jobforge-lineage.json` | `npx job-forge lineage:*` |
|
|
41
|
+
|
|
42
|
+
## Mandatory Uses
|
|
43
|
+
|
|
44
|
+
- Before duplicate-sensitive apply dispatches, use `canon:key`, `index:has`, `facts:has`, or `ledger:has` as cheap prefilters when useful, then still apply the H2 four-source grep unless the candidate JSON already materializes those sources.
|
|
45
|
+
- For tracker additions, prefer `tracker-line --write`; if TSV is emitted manually, `merge` and `verify` must validate it through `templates/contracts.json`.
|
|
46
|
+
- For score-driven apply/PDF decisions, run `score:check --input <file>` and `score:gate --input <file> --gate apply`.
|
|
47
|
+
- For follow-up triage, run `timeline:due`; use `timeline:check --fail-on overdue` when stale follow-ups should fail the workflow.
|
|
48
|
+
- For next-action or replacement-candidate selection, run `prioritize:build` or `prioritize:select --limit N`.
|
|
49
|
+
- 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>...`.
|
|
50
|
+
- Before exporting traces, prompts, reports, or fixtures outside the project, run `redact:scan`, `redact:apply`, or `redact:verify`.
|
|
51
|
+
- 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.
|
|
52
|
+
|
|
53
|
+
## Enforcement
|
|
54
|
+
|
|
55
|
+
The integration surface is checked by `npm run lint:helpers`. That check verifies helper dependencies, package scripts, scaffolder scripts, migration scripts, generated ignores, templates, and this reference stay aligned.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-forge",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.35",
|
|
4
4
|
"description": "AI-powered job search pipeline built on opencode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -85,6 +85,20 @@
|
|
|
85
85
|
"timeline:check": "node bin/job-forge.mjs timeline:check",
|
|
86
86
|
"timeline:verify": "node bin/job-forge.mjs timeline:verify",
|
|
87
87
|
"timeline:explain": "node bin/job-forge.mjs timeline:explain",
|
|
88
|
+
"prioritize:status": "node bin/job-forge.mjs prioritize:status",
|
|
89
|
+
"prioritize:items": "node bin/job-forge.mjs prioritize:items",
|
|
90
|
+
"prioritize:build": "node bin/job-forge.mjs prioritize:build",
|
|
91
|
+
"prioritize:rank": "node bin/job-forge.mjs prioritize:rank",
|
|
92
|
+
"prioritize:select": "node bin/job-forge.mjs prioritize:select",
|
|
93
|
+
"prioritize:check": "node bin/job-forge.mjs prioritize:check",
|
|
94
|
+
"prioritize:verify": "node bin/job-forge.mjs prioritize:verify",
|
|
95
|
+
"prioritize:explain": "node bin/job-forge.mjs prioritize:explain",
|
|
96
|
+
"lineage:status": "node bin/job-forge.mjs lineage:status",
|
|
97
|
+
"lineage:record": "node bin/job-forge.mjs lineage:record",
|
|
98
|
+
"lineage:check": "node bin/job-forge.mjs lineage:check",
|
|
99
|
+
"lineage:stale": "node bin/job-forge.mjs lineage:stale",
|
|
100
|
+
"lineage:verify": "node bin/job-forge.mjs lineage:verify",
|
|
101
|
+
"lineage:explain": "node bin/job-forge.mjs lineage:explain",
|
|
88
102
|
"redact:scan": "node bin/job-forge.mjs redact:scan",
|
|
89
103
|
"redact:verify": "node bin/job-forge.mjs redact:verify",
|
|
90
104
|
"redact:apply": "node bin/job-forge.mjs redact:apply",
|
|
@@ -96,11 +110,12 @@
|
|
|
96
110
|
"plan": "iso plan .",
|
|
97
111
|
"lint:agentmd": "agentmd lint iso/instructions.md",
|
|
98
112
|
"lint:modes": "isolint lint modes/",
|
|
113
|
+
"lint:helpers": "node scripts/check-helper-integration.mjs .",
|
|
99
114
|
"test:agentmd": "agentmd test iso/instructions.md --fixtures fixtures/instructions.yml --via claude-code --model claude-haiku-4-5 --concurrency 2",
|
|
100
115
|
"test:agentmd:baseline": "agentmd test iso/instructions.md --fixtures fixtures/instructions.yml --via claude-code --model claude-haiku-4-5 --concurrency 2 --trials 3 --format json --out fixtures/baseline.json",
|
|
101
116
|
"test:agentmd:apply": "agentmd test modes/apply.md --fixtures fixtures/modes/apply.yml --via claude-code --model claude-haiku-4-5 --concurrency 2 --trials 3",
|
|
102
117
|
"lint:agentmd:modes": "agentmd lint modes/apply.md",
|
|
103
|
-
"smoke:iso": "iso plan . && iso build . --dry-run && iso-route verify models.yaml && node scripts/check-iso-smoke.mjs . && JOBFORGE_ROOT=$PWD iso-eval run fixtures/iso-smoke/eval.yml",
|
|
118
|
+
"smoke:iso": "iso plan . && iso build . --dry-run && iso-route verify models.yaml && node scripts/check-iso-smoke.mjs . && node scripts/check-helper-integration.mjs . && JOBFORGE_ROOT=$PWD iso-eval run fixtures/iso-smoke/eval.yml",
|
|
104
119
|
"build:config": "iso build .",
|
|
105
120
|
"prepack": "iso build .",
|
|
106
121
|
"release:check-source": "node ./scripts/release/check-source.mjs",
|
|
@@ -168,10 +183,12 @@
|
|
|
168
183
|
"@razroo/iso-guard": "^0.1.0",
|
|
169
184
|
"@razroo/iso-index": "^0.1.0",
|
|
170
185
|
"@razroo/iso-ledger": "^0.1.0",
|
|
186
|
+
"@razroo/iso-lineage": "^0.1.0",
|
|
171
187
|
"@razroo/iso-migrate": "^0.1.0",
|
|
172
188
|
"@razroo/iso-orchestrator": "^0.1.0",
|
|
173
189
|
"@razroo/iso-postflight": "^0.1.0",
|
|
174
190
|
"@razroo/iso-preflight": "^0.1.0",
|
|
191
|
+
"@razroo/iso-prioritize": "^0.1.0",
|
|
175
192
|
"@razroo/iso-redact": "^0.1.0",
|
|
176
193
|
"@razroo/iso-score": "^0.1.0",
|
|
177
194
|
"@razroo/iso-timeline": "^0.1.0",
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const root = resolve(process.argv[2] || '.');
|
|
7
|
+
|
|
8
|
+
const pkg = readJson('package.json');
|
|
9
|
+
const migrations = readJson('templates/migrations.json');
|
|
10
|
+
const bin = readText('bin/job-forge.mjs');
|
|
11
|
+
const create = readText('bin/create-job-forge.mjs');
|
|
12
|
+
const reference = readText('modes/reference-local-helpers.md');
|
|
13
|
+
const rootIgnore = readText('.gitignore');
|
|
14
|
+
const architecture = readText('docs/ARCHITECTURE.md');
|
|
15
|
+
|
|
16
|
+
const migrationScripts = migrationValue('jobforge-managed-scripts', '/scripts');
|
|
17
|
+
const migrationIgnores = migrationValue('jobforge-generated-ignores');
|
|
18
|
+
|
|
19
|
+
const groups = [
|
|
20
|
+
helper('trace', '@razroo/iso-trace', ['list', 'stats', 'show']),
|
|
21
|
+
helper('telemetry', '', ['list', 'status', 'show', 'watch']),
|
|
22
|
+
helper('guard', '@razroo/iso-guard', ['audit', 'explain'], { template: 'templates/guards/jobforge-baseline.yaml' }),
|
|
23
|
+
helper('ledger', '@razroo/iso-ledger', ['status', 'rebuild', 'verify', 'has', 'query'], { artifacts: ['.jobforge-ledger/'] }),
|
|
24
|
+
helper('capabilities', '@razroo/iso-capabilities', ['list', 'explain', 'check', 'render'], { template: 'templates/capabilities.json', migrated: true }),
|
|
25
|
+
helper('context', '@razroo/iso-context', ['list', 'explain', 'plan', 'check', 'render'], { template: 'templates/context.json', migrated: true }),
|
|
26
|
+
helper('cache', '@razroo/iso-cache', ['key', 'has', 'get', 'put', 'status', 'list', 'verify', 'prune'], { artifacts: ['.jobforge-cache/'], migrated: true }),
|
|
27
|
+
helper('index', '@razroo/iso-index', ['build', 'status', 'query', 'has', 'verify', 'explain'], { template: 'templates/index.json', artifacts: ['.jobforge-index.json'], migrated: true }),
|
|
28
|
+
helper('facts', '@razroo/iso-facts', ['build', 'status', 'verify', 'check', 'has', 'query', 'explain'], { template: 'templates/facts.json', artifacts: ['.jobforge-facts.json'], migrated: true }),
|
|
29
|
+
helper('score', '@razroo/iso-score', ['compute', 'verify', 'check', 'gate', 'compare', 'explain'], { template: 'templates/score.json', migrated: true }),
|
|
30
|
+
helper('canon', '@razroo/iso-canon', ['normalize', 'key', 'compare', 'explain'], { template: 'templates/canon.json', migrated: true }),
|
|
31
|
+
helper('preflight', '@razroo/iso-preflight', ['plan', 'check', 'explain'], { template: 'templates/preflight.json', artifacts: ['batch/preflight-candidates.json', 'batch/preflight-plan.json'], migrated: true }),
|
|
32
|
+
helper('postflight', '@razroo/iso-postflight', ['status', 'check', 'explain'], { template: 'templates/postflight.json', artifacts: ['batch/postflight-outcomes.json'], migrated: true }),
|
|
33
|
+
helper('timeline', '@razroo/iso-timeline', ['status', 'build', 'plan', 'due', 'check', 'verify', 'explain'], { template: 'templates/timeline.json', artifacts: ['.jobforge-timeline.json', '.jobforge-timeline-events.jsonl', 'data/timeline-events.jsonl'], migrated: true }),
|
|
34
|
+
helper('prioritize', '@razroo/iso-prioritize', ['status', 'items', 'build', 'rank', 'select', 'check', 'verify', 'explain'], { template: 'templates/prioritize.json', artifacts: ['.jobforge-prioritize.json', '.jobforge-prioritize-items.json'], migrated: true }),
|
|
35
|
+
helper('lineage', '@razroo/iso-lineage', ['status', 'record', 'check', 'stale', 'verify', 'explain'], { artifacts: ['.jobforge-lineage.json'], migrated: true }),
|
|
36
|
+
helper('redact', '@razroo/iso-redact', ['scan', 'verify', 'apply', 'explain'], { template: 'templates/redact.json', artifacts: ['.jobforge-redacted/'], migrated: true }),
|
|
37
|
+
helper('migrate', '@razroo/iso-migrate', ['plan', 'apply', 'check', 'explain'], { template: 'templates/migrations.json', migrated: true }),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const packageOnly = [
|
|
41
|
+
{ id: 'contract', pkg: '@razroo/iso-contract', template: 'templates/contracts.json', needles: ['templates/contracts.json', 'tracker-line'] },
|
|
42
|
+
{ id: 'orchestrator', pkg: '@razroo/iso-orchestrator', file: 'scripts/batch-orchestrator.mjs', needles: ['iso-orchestrator'] },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const errors = [];
|
|
46
|
+
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
if (group.pkg) hasDependency(group.pkg, group.id);
|
|
49
|
+
if (group.template) fileExists(group.template, group.id);
|
|
50
|
+
fileExists(`scripts/${group.id}.mjs`, group.id);
|
|
51
|
+
|
|
52
|
+
if (!bin.includes(`const ${group.id}Aliases`)) {
|
|
53
|
+
errors.push(`${group.id}: missing ${group.id}Aliases in bin/job-forge.mjs`);
|
|
54
|
+
}
|
|
55
|
+
if (!bin.includes(`scripts/${group.id}.mjs`)) {
|
|
56
|
+
errors.push(`${group.id}: bin/job-forge.mjs does not dispatch scripts/${group.id}.mjs`);
|
|
57
|
+
}
|
|
58
|
+
if (!reference.includes(`job-forge ${group.id}:*`)) {
|
|
59
|
+
errors.push(`${group.id}: modes/reference-local-helpers.md does not mention job-forge ${group.id}:*`);
|
|
60
|
+
}
|
|
61
|
+
if (!architecture.includes(`scripts/${group.id}.mjs`)) {
|
|
62
|
+
errors.push(`${group.id}: docs/ARCHITECTURE.md script table does not mention scripts/${group.id}.mjs`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const alias of group.aliases) {
|
|
66
|
+
if (!pkg.scripts?.[alias]) errors.push(`${group.id}: package.json missing script ${alias}`);
|
|
67
|
+
if (!createIncludesScript(alias)) errors.push(`${group.id}: create-job-forge missing script ${alias}`);
|
|
68
|
+
if (group.migrated && migrationScripts?.[alias] !== `job-forge ${alias}`) {
|
|
69
|
+
errors.push(`${group.id}: migrations.json missing managed script ${alias}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const artifact of group.artifacts) {
|
|
74
|
+
if (!rootIgnore.includes(artifact)) errors.push(`${group.id}: .gitignore missing ${artifact}`);
|
|
75
|
+
if (!create.includes(artifact)) errors.push(`${group.id}: create-job-forge .gitignore template missing ${artifact}`);
|
|
76
|
+
if (group.migrated && !migrationIgnores.includes(artifact)) {
|
|
77
|
+
errors.push(`${group.id}: migrations.json generated ignores missing ${artifact}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const item of packageOnly) {
|
|
83
|
+
hasDependency(item.pkg, item.id);
|
|
84
|
+
if (item.template) fileExists(item.template, item.id);
|
|
85
|
+
if (item.file) fileExists(item.file, item.id);
|
|
86
|
+
for (const needle of item.needles || []) {
|
|
87
|
+
if (!reference.includes(needle) && !architecture.includes(needle)) {
|
|
88
|
+
errors.push(`${item.id}: expected documentation needle "${needle}"`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (/\[D(?:9|1\d|2[0-9])\]/.test(readText('iso/instructions.md'))) {
|
|
94
|
+
errors.push('iso/instructions.md still has detailed helper D-rules; keep helper details in modes/reference-local-helpers.md');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (errors.length) {
|
|
98
|
+
console.error('JobForge helper integration check failed:');
|
|
99
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`JobForge helper integration passed (${groups.length + packageOnly.length} helpers).`);
|
|
104
|
+
|
|
105
|
+
function helper(id, pkg, commands, options = {}) {
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
pkg,
|
|
109
|
+
aliases: commands.map((command) => `${id}:${command}`),
|
|
110
|
+
template: options.template || '',
|
|
111
|
+
artifacts: options.artifacts || [],
|
|
112
|
+
migrated: Boolean(options.migrated),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readJson(path) {
|
|
117
|
+
return JSON.parse(readText(path));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readText(path) {
|
|
121
|
+
return readFileSync(join(root, path), 'utf8');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function fileExists(path, id) {
|
|
125
|
+
if (!existsSync(join(root, path))) errors.push(`${id}: missing ${path}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasDependency(name, id) {
|
|
129
|
+
if (!pkg.dependencies?.[name] && !pkg.devDependencies?.[name]) {
|
|
130
|
+
errors.push(`${id}: package.json missing dependency ${name}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function migrationValue(id, pointer = '') {
|
|
135
|
+
const migration = migrations.migrations?.find((item) => item.id === id);
|
|
136
|
+
const operation = migration?.operations?.find((item) => pointer ? item.pointer === pointer : item.type === 'ensure-lines');
|
|
137
|
+
return pointer ? operation?.value : operation?.lines || [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createIncludesScript(alias) {
|
|
141
|
+
return create.includes(`'${alias}': 'job-forge ${alias}'`) ||
|
|
142
|
+
create.includes(`"${alias}": "job-forge ${alias}"`);
|
|
143
|
+
}
|
|
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
|
|
|
5
5
|
const root = resolve(process.argv[2] ?? ".");
|
|
6
6
|
const files = {
|
|
7
7
|
instructions: readFileSync(resolve(root, "iso/instructions.md"), "utf8"),
|
|
8
|
+
helpers: readFileSync(resolve(root, "modes/reference-local-helpers.md"), "utf8"),
|
|
8
9
|
apply: readFileSync(resolve(root, "modes/apply.md"), "utf8"),
|
|
9
10
|
models: readFileSync(resolve(root, "models.yaml"), "utf8"),
|
|
10
11
|
config: readFileSync(resolve(root, "iso/config.json"), "utf8"),
|
|
@@ -19,8 +20,9 @@ const checks = [
|
|
|
19
20
|
["H5 blocks same-company concurrent retry", () => every(files.instructions, ["Re-dispatch the same company only AFTER", "previous subagent returns"])],
|
|
20
21
|
["H6 requires merge and verify", () => every(files.instructions, ["batch/tracker-additions/*.tsv", "npx job-forge merge", "npx job-forge verify"])],
|
|
21
22
|
["H7 distrusts subagent prose", () => every(files.instructions, ["must originate from a file", "not from prior subagent prose"])],
|
|
22
|
-
["
|
|
23
|
-
["
|
|
23
|
+
["root points to consolidated helper reference", () => every(files.instructions, ["[D8]", "modes/reference-local-helpers.md", "deterministic local helpers"])],
|
|
24
|
+
["helper reference covers score/timeline/prioritize/lineage", () => every(files.helpers, ["templates/score.json", "npx job-forge score:*", "templates/timeline.json", "npx job-forge timeline:*", "templates/prioritize.json", "npx job-forge prioritize:*", ".jobforge-lineage.json", "npx job-forge lineage:*"])],
|
|
25
|
+
["root helper defaults are consolidated", () => !/\[D(?:9|1\d|2[0-9])\]/.test(files.instructions)],
|
|
24
26
|
["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"])],
|
|
25
27
|
["apply mode owns high-stakes upgrade", () => every(files.apply, ["[D8]", "@general-paid", "4.0/5", "high-stakes"])],
|
|
26
28
|
["apply mode blocks provider auto-downgrade", () => every(files.apply, ["[D9]", "do not auto-downgrade", "inspect telemetry before retrying"])],
|