job-forge 2.14.37 → 2.14.39

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.
Files changed (58) hide show
  1. package/.codex/config.toml +1 -1
  2. package/.cursor/rules/main.mdc +1 -1
  3. package/.opencode/instructions.md +8 -0
  4. package/AGENTS.md +1 -1
  5. package/CLAUDE.md +1 -1
  6. package/README.md +5 -5
  7. package/batch/README.md +8 -7
  8. package/batch/batch-runner.sh +2 -2
  9. package/bin/create-job-forge.mjs +7 -4
  10. package/bin/sync.mjs +36 -2
  11. package/docs/ARCHITECTURE.md +27 -26
  12. package/docs/CUSTOMIZATION.md +25 -25
  13. package/docs/README.md +1 -1
  14. package/docs/SETUP.md +2 -2
  15. package/iso/instructions.md +1 -1
  16. package/iso/instructions.opencode.md +8 -0
  17. package/lib/jobforge-cache.mjs +1 -1
  18. package/lib/jobforge-canon.mjs +1 -1
  19. package/lib/jobforge-capabilities.mjs +1 -1
  20. package/lib/jobforge-context.mjs +1 -1
  21. package/lib/jobforge-contracts.mjs +1 -1
  22. package/lib/jobforge-facts.mjs +1 -1
  23. package/lib/jobforge-index.mjs +1 -1
  24. package/lib/jobforge-ledger.mjs +1 -1
  25. package/lib/jobforge-lineage.mjs +1 -1
  26. package/lib/jobforge-migrate.mjs +1 -1
  27. package/lib/jobforge-observability.mjs +847 -0
  28. package/lib/jobforge-postflight.mjs +1 -1
  29. package/lib/jobforge-preflight.mjs +1 -1
  30. package/lib/jobforge-prioritize.mjs +1 -1
  31. package/lib/jobforge-redact.mjs +1 -1
  32. package/lib/jobforge-score.mjs +1 -1
  33. package/lib/jobforge-timeline.mjs +1 -1
  34. package/models.yaml +1 -1
  35. package/modes/batch.md +1 -1
  36. package/modes/reference-setup.md +1 -1
  37. package/opencode.json +2 -1
  38. package/package.json +29 -26
  39. package/scripts/batch-orchestrator.mjs +160 -11
  40. package/scripts/cache.mjs +1 -1
  41. package/scripts/canon.mjs +1 -1
  42. package/scripts/check-helper-integration.mjs +19 -19
  43. package/scripts/check-iso-smoke.mjs +2 -0
  44. package/scripts/facts.mjs +1 -1
  45. package/scripts/guard.mjs +115 -191
  46. package/scripts/index.mjs +1 -1
  47. package/scripts/ledger.mjs +1 -1
  48. package/scripts/lineage.mjs +1 -1
  49. package/scripts/migrate.mjs +1 -1
  50. package/scripts/postflight.mjs +1 -1
  51. package/scripts/preflight.mjs +1 -1
  52. package/scripts/prioritize.mjs +1 -1
  53. package/scripts/redact.mjs +1 -1
  54. package/scripts/score.mjs +1 -1
  55. package/scripts/telemetry.mjs +214 -450
  56. package/scripts/timeline.mjs +1 -1
  57. package/scripts/trace.mjs +104 -233
  58. package/templates/portals.example.yml +1 -1
package/scripts/guard.mjs CHANGED
@@ -1,26 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawnSync } from 'child_process';
4
- import { existsSync } from 'fs';
5
3
  import { dirname, join, relative, resolve } from 'path';
6
4
  import { fileURLToPath } from 'url';
7
- import { audit, formatAuditResult, formatPolicyExplanation, loadPolicy, resultFails } from '@razroo/iso-guard';
8
- import { defaultOpenCodeDbPath, findSessionById, parseSinceCutoff } from '@razroo/iso-trace';
5
+ import { audit, formatAuditResult, formatPolicyExplanation, loadPolicy, resultFails } from '@agent-pattern-labs/iso-guard';
6
+ import {
7
+ buildSessionGraph,
8
+ collectDispatchCalls,
9
+ descendantIds,
10
+ discoverProjectSessions,
11
+ findObservedSession,
12
+ loadObservedSession,
13
+ messageEvents,
14
+ objectOrEmpty,
15
+ redactSecrets,
16
+ safeJson,
17
+ stringValue,
18
+ toolRecords,
19
+ } from '../lib/jobforge-observability.mjs';
9
20
 
10
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
22
  const PKG_ROOT = resolve(__dirname, '..');
12
23
  const PROJECT_DIR = process.env.JOB_FORGE_PROJECT || process.cwd();
13
24
  const DEFAULT_SINCE = '24h';
14
25
 
15
- const USAGE = `job-forge guard - deterministic JobForge policy audits over local OpenCode traces
26
+ const USAGE = `job-forge guard - deterministic JobForge policy audits over local traces
16
27
 
17
28
  Usage:
18
- job-forge guard:audit [latest|<id-or-prefix>] [--since 24h] [--cwd <dir>] [--policy <path>] [--json] [--fail-on error|warn|off] [--root-only]
29
+ job-forge guard:audit [latest|<id-or-prefix>] [--since 24h] [--cwd <dir>] [--harness <name>] [--policy <path>] [--json] [--fail-on error|warn|off] [--root-only]
19
30
  job-forge guard:explain [--policy <path>] [--json]
20
31
 
21
32
  The default policy is templates/guards/jobforge-baseline.yaml. Guard audits are
22
- local-only and passive: JobForge converts OpenCode SQLite rows into iso-guard
23
- events and never asks agents or MCPs to emit extra telemetry.`;
33
+ local-only and passive: JobForge converts normalized session events into
34
+ iso-guard inputs and never asks agents or MCPs to emit extra telemetry.`;
24
35
 
25
36
  const [cmd = 'help', ...args] = process.argv.slice(2);
26
37
 
@@ -32,6 +43,7 @@ function parseArgs(rawArgs, { allowSession = false } = {}) {
32
43
  const opts = {
33
44
  since: DEFAULT_SINCE,
34
45
  cwd: PROJECT_DIR,
46
+ harness: '',
35
47
  policy: defaultPolicyPath(),
36
48
  json: false,
37
49
  failOn: 'error',
@@ -49,6 +61,10 @@ function parseArgs(rawArgs, { allowSession = false } = {}) {
49
61
  opts.cwd = valueAfter(rawArgs, ++i, '--cwd');
50
62
  } else if (arg.startsWith('--cwd=')) {
51
63
  opts.cwd = arg.slice('--cwd='.length);
64
+ } else if (arg === '--harness') {
65
+ opts.harness = valueAfter(rawArgs, ++i, '--harness');
66
+ } else if (arg.startsWith('--harness=')) {
67
+ opts.harness = arg.slice('--harness='.length);
52
68
  } else if (arg === '--policy') {
53
69
  opts.policy = valueAfter(rawArgs, ++i, '--policy');
54
70
  } else if (arg.startsWith('--policy=')) {
@@ -86,165 +102,57 @@ function valueAfter(values, index, flag) {
86
102
  return value;
87
103
  }
88
104
 
89
- function queryOpenCodeDb(dbPath, sql) {
90
- const result = spawnSync('sqlite3', ['-json', dbPath, sql], {
91
- encoding: 'utf8',
92
- maxBuffer: 32 * 1024 * 1024,
93
- });
94
- if ((result.status ?? 0) !== 0) {
95
- const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status ?? 1}`;
96
- throw new Error(`job-forge guard: sqlite3 query failed: ${detail}`);
97
- }
98
- return JSON.parse(result.stdout || '[]');
99
- }
100
-
101
- function sqlString(value) {
102
- return `'${String(value).replaceAll("'", "''")}'`;
103
- }
104
-
105
- function msToIso(ms) {
106
- return new Date(Number(ms)).toISOString();
107
- }
108
-
109
- function discoverSessions(opts, { includeAllForShow = false } = {}) {
110
- const dbPath = defaultOpenCodeDbPath();
111
- if (!existsSync(dbPath)) return [];
112
-
113
- const where = [
114
- 's.time_archived is null',
115
- `s.directory = ${sqlString(opts.cwd)}`,
116
- ];
117
- const sinceMs = includeAllForShow ? undefined : parseSinceCutoff(opts.since);
118
- if (sinceMs !== undefined) where.push(`s.time_created >= ${Number(sinceMs)}`);
119
-
120
- const rows = queryOpenCodeDb(dbPath, [
121
- 'select',
122
- ' s.id,',
123
- ' s.parent_id,',
124
- ' s.title,',
125
- ' s.directory,',
126
- ' s.time_created,',
127
- ' s.time_updated,',
128
- ' (select count(*) from message m where m.session_id = s.id) as turn_count,',
129
- ' (',
130
- ' (select coalesce(sum(length(data)), 0) from message m where m.session_id = s.id) +',
131
- ' (select coalesce(sum(length(data)), 0) from part p where p.session_id = s.id)',
132
- ' ) as size_bytes',
133
- 'from session s',
134
- `where ${where.join(' and ')}`,
135
- 'order by s.time_updated desc',
136
- ].join(' '));
137
-
138
- return rows.map((row) => ({
139
- id: row.id,
140
- parentId: row.parent_id || null,
141
- title: row.title || '',
142
- cwd: row.directory,
143
- startedAt: msToIso(row.time_created),
144
- startedAtMs: Number(row.time_created),
145
- endedAt: msToIso(row.time_updated),
146
- endedAtMs: Number(row.time_updated),
147
- turnCount: row.turn_count ?? 0,
148
- sizeBytes: row.size_bytes ?? 0,
149
- }));
150
- }
151
-
152
- function loadRows(sessionId) {
153
- const dbPath = defaultOpenCodeDbPath();
154
- const id = sqlString(sessionId);
155
- return {
156
- messages: queryOpenCodeDb(dbPath, `select id, time_created, data from message where session_id = ${id} order by time_created, id`),
157
- parts: queryOpenCodeDb(dbPath, `select id, message_id, time_created, data from part where session_id = ${id} order by time_created, id`),
158
- };
159
- }
160
-
161
- function parseJson(raw) {
162
- try {
163
- return JSON.parse(raw || '{}');
164
- } catch (error) {
165
- const message = error instanceof Error ? error.message : String(error);
166
- return { __parseError: message, __raw: raw || '' };
167
- }
168
- }
169
-
170
- function selectSession(refs, positional) {
105
+ function selectSession(refs, roots, positional) {
171
106
  const requested = positional[0];
172
- if (!requested || requested === 'latest') {
173
- return refs.find((ref) => !ref.parentId) || refs[0] || null;
174
- }
175
- return findSessionById(refs, requested);
176
- }
177
-
178
- function sessionsForAudit(selected, allSessions, includeChildren) {
179
- if (!includeChildren) return [selected];
180
- const children = allSessions
181
- .filter((candidate) => candidate.parentId === selected.id)
182
- .sort((a, b) => a.startedAtMs - b.startedAtMs);
183
- return [selected, ...children];
107
+ if (!requested || requested === 'latest') return roots[0] || refs[0] || null;
108
+ return findObservedSession(refs, requested);
184
109
  }
185
110
 
186
- function buildGuardEvents(sessions) {
111
+ function buildGuardEvents(sessionEntries, childIds) {
187
112
  const events = [];
188
- const rootId = sessions[0]?.id;
189
113
 
190
- for (const session of sessions) {
191
- const rows = loadRows(session.id);
192
- const messages = rows.messages.map((row) => ({ row, data: parseJson(row.data) }));
193
- const messageById = new Map(messages.map((message) => [message.row.id, message.data]));
114
+ for (const entry of sessionEntries) {
115
+ const { ref, session } = entry;
194
116
  let requestIndex = 0;
195
117
 
196
- for (const row of rows.parts) {
197
- const data = parseJson(row.data);
198
- const message = messageById.get(row.message_id) || {};
199
- const role = message.role || 'unknown';
200
- const at = msToIso(row.time_created);
118
+ for (const message of messageEvents(session)) {
119
+ if (message.role === 'user') requestIndex += 1;
120
+ events.push({
121
+ type: 'message',
122
+ name: message.role,
123
+ at: message.at,
124
+ source: `${ref.source.harness}:${session.id}`,
125
+ text: message.text || '',
126
+ data: {
127
+ sessionId: session.id,
128
+ sessionTitle: ref.title || session.title || '',
129
+ isChildSession: childIds.has(session.id),
130
+ requestIndex,
131
+ },
132
+ });
133
+ }
134
+
135
+ for (const record of toolRecords(session)) {
136
+ if (!record.name) continue;
137
+ const text = toolText(record);
201
138
  const base = {
202
139
  sessionId: session.id,
203
- sessionTitle: session.title,
204
- parentId: session.parentId,
205
- isChildSession: session.id !== rootId,
206
- role,
207
- messageId: row.message_id,
208
- sessionMessageId: `${session.id}:${row.message_id}`,
209
- partId: row.id,
140
+ sessionTitle: ref.title || session.title || '',
141
+ isChildSession: childIds.has(session.id),
210
142
  requestIndex,
143
+ status: record.result ? (record.result.error ? 'failed' : 'completed') : 'unknown',
144
+ input: objectOrEmpty(record.input),
211
145
  };
212
-
213
- if (data.type === 'text') {
214
- if (role === 'user') requestIndex += 1;
215
- events.push({
216
- type: 'message',
217
- name: role,
218
- at,
219
- source: `opencode:${session.id}`,
220
- text: data.text || '',
221
- data: { ...base, requestIndex },
222
- });
223
- continue;
224
- }
225
-
226
- if (data.type === 'tool') {
227
- const input = objectOrEmpty(data.state?.input);
228
- const metadata = objectOrEmpty(data.state?.metadata);
229
- const toolName = data.tool || 'unknown';
230
- const text = toolText(toolName, input, metadata, data.state);
231
- const toolEvent = {
232
- type: 'tool_call',
233
- name: toolName,
234
- at,
235
- source: `opencode:${session.id}`,
236
- text,
237
- data: {
238
- ...base,
239
- tool: toolName,
240
- status: stringValue(data.state?.status),
241
- input,
242
- metadata,
243
- },
244
- };
245
- events.push(toolEvent);
246
- events.push(...derivedToolEvents(toolEvent));
247
- }
146
+ const event = {
147
+ type: 'tool_call',
148
+ name: record.name,
149
+ at: record.at,
150
+ source: `${ref.source.harness}:${session.id}`,
151
+ text,
152
+ data: base,
153
+ };
154
+ events.push(event);
155
+ events.push(...derivedToolEvents(event));
248
156
  }
249
157
  }
250
158
 
@@ -286,26 +194,14 @@ function runsCommand(text, pattern) {
286
194
  return /(^|[\s"])(bash|shell|exec|command|terminal|run_command)\b/i.test(text) && pattern.test(text);
287
195
  }
288
196
 
289
- function toolText(toolName, input, metadata, state) {
290
- const fragments = [toolName, safeJson(input), safeJson(metadata)];
291
- if (state?.output && typeof state.output === 'string') fragments.push(state.output);
292
- return fragments.filter(Boolean).join('\n');
293
- }
294
-
295
- function objectOrEmpty(value) {
296
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
297
- }
298
-
299
- function stringValue(value) {
300
- return typeof value === 'string' ? value : value == null ? '' : String(value);
301
- }
302
-
303
- function safeJson(value) {
304
- try {
305
- return JSON.stringify(value);
306
- } catch {
307
- return '';
308
- }
197
+ function toolText(record) {
198
+ const fragments = [
199
+ record.name,
200
+ safeJson(record.input),
201
+ record.result?.output || '',
202
+ record.result?.error || '',
203
+ ];
204
+ return redactSecrets(fragments.filter(Boolean).join('\n'));
309
205
  }
310
206
 
311
207
  function printablePath(path) {
@@ -313,8 +209,8 @@ function printablePath(path) {
313
209
  return rel && !rel.startsWith('..') ? rel : path;
314
210
  }
315
211
 
316
- function printAudit({ selected, includedSessions, policy, result }) {
317
- const children = includedSessions.length - 1;
212
+ function printAudit({ selected, includedRefs, policy, result }) {
213
+ const children = includedRefs.length - 1;
318
214
  console.log(`session: ${selected.id}${selected.title ? ` (${selected.title})` : ''}`);
319
215
  if (children > 0) console.log(`children: ${children}`);
320
216
  console.log(`policy: ${printablePath(policy.sourcePath || defaultPolicyPath())}`);
@@ -356,36 +252,64 @@ async function main() {
356
252
  console.error(`job-forge guard:audit: ${opts.error}`);
357
253
  return 2;
358
254
  }
359
- const refs = discoverSessions(opts, { includeAllForShow: Boolean(positional[0] && positional[0] !== 'latest') });
255
+
256
+ const refs = await discoverProjectSessions(opts);
360
257
  if (refs.length === 0) {
361
- console.error('job-forge guard:audit: no OpenCode sessions found for this project');
258
+ console.error('job-forge guard:audit: no sessions found for this project');
362
259
  return 2;
363
260
  }
364
- const selected = selectSession(refs, positional);
261
+
262
+ const sessionsById = new Map();
263
+ const loadErrors = new Map();
264
+ for (const ref of refs) {
265
+ try {
266
+ sessionsById.set(ref.id, loadObservedSession(ref));
267
+ } catch (error) {
268
+ loadErrors.set(ref.id, error instanceof Error ? error.message : String(error));
269
+ }
270
+ }
271
+ const loadedRefs = refs.filter((ref) => sessionsById.has(ref.id));
272
+ const graph = buildSessionGraph(loadedRefs, sessionsById);
273
+ const selected = selectSession(loadedRefs, graph.roots, positional);
365
274
  if (!selected) {
366
- console.error(`job-forge guard:audit: no OpenCode session matches "${positional[0]}"`);
275
+ console.error(`job-forge guard:audit: no session matches "${positional[0]}"`);
367
276
  return 2;
368
277
  }
369
- const includedSessions = sessionsForAudit(selected, refs, opts.includeChildren);
278
+ if (!sessionsById.has(selected.id)) {
279
+ console.error(`job-forge guard:audit: could not load session "${selected.id}": ${loadErrors.get(selected.id) || 'unknown parse error'}`);
280
+ return 2;
281
+ }
282
+
283
+ const includedIds = opts.includeChildren
284
+ ? [selected.id, ...descendantIds(selected.id, graph.childrenBySession)]
285
+ : [selected.id];
286
+ const childIds = new Set(includedIds.slice(1));
287
+ const includedRefs = includedIds
288
+ .map((id) => loadedRefs.find((ref) => ref.id === id))
289
+ .filter(Boolean);
290
+ const sessionEntries = includedRefs.map((ref) => ({
291
+ ref,
292
+ session: sessionsById.get(ref.id),
293
+ })).filter((entry) => entry.session);
370
294
  const policy = loadPolicy(opts.policy);
371
- const events = buildGuardEvents(includedSessions);
295
+ const events = buildGuardEvents(sessionEntries, childIds);
372
296
  const result = audit(policy, events);
373
297
 
374
298
  if (opts.json) {
375
299
  console.log(JSON.stringify({
376
300
  session: selected,
377
- includedSessions: includedSessions.map((session) => ({
378
- id: session.id,
379
- parentId: session.parentId,
380
- title: session.title,
381
- startedAt: session.startedAt,
382
- endedAt: session.endedAt,
301
+ includedSessions: includedRefs.map((ref) => ({
302
+ id: ref.id,
303
+ title: ref.title,
304
+ startedAt: ref.startedAt,
305
+ endedAt: ref.endedAt,
306
+ harness: ref.source.harness,
383
307
  })),
384
308
  policy: policy.sourcePath,
385
309
  result,
386
310
  }, null, 2));
387
311
  } else {
388
- printAudit({ selected, includedSessions, policy, result });
312
+ printAudit({ selected, includedRefs, policy, result });
389
313
  }
390
314
  return resultFails(result, opts.failOn) ? 1 : 0;
391
315
  }
package/scripts/index.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  formatConfigSummary,
7
7
  formatIndexRecords,
8
8
  formatVerifyResult,
9
- } from '@razroo/iso-index';
9
+ } from '@agent-pattern-labs/iso-index';
10
10
  import { PROJECT_DIR } from '../tracker-lib.mjs';
11
11
  import {
12
12
  buildJobForgeIndex,
@@ -6,7 +6,7 @@ import {
6
6
  formatEvents,
7
7
  formatVerifyResult,
8
8
  queryEvents,
9
- } from '@razroo/iso-ledger';
9
+ } from '@agent-pattern-labs/iso-ledger';
10
10
  import { PROJECT_DIR, readAllEntries } from '../tracker-lib.mjs';
11
11
  import {
12
12
  appendJobForgeEvent,
@@ -8,7 +8,7 @@ import {
8
8
  formatStaleResult,
9
9
  formatVerifyResult,
10
10
  parseJson,
11
- } from '@razroo/iso-lineage';
11
+ } from '@agent-pattern-labs/iso-lineage';
12
12
  import { PROJECT_DIR } from '../tracker-lib.mjs';
13
13
  import {
14
14
  checkJobForgeLineage,
@@ -4,7 +4,7 @@ import { relative } from 'path';
4
4
  import {
5
5
  formatConfigSummary,
6
6
  formatMigrationResult,
7
- } from '@razroo/iso-migrate';
7
+ } from '@agent-pattern-labs/iso-migrate';
8
8
  import { PROJECT_DIR } from '../tracker-lib.mjs';
9
9
  import {
10
10
  jobForgeMigrationConfigPath,
@@ -8,7 +8,7 @@ import {
8
8
  loadPostflightConfig,
9
9
  parseJson,
10
10
  settlePostflight,
11
- } from '@razroo/iso-postflight';
11
+ } from '@agent-pattern-labs/iso-postflight';
12
12
  import { PROJECT_DIR } from '../tracker-lib.mjs';
13
13
  import {
14
14
  jobForgePostflightConfigPath,
@@ -8,7 +8,7 @@ import {
8
8
  loadPreflightConfig,
9
9
  parseJson,
10
10
  planPreflight,
11
- } from '@razroo/iso-preflight';
11
+ } from '@agent-pattern-labs/iso-preflight';
12
12
  import { PROJECT_DIR } from '../tracker-lib.mjs';
13
13
  import {
14
14
  jobForgePreflightConfigPath,
@@ -9,7 +9,7 @@ import {
9
9
  formatVerifyResult,
10
10
  loadPrioritizeItems,
11
11
  parseJson,
12
- } from '@razroo/iso-prioritize';
12
+ } from '@agent-pattern-labs/iso-prioritize';
13
13
  import { PROJECT_DIR } from '../tracker-lib.mjs';
14
14
  import {
15
15
  buildJobForgePrioritizeItems,
@@ -9,7 +9,7 @@ import {
9
9
  parseJson,
10
10
  redactText,
11
11
  scanSources,
12
- } from '@razroo/iso-redact';
12
+ } from '@agent-pattern-labs/iso-redact';
13
13
  import { PROJECT_DIR } from '../tracker-lib.mjs';
14
14
  import {
15
15
  jobForgeRedactConfigPath,
package/scripts/score.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  formatGateResult,
10
10
  formatScoreResult,
11
11
  formatVerifyResult,
12
- } from '@razroo/iso-score';
12
+ } from '@agent-pattern-labs/iso-score';
13
13
  import { PROJECT_DIR } from '../tracker-lib.mjs';
14
14
  import {
15
15
  checkJobForgeScore,