@fitlab-ai/agent-infra 0.11.8 → 0.11.10

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 (163) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +0 -6
  4. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  5. package/dist/lib/agent-clients/next-steps.js +1 -1
  6. package/dist/lib/agent-clients/reconcile.js +3 -3
  7. package/dist/lib/agent-clients/schema.js +18 -10
  8. package/dist/lib/git/workflow.js +1 -1
  9. package/dist/lib/init.js +2 -2
  10. package/dist/lib/internal/agent-client.js +3 -3
  11. package/dist/lib/internal/cli-route-inventory.js +0 -3
  12. package/dist/lib/internal/sandbox-control.js +30 -5
  13. package/dist/lib/internal/task-finalization.js +14 -23
  14. package/dist/lib/internal/task-operation-registry.js +2 -3
  15. package/dist/lib/internal/task-platform-recovery.js +9 -4
  16. package/dist/lib/platform/completion-backfill.js +2 -1
  17. package/dist/lib/platform/context.js +4 -2
  18. package/dist/lib/platform/github-client.js +20 -17
  19. package/dist/lib/platform/github-provider.js +42 -22
  20. package/dist/lib/platform/github-release-notes.js +4 -2
  21. package/dist/lib/platform/in-label-sync.js +2 -1
  22. package/dist/lib/platform/issue-comments.js +13 -9
  23. package/dist/lib/platform/operation-coordinator.js +1 -1
  24. package/dist/lib/platform/pr-review.js +2 -1
  25. package/dist/lib/platform/provider-bridge.js +4 -5
  26. package/dist/lib/platform/provider-contract.d.ts +3 -6
  27. package/dist/lib/platform/provider-contract.js +3 -15
  28. package/dist/lib/platform/provider-validation.js +9 -4
  29. package/dist/lib/platform/pull-requests.js +4 -2
  30. package/dist/lib/sandbox/control/client.js +54 -6
  31. package/dist/lib/sandbox/control/executor.js +8 -3
  32. package/dist/lib/sandbox/recovery.js +1 -22
  33. package/dist/lib/sandbox/removal.js +0 -1
  34. package/dist/lib/sandbox/tools.js +2 -0
  35. package/dist/lib/task/artifact-command.js +11 -1
  36. package/dist/lib/task/artifact-lifecycle.js +119 -62
  37. package/dist/lib/task/artifact-selection.js +1 -1
  38. package/dist/lib/task/capabilities.js +55 -21
  39. package/dist/lib/task/commands/status.js +0 -27
  40. package/dist/lib/task/commit-identity.js +22 -7
  41. package/dist/lib/task/commit-intent.js +67 -26
  42. package/dist/lib/task/commit-operation.js +19 -8
  43. package/dist/lib/task/completion-facts.js +32 -10
  44. package/dist/lib/task/control-authority.js +2 -4
  45. package/dist/lib/task/current-contract.js +1 -5
  46. package/dist/lib/task/events.js +8 -274
  47. package/dist/lib/task/execution-inputs.js +14 -0
  48. package/dist/lib/task/finalization-envelope.js +4 -0
  49. package/dist/lib/task/finalization.js +100 -129
  50. package/dist/lib/task/frontmatter.js +1 -1
  51. package/dist/lib/task/ledger-intents.js +4 -7
  52. package/dist/lib/task/orchestration.js +11 -10
  53. package/dist/lib/task/platform-operation-journal.js +17 -10
  54. package/dist/lib/task/platform-operation-recovery.js +95 -28
  55. package/dist/lib/task/review-fingerprint.js +4 -26
  56. package/dist/lib/task/verification-engine.js +95 -6
  57. package/dist/lib/task/verification.js +0 -1
  58. package/dist/lib/task/write.js +0 -11
  59. package/dist/lib/update.js +2 -2
  60. package/dist/lifecycle-build-manifest.json +3 -3
  61. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  62. package/lib/agent-clients/next-steps.ts +3 -3
  63. package/lib/agent-clients/reconcile.ts +3 -3
  64. package/lib/agent-clients/schema.ts +18 -10
  65. package/lib/git/workflow.ts +1 -1
  66. package/lib/init.ts +2 -2
  67. package/lib/internal/agent-client.ts +3 -3
  68. package/lib/internal/cli-route-inventory.ts +0 -2
  69. package/lib/internal/sandbox-control.ts +39 -6
  70. package/lib/internal/task-finalization.ts +16 -24
  71. package/lib/internal/task-operation-registry.ts +2 -3
  72. package/lib/internal/task-platform-recovery.ts +9 -4
  73. package/lib/platform/completion-backfill.ts +2 -1
  74. package/lib/platform/context.ts +8 -4
  75. package/lib/platform/github-client.ts +24 -20
  76. package/lib/platform/github-provider.ts +47 -26
  77. package/lib/platform/github-release-notes.ts +5 -3
  78. package/lib/platform/in-label-sync.ts +2 -1
  79. package/lib/platform/issue-comments.ts +14 -9
  80. package/lib/platform/operation-coordinator.ts +1 -1
  81. package/lib/platform/pr-review.ts +2 -1
  82. package/lib/platform/provider-bridge.ts +10 -9
  83. package/lib/platform/provider-contract.ts +6 -21
  84. package/lib/platform/provider-loader.ts +7 -6
  85. package/lib/platform/provider-validation.ts +30 -8
  86. package/lib/platform/pull-requests.ts +4 -2
  87. package/lib/platform/repository-metadata.ts +3 -2
  88. package/lib/platform/security-alerts.ts +1 -1
  89. package/lib/platform/types.ts +2 -0
  90. package/lib/sandbox/control/client.ts +57 -6
  91. package/lib/sandbox/control/executor.ts +8 -3
  92. package/lib/sandbox/recovery.ts +0 -25
  93. package/lib/sandbox/removal.ts +2 -10
  94. package/lib/sandbox/tool-types.ts +1 -0
  95. package/lib/sandbox/tools.ts +2 -0
  96. package/lib/task/artifact-command.ts +10 -1
  97. package/lib/task/artifact-lifecycle.ts +100 -52
  98. package/lib/task/artifact-selection.ts +1 -1
  99. package/lib/task/capabilities.ts +47 -24
  100. package/lib/task/commands/status.ts +0 -31
  101. package/lib/task/commit-identity.ts +24 -7
  102. package/lib/task/commit-intent.ts +58 -24
  103. package/lib/task/commit-operation.ts +17 -8
  104. package/lib/task/completion-facts.ts +31 -9
  105. package/lib/task/control-authority.ts +1 -4
  106. package/lib/task/current-contract.ts +1 -6
  107. package/lib/task/events.ts +8 -247
  108. package/lib/task/execution-inputs.ts +25 -0
  109. package/lib/task/finalization-envelope.ts +21 -0
  110. package/lib/task/finalization.ts +101 -123
  111. package/lib/task/frontmatter.ts +1 -1
  112. package/lib/task/ledger-intents.ts +4 -7
  113. package/lib/task/orchestration.ts +11 -17
  114. package/lib/task/platform-operation-journal.ts +18 -11
  115. package/lib/task/platform-operation-recovery.ts +90 -31
  116. package/lib/task/review-fingerprint.ts +4 -26
  117. package/lib/task/verification-engine.ts +103 -9
  118. package/lib/task/verification.ts +1 -2
  119. package/lib/task/write.ts +0 -19
  120. package/lib/update.ts +2 -3
  121. package/package.json +1 -1
  122. package/templates/.agents/README.en.md +2 -50
  123. package/templates/.agents/README.zh-CN.md +2 -50
  124. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  125. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  126. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  127. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  128. package/templates/.agents/rules/next-step-output.en.md +1 -1
  129. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  130. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  131. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  132. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
  133. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  134. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  135. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  136. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  137. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  138. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  139. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  142. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  143. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  144. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  145. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  148. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  149. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  150. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  151. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +2 -268
  152. package/templates/.agents/templates/task.zh-CN.md +0 -12
  153. package/templates/.github/workflows/pr-label.yml +1 -2
  154. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  155. package/dist/lib/internal/task-invalidation.js +0 -63
  156. package/dist/lib/task/intermediate-cleanup.js +0 -327
  157. package/dist/lib/task/invalidation-command.js +0 -88
  158. package/dist/lib/task/invalidation.js +0 -242
  159. package/lib/agent-clients/custom-tuis.ts +0 -149
  160. package/lib/internal/task-invalidation.ts +0 -41
  161. package/lib/task/intermediate-cleanup.ts +0 -393
  162. package/lib/task/invalidation-command.ts +0 -88
  163. package/lib/task/invalidation.ts +0 -313
@@ -18,6 +18,8 @@ import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
18
18
  import { platformResult } from '../platform/types.js';
19
19
  import { resolveTaskRef } from './resolve-ref.js';
20
20
  import { readPlatformOperationJournal, recordPlatformOperation } from './platform-operation-journal.js';
21
+ import { providerErrorRetryable } from '../platform/provider-validation.js';
22
+ const DEFAULT_RECOVERY_ATTEMPTS = 3;
21
23
  function labelsMatchOwnedPrefix(actual, expected, prefix) {
22
24
  return actual.filter((label) => label.startsWith(prefix)).sort().join('\0')
23
25
  === expected.filter((label) => label.startsWith(prefix)).sort().join('\0');
@@ -26,7 +28,7 @@ function fieldsMatchExpected(actual, expected) {
26
28
  return Object.entries(expected).every(([name, value]) => actual[name] === value);
27
29
  }
28
30
  function result(status, recovered, pending, error = null) {
29
- return { status, changed: recovered.length > 0, recovered, pending, error };
31
+ return { status, changed: recovered.length > 0, recovered, pending, error: error ? { code: error.code, message: error.message } : null };
30
32
  }
31
33
  function summaryPayload(taskDir, taskId) {
32
34
  const file = path.join(taskDir, '.delivery-summary.json');
@@ -198,7 +200,7 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
198
200
  ? await resolved.value.provider.comments.list({ context: providerOperationContext(resolved.value), parent: identity })
199
201
  : unsupportedProviderOperation(resolved.value.provider, 'comments.list');
200
202
  if (!listed.ok)
201
- return platformResult(listed.error.retryable ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
203
+ return platformResult(providerErrorRetryable(listed.error.code) ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
202
204
  const marker = `<!-- sync-pr:${taskId}:summary -->`;
203
205
  const summaries = listed.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === marker);
204
206
  if (summaries.length > 1)
@@ -293,7 +295,7 @@ async function replayPullRequest(taskId, operation, agent, cwd) {
293
295
  }
294
296
  return recoverCreatedPullRequest(taskId, { agent, cwd, base: intent.baseRef || '', head: intent.headRef || '', skipQueue: true });
295
297
  }
296
- async function recoverPlatformOperations(taskRef, selection, options) {
298
+ async function recoverPlatformOperationsOnce(taskRef, selection, options) {
297
299
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
298
300
  if (!resolved.ok)
299
301
  return result('failed', [], [], { code: resolved.code, message: resolved.message, retryable: false });
@@ -307,34 +309,23 @@ async function recoverPlatformOperations(taskRef, selection, options) {
307
309
  }
308
310
  const candidates = journal.operations.filter((operation) => operation.id !== options.excludeId
309
311
  && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
310
- || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')))
311
- .filter((operation) => selection === 'all' || operation.dependency === selection)
312
- .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
312
+ || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')));
313
313
  if (!candidates.length)
314
314
  return result('no-op', [], []);
315
+ const head = candidates[0];
316
+ if (selection !== 'all' && head.dependency !== selection) {
317
+ return result('blocked', [], candidates.map((operation) => operation.id), {
318
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
319
+ message: 'The first unresolved platform operation belongs to another recovery selection',
320
+ retryable: true
321
+ });
322
+ }
315
323
  const limit = options.limit === undefined ? candidates.length : Math.max(1, options.limit);
316
324
  const recovered = [];
317
325
  const pending = [];
318
326
  const selected = candidates.slice(0, limit);
319
327
  for (const [index, operation] of selected.entries()) {
320
- if (operation.state === 'failed') {
321
- pending.push(operation.id);
322
- return result('blocked', recovered, pending, {
323
- code: operation.lastCode || 'PLATFORM_OPERATION_FAILED',
324
- message: 'A previous platform operation failed permanently; later writes remain queued',
325
- retryable: false
326
- });
327
- }
328
- if (operation.attempts >= operation.maxAttempts) {
329
- pending.push(operation.id, ...selected.slice(index + 1).map((next) => next.id));
330
- pending.push(...candidates.slice(limit).map((next) => next.id));
331
- return result('blocked', recovered, pending, {
332
- code: operation.lastCode || 'PLATFORM_OPERATION_RETRY_LIMIT_REACHED',
333
- message: 'A previous platform operation exhausted its recovery attempts; later writes remain queued',
334
- retryable: false
335
- });
336
- }
337
- if (operation.kind !== 'issue-create') {
328
+ {
338
329
  try {
339
330
  recordPlatformOperation({
340
331
  taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind,
@@ -368,8 +359,8 @@ async function recoverPlatformOperations(taskRef, selection, options) {
368
359
  if (current.id !== operation.id) {
369
360
  try {
370
361
  recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
371
- expectedDigest: operation.expectedDigest, issueMetadata: operation.issueMetadata, dependency: operation.dependency,
372
- state: 'failed', lastCode: 'PLATFORM_OPERATION_SUPERSEDED' });
362
+ expectedDigest: current.expectedDigest, issueMetadata: current.issueMetadata, dependency: operation.dependency,
363
+ state: 'queued', replaceOperationId: operation.id });
373
364
  targetOperation = { ...operation, expectedDigest: current.expectedDigest, issueMetadata: current.issueMetadata, id: current.id };
374
365
  }
375
366
  catch (error) {
@@ -396,7 +387,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
396
387
  if (current.id !== operation.id) {
397
388
  try {
398
389
  recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
399
- expectedDigest: operation.expectedDigest, dependency: operation.dependency, state: 'failed', lastCode: 'PLATFORM_OPERATION_SUPERSEDED' });
390
+ expectedDigest: current.expectedDigest, dependency: operation.dependency, state: 'queued', replaceOperationId: operation.id });
400
391
  targetOperation = { ...operation, expectedDigest: current.expectedDigest, id: current.id };
401
392
  }
402
393
  catch (error) {
@@ -437,7 +428,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
437
428
  ...(operation.pullRequestSummary ? { pullRequestSummary: operation.pullRequestSummary } : {}),
438
429
  ...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {}),
439
430
  dependency: operation.dependency,
440
- state: succeeded ? 'succeeded' : remote.status === 'failed' && remote.error?.retryable === false ? 'failed' : 'unknown',
431
+ state: succeeded ? 'succeeded' : 'unknown',
441
432
  lastCode: remote.error?.code ?? null
442
433
  });
443
434
  }
@@ -455,5 +446,81 @@ async function recoverPlatformOperations(taskRef, selection, options) {
455
446
  pending.push(...remaining);
456
447
  return result(pending.length ? 'blocked' : recovered.length ? 'applied' : 'no-op', recovered, pending, pending.length ? { code: 'PLATFORM_OPERATION_RECOVERY_PENDING', message: 'Platform operations remain pending after the recovery budget', retryable: true } : null);
457
448
  }
449
+ async function recoverPlatformOperations(taskRef, selection, options) {
450
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
451
+ if (!resolved.ok)
452
+ return result('failed', [], [], { code: resolved.code, message: resolved.message, retryable: false });
453
+ let initial;
454
+ try {
455
+ initial = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
456
+ }
457
+ catch (error) {
458
+ const value = error;
459
+ return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
460
+ }
461
+ const eligible = (operation) => operation.id !== options.excludeId
462
+ && (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
463
+ || (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED'));
464
+ const first = initial.operations.filter(eligible);
465
+ if (!first.length)
466
+ return result('no-op', [], []);
467
+ const operationLimit = options.limit === undefined ? first.length : Math.max(1, options.limit);
468
+ const attemptLimit = options.attempts ?? DEFAULT_RECOVERY_ATTEMPTS;
469
+ if (!Number.isSafeInteger(attemptLimit) || attemptLimit <= 0) {
470
+ return result('failed', [], [], { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'Recovery attempts must be a positive safe integer', retryable: false });
471
+ }
472
+ const recovered = [];
473
+ for (let operationIndex = 0; operationIndex < operationLimit; operationIndex += 1) {
474
+ let latest;
475
+ try {
476
+ latest = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
477
+ }
478
+ catch (error) {
479
+ const value = error;
480
+ return result('failed', recovered, [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
481
+ }
482
+ const head = latest.operations.find(eligible);
483
+ if (!head)
484
+ break;
485
+ if (selection !== 'all' && head.dependency !== selection) {
486
+ const pending = latest.operations.filter(eligible).map((operation) => operation.id);
487
+ return result('blocked', recovered, pending, {
488
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
489
+ message: 'The first unresolved platform operation belongs to another recovery selection',
490
+ retryable: true
491
+ });
492
+ }
493
+ let last = null;
494
+ for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
495
+ last = await recoverPlatformOperationsOnce(taskRef, selection, { ...options, limit: 1 });
496
+ recovered.push(...last.recovered.filter((id) => !recovered.includes(id)));
497
+ if (last.recovered.length > 0 || last.status === 'applied' || last.status === 'no-op')
498
+ break;
499
+ }
500
+ if (!last || (last.recovered.length === 0 && last.status !== 'applied' && last.status !== 'no-op')) {
501
+ let current;
502
+ try {
503
+ current = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
504
+ }
505
+ catch {
506
+ return result('blocked', recovered, [head.id], last?.error ?? null);
507
+ }
508
+ const pending = current.operations.filter(eligible).map((operation) => operation.id);
509
+ return result(last?.status === 'failed' ? 'failed' : 'blocked', recovered, pending, last?.error ?? {
510
+ code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
511
+ message: 'The first queued platform operation remains unresolved after this recovery run',
512
+ retryable: true
513
+ });
514
+ }
515
+ }
516
+ let remaining;
517
+ try {
518
+ remaining = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot).operations.filter(eligible).map((operation) => operation.id);
519
+ }
520
+ catch {
521
+ remaining = [];
522
+ }
523
+ return result(remaining.length ? 'blocked' : recovered.length ? 'applied' : 'no-op', recovered, remaining, remaining.length ? { code: 'PLATFORM_OPERATION_RECOVERY_PENDING', message: 'Platform operations remain queued after this recovery run', retryable: true } : null);
524
+ }
458
525
  export { fieldsMatchExpected, labelsMatchOwnedPrefix, recoverPlatformOperations };
459
526
  //# sourceMappingURL=platform-operation-recovery.js.map
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { artifactName, maxArtifactRound } from "./artifact-name.js";
4
- import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from "./invalidation.js";
3
+ import { inspectArtifactDirectory } from "./artifact-lifecycle.js";
5
4
  const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
6
5
  export function parseReviewedGitTree(value) {
7
6
  return REVIEWED_TREE_RE.test(value) ? value : null;
@@ -28,32 +27,11 @@ export function resolvePostReviewGlobs(config = {}, reviewConfig = {}) {
28
27
  return [":/", ...excludes.map((p) => `:(top,exclude)${p}`)];
29
28
  }
30
29
  export function findAuthoritativeReviewCodeArtifact(taskDir) {
31
- const entries = fs.existsSync(taskDir) ? fs.readdirSync(taskDir) : [];
32
- const round = maxArtifactRound(entries, "review-code");
33
- if (round === 0) {
30
+ const inventory = inspectArtifactDirectory(taskDir, 'review-code');
31
+ if (inventory.status !== 'ready' || !inventory.latest) {
34
32
  return { ok: false, round: 0, fileName: null, path: null, error: null };
35
33
  }
36
- const fileName = artifactName("review-code", round);
37
- let taskContent;
38
- try {
39
- taskContent = fs.readFileSync(path.join(taskDir, "task.md"), "utf8");
40
- }
41
- catch (error) {
42
- return {
43
- ok: false, round, fileName, path: null,
44
- error: error instanceof Error ? error.message : String(error)
45
- };
46
- }
47
- const invalidation = parseInvalidationDocument(taskContent);
48
- if (!invalidation.ok) {
49
- return { ok: false, round, fileName, path: null, error: invalidation.message };
50
- }
51
- if (invalidationBlocks(invalidation.document)) {
52
- return { ok: false, round, fileName, path: null, error: "artifact invalidation is incomplete; reconcile task.md before using review-code" };
53
- }
54
- if (isArtifactInvalidated(invalidation.document, "review-code", fileName)) {
55
- return { ok: false, round, fileName, path: null, error: `${fileName} is invalidated; re-run review-code` };
56
- }
34
+ const { round, name: fileName } = inventory.latest;
57
35
  return {
58
36
  ok: true,
59
37
  round,
@@ -19,7 +19,7 @@ import { inspectDecisionDetailDuplicates } from "./decision-details.js";
19
19
  import { parseImplementationInputs } from "./implementation-inputs.js";
20
20
  import { scanVisibleMarkdown } from "./markdown.js";
21
21
  import { loadVerificationConfig } from "./verification-config.js";
22
- import { inspectActivityLog } from "./activity-log.js";
22
+ import { inspectActivityLog, pairEntries } from "./activity-log.js";
23
23
  import { parseWorkflowWarnings } from "./workflow-warnings.js";
24
24
  import { OrchestrationStateError, readRun } from "./orchestration.js";
25
25
  import { readPrDeliveryFact } from "./pr-delivery-fact.js";
@@ -30,9 +30,11 @@ import { normalizeVerificationRecord } from './gate-policy.js';
30
30
  import { manualValidationFinalSummaryProjectionMatches } from "./manual-validation-receipt.js";
31
31
  import { readManualValidationCompletion } from "./manual-validation-completion.js";
32
32
  import { sha256File } from "./artifact-receipts.js";
33
+ import { inspectArtifactDirectory } from "./artifact-lifecycle.js";
34
+ import { parseArtifactName } from "./artifact-name.js";
33
35
  import { summaryCommentState } from "../platform/pr-summary.js";
34
36
  import { taskIssueIdentity } from "../platform/task-identities.js";
35
- import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
37
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog, parseCompletionFacts } from './completion-facts.js';
36
38
  import { inspectReviewIdentity } from './review-identity.js';
37
39
  const TASK_ENUMS = {
38
40
  type: ["feature", "bugfix", "refactor", "docs", "chore"],
@@ -264,7 +266,7 @@ function checkOrchestrationEvidence({ taskDir }) {
264
266
  return passResult('orchestration-evidence', 'Persisted orchestration model and delegation evidence is internally consistent');
265
267
  }
266
268
  // === Check Functions ===
267
- function checkTaskMeta({ taskDir, config, repositoryRoot }) {
269
+ function checkTaskMeta({ taskDir, config, repositoryRoot, skillName, artifactFile }) {
268
270
  const task = loadTask(taskDir);
269
271
  if (!task.ok) {
270
272
  return failResult("task-meta", task.message);
@@ -306,7 +308,22 @@ function checkTaskMeta({ taskDir, config, repositoryRoot }) {
306
308
  }
307
309
  const expectedStep = config.expected_step;
308
310
  if (expectedStep && metadata.current_step !== expectedStep) {
309
- return failResult("task-meta", `Expected current_step '${expectedStep}', got '${metadata.current_step || "(empty)"}'`);
311
+ let validLifecycleArtifact = false;
312
+ if (currentTask) {
313
+ const activityConfig = loadVerificationConfig(repositoryRoot, skillName).checks?.["activity-log"];
314
+ const { section, invalidEntries } = inspectActivityLog(task.content);
315
+ if (activityConfig && section && invalidEntries.length === 0) {
316
+ let latestAction = "";
317
+ for (const entry of section.entries) {
318
+ if (!ACTIVITY_LOG_STARTED_RE.test(entry.step))
319
+ latestAction = entry.step;
320
+ }
321
+ validLifecycleArtifact = checkArtifactLifecycleActivityLog(taskDir, artifactFile, skillName, activityConfig, task, section.entries, latestAction)?.status === "pass";
322
+ }
323
+ }
324
+ if (!validLifecycleArtifact) {
325
+ return failResult("task-meta", `Expected current_step '${expectedStep}', got '${metadata.current_step || "(empty)"}'`);
326
+ }
310
327
  }
311
328
  const expectedStatus = config.expected_status;
312
329
  if (expectedStatus && metadata.status !== expectedStatus) {
@@ -604,7 +621,73 @@ function checkImplementationInput({ taskDir, artifactFile }) {
604
621
  }
605
622
  return passResult("implementation-input", `${actionDecision} matches Activity Log, report, and task table`);
606
623
  }
607
- function checkActivityLog({ taskDir, config }) {
624
+ const ARTIFACT_ACTIVITY_STAGES = {
625
+ "analyze-task": { family: "analysis", event: "analyze.completed" },
626
+ "plan-task": { family: "plan", event: "plan.completed" },
627
+ "review-analysis": { family: "review-analysis", event: "review-analysis.completed" },
628
+ "review-plan": { family: "review-plan", event: "review-plan.completed" },
629
+ "code-task": { family: "code", event: "code.completed" },
630
+ "review-code": { family: "review-code", event: "review-code.completed" }
631
+ };
632
+ function checkArtifactLifecycleActivityLog(taskDir, artifactFile, skillName, config, task, entries, latestAction) {
633
+ const stage = ARTIFACT_ACTIVITY_STAGES[skillName];
634
+ if (!stage)
635
+ return null;
636
+ const inventory = inspectArtifactDirectory(taskDir, stage.family);
637
+ if (inventory.status !== "ready" || !inventory.latest) {
638
+ return failResult("activity-log", `Current ${stage.family} artifact is unavailable: ${inventory.error?.message ?? "no artifact exists"}`);
639
+ }
640
+ const output = artifactFile || inventory.latest.name;
641
+ if (output !== inventory.latest.name) {
642
+ return failResult("activity-log", `Artifact '${output}' is not the current ${stage.family} artifact; rerun ${skillName} for '${inventory.latest.name}'`);
643
+ }
644
+ const identity = parseArtifactName(output);
645
+ if (!identity || identity.family !== stage.family) {
646
+ return failResult("activity-log", `Artifact '${output}' does not belong to ${stage.family}`);
647
+ }
648
+ if (!hasArtifactCompletionFact(task.content, path.join(taskDir, output), stage.event)) {
649
+ return failResult("activity-log", `Completion fact for '${output}' is missing or stale; rerun ${skillName} to record a current completion`);
650
+ }
651
+ const frontmatter = parseTypedTaskFrontmatter(task.content);
652
+ const fact = parseCompletionFacts(frontmatter.completion_facts).find((item) => item.event === stage.event && item.output === output);
653
+ if (!fact)
654
+ return failResult("activity-log", `Completion fact for '${output}' is missing; rerun ${skillName}`);
655
+ if (!Array.isArray(fact.lifecycleInputs)) {
656
+ return failResult("activity-log", `Completion fact for '${output}' has no input snapshot; rerun ${skillName}`);
657
+ }
658
+ for (const input of fact.lifecycleInputs) {
659
+ if (!input || typeof input.name !== "string" || !/^[a-f0-9]{64}$/u.test(input.sha256)) {
660
+ return failResult("activity-log", `Completion fact for '${output}' has an invalid input snapshot; rerun ${skillName}`);
661
+ }
662
+ const inputIdentity = parseArtifactName(input.name);
663
+ if (!inputIdentity)
664
+ return failResult("activity-log", `Completion fact for '${output}' has invalid input '${input.name}'; rerun ${skillName}`);
665
+ const inputInventory = inspectArtifactDirectory(taskDir, inputIdentity.family);
666
+ if (inputInventory.status !== "ready" || inputInventory.latest?.name !== input.name
667
+ || sha256File(path.join(taskDir, input.name)) !== input.sha256) {
668
+ return failResult("activity-log", `Input '${input.name}' changed or is no longer current; rerun ${skillName} to refresh '${output}'`);
669
+ }
670
+ }
671
+ const expectedAction = new RegExp(config.expected_action_pattern);
672
+ const roundAction = entries.find((entry) => {
673
+ if (ACTIVITY_LOG_STARTED_RE.test(entry.step))
674
+ return false;
675
+ if (!expectedAction.test(entry.step))
676
+ return false;
677
+ expectedAction.lastIndex = 0;
678
+ const actionRound = /\(Round (\d+)/.exec(entry.step)?.[1];
679
+ return actionRound === String(identity.round) && entry.note.includes(`→ ${output}`);
680
+ });
681
+ if (!roundAction) {
682
+ return failResult("activity-log", `No completed Activity Log row records '${output}' for ${skillName} Round ${identity.round}`);
683
+ }
684
+ if (skillName === "plan-task" && /^(?:Human Decision|Decision)\b/.test(latestAction)
685
+ && expectedAction.test(latestAction)) {
686
+ return passResult("activity-log", `Plan completion '${output}' is current; latest action '${latestAction}' is a human decision`);
687
+ }
688
+ return passResult("activity-log", `Current ${stage.family} completion '${output}' is valid for Round ${identity.round}`);
689
+ }
690
+ function checkActivityLog({ taskDir, config, skillName, artifactFile }) {
608
691
  const task = loadTask(taskDir);
609
692
  if (!task.ok) {
610
693
  return failResult("activity-log", task.message);
@@ -638,6 +721,9 @@ function checkActivityLog({ taskDir, config }) {
638
721
  doneActions.push(action);
639
722
  }
640
723
  }
724
+ const artifactLifecycleResult = checkArtifactLifecycleActivityLog(taskDir, artifactFile, skillName, config, task, entries, latestAction);
725
+ if (artifactLifecycleResult)
726
+ return artifactLifecycleResult;
641
727
  if (config.expected_action_pattern && !new RegExp(config.expected_action_pattern).test(latestAction)) {
642
728
  const expected = new RegExp(config.expected_action_pattern);
643
729
  let index = doneActions.length - 1;
@@ -799,7 +885,10 @@ async function checkManualValidation({ taskDir, repositoryRoot }) {
799
885
  const inspectedLog = inspectActivityLog(task.content);
800
886
  if (!inspectedLog.section)
801
887
  return failResult("manual-validation", "Activity Log is missing or invalid");
802
- const reviewDoneIndex = inspectedLog.section.entries.findIndex((entry) => entry.step === `Review Code (Round ${review.round})`);
888
+ const reviewRow = pairEntries(inspectedLog.section.entries).find((row) => row.step === `Review Code (Round ${review.round})` && row.started !== '' && row.done !== '');
889
+ const reviewDoneIndex = reviewRow
890
+ ? inspectedLog.section.entries.findIndex((entry) => entry.step === reviewRow.step && entry.time === reviewRow.done)
891
+ : -1;
803
892
  const completedIndex = inspectedLog.section.entries.findIndex((entry) => entry.step === "Complete Manual Validation"
804
893
  && entry.note.includes(`Manual validation passed → ${artifactName};`)
805
894
  && entry.note.includes(`transaction=${receipt.transactionId};`)
@@ -22,7 +22,6 @@ const VERIFICATION_CATALOG = {
22
22
  'commit.completed': gate('commit', 'active'),
23
23
  'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
24
24
  'complete-task.hard-preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
25
- 'complete-task.prepared': gate('complete-task', 'active'),
26
25
  'complete-task.completed': gate('complete-task', 'completed'),
27
26
  'create-pr.completed': gate('create-pr', 'active'),
28
27
  'create-task.completed': gate('create-task', 'active'),
@@ -6,7 +6,6 @@ import { parseTypedTaskFrontmatter, updateTaskFrontmatter } from './frontmatter.
6
6
  import { resolveTaskRef } from './resolve-ref.js';
7
7
  import { mutateTableRow, upsertSection } from './sections.js';
8
8
  import { validateCurrentTaskContract } from './current-contract.js';
9
- import { invalidationBlocks, parseInvalidationDocument } from './invalidation.js';
10
9
  const DEFAULT_FILE_SYSTEM = {
11
10
  readFileSync: (file) => fs.readFileSync(file, 'utf8'),
12
11
  statModeSync: (file) => fs.statSync(file).mode,
@@ -91,16 +90,6 @@ function writeTaskCore(request, options = {}) {
91
90
  if (!contract.ok) {
92
91
  return failure(request, identity, contract.code, contract.message);
93
92
  }
94
- if (invalidationBlocks(contract.invalidation)
95
- && options.invalidationContext !== 'source-completion'
96
- && options.invalidationContext !== 'reconcile') {
97
- return failure(request, identity, 'TASK_INVALIDATION_BLOCKED', 'task has an incomplete artifact invalidation operation; reconcile it before writing downstream state');
98
- }
99
- }
100
- else {
101
- const invalidation = parseInvalidationDocument(original);
102
- if (!invalidation.ok)
103
- return failure(request, identity, 'TASK_INVALIDATION_INVALID', invalidation.message);
104
93
  }
105
94
  let candidate = original;
106
95
  const operations = [];
@@ -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.8",
4
- "sourceInputHash": "6ddb72f8b50137d5148a47172bd7eb2628b17a937ae647ac38350e582ba08124",
5
- "internalExecutableBuildHash": "c886e015506879620bb490f1ea5977d9952f2bbb4515d3a0b49e0ec184b48028"
3
+ "packageVersion": "0.11.10",
4
+ "sourceInputHash": "3500e8eb7fac8bbad3d95124002f8042af3d160185a5b635eb9b4d8f88a991cb",
5
+ "internalExecutableBuildHash": "51a4c472b6a54b3ca3c28ab049683175d3ea4730fe560868f853cd603b5a98f1"
6
6
  }
@@ -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) =>