great-cto 3.63.0 → 3.63.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "great-cto",
3
3
  "description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
4
- "version": "3.63.0",
4
+ "version": "3.63.1",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -32,7 +32,7 @@ export const EVENTS_FILE = 'events.jsonl';
32
32
  // `hint`: a context hook (edit-impact, lesson-tripwire) put text in front of the model.
33
33
  // Recorded so its noise can be measured — how often, on what, how long — not what it said.
34
34
  export const EVENT_KINDS = Object.freeze(['agent-start', 'agent-stop', 'tool', 'denied', 'stop', 'pipeline', 'hint']);
35
- export const EVENT_FIELDS = Object.freeze(['v', 'ts', 'kind', 'session', 'agent', 'tool', 'paths', 'ok', 'duration_ms', 'outcome', 'verdict', 'hook', 'chars', 'host']);
35
+ export const EVENT_FIELDS = Object.freeze(['v', 'ts', 'kind', 'session', 'agent', 'tool', 'paths', 'ok', 'duration_ms', 'outcome', 'verdict', 'hook', 'chars', 'host', 'invocation_id']);
36
36
  export const MAX_BYTES = 5 * 1024 * 1024;
37
37
 
38
38
  const MAX_PATHS = 10;
@@ -49,6 +49,7 @@ export function makeEvent(input, { now = Date.now() } = {}) {
49
49
  const e = { v: 1, ts: iso(now), kind: input.kind };
50
50
  const session = short(input.session, 80); if (session) e.session = session;
51
51
  const agent = short(input.agent, 80); if (agent) e.agent = agent;
52
+ if (typeof input.invocation_id === 'string' && /^[a-f0-9]{64}$/.test(input.invocation_id)) e.invocation_id = input.invocation_id;
52
53
  const tool = short(input.tool, 80); if (tool) e.tool = tool;
53
54
  if (Array.isArray(input.paths)) {
54
55
  const paths = input.paths.filter((p) => typeof p === 'string' && p.trim()).map((p) => p.slice(0, 300)).slice(0, MAX_PATHS);
@@ -23,8 +23,9 @@ export function projectCodexState(state) {
23
23
  controllerDispatch: dispatchEvidenceSummary(state),
24
24
  wave: state.wave ? { id: state.wave.id, roles: state.wave.roles, hosts: state.wave.hosts, status: state.wave.status } : null,
25
25
  rolesCompleted: Object.keys(state.results || {}),
26
- attempts: (state.attempts || []).map(({ id, role, host, number, phase, status, startedAt, finishedAt }) =>
27
- ({ id, role, host: host || 'codex', number, phase, status, startedAt, finishedAt })),
26
+ attempts: (state.attempts || []).map(({ id, role, host, number, phase, status, startedAt, finishedAt, invocationId }) =>
27
+ ({ id, role, host: host || 'codex', number, phase, status, startedAt, finishedAt,
28
+ ...(invocationId ? { invocationId } : {}) })),
28
29
  pending: state.pending ? { role: state.pending.role, gates: state.pending.gates || [], createdAt: state.pending.createdAt } : null,
29
30
  release: state.release ? {
30
31
  adapter: state.release.adapter,
@@ -18,6 +18,7 @@ import { readExecutionBudget, withAgentBudget, requireAgents, releaseAgent, budg
18
18
  import { validateSpecialistPolicy, assertSpecialistEpoch, schedulePreparation, scheduleSpecialists, specialistRole, validateReviewFiles, recordReviewFiles } from './controlled-specialists.mjs';
19
19
  import { scopedReviewInput, scopedReviewCandidate, attestScopedReview, completeScopeAttestation } from './scoped-review-reuse.mjs';
20
20
  import { observeControllerCall } from './controller-dispatch-evidence.mjs';
21
+ import { invocationIdentity } from './invocation-identity.mjs';
21
22
 
22
23
  export const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
23
24
  const hash = text => createHash('sha256').update(text).digest('hex');
@@ -70,9 +71,9 @@ function cleanResponse(response) {
70
71
  function emit(state, event) {
71
72
  return appendEvent(join(state.root, '.great_cto'), { ...event, session: state.id });
72
73
  }
73
- const toolListener = (state, agent) => (ev) => {
74
+ const toolListener = (state, agent, invocationId = null) => (ev) => {
74
75
  const e = codexToolEvent(ev);
75
- if (e) emit(state, { ...e, agent });
76
+ if (e) emit(state, { ...e, agent, invocation_id: invocationId });
76
77
  };
77
78
 
78
79
  /**
@@ -171,16 +172,18 @@ function releaseSummary(state) {
171
172
 
172
173
  export async function verifyStage(state, role, proposal, execute) {
173
174
  const agent = 'codex-verifier';
175
+ const invocationId = invocationIdentity({ session_id: state.id,
176
+ agent_id: `${state.attempts?.findLast(a => a.role === role)?.id || randomUUID()}:verifier` });
174
177
  const t0 = Date.now();
175
178
  let ok = false;
176
- emit(state, { kind: 'agent-start', agent });
179
+ emit(state, { kind: 'agent-start', agent, invocation_id: invocationId });
177
180
  const wave = waveEvidence(state);
178
181
  const attestWorkflow = wave?.roles.includes(role);
179
182
  try {
180
183
  const result = cleanResponse(await execute({
181
184
  cwd: state.root, sandbox: 'read-only', ephemeral: true, extraArgs: workerArgs,
182
185
  bin: process.env.GREAT_CTO_CODEX_BIN || 'codex', timeoutMs: 300000,
183
- onEvent: toolListener(state, agent),
186
+ invocationId, onEvent: toolListener(state, agent, invocationId),
184
187
  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` +
185
188
  frozenPolicyContext(state) +
186
189
  (state.specialistStages?.[role] ? `Controlled phase: ${state.specialistStages[role].phase}, before implementation. Verify an implementable contract/threat model and its evidence; do not require nonexistent implementation or treat design sign-off as code approval. Capability: ${codexRoleProfile(specialistRole(state, role))}\n` : '') +
@@ -224,7 +227,7 @@ export async function verifyStage(state, role, proposal, execute) {
224
227
  ok = true;
225
228
  return result;
226
229
  } finally {
227
- emit(state, { kind: 'agent-stop', agent, ok, duration_ms: Date.now() - t0 });
230
+ emit(state, { kind: 'agent-stop', agent, invocation_id: invocationId, ok, duration_ms: Date.now() - t0 });
228
231
  }
229
232
  }
230
233
 
@@ -629,11 +632,12 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
629
632
  const head = workerHead(state, role);
630
633
  // Additive v1 fields: old runs retain their original single-attempt policy.
631
634
  state.attempts ??= [];
632
- const attempt = { id: randomUUID(), role, number: state.attempts.filter(a => a.role === role).length + 1,
635
+ const attempt = { id: prepared?.attemptId || randomUUID(), role, number: state.attempts.filter(a => a.role === role).length + 1,
633
636
  host: roleHost(state, role), status: 'running', phase: 'worker', startedAt: new Date().toISOString(),
634
637
  inputReceipt: stageReceipt(state, prepared ? prepared.receipt : treeReceipt(state.root), 'stage input') };
635
638
  if (prepared?.callId) attempt.workerCallId = prepared.callId;
636
639
  if (attempt.number > (state.maxAttempts ?? 1)) throw Error('stage attempt limit reached');
640
+ attempt.invocationId = invocationIdentity({ session_id: state.id, agent_id: attempt.id });
637
641
  state.attempts.push(attempt);
638
642
  state.active = role; state.steps++; save(state);
639
643
  // Recorded only once the stage is really dispatched: a run awaiting a gate, or
@@ -692,10 +696,10 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
692
696
  }
693
697
  if (!prepared && typeof runner !== 'function') throw Error(`no runner for ${roleHost(state, role)}`);
694
698
  const response = prepared ? prepared.response : reused || await withAgentBudget(state, { callId: `${attempt.id}:worker`, host: roleHost(state, role), role }, async () => {
695
- stageStarted = true; emit(state, { kind: 'agent-start', agent });
699
+ stageStarted = true; emit(state, { kind: 'agent-start', agent, invocation_id: attempt.invocationId });
696
700
  return observeControllerCall(state, { id: `${attempt.id}:worker`, host: roleHost(state, role), role, kind: 'worker' }, () => runner({ prompt, cwd: state.root, sandbox: 'read-only', ephemeral: true,
697
701
  bin: roleHost(state, role) === 'codex' ? process.env.GREAT_CTO_CODEX_BIN || 'codex' : process.env.GREAT_CTO_CLAUDE_BIN || 'claude',
698
- timeoutMs: 300000, extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent) }), save);
702
+ invocationId: attempt.invocationId, timeoutMs: 300000, extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent, attempt.invocationId) }), save);
699
703
  });
700
704
  // Codex can recover its session-index lookup without degrading the worker.
701
705
  // Keep the diagnostic in the receipt; every other warning/error blocks.
@@ -806,7 +810,7 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
806
810
  // re-reviews the candidate. Human gates and repair limits remain unchanged.
807
811
  if (state.rework?.role === role) state.rework = null;
808
812
  state.results[role] = { verdict: proposal.verdict, summary: proposal.summary, meta: proposal.meta || {},
809
- attemptId: attempt.id, host: roleHost(state, role), checks: attempt.checks ?? null, receipt,
813
+ attemptId: attempt.id, invocationId: attempt.invocationId, host: roleHost(state, role), checks: attempt.checks ?? null, receipt,
810
814
  receiptEvidence: receipt ? 'git-tree' : 'report-artifacts-only', verification,
811
815
  digest: hash(JSON.stringify({ attemptId: attempt.id, proposal })), usage: response.usage ?? null,
812
816
  diagnostics: response.errors || [], at: new Date().toISOString() };
@@ -825,7 +829,7 @@ export async function runStage(state, { execute = null, runners = { codex: runCo
825
829
  // Keep active set: a partial write or interrupted process must not be replayed.
826
830
  save(state);
827
831
  } finally {
828
- if (stageStarted) emit(state, { kind: 'agent-stop', agent, ok: stageOk, duration_ms: Date.now() - t0 });
832
+ if (stageStarted) emit(state, { kind: 'agent-stop', agent, invocation_id: attempt.invocationId, ok: stageOk, duration_ms: Date.now() - t0 });
829
833
  // ADR-023: a Codex stage is a turn. Recorded after every tree check above, and it
830
834
  // writes only git objects and a ref, never a working file. Never throws.
831
835
  if (snapshotTurn(state.root, { session: state.id }).state === 'recorded') pruneTurns(state.root, { session: state.id, keep: 50 });
@@ -893,13 +897,15 @@ export async function runParallelWave(state, { runners = { codex: runCodexExec,
893
897
  const waveId = randomUUID();
894
898
  const leases = requireAgents(state.executionBudget, roles.map(role => ({ callId: `${waveId}:${role}`, host: roleHost(state, role), role, depth: 1 })));
895
899
  state.wave = { id: waveId, roles, status: 'running', receipt,
900
+ attemptIds: Object.fromEntries(roles.map(role => [role, randomUUID()])),
896
901
  hosts: Object.fromEntries(roles.map(role => [role, roleHost(state, role)])), startedAt: new Date().toISOString() };
897
902
  const context = inlineContext(state);
898
903
  state.wave.context = context;
899
904
  try { save(state); } catch (error) { for (const lease of leases) releaseAgent(state.executionBudget, lease); throw error; }
900
905
  const calls = roles.map(async (role, index) => {
901
906
  const agent = hostAgent(state, role), started = Date.now();
902
- emit(state, { kind: 'agent-start', agent });
907
+ const invocationId = invocationIdentity({ session_id: state.id, agent_id: state.wave.attemptIds[role] });
908
+ emit(state, { kind: 'agent-start', agent, invocation_id: invocationId });
903
909
  let ok = false;
904
910
  try {
905
911
  const runner = hostRunner(state, role, runners);
@@ -907,12 +913,12 @@ export async function runParallelWave(state, { runners = { codex: runCodexExec,
907
913
  const result = await observeControllerCall(state, { id: `${waveId}:${role}`, host: roleHost(state, role), role, kind: 'worker' }, () => runner({ prompt: workerHead(state, role) + context.text, cwd: state.root,
908
914
  sandbox: 'read-only', ephemeral: true, timeoutMs: 300000,
909
915
  bin: roleHost(state, role) === 'codex' ? process.env.GREAT_CTO_CODEX_BIN || 'codex' : process.env.GREAT_CTO_CLAUDE_BIN || 'claude',
910
- extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent) }), save);
916
+ invocationId, extraArgs: roleHost(state, role) === 'codex' ? workerArgs : [], onEvent: toolListener(state, agent, invocationId) }), save);
911
917
  ok = true;
912
918
  return result;
913
919
  } finally {
914
920
  releaseAgent(state.executionBudget, leases[index]);
915
- emit(state, { kind: 'agent-stop', agent, ok, duration_ms: Date.now() - started });
921
+ emit(state, { kind: 'agent-stop', agent, invocation_id: invocationId, ok, duration_ms: Date.now() - started });
916
922
  }
917
923
  });
918
924
  const settled = await Promise.allSettled(calls);
@@ -956,6 +962,7 @@ export async function runParallelWave(state, { runners = { codex: runCodexExec,
956
962
  save(state); return state;
957
963
  }
958
964
  await runStage(state, { prepared: { response: state.wave.responses[role], receipt: state.wave.receipt,
965
+ attemptId: state.wave.attemptIds?.[role],
959
966
  context: state.wave.context, callId: `${state.wave.id}:${role}` }, runners, verify, checks, save, contextStore });
960
967
  if (!state.results[role]) { state.wave.status = 'discarded'; break; } // Other snapshot is invalid.
961
968
  }
@@ -0,0 +1,10 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /** Host-provided identity, never inferred from a shared role log or session transcript. */
4
+ export function invocationIdentity(payload) {
5
+ const valid = (v) => typeof v === 'string' && /^[A-Za-z0-9_.:-]{1,160}$/.test(v);
6
+ if (!valid(payload?.agent_id) || !valid(payload?.session_id)) return null;
7
+ // Tuple encoding avoids ambiguous delimiters; a bounded digest fits marker
8
+ // filenames even when the provider supplies long identifiers.
9
+ return createHash('sha256').update(JSON.stringify([payload.session_id, payload.agent_id])).digest('hex');
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.63.0",
3
+ "version": "3.63.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",