@fitlab-ai/agent-infra 0.11.7 → 0.11.9

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 (128) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +8 -2
  4. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  5. package/dist/lib/agent-clients/config.js +11 -3
  6. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  7. package/dist/lib/agent-clients/next-steps.js +1 -1
  8. package/dist/lib/agent-clients/reconcile.js +3 -3
  9. package/dist/lib/agent-clients/schema.js +18 -10
  10. package/dist/lib/defaults.json +6 -3
  11. package/dist/lib/init.js +2 -2
  12. package/dist/lib/internal/agent-client.js +3 -3
  13. package/dist/lib/internal/cli-route-inventory.js +3 -0
  14. package/dist/lib/internal/platform-pr-review.js +20 -3
  15. package/dist/lib/internal/task-operation-registry.js +3 -1
  16. package/dist/lib/internal/task-platform-recovery.js +56 -0
  17. package/dist/lib/merge.js +25 -5
  18. package/dist/lib/platform/completion-backfill.js +2 -1
  19. package/dist/lib/platform/in-label-sync.js +12 -1
  20. package/dist/lib/platform/issue-comments.js +58 -4
  21. package/dist/lib/platform/issues.js +105 -5
  22. package/dist/lib/platform/operation-coordinator.js +74 -0
  23. package/dist/lib/platform/pr-review.js +107 -1
  24. package/dist/lib/platform/pr-summary.js +79 -57
  25. package/dist/lib/platform/pull-requests.js +96 -4
  26. package/dist/lib/sandbox/commands/create.js +6 -3
  27. package/dist/lib/sandbox/commands/ls.js +4 -3
  28. package/dist/lib/sandbox/commands/rm.js +1 -1
  29. package/dist/lib/sandbox/config.js +22 -4
  30. package/dist/lib/sandbox/git-metadata.js +50 -0
  31. package/dist/lib/sandbox/index.js +1 -1
  32. package/dist/lib/sandbox/mounts.js +12 -0
  33. package/dist/lib/sandbox/recovery.js +12 -22
  34. package/dist/lib/sandbox/removal.js +58 -20
  35. package/dist/lib/sandbox/tools.js +31 -7
  36. package/dist/lib/task/activity-intent.js +4 -3
  37. package/dist/lib/task/create-service.js +5 -3
  38. package/dist/lib/task/create.js +22 -5
  39. package/dist/lib/task/finalization-artifacts.js +4 -1
  40. package/dist/lib/task/finalization.js +243 -113
  41. package/dist/lib/task/lifecycle.js +38 -2
  42. package/dist/lib/task/platform-operation-journal.js +152 -0
  43. package/dist/lib/task/platform-operation-recovery.js +459 -0
  44. package/dist/lib/task/verification.js +0 -1
  45. package/dist/lib/task/workflow-warning-intents.js +7 -1
  46. package/dist/lib/update.js +2 -2
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapters/codex.ts +1 -1
  49. package/lib/agent-clients/config.ts +9 -3
  50. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  51. package/lib/agent-clients/next-steps.ts +3 -3
  52. package/lib/agent-clients/reconcile.ts +3 -3
  53. package/lib/agent-clients/schema.ts +18 -10
  54. package/lib/defaults.json +6 -3
  55. package/lib/init.ts +2 -2
  56. package/lib/internal/agent-client.ts +3 -3
  57. package/lib/internal/cli-route-inventory.ts +2 -0
  58. package/lib/internal/platform-pr-review.ts +14 -3
  59. package/lib/internal/task-operation-registry.ts +3 -1
  60. package/lib/internal/task-platform-recovery.ts +53 -0
  61. package/lib/merge.ts +26 -6
  62. package/lib/platform/completion-backfill.ts +2 -1
  63. package/lib/platform/in-label-sync.ts +12 -0
  64. package/lib/platform/issue-comments.ts +59 -3
  65. package/lib/platform/issues.ts +103 -5
  66. package/lib/platform/operation-coordinator.ts +96 -0
  67. package/lib/platform/pr-review.ts +113 -1
  68. package/lib/platform/pr-summary.ts +22 -1
  69. package/lib/platform/pull-requests.ts +105 -5
  70. package/lib/sandbox/commands/create.ts +7 -8
  71. package/lib/sandbox/commands/ls.ts +4 -3
  72. package/lib/sandbox/commands/rm.ts +1 -1
  73. package/lib/sandbox/config.ts +23 -5
  74. package/lib/sandbox/git-metadata.ts +66 -0
  75. package/lib/sandbox/index.ts +1 -1
  76. package/lib/sandbox/mounts.ts +14 -0
  77. package/lib/sandbox/recovery.ts +13 -25
  78. package/lib/sandbox/removal.ts +57 -19
  79. package/lib/sandbox/tool-types.ts +2 -1
  80. package/lib/sandbox/tools.ts +38 -7
  81. package/lib/task/activity-intent.ts +4 -3
  82. package/lib/task/create-service.ts +5 -3
  83. package/lib/task/create.ts +17 -5
  84. package/lib/task/finalization-artifacts.ts +4 -1
  85. package/lib/task/finalization.ts +242 -106
  86. package/lib/task/lifecycle.ts +35 -3
  87. package/lib/task/platform-operation-journal.ts +230 -0
  88. package/lib/task/platform-operation-recovery.ts +435 -0
  89. package/lib/task/verification.ts +1 -2
  90. package/lib/task/workflow-warning-intents.ts +9 -1
  91. package/lib/update.ts +2 -3
  92. package/package.json +1 -1
  93. package/templates/.agents/README.en.md +20 -66
  94. package/templates/.agents/README.zh-CN.md +19 -65
  95. package/templates/.agents/rules/README.en.md +1 -1
  96. package/templates/.agents/rules/README.zh-CN.md +1 -1
  97. package/templates/.agents/rules/next-step-output.en.md +1 -1
  98. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  99. package/templates/.agents/rules/task-management.en.md +12 -0
  100. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  101. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  102. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
  103. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  106. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  108. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  109. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  110. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  111. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  112. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  113. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  114. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  115. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  116. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  117. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  118. package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
  119. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
  120. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  121. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  122. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  123. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  124. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  125. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  126. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +21 -274
  127. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  128. package/lib/agent-clients/custom-tuis.ts +0 -149
package/README.md CHANGED
@@ -240,7 +240,7 @@ In-depth guides live under [`docs/en/`](./docs/en/README.md):
240
240
  - [Feishu Bridge](./docs/en/feishu-bridge.md) — configure the Feishu long-connection adapter, command routing, permissions, and TUI selection
241
241
  - [Built-in AI Skills](./docs/en/skills.md) — the full skill catalog by use case
242
242
  - [Custom Skills](./docs/en/custom-skills.md) — create and sync project-specific skills
243
- - [Custom TUI Configuration](./docs/en/custom-tui.md) — adapt agent-infra to non-built-in AI TUIs
243
+ - [Custom Tool Invocations](./docs/en/custom-tool-invocations.md) — configure custom-tool next-step commands
244
244
  - [Prebuilt Workflows](./docs/en/workflows.md) — the gated delivery lifecycle and example flow
245
245
  - [Configuration Reference](./docs/en/configuration.md) — `.agents/.airc.json`, external sources, version management
246
246
  - [File Management Strategies](./docs/en/file-management.md) — managed / merged / ejected update strategies
package/README.zh-CN.md CHANGED
@@ -239,7 +239,7 @@ my-project/
239
239
  - [飞书桥接](./docs/zh-CN/feishu-bridge.md) — 配置飞书长连接 adapter、命令路由、权限和 TUI 选择
240
240
  - [内置 AI Skills](./docs/zh-CN/skills.md) — 按使用场景分组的完整 skill 清单
241
241
  - [自定义 Skills](./docs/zh-CN/custom-skills.md) — 创建并同步项目专属 skill
242
- - [自定义 TUI 配置](./docs/zh-CN/custom-tui.md) — 适配非内置的 AI TUI
242
+ - [自定义工具调用](./docs/zh-CN/custom-tool-invocations.md) — 配置自定义工具的下一步命令
243
243
  - [预置工作流](./docs/zh-CN/workflows.md) — 分阶段交付链路与示例流程
244
244
  - [配置参考](./docs/zh-CN/configuration.md) — `.agents/.airc.json`、外部源、版本管理
245
245
  - [文件管理策略](./docs/zh-CN/file-management.md) — managed / merged / ejected 更新策略
@@ -15,6 +15,7 @@ const taskWorkflowCommand = command === 'task-artifact'
15
15
  || command === 'task-ledger'
16
16
  || command === 'task-invalidation'
17
17
  || command === 'task-warning';
18
+ const taskPlatformRecoveryCommand = command === 'task-platform-recovery';
18
19
  const manualValidationWorkflowCommand = command === 'manual-validation';
19
20
  const taskControlMarkers = [
20
21
  'AGENT_INFRA_TASK_ID', 'AGENT_INFRA_CONTROL_TOKEN', 'AGENT_INFRA_CONTROL_GENERATION',
@@ -48,9 +49,9 @@ function taskControlTransportFailure(message, code = 'TASK_CONTROL_TRANSPORT_INV
48
49
  process.exit(1);
49
50
  }
50
51
  let controlRouted = false;
51
- if (!taskViewGuardFailed && !markerlessHelp && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand)) {
52
+ if (!taskViewGuardFailed && !markerlessHelp && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand || taskPlatformRecoveryCommand)) {
52
53
  const transport = resolveSandboxControlTransport(process.env, {
53
- localWorkflow: command === 'task-orchestration' || command === 'task-finalization' || taskWorkflowCommand || manualValidationWorkflowCommand
54
+ localWorkflow: command === 'task-orchestration' || command === 'task-finalization' || taskWorkflowCommand || manualValidationWorkflowCommand || taskPlatformRecoveryCommand
54
55
  });
55
56
  switch (transport.kind) {
56
57
  case 'fail-closed': {
@@ -90,6 +91,11 @@ if (!controlRouted && !taskViewGuardFailed && internalRouteRegistered)
90
91
  await taskFinalization(process.argv.slice(3));
91
92
  break;
92
93
  }
94
+ case 'task-platform-recovery': {
95
+ const { taskPlatformRecovery } = await import('../lib/internal/task-platform-recovery.js');
96
+ await taskPlatformRecovery(process.argv.slice(3));
97
+ break;
98
+ }
93
99
  case 'task-create': {
94
100
  const { taskCreate } = await import('../lib/internal/task-create.js');
95
101
  await taskCreate(process.argv.slice(3));
@@ -50,7 +50,7 @@ const codexAdapter = defineAgentClientAdapter({
50
50
  containerMount: '/home/devuser/.codex',
51
51
  versionCmd: 'codex --version',
52
52
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
53
- tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
53
+ tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'], exec: true },
54
54
  hostLiveMounts: [
55
55
  {
56
56
  hostPath: hostJoin(home, '.codex', 'auth.json'),
@@ -114,10 +114,18 @@ function normalizeAgentClients(input) {
114
114
  if (hasOwn(input, 'tuis'))
115
115
  fail('INVALID_AGENT_CLIENTS', 'tuis');
116
116
  const sandbox = isRecord(input.sandbox) ? input.sandbox : undefined;
117
- if (Array.isArray(sandbox?.tools)) {
118
- for (const [index, tool] of sandbox.tools.entries()) {
117
+ if (sandbox && hasOwn(sandbox, 'customTools')) {
118
+ fail('INVALID_AGENT_CLIENTS', 'sandbox.customTools');
119
+ }
120
+ if (sandbox && hasOwn(sandbox, 'tools')) {
121
+ if (!isRecord(sandbox.tools))
122
+ fail('INVALID_AGENT_CLIENTS', 'sandbox.tools');
123
+ const tools = sandbox.tools;
124
+ if (!Array.isArray(tools.ids))
125
+ fail('INVALID_AGENT_CLIENTS', 'sandbox.tools.ids');
126
+ for (const [index, tool] of tools.ids.entries()) {
119
127
  if (isAgentClientId(tool)) {
120
- fail('INVALID_AGENT_CLIENTS', `sandbox.tools[${index}]`);
128
+ fail('INVALID_AGENT_CLIENTS', `sandbox.tools.ids[${index}]`);
121
129
  }
122
130
  }
123
131
  }
@@ -0,0 +1,92 @@
1
+ const ALLOWED_PLACEHOLDERS = Object.freeze(['skillName', 'projectName']);
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function isNonEmptySingleLine(value) {
6
+ return typeof value === 'string'
7
+ && value.trim() !== ''
8
+ && !/[\r\n]/.test(value);
9
+ }
10
+ function hasValidPlaceholders(invocation) {
11
+ const placeholders = [
12
+ ...invocation.matchAll(/\$\{([^}]+)\}/g)
13
+ ].map((match) => match[1]);
14
+ return placeholders.includes('skillName')
15
+ && placeholders.every((placeholder) => ALLOWED_PLACEHOLDERS.includes(placeholder))
16
+ && !invocation
17
+ .replaceAll('${skillName}', '')
18
+ .replaceAll('${projectName}', '')
19
+ .includes('${');
20
+ }
21
+ function normalizeCustomToolInvocations(config) {
22
+ const items = [];
23
+ const diagnostics = [];
24
+ const root = isRecord(config) ? config : {};
25
+ const sandbox = isRecord(root.sandbox) ? root.sandbox : {};
26
+ const sandboxTools = isRecord(sandbox.tools) ? sandbox.tools : { ids: ['agent-infra'] };
27
+ const definitions = sandboxTools.definitions ?? {};
28
+ const selectedIds = sandboxTools.ids;
29
+ if (!Array.isArray(selectedIds)) {
30
+ return Object.freeze({
31
+ items: Object.freeze([]),
32
+ diagnostics: Object.freeze([
33
+ Object.freeze({
34
+ code: 'INVALID_CUSTOM_TOOL_CONFIG',
35
+ path: 'sandbox.tools.ids'
36
+ })
37
+ ])
38
+ });
39
+ }
40
+ if (!isRecord(definitions)) {
41
+ return Object.freeze({
42
+ items: Object.freeze([]),
43
+ diagnostics: Object.freeze([
44
+ Object.freeze({
45
+ code: 'INVALID_CUSTOM_TOOL_CONFIG',
46
+ path: 'sandbox.tools.definitions'
47
+ })
48
+ ])
49
+ });
50
+ }
51
+ for (const [index, id] of selectedIds.entries()) {
52
+ if (typeof id !== 'string') {
53
+ diagnostics.push({
54
+ code: 'INVALID_CUSTOM_TOOL_INVOCATION',
55
+ path: `sandbox.tools.ids[${index}]`
56
+ });
57
+ continue;
58
+ }
59
+ const candidate = definitions[id];
60
+ if (!isRecord(candidate))
61
+ continue;
62
+ if (candidate.invoke === undefined)
63
+ continue;
64
+ const base = `sandbox.tools.definitions.${id}`;
65
+ const name = candidate.name === undefined ? id : candidate.name;
66
+ if (!isNonEmptySingleLine(name)) {
67
+ diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.name` });
68
+ continue;
69
+ }
70
+ if (!isNonEmptySingleLine(candidate.invoke)) {
71
+ diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.invoke` });
72
+ continue;
73
+ }
74
+ if (!hasValidPlaceholders(candidate.invoke)) {
75
+ diagnostics.push({
76
+ code: 'INVALID_CUSTOM_TOOL_INVOCATION_PLACEHOLDER',
77
+ path: `${base}.invoke`
78
+ });
79
+ continue;
80
+ }
81
+ items.push(Object.freeze({
82
+ name,
83
+ invocation: candidate.invoke
84
+ }));
85
+ }
86
+ return Object.freeze({
87
+ items: Object.freeze(items),
88
+ diagnostics: Object.freeze(diagnostics.map((diagnostic) => Object.freeze(diagnostic)))
89
+ });
90
+ }
91
+ export { normalizeCustomToolInvocations };
92
+ //# sourceMappingURL=custom-tool-invocations.js.map
@@ -37,7 +37,7 @@ function renderNextStepCommands(input) {
37
37
  args
38
38
  })
39
39
  }));
40
- const custom = input.customTUIs.map((tool) => Object.freeze({
40
+ const custom = input.customToolInvocations.map((tool) => Object.freeze({
41
41
  source: 'custom',
42
42
  displayName: tool.name,
43
43
  command: renderAgentClientInvocation(tool.invocation, {
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import defaults from '../defaults.json' with { type: 'json' };
5
5
  import { normalizeAgentClients, serializeAgentClients } from './config.js';
6
- import { normalizeCustomTUIs } from './custom-tuis.js';
6
+ import { normalizeCustomToolInvocations } from './custom-tool-invocations.js';
7
7
  import { renderNextStepCommands } from './next-steps.js';
8
8
  import { planAgentClientProjectAssets } from './project-assets.js';
9
9
  import { listAgentClientAdapters, listEnabledAgentClientAdapters } from './registry.js';
@@ -141,11 +141,11 @@ function planAgentClientReconciliation(input) {
141
141
  if (Object.keys(baselines).length === 0) {
142
142
  delete nextConfig.files.managedBaselines;
143
143
  }
144
- const custom = normalizeCustomTUIs(input.projectRoot, input.config.customTUIs ?? []);
144
+ const custom = normalizeCustomToolInvocations(input.config);
145
145
  const nextSteps = renderNextStepCommands({
146
146
  projectName,
147
147
  state: desired,
148
- customTUIs: custom.items,
148
+ customToolInvocations: custom.items,
149
149
  skillName: 'update-agent-infra'
150
150
  });
151
151
  const actionableSeed = seedOperations.some((operation) => operation.kind === 'write' || operation.kind === 'remove');
@@ -38,16 +38,24 @@ const AGENT_CLIENTS_SCHEMA = {
38
38
  }
39
39
  }))
40
40
  },
41
- customTUIs: {
42
- type: 'array',
43
- items: {
44
- type: 'object',
45
- additionalProperties: true,
46
- required: ['name', 'dir', 'invoke'],
47
- properties: {
48
- name: { type: 'string' },
49
- dir: { type: 'string' },
50
- invoke: { type: 'string' }
41
+ sandbox: {
42
+ type: 'object',
43
+ properties: {
44
+ tools: {
45
+ type: 'object',
46
+ properties: {
47
+ ids: { type: 'array', items: { type: 'string' } },
48
+ definitions: {
49
+ type: 'object',
50
+ additionalProperties: {
51
+ type: 'object',
52
+ properties: {
53
+ name: { type: 'string' },
54
+ invoke: { type: 'string' }
55
+ }
56
+ }
57
+ }
58
+ }
51
59
  }
52
60
  }
53
61
  }
@@ -34,9 +34,12 @@
34
34
  "runtimes": [
35
35
  "node22"
36
36
  ],
37
- "tools": [
38
- "agent-infra"
39
- ],
37
+ "tools": {
38
+ "ids": [
39
+ "agent-infra"
40
+ ],
41
+ "definitions": {}
42
+ },
40
43
  "refreshIntervalDays": 7,
41
44
  "dockerfile": null,
42
45
  "vm": {
package/dist/lib/init.js CHANGED
@@ -228,8 +228,8 @@ async function cmdInit() {
228
228
  console.log(' npm install -g @fitlab-ai/agent-infra');
229
229
  console.log('');
230
230
  if (workflowPlan.nextSteps.length === 0) {
231
- console.log(' No Agent Client project integration enabled.');
232
- console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
231
+ console.log(' No Agent Client or sandbox custom tool integration enabled.');
232
+ console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${configPath}.`);
233
233
  console.log('');
234
234
  }
235
235
  else {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { AgentClientConfigError, normalizeAgentClients } from '../agent-clients/config.js';
4
- import { normalizeCustomTUIs } from '../agent-clients/custom-tuis.js';
4
+ import { normalizeCustomToolInvocations } from '../agent-clients/custom-tool-invocations.js';
5
5
  import { renderNextStepCommands } from '../agent-clients/next-steps.js';
6
6
  import { getAgentClientModelSelection } from '../agent-clients/registry.js';
7
7
  import { isAgentClientId } from '../agent-clients/types.js';
@@ -139,11 +139,11 @@ function agentClient(args = []) {
139
139
  }
140
140
  try {
141
141
  const clients = normalizeAgentClients(config);
142
- const custom = normalizeCustomTUIs(process.cwd(), config.customTUIs ?? []);
142
+ const custom = normalizeCustomToolInvocations(config);
143
143
  const commands = renderNextStepCommands({
144
144
  projectName: String(config.project ?? ''),
145
145
  state: clients.state,
146
- customTUIs: custom.items,
146
+ customToolInvocations: custom.items,
147
147
  skillName: parsed.skillName,
148
148
  ...(parsed.taskRef ? { taskRef: parsed.taskRef } : {}),
149
149
  ...(parsed.version ? { version: parsed.version } : {})
@@ -60,6 +60,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
60
60
  'task-invalidation': ['reconcile'],
61
61
  'task-lifecycle': ['intent'],
62
62
  'task-finalization': ['complete'],
63
+ 'task-platform-recovery': ['inspect', 'recover'],
63
64
  'task-short-id': ['list', 'list-verify', 'alloc', 'release', 'resolve'],
64
65
  'task-snapshot': ['snapshot'],
65
66
  'task-verify': ['event'],
@@ -88,6 +89,8 @@ export function internalRouteSelector(command, args) {
88
89
  return args[1] ? 'intent' : '';
89
90
  if (command === 'task-finalization')
90
91
  return args[1] === 'complete' ? 'complete' : '';
92
+ if (command === 'task-platform-recovery')
93
+ return args[1] ?? '';
91
94
  if (command === 'task-event')
92
95
  return args[1] ? 'event' : '';
93
96
  if (command === 'task-verify')
@@ -3,12 +3,13 @@ import path from 'node:path';
3
3
  import { inspectPlatformPullRequestByNumber } from '../platform/pull-requests.js';
4
4
  import { listPrReviews, publishPrReview } from '../platform/pr-review.js';
5
5
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
6
+ import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
6
7
  const USAGE = `Usage: agent-infra-internal platform-pr-review inspect --pr <token> [--cwd <path>]
7
8
  agent-infra-internal platform-pr-review list --pr <token> [--cwd <path>]
8
- agent-infra-internal platform-pr-review publish --pr <token> --scope <taskId|pr{N}> --round <N> --commit <sha> --event <COMMENT|APPROVE|REQUEST_CHANGES> --body-file <path|-> [--dry-run] [--cwd <path>]
9
+ agent-infra-internal platform-pr-review publish --pr <token> --scope <taskId|pr{N}> --round <N> --commit <sha> --event <COMMENT|APPROVE|REQUEST_CHANGES> --body-file <path|-> [--artifact <pr-review-artifact>] [--agent <agent>] [--dry-run] [--cwd <path>]
9
10
  `;
10
11
  const BOOLEAN_FLAGS = new Set(['--dry-run']);
11
- const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file']);
12
+ const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file', '--artifact', '--agent']);
12
13
  function key(flag) {
13
14
  return flag.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
14
15
  }
@@ -74,7 +75,7 @@ async function platformPrReview(args = []) {
74
75
  const allowed = {
75
76
  inspect: ['cwd', 'pr'],
76
77
  list: ['cwd', 'pr'],
77
- publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'dryRun']
78
+ publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'artifact', 'agent', 'dryRun']
78
79
  };
79
80
  const unexpected = Object.keys(values).find((name) => !allowed[operation].includes(name));
80
81
  if (unexpected) {
@@ -99,6 +100,8 @@ async function platformPrReview(args = []) {
99
100
  const round = Number(values.round);
100
101
  const commit = typeof values.commit === 'string' ? values.commit : '';
101
102
  const event = values.event;
103
+ const rawAgent = typeof values.agent === 'string' ? values.agent : '';
104
+ const agent = rawAgent ? normalizeAgentToken(rawAgent) : null;
102
105
  if (!scope || !/^(?:pr\d+|TASK-\d{8}-\d{6})$/.test(scope)) {
103
106
  fail('publish requires --scope <taskId|pr{N}>');
104
107
  return;
@@ -124,6 +127,18 @@ async function platformPrReview(args = []) {
124
127
  fail('publish requires --body-file');
125
128
  return;
126
129
  }
130
+ if (/^TASK-\d{8}-\d{6}$/u.test(scope) && typeof values.artifact !== 'string') {
131
+ fail('task-scoped publish requires --artifact <pr-review-artifact>');
132
+ return;
133
+ }
134
+ if (/^TASK-\d{8}-\d{6}$/u.test(scope) && !agent) {
135
+ fail(`task-scoped publish requires --agent <agent>${rawAgent ? `: invalid '${rawAgent}'` : ''}: ${AGENT_USAGE_HINT}`);
136
+ return;
137
+ }
138
+ if (rawAgent && !agent) {
139
+ fail(`invalid --agent '${rawAgent}': ${AGENT_USAGE_HINT}`);
140
+ return;
141
+ }
127
142
  let body;
128
143
  try {
129
144
  body = readBodyFile(values.bodyFile, cwd);
@@ -138,6 +153,8 @@ async function platformPrReview(args = []) {
138
153
  identity: { scope, round, commitSha: commit },
139
154
  event: event,
140
155
  body,
156
+ ...(typeof values.artifact === 'string' ? { recoveryArtifact: values.artifact } : {}),
157
+ ...(agent ? { agent } : {}),
141
158
  dryRun: values.dryRun === true
142
159
  }));
143
160
  }
@@ -105,6 +105,8 @@ function internalTaskRoutes() {
105
105
  descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
106
106
  descriptor('internal', 'task-lifecycle', 'intent', 'task-bound', 'progress'),
107
107
  descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'progress'),
108
+ descriptor('internal', 'task-platform-recovery', 'inspect', 'task-bound', 'diagnostic'),
109
+ descriptor('internal', 'task-platform-recovery', 'recover', 'task-bound', 'recovery'),
108
110
  descriptor('internal', 'task-short-id', 'list', 'conditional', 'diagnostic'),
109
111
  descriptor('internal', 'task-short-id', 'list-verify', 'conditional', 'diagnostic'),
110
112
  descriptor('internal', 'task-short-id', 'alloc', 'task-bound', 'progress'),
@@ -178,7 +180,7 @@ export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
178
180
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
179
181
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
180
182
  'task-orchestration', 'task-review', 'task-event', 'task-invalidation', 'task-lifecycle',
181
- 'task-finalization', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
183
+ 'task-finalization', 'task-platform-recovery', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
182
184
  ]);
183
185
  export const PUBLIC_DISPATCHER_ROUTES = Object.freeze([
184
186
  'agent-client', 'cp', 'data', 'decide', 'help', 'init', 'merge', 'run', 'sandbox', 'server', 'task', 'sync', 'update', 'version'
@@ -0,0 +1,56 @@
1
+ import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
2
+ import { readPlatformOperationJournal } from '../task/platform-operation-journal.js';
3
+ import { recoverPlatformOperations } from '../task/platform-operation-recovery.js';
4
+ import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
5
+ const USAGE = 'Usage: agent-infra-internal task-platform-recovery <N | TASK-id> <inspect | recover> [--agent <agent>] [--selection required|deferred|all]\n';
6
+ function option(args, name) {
7
+ const index = args.indexOf(name);
8
+ return index >= 0 ? args[index + 1] : undefined;
9
+ }
10
+ async function taskPlatformRecovery(args = []) {
11
+ if (args[0] === '--help' || args[0] === '-h') {
12
+ process.stdout.write(USAGE);
13
+ return;
14
+ }
15
+ if (!ensureInternalHandlerRoute('task-platform-recovery', args))
16
+ return;
17
+ const action = args[1];
18
+ const registered = action === 'inspect'
19
+ ? internalHandlerRoute('task-platform-recovery', 'inspect', action)
20
+ : action === 'recover' && internalHandlerRoute('task-platform-recovery', 'recover', action);
21
+ if (!registered)
22
+ return;
23
+ const repoRoot = detectRepoRoot();
24
+ const task = resolveTaskRef(args[0] ?? '', { repoRoot });
25
+ if (!task.ok) {
26
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: task.code, message: task.message } })}\n`);
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ if (action === 'inspect') {
31
+ try {
32
+ const journal = readPlatformOperationJournal(task.taskId, repoRoot);
33
+ process.stdout.write(`${JSON.stringify({ status: 'ready', changed: false, taskId: task.taskId, journal })}\n`);
34
+ }
35
+ catch (error) {
36
+ const value = error;
37
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error) } })}\n`);
38
+ process.exitCode = 1;
39
+ }
40
+ return;
41
+ }
42
+ const agent = option(args, '--agent');
43
+ const selection = option(args, '--selection') ?? 'all';
44
+ if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(agent)
45
+ || !['required', 'deferred', 'all'].includes(selection)) {
46
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'recover requires a valid --agent and --selection' } })}\n`);
47
+ process.stderr.write(USAGE);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ const result = await recoverPlatformOperations(task.taskId, selection, { agent, cwd: repoRoot });
52
+ process.stdout.write(`${JSON.stringify(result)}\n`);
53
+ process.exitCode = result.status === 'applied' || result.status === 'no-op' ? 0 : result.status === 'blocked' ? 2 : 1;
54
+ }
55
+ export { taskPlatformRecovery };
56
+ //# sourceMappingURL=task-platform-recovery.js.map
package/dist/lib/merge.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import crypto from 'node:crypto';
4
5
  import { removeDirRecursive } from './remove-dir.js';
@@ -305,6 +306,23 @@ function formatBackupTimestamp(date) {
305
306
  String(date.getDate()).padStart(2, '0')
306
307
  ].join('') + `-${String(date.getHours()).padStart(2, '0')}${String(date.getMinutes()).padStart(2, '0')}${String(date.getSeconds()).padStart(2, '0')}`;
307
308
  }
309
+ function resolveRecoveryProjectKey(repoRoot) {
310
+ let projectKey;
311
+ try {
312
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
313
+ if (typeof config.project === 'string' && config.project.trim() !== '') {
314
+ projectKey = config.project.trim();
315
+ }
316
+ }
317
+ catch {
318
+ // Fall back to the repository directory when project config is unavailable.
319
+ }
320
+ projectKey ??= path.basename(repoRoot);
321
+ if (!/^[A-Za-z0-9_.@-]+$/.test(projectKey) || projectKey === '.' || projectKey === '..') {
322
+ throw new Error(`Invalid project name for recovery backups: ${projectKey}`);
323
+ }
324
+ return projectKey;
325
+ }
308
326
  function toPosixPath(relativePath) {
309
327
  return relativePath.split(path.sep).join('/');
310
328
  }
@@ -400,7 +418,7 @@ function buildWorkspaceIndex(workspaceDir) {
400
418
  function backupTaskDir(backupRoot, section, taskDir, taskId) {
401
419
  const backupDir = path.join(backupRoot, section, taskId);
402
420
  fs.mkdirSync(path.dirname(backupDir), { recursive: true });
403
- fs.cpSync(taskDir, backupDir, { recursive: true });
421
+ fs.cpSync(taskDir, backupDir, { recursive: true, verbatimSymlinks: true });
404
422
  return backupDir;
405
423
  }
406
424
  function copyTaskToSection(sourceTask, workspaceDir) {
@@ -872,12 +890,14 @@ async function cmdMerge(args) {
872
890
  if (!fs.lstatSync(resolvedSource).isDirectory()) {
873
891
  throw new Error(`Source path is not a directory: ${sourcePath}`);
874
892
  }
875
- const workspaceDir = path.join(process.cwd(), '.agents', 'workspace');
893
+ const repoRoot = process.cwd();
894
+ const workspaceDir = path.join(repoRoot, '.agents', 'workspace');
876
895
  const archiveDir = path.join(workspaceDir, 'archive');
877
896
  const backupStamp = formatBackupTimestamp(new Date());
878
- const backupRootRelative = `.agents/workspace/.merge-backup/${backupStamp}/`;
879
- const backupRoot = path.join(workspaceDir, '.merge-backup', backupStamp);
880
- const report = createReport(resolvedSource, backupRootRelative);
897
+ const projectKey = resolveRecoveryProjectKey(repoRoot);
898
+ const recoveryRoot = path.join(os.homedir(), '.agent-infra', 'recovery-backups', projectKey, 'workspace-merge');
899
+ const backupRoot = path.join(recoveryRoot, backupStamp);
900
+ const report = createReport(resolvedSource, backupRoot);
881
901
  detectSourceMode(resolvedSource);
882
902
  validateSourceWorkspace(resolvedSource);
883
903
  validateArchiveSection(workspaceDir, new Map());
@@ -19,7 +19,7 @@ function referencedBackfillArtifact(taskDir, message) {
19
19
  if (names.length !== 1)
20
20
  return { status: 'ignored', artifact: null, error: null };
21
21
  const parsed = parseArtifactName(names[0]);
22
- if (!parsed || (parsed.family !== 'pr-review' && !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family))) {
22
+ if (!parsed || !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family)) {
23
23
  return { status: 'ignored', artifact: null, error: null };
24
24
  }
25
25
  const artifactPath = path.join(taskDir, parsed.name);
@@ -112,6 +112,7 @@ async function backfillCompletionComments(taskRef, options) {
112
112
  artifact: artifact.name,
113
113
  agent: options.agent,
114
114
  backfill: true,
115
+ dependency: 'required',
115
116
  cwd: resolved.repoRoot,
116
117
  client: options.client
117
118
  });
@@ -1,4 +1,5 @@
1
1
  import { computeInLabels } from './metadata-labels.js';
2
+ import { createHash } from 'node:crypto';
2
3
  function inLabels(labels) {
3
4
  return [...new Set(labels.filter((label) => label.startsWith('in:')))].sort();
4
5
  }
@@ -26,6 +27,16 @@ function validateInLabelMapping(value) {
26
27
  }
27
28
  return { ok: true, value: normalized };
28
29
  }
30
+ function inLabelMappingDigest(value) {
31
+ const mapping = validateInLabelMapping(value);
32
+ if (!mapping.ok)
33
+ return mapping;
34
+ const canonical = Object.fromEntries(Object.keys(mapping.value).sort().map((name) => [
35
+ name,
36
+ [...new Set(mapping.value[name])].sort()
37
+ ]));
38
+ return { ok: true, digest: createHash('sha256').update(JSON.stringify(canonical)).digest('hex') };
39
+ }
29
40
  function repositoryLabelPages(value) {
30
41
  if (!Array.isArray(value))
31
42
  return null;
@@ -139,5 +150,5 @@ function extractPullRequestFileNames(value) {
139
150
  function resourceSnapshot(kind, number, labels) {
140
151
  return { kind, number, labels: [...labels].sort() };
141
152
  }
142
- export { extractPullRequestFileNames, extractRepositoryLabelNames, flattenPages, inLabels, labelDelta, mergeInLabels, planInLabelUpdate, resourceSnapshot, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload };
153
+ export { extractPullRequestFileNames, extractRepositoryLabelNames, flattenPages, inLabels, inLabelMappingDigest, labelDelta, mergeInLabels, planInLabelUpdate, resourceSnapshot, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload };
143
154
  //# sourceMappingURL=in-label-sync.js.map