@fitlab-ai/agent-infra 0.9.7 → 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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- 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 +161 -288
- 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 +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- 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/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- 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 +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- 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 +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- 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/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- 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
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -11,6 +11,8 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
11
11
|
import { validateLifecycleExecution } from './lifecycle-execution.ts';
|
|
12
12
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
13
13
|
import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
|
|
14
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
15
|
+
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
14
16
|
|
|
15
17
|
type ReviewFinalizationErrorCode =
|
|
16
18
|
| ResolveTaskRefErrorCode
|
|
@@ -62,6 +64,8 @@ type ReviewFinalizationOptions = {
|
|
|
62
64
|
repoRoot?: string;
|
|
63
65
|
randomSuffix?: () => string;
|
|
64
66
|
fileSystem?: Partial<ReviewFileSystem>;
|
|
67
|
+
manualOverride?: ManualOverrideCapability;
|
|
68
|
+
lockAlreadyHeld?: boolean;
|
|
65
69
|
};
|
|
66
70
|
|
|
67
71
|
const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code'; action: string }> = {
|
|
@@ -127,7 +131,7 @@ function finalizeReviewSummaryUnlocked(
|
|
|
127
131
|
}
|
|
128
132
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
129
133
|
if (!resolved.ok) return failed(request, resolved.code, resolved.message, resolved.taskId);
|
|
130
|
-
if (resolved.state !== 'active') {
|
|
134
|
+
if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'review-finalization', 'TASK_STATE_MISMATCH')) {
|
|
131
135
|
return failed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
132
136
|
}
|
|
133
137
|
const parsedArtifact = parseArtifactName(request.artifact);
|
|
@@ -275,7 +279,7 @@ function finalizeReviewSummary(
|
|
|
275
279
|
request: ReviewFinalizationRequest,
|
|
276
280
|
options: ReviewFinalizationOptions = {}
|
|
277
281
|
): ReviewFinalizationResult {
|
|
278
|
-
if (request.dryRun) return finalizeReviewSummaryUnlocked(request, options);
|
|
282
|
+
if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, options);
|
|
279
283
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
280
284
|
if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, options);
|
|
281
285
|
try {
|
package/lib/task/short-id.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
154
|
+
run = readRun(taskDir);
|
|
171
155
|
} catch (error) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (isFailClosedLegacyPause(run)) {
|
|
175
|
-
return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
|
|
156
|
+
const message = error instanceof OrchestrationStateError ? error.message : String(error);
|
|
157
|
+
return failResult('orchestration-state', `Invalid orchestration.json: ${message}`);
|
|
176
158
|
}
|
|
177
|
-
if (!
|
|
178
|
-
return failResult('orchestration-state', `Expected paused or completed run, received '${run
|
|
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 =
|
|
227
|
+
run = readRun(taskDir);
|
|
246
228
|
} catch (error) {
|
|
247
|
-
|
|
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,18 +249,11 @@ 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
|
|
280
|
-
&& recovery.previousSchemaVersion === 2
|
|
253
|
+
const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
281
254
|
&& recovery.previousStatus === 'paused'
|
|
282
255
|
&& recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
283
|
-
&& recovery.client === '
|
|
256
|
+
&& recovery.client === 'claude-code'
|
|
284
257
|
&& recovery.resultingStatus === 'running'
|
|
285
258
|
&& exactText(recovery.recoveredAt)
|
|
286
259
|
&& guards?.stepCount === 0
|
|
@@ -289,9 +262,8 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
|
|
|
289
262
|
&& guards?.receiptCount === 0
|
|
290
263
|
&& guards?.pendingDelegation === false
|
|
291
264
|
&& guards?.commitAuthorizationUnused === true
|
|
292
|
-
&& guards?.completionEvidenceAbsent === true
|
|
293
|
-
|
|
294
|
-
if (!validModelRecovery && !validCapabilityRecovery) {
|
|
265
|
+
&& guards?.completionEvidenceAbsent === true;
|
|
266
|
+
if (!validClaudeCodeRecovery) {
|
|
295
267
|
return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
|
|
296
268
|
}
|
|
297
269
|
}
|
|
@@ -322,40 +294,76 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
|
|
|
322
294
|
}
|
|
323
295
|
const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
|
|
324
296
|
if (activated) {
|
|
325
|
-
|
|
326
|
-
|
|
297
|
+
const isClaudeCode = receipt.client === 'claude-code';
|
|
298
|
+
if (!isClaudeCode) {
|
|
299
|
+
if (!exactText(receipt.actualModel)) {
|
|
300
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
|
|
301
|
+
}
|
|
302
|
+
if (!exactText(receipt.actualReasoningEffort)) {
|
|
303
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
|
|
304
|
+
}
|
|
327
305
|
}
|
|
328
|
-
|
|
329
|
-
|
|
306
|
+
// parentId/childId 校验对所有 client 保持统一(不新增分支);spawnMode 检查按 client 判断
|
|
307
|
+
if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId) {
|
|
308
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid delegation identity`);
|
|
330
309
|
}
|
|
331
|
-
if (!
|
|
310
|
+
if (!isClaudeCode && receipt.spawnMode !== 'fresh') {
|
|
332
311
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
|
|
333
312
|
}
|
|
334
|
-
if (receipt.actualModel
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
|
|
313
|
+
if (receipt.actualModel != null && receipt.actualModel !== receipt.requestedModel) {
|
|
314
|
+
if (!isClaudeCode && !exactText(receipt.modelFallbackReason)) {
|
|
315
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
|
|
316
|
+
}
|
|
339
317
|
}
|
|
340
318
|
if (
|
|
341
|
-
receipt.
|
|
342
|
-
&&
|
|
319
|
+
(receipt.actualModel == null || receipt.actualModel === receipt.requestedModel)
|
|
320
|
+
&& receipt.modelFallbackReason !== null
|
|
343
321
|
) {
|
|
344
|
-
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}'
|
|
322
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
|
|
323
|
+
}
|
|
324
|
+
if (receipt.actualReasoningEffort != null && receipt.actualReasoningEffort !== receipt.requestedReasoningEffort) {
|
|
325
|
+
if (!isClaudeCode && !exactText(receipt.reasoningEffortFallbackReason)) {
|
|
326
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
|
|
327
|
+
}
|
|
345
328
|
}
|
|
346
329
|
if (
|
|
347
|
-
receipt.actualReasoningEffort === receipt.requestedReasoningEffort
|
|
330
|
+
(receipt.actualReasoningEffort == null || receipt.actualReasoningEffort === receipt.requestedReasoningEffort)
|
|
348
331
|
&& receipt.reasoningEffortFallbackReason !== null
|
|
349
332
|
) {
|
|
350
333
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
|
|
351
334
|
}
|
|
352
335
|
if (receipt.client === 'codex') {
|
|
336
|
+
const activatedCodex = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
|
|
353
337
|
const host = receipt.hostEvidence;
|
|
338
|
+
const provenance = receipt.lifecycleProvenance;
|
|
354
339
|
if (
|
|
355
|
-
|
|
356
|
-
||
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
))
|
|
359
367
|
) {
|
|
360
368
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
|
|
361
369
|
}
|
|
@@ -484,6 +492,63 @@ function checkTaskMeta({ taskDir, config }: any): any {
|
|
|
484
492
|
);
|
|
485
493
|
}
|
|
486
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
|
+
|
|
487
552
|
function validateTaskBranch(metadata: any): any {
|
|
488
553
|
if (isBlank(metadata.branch)) {
|
|
489
554
|
return null;
|
|
@@ -902,7 +967,7 @@ function checkReviewLedger({ taskDir, config }: any): any {
|
|
|
902
967
|
}
|
|
903
968
|
|
|
904
969
|
const stageScope = Array.isArray(config.stage_scope) ? config.stage_scope : null;
|
|
905
|
-
const maxRounds = Number(resolveReviewSetting(config, "
|
|
970
|
+
const maxRounds = Number(resolveReviewSetting(config, "maxHandshakeRounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
|
|
906
971
|
const problems = [];
|
|
907
972
|
let inScopeCount = 0;
|
|
908
973
|
|
|
@@ -1574,7 +1639,14 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
|
|
|
1574
1639
|
const checks = [];
|
|
1575
1640
|
for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
|
|
1576
1641
|
if (checkConfig === null) continue;
|
|
1577
|
-
const result = runCheck(type, {
|
|
1642
|
+
const result = runCheck(type, {
|
|
1643
|
+
mode,
|
|
1644
|
+
skillName,
|
|
1645
|
+
taskDir: path.resolve(taskDir),
|
|
1646
|
+
artifactFile,
|
|
1647
|
+
repositoryRoot: repoRoot,
|
|
1648
|
+
config: checkConfig
|
|
1649
|
+
}, shared);
|
|
1578
1650
|
checks.push(result);
|
|
1579
1651
|
if (result.status === "blocked") break;
|
|
1580
1652
|
}
|
|
@@ -1585,7 +1657,16 @@ function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks: reque
|
|
|
1585
1657
|
const config = (verifyConfig.checks || {})[type];
|
|
1586
1658
|
if (config === undefined) return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
|
|
1587
1659
|
if (config === null) return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
|
|
1588
|
-
return {
|
|
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
|
+
};
|
|
1589
1670
|
}
|
|
1590
1671
|
|
|
1591
1672
|
export { verifyInProcess };
|
package/lib/task/verification.ts
CHANGED
|
@@ -7,13 +7,15 @@ import { parseArtifactName } from './artifact-lifecycle.ts';
|
|
|
7
7
|
import type { ArtifactFamily } from './artifact-lifecycle.ts';
|
|
8
8
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
9
9
|
import type { TaskWorkspaceState } from './resolve-ref.ts';
|
|
10
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
11
|
+
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
10
12
|
|
|
11
13
|
type VerificationEvent =
|
|
12
14
|
| 'analyze.awaiting-input' | 'analyze.completed'
|
|
13
15
|
| 'review-analysis.completed' | 'plan.completed' | 'review-plan.completed'
|
|
14
16
|
| 'code.completed' | 'review-code.completed' | 'manual-validation.completed' | 'validation-run.completed'
|
|
15
17
|
| 'block-task.completed' | 'cancel-task.completed' | 'commit.completed'
|
|
16
|
-
| 'complete-task.preflight' | 'complete-task.completed'
|
|
18
|
+
| 'complete-task.preflight' | 'complete-task.hard-preflight' | 'complete-task.completed'
|
|
17
19
|
| 'create-pr.completed' | 'create-task.completed'
|
|
18
20
|
| 'import-codescan.completed' | 'import-dependabot.completed'
|
|
19
21
|
| 'import-issue.completed' | 'watch-pr.completed'
|
|
@@ -48,6 +50,7 @@ type TaskVerificationResult = {
|
|
|
48
50
|
type VerificationOptions = {
|
|
49
51
|
repoRoot?: string;
|
|
50
52
|
engine?: typeof verifyInProcess;
|
|
53
|
+
manualOverride?: ManualOverrideCapability;
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
const gate = (skill: string, expectedState: VerificationSpec['expectedState'], artifactFamily?: ArtifactFamily): VerificationSpec => ({
|
|
@@ -66,7 +69,8 @@ const VERIFICATION_CATALOG: Readonly<Record<VerificationEvent, VerificationSpec>
|
|
|
66
69
|
'block-task.completed': gate('block-task', 'blocked'),
|
|
67
70
|
'cancel-task.completed': gate('cancel-task', 'completed'),
|
|
68
71
|
'commit.completed': gate('commit', 'active'),
|
|
69
|
-
'complete-task.preflight': { skill: 'complete-task', expectedState: 'active', mode: 'checks', checks: ['
|
|
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'] },
|
|
70
74
|
'complete-task.completed': gate('complete-task', 'completed'),
|
|
71
75
|
'create-pr.completed': gate('create-pr', 'active'),
|
|
72
76
|
'create-task.completed': gate('create-task', 'active'),
|
|
@@ -93,7 +97,7 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
|
|
|
93
97
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
94
98
|
if (!resolved.ok) return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
|
|
95
99
|
const identity = { taskId: resolved.taskId, taskDir: resolved.taskDir, taskState: resolved.state, skill: spec.skill, mode: spec.mode } as const;
|
|
96
|
-
if (resolved.state !== spec.expectedState) {
|
|
100
|
+
if (resolved.state !== spec.expectedState && !allowsManualOverride(options.manualOverride, 'task-verify', 'VERIFY_TASK_STATE_MISMATCH')) {
|
|
97
101
|
return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
|
|
98
102
|
}
|
|
99
103
|
if (spec.artifactFamily) {
|
|
@@ -111,11 +115,22 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
|
|
|
111
115
|
if (spec.mode === 'gate') {
|
|
112
116
|
const payload = engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
|
|
113
117
|
const status = payload.gate as 'pass' | 'fail' | 'blocked';
|
|
118
|
+
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
119
|
+
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
|
|
120
|
+
return {
|
|
121
|
+
status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
|
|
122
|
+
...identity, artifact: request.artifact ?? null, invocations, error: null
|
|
123
|
+
};
|
|
124
|
+
}
|
|
114
125
|
invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
|
|
115
126
|
} else {
|
|
116
127
|
for (const check of spec.checks ?? []) {
|
|
117
128
|
const payload = engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
|
|
118
129
|
const status = payload.status as 'pass' | 'fail' | 'blocked';
|
|
130
|
+
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
131
|
+
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
119
134
|
invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
|
|
120
135
|
if (status !== 'pass') break;
|
|
121
136
|
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
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
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
15
|
+
import { retryHintForWarning, type OperationWarning } from './operation-outcome.ts';
|
|
14
16
|
|
|
15
17
|
type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
|
|
16
18
|
type WarningStatus = 'open' | 'resolved' | 'ignored';
|
|
@@ -27,6 +29,12 @@ type WorkflowWarningIntentResult = {
|
|
|
27
29
|
error: { code: string; message: string } | null;
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
type FinalizationWarningProjectionResult = Readonly<{
|
|
33
|
+
status: 'applied' | 'no-op' | 'failed';
|
|
34
|
+
changed: boolean;
|
|
35
|
+
error: { code: string; message: string } | null;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
30
38
|
function failed(intent: WorkflowWarningIntent, code: string, message: string, taskId: string | null = null, entityId: string | null = null): WorkflowWarningIntentResult {
|
|
31
39
|
return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, warnings: [], operations: [], error: { code, message } };
|
|
32
40
|
}
|
|
@@ -79,7 +87,8 @@ function rowMutation(row: WorkflowWarning): TaskMutation {
|
|
|
79
87
|
function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: TaskWriteOptions = {}): WorkflowWarningIntentResult {
|
|
80
88
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
81
89
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
82
|
-
|
|
90
|
+
const stateOverride = allowsManualOverride(options.manualOverride, 'workflow-warning', 'TASK_STATE_MISMATCH');
|
|
91
|
+
if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
83
92
|
let content: string;
|
|
84
93
|
let rows: WorkflowWarning[];
|
|
85
94
|
try {
|
|
@@ -125,7 +134,7 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
|
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
const writeResult = writeTask({
|
|
128
|
-
taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
|
|
137
|
+
taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
|
|
129
138
|
}, {
|
|
130
139
|
...options,
|
|
131
140
|
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
|
|
@@ -138,5 +147,53 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
|
|
|
138
147
|
};
|
|
139
148
|
}
|
|
140
149
|
|
|
141
|
-
|
|
142
|
-
|
|
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/lib/task/write.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
ResolveTaskRefErrorCode,
|
|
13
13
|
TaskWorkspaceState
|
|
14
14
|
} from './resolve-ref.ts';
|
|
15
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
16
|
+
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
15
17
|
import { mutateTableRow, upsertSection } from './sections.ts';
|
|
16
18
|
import type {
|
|
17
19
|
TableRowDeleteMutation,
|
|
@@ -156,6 +158,7 @@ type TaskWriteOptions = {
|
|
|
156
158
|
metadataProvider?: () => TaskWriteMetadata;
|
|
157
159
|
randomSuffix?: () => string;
|
|
158
160
|
fileSystem?: Partial<TaskFileSystem>;
|
|
161
|
+
manualOverride?: ManualOverrideCapability;
|
|
159
162
|
};
|
|
160
163
|
|
|
161
164
|
const DEFAULT_FILE_SYSTEM: TaskFileSystem = {
|
|
@@ -241,7 +244,7 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
|
|
|
241
244
|
taskMdPath: resolved.taskMdPath,
|
|
242
245
|
actualState: resolved.state
|
|
243
246
|
};
|
|
244
|
-
if (resolved.state !== request.expectedState) {
|
|
247
|
+
if (resolved.state !== request.expectedState && !allowsManualOverride(options.manualOverride, 'task.write', 'TASK_STATE_MISMATCH')) {
|
|
245
248
|
return failure(
|
|
246
249
|
request,
|
|
247
250
|
identity,
|