@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,86 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
type GitWorktreeIdentity = Readonly<{
|
|
6
|
+
worktreeRoot: string;
|
|
7
|
+
commonDir: string;
|
|
8
|
+
branch: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
function normalizeCanonicalPath(value: string, platform: NodeJS.Platform = process.platform): string {
|
|
12
|
+
return platform === 'win32'
|
|
13
|
+
? path.win32.normalize(value).replaceAll('/', '\\').toLowerCase()
|
|
14
|
+
: path.posix.normalize(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sameFilesystemEntry(left: string, right: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
18
|
+
const leftReal = fs.realpathSync.native(left);
|
|
19
|
+
const rightReal = fs.realpathSync.native(right);
|
|
20
|
+
const leftStat = fs.statSync(leftReal, { bigint: true });
|
|
21
|
+
const rightStat = fs.statSync(rightReal, { bigint: true });
|
|
22
|
+
if (leftStat.ino !== 0n && rightStat.ino !== 0n) {
|
|
23
|
+
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
24
|
+
}
|
|
25
|
+
return normalizeCanonicalPath(leftReal, platform) === normalizeCanonicalPath(rightReal, platform);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function git(root: string, args: string[]): string {
|
|
29
|
+
return execFileSync('git', args, {
|
|
30
|
+
cwd: root,
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveGitWorktreeIdentity(worktreeRoot: string): GitWorktreeIdentity {
|
|
37
|
+
try {
|
|
38
|
+
const canonical = fs.realpathSync.native(worktreeRoot);
|
|
39
|
+
const topLevel = fs.realpathSync.native(git(canonical, ['rev-parse', '--show-toplevel']));
|
|
40
|
+
if (!sameFilesystemEntry(canonical, topLevel)) {
|
|
41
|
+
throw new Error('worktree root is not the Git top-level directory');
|
|
42
|
+
}
|
|
43
|
+
const commonDir = fs.realpathSync.native(git(canonical, [
|
|
44
|
+
'rev-parse', '--path-format=absolute', '--git-common-dir'
|
|
45
|
+
]));
|
|
46
|
+
const branch = git(canonical, ['symbolic-ref', '--short', 'HEAD']);
|
|
47
|
+
return { worktreeRoot: canonical, commonDir, branch };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertGitWorktreeBinding(
|
|
55
|
+
stateRoot: string,
|
|
56
|
+
worktreeRoot: string,
|
|
57
|
+
expectedBranch: string
|
|
58
|
+
): GitWorktreeIdentity {
|
|
59
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
60
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
61
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
62
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
63
|
+
}
|
|
64
|
+
if (worktree.branch !== expectedBranch) {
|
|
65
|
+
throw new Error(`SANDBOX_CONTROL_WORKTREE_INVALID: expected branch '${expectedBranch}', got '${worktree.branch}'`);
|
|
66
|
+
}
|
|
67
|
+
return worktree;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertGitRepositoryBinding(stateRoot: string, worktreeRoot: string): GitWorktreeIdentity {
|
|
71
|
+
const state = resolveGitWorktreeIdentity(stateRoot);
|
|
72
|
+
const worktree = resolveGitWorktreeIdentity(worktreeRoot);
|
|
73
|
+
if (!sameFilesystemEntry(state.commonDir, worktree.commonDir)) {
|
|
74
|
+
throw new Error('SANDBOX_CONTROL_WORKTREE_INVALID: state and worktree roots have different Git common directories');
|
|
75
|
+
}
|
|
76
|
+
return worktree;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
assertGitWorktreeBinding,
|
|
81
|
+
assertGitRepositoryBinding,
|
|
82
|
+
normalizeCanonicalPath,
|
|
83
|
+
resolveGitWorktreeIdentity,
|
|
84
|
+
sameFilesystemEntry
|
|
85
|
+
};
|
|
86
|
+
export type { GitWorktreeIdentity };
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
preflightCodexLifecycleEvidence,
|
|
8
|
+
resolveCodexTerminal,
|
|
9
|
+
resolveCodexThread
|
|
10
|
+
} from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
|
|
11
|
+
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
12
|
+
import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
|
|
13
|
+
import {
|
|
14
|
+
activateCodexOrchestrationDelegation,
|
|
15
|
+
activateCodexSpawnDelegation,
|
|
16
|
+
reconcileCodexOrchestrationDelegation,
|
|
17
|
+
sealCodexOrchestrationDelegation,
|
|
18
|
+
sealCodexParentDelegation
|
|
19
|
+
} from '../task/codex-orchestration.ts';
|
|
20
|
+
|
|
21
|
+
const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
|
|
22
|
+
const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
|
|
23
|
+
|
|
24
|
+
type Parsed = Readonly<{ operation: string; values: Readonly<Record<string, string>> }>;
|
|
25
|
+
type UnresolvedHookChild = Omit<Extract<CodexLifecycleEvent, { type: 'hook-child' }>, 'parentThreadId'>;
|
|
26
|
+
|
|
27
|
+
function output(value: unknown): void {
|
|
28
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function failure(code: string, message: string): void {
|
|
32
|
+
output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function outputBridgeResult(result: Awaited<ReturnType<typeof activateCodexOrchestrationDelegation>>): void {
|
|
37
|
+
if (result.error?.code === 'ORCHESTRATION_DELEGATION_MISSING') {
|
|
38
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
output(result);
|
|
42
|
+
if (result.status !== 'running') process.exitCode = 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parse(args: string[]): Parsed | null {
|
|
46
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
47
|
+
process.stdout.write(USAGE);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const operation = args[0];
|
|
51
|
+
if (!operation) {
|
|
52
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'operation is required');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const allowed = {
|
|
56
|
+
'hook-event': ['--event', '--bridge'],
|
|
57
|
+
'resolve-start': ['--child-id'],
|
|
58
|
+
'resolve-stop': ['--child-id'],
|
|
59
|
+
preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
|
|
60
|
+
consume: ['--child-id', '--consumer']
|
|
61
|
+
}[operation];
|
|
62
|
+
if (!allowed) {
|
|
63
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'unknown operation');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const values: Record<string, string> = {};
|
|
67
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
68
|
+
const flag = args[index]!;
|
|
69
|
+
if (!allowed.includes(flag) || Object.hasOwn(values, flag)) {
|
|
70
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const value = args[++index];
|
|
74
|
+
if (!value || value.startsWith('--')) {
|
|
75
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `option '${flag}' requires a value`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
values[flag] = value;
|
|
79
|
+
}
|
|
80
|
+
return Object.freeze({ operation, values: Object.freeze(values) });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cliVersion(): string {
|
|
84
|
+
const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
85
|
+
const version = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(result.stdout ?? '')?.[1];
|
|
86
|
+
if (result.status !== 0 || !version) throw new Error('Codex CLI version is unavailable');
|
|
87
|
+
return version;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hookDefinitionHash(): string {
|
|
91
|
+
const file = path.join(process.cwd(), '.codex', 'hooks.json');
|
|
92
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readStdin(): Promise<unknown> {
|
|
96
|
+
let input = '';
|
|
97
|
+
process.stdin.setEncoding('utf8');
|
|
98
|
+
for await (const chunk of process.stdin) {
|
|
99
|
+
input += chunk;
|
|
100
|
+
if (Buffer.byteLength(input) > 64 * 1024) throw new Error('Codex lifecycle input exceeds 64 KiB');
|
|
101
|
+
}
|
|
102
|
+
return JSON.parse(input || '{}') as unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent | UnresolvedHookChild | null {
|
|
106
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
107
|
+
throw new Error('Codex lifecycle hook payload must be an object');
|
|
108
|
+
}
|
|
109
|
+
const value = payload as Record<string, unknown>;
|
|
110
|
+
const text = (key: string) => typeof value[key] === 'string' ? value[key] as string : '';
|
|
111
|
+
if (!MANAGED_AGENT.test(text('nativeAgent'))) return null;
|
|
112
|
+
if (phase === 'pre-tool') return {
|
|
113
|
+
type: 'hook-spawn',
|
|
114
|
+
sessionId: text('sessionId'),
|
|
115
|
+
turnId: text('turnId'),
|
|
116
|
+
toolUseId: text('toolUseId'),
|
|
117
|
+
nativeAgent: text('nativeAgent'),
|
|
118
|
+
...(text('requestedModel') ? { requestedModel: text('requestedModel') } : {}),
|
|
119
|
+
...(text('requestedReasoningEffort') ? { requestedReasoningEffort: text('requestedReasoningEffort') } : {}),
|
|
120
|
+
hookDefinitionHash: text('hookDefinitionHash')
|
|
121
|
+
};
|
|
122
|
+
if (phase === 'subagent-start') return {
|
|
123
|
+
type: 'hook-child',
|
|
124
|
+
sessionId: text('sessionId'),
|
|
125
|
+
turnId: text('turnId'),
|
|
126
|
+
childThreadId: text('childThreadId'),
|
|
127
|
+
nativeAgent: text('nativeAgent')
|
|
128
|
+
};
|
|
129
|
+
if (phase === 'subagent-stop') return {
|
|
130
|
+
type: 'hook-stop',
|
|
131
|
+
sessionId: text('sessionId'),
|
|
132
|
+
turnId: text('turnId'),
|
|
133
|
+
childThreadId: text('childThreadId'),
|
|
134
|
+
nativeAgent: text('nativeAgent')
|
|
135
|
+
};
|
|
136
|
+
if (phase === 'post-tool') return null;
|
|
137
|
+
throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function payloadText(payload: unknown, key: string): string {
|
|
141
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
142
|
+
&& typeof (payload as Record<string, unknown>)[key] === 'string'
|
|
143
|
+
? (payload as Record<string, string>)[key]!
|
|
144
|
+
: '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function codexLifecycle(args: string[] = []): Promise<void> {
|
|
148
|
+
const parsed = parse(args);
|
|
149
|
+
if (!parsed || process.exitCode) return;
|
|
150
|
+
try {
|
|
151
|
+
if (parsed.operation === 'preflight') {
|
|
152
|
+
const format = parsed.values['--format'] ?? 'json';
|
|
153
|
+
if (!['text', 'json'].includes(format)) throw new Error("format must be 'text' or 'json'");
|
|
154
|
+
const runtimeFields = [
|
|
155
|
+
parsed.values['--session-id'],
|
|
156
|
+
parsed.values['--turn-id'],
|
|
157
|
+
parsed.values['--tool-use-id']
|
|
158
|
+
];
|
|
159
|
+
if (runtimeFields.some(Boolean) && !runtimeFields.every(Boolean)) {
|
|
160
|
+
throw new Error('preflight runtime identity requires --session-id, --turn-id, and --tool-use-id together');
|
|
161
|
+
}
|
|
162
|
+
const runtimeIdentity = runtimeFields.every(Boolean) ? {
|
|
163
|
+
sessionId: runtimeFields[0]!,
|
|
164
|
+
turnId: runtimeFields[1]!,
|
|
165
|
+
toolUseId: runtimeFields[2]!
|
|
166
|
+
} : undefined;
|
|
167
|
+
const result = await preflightCodexLifecycleEvidence(process.cwd(), runtimeIdentity);
|
|
168
|
+
if (format === 'text') {
|
|
169
|
+
process.stdout.write(`Codex lifecycle static preflight: ready\nCLI: ${result.cliVersion}\nRuntime hook liveness: ${result.runtimeLiveness ? 'observed' : 'not-yet-observed'}\n`);
|
|
170
|
+
} else output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
|
|
175
|
+
if (parsed.operation === 'hook-event') {
|
|
176
|
+
const phase = parsed.values['--event'];
|
|
177
|
+
if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
|
|
178
|
+
throw new Error('hook-event requires a known --event');
|
|
179
|
+
}
|
|
180
|
+
if (parsed.values['--bridge'] !== undefined && parsed.values['--bridge'] !== 'true') {
|
|
181
|
+
throw new Error("hook-event --bridge must be 'true'");
|
|
182
|
+
}
|
|
183
|
+
const payload = await readStdin();
|
|
184
|
+
if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
|
|
185
|
+
const toolName = payloadText(payload, 'toolName');
|
|
186
|
+
if (toolName === 'collaborationspawn_agent') {
|
|
187
|
+
const nativeAgent = payloadText(payload, 'nativeAgent');
|
|
188
|
+
if (!MANAGED_AGENT.test(nativeAgent)) {
|
|
189
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const requestedModel = payloadText(payload, 'requestedModel');
|
|
193
|
+
const requestedReasoningEffort = payloadText(payload, 'requestedReasoningEffort');
|
|
194
|
+
const bridged = await activateCodexSpawnDelegation({
|
|
195
|
+
sessionId: payloadText(payload, 'sessionId'),
|
|
196
|
+
turnId: payloadText(payload, 'turnId'),
|
|
197
|
+
toolUseId: payloadText(payload, 'toolUseId'),
|
|
198
|
+
transcriptPath: payloadText(payload, 'transcriptPath'),
|
|
199
|
+
nativeAgent,
|
|
200
|
+
taskName: payloadText(payload, 'taskName'),
|
|
201
|
+
...(requestedModel ? { requestedModel } : {}),
|
|
202
|
+
...(requestedReasoningEffort ? { requestedReasoningEffort } : {})
|
|
203
|
+
}, { store });
|
|
204
|
+
outputBridgeResult(bridged);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (toolName === 'collaborationwait_agent') {
|
|
208
|
+
outputBridgeResult(await sealCodexParentDelegation(payloadText(payload, 'sessionId'), { store }));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const event = recordFromPayload(phase, payload);
|
|
215
|
+
if (!event) {
|
|
216
|
+
if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
|
|
217
|
+
const value = payload as Record<string, unknown>;
|
|
218
|
+
const childThreadId = String(value.childThreadId ?? '');
|
|
219
|
+
const reconciled = reconcileCodexOrchestrationDelegation(childThreadId);
|
|
220
|
+
outputBridgeResult(reconciled);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
|
|
227
|
+
try {
|
|
228
|
+
if (store.read(event.childThreadId).consumer) {
|
|
229
|
+
const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
|
|
230
|
+
outputBridgeResult(bridged);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// The normal apply path below owns missing or invalid evidence errors.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (parsed.values['--bridge'] === 'true' && event.type === 'hook-child') {
|
|
238
|
+
const resolved = await resolveCodexThread(event.childThreadId);
|
|
239
|
+
store.apply({
|
|
240
|
+
...event,
|
|
241
|
+
parentThreadId: resolved.resolution.thread.parentThreadId
|
|
242
|
+
});
|
|
243
|
+
const bridged = await activateCodexOrchestrationDelegation(event.childThreadId, {
|
|
244
|
+
store,
|
|
245
|
+
resolveThread: async () => resolved
|
|
246
|
+
});
|
|
247
|
+
outputBridgeResult(bridged);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (event.type === 'hook-child') {
|
|
251
|
+
const resolved = await resolveCodexThread(event.childThreadId);
|
|
252
|
+
const result = store.apply({
|
|
253
|
+
...event,
|
|
254
|
+
parentThreadId: resolved.resolution.thread.parentThreadId
|
|
255
|
+
});
|
|
256
|
+
output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: resolved.diagnostics, error: result.state.error });
|
|
257
|
+
if (result.state.status === 'invalid') process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const result = store.apply(event);
|
|
261
|
+
if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
|
|
262
|
+
const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
|
|
263
|
+
outputBridgeResult(bridged);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
|
|
267
|
+
if (result.state.status === 'invalid') process.exitCode = 1;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const childThreadId = parsed.values['--child-id'];
|
|
272
|
+
if (!childThreadId) throw new Error(`operation '${parsed.operation}' requires --child-id`);
|
|
273
|
+
if (parsed.operation === 'resolve-start') {
|
|
274
|
+
if (store.read(childThreadId).state.spawn?.hookDefinitionHash !== hookDefinitionHash()) {
|
|
275
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
276
|
+
}
|
|
277
|
+
const resolved = await resolveCodexThread(childThreadId);
|
|
278
|
+
let latest = store.apply(resolved.resolution.thread);
|
|
279
|
+
for (const reroute of resolved.reroutes) latest = store.apply(reroute);
|
|
280
|
+
latest = store.apply(resolved.resolution.settings);
|
|
281
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.startEvidence, diagnostics: resolved.diagnostics, error: latest.state.error });
|
|
282
|
+
if (latest.state.status !== 'start-ready') process.exitCode = 1;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (parsed.operation === 'resolve-stop') {
|
|
286
|
+
const terminal = await resolveCodexTerminal(childThreadId);
|
|
287
|
+
const latest = store.apply(terminal);
|
|
288
|
+
if (latest.state.status !== 'stop-ready') {
|
|
289
|
+
throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
|
|
290
|
+
}
|
|
291
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.stopEvidence, diagnostics: [], error: latest.state.error });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (parsed.operation === 'consume') {
|
|
295
|
+
const consumer = parsed.values['--consumer'];
|
|
296
|
+
if (!consumer) throw new Error('consume requires --consumer');
|
|
297
|
+
const consumed = store.consume(childThreadId, consumer, hookDefinitionHash());
|
|
298
|
+
output({ status: 'consumed', changed: true, evidence: {
|
|
299
|
+
start: consumed.state.startEvidence,
|
|
300
|
+
stop: consumed.state.stopEvidence
|
|
301
|
+
}, diagnostics: [], error: null });
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
failure('CODEX_LIFECYCLE_FAILED', error instanceof Error ? error.message : String(error));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export { codexLifecycle };
|
|
@@ -8,7 +8,7 @@ import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/wor
|
|
|
8
8
|
import { inspectPlatformRelease, reconcileReleaseMilestones } from '../platform/releases.ts';
|
|
9
9
|
import { inspectHomebrewChannel, inspectNpmChannel } from '../release/channels.ts';
|
|
10
10
|
import { releaseSnapshot } from '../release/workflow.ts';
|
|
11
|
-
import type { ReleaseFacts } from '../release/workflow.ts';
|
|
11
|
+
import type { PostReleaseFacts, ReleaseFacts } from '../release/workflow.ts';
|
|
12
12
|
|
|
13
13
|
function command(cwd: string, executable: string, args: string[]) {
|
|
14
14
|
return spawnSync(executable, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -168,12 +168,52 @@ function inspectLocalReleaseFacts(cwd: string, version: string, run: CommandRunn
|
|
|
168
168
|
const localTagConflict = Boolean(tagCommit && !localTag && !localTagAncestor);
|
|
169
169
|
const postSubject = `chore: prepare next dev iteration after v${version}`;
|
|
170
170
|
const postLog = localTag || localTagAncestor
|
|
171
|
-
? git(cwd, ['log', '--format=%s', `${tag}..HEAD`], run) ?? ''
|
|
171
|
+
? git(cwd, ['log', '--format=%H%x00%s', `${tag}..HEAD`], run) ?? ''
|
|
172
172
|
: '';
|
|
173
|
-
const postCommit = postLog.split('\n').
|
|
173
|
+
const postCommit = postLog.split('\n').map((line) => line.split('\0'))
|
|
174
|
+
.find(([, subject]) => subject === postSubject)?.[0] ?? null;
|
|
174
175
|
return { localTag, localTagAncestor, localTagConflict, postCommit };
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
function inspectPostReleaseFacts(
|
|
179
|
+
cwd: string,
|
|
180
|
+
commit: string | null,
|
|
181
|
+
run: CommandRunner = command
|
|
182
|
+
): PostReleaseFacts {
|
|
183
|
+
const inspected = inspectGitWorkflow(cwd);
|
|
184
|
+
const snapshot = inspected.snapshot;
|
|
185
|
+
const branch = snapshot?.branch ?? '';
|
|
186
|
+
const remoteLine = branch ? git(cwd, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`], run) : null;
|
|
187
|
+
const remoteHead = remoteLine?.split(/\s+/)[0] ?? null;
|
|
188
|
+
const packageJson = commit ? git(cwd, ['show', `${commit}:package.json`], run, false) : null;
|
|
189
|
+
let newVersion: string | null = null;
|
|
190
|
+
if (packageJson) {
|
|
191
|
+
try {
|
|
192
|
+
const version = (JSON.parse(packageJson) as { version?: unknown }).version;
|
|
193
|
+
newVersion = typeof version === 'string' ? version : null;
|
|
194
|
+
} catch {
|
|
195
|
+
newVersion = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const paths = commit ? git(cwd, ['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', '-z', commit], run, false) : null;
|
|
199
|
+
const changedPaths = paths ? paths.split('\0').filter(Boolean)
|
|
200
|
+
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right))) : [];
|
|
201
|
+
const digest = commit ? git(cwd, ['show', `${commit}:${DEMO_DIGEST_PATH}`], run) : null;
|
|
202
|
+
return {
|
|
203
|
+
commit,
|
|
204
|
+
isHead: Boolean(commit && snapshot?.head === commit),
|
|
205
|
+
published: Boolean(commit && remoteHead === commit),
|
|
206
|
+
branch,
|
|
207
|
+
upstream: snapshot?.upstream ?? null,
|
|
208
|
+
remoteHead,
|
|
209
|
+
newVersion,
|
|
210
|
+
changedPaths,
|
|
211
|
+
demoInputSha256: digest && /^[0-9a-f]{64}$/.test(digest) ? digest : null,
|
|
212
|
+
worktree: snapshot?.worktree ?? [],
|
|
213
|
+
staged: snapshot?.staged ?? []
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
177
217
|
function releaseSmokeStatus(workflows: Array<Record<string, unknown>>, version: string, tagCommit: string | null): ReleaseFacts['smoke'] {
|
|
178
218
|
const manualTitle = `Post-Release Smoke v${version}`;
|
|
179
219
|
const targets = workflows.filter((run) => {
|
|
@@ -220,12 +260,15 @@ async function inspectFacts(cwd: string, version: string): Promise<ReleaseFacts>
|
|
|
220
260
|
const workflows = platform.workflows as Array<Record<string, unknown>>;
|
|
221
261
|
const smoke = releaseSmokeStatus(workflows, version, localTagTarget);
|
|
222
262
|
return {
|
|
223
|
-
|
|
263
|
+
localTag: local.localTag,
|
|
264
|
+
localTagAncestor: local.localTagAncestor,
|
|
265
|
+
localTagConflict: local.localTagConflict,
|
|
224
266
|
remoteBranch: Boolean(remoteBranch && remoteBranch.split(/\s+/)[0] === head), remoteTag: Boolean(remoteTag),
|
|
225
267
|
githubRelease: platform.platform.type !== 'github'
|
|
226
268
|
? true
|
|
227
269
|
: platform.status === 'blocked' ? null : Boolean(platform.release?.published),
|
|
228
|
-
npm: npm.published, homebrew: homebrew.published, smoke
|
|
270
|
+
npm: npm.published, homebrew: homebrew.published, smoke,
|
|
271
|
+
post: inspectPostReleaseFacts(cwd, local.postCommit)
|
|
229
272
|
};
|
|
230
273
|
}
|
|
231
274
|
|
|
@@ -258,13 +301,21 @@ function updateReleaseMetadata(cwd: string, version: string): void {
|
|
|
258
301
|
}
|
|
259
302
|
}
|
|
260
303
|
|
|
304
|
+
function optionValues(args: string[], name: string): string[] {
|
|
305
|
+
const values: string[] = [];
|
|
306
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
307
|
+
if (args[index] === name) values.push(args[index + 1] ?? '');
|
|
308
|
+
}
|
|
309
|
+
return values;
|
|
310
|
+
}
|
|
311
|
+
|
|
261
312
|
async function releaseWorkflow(args: string[] = []): Promise<void> {
|
|
262
313
|
const [action, rawVersion, ...rest] = args;
|
|
263
314
|
const version = String(rawVersion || '').replace(/^v/, '');
|
|
264
315
|
const cwdIndex = rest.indexOf('--cwd');
|
|
265
316
|
const cwd = path.resolve(cwdIndex >= 0 && rest[cwdIndex + 1] ? rest[cwdIndex + 1]! : process.cwd());
|
|
266
|
-
if (!['inspect', 'prepare', 'publish', 'post'].includes(action || '') || !semver.valid(version)) {
|
|
267
|
-
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post <version>' } })}\n`);
|
|
317
|
+
if (!['inspect', 'prepare', 'publish', 'post-prepare', 'post-publish'].includes(action || '') || !semver.valid(version)) {
|
|
318
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post-prepare|post-publish <version>' } })}\n`);
|
|
268
319
|
process.exitCode = 1; return;
|
|
269
320
|
}
|
|
270
321
|
const before = releaseSnapshot(version, await inspectFacts(cwd, version));
|
|
@@ -310,10 +361,51 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
|
|
|
310
361
|
const result = pushGitRefs({ cwd, remote: 'origin', refs });
|
|
311
362
|
process.stdout.write(`${JSON.stringify({ ...result, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`); process.exitCode = result.status === 'failed' ? 1 : result.status === 'degraded' ? 2 : 0; return;
|
|
312
363
|
}
|
|
364
|
+
if (action === 'post-publish') {
|
|
365
|
+
const expectedValues = optionValues(rest, '--expected-sha256');
|
|
366
|
+
if (expectedValues.length !== 1 || !/^sha256:[0-9a-f]{64}$/.test(expectedValues[0]!)) {
|
|
367
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_INPUT_INVALID', message: 'post-publish requires one --expected-sha256 sha256:<64 hex>' } })}\n`);
|
|
368
|
+
process.exitCode = 1; return;
|
|
369
|
+
}
|
|
370
|
+
if (before.phase === 'complete') {
|
|
371
|
+
process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
|
|
375
|
+
if (before.phase !== 'post-prepared' || !confirmation) {
|
|
376
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: confirmation ? 'RELEASE_PHASE_INVALID' : 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post-release facts cannot authorize publish' } })}\n`);
|
|
377
|
+
process.exitCode = 1; return;
|
|
378
|
+
}
|
|
379
|
+
if (confirmation.sha256 !== expectedValues[0]) {
|
|
380
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_POST_SNAPSHOT_MISMATCH', message: 'Post-release confirmation snapshot changed' } })}\n`);
|
|
381
|
+
process.exitCode = 1; return;
|
|
382
|
+
}
|
|
383
|
+
const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
|
|
384
|
+
if (!channelsComplete || before.facts.smoke !== 'success') {
|
|
385
|
+
process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`);
|
|
386
|
+
process.exitCode = before.facts.smoke === 'failed' ? 1 : 2; return;
|
|
387
|
+
}
|
|
388
|
+
const pushed = pushGitRefs({ cwd, remote: 'origin', refs: [before.facts.post.branch] });
|
|
389
|
+
const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
|
|
390
|
+
if (snapshot.phase === 'complete') {
|
|
391
|
+
process.stdout.write(`${JSON.stringify({ ...pushed, status: 'applied', changed: true, snapshot, error: null })}\n`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const blocked = pushed.status !== 'failed';
|
|
395
|
+
process.stdout.write(`${JSON.stringify({ ...pushed, status: blocked ? 'blocked' : 'failed', snapshot, error: pushed.error ?? { code: 'RELEASE_PROGRESS_PENDING', message: 'Post-release push is not visible at the expected remote head' } })}\n`);
|
|
396
|
+
process.exitCode = blocked ? 2 : 1; return;
|
|
397
|
+
}
|
|
398
|
+
|
|
313
399
|
if (before.phase === 'complete') {
|
|
314
400
|
process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
|
|
315
401
|
return;
|
|
316
402
|
}
|
|
403
|
+
if (before.phase === 'post-prepared') {
|
|
404
|
+
const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
|
|
405
|
+
const status = confirmation ? 'no-op' : 'blocked';
|
|
406
|
+
process.stdout.write(`${JSON.stringify({ status, changed: false, snapshot: before, error: confirmation ? null : { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post commit is not the clean current HEAD' } })}\n`);
|
|
407
|
+
process.exitCode = confirmation ? 0 : 2; return;
|
|
408
|
+
}
|
|
317
409
|
const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
|
|
318
410
|
if (!['published', 'post-pending'].includes(before.phase) || !channelsComplete || before.facts.smoke !== 'success') {
|
|
319
411
|
process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`); process.exitCode = before.facts.smoke === 'failed' ? 1 : 2; return;
|
|
@@ -338,10 +430,14 @@ async function releaseWorkflow(args: string[] = []): Promise<void> {
|
|
|
338
430
|
}
|
|
339
431
|
const committed = commitExplicitPaths({ cwd, paths: changedPaths(cwd), message: `chore: prepare next dev iteration after v${version}` });
|
|
340
432
|
if (committed.status === 'failed') { process.stdout.write(`${JSON.stringify(committed)}\n`); process.exitCode = 1; return; }
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
433
|
+
const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
|
|
434
|
+
const confirmation = 'postConfirmation' in snapshot ? snapshot.postConfirmation : null;
|
|
435
|
+
if (snapshot.phase !== 'post-prepared' || !confirmation) {
|
|
436
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: true, demo, snapshot, error: { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Prepared post commit did not produce a confirmation snapshot' } })}\n`);
|
|
437
|
+
process.exitCode = 1; return;
|
|
438
|
+
}
|
|
439
|
+
process.stdout.write(`${JSON.stringify({ status: 'applied', changed: true, demo, snapshot, error: null })}\n`);
|
|
344
440
|
}
|
|
345
441
|
|
|
346
|
-
export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
|
|
442
|
+
export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
|
|
347
443
|
export type { CommandRunner, DemoResult };
|
|
@@ -1,24 +1,56 @@
|
|
|
1
|
-
import { requestSandboxControl } from '../sandbox/control/client.ts';
|
|
1
|
+
import { requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.ts';
|
|
2
2
|
import { serveSandboxControl } from '../sandbox/control/server.ts';
|
|
3
|
+
import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
|
|
3
4
|
|
|
4
|
-
function sandboxControl(args: string[]): void {
|
|
5
|
+
async function sandboxControl(args: string[]): Promise<void> {
|
|
5
6
|
const [operation, ...rest] = args;
|
|
6
7
|
if (operation === 'serve') {
|
|
7
8
|
const manifestIndex = rest.indexOf('--manifest');
|
|
8
9
|
const manifest = manifestIndex >= 0 ? rest[manifestIndex + 1] : undefined;
|
|
9
10
|
if (!manifest) throw new Error('sandbox-control serve requires --manifest');
|
|
10
|
-
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const abort = () => controller.abort();
|
|
13
|
+
process.once('SIGINT', abort);
|
|
14
|
+
process.once('SIGTERM', abort);
|
|
15
|
+
try {
|
|
16
|
+
await serveSandboxControl(manifest, controller.signal);
|
|
17
|
+
} finally {
|
|
18
|
+
process.off('SIGINT', abort);
|
|
19
|
+
process.off('SIGTERM', abort);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (operation === 'execute') {
|
|
24
|
+
const requestIndex = rest.indexOf('--request');
|
|
25
|
+
const nonceIndex = rest.indexOf('--nonce');
|
|
26
|
+
const request = requestIndex >= 0 ? rest[requestIndex + 1] : undefined;
|
|
27
|
+
const nonce = nonceIndex >= 0 ? rest[nonceIndex + 1] : undefined;
|
|
28
|
+
if (!request || !nonce) throw new Error('sandbox-control execute requires --request and --nonce');
|
|
29
|
+
await runSandboxControlExecutor(request, nonce);
|
|
11
30
|
return;
|
|
12
31
|
}
|
|
13
32
|
if (operation === 'client') {
|
|
14
33
|
const [family = '', ...commandArgs] = rest;
|
|
15
|
-
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = requestSandboxControl({ family, args: commandArgs });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!(error instanceof SandboxControlClientError)) throw error;
|
|
39
|
+
process.stderr.write(`${error.detail.message}\n`);
|
|
40
|
+
process.exitCode = error.detail.retryable ? 75 : 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
16
43
|
process.stdout.write(response.stdout);
|
|
17
44
|
process.stderr.write(response.stderr);
|
|
18
|
-
|
|
45
|
+
if (response.phase === 'rejected') {
|
|
46
|
+
process.stderr.write(response.error?.message ?? response.stderr);
|
|
47
|
+
process.exitCode = response.error?.retryable ? 75 : 1;
|
|
48
|
+
} else {
|
|
49
|
+
process.exitCode = response.exitCode ?? 1;
|
|
50
|
+
}
|
|
19
51
|
return;
|
|
20
52
|
}
|
|
21
|
-
throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | client <family> [args...]');
|
|
53
|
+
throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | execute --request <path> --nonce <nonce> | client <family> [args...]');
|
|
22
54
|
}
|
|
23
55
|
|
|
24
56
|
export { sandboxControl };
|