@fitlab-ai/agent-infra 0.11.13 → 0.11.14

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 (89) hide show
  1. package/dist/bin/internal-cli.js +2 -7
  2. package/dist/lib/agent-clients/adapter.js +0 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
  6. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
  7. package/dist/lib/agent-clients/adapters/codex.js +0 -2
  8. package/dist/lib/internal/cli-route-inventory.js +0 -1
  9. package/dist/lib/internal/task-finalization.js +6 -4
  10. package/dist/lib/internal/task-lifecycle.js +3 -1
  11. package/dist/lib/internal/task-operation-registry.js +1 -3
  12. package/dist/lib/internal/task-validate.js +14 -124
  13. package/dist/lib/platform/release-notes.js +2 -2
  14. package/dist/lib/sandbox/commands/control-status.js +0 -1
  15. package/dist/lib/sandbox/commands/create.js +5 -4
  16. package/dist/lib/sandbox/config.js +12 -0
  17. package/dist/lib/sandbox/control/audit.js +1 -2
  18. package/dist/lib/sandbox/control/client.js +12 -47
  19. package/dist/lib/sandbox/control/executor.js +6 -10
  20. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  21. package/dist/lib/sandbox/control/lifecycle.js +20 -16
  22. package/dist/lib/sandbox/control/protocol.js +8 -36
  23. package/dist/lib/sandbox/control/server.js +2 -21
  24. package/dist/lib/sandbox/environment.js +7 -8
  25. package/dist/lib/sandbox/mounts.js +71 -59
  26. package/dist/lib/sandbox/recovery.js +2 -3
  27. package/dist/lib/sandbox/removal.js +19 -4
  28. package/dist/lib/sandbox/workspace-view.js +41 -4
  29. package/dist/lib/task/control-authority.js +39 -17
  30. package/dist/lib/task/control-recovery.js +1 -2
  31. package/dist/lib/task/finalization.js +13 -2
  32. package/dist/lib/task/lifecycle.js +92 -33
  33. package/dist/lifecycle-build-manifest.json +3 -3
  34. package/lib/agent-clients/adapter.ts +0 -27
  35. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  36. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  37. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  38. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  39. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
  40. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  41. package/lib/agent-clients/adapters/codex.ts +0 -2
  42. package/lib/internal/cli-route-inventory.ts +0 -1
  43. package/lib/internal/task-finalization.ts +6 -4
  44. package/lib/internal/task-lifecycle.ts +3 -1
  45. package/lib/internal/task-operation-registry.ts +1 -3
  46. package/lib/internal/task-validate.ts +13 -127
  47. package/lib/platform/release-notes.ts +2 -2
  48. package/lib/sandbox/commands/control-status.ts +0 -1
  49. package/lib/sandbox/commands/create.ts +5 -4
  50. package/lib/sandbox/config.ts +13 -0
  51. package/lib/sandbox/control/audit.ts +1 -2
  52. package/lib/sandbox/control/client.ts +13 -61
  53. package/lib/sandbox/control/executor.ts +7 -13
  54. package/lib/sandbox/control/host-recovery.ts +94 -21
  55. package/lib/sandbox/control/lifecycle.ts +18 -16
  56. package/lib/sandbox/control/protocol.ts +9 -45
  57. package/lib/sandbox/control/server.ts +2 -17
  58. package/lib/sandbox/environment.ts +7 -8
  59. package/lib/sandbox/mounts.ts +92 -75
  60. package/lib/sandbox/recovery.ts +2 -3
  61. package/lib/sandbox/removal.ts +16 -4
  62. package/lib/sandbox/workspace-view.ts +40 -5
  63. package/lib/task/control-authority.ts +40 -26
  64. package/lib/task/control-recovery.ts +2 -3
  65. package/lib/task/finalization.ts +15 -2
  66. package/lib/task/lifecycle.ts +103 -31
  67. package/package.json +1 -1
  68. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  69. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  70. package/templates/.agents/rules/task-short-id.en.md +18 -12
  71. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  72. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  73. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  74. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  75. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  76. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  77. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  78. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  79. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  80. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  81. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  82. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  83. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  84. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  85. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  86. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  87. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  88. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  89. package/lib/sandbox/control/agent-operation.ts +0 -49
@@ -106,6 +106,8 @@ type LifecycleFileSystem = {
106
106
  };
107
107
  type TaskLifecycleOptions = {
108
108
  repoRoot?: string;
109
+ controlRoot?: string;
110
+ controlProject?: string;
109
111
  metadataProvider?: () => TaskWriteMetadata;
110
112
  fileSystem?: Partial<LifecycleFileSystem>;
111
113
  taskFileSystem?: Partial<TaskFileSystem>;
@@ -276,6 +278,105 @@ function transferTaskContents(sourcePath: string, targetPath: string): void {
276
278
  }
277
279
  }
278
280
 
281
+ function pathExists(candidate: string): boolean {
282
+ try { fs.lstatSync(candidate); return true; }
283
+ catch (error) {
284
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ function assertTaskControlRootPath(controlRoot: string, taskId: string, expectedProject?: string): void {
290
+ if (path.basename(controlRoot) !== taskId || path.basename(path.dirname(controlRoot)) !== 'tasks'
291
+ || (expectedProject !== undefined && path.basename(path.dirname(path.dirname(controlRoot))) !== expectedProject)) {
292
+ throw new Error('control root path does not match the task/project layout');
293
+ }
294
+ const managedBase = path.dirname(path.dirname(path.dirname(controlRoot)));
295
+ let current = managedBase;
296
+ for (const segment of path.relative(managedBase, controlRoot).split(path.sep).filter(Boolean)) {
297
+ const stat = fs.lstatSync(current);
298
+ if (stat.isSymbolicLink()) throw new Error('control root ancestor is a symbolic link');
299
+ current = path.join(current, segment);
300
+ }
301
+ const stat = fs.lstatSync(controlRoot);
302
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('control root is not a real directory');
303
+ }
304
+
305
+ function assertTaskControlManifestIdentity(controlRoot: string, taskId: string, repoRoot: string, expectedProject?: string): void {
306
+ const controlManifest = path.join(controlRoot, 'manifest.json');
307
+ if (!fs.existsSync(controlManifest)) throw new Error('control manifest is missing');
308
+ const manifestStat = fs.lstatSync(controlManifest);
309
+ if (!manifestStat.isFile() || manifestStat.isSymbolicLink()) throw new Error('control manifest is not a real file');
310
+ const manifest = JSON.parse(fs.readFileSync(controlManifest, 'utf8')) as {
311
+ mode?: unknown; taskId?: unknown; project?: unknown; repoRoot?: unknown;
312
+ };
313
+ const project = expectedProject ?? path.basename(path.dirname(path.dirname(controlRoot)));
314
+ if (manifest.mode !== 'task-bound' || manifest.taskId !== taskId || manifest.project !== project
315
+ || typeof manifest.repoRoot !== 'string' || fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
316
+ throw new Error('control manifest identity does not match the task');
317
+ }
318
+ }
319
+
320
+ function taskBoundControlMoveSafety(
321
+ request: TaskLifecycleRequest,
322
+ controlRoot: string | undefined,
323
+ options: TaskLifecycleOptions,
324
+ taskId: string,
325
+ repoRoot: string,
326
+ validateManifestIdentity: boolean
327
+ ): LifecycleError | null {
328
+ if (!controlRoot) return null;
329
+ const controlManifest = path.join(controlRoot, 'manifest.json');
330
+ const controlLease = path.join(controlRoot, 'lease.json');
331
+ const stateExists = pathExists(controlRoot) || pathExists(controlManifest) || pathExists(controlLease);
332
+ if (stateExists) {
333
+ try {
334
+ assertTaskControlRootPath(controlRoot, taskId, options.controlProject);
335
+ if (validateManifestIdentity) {
336
+ assertTaskControlManifestIdentity(controlRoot, taskId, repoRoot, options.controlProject);
337
+ } else {
338
+ const manifestStat = fs.lstatSync(controlManifest);
339
+ if (!manifestStat.isFile() || manifestStat.isSymbolicLink()) throw new Error('control manifest is not a real file');
340
+ }
341
+ }
342
+ catch {
343
+ return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task-bound sandbox control root is missing, malformed, or belongs to another task' };
344
+ }
345
+ }
346
+ if (request.intent !== 'complete' && (pathExists(controlManifest) || pathExists(controlLease))) {
347
+ return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task-bound sandbox control state exists; stop and remove the verified sandbox before moving the task directory' };
348
+ }
349
+ return null;
350
+ }
351
+
352
+ function orchestrationMoveSafety(sourcePath: string): LifecycleError | null {
353
+ const orchestrationPath = path.join(sourcePath, '.runtime', 'orchestration.json');
354
+ if (!pathExists(orchestrationPath)) return null;
355
+ try {
356
+ const run = JSON.parse(fs.readFileSync(orchestrationPath, 'utf8')) as { status?: unknown; pendingDelegation?: unknown };
357
+ if (run.status === 'running' || run.pendingDelegation !== null && run.pendingDelegation !== undefined) {
358
+ return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task orchestration has an active or pending delegation; settle it before moving the task directory' };
359
+ }
360
+ } catch {
361
+ return { code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE', message: 'task orchestration state is unreadable; refusing to move the task directory' };
362
+ }
363
+ return null;
364
+ }
365
+
366
+ function runtimeMoveSafety(
367
+ request: TaskLifecycleRequest,
368
+ movingTaskDirectory: boolean,
369
+ options: TaskLifecycleOptions,
370
+ taskId: string,
371
+ repoRoot: string,
372
+ sourcePath: string,
373
+ validateControlManifestIdentity: boolean
374
+ ): LifecycleError | null {
375
+ if (!movingTaskDirectory) return null;
376
+ const controlSafety = taskBoundControlMoveSafety(request, options.controlRoot, options, taskId, repoRoot, validateControlManifestIdentity);
377
+ return controlSafety ?? orchestrationMoveSafety(sourcePath);
378
+ }
379
+
279
380
  function targetContainsTask(targetPath: string, taskId: string, request: TaskLifecycleRequest): boolean {
280
381
  try {
281
382
  const stat = fs.lstatSync(targetPath);
@@ -554,35 +655,6 @@ function applyTaskLifecycleUnlocked(requestInput: TaskLifecycleRequest, options:
554
655
  const targetPath = path.join(repoRoot, '.agents', 'workspace', spec.target, taskId);
555
656
  const requiredSteps = stepsForIntent(request.intent);
556
657
  const movingTaskDirectory = sourcePath !== targetPath;
557
- const assertRuntimeMoveSafe = (): LifecycleError | null => {
558
- if (!movingTaskDirectory) return null;
559
- const controlManifest = path.join(sourcePath, '.runtime', 'sandbox-control', 'manifest.json');
560
- const controlLease = path.join(sourcePath, '.runtime', 'sandbox-control', 'lease.json');
561
- if (request.intent !== 'complete' && (fs.existsSync(controlManifest) || fs.existsSync(controlLease))) {
562
- return {
563
- code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
564
- message: 'task-bound sandbox control state exists; stop and remove the verified sandbox before moving the task directory'
565
- };
566
- }
567
- const orchestrationPath = path.join(sourcePath, '.runtime', 'orchestration.json');
568
- if (fs.existsSync(orchestrationPath)) {
569
- try {
570
- const run = JSON.parse(fs.readFileSync(orchestrationPath, 'utf8')) as { status?: unknown; pendingDelegation?: unknown };
571
- if (run.status === 'running' || run.pendingDelegation !== null && run.pendingDelegation !== undefined) {
572
- return {
573
- code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
574
- message: 'task orchestration has an active or pending delegation; settle it before moving the task directory'
575
- };
576
- }
577
- } catch {
578
- return {
579
- code: 'LIFECYCLE_RUNTIME_MOVE_UNSAFE',
580
- message: 'task orchestration state is unreadable; refusing to move the task directory'
581
- };
582
- }
583
- }
584
- return null;
585
- };
586
658
  const currentTaskFile = path.join(sourcePath, 'task.md');
587
659
  const existingJournal = fs.existsSync(path.join(sourcePath, '.task-lifecycle.json'));
588
660
  if (sourceState === spec.target && !existingJournal) {
@@ -605,7 +677,7 @@ function applyTaskLifecycleUnlocked(requestInput: TaskLifecycleRequest, options:
605
677
  && !(request.intent === 'complete' && targetContainsTask(targetPath, taskId, request))) {
606
678
  return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
607
679
  }
608
- const moveSafety = assertRuntimeMoveSafe();
680
+ const moveSafety = runtimeMoveSafety(request, movingTaskDirectory, options, taskId, repoRoot, sourcePath, true);
609
681
  if (moveSafety) return failed(request, moveSafety, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
610
682
  const content = io.readFileSync(currentTaskFile);
611
683
  let frontmatter;
@@ -732,7 +804,7 @@ function applyTaskLifecycleUnlocked(requestInput: TaskLifecycleRequest, options:
732
804
  }
733
805
  if (fs.existsSync(targetPath) && !fs.existsSync(path.join(sourcePath, 'task.md'))) completed.add('directory-moved');
734
806
  if (!completed.has('directory-moved')) {
735
- const moveSafety = assertRuntimeMoveSafe();
807
+ const moveSafety = runtimeMoveSafety(request, movingTaskDirectory, options, taskId, repoRoot, sourcePath, false);
736
808
  if (moveSafety) return failed(request, moveSafety, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0, timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
737
809
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
738
810
  if (request.intent === 'complete') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.13",
3
+ "version": "0.11.14",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,6 @@
15
15
  ## Current Host
16
16
 
17
17
  - Use native spawn, wait, and App Server results from the current host. A task-bound sandbox executes ordinary task orchestration locally in the current worktree.
18
- - Codex controller `open`, `verify`, and `close` remain restricted broker operations. After `open`, complete typed `verify` with the same lease proof before accepting the task, control-generation, and controller-instance binding.
19
18
  - Prepare validates the current task, model policy, and host preflight. Start and terminal records associate observed parent/child identity; failure must not be recorded as success.
20
19
  - Client-specific preflight, event sources, and stop-evidence validation belong to the client adapter. The common orchestrator calls only the shared capability and contains no client-ID branches.
21
20
 
@@ -28,9 +27,9 @@
28
27
 
29
28
  ## Current-only Cutover and Rollback
30
29
 
31
- - Enter a quiet window before cutover: let in-flight orchestration intents return, stop the wrapper/controller, and preserve the worktree, task/run/orchestration state, receipts, registration, and audit. For an unknown result, inspect local `status`/`route` before using existing recover/advance semantics; do not replay an old broker request.
32
- - Deploy or roll back the CLI, broker, controller adapter, and protocol as one versioned unit. After stopping the old container/broker, rebuild and run `ai sandbox start --recreate <task-ref>` to resume with a new control generation; never reuse an old token, proof, or request.
33
- - On either the new or rolled-back version, first confirm the same task through `status`/`route`, then require a successful typed controller `open`→`verify`. Keep the quiet window and evidence intact if any check fails; do not mix component versions or rewrite domain facts.
30
+ - Enter a quiet window before cutover: let in-flight orchestration intents return and preserve the worktree, task/run/orchestration state, receipts, and audit. For an unknown result, inspect local `status`/`route` before using existing recover/advance semantics; do not replay an old broker request.
31
+ - Deploy or roll back the CLI, broker, and protocol as one versioned unit. After stopping the old container/broker, rebuild and run `ai sandbox start --recreate <task-ref>` to resume with a new control generation; never reuse an old token or request.
32
+ - On either the new or rolled-back version, first confirm the same task through `status`/`route`. Keep the quiet window and evidence intact if any check fails; do not mix component versions or rewrite domain facts.
34
33
 
35
34
  ## Model Policy
36
35
 
@@ -15,7 +15,6 @@
15
15
  ## 当前宿主
16
16
 
17
17
  - 使用当前宿主的原生 spawn、wait 与 App Server 结果;task-bound sandbox 在当前 worktree 本地执行普通任务编排。
18
- - Codex controller 的 `open`、`verify`、`close` 继续通过受限 broker 执行。`open` 后必须用同一 lease proof 完成 typed `verify`,再接受 task、control generation 和 controller instance 绑定。
19
18
  - prepare 校验当前任务、模型策略与宿主 preflight。启动和完成记录关联实际 parent/child 身份,失败不得写成成功。
20
19
  - 客户端特有的 preflight、事件来源与停止证据校验由客户端适配器实现;公共总控只调用统一能力。不得在公共流程中新增客户端 ID 分支。
21
20
 
@@ -28,9 +27,9 @@
28
27
 
29
28
  ## Current-only 切换与回滚
30
29
 
31
- - 切换前进入静默窗口:等待在途编排 intent 返回,停止 wrapper/controller,并保留 worktree、task/run/orchestration 状态、receipts、registration 与 audit。结果未知时先用本地 `status`/`route` 判断 domain 是否已提交,再按现有 recover/advance 语义处理;旧 broker request 不重放。
32
- - CLI、broker、controller adapter 和协议必须作为同一版本单元部署或回退。停止旧 container/broker 后执行 rebuild 与 `ai sandbox start --recreate <task-ref>`,以新 control generation 恢复执行;旧 token、proof 和 request 不得复用。
33
- - 新版本或回退版本都必须先核对同一任务的 `status`/`route`,再要求 controller typed `open`→`verify` 成功。任一检查失败时继续保持静默并保留证据,不局部混用组件或改写 domain 事实。
30
+ - 切换前进入静默窗口:等待在途编排 intent 返回,并保留 worktree、task/run/orchestration 状态、receipts 与 audit。结果未知时先用本地 `status`/`route` 判断 domain 是否已提交,再按现有 recover/advance 语义处理;不重放旧 broker request。
31
+ - CLI、broker 与协议必须作为同一版本单元部署或回退。停止旧 container/broker 后执行 rebuild 与 `ai sandbox start --recreate <task-ref>`,以新的 control generation 恢复执行;旧 token 和 request 不得复用。
32
+ - 新版本或回滚版本都必须先核对同一任务的 `status`/`route`。任一检查失败时继续保持静默并保留证据,不局部混用组件或改写 domain 事实。
34
33
 
35
34
  ## 模型策略
36
35
 
@@ -24,12 +24,13 @@ Task short ids let mobile-style SKILL invocations replace the full 22-char
24
24
  |------------|-----------------------------------------------------------------------|---------------------------------------------------------------|
25
25
  | alloc | `create-task`, `import-issue`, `import-codescan`, `import-dependabot` | Assigns lowest free `NN` in the registry. |
26
26
  | resolve | Lifecycle SKILLs (`analyze-task`, `plan-task`, `code-task`, …) | Looks up `NN` → full task id. Does not allocate. |
27
- | release | `complete-task`, `cancel-task`, `block-task`, `close-codescan`, `close-dependabot` | Removes the registry entry. |
27
+ | release | Successful sandbox cleanup; also `cancel-task`, `block-task`, `close-codescan`, and `close-dependabot` | Removes the registry entry. |
28
28
  | re-alloc | `restore-task` | Re-allocates a (possibly new) `NN` in the registry. |
29
29
 
30
- Short ids are valid only while a task lives in `.agents/workspace/active/`.
31
- Once it is moved to `completed/`, `blocked/`, or `archive/`, the `NN` slot is
32
- freed and may be reused by a new task.
30
+ Completion retains the short id so a sandbox still associated with the
31
+ completed task can resolve it before cleanup. The id is released only after
32
+ managed sandbox cleanup confirms that the container was deleted successfully.
33
+ Other lifecycle intents release ids at the times listed in the table.
33
34
 
34
35
  ## Configuration
35
36
 
@@ -52,10 +53,13 @@ archiving all active tasks first (the registry key width depends on it).
52
53
  | Entrypoint | Hit | Miss |
53
54
  |-------------------------------------------------------------|----------------------|------------------------------------------------------|
54
55
  | SKILL parameter resolver (lifecycle SKILLs) | resolve to full id | **strict error** — short id not found / invalid |
55
- | `ai sandbox exec <N>` / `ai sandbox create <N>` | resolve to full id, then read `branch` from task.md | **strict error** — no ls-index fallback, no literal-branch fallback; hint the user to pass a short id / `TASK-id` / branch name |
56
+ | `ai sandbox exec <N>` / `ai sandbox create <N>` | resolve to an active task id, then read `branch` from task.md | **strict error** — no ls-index fallback, no literal-branch fallback; hint the user to pass a short id / `TASK-id` / branch name |
57
+ | `ai sandbox rm <N>` | resolve the retained mapping, then read active or completed identity by full task id | **strict error** — stop cleanup when the short id is missing |
56
58
 
57
- `list --verify` is strictly read-only: it reports discrepancies between the
58
- active dir and the registry, but never writes.
59
+ `list --verify` is strictly read-only: every active task must have a mapping;
60
+ registry mappings may point to a valid active task or to a
61
+ `completed/{taskId}/task.md` whose identity matches and whose status is
62
+ `completed`. Other mappings are reported as orphans; no state is modified.
59
63
 
60
64
  ## SKILL parameter resolver
61
65
 
@@ -94,11 +98,13 @@ Short ids are pure local state, persisted only in the registry
94
98
  - Created on demand by the first `alloc`; an absent file is treated as an empty
95
99
  registry.
96
100
  - Short ids are assigned only by an explicit `alloc` (`create-task` /
97
- `import-*` / `restore-task`); `resolve` / `list` / `release` never allocate —
98
- they only clean up stale entries pointing at non-active tasks.
99
- - After archive (complete-task / cancel-task / block-task / close-*) the
100
- registry entry is deleted immediately and the short id may be reused; archived
101
- tasks are referenced by their full `TASK-…` id.
101
+ `import-*` / `restore-task`); `resolve` and `list` are read-only and do not
102
+ perform implicit cleanup.
103
+ - `alloc` may clean up stale entries that point neither to an active task nor
104
+ to valid completed task content. Completed mappings stay until managed
105
+ sandbox cleanup succeeds.
106
+ - Task operations may resolve a completed mapping and then reject it at their
107
+ own state gate; `ai sandbox create <N>` still accepts active tasks only.
102
108
 
103
109
  `resolve(<N>)` workflow: ① validate arg matches `^[0-9]+$` →
104
110
  ② strip leading zeros and take the numeric value `n`; classify as reserved
@@ -19,11 +19,12 @@
19
19
  |-----------|---------------------------------------------------------------------------------------------|------------------------------------------------------------------|
20
20
  | alloc | `create-task`、`import-issue`、`import-codescan`、`import-dependabot` | 分配最小可用 `NN`,写入注册表。 |
21
21
  | resolve | 生命周期 SKILL(`analyze-task` / `plan-task` / `code-task` / `review-*` / `commit` / …) | `NN` → 完整 task id 查询,不分配。 |
22
- | release | `complete-task`、`cancel-task`、`block-task`、`close-codescan`、`close-dependabot` | 从注册表移除。 |
22
+ | release | sandbox cleanup 成功;以及 `cancel-task`、`block-task`、`close-codescan`、`close-dependabot` | 从注册表移除。 |
23
23
  | re-alloc | `restore-task` | 重新分配(可能与历史不同),写入注册表。 |
24
24
 
25
- 短号仅在任务处于 `.agents/workspace/active/` 期间有效;任务移动到
26
- `completed/` / `blocked/` / `archive/` 后短号立即释放,可被新任务复用。
25
+ complete 保留短号,供 completed task 与仍关联的 sandbox 在 cleanup 前继续解析。
26
+ 受管 sandbox cleanup 确认容器已删除且清理成功后,才释放对应 completed task 的短号。
27
+ 其他 lifecycle intent 继续按表中时机释放短号。
27
28
 
28
29
  ## 配置
29
30
 
@@ -45,9 +46,12 @@
45
46
  | 入口 | 注册表命中 | 注册表未命中 |
46
47
  |-----------------------------------------------------------|----------------------|--------------------------------------------------------|
47
48
  | SKILL 入参解析器(生命周期 SKILL) | 解析为完整 task id | **严格报错** —— 短号不存在 / 格式错误 |
48
- | `ai sandbox exec <N>` / `ai sandbox create <N>` | 解析为完整 task id 后查 task.md 取 `branch` | **严格报错** —— 不再回退到 ls 行号或字面分支名;提示用任务短号 / `TASK-id` / 分支名 |
49
+ | `ai sandbox exec <N>` / `ai sandbox create <N>` | 解析为 active task id 后查 task.md 取 `branch` | **严格报错** —— 不再回退到 ls 行号或字面分支名;提示用任务短号 / `TASK-id` / 分支名 |
50
+ | `ai sandbox rm <N>` | 解析保留映射,再按完整 task id 读取 active 或 completed 身份 | **严格报错** —— 短号不存在时停止清理 |
49
51
 
50
- `list --verify` 严格只读:报告 active 目录 / 注册表 两者差异,但不修改任何状态。
52
+ `list --verify` 严格只读:active task 必须有映射;注册表映射可指向有效 active task
53
+ 或 `completed/{taskId}/task.md` 中身份匹配且状态为 completed 的 task。其他映射作为 orphan
54
+ 报告,不修改任何状态。
51
55
 
52
56
  ## SKILL 入参解析
53
57
 
@@ -78,8 +82,9 @@ fi
78
82
  - key 是零填充到 `task.shortIdLength` 位的字符串,value 是完整 `TASK-…` task id
79
83
  - 自动 git ignore(active 工作区整体 ignore;无需新增 ignore 条目)
80
84
  - 首次 `alloc` 时按需自动创建;不存在时按空注册表处理
81
- - 短号只由显式 `alloc`(`create-task` / `import-*` / `restore-task`)分配;`resolve` / `list` / `release` 不分配,仅在执行时自动清理指向非 active 任务的 stale entry
82
- - 归档(complete-task / cancel-task / block-task / close-*)后注册表 entry 立即删除,短号可被新任务复用;归档后引用任务一律用完整 `TASK-…` id
85
+ - 短号只由显式 `alloc`(`create-task` / `import-*` / `restore-task`)分配;`resolve` 与 `list` 只读,不做隐式清理
86
+ - alloc 可清理既非 active、也没有有效 completed task 内容的 stale entry;completed mapping 保留至受管 sandbox cleanup 成功
87
+ - task 操作可按 completed mapping 解析后由各自状态门禁拒绝;`ai sandbox create <N>` 仍只接受 active task
83
88
 
84
89
  `resolve(<N>)` 工作流:① 校验入参匹配 `^[0-9]+$` → ② 去前导零取
85
90
  数值 `n`,按 `n` 是否 `== 0`(保留)/ `> 10^shortIdLength - 1`(超容量)/
@@ -63,6 +63,8 @@ agent-infra-internal platform-release-notes context \
63
63
 
64
64
  Use `pullRequests` and `commits` from Step 3. Each commit's `authors` contains only that commit's platform authors; `pullRequestNumbers` records the platform's PR associations. The skill does not interpret raw platform fields or email rules. A PR's `closingIssues` contains its actual closing Issues; never infer a relationship from matching PR and Issue numbers.
65
65
 
66
+ Release notes describe changes actually delivered between the tags. For commits without an associated PR, inspect the actual diff using the `oid` from the typed context to determine whether it records a delivered change or release workflow mechanics. A commit made up only of version metadata updates, generated release artifacts, or other mechanical release preparation/publication work is outside the release's change set and contributor counts. When a commit also contains an independent product, documentation, or maintenance change, retain that change and its authors. If the diff does not establish the commit's purpose, keep it and describe the actual change. Use this same filtered commit set for the classification in Step 6 and contributor counts in Step 7.
67
+
66
68
  ### 5. Collect Related Issues
67
69
 
68
70
  Use each PR's `closingIssues` from Step 3. Do not parse platform-specific reference syntax in this generic skill.
@@ -112,7 +114,7 @@ If no historical release notes exist, use the following default Markdown format:
112
114
  5. **Contributor collection**:
113
115
  - **Data sources**:
114
116
  - PR authors from the typed context
115
- - Commit co-authors from the typed context's commit `authors`
117
+ - Commit co-authors from `authors` of commits retained as release changes in Step 4
116
118
  - Issue reporters from typed `closingIssues[].author`
117
119
  - **Contribution count**: `PR count + co-authored commit count` for the same identity, merged across both sources
118
120
  - **`@login` mapping**: follow the identity safety boundary in `.agents/rules/release-commands.md`
@@ -63,6 +63,8 @@ agent-infra-internal platform-release-notes context \
63
63
 
64
64
  使用步骤 3 返回的 `pullRequests` 与 `commits`。每个 commit 的 `authors` 只包含该 commit 的平台作者;`pullRequestNumbers` 表示平台记录的 PR 关联。技能不读取平台原始字段或邮箱规则。PR 的 `closingIssues` 是该 PR 实际关闭的 Issue,不按 PR/Issue 编号推断关联。
65
65
 
66
+ 发布说明记录标签间实际交付的变更。对于没有关联 PR 的 commit,依据 typed context 中的 `oid` 检查实际 diff,判断它记录的是版本交付还是发布流程本身。仅包含版本元数据更新、生成发布产物或其他版本准备/发布机械操作的 commit 不属于本版本变更,也不计入贡献者;同一 commit 若还包含独立的产品、文档或维护改动,则保留这些改动及其作者。diff 无法说明提交用途时,保留该 commit,并根据实际改动描述。步骤 6 的变更分类和步骤 7 的贡献者统计使用同一筛选结果。
67
+
66
68
  ### 5. 收集关联 Issue
67
69
 
68
70
  使用步骤 3 中每个 PR 的 `closingIssues`,不在通用技能中解析平台专有引用语法。
@@ -112,7 +114,7 @@ agent-infra-internal platform-release-notes context \
112
114
  5. **贡献者搜集**:
113
115
  - **数据源**:
114
116
  - PR author:来自 `.agents/rules/release-commands.md` 中已合并 PR 查询规则
115
- - Commit co-authors:来自步骤 3 typed context 的 commit `authors`
117
+ - Commit co-authors:来自步骤 3 typed context 中按步骤 4 纳入版本变更的 commit `authors`
116
118
  - Issue reporters:来自步骤 3 typed context 的 `closingIssues[].author`
117
119
  - **贡献数定义**:`该人的 PR 数 + 该人作为 co-author 的 commit 数`(同一身份跨来源合并计数)
118
120
  - **`@login` 映射**:遵循 `.agents/rules/release-commands.md` 的身份安全边界
@@ -14,7 +14,7 @@ The entry point may omit the task ref; explicit task scope accepts only `--task
14
14
  The orchestrator delegates only and never executes a stage skill itself. First read `.agents/rules/no-mid-flow-questions.md`, `.agents/rules/lifecycle-orchestration.md`, and `reference/host-validation.md`.
15
15
 
16
16
  1. Resolve the canonical task ID, current Agent Client, and optional atomic policy flags, then run `task-snapshot`. If one explicit role field is present, all four are required.
17
- 2. Use the current host's native child spawn, wait, and result interfaces. Task-bound sandboxes execute ordinary task orchestration locally in the current worktree; Codex controller `open`, `verify`, and `close` remain restricted broker operations. Retain task binding and checks for real external process operations.
17
+ 2. Use the current host's native child spawn, wait, and result interfaces. Task-bound sandboxes execute ordinary task orchestration locally in the current worktree. Retain task binding and checks for real external process operations.
18
18
  3. Before each loop, run `agent-infra-internal task-lifecycle {task-id} recover-started --agent {client} --auto`. The client adapter owns recovery capability and stop-evidence validation; an unsupported adapter or a run that needs no recovery returns `no-op`. Continue only after `no-op` or `applied`. Stop on every other result; a later operator invocation retries from persisted state without routing or dispatching a child.
19
19
  4. Run `task-orchestration {task-id} begin-or-resume --client {client}` with the complete explicit policy. With no explicit policy, core reads that client's configured policy; existing runs use persisted policy. Disk state outside the complete current structure fails closed without rewriting; finish or clear active runs before upgrading. Stop on paused/completed results.
20
20
  5. Run `route`. On `completed`, run `agent-infra-internal task-verify {task-id} run-task.completed --format text`, present its summary per `.agents/rules/validation-output.md` while retaining necessary diagnostics, and stop. Only a `running` result with non-null `next` supplies action, role, round, artifact, requested model, and requested effort.
@@ -14,7 +14,7 @@ description: >
14
14
  总控只编排,不直接执行任何阶段技能。执行前先读取 `.agents/rules/no-mid-flow-questions.md`、`.agents/rules/lifecycle-orchestration.md` 与 `reference/host-validation.md`。
15
15
 
16
16
  1. 解析规范任务 ID、当前 Agent Client,以及可选的原子策略 `--executor-model`、`--executor-reasoning-effort`、`--reviewer-model`、`--reviewer-reasoning-effort`,并执行 `agent-infra-internal task-snapshot {task-id} --format text`。任一显式策略字段出现时四个 role 字段必须完整,不得与配置拼接;两个角色可以使用同一模型。
17
- 2. 使用当前宿主的原生子 Agent 启动、等待与结果接口。task-bound sandbox 在当前 worktree 本地执行普通任务编排;Codex controller 的 `open`、`verify`、`close` 继续通过受限 broker 执行。保留任务绑定与真实外部进程操作检查。
17
+ 2. 使用当前宿主的原生子 Agent 启动、等待与结果接口。task-bound sandbox 在当前 worktree 本地执行普通任务编排。保留任务绑定与真实外部进程操作检查。
18
18
  3. 每次进入循环时先调用 `agent-infra-internal task-lifecycle {task-id} recover-started --agent {client} --auto`。客户端适配器负责恢复能力和停止证据校验;没有该能力或无需恢复时返回 `no-op`。仅在结构化结果为 `no-op` 或 `applied` 时继续,其他结果安全停止;下一次人为调用按持久化状态幂等重试,不 route、不派发 child。
19
19
  4. 调用 `agent-infra-internal task-orchestration {task-id} begin-or-resume --client {client}` 并转发完整显式策略。完全没有显式策略时由核心读取当前 client 的 `agentClients[].orchestration`;existing run 使用持久化策略。磁盘状态不符合当前完整结构时核心失败关闭且不改写;升级前必须完成或清空 active run。仅当核心返回 `ORCHESTRATION_MODEL_POLICY_REQUIRED` 时,先用 `agent-client model-selection` 展示 complete/partial/interactive-only 来源,再一次收集完整策略;未回答则不创建 run。若为 paused/completed,按结构化结果停止。
20
20
  5. 调用 `route` 并读取结构化结果。若返回 `completed`,立即运行 `agent-infra-internal task-verify {task-id} run-task.completed --format text` 并停止;按 `.agents/rules/validation-output.md` 展示摘要并保留必要诊断。仅当返回 `running` 且 `next` 非空时读取唯一 action、role、round、artifact、`requestedModel` 和 `requestedReasoningEffort`,不得自行推断。
@@ -1,130 +0,0 @@
1
- import { SandboxControlClientError, requestSandboxAgentOperation } from '../../../sandbox/control/client.js';
2
- function fail(code, message, accepted = true) {
3
- throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable: false }, accepted);
4
- }
5
- function exactKeys(value, keys) {
6
- return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
7
- }
8
- function validProcess(value) {
9
- if (!value || typeof value !== 'object' || Array.isArray(value))
10
- return false;
11
- const process = value;
12
- return exactKeys(process, ['pid', 'startTime'])
13
- && Number.isSafeInteger(process.pid) && process.pid > 0
14
- && Number.isSafeInteger(process.startTime) && process.startTime >= 0;
15
- }
16
- function validBuild(value) {
17
- if (!value || typeof value !== 'object' || Array.isArray(value))
18
- return false;
19
- const build = value;
20
- return exactKeys(build, ['internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'])
21
- && build.protocolVersion === 3
22
- && typeof build.packageVersion === 'string' && build.packageVersion.length > 0
23
- && typeof build.internalExecutableBuildHash === 'string' && /^[a-f0-9]{64}$/u.test(build.internalExecutableBuildHash)
24
- && typeof build.lifecycleContractHash === 'string' && /^[a-f0-9]{64}$/u.test(build.lifecycleContractHash);
25
- }
26
- function parseCodexControllerResult(response) {
27
- if (response.phase !== 'completed' || response.error !== null || response.stderr !== '') {
28
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller result outer response is invalid');
29
- }
30
- let value;
31
- try {
32
- if (!response.stdout.endsWith('\n') || response.stdout.slice(0, -1).includes('\n'))
33
- throw new Error('not canonical');
34
- value = JSON.parse(response.stdout);
35
- }
36
- catch {
37
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid');
38
- }
39
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
40
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller result payload is invalid');
41
- }
42
- const result = value;
43
- const keys = result.status === 'verified'
44
- ? ['binding', 'changed', 'error', 'lease', 'status', 'version']
45
- : ['changed', 'error', 'lease', 'status', 'version'];
46
- if (!exactKeys(result, keys) || result.version !== 1
47
- || !['opened', 'closed', 'failed', 'verified'].includes(result.status)) {
48
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller result schema is invalid');
49
- }
50
- if (result.status === 'failed') {
51
- const error = result.error;
52
- if (response.exitCode !== 1 || result.changed !== false || result.lease !== null || !error
53
- || !exactKeys(error, ['code', 'message', 'retryable'])
54
- || typeof error.code !== 'string' || !/^[A-Z][A-Z0-9_]+$/u.test(error.code)
55
- || typeof error.message !== 'string' || error.retryable !== false) {
56
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller failure result is invalid');
57
- }
58
- fail(error.code, error.message);
59
- }
60
- if (response.exitCode !== 0 || result.error !== null) {
61
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller success result is invalid');
62
- }
63
- if (result.status === 'closed') {
64
- if (typeof result.changed !== 'boolean' || result.lease !== null)
65
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller close result is invalid');
66
- return result;
67
- }
68
- if (result.status === 'verified') {
69
- const binding = result.binding;
70
- if (result.changed !== false || result.lease !== null || !binding
71
- || !exactKeys(binding, ['controllerInstanceDigest', 'controlGeneration', 'taskId'])
72
- || typeof binding.taskId !== 'string' || binding.taskId.length === 0
73
- || typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0
74
- || typeof binding.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.controllerInstanceDigest)) {
75
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify result is invalid');
76
- }
77
- return result;
78
- }
79
- const lease = result.lease;
80
- if (result.changed !== true || !lease
81
- || !exactKeys(lease, ['buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerProcess', 'expiresAt', 'issuedAt', 'leaseId', 'leaseSecret', 'taskId', 'version'])
82
- || lease.version !== 1 || typeof lease.leaseId !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseId)
83
- || typeof lease.leaseSecret !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.leaseSecret)
84
- || typeof lease.taskId !== 'string' || lease.taskId.length === 0
85
- || typeof lease.controlGeneration !== 'string' || lease.controlGeneration.length === 0
86
- || typeof lease.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(lease.controllerInstanceDigest)
87
- || !validProcess(lease.controllerProcess) || !validBuild(lease.buildIdentity)
88
- || !Number.isSafeInteger(lease.issuedAt) || !Number.isSafeInteger(lease.expiresAt)
89
- || lease.expiresAt <= lease.issuedAt) {
90
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller open result is invalid');
91
- }
92
- return result;
93
- }
94
- function requestController(params) {
95
- const result = parseCodexControllerResult(requestSandboxAgentOperation({
96
- agentClient: 'codex',
97
- operation: `controller.${params.command}`,
98
- payload: params.command === 'open'
99
- ? { controllerProcess: params.controllerProcess }
100
- : { proof: params.controllerProof },
101
- ...params
102
- }));
103
- if (result.status === 'opened'
104
- && (result.lease.controlGeneration !== (params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION)
105
- || result.lease.controllerProcess.pid !== params.controllerProcess.pid
106
- || result.lease.controllerProcess.startTime !== params.controllerProcess.startTime)) {
107
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller result does not match the request');
108
- }
109
- return result;
110
- }
111
- function requestCodexControllerOpen(params) {
112
- const result = requestController({ ...params, command: 'open', controllerProof: null });
113
- if (result.status !== 'opened')
114
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller open returned the wrong result');
115
- return result;
116
- }
117
- function requestCodexControllerClose(params) {
118
- const result = requestController({ ...params, command: 'close' });
119
- if (result.status !== 'closed')
120
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller close returned the wrong result');
121
- return result;
122
- }
123
- function requestCodexControllerVerify(params) {
124
- const result = requestController({ ...params, controllerProcess: params.controllerProof.controllerProcess, command: 'verify' });
125
- if (result.status !== 'verified')
126
- fail('SANDBOX_CONTROL_RESULT_INVALID', 'controller verify returned the wrong result');
127
- return result;
128
- }
129
- export { parseCodexControllerResult, requestCodexControllerClose, requestCodexControllerOpen, requestCodexControllerVerify };
130
- //# sourceMappingURL=controller-client.js.map