@fitlab-ai/agent-infra 0.9.14 → 0.9.15
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 +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/internal-cli.js +93 -30
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +37 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/cli-route-inventory.js +3 -1
- package/dist/lib/internal/codex-lifecycle.js +6 -6
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +322 -0
- package/dist/lib/internal/platform-pr.js +18 -4
- package/dist/lib/internal/release-workflow.js +63 -84
- package/dist/lib/internal/sandbox-control.js +149 -2
- package/dist/lib/internal/task-artifact.js +15 -262
- package/dist/lib/internal/task-create.js +6 -5
- package/dist/lib/internal/task-event.js +37 -37
- package/dist/lib/internal/task-operation-registry.js +101 -3
- package/dist/lib/internal/task-review.js +8 -53
- package/dist/lib/internal/task-validate.js +30 -10
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +4 -1
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +15 -20
- package/dist/lib/platform/issue-comments.js +2 -2
- package/dist/lib/platform/issues.js +1 -179
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +55 -3
- package/dist/lib/platform/pull-requests.js +2 -105
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/verification-sync.js +127 -142
- package/dist/lib/release/workflow.js +1 -1
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/rm.js +3 -1514
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +93 -54
- package/dist/lib/sandbox/control/executor.js +129 -65
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +36 -2
- package/dist/lib/sandbox/control/native-file-lock.js +6 -98
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +491 -120
- package/dist/lib/sandbox/control/state.js +58 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +99 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/recovery.js +28 -4
- package/dist/lib/sandbox/removal.js +1717 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-view.js +13 -14
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +101 -0
- package/dist/lib/task/artifact-lifecycle.js +30 -28
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +23 -69
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-repair-intent.js +87 -16
- package/dist/lib/task/artifact-schema.js +6 -14
- package/dist/lib/task/capabilities.js +12 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +2 -16
- package/dist/lib/task/control-authority.js +371 -5
- package/dist/lib/task/control-recovery.js +128 -0
- package/dist/lib/task/create.js +62 -9
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/events.js +120 -9
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +3 -6
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/intermediate-cleanup.js +383 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle.js +19 -1
- package/dist/lib/task/local-artifact-finalization.js +132 -157
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +101 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +14 -17
- package/dist/lib/task/qualification-audit.js +20 -34
- package/dist/lib/task/review-artifacts.js +2 -18
- package/dist/lib/task/review-command.js +25 -0
- package/dist/lib/task/review-finalization.js +125 -193
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -56
- package/dist/lib/task/verification-engine.js +116 -323
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +17 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
- package/lib/fs/durable-write.ts +29 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/cli-route-inventory.ts +3 -1
- package/lib/internal/codex-lifecycle.ts +6 -6
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +306 -0
- package/lib/internal/platform-pr.ts +15 -4
- package/lib/internal/release-workflow.ts +74 -82
- package/lib/internal/sandbox-control.ts +162 -1
- package/lib/internal/task-artifact.ts +13 -216
- package/lib/internal/task-create.ts +5 -5
- package/lib/internal/task-event.ts +20 -11
- package/lib/internal/task-operation-registry.ts +111 -3
- package/lib/internal/task-review.ts +6 -51
- package/lib/internal/task-validate.ts +26 -10
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +5 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +15 -20
- package/lib/platform/issue-comments.ts +3 -4
- package/lib/platform/issues.ts +4 -245
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +61 -5
- package/lib/platform/pull-requests.ts +3 -170
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/verification-sync.ts +145 -155
- package/lib/release/workflow.ts +3 -3
- package/lib/sandbox/commands/create.ts +7 -6
- package/lib/sandbox/commands/rm.ts +3 -1909
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +109 -66
- package/lib/sandbox/control/executor.ts +134 -74
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +45 -3
- package/lib/sandbox/control/native-file-lock.ts +6 -107
- package/lib/sandbox/control/protocol.ts +75 -6
- package/lib/sandbox/control/server.ts +538 -123
- package/lib/sandbox/control/state.ts +78 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +102 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/recovery.ts +28 -5
- package/lib/sandbox/removal.ts +2144 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-view.ts +17 -15
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +104 -0
- package/lib/task/artifact-lifecycle.ts +31 -26
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +22 -68
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-repair-intent.ts +99 -18
- package/lib/task/artifact-schema.ts +5 -34
- package/lib/task/capabilities.ts +13 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +2 -16
- package/lib/task/control-authority.ts +502 -5
- package/lib/task/control-recovery.ts +174 -0
- package/lib/task/create.ts +63 -8
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/events.ts +131 -10
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +3 -8
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/intermediate-cleanup.ts +455 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle.ts +17 -1
- package/lib/task/local-artifact-finalization.ts +148 -182
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +152 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +19 -19
- package/lib/task/qualification-audit.ts +22 -33
- package/lib/task/review-artifacts.ts +1 -20
- package/lib/task/review-command.ts +23 -0
- package/lib/task/review-finalization.ts +131 -259
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -54
- package/lib/task/verification-engine.ts +110 -366
- package/lib/task/verification-types.ts +17 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +18 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/package.json +6 -6
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +17 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
import { validateManualValidationReceipt } from './manual-validation-receipt.ts';
|
|
6
|
+
import {
|
|
7
|
+
MANUAL_VALIDATION_ARTIFACT as ARTIFACT,
|
|
8
|
+
MANUAL_VALIDATION_SHA40 as SHA40,
|
|
9
|
+
MANUAL_VALIDATION_SHA64 as SHA64,
|
|
10
|
+
MANUAL_VALIDATION_TASK_ID as TASK_ID,
|
|
11
|
+
exactKeys,
|
|
12
|
+
isRecord,
|
|
13
|
+
validTimestamp,
|
|
14
|
+
writeJsonAtomic
|
|
15
|
+
} from './manual-validation-shared.ts';
|
|
16
|
+
|
|
17
|
+
const MANUAL_VALIDATION_TRANSACTION_SCHEMA = 'agent-infra/manual-validation-transaction';
|
|
18
|
+
const MANUAL_VALIDATION_TRANSACTION_VERSION = 1;
|
|
19
|
+
const TRANSACTION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
20
|
+
|
|
21
|
+
type ManualValidationTransactionPhase = 'prepared' | 'summary-staged' | 'receipt-committed' | 'final-promotion-in-progress' | 'committed' | 'aborted' | 'recovery-required';
|
|
22
|
+
type ManualValidationSummaryPreimage = Readonly<{ commentId: number | string | null; body: string; digest: string }>;
|
|
23
|
+
type ManualValidationTransaction = Readonly<{
|
|
24
|
+
schema: typeof MANUAL_VALIDATION_TRANSACTION_SCHEMA;
|
|
25
|
+
version: typeof MANUAL_VALIDATION_TRANSACTION_VERSION;
|
|
26
|
+
transactionId: string;
|
|
27
|
+
taskId: string;
|
|
28
|
+
prNumber: number;
|
|
29
|
+
prHeadSha: string;
|
|
30
|
+
evidenceDigest: string;
|
|
31
|
+
summaryPreimage: ManualValidationSummaryPreimage;
|
|
32
|
+
pendingSummaryDigest: string;
|
|
33
|
+
finalSummaryDigest: string;
|
|
34
|
+
artifact: string;
|
|
35
|
+
phase: ManualValidationTransactionPhase;
|
|
36
|
+
committedReceipt: string | null;
|
|
37
|
+
eventAppended: boolean;
|
|
38
|
+
attempt: number;
|
|
39
|
+
postWriteVerified: boolean;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
error: string | null;
|
|
43
|
+
}>;
|
|
44
|
+
type ManualValidationTransactionInput = Omit<ManualValidationTransaction, 'schema' | 'version' | 'phase' | 'committedReceipt' | 'eventAppended' | 'postWriteVerified' | 'error'> & {
|
|
45
|
+
transactionId?: string;
|
|
46
|
+
};
|
|
47
|
+
type ManualValidationTransactionPatch = Partial<Pick<ManualValidationTransaction, 'committedReceipt' | 'eventAppended' | 'postWriteVerified' | 'error' | 'updatedAt' | 'attempt'>>;
|
|
48
|
+
type ManualValidationTransactionErrorCode =
|
|
49
|
+
| 'MANUAL_VALIDATION_TRANSACTION_INVALID'
|
|
50
|
+
| 'MANUAL_VALIDATION_TRANSACTION_MISSING'
|
|
51
|
+
| 'MANUAL_VALIDATION_TRANSACTION_IDENTITY_MISMATCH'
|
|
52
|
+
| 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID';
|
|
53
|
+
type ManualValidationTransactionError = { code: ManualValidationTransactionErrorCode; message: string };
|
|
54
|
+
type ManualValidationTransactionResult =
|
|
55
|
+
| { ok: true; value: ManualValidationTransaction }
|
|
56
|
+
| { ok: false; error: ManualValidationTransactionError };
|
|
57
|
+
type ManualValidationGenerationArchiveOptions = Readonly<{
|
|
58
|
+
afterReceiptMove?: () => void;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
function invalid(message: string): ManualValidationTransactionResult {
|
|
62
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_INVALID', message } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function summaryPreimageDigest(body: string): string {
|
|
66
|
+
return createHash('sha256').update(body, 'utf8').digest('hex');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateManualValidationTransaction(value: unknown, expected?: Partial<Pick<ManualValidationTransaction, 'transactionId' | 'taskId' | 'prNumber' | 'prHeadSha' | 'evidenceDigest' | 'artifact'>>): ManualValidationTransactionResult {
|
|
70
|
+
if (!isRecord(value)) return invalid('transaction must be a JSON object');
|
|
71
|
+
const keys = ['schema', 'version', 'transactionId', 'taskId', 'prNumber', 'prHeadSha', 'evidenceDigest', 'summaryPreimage', 'pendingSummaryDigest', 'finalSummaryDigest', 'artifact', 'phase', 'committedReceipt', 'eventAppended', 'attempt', 'postWriteVerified', 'createdAt', 'updatedAt', 'error'];
|
|
72
|
+
const keyError = exactKeys(value, keys, 'transaction');
|
|
73
|
+
if (keyError) return invalid(keyError);
|
|
74
|
+
if (value.schema !== MANUAL_VALIDATION_TRANSACTION_SCHEMA || value.version !== MANUAL_VALIDATION_TRANSACTION_VERSION) return invalid('transaction schema or version is unsupported');
|
|
75
|
+
if (typeof value.transactionId !== 'string' || !TRANSACTION_ID.test(value.transactionId)) return invalid('transactionId is invalid');
|
|
76
|
+
if (typeof value.taskId !== 'string' || !TASK_ID.test(value.taskId)) return invalid('taskId is invalid');
|
|
77
|
+
const prNumber = value.prNumber;
|
|
78
|
+
if (!Number.isSafeInteger(prNumber) || (prNumber as number) <= 0) return invalid('prNumber is invalid');
|
|
79
|
+
if (typeof value.prHeadSha !== 'string' || !SHA40.test(value.prHeadSha)) return invalid('prHeadSha is invalid');
|
|
80
|
+
if (typeof value.evidenceDigest !== 'string' || !SHA64.test(value.evidenceDigest)) return invalid('evidenceDigest is invalid');
|
|
81
|
+
if (!isRecord(value.summaryPreimage)) return invalid('summaryPreimage is invalid');
|
|
82
|
+
const preimage = value.summaryPreimage;
|
|
83
|
+
const preimageKeyError = exactKeys(preimage, ['commentId', 'body', 'digest'], 'summaryPreimage');
|
|
84
|
+
if (preimageKeyError) return invalid(preimageKeyError);
|
|
85
|
+
if (preimage.commentId !== null && typeof preimage.commentId !== 'number' && typeof preimage.commentId !== 'string') return invalid('summaryPreimage.commentId is invalid');
|
|
86
|
+
if (typeof preimage.body !== 'string' || typeof preimage.digest !== 'string' || !SHA64.test(preimage.digest) || summaryPreimageDigest(preimage.body) !== preimage.digest) return invalid('summaryPreimage digest is invalid');
|
|
87
|
+
for (const field of ['pendingSummaryDigest', 'finalSummaryDigest'] as const) if (typeof value[field] !== 'string' || !SHA64.test(value[field])) return invalid(`${field} is invalid`);
|
|
88
|
+
if (typeof value.artifact !== 'string' || !ARTIFACT.test(value.artifact)) return invalid('artifact is invalid');
|
|
89
|
+
if (!['prepared', 'summary-staged', 'receipt-committed', 'final-promotion-in-progress', 'committed', 'aborted', 'recovery-required'].includes(value.phase as string)) return invalid('phase is invalid');
|
|
90
|
+
if (value.committedReceipt !== null && (typeof value.committedReceipt !== 'string' || !SHA64.test(value.committedReceipt))) return invalid('committedReceipt is invalid');
|
|
91
|
+
if (typeof value.eventAppended !== 'boolean' || typeof value.postWriteVerified !== 'boolean') return invalid('transaction completion flags are invalid');
|
|
92
|
+
const attempt = value.attempt;
|
|
93
|
+
if (!Number.isSafeInteger(attempt) || (attempt as number) < 1) return invalid('attempt is invalid');
|
|
94
|
+
if (!validTimestamp(value.createdAt) || !validTimestamp(value.updatedAt)) return invalid('transaction timestamps are invalid');
|
|
95
|
+
if (value.error !== null && (typeof value.error !== 'string' || !value.error || /[\r\n]/u.test(value.error))) return invalid('transaction error is invalid');
|
|
96
|
+
if (['receipt-committed', 'final-promotion-in-progress', 'committed'].includes(value.phase as string) && value.committedReceipt === null) return invalid('receipt-backed phases require committedReceipt');
|
|
97
|
+
if (['final-promotion-in-progress', 'committed'].includes(value.phase as string) && !value.eventAppended) return invalid('final promotion requires an appended completion event');
|
|
98
|
+
if (value.phase === 'committed' && !value.postWriteVerified) return invalid('committed phase requires post-write verification');
|
|
99
|
+
const transaction = value as unknown as ManualValidationTransaction;
|
|
100
|
+
if (expected && Object.entries(expected).some(([key, expectedValue]) => expectedValue !== undefined && transaction[key as keyof ManualValidationTransaction] !== expectedValue)) {
|
|
101
|
+
return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_IDENTITY_MISMATCH', message: 'transaction identity does not match the expected operation' } };
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, value: transaction };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createManualValidationTransaction(input: ManualValidationTransactionInput): ManualValidationTransaction {
|
|
107
|
+
const transaction: ManualValidationTransaction = {
|
|
108
|
+
schema: MANUAL_VALIDATION_TRANSACTION_SCHEMA,
|
|
109
|
+
version: MANUAL_VALIDATION_TRANSACTION_VERSION,
|
|
110
|
+
transactionId: input.transactionId ?? `mv-${randomUUID()}`,
|
|
111
|
+
taskId: input.taskId,
|
|
112
|
+
prNumber: input.prNumber,
|
|
113
|
+
prHeadSha: input.prHeadSha,
|
|
114
|
+
evidenceDigest: input.evidenceDigest,
|
|
115
|
+
summaryPreimage: input.summaryPreimage,
|
|
116
|
+
pendingSummaryDigest: input.pendingSummaryDigest,
|
|
117
|
+
finalSummaryDigest: input.finalSummaryDigest,
|
|
118
|
+
artifact: input.artifact,
|
|
119
|
+
phase: 'prepared',
|
|
120
|
+
committedReceipt: null,
|
|
121
|
+
eventAppended: false,
|
|
122
|
+
attempt: input.attempt,
|
|
123
|
+
postWriteVerified: false,
|
|
124
|
+
createdAt: input.createdAt,
|
|
125
|
+
updatedAt: input.updatedAt,
|
|
126
|
+
error: null
|
|
127
|
+
};
|
|
128
|
+
const checked = validateManualValidationTransaction(transaction);
|
|
129
|
+
if (!checked.ok) throw new Error(`${checked.error.code}: ${checked.error.message}`);
|
|
130
|
+
return checked.value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const allowedTransitions: Record<ManualValidationTransactionPhase, readonly ManualValidationTransactionPhase[]> = {
|
|
134
|
+
prepared: ['prepared', 'summary-staged', 'aborted', 'recovery-required'],
|
|
135
|
+
'summary-staged': ['summary-staged', 'receipt-committed', 'aborted', 'recovery-required'],
|
|
136
|
+
'receipt-committed': ['receipt-committed', 'final-promotion-in-progress', 'aborted', 'recovery-required'],
|
|
137
|
+
'final-promotion-in-progress': ['final-promotion-in-progress', 'committed', 'aborted', 'recovery-required'],
|
|
138
|
+
committed: ['committed'],
|
|
139
|
+
aborted: ['aborted', 'prepared'],
|
|
140
|
+
'recovery-required': ['recovery-required', 'prepared']
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function transitionManualValidationTransaction(
|
|
144
|
+
current: ManualValidationTransaction,
|
|
145
|
+
phase: ManualValidationTransactionPhase,
|
|
146
|
+
patch: ManualValidationTransactionPatch = {}
|
|
147
|
+
): ManualValidationTransactionResult {
|
|
148
|
+
if (!allowedTransitions[current.phase].includes(phase)) return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: `cannot transition from ${current.phase} to ${phase}` } };
|
|
149
|
+
if (phase === 'final-promotion-in-progress' && (!current.eventAppended && patch.eventAppended !== true)) return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final promotion requires an appended completion event' } };
|
|
150
|
+
const next: ManualValidationTransaction = {
|
|
151
|
+
...current,
|
|
152
|
+
phase,
|
|
153
|
+
committedReceipt: patch.committedReceipt !== undefined ? patch.committedReceipt : current.committedReceipt,
|
|
154
|
+
eventAppended: patch.eventAppended ?? current.eventAppended,
|
|
155
|
+
postWriteVerified: patch.postWriteVerified ?? current.postWriteVerified,
|
|
156
|
+
attempt: patch.attempt ?? current.attempt,
|
|
157
|
+
error: patch.error !== undefined ? patch.error : current.error,
|
|
158
|
+
updatedAt: patch.updatedAt ?? new Date().toISOString()
|
|
159
|
+
};
|
|
160
|
+
const checked = validateManualValidationTransaction(next);
|
|
161
|
+
if (!checked.ok) return checked;
|
|
162
|
+
return checked;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function retryManualValidationTransaction(current: ManualValidationTransaction, now = new Date().toISOString()): ManualValidationTransactionResult {
|
|
166
|
+
if (!['aborted', 'recovery-required'].includes(current.phase)) return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'only aborted or recovery-required transactions can be retried' } };
|
|
167
|
+
return transitionManualValidationTransaction(current, 'prepared', {
|
|
168
|
+
updatedAt: now,
|
|
169
|
+
error: null,
|
|
170
|
+
committedReceipt: null,
|
|
171
|
+
eventAppended: false,
|
|
172
|
+
postWriteVerified: false,
|
|
173
|
+
attempt: current.attempt + 1
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function manualValidationTransactionPath(taskDir: string): string {
|
|
178
|
+
return path.join(taskDir, '.manual-validation', 'transaction.json');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function manualValidationGenerationPaths(taskDir: string, transaction: ManualValidationTransaction): {
|
|
182
|
+
receipt: { current: string; history: string };
|
|
183
|
+
transaction: { current: string; history: string };
|
|
184
|
+
} {
|
|
185
|
+
const stateDir = path.join(taskDir, '.manual-validation');
|
|
186
|
+
const historyDir = path.join(stateDir, 'history');
|
|
187
|
+
return {
|
|
188
|
+
receipt: {
|
|
189
|
+
current: path.join(stateDir, 'receipt.json'),
|
|
190
|
+
history: path.join(historyDir, `receipt-${transaction.transactionId}-attempt-${transaction.attempt}.json`)
|
|
191
|
+
},
|
|
192
|
+
transaction: {
|
|
193
|
+
current: manualValidationTransactionPath(taskDir),
|
|
194
|
+
history: path.join(historyDir, `transaction-${transaction.transactionId}-attempt-${transaction.attempt}.json`)
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readReceiptAt(file: string, transaction: ManualValidationTransaction): 'missing' | 'valid' {
|
|
200
|
+
let value: unknown;
|
|
201
|
+
try {
|
|
202
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8')) as unknown;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';
|
|
205
|
+
throw new Error(`manual-validation receipt is unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
}
|
|
207
|
+
const checked = validateManualValidationReceipt(value, {
|
|
208
|
+
transactionId: transaction.transactionId,
|
|
209
|
+
taskId: transaction.taskId,
|
|
210
|
+
prNumber: transaction.prNumber,
|
|
211
|
+
prHeadSha: transaction.prHeadSha,
|
|
212
|
+
evidenceDigest: transaction.evidenceDigest,
|
|
213
|
+
artifact: transaction.artifact
|
|
214
|
+
});
|
|
215
|
+
if (!checked.ok) throw new Error(`manual-validation receipt is invalid: ${checked.error.message}`);
|
|
216
|
+
if (transaction.committedReceipt !== null && checked.value.receiptDigest !== transaction.committedReceipt) {
|
|
217
|
+
throw new Error('manual-validation receipt does not match the committed transaction');
|
|
218
|
+
}
|
|
219
|
+
return 'valid';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateManualValidationGenerationArchive(taskDir: string, transaction: ManualValidationTransaction, requireReceipt = false): void {
|
|
223
|
+
const paths = manualValidationGenerationPaths(taskDir, transaction);
|
|
224
|
+
const currentTransaction = fs.existsSync(paths.transaction.current);
|
|
225
|
+
const archivedTransaction = fs.existsSync(paths.transaction.history);
|
|
226
|
+
if (currentTransaction && archivedTransaction) throw new Error(`manual-validation archive has conflicting transaction files for ${transaction.transactionId}`);
|
|
227
|
+
if (!currentTransaction && !archivedTransaction) throw new Error(`manual-validation archive source is missing: ${paths.transaction.current}`);
|
|
228
|
+
const currentReceipt = requireReceipt
|
|
229
|
+
? readReceiptAt(paths.receipt.current, transaction)
|
|
230
|
+
: (fs.existsSync(paths.receipt.current) ? 'valid' : 'missing');
|
|
231
|
+
const archivedReceipt = requireReceipt
|
|
232
|
+
? readReceiptAt(paths.receipt.history, transaction)
|
|
233
|
+
: (fs.existsSync(paths.receipt.history) ? 'valid' : 'missing');
|
|
234
|
+
if (requireReceipt && currentReceipt === 'missing' && archivedReceipt === 'missing') {
|
|
235
|
+
throw new Error(`manual-validation receipt is missing for transaction ${transaction.transactionId}`);
|
|
236
|
+
}
|
|
237
|
+
if (currentReceipt === 'valid' && archivedReceipt === 'valid') {
|
|
238
|
+
throw new Error(`manual-validation archive has conflicting receipt files for ${transaction.transactionId}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function archiveManualValidationGeneration(
|
|
243
|
+
taskDir: string,
|
|
244
|
+
transaction: ManualValidationTransaction,
|
|
245
|
+
requireReceipt = false,
|
|
246
|
+
options: ManualValidationGenerationArchiveOptions = {}
|
|
247
|
+
): void {
|
|
248
|
+
const paths = manualValidationGenerationPaths(taskDir, transaction);
|
|
249
|
+
validateManualValidationGenerationArchive(taskDir, transaction, requireReceipt);
|
|
250
|
+
fs.mkdirSync(path.dirname(paths.receipt.history), { recursive: true });
|
|
251
|
+
const move = (source: string, target: string, required: boolean): void => {
|
|
252
|
+
if (!fs.existsSync(source)) {
|
|
253
|
+
if (fs.existsSync(target) || !required) return;
|
|
254
|
+
throw new Error(`manual-validation archive source is missing: ${source}`);
|
|
255
|
+
}
|
|
256
|
+
if (fs.existsSync(target)) throw new Error(`manual-validation archive target already exists: ${target}`);
|
|
257
|
+
fs.renameSync(source, target);
|
|
258
|
+
};
|
|
259
|
+
move(paths.receipt.current, paths.receipt.history, requireReceipt);
|
|
260
|
+
options.afterReceiptMove?.();
|
|
261
|
+
move(paths.transaction.current, paths.transaction.history, true);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function writeManualValidationTransactionAtomic(taskDir: string, transaction: ManualValidationTransaction): string {
|
|
265
|
+
const checked = validateManualValidationTransaction(transaction);
|
|
266
|
+
if (!checked.ok) throw new Error(`${checked.error.code}: ${checked.error.message}`);
|
|
267
|
+
const target = manualValidationTransactionPath(taskDir);
|
|
268
|
+
return writeJsonAtomic(target, transaction);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readManualValidationTransaction(taskDir: string, expected?: Parameters<typeof validateManualValidationTransaction>[1]): ManualValidationTransactionResult {
|
|
272
|
+
const file = manualValidationTransactionPath(taskDir);
|
|
273
|
+
let value: unknown;
|
|
274
|
+
try { value = JSON.parse(fs.readFileSync(file, 'utf8')) as unknown; }
|
|
275
|
+
catch (error) {
|
|
276
|
+
return { ok: false, error: { code: (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'MANUAL_VALIDATION_TRANSACTION_MISSING' : 'MANUAL_VALIDATION_TRANSACTION_INVALID', message: error instanceof Error ? error.message : String(error) } };
|
|
277
|
+
}
|
|
278
|
+
return validateManualValidationTransaction(value, expected);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export {
|
|
282
|
+
MANUAL_VALIDATION_TRANSACTION_SCHEMA,
|
|
283
|
+
MANUAL_VALIDATION_TRANSACTION_VERSION,
|
|
284
|
+
archiveManualValidationGeneration,
|
|
285
|
+
createManualValidationTransaction,
|
|
286
|
+
manualValidationTransactionPath,
|
|
287
|
+
validateManualValidationGenerationArchive,
|
|
288
|
+
readManualValidationTransaction,
|
|
289
|
+
retryManualValidationTransaction,
|
|
290
|
+
summaryPreimageDigest,
|
|
291
|
+
transitionManualValidationTransaction,
|
|
292
|
+
validateManualValidationTransaction,
|
|
293
|
+
writeManualValidationTransactionAtomic
|
|
294
|
+
};
|
|
295
|
+
export type {
|
|
296
|
+
ManualValidationSummaryPreimage,
|
|
297
|
+
ManualValidationTransaction,
|
|
298
|
+
ManualValidationTransactionError,
|
|
299
|
+
ManualValidationTransactionErrorCode,
|
|
300
|
+
ManualValidationTransactionInput,
|
|
301
|
+
ManualValidationTransactionPatch,
|
|
302
|
+
ManualValidationTransactionPhase,
|
|
303
|
+
ManualValidationTransactionResult
|
|
304
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
type SourceLine = {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type VisibleHeading = SourceLine & {
|
|
8
|
+
level: number;
|
|
9
|
+
text: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type VisibleAnchor = SourceLine & {
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type VisibleMarkdown = {
|
|
17
|
+
hasUnclosedFence: boolean;
|
|
18
|
+
lines: SourceLine[];
|
|
19
|
+
headings: VisibleHeading[];
|
|
20
|
+
anchors: VisibleAnchor[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/;
|
|
24
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
25
|
+
const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
|
|
26
|
+
const ANCHOR = /^\s*<a\s+id=(['"])([^'"]+)\1\s*><\/a>\s*$/;
|
|
27
|
+
|
|
28
|
+
function sourceLines(content: string): SourceLine[] {
|
|
29
|
+
const lines: SourceLine[] = [];
|
|
30
|
+
let start = 0;
|
|
31
|
+
while (start < content.length) {
|
|
32
|
+
const newline = content.indexOf('\n', start);
|
|
33
|
+
const end = newline === -1 ? content.length : newline;
|
|
34
|
+
lines.push({ start, end, text: content.slice(start, end).replace(/\r$/, '') });
|
|
35
|
+
start = newline === -1 ? content.length : newline + 1;
|
|
36
|
+
}
|
|
37
|
+
return lines;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function scanVisibleMarkdown(content: string): VisibleMarkdown {
|
|
41
|
+
const lines = sourceLines(content);
|
|
42
|
+
const visible: SourceLine[] = [];
|
|
43
|
+
const headings: VisibleHeading[] = [];
|
|
44
|
+
const anchors: VisibleAnchor[] = [];
|
|
45
|
+
let fence: { character: '`' | '~'; length: number } | null = null;
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (fence) {
|
|
49
|
+
const close = line.text.match(FENCE_CLOSE);
|
|
50
|
+
if (close && close[1]![0] === fence.character && close[1]!.length >= fence.length) fence = null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const open = line.text.match(FENCE_OPEN);
|
|
54
|
+
const marker = open?.[1];
|
|
55
|
+
const info = open?.[2] ?? '';
|
|
56
|
+
if (marker && !(marker[0] === '`' && info.includes('`'))) {
|
|
57
|
+
fence = { character: marker[0] as '`' | '~', length: marker.length };
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
visible.push(line);
|
|
61
|
+
const heading = line.text.match(HEADING);
|
|
62
|
+
if (heading) headings.push({ ...line, level: heading[1]!.length, text: heading[2]!.trim() });
|
|
63
|
+
const anchor = line.text.match(ANCHOR);
|
|
64
|
+
if (anchor) anchors.push({ ...line, id: anchor[2]! });
|
|
65
|
+
}
|
|
66
|
+
return { lines: visible, headings, anchors, hasUnclosedFence: fence !== null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { scanVisibleMarkdown };
|
|
70
|
+
export type { SourceLine, VisibleHeading, VisibleMarkdown };
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
5
|
+
import { artifactName, maxArtifactRound } from './artifact-name.ts';
|
|
5
6
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
6
7
|
import { validateCurrentTaskContract } from './current-contract.ts';
|
|
7
8
|
import { parseReviewSummary } from './review-artifacts.ts';
|
|
@@ -39,6 +40,7 @@ import type {
|
|
|
39
40
|
OrchestrationModelPolicy,
|
|
40
41
|
OrchestrationRolePolicy
|
|
41
42
|
} from '../agent-clients/types.ts';
|
|
43
|
+
import type { LifecycleRecoveryAttestationV1 } from './control-authority.ts';
|
|
42
44
|
import {
|
|
43
45
|
captureRepositorySnapshot,
|
|
44
46
|
captureWorkspaceSnapshot,
|
|
@@ -173,6 +175,7 @@ type OrchestrationOptions = {
|
|
|
173
175
|
supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
|
|
174
176
|
validateLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
175
177
|
consumeLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
178
|
+
lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
|
|
176
179
|
token?: () => string;
|
|
177
180
|
monotonicNow?: () => number;
|
|
178
181
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
@@ -275,7 +278,7 @@ function isRecovery(value: unknown): value is OrchestrationRecovery {
|
|
|
275
278
|
&& value.resultingStatus === 'running';
|
|
276
279
|
}
|
|
277
280
|
|
|
278
|
-
function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence {
|
|
281
|
+
export function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence {
|
|
279
282
|
return hasExactKeys(value, [
|
|
280
283
|
'kind', 'observedAt', 'head', 'headTree', 'worktreeTree', 'lastReviewedCommit', 'prNumber', 'prHead'
|
|
281
284
|
])
|
|
@@ -604,25 +607,13 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
604
607
|
return { status: 'running', changed: true, taskId: resolved.taskId, run, next: null, error: null };
|
|
605
608
|
}
|
|
606
609
|
|
|
607
|
-
function highestRound(taskDir: string, family: string): number {
|
|
608
|
-
const pattern = new RegExp(`^${family}(?:-r(\\d+))?\\.md$`);
|
|
609
|
-
return fs.readdirSync(taskDir).reduce((max, name) => {
|
|
610
|
-
const match = pattern.exec(name);
|
|
611
|
-
return match ? Math.max(max, match[1] ? Number(match[1]) : 1) : max;
|
|
612
|
-
}, 0);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function artifactName(family: string, round: number): string {
|
|
616
|
-
return round === 1 ? `${family}.md` : `${family}-r${round}.md`;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
610
|
function validateSavedCurrentDiffBase(
|
|
620
611
|
repoRoot: string,
|
|
621
612
|
metadata: ReturnType<typeof parseTypedTaskFrontmatter>,
|
|
622
613
|
taskDir: string,
|
|
623
614
|
reviewedHead: string
|
|
624
615
|
): Readonly<{ code: string; message: string }> | null {
|
|
625
|
-
const reviewRound =
|
|
616
|
+
const reviewRound = maxArtifactRound(fs.readdirSync(taskDir), 'review-code');
|
|
626
617
|
const reviewContent = fs.readFileSync(path.join(taskDir, artifactName('review-code', reviewRound)), 'utf8');
|
|
627
618
|
const savedTargetHead = extractReviewTargetHead(reviewContent);
|
|
628
619
|
const savedDiffBase = extractReviewDiffBase(reviewContent);
|
|
@@ -660,10 +651,9 @@ function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'request
|
|
|
660
651
|
const action = recommendation.action;
|
|
661
652
|
if (action === 'manual-validation' || action === 'validation-run') return null;
|
|
662
653
|
const family = action === 'analysis' ? 'analysis' : action;
|
|
663
|
-
const round =
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
})) + 1;
|
|
654
|
+
const round = maxArtifactRound([
|
|
655
|
+
...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])
|
|
656
|
+
], family) + 1;
|
|
667
657
|
const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
|
|
668
658
|
const role = action.startsWith('review-') ? 'reviewer' : 'executor';
|
|
669
659
|
return { action: command, role, stage: action, round, artifact: artifactName(family, round) };
|
|
@@ -719,7 +709,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
719
709
|
const routed = routeFromFacts(facts.facts);
|
|
720
710
|
if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
|
|
721
711
|
if ('completion' in routed) {
|
|
722
|
-
const reviewRound =
|
|
712
|
+
const reviewRound = maxArtifactRound(fs.readdirSync(resolved.taskDir), 'review-code');
|
|
723
713
|
const review = parseReviewSummary(fs.readFileSync(
|
|
724
714
|
path.join(resolved.taskDir, artifactName('review-code', reviewRound)),
|
|
725
715
|
'utf8'
|
|
@@ -830,6 +820,7 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
830
820
|
requestedModel?: string;
|
|
831
821
|
requestedReasoningEffort?: string;
|
|
832
822
|
lifecycleProvenance?: DelegationLifecycleProvenance;
|
|
823
|
+
lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
|
|
833
824
|
}>,
|
|
834
825
|
options: OrchestrationOptions = {}
|
|
835
826
|
): OrchestrationResult {
|
|
@@ -866,6 +857,13 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
866
857
|
if (input.client === 'codex' && !input.lifecycleProvenance) {
|
|
867
858
|
return failed('ORCHESTRATION_CODEX_PROVENANCE_REQUIRED', 'Codex prepare requires lifecycle provenance', resolved.taskId);
|
|
868
859
|
}
|
|
860
|
+
if (input.lifecycleRecoveryAttestation
|
|
861
|
+
&& (input.lifecycleRecoveryAttestation.phase !== 'orchestration.prepare'
|
|
862
|
+
|| input.lifecycleRecoveryAttestation.taskId !== resolved.taskId
|
|
863
|
+
|| input.lifecycleRecoveryAttestation.artifact !== next.artifact
|
|
864
|
+
|| input.lifecycleRecoveryAttestation.round !== next.round)) {
|
|
865
|
+
return failed('LIFECYCLE_AUTHORITY_TUPLE_MISMATCH', 'orchestration authority does not match the routed stage', resolved.taskId);
|
|
866
|
+
}
|
|
869
867
|
const expectedPolicy = run.modelPolicy[next.role];
|
|
870
868
|
if (input.requestedModel !== expectedPolicy.model) {
|
|
871
869
|
return failed('ORCHESTRATION_REQUESTED_MODEL_MISMATCH', `requested model does not match the persisted ${next.role} model`, resolved.taskId);
|
|
@@ -930,6 +928,7 @@ function prepareOrchestrationDelegation(
|
|
|
930
928
|
requestedModel?: string;
|
|
931
929
|
requestedReasoningEffort?: string;
|
|
932
930
|
lifecycleProvenance?: DelegationLifecycleProvenance;
|
|
931
|
+
lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
|
|
933
932
|
}>,
|
|
934
933
|
options: OrchestrationOptions = {}
|
|
935
934
|
): OrchestrationResult {
|
|
@@ -1606,6 +1605,7 @@ export {
|
|
|
1606
1605
|
statusOrchestration,
|
|
1607
1606
|
};
|
|
1608
1607
|
export type {
|
|
1608
|
+
CleanCompletionEvidence,
|
|
1609
1609
|
OrchestrationCompletionPlanResult,
|
|
1610
1610
|
OrchestrationModelPolicy,
|
|
1611
1611
|
OrchestrationNext,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
3
4
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
4
|
-
import { parseTable } from './sections.ts';
|
|
5
|
+
import { findSectionRange, parseTable } from './sections.ts';
|
|
6
|
+
import { scanVisibleMarkdown } from './markdown.ts';
|
|
5
7
|
|
|
6
8
|
const TASK_CONSTRAINT_HEADINGS = ['约束', 'Constraints'] as const;
|
|
7
9
|
const TASK_CANDIDATE_HEADINGS = ['候选与否决方案', 'Candidate and Rejected Options', 'Candidates and Rejected Options', 'Candidates and Rejected Alternatives'] as const;
|
|
@@ -170,21 +172,9 @@ function taskInputDigest(qualification: Pick<TaskQualification, 'constraints' |
|
|
|
170
172
|
return digest(canonicalTaskInput(qualification));
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
function sectionBody(content: string, aliases: readonly string[], level = 2): string | null {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const match = new RegExp(`^${marker}\\s+(${heading})\\s*$`, 'm').exec(content);
|
|
177
|
-
if (!match) return null;
|
|
178
|
-
const rest = content.slice((match.index ?? 0) + match[0].length);
|
|
179
|
-
const end = rest.search(new RegExp(`^#{2,${level}}\\s+`, 'm'));
|
|
180
|
-
return rest.slice(0, end < 0 ? rest.length : end);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function stripSection(content: string, aliases: readonly string[]): string {
|
|
184
|
-
const body = sectionBody(content, aliases);
|
|
185
|
-
if (body === null) return normalizeText(content);
|
|
186
|
-
const heading = aliases.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
187
|
-
return normalizeText(content.replace(new RegExp(`^##\\s+(?:${heading})\\s*$[\\s\\S]*?(?=^##\\s+|(?![\\s\\S]))`, 'm'), ''));
|
|
175
|
+
function sectionBody(content: string, aliases: readonly string[], level: 2 | 3 = 2): string | null {
|
|
176
|
+
const section = findSectionRange(content, aliases, level);
|
|
177
|
+
return section ? content.slice(section.bodyStart, section.end) : null;
|
|
188
178
|
}
|
|
189
179
|
|
|
190
180
|
function stripFrontmatter(content: string): string {
|
|
@@ -218,7 +208,7 @@ function nonConstraintInputDigest(content: string): string {
|
|
|
218
208
|
['返工意图', 'Rework Intents'],
|
|
219
209
|
CONFIRMATION_HEADINGS,
|
|
220
210
|
['完成检查清单', 'Completion Checklist']
|
|
221
|
-
].reduce((value, aliases) =>
|
|
211
|
+
].reduce((value, aliases) => stripSectionAtLevel(value, aliases, 2), projection);
|
|
222
212
|
return digest(projection);
|
|
223
213
|
}
|
|
224
214
|
|
|
@@ -231,12 +221,9 @@ function stripTaskInputSection(content: string, aliases: readonly string[]): str
|
|
|
231
221
|
return stripSectionAtLevel(result, aliases, 2);
|
|
232
222
|
}
|
|
233
223
|
|
|
234
|
-
function stripSectionAtLevel(content: string, aliases: readonly string[], level:
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
const heading = aliases.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
238
|
-
const marker = '#'.repeat(level);
|
|
239
|
-
return normalizeText(content.replace(new RegExp(`^${marker}\\s+(?:${heading})\\s*$[\\s\\S]*?(?=^#{2,${level}}\\s+|(?![\\s\\S]))`, 'm'), ''));
|
|
224
|
+
function stripSectionAtLevel(content: string, aliases: readonly string[], level: 2 | 3): string {
|
|
225
|
+
const section = findSectionRange(content, aliases, level);
|
|
226
|
+
return normalizeText(section ? content.slice(0, section.start) + content.slice(section.end) : content);
|
|
240
227
|
}
|
|
241
228
|
|
|
242
229
|
function parseTaskQualification(content: string): { ok: true; qualification: TaskQualification } | { ok: false; code: string; message: string } {
|
|
@@ -246,12 +233,14 @@ function parseTaskQualification(content: string): { ok: true; qualification: Tas
|
|
|
246
233
|
const hasCandidateHeading = candidateBody !== null;
|
|
247
234
|
if ((!hasConstraintHeading && !hasCandidateHeading)
|
|
248
235
|
|| (hasConstraintHeading && hasCandidateHeading
|
|
249
|
-
&&
|
|
236
|
+
&& !scanVisibleMarkdown(constraintBody!).lines.some((line) => line.text.trim().startsWith('|'))
|
|
237
|
+
&& !scanVisibleMarkdown(candidateBody!).lines.some((line) => line.text.trim().startsWith('|')))) {
|
|
250
238
|
return { ok: true, qualification: { present: false, constraints: [], candidates: [], constraintDigest: digest([]), taskInputDigest: digest({ constraints: [], candidates: [] }), nonConstraintInputDigest: nonConstraintInputDigest(content) } };
|
|
251
239
|
}
|
|
252
240
|
if (!hasConstraintHeading || !hasCandidateHeading) {
|
|
253
|
-
const
|
|
254
|
-
|
|
241
|
+
const visible = scanVisibleMarkdown(content).lines.map((line) => line.text).join('\n');
|
|
242
|
+
const hasCanonicalTable = new RegExp(`\\|\\s*${CONSTRAINT_COLUMNS.join('\\s*\\|\\s*')}\\s*\\|`).test(visible)
|
|
243
|
+
|| new RegExp(`\\|\\s*${CANDIDATE_COLUMNS.join('\\s*\\|\\s*')}\\s*\\|`).test(visible);
|
|
255
244
|
if (hasCanonicalTable) return { ok: false, code: 'QUALIFICATION_TASK_CONTRACT_INVALID', message: 'task qualification requires both constraints and candidates sections' };
|
|
256
245
|
return { ok: true, qualification: { present: false, constraints: [], candidates: [], constraintDigest: digest([]), taskInputDigest: digest({ constraints: [], candidates: [] }), nonConstraintInputDigest: nonConstraintInputDigest(content) } };
|
|
257
246
|
}
|
|
@@ -315,10 +304,10 @@ function parseAuditTable(content: string, heading: (typeof AUDIT_SUBSECTIONS)[nu
|
|
|
315
304
|
return table?.rows.map((row) => ({ ...row.values })) ?? null;
|
|
316
305
|
}
|
|
317
306
|
|
|
318
|
-
function
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
307
|
+
function parseAuditArtifactName(value: string): { family: ArtifactFamily; round: number } | null {
|
|
308
|
+
const identity = parseArtifactName(value);
|
|
309
|
+
return identity && ARTIFACT_FAMILIES.includes(identity.family as ArtifactFamily)
|
|
310
|
+
? { family: identity.family as ArtifactFamily, round: identity.round } : null;
|
|
322
311
|
}
|
|
323
312
|
|
|
324
313
|
function upstreamArtifactDigest(rows: readonly UpstreamRelation[]): string {
|
|
@@ -327,7 +316,7 @@ function upstreamArtifactDigest(rows: readonly UpstreamRelation[]): string {
|
|
|
327
316
|
}
|
|
328
317
|
|
|
329
318
|
function validateUpstreamRelation(row: UpstreamRelation): void {
|
|
330
|
-
const identity =
|
|
319
|
+
const identity = parseAuditArtifactName(row.upstreamArtifact);
|
|
331
320
|
if (!ARTIFACT_FAMILIES.includes(row.upstreamFamily) || !identity || identity.family !== row.upstreamFamily
|
|
332
321
|
|| identity.round !== row.upstreamRound || !Number.isSafeInteger(row.upstreamRound)
|
|
333
322
|
|| !/^[a-f0-9]{64}$/i.test(row.upstreamSha256) || !RELATIONS.includes(row.relation)) {
|
|
@@ -368,7 +357,7 @@ function expectedQualificationRelations(
|
|
|
368
357
|
const inputName = family === 'code' ? frontmatter.code_input_artifact : family.startsWith('review-') ? frontmatter.review_input_artifact : '';
|
|
369
358
|
const inputSha256 = family === 'code' ? frontmatter.code_input_sha256 : family.startsWith('review-') ? frontmatter.review_input_sha256 : '';
|
|
370
359
|
if (typeof inputName !== 'string' || !inputName || typeof inputSha256 !== 'string' || !inputSha256) return { ok: true, relations: undefined };
|
|
371
|
-
const identity =
|
|
360
|
+
const identity = parseAuditArtifactName(inputName);
|
|
372
361
|
if (!identity) return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: `started input '${inputName}' is not canonical` };
|
|
373
362
|
const relation: UpstreamRelation = {
|
|
374
363
|
upstreamFamily: identity.family, upstreamArtifact: inputName, upstreamRound: identity.round,
|
|
@@ -507,7 +496,7 @@ function validateQualificationAudit(
|
|
|
507
496
|
|| upstreamArtifactDigest(options.expectedUpstreamRelations) !== upstreamArtifactDigest(audit.audit.upstreamRelations)
|
|
508
497
|
)) return { ok: false, code: 'QUALIFICATION_UPSTREAM_RELATION_MISMATCH', message: 'qualification audit upstream relations do not match the started artifact inputs' };
|
|
509
498
|
if (options.family && options.artifact) {
|
|
510
|
-
const identity =
|
|
499
|
+
const identity = parseAuditArtifactName(options.artifact);
|
|
511
500
|
if (!identity || identity.family !== options.family) return { ok: false, code: 'QUALIFICATION_ARTIFACT_IDENTITY_INVALID', message: `artifact '${options.artifact}' is not canonical for '${options.family}'` };
|
|
512
501
|
}
|
|
513
502
|
return { ok: true, qualification: task.qualification, audit: audit.audit };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { scanVisibleMarkdown } from './
|
|
4
|
+
import { scanVisibleMarkdown } from './markdown.ts';
|
|
5
5
|
|
|
6
6
|
type ReviewFindingCounts = {
|
|
7
7
|
blocker: number;
|
|
@@ -53,23 +53,6 @@ function escapeRegExp(value: string): string {
|
|
|
53
53
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function maxRound(entries: string[], stem: string): number {
|
|
57
|
-
let max = 0;
|
|
58
|
-
for (const entry of entries) {
|
|
59
|
-
if (entry === `${stem}.md`) {
|
|
60
|
-
max = Math.max(max, 1);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const match = entry.match(new RegExp(`^${escapeRegExp(stem)}-r(\\d+)\\.md$`));
|
|
64
|
-
if (match) max = Math.max(max, Number(match[1]));
|
|
65
|
-
}
|
|
66
|
-
return max;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function artifactName(stem: string, round: number): string {
|
|
70
|
-
return round === 1 ? `${stem}.md` : `${stem}-r${round}.md`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
56
|
function normalizeVerdict(raw: unknown): ReviewVerdict | '' {
|
|
74
57
|
const value = String(raw).trim().toLowerCase();
|
|
75
58
|
if (value === '通过' || value === 'approved') return 'Approved';
|
|
@@ -260,12 +243,10 @@ function parseVerdict(reviewPath: string): PathVerdictResult {
|
|
|
260
243
|
}
|
|
261
244
|
|
|
262
245
|
export {
|
|
263
|
-
artifactName,
|
|
264
246
|
equalCounts,
|
|
265
247
|
escapeRegExp,
|
|
266
248
|
extractSection,
|
|
267
249
|
finalizeReviewSummaryContent,
|
|
268
|
-
maxRound,
|
|
269
250
|
normalizeVerdict,
|
|
270
251
|
parseReviewSummary,
|
|
271
252
|
parseVerdict,
|