@fitlab-ai/agent-infra 0.11.12 → 0.11.13

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 (68) hide show
  1. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
  2. package/dist/lib/internal/sandbox-control.js +1 -81
  3. package/dist/lib/internal/task-finalization.js +26 -19
  4. package/dist/lib/internal/task-lifecycle.js +1 -1
  5. package/dist/lib/platform/github-client.js +33 -3
  6. package/dist/lib/sandbox/commands/create.js +6 -2
  7. package/dist/lib/sandbox/commands/ls.js +4 -5
  8. package/dist/lib/sandbox/commands/rm.js +1 -1
  9. package/dist/lib/sandbox/control/client.js +4 -64
  10. package/dist/lib/sandbox/control/executor.js +4 -50
  11. package/dist/lib/sandbox/control/lifecycle.js +66 -4
  12. package/dist/lib/sandbox/control/protocol.js +5 -16
  13. package/dist/lib/sandbox/control/server.js +203 -234
  14. package/dist/lib/sandbox/control/task-view.js +0 -13
  15. package/dist/lib/sandbox/mounts.js +7 -0
  16. package/dist/lib/sandbox/recovery.js +28 -9
  17. package/dist/lib/sandbox/removal.js +19 -0
  18. package/dist/lib/sandbox/workspace-identity.js +18 -6
  19. package/dist/lib/sandbox/workspace-view.js +48 -2
  20. package/dist/lib/task/commands/ls.js +3 -3
  21. package/dist/lib/task/control-authority.js +2 -0
  22. package/dist/lib/task/control-recovery.js +1 -2
  23. package/dist/lib/task/finalization.js +14 -0
  24. package/dist/lib/task/lifecycle.js +203 -31
  25. package/dist/lib/task/orchestration.js +26 -72
  26. package/dist/lib/task/short-id.js +37 -27
  27. package/dist/lib/task/verification-engine.js +4 -11
  28. package/dist/lifecycle-build-manifest.json +3 -3
  29. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +20 -23
  30. package/lib/internal/sandbox-control.ts +0 -97
  31. package/lib/internal/task-finalization.ts +30 -18
  32. package/lib/internal/task-lifecycle.ts +1 -1
  33. package/lib/platform/github-client.ts +34 -3
  34. package/lib/sandbox/commands/create.ts +6 -1
  35. package/lib/sandbox/commands/ls.ts +4 -5
  36. package/lib/sandbox/commands/rm.ts +1 -1
  37. package/lib/sandbox/control/client.ts +5 -78
  38. package/lib/sandbox/control/executor.ts +2 -53
  39. package/lib/sandbox/control/lifecycle.ts +67 -4
  40. package/lib/sandbox/control/protocol.ts +6 -20
  41. package/lib/sandbox/control/server.ts +9 -44
  42. package/lib/sandbox/control/task-view.ts +0 -19
  43. package/lib/sandbox/mounts.ts +8 -0
  44. package/lib/sandbox/recovery.ts +35 -16
  45. package/lib/sandbox/removal.ts +21 -2
  46. package/lib/sandbox/workspace-identity.ts +16 -6
  47. package/lib/sandbox/workspace-view.ts +41 -2
  48. package/lib/task/commands/ls.ts +3 -3
  49. package/lib/task/control-authority.ts +1 -0
  50. package/lib/task/control-recovery.ts +2 -3
  51. package/lib/task/finalization.ts +14 -0
  52. package/lib/task/lifecycle.ts +180 -27
  53. package/lib/task/orchestration.ts +30 -68
  54. package/lib/task/short-id.ts +33 -40
  55. package/lib/task/verification-engine.ts +3 -9
  56. package/package.json +1 -1
  57. package/templates/.agents/rules/README.en.md +1 -0
  58. package/templates/.agents/rules/README.zh-CN.md +1 -0
  59. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  60. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  61. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  62. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  63. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
  64. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
  65. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  66. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  67. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  68. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
@@ -8,7 +8,7 @@ import { validateCurrentTaskContract } from './current-contract.ts';
8
8
  import { parseReviewSummary } from './review-artifacts.ts';
9
9
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
10
10
  import type { LedgerDocument } from './ledger.ts';
11
- import { resolveTaskRef } from './resolve-ref.ts';
11
+ import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
12
12
  import {
13
13
  activateDelegation,
14
14
  abortActivatedDelegation,
@@ -75,7 +75,6 @@ type ClientCapabilityRecovery = Readonly<{
75
75
  baselineEmpty: true;
76
76
  receiptCount: 0;
77
77
  pendingDelegation: false;
78
- commitAuthorizationUnused: true;
79
78
  completionEvidenceAbsent: true;
80
79
  }>;
81
80
  resultingStatus: 'running';
@@ -105,7 +104,6 @@ type OrchestrationRun = Readonly<{
105
104
  pendingDelegation: DelegationReceipt | null;
106
105
  receipts: readonly DelegationReceipt[];
107
106
  pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
108
- commitAuthorization: Readonly<{ issuedAt: string | null; consumedAt: string | null }>;
109
107
  completionEvidence: CleanCompletionEvidence | null;
110
108
  createdAt: string;
111
109
  updatedAt: string;
@@ -249,7 +247,7 @@ function isModelPolicy(value: unknown): value is OrchestrationModelPolicy {
249
247
 
250
248
  const RECOVERY_GUARD_KEYS = [
251
249
  'stepCount', 'nextStage', 'baselineEmpty', 'receiptCount', 'pendingDelegation',
252
- 'commitAuthorizationUnused', 'completionEvidenceAbsent'
250
+ 'completionEvidenceAbsent'
253
251
  ] as const;
254
252
 
255
253
  function isRecovery(value: unknown): value is OrchestrationRecovery {
@@ -269,7 +267,6 @@ function isRecovery(value: unknown): value is OrchestrationRecovery {
269
267
  && guards.baselineEmpty === true
270
268
  && guards.receiptCount === 0
271
269
  && guards.pendingDelegation === false
272
- && guards.commitAuthorizationUnused === true
273
270
  && guards.completionEvidenceAbsent === true
274
271
  && value.resultingStatus === 'running';
275
272
  }
@@ -291,7 +288,7 @@ export function isCompletionEvidence(value: unknown): value is CleanCompletionEv
291
288
  const ORCHESTRATION_RUN_KEYS = [
292
289
  'taskId', 'runId', 'status', 'nextStage', 'stepCount', 'maxSteps', 'modelPolicy',
293
290
  'modelPolicySource', 'recoveryHistory', 'baseline', 'pendingDelegation', 'receipts',
294
- 'pause', 'commitAuthorization', 'completionEvidence', 'createdAt', 'updatedAt'
291
+ 'pause', 'completionEvidence', 'createdAt', 'updatedAt'
295
292
  ] as const;
296
293
 
297
294
  function orchestrationStateReasonCodes(value: unknown): string[] {
@@ -313,12 +310,6 @@ function orchestrationStateReasonCodes(value: unknown): string[] {
313
310
  if (!(record.pendingDelegation === null || isDelegationReceipt(record.pendingDelegation))) reasons.push('pendingDelegation');
314
311
  if (!Array.isArray(record.receipts) || !record.receipts.every(isDelegationReceipt)) reasons.push('receipts');
315
312
  if (!(record.pause === null || isPause(record.pause))) reasons.push('pause');
316
- if (!hasExactKeys(record.commitAuthorization, ['issuedAt', 'consumedAt'])) {
317
- reasons.push('commitAuthorization');
318
- } else {
319
- if (!nullableText(record.commitAuthorization.issuedAt)) reasons.push('commitAuthorization.issuedAt');
320
- if (!nullableText(record.commitAuthorization.consumedAt)) reasons.push('commitAuthorization.consumedAt');
321
- }
322
313
  if (!(record.completionEvidence === null || isCompletionEvidence(record.completionEvidence))) reasons.push('completionEvidence');
323
314
  if (!exactText(record.createdAt)) reasons.push('createdAt');
324
315
  if (!exactText(record.updatedAt)) reasons.push('updatedAt');
@@ -584,8 +575,6 @@ function canRecoverUnsupportedClientPause(run: OrchestrationRun): boolean {
584
575
  && run.baseline === ''
585
576
  && run.pendingDelegation === null
586
577
  && run.receipts.length === 0
587
- && run.commitAuthorization?.issuedAt === null
588
- && run.commitAuthorization?.consumedAt === null
589
578
  && run.completionEvidence == null
590
579
  && run.recoveryHistory.length === 0;
591
580
  }
@@ -633,7 +622,6 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
633
622
  baselineEmpty: true,
634
623
  receiptCount: 0,
635
624
  pendingDelegation: false,
636
- commitAuthorizationUnused: true,
637
625
  completionEvidenceAbsent: true
638
626
  },
639
627
  resultingStatus: 'running'
@@ -686,7 +674,6 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
686
674
  pendingDelegation: null,
687
675
  receipts: [],
688
676
  pause: null,
689
- commitAuthorization: { issuedAt: null, consumedAt: null },
690
677
  completionEvidence: null,
691
678
  createdAt: now,
692
679
  updatedAt: now
@@ -1087,56 +1074,28 @@ function dispatchOrchestrationDelegation(
1087
1074
  }
1088
1075
  }
1089
1076
 
1077
+ type MatchingDelegationsResult =
1078
+ | Readonly<{ ok: true; matches: readonly { taskId: string; run: OrchestrationRun }[] }>
1079
+ | Readonly<{ ok: false; error: OrchestrationResult }>;
1080
+
1090
1081
  function matchingDelegations(
1091
1082
  predicate: (receipt: DelegationReceipt) => boolean,
1092
1083
  options: OrchestrationOptions
1093
- ): Array<{ taskId: string; run: OrchestrationRun }> {
1094
- const repoRoot = options.repoRoot ?? process.cwd();
1095
- if (options.taskId) {
1096
- const taskDir = path.join(repoRoot, '.agents', 'workspace', 'active', options.taskId);
1097
- const run = readRun(taskDir, options);
1098
- return run && run.status === 'running' && run.pendingDelegation && predicate(run.pendingDelegation)
1099
- ? [{ taskId: options.taskId, run }]
1100
- : [];
1084
+ ): MatchingDelegationsResult {
1085
+ const taskId: unknown = options.taskId;
1086
+ if (taskId === undefined || taskId === '') {
1087
+ return { ok: false, error: failed('ORCHESTRATION_TASK_ID_REQUIRED', 'taskId is required for lifecycle matching') };
1101
1088
  }
1102
- const activeRoot = path.join(repoRoot, '.agents', 'workspace', 'active');
1103
- if (!fs.existsSync(activeRoot)) return [];
1104
- emitDiagnostic(options, 'orchestration-active-scan-start', {
1105
- activeRoot
1106
- });
1107
- const matches = fs.readdirSync(activeRoot, { withFileTypes: true })
1108
- .filter((entry) => entry.isDirectory())
1109
- .flatMap((entry) => {
1110
- const candidateTaskId = entry.name;
1111
- const candidateTaskDir = path.join(activeRoot, candidateTaskId);
1112
- emitDiagnostic(options, 'orchestration-active-scan-candidate', {
1113
- candidateTaskId,
1114
- candidateTaskDir,
1115
- orchestrationPath: orchestrationPath(candidateTaskDir)
1116
- });
1117
- let run: OrchestrationRun | null;
1118
- try {
1119
- run = readRun(candidateTaskDir, options);
1120
- } catch (error) {
1121
- emitDiagnostic(options, 'orchestration-active-scan-candidate-failed', {
1122
- candidateTaskId,
1123
- candidateTaskDir,
1124
- errorType: error instanceof Error ? error.name : typeof error,
1125
- reasonCodes: error instanceof OrchestrationStateError ? error.reasonCodes.join(',') : null
1126
- });
1127
- throw error;
1128
- }
1129
- if (!run || run.status !== 'running' || !run.pendingDelegation) return [];
1130
- const receipt = run.pendingDelegation;
1131
- return predicate(receipt)
1132
- ? [{ taskId: candidateTaskId, run }]
1133
- : [];
1134
- });
1135
- emitDiagnostic(options, 'orchestration-active-scan-finished', {
1136
- activeRoot,
1137
- matchingDelegations: matches.length
1138
- });
1139
- return matches;
1089
+ if (typeof taskId !== 'string' || !TASK_ID_RE.test(taskId)) {
1090
+ return { ok: false, error: failed('ORCHESTRATION_TASK_ID_INVALID', 'taskId must be a canonical TASK-id') };
1091
+ }
1092
+ const repoRoot = options.repoRoot ?? process.cwd();
1093
+ const taskDir = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
1094
+ const run = readRun(taskDir, options);
1095
+ const matches = run && run.status === 'running' && run.pendingDelegation && predicate(run.pendingDelegation)
1096
+ ? [{ taskId, run }]
1097
+ : [];
1098
+ return { ok: true, matches };
1140
1099
  }
1141
1100
 
1142
1101
  function uniqueMatchingDelegation(
@@ -1144,12 +1103,13 @@ function uniqueMatchingDelegation(
1144
1103
  predicate: (receipt: DelegationReceipt) => boolean,
1145
1104
  options: OrchestrationOptions
1146
1105
  ): { taskId: string; run: OrchestrationRun } | OrchestrationResult {
1147
- const matches = matchingDelegations(
1106
+ const lookup = matchingDelegations(
1148
1107
  (receipt) => receipt.client === client && predicate(receipt),
1149
1108
  options
1150
1109
  );
1110
+ if (!lookup.ok) return lookup.error;
1111
+ const matches = lookup.matches;
1151
1112
  if (matches.length === 0) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no matching lifecycle delegation exists');
1152
- if (matches.length > 1) return failed('ORCHESTRATION_DELEGATION_AMBIGUOUS', 'multiple lifecycle delegations match the native hook event');
1153
1113
  return matches[0]!;
1154
1114
  }
1155
1115
 
@@ -1158,13 +1118,14 @@ function hasActivatableOrchestrationDelegation(
1158
1118
  childId: string,
1159
1119
  options: OrchestrationOptions = {}
1160
1120
  ): boolean {
1161
- return matchingDelegations(
1121
+ const lookup = matchingDelegations(
1162
1122
  (receipt) => receipt.client === client && (
1163
1123
  receipt.status === 'prepared'
1164
1124
  || (receipt.childId === childId && ['activated', 'stage-completed'].includes(receipt.status))
1165
1125
  ),
1166
1126
  options
1167
- ).length > 0;
1127
+ );
1128
+ return lookup.ok && lookup.matches.length > 0;
1168
1129
  }
1169
1130
 
1170
1131
  function hasSealableOrchestrationDelegation(
@@ -1172,12 +1133,13 @@ function hasSealableOrchestrationDelegation(
1172
1133
  childId: string,
1173
1134
  options: OrchestrationOptions = {}
1174
1135
  ): boolean {
1175
- return matchingDelegations(
1136
+ const lookup = matchingDelegations(
1176
1137
  (receipt) => receipt.client === client
1177
1138
  && receipt.childId === childId
1178
1139
  && ['stage-completed', 'sealed'].includes(receipt.status),
1179
1140
  options
1180
- ).length > 0;
1141
+ );
1142
+ return lookup.ok && lookup.matches.length > 0;
1181
1143
  }
1182
1144
 
1183
1145
  function activateMatchingOrchestrationDelegation(
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { parseTypedTaskFrontmatter } from './frontmatter.ts';
3
4
 
4
5
  const REGISTRY_NAME = '.short-ids.json';
5
6
  const LOCK_NAME = '.short-ids.json.lock';
@@ -159,43 +160,35 @@ function resolveShortIdReadOnly(
159
160
  const taskId = ids[key];
160
161
  if (!taskId) {
161
162
  const message = Object.keys(ids).length === 0
162
- ? `short id '${key}' not found; active task registry is empty.`
163
- : `short id '${key}' not found in active task registry (it may have been cleaned up after archival; check 'task-short-id.js list').`;
163
+ ? `short id '${key}' not found; task short-id registry is empty.`
164
+ : `short id '${key}' not found in task short-id registry.`;
164
165
  return shortIdFailure('SHORT_ID_NOT_FOUND', message);
165
166
  }
166
- const taskMdPath = path.join(
167
- repoRoot,
168
- '.agents',
169
- 'workspace',
170
- 'active',
171
- taskId,
172
- 'task.md'
173
- );
174
- if (!fs.existsSync(taskMdPath)) {
175
- const remainingActiveEntries = Object.values(ids).filter((candidateTaskId) =>
176
- fs.existsSync(
177
- path.join(
178
- repoRoot,
179
- '.agents',
180
- 'workspace',
181
- 'active',
182
- candidateTaskId,
183
- 'task.md'
184
- )
185
- )
186
- );
187
- const message = remainingActiveEntries.length === 0
188
- ? `short id '${key}' not found; active task registry is empty.`
189
- : `short id '${key}' not found in active task registry (it may have been cleaned up after archival; check 'task-short-id.js list').`;
167
+ const activeTask = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
168
+ const completedTask = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId, 'task.md');
169
+ let isValidCompletedTask = false;
170
+ try {
171
+ const task = parseTypedTaskFrontmatter(fs.readFileSync(completedTask, 'utf8'));
172
+ isValidCompletedTask = task.id === taskId && task.status === 'completed';
173
+ } catch { /* a mapping is valid only while its task content is present */ }
174
+ if (!fs.existsSync(activeTask) && !isValidCompletedTask) {
190
175
  return shortIdFailure(
191
176
  'SHORT_ID_STALE',
192
- message,
177
+ `short id '${key}' points to no active or completed task content.`,
193
178
  taskId
194
179
  );
195
180
  }
196
181
  return { ok: true, taskId };
197
182
  }
198
183
 
184
+ function hasRetainedCompletedTask(activeDir: string, taskId: string): boolean {
185
+ try {
186
+ const taskPath = path.join(path.dirname(activeDir), 'completed', taskId, 'task.md');
187
+ const task = parseTypedTaskFrontmatter(fs.readFileSync(taskPath, 'utf8'));
188
+ return task.id === taskId && task.status === 'completed';
189
+ } catch { return false; }
190
+ }
191
+
199
192
  type RegistrySchema = {
200
193
  version: number;
201
194
  ids: Record<string, string>;
@@ -339,7 +332,8 @@ function mutateShortIdRegistryAt(
339
332
  const original = JSON.stringify(registry.ids);
340
333
  if (pruneOrphans) {
341
334
  for (const [key, candidate] of Object.entries(registry.ids)) {
342
- if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md'))) delete registry.ids[key];
335
+ if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md'))
336
+ && !hasRetainedCompletedTask(activeDir, candidate)) delete registry.ids[key];
343
337
  }
344
338
  }
345
339
  const existing = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId);
@@ -406,28 +400,27 @@ function executeShortIdCommand(request: ShortIdCommandRequest): ShortIdCommandRe
406
400
  if (normalized.kind !== 'shortId') {
407
401
  throw Object.assign(new Error(`invalid short id format '${request.argument}'`), { code: 'SHORT_ID_FORMAT_INVALID' });
408
402
  }
409
- return withRegistryLock(request.activeDir, () => {
410
- const registry = readRegistryStrict(registryPath, request.shortIdLength);
411
- let changed = false;
412
- for (const [key, taskId] of Object.entries(registry.ids)) {
413
- if (!fs.existsSync(path.join(request.activeDir, taskId, 'task.md'))) { delete registry.ids[key]; changed = true; }
414
- }
415
- if (changed) writeRegistry(registryPath, registry);
416
- const taskId = registry.ids[normalized.value];
417
- if (!taskId) throw Object.assign(new Error(`short id '${normalized.value}' not found in active task registry`), { code: 'SHORT_ID_NOT_FOUND' });
418
- return { status: changed ? 'applied' : 'no-op', changed, output: taskId, error: null };
419
- });
403
+ const registry = readRegistryStrict(registryPath, request.shortIdLength);
404
+ const taskId = registry.ids[normalized.value];
405
+ if (!taskId) throw Object.assign(new Error(`short id '${normalized.value}' not found in task short-id registry`), { code: 'SHORT_ID_NOT_FOUND' });
406
+ if (!fs.existsSync(path.join(request.activeDir, taskId, 'task.md'))
407
+ && !hasRetainedCompletedTask(request.activeDir, taskId)) {
408
+ throw Object.assign(new Error(`short id '${normalized.value}' points to a task without active or completed task content`), { code: 'SHORT_ID_STALE' });
409
+ }
410
+ return { status: 'no-op', changed: false, output: taskId, error: null };
420
411
  }
421
412
  const registry = readRegistryStrict(registryPath, request.shortIdLength);
422
413
  if (request.verify) {
423
414
  const active = fs.existsSync(request.activeDir)
424
415
  ? fs.readdirSync(request.activeDir).filter((entry) => /^TASK-\d{8}-\d{6}$/.test(entry) && fs.existsSync(path.join(request.activeDir, entry, 'task.md')))
425
416
  : [];
417
+ const retained = Object.values(registry.ids).filter((taskId) => hasRetainedCompletedTask(request.activeDir, taskId));
418
+ const validTasks = new Set([...active, ...retained]);
426
419
  const registered = new Map<string, string[]>();
427
420
  for (const [key, taskId] of Object.entries(registry.ids)) registered.set(taskId, [...(registered.get(taskId) ?? []), key]);
428
421
  const diff = {
429
422
  missing_in_registry: active.filter((taskId) => !registered.has(taskId)).map((taskId) => ({ taskId })),
430
- orphans_in_registry: Object.entries(registry.ids).filter(([, taskId]) => !active.includes(taskId)).map(([key, taskId]) => ({ key, taskId })),
423
+ orphans_in_registry: Object.entries(registry.ids).filter(([, taskId]) => !validTasks.has(taskId)).map(([key, taskId]) => ({ key, taskId })),
431
424
  duplicate_registry_keys: [...registered.entries()].filter(([, keys]) => keys.length > 1).map(([taskId, keys]) => ({ taskId, keys }))
432
425
  };
433
426
  const clean = Object.values(diff).every((items) => items.length === 0);
@@ -167,12 +167,8 @@ function checkOrchestrationState({ taskDir }: any): any {
167
167
  return failResult('orchestration-state', 'Paused run requires a stable pause code and message');
168
168
  }
169
169
  if (run.status === 'completed') {
170
- if (run.completionEvidence != null) {
171
- const evidenceError = validateCleanCompletionEvidence(run);
172
- if (evidenceError) return failResult('orchestration-state', evidenceError);
173
- } else if (run.pendingDelegation !== null || !run.commitAuthorization?.consumedAt) {
174
- return failResult('orchestration-state', 'Completed run must consume commit authorization and clear pending delegation');
175
- }
170
+ const evidenceError = validateCleanCompletionEvidence(run);
171
+ if (evidenceError) return failResult('orchestration-state', evidenceError);
176
172
  } else if (run.completionEvidence != null) {
177
173
  return failResult('orchestration-state', 'Clean completion evidence requires a completed run');
178
174
  } else if (run.pendingDelegation?.status === 'sealed') {
@@ -209,11 +205,9 @@ function validateCleanCompletionEvidence(run: OrchestrationRun): string | null {
209
205
  if (
210
206
  run.status !== 'completed'
211
207
  || run.pendingDelegation !== null
212
- || run.commitAuthorization?.issuedAt !== null
213
- || run.commitAuthorization?.consumedAt !== null
214
208
  || run.receipts.some((receipt) => receipt.stage === 'commit')
215
209
  ) {
216
- return 'Clean completion evidence conflicts with commit delegation state';
210
+ return 'Clean completion evidence conflicts with pending delegation or commit receipt state';
217
211
  }
218
212
  return null;
219
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.11.12",
3
+ "version": "0.11.13",
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",
@@ -17,6 +17,7 @@ so you can quickly find "which ones to read" without opening each file.
17
17
  - [`validation-output.md`](validation-output.md) — User-facing validation summaries, failure diagnostics, soft warnings, and consumer boundaries.
18
18
  - [`sync-content-generation.md`](sync-content-generation.md) — Producer-side Markdown constraints for task and lifecycle content synchronized to Issues.
19
19
  - [`decision-qualification.md`](decision-qualification.md) — Constraint, candidate, human-confirmation, and six-artifact qualification-audit contract.
20
+ - [`strategy-pattern.md`](strategy-pattern.md) — Identify shared flows and different behaviors, then use interfaces and the Strategy Pattern to isolate implementation differences.
20
21
 
21
22
  ## Issue / PR
22
23
 
@@ -16,6 +16,7 @@
16
16
  - [`validation-output.md`](validation-output.md) — 面向用户的校验状态摘要、失败诊断、软警告及消费边界。
17
17
  - [`sync-content-generation.md`](sync-content-generation.md) — 同步到 Issue 的任务和生命周期 Markdown 生成约束。
18
18
  - [`decision-qualification.md`](decision-qualification.md) — 约束、候选、人工确认和六类 artifact 资格审计契约。
19
+ - [`strategy-pattern.md`](strategy-pattern.md) — 识别共用流程和不同行为,通过接口与策略模式隔离实现差异。
19
20
 
20
21
  ## Issue / PR
21
22
 
@@ -0,0 +1,25 @@
1
+ # Strategy Pattern
2
+
3
+ This rule applies during technical design and implementation. When behavior differs by platform, client, protocol, workflow, business rule, or algorithm, use interfaces to isolate implementations so shared flows can reuse them instead of creating a dedicated path for every scenario.
4
+
5
+ ## Design Steps
6
+
7
+ 1. **Separate shared flows from different implementations**: identify common steps and the behavior that differs by platform, client, protocol, workflow, business rule, algorithm, or lifecycle.
8
+ 2. **Define the interface contract**: specify the inputs, outputs, error semantics, and invocation timing shared by implementations. The common flow depends on the interface, not a concrete implementation.
9
+ 3. **Apply the Strategy Pattern**: provide an implementation for each behavior and select a strategy at registration or assembly. The common flow orchestrates shared steps without spreading conditions that identify concrete implementations.
10
+ 4. **Verify substitutability**: a new implementation should connect through registration or assembly. Write shared tests for the interface contract and the necessary tests for strategy selection and full assembly.
11
+
12
+ ## Implementation Requirements
13
+
14
+ - When the requirement clearly includes multiple behaviors, prefer the Strategy Pattern even if there is currently one implementation; the interface should express their stable shared contract.
15
+ - When adding behavior, prefer adding a strategy implementation and registering or assembling it in one place. Do not copy a full business flow or repeat the same branch across callers.
16
+ - If branches that identify implementations are already scattered across paths, consolidate those decisions at strategy selection and make callers depend on the interface.
17
+ - Reuse existing interfaces and domain terms. Do not rename code to apply a pattern or add interfaces to stable behavior that has no meaningful alternatives.
18
+ - Keep stable, single-purpose behavior direct when there is no evidence it will vary. Do not mechanically add interfaces, configuration, or extension points.
19
+
20
+ ## Design Review Questions
21
+
22
+ - Which steps are shared by all implementations, and which behaviors differ by platform, client, or business rule?
23
+ - Does the interface express the contract shared by the implementations?
24
+ - Can a new behavior be added by implementing a strategy and assembling it in one place? If several call paths must change, is centralized strategy selection missing?
25
+ - Can the same interface tests validate the shared contract while strategy differences are tested independently?
@@ -0,0 +1,25 @@
1
+ # 策略模式
2
+
3
+ 本规则适用于方案设计和代码实现。当平台、客户端、协议、工作流、业务规则或算法存在多种行为时,用接口隔离这些实现,让共用流程能够复用不同实现,避免每种场景都形成一条专用链路。
4
+
5
+ ## 设计步骤
6
+
7
+ 1. **区分共用流程和不同实现**:找出所有实现共同执行的步骤,以及会因平台、客户端、协议、工作流、业务规则、算法或生命周期而不同的行为。
8
+ 2. **定义接口契约**:明确不同实现的输入、输出、错误语义和调用时机。共用流程依赖接口,不直接调用某个具体实现。
9
+ 3. **应用策略模式**:为每种行为提供独立实现,在注册或装配处选择策略。共用流程负责编排共同步骤,不散布识别具体实现的条件分支。
10
+ 4. **验证实现可替换**:新增实现应能通过注册或装配接入;为接口约定编写共享测试,并为策略选择和整体装配编写必要测试。
11
+
12
+ ## 实现要求
13
+
14
+ - 需求明确包含多种行为时,优先使用策略模式,即使当前只有一个实现;接口应表达这些实现之间稳定的共同约定。
15
+ - 新增行为时,优先增加一个策略实现并在统一位置注册或装配,不复制完整业务链,也不在多个调用方重复增加同类分支。
16
+ - 如果识别不同实现的分支已经散落在多条路径,先把这些判断收敛到策略选择处,再让调用方依赖接口。
17
+ - 复用现有接口和领域词汇;不要为了套用模式而改名,也不要为没有不同行为的稳定逻辑增加接口。
18
+ - 对稳定、单一且没有证据表明会变化的逻辑,保持直接实现,不机械添加接口、配置或扩展点。
19
+
20
+ ## 设计审查问题
21
+
22
+ - 所有实现共用哪些流程?哪些行为因平台、客户端或业务规则而不同?
23
+ - 接口是否表达了不同实现共同遵守的约定?
24
+ - 新实现能否只新增策略并在一个位置装配?如果必须修改多处调用链,是否缺少统一的策略选择?
25
+ - 共用约定能否通过同一套接口测试验证,不同策略的差异能否独立验证?
@@ -19,6 +19,10 @@ Follow the `code` step in `.agents/workflows/feature-development.yaml`.
19
19
  3. **Keep testing continuously**: run the **fast smoke subset** continuously as work progresses (see the `test` skill)
20
20
  4. **Keep it simple**: do not add unplanned features
21
21
 
22
+ ## Strategy Pattern
23
+
24
+ Read `.agents/rules/strategy-pattern.md` before implementation. When behavior differs by platform, client, protocol, workflow, business rule, or algorithm, follow the interface selected in the plan and make the core flow depend on that interface rather than concrete classes. Add each behavior as a strategy and select or assemble it in one place. If branches identifying implementations are scattered across paths, centralize strategy selection before editing callers. Keep stable behavior direct when it has no meaningful alternatives; do not add interfaces mechanically.
25
+
22
26
  ## Run Test Verification
23
27
 
24
28
  During implementation:
@@ -19,6 +19,10 @@
19
19
  3. **持续测试**:实现过程中持续运行 **fast smoke 子集**(参见 `test` skill)
20
20
  4. **保持简单**:不要加入未计划的功能
21
21
 
22
+ ## 策略模式
23
+
24
+ 实现前读取 `.agents/rules/strategy-pattern.md`。当实现涉及平台、客户端、协议、工作流、业务规则或算法的不同实现时,沿用方案确定的接口,让主流程依赖接口而不是具体类;新增行为应作为策略实现,在统一位置选择或装配。若识别不同实现的分支已散落在多条路径,先收敛策略选择,再修改调用方。行为稳定且没有明确替代实现时,保留直接实现,不机械增加接口。
25
+
22
26
  ## 运行测试验证
23
27
 
24
28
  实现过程中:
@@ -158,7 +158,7 @@ The core deduplicates the stable `step/code/target` tuple. Callers must not allo
158
158
 
159
159
  ### 5. Run the Active Pre-completion Hard Gate
160
160
 
161
- After platform writes succeed and before moving the directory or releasing the short id, run:
161
+ After platform writes succeed and before moving the directory, run:
162
162
 
163
163
  ```bash
164
164
  agent-infra-internal task-verify {task-id} complete-task.preflight --format text
@@ -182,32 +182,11 @@ For an active task, finalization first recovers pending platform operations, the
182
182
 
183
183
  Finalization runs allowed artifact backfill -> lifecycle -> terminal task comment -> core verification -> warning task-comment update -> summary seal in a fixed order. Every backfill that actually runs must reach a successful terminal result before lifecycle starts; an ordinary replay with a complete receipt and no recovery fact does not rerun backfill, comments, verification, or summary. A completed recovery that may write a managed comment first persists a done summary as pending, then seals it only at the end. Summary seal always reads the durable staging record retained in the task directory; the platform entry point validates marker, owner, and digest, performs any required delete/recreate, and reads back final remote order. `result=completed` means the host safely completed the task from structured results and the receipt; if peripheral warnings or pending steps remain, return `result=completed_with_warnings`, warnings, and pending steps. Use `result=failed` or `result=blocked` only for hard or receipt/capability failures, then fix the cause and retry through the same entry point; do not claim completion or hand-repair partial state. A sandbox must not run `ls completed` or a local terminal verification against its historical mount to re-decide this result.
184
184
 
185
- ### 7. Handle Finalization Retries and Results
185
+ ### 7. Handle the Finalization Result
186
186
 
187
- Both Scenario A and Scenario B `finalization-retry` run the same `task-finalization` entry point from the host. The receipt is only a re-entry hint, not canonical truth: every re-entry revalidates allowed artifact backfill, the terminal task comment, and completion verification, then repeats summary seal from the retained durable staging record; only an already `completed` task with its short-id registry entry released may skip the irreversible lifecycle. Do not split the operation into the former lifecycle, comment-sync, or completion-gate commands. If backfill, the task comment, summary seal, or verification is blocked by the network, preserve the receipt, durable staging record, and completed steps, fix the network, and rerun complete-task. If lifecycle has not completed, keep the task active and resume the pending steps from the receipt.
187
+ The sandbox-local entry calls the existing finalization prepare/commit flow directly. Before lifecycle transfer, it stops this task's broker from accepting new requests and waits for accepted work to finish through its existing path. Transfer starts only after the broker owner exits. On success, use the structured result and receipt returned by this command. On failure or blocked status, preserve source data and the receipt, follow the returned error, and do not claim completion or manually rewrite terminal state.
188
188
 
189
- Consume the structured finalization result, receipt, and warning projection directly. Continue only for `completed` or `completed_with_warnings`. Treat `failed` and `blocked` as explicit host failure or blocked outcomes: preserve the receipt, stop the current flow, and follow the returned status and error; call finalization again only when the host state permits it and the cause has been fixed. If an `accepted=true` request is `unknown`, preserve the receipt and original request ID. Never resubmit task-finalization; query only the original request with `sandbox-control recover <same-request-id>`. Create a new request only when the request was rejected before acceptance and its error permits a retry. Do not run `ls completed` or `task-verify complete-task.completed` from a stale sandbox mount, and do not let that local result overrule the host result.
190
-
191
- ### Recovering an accepted sandbox-control result
192
-
193
- When this skill runs in a sandbox and an accepted task-finalization request has
194
- no terminal result yet, the client automatically recovers under the same
195
- request ID. The five-minute total budget starts with the first unknown result;
196
- each recover call is limited to 30 seconds. While a published unknown response
197
- is present, the client rereads it every 25 ms during that call. When a recover
198
- call times out with unknown, the client continues immediately without an extra
199
- wait. The client prints `SANDBOX_CONTROL_REQUEST_ID: <request-id>` on stderr.
200
-
201
- If no terminal result arrives within five minutes, preserve the receipt and
202
- request ID and treat the result as pending/unknown. Do not resubmit
203
- task-finalization. Once the broker is healthy, continue querying only the
204
- original request ID:
205
-
206
- ```bash
207
- agent-infra-internal sandbox-control recover <request-id>
208
- ```
209
-
210
- Do not submit a new request for an accepted task finalization. If the request ID is missing or the receipt cannot be read, keep the result pending and recover the original ID only from stderr, a handoff, or the receipt; do not look it up by task key. The broker's `processing/<request-id>/result.json` is private transport evidence, not a task receipt, and cannot by itself prove completion. The finalization receipt and host completion gate remain authoritative. If recovery with the same ID still has no terminal result, inspect broker and host executor activity. Stop and preserve the original request ID while a request is active, the broker is busy, or status is unknown. Retry with a new request ID only after confirming that the original request never started or its executor has ended; the new request performs the full prepare and commit after broker admission. Only a request rejected before acceptance may be retried directly with a new request ID according to the error's retryability.
189
+ Completion keeps the short-id mapping. Managed sandbox cleanup releases it only after the container is confirmed removed and cleanup succeeds. To clean up, pass the retained short id or full `{task-id}` to `ai sandbox rm`; do not release the short id separately during completion.
211
190
 
212
191
  ### 8. Inform User
213
192
 
@@ -230,7 +209,7 @@ Deliverables:
230
209
  Optional: clean up this task's sandbox
231
210
  (The task is completed; the sandbox container and per-branch config directory are not reclaimed automatically. Run this if you no longer need them:)
232
211
 
233
- ai sandbox rm {task-id}
212
+ ai sandbox rm {task-id} # or use the task's retained short id
234
213
 
235
214
  ```
236
215
 
@@ -157,7 +157,7 @@ agent-infra-internal task-warning {task-id} add --step complete-task --severity
157
157
 
158
158
  ### 5. 运行 active 预完成硬门禁
159
159
 
160
- 平台写入成功后、移动目录和释放短号之前运行:
160
+ 平台写入成功后、移动目录前运行:
161
161
 
162
162
  ```bash
163
163
  agent-infra-internal task-verify {task-id} complete-task.preflight --format text
@@ -181,29 +181,11 @@ active 任务的 finalization 先恢复账本中的待处理平台操作,并
181
181
 
182
182
  finalization 按允许的 artifact backfill → lifecycle → task 评论 → core verification → warning task 评论更新 → summary seal 的固定顺序执行。每项实际运行的 backfill 必须取得成功终态,否则不得进入 lifecycle;完整 receipt 且无恢复事实的普通重入不执行 backfill、评论、验证或 summary。任何可能写入 managed comment 的 completed 恢复会先把 done summary 持久化为 pending,恢复末尾才重新 seal。summary seal 始终读取任务目录中保留的 durable staging record;平台入口负责校验 marker、owner 和 digest,必要时删除并重建摘要,再复读最终远端顺序。`result=completed` 即表示宿主已依据结构化结果和 receipt 安全完成;若还有外围 warning 或 pending step,返回 `result=completed_with_warnings`、warnings 和 pending steps。`result=failed` 或 `result=blocked` 仅用于硬失败或 receipt/capability 失败,修复原因后以同一入口重试,不得宣称完成或手工补写局部状态。沙箱不得从旧挂载执行 `ls completed` 或本地终态校验来重新裁决该结果。
183
183
 
184
- ### 7. 处理 finalization 重试与结果
184
+ ### 7. 处理 finalization 结果
185
185
 
186
- 场景 A 与场景 B `finalization-retry` 都从宿主执行同一个 `task-finalization` 入口。receipt 只是重入提示,不是 canonical truth:每次重入都要重新核对允许的 artifact 回填、终态 task 评论、完成校验,并使用保留的 durable staging record 重做 summary seal;只有任务已处于 `completed` 且短号 registry 已释放时,才可跳过不可逆的 lifecycle。不得拆开调用旧的 lifecycle、评论同步或完成校验命令。若回填、task 评论、summary seal 或校验因网络问题返回 `blocked`,保留 receipt、durable staging record 和已完成状态,修复网络后重跑 complete-task;若生命周期仍未完成,任务保持 active 并从 receipt 的待处理步骤继续。
186
+ sandbox-local 入口直接执行已有 finalization prepare/commit。lifecycle 移动前,它先停止本任务 broker 接收新请求,并等待已接收操作按原路径完成;只有 broker owner 退出后才开始迁移 task 目录。成功时消费本次命令返回的结构化结果和 receipt。失败或阻塞时保留源数据与 receipt,按返回的错误处理,不输出完成消息,也不手工改写终态。
187
187
 
188
- 完成结果必须直接消费本次宿主 finalization 的结构化输出、receipt 和 warning projection。只有 `completed` 或 `completed_with_warnings` 允许继续。`failed` / `blocked` 是宿主明确返回的失败或阻塞结果:保留 receipt,停止当前流程,并按宿主返回的状态和错误处理;只有宿主状态允许且原因已修复时,才重新调用 finalization。若 `accepted=true` 的请求结果为 `unknown`,保留 receipt 和原 request ID;不得重发 task-finalization,只能通过 `sandbox-control recover <同一 request-id>` 查询原请求。只有请求在 accepted 之前被拒绝,且错误允许重试时,才可创建新请求。不要在沙箱旧挂载中另行运行 `ls completed` 或 `task-verify complete-task.completed`,也不要用其结果推翻宿主结果。
189
-
190
- ### accepted 后的 sandbox-control 结果恢复
191
-
192
- 如果本技能在沙箱内执行,且 task-finalization 请求已经 accepted 但暂时没有
193
- terminal result,client 会从首次 unknown 开始自动按同一个 request ID 恢复,
194
- 总预算为 5 分钟,每次 recover 最多 30 秒;published unknown 会在当前调用中
195
- 按 25ms 间隔重读,预算用尽后立即继续 recover,不额外等待。client 会在 stderr
196
- 输出 `SANDBOX_CONTROL_REQUEST_ID: <request-id>`。
197
-
198
- 5 分钟后仍没有 terminal result 时,必须保留 receipt 和 request ID,并将结果视为
199
- pending/unknown;不得重发 task-finalization。broker 恢复健康后,只能继续查询原
200
- request ID:
201
-
202
- ```bash
203
- agent-infra-internal sandbox-control recover <request-id>
204
- ```
205
-
206
- accepted 的 task finalization 不得提交新请求。若 request ID 缺失或 receipt 无法读取,保持 pending;只从 stderr、handoff 或 receipt 中恢复原 ID,不按 task key 查询。broker 的 `processing/<request-id>/result.json` 只是私有 transport evidence,不是 task receipt,单凭它不能证明任务完成;finalization receipt 和宿主完成校验仍是权威。若同 ID 恢复仍没有 terminal result,必须检查 broker 和宿主 executor 的活动状态:活动请求、`busy` 或状态未知时停止并保留原 request ID。只有确认原请求从未启动或 executor 已结束后,才可用新的 request ID 重试;新请求会在 broker 接纳后执行完整 prepare 和 commit。仅当请求在 accepted 之前已被拒绝时,才可直接依据错误的 retryability 决定是否提交新的 request ID.
188
+ complete 保留短号映射。它只会在受管 sandbox 清理确认容器已删除且清理成功后释放。需要清理时可用保留短号或完整 `{task-id}` 调用 `ai sandbox rm`;不要在 complete 中单独释放短号。
207
189
 
208
190
  ### 8. 告知用户
209
191
 
@@ -226,7 +208,7 @@ accepted 的 task finalization 不得提交新请求。若 request ID 缺失或
226
208
  可选:清理本任务的沙箱
227
209
  (任务已完成,沙箱容器和 per-branch 配置目录不会自动回收。如果不再需要可执行:)
228
210
 
229
- ai sandbox rm {task-id}
211
+ ai sandbox rm {task-id} # 或使用本任务仍保留的短号
230
212
 
231
213
  ```
232
214
 
@@ -91,6 +91,8 @@ Use it to understand:
91
91
 
92
92
  ### 5. Design the Technical Plan
93
93
 
94
+ Before designing, read `.agents/rules/strategy-pattern.md`. Identify shared flows and behavior differences by platform, client, protocol, workflow, business rule, or algorithm; make the core flow depend on interfaces and put differences in strategy implementations instead of adding a dedicated branch to every path.
95
+
94
96
  Follow the `technical-design` step in `.agents/workflows/feature-development.yaml`:
95
97
 
96
98
  **Required tasks**:
@@ -105,10 +107,11 @@ When this round introduces a new key design decision, first run `agent-infra-int
105
107
 
106
108
  **Design principles**:
107
109
  1. **Architectural soundness**: choose the structurally correct approach; diff size is not the primary criterion. Do not pile changes onto an unsound structure just to keep the diff small
108
- 2. **Simplicity**: given a sound architecture, prefer the simplest approach and avoid over-engineering
109
- 3. **Consistency**: follow existing code patterns and conventions
110
- 4. **Testability**: design for straightforward testing
111
- 5. **Reversibility**: prefer changes that are easy to roll back
110
+ 2. **Strategy Pattern**: identify shared flows and different behaviors; make the core flow depend on interfaces, not concrete implementations, and select or assemble strategies in one place
111
+ 3. **Simplicity**: given a sound architecture, prefer the simplest approach; do not add strategy interfaces mechanically when behavior is stable and has no meaningful alternatives
112
+ 4. **Consistency**: follow existing code patterns and conventions
113
+ 5. **Testability**: design for straightforward testing
114
+ 6. **Reversibility**: prefer changes that are easy to roll back
112
115
 
113
116
  ### 6. Output Plan Document
114
117