job-forge 2.14.14 → 2.14.16
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 +6 -3
- package/.opencode/skills/job-forge.md +7 -0
- package/AGENTS.md +6 -3
- package/CLAUDE.md +6 -3
- package/README.md +5 -1
- package/bin/create-job-forge.mjs +11 -1
- package/bin/job-forge.mjs +55 -0
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/CUSTOMIZATION.md +25 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +5 -0
- package/iso/commands/job-forge.md +7 -0
- package/iso/instructions.md +6 -3
- package/lib/jobforge-ledger.mjs +214 -0
- package/merge-tracker.mjs +23 -0
- package/package.json +10 -1
- package/scripts/guard.mjs +404 -0
- package/scripts/ledger.mjs +359 -0
- package/scripts/telemetry.mjs +14 -0
- package/scripts/tracker-line.mjs +8 -0
- package/templates/guards/jobforge-baseline.yaml +50 -0
- package/verify-pipeline.mjs +21 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'fs';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
formatEvents,
|
|
7
|
+
formatVerifyResult,
|
|
8
|
+
queryEvents,
|
|
9
|
+
} from '@razroo/iso-ledger';
|
|
10
|
+
import { PROJECT_DIR, readAllEntries } from '../tracker-lib.mjs';
|
|
11
|
+
import {
|
|
12
|
+
appendJobForgeEvent,
|
|
13
|
+
buildApplicationEvent,
|
|
14
|
+
buildPipelineEvent,
|
|
15
|
+
companyRoleKey,
|
|
16
|
+
jobForgeLedgerPath,
|
|
17
|
+
jobForgeLedgerSummary,
|
|
18
|
+
ledgerExists,
|
|
19
|
+
readJobForgeLedger,
|
|
20
|
+
urlKey,
|
|
21
|
+
verifyJobForgeLedger,
|
|
22
|
+
} from '../lib/jobforge-ledger.mjs';
|
|
23
|
+
|
|
24
|
+
const USAGE = `job-forge ledger - local deterministic workflow state
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
job-forge ledger:status [--json]
|
|
28
|
+
job-forge ledger:rebuild [--reset] [--json]
|
|
29
|
+
job-forge ledger:verify [--json]
|
|
30
|
+
job-forge ledger:has --url <url> [--json]
|
|
31
|
+
job-forge ledger:has --company <name> --role <role> [--status Applied] [--json]
|
|
32
|
+
job-forge ledger:query [--type <type>] [--key <key>] [--where field=value] [--limit N] [--json]
|
|
33
|
+
job-forge ledger:path
|
|
34
|
+
|
|
35
|
+
The ledger is stored at .jobforge-ledger/events.jsonl by default. It is local
|
|
36
|
+
personal workflow state, not an MCP and not prompt context.`;
|
|
37
|
+
|
|
38
|
+
const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
|
|
39
|
+
const opts = parseArgs(rawArgs);
|
|
40
|
+
|
|
41
|
+
if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
42
|
+
console.log(USAGE);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (cmd === 'path') {
|
|
48
|
+
console.log(jobForgeLedgerPath(PROJECT_DIR));
|
|
49
|
+
} else if (cmd === 'status') {
|
|
50
|
+
status(opts);
|
|
51
|
+
} else if (cmd === 'rebuild') {
|
|
52
|
+
rebuild(opts);
|
|
53
|
+
} else if (cmd === 'verify') {
|
|
54
|
+
verify(opts);
|
|
55
|
+
} else if (cmd === 'has') {
|
|
56
|
+
has(opts);
|
|
57
|
+
} else if (cmd === 'query') {
|
|
58
|
+
query(opts);
|
|
59
|
+
} else {
|
|
60
|
+
console.error(`unknown ledger command "${cmd}"\n`);
|
|
61
|
+
console.error(USAGE);
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseArgs(args) {
|
|
70
|
+
const opts = { where: {}, json: false, reset: false };
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
const arg = args[i];
|
|
73
|
+
if (arg === '--json') {
|
|
74
|
+
opts.json = true;
|
|
75
|
+
} else if (arg === '--reset') {
|
|
76
|
+
opts.reset = true;
|
|
77
|
+
} else if (arg === '--url') {
|
|
78
|
+
opts.url = valueAfter(args, ++i, '--url');
|
|
79
|
+
} else if (arg.startsWith('--url=')) {
|
|
80
|
+
opts.url = arg.slice('--url='.length);
|
|
81
|
+
} else if (arg === '--company') {
|
|
82
|
+
opts.company = valueAfter(args, ++i, '--company');
|
|
83
|
+
} else if (arg.startsWith('--company=')) {
|
|
84
|
+
opts.company = arg.slice('--company='.length);
|
|
85
|
+
} else if (arg === '--role') {
|
|
86
|
+
opts.role = valueAfter(args, ++i, '--role');
|
|
87
|
+
} else if (arg.startsWith('--role=')) {
|
|
88
|
+
opts.role = arg.slice('--role='.length);
|
|
89
|
+
} else if (arg === '--status') {
|
|
90
|
+
opts.status = valueAfter(args, ++i, '--status');
|
|
91
|
+
} else if (arg.startsWith('--status=')) {
|
|
92
|
+
opts.status = arg.slice('--status='.length);
|
|
93
|
+
} else if (arg === '--type') {
|
|
94
|
+
opts.type = valueAfter(args, ++i, '--type');
|
|
95
|
+
} else if (arg.startsWith('--type=')) {
|
|
96
|
+
opts.type = arg.slice('--type='.length);
|
|
97
|
+
} else if (arg === '--key') {
|
|
98
|
+
opts.key = valueAfter(args, ++i, '--key');
|
|
99
|
+
} else if (arg.startsWith('--key=')) {
|
|
100
|
+
opts.key = arg.slice('--key='.length);
|
|
101
|
+
} else if (arg === '--where') {
|
|
102
|
+
addWhere(opts.where, valueAfter(args, ++i, '--where'));
|
|
103
|
+
} else if (arg.startsWith('--where=')) {
|
|
104
|
+
addWhere(opts.where, arg.slice('--where='.length));
|
|
105
|
+
} else if (arg === '--limit') {
|
|
106
|
+
opts.limit = Number(valueAfter(args, ++i, '--limit'));
|
|
107
|
+
} else if (arg.startsWith('--limit=')) {
|
|
108
|
+
opts.limit = Number(arg.slice('--limit='.length));
|
|
109
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
110
|
+
opts.help = true;
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (opts.status) opts.where.status = opts.status;
|
|
116
|
+
return opts;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function valueAfter(values, index, flag) {
|
|
120
|
+
const value = values[index];
|
|
121
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function addWhere(where, raw) {
|
|
126
|
+
const index = raw.indexOf('=');
|
|
127
|
+
if (index <= 0) throw new Error('--where must be field=value');
|
|
128
|
+
where[raw.slice(0, index)] = parsePrimitive(raw.slice(index + 1));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parsePrimitive(value) {
|
|
132
|
+
if (value === 'true') return true;
|
|
133
|
+
if (value === 'false') return false;
|
|
134
|
+
if (value === 'null') return null;
|
|
135
|
+
const number = Number(value);
|
|
136
|
+
return Number.isFinite(number) && value.trim() !== '' ? number : value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function status(opts) {
|
|
140
|
+
const summary = jobForgeLedgerSummary(PROJECT_DIR);
|
|
141
|
+
if (opts.json) {
|
|
142
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!summary.exists) {
|
|
146
|
+
console.log(`ledger: missing (${relativeLedgerPath()})`);
|
|
147
|
+
console.log('run: job-forge ledger:rebuild');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const verifyResult = verifyJobForgeLedger(PROJECT_DIR);
|
|
151
|
+
console.log(`ledger: ${relativeLedgerPath()}`);
|
|
152
|
+
console.log(`events: ${summary.events}`);
|
|
153
|
+
console.log(`entities: ${summary.entities}`);
|
|
154
|
+
console.log(`verify: ${verifyResult.ok ? 'PASS' : 'FAIL'} (${verifyResult.errors} errors, ${verifyResult.warnings} warnings)`);
|
|
155
|
+
if (summary.latest) {
|
|
156
|
+
console.log(`latest: ${summary.latest.type} @ ${summary.latest.at}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function rebuild(opts) {
|
|
161
|
+
const ledgerPath = jobForgeLedgerPath(PROJECT_DIR);
|
|
162
|
+
if (opts.reset && existsSync(ledgerPath)) rmSync(ledgerPath);
|
|
163
|
+
mkdirSync(dirname(ledgerPath), { recursive: true });
|
|
164
|
+
|
|
165
|
+
const results = [];
|
|
166
|
+
for (const event of collectProjectEvents()) {
|
|
167
|
+
results.push(appendJobForgeEvent(event, PROJECT_DIR));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const summary = {
|
|
171
|
+
path: ledgerPath,
|
|
172
|
+
eventsSeen: results.length,
|
|
173
|
+
appended: results.filter((result) => result.appended).length,
|
|
174
|
+
deduped: results.filter((result) => !result.appended).length,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (opts.json) {
|
|
178
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(`ledger: ${relativeLedgerPath()}`);
|
|
182
|
+
console.log(`events: ${summary.appended} appended, ${summary.deduped} already present`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function verify(opts) {
|
|
186
|
+
if (!ledgerExists(PROJECT_DIR)) {
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgeLedgerPath(PROJECT_DIR) }, null, 2));
|
|
189
|
+
} else {
|
|
190
|
+
console.log(`ledger: missing (${relativeLedgerPath()})`);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const result = verifyJobForgeLedger(PROJECT_DIR);
|
|
195
|
+
if (opts.json) {
|
|
196
|
+
console.log(JSON.stringify(result, null, 2));
|
|
197
|
+
} else {
|
|
198
|
+
console.log(formatVerifyResult(result));
|
|
199
|
+
}
|
|
200
|
+
process.exit(result.errors > 0 ? 1 : 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function has(opts) {
|
|
204
|
+
const filters = queryFilters(opts);
|
|
205
|
+
const events = queryEvents(readJobForgeLedger(PROJECT_DIR), filters);
|
|
206
|
+
if (opts.json) {
|
|
207
|
+
console.log(JSON.stringify({ match: events.length > 0, count: events.length, filters }, null, 2));
|
|
208
|
+
} else if (events.length > 0) {
|
|
209
|
+
console.log(`MATCH (${events.length} event(s))`);
|
|
210
|
+
} else {
|
|
211
|
+
console.log('MISS');
|
|
212
|
+
}
|
|
213
|
+
process.exit(events.length > 0 ? 0 : 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function query(opts) {
|
|
217
|
+
const filters = queryFilters(opts);
|
|
218
|
+
const events = queryEvents(readJobForgeLedger(PROJECT_DIR), filters);
|
|
219
|
+
if (opts.json) {
|
|
220
|
+
console.log(JSON.stringify(events, null, 2));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
console.log(formatEvents(events));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function queryFilters(opts) {
|
|
227
|
+
const filters = {};
|
|
228
|
+
if (opts.type) filters.type = opts.type;
|
|
229
|
+
if (opts.key) filters.key = opts.key;
|
|
230
|
+
if (opts.url) filters.key = urlKey(opts.url);
|
|
231
|
+
if (opts.company || opts.role) {
|
|
232
|
+
if (!opts.company || !opts.role) throw new Error('--company and --role must be provided together');
|
|
233
|
+
filters.key = companyRoleKey(opts.company, opts.role);
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(opts.where || {}).length > 0) filters.where = opts.where;
|
|
236
|
+
if (Number.isFinite(opts.limit) && opts.limit > 0) filters.limit = opts.limit;
|
|
237
|
+
return filters;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function collectProjectEvents() {
|
|
241
|
+
const events = [];
|
|
242
|
+
const { entries } = readAllEntries();
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
events.push(buildApplicationEvent('jobforge.application.tracker', entry, {
|
|
245
|
+
projectDir: PROJECT_DIR,
|
|
246
|
+
sourceFile: entry._sourceFile,
|
|
247
|
+
idempotencyPrefix: 'tracker-entry',
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const item of collectTrackerTsvs('batch/tracker-additions', 'pending')) {
|
|
252
|
+
events.push(buildApplicationEvent(`jobforge.tracker_addition.${item.state}`, item.addition, {
|
|
253
|
+
projectDir: PROJECT_DIR,
|
|
254
|
+
sourceFile: item.path,
|
|
255
|
+
idempotencyPrefix: `tracker-addition-${item.state}`,
|
|
256
|
+
data: { state: item.state },
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const item of collectTrackerTsvs('batch/tracker-additions/merged', 'merged')) {
|
|
261
|
+
events.push(buildApplicationEvent(`jobforge.tracker_addition.${item.state}`, item.addition, {
|
|
262
|
+
projectDir: PROJECT_DIR,
|
|
263
|
+
sourceFile: item.path,
|
|
264
|
+
idempotencyPrefix: `tracker-addition-${item.state}`,
|
|
265
|
+
data: { state: item.state },
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const item of collectPipelineItems()) {
|
|
270
|
+
events.push(buildPipelineEvent(item, {
|
|
271
|
+
projectDir: PROJECT_DIR,
|
|
272
|
+
sourceFile: join(PROJECT_DIR, 'data', 'pipeline.md'),
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return events;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function collectTrackerTsvs(relDir, state) {
|
|
280
|
+
const dir = join(PROJECT_DIR, relDir);
|
|
281
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
282
|
+
const out = [];
|
|
283
|
+
for (const name of readdirSync(dir).filter((file) => file.endsWith('.tsv')).sort()) {
|
|
284
|
+
const path = join(dir, name);
|
|
285
|
+
const addition = parseTsvContent(readFileSync(path, 'utf8'), name);
|
|
286
|
+
if (addition) out.push({ path, state, addition });
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseTsvContent(content, filename) {
|
|
292
|
+
const text = content.trim();
|
|
293
|
+
if (!text) return null;
|
|
294
|
+
let parts;
|
|
295
|
+
if (text.startsWith('|')) {
|
|
296
|
+
parts = text.split('|').map((part) => part.trim()).filter(Boolean);
|
|
297
|
+
if (parts.length < 8) return null;
|
|
298
|
+
return {
|
|
299
|
+
num: parts[0],
|
|
300
|
+
date: parts[1],
|
|
301
|
+
company: parts[2],
|
|
302
|
+
role: parts[3],
|
|
303
|
+
score: parts[4],
|
|
304
|
+
status: parts[5],
|
|
305
|
+
pdf: parts[6],
|
|
306
|
+
report: parts[7],
|
|
307
|
+
notes: parts[8] || '',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
parts = text.split('\t');
|
|
312
|
+
if (parts.length < 8) return null;
|
|
313
|
+
const col4 = parts[4].trim();
|
|
314
|
+
const col5 = parts[5].trim();
|
|
315
|
+
const col4LooksLikeScore = looksLikeScore(col4);
|
|
316
|
+
const col5LooksLikeScore = looksLikeScore(col5);
|
|
317
|
+
return {
|
|
318
|
+
num: parts[0],
|
|
319
|
+
date: parts[1],
|
|
320
|
+
company: parts[2],
|
|
321
|
+
role: parts[3],
|
|
322
|
+
status: col4LooksLikeScore && !col5LooksLikeScore ? col5 : col4,
|
|
323
|
+
score: col4LooksLikeScore && !col5LooksLikeScore ? col4 : col5,
|
|
324
|
+
pdf: parts[6],
|
|
325
|
+
report: parts[7],
|
|
326
|
+
notes: parts[8] || '',
|
|
327
|
+
sourceFile: filename,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function looksLikeScore(value) {
|
|
332
|
+
return /^\d+\.?\d*\/5$/.test(value) || value === 'N/A' || value === 'DUP';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function collectPipelineItems() {
|
|
336
|
+
const path = join(PROJECT_DIR, 'data', 'pipeline.md');
|
|
337
|
+
if (!existsSync(path)) return [];
|
|
338
|
+
const lines = readFileSync(path, 'utf8').split('\n');
|
|
339
|
+
const out = [];
|
|
340
|
+
lines.forEach((line, index) => {
|
|
341
|
+
const match = line.match(/^\s*-\s*\[([ xX])\]\s+([^|#\s]+)(.*)$/);
|
|
342
|
+
if (!match) return;
|
|
343
|
+
const rest = match[3] || '';
|
|
344
|
+
const fields = rest.split('|').map((field) => field.trim()).filter(Boolean);
|
|
345
|
+
out.push({
|
|
346
|
+
checked: match[1].toLowerCase() === 'x',
|
|
347
|
+
url: match[2].trim(),
|
|
348
|
+
company: fields[0] || '',
|
|
349
|
+
role: fields[1] || '',
|
|
350
|
+
line,
|
|
351
|
+
lineNumber: index + 1,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function relativeLedgerPath() {
|
|
358
|
+
return jobForgeLedgerPath(PROJECT_DIR).replace(`${PROJECT_DIR}/`, '');
|
|
359
|
+
}
|
package/scripts/telemetry.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { spawnSync } from 'child_process';
|
|
|
4
4
|
import { existsSync, readdirSync, statSync } from 'fs';
|
|
5
5
|
import { join, resolve } from 'path';
|
|
6
6
|
import { defaultOpenCodeDbPath, findSessionById, parseSinceCutoff } from '@razroo/iso-trace';
|
|
7
|
+
import { jobForgeLedgerSummary } from '../lib/jobforge-ledger.mjs';
|
|
7
8
|
|
|
8
9
|
const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
9
10
|
const DEFAULT_SINCE = '24h';
|
|
@@ -485,9 +486,21 @@ function sessionStatus({ taskCalls, children, childOutcomes, childProviderErrors
|
|
|
485
486
|
function trackerStatus(projectDir) {
|
|
486
487
|
const pendingDir = join(projectDir, 'batch', 'tracker-additions');
|
|
487
488
|
const mergedDir = join(pendingDir, 'merged');
|
|
489
|
+
let ledger;
|
|
490
|
+
try {
|
|
491
|
+
ledger = jobForgeLedgerSummary(projectDir);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
ledger = {
|
|
494
|
+
exists: true,
|
|
495
|
+
events: 0,
|
|
496
|
+
entities: 0,
|
|
497
|
+
error: error instanceof Error ? error.message : String(error),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
488
500
|
return {
|
|
489
501
|
pending: listTsv(pendingDir),
|
|
490
502
|
mergedCount: listTsv(mergedDir).length,
|
|
503
|
+
ledger,
|
|
491
504
|
};
|
|
492
505
|
}
|
|
493
506
|
|
|
@@ -636,6 +649,7 @@ function printStatus(telemetry) {
|
|
|
636
649
|
console.log(`tasks: ${telemetry.tasks.total} (${telemetry.tasks.statusPolls} status-poll, ${telemetry.tasks.running} running)`);
|
|
637
650
|
console.log(`children: ${telemetry.children.withOutcomes}/${telemetry.children.total} with outcomes`);
|
|
638
651
|
console.log(`tracker: ${telemetry.tracker.pending.length} pending TSVs, ${telemetry.tracker.mergedCount} merged TSVs`);
|
|
652
|
+
console.log(`ledger: ${telemetry.tracker.ledger.error ? `error: ${telemetry.tracker.ledger.error}` : telemetry.tracker.ledger.exists ? `${telemetry.tracker.ledger.events} events` : 'missing'}`);
|
|
639
653
|
console.log(`models: ${telemetry.models.slice(0, 3).map(modelLabel).join(', ') || 'none'}`);
|
|
640
654
|
console.log(`errors: ${telemetry.providerErrors.length} root, ${telemetry.children.providerErrors} child provider errors, ${telemetry.children.toolErrors} child tool errors`);
|
|
641
655
|
console.log(`issues: ${telemetry.policyIssues.length}`);
|
package/scripts/tracker-line.mjs
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
26
26
|
import { join } from 'path';
|
|
27
|
+
import { recordTrackerAdditionWritten } from '../lib/jobforge-ledger.mjs';
|
|
27
28
|
|
|
28
29
|
const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
|
|
29
30
|
|
|
@@ -61,6 +62,13 @@ if (write) {
|
|
|
61
62
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
62
63
|
const path = join(dir, `${num}.tsv`);
|
|
63
64
|
writeFileSync(path, line + '\n', 'utf-8');
|
|
65
|
+
try {
|
|
66
|
+
recordTrackerAdditionWritten({
|
|
67
|
+
num, date, company, role, status, score: scoreField, pdf, report: reportLink, notes,
|
|
68
|
+
}, { projectDir: PROJECT_DIR, sourceFile: path });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn(`warning: could not append tracker-line ledger event: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
64
72
|
console.log(path);
|
|
65
73
|
} else {
|
|
66
74
|
console.log(line);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
rules:
|
|
3
|
+
- id: JF-H1
|
|
4
|
+
type: max-per-group
|
|
5
|
+
severity: error
|
|
6
|
+
description: Do not dispatch more than two task subagents from one assistant message.
|
|
7
|
+
match:
|
|
8
|
+
type: tool_call
|
|
9
|
+
name: task
|
|
10
|
+
groupBy: sessionMessageId
|
|
11
|
+
max: 2
|
|
12
|
+
|
|
13
|
+
- id: JF-H5b
|
|
14
|
+
type: forbid-text
|
|
15
|
+
severity: error
|
|
16
|
+
description: Do not use task to poll task/session status.
|
|
17
|
+
match:
|
|
18
|
+
type: tool_call
|
|
19
|
+
name: task
|
|
20
|
+
patterns:
|
|
21
|
+
- source: '"task_id"\s*:'
|
|
22
|
+
- source: '\b(return your final outcome now|report your current status|current status|still working|still running)\b'
|
|
23
|
+
flags: i
|
|
24
|
+
- source: '\b(check|poll|fetch|ask)\b.{0,80}\b(task|session)\b.{0,60}\b(status|state|result|outcome)\b'
|
|
25
|
+
flags: i
|
|
26
|
+
|
|
27
|
+
- id: JF-H5-child-no-task
|
|
28
|
+
type: forbid-text
|
|
29
|
+
severity: error
|
|
30
|
+
description: Child/subagent sessions must not spawn more task subagents.
|
|
31
|
+
match:
|
|
32
|
+
type: tool_call
|
|
33
|
+
name: task
|
|
34
|
+
fields:
|
|
35
|
+
isChildSession: true
|
|
36
|
+
patterns:
|
|
37
|
+
- source: '[\s\S]'
|
|
38
|
+
|
|
39
|
+
- id: JF-H8
|
|
40
|
+
type: forbid-text
|
|
41
|
+
severity: error
|
|
42
|
+
description: Task prompts must not inline proxy configuration or proxy credentials.
|
|
43
|
+
match:
|
|
44
|
+
type: tool_call
|
|
45
|
+
name: task
|
|
46
|
+
patterns:
|
|
47
|
+
- source: '\bproxy\s*:\s*\{'
|
|
48
|
+
flags: i
|
|
49
|
+
- source: '\bproxy[_.-]?(server|username|password|bypass)\b\s*[:=]'
|
|
50
|
+
flags: i
|
package/verify-pipeline.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 6. No pending TSVs in tracker-additions/ (runs even when tracker file is missing)
|
|
16
16
|
* 7. No markdown bold in score column
|
|
17
17
|
* 8. Drift warning if states.yml ids differ from the built-in fallback list
|
|
18
|
+
* 9. Ledger file verifies if .jobforge-ledger/events.jsonl exists
|
|
18
19
|
*
|
|
19
20
|
* Run: node verify-pipeline.mjs (from repo root; same as npm run verify)
|
|
20
21
|
*/
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
PROJECT_DIR, DATA_APPS_DIR, DATA_APPS_FILE, ROOT_APPS_FILE,
|
|
27
28
|
usesDayFiles, readAllEntries, listDayFiles, dayFilePath,
|
|
28
29
|
} from './tracker-lib.mjs';
|
|
30
|
+
import { jobForgeLedgerPath, ledgerExists, verifyJobForgeLedger } from './lib/jobforge-ledger.mjs';
|
|
29
31
|
|
|
30
32
|
const ADDITIONS_DIR = join(PROJECT_DIR, 'batch/tracker-additions');
|
|
31
33
|
const STATES_FILE = existsSync(join(PROJECT_DIR, 'templates/states.yml'))
|
|
@@ -127,6 +129,23 @@ function verifyStatesYamlDrift() {
|
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
function verifyLedgerIfPresent() {
|
|
133
|
+
if (!ledgerExists(PROJECT_DIR)) {
|
|
134
|
+
ok('Ledger not initialized');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const result = verifyJobForgeLedger(PROJECT_DIR);
|
|
138
|
+
for (const issue of result.issues) {
|
|
139
|
+
const prefix = issue.line ? `ledger line ${issue.line}` : 'ledger';
|
|
140
|
+
const msg = `${prefix}: ${issue.code}: ${issue.message}`;
|
|
141
|
+
if (issue.severity === 'error') error(msg);
|
|
142
|
+
else warn(msg);
|
|
143
|
+
}
|
|
144
|
+
if (result.errors === 0) {
|
|
145
|
+
ok(`Ledger valid (${result.eventCount} events at ${relative(PROJECT_DIR, jobForgeLedgerPath(PROJECT_DIR))})`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
// --- Read entries ---
|
|
131
150
|
const { entries, source } = readAllEntries();
|
|
132
151
|
|
|
@@ -135,6 +154,7 @@ if (entries.length === 0) {
|
|
|
135
154
|
console.log(' This is normal for a fresh setup.\n');
|
|
136
155
|
checkPendingTrackerAdditions();
|
|
137
156
|
verifyStatesYamlDrift();
|
|
157
|
+
verifyLedgerIfPresent();
|
|
138
158
|
console.log('\n' + '='.repeat(50));
|
|
139
159
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|
|
140
160
|
if (errors === 0 && warnings === 0) console.log('🟢 Pipeline is clean!');
|
|
@@ -254,6 +274,7 @@ for (const e of entries) {
|
|
|
254
274
|
if (boldScores === 0) ok('No bold in scores');
|
|
255
275
|
|
|
256
276
|
verifyStatesYamlDrift();
|
|
277
|
+
verifyLedgerIfPresent();
|
|
257
278
|
|
|
258
279
|
console.log('\n' + '='.repeat(50));
|
|
259
280
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|