godpowers 3.0.2 → 3.11.0
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/CHANGELOG.md +229 -0
- package/README.md +16 -10
- package/RELEASE.md +21 -33
- package/bin/install.js +34 -0
- package/fixtures/gate/harden-pass/.godpowers/state.json +26 -0
- package/lib/artifact-map.js +2 -1
- package/lib/cli-dispatch.js +409 -2
- package/lib/evidence/.provenance.json +45 -0
- package/lib/evidence-import.js +147 -0
- package/lib/evidence.js +908 -0
- package/lib/gate.js +26 -15
- package/lib/installer-args.js +219 -1
- package/lib/quarterback.js +183 -0
- package/lib/work-report.js +137 -0
- package/package.json +1 -1
- package/references/orchestration/GOD-ORCHESTRATOR-RUNBOOK.md +9 -4
- package/skills/god-harden.md +5 -2
package/lib/evidence.js
ADDED
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence engine (enforced producer of verification records).
|
|
3
|
+
*
|
|
4
|
+
* Vendored from mythify-mcp@3.6.3 (mcp-server/src/index.js@7cbd601, the
|
|
5
|
+
* verify_run / verify_claim tools). Engine logic only; do not hand-edit the
|
|
6
|
+
* record shapes. Re-sync the upstream engine with scripts/sync-evidence-engine.js.
|
|
7
|
+
* Provenance of origin and the recorded adaptations live in
|
|
8
|
+
* lib/evidence/.provenance.json.
|
|
9
|
+
*
|
|
10
|
+
* Adaptations from the upstream Node engine (see .provenance.json):
|
|
11
|
+
* - Mythify's plan/step context becomes Godpowers' arc/substep context.
|
|
12
|
+
* - The .mythify/ state dir becomes .godpowers/ledger/.
|
|
13
|
+
* - The jsonl append goes through lib/atomic-write.js (temp + rename) so a
|
|
14
|
+
* torn record is never visible.
|
|
15
|
+
*
|
|
16
|
+
* What this adds on top of the upstream engine (the Godpowers integration):
|
|
17
|
+
* 1. .godpowers/ledger/verifications.jsonl: append-only, Mythify-shape record,
|
|
18
|
+
* the durable audit trail and source of truth.
|
|
19
|
+
* 2. state.json substep verification.commands[]: a rollup of the latest verdict
|
|
20
|
+
* per command for that substep, in the existing Godpowers gate shape, written
|
|
21
|
+
* through lib/state.js so PROGRESS.md regenerates. Additive: status is never
|
|
22
|
+
* changed here (closing on evidence is Phase 1, not Phase 0).
|
|
23
|
+
* 3. .godpowers/runs/<id>/events.jsonl: a gate.pass / gate.fail event on the
|
|
24
|
+
* existing hash-chained stream via lib/events.js.
|
|
25
|
+
*
|
|
26
|
+
* This module is a self-contained domain module, peer to lib/linkage.js.
|
|
27
|
+
*
|
|
28
|
+
* @typedef {Object} ExecutedRecord
|
|
29
|
+
* @property {"executed"} kind Record class.
|
|
30
|
+
* @property {string|null} claim The claim the command verifies.
|
|
31
|
+
* @property {string} command The exact command executed.
|
|
32
|
+
* @property {number} exit_code Process exit code (-1 for timeout or no exit).
|
|
33
|
+
* @property {number} duration_seconds Wall-clock duration, three decimals.
|
|
34
|
+
* @property {string} stdout_tail Last 4000 chars of stdout.
|
|
35
|
+
* @property {string} stderr_tail Last 4000 chars of stderr.
|
|
36
|
+
* @property {boolean} verified True only when not timed out and exit code 0.
|
|
37
|
+
* @property {string} timestamp ISO-8601 timestamp.
|
|
38
|
+
* @property {string|null} arc Active arc, when known.
|
|
39
|
+
* @property {string|null} substep Canonical substep id such as tier-2.build.
|
|
40
|
+
* @property {string|null} substep_status Substep status when the record was made.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {Object} VerifyResult
|
|
43
|
+
* @property {ExecutedRecord} record The ledger record that was appended.
|
|
44
|
+
* @property {Object} rollup The state.json rollup outcome.
|
|
45
|
+
* @property {Object} event The events.jsonl emission outcome.
|
|
46
|
+
* @property {boolean} verified Convenience copy of record.verified.
|
|
47
|
+
* @property {string} ledger Absolute path to verifications.jsonl.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
const fs = require('fs');
|
|
51
|
+
const os = require('os');
|
|
52
|
+
const path = require('path');
|
|
53
|
+
const { spawnSync } = require('child_process');
|
|
54
|
+
|
|
55
|
+
const atomic = require('./atomic-write');
|
|
56
|
+
const stateStore = require('./state');
|
|
57
|
+
const stateLock = require('./state-lock');
|
|
58
|
+
const stateAdvance = require('./state-advance');
|
|
59
|
+
const events = require('./events');
|
|
60
|
+
|
|
61
|
+
const TAIL_CHARS = 4000;
|
|
62
|
+
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
63
|
+
const DIAGNOSTICS_LIMIT = 1000;
|
|
64
|
+
|
|
65
|
+
// Substeps whose close gate requires an executed, verified:true record (the
|
|
66
|
+
// runtime/executable-gated tiers). Other substeps (planning, repo, observe,
|
|
67
|
+
// launch) may close on an attested record plus gate.js's artifact and have-nots
|
|
68
|
+
// checks. This mirrors the build-only evidence requirement gate.js enforces
|
|
69
|
+
// today and the tier-appropriate split in docs/FUSION-ARCHITECTURE.md section 4.2.
|
|
70
|
+
const EXECUTED_REQUIRED_SUBSTEPS = new Set(['build', 'deploy', 'harden']);
|
|
71
|
+
|
|
72
|
+
// Reflection outcomes, mirroring the upstream reflect tool.
|
|
73
|
+
const REFLECTION_OUTCOMES = new Set(['success', 'partial', 'failure']);
|
|
74
|
+
|
|
75
|
+
// Memory categories, mirroring the upstream memory store.
|
|
76
|
+
const MEMORY_CATEGORIES = new Set(['fact', 'decision', 'discovery', 'state']);
|
|
77
|
+
|
|
78
|
+
// Lesson scopes, mirroring the upstream lessons store (project or global).
|
|
79
|
+
const LESSON_SCOPES = new Set(['project', 'global']);
|
|
80
|
+
|
|
81
|
+
let PROVENANCE = null;
|
|
82
|
+
try {
|
|
83
|
+
// eslint-disable-next-line global-require
|
|
84
|
+
PROVENANCE = require('./evidence/.provenance.json');
|
|
85
|
+
} catch (_) {
|
|
86
|
+
PROVENANCE = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Paths (peer to lib/linkage.js path helpers)
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
function ledgerDir(projectRoot) {
|
|
94
|
+
return path.join(projectRoot, '.godpowers', 'ledger');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function verificationsPath(projectRoot) {
|
|
98
|
+
return path.join(ledgerDir(projectRoot), 'verifications.jsonl');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function reflectionsPath(projectRoot) {
|
|
102
|
+
return path.join(ledgerDir(projectRoot), 'reflections.jsonl');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function memoryPath(projectRoot) {
|
|
106
|
+
return path.join(ledgerDir(projectRoot), 'memory.json');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function lessonsPath(projectRoot, scope) {
|
|
110
|
+
if (scope === 'global') return path.join(os.homedir(), '.godpowers', 'lessons.jsonl');
|
|
111
|
+
return path.join(ledgerDir(projectRoot), 'lessons.jsonl');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function outcomeDir(projectRoot, slug) {
|
|
115
|
+
return path.join(ledgerDir(projectRoot), 'outcomes', slug);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function outcomeGoalPath(projectRoot, slug) {
|
|
119
|
+
return path.join(outcomeDir(projectRoot, slug), 'goal.json');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function outcomeIterationsPath(projectRoot, slug) {
|
|
123
|
+
return path.join(outcomeDir(projectRoot, slug), 'iterations.jsonl');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function logPath(projectRoot) {
|
|
127
|
+
return path.join(ledgerDir(projectRoot), 'LEDGER-LOG.md');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ensureLedgerDir(projectRoot) {
|
|
131
|
+
const dir = ledgerDir(projectRoot);
|
|
132
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Time and string helpers (lifted from the upstream engine)
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
function isoNow() {
|
|
140
|
+
return new Date().toISOString();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function tail(text) {
|
|
144
|
+
const s = String(text == null ? '' : text);
|
|
145
|
+
return s.length > TAIL_CHARS ? s.slice(-TAIL_CHARS) : s;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeTimeout(timeout) {
|
|
149
|
+
const n = Number(timeout);
|
|
150
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
151
|
+
return DEFAULT_TIMEOUT_SECONDS;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Shared slug contract, mirroring the upstream engine: lowercase, collapse runs
|
|
155
|
+
// of non-alphanumerics to "-", strip edge "-", truncate to 40 characters.
|
|
156
|
+
function slugify(text) {
|
|
157
|
+
return String(text)
|
|
158
|
+
.toLowerCase()
|
|
159
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
160
|
+
.replace(/^-+|-+$/g, '')
|
|
161
|
+
.slice(0, 40);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Ledger append (atomic, via lib/atomic-write.js) and tolerant read
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
function appendJsonlAtomic(file, record) {
|
|
169
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
170
|
+
let existing = '';
|
|
171
|
+
try {
|
|
172
|
+
existing = fs.readFileSync(file, 'utf8');
|
|
173
|
+
} catch (_) {
|
|
174
|
+
existing = '';
|
|
175
|
+
}
|
|
176
|
+
atomic.writeFileAtomic(file, existing + JSON.stringify(record) + '\n');
|
|
177
|
+
return file;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function readJsonl(file) {
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
184
|
+
} catch (_) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
189
|
+
const trimmed = line.trim();
|
|
190
|
+
if (trimmed === '') continue;
|
|
191
|
+
try {
|
|
192
|
+
out.push(JSON.parse(trimmed));
|
|
193
|
+
} catch (_) {
|
|
194
|
+
// Skip a torn or partial jsonl record, matching the tolerant readers in
|
|
195
|
+
// lib/events.js and the upstream engine.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function appendRecord(projectRoot, record) {
|
|
202
|
+
ensureLedgerDir(projectRoot);
|
|
203
|
+
return appendJsonlAtomic(verificationsPath(projectRoot), record);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function appendLog(projectRoot, message) {
|
|
207
|
+
ensureLedgerDir(projectRoot);
|
|
208
|
+
fs.appendFileSync(logPath(projectRoot), `${isoNow()} ${message}\n`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readLedger(projectRoot) {
|
|
212
|
+
return readJsonl(verificationsPath(projectRoot));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Substep context (rebinds the upstream verificationStepContext)
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
function resolveTarget(state, substepToken) {
|
|
220
|
+
if (!state || !substepToken) return null;
|
|
221
|
+
try {
|
|
222
|
+
return stateAdvance.resolveStep(state, substepToken);
|
|
223
|
+
} catch (_) {
|
|
224
|
+
// Ambiguous or malformed token: leave the substep unresolved.
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function substepContext(state, substepToken) {
|
|
230
|
+
const context = {
|
|
231
|
+
arc: state ? (state['active-arc'] || state.arc || null) : null,
|
|
232
|
+
substep: substepToken || null,
|
|
233
|
+
substep_status: null
|
|
234
|
+
};
|
|
235
|
+
const target = resolveTarget(state, substepToken);
|
|
236
|
+
if (target) {
|
|
237
|
+
context.substep = `${target.tierKey}.${target.subStepKey}`;
|
|
238
|
+
context.substep_status = target.status || null;
|
|
239
|
+
}
|
|
240
|
+
return context;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function canonicalSubstep(projectRoot, token) {
|
|
244
|
+
const target = resolveTarget(stateStore.read(projectRoot), token);
|
|
245
|
+
return target ? `${target.tierKey}.${target.subStepKey}` : token;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Command execution (field-for-field with the upstream verify_run)
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
function runCommand(command, timeoutSeconds) {
|
|
253
|
+
const startedAt = process.hrtime.bigint();
|
|
254
|
+
const run = spawnSync(command, {
|
|
255
|
+
shell: true,
|
|
256
|
+
encoding: 'utf8',
|
|
257
|
+
timeout: Math.round(timeoutSeconds * 1000),
|
|
258
|
+
maxBuffer: 16 * 1024 * 1024
|
|
259
|
+
});
|
|
260
|
+
const durationSeconds = Number(process.hrtime.bigint() - startedAt) / 1e9;
|
|
261
|
+
let stdoutTail = tail(run.stdout);
|
|
262
|
+
let stderrTail = tail(run.stderr);
|
|
263
|
+
const timedOut = Boolean(run.error && run.error.code === 'ETIMEDOUT');
|
|
264
|
+
let exitCode;
|
|
265
|
+
let verified;
|
|
266
|
+
if (timedOut) {
|
|
267
|
+
exitCode = -1;
|
|
268
|
+
verified = false;
|
|
269
|
+
stderrTail = stderrTail + (stderrTail ? '\n' : '') + `(timed out after ${timeoutSeconds} seconds)`;
|
|
270
|
+
} else if (typeof run.status === 'number') {
|
|
271
|
+
exitCode = run.status;
|
|
272
|
+
verified = exitCode === 0;
|
|
273
|
+
} else {
|
|
274
|
+
exitCode = -1;
|
|
275
|
+
verified = false;
|
|
276
|
+
const reason = run.error
|
|
277
|
+
? run.error.message
|
|
278
|
+
: run.signal
|
|
279
|
+
? `terminated by signal ${run.signal}`
|
|
280
|
+
: 'command did not produce an exit code';
|
|
281
|
+
stderrTail = stderrTail + (stderrTail ? '\n' : '') + `(${reason})`;
|
|
282
|
+
}
|
|
283
|
+
return { exitCode, verified, durationSeconds, stdoutTail, stderrTail, timedOut };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Rollup into state.json (Godpowers gate shape, through lib/state.js)
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
function commandName(entry) {
|
|
291
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
292
|
+
const value = entry.command || entry.cmd || entry.name;
|
|
293
|
+
return value ? String(value).trim() : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function diagnosticsFor(record) {
|
|
297
|
+
const detail = (record.stderr_tail && record.stderr_tail.trim()) ||
|
|
298
|
+
(record.stdout_tail && record.stdout_tail.trim()) || '';
|
|
299
|
+
let text = detail ? `exit ${record.exit_code}: ${detail}` : `exit ${record.exit_code}`;
|
|
300
|
+
if (text.length > DIAGNOSTICS_LIMIT) text = text.slice(-DIAGNOSTICS_LIMIT);
|
|
301
|
+
return text;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function toStateCommand(record) {
|
|
305
|
+
return {
|
|
306
|
+
command: record.command,
|
|
307
|
+
status: record.verified ? 'pass' : 'fail',
|
|
308
|
+
exitCode: record.exit_code,
|
|
309
|
+
ranAt: record.timestamp,
|
|
310
|
+
durationMs: Math.max(0, Math.round(record.duration_seconds * 1000)),
|
|
311
|
+
diagnostics: record.verified ? '' : diagnosticsFor(record)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function rollUp(projectRoot, substepToken, record) {
|
|
316
|
+
const current = stateStore.read(projectRoot);
|
|
317
|
+
if (!current) return { applied: false, reason: 'no-state' };
|
|
318
|
+
if (!substepToken) return { applied: false, reason: 'no-substep' };
|
|
319
|
+
|
|
320
|
+
let target;
|
|
321
|
+
try {
|
|
322
|
+
target = stateAdvance.resolveStep(current, substepToken);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
if (e.code === 'AMBIGUOUS_STEP') {
|
|
325
|
+
return { applied: false, reason: 'ambiguous-substep', matches: e.matches };
|
|
326
|
+
}
|
|
327
|
+
throw e;
|
|
328
|
+
}
|
|
329
|
+
if (!target) return { applied: false, reason: 'substep-not-found', substep: substepToken };
|
|
330
|
+
|
|
331
|
+
const holder = `godpowers-evidence:${process.pid}`;
|
|
332
|
+
const scope = `${target.tierKey}.${target.subStepKey}`;
|
|
333
|
+
const lock = stateLock.acquire(projectRoot, { holder, scope });
|
|
334
|
+
if (!lock.acquired) {
|
|
335
|
+
return { applied: false, reason: 'lock-unavailable', holder: lock.holder, scope: lock.scope };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const warnings = [];
|
|
339
|
+
try {
|
|
340
|
+
const fresh = stateStore.read(projectRoot);
|
|
341
|
+
if (!fresh.tiers[target.tierKey]) fresh.tiers[target.tierKey] = {};
|
|
342
|
+
const sub = fresh.tiers[target.tierKey][target.subStepKey] || { status: 'pending' };
|
|
343
|
+
const verification = (sub.verification && typeof sub.verification === 'object')
|
|
344
|
+
? { ...sub.verification }
|
|
345
|
+
: {};
|
|
346
|
+
const commands = Array.isArray(verification.commands) ? verification.commands.slice() : [];
|
|
347
|
+
const entry = toStateCommand(record);
|
|
348
|
+
const idx = commands.findIndex((existing) => commandName(existing) === record.command);
|
|
349
|
+
if (idx >= 0) commands[idx] = entry;
|
|
350
|
+
else commands.push(entry);
|
|
351
|
+
verification.commands = commands;
|
|
352
|
+
// Additive only: roll the verdict into the existing verification slot.
|
|
353
|
+
// Do not touch sub.status; closing on evidence is Phase 1.
|
|
354
|
+
fresh.tiers[target.tierKey][target.subStepKey] = { ...sub, verification };
|
|
355
|
+
stateStore.write(projectRoot, fresh, { onStateViewWarning: (w) => warnings.push(w) });
|
|
356
|
+
return {
|
|
357
|
+
applied: true,
|
|
358
|
+
tierKey: target.tierKey,
|
|
359
|
+
subStepKey: target.subStepKey,
|
|
360
|
+
substep: scope,
|
|
361
|
+
command: record.command,
|
|
362
|
+
status: entry.status,
|
|
363
|
+
commands: commands.length,
|
|
364
|
+
warnings
|
|
365
|
+
};
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return { applied: false, reason: 'write-error', error: e.message };
|
|
368
|
+
} finally {
|
|
369
|
+
stateLock.release(projectRoot, holder);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Event emission (gate.pass / gate.fail on the hash-chained stream)
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
function emitGateEvent(projectRoot, record, rollup, now) {
|
|
378
|
+
const name = record.verified ? 'gate.pass' : 'gate.fail';
|
|
379
|
+
const attrs = {
|
|
380
|
+
tier: rollup && rollup.tierKey ? rollup.tierKey : 'evidence',
|
|
381
|
+
substep: record.substep || (rollup && rollup.substep) || null,
|
|
382
|
+
command: record.command,
|
|
383
|
+
claim: record.claim,
|
|
384
|
+
exitCode: record.exit_code,
|
|
385
|
+
durationMs: Math.max(0, Math.round(record.duration_seconds * 1000)),
|
|
386
|
+
verified: record.verified,
|
|
387
|
+
kind: record.kind
|
|
388
|
+
};
|
|
389
|
+
try {
|
|
390
|
+
const runs = events.listRuns(projectRoot);
|
|
391
|
+
if (runs.length > 0) {
|
|
392
|
+
const latest = runs[runs.length - 1];
|
|
393
|
+
const file = events.eventsPath(projectRoot, latest);
|
|
394
|
+
const existing = events.readRun(projectRoot, latest);
|
|
395
|
+
const root = existing.find((entry) => entry && entry.trace_id);
|
|
396
|
+
const traceId = root && root.trace_id ? root.trace_id : events.generateTraceId();
|
|
397
|
+
const spanId = events.generateSpanId();
|
|
398
|
+
const event = { trace_id: traceId, span_id: spanId, name, attrs };
|
|
399
|
+
if (now) event.ts = now;
|
|
400
|
+
events.emit(file, event);
|
|
401
|
+
return { emitted: true, name, runId: latest, file, traceId, spanId };
|
|
402
|
+
}
|
|
403
|
+
const handle = events.startRun(projectRoot, { workflow: 'evidence' });
|
|
404
|
+
const spanId = events.generateSpanId();
|
|
405
|
+
const event = { span_id: spanId, name, attrs };
|
|
406
|
+
if (now) event.ts = now;
|
|
407
|
+
handle.emit(event);
|
|
408
|
+
return { emitted: true, name, runId: handle.runId, file: handle.file, traceId: handle.traceId, spanId };
|
|
409
|
+
} catch (e) {
|
|
410
|
+
return { emitted: false, name, error: e.message };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Public surface
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Execute a command as an executed verification: append the ledger record, roll
|
|
420
|
+
* the latest verdict into state.json, and emit gate.pass / gate.fail.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} command Shell command to execute.
|
|
423
|
+
* @param {{ substep?: string, claim?: string, timeout?: number, projectRoot?: string, now?: string }} [opts]
|
|
424
|
+
* @returns {VerifyResult}
|
|
425
|
+
*/
|
|
426
|
+
function verify(command, opts = {}) {
|
|
427
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
428
|
+
throw new Error('evidence.verify requires a non-empty command string');
|
|
429
|
+
}
|
|
430
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
431
|
+
const timeoutSeconds = normalizeTimeout(opts.timeout);
|
|
432
|
+
const state = stateStore.read(projectRoot);
|
|
433
|
+
const context = substepContext(state, opts.substep);
|
|
434
|
+
|
|
435
|
+
const exec = runCommand(command, timeoutSeconds);
|
|
436
|
+
const record = {
|
|
437
|
+
kind: 'executed',
|
|
438
|
+
claim: opts.claim !== undefined && opts.claim !== null ? opts.claim : null,
|
|
439
|
+
command,
|
|
440
|
+
exit_code: exec.exitCode,
|
|
441
|
+
duration_seconds: Number(exec.durationSeconds.toFixed(3)),
|
|
442
|
+
stdout_tail: exec.stdoutTail,
|
|
443
|
+
stderr_tail: exec.stderrTail,
|
|
444
|
+
verified: exec.verified,
|
|
445
|
+
timestamp: opts.now || isoNow(),
|
|
446
|
+
...context
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
appendRecord(projectRoot, record);
|
|
450
|
+
appendLog(
|
|
451
|
+
projectRoot,
|
|
452
|
+
`verify ${record.verified ? 'PASS' : 'FAIL'} substep=${context.substep || '-'} exit=${record.exit_code} cmd=\`${command}\``
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const rollup = rollUp(projectRoot, opts.substep, record);
|
|
456
|
+
const event = emitGateEvent(projectRoot, record, rollup, opts.now);
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
record,
|
|
460
|
+
rollup,
|
|
461
|
+
event,
|
|
462
|
+
verified: record.verified,
|
|
463
|
+
ledger: verificationsPath(projectRoot)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Record a second-class attested verification. Never marked verified, never
|
|
469
|
+
* rolled up into state.json, never emitted as a gate event.
|
|
470
|
+
*
|
|
471
|
+
* @param {string} claim The claim being attested.
|
|
472
|
+
* @param {string} evidenceText Self-reported supporting evidence.
|
|
473
|
+
* @param {{ substep?: string, projectRoot?: string, now?: string }} [opts]
|
|
474
|
+
* @returns {{ record: Object, verified: null, ledger: string }}
|
|
475
|
+
*/
|
|
476
|
+
function verifyClaim(claim, evidenceText, opts = {}) {
|
|
477
|
+
if (typeof claim !== 'string' || claim.trim() === '') {
|
|
478
|
+
throw new Error('evidence.verifyClaim requires a non-empty claim');
|
|
479
|
+
}
|
|
480
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
481
|
+
const state = stateStore.read(projectRoot);
|
|
482
|
+
const context = substepContext(state, opts.substep);
|
|
483
|
+
const record = {
|
|
484
|
+
kind: 'attested',
|
|
485
|
+
claim,
|
|
486
|
+
evidence: evidenceText !== undefined && evidenceText !== null ? evidenceText : null,
|
|
487
|
+
verified: null,
|
|
488
|
+
timestamp: opts.now || isoNow(),
|
|
489
|
+
...context
|
|
490
|
+
};
|
|
491
|
+
appendRecord(projectRoot, record);
|
|
492
|
+
appendLog(projectRoot, `attest substep=${context.substep || '-'} claim=\`${claim}\``);
|
|
493
|
+
return { record, verified: null, ledger: verificationsPath(projectRoot) };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Read ledger records, newest last. Optionally filtered to one substep and
|
|
498
|
+
* limited to the most recent N.
|
|
499
|
+
*
|
|
500
|
+
* @param {{ substep?: string, recent?: number, projectRoot?: string }} [opts]
|
|
501
|
+
* @returns {Object[]}
|
|
502
|
+
*/
|
|
503
|
+
function history(opts = {}) {
|
|
504
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
505
|
+
let records = readLedger(projectRoot);
|
|
506
|
+
if (opts.substep) {
|
|
507
|
+
const canonical = canonicalSubstep(projectRoot, opts.substep);
|
|
508
|
+
records = records.filter((r) => r && (r.substep === canonical || r.substep === opts.substep));
|
|
509
|
+
}
|
|
510
|
+
if (opts.recent !== undefined && Number.isFinite(Number(opts.recent))) {
|
|
511
|
+
const n = Math.max(0, Math.floor(Number(opts.recent)));
|
|
512
|
+
records = records.slice(-n);
|
|
513
|
+
}
|
|
514
|
+
return records;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* The strict close gate, rebound from Mythify's completion rule (cmd_step):
|
|
519
|
+
* a substep may close to done only when evidence bound to it since it went
|
|
520
|
+
* in-flight supports the close. This is a read-only predicate. It does NOT
|
|
521
|
+
* mutate state and is NOT yet wired into gate.js or the close path; that wiring
|
|
522
|
+
* is the deliberate behavior change tracked as the rest of Phase 1.
|
|
523
|
+
*
|
|
524
|
+
* Tier-appropriate (docs/FUSION-ARCHITECTURE.md section 4.2):
|
|
525
|
+
* - Executable-gated substeps (build, deploy, harden) require the latest
|
|
526
|
+
* executed record since in-flight to be verified:true.
|
|
527
|
+
* - Other substeps may close on an executed pass or an attested record since
|
|
528
|
+
* in-flight; a failed executed record still blocks (a red is a red).
|
|
529
|
+
*
|
|
530
|
+
* "Since in-flight" is the substep's last status-change timestamp
|
|
531
|
+
* (state.json `updated`), which is when it most recently went in-flight.
|
|
532
|
+
*
|
|
533
|
+
* @param {string} substep Substep token such as tier-2.build or build.
|
|
534
|
+
* @param {{ projectRoot?: string, executedRequired?: boolean }} [opts]
|
|
535
|
+
* @returns {{ canClose: boolean, reason: string, strategy: string|null,
|
|
536
|
+
* record: Object|null, substep?: string, wentInFlightAt?: string|null }}
|
|
537
|
+
*/
|
|
538
|
+
function canClose(substep, opts = {}) {
|
|
539
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
540
|
+
const state = stateStore.read(projectRoot);
|
|
541
|
+
if (!state) return { canClose: false, reason: 'no-state', strategy: null, record: null };
|
|
542
|
+
|
|
543
|
+
const target = resolveTarget(state, substep);
|
|
544
|
+
if (!target) {
|
|
545
|
+
return { canClose: false, reason: 'substep-not-found', strategy: null, record: null, substep };
|
|
546
|
+
}
|
|
547
|
+
const canonical = `${target.tierKey}.${target.subStepKey}`;
|
|
548
|
+
const wentInFlightAt = target.updated || null;
|
|
549
|
+
const sinceInFlight = (record) =>
|
|
550
|
+
!wentInFlightAt || (record.timestamp && record.timestamp >= wentInFlightAt);
|
|
551
|
+
|
|
552
|
+
const executedRequired = opts.executedRequired !== undefined
|
|
553
|
+
? Boolean(opts.executedRequired)
|
|
554
|
+
: EXECUTED_REQUIRED_SUBSTEPS.has(target.subStepKey);
|
|
555
|
+
const strategy = executedRequired ? 'executed' : 'attested-ok';
|
|
556
|
+
|
|
557
|
+
const ledger = readLedger(projectRoot);
|
|
558
|
+
const forSubstep = (record) => record && record.substep === canonical && sinceInFlight(record);
|
|
559
|
+
const executed = ledger.filter((r) => forSubstep(r) && r.kind === 'executed');
|
|
560
|
+
|
|
561
|
+
const verdict = (canCloseValue, reason, record) => ({
|
|
562
|
+
canClose: canCloseValue,
|
|
563
|
+
reason,
|
|
564
|
+
strategy,
|
|
565
|
+
record: record || null,
|
|
566
|
+
substep: canonical,
|
|
567
|
+
wentInFlightAt
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
if (executedRequired) {
|
|
571
|
+
if (executed.length === 0) return verdict(false, 'no-executed-record-since-in-flight', null);
|
|
572
|
+
const latest = executed[executed.length - 1];
|
|
573
|
+
if (!latest.verified) return verdict(false, 'latest-executed-record-failed', latest);
|
|
574
|
+
return verdict(true, 'executed-pass', latest);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const passing = executed.filter((r) => r.verified);
|
|
578
|
+
if (passing.length > 0) return verdict(true, 'executed-pass', passing[passing.length - 1]);
|
|
579
|
+
if (executed.length > 0) return verdict(false, 'executed-record-failed', executed[executed.length - 1]);
|
|
580
|
+
|
|
581
|
+
const attested = ledger.filter((r) => forSubstep(r) && r.kind === 'attested');
|
|
582
|
+
if (attested.length > 0) return verdict(true, 'attested', attested[attested.length - 1]);
|
|
583
|
+
|
|
584
|
+
return verdict(false, 'no-record-since-in-flight', null);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Record a structured reflection, rebound from the upstream reflect tool: what
|
|
589
|
+
* was attempted, how it went, what was observed, the root cause when known, and
|
|
590
|
+
* the next action. Appends to .godpowers/ledger/reflections.jsonl. Used after a
|
|
591
|
+
* significant action or failure so course corrections rest on recorded
|
|
592
|
+
* observations rather than guesswork.
|
|
593
|
+
*
|
|
594
|
+
* @param {{ action: string, outcome?: string, observation?: string,
|
|
595
|
+
* rootCause?: string, next?: string, lesson?: string }} reflection
|
|
596
|
+
* @param {{ substep?: string, projectRoot?: string, now?: string }} [opts]
|
|
597
|
+
* @returns {{ record: Object, reflections: string }}
|
|
598
|
+
*/
|
|
599
|
+
function reflect(reflection, opts = {}) {
|
|
600
|
+
const input = reflection || {};
|
|
601
|
+
if (typeof input.action !== 'string' || input.action.trim() === '') {
|
|
602
|
+
throw new Error('evidence.reflect requires a non-empty action');
|
|
603
|
+
}
|
|
604
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
605
|
+
const outcome = REFLECTION_OUTCOMES.has(input.outcome) ? input.outcome : 'partial';
|
|
606
|
+
const state = stateStore.read(projectRoot);
|
|
607
|
+
const context = substepContext(state, opts.substep);
|
|
608
|
+
const record = {
|
|
609
|
+
action: input.action,
|
|
610
|
+
outcome,
|
|
611
|
+
observation: input.observation !== undefined && input.observation !== null ? input.observation : null,
|
|
612
|
+
root_cause: input.rootCause !== undefined && input.rootCause !== null ? input.rootCause : null,
|
|
613
|
+
next: input.next !== undefined && input.next !== null ? input.next : null,
|
|
614
|
+
lesson: input.lesson !== undefined && input.lesson !== null ? input.lesson : null,
|
|
615
|
+
timestamp: opts.now || isoNow(),
|
|
616
|
+
...context
|
|
617
|
+
};
|
|
618
|
+
ensureLedgerDir(projectRoot);
|
|
619
|
+
appendJsonlAtomic(reflectionsPath(projectRoot), record);
|
|
620
|
+
appendLog(projectRoot, `reflect outcome=${outcome} substep=${context.substep || '-'} action=\`${input.action}\``);
|
|
621
|
+
let recordedLesson = null;
|
|
622
|
+
if (record.lesson !== null && record.lesson.trim() !== '') {
|
|
623
|
+
recordedLesson = lessonAdd(record.lesson, {
|
|
624
|
+
detail: `Auto-recorded from a reflection (outcome: ${outcome}). Action: ${input.action}`,
|
|
625
|
+
tags: ['auto-reflected'],
|
|
626
|
+
scope: 'project',
|
|
627
|
+
projectRoot,
|
|
628
|
+
now: record.timestamp
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return { record, reflections: reflectionsPath(projectRoot), lesson: recordedLesson };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Read reflection records, newest last. Optionally filtered to one substep and
|
|
636
|
+
* limited to the most recent N.
|
|
637
|
+
*
|
|
638
|
+
* @param {{ substep?: string, recent?: number, projectRoot?: string }} [opts]
|
|
639
|
+
* @returns {Object[]}
|
|
640
|
+
*/
|
|
641
|
+
function reflections(opts = {}) {
|
|
642
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
643
|
+
let records = readJsonl(reflectionsPath(projectRoot));
|
|
644
|
+
if (opts.substep) {
|
|
645
|
+
const canonical = canonicalSubstep(projectRoot, opts.substep);
|
|
646
|
+
records = records.filter((r) => r && (r.substep === canonical || r.substep === opts.substep));
|
|
647
|
+
}
|
|
648
|
+
if (opts.recent !== undefined && Number.isFinite(Number(opts.recent))) {
|
|
649
|
+
const n = Math.max(0, Math.floor(Number(opts.recent)));
|
|
650
|
+
records = records.slice(-n);
|
|
651
|
+
}
|
|
652
|
+
return records;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// Memory store (rebound from the upstream memory.json)
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
function freshMemory(now) {
|
|
660
|
+
return { entries: [], metadata: { created: now, last_updated: now, total_entries: 0 } };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function readMemory(projectRoot) {
|
|
664
|
+
try {
|
|
665
|
+
const parsed = JSON.parse(fs.readFileSync(memoryPath(projectRoot), 'utf8'));
|
|
666
|
+
if (!parsed || !Array.isArray(parsed.entries)) return freshMemory(isoNow());
|
|
667
|
+
return parsed;
|
|
668
|
+
} catch (_) {
|
|
669
|
+
return freshMemory(isoNow());
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function writeMemory(projectRoot, mem) {
|
|
674
|
+
ensureLedgerDir(projectRoot);
|
|
675
|
+
mem.metadata = mem.metadata || {};
|
|
676
|
+
mem.metadata.last_updated = isoNow();
|
|
677
|
+
mem.metadata.total_entries = mem.entries.length;
|
|
678
|
+
atomic.writeJsonAtomic(memoryPath(projectRoot), mem);
|
|
679
|
+
return mem;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function memorySet(key, value, opts = {}) {
|
|
683
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
684
|
+
throw new Error('evidence.memory.set requires a non-empty key');
|
|
685
|
+
}
|
|
686
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
687
|
+
const category = MEMORY_CATEGORIES.has(opts.category) ? opts.category : 'fact';
|
|
688
|
+
const now = opts.now || isoNow();
|
|
689
|
+
const mem = readMemory(projectRoot);
|
|
690
|
+
const existing = mem.entries.find((entry) => entry && entry.key === key);
|
|
691
|
+
if (existing) {
|
|
692
|
+
existing.value = value;
|
|
693
|
+
existing.category = category;
|
|
694
|
+
existing.updated = now;
|
|
695
|
+
} else {
|
|
696
|
+
mem.entries.push({ key, value, category, updated: now });
|
|
697
|
+
}
|
|
698
|
+
writeMemory(projectRoot, mem);
|
|
699
|
+
appendLog(projectRoot, `memory set ${category} ${key}`);
|
|
700
|
+
return { key, value, category, updated: now };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function memoryGet(key, opts = {}) {
|
|
704
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
705
|
+
const mem = readMemory(projectRoot);
|
|
706
|
+
return mem.entries.find((entry) => entry && entry.key === key) || null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function memoryList(opts = {}) {
|
|
710
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
711
|
+
const mem = readMemory(projectRoot);
|
|
712
|
+
if (opts.category) return mem.entries.filter((entry) => entry && entry.category === opts.category);
|
|
713
|
+
return mem.entries.slice();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function memoryClear(key, opts = {}) {
|
|
717
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
718
|
+
const mem = readMemory(projectRoot);
|
|
719
|
+
const before = mem.entries.length;
|
|
720
|
+
if (key === undefined || key === null) {
|
|
721
|
+
mem.entries = [];
|
|
722
|
+
} else {
|
|
723
|
+
mem.entries = mem.entries.filter((entry) => !(entry && entry.key === key));
|
|
724
|
+
}
|
|
725
|
+
writeMemory(projectRoot, mem);
|
|
726
|
+
const removed = before - mem.entries.length;
|
|
727
|
+
appendLog(projectRoot, `memory clear ${key || '(all)'} removed=${removed}`);
|
|
728
|
+
return { removed };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const memory = { set: memorySet, get: memoryGet, list: memoryList, clear: memoryClear };
|
|
732
|
+
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// Lessons store (rebound from the upstream lessons/*.json; project or global)
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
function lessonAdd(text, opts = {}) {
|
|
738
|
+
if (typeof text !== 'string' || text.trim() === '') {
|
|
739
|
+
throw new Error('evidence.lesson.add requires a non-empty lesson');
|
|
740
|
+
}
|
|
741
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
742
|
+
const scope = LESSON_SCOPES.has(opts.scope) ? opts.scope : 'project';
|
|
743
|
+
const tags = Array.isArray(opts.tags) ? opts.tags : (opts.tags ? [opts.tags] : []);
|
|
744
|
+
const record = {
|
|
745
|
+
lesson: text,
|
|
746
|
+
detail: opts.detail !== undefined && opts.detail !== null ? opts.detail : null,
|
|
747
|
+
tags,
|
|
748
|
+
scope,
|
|
749
|
+
timestamp: opts.now || isoNow()
|
|
750
|
+
};
|
|
751
|
+
appendJsonlAtomic(lessonsPath(projectRoot, scope), record);
|
|
752
|
+
if (scope === 'project') appendLog(projectRoot, `lesson add ${tags.length ? `[${tags.join(',')}] ` : ''}${text}`);
|
|
753
|
+
return record;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function lessonList(opts = {}) {
|
|
757
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
758
|
+
let records;
|
|
759
|
+
if (opts.scope === 'global') {
|
|
760
|
+
records = readJsonl(lessonsPath(projectRoot, 'global'));
|
|
761
|
+
} else if (opts.scope === 'project') {
|
|
762
|
+
records = readJsonl(lessonsPath(projectRoot, 'project'));
|
|
763
|
+
} else {
|
|
764
|
+
records = readJsonl(lessonsPath(projectRoot, 'project')).concat(readJsonl(lessonsPath(projectRoot, 'global')));
|
|
765
|
+
}
|
|
766
|
+
if (opts.recent !== undefined && Number.isFinite(Number(opts.recent))) {
|
|
767
|
+
records = records.slice(-Math.max(0, Math.floor(Number(opts.recent))));
|
|
768
|
+
}
|
|
769
|
+
return records;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const lesson = { add: lessonAdd, list: lessonList };
|
|
773
|
+
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
775
|
+
// Outcome loops (rebound from the upstream outcomes/<slug>/ store)
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
|
|
778
|
+
function readGoal(projectRoot, slug) {
|
|
779
|
+
try {
|
|
780
|
+
return JSON.parse(fs.readFileSync(outcomeGoalPath(projectRoot, slug), 'utf8'));
|
|
781
|
+
} catch (_) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function outcomeStart(name, opts = {}) {
|
|
787
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
788
|
+
throw new Error('evidence.outcome.start requires a non-empty name');
|
|
789
|
+
}
|
|
790
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
791
|
+
const slug = slugify(name) || 'outcome';
|
|
792
|
+
const budget = Number.isFinite(Number(opts.budget)) && Number(opts.budget) > 0
|
|
793
|
+
? Math.floor(Number(opts.budget))
|
|
794
|
+
: 3;
|
|
795
|
+
const now = opts.now || isoNow();
|
|
796
|
+
const goal = {
|
|
797
|
+
slug,
|
|
798
|
+
title: typeof opts.goal === 'string' && opts.goal.trim() ? opts.goal : name,
|
|
799
|
+
verifier: typeof opts.verifier === 'string' && opts.verifier.trim() ? opts.verifier : null,
|
|
800
|
+
substep: opts.substep || null,
|
|
801
|
+
budget,
|
|
802
|
+
iterations: 0,
|
|
803
|
+
status: 'active',
|
|
804
|
+
created: now,
|
|
805
|
+
last_updated: now
|
|
806
|
+
};
|
|
807
|
+
fs.mkdirSync(outcomeDir(projectRoot, slug), { recursive: true });
|
|
808
|
+
atomic.writeJsonAtomic(outcomeGoalPath(projectRoot, slug), goal);
|
|
809
|
+
appendLog(projectRoot, `outcome start ${slug} budget=${budget}`);
|
|
810
|
+
return goal;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function outcomeCheck(name, opts = {}) {
|
|
814
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
815
|
+
const slug = slugify(name) || 'outcome';
|
|
816
|
+
const goal = readGoal(projectRoot, slug);
|
|
817
|
+
if (!goal) return { ran: false, reason: 'outcome-not-found', slug };
|
|
818
|
+
if (goal.status !== 'active') return { ran: false, reason: `outcome-${goal.status}`, goal };
|
|
819
|
+
if (!goal.verifier) return { ran: false, reason: 'no-verifier', goal };
|
|
820
|
+
|
|
821
|
+
const result = verify(goal.verifier, {
|
|
822
|
+
substep: goal.substep || undefined,
|
|
823
|
+
claim: goal.title,
|
|
824
|
+
timeout: opts.timeout,
|
|
825
|
+
projectRoot
|
|
826
|
+
});
|
|
827
|
+
goal.iterations += 1;
|
|
828
|
+
goal.last_updated = result.record.timestamp;
|
|
829
|
+
let statusAfter;
|
|
830
|
+
if (result.verified) statusAfter = 'succeeded';
|
|
831
|
+
else if (goal.iterations >= goal.budget) statusAfter = 'failed';
|
|
832
|
+
else statusAfter = 'active';
|
|
833
|
+
goal.status = statusAfter;
|
|
834
|
+
|
|
835
|
+
const iteration = {
|
|
836
|
+
iteration: goal.iterations,
|
|
837
|
+
verified: result.verified,
|
|
838
|
+
verify: { exit_code: result.record.exit_code, duration_seconds: result.record.duration_seconds },
|
|
839
|
+
status_after: statusAfter,
|
|
840
|
+
timestamp: result.record.timestamp
|
|
841
|
+
};
|
|
842
|
+
appendJsonlAtomic(outcomeIterationsPath(projectRoot, slug), iteration);
|
|
843
|
+
atomic.writeJsonAtomic(outcomeGoalPath(projectRoot, slug), goal);
|
|
844
|
+
appendLog(projectRoot, `outcome check ${slug} iteration=${goal.iterations} verified=${result.verified} status=${statusAfter}`);
|
|
845
|
+
return { ran: true, goal, iteration, verified: result.verified };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function outcomeStop(name, reason, opts = {}) {
|
|
849
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
850
|
+
const slug = slugify(name) || 'outcome';
|
|
851
|
+
const goal = readGoal(projectRoot, slug);
|
|
852
|
+
if (!goal) return { stopped: false, reason: 'outcome-not-found', slug };
|
|
853
|
+
goal.status = 'stopped';
|
|
854
|
+
goal.stop_reason = reason !== undefined && reason !== null ? reason : null;
|
|
855
|
+
goal.last_updated = opts.now || isoNow();
|
|
856
|
+
atomic.writeJsonAtomic(outcomeGoalPath(projectRoot, slug), goal);
|
|
857
|
+
appendLog(projectRoot, `outcome stop ${slug}: ${goal.stop_reason || ''}`);
|
|
858
|
+
return { stopped: true, goal };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function outcomeStatus(name, opts = {}) {
|
|
862
|
+
const projectRoot = path.resolve(opts.projectRoot || process.cwd());
|
|
863
|
+
const slug = slugify(name) || 'outcome';
|
|
864
|
+
const goal = readGoal(projectRoot, slug);
|
|
865
|
+
if (!goal) return null;
|
|
866
|
+
const iterations = readJsonl(outcomeIterationsPath(projectRoot, slug));
|
|
867
|
+
return { goal, iterations };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const outcome = { start: outcomeStart, check: outcomeCheck, stop: outcomeStop, status: outcomeStatus };
|
|
871
|
+
|
|
872
|
+
function provenance() {
|
|
873
|
+
return PROVENANCE;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
module.exports = {
|
|
877
|
+
verify,
|
|
878
|
+
verifyClaim,
|
|
879
|
+
canClose,
|
|
880
|
+
reflect,
|
|
881
|
+
reflections,
|
|
882
|
+
memory,
|
|
883
|
+
lesson,
|
|
884
|
+
outcome,
|
|
885
|
+
history,
|
|
886
|
+
read: readLedger,
|
|
887
|
+
provenance,
|
|
888
|
+
ledgerDir,
|
|
889
|
+
verificationsPath,
|
|
890
|
+
reflectionsPath,
|
|
891
|
+
memoryPath,
|
|
892
|
+
lessonsPath,
|
|
893
|
+
logPath,
|
|
894
|
+
readJsonl,
|
|
895
|
+
appendJsonlAtomic,
|
|
896
|
+
EXECUTED_REQUIRED_SUBSTEPS,
|
|
897
|
+
REFLECTION_OUTCOMES,
|
|
898
|
+
MEMORY_CATEGORIES,
|
|
899
|
+
LESSON_SCOPES,
|
|
900
|
+
TAIL_CHARS,
|
|
901
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
902
|
+
// Internals exposed for tests and the re-sync script.
|
|
903
|
+
_runCommand: runCommand,
|
|
904
|
+
_toStateCommand: toStateCommand,
|
|
905
|
+
_substepContext: substepContext,
|
|
906
|
+
_rollUp: rollUp,
|
|
907
|
+
_emitGateEvent: emitGateEvent
|
|
908
|
+
};
|