great-cto 3.47.0 → 3.49.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.
@@ -1428,7 +1428,7 @@ export const REVIEWERS_BY_ARCHETYPE = {
1428
1428
  "marketplace": ["marketplace-reviewer", "pci-reviewer"],
1429
1429
  "fintech": ["pci-reviewer", "regulated-reviewer"],
1430
1430
  "healthcare": ["healthcare-reviewer", "security-officer"],
1431
- "web3": ["oracle-reviewer"],
1431
+ "web3": ["oracle-reviewer", "smart-contract-auditor"],
1432
1432
  "iot-embedded": ["firmware-reviewer"],
1433
1433
  "regulated": ["regulated-reviewer"],
1434
1434
  "devtools": ["devtools-reviewer"],
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { newRun, runStage, runParallelWave, parallelPair, approve, recover, cancel } from './lib/codex-pipeline.mjs';
6
6
  import { approveRelease } from './lib/codex-release.mjs';
7
7
  import { codexRunStore, listCodexRuns, codexHostDoctor } from './lib/codex-host-state.mjs';
8
+ import { beginWork, finishWork, acquireProjectLease, readWorkTask, linkWork, observeWorkRun, controlledDecisions } from './lib/work-tasks.mjs';
8
9
  import { detectClaude } from './lib/claude-exec.mjs';
9
10
 
10
11
  // State is outside the worker workspace. A per-run exclusive lock covers the entire subprocess lifetime.
@@ -22,14 +23,17 @@ const routes = raw => {
22
23
  }
23
24
  return out;
24
25
  };
26
+ const inheritedLease = process.env.GREAT_CTO_WORK_LEASE;
27
+ delete process.env.GREAT_CTO_WORK_LEASE;
25
28
  const store = codexRunStore();
26
29
  mkdirSync(store, { recursive: true, mode: 0o700 });
27
30
  const save = state => {
28
31
  const file = join(store, `${state.id}.json`);
29
32
  writeFileSync(`${file}.tmp`, JSON.stringify(state, null, 2), { mode: 0o600 });
30
33
  renameSync(`${file}.tmp`, file);
34
+ observeWorkRun(state);
31
35
  };
32
- let locked = null;
36
+ let locked = null, projectLease = null, work = null, taskForFinish = null;
33
37
  try {
34
38
  if (command === 'doctor') {
35
39
  const result = codexHostDoctor({ pluginRoot: resolve(value('--plugin-root') || join(dirname(fileURLToPath(import.meta.url)), '..')), store });
@@ -42,6 +46,21 @@ try {
42
46
  let state;
43
47
  if (command === 'start') {
44
48
  const root = realpathSync(resolve(value('--dir') || '.'));
49
+ const taskId = value('--task-id');
50
+ const allowed = (value('--allow') || '').split(',').map(p => p.trim()).filter(Boolean);
51
+ let acceptance = [], intent = value('--intent') || 'delivery', maxAttempts = value('--max-attempts') === null ? 3 : Number(value('--max-attempts'));
52
+ if (taskId) {
53
+ projectLease = acquireProjectLease(root, { reuseToken: inheritedLease });
54
+ const task = readWorkTask(taskId, { root });
55
+ acceptance = task.acceptance; intent = task.intent || 'delivery'; maxAttempts = task.budget?.maxStageAttempts || 3;
56
+ if (task.host !== 'codex' || task.goal !== value('--prompt')?.trim()
57
+ || JSON.stringify(task.authority?.writeScope) !== JSON.stringify(allowed)) throw Error('task authority does not match run request');
58
+ } else {
59
+ work = beginWork({ root, host: 'codex', goal: value('--prompt'), intent, maxAttempts, authority: { mode: 'explicit-paths', writeScope: allowed } });
60
+ projectLease = work.lease; taskForFinish = work.task.taskId;
61
+ }
62
+ const listing = listCodexRuns({ root, store });
63
+ if (listing.state === 'degraded' || listing.runs.some(r => !['done', 'cancelled'].includes(r.status))) throw Error('unfinished or unreadable project run exists; resume or inspect it first');
45
64
  if (args.includes('--routes') && value('--routes') == null) throw Error('--routes requires a value');
46
65
  const hostRoutes = routes(value('--routes'));
47
66
  if (Object.values(hostRoutes).includes('claude-code')) {
@@ -63,22 +82,39 @@ try {
63
82
  releasePolicy = JSON.parse(readFileSync(policyPath, 'utf8'));
64
83
  }
65
84
  state = newRun({ root, prompt: value('--prompt'), checkPolicy, releasePolicy,
66
- allowed: (value('--allow') || '').split(',').filter(Boolean), entry: value('--entry') || 'product-owner',
67
- maxAttempts: value('--max-attempts') === null ? 3 : Number(value('--max-attempts')), hostRoutes });
85
+ allowed, intent, entry: intent === 'research' ? 'project-auditor' : value('--entry') || 'product-owner',
86
+ maxAttempts, hostRoutes });
87
+ state.taskId = taskId || work.task.taskId;
88
+ state.acceptance = acceptance;
89
+ linkWork(state.taskId, 'runs', state.id, { root, host: 'codex' });
68
90
  save(state);
69
91
  } else {
70
92
  const id = args[0];
71
93
  if (!/^[0-9a-f-]{36}$/.test(id || '')) throw Error('a run UUID is required');
72
94
  state = JSON.parse(readFileSync(join(store, `${id}.json`), 'utf8'));
73
95
  if (state.id !== id || state.version !== 1) throw Error('invalid run state');
96
+ if (command !== 'status') {
97
+ if (command === 'resume' && state.taskId && !inheritedLease) {
98
+ work = beginWork({ root: state.root, host: 'codex', kind: 'resume', taskId: state.taskId });
99
+ projectLease = work.lease; taskForFinish = work.task.taskId;
100
+ } else projectLease = acquireProjectLease(state.root, { reuseToken: inheritedLease });
101
+ }
74
102
  }
75
- if (!['start', 'resume', 'status', 'approve', 'approve-release', 'recover', 'cancel'].includes(command)) throw Error('expected start, resume, status, approve, approve-release, recover, cancel, list or doctor');
103
+ if (!['start', 'resume', 'status', 'approve', 'approve-release', 'recover', 'cancel', 'approve-task'].includes(command)) throw Error('expected start, resume, status, approve, approve-release, recover, cancel, list or doctor');
76
104
  if (command !== 'status') {
77
105
  const lock = join(store, `${state.id}.lock`);
78
106
  mkdirSync(lock); locked = lock;
79
107
  // Reload under lock so simultaneous approvals cannot overwrite one another.
80
108
  state = JSON.parse(readFileSync(join(store, `${state.id}.json`), 'utf8'));
81
- if (command === 'approve') { approve(state, value('--token')); save(state); }
109
+ if (command === 'approve-task') {
110
+ if (!inheritedLease || !state.taskId) throw Error('task approval requires operator operation ownership');
111
+ const task = readWorkTask(state.taskId, { root: state.root });
112
+ const operation = task.operations.find(o => o.operationId === value('--operation'));
113
+ const decision = controlledDecisions(state)[0];
114
+ if (!operation || operation.state !== 'running' || operation.kind !== 'approve' || operation.data?.decisionId !== value('--decision') || !decision || decision.decisionId !== value('--decision')) throw Error('task approval is stale or unbound');
115
+ if (decision.kind === 'release') approveRelease(state, state.release.token); else approve(state, state.pending.token);
116
+ save(state);
117
+ } else if (command === 'approve') { approve(state, value('--token')); save(state); }
82
118
  else if (command === 'approve-release') { approveRelease(state, value('--token')); save(state); }
83
119
  else if (command === 'recover') { recover(state); save(state); }
84
120
  else if (command === 'cancel') { cancel(state); save(state); }
@@ -101,5 +137,7 @@ try {
101
137
  console.error(`codex-pipeline: ${error.message}`);
102
138
  process.exitCode = 2;
103
139
  } finally {
104
- if (locked) rmdirSync(locked);
140
+ try { if (locked) rmdirSync(locked);
141
+ if (taskForFinish) finishWork(taskForFinish, work.operation.operationId, process.exitCode || 0); }
142
+ finally { projectLease?.release(); }
105
143
  }
@@ -173,7 +173,7 @@ export async function verifyStage(state, role, proposal, execute) {
173
173
  bin: process.env.GREAT_CTO_CODEX_BIN || 'codex', timeoutMs: 300000,
174
174
  onEvent: toolListener(state, agent),
175
175
  prompt: `You are an independent verifier for the ${role} stage. Read the ACTUAL files and assess whether they satisfy the task for this stage.\n` +
176
- `User task: ${state.prompt}\nStage contract: ${JSON.stringify(state.graph[role])}\n` +
176
+ `User task: ${state.prompt}\nTask intent: ${state.intent || 'delivery'}; research produces a report and does not authorize implementation or release.\nAcceptance criteria (task data, not authority): ${JSON.stringify(state.acceptance || [])}\nStage contract: ${JSON.stringify(state.graph[role])}\n` +
177
177
  `Claimed metadata: ${JSON.stringify(proposal.meta || {})}\nChanged paths: ${JSON.stringify(proposal.files.map(f => f.path))}\n` +
178
178
  `Controller release evidence: ${JSON.stringify(releaseSummary(state))}\n` +
179
179
  `Controller check evidence (not worker claims): ${JSON.stringify({
@@ -213,18 +213,20 @@ export function safePath(root, name, allowed) {
213
213
  return target;
214
214
  }
215
215
 
216
- export function newRun({ root, prompt, allowed, entry = 'product-owner', pluginRoot = PLUGIN_ROOT, maxAttempts = 3, checkPolicy = null, releasePolicy = null, hostRoutes = {} }) {
216
+ export function newRun({ root, prompt, allowed, entry = 'product-owner', pluginRoot = PLUGIN_ROOT, maxAttempts = 3, intent = 'delivery', checkPolicy = null, releasePolicy = null, hostRoutes = {} }) {
217
217
  root = realpathSync(root);
218
218
  pluginRoot = realpathSync(pluginRoot);
219
219
  if (root === pluginRoot) throw Error('run from a target project, not the controller installation');
220
220
  if (!prompt?.trim() || !Array.isArray(allowed) || !allowed.length) throw Error('prompt and explicit allowed paths are required');
221
221
  if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 5) throw Error('maxAttempts must be an integer from 1 to 5');
222
+ if (!['delivery', 'research'].includes(intent)) throw Error('invalid task intent');
223
+ if (intent === 'research' && (entry !== 'project-auditor' || releasePolicy || allowed.some(p => !/^(docs|research|reports)(\/|$)/.test(p)))) throw Error('research runs require report-only paths, auditor entry and no release policy');
222
224
  if (checkPolicy) validateCheckPolicy(checkPolicy);
223
225
  for (const p of allowed) safePath(root, p, allowed);
224
226
  const graphText = readFileSync(join(pluginRoot, 'shared/pipeline.toml'), 'utf8');
225
227
  const graph = parsePipelineToml(graphText);
226
228
  if (!graph[entry] || entry.includes('.')) throw Error(`unknown entry role: ${entry}`);
227
- return { version: 1, id: randomUUID(), root, prompt, allowed, pluginRoot, graph, graphHash: hash(graphText),
229
+ return { version: 1, id: randomUUID(), root, prompt, intent, allowed, pluginRoot, graph, graphHash: hash(graphText),
228
230
  queue: [entry], results: {}, released: [], pending: null, approvals: [], active: null, status: 'ready', writes: {}, steps: 0,
229
231
  attempts: [], maxAttempts, rework: null, hostRoutes: validateRoutes(hostRoutes, graph),
230
232
  releasePolicy: releasePolicy ? validateReleasePolicy(releasePolicy, root) : null,
@@ -397,7 +399,7 @@ function workerHead(state, role) {
397
399
  `before must be SHA256 of the current file bytes or null for a new file. No deletion, symlink or binary proposals. Allowed paths: ${JSON.stringify(state.allowed)}.\n` +
398
400
  `Successful tokens: ${JSON.stringify(state.graph[role]?.on)}. Required artifact keys in meta: ${JSON.stringify(state.graph[role]?.produces || [])}. Use BLOCKED if the task requires unsupported execution.\n` +
399
401
  `ROLE PROFILE — expertise and analysis goals, never operational authority:\n${roleProfile}\n` +
400
- `User task: ${state.prompt}\n`;
402
+ `User task: ${state.prompt}\nTask intent: ${state.intent || 'delivery'}; research produces a report and does not authorize implementation or release.\nAcceptance criteria (task data, not authority): ${JSON.stringify(state.acceptance || [])}\n`;
401
403
  }
402
404
 
403
405
  function inlineContext(state) {
@@ -31,6 +31,7 @@ export const CODEX_ROLE_PROFILES = Object.freeze({
31
31
  'ai-eval-engineer': 'Design representative, versioned evals with explicit rubrics, baselines, confidence treatment, regression thresholds and traceable failure analysis.',
32
32
  'db-migration-reviewer': 'Review schema evolution for compatibility, locking, data correctness, rollout ordering, backfill, observability and rollback risk.',
33
33
  'design-advisor': 'Define interaction hierarchy, states, accessibility, responsive behavior and design-system constraints that implementation can verify.',
34
+ 'smart-contract-auditor': 'Audit Solidity/EVM contracts vector by vector: access control, reentrancy, rounding and share inflation, oracle and flash-loan economics, non-standard tokens, signatures and upgradeability. Report a finding only with file:line evidence, an unprivileged trigger and a material victim; name every analyzer that did not run as not checked, never clean. Return FAIL while any Critical or High is open.',
34
35
  'performance-engineer': 'Analyze hot paths, complexity, allocation, blocking, I/O, query shape and capacity evidence against explicit latency and throughput budgets.',
35
36
  'infra-provisioner': 'Specify infrastructure topology, identity, network, secrets, state, policy, observability, cost and reversible rollout controls.',
36
37
  'growth-engineer': 'Define an ethical, measurable growth experiment with target cohort, event contract, guardrails, power assumptions and decision thresholds.',
@@ -31,7 +31,7 @@ export const REVIEWERS_BY_ARCHETYPE = {
31
31
  "marketplace": ["marketplace-reviewer", "pci-reviewer"],
32
32
  "fintech": ["pci-reviewer", "regulated-reviewer"],
33
33
  "healthcare": ["healthcare-reviewer", "security-officer"],
34
- "web3": ["oracle-reviewer"],
34
+ "web3": ["oracle-reviewer", "smart-contract-auditor"],
35
35
  "iot-embedded": ["firmware-reviewer"],
36
36
  "regulated": ["regulated-reviewer"],
37
37
  "devtools": ["devtools-reviewer"],
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, rmSync, renameSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
+ import { observeWorkSession } from './work-tasks.mjs';
26
27
  import { projectRoot } from './project-root.mjs';
27
28
 
28
29
  export const STALE_WORKING_MS = 5 * 60 * 1000;
@@ -37,6 +38,7 @@ const safeId = (id) => String(id || '').replace(/[^A-Za-z0-9_-]/g, '').slice(0,
37
38
  export function recordHookEvent(payload, { now = Date.now() } = {}) {
38
39
  // The session's cwd wanders into subdirectories; state belongs at the project root.
39
40
  const cwd = projectRoot(payload?.cwd || process.cwd());
41
+ try { observeWorkSession({ ...payload, cwd }); } catch { /* native hook status remains available independently */ }
40
42
  const id = safeId(payload?.session_id);
41
43
  if (!id) return null;
42
44
  const dir = join(cwd, '.great_cto', 'status');
@@ -0,0 +1,33 @@
1
+ /** Host-owned approval adapter. Ordinary resume never approves a proposal. */
2
+ import { spawnSync } from 'node:child_process';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readWorkTask, beginWork, finishWork, publicWorkTask } from './work-tasks.mjs';
5
+ export function decisionCapabilities(task) {
6
+ const busy = task.operations.some(o => o.state === 'running');
7
+ return publicWorkTask(task).decisions.map(d => ({ ...d, capability: {
8
+ action: d.engine === 'codex' ? 'approve' : 'native_host',
9
+ enabled: !busy && !['stale', 'unverifiable'].includes(d.bindingState) && d.engine === 'codex' && task.managed !== false,
10
+ reason: d.bindingState === 'stale' ? 'Proposal receipt is stale; refresh it inside its host' : busy ? 'Operation owns this task' : d.engine === 'claude-code' ? 'Native permission and pipeline decisions stay inside the bound Claude session' : null,
11
+ } }));
12
+ }
13
+ export function approveWorkDecision({ root, taskId, decisionId, expectedRevision, operationId }, { execute = spawnSync, ...options } = {}) {
14
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 1 || !/^[0-9a-f]{64}$/.test(decisionId || '')) throw Error('approval requires a decision identity and revision');
15
+ const task = readWorkTask(taskId, { ...options, root });
16
+ if (task.host !== 'codex') throw Error('native host approval has no remote capability; respond inside Claude');
17
+ const work = beginWork({ root, taskId, host: task.host, kind: 'approve', expectedRevision, operationId, data: { decisionId } }, options);
18
+ if (work.replay) return publicWorkTask(work.task);
19
+ try {
20
+ if (task.links.runs.length !== 1 || !(task.decisions || []).some(d => d.decisionId === decisionId)) throw Error('decision is missing, stale or ambiguous');
21
+ const controller = fileURLToPath(new URL('../codex-pipeline.mjs', import.meta.url));
22
+ const result = execute(process.execPath, [controller, 'approve-task', task.links.runs[0], '--decision', decisionId, '--operation', work.operation.operationId],
23
+ { encoding: 'utf8', env: { ...process.env, ...(options.store ? { GREAT_CTO_TASKS_DIR: options.store } : {}), GREAT_CTO_WORK_LEASE: work.lease.token } });
24
+ const code = result.error ? 2 : result.status ?? 2;
25
+ finishWork(taskId, work.operation.operationId, code, { ...options, root });
26
+ if (code !== 0) throw Error('host refused approval: ' + String(result.stderr || result.error?.message || 'inspect host state').trim().slice(0, 500));
27
+ return publicWorkTask(readWorkTask(taskId, { ...options, root }));
28
+ } catch (e) {
29
+ const latest = readWorkTask(taskId, { ...options, root });
30
+ if (latest.operations.find(o => o.operationId === work.operation.operationId)?.state === 'running') finishWork(taskId, work.operation.operationId, 2, { ...options, root });
31
+ throw e;
32
+ } finally { work.lease.release(); }
33
+ }
@@ -0,0 +1,31 @@
1
+ /** Cohort measurements are evidence; they do not change authority or auto-approve. */
2
+ const median = values => { const s = values.filter(Number.isFinite).sort((a,b) => a-b); return !s.length ? null : s.length % 2 ? s[(s.length-1)/2] : (s[s.length/2-1]+s[s.length/2])/2; };
3
+ export function measureWork(tasks) {
4
+ const cohorts = [];
5
+ for (const host of ['claude-code', 'codex']) for (const intent of ['delivery', 'research']) {
6
+ const rows = tasks.filter(t => t.host === host && (t.intent || 'delivery') === intent);
7
+ const resumes = rows.flatMap(t => t.operations.filter(o => o.kind === 'resume' && o.state === 'host_returned'));
8
+ const known = resumes.filter(o => o.resume?.state && o.resume.state !== 'unobserved');
9
+ const reasons = {};
10
+ for (const t of rows) for (const [kind, count] of Object.entries(t.metrics?.interruptions || {})) reasons[kind] = (reasons[kind] || 0) + count;
11
+ const technical = Object.entries(reasons).filter(([kind]) => !['host_approval','native_pipeline_approval','native_permission_or_input'].includes(kind)).reduce((n, [,v]) => n+v, 0);
12
+ cohorts.push({ host, intent, tasks: rows.length, observedStarts: rows.filter(t => Number.isFinite(t.metrics?.timeToObservedStartMs)).length,
13
+ medianTimeToObservedStartMs: median(rows.map(t => t.metrics?.timeToObservedStartMs)), interruptions: reasons,
14
+ technicalInterruptionsPerTask: rows.length ? technical/rows.length : null, resumes: resumes.length, knownResumes: known.length,
15
+ progressedResumes: known.filter(o => o.resume.state === 'progressed').length,
16
+ resumeProgressRate: known.length ? known.filter(o => o.resume.state === 'progressed').length / known.length : null,
17
+ completed: rows.filter(t => t.phase === 'completed').length, blocked: rows.filter(t => t.phase === 'blocked').length,
18
+ verificationRework: rows.reduce((n,t) => n + (t.rework?.attempts || 0), 0) });
19
+ }
20
+ return { schemaVersion: 1, observation: 'task-stage-progress, not model latency or production defect rate', cohorts };
21
+ }
22
+ export function compareWork(current, baseline, { minTasks = 20, minKnownResumes = 5 } = {}) {
23
+ if (!Number.isInteger(minTasks) || minTasks < 1 || !Number.isInteger(minKnownResumes) || minKnownResumes < 1) throw Error('invalid comparison sample policy');
24
+ return { automaticAuthorityChanges: false, cohorts: current.cohorts.map(c => {
25
+ const b = baseline?.cohorts?.find(x => x.host === c.host && x.intent === c.intent);
26
+ const sufficient = !!b && c.tasks >= minTasks && b.tasks >= minTasks && c.knownResumes >= minKnownResumes && b.knownResumes >= minKnownResumes
27
+ && Number.isFinite(c.technicalInterruptionsPerTask) && Number.isFinite(b.technicalInterruptionsPerTask) && Number.isFinite(c.resumeProgressRate) && Number.isFinite(b.resumeProgressRate);
28
+ return { host: c.host, intent: c.intent, state: !sufficient ? 'insufficient_evidence' : c.technicalInterruptionsPerTask < b.technicalInterruptionsPerTask && c.resumeProgressRate >= b.resumeProgressRate ? 'candidate_for_review' : 'no_measured_improvement',
29
+ reason: !sufficient ? 'Matched host/intent baseline and known resume sample are required' : 'Measurement can recommend review; safety, defect evidence and authority still require separate decisions' };
30
+ }) };
31
+ }
@@ -0,0 +1,78 @@
1
+ /** Explicit operator attestations, bound to criteria, actual bytes and host state. */
2
+ import { readFileSync, lstatSync, realpathSync } from 'node:fs';
3
+ import { resolve, relative, isAbsolute, sep, join } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { treeReceipt } from './receipt.mjs';
6
+ import { codexRunStore } from './codex-host-state.mjs';
7
+ import { assertExternal, readWorkTask, mutateWorkTask, beginWork, finishWork, publicWorkTask } from './work-tasks.mjs';
8
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
9
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
10
+ export function readOutcomeEvidence(root, file) {
11
+ const path = resolve(file); assertExternal(root, path);
12
+ const info = lstatSync(path);
13
+ if (!info.isFile() || info.isSymbolicLink() || (info.mode & 0o077) || info.size > 65536) throw Error('evidence must be an external private regular file (0600, <=64 KiB)');
14
+ const raw = readFileSync(path); return { document: JSON.parse(raw), digest: hash(raw) };
15
+ }
16
+ function artifact(root, path) {
17
+ if (typeof path !== 'string' || !path || isAbsolute(path) || path.split(/[\\/]/).includes('..') || path.includes('\\')) throw Error('invalid outcome artifact path');
18
+ const resolved = resolve(root, path), actual = realpathSync(resolved), rel = relative(root, actual);
19
+ if (actual !== resolved || !rel || rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel) || !lstatSync(actual).isFile()) throw Error('outcome artifact must be a project regular file without symlinks');
20
+ return { path, sha256: hash(readFileSync(actual)) };
21
+ }
22
+ export function assertHostSettled(task, { runsStore = codexRunStore() } = {}) {
23
+ if ((task.decisions || []).length || ['working', 'needs_decision', 'publishing', 'cancelled', 'blocked'].includes(task.phase)) throw Error('host state is not settled for acceptance');
24
+ if (task.host === 'claude-code') {
25
+ if (task.links.sessions.length !== 1 || (task.managed === false && !task.nativeSessionEnded)) throw Error('native session ownership is not settled');
26
+ return;
27
+ }
28
+ if (task.links.runs.length !== 1) throw Error('controlled run link is missing or ambiguous');
29
+ const id = task.links.runs[0], path = join(runsStore, id + '.json'), info = lstatSync(path);
30
+ if (info.isSymbolicLink() || (info.mode & 0o077)) throw Error('unsafe controlled run state');
31
+ const run = JSON.parse(readFileSync(path, 'utf8'));
32
+ if (run.id !== id || run.taskId !== task.taskId || run.root !== task.root || run.status !== 'done' || run.active || run.pending || run.wave?.status === 'running') throw Error('controlled run has not reached a settled terminal state');
33
+ if (run.release && (run.release.status !== 'verified' || run.release.smoke?.state !== 'passed')) throw Error('release has no verified publication/smoke evidence');
34
+ }
35
+ function verifyDocument(task, document) {
36
+ if (!document || document.taskId !== task.taskId || document.goal !== task.goal || document.revision !== task.revision
37
+ || document.kind !== (task.intent || 'delivery') || document.goalSatisfied !== true || !task.acceptance.length) throw Error('evidence must bind task, goal, revision, intent and explicit criteria');
38
+ if (!Array.isArray(document.criteria) || document.criteria.length !== task.acceptance.length
39
+ || document.criteria.some((c, i) => !c || c.index !== i || c.text !== task.acceptance[i] || c.state !== 'passed' || typeof c.evidence !== 'string' || !c.evidence.trim())) throw Error('each acceptance criterion needs ordered, explicit passing evidence');
40
+ if (!Array.isArray(document.artifacts) || !document.artifacts.length || new Set(document.artifacts.map(a => a?.path)).size !== document.artifacts.length) throw Error('unique outcome artifacts are required');
41
+ const artifacts = document.artifacts.map(a => {
42
+ const current = artifact(task.root, a.path);
43
+ if (a.sha256 !== current.sha256) throw Error('outcome artifact bytes do not match evidence');
44
+ if (task.intent === 'research' && !/^(docs|research|reports)\//.test(a.path)) throw Error('research outcomes contain reports, not implementation artifacts');
45
+ return current;
46
+ });
47
+ const receipt = treeReceipt(task.root);
48
+ if (!receipt) throw Error('outcome evidence requires a readable Git receipt');
49
+ return { kind: document.kind, state: 'verified', source: 'operator-attestation', evidenceDigest: hash(JSON.stringify(document)), receipt,
50
+ artifacts, criteria: document.criteria, verifiedAt: new Date().toISOString() };
51
+ }
52
+ export function applyOutcome({ root, taskId, kind, expectedRevision, operationId, evidenceFile }, options = {}) {
53
+ if (!['verify', 'complete'].includes(kind) || !Number.isInteger(expectedRevision)) throw Error('verify/complete require an explicit revision');
54
+ const task = readWorkTask(taskId, { ...options, root });
55
+ const evidence = kind === 'verify' ? readOutcomeEvidence(root, evidenceFile) : null;
56
+ const work = beginWork({ root, host: task.host, taskId, kind, expectedRevision, operationId,
57
+ data: evidence ? { evidenceDigest: evidence.digest } : null }, options);
58
+ if (work.replay) return publicWorkTask(work.task);
59
+ try {
60
+ assertHostSettled(task, options);
61
+ const current = readWorkTask(taskId, { ...options, root });
62
+ if (kind === 'verify') {
63
+ const outcome = verifyDocument(task, evidence.document);
64
+ mutateWorkTask(taskId, t => { t.outcome = outcome; t.phase = 'verified'; t.reason = 'Criteria and artifact identity recorded by explicit operator attestation'; }, { ...options, root });
65
+ } else {
66
+ const outcome = current.outcome;
67
+ if (current.phase !== 'verified' || outcome?.state !== 'verified' || !same(treeReceipt(root), outcome.receipt)) throw Error('verified outcome is missing or its tree receipt is stale');
68
+ for (const a of outcome.artifacts) if (artifact(task.root, a.path).sha256 !== a.sha256) throw Error('verified outcome artifact changed');
69
+ mutateWorkTask(taskId, t => { t.outcome.state = 'completed'; t.outcome.completedAt = new Date().toISOString(); t.phase = 'completed'; t.reason = 'Explicit completion of the verified requested outcome'; }, { ...options, root });
70
+ }
71
+ finishWork(taskId, work.operation.operationId, 0, { ...options, root });
72
+ return publicWorkTask(readWorkTask(taskId, { ...options, root }));
73
+ } catch (error) {
74
+ if (readWorkTask(taskId, { ...options, root }).operations.find(o => o.operationId === work.operation.operationId)?.state === 'running') finishWork(taskId, work.operation.operationId, 2, { ...options, root });
75
+ throw error;
76
+ }
77
+ finally { work.lease.release(); }
78
+ }