@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
@@ -132,8 +132,8 @@ async function cmdSync() {
132
132
  ok('Seed files synced successfully!');
133
133
  console.log('');
134
134
  if (workflowPlan.nextSteps.length === 0) {
135
- console.log(' No Agent Client project integration enabled.');
136
- console.log(` Configure "customTUIs" in ${CONFIG_PATH} if needed.`);
135
+ console.log(' No Agent Client or sandbox custom tool integration enabled.');
136
+ console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${CONFIG_PATH}.`);
137
137
  console.log('');
138
138
  }
139
139
  else {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.7",
4
- "sourceInputHash": "095f0cf30aa29fcd2dd45123dc755e5097d7a9f81de33420461fd943cff122b2",
5
- "internalExecutableBuildHash": "a956f2ed366e025a111a87bc1f542b87d83d4025e068304f95b1aae704709d3f"
3
+ "packageVersion": "0.11.9",
4
+ "sourceInputHash": "17fe6c91408c93e3aa5185e816025d590ded7db7e6a305fdce00751e78723abc",
5
+ "internalExecutableBuildHash": "8de9870e4ece1050564d7a3e77ee211a789dd975e3f94593300b7362ed815a00"
6
6
  }
@@ -54,7 +54,7 @@ const codexAdapter = defineAgentClientAdapter({
54
54
  containerMount: '/home/devuser/.codex',
55
55
  versionCmd: 'codex --version',
56
56
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
57
- tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
57
+ tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'], exec: true },
58
58
  hostLiveMounts: [
59
59
  {
60
60
  hostPath: hostJoin(home, '.codex', 'auth.json'),
@@ -167,10 +167,16 @@ function normalizeAgentClients(
167
167
  }
168
168
  if (hasOwn(input, 'tuis')) fail('INVALID_AGENT_CLIENTS', 'tuis');
169
169
  const sandbox = isRecord(input.sandbox) ? input.sandbox : undefined;
170
- if (Array.isArray(sandbox?.tools)) {
171
- for (const [index, tool] of sandbox.tools.entries()) {
170
+ if (sandbox && hasOwn(sandbox, 'customTools')) {
171
+ fail('INVALID_AGENT_CLIENTS', 'sandbox.customTools');
172
+ }
173
+ if (sandbox && hasOwn(sandbox, 'tools')) {
174
+ if (!isRecord(sandbox.tools)) fail('INVALID_AGENT_CLIENTS', 'sandbox.tools');
175
+ const tools = sandbox.tools;
176
+ if (!Array.isArray(tools.ids)) fail('INVALID_AGENT_CLIENTS', 'sandbox.tools.ids');
177
+ for (const [index, tool] of tools.ids.entries()) {
172
178
  if (isAgentClientId(tool)) {
173
- fail('INVALID_AGENT_CLIENTS', `sandbox.tools[${index}]`);
179
+ fail('INVALID_AGENT_CLIENTS', `sandbox.tools.ids[${index}]`);
174
180
  }
175
181
  }
176
182
  }
@@ -0,0 +1,129 @@
1
+ type CustomToolInvocation = Readonly<{
2
+ name: string;
3
+ invocation: string;
4
+ }>;
5
+
6
+ type CustomToolInvocationDiagnosticCode =
7
+ | 'INVALID_CUSTOM_TOOL_CONFIG'
8
+ | 'INVALID_CUSTOM_TOOL_INVOCATION'
9
+ | 'INVALID_CUSTOM_TOOL_INVOCATION_PLACEHOLDER';
10
+
11
+ type CustomToolInvocationDiagnostic = Readonly<{
12
+ code: CustomToolInvocationDiagnosticCode;
13
+ path: string;
14
+ }>;
15
+
16
+ type NormalizeCustomToolInvocationsResult = Readonly<{
17
+ items: readonly CustomToolInvocation[];
18
+ diagnostics: readonly CustomToolInvocationDiagnostic[];
19
+ }>;
20
+
21
+ const ALLOWED_PLACEHOLDERS = Object.freeze(['skillName', 'projectName']);
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ function isNonEmptySingleLine(value: unknown): value is string {
28
+ return typeof value === 'string'
29
+ && value.trim() !== ''
30
+ && !/[\r\n]/.test(value);
31
+ }
32
+
33
+ function hasValidPlaceholders(invocation: string): boolean {
34
+ const placeholders = [
35
+ ...invocation.matchAll(/\$\{([^}]+)\}/g)
36
+ ].map((match) => match[1]!);
37
+ return placeholders.includes('skillName')
38
+ && placeholders.every((placeholder) =>
39
+ ALLOWED_PLACEHOLDERS.includes(placeholder)
40
+ )
41
+ && !invocation
42
+ .replaceAll('${skillName}', '')
43
+ .replaceAll('${projectName}', '')
44
+ .includes('${');
45
+ }
46
+
47
+ function normalizeCustomToolInvocations(config: unknown): NormalizeCustomToolInvocationsResult {
48
+ const items: CustomToolInvocation[] = [];
49
+ const diagnostics: CustomToolInvocationDiagnostic[] = [];
50
+
51
+ const root = isRecord(config) ? config : {};
52
+ const sandbox = isRecord(root.sandbox) ? root.sandbox : {};
53
+ const sandboxTools = isRecord(sandbox.tools) ? sandbox.tools : { ids: ['agent-infra'] };
54
+ const definitions = sandboxTools.definitions ?? {};
55
+ const selectedIds = sandboxTools.ids;
56
+
57
+ if (!Array.isArray(selectedIds)) {
58
+ return Object.freeze({
59
+ items: Object.freeze([]),
60
+ diagnostics: Object.freeze([
61
+ Object.freeze({
62
+ code: 'INVALID_CUSTOM_TOOL_CONFIG' as const,
63
+ path: 'sandbox.tools.ids'
64
+ })
65
+ ])
66
+ });
67
+ }
68
+
69
+ if (!isRecord(definitions)) {
70
+ return Object.freeze({
71
+ items: Object.freeze([]),
72
+ diagnostics: Object.freeze([
73
+ Object.freeze({
74
+ code: 'INVALID_CUSTOM_TOOL_CONFIG' as const,
75
+ path: 'sandbox.tools.definitions'
76
+ })
77
+ ])
78
+ });
79
+ }
80
+
81
+ for (const [index, id] of selectedIds.entries()) {
82
+ if (typeof id !== 'string') {
83
+ diagnostics.push({
84
+ code: 'INVALID_CUSTOM_TOOL_INVOCATION',
85
+ path: `sandbox.tools.ids[${index}]`
86
+ });
87
+ continue;
88
+ }
89
+ const candidate = definitions[id];
90
+ if (!isRecord(candidate)) continue;
91
+ if (candidate.invoke === undefined) continue;
92
+ const base = `sandbox.tools.definitions.${id}`;
93
+ const name = candidate.name === undefined ? id : candidate.name;
94
+ if (!isNonEmptySingleLine(name)) {
95
+ diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.name` });
96
+ continue;
97
+ }
98
+ if (!isNonEmptySingleLine(candidate.invoke)) {
99
+ diagnostics.push({ code: 'INVALID_CUSTOM_TOOL_INVOCATION', path: `${base}.invoke` });
100
+ continue;
101
+ }
102
+ if (!hasValidPlaceholders(candidate.invoke)) {
103
+ diagnostics.push({
104
+ code: 'INVALID_CUSTOM_TOOL_INVOCATION_PLACEHOLDER',
105
+ path: `${base}.invoke`
106
+ });
107
+ continue;
108
+ }
109
+ items.push(Object.freeze({
110
+ name,
111
+ invocation: candidate.invoke
112
+ }));
113
+ }
114
+
115
+ return Object.freeze({
116
+ items: Object.freeze(items),
117
+ diagnostics: Object.freeze(
118
+ diagnostics.map((diagnostic) => Object.freeze(diagnostic))
119
+ )
120
+ });
121
+ }
122
+
123
+ export { normalizeCustomToolInvocations };
124
+ export type {
125
+ CustomToolInvocation,
126
+ CustomToolInvocationDiagnostic,
127
+ CustomToolInvocationDiagnosticCode,
128
+ NormalizeCustomToolInvocationsResult
129
+ };
@@ -2,7 +2,7 @@ import { listEnabledAgentClientAdapters } from './registry.ts';
2
2
  import { renderAgentClientInvocation } from './invocation.ts';
3
3
  import semver from 'semver';
4
4
  import { isTaskScopeSkill } from '../task/skill-scope.ts';
5
- import type { CustomTUI } from './custom-tuis.ts';
5
+ import type { CustomToolInvocation } from './custom-tool-invocations.ts';
6
6
  import type { AgentClientId, AgentClientState } from './types.ts';
7
7
 
8
8
  type NextStepCommand = Readonly<{
@@ -15,7 +15,7 @@ type NextStepCommand = Readonly<{
15
15
  type RenderNextStepsInput = Readonly<{
16
16
  projectName: string;
17
17
  state: AgentClientState;
18
- customTUIs: readonly CustomTUI[];
18
+ customToolInvocations: readonly CustomToolInvocation[];
19
19
  skillName: string;
20
20
  taskRef?: string;
21
21
  version?: string;
@@ -69,7 +69,7 @@ function renderNextStepCommands(
69
69
  )
70
70
  })
71
71
  );
72
- const custom = input.customTUIs.map(
72
+ const custom = input.customToolInvocations.map(
73
73
  (tool): NextStepCommand => Object.freeze({
74
74
  source: 'custom',
75
75
  displayName: tool.name,
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
 
5
5
  import defaults from '../defaults.json' with { type: 'json' };
6
6
  import { normalizeAgentClients, serializeAgentClients } from './config.ts';
7
- import { normalizeCustomTUIs } from './custom-tuis.ts';
7
+ import { normalizeCustomToolInvocations } from './custom-tool-invocations.ts';
8
8
  import { renderNextStepCommands } from './next-steps.ts';
9
9
  import { planAgentClientProjectAssets } from './project-assets.ts';
10
10
  import {
@@ -216,11 +216,11 @@ function planAgentClientReconciliation(input: Readonly<{
216
216
  delete (nextConfig.files as Record<string, unknown>).managedBaselines;
217
217
  }
218
218
 
219
- const custom = normalizeCustomTUIs(input.projectRoot, input.config.customTUIs ?? []);
219
+ const custom = normalizeCustomToolInvocations(input.config);
220
220
  const nextSteps = renderNextStepCommands({
221
221
  projectName,
222
222
  state: desired,
223
- customTUIs: custom.items,
223
+ customToolInvocations: custom.items,
224
224
  skillName: 'update-agent-infra'
225
225
  });
226
226
  const actionableSeed = seedOperations.some((operation) =>
@@ -43,16 +43,24 @@ const AGENT_CLIENTS_SCHEMA = {
43
43
  }
44
44
  }))
45
45
  },
46
- customTUIs: {
47
- type: 'array',
48
- items: {
49
- type: 'object',
50
- additionalProperties: true,
51
- required: ['name', 'dir', 'invoke'],
52
- properties: {
53
- name: { type: 'string' },
54
- dir: { type: 'string' },
55
- invoke: { type: 'string' }
46
+ sandbox: {
47
+ type: 'object',
48
+ properties: {
49
+ tools: {
50
+ type: 'object',
51
+ properties: {
52
+ ids: { type: 'array', items: { type: 'string' } },
53
+ definitions: {
54
+ type: 'object',
55
+ additionalProperties: {
56
+ type: 'object',
57
+ properties: {
58
+ name: { type: 'string' },
59
+ invoke: { type: 'string' }
60
+ }
61
+ }
62
+ }
63
+ }
56
64
  }
57
65
  }
58
66
  }
package/lib/defaults.json CHANGED
@@ -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/lib/init.ts CHANGED
@@ -314,8 +314,8 @@ async function cmdInit(): Promise<void> {
314
314
  console.log(' npm install -g @fitlab-ai/agent-infra');
315
315
  console.log('');
316
316
  if (workflowPlan.nextSteps.length === 0) {
317
- console.log(' No Agent Client project integration enabled.');
318
- console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
317
+ console.log(' No Agent Client or sandbox custom tool integration enabled.');
318
+ console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${configPath}.`);
319
319
  console.log('');
320
320
  } else {
321
321
  console.log(' Next step: open this project in any AI TUI and run:');
@@ -5,7 +5,7 @@ import {
5
5
  AgentClientConfigError,
6
6
  normalizeAgentClients
7
7
  } from '../agent-clients/config.ts';
8
- import { normalizeCustomTUIs } from '../agent-clients/custom-tuis.ts';
8
+ import { normalizeCustomToolInvocations } from '../agent-clients/custom-tool-invocations.ts';
9
9
  import { renderNextStepCommands } from '../agent-clients/next-steps.ts';
10
10
  import { getAgentClientModelSelection } from '../agent-clients/registry.ts';
11
11
  import { isAgentClientId } from '../agent-clients/types.ts';
@@ -148,11 +148,11 @@ function agentClient(args: string[] = []): void {
148
148
 
149
149
  try {
150
150
  const clients = normalizeAgentClients(config);
151
- const custom = normalizeCustomTUIs(process.cwd(), config.customTUIs ?? []);
151
+ const custom = normalizeCustomToolInvocations(config);
152
152
  const commands = renderNextStepCommands({
153
153
  projectName: String(config.project ?? ''),
154
154
  state: clients.state,
155
- customTUIs: custom.items,
155
+ customToolInvocations: custom.items,
156
156
  skillName: parsed.skillName,
157
157
  ...(parsed.taskRef ? { taskRef: parsed.taskRef } : {}),
158
158
  ...(parsed.version ? { version: parsed.version } : {})
@@ -63,6 +63,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
63
63
  'task-invalidation': ['reconcile'],
64
64
  'task-lifecycle': ['intent'],
65
65
  'task-finalization': ['complete'],
66
+ 'task-platform-recovery': ['inspect', 'recover'],
66
67
  'task-short-id': ['list', 'list-verify', 'alloc', 'release', 'resolve'],
67
68
  'task-snapshot': ['snapshot'],
68
69
  'task-verify': ['event'],
@@ -89,6 +90,7 @@ export function internalRouteSelector(command: string, args: readonly string[]):
89
90
  if (command === 'task-qualification') return args[1] ?? '';
90
91
  if (command === 'task-lifecycle') return args[1] ? 'intent' : '';
91
92
  if (command === 'task-finalization') return args[1] === 'complete' ? 'complete' : '';
93
+ if (command === 'task-platform-recovery') return args[1] ?? '';
92
94
  if (command === 'task-event') return args[1] ? 'event' : '';
93
95
  if (command === 'task-verify') return args[0] && args[1] ? 'event' : '';
94
96
  if (command === 'task-snapshot') return 'snapshot';
@@ -6,14 +6,15 @@ import { listPrReviews, publishPrReview } from '../platform/pr-review.ts';
6
6
  import type { PrReviewEvent } from '../platform/pr-review.ts';
7
7
  import type { PlatformResult } from '../platform/types.ts';
8
8
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
9
+ import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
9
10
 
10
11
  const USAGE = `Usage: agent-infra-internal platform-pr-review inspect --pr <token> [--cwd <path>]
11
12
  agent-infra-internal platform-pr-review list --pr <token> [--cwd <path>]
12
- 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>]
13
+ 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>]
13
14
  `;
14
15
 
15
16
  const BOOLEAN_FLAGS = new Set(['--dry-run']);
16
- const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file']);
17
+ const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file', '--artifact', '--agent']);
17
18
 
18
19
  function key(flag: string): string {
19
20
  return flag.slice(2).replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
@@ -67,7 +68,7 @@ async function platformPrReview(args: string[] = []): Promise<void> {
67
68
  const allowed: Record<string, string[]> = {
68
69
  inspect: ['cwd', 'pr'],
69
70
  list: ['cwd', 'pr'],
70
- publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'dryRun']
71
+ publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'artifact', 'agent', 'dryRun']
71
72
  };
72
73
  const unexpected = Object.keys(values).find((name) => !allowed[operation]!.includes(name));
73
74
  if (unexpected) { fail(`${operation} does not accept --${unexpected}`); return; }
@@ -86,6 +87,8 @@ async function platformPrReview(args: string[] = []): Promise<void> {
86
87
  const round = Number(values.round);
87
88
  const commit = typeof values.commit === 'string' ? values.commit : '';
88
89
  const event = values.event as string;
90
+ const rawAgent = typeof values.agent === 'string' ? values.agent : '';
91
+ const agent = rawAgent ? normalizeAgentToken(rawAgent) : null;
89
92
  if (!scope || !/^(?:pr\d+|TASK-\d{8}-\d{6})$/.test(scope)) { fail('publish requires --scope <taskId|pr{N}>'); return; }
90
93
  if (!internalHandlerRoute('platform-pr-review', 'publish-task', publishSelector)
91
94
  && !internalHandlerRoute('platform-pr-review', 'publish-pr', publishSelector)) {
@@ -95,6 +98,12 @@ async function platformPrReview(args: string[] = []): Promise<void> {
95
98
  if (!/^[0-9a-f]{7,40}$/i.test(commit)) { fail('publish requires --commit <sha>'); return; }
96
99
  if (!['COMMENT', 'APPROVE', 'REQUEST_CHANGES'].includes(event)) { fail('publish requires --event <COMMENT|APPROVE|REQUEST_CHANGES>'); return; }
97
100
  if (typeof values.bodyFile !== 'string') { fail('publish requires --body-file'); return; }
101
+ if (/^TASK-\d{8}-\d{6}$/u.test(scope) && typeof values.artifact !== 'string') { fail('task-scoped publish requires --artifact <pr-review-artifact>'); return; }
102
+ if (/^TASK-\d{8}-\d{6}$/u.test(scope) && !agent) {
103
+ fail(`task-scoped publish requires --agent <agent>${rawAgent ? `: invalid '${rawAgent}'` : ''}: ${AGENT_USAGE_HINT}`);
104
+ return;
105
+ }
106
+ if (rawAgent && !agent) { fail(`invalid --agent '${rawAgent}': ${AGENT_USAGE_HINT}`); return; }
98
107
  let body: string;
99
108
  try {
100
109
  body = readBodyFile(values.bodyFile, cwd);
@@ -108,6 +117,8 @@ async function platformPrReview(args: string[] = []): Promise<void> {
108
117
  identity: { scope, round, commitSha: commit },
109
118
  event: event as PrReviewEvent,
110
119
  body,
120
+ ...(typeof values.artifact === 'string' ? { recoveryArtifact: values.artifact } : {}),
121
+ ...(agent ? { agent } : {}),
111
122
  dryRun: values.dryRun === true
112
123
  }));
113
124
  }
@@ -145,6 +145,8 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
145
145
  descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
146
146
  descriptor('internal', 'task-lifecycle', 'intent', 'task-bound', 'progress'),
147
147
  descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'progress'),
148
+ descriptor('internal', 'task-platform-recovery', 'inspect', 'task-bound', 'diagnostic'),
149
+ descriptor('internal', 'task-platform-recovery', 'recover', 'task-bound', 'recovery'),
148
150
  descriptor('internal', 'task-short-id', 'list', 'conditional', 'diagnostic'),
149
151
  descriptor('internal', 'task-short-id', 'list-verify', 'conditional', 'diagnostic'),
150
152
  descriptor('internal', 'task-short-id', 'alloc', 'task-bound', 'progress'),
@@ -221,7 +223,7 @@ export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
221
223
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
222
224
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
223
225
  'task-orchestration', 'task-review', 'task-event', 'task-invalidation', 'task-lifecycle',
224
- 'task-finalization', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
226
+ 'task-finalization', 'task-platform-recovery', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
225
227
  ]);
226
228
  export const PUBLIC_DISPATCHER_ROUTES = Object.freeze([
227
229
  'agent-client', 'cp', 'data', 'decide', 'help', 'init', 'merge', 'run', 'sandbox', 'server', 'task', 'sync', 'update', 'version'
@@ -0,0 +1,53 @@
1
+ import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
2
+ import { readPlatformOperationJournal } from '../task/platform-operation-journal.ts';
3
+ import { recoverPlatformOperations } from '../task/platform-operation-recovery.ts';
4
+ import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
5
+
6
+ const USAGE = 'Usage: agent-infra-internal task-platform-recovery <N | TASK-id> <inspect | recover> [--agent <agent>] [--selection required|deferred|all]\n';
7
+
8
+ function option(args: readonly string[], name: string): string | undefined {
9
+ const index = args.indexOf(name);
10
+ return index >= 0 ? args[index + 1] : undefined;
11
+ }
12
+
13
+ async function taskPlatformRecovery(args: string[] = []): Promise<void> {
14
+ if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(USAGE); return; }
15
+ if (!ensureInternalHandlerRoute('task-platform-recovery', args)) return;
16
+ const action = args[1];
17
+ const registered = action === 'inspect'
18
+ ? internalHandlerRoute('task-platform-recovery', 'inspect', action)
19
+ : action === 'recover' && internalHandlerRoute('task-platform-recovery', 'recover', action);
20
+ if (!registered) return;
21
+ const repoRoot = detectRepoRoot();
22
+ const task = resolveTaskRef(args[0] ?? '', { repoRoot });
23
+ if (!task.ok) {
24
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: task.code, message: task.message } })}\n`);
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ if (action === 'inspect') {
29
+ try {
30
+ const journal = readPlatformOperationJournal(task.taskId, repoRoot);
31
+ process.stdout.write(`${JSON.stringify({ status: 'ready', changed: false, taskId: task.taskId, journal })}\n`);
32
+ } catch (error) {
33
+ const value = error as { code?: string; message?: string };
34
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error) } })}\n`);
35
+ process.exitCode = 1;
36
+ }
37
+ return;
38
+ }
39
+ const agent = option(args, '--agent');
40
+ const selection = option(args, '--selection') ?? 'all';
41
+ if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(agent)
42
+ || !['required', 'deferred', 'all'].includes(selection)) {
43
+ 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`);
44
+ process.stderr.write(USAGE);
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ const result = await recoverPlatformOperations(task.taskId, selection as 'required' | 'deferred' | 'all', { agent, cwd: repoRoot });
49
+ process.stdout.write(`${JSON.stringify(result)}\n`);
50
+ process.exitCode = result.status === 'applied' || result.status === 'no-op' ? 0 : result.status === 'blocked' ? 2 : 1;
51
+ }
52
+
53
+ export { taskPlatformRecovery };
package/lib/merge.ts 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.ts';
@@ -424,6 +425,24 @@ function formatBackupTimestamp(date: Date): string {
424
425
  ].join('') + `-${String(date.getHours()).padStart(2, '0')}${String(date.getMinutes()).padStart(2, '0')}${String(date.getSeconds()).padStart(2, '0')}`;
425
426
  }
426
427
 
428
+ function resolveRecoveryProjectKey(repoRoot: string): string {
429
+ let projectKey: string | undefined;
430
+ try {
431
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
432
+ if (typeof config.project === 'string' && config.project.trim() !== '') {
433
+ projectKey = config.project.trim();
434
+ }
435
+ } catch {
436
+ // Fall back to the repository directory when project config is unavailable.
437
+ }
438
+
439
+ projectKey ??= path.basename(repoRoot);
440
+ if (!/^[A-Za-z0-9_.@-]+$/.test(projectKey) || projectKey === '.' || projectKey === '..') {
441
+ throw new Error(`Invalid project name for recovery backups: ${projectKey}`);
442
+ }
443
+ return projectKey;
444
+ }
445
+
427
446
  function toPosixPath(relativePath: string): string {
428
447
  return relativePath.split(path.sep).join('/');
429
448
  }
@@ -541,7 +560,7 @@ function buildWorkspaceIndex(workspaceDir: string): Map<string, WorkspaceRecord>
541
560
  function backupTaskDir(backupRoot: string, section: MutableSection, taskDir: string, taskId: string): string {
542
561
  const backupDir = path.join(backupRoot, section, taskId);
543
562
  fs.mkdirSync(path.dirname(backupDir), { recursive: true });
544
- fs.cpSync(taskDir, backupDir, { recursive: true });
563
+ fs.cpSync(taskDir, backupDir, { recursive: true, verbatimSymlinks: true });
545
564
  return backupDir;
546
565
  }
547
566
 
@@ -1085,16 +1104,17 @@ async function cmdMerge(args: string[]): Promise<void> {
1085
1104
  throw new Error(`Source path is not a directory: ${sourcePath}`);
1086
1105
  }
1087
1106
 
1088
- const workspaceDir = path.join(process.cwd(), '.agents', 'workspace');
1107
+ const repoRoot = process.cwd();
1108
+ const workspaceDir = path.join(repoRoot, '.agents', 'workspace');
1089
1109
  const archiveDir = path.join(workspaceDir, 'archive');
1090
1110
  const backupStamp = formatBackupTimestamp(new Date());
1091
- const backupRootRelative = `.agents/workspace/.merge-backup/${backupStamp}/`;
1092
- const backupRoot = path.join(workspaceDir, '.merge-backup', backupStamp);
1093
- const report = createReport(resolvedSource, backupRootRelative);
1111
+ const projectKey = resolveRecoveryProjectKey(repoRoot);
1112
+ const recoveryRoot = path.join(os.homedir(), '.agent-infra', 'recovery-backups', projectKey, 'workspace-merge');
1113
+ const backupRoot = path.join(recoveryRoot, backupStamp);
1114
+ const report = createReport(resolvedSource, backupRoot);
1094
1115
  detectSourceMode(resolvedSource);
1095
1116
  validateSourceWorkspace(resolvedSource);
1096
1117
  validateArchiveSection(workspaceDir, new Map());
1097
-
1098
1118
  for (const section of ALL_SECTIONS) {
1099
1119
  fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
1100
1120
  }
@@ -51,7 +51,7 @@ function referencedBackfillArtifact(taskDir: string, message: string): BackfillA
51
51
  .map((match) => match[1]!);
52
52
  if (names.length !== 1) return { status: 'ignored', artifact: null, error: null };
53
53
  const parsed = parseArtifactName(names[0]!);
54
- if (!parsed || (parsed.family !== 'pr-review' && !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family as typeof COMPLETION_BACKFILL_FAMILIES[number]))) {
54
+ if (!parsed || !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family as typeof COMPLETION_BACKFILL_FAMILIES[number])) {
55
55
  return { status: 'ignored', artifact: null, error: null };
56
56
  }
57
57
  const artifactPath = path.join(taskDir, parsed.name);
@@ -141,6 +141,7 @@ async function backfillCompletionComments(
141
141
  artifact: artifact.name,
142
142
  agent: options.agent,
143
143
  backfill: true,
144
+ dependency: 'required',
144
145
  cwd: resolved.repoRoot,
145
146
  client: options.client
146
147
  });
@@ -1,4 +1,5 @@
1
1
  import { computeInLabels } from './metadata-labels.ts';
2
+ import { createHash } from 'node:crypto';
2
3
  import type { PlatformError } from './types.ts';
3
4
 
4
5
  type InLabelPlan = {
@@ -68,6 +69,16 @@ function validateInLabelMapping(value: unknown): { ok: true; value: Record<strin
68
69
  return { ok: true, value: normalized };
69
70
  }
70
71
 
72
+ function inLabelMappingDigest(value: unknown): { ok: true; digest: string } | { ok: false; error: InLabelValidationError } {
73
+ const mapping = validateInLabelMapping(value);
74
+ if (!mapping.ok) return mapping;
75
+ const canonical = Object.fromEntries(Object.keys(mapping.value).sort().map((name) => [
76
+ name,
77
+ [...new Set(mapping.value[name]!)].sort()
78
+ ]));
79
+ return { ok: true, digest: createHash('sha256').update(JSON.stringify(canonical)).digest('hex') };
80
+ }
81
+
71
82
  function repositoryLabelPages(value: unknown): unknown[][] | null {
72
83
  if (!Array.isArray(value)) return null;
73
84
  if (value.length === 0) return [];
@@ -197,6 +208,7 @@ export {
197
208
  extractRepositoryLabelNames,
198
209
  flattenPages,
199
210
  inLabels,
211
+ inLabelMappingDigest,
200
212
  labelDelta,
201
213
  mergeInLabels,
202
214
  planInLabelUpdate,