@fitlab-ai/agent-infra 0.9.3 → 0.9.5

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.
Files changed (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -0,0 +1,163 @@
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.sessionId
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 && thread && child.childThreadId !== thread.childThreadId) {
43
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server child identity do not match');
44
+ }
45
+ if (spawn && thread && (thread.parentThreadId !== spawn.sessionId
46
+ || thread.sourceParentThreadId !== spawn.sessionId)) {
47
+ return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'App Server parent identity does not match the spawning session');
48
+ }
49
+ if (thread?.forkedFromId !== null && thread?.forkedFromId !== undefined) {
50
+ return invalid(state, 'CODEX_EVIDENCE_FORK_FORBIDDEN', 'forked child threads are not fresh lifecycle evidence');
51
+ }
52
+ if (settings && child && settings.childThreadId !== child.childThreadId) {
53
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'resolved settings belong to a different child');
54
+ }
55
+ if (reroute && child && reroute.childThreadId !== child.childThreadId) {
56
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'model reroute belongs to a different child');
57
+ }
58
+ if (spawn?.requestedModel && settings && spawn.requestedModel !== settings.model) {
59
+ if (!reroute
60
+ || reroute.fromModel !== spawn.requestedModel
61
+ || reroute.toModel !== settings.model
62
+ || !reroute.reason.trim()) {
63
+ return invalid(state, 'CODEX_EVIDENCE_MODEL_UNEXPLAINED', 'resolved model differs without a matching host reroute');
64
+ }
65
+ }
66
+ if (spawn?.requestedReasoningEffort
67
+ && settings
68
+ && spawn.requestedReasoningEffort !== settings.reasoningEffort) {
69
+ return invalid(state, 'CODEX_EVIDENCE_EFFORT_UNEXPLAINED', 'resolved reasoning effort differs without structured host evidence');
70
+ }
71
+ if (terminal && terminal.status !== 'completed') {
72
+ return invalid(state, 'CODEX_EVIDENCE_TERMINAL_INVALID', `child turn ended as '${terminal.status}'`);
73
+ }
74
+ if (terminal && child && terminal.childThreadId !== child.childThreadId) {
75
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'terminal event belongs to a different child');
76
+ }
77
+ if (stop && child && (stop.sessionId !== child.sessionId
78
+ || stop.turnId !== child.turnId
79
+ || stop.childThreadId !== child.childThreadId
80
+ || stop.nativeAgent !== child.nativeAgent)) {
81
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook stop identity does not match the child');
82
+ }
83
+ let startEvidence = null;
84
+ if (spawn && child && thread && settings) {
85
+ startEvidence = Object.freeze({
86
+ schemaVersion: 1,
87
+ cliVersion: state.cliVersion,
88
+ parentThreadId: spawn.sessionId,
89
+ parentTurnId: spawn.turnId,
90
+ spawnToolUseId: spawn.toolUseId,
91
+ childThreadId: child.childThreadId,
92
+ nativeAgent: child.nativeAgent,
93
+ spawnMode: 'fresh',
94
+ actualModel: Object.freeze({ value: settings.model, source: 'codex-app-server-settings' }),
95
+ actualReasoningEffort: Object.freeze({ value: settings.reasoningEffort, source: 'codex-app-server-settings' }),
96
+ modelFallbackReason: reroute?.reason ?? null,
97
+ reasoningEffortFallbackReason: null,
98
+ hookDefinitionHash: spawn.hookDefinitionHash
99
+ });
100
+ }
101
+ let stopEvidence = null;
102
+ if (startEvidence && terminal?.status === 'completed' && stop) {
103
+ stopEvidence = Object.freeze({
104
+ schemaVersion: 1,
105
+ childThreadId: startEvidence.childThreadId,
106
+ turnId: terminal.turnId,
107
+ terminalStatus: 'completed',
108
+ hookStopObserved: true
109
+ });
110
+ }
111
+ const status = stopEvidence
112
+ ? 'stop-ready'
113
+ : terminal
114
+ ? 'observed-terminal'
115
+ : startEvidence
116
+ ? 'start-ready'
117
+ : child
118
+ ? 'observed-child'
119
+ : spawn
120
+ ? 'observed-spawn'
121
+ : 'empty';
122
+ return Object.freeze({ ...state, status, startEvidence, stopEvidence, error: null });
123
+ }
124
+ function reduceCodexLifecycleEvent(state, event) {
125
+ if (state.status === 'invalid' || state.status === 'expired')
126
+ return state;
127
+ if (!requiredStrings(event)) {
128
+ return invalid(state, 'CODEX_EVIDENCE_FIELD_MISSING', 'lifecycle event contains an empty required field');
129
+ }
130
+ const key = {
131
+ 'hook-spawn': 'spawn',
132
+ 'hook-child': 'child',
133
+ 'app-thread': 'thread',
134
+ 'app-settings': 'settings',
135
+ 'app-reroute': 'reroute',
136
+ 'app-terminal': 'terminal',
137
+ 'hook-stop': 'stop'
138
+ }[event.type];
139
+ const existing = state[key];
140
+ if (existing) {
141
+ if (same(existing, event))
142
+ return state;
143
+ return invalid(state, 'CODEX_EVIDENCE_REPLAY_CONFLICT', `conflicting '${event.type}' event`);
144
+ }
145
+ return derive(Object.freeze({ ...state, [key]: Object.freeze({ ...event }) }));
146
+ }
147
+ function expireCodexLifecycleState(state) {
148
+ if (!['observed-spawn', 'observed-child', 'start-ready', 'observed-terminal'].includes(state.status)) {
149
+ return state;
150
+ }
151
+ return Object.freeze({
152
+ ...state,
153
+ status: 'expired',
154
+ startEvidence: null,
155
+ stopEvidence: null,
156
+ error: Object.freeze({
157
+ code: 'CODEX_EVIDENCE_EXPIRED',
158
+ message: 'Codex lifecycle evidence expired before completion'
159
+ })
160
+ });
161
+ }
162
+ export { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent };
163
+ //# sourceMappingURL=evidence.js.map
@@ -0,0 +1,211 @@
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.sessionId
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 read(childThreadId) {
150
+ const matches = findByChild(childThreadId);
151
+ if (matches.length !== 1)
152
+ throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
153
+ return readRecord(matches[0]);
154
+ }
155
+ function consume(childThreadId, consumer, expectedHookDefinitionHash) {
156
+ if (!consumer.trim())
157
+ throw new Error('Codex lifecycle consumer is required');
158
+ return withWriteLock(() => {
159
+ const matches = findByChild(childThreadId);
160
+ if (matches.length !== 1)
161
+ throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
162
+ const file = matches[0];
163
+ const current = readRecord(file);
164
+ if (current.consumer)
165
+ throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
166
+ if (current.state.status !== 'stop-ready')
167
+ throw new Error('Codex lifecycle evidence is not stop-ready');
168
+ if (expectedHookDefinitionHash
169
+ && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
170
+ throw new Error('Codex lifecycle hook definition hash is stale');
171
+ const next = Object.freeze({
172
+ ...current,
173
+ revision: current.revision + 1,
174
+ consumer,
175
+ consumedAt: now(),
176
+ updatedAt: now()
177
+ });
178
+ writeRecord(file, next, current.revision);
179
+ return next;
180
+ });
181
+ }
182
+ function expireBefore(cutoff) {
183
+ return withWriteLock(() => {
184
+ let changed = 0;
185
+ for (const file of recordFiles(root)) {
186
+ const current = readRecord(file);
187
+ if (current.updatedAt >= cutoff)
188
+ continue;
189
+ if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
190
+ fs.unlinkSync(file);
191
+ changed += 1;
192
+ continue;
193
+ }
194
+ const expiredState = expireCodexLifecycleState(current.state);
195
+ if (expiredState === current.state)
196
+ continue;
197
+ writeRecord(file, Object.freeze({
198
+ ...current,
199
+ revision: current.revision + 1,
200
+ state: expiredState,
201
+ updatedAt: now()
202
+ }), current.revision);
203
+ changed += 1;
204
+ }
205
+ return changed;
206
+ });
207
+ }
208
+ return Object.freeze({ root, apply, consume, expireBefore, read });
209
+ }
210
+ export { createCodexLifecycleStore };
211
+ //# sourceMappingURL=store.js.map
@@ -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: 'unavailable',
25
- actualReasoningEffort: 'unavailable'
24
+ actualModel: 'app-server',
25
+ actualReasoningEffort: 'app-server'
26
26
  },
27
27
  project: {
28
28
  ownedPathPrefixes: ['.codex/'],
@@ -0,0 +1,108 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ function readJsonObject(filePath) {
4
+ try {
5
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
6
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
7
+ ? parsed
8
+ : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ function resolveXdgRoot(value, fallback) {
15
+ return typeof value === 'string' && value !== '' && path.isAbsolute(value)
16
+ ? value
17
+ : fallback;
18
+ }
19
+ function ensureOpenCodeSandboxState(toolDir, hostHome, hostEnv = {}) {
20
+ const canonicalConfigDir = path.join(toolDir, '.xdg', 'config', 'opencode');
21
+ const canonicalStateDir = path.join(toolDir, '.xdg', 'state', 'opencode');
22
+ const canonicalConfigPath = path.join(canonicalConfigDir, 'opencode.json');
23
+ const legacyConfigPath = path.join(toolDir, 'opencode.json');
24
+ fs.mkdirSync(canonicalConfigDir, { recursive: true });
25
+ fs.mkdirSync(canonicalStateDir, { recursive: true });
26
+ if (!fs.existsSync(canonicalConfigPath) && fs.existsSync(legacyConfigPath)) {
27
+ fs.copyFileSync(legacyConfigPath, canonicalConfigPath, fs.constants.COPYFILE_EXCL);
28
+ }
29
+ const hostConfigRoot = resolveXdgRoot(hostEnv.XDG_CONFIG_HOME, path.join(hostHome, '.config'));
30
+ const hostConfig = readJsonObject(path.join(hostConfigRoot, 'opencode', 'opencode.json'));
31
+ if (!hostConfig) {
32
+ return;
33
+ }
34
+ let sandboxConfig = {};
35
+ if (fs.existsSync(canonicalConfigPath)) {
36
+ const existing = readJsonObject(canonicalConfigPath);
37
+ if (!existing) {
38
+ return;
39
+ }
40
+ sandboxConfig = existing;
41
+ }
42
+ let changed = false;
43
+ for (const key of ['model', 'small_model']) {
44
+ if (Object.hasOwn(sandboxConfig, key)) {
45
+ continue;
46
+ }
47
+ const value = hostConfig[key];
48
+ if (typeof value === 'string' && value !== '') {
49
+ sandboxConfig[key] = value;
50
+ changed = true;
51
+ }
52
+ }
53
+ if (changed) {
54
+ fs.writeFileSync(canonicalConfigPath, JSON.stringify(sandboxConfig, null, 2), 'utf8');
55
+ }
56
+ }
57
+ const opencodeBeforeContainerCreateHook = {
58
+ id: 'opencode-before-container-create',
59
+ phase: 'before-container-create',
60
+ async run(context) {
61
+ const create = context.create;
62
+ const entry = create?.resolvedTools.find(({ tool }) => tool.id === 'opencode');
63
+ if (!create || !entry) {
64
+ return {
65
+ status: 'fatal',
66
+ message: 'OpenCode sandbox hook requires its resolved tool state.'
67
+ };
68
+ }
69
+ ensureOpenCodeSandboxState(entry.dir, create.hostHome, create.hostEnv);
70
+ return { status: 'ready' };
71
+ }
72
+ };
73
+ const opencodeRecoveryChecks = [
74
+ {
75
+ id: 'command-available',
76
+ probe: { script: 'command -v "$1"', args: ['opencode'] },
77
+ finding: {
78
+ repairKind: 'hard-failure',
79
+ message: 'OpenCode is not available on PATH inside the sandbox.'
80
+ }
81
+ },
82
+ {
83
+ id: 'config-writable',
84
+ probe: {
85
+ script: 'probe="$1/.agent-infra-opencode-config-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
86
+ args: ['/home/devuser/.local/share/opencode/.xdg/config/opencode']
87
+ },
88
+ finding: {
89
+ repairKind: 'permissions',
90
+ message: 'OpenCode config directory is not writable by devuser.',
91
+ path: '/home/devuser/.local/share/opencode/.xdg/config/opencode'
92
+ }
93
+ },
94
+ {
95
+ id: 'state-writable',
96
+ probe: {
97
+ script: 'probe="$1/.agent-infra-opencode-state-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
98
+ args: ['/home/devuser/.local/share/opencode/.xdg/state/opencode']
99
+ },
100
+ finding: {
101
+ repairKind: 'permissions',
102
+ message: 'OpenCode state directory is not writable by devuser.',
103
+ path: '/home/devuser/.local/share/opencode/.xdg/state/opencode'
104
+ }
105
+ }
106
+ ];
107
+ export { ensureOpenCodeSandboxState, opencodeBeforeContainerCreateHook, opencodeRecoveryChecks };
108
+ //# sourceMappingURL=opencode-sandbox.js.map
@@ -1,5 +1,6 @@
1
1
  import { defineAgentClientAdapter } from '../adapter.js';
2
2
  import { hostJoin } from '../../sandbox/engines/wsl2-paths.js';
3
+ import { opencodeBeforeContainerCreateHook, opencodeRecoveryChecks } from './opencode-sandbox.js';
3
4
  const YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
4
5
  const opencodeAdapter = defineAgentClientAdapter({
5
6
  id: 'opencode',
@@ -35,9 +36,17 @@ const opencodeAdapter = defineAgentClientAdapter({
35
36
  'zh-CN': '.opencode/commands/update-agent-infra.zh-CN.md'
36
37
  },
37
38
  target: '.opencode/commands/update-agent-infra.md'
38
- }]
39
+ }],
40
+ customCommand: {
41
+ target: '.opencode/commands/${skillName}.md',
42
+ frontmatter: { agent: 'general', subtask: false },
43
+ argumentsToken: '$ARGUMENTS'
44
+ }
39
45
  },
40
46
  sandbox: {
47
+ image: {
48
+ dotfilesExclusions: ['.config/opencode']
49
+ },
41
50
  createTool: ({ home }) => ({
42
51
  id: 'opencode',
43
52
  name: 'OpenCode',
@@ -47,7 +56,9 @@ const opencodeAdapter = defineAgentClientAdapter({
47
56
  versionCmd: 'opencode --version',
48
57
  setupHint: 'Configure OpenCode credentials inside the container before first use.',
49
58
  envVars: {
50
- OPENCODE_CONFIG: '/home/devuser/.local/share/opencode/opencode.json'
59
+ XDG_DATA_HOME: '/home/devuser/.local/share',
60
+ XDG_CONFIG_HOME: '/home/devuser/.local/share/opencode/.xdg/config',
61
+ XDG_STATE_HOME: '/home/devuser/.local/share/opencode/.xdg/state'
51
62
  },
52
63
  hostLiveMounts: [
53
64
  {
@@ -66,7 +77,8 @@ const opencodeAdapter = defineAgentClientAdapter({
66
77
  command: `OPENCODE_PERMISSION='${YOLO_PERMISSION}' opencode; tput ed`
67
78
  }
68
79
  ],
69
- hooks: []
80
+ hooks: [opencodeBeforeContainerCreateHook],
81
+ recoveryChecks: opencodeRecoveryChecks
70
82
  }
71
83
  });
72
84
  export { opencodeAdapter };
@@ -0,0 +1,65 @@
1
+ RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/cc-token-status
2
+ #!/bin/sh
3
+ set -eu
4
+
5
+ SETTINGS_FILE="/home/devuser/.claude/settings.json"
6
+ if [ -r "$SETTINGS_FILE" ] && jq -e '
7
+ def has_nonempty($v):
8
+ if ($v | type) == "string" then ($v | gsub("^\\s+|\\s+$"; "") | length) > 0 else false end;
9
+ (if type == "object" then . else {} end) as $settings
10
+ | (if ($settings.env | type) == "object" then $settings.env else {} end) as $env
11
+ | has_nonempty($env.ANTHROPIC_AUTH_TOKEN)
12
+ or has_nonempty($env.ANTHROPIC_API_KEY)
13
+ or has_nonempty($settings.apiKeyHelper)
14
+ ' "$SETTINGS_FILE" >/dev/null 2>&1; then
15
+ exit 0
16
+ fi
17
+
18
+ CRED_FILE="/home/devuser/.claude/.credentials.json"
19
+ [ -r "$CRED_FILE" ] || exit 0
20
+
21
+ EXPIRES_MS=$(jq -r '(.claudeAiOauth.expiresAt // .expiresAt) // empty' "$CRED_FILE" 2>/dev/null || true)
22
+ case "$EXPIRES_MS" in
23
+ ''|*[!0-9]*) exit 0 ;;
24
+ esac
25
+
26
+ NOW_MS=$(($(date +%s) * 1000))
27
+ DIFF_MS=$((EXPIRES_MS - NOW_MS))
28
+ DIFF_S=$((DIFF_MS / 1000))
29
+
30
+ DIM='#[fg=colour245]'
31
+ YELLOW='#[fg=yellow]'
32
+ YELLOW_BOLD='#[fg=yellow,bold]'
33
+ RED_BOLD='#[fg=red,bold]'
34
+ RED_REV='#[fg=red,reverse]'
35
+ RESET='#[default]'
36
+
37
+ if [ "$DIFF_S" -le 0 ]; then
38
+ ELAPSED=$(( -DIFF_S ))
39
+ M=$((ELAPSED / 60))
40
+ printf '%sClaude Code auth EXPIRED %dm ago%s' "$RED_REV" "$M" "$RESET"
41
+ elif [ "$DIFF_S" -lt 60 ]; then
42
+ printf '%sClaude Code auth expires in %ds%s' "$RED_BOLD" "$DIFF_S" "$RESET"
43
+ elif [ "$DIFF_S" -lt 300 ]; then
44
+ M=$((DIFF_S / 60))
45
+ S=$((DIFF_S % 60))
46
+ printf '%sClaude Code auth expires in %dm %ds%s' "$RED_BOLD" "$M" "$S" "$RESET"
47
+ elif [ "$DIFF_S" -lt 1800 ]; then
48
+ M=$((DIFF_S / 60))
49
+ printf '%sClaude Code auth expires in %dm%s' "$YELLOW_BOLD" "$M" "$RESET"
50
+ elif [ "$DIFF_S" -lt 3600 ]; then
51
+ M=$((DIFF_S / 60))
52
+ printf '%sClaude Code auth expires in %dm%s' "$YELLOW" "$M" "$RESET"
53
+ else
54
+ TOTAL_M=$((DIFF_S / 60))
55
+ H=$((TOTAL_M / 60))
56
+ M=$((TOTAL_M % 60))
57
+ printf '%sClaude Code auth expires in %dh %dm%s' "$DIM" "$H" "$M" "$RESET"
58
+ fi
59
+ SCRIPT
60
+
61
+ RUN test -s /usr/local/bin/cc-token-status && test -x /usr/local/bin/cc-token-status
62
+
63
+ RUN printf '%s\n' \
64
+ "set -g status-right '#(/usr/local/bin/cc-token-status) | %H:%M'" \
65
+ >> /etc/tmux.conf
@@ -71,7 +71,15 @@ function createAgentClientManifest() {
71
71
  ownedPathPrefixes: Object.freeze([...adapter.project.ownedPathPrefixes]),
72
72
  managed: Object.freeze([...adapter.project.managed]),
73
73
  merged: Object.freeze([...adapter.project.merged]),
74
- ejected: Object.freeze([...adapter.project.ejected])
74
+ ejected: Object.freeze([...adapter.project.ejected]),
75
+ ...(adapter.project.customCommand === undefined
76
+ ? {}
77
+ : {
78
+ customCommand: Object.freeze({
79
+ ...adapter.project.customCommand,
80
+ frontmatter: Object.freeze({ ...adapter.project.customCommand.frontmatter })
81
+ })
82
+ })
75
83
  })));
76
84
  }
77
85
  export { AGENT_CLIENT_REGISTRY, createAgentClientManifest, getAgentClientAdapter, getAgentClientCapability, getAgentClientDelegationEvidence, getAgentClientModelSelection, listAgentClientAdapters, listEnabledAgentClientAdapters, listEnabledAgentClientAdaptersByCapability, listInstalledAgentClientAdapters };