@fitlab-ai/agent-infra 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
  10. package/dist/lib/git/workflow.js +10 -1
  11. package/dist/lib/internal/codex-lifecycle.js +19 -5
  12. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  13. package/dist/lib/internal/git-workflow.js +5 -7
  14. package/dist/lib/internal/platform-pr.js +20 -7
  15. package/dist/lib/internal/release-workflow.js +2 -1
  16. package/dist/lib/internal/sandbox-control.js +79 -2
  17. package/dist/lib/internal/task-finalization.js +54 -0
  18. package/dist/lib/internal/task-orchestration.js +156 -291
  19. package/dist/lib/platform/pr-summary.js +48 -10
  20. package/dist/lib/platform/pull-requests.js +205 -41
  21. package/dist/lib/sandbox/commands/rm.js +316 -55
  22. package/dist/lib/sandbox/control/client.js +190 -6
  23. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  24. package/dist/lib/sandbox/control/executor.js +137 -5
  25. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  26. package/dist/lib/sandbox/control/protocol.js +67 -6
  27. package/dist/lib/sandbox/index.js +5 -3
  28. package/dist/lib/sandbox/managed-fs.js +5 -0
  29. package/dist/lib/sandbox/task-resolver.js +50 -6
  30. package/dist/lib/sandbox/workspace-identity.js +27 -0
  31. package/dist/lib/sandbox/worktree-safety.js +215 -2
  32. package/dist/lib/task/codex-orchestration.js +158 -33
  33. package/dist/lib/task/commands/status.js +59 -1
  34. package/dist/lib/task/commit-operation.js +335 -0
  35. package/dist/lib/task/commit-policy.js +18 -0
  36. package/dist/lib/task/delegation-receipts.js +243 -2
  37. package/dist/lib/task/finalization.js +567 -0
  38. package/dist/lib/task/operation-outcome.js +33 -0
  39. package/dist/lib/task/orchestration.js +187 -761
  40. package/dist/lib/task/short-id.js +59 -1
  41. package/dist/lib/task/task-execution-lock.js +6 -1
  42. package/dist/lib/task/verification-engine.js +113 -60
  43. package/dist/lib/task/verification.js +2 -1
  44. package/dist/lib/task/workflow-warning-intents.js +50 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  47. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  49. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  51. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
  53. package/lib/git/workflow.ts +11 -2
  54. package/lib/internal/codex-lifecycle.ts +18 -5
  55. package/lib/internal/codex-sandbox-controller.ts +6 -12
  56. package/lib/internal/git-workflow.ts +5 -6
  57. package/lib/internal/platform-pr.ts +14 -7
  58. package/lib/internal/release-workflow.ts +2 -1
  59. package/lib/internal/sandbox-control.ts +92 -2
  60. package/lib/internal/task-finalization.ts +68 -0
  61. package/lib/internal/task-orchestration.ts +20 -103
  62. package/lib/platform/pr-summary.ts +55 -11
  63. package/lib/platform/pull-requests.ts +211 -41
  64. package/lib/sandbox/commands/rm.ts +413 -61
  65. package/lib/sandbox/control/client.ts +255 -6
  66. package/lib/sandbox/control/controller-registration.ts +347 -0
  67. package/lib/sandbox/control/executor.ts +163 -5
  68. package/lib/sandbox/control/lifecycle.ts +2 -1
  69. package/lib/sandbox/control/protocol.ts +80 -7
  70. package/lib/sandbox/index.ts +5 -3
  71. package/lib/sandbox/managed-fs.ts +5 -0
  72. package/lib/sandbox/task-resolver.ts +58 -7
  73. package/lib/sandbox/workspace-identity.ts +50 -2
  74. package/lib/sandbox/workspace-view.ts +3 -3
  75. package/lib/sandbox/worktree-safety.ts +236 -2
  76. package/lib/task/codex-orchestration.ts +167 -33
  77. package/lib/task/commands/status.ts +78 -1
  78. package/lib/task/commit-operation.ts +430 -0
  79. package/lib/task/commit-policy.ts +30 -0
  80. package/lib/task/delegation-receipts.ts +258 -1
  81. package/lib/task/finalization.ts +719 -0
  82. package/lib/task/operation-outcome.ts +48 -0
  83. package/lib/task/orchestration.ts +217 -982
  84. package/lib/task/short-id.ts +65 -1
  85. package/lib/task/task-execution-lock.ts +10 -1
  86. package/lib/task/verification-engine.ts +118 -63
  87. package/lib/task/verification.ts +3 -2
  88. package/lib/task/workflow-warning-intents.ts +57 -2
  89. package/package.json +2 -2
  90. package/templates/.agents/QUICKSTART.en.md +2 -2
  91. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  92. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  95. package/templates/.agents/rules/task-management.en.md +2 -2
  96. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  97. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  98. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  99. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  100. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  101. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  102. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  104. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  105. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  106. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  107. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  108. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  115. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  116. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  117. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  118. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  119. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  120. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  121. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  122. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  123. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  124. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  125. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  126. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  127. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  128. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  129. package/templates/.codex/hooks.json +4 -4
  130. package/dist/lib/task/commit-finalization.js +0 -305
  131. package/dist/lib/task/commit-intent.js +0 -165
  132. package/lib/task/commit-finalization.ts +0 -419
  133. package/lib/task/commit-intent.ts +0 -230
@@ -201,6 +201,10 @@ type RegistrySchema = {
201
201
  ids: Record<string, string>;
202
202
  };
203
203
 
204
+ type ShortIdRegistryInspection =
205
+ | { status: 'valid'; shortIds: Map<string, string> }
206
+ | { status: 'missing' | 'invalid'; error: { code: string; message: string } };
207
+
204
208
  type ShortIdMutationEffect = 'alloc' | 'release' | 'none';
205
209
  type ShortIdMutationResult = {
206
210
  effect: 'allocated' | 'released' | 'unchanged';
@@ -446,6 +450,64 @@ function loadShortIdByTaskId(repoRoot: string): Map<string, string> {
446
450
  return map;
447
451
  }
448
452
 
453
+ function inspectShortIdRegistry(repoRoot: string): ShortIdRegistryInspection {
454
+ const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', REGISTRY_NAME);
455
+ if (!fs.existsSync(registryPath)) {
456
+ return {
457
+ status: 'missing',
458
+ error: {
459
+ code: 'SHORT_ID_REGISTRY_NOT_FOUND',
460
+ message: 'canonical short-id registry is missing'
461
+ }
462
+ };
463
+ }
464
+
465
+ let raw: string;
466
+ try {
467
+ raw = fs.readFileSync(registryPath, 'utf8');
468
+ } catch {
469
+ return {
470
+ status: 'invalid',
471
+ error: {
472
+ code: 'SHORT_ID_REGISTRY_READ_FAILED',
473
+ message: 'canonical short-id registry is unreadable'
474
+ }
475
+ };
476
+ }
477
+
478
+ let parsed: unknown;
479
+ try {
480
+ parsed = JSON.parse(raw);
481
+ } catch {
482
+ return {
483
+ status: 'invalid',
484
+ error: {
485
+ code: 'SHORT_ID_REGISTRY_INVALID_JSON',
486
+ message: 'canonical short-id registry contains invalid JSON'
487
+ }
488
+ };
489
+ }
490
+
491
+ try {
492
+ const registry = validateRegistry(parsed, registryPath, configuredShortIdLength(repoRoot));
493
+ const shortIds = new Map<string, string>();
494
+ for (const [key, taskId] of Object.entries(registry.ids)) shortIds.set(taskId, key);
495
+ return { status: 'valid', shortIds };
496
+ } catch (error) {
497
+ return {
498
+ status: 'invalid',
499
+ error: {
500
+ code: typeof (error as { code?: unknown }).code === 'string'
501
+ ? (error as { code: string }).code
502
+ : 'SHORT_ID_REGISTRY_INVALID_SCHEMA',
503
+ message: (error as { code?: unknown }).code === 'SHORT_ID_REGISTRY_DUPLICATE_TASK'
504
+ ? 'canonical short-id registry contains duplicate task entries'
505
+ : 'canonical short-id registry has invalid schema'
506
+ }
507
+ };
508
+ }
509
+ }
510
+
449
511
  /**
450
512
  * Resolve a branch to its active-task short id (`NN`), or `null` when no
451
513
  * active task is bound to that branch.
@@ -485,6 +547,7 @@ export {
485
547
  resolveShortIdReadOnly,
486
548
  lookupShortIdByBranch,
487
549
  loadShortIdByTaskId,
550
+ inspectShortIdRegistry,
488
551
  configuredShortIdLength,
489
552
  mutateShortIdRegistry,
490
553
  executeShortIdCommand
@@ -497,5 +560,6 @@ export type {
497
560
  ShortIdMutationEffect,
498
561
  ShortIdMutationResult,
499
562
  ShortIdCommandRequest,
500
- ShortIdCommandResult
563
+ ShortIdCommandResult,
564
+ ShortIdRegistryInspection
501
565
  };
@@ -272,6 +272,7 @@ function withTaskExecutionLock<T>(
272
272
  return callback();
273
273
  } catch (error) {
274
274
  if (error instanceof TaskExecutionLockError) throw error;
275
+ if (error instanceof Error && error.name === 'OrchestrationStateError') throw error;
275
276
  throw new TaskExecutionLockError(
276
277
  'ORCHESTRATION_LOCK_FAILED',
277
278
  'lifecycle task lock operation failed',
@@ -294,5 +295,13 @@ function withTaskExecutionLock<T>(
294
295
  }
295
296
  }
296
297
 
297
- export { TaskExecutionLockError, lockKey, mapLinkError, withTaskExecutionLock };
298
+ function withRepositoryMutationLock<T>(
299
+ repoRoot: string,
300
+ callback: () => T,
301
+ options: TaskExecutionLockOptions = {}
302
+ ): T {
303
+ return withTaskExecutionLock(repoRoot, '__repository_mutation__', 'repository-mutation', callback, options);
304
+ }
305
+
306
+ export { TaskExecutionLockError, lockKey, mapLinkError, withRepositoryMutationLock, withTaskExecutionLock };
298
307
  export type { LinkDisposition, LinkOperation, TaskExecutionLockErrorCode, TaskExecutionLockOptions, TaskExecutionLockOwner };
@@ -27,6 +27,7 @@ import type { LedgerRow } from "./ledger.ts";
27
27
  import { equalCounts, parseReviewSummary } from "./review-artifacts.ts";
28
28
  import { loadVerificationConfig } from "./verification-config.ts";
29
29
  import { snapshotReview } from "../git/review-snapshot.ts";
30
+ import { OrchestrationStateError, readRun } from "./orchestration.ts";
30
31
 
31
32
  const TASK_ENUMS = {
32
33
  type: ["feature", "bugfix", "refactor", "docs", "chore"],
@@ -123,6 +124,8 @@ function runCheck(type: any, context: any, shared: any): any {
123
124
  return checkReviewFact(context);
124
125
  case "post-review-commit":
125
126
  return checkPostReviewCommit(context);
127
+ case "required-pr-delivery":
128
+ return checkRequiredPrDelivery(context);
126
129
  case "orchestration-state":
127
130
  return checkOrchestrationState(context);
128
131
  case "orchestration-evidence":
@@ -142,40 +145,19 @@ function runCheck(type: any, context: any, shared: any): any {
142
145
  }
143
146
  }
144
147
 
145
- function isFailClosedLegacyPause(run: any): boolean {
146
- if (
147
- run.schemaVersion !== 1
148
- || run.status !== 'paused'
149
- || run.pause?.recoverable !== false
150
- || !exactText(run.pause?.message)
151
- || !Array.isArray(run.receipts)
152
- ) {
153
- return false;
154
- }
155
- if (run.pause.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED') {
156
- return run.receipts.length > 0 && run.pendingDelegation === null;
157
- }
158
- if (run.pause.code === 'ORCHESTRATION_DELEGATION_BUSY') {
159
- return run.pendingDelegation != null && run.pendingDelegation.status !== 'sealed';
160
- }
161
- return false;
162
- }
163
-
164
148
  function checkOrchestrationState({ taskDir }: any): any {
165
149
  const file = path.join(taskDir, 'orchestration.json');
166
150
  const stat = safeStat(file);
167
151
  if (!stat?.isFile()) return failResult('orchestration-state', 'orchestration.json is missing');
168
152
  let run: any;
169
153
  try {
170
- run = JSON.parse(fs.readFileSync(file, 'utf8'));
154
+ run = readRun(taskDir);
171
155
  } catch (error) {
172
- return failResult('orchestration-state', `Invalid orchestration.json: ${String(error)}`);
156
+ const message = error instanceof OrchestrationStateError ? error.message : String(error);
157
+ return failResult('orchestration-state', `Invalid orchestration.json: ${message}`);
173
158
  }
174
- if (isFailClosedLegacyPause(run)) {
175
- return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
176
- }
177
- if (![2, 3].includes(run.schemaVersion) || !['paused', 'completed'].includes(run.status)) {
178
- return failResult('orchestration-state', `Expected paused or completed run, received '${run.status}'`);
159
+ if (!run || !['paused', 'completed'].includes(run.status)) {
160
+ return failResult('orchestration-state', `Expected paused or completed run, received '${run?.status ?? 'missing'}'`);
179
161
  }
180
162
  if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
181
163
  return failResult('orchestration-state', 'Paused run requires a stable pause code and message');
@@ -242,12 +224,10 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
242
224
  if (!stat?.isFile()) return failResult('orchestration-evidence', 'orchestration.json is missing');
243
225
  let run: any;
244
226
  try {
245
- run = JSON.parse(fs.readFileSync(file, 'utf8'));
227
+ run = readRun(taskDir);
246
228
  } catch (error) {
247
- return failResult('orchestration-evidence', `Invalid orchestration.json: ${String(error)}`);
248
- }
249
- if (isFailClosedLegacyPause(run)) {
250
- return passResult('orchestration-evidence', 'Legacy orchestration evidence is preserved in a fail-closed pause');
229
+ const message = error instanceof OrchestrationStateError ? error.message : String(error);
230
+ return failResult('orchestration-evidence', `Invalid orchestration.json: ${message}`);
251
231
  }
252
232
  const policy = run.modelPolicy;
253
233
  if (
@@ -269,30 +249,8 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
269
249
  return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
270
250
  }
271
251
  for (const recovery of run.recoveryHistory) {
272
- const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
273
- && recovery.previousSchemaVersion === 1
274
- && recovery.receiptCount === 0
275
- && recovery.pendingDelegation === false
276
- && exactText(recovery.recoveredAt)
277
- && recovery.policySource;
278
252
  const guards = recovery.guards;
279
- const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
280
- && recovery.previousSchemaVersion === 2
281
- && recovery.previousStatus === 'paused'
282
- && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
283
- && recovery.client === 'codex'
284
- && recovery.resultingStatus === 'running'
285
- && exactText(recovery.recoveredAt)
286
- && guards?.stepCount === 0
287
- && guards?.nextStage === null
288
- && guards?.baselineEmpty === true
289
- && guards?.receiptCount === 0
290
- && guards?.pendingDelegation === false
291
- && guards?.commitAuthorizationUnused === true
292
- && guards?.completionEvidenceAbsent === true
293
- && guards?.commitIntentAbsent === true;
294
- const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION'
295
- && recovery.previousSchemaVersion === 3
253
+ const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
296
254
  && recovery.previousStatus === 'paused'
297
255
  && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
298
256
  && recovery.client === 'claude-code'
@@ -304,9 +262,8 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
304
262
  && guards?.receiptCount === 0
305
263
  && guards?.pendingDelegation === false
306
264
  && guards?.commitAuthorizationUnused === true
307
- && guards?.completionEvidenceAbsent === true
308
- && guards?.commitIntentAbsent === true;
309
- if (!validModelRecovery && !validCapabilityRecovery && !validClaudeCodeRecovery) {
265
+ && guards?.completionEvidenceAbsent === true;
266
+ if (!validClaudeCodeRecovery) {
310
267
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
311
268
  }
312
269
  }
@@ -376,12 +333,37 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
376
333
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
377
334
  }
378
335
  if (receipt.client === 'codex') {
336
+ const activatedCodex = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
379
337
  const host = receipt.hostEvidence;
338
+ const provenance = receipt.lifecycleProvenance;
380
339
  if (
381
- host?.kind !== 'codex-lifecycle-v1'
382
- || !exactText(host.hookDefinitionHash)
383
- || !Number.isSafeInteger(host.startRevision)
384
- || host.startRevision < 1
340
+ !provenance
341
+ || (activatedCodex && (
342
+ host?.kind !== 'codex-lifecycle-v2'
343
+ || host.protocolVersion !== provenance.protocolVersion
344
+ || host.packageVersion !== provenance.packageVersion
345
+ || host.internalExecutableBuildHash !== provenance.internalExecutableBuildHash
346
+ || host.lifecycleContractHash !== provenance.lifecycleContractHash
347
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
348
+ || host.hookSource !== provenance.hookSource
349
+ || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
350
+ || host.hookSourceHash !== provenance.hookSourceHash
351
+ || host.capabilitySessionId !== provenance.capabilitySessionId
352
+ || receipt.parentId !== provenance.capabilitySessionId
353
+ || host.capabilityTurnId !== provenance.capabilityTurnId
354
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
355
+ || host.controlGeneration !== provenance.controlGeneration
356
+ || host.spawnToolUseId === provenance.capabilityToolUseId
357
+ || !exactText(host.spawnToolUseId)
358
+ || !Number.isFinite(Date.parse(host.spawnObservedAt ?? ''))
359
+ || !Number.isFinite(Date.parse(receipt.spawnDispatchedAt ?? ''))
360
+ || !Number.isFinite(Date.parse(receipt.activationDeadlineAt ?? ''))
361
+ || Date.parse(host.spawnObservedAt ?? '') < Date.parse(receipt.spawnDispatchedAt ?? '')
362
+ || Date.parse(host.spawnObservedAt ?? '') > Date.parse(receipt.activationDeadlineAt ?? '')
363
+ || !exactText(host.hookDefinitionHash)
364
+ || !Number.isSafeInteger(host.startRevision)
365
+ || host.startRevision < 1
366
+ ))
385
367
  ) {
386
368
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
387
369
  }
@@ -510,6 +492,63 @@ function checkTaskMeta({ taskDir, config }: any): any {
510
492
  );
511
493
  }
512
494
 
495
+ function checkRequiredPrDelivery({ taskDir, repositoryRoot, mode }: any): any {
496
+ const task = loadTask(taskDir);
497
+ if (!task.ok) return failResult('required-pr-delivery', task.message);
498
+ let projectConfig: any = {};
499
+ try {
500
+ const file = path.join(repositoryRoot, '.agents', '.airc.json');
501
+ if (safeStat(file)?.isFile()) projectConfig = JSON.parse(fs.readFileSync(file, 'utf8')) as any;
502
+ } catch {
503
+ return failResult('required-pr-delivery', 'Project configuration is invalid');
504
+ }
505
+ const flow = projectConfig.prFlow;
506
+ const status = String(task.metadata.pr_status || 'pending');
507
+ const prNumber = parsePrNumber(task.metadata.pr_number);
508
+ if (flow === 'disabled') return passResult('required-pr-delivery', 'Pull request delivery is disabled by project policy');
509
+ if (flow === 'required' && (status !== 'created' || prNumber === null)) {
510
+ return failResult('required-pr-delivery', 'Project policy requires a bound pull request before completion');
511
+ }
512
+ if (flow !== 'required' && status === 'pending') {
513
+ return failResult('required-pr-delivery', 'Pull request delivery is pending; create a PR or explicitly skip it');
514
+ }
515
+ if (status === 'created' && prNumber === null) {
516
+ return failResult('required-pr-delivery', 'Task claims a created pull request without a valid pr_number');
517
+ }
518
+ if (flow === 'required') {
519
+ if (mode === 'gate') {
520
+ return passResult('required-pr-delivery', 'Bound pull request shape is valid; merged-state inspection is reserved for hard preflight');
521
+ }
522
+ const inspected = inspectPlatformPullRequest(task.metadata.id, { cwd: repositoryRoot });
523
+ if (inspected.status === 'blocked') {
524
+ return blockedResult(
525
+ 'required-pr-delivery',
526
+ inspected.error?.message || 'Unable to inspect the required pull request',
527
+ inspected.error?.code || 'PR_INSPECTION_BLOCKED'
528
+ );
529
+ }
530
+ const pullRequest = inspected.pullRequest;
531
+ const repository = inspected.platform.repository?.toLowerCase() || '';
532
+ const taskBranch = String(task.metadata.branch || '').trim();
533
+ if (inspected.status !== 'no-op' || !pullRequest || pullRequest.number !== prNumber) {
534
+ return failResult('required-pr-delivery', 'Bound pull request identity could not be verified');
535
+ }
536
+ if (
537
+ !repository
538
+ || pullRequest.repository.toLowerCase() !== repository
539
+ || pullRequest.base.repository.toLowerCase() !== repository
540
+ || (taskBranch && pullRequest.head.ref !== taskBranch)
541
+ ) {
542
+ return failResult('required-pr-delivery', 'Bound pull request identity does not match the task');
543
+ }
544
+ if (pullRequest.state !== 'closed' || !pullRequest.mergedAt || !pullRequest.mergeCommitSha) {
545
+ return failResult('required-pr-delivery', 'Project policy requires the bound pull request to be merged');
546
+ }
547
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (merged PR #${prNumber})`);
548
+ }
549
+ return passResult('required-pr-delivery', `Pull request delivery policy satisfied (${status})`);
550
+ }
551
+
513
552
  function validateTaskBranch(metadata: any): any {
514
553
  if (isBlank(metadata.branch)) {
515
554
  return null;
@@ -1600,7 +1639,14 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1600
1639
  const checks = [];
1601
1640
  for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
1602
1641
  if (checkConfig === null) continue;
1603
- const result = runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config: checkConfig }, shared);
1642
+ const result = runCheck(type, {
1643
+ mode,
1644
+ skillName,
1645
+ taskDir: path.resolve(taskDir),
1646
+ artifactFile,
1647
+ repositoryRoot: repoRoot,
1648
+ config: checkConfig
1649
+ }, shared);
1604
1650
  checks.push(result);
1605
1651
  if (result.status === "blocked") break;
1606
1652
  }
@@ -1611,7 +1657,16 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
1611
1657
  const config = (verifyConfig.checks || {})[type];
1612
1658
  if (config === undefined) return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
1613
1659
  if (config === null) return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
1614
- return { skill: skillName, ...runCheck(type, { skillName, taskDir: path.resolve(taskDir), artifactFile, config }, shared) };
1660
+ return {
1661
+ skill: skillName,
1662
+ ...runCheck(type, {
1663
+ skillName,
1664
+ taskDir: path.resolve(taskDir),
1665
+ artifactFile,
1666
+ repositoryRoot: repoRoot,
1667
+ config
1668
+ }, shared)
1669
+ };
1615
1670
  }
1616
1671
 
1617
1672
  export { verifyInProcess };
@@ -15,7 +15,7 @@ type VerificationEvent =
15
15
  | 'review-analysis.completed' | 'plan.completed' | 'review-plan.completed'
16
16
  | 'code.completed' | 'review-code.completed' | 'manual-validation.completed' | 'validation-run.completed'
17
17
  | 'block-task.completed' | 'cancel-task.completed' | 'commit.completed'
18
- | 'complete-task.preflight' | 'complete-task.completed'
18
+ | 'complete-task.preflight' | 'complete-task.hard-preflight' | 'complete-task.completed'
19
19
  | 'create-pr.completed' | 'create-task.completed'
20
20
  | 'import-codescan.completed' | 'import-dependabot.completed'
21
21
  | 'import-issue.completed' | 'watch-pr.completed'
@@ -69,7 +69,8 @@ const VERIFICATION_CATALOG: Readonly<Record<VerificationEvent, VerificationSpec>
69
69
  'block-task.completed': gate('block-task', 'blocked'),
70
70
  'cancel-task.completed': gate('cancel-task', 'completed'),
71
71
  'commit.completed': gate('commit', 'active'),
72
- 'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['review-ledger', 'manual-validation', 'post-review-commit', 'platform-sync-preflight'] },
72
+ 'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
73
+ 'complete-task.hard-preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['required-pr-delivery'] },
73
74
  'complete-task.completed': gate('complete-task', 'completed'),
74
75
  'create-pr.completed': gate('create-pr', 'active'),
75
76
  'create-task.completed': gate('create-task', 'active'),
@@ -12,6 +12,7 @@ import type { WorkflowWarning } from './workflow-warnings.ts';
12
12
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
13
13
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
14
14
  import { allowsManualOverride } from './guard-override.ts';
15
+ import { retryHintForWarning, type OperationWarning } from './operation-outcome.ts';
15
16
 
16
17
  type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
17
18
  type WarningStatus = 'open' | 'resolved' | 'ignored';
@@ -28,6 +29,12 @@ type WorkflowWarningIntentResult = {
28
29
  error: { code: string; message: string } | null;
29
30
  };
30
31
 
32
+ type FinalizationWarningProjectionResult = Readonly<{
33
+ status: 'applied' | 'no-op' | 'failed';
34
+ changed: boolean;
35
+ error: { code: string; message: string } | null;
36
+ }>;
37
+
31
38
  function failed(intent: WorkflowWarningIntent, code: string, message: string, taskId: string | null = null, entityId: string | null = null): WorkflowWarningIntentResult {
32
39
  return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, warnings: [], operations: [], error: { code, message } };
33
40
  }
@@ -140,5 +147,53 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
140
147
  };
141
148
  }
142
149
 
143
- export { applyWorkflowWarningIntent };
144
- export type { WarningSeverity, WarningStatus, WorkflowWarningIntent, WorkflowWarningIntentResult };
150
+ function projectFinalizationWarning(
151
+ taskRef: string,
152
+ warning: OperationWarning & { status: 'open' | 'resolved'; resolvedAt: string | null },
153
+ options: TaskWriteOptions = {}
154
+ ): FinalizationWarningProjectionResult {
155
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
156
+ if (!resolved.ok) return { status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } };
157
+ if (resolved.state !== 'completed') return { status: 'failed', changed: false, error: { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected completed` } };
158
+ let content: string;
159
+ let rows: WorkflowWarning[];
160
+ try {
161
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
162
+ rows = parseWorkflowWarnings(content);
163
+ } catch (error) {
164
+ return { status: 'failed', changed: false, error: { code: 'WARNING_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) } };
165
+ }
166
+ const metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
167
+ const existing = rows.find((row) => row.step === warning.step && row.code === warning.code && row.target === warning.target);
168
+ const after: WorkflowWarning = existing
169
+ ? {
170
+ ...existing,
171
+ severity: warning.severity,
172
+ status: warning.status,
173
+ message: warning.message,
174
+ action: retryHintForWarning(warning),
175
+ resolvedAt: warning.resolvedAt || (warning.status === 'resolved' ? metadata.timestamp : ''),
176
+ resolution: warning.status === 'resolved' ? 'Resolved by a successful finalization retry' : ''
177
+ }
178
+ : {
179
+ id: nextWarningId(rows), time: metadata.timestamp, step: warning.step,
180
+ severity: warning.severity, code: warning.code, status: warning.status,
181
+ target: warning.target, message: warning.message, action: retryHintForWarning(warning),
182
+ resolvedAt: warning.resolvedAt || '', resolution: warning.status === 'resolved' ? 'Resolved by a successful finalization retry' : ''
183
+ };
184
+ if (existing && JSON.stringify(existing) === JSON.stringify(after)) return { status: 'no-op', changed: false, error: null };
185
+ const written = writeTask({
186
+ taskRef: resolved.taskId,
187
+ expectedState: 'completed',
188
+ mutations: [...sectionMutation(content), rowMutation(after)]
189
+ }, {
190
+ ...options,
191
+ taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: 'completed' },
192
+ metadataProvider: () => metadata
193
+ });
194
+ if (written.status === 'failed') return { status: 'failed', changed: false, error: { code: written.error.code, message: written.error.message } };
195
+ return { status: written.status === 'applied' ? 'applied' : 'no-op', changed: written.changed, error: null };
196
+ }
197
+
198
+ export { applyWorkflowWarningIntent, projectFinalizationWarning };
199
+ export type { FinalizationWarningProjectionResult, WarningSeverity, WarningStatus, WorkflowWarningIntent, WorkflowWarningIntentResult };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
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",
@@ -54,7 +54,7 @@
54
54
  "yaml": "^2.9.0"
55
55
  },
56
56
  "scripts": {
57
- "build": "tsc -p tsconfig.json && node scripts/build.js && node scripts/build-inline.js",
57
+ "build": "node scripts/run-build.js",
58
58
  "demo:regen": "sh scripts/demo-regen.sh",
59
59
  "prepare": "git config core.hooksPath .git-hooks || true",
60
60
  "typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
@@ -20,14 +20,14 @@ This makes Git invoke the hooks in the project repository's `.git-hooks/` direct
20
20
 
21
21
  ## Development Checkout CLI Alignment
22
22
 
23
- When developing agent-infra from a source checkout, install dependencies and rebuild the local CLI before running lifecycle orchestration:
23
+ When developing the agent-infra CLI from a source checkout, install dependencies and rebuild it as needed:
24
24
 
25
25
  ```bash
26
26
  npm install
27
27
  npm run build
28
28
  ```
29
29
 
30
- Lifecycle hooks prefer the checkout's `dist/bin/internal-cli.js` when it exists and fall back to `agent-infra-internal` on `PATH` otherwise. Rebuild after CLI source changes. If shell commands must also resolve to this checkout, link it and compare the active version with the package version:
30
+ Lifecycle hooks always invoke `agent-infra-internal` through `PATH`; they do not implicitly switch to checkout source or `dist`. In a Codex sandbox, the controller's `PATH`-first shim is authoritative and binds the controller's selected executable. Skills, rules, hooks, and task data continue to come from the current checkout. The commands above develop the CLI itself and do not change hook executable selection. If shell commands must also resolve to this checkout, link it and compare the active version with the package version:
31
31
 
32
32
  ```bash
33
33
  npm link
@@ -20,14 +20,14 @@ git config core.hooksPath .git-hooks
20
20
 
21
21
  ## 开发检出中的 CLI 对齐
22
22
 
23
- 从源码检出开发 agent-infra 时,请先安装依赖并重新构建本地 CLI,再运行生命周期编排:
23
+ 从源码检出开发 agent-infra CLI 时,请按需安装依赖并重新构建 CLI:
24
24
 
25
25
  ```bash
26
26
  npm install
27
27
  npm run build
28
28
  ```
29
29
 
30
- 本地 `dist/bin/internal-cli.js` 存在时,生命周期 hook 会优先使用它;不存在时则回退到 `PATH` 上的 `agent-infra-internal`。修改 CLI 源码后需要重新构建。如果 shell 命令也必须解析到当前检出,请建立链接并比较当前版本与包版本:
30
+ 生命周期 hook 始终通过 `PATH` 调用 `agent-infra-internal`,不会隐式切换到当前检出的 source 或 `dist`。在 Codex sandbox 中,controller 放在 `PATH` 首位的 shim 是权威入口,并绑定 controller 选定的 executable。skills、rules、hooks 和任务数据仍从当前检出读取。上面的命令只用于开发 CLI 本身,不会改变 hook 的 executable 选择。如果 shell 命令也必须解析到当前检出,请建立链接并比较当前版本与包版本:
31
31
 
32
32
  ```bash
33
33
  npm link
@@ -1,13 +1,9 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync } from 'node:fs';
4
3
  import { readFileSync } from 'node:fs';
5
4
  import { fileURLToPath } from 'node:url';
6
5
 
7
6
  const MAX_INPUT_BYTES = 64 * 1024;
8
- const repoPackageJson = fileURLToPath(new URL('../../package.json', import.meta.url));
9
- const sourceInternalCli = fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url));
10
- const localInternalCli = fileURLToPath(new URL('../../dist/bin/internal-cli.js', import.meta.url));
11
7
  const codexHooks = fileURLToPath(new URL('../../.codex/hooks.json', import.meta.url));
12
8
  const clientIndex = process.argv.indexOf('--client');
13
9
  const client = clientIndex >= 0 ? process.argv[clientIndex + 1] : '';
@@ -17,22 +13,6 @@ if (client !== 'claude-code' && client !== 'codex') {
17
13
  }
18
14
 
19
15
  function internalCliInvocation(args) {
20
- let isAgentInfraSourceCheckout = false;
21
- try {
22
- isAgentInfraSourceCheckout = JSON.parse(readFileSync(repoPackageJson, 'utf8')).name === '@fitlab-ai/agent-infra';
23
- } catch {
24
- // Installed project hooks do not require a repository package.json.
25
- }
26
- if (isAgentInfraSourceCheckout && existsSync(sourceInternalCli)) {
27
- return {
28
- command: process.execPath,
29
- commandArgs: ['--experimental-strip-types', sourceInternalCli, ...args],
30
- shell: false
31
- };
32
- }
33
- if (existsSync(localInternalCli)) {
34
- return { command: process.execPath, commandArgs: [localInternalCli, ...args], shell: false };
35
- }
36
16
  return { command: 'agent-infra-internal', commandArgs: args, shell: process.platform === 'win32' };
37
17
  }
38
18
 
@@ -125,7 +105,7 @@ process.stdin.on('end', () => {
125
105
  input: JSON.stringify(normalized),
126
106
  shell,
127
107
  stdio: ['pipe', 'pipe', 'pipe'],
128
- timeout: 15_000
108
+ timeout: 30_000
129
109
  });
130
110
  process.stdout.write(output);
131
111
  } catch (error) {
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## Recovery
12
12
 
13
- `orchestration.json` is the detailed state source. A v3 run persists complete policy, append-only recovery, build/contract/hook-source/controller provenance, and activation monotonic timestamps. Historical v2 state is read-compatible, but new Codex delegations require v3. An expired prepared orphan may be explicitly recovered only with the exact task fingerprint, no commit intent or consumed authorization, and no matching unconsumed active lifecycle evidence. Unknown schemas and incomplete active v3 state fail closed.
13
+ `orchestration.json` is the detailed state source. A current run persists complete policy, append-only recovery, build/contract/hook-source/controller provenance, and activation monotonic timestamps. Readers accept only the complete structure emitted by the current writer; unknown fields, missing fields, invalid provenance, and old runs fail closed without rewriting. Finish or clear every active run before upgrading agent-infra. An expired prepared orphan may be explicitly recovered only with the exact task fingerprint, no consumed authorization, and no matching unconsumed active lifecycle evidence.
14
14
 
15
15
  ## Codex Host and Capability
16
16
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## 恢复语义
12
12
 
13
- `orchestration.json` 是详细状态源。v3 run 保存完整策略、append-only 恢复历史、build/contract/hook source/controller provenance 与 activation 单调时钟。只有无 pending delegation 且零历史 receipt 的 v1 run 可在补齐完整策略后升级;历史 v2 状态仅兼容读取,新的 Codex delegation 必须写 v3。prepared orphan 只允许在 deadline 已过、task fingerprint 精确不变、无 commit intent/authorization 消耗且无匹配的未消费 active lifecycle evidence 时显式恢复。迁移是 forward-only,未知 schema 或缺 provenance 的 active v3 一律失败关闭。
13
+ `orchestration.json` 是详细状态源。current run 保存完整策略、append-only 恢复历史、build/contract/hook source/controller provenance 与 activation 单调时钟。读取时只接受当前 writer 的完整结构;未知字段、缺失字段、非法 provenance 或旧 run 一律失败关闭且不改写。升级 agent-infra 前必须完成或清空所有 active run。prepared orphan 只允许在 deadline 已过、task fingerprint 精确不变、无 authorization 消耗且无匹配的未消费 active lifecycle evidence 时显式恢复。
14
14
 
15
15
  ## Codex 宿主与 capability
16
16
 
@@ -28,7 +28,7 @@ Map user intent to the corresponding workflow command:
28
28
  - Historical `gemini` / `gemini-cli` remain read-only for `ai task log` rendering and are not writable
29
29
  - Manual-step exception: no matter which agent is used, a human-made decision is always declared as `human`
30
30
 
31
- The six internal commands (`task-event` / `task-lifecycle` / `platform-issue` / `platform-comment` / `platform-pr` / `task-orchestration`) hard-validate `--agent` against a whitelist: they accept a short token, a long name, or `human`, and reject non-standard values with the matching `*_PAYLOAD_INVALID` error; long names are normalized to the short token before being persisted or forwarded. The `ai task log` renderer keeps unknown tokens grouped as `human` but appends a visible `(unknown)` marker.
31
+ The seven internal commands (`task-event` / `task-lifecycle` / `task-finalization` / `platform-issue` / `platform-comment` / `platform-pr` / `task-orchestration`) hard-validate `--agent` against a whitelist: they accept a short token, a long name, or `human`, and reject non-standard values with the matching `*_PAYLOAD_INVALID` error; long names are normalized to the short token before being persisted or forwarded. The `ai task log` renderer keeps unknown tokens grouped as `human` but appends a visible `(unknown)` marker.
32
32
 
33
33
  ## State Snapshot and Completion Verification Entrypoints
34
34
 
@@ -68,7 +68,7 @@ The six internal commands (`task-event` / `task-lifecycle` / `platform-issue` /
68
68
  `- {time} — **{base} [started]** by {agent} — started`
69
69
  - **done line** (written when the step completes, unchanged from today): the action is the base itself:
70
70
  `- {time} — **{base}** by {agent} — {completion summary}`
71
- - **Commit-specific attempt**: the commit skill must not hand-write started. `commit-start` writes a structured `Commit [started]` with `attempt`, `baseline`, and `agent` under the task lock; `commit-terminate` writes the matching `Commit [aborted]` when an attempt is abandoned before side effects. Aborted closes activity only and is never successful commit evidence.
71
+ - **Commit execution boundary**: the commit skill must not hand-write task Activity Log, review anchors, receipts, or checkpoints. Task-bound direct and orchestrated modes both call the single `commit-operation.execute`; taskless direct is allowed only for `TASK_CONTEXT_NOT_FOUND` and creates no task-state side effects.
72
72
  - `{base}` is that skill's existing done action text, including `(Round {N})` (e.g. `Plan Task (Round 1)`). started and done must share the same `{base}` to pair.
73
73
 
74
74
  **Pairing and rendering** (`ai task log`): a started entry pairs with the next same-`{base}` done entry onto one row (repeated executions of the same base pair FIFO by ascending time). The STARTED column shows the start time, DONE the completion time; started with no done = in progress (DONE shows `(in progress)`); done with no started (legacy logs) = a standalone completed row. All three shapes are valid and never error.
@@ -28,7 +28,7 @@
28
28
  - 历史 `gemini` / `gemini-cli` 仅保留 `ai task log` 渲染兼容,不接受写入
29
29
  - 人工步骤例外:无论使用哪个 Agent,只要是人做的决定,统一声明 `human`
30
30
 
31
- 六个内部命令(`task-event` / `task-lifecycle` / `platform-issue` / `platform-comment` / `platform-pr` / `task-orchestration`)对 `--agent` 做白名单硬校验:接受短名、长名或 `human`,非标准值返回对应 `*_PAYLOAD_INVALID` 错误;长名在落库/透传前归一化为短名。渲染端 `ai task log` 对无法识别的 token 保留 `human` 归类并附加 `(unknown)` 标记。
31
+ 七个内部命令(`task-event` / `task-lifecycle` / `task-finalization` / `platform-issue` / `platform-comment` / `platform-pr` / `task-orchestration`)对 `--agent` 做白名单硬校验:接受短名、长名或 `human`,非标准值返回对应 `*_PAYLOAD_INVALID` 错误;长名在落库/透传前归一化为短名。渲染端 `ai task log` 对无法识别的 token 保留 `human` 归类并附加 `(unknown)` 标记。
32
32
 
33
33
  ## 状态快照与完成验证入口
34
34
 
@@ -70,7 +70,7 @@
70
70
  `- {time} — **{基名} [started]** by {agent} — started`
71
71
  - **done 行**(步骤完成时写,与现状一致):action 即基名本身:
72
72
  `- {time} — **{基名}** by {agent} — {完成说明}`
73
- - **Commit 专用 attempt**:commit 技能不得手写 started。`commit-start` 在任务锁内写入带 `attempt`、`baseline`、`agent` 的结构化 `Commit [started]`;无副作用放弃时由 `commit-terminate` 写匹配的 `Commit [aborted]`。aborted 只闭合活动日志,不构成提交成功证据。
73
+ - **Commit 执行边界**:commit 技能不得手写 task Activity Log、review anchor、receipt 或 checkpoint。task-bound direct 与 orchestrated 都调用唯一 `commit-operation.execute`;taskless direct 只在 `TASK_CONTEXT_NOT_FOUND` 时允许,且不产生任务状态副作用。
74
74
  - `{基名}` 指该 SKILL 既有 done 条目的 action 文本,含 `(Round {N})`(如 `Plan Task (Round 1)`)。
75
75
  started 与 done 共用同一 `{基名}` 才能配对。
76
76