@fitlab-ai/agent-infra 0.11.9 → 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 (124) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/git/workflow.js +1 -1
  3. package/dist/lib/internal/cli-route-inventory.js +0 -3
  4. package/dist/lib/internal/sandbox-control.js +30 -5
  5. package/dist/lib/internal/task-finalization.js +14 -23
  6. package/dist/lib/internal/task-operation-registry.js +2 -3
  7. package/dist/lib/internal/task-platform-recovery.js +9 -4
  8. package/dist/lib/platform/completion-backfill.js +2 -1
  9. package/dist/lib/platform/context.js +4 -2
  10. package/dist/lib/platform/github-client.js +20 -17
  11. package/dist/lib/platform/github-provider.js +42 -22
  12. package/dist/lib/platform/github-release-notes.js +4 -2
  13. package/dist/lib/platform/in-label-sync.js +2 -1
  14. package/dist/lib/platform/issue-comments.js +13 -9
  15. package/dist/lib/platform/operation-coordinator.js +1 -1
  16. package/dist/lib/platform/pr-review.js +2 -1
  17. package/dist/lib/platform/provider-bridge.js +4 -5
  18. package/dist/lib/platform/provider-contract.d.ts +3 -6
  19. package/dist/lib/platform/provider-contract.js +3 -15
  20. package/dist/lib/platform/provider-validation.js +9 -4
  21. package/dist/lib/platform/pull-requests.js +4 -2
  22. package/dist/lib/sandbox/control/client.js +54 -6
  23. package/dist/lib/sandbox/control/executor.js +8 -3
  24. package/dist/lib/sandbox/removal.js +0 -1
  25. package/dist/lib/task/artifact-command.js +11 -1
  26. package/dist/lib/task/artifact-lifecycle.js +119 -62
  27. package/dist/lib/task/artifact-selection.js +1 -1
  28. package/dist/lib/task/capabilities.js +55 -21
  29. package/dist/lib/task/commands/status.js +0 -27
  30. package/dist/lib/task/commit-identity.js +22 -7
  31. package/dist/lib/task/commit-intent.js +67 -26
  32. package/dist/lib/task/commit-operation.js +19 -8
  33. package/dist/lib/task/completion-facts.js +32 -10
  34. package/dist/lib/task/control-authority.js +2 -4
  35. package/dist/lib/task/current-contract.js +1 -5
  36. package/dist/lib/task/events.js +8 -274
  37. package/dist/lib/task/execution-inputs.js +14 -0
  38. package/dist/lib/task/finalization-envelope.js +4 -0
  39. package/dist/lib/task/finalization.js +10 -36
  40. package/dist/lib/task/frontmatter.js +1 -1
  41. package/dist/lib/task/ledger-intents.js +4 -7
  42. package/dist/lib/task/orchestration.js +11 -10
  43. package/dist/lib/task/platform-operation-journal.js +17 -10
  44. package/dist/lib/task/platform-operation-recovery.js +95 -28
  45. package/dist/lib/task/review-fingerprint.js +4 -26
  46. package/dist/lib/task/verification-engine.js +95 -6
  47. package/dist/lib/task/write.js +0 -11
  48. package/dist/lifecycle-build-manifest.json +3 -3
  49. package/lib/git/workflow.ts +1 -1
  50. package/lib/internal/cli-route-inventory.ts +0 -2
  51. package/lib/internal/sandbox-control.ts +39 -6
  52. package/lib/internal/task-finalization.ts +16 -24
  53. package/lib/internal/task-operation-registry.ts +2 -3
  54. package/lib/internal/task-platform-recovery.ts +9 -4
  55. package/lib/platform/completion-backfill.ts +2 -1
  56. package/lib/platform/context.ts +8 -4
  57. package/lib/platform/github-client.ts +24 -20
  58. package/lib/platform/github-provider.ts +47 -26
  59. package/lib/platform/github-release-notes.ts +5 -3
  60. package/lib/platform/in-label-sync.ts +2 -1
  61. package/lib/platform/issue-comments.ts +14 -9
  62. package/lib/platform/operation-coordinator.ts +1 -1
  63. package/lib/platform/pr-review.ts +2 -1
  64. package/lib/platform/provider-bridge.ts +10 -9
  65. package/lib/platform/provider-contract.ts +6 -21
  66. package/lib/platform/provider-loader.ts +7 -6
  67. package/lib/platform/provider-validation.ts +30 -8
  68. package/lib/platform/pull-requests.ts +4 -2
  69. package/lib/platform/repository-metadata.ts +3 -2
  70. package/lib/platform/security-alerts.ts +1 -1
  71. package/lib/platform/types.ts +2 -0
  72. package/lib/sandbox/control/client.ts +57 -6
  73. package/lib/sandbox/control/executor.ts +8 -3
  74. package/lib/sandbox/removal.ts +2 -10
  75. package/lib/task/artifact-command.ts +10 -1
  76. package/lib/task/artifact-lifecycle.ts +100 -52
  77. package/lib/task/artifact-selection.ts +1 -1
  78. package/lib/task/capabilities.ts +47 -24
  79. package/lib/task/commands/status.ts +0 -31
  80. package/lib/task/commit-identity.ts +24 -7
  81. package/lib/task/commit-intent.ts +58 -24
  82. package/lib/task/commit-operation.ts +17 -8
  83. package/lib/task/completion-facts.ts +31 -9
  84. package/lib/task/control-authority.ts +1 -4
  85. package/lib/task/current-contract.ts +1 -6
  86. package/lib/task/events.ts +8 -247
  87. package/lib/task/execution-inputs.ts +25 -0
  88. package/lib/task/finalization-envelope.ts +21 -0
  89. package/lib/task/finalization.ts +10 -35
  90. package/lib/task/frontmatter.ts +1 -1
  91. package/lib/task/ledger-intents.ts +4 -7
  92. package/lib/task/orchestration.ts +11 -17
  93. package/lib/task/platform-operation-journal.ts +18 -11
  94. package/lib/task/platform-operation-recovery.ts +90 -31
  95. package/lib/task/review-fingerprint.ts +4 -26
  96. package/lib/task/verification-engine.ts +103 -9
  97. package/lib/task/write.ts +0 -19
  98. package/package.json +1 -1
  99. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  100. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  101. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  102. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  104. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
  105. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  106. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  107. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  108. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  109. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  110. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  113. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  114. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  115. package/templates/.agents/templates/task.zh-CN.md +0 -12
  116. package/templates/.github/workflows/pr-label.yml +1 -2
  117. package/dist/lib/internal/task-invalidation.js +0 -63
  118. package/dist/lib/task/intermediate-cleanup.js +0 -327
  119. package/dist/lib/task/invalidation-command.js +0 -88
  120. package/dist/lib/task/invalidation.js +0 -242
  121. package/lib/internal/task-invalidation.ts +0 -41
  122. package/lib/task/intermediate-cleanup.ts +0 -393
  123. package/lib/task/invalidation-command.ts +0 -88
  124. package/lib/task/invalidation.ts +0 -313
@@ -4,7 +4,6 @@ import { createHash, randomUUID } from 'node:crypto';
4
4
  import { isResourceIdentity } from '../platform/resource-identity.js';
5
5
  import { resolveTaskRef } from './resolve-ref.js';
6
6
  const JOURNAL_FILE = '.platform-operations.json';
7
- const MAX_ATTEMPTS = 3;
8
7
  function operationId(input) {
9
8
  return createHash('sha256')
10
9
  .update(`${input.kind}\0${input.target}\0${input.expectedDigest}`)
@@ -36,8 +35,7 @@ function parseJournal(file, taskId) {
36
35
  || !/^[a-f0-9]{64}$/u.test(item.expectedDigest)
37
36
  || !['deferred', 'required'].includes(item.dependency)
38
37
  || !['queued', 'pending', 'unknown', 'succeeded', 'failed'].includes(item.state)
39
- || !Number.isSafeInteger(item.attempts) || item.attempts < 0 || item.attempts > MAX_ATTEMPTS
40
- || item.maxAttempts !== MAX_ATTEMPTS
38
+ || !Number.isSafeInteger(item.attempts) || item.attempts < 0
41
39
  || !(item.lastCode === null || typeof item.lastCode === 'string')
42
40
  || (item.kind === 'issue-metadata'
43
41
  ? !item.issueMetadata || typeof item.issueMetadata.requirements !== 'boolean'
@@ -120,7 +118,13 @@ function recordPlatformOperation(input) {
120
118
  const { taskId, file } = resolveJournal(input.taskRef, input.cwd);
121
119
  const journal = parseJournal(file, taskId);
122
120
  const id = operationId(input);
123
- const previous = journal.operations.find((item) => item.id === id);
121
+ const targetIndex = journal.operations.findIndex((item) => item.id === id);
122
+ const replacementIndex = input.replaceOperationId
123
+ ? journal.operations.findIndex((item) => item.id === input.replaceOperationId)
124
+ : -1;
125
+ const target = targetIndex >= 0 ? journal.operations[targetIndex] : undefined;
126
+ const replacement = replacementIndex >= 0 ? journal.operations[replacementIndex] : undefined;
127
+ const previousAttempts = Math.max(target?.attempts ?? 0, replacement?.attempts ?? 0);
124
128
  const next = {
125
129
  id,
126
130
  kind: input.kind,
@@ -128,8 +132,7 @@ function recordPlatformOperation(input) {
128
132
  expectedDigest: input.expectedDigest,
129
133
  dependency: input.dependency,
130
134
  state: input.state,
131
- attempts: Math.min(MAX_ATTEMPTS, (previous?.attempts ?? 0) + (input.state === 'pending' ? 1 : 0)),
132
- maxAttempts: MAX_ATTEMPTS,
135
+ attempts: previousAttempts + (input.state === 'pending' ? 1 : 0),
133
136
  lastCode: input.lastCode ?? null,
134
137
  ...(input.issueMetadata ? { issueMetadata: input.issueMetadata } : {}),
135
138
  ...(input.issueCreate ? { issueCreate: input.issueCreate } : {}),
@@ -138,9 +141,13 @@ function recordPlatformOperation(input) {
138
141
  ...(input.pullRequestReview ? { pullRequestReview: input.pullRequestReview } : {}),
139
142
  updatedAt: new Date().toISOString()
140
143
  };
141
- const operations = previous
142
- ? journal.operations.map((item) => item.id === id ? next : item)
143
- : [...journal.operations, next];
144
+ const operations = replacementIndex >= 0
145
+ ? journal.operations.flatMap((item, index) => index === replacementIndex
146
+ ? [next]
147
+ : index === targetIndex ? [] : [item])
148
+ : targetIndex >= 0
149
+ ? journal.operations.map((item, index) => index === targetIndex ? next : item)
150
+ : [...journal.operations, next];
144
151
  writeJournal(file, { version: 1, taskId, operations });
145
152
  return next;
146
153
  }
@@ -148,5 +155,5 @@ function readPlatformOperationJournal(taskRef, cwd) {
148
155
  const { taskId, file } = resolveJournal(taskRef, cwd);
149
156
  return parseJournal(file, taskId);
150
157
  }
151
- export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, MAX_ATTEMPTS as PLATFORM_OPERATION_MAX_ATTEMPTS, operationId, recordPlatformOperation, readPlatformOperationJournal };
158
+ export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, operationId, recordPlatformOperation, readPlatformOperationJournal };
152
159
  //# sourceMappingURL=platform-operation-journal.js.map
@@ -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};`)
@@ -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 = [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.9",
4
- "sourceInputHash": "17fe6c91408c93e3aa5185e816025d590ded7db7e6a305fdce00751e78723abc",
5
- "internalExecutableBuildHash": "8de9870e4ece1050564d7a3e77ee211a789dd975e3f94593300b7362ed815a00"
3
+ "packageVersion": "0.11.10",
4
+ "sourceInputHash": "3500e8eb7fac8bbad3d95124002f8042af3d160185a5b635eb9b4d8f88a991cb",
5
+ "internalExecutableBuildHash": "51a4c472b6a54b3ca3c28ab049683175d3ea4730fe560868f853cd603b5a98f1"
6
6
  }
@@ -100,7 +100,7 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
100
100
  const tree = value(runner, input.cwd, ['write-tree']);
101
101
  if (tree !== input.expectedTree) return { status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'applied' as const }], error: { code: 'GIT_TREE_MISMATCH', message: `Expected staged tree ${input.expectedTree}, received ${tree ?? 'unavailable'}` } };
102
102
  }
103
- const commit = run(runner, input.cwd, ['commit', '-m', input.message]);
103
+ const commit = run(runner, input.cwd, ['commit', '--cleanup=verbatim', '-m', input.message]);
104
104
  const after = inspectGitWorkflow(input.cwd, runner);
105
105
  if (commit.status !== 0) return { status: 'failed' as const, changed: false, snapshot: after.snapshot, operations: [{ name: 'commit', status: 'failed' as const, message: commit.stderr.trim() }], error: { code: 'GIT_COMMIT_FAILED', message: commit.stderr.trim() } };
106
106
  return { status: 'applied' as const, changed: true, snapshot: after.snapshot, operations: [{ name: 'stage', status: 'applied' as const }, { name: 'commit', status: 'applied' as const }], error: null };
@@ -60,7 +60,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
60
60
  'task-orchestration': ['status', 'progress'],
61
61
  'task-review': ['preflight', 'finalize-summary'],
62
62
  'task-event': ['event'],
63
- 'task-invalidation': ['reconcile'],
64
63
  'task-lifecycle': ['intent'],
65
64
  'task-finalization': ['complete'],
66
65
  'task-platform-recovery': ['inspect', 'recover'],
@@ -98,7 +97,6 @@ export function internalRouteSelector(command: string, args: readonly string[]):
98
97
  if (command === 'task-short-id') return first(args) === 'list' && args.includes('--verify') ? 'list-verify' : first(args);
99
98
  if (command === 'task-orchestration') return args[1] === 'status' ? 'status' : args[1] ? 'progress' : '';
100
99
  if (command === 'task-review') return args[1] ?? '';
101
- if (command === 'task-invalidation') return args[1] ?? '';
102
100
  if (command === 'task-artifact') return args[1] ?? '';
103
101
  if (command === 'platform-pr-review' && first(args) === 'publish') {
104
102
  const scope = optionValue(args, '--scope') ?? '';
@@ -9,6 +9,7 @@ import { serveSandboxControl } from '../sandbox/control/server.ts';
9
9
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
10
10
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
11
11
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
12
+ import { serializeTaskFinalizationEnvelope, type TaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
12
13
 
13
14
  type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
14
15
 
@@ -16,10 +17,11 @@ function writeFinalizationEnvelope(
16
17
  status: FinalizationStatus,
17
18
  accepted: boolean,
18
19
  error: { code: string; message: string; retryable: boolean },
19
- changed = false,
20
- result: unknown = null
20
+ changed: boolean | null = false,
21
+ result: unknown = null,
22
+ requestId: string | null = null
21
23
  ): void {
22
- process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
24
+ process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
23
25
  }
24
26
 
25
27
  function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
@@ -43,6 +45,28 @@ function recoveredExitCode(response: Awaited<ReturnType<typeof recoverSandboxCon
43
45
  return response.exitCode ?? 1;
44
46
  }
45
47
 
48
+ function finalizationResponseOutput(response: Awaited<ReturnType<typeof requestSandboxTaskFinalization>>): string {
49
+ try {
50
+ const value = JSON.parse(response.stdout) as Partial<TaskFinalizationEnvelope>;
51
+ if (value.version === 2
52
+ && value.status
53
+ && ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
54
+ && (typeof value.changed === 'boolean' || value.changed === null)) {
55
+ return serializeTaskFinalizationEnvelope({
56
+ status: value.status,
57
+ changed: value.changed,
58
+ accepted: true,
59
+ requestId: response.id,
60
+ result: value.result ?? null,
61
+ error: value.error ?? null
62
+ });
63
+ }
64
+ } catch {
65
+ // Preserve non-envelope output for diagnostics from the executor.
66
+ }
67
+ return response.stdout;
68
+ }
69
+
46
70
  function sandboxFinalizationClient(args: string[]): void {
47
71
  if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
48
72
  const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
@@ -63,7 +87,8 @@ function sandboxFinalizationClient(args: string[]): void {
63
87
  response = requestSandboxTaskFinalization({ agent });
64
88
  } catch (error) {
65
89
  if (!(error instanceof SandboxControlClientError)) throw error;
66
- writeFinalizationEnvelope(finalizationErrorStatus(error.detail), error.accepted, error.detail);
90
+ const status = finalizationErrorStatus(error.detail);
91
+ writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
67
92
  writeClientError(error);
68
93
  process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
69
94
  return;
@@ -74,12 +99,20 @@ function sandboxFinalizationClient(args: string[]): void {
74
99
  message: 'sandbox control rejected the finalization request',
75
100
  retryable: false
76
101
  };
77
- writeFinalizationEnvelope(finalizationErrorStatus(error), error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error);
102
+ const status = finalizationErrorStatus(error);
103
+ writeFinalizationEnvelope(
104
+ status,
105
+ error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN',
106
+ error,
107
+ status === 'unknown' ? null : false,
108
+ null,
109
+ response.id
110
+ );
78
111
  process.stderr.write(`${error.message}\n`);
79
112
  process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
80
113
  return;
81
114
  }
82
- process.stdout.write(response.stdout);
115
+ process.stdout.write(finalizationResponseOutput(response));
83
116
  process.stderr.write(response.stderr);
84
117
  process.exitCode = response.exitCode ?? 1;
85
118
  }