@fitlab-ai/agent-infra 0.9.4 → 0.9.6
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
function createCodexLifecycleState(cliVersion) {
|
|
2
|
+
if (!cliVersion.trim())
|
|
3
|
+
throw new Error('Codex CLI version is required');
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
cliVersion,
|
|
7
|
+
status: 'empty',
|
|
8
|
+
spawn: null,
|
|
9
|
+
child: null,
|
|
10
|
+
thread: null,
|
|
11
|
+
settings: null,
|
|
12
|
+
reroute: null,
|
|
13
|
+
terminal: null,
|
|
14
|
+
stop: null,
|
|
15
|
+
startEvidence: null,
|
|
16
|
+
stopEvidence: null,
|
|
17
|
+
error: null
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function invalid(state, code, message) {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
...state,
|
|
23
|
+
status: 'invalid',
|
|
24
|
+
startEvidence: null,
|
|
25
|
+
stopEvidence: null,
|
|
26
|
+
error: Object.freeze({ code, message })
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function same(left, right) {
|
|
30
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
31
|
+
}
|
|
32
|
+
function requiredStrings(event) {
|
|
33
|
+
return Object.entries(event).every(([key, value]) => key === 'forkedFromId'
|
|
34
|
+
|| (typeof value !== 'string' || value.trim() !== ''));
|
|
35
|
+
}
|
|
36
|
+
function derive(state) {
|
|
37
|
+
const { spawn, child, thread, settings, reroute, terminal, stop } = state;
|
|
38
|
+
if (spawn && child && (spawn.sessionId !== child.parentThreadId
|
|
39
|
+
|| spawn.nativeAgent !== child.nativeAgent)) {
|
|
40
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook spawn and child identity do not match');
|
|
41
|
+
}
|
|
42
|
+
if (child && child.sessionId !== child.parentThreadId) {
|
|
43
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'child event session does not match the parent thread');
|
|
44
|
+
}
|
|
45
|
+
if (child && thread && child.childThreadId !== thread.childThreadId) {
|
|
46
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server child identity do not match');
|
|
47
|
+
}
|
|
48
|
+
if (child && thread && child.nativeAgent !== thread.nativeAgent) {
|
|
49
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server agent roles do not match');
|
|
50
|
+
}
|
|
51
|
+
if (child && thread && child.parentThreadId !== thread.parentThreadId) {
|
|
52
|
+
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'hook and App Server parent identity do not match');
|
|
53
|
+
}
|
|
54
|
+
if (spawn && thread && (thread.parentThreadId !== spawn.sessionId
|
|
55
|
+
|| thread.sourceParentThreadId !== spawn.sessionId)) {
|
|
56
|
+
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'App Server parent identity does not match the spawning session');
|
|
57
|
+
}
|
|
58
|
+
if (spawn && thread && spawn.nativeAgent !== thread.nativeAgent) {
|
|
59
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'spawn and App Server agent roles do not match');
|
|
60
|
+
}
|
|
61
|
+
if (thread?.forkedFromId !== null && thread?.forkedFromId !== undefined) {
|
|
62
|
+
return invalid(state, 'CODEX_EVIDENCE_FORK_FORBIDDEN', 'forked child threads are not fresh lifecycle evidence');
|
|
63
|
+
}
|
|
64
|
+
if (settings && child && settings.childThreadId !== child.childThreadId) {
|
|
65
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'resolved settings belong to a different child');
|
|
66
|
+
}
|
|
67
|
+
if (reroute && child && reroute.childThreadId !== child.childThreadId) {
|
|
68
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'model reroute belongs to a different child');
|
|
69
|
+
}
|
|
70
|
+
if (spawn?.requestedModel && settings && spawn.requestedModel !== settings.model) {
|
|
71
|
+
if (!reroute
|
|
72
|
+
|| reroute.fromModel !== spawn.requestedModel
|
|
73
|
+
|| reroute.toModel !== settings.model
|
|
74
|
+
|| !reroute.reason.trim()) {
|
|
75
|
+
return invalid(state, 'CODEX_EVIDENCE_MODEL_UNEXPLAINED', 'resolved model differs without a matching host reroute');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (spawn?.requestedReasoningEffort
|
|
79
|
+
&& settings
|
|
80
|
+
&& spawn.requestedReasoningEffort !== settings.reasoningEffort) {
|
|
81
|
+
return invalid(state, 'CODEX_EVIDENCE_EFFORT_UNEXPLAINED', 'resolved reasoning effort differs without structured host evidence');
|
|
82
|
+
}
|
|
83
|
+
if (terminal && terminal.status !== 'completed') {
|
|
84
|
+
return invalid(state, 'CODEX_EVIDENCE_TERMINAL_INVALID', `child turn ended as '${terminal.status}'`);
|
|
85
|
+
}
|
|
86
|
+
if (terminal && child && terminal.childThreadId !== child.childThreadId) {
|
|
87
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'terminal event belongs to a different child');
|
|
88
|
+
}
|
|
89
|
+
if (stop && child && (stop.sessionId !== child.sessionId
|
|
90
|
+
|| stop.turnId !== child.turnId
|
|
91
|
+
|| stop.childThreadId !== child.childThreadId
|
|
92
|
+
|| stop.nativeAgent !== child.nativeAgent)) {
|
|
93
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook stop identity does not match the child');
|
|
94
|
+
}
|
|
95
|
+
let startEvidence = null;
|
|
96
|
+
if (spawn && child && thread && settings) {
|
|
97
|
+
startEvidence = Object.freeze({
|
|
98
|
+
schemaVersion: 1,
|
|
99
|
+
cliVersion: state.cliVersion,
|
|
100
|
+
parentThreadId: spawn.sessionId,
|
|
101
|
+
parentTurnId: spawn.turnId,
|
|
102
|
+
spawnToolUseId: spawn.toolUseId,
|
|
103
|
+
childThreadId: child.childThreadId,
|
|
104
|
+
nativeAgent: child.nativeAgent,
|
|
105
|
+
spawnMode: 'fresh',
|
|
106
|
+
actualModel: Object.freeze({ value: settings.model, source: 'codex-app-server-settings' }),
|
|
107
|
+
actualReasoningEffort: Object.freeze({ value: settings.reasoningEffort, source: 'codex-app-server-settings' }),
|
|
108
|
+
modelFallbackReason: reroute?.reason ?? null,
|
|
109
|
+
reasoningEffortFallbackReason: null,
|
|
110
|
+
hookDefinitionHash: spawn.hookDefinitionHash
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
let stopEvidence = null;
|
|
114
|
+
if (startEvidence && terminal?.status === 'completed' && stop) {
|
|
115
|
+
stopEvidence = Object.freeze({
|
|
116
|
+
schemaVersion: 1,
|
|
117
|
+
childThreadId: startEvidence.childThreadId,
|
|
118
|
+
turnId: terminal.turnId,
|
|
119
|
+
terminalStatus: 'completed',
|
|
120
|
+
hookStopObserved: stop.source !== 'parent-rollout'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const status = stopEvidence
|
|
124
|
+
? 'stop-ready'
|
|
125
|
+
: terminal
|
|
126
|
+
? 'observed-terminal'
|
|
127
|
+
: startEvidence
|
|
128
|
+
? 'start-ready'
|
|
129
|
+
: child
|
|
130
|
+
? 'observed-child'
|
|
131
|
+
: spawn
|
|
132
|
+
? 'observed-spawn'
|
|
133
|
+
: 'empty';
|
|
134
|
+
return Object.freeze({ ...state, status, startEvidence, stopEvidence, error: null });
|
|
135
|
+
}
|
|
136
|
+
function reduceCodexLifecycleEvent(state, event) {
|
|
137
|
+
if (state.status === 'invalid' || state.status === 'expired')
|
|
138
|
+
return state;
|
|
139
|
+
if (!requiredStrings(event)) {
|
|
140
|
+
return invalid(state, 'CODEX_EVIDENCE_FIELD_MISSING', 'lifecycle event contains an empty required field');
|
|
141
|
+
}
|
|
142
|
+
const key = {
|
|
143
|
+
'hook-spawn': 'spawn',
|
|
144
|
+
'hook-child': 'child',
|
|
145
|
+
'app-thread': 'thread',
|
|
146
|
+
'app-settings': 'settings',
|
|
147
|
+
'app-reroute': 'reroute',
|
|
148
|
+
'app-terminal': 'terminal',
|
|
149
|
+
'hook-stop': 'stop'
|
|
150
|
+
}[event.type];
|
|
151
|
+
const existing = state[key];
|
|
152
|
+
if (existing) {
|
|
153
|
+
if (same(existing, event))
|
|
154
|
+
return state;
|
|
155
|
+
return invalid(state, 'CODEX_EVIDENCE_REPLAY_CONFLICT', `conflicting '${event.type}' event`);
|
|
156
|
+
}
|
|
157
|
+
return derive(Object.freeze({ ...state, [key]: Object.freeze({ ...event }) }));
|
|
158
|
+
}
|
|
159
|
+
function expireCodexLifecycleState(state) {
|
|
160
|
+
if (!['observed-spawn', 'observed-child', 'start-ready', 'observed-terminal'].includes(state.status)) {
|
|
161
|
+
return state;
|
|
162
|
+
}
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
...state,
|
|
165
|
+
status: 'expired',
|
|
166
|
+
startEvidence: null,
|
|
167
|
+
stopEvidence: null,
|
|
168
|
+
error: Object.freeze({
|
|
169
|
+
code: 'CODEX_EVIDENCE_EXPIRED',
|
|
170
|
+
message: 'Codex lifecycle evidence expired before completion'
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent };
|
|
175
|
+
//# sourceMappingURL=evidence.js.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent } from './evidence.js';
|
|
5
|
+
const LOCK_RETRY_MS = 10;
|
|
6
|
+
const LOCK_TIMEOUT_MS = 1_000;
|
|
7
|
+
const LOCK_STALE_MS = 30_000;
|
|
8
|
+
function digest(value) {
|
|
9
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
function readRecord(file) {
|
|
12
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
13
|
+
if (value.schemaVersion !== 1
|
|
14
|
+
|| !Number.isSafeInteger(value.revision)
|
|
15
|
+
|| value.revision < 1
|
|
16
|
+
|| !value.state
|
|
17
|
+
|| value.state.schemaVersion !== 1) {
|
|
18
|
+
throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function recordFiles(root) {
|
|
23
|
+
if (!fs.existsSync(root))
|
|
24
|
+
return [];
|
|
25
|
+
return fs.readdirSync(root)
|
|
26
|
+
.filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
|
|
27
|
+
.map((name) => path.join(root, name));
|
|
28
|
+
}
|
|
29
|
+
function writeRecord(file, record, expectedRevision) {
|
|
30
|
+
const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
31
|
+
fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
flag: 'wx',
|
|
34
|
+
mode: 0o600
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
const actualRevision = fs.existsSync(file) ? readRecord(file).revision : 0;
|
|
38
|
+
if (actualRevision !== expectedRevision) {
|
|
39
|
+
throw new Error(`Codex lifecycle revision changed from ${expectedRevision} to ${actualRevision}`);
|
|
40
|
+
}
|
|
41
|
+
fs.renameSync(temp, file);
|
|
42
|
+
fs.chmodSync(file, 0o600);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (fs.existsSync(temp))
|
|
46
|
+
fs.unlinkSync(temp);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function createCodexLifecycleStore(options) {
|
|
50
|
+
const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
|
|
51
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
52
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
53
|
+
fs.chmodSync(root, 0o700);
|
|
54
|
+
function withWriteLock(operation) {
|
|
55
|
+
const lock = path.join(root, '.write.lock');
|
|
56
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
57
|
+
let descriptor = null;
|
|
58
|
+
while (descriptor === null) {
|
|
59
|
+
try {
|
|
60
|
+
descriptor = fs.openSync(lock, 'wx', 0o600);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== 'EEXIST')
|
|
64
|
+
throw error;
|
|
65
|
+
try {
|
|
66
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
67
|
+
fs.unlinkSync(lock);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (lockError) {
|
|
72
|
+
if (lockError.code === 'ENOENT')
|
|
73
|
+
continue;
|
|
74
|
+
throw lockError;
|
|
75
|
+
}
|
|
76
|
+
if (Date.now() >= deadline)
|
|
77
|
+
throw new Error('Codex lifecycle store is busy');
|
|
78
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return operation();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
fs.closeSync(descriptor);
|
|
86
|
+
fs.unlinkSync(lock);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function findByChild(childThreadId) {
|
|
90
|
+
return recordFiles(root).filter((file) => {
|
|
91
|
+
const child = readRecord(file).state.child;
|
|
92
|
+
return child?.childThreadId === childThreadId;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function locate(event) {
|
|
96
|
+
if (event.type === 'hook-spawn') {
|
|
97
|
+
return path.join(root, `${digest(`${event.sessionId}\0${event.turnId}\0${event.toolUseId}`)}.json`);
|
|
98
|
+
}
|
|
99
|
+
if (event.type === 'hook-child') {
|
|
100
|
+
const matches = recordFiles(root).filter((file) => {
|
|
101
|
+
const state = readRecord(file).state;
|
|
102
|
+
return state.spawn?.sessionId === event.parentThreadId
|
|
103
|
+
&& state.spawn.nativeAgent === event.nativeAgent
|
|
104
|
+
&& (!state.child || state.child.childThreadId === event.childThreadId);
|
|
105
|
+
});
|
|
106
|
+
if (matches.length !== 1) {
|
|
107
|
+
throw new Error(matches.length === 0
|
|
108
|
+
? 'Codex lifecycle parent session and agent correlation was not found'
|
|
109
|
+
: 'Codex lifecycle parent session and agent correlation is ambiguous');
|
|
110
|
+
}
|
|
111
|
+
return matches[0];
|
|
112
|
+
}
|
|
113
|
+
const matches = findByChild(event.childThreadId);
|
|
114
|
+
if (matches.length !== 1) {
|
|
115
|
+
throw new Error(matches.length === 0
|
|
116
|
+
? `Codex lifecycle child '${event.childThreadId}' was not found`
|
|
117
|
+
: `Codex lifecycle child '${event.childThreadId}' is ambiguous`);
|
|
118
|
+
}
|
|
119
|
+
return matches[0];
|
|
120
|
+
}
|
|
121
|
+
function apply(event) {
|
|
122
|
+
return withWriteLock(() => {
|
|
123
|
+
const file = locate(event);
|
|
124
|
+
const current = fs.existsSync(file)
|
|
125
|
+
? readRecord(file)
|
|
126
|
+
: Object.freeze({
|
|
127
|
+
schemaVersion: 1,
|
|
128
|
+
revision: 0,
|
|
129
|
+
state: createCodexLifecycleState(options.cliVersion),
|
|
130
|
+
consumer: null,
|
|
131
|
+
consumedAt: null,
|
|
132
|
+
updatedAt: now()
|
|
133
|
+
});
|
|
134
|
+
if (current.consumer)
|
|
135
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
136
|
+
const nextState = reduceCodexLifecycleEvent(current.state, event);
|
|
137
|
+
const next = Object.freeze({
|
|
138
|
+
schemaVersion: 1,
|
|
139
|
+
revision: current.revision + 1,
|
|
140
|
+
state: nextState,
|
|
141
|
+
consumer: null,
|
|
142
|
+
consumedAt: null,
|
|
143
|
+
updatedAt: now()
|
|
144
|
+
});
|
|
145
|
+
writeRecord(file, next, current.revision);
|
|
146
|
+
return Object.freeze({ path: file, revision: next.revision, state: next.state });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function applyToSpawn(identity, event) {
|
|
150
|
+
return withWriteLock(() => {
|
|
151
|
+
const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
|
|
152
|
+
if (!fs.existsSync(file))
|
|
153
|
+
throw new Error('Codex lifecycle spawn identity was not found');
|
|
154
|
+
const current = readRecord(file);
|
|
155
|
+
if (current.consumer)
|
|
156
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
157
|
+
if (current.state.spawn?.sessionId !== identity.sessionId
|
|
158
|
+
|| current.state.spawn.turnId !== identity.turnId
|
|
159
|
+
|| current.state.spawn.toolUseId !== identity.toolUseId)
|
|
160
|
+
throw new Error('Codex lifecycle spawn identity does not match the stored event');
|
|
161
|
+
const nextState = reduceCodexLifecycleEvent(current.state, event);
|
|
162
|
+
const next = Object.freeze({
|
|
163
|
+
...current,
|
|
164
|
+
revision: current.revision + 1,
|
|
165
|
+
state: nextState,
|
|
166
|
+
updatedAt: now()
|
|
167
|
+
});
|
|
168
|
+
writeRecord(file, next, current.revision);
|
|
169
|
+
return Object.freeze({ path: file, revision: next.revision, state: next.state });
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function findByParent(parentThreadId) {
|
|
173
|
+
const matches = recordFiles(root)
|
|
174
|
+
.map(readRecord)
|
|
175
|
+
.filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
|
|
176
|
+
&& ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
|
|
177
|
+
const unconsumed = matches.filter((record) => !record.consumer);
|
|
178
|
+
if (unconsumed.length > 1)
|
|
179
|
+
throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
|
|
180
|
+
if (unconsumed.length === 1)
|
|
181
|
+
return Object.freeze(unconsumed);
|
|
182
|
+
return Object.freeze(matches
|
|
183
|
+
.filter((record) => record.consumer && record.state.status === 'stop-ready')
|
|
184
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
|
|
185
|
+
}
|
|
186
|
+
function read(childThreadId) {
|
|
187
|
+
const matches = findByChild(childThreadId);
|
|
188
|
+
if (matches.length !== 1)
|
|
189
|
+
throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
190
|
+
return readRecord(matches[0]);
|
|
191
|
+
}
|
|
192
|
+
function consume(childThreadId, consumer, expectedHookDefinitionHash) {
|
|
193
|
+
if (!consumer.trim())
|
|
194
|
+
throw new Error('Codex lifecycle consumer is required');
|
|
195
|
+
return withWriteLock(() => {
|
|
196
|
+
const matches = findByChild(childThreadId);
|
|
197
|
+
if (matches.length !== 1)
|
|
198
|
+
throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
199
|
+
const file = matches[0];
|
|
200
|
+
const current = readRecord(file);
|
|
201
|
+
if (current.consumer) {
|
|
202
|
+
if (current.consumer !== consumer) {
|
|
203
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
204
|
+
}
|
|
205
|
+
if (expectedHookDefinitionHash
|
|
206
|
+
&& current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
|
|
207
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
208
|
+
return current;
|
|
209
|
+
}
|
|
210
|
+
if (current.state.status !== 'stop-ready')
|
|
211
|
+
throw new Error('Codex lifecycle evidence is not stop-ready');
|
|
212
|
+
if (expectedHookDefinitionHash
|
|
213
|
+
&& current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
|
|
214
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
215
|
+
const next = Object.freeze({
|
|
216
|
+
...current,
|
|
217
|
+
revision: current.revision + 1,
|
|
218
|
+
consumer,
|
|
219
|
+
consumedAt: now(),
|
|
220
|
+
updatedAt: now()
|
|
221
|
+
});
|
|
222
|
+
writeRecord(file, next, current.revision);
|
|
223
|
+
return next;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function expireBefore(cutoff) {
|
|
227
|
+
return withWriteLock(() => {
|
|
228
|
+
let changed = 0;
|
|
229
|
+
for (const file of recordFiles(root)) {
|
|
230
|
+
const current = readRecord(file);
|
|
231
|
+
if (current.updatedAt >= cutoff)
|
|
232
|
+
continue;
|
|
233
|
+
if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
|
|
234
|
+
fs.unlinkSync(file);
|
|
235
|
+
changed += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const expiredState = expireCodexLifecycleState(current.state);
|
|
239
|
+
if (expiredState === current.state)
|
|
240
|
+
continue;
|
|
241
|
+
writeRecord(file, Object.freeze({
|
|
242
|
+
...current,
|
|
243
|
+
revision: current.revision + 1,
|
|
244
|
+
state: expiredState,
|
|
245
|
+
updatedAt: now()
|
|
246
|
+
}), current.revision);
|
|
247
|
+
changed += 1;
|
|
248
|
+
}
|
|
249
|
+
return changed;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
|
|
253
|
+
}
|
|
254
|
+
export { createCodexLifecycleStore };
|
|
255
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -11,7 +11,7 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
11
11
|
commands: { level: 'integrated' },
|
|
12
12
|
hooks: { level: 'integrated' },
|
|
13
13
|
subagents: { level: 'experimental' },
|
|
14
|
-
orchestration: { level: '
|
|
14
|
+
orchestration: { level: 'experimental' },
|
|
15
15
|
sandbox: { level: 'integrated' },
|
|
16
16
|
verification: { level: 'compatible' }
|
|
17
17
|
},
|
|
@@ -21,8 +21,8 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
21
21
|
guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
|
|
22
22
|
},
|
|
23
23
|
delegationEvidence: {
|
|
24
|
-
actualModel: '
|
|
25
|
-
actualReasoningEffort: '
|
|
24
|
+
actualModel: 'app-server',
|
|
25
|
+
actualReasoningEffort: 'app-server'
|
|
26
26
|
},
|
|
27
27
|
project: {
|
|
28
28
|
ownedPathPrefixes: ['.codex/'],
|
package/dist/lib/git/workflow.js
CHANGED
|
@@ -119,7 +119,6 @@ function pushRebasedBranch(input, runner = defaultRunner) {
|
|
|
119
119
|
.map((name) => value(runner, input.cwd, ['rev-parse', '--git-path', name]))
|
|
120
120
|
.filter((candidate) => Boolean(candidate));
|
|
121
121
|
if (snapshot.worktree.length > 0 || snapshot.staged.length > 0 ||
|
|
122
|
-
run(runner, input.cwd, ['rev-parse', '-q', '--verify', 'REBASE_HEAD']).status === 0 ||
|
|
123
122
|
rebasePaths.some((candidate) => fs.existsSync(path.isAbsolute(candidate) ? candidate : path.join(input.cwd, candidate)))) {
|
|
124
123
|
return { status: 'failed', changed: false, snapshot, operations: [], error: { code: 'GIT_LOCAL_STATE_UNSAFE', message: 'Working tree must be clean with no rebase in progress' } };
|
|
125
124
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function normalizeCanonicalPath(value, platform = process.platform) {
|
|
5
|
+
return platform === 'win32'
|
|
6
|
+
? path.win32.normalize(value).replaceAll('/', '\\').toLowerCase()
|
|
7
|
+
: path.posix.normalize(value);
|
|
8
|
+
}
|
|
9
|
+
function sameFilesystemEntry(left, right, platform = process.platform) {
|
|
10
|
+
const leftReal = fs.realpathSync.native(left);
|
|
11
|
+
const rightReal = fs.realpathSync.native(right);
|
|
12
|
+
const leftStat = fs.statSync(leftReal, { bigint: true });
|
|
13
|
+
const rightStat = fs.statSync(rightReal, { bigint: true });
|
|
14
|
+
if (leftStat.ino !== 0n && rightStat.ino !== 0n) {
|
|
15
|
+
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
16
|
+
}
|
|
17
|
+
return normalizeCanonicalPath(leftReal, platform) === normalizeCanonicalPath(rightReal, platform);
|
|
18
|
+
}
|
|
19
|
+
function git(root, args) {
|
|
20
|
+
return execFileSync('git', args, {
|
|
21
|
+
cwd: root,
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
24
|
+
}).trim();
|
|
25
|
+
}
|
|
26
|
+
function resolveGitWorktreeIdentity(worktreeRoot) {
|
|
27
|
+
try {
|
|
28
|
+
const canonical = fs.realpathSync.native(worktreeRoot);
|
|
29
|
+
const topLevel = fs.realpathSync.native(git(canonical, ['rev-parse', '--show-toplevel']));
|
|
30
|
+
if (!sameFilesystemEntry(canonical, topLevel)) {
|
|
31
|
+
throw new Error('worktree root is not the Git top-level directory');
|
|
32
|
+
}
|
|
33
|
+
const commonDir = fs.realpathSync.native(git(canonical, [
|
|
34
|
+
'rev-parse', '--path-format=absolute', '--git-common-dir'
|
|
35
|
+
]));
|
|
36
|
+
const branch = git(canonical, ['symbolic-ref', '--short', 'HEAD']);
|
|
37
|
+
return { worktreeRoot: canonical, commonDir, branch };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: ${message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertGitWorktreeBinding(stateRoot, worktreeRoot, expectedBranch) {
|
|
45
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
46
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
47
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
48
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
49
|
+
}
|
|
50
|
+
if (worktree.branch !== expectedBranch) {
|
|
51
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: expected branch '${expectedBranch}', got '${worktree.branch}'`);
|
|
52
|
+
}
|
|
53
|
+
return worktree;
|
|
54
|
+
}
|
|
55
|
+
function assertGitRepositoryBinding(stateRoot, worktreeRoot) {
|
|
56
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
57
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
58
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
59
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
60
|
+
}
|
|
61
|
+
return worktree;
|
|
62
|
+
}
|
|
63
|
+
export { assertGitWorktreeBinding, assertGitRepositoryBinding, normalizeCanonicalPath, resolveGitWorktreeIdentity, sameFilesystemEntry };
|
|
64
|
+
//# sourceMappingURL=worktree-identity.js.map
|