@fitlab-ai/agent-infra 0.9.7 → 0.9.8

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 (96) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  7. package/dist/lib/decide.js +68 -14
  8. package/dist/lib/init.js +1 -1
  9. package/dist/lib/internal/release-workflow.js +1 -3
  10. package/dist/lib/internal/task-activity.js +57 -7
  11. package/dist/lib/internal/task-event.js +57 -4
  12. package/dist/lib/internal/task-ledger.js +59 -5
  13. package/dist/lib/internal/task-lifecycle.js +59 -8
  14. package/dist/lib/internal/task-orchestration.js +11 -3
  15. package/dist/lib/internal/task-override.js +131 -0
  16. package/dist/lib/internal/task-review.js +64 -6
  17. package/dist/lib/internal/task-verify.js +65 -3
  18. package/dist/lib/internal/task-warning.js +57 -4
  19. package/dist/lib/platform/pr-summary.js +5 -3
  20. package/dist/lib/runtime/agent-runtime.js +56 -0
  21. package/dist/lib/sandbox/commands/create.js +389 -311
  22. package/dist/lib/sandbox/control/container-identity.js +1 -1
  23. package/dist/lib/sandbox/control/executor.js +10 -2
  24. package/dist/lib/sandbox/control/lifecycle.js +392 -72
  25. package/dist/lib/sandbox/control/state.js +27 -11
  26. package/dist/lib/sandbox/engine.js +14 -7
  27. package/dist/lib/sandbox/mounts.js +20 -5
  28. package/dist/lib/sandbox/recovery.js +78 -10
  29. package/dist/lib/sandbox/workspace-view.js +62 -40
  30. package/dist/lib/server/process-state.js +51 -1
  31. package/dist/lib/task/activity-intent.js +4 -1
  32. package/dist/lib/task/decision-intents.js +4 -2
  33. package/dist/lib/task/delegation-receipts.js +37 -16
  34. package/dist/lib/task/events.js +9 -5
  35. package/dist/lib/task/guard-override.js +31 -0
  36. package/dist/lib/task/human-override.js +1240 -0
  37. package/dist/lib/task/ledger-intents.js +6 -3
  38. package/dist/lib/task/lifecycle.js +69 -14
  39. package/dist/lib/task/orchestration.js +60 -6
  40. package/dist/lib/task/review-finalization.js +3 -2
  41. package/dist/lib/task/verification-engine.js +40 -14
  42. package/dist/lib/task/verification.js +13 -1
  43. package/dist/lib/task/workflow-warning-intents.js +4 -2
  44. package/dist/lib/task/write.js +2 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  47. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
  49. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  51. package/lib/decide.ts +59 -14
  52. package/lib/init.ts +1 -1
  53. package/lib/internal/release-workflow.ts +1 -3
  54. package/lib/internal/task-activity.ts +46 -6
  55. package/lib/internal/task-event.ts +53 -4
  56. package/lib/internal/task-ledger.ts +48 -4
  57. package/lib/internal/task-lifecycle.ts +56 -12
  58. package/lib/internal/task-orchestration.ts +12 -4
  59. package/lib/internal/task-override.ts +138 -0
  60. package/lib/internal/task-review.ts +51 -6
  61. package/lib/internal/task-verify.ts +52 -3
  62. package/lib/internal/task-warning.ts +44 -3
  63. package/lib/platform/pr-summary.ts +10 -3
  64. package/lib/runtime/agent-runtime.ts +76 -0
  65. package/lib/sandbox/commands/create.ts +88 -4
  66. package/lib/sandbox/config.ts +2 -1
  67. package/lib/sandbox/control/container-identity.ts +3 -3
  68. package/lib/sandbox/control/executor.ts +10 -2
  69. package/lib/sandbox/control/lifecycle.ts +463 -76
  70. package/lib/sandbox/control/protocol.ts +5 -2
  71. package/lib/sandbox/control/state.ts +35 -18
  72. package/lib/sandbox/engine.ts +20 -9
  73. package/lib/sandbox/mounts.ts +23 -5
  74. package/lib/sandbox/recovery.ts +90 -9
  75. package/lib/sandbox/workspace-view.ts +70 -40
  76. package/lib/server/process-state.ts +48 -1
  77. package/lib/task/activity-intent.ts +4 -2
  78. package/lib/task/decision-intents.ts +4 -2
  79. package/lib/task/delegation-receipts.ts +46 -18
  80. package/lib/task/events.ts +10 -5
  81. package/lib/task/guard-override.ts +50 -0
  82. package/lib/task/human-override.ts +1503 -0
  83. package/lib/task/ledger-intents.ts +6 -3
  84. package/lib/task/lifecycle.ts +71 -14
  85. package/lib/task/orchestration.ts +89 -7
  86. package/lib/task/review-finalization.ts +6 -2
  87. package/lib/task/verification-engine.ts +42 -16
  88. package/lib/task/verification.ts +15 -1
  89. package/lib/task/workflow-warning-intents.ts +4 -2
  90. package/lib/task/write.ts +4 -1
  91. package/package.json +1 -1
  92. package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
  95. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  96. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
@@ -9,6 +9,9 @@ export type ProcessIdentity = Readonly<{
9
9
  startTime: ProcessStartTime;
10
10
  }>;
11
11
 
12
+ export type ProcessIdentityState = 'alive' | 'dead' | 'unknown';
13
+ export type ProcessIdentityProbe = (identity: ProcessIdentity) => ProcessIdentityState;
14
+
12
15
  export type PidRecord = ProcessIdentity & {
13
16
  version: 2;
14
17
  };
@@ -127,6 +130,50 @@ export function getProcessStartTime(
127
130
  }
128
131
  }
129
132
 
133
+ export function getProcessIdentityState(
134
+ identity: ProcessIdentity,
135
+ platform: NodeJS.Platform = process.platform
136
+ ): ProcessIdentityState {
137
+ if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
138
+ return 'dead';
139
+ }
140
+ if (platform === 'linux') {
141
+ try {
142
+ const stat = parseLinuxProcessStat(fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8'));
143
+ if (stat === null) return 'unknown';
144
+ if (stat.state === 'Z') return 'dead';
145
+ return stat.startTime === identity.startTime ? 'alive' : 'dead';
146
+ } catch (error) {
147
+ return (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'dead' : 'unknown';
148
+ }
149
+ }
150
+ try {
151
+ process.kill(identity.pid, 0);
152
+ } catch (error) {
153
+ const code = (error as NodeJS.ErrnoException).code;
154
+ if (code === 'ESRCH') return 'dead';
155
+ if (code !== 'EPERM') return 'unknown';
156
+ }
157
+ const query = buildProcessStartTimeQuery(identity.pid, platform);
158
+ if (query === null) return 'unknown';
159
+ try {
160
+ const output = execFileSync(query.command, query.args, {
161
+ encoding: 'utf8',
162
+ env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
163
+ }).trim();
164
+ if (output.length === 0) return 'unknown';
165
+ const startTime = platform === 'win32'
166
+ ? new Date(output).getTime()
167
+ : platform === 'darwin'
168
+ ? parseDarwinStartTime(output)
169
+ : null;
170
+ if (startTime === null || Number.isNaN(startTime)) return 'unknown';
171
+ return startTime === identity.startTime ? 'alive' : 'dead';
172
+ } catch {
173
+ return 'unknown';
174
+ }
175
+ }
176
+
130
177
  function parsePidRecord(
131
178
  raw: string
132
179
  ):
@@ -234,5 +281,5 @@ export function removePidRecordIfMatches(pidFile: string, expected: PidRecord):
234
281
  }
235
282
 
236
283
  export function processIdentityMatches(identity: ProcessIdentity): boolean {
237
- return getProcessStartTime(identity.pid) === identity.startTime;
284
+ return getProcessIdentityState(identity) === 'alive';
238
285
  }
@@ -14,6 +14,7 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
14
14
  import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
15
15
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
16
16
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
17
+ import { allowsManualOverride } from './guard-override.ts';
17
18
 
18
19
  type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
19
20
  type PrReviewOutcome = 'aborted' | 'superseded';
@@ -95,7 +96,7 @@ export type PrReviewInspectionResult = {
95
96
  error: { code: string; message: string } | null;
96
97
  };
97
98
 
98
- type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions };
99
+ type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions; lockAlreadyHeld?: boolean };
99
100
  type ArtifactRecord = { identity: ArtifactIdentity; head: string; status: PrReviewArtifactStatus };
100
101
  type RoundState = ArtifactRecord & {
101
102
  startedAgent: string | null;
@@ -397,9 +398,10 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
397
398
  if ('code' in validated) return failed(intent, validated.code, validated.message);
398
399
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
399
400
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
400
- if (resolved.state !== 'active') {
401
+ if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
401
402
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
402
403
  }
404
+ if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
403
405
  try {
404
406
  return withTaskExecutionLock(
405
407
  resolved.repoRoot,
@@ -16,6 +16,7 @@ import { resolveTaskContext } from './resolve-ref.ts';
16
16
  import { extractSection, extractSubSection, findSectionHeading } from './sections.ts';
17
17
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
18
18
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
19
+ import { allowsManualOverride } from './guard-override.ts';
19
20
 
20
21
  type HumanDecisionRequest = {
21
22
  taskRef?: string;
@@ -85,7 +86,8 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
85
86
  if (!request.selector || !request.decision.trim()) return failed('DECISION_PAYLOAD_INVALID', 'selector and decision are required');
86
87
  const resolved = resolveTaskContext(request.taskRef, { repoRoot: options.repoRoot });
87
88
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
88
- if (resolved.state !== 'active') return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
89
+ const stateOverride = allowsManualOverride(options.manualOverride, 'decision-intent', 'TASK_STATE_MISMATCH');
90
+ if (resolved.state !== 'active' && !stateOverride) return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
89
91
  let content: string;
90
92
  let rows: LedgerRow[];
91
93
  try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); rows = parseLedger(content); }
@@ -170,7 +172,7 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
170
172
  kind: 'section', aliases: ACTIVITY_ALIASES, heading: activity.heading,
171
173
  body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Human Decision', agent: 'human', note: `${row.id} decided → ${recordId}` })
172
174
  });
173
- const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: 'active', mutations, dryRun: request.dryRun }, {
175
+ const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: stateOverride ? resolved.state : 'active', mutations, dryRun: request.dryRun }, {
174
176
  ...options,
175
177
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
176
178
  metadataProvider: () => metadata
@@ -100,6 +100,11 @@ function fail(code: string, message: string): ReceiptFailure {
100
100
  return { ok: false, code, message };
101
101
  }
102
102
 
103
+ // 空白串折叠为 null,保持"缺失即 null"的单一表示(Start/Stop 两处共用,避免任一入口把 " " 当成已观察到的值入账)
104
+ function foldBlankToNull(value: string | undefined): string | null {
105
+ return value?.trim() ? value : null;
106
+ }
107
+
103
108
  function prepareDelegation(
104
109
  input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'hostEvidence' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'preparedMonotonicMs' | 'spawnDispatchMonotonicMs' | 'activationDeadlineMonotonicMs' | 'spawnDispatchedAt' | 'activationDeadlineAt' | 'startEvidenceMonotonicMs' | 'activatedMonotonicMs' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
105
110
  options: { id?: () => string; now?: () => string; monotonicNow?: () => number } = {}
@@ -162,7 +167,7 @@ function activateDelegation(
162
167
  nativeAgent: string;
163
168
  childId: string;
164
169
  parentId: string;
165
- spawnMode: string;
170
+ spawnMode?: string;
166
171
  actualModel?: string;
167
172
  actualReasoningEffort?: string;
168
173
  modelFallbackReason?: string;
@@ -211,32 +216,46 @@ function activateDelegation(
211
216
  if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
212
217
  return fail('DELEGATION_IDENTITY_INVALID', 'native parent/child identity does not match the prepared delegation');
213
218
  }
214
- if (event.spawnMode !== 'fresh') return fail('DELEGATION_FORK_FORBIDDEN', `spawn mode '${event.spawnMode}' is not fresh`);
215
- const actualModel = event.actualModel;
216
- if (!actualModel || actualModel.trim() !== actualModel) {
217
- return fail('DELEGATION_MODEL_IDENTITY_MISSING', 'native start event must provide a non-empty actual model identity');
219
+ // spawnMode 的必需性按 client 判断:claude-code 宿主结构上不提供该字段,跳过;其余 client 维持硬校验
220
+ if (receipt.client !== 'claude-code' && event.spawnMode !== 'fresh') {
221
+ return fail('DELEGATION_FORK_FORBIDDEN', `spawn mode '${event.spawnMode}' is not fresh`);
222
+ }
223
+ // model/effort 维度:claude-code 如实记录、不做门禁;其余 client 维持现状硬校验(HDR-2)
224
+ const actualModel = foldBlankToNull(event.actualModel);
225
+ const actualReasoningEffort = foldBlankToNull(event.actualReasoningEffort);
226
+ if (receipt.client !== 'claude-code') {
227
+ if (!actualModel || actualModel.trim() !== actualModel) {
228
+ return fail('DELEGATION_MODEL_IDENTITY_MISSING', 'native start event must provide a non-empty actual model identity');
229
+ }
218
230
  }
219
- if (actualModel !== receipt.requestedModel && (!event.modelFallbackReason || event.modelFallbackReason.trim() === '')) {
220
- return fail('DELEGATION_MODEL_FALLBACK_UNRECORDED', 'actual model differs from requested model without a fallback reason');
231
+ if (actualModel !== null && actualModel !== receipt.requestedModel) {
232
+ if (receipt.client !== 'claude-code' && (!event.modelFallbackReason || event.modelFallbackReason.trim() === '')) {
233
+ return fail('DELEGATION_MODEL_FALLBACK_UNRECORDED', 'actual model differs from requested model without a fallback reason');
234
+ }
221
235
  }
222
- const actualReasoningEffort = event.actualReasoningEffort;
223
- if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
224
- return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
236
+ if (receipt.client !== 'claude-code') {
237
+ if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
238
+ return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
239
+ }
225
240
  }
226
241
  if (
227
242
  receipt.client === 'codex'
228
243
  && event.hostEvidence?.kind !== 'codex-lifecycle-v2'
229
244
  ) return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
230
- if (
231
- actualReasoningEffort !== receipt.requestedReasoningEffort
232
- && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')
233
- ) {
234
- return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
245
+ if (actualReasoningEffort !== null && actualReasoningEffort !== receipt.requestedReasoningEffort) {
246
+ if (receipt.client !== 'claude-code' && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')) {
247
+ return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
248
+ }
235
249
  }
236
- if (actualModel === receipt.requestedModel && event.modelFallbackReason) {
250
+ // "无关 fallback 理由"检查对所有 client 保持不变——防止编造理由的红线,不属于 HDR-2 放宽范围
251
+ // actualModel/actualReasoningEffort 为 null(未观察到)时同样没有"实际不同"这回事,理由一样是无关的
252
+ if ((actualModel === null || actualModel === receipt.requestedModel) && event.modelFallbackReason) {
237
253
  return fail('DELEGATION_MODEL_FALLBACK_INVALID', 'model fallback reason is only valid when actual model differs');
238
254
  }
239
- if (actualReasoningEffort === receipt.requestedReasoningEffort && event.reasoningEffortFallbackReason) {
255
+ if (
256
+ (actualReasoningEffort === null || actualReasoningEffort === receipt.requestedReasoningEffort)
257
+ && event.reasoningEffortFallbackReason
258
+ ) {
240
259
  return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
241
260
  }
242
261
  if (
@@ -280,7 +299,7 @@ function activateDelegation(
280
299
  status: 'activated',
281
300
  parentId: event.parentId,
282
301
  childId: event.childId,
283
- spawnMode: event.spawnMode,
302
+ spawnMode: event.spawnMode ?? null,
284
303
  actualModel,
285
304
  actualReasoningEffort,
286
305
  modelFallbackReason: event.modelFallbackReason ?? null,
@@ -328,6 +347,10 @@ function sealDelegation(
328
347
  exitCode: number;
329
348
  afterFingerprint: string;
330
349
  changedPaths: readonly string[];
350
+ actualModel?: string;
351
+ actualReasoningEffort?: string;
352
+ modelFallbackReason?: string;
353
+ reasoningEffortFallbackReason?: string;
331
354
  hostEvidence?: Readonly<{ stopRevision: number; consumer: string; consumedAt: string }>;
332
355
  }>,
333
356
  options: { now?: () => string; requireHostEvidence?: boolean } = {}
@@ -361,6 +384,10 @@ function sealDelegation(
361
384
  status: 'sealed',
362
385
  afterFingerprint: event.afterFingerprint,
363
386
  changedPaths: Object.freeze([...event.changedPaths]),
387
+ actualModel: receipt.actualModel ?? foldBlankToNull(event.actualModel),
388
+ actualReasoningEffort: receipt.actualReasoningEffort ?? foldBlankToNull(event.actualReasoningEffort),
389
+ modelFallbackReason: receipt.modelFallbackReason ?? event.modelFallbackReason ?? null,
390
+ reasoningEffortFallbackReason: receipt.reasoningEffortFallbackReason ?? event.reasoningEffortFallbackReason ?? null,
364
391
  hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
365
392
  ...receipt.hostEvidence,
366
393
  ...event.hostEvidence
@@ -385,6 +412,7 @@ export {
385
412
  completeDelegationStage,
386
413
  consumeDelegation,
387
414
  dispatchDelegation,
415
+ foldBlankToNull,
388
416
  managedDelegationRole,
389
417
  prepareDelegation,
390
418
  sealDelegation
@@ -32,6 +32,7 @@ import type { OrchestrationStageCompletion } from './orchestration.ts';
32
32
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
33
33
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
34
34
  import type { TaskOperationSummary, TaskWriteErrorCode, TaskWriteOptions } from './write.ts';
35
+ import { allowsManualOverride } from './guard-override.ts';
35
36
 
36
37
  const eventCatalog = [
37
38
  'analyze.started', 'analyze.awaiting-input', 'analyze.completed',
@@ -53,6 +54,7 @@ type TaskEventErrorCode =
53
54
  | ArtifactErrorCode | TaskWriteErrorCode;
54
55
  type TaskEventRequest = {
55
56
  taskRef: string; event: TaskEventName | string; agent: string; dryRun?: boolean; orchestrated?: boolean;
57
+ overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
56
58
  round?: number; question?: number; artifact?: string; fixFor?: string; implementationInput?: string;
57
59
  verdict?: Verdict; blockers?: number; major?: number; minor?: number;
58
60
  manualValidation?: number; filesModified?: number; testsPassed?: number;
@@ -61,6 +63,7 @@ type TaskEventRequest = {
61
63
  type TaskEventError = { code: TaskEventErrorCode; message: string };
62
64
  type TaskEventOptions = TaskWriteOptions & {
63
65
  commitOrchestrationCompletion?: (plan: OrchestrationStageCompletion) => void;
66
+ lockAlreadyHeld?: boolean;
64
67
  };
65
68
  type TaskEventResult = {
66
69
  status: 'planned' | 'applied' | 'no-op' | 'failed'; changed: boolean;
@@ -73,7 +76,7 @@ type TaskEventResult = {
73
76
  operations: readonly TaskOperationSummary[]; error: TaskEventError | null;
74
77
  };
75
78
 
76
- const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun']);
79
+ const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']);
77
80
  const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
78
81
  'analyze.started': { optional: ['round'] },
79
82
  'analyze.awaiting-input': { required: ['question'] },
@@ -357,7 +360,8 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
357
360
  if (invalid) return failed(request, invalid);
358
361
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
359
362
  if (!resolved.ok) return failed(request, { code: resolved.code, message: resolved.message }, { taskId: resolved.taskId });
360
- if (resolved.state !== 'active') return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
363
+ const stateOverride = allowsManualOverride(options.manualOverride, 'task-event', 'TASK_STATE_MISMATCH');
364
+ if (resolved.state !== 'active' && !stateOverride) return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
361
365
  let content: string;
362
366
  try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
363
367
  catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
@@ -406,7 +410,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
406
410
  const done = completedRows.find((item) => item.note === eventIdentity.note);
407
411
  if (eventIdentity.phase === 'completed' && done) return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
408
412
  if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity is already completed with a different payload' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
409
- if (eventIdentity.phase === 'completed' && !row?.started) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires one open matching started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
413
+ if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING')) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires one open matching started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
410
414
  if (eventIdentity.phase === 'completed') {
411
415
  const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
412
416
  if (!validated.ok) return failed(normalized, validated.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
@@ -417,7 +421,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
417
421
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, existing.time, frontmatter, artifactContext);
418
422
  }
419
423
  const allowed = FAMILY[eventIdentity.family][eventIdentity.phase === 'started' ? 'started' : 'completed'];
420
- if (!(allowed as readonly string[]).includes(currentStep)) return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${normalized.event} is not allowed from '${currentStep}'` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
424
+ if (!(allowed as readonly string[]).includes(currentStep) && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID')) return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${normalized.event} is not allowed from '${currentStep}'` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
421
425
  const findingCountError = validateReviewFindingCounts(
422
426
  normalized,
423
427
  content,
@@ -522,7 +526,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
522
526
  });
523
527
  }
524
528
  mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
525
- const result = writeTask({ taskRef: normalized.taskRef, expectedState: 'active', dryRun: normalized.dryRun, mutations }, { ...options, metadataProvider: () => metadata });
529
+ const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, metadataProvider: () => metadata });
526
530
  if (result.status === 'failed') return failed(normalized, result.error, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
527
531
  if (!normalized.dryRun && orchestrationCompletion) {
528
532
  try {
@@ -560,6 +564,7 @@ function applyTaskEvent(request: TaskEventRequest, options: TaskEventOptions = {
560
564
  const invalid = validateTaskEventRequest(request);
561
565
  if (invalid) return failed(request, invalid);
562
566
  const parts = eventParts(request.event);
567
+ if (options.lockAlreadyHeld) return applyTaskEventUnlocked(request, options);
563
568
  if (request.dryRun || parts.phase !== 'completed' || parts.family === 'manual-validation') {
564
569
  return applyTaskEventUnlocked(request, options);
565
570
  }
@@ -0,0 +1,50 @@
1
+ type ManualOverrideCapability = Readonly<{
2
+ failureId: string;
3
+ operator: string;
4
+ reason: string;
5
+ }>;
6
+
7
+ type GuardProducer = Readonly<{
8
+ producerId: string;
9
+ guardId: `G-${string}`;
10
+ code: string;
11
+ }>;
12
+
13
+ const guardProducerCatalog: readonly GuardProducer[] = [
14
+ { producerId: 'task.resolve', guardId: 'G-01', code: 'TASK_NOT_FOUND' },
15
+ { producerId: 'task.resolve', guardId: 'G-01', code: 'TASK_IDENTITY_CONFLICT' },
16
+ { producerId: 'task.write', guardId: 'G-02', code: 'TASK_STATE_MISMATCH' },
17
+ { producerId: 'task-event', guardId: 'G-04', code: 'TASK_STATE_MISMATCH' },
18
+ { producerId: 'task-event', guardId: 'G-04', code: 'EVENT_TRANSITION_INVALID' },
19
+ { producerId: 'task-event', guardId: 'G-04', code: 'EVENT_START_MISSING' },
20
+ { producerId: 'task-verify', guardId: 'G-05', code: 'VERIFY_TASK_STATE_MISMATCH' },
21
+ { producerId: 'task-verify', guardId: 'G-05', code: 'VERIFY_ARTIFACT_INVALID' },
22
+ { producerId: 'verification-engine', guardId: 'G-06', code: 'CHECK_FAILED' },
23
+ { producerId: 'verification-engine', guardId: 'G-06', code: 'CHECK_BLOCKED' },
24
+ { producerId: 'activity-intent', guardId: 'G-07', code: 'TASK_STATE_MISMATCH' },
25
+ { producerId: 'ledger-intent', guardId: 'G-07', code: 'TASK_STATE_MISMATCH' },
26
+ { producerId: 'decision-intent', guardId: 'G-07', code: 'TASK_STATE_MISMATCH' },
27
+ { producerId: 'workflow-warning', guardId: 'G-07', code: 'TASK_STATE_MISMATCH' },
28
+ { producerId: 'review-finalization', guardId: 'G-07', code: 'TASK_STATE_MISMATCH' },
29
+ { producerId: 'ledger-intent', guardId: 'G-08', code: 'LEDGER_TRANSITION_INVALID' },
30
+ { producerId: 'lifecycle-execution', guardId: 'G-09', code: 'ORCHESTRATION_STANDALONE_BUSY' },
31
+ { producerId: 'lifecycle-execution', guardId: 'G-09', code: 'ORCHESTRATION_PROVENANCE_MISMATCH' },
32
+ { producerId: 'lifecycle-execution', guardId: 'G-09', code: 'ORCHESTRATION_STATE_INVALID' },
33
+ { producerId: 'platform.issue', guardId: 'G-10', code: 'PLATFORM_BIND_FAILED' },
34
+ { producerId: 'platform.pull-request', guardId: 'G-10', code: 'PLATFORM_BIND_FAILED' }
35
+ ];
36
+
37
+ function guardFailureId(producerId: string, code: string): string {
38
+ return `${producerId}:${code}`;
39
+ }
40
+
41
+ function allowsManualOverride(
42
+ capability: ManualOverrideCapability | undefined,
43
+ producerId: string,
44
+ code: string
45
+ ): boolean {
46
+ return capability?.failureId === guardFailureId(producerId, code);
47
+ }
48
+
49
+ export { allowsManualOverride, guardFailureId, guardProducerCatalog };
50
+ export type { GuardProducer, ManualOverrideCapability };