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,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"defaults": {
|
|
4
|
+
"profile": "jobforge-next-action",
|
|
5
|
+
"limit": 3
|
|
6
|
+
},
|
|
7
|
+
"profiles": [
|
|
8
|
+
{
|
|
9
|
+
"name": "jobforge-next-action",
|
|
10
|
+
"description": "Rank JobForge follow-up, pipeline, and apply work by fit, urgency, age, and source quality.",
|
|
11
|
+
"criteria": [
|
|
12
|
+
{
|
|
13
|
+
"id": "fit-score",
|
|
14
|
+
"label": "Fit score",
|
|
15
|
+
"field": "score",
|
|
16
|
+
"weight": 45,
|
|
17
|
+
"direction": "desc",
|
|
18
|
+
"min": 0,
|
|
19
|
+
"max": 5,
|
|
20
|
+
"required": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "urgency",
|
|
24
|
+
"label": "Urgency",
|
|
25
|
+
"field": "urgency",
|
|
26
|
+
"weight": 30,
|
|
27
|
+
"direction": "desc",
|
|
28
|
+
"min": 0,
|
|
29
|
+
"max": 10,
|
|
30
|
+
"default": 0
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "age",
|
|
34
|
+
"label": "Age in days",
|
|
35
|
+
"field": "ageDays",
|
|
36
|
+
"weight": 15,
|
|
37
|
+
"direction": "desc",
|
|
38
|
+
"min": 0,
|
|
39
|
+
"max": 30,
|
|
40
|
+
"default": 0
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "source-quality",
|
|
44
|
+
"label": "Source quality",
|
|
45
|
+
"field": "sourceQuality",
|
|
46
|
+
"weight": 10,
|
|
47
|
+
"direction": "desc",
|
|
48
|
+
"min": 0,
|
|
49
|
+
"max": 1,
|
|
50
|
+
"default": 0.5
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"gates": [
|
|
54
|
+
{
|
|
55
|
+
"id": "already-rejected",
|
|
56
|
+
"action": "skip",
|
|
57
|
+
"reason": "terminal tracker state",
|
|
58
|
+
"when": {
|
|
59
|
+
"where": {
|
|
60
|
+
"status": [
|
|
61
|
+
"Rejected",
|
|
62
|
+
"Discarded",
|
|
63
|
+
"SKIP"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "duplicate",
|
|
70
|
+
"action": "skip",
|
|
71
|
+
"reason": "duplicate candidate",
|
|
72
|
+
"when": {
|
|
73
|
+
"where": {
|
|
74
|
+
"duplicate": true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"adjustments": [
|
|
80
|
+
{
|
|
81
|
+
"id": "due-follow-up",
|
|
82
|
+
"value": 6,
|
|
83
|
+
"reason": "follow-up is due now",
|
|
84
|
+
"when": {
|
|
85
|
+
"type": "followup",
|
|
86
|
+
"where": {
|
|
87
|
+
"timelineState": [
|
|
88
|
+
"due",
|
|
89
|
+
"overdue"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "dream-company",
|
|
96
|
+
"value": 8,
|
|
97
|
+
"reason": "company is on the priority list",
|
|
98
|
+
"when": {
|
|
99
|
+
"where": {
|
|
100
|
+
"priorityCompany": true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "weak-source",
|
|
106
|
+
"value": -12,
|
|
107
|
+
"reason": "source lacks a report or score provenance",
|
|
108
|
+
"when": {
|
|
109
|
+
"where": {
|
|
110
|
+
"sourceQuality": 0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"quotas": [
|
|
116
|
+
{
|
|
117
|
+
"id": "one-per-company",
|
|
118
|
+
"field": "company",
|
|
119
|
+
"max": 1,
|
|
120
|
+
"reason": "keep the selected queue diverse"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
@@ -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,9 @@
|
|
|
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
|
|
23
|
+
* 14. Priority queue verifies if .jobforge-prioritize.json exists
|
|
24
|
+
* 15. Artifact lineage verifies/checks if .jobforge-lineage.json exists
|
|
22
25
|
*
|
|
23
26
|
* Run: node verify-pipeline.mjs (from repo root; same as npm run verify)
|
|
24
27
|
*/
|
|
@@ -33,6 +36,9 @@ import {
|
|
|
33
36
|
import { jobForgeLedgerPath, ledgerExists, verifyJobForgeLedger } from './lib/jobforge-ledger.mjs';
|
|
34
37
|
import { indexExists, jobForgeIndexPath, verifyJobForgeIndex } from './lib/jobforge-index.mjs';
|
|
35
38
|
import { factsExist, jobForgeFactsPath, verifyJobForgeFacts } from './lib/jobforge-facts.mjs';
|
|
39
|
+
import { jobForgeTimelinePath, timelineExists, verifyJobForgeTimeline } from './lib/jobforge-timeline.mjs';
|
|
40
|
+
import { jobForgePrioritizePath, prioritizeExists, verifyJobForgePrioritize } from './lib/jobforge-prioritize.mjs';
|
|
41
|
+
import { checkJobForgeLineage, jobForgeLineagePath, lineageExists, verifyJobForgeLineage } from './lib/jobforge-lineage.mjs';
|
|
36
42
|
import {
|
|
37
43
|
canonicalStatusValues,
|
|
38
44
|
formatContractIssues,
|
|
@@ -189,6 +195,62 @@ function verifyFactsIfPresent() {
|
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
function verifyTimelineIfPresent() {
|
|
199
|
+
if (!timelineExists(PROJECT_DIR)) {
|
|
200
|
+
ok('Timeline not initialized');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const result = verifyJobForgeTimeline({}, PROJECT_DIR);
|
|
204
|
+
for (const issue of result.issues) {
|
|
205
|
+
const msg = `timeline: ${issue.code}: ${issue.message}`;
|
|
206
|
+
if (issue.severity === 'error') error(msg);
|
|
207
|
+
else warn(msg);
|
|
208
|
+
}
|
|
209
|
+
if (result.ok) {
|
|
210
|
+
ok(`Timeline valid (${relative(PROJECT_DIR, jobForgeTimelinePath(PROJECT_DIR))})`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function verifyPrioritizeIfPresent() {
|
|
215
|
+
if (!prioritizeExists(PROJECT_DIR)) {
|
|
216
|
+
ok('Priority queue not initialized');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const result = verifyJobForgePrioritize({}, PROJECT_DIR);
|
|
220
|
+
for (const issue of result.issues) {
|
|
221
|
+
const msg = `prioritize: ${issue.code}: ${issue.message}`;
|
|
222
|
+
if (issue.severity === 'error') error(msg);
|
|
223
|
+
else warn(msg);
|
|
224
|
+
}
|
|
225
|
+
if (result.ok) {
|
|
226
|
+
ok(`Priority queue valid (${relative(PROJECT_DIR, jobForgePrioritizePath(PROJECT_DIR))})`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function verifyLineageIfPresent() {
|
|
231
|
+
if (!lineageExists(PROJECT_DIR)) {
|
|
232
|
+
ok('Artifact lineage not initialized');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const verifyResult = verifyJobForgeLineage({}, PROJECT_DIR);
|
|
236
|
+
for (const issue of verifyResult.issues) {
|
|
237
|
+
const msg = `lineage: ${issue.code}: ${issue.message}`;
|
|
238
|
+
if (issue.severity === 'error') error(msg);
|
|
239
|
+
else warn(msg);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const checkResult = checkJobForgeLineage({}, PROJECT_DIR);
|
|
243
|
+
for (const issue of checkResult.issues) {
|
|
244
|
+
const msg = `lineage: ${issue.code}: ${issue.message}`;
|
|
245
|
+
if (issue.severity === 'error') error(msg);
|
|
246
|
+
else warn(msg);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (verifyResult.ok && checkResult.ok) {
|
|
250
|
+
ok(`Artifact lineage current (${checkResult.current}/${checkResult.total} records at ${relative(PROJECT_DIR, jobForgeLineagePath(PROJECT_DIR))})`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
192
254
|
// --- Read entries ---
|
|
193
255
|
const { entries, source } = readAllEntries();
|
|
194
256
|
|
|
@@ -200,6 +262,9 @@ if (entries.length === 0) {
|
|
|
200
262
|
verifyLedgerIfPresent();
|
|
201
263
|
verifyIndexIfPresent();
|
|
202
264
|
verifyFactsIfPresent();
|
|
265
|
+
verifyTimelineIfPresent();
|
|
266
|
+
verifyPrioritizeIfPresent();
|
|
267
|
+
verifyLineageIfPresent();
|
|
203
268
|
console.log('\n' + '='.repeat(50));
|
|
204
269
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|
|
205
270
|
if (errors === 0 && warnings === 0) console.log('🟢 Pipeline is clean!');
|
|
@@ -337,6 +402,9 @@ verifyStatesYamlDrift();
|
|
|
337
402
|
verifyLedgerIfPresent();
|
|
338
403
|
verifyIndexIfPresent();
|
|
339
404
|
verifyFactsIfPresent();
|
|
405
|
+
verifyTimelineIfPresent();
|
|
406
|
+
verifyPrioritizeIfPresent();
|
|
407
|
+
verifyLineageIfPresent();
|
|
340
408
|
|
|
341
409
|
console.log('\n' + '='.repeat(50));
|
|
342
410
|
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
|