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.
|
|
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
|
-
|
|
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