great-cto 3.69.0 → 3.70.1

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.
@@ -0,0 +1,146 @@
1
+ /** Controller-owned observations. Never an execution or approval authority. */
2
+ import { createHash } from 'node:crypto';
3
+
4
+ export const TRACE_LIMITS = Object.freeze({ nodes: 512, edges: 2048, snapshots: 64 });
5
+ const sha = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
6
+ const digest = value => /^[a-f0-9]{64}$/.test(value || '') ? value : null;
7
+ const safe = value => typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,160}$/.test(value) ? value : null;
8
+ const kinds = new Set(['attempt', 'context', 'checks', 'verifier', 'result', 'transition', 'gate', 'approval', 'rework', 'recovery', 'wave', 'manual', 'cancel', 'release', 'reuse']);
9
+ const relations = new Set(['dispatch', 'input', 'checks', 'verification', 'outcome', 'transition', 'join', 'gate', 'approval', 'retry', 'recovery', 'wave', 'cancel', 'release']);
10
+ const statuses = new Set(['intent', 'recorded', 'verified', 'rework', 'unverifiable', 'blocked', 'passed', 'failed', 'pending', 'approved', 'rejected', 'cancelled', 'recovered', 'manual-action', 'returned', 'threw', 'published', 'skipped']);
11
+ const array = value => Array.isArray(value) ? value : value == null ? [] : [value];
12
+ const limited = value => array(value).slice(0, 16).filter(safe);
13
+
14
+ export function initializeTrace(state) {
15
+ state.executionTrace = { version: 1, runId: state.id, complete: true, truncated: false,
16
+ nodes: [], edges: [], snapshots: [], keys: {}, incoming: {}, approved: {} };
17
+ const entry = recordTrace(state, 'transition', { key: 'entry', role: state.queue?.[0] });
18
+ for (const role of state.queue || []) traceSchedule(state, role, [entry]);
19
+ return state.executionTrace;
20
+ }
21
+ function traceFor(state) {
22
+ const t = state.executionTrace;
23
+ return t?.version === 1 && t.runId === state.id && Array.isArray(t.nodes) && Array.isArray(t.edges)
24
+ && Array.isArray(t.snapshots) && [t.keys, t.incoming, t.approved].every(v => v && typeof v === 'object' && !Array.isArray(v)) ? t : null;
25
+ }
26
+ function incomplete(t, truncated = false) { t.complete = false; t.truncated ||= truncated; return null; }
27
+
28
+ function snapshot(state, t) {
29
+ const graphDigest = sha(state.graph);
30
+ if (!t.snapshots.some(s => s.digest === graphDigest)) {
31
+ if (t.snapshots.length >= TRACE_LIMITS.snapshots) return incomplete(t, true);
32
+ const rules = Object.entries(state.graph || {});
33
+ if (rules.length > 256) return incomplete(t, true);
34
+ if (rules.some(([, r]) => ['on', 'next', 'join', 'gate'].some(k => array(r?.[k]).length > 16))) incomplete(t, true);
35
+ t.snapshots.push({ digest: graphDigest, sourceHash: digest(state.graphHash), rules: rules.map(([role, rule]) => ({
36
+ role: safe(role), on: limited(rule.on),
37
+ next: limited(rule.next), join: limited(rule.join), gates: limited(rule.gate),
38
+ })) });
39
+ }
40
+ return graphDigest;
41
+ }
42
+
43
+ export const receiptDigest = receipt => receipt && typeof receipt === 'object' ? sha(receipt) : null;
44
+ export const traceKey = (state, key) => { const keys = traceFor(state)?.keys; return keys && Object.hasOwn(keys, key) ? keys[key] : null; };
45
+ export const traceIncoming = (state, role) => { const v = traceFor(state)?.incoming[role]; return Array.isArray(v) ? [...v] : []; };
46
+ export function traceSchedule(state, role, ids, append = false) {
47
+ const t = traceFor(state); if (!t) return;
48
+ t.incoming[role] = [...new Set([...(append ? traceIncoming(state, role) : []), ...ids.filter(Boolean)])];
49
+ }
50
+ export function traceParents(state, roles, includeApprovals = true) {
51
+ const t = traceFor(state); if (!t) return [];
52
+ return [...new Set(roles.flatMap(role => {
53
+ const result = state.results?.[role];
54
+ if (!result) return [];
55
+ const parent = traceKey(state, `result:${result.attemptId}`) || (role === 'devops' ? traceKey(state, `release:${state.release?.id}:published`) : null);
56
+ if (!parent) incomplete(t);
57
+ return [parent, ...(includeApprovals ? array(t.approved[result.digest]) : [])].filter(Boolean);
58
+ }))];
59
+ }
60
+
61
+ /** All parent edges are explicit controller decisions, never time-derived. */
62
+ export function recordTrace(state, kind, { key, role = null, attemptId = null, host = null, status = 'recorded',
63
+ parents = [], relation = 'transition', input = null, output = null, resultDigest = null,
64
+ contextDigest = null, waveId = null, gates = [], verdict = null } = {}) {
65
+ const t = traceFor(state);
66
+ if (!t) return null; // Never invent history for old runs.
67
+ if (key && Object.hasOwn(t.keys, key)) return t.keys[key];
68
+ if (!kinds.has(kind) || !statuses.has(status) || !relations.has(relation)) return incomplete(t);
69
+ if (t.nodes.some(n => !n || typeof n.id !== 'string')) return incomplete(t);
70
+ const known = new Set(t.nodes.map(n => n.id));
71
+ const ids = [...new Set(parents.filter(Boolean))];
72
+ if (ids.some(id => !known.has(id))) return incomplete(t);
73
+ if (t.nodes.length >= TRACE_LIMITS.nodes || t.edges.length + ids.length > TRACE_LIMITS.edges) return incomplete(t, true);
74
+ const graphDigest = snapshot(state, t);
75
+ if (!graphDigest) return null;
76
+ if (array(gates).length > 16) incomplete(t, true);
77
+ const id = `node-${t.nodes.length + 1}`;
78
+ t.nodes.push({ id, kind, role: safe(role), attemptId: safe(attemptId), host: ['codex', 'claude-code'].includes(host) ? host : null,
79
+ status, at: new Date().toISOString(), graphDigest, inputDigest: receiptDigest(input), outputDigest: receiptDigest(output),
80
+ resultDigest: digest(resultDigest), contextDigest: digest(contextDigest), waveId: safe(waveId),
81
+ gates: limited(gates), verdict: safe(verdict) });
82
+ for (const from of ids) t.edges.push({ from, to: id, kind: relation });
83
+ if (key) t.keys[key] = id;
84
+ return id;
85
+ }
86
+
87
+ export function traceAttempt(state, attempt, { parents = traceIncoming(state, attempt.role), waveId = null } = {}) {
88
+ const node = recordTrace(state, 'attempt', { key: `attempt:${attempt.id}`, role: attempt.role, attemptId: attempt.id,
89
+ host: attempt.host, status: 'intent', input: attempt.inputReceipt, parents, relation: waveId ? 'wave' : 'dispatch', waveId });
90
+ traceSchedule(state, attempt.role, []);
91
+ return node;
92
+ }
93
+
94
+ export function traceGate(state) {
95
+ const p = state.pending; if (!p) return null;
96
+ const key = `gate:${sha({ role: p.role, result: p.result, gates: p.gates, receipt: p.receipt })}`;
97
+ const rule = state.graph[`${p.role}.${state.results[p.role]?.verdict}`] || state.graph[p.role];
98
+ return recordTrace(state, 'gate', { key, role: p.role, status: 'pending', gates: p.gates,
99
+ parents: traceParents(state, [p.role, ...array(rule?.join)]), relation: 'gate', input: p.receipt, resultDigest: p.result });
100
+ }
101
+ export function traceApproval(state, pending, rejected = false) {
102
+ const t = traceFor(state); if (!t) return null;
103
+ const parent = traceGate(state);
104
+ const id = recordTrace(state, 'approval', { role: pending.role, status: rejected ? 'rejected' : 'approved',
105
+ parents: [parent], relation: 'approval', input: pending.receipt, resultDigest: pending.result, gates: pending.gates });
106
+ if (id && !rejected) (t.approved[pending.result] ||= []).push(id);
107
+ return id;
108
+ }
109
+
110
+ /** Strict allowlist; no raw prompt, path, receipt map, gate token or report. */
111
+ export function projectExecutionTrace(state) {
112
+ const t = traceFor(state);
113
+ const unavailable = reason => ({ state: 'unavailable', complete: false, truncated: false, nodes: [], edges: [], snapshots: [], reason });
114
+ if (!t) return unavailable('This run has no controller-recorded causal history. Legacy and native observations are not reconstructed.');
115
+ if (t.nodes.length > TRACE_LIMITS.nodes || t.edges.length > TRACE_LIMITS.edges || t.snapshots.length > TRACE_LIMITS.snapshots)
116
+ return unavailable('Execution history exceeds the read limit.');
117
+ const ids = new Set(), snapshots = new Set(t.snapshots.map(s => digest(s?.digest)).filter(Boolean));
118
+ const nodes = [];
119
+ for (const n of t.nodes) {
120
+ if (!n || !safe(n.id) || ids.has(n.id) || !kinds.has(n.kind) || !statuses.has(n.status)
121
+ || !snapshots.has(n.graphDigest) || array(n.gates).length > 16 || typeof n.at !== 'string' || n.at.length > 40 || !Number.isFinite(Date.parse(n.at))) return unavailable('Execution history is malformed.');
122
+ ids.add(n.id);
123
+ nodes.push({ id: n.id, kind: n.kind, role: safe(n.role), attemptId: safe(n.attemptId), host: ['codex', 'claude-code'].includes(n.host) ? n.host : null,
124
+ status: n.status, at: n.at, graphDigest: n.graphDigest, inputDigest: digest(n.inputDigest), outputDigest: digest(n.outputDigest),
125
+ resultDigest: digest(n.resultDigest), contextDigest: digest(n.contextDigest), waveId: safe(n.waveId),
126
+ gates: limited(n.gates), verdict: safe(n.verdict) });
127
+ }
128
+ const order = new Map(nodes.map((n, i) => [n.id, i]));
129
+ const edges = [];
130
+ for (const e of t.edges) {
131
+ if (!e || !ids.has(e.from) || !ids.has(e.to) || order.get(e.from) >= order.get(e.to) || !relations.has(e.kind))
132
+ return unavailable('Execution dependencies are malformed or cyclic.');
133
+ edges.push({ from: e.from, to: e.to, kind: e.kind });
134
+ }
135
+ const projectedSnapshots = [];
136
+ for (const s of t.snapshots) {
137
+ if (!digest(s?.digest) || !Array.isArray(s.rules) || s.rules.length > 256) return unavailable('Workflow snapshot is malformed.');
138
+ if (s.rules.some(r => ['on', 'next', 'join', 'gates'].some(key => array(r?.[key]).length > 16))) return unavailable('Workflow snapshot exceeds nested limits.');
139
+ projectedSnapshots.push({ digest: s.digest, sourceHash: digest(s.sourceHash), rules: s.rules.map(r => ({
140
+ role: safe(r?.role), on: limited(r?.on), next: limited(r?.next), join: limited(r?.join), gates: limited(r?.gates),
141
+ })) });
142
+ }
143
+ return { state: nodes.length ? 'recorded' : 'empty', complete: t.complete === true && !t.truncated,
144
+ truncated: t.truncated === true, nodes, edges, snapshots: projectedSnapshots,
145
+ reason: t.complete === true && !t.truncated ? null : 'Controller history is incomplete; missing dependencies are not inferred.' };
146
+ }
package/dist/main.js CHANGED
@@ -1680,7 +1680,13 @@ async function main() {
1680
1680
  if (args.command === "run" || args.command === "status" || args.command === "resume") {
1681
1681
  const { runDaily } = await import('./run.js');
1682
1682
  const dailyArgs = [...(args.hostInput !== undefined ? ['--host', args.hostInput] : []), ...(args.taskArgs ?? [])];
1683
- await finish(runDaily(args.command, dailyArgs, { cwd: args.dir }));
1683
+ const code = runDaily(args.command, dailyArgs, { cwd: args.dir });
1684
+ if (args.command === 'status') {
1685
+ await new Promise((resolve, reject) => {
1686
+ process.stdout.write('', error => error ? reject(error) : resolve());
1687
+ });
1688
+ }
1689
+ await finish(code);
1684
1690
  }
1685
1691
  if (args.command === "serve") {
1686
1692
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.69.0",
3
+ "version": "3.70.1",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",