@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.
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/release-workflow.js +1 -3
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +11 -3
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +5 -3
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/executor.js +10 -2
- package/dist/lib/sandbox/control/lifecycle.js +392 -72
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +37 -16
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +60 -6
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/verification-engine.js +40 -14
- package/dist/lib/task/verification.js +13 -1
- package/dist/lib/task/workflow-warning-intents.js +4 -2
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/init.ts +1 -1
- package/lib/internal/release-workflow.ts +1 -3
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +12 -4
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +10 -3
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/executor.ts +10 -2
- package/lib/sandbox/control/lifecycle.ts +463 -76
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/workspace-view.ts +70 -40
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +46 -18
- package/lib/task/events.ts +10 -5
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +89 -7
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/verification-engine.ts +42 -16
- package/lib/task/verification.ts +15 -1
- package/lib/task/workflow-warning-intents.ts +4 -2
- package/lib/task/write.ts +4 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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 !==
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
package/lib/task/events.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|