job-forge 2.14.36 → 2.14.38
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/.claude/agents/general-free.md +3 -2
- package/.codex/config.toml +1 -1
- package/.cursor/mcp.json +1 -1
- package/.cursor/rules/agent-general-free.mdc +3 -2
- package/.cursor/rules/main.mdc +3 -3
- package/.mcp.json +1 -1
- package/.opencode/agents/general-free.md +3 -2
- package/.opencode/instructions.md +8 -0
- package/.opencode/skills/job-forge.md +1 -1
- package/AGENTS.md +3 -3
- package/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/batch/README.md +7 -6
- package/batch/batch-runner.sh +2 -2
- package/bin/create-job-forge.mjs +6 -3
- package/bin/sync.mjs +35 -1
- package/config/profile.example.yml +8 -5
- package/docs/ARCHITECTURE.md +9 -8
- package/docs/CUSTOMIZATION.md +6 -6
- package/docs/SETUP.md +1 -1
- package/iso/agents/general-free.md +3 -2
- package/iso/commands/job-forge.md +1 -1
- package/iso/instructions.md +3 -3
- package/iso/instructions.opencode.md +8 -0
- package/iso/mcp.json +1 -1
- package/lib/jobforge-observability.mjs +847 -0
- package/modes/apply.md +4 -3
- package/modes/auto-pipeline.md +2 -2
- package/modes/pipeline.md +2 -2
- package/modes/reference-geometra.md +5 -5
- package/modes/reference-portals.md +10 -10
- package/modes/scan.md +3 -3
- package/opencode.json +3 -2
- package/package.json +8 -5
- package/scripts/batch-orchestrator.mjs +158 -9
- package/scripts/check-iso-smoke.mjs +3 -0
- package/scripts/guard.mjs +114 -190
- package/scripts/telemetry.mjs +214 -450
- package/scripts/trace.mjs +103 -232
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
5
|
import { audit, formatAuditResult, formatPolicyExplanation, loadPolicy, resultFails } from '@razroo/iso-guard';
|
|
8
|
-
import {
|
|
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
|
|
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
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
111
|
+
function buildGuardEvents(sessionEntries, childIds) {
|
|
187
112
|
const events = [];
|
|
188
|
-
const rootId = sessions[0]?.id;
|
|
189
113
|
|
|
190
|
-
for (const
|
|
191
|
-
const
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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(
|
|
290
|
-
const fragments = [
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return
|
|
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,
|
|
317
|
-
const children =
|
|
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
|
-
|
|
255
|
+
|
|
256
|
+
const refs = await discoverProjectSessions(opts);
|
|
360
257
|
if (refs.length === 0) {
|
|
361
|
-
console.error('job-forge guard:audit: no
|
|
258
|
+
console.error('job-forge guard:audit: no sessions found for this project');
|
|
362
259
|
return 2;
|
|
363
260
|
}
|
|
364
|
-
|
|
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
|
|
275
|
+
console.error(`job-forge guard:audit: no session matches "${positional[0]}"`);
|
|
367
276
|
return 2;
|
|
368
277
|
}
|
|
369
|
-
|
|
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(
|
|
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:
|
|
378
|
-
id:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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,
|
|
312
|
+
printAudit({ selected, includedRefs, policy, result });
|
|
389
313
|
}
|
|
390
314
|
return resultFails(result, opts.failOn) ? 1 : 0;
|
|
391
315
|
}
|