job-forge 2.14.32 → 2.14.34
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 +12 -3
- package/.opencode/skills/job-forge.md +4 -0
- package/AGENTS.md +12 -3
- package/CLAUDE.md +12 -3
- package/README.md +11 -5
- package/bin/create-job-forge.mjs +21 -0
- package/bin/job-forge.mjs +103 -0
- package/docs/ARCHITECTURE.md +19 -3
- package/docs/CUSTOMIZATION.md +12 -0
- package/docs/README.md +1 -1
- package/docs/SETUP.md +7 -0
- package/iso/commands/job-forge.md +4 -0
- package/iso/instructions.md +12 -3
- package/lib/jobforge-lineage.mjs +122 -0
- package/lib/jobforge-prioritize.mjs +294 -0
- package/lib/jobforge-timeline.mjs +294 -0
- package/modes/followup.md +6 -6
- package/package.json +25 -1
- package/scripts/check-iso-smoke.mjs +3 -0
- package/scripts/lineage.mjs +247 -0
- package/scripts/prioritize.mjs +323 -0
- package/scripts/timeline.mjs +237 -0
- package/templates/migrations.json +27 -0
- package/templates/prioritize.json +125 -0
- package/templates/timeline.json +86 -0
- package/verify-pipeline.mjs +68 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { isAbsolute, relative, resolve } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
formatCheckResult,
|
|
7
|
+
formatConfigSummary,
|
|
8
|
+
formatPrioritizeResult,
|
|
9
|
+
formatVerifyResult,
|
|
10
|
+
loadPrioritizeItems,
|
|
11
|
+
parseJson,
|
|
12
|
+
} from '@razroo/iso-prioritize';
|
|
13
|
+
import { PROJECT_DIR } from '../tracker-lib.mjs';
|
|
14
|
+
import {
|
|
15
|
+
buildJobForgePrioritizeItems,
|
|
16
|
+
checkJobForgePrioritize,
|
|
17
|
+
jobForgePrioritizeConfigPath,
|
|
18
|
+
jobForgePrioritizeItemsPath,
|
|
19
|
+
jobForgePrioritizePath,
|
|
20
|
+
jobForgePrioritizeSummary,
|
|
21
|
+
prioritizeExists,
|
|
22
|
+
rankJobForgePrioritize,
|
|
23
|
+
readJobForgePrioritizeConfig,
|
|
24
|
+
selectJobForgePrioritize,
|
|
25
|
+
verifyJobForgePrioritize,
|
|
26
|
+
writeJobForgePrioritize,
|
|
27
|
+
writeJobForgePrioritizeItems,
|
|
28
|
+
} from '../lib/jobforge-prioritize.mjs';
|
|
29
|
+
|
|
30
|
+
const USAGE = `job-forge prioritize - deterministic next-action ranking
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
job-forge prioritize:status [--json]
|
|
34
|
+
job-forge prioritize:items [--now <iso>] [--out <file>] [--json]
|
|
35
|
+
job-forge prioritize:build [--now <iso>] [--profile <name>] [--limit N] [--out <file>] [--items-out <file>] [--json]
|
|
36
|
+
job-forge prioritize:rank [--items <file>] [--profile <name>] [--limit N] [--json]
|
|
37
|
+
job-forge prioritize:select [--items <file>] [--profile <name>] [--limit N] [--json]
|
|
38
|
+
job-forge prioritize:check [--items <file>] [--profile <name>] [--limit N] [--min-selected N] [--fail-on blocked|skipped,blocked|none] [--json]
|
|
39
|
+
job-forge prioritize:verify [--json]
|
|
40
|
+
job-forge prioritize:explain [--profile <name>] [--json]
|
|
41
|
+
job-forge prioritize:path [--config|--items]
|
|
42
|
+
|
|
43
|
+
Default policy is templates/prioritize.json. The generated queue is local
|
|
44
|
+
project state (.jobforge-prioritize.json), derived from source-backed facts and
|
|
45
|
+
due timeline items.`;
|
|
46
|
+
|
|
47
|
+
const [cmd = 'help', ...rawArgs] = process.argv.slice(2);
|
|
48
|
+
const opts = parseArgs(rawArgs);
|
|
49
|
+
|
|
50
|
+
if (opts.help || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
51
|
+
console.log(USAGE);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (cmd === 'path') {
|
|
57
|
+
path(opts);
|
|
58
|
+
} else if (cmd === 'status') {
|
|
59
|
+
status(opts);
|
|
60
|
+
} else if (cmd === 'items') {
|
|
61
|
+
items(opts);
|
|
62
|
+
} else if (cmd === 'build') {
|
|
63
|
+
build(opts);
|
|
64
|
+
} else if (cmd === 'rank') {
|
|
65
|
+
rank(opts);
|
|
66
|
+
} else if (cmd === 'select') {
|
|
67
|
+
select(opts);
|
|
68
|
+
} else if (cmd === 'check') {
|
|
69
|
+
check(opts);
|
|
70
|
+
} else if (cmd === 'verify') {
|
|
71
|
+
verify(opts);
|
|
72
|
+
} else if (cmd === 'explain') {
|
|
73
|
+
explain(opts);
|
|
74
|
+
} else {
|
|
75
|
+
console.error(`unknown prioritize command "${cmd}"\n`);
|
|
76
|
+
console.error(USAGE);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(args) {
|
|
85
|
+
const opts = {
|
|
86
|
+
json: false,
|
|
87
|
+
help: false,
|
|
88
|
+
rebuild: true,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < args.length; i++) {
|
|
92
|
+
const arg = args[i];
|
|
93
|
+
if (arg === '--json') {
|
|
94
|
+
opts.json = true;
|
|
95
|
+
} else if (arg === '--no-rebuild') {
|
|
96
|
+
opts.rebuild = false;
|
|
97
|
+
} else if (arg === '--profile') {
|
|
98
|
+
opts.profile = valueAfter(args, ++i, '--profile');
|
|
99
|
+
} else if (arg.startsWith('--profile=')) {
|
|
100
|
+
opts.profile = arg.slice('--profile='.length);
|
|
101
|
+
} else if (arg === '--limit') {
|
|
102
|
+
opts.limit = parsePositiveInteger(valueAfter(args, ++i, '--limit'), '--limit');
|
|
103
|
+
} else if (arg.startsWith('--limit=')) {
|
|
104
|
+
opts.limit = parsePositiveInteger(arg.slice('--limit='.length), '--limit');
|
|
105
|
+
} else if (arg === '--min-selected') {
|
|
106
|
+
opts.minSelected = parseNonNegativeInteger(valueAfter(args, ++i, '--min-selected'), '--min-selected');
|
|
107
|
+
} else if (arg.startsWith('--min-selected=')) {
|
|
108
|
+
opts.minSelected = parseNonNegativeInteger(arg.slice('--min-selected='.length), '--min-selected');
|
|
109
|
+
} else if (arg === '--fail-on') {
|
|
110
|
+
opts.failOn = parseFailOn(valueAfter(args, ++i, '--fail-on'));
|
|
111
|
+
} else if (arg.startsWith('--fail-on=')) {
|
|
112
|
+
opts.failOn = parseFailOn(arg.slice('--fail-on='.length));
|
|
113
|
+
} else if (arg === '--now') {
|
|
114
|
+
opts.now = valueAfter(args, ++i, '--now');
|
|
115
|
+
} else if (arg.startsWith('--now=')) {
|
|
116
|
+
opts.now = arg.slice('--now='.length);
|
|
117
|
+
} else if (arg === '--items') {
|
|
118
|
+
if (!args[i + 1] || args[i + 1].startsWith('--')) {
|
|
119
|
+
opts.itemsPath = true;
|
|
120
|
+
} else {
|
|
121
|
+
opts.items = valueAfter(args, ++i, '--items');
|
|
122
|
+
}
|
|
123
|
+
} else if (arg.startsWith('--items=')) {
|
|
124
|
+
opts.items = arg.slice('--items='.length);
|
|
125
|
+
} else if (arg === '--out') {
|
|
126
|
+
opts.out = resolveInputPath(valueAfter(args, ++i, '--out'));
|
|
127
|
+
} else if (arg.startsWith('--out=')) {
|
|
128
|
+
opts.out = resolveInputPath(arg.slice('--out='.length));
|
|
129
|
+
} else if (arg === '--items-out') {
|
|
130
|
+
opts.itemsOut = resolveInputPath(valueAfter(args, ++i, '--items-out'));
|
|
131
|
+
} else if (arg.startsWith('--items-out=')) {
|
|
132
|
+
opts.itemsOut = resolveInputPath(arg.slice('--items-out='.length));
|
|
133
|
+
} else if (arg === '--config') {
|
|
134
|
+
opts.configPath = true;
|
|
135
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
136
|
+
opts.help = true;
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error(`unknown flag "${arg}"`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return opts;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function valueAfter(values, index, flag) {
|
|
146
|
+
const value = values[index];
|
|
147
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parsePositiveInteger(value, flag) {
|
|
152
|
+
const parsed = Number(value);
|
|
153
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} must be a positive integer`);
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseNonNegativeInteger(value, flag) {
|
|
158
|
+
const parsed = Number(value);
|
|
159
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} must be a non-negative integer`);
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseFailOn(value) {
|
|
164
|
+
if (value === 'none') return 'none';
|
|
165
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function path(opts) {
|
|
169
|
+
if (opts.configPath) {
|
|
170
|
+
console.log(jobForgePrioritizeConfigPath(PROJECT_DIR));
|
|
171
|
+
} else if (opts.itemsPath) {
|
|
172
|
+
console.log(jobForgePrioritizeItemsPath(PROJECT_DIR));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(jobForgePrioritizePath(PROJECT_DIR));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function status(opts) {
|
|
179
|
+
const summary = jobForgePrioritizeSummary(PROJECT_DIR);
|
|
180
|
+
if (opts.json) {
|
|
181
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!summary.exists) {
|
|
185
|
+
console.log(`prioritize: missing (${relativePath(summary.path)})`);
|
|
186
|
+
console.log('run: job-forge prioritize:build');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const result = verifyJobForgePrioritize({}, PROJECT_DIR);
|
|
190
|
+
console.log(`prioritize: ${relativePath(summary.path)}`);
|
|
191
|
+
console.log(`items: ${relativePath(summary.itemsPath)}`);
|
|
192
|
+
console.log(`profile: ${summary.profile}`);
|
|
193
|
+
console.log(`total: ${summary.items}`);
|
|
194
|
+
console.log(`selected: ${summary.selected}`);
|
|
195
|
+
console.log(`candidate: ${summary.candidate}`);
|
|
196
|
+
console.log(`skipped: ${summary.skipped}`);
|
|
197
|
+
console.log(`blocked: ${summary.blocked}`);
|
|
198
|
+
console.log(`verify: ${result.ok ? 'PASS' : 'FAIL'} (${result.errors} errors, ${result.warnings} warnings)`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function items(opts) {
|
|
202
|
+
const input = buildJobForgePrioritizeItems({ now: opts.now, rebuild: opts.rebuild }, PROJECT_DIR);
|
|
203
|
+
if (opts.out) writeJobForgePrioritizeItems(input, { out: opts.out }, PROJECT_DIR);
|
|
204
|
+
if (opts.json) {
|
|
205
|
+
console.log(JSON.stringify(input, null, 2));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (opts.out) console.log(`items: wrote ${relativePath(opts.out)}`);
|
|
209
|
+
console.log(`items: ${input.items.length}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function build(opts) {
|
|
213
|
+
const input = readItems(opts) || buildJobForgePrioritizeItems({ now: opts.now, rebuild: opts.rebuild }, PROJECT_DIR);
|
|
214
|
+
const result = rankJobForgePrioritize({
|
|
215
|
+
items: input,
|
|
216
|
+
profile: opts.profile,
|
|
217
|
+
limit: opts.limit,
|
|
218
|
+
}, PROJECT_DIR);
|
|
219
|
+
const itemsOut = writeJobForgePrioritizeItems(input, { out: opts.itemsOut }, PROJECT_DIR);
|
|
220
|
+
const out = writeJobForgePrioritize(result, { out: opts.out }, PROJECT_DIR);
|
|
221
|
+
if (opts.json) {
|
|
222
|
+
console.log(JSON.stringify({ out, itemsOut, result }, null, 2));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
console.log(`prioritize: wrote ${relativePath(out)}`);
|
|
226
|
+
console.log(`items: wrote ${relativePath(itemsOut)}`);
|
|
227
|
+
console.log(formatPrioritizeResult(result));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rank(opts) {
|
|
231
|
+
const result = rankJobForgePrioritize({
|
|
232
|
+
items: readItems(opts),
|
|
233
|
+
profile: opts.profile,
|
|
234
|
+
limit: opts.limit,
|
|
235
|
+
now: opts.now,
|
|
236
|
+
rebuild: opts.rebuild,
|
|
237
|
+
}, PROJECT_DIR);
|
|
238
|
+
if (opts.out) writeJobForgePrioritize(result, { out: opts.out }, PROJECT_DIR);
|
|
239
|
+
if (opts.json) {
|
|
240
|
+
console.log(JSON.stringify(result, null, 2));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
console.log(formatPrioritizeResult(result));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function select(opts) {
|
|
247
|
+
const result = selectJobForgePrioritize({
|
|
248
|
+
items: readItems(opts),
|
|
249
|
+
profile: opts.profile,
|
|
250
|
+
limit: opts.limit,
|
|
251
|
+
now: opts.now,
|
|
252
|
+
rebuild: opts.rebuild,
|
|
253
|
+
}, PROJECT_DIR);
|
|
254
|
+
if (opts.out) writeJobForgePrioritize(result, { out: opts.out }, PROJECT_DIR);
|
|
255
|
+
if (opts.json) {
|
|
256
|
+
console.log(JSON.stringify(result, null, 2));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.log(formatPrioritizeResult(result));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function check(opts) {
|
|
263
|
+
const result = checkJobForgePrioritize({
|
|
264
|
+
items: readItems(opts),
|
|
265
|
+
profile: opts.profile,
|
|
266
|
+
limit: opts.limit,
|
|
267
|
+
minSelected: opts.minSelected,
|
|
268
|
+
failOn: opts.failOn,
|
|
269
|
+
now: opts.now,
|
|
270
|
+
rebuild: opts.rebuild,
|
|
271
|
+
}, PROJECT_DIR);
|
|
272
|
+
if (opts.json) {
|
|
273
|
+
console.log(JSON.stringify(result, null, 2));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(formatCheckResult(result));
|
|
276
|
+
}
|
|
277
|
+
process.exit(result.ok ? 0 : 1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function verify(opts) {
|
|
281
|
+
if (!prioritizeExists(PROJECT_DIR)) {
|
|
282
|
+
if (opts.json) {
|
|
283
|
+
console.log(JSON.stringify({ ok: true, missing: true, path: jobForgePrioritizePath(PROJECT_DIR) }, null, 2));
|
|
284
|
+
} else {
|
|
285
|
+
console.log(`prioritize: missing (${relativePath(jobForgePrioritizePath(PROJECT_DIR))})`);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const result = verifyJobForgePrioritize({}, PROJECT_DIR);
|
|
290
|
+
if (opts.json) {
|
|
291
|
+
console.log(JSON.stringify(result, null, 2));
|
|
292
|
+
} else {
|
|
293
|
+
console.log(formatVerifyResult(result));
|
|
294
|
+
}
|
|
295
|
+
process.exit(result.ok ? 0 : 1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function explain(opts) {
|
|
299
|
+
const config = readJobForgePrioritizeConfig(PROJECT_DIR);
|
|
300
|
+
if (opts.json) {
|
|
301
|
+
const value = opts.profile
|
|
302
|
+
? { ...config, profiles: config.profiles.filter((profile) => profile.name === opts.profile) }
|
|
303
|
+
: config;
|
|
304
|
+
console.log(JSON.stringify(value, null, 2));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
console.log(`config: ${relativePath(jobForgePrioritizeConfigPath(PROJECT_DIR))}`);
|
|
308
|
+
console.log(formatConfigSummary(config, opts.profile));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function readItems(opts) {
|
|
312
|
+
if (!opts.items) return undefined;
|
|
313
|
+
const path = resolveInputPath(opts.items);
|
|
314
|
+
return { items: loadPrioritizeItems(parseJson(readFileSync(path, 'utf8'), path)) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function resolveInputPath(path) {
|
|
318
|
+
return isAbsolute(path) ? path : resolve(PROJECT_DIR, path);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function relativePath(path) {
|
|
322
|
+
return relative(PROJECT_DIR, path) || '.';
|
|
323
|
+
}
|
|
@@ -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,27 @@
|
|
|
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",
|
|
66
|
+
"prioritize:status": "job-forge prioritize:status",
|
|
67
|
+
"prioritize:items": "job-forge prioritize:items",
|
|
68
|
+
"prioritize:build": "job-forge prioritize:build",
|
|
69
|
+
"prioritize:rank": "job-forge prioritize:rank",
|
|
70
|
+
"prioritize:select": "job-forge prioritize:select",
|
|
71
|
+
"prioritize:check": "job-forge prioritize:check",
|
|
72
|
+
"prioritize:verify": "job-forge prioritize:verify",
|
|
73
|
+
"prioritize:explain": "job-forge prioritize:explain",
|
|
74
|
+
"lineage:status": "job-forge lineage:status",
|
|
75
|
+
"lineage:record": "job-forge lineage:record",
|
|
76
|
+
"lineage:check": "job-forge lineage:check",
|
|
77
|
+
"lineage:stale": "job-forge lineage:stale",
|
|
78
|
+
"lineage:verify": "job-forge lineage:verify",
|
|
79
|
+
"lineage:explain": "job-forge lineage:explain",
|
|
59
80
|
"redact:scan": "job-forge redact:scan",
|
|
60
81
|
"redact:verify": "job-forge redact:verify",
|
|
61
82
|
"redact:apply": "job-forge redact:apply",
|
|
@@ -83,7 +104,13 @@
|
|
|
83
104
|
".jobforge-cache/",
|
|
84
105
|
".jobforge-index.json",
|
|
85
106
|
".jobforge-facts.json",
|
|
107
|
+
".jobforge-timeline.json",
|
|
108
|
+
".jobforge-timeline-events.jsonl",
|
|
109
|
+
".jobforge-prioritize.json",
|
|
110
|
+
".jobforge-prioritize-items.json",
|
|
111
|
+
".jobforge-lineage.json",
|
|
86
112
|
".jobforge-redacted/",
|
|
113
|
+
"data/timeline-events.jsonl",
|
|
87
114
|
"batch/preflight-candidates.json",
|
|
88
115
|
"batch/preflight-plan.json",
|
|
89
116
|
"batch/postflight-outcomes.json"
|