@fitlab-ai/agent-infra 0.9.12 → 0.9.13
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 +11 -11
- package/README.zh-CN.md +11 -11
- package/dist/bin/internal-cli.js +24 -19
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +63 -24
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +1 -1
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +119 -104
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +520 -50
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +783 -537
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +11 -12
- package/dist/lib/platform/verification-sync.js +112 -238
- package/dist/lib/pr-review/evidence-grading.js +19 -6
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +33 -5
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-operation.js +12 -1
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/current-contract.js +5 -1
- package/dist/lib/task/decision-details.js +1 -1
- package/dist/lib/task/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +58 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +49 -93
- package/dist/lib/task/pr-delivery-fact.js +102 -101
- package/dist/lib/task/review-fingerprint.js +24 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +43 -16
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/version.js +11 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/init.ts +1 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +47 -22
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +2 -2
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +126 -106
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +560 -53
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +826 -543
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +11 -13
- package/lib/platform/verification-sync.ts +112 -292
- package/lib/pr-review/evidence-grading.ts +25 -8
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +32 -6
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-operation.ts +12 -1
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +6 -2
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +70 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +52 -85
- package/lib/task/pr-delivery-fact.ts +131 -148
- package/lib/task/review-fingerprint.ts +23 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +52 -17
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +1 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
- package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
- package/templates/.agents/rules/pr-sync.en.md +10 -4
- package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
- package/templates/.agents/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +18 -1
- package/templates/.agents/templates/task.zh-CN.md +18 -1
- package/templates/.github/workflows/pr-label.yml +7 -26
package/lib/task/events.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
|
|
3
4
|
import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
|
|
4
5
|
import {
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
validateCompletedArtifact
|
|
13
14
|
} from './artifact-lifecycle.ts';
|
|
14
15
|
import type { ArtifactContextResult, ArtifactErrorCode, ArtifactFamily, ArtifactIdentity } from './artifact-lifecycle.ts';
|
|
15
|
-
import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.ts';
|
|
16
|
+
import { ArtifactReceiptError, parseArtifactReceipts, sha256File, upsertArtifactReceipt } from './artifact-receipts.ts';
|
|
16
17
|
import type { ArtifactReceipt } from './artifact-receipts.ts';
|
|
17
18
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
18
19
|
import {
|
|
@@ -33,6 +34,20 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
33
34
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
34
35
|
import type { TaskOperationSummary, TaskWriteErrorCode, TaskWriteOptions } from './write.ts';
|
|
35
36
|
import { allowsManualOverride } from './guard-override.ts';
|
|
37
|
+
import {
|
|
38
|
+
LOCAL_ARTIFACT_REQUIRED_PATTERNS,
|
|
39
|
+
LOCAL_ARTIFACT_REQUIRED_SECTIONS,
|
|
40
|
+
consumeLocalArtifactFinalizationIntent,
|
|
41
|
+
readLocalArtifactFinalizationIntent,
|
|
42
|
+
validateLocalArtifact
|
|
43
|
+
} from './local-artifact-finalization.ts';
|
|
44
|
+
import type { LocalArtifactFamily, LocalArtifactFinalizationIntent } from './local-artifact-finalization.ts';
|
|
45
|
+
import { loadVerificationConfig } from './verification-config.ts';
|
|
46
|
+
import { buildLifecycleFacts, canStart } from './capabilities.ts';
|
|
47
|
+
import type { ExplicitTrigger, LifecycleAction, TriggerInitiator, TriggerReason } from './capabilities.ts';
|
|
48
|
+
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.ts';
|
|
49
|
+
import type { InvalidationTargetKind } from './invalidation.ts';
|
|
50
|
+
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.ts';
|
|
36
51
|
|
|
37
52
|
const eventCatalog = [
|
|
38
53
|
'analyze.started', 'analyze.awaiting-input', 'analyze.completed',
|
|
@@ -48,6 +63,7 @@ type TaskEventName = (typeof eventCatalog)[number];
|
|
|
48
63
|
type Verdict = 'approved' | 'changes-requested' | 'rejected';
|
|
49
64
|
type TaskEventErrorCode =
|
|
50
65
|
| 'EVENT_UNKNOWN' | 'EVENT_PAYLOAD_INVALID' | 'EVENT_TRANSITION_INVALID'
|
|
66
|
+
| 'EVENT_TRIGGER_REQUIRED'
|
|
51
67
|
| 'EVENT_LOG_MISSING' | 'EVENT_START_MISSING' | 'EVENT_ALREADY_COMPLETED'
|
|
52
68
|
| 'EVENT_LOG_CONFLICT' | 'EVENT_ARTIFACT_CONFLICT' | 'EVENT_FINDING_COUNT_MISMATCH'
|
|
53
69
|
| 'EVENT_VERDICT_INVALID'
|
|
@@ -56,7 +72,10 @@ type TaskEventErrorCode =
|
|
|
56
72
|
type TaskEventRequest = {
|
|
57
73
|
taskRef: string; event: TaskEventName | string; agent: string; dryRun?: boolean; orchestrated?: boolean;
|
|
58
74
|
overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
|
|
75
|
+
initiator?: TriggerInitiator; requestId?: string; reasonCode?: TriggerReason;
|
|
76
|
+
sourceFinding?: string; sourceArtifact?: string; sourceSha256?: string;
|
|
59
77
|
round?: number; question?: number; artifact?: string; fixFor?: string; implementationInput?: string;
|
|
78
|
+
artifactSha256?: string; semanticDigest?: string;
|
|
60
79
|
verdict?: Verdict; blockers?: number; major?: number; minor?: number;
|
|
61
80
|
manualValidation?: number; filesModified?: number; testsPassed?: number;
|
|
62
81
|
summaryResult?: string;
|
|
@@ -77,19 +96,19 @@ type TaskEventResult = {
|
|
|
77
96
|
operations: readonly TaskOperationSummary[]; error: TaskEventError | null;
|
|
78
97
|
};
|
|
79
98
|
|
|
80
|
-
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']);
|
|
99
|
+
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
|
|
81
100
|
const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
|
|
82
101
|
'analyze.started': { optional: ['round'] },
|
|
83
102
|
'analyze.awaiting-input': { required: ['question'] },
|
|
84
|
-
'analyze.completed': { required: ['artifact'], optional: ['round', 'orchestrated'] },
|
|
103
|
+
'analyze.completed': { required: ['artifact', 'artifactSha256', 'semanticDigest'], optional: ['round', 'orchestrated'] },
|
|
85
104
|
'review-analysis.started': { optional: ['round'] },
|
|
86
105
|
'review-analysis.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
|
|
87
106
|
'plan.started': { optional: ['round'] },
|
|
88
|
-
'plan.completed': { required: ['artifact'], optional: ['round', 'orchestrated'] },
|
|
107
|
+
'plan.completed': { required: ['artifact', 'artifactSha256', 'semanticDigest'], optional: ['round', 'orchestrated'] },
|
|
89
108
|
'review-plan.started': { optional: ['round'] },
|
|
90
109
|
'review-plan.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
|
|
91
110
|
'code.started': { optional: ['round', 'fixFor', 'implementationInput'] },
|
|
92
|
-
'code.completed': { required: ['artifact'], optional: ['round', 'fixFor', 'implementationInput', 'filesModified', 'testsPassed', 'blockers', 'major', 'minor', 'manualValidation', 'orchestrated'] },
|
|
111
|
+
'code.completed': { required: ['artifact', 'artifactSha256', 'semanticDigest'], optional: ['round', 'fixFor', 'implementationInput', 'filesModified', 'testsPassed', 'blockers', 'major', 'minor', 'manualValidation', 'orchestrated'] },
|
|
93
112
|
'review-code.started': { optional: ['round'] },
|
|
94
113
|
'review-code.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
|
|
95
114
|
'manual-validation.started': { optional: ['round'] },
|
|
@@ -98,6 +117,30 @@ const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[]
|
|
|
98
117
|
'validation-run.completed': { required: ['artifact'], optional: ['round'] }
|
|
99
118
|
};
|
|
100
119
|
|
|
120
|
+
function localArtifactValidationConfig(repoRoot: string, family: LocalArtifactFamily) {
|
|
121
|
+
const fallback = {
|
|
122
|
+
requiredSections: LOCAL_ARTIFACT_REQUIRED_SECTIONS[family],
|
|
123
|
+
requiredPatterns: LOCAL_ARTIFACT_REQUIRED_PATTERNS
|
|
124
|
+
};
|
|
125
|
+
const skillName = family === 'analysis' ? 'analyze-task' : family === 'plan' ? 'plan-task' : 'code-task';
|
|
126
|
+
try {
|
|
127
|
+
const artifact = loadVerificationConfig(repoRoot, skillName).checks.artifact;
|
|
128
|
+
if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact)) return fallback;
|
|
129
|
+
const sections = artifact.required_sections;
|
|
130
|
+
const patterns = artifact.required_patterns;
|
|
131
|
+
return {
|
|
132
|
+
requiredSections: Array.isArray(sections)
|
|
133
|
+
? sections.filter((value): value is string => typeof value === 'string')
|
|
134
|
+
: fallback.requiredSections,
|
|
135
|
+
requiredPatterns: Array.isArray(patterns)
|
|
136
|
+
? patterns.filter((value): value is string => typeof value === 'string')
|
|
137
|
+
: fallback.requiredPatterns
|
|
138
|
+
};
|
|
139
|
+
} catch {
|
|
140
|
+
return fallback;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
101
144
|
function validateTaskEventRequest(request: TaskEventRequest): TaskEventError | null {
|
|
102
145
|
if (!eventCatalog.includes(request.event as TaskEventName)) return { code: 'EVENT_UNKNOWN', message: `unknown task event '${request.event}'` };
|
|
103
146
|
if (!request.taskRef || !request.agent) return { code: 'EVENT_PAYLOAD_INVALID', message: 'taskRef and agent are required' };
|
|
@@ -113,6 +156,13 @@ function validateTaskEventRequest(request: TaskEventRequest): TaskEventError | n
|
|
|
113
156
|
if (value !== undefined && (!Number.isInteger(value) || value < (key === 'round' || key === 'question' ? 1 : 0))) return { code: 'EVENT_PAYLOAD_INVALID', message: `'${key}' must be a ${key === 'round' || key === 'question' ? 'positive' : 'non-negative'} integer` };
|
|
114
157
|
}
|
|
115
158
|
if (request.verdict && !['approved', 'changes-requested', 'rejected'].includes(request.verdict)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'verdict is invalid' };
|
|
159
|
+
if (request.initiator && !['human', 'model', 'orchestrator'].includes(request.initiator)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'initiator is invalid' };
|
|
160
|
+
if (request.requestId !== undefined && (!request.requestId.trim() || /[\r\n]/.test(request.requestId))) return { code: 'EVENT_PAYLOAD_INVALID', message: 'requestId must be a non-empty single line' };
|
|
161
|
+
if (request.reasonCode && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'].includes(request.reasonCode)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'reasonCode is invalid' };
|
|
162
|
+
for (const [name, value] of [['sourceFinding', request.sourceFinding], ['sourceArtifact', request.sourceArtifact], ['sourceSha256', request.sourceSha256]] as const) {
|
|
163
|
+
if (value !== undefined && (!value.trim() || /[\r\n]/.test(value))) return { code: 'EVENT_PAYLOAD_INVALID', message: `${name} must be a non-empty single line` };
|
|
164
|
+
}
|
|
165
|
+
if (request.sourceSha256 !== undefined && !/^[a-f0-9]{64}$/.test(request.sourceSha256)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'sourceSha256 must be a sha256 digest' };
|
|
116
166
|
if (request.event === 'code.completed') {
|
|
117
167
|
const fix = request.fixFor !== undefined || ['blockers', 'major', 'minor', 'manualValidation'].some((key) => request[key as keyof TaskEventRequest] !== undefined);
|
|
118
168
|
const modeRequired = fix ? ['fixFor', 'blockers', 'major', 'minor', 'manualValidation'] : ['filesModified', 'testsPassed'];
|
|
@@ -121,20 +171,27 @@ function validateTaskEventRequest(request: TaskEventRequest): TaskEventError | n
|
|
|
121
171
|
}
|
|
122
172
|
if (request.fixFor && !/^review-code(?:-r(?:[2-9]|[1-9]\d+))?\.md$/.test(request.fixFor)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'fixFor must reference a canonical review-code artifact' };
|
|
123
173
|
if (request.implementationInput && !/^II-[1-9]\d*$/.test(request.implementationInput)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'implementationInput must be a canonical II-N id' };
|
|
174
|
+
if (request.artifactSha256 !== undefined && !/^[0-9a-f]{64}$/i.test(request.artifactSha256)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'artifactSha256 must be a 64-character hexadecimal digest' };
|
|
175
|
+
if (request.semanticDigest !== undefined && !/^[0-9a-f]{64}$/i.test(request.semanticDigest)) return { code: 'EVENT_PAYLOAD_INVALID', message: 'semanticDigest must be a 64-character hexadecimal digest' };
|
|
124
176
|
if (request.fixFor && request.implementationInput) return { code: 'EVENT_PAYLOAD_INVALID', message: 'fixFor and implementationInput are mutually exclusive' };
|
|
125
177
|
if (request.summaryResult !== undefined && (!request.summaryResult.trim() || /[\r\n]/.test(request.summaryResult))) return { code: 'EVENT_PAYLOAD_INVALID', message: 'summaryResult must be a non-empty single line' };
|
|
178
|
+
if (request.event !== 'analyze.awaiting-input' && /\.(?:started|completed)$/.test(request.event)) {
|
|
179
|
+
if (!request.initiator || !request.requestId || !request.reasonCode) {
|
|
180
|
+
return { code: 'EVENT_TRIGGER_REQUIRED', message: `${request.event} requires --initiator, --request-id, and --reason-code` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
126
183
|
return null;
|
|
127
184
|
}
|
|
128
185
|
|
|
129
186
|
const FAMILY = {
|
|
130
|
-
analyze: { artifact: 'analysis',
|
|
131
|
-
'review-analysis': { artifact: 'review-analysis',
|
|
132
|
-
plan: { artifact: 'plan',
|
|
133
|
-
'review-plan': { artifact: 'review-plan',
|
|
134
|
-
code: { artifact: 'code',
|
|
135
|
-
'review-code': { artifact: 'review-code',
|
|
136
|
-
'manual-validation': { artifact: 'manual-validation',
|
|
137
|
-
'validation-run': { artifact: 'validation-run',
|
|
187
|
+
analyze: { artifact: 'analysis', target: 'requirement-analysis', label: 'Analyze Task' },
|
|
188
|
+
'review-analysis': { artifact: 'review-analysis', target: 'requirement-analysis-review', label: 'Review Analysis' },
|
|
189
|
+
plan: { artifact: 'plan', target: 'technical-design', label: 'Plan Task' },
|
|
190
|
+
'review-plan': { artifact: 'review-plan', target: 'technical-design-review', label: 'Review Plan' },
|
|
191
|
+
code: { artifact: 'code', target: 'code', label: 'Code Task' },
|
|
192
|
+
'review-code': { artifact: 'review-code', target: 'code-review', label: 'Review Code' },
|
|
193
|
+
'manual-validation': { artifact: 'manual-validation', target: null, label: 'Complete Manual Validation' },
|
|
194
|
+
'validation-run': { artifact: 'validation-run', target: null, label: 'Run Manual Validation' }
|
|
138
195
|
} as const;
|
|
139
196
|
type EventFamily = keyof typeof FAMILY;
|
|
140
197
|
|
|
@@ -290,6 +347,126 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
|
|
|
290
347
|
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
291
348
|
}
|
|
292
349
|
|
|
350
|
+
function lifecycleAction(family: EventFamily): LifecycleAction {
|
|
351
|
+
return family === 'analyze' ? 'analysis' : family;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function eventTrigger(request: TaskEventRequest, family: EventFamily): ExplicitTrigger {
|
|
355
|
+
return {
|
|
356
|
+
initiator: request.initiator ?? (request.orchestrated ? 'orchestrator' : request.agent === 'human' ? 'human' : 'model'),
|
|
357
|
+
requestId: request.requestId ?? `${request.taskRef}:${request.event}:${request.round ?? 1}`,
|
|
358
|
+
requestedAction: lifecycleAction(family),
|
|
359
|
+
reasonCode: request.reasonCode ?? (request.fixFor || request.implementationInput ? 'review-finding' : 'user-request'),
|
|
360
|
+
...(request.sourceFinding ? { sourceFinding: request.sourceFinding } : {}),
|
|
361
|
+
...(request.sourceArtifact ? { sourceArtifact: request.sourceArtifact } : {}),
|
|
362
|
+
...(request.sourceSha256 ? { sourceSha256: request.sourceSha256 } : {}),
|
|
363
|
+
...(request.implementationInput ? { implementationInput: request.implementationInput } : {}),
|
|
364
|
+
explicitRequest: request.requestId !== undefined || request.reasonCode !== undefined || request.initiator !== undefined || request.orchestrated === true
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function invalidationMutationForCompletion(
|
|
369
|
+
content: string,
|
|
370
|
+
taskDir: string,
|
|
371
|
+
family: EventFamily,
|
|
372
|
+
artifact: ArtifactIdentity,
|
|
373
|
+
timestamp: string
|
|
374
|
+
): { mutation: ReturnType<typeof invalidationMutation> | null } | { error: string } {
|
|
375
|
+
if (!['analyze', 'plan', 'code'].includes(family)) return { mutation: null };
|
|
376
|
+
const parsed = parseInvalidationDocument(content);
|
|
377
|
+
if (!parsed.ok) return { error: parsed.message };
|
|
378
|
+
const downstream: Record<'analyze' | 'plan' | 'code', readonly string[]> = {
|
|
379
|
+
analyze: ['review-analysis', 'plan', 'review-plan', 'code', 'review-code'],
|
|
380
|
+
plan: ['review-plan', 'code', 'review-code'],
|
|
381
|
+
code: ['review-code']
|
|
382
|
+
};
|
|
383
|
+
const sourceFamily = family as 'analyze' | 'plan' | 'code';
|
|
384
|
+
let receipts: readonly ArtifactReceipt[] = [];
|
|
385
|
+
try {
|
|
386
|
+
receipts = parseArtifactReceipts(content).rows;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
389
|
+
}
|
|
390
|
+
const targets = downstream[sourceFamily].flatMap((targetFamily: string) => {
|
|
391
|
+
const names = fs.readdirSync(taskDir).filter((name) => {
|
|
392
|
+
const pattern = targetFamily === 'review-analysis' ? /^review-analysis(?:-r\d+)?\.md$/
|
|
393
|
+
: targetFamily === 'review-plan' ? /^review-plan(?:-r\d+)?\.md$/
|
|
394
|
+
: targetFamily === 'review-code' ? /^review-code(?:-r\d+)?\.md$/
|
|
395
|
+
: new RegExp(`^${targetFamily}(?:-r\\d+)?\\.md$`);
|
|
396
|
+
return pattern.test(name);
|
|
397
|
+
});
|
|
398
|
+
return names.flatMap((name) => {
|
|
399
|
+
try {
|
|
400
|
+
const stat = fs.lstatSync(path.join(taskDir, name));
|
|
401
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return [];
|
|
402
|
+
const targetRound = parseArtifactName(name)?.round ?? 1;
|
|
403
|
+
const targetSha256 = sha256File(path.join(taskDir, name));
|
|
404
|
+
const shapes: Array<{ targetKind: InvalidationTargetKind; targetFamily: string; targetArtifact: string; targetRound: number; targetSha256: string }> = [
|
|
405
|
+
{ targetKind: 'artifact', targetFamily, targetArtifact: name, targetRound, targetSha256 }
|
|
406
|
+
];
|
|
407
|
+
const receipt = receipts.find((candidate) => candidate.output === name);
|
|
408
|
+
if (receipt) shapes.push({
|
|
409
|
+
targetKind: 'receipt', targetFamily, targetArtifact: name, targetRound,
|
|
410
|
+
targetSha256: receipt.inputSha256
|
|
411
|
+
});
|
|
412
|
+
if (targetFamily.startsWith('review-')) {
|
|
413
|
+
shapes.push({ targetKind: 'approval', targetFamily, targetArtifact: name, targetRound, targetSha256 });
|
|
414
|
+
}
|
|
415
|
+
if (targetFamily === 'review-code') {
|
|
416
|
+
shapes.push({ targetKind: 'reviewed-snapshot', targetFamily, targetArtifact: name, targetRound, targetSha256 });
|
|
417
|
+
}
|
|
418
|
+
return shapes.map((targetShape) => ({
|
|
419
|
+
...targetShape,
|
|
420
|
+
targetId: targetIdFor('pending', targetShape), operationId: 'pending', status: 'pending' as const,
|
|
421
|
+
reasonCode: 'upstream-replaced', updatedAt: timestamp
|
|
422
|
+
}));
|
|
423
|
+
} catch (error) {
|
|
424
|
+
throw new Error(`cannot inspect invalidation target '${name}': ${error instanceof Error ? error.message : String(error)}`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
if (targets.length === 0) return { mutation: null };
|
|
429
|
+
const source = {
|
|
430
|
+
sourceFamily: lifecycleAction(family), sourceArtifact: artifact.name,
|
|
431
|
+
sourceRound: parseArtifactName(artifact.name)?.round ?? 1, sourceSha256: sha256File(artifact.path),
|
|
432
|
+
createdAt: timestamp, updatedAt: timestamp
|
|
433
|
+
};
|
|
434
|
+
const operation = createInvalidationOperation(source);
|
|
435
|
+
const normalizedTargets = targets.map((target: (typeof targets)[number]) => ({
|
|
436
|
+
...target, operationId: operation.operationId,
|
|
437
|
+
targetId: targetIdFor(operation.operationId, target)
|
|
438
|
+
}));
|
|
439
|
+
const operationWithTotal = createInvalidationOperation(source, normalizedTargets);
|
|
440
|
+
const next = upsertInvalidation(parsed.document, operationWithTotal, normalizedTargets);
|
|
441
|
+
if (!next.ok) return { error: next.message };
|
|
442
|
+
return { mutation: next.changed ? invalidationMutation(content, next.document) : null };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function reworkIntentMutationForCompletion(
|
|
446
|
+
content: string,
|
|
447
|
+
taskDir: string,
|
|
448
|
+
family: EventFamily,
|
|
449
|
+
artifact: ArtifactIdentity,
|
|
450
|
+
timestamp: string
|
|
451
|
+
): { mutation: ReturnType<typeof reworkIntentMutation> | null } | { error: string } {
|
|
452
|
+
const parsed = parseReworkIntentDocument(content);
|
|
453
|
+
if (!parsed.ok) return { error: parsed.message };
|
|
454
|
+
const hash = sha256File(artifact.path);
|
|
455
|
+
let next = parsed.intents;
|
|
456
|
+
if (family === 'analyze' || family === 'plan' || family === 'code') {
|
|
457
|
+
const hashes = Object.fromEntries(fs.readdirSync(taskDir).flatMap((name) => {
|
|
458
|
+
if (!/^review-(?:analysis|plan|code)(?:-r\d+)?\.md$/.test(name)) return [];
|
|
459
|
+
try { return [[name, sha256File(path.join(taskDir, name))]]; }
|
|
460
|
+
catch { return []; }
|
|
461
|
+
}));
|
|
462
|
+
next = consumeReworkIntents(next, family === 'analyze' ? 'analysis' : family, hashes, timestamp).intents;
|
|
463
|
+
}
|
|
464
|
+
if (family === 'review-analysis' || family === 'review-plan' || family === 'review-code') {
|
|
465
|
+
next = supersedeReworkIntents(next, artifact.name, hash, timestamp).intents;
|
|
466
|
+
}
|
|
467
|
+
return { mutation: JSON.stringify(next) === JSON.stringify(parsed.intents) ? null : reworkIntentMutation(content, next) };
|
|
468
|
+
}
|
|
469
|
+
|
|
293
470
|
function buildCompletionReceipt(
|
|
294
471
|
taskDir: string,
|
|
295
472
|
family: EventFamily,
|
|
@@ -407,6 +584,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
407
584
|
const completedRows = matchingRows.filter((item) => item.done);
|
|
408
585
|
const row = manual ? openRows.at(-1) : matchingRows[0];
|
|
409
586
|
let completedArtifact: ArtifactIdentity | null = null;
|
|
587
|
+
let localFinalizationIntent: LocalArtifactFinalizationIntent | null = null;
|
|
410
588
|
if (eventIdentity.phase === 'started' && row) {
|
|
411
589
|
if (row.agent !== normalized.agent) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
412
590
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
|
|
@@ -420,13 +598,81 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
420
598
|
const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
|
|
421
599
|
if (!validated.ok) return failed(normalized, validated.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
422
600
|
completedArtifact = validated.artifact;
|
|
601
|
+
if (eventIdentity.family === 'analyze' || eventIdentity.family === 'plan' || eventIdentity.family === 'code') {
|
|
602
|
+
const localFamily = eventIdentity.family === 'analyze'
|
|
603
|
+
? 'analysis'
|
|
604
|
+
: eventIdentity.family === 'plan' ? 'plan' : 'code';
|
|
605
|
+
let artifactContent: string;
|
|
606
|
+
try { artifactContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
|
|
607
|
+
catch (error) {
|
|
608
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot read ${completedArtifact.name}: ${String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
609
|
+
}
|
|
610
|
+
const validationConfig = localArtifactValidationConfig(resolved.repoRoot, localFamily);
|
|
611
|
+
const local = validateLocalArtifact(artifactContent, {
|
|
612
|
+
family: localFamily,
|
|
613
|
+
requiredSections: validationConfig.requiredSections,
|
|
614
|
+
requiredPatterns: validationConfig.requiredPatterns
|
|
615
|
+
});
|
|
616
|
+
if (!local.ok) {
|
|
617
|
+
return failed(normalized, {
|
|
618
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
619
|
+
message: `LOCAL_ARTIFACT_INVALID: ${local.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')}`
|
|
620
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
621
|
+
}
|
|
622
|
+
const actualSha256 = sha256File(completedArtifact.path);
|
|
623
|
+
if (actualSha256 !== normalized.artifactSha256) {
|
|
624
|
+
return failed(normalized, {
|
|
625
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
626
|
+
message: `artifact SHA-256 does not match finalizer result for ${completedArtifact.name}`
|
|
627
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
628
|
+
}
|
|
629
|
+
if (local.semanticDigest !== normalized.semanticDigest) {
|
|
630
|
+
return failed(normalized, {
|
|
631
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
632
|
+
message: `semantic digest does not match finalizer result for ${completedArtifact.name}`
|
|
633
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
localFinalizationIntent = readLocalArtifactFinalizationIntent(
|
|
637
|
+
resolved.repoRoot, resolved.taskId, localFamily, completedArtifact.name
|
|
638
|
+
);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
return failed(normalized, {
|
|
641
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
642
|
+
message: `cannot read local finalizer provenance for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
643
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
644
|
+
}
|
|
645
|
+
if (!localFinalizationIntent || !['passed', 'consumed'].includes(localFinalizationIntent.state)) {
|
|
646
|
+
return failed(normalized, {
|
|
647
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
648
|
+
message: `local finalizer provenance is missing or incomplete for ${completedArtifact.name}`
|
|
649
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
650
|
+
}
|
|
651
|
+
if (localFinalizationIntent.artifactSha256 !== actualSha256 || localFinalizationIntent.semanticDigest !== local.semanticDigest) {
|
|
652
|
+
return failed(normalized, {
|
|
653
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
654
|
+
message: `local finalizer provenance does not match ${completedArtifact.name}`
|
|
655
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
423
658
|
}
|
|
424
659
|
if (eventIdentity.phase === 'waiting' && section.entries.some((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)) {
|
|
425
660
|
const existing = section.entries.find((entry) => entry.step === eventIdentity.action && entry.note === eventIdentity.note)!;
|
|
426
661
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, existing.time, frontmatter, artifactContext);
|
|
427
662
|
}
|
|
428
|
-
|
|
429
|
-
|
|
663
|
+
if (eventIdentity.phase === 'started') {
|
|
664
|
+
const trigger = eventTrigger(normalized, eventIdentity.family);
|
|
665
|
+
const facts = buildLifecycleFacts(resolved.taskDir, content, resolved.state);
|
|
666
|
+
if (!facts.ok) return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${facts.code}: ${facts.message}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
667
|
+
const capability = canStart(lifecycleAction(eventIdentity.family), facts.facts, trigger);
|
|
668
|
+
const capabilityOverride = capability.reasonCode === 'TASK_NOT_ACTIVE'
|
|
669
|
+
? stateOverride
|
|
670
|
+
: allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID');
|
|
671
|
+
if (!capability.allowed && !capabilityOverride) {
|
|
672
|
+
const code = capability.reasonCode === 'INVALIDATION_INCOMPLETE' ? 'TASK_INVALIDATION_BLOCKED' : 'EVENT_TRANSITION_INVALID';
|
|
673
|
+
return failed(normalized, { code, message: `${capability.reasonCode}: ${capability.evidence.join(', ')}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
430
676
|
const findingCountError = validateReviewFindingCounts(
|
|
431
677
|
normalized,
|
|
432
678
|
content,
|
|
@@ -460,12 +706,24 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
460
706
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
461
707
|
catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
462
708
|
let completionReceipt: ArtifactReceipt | null = null;
|
|
709
|
+
let completionInvalidation: ReturnType<typeof invalidationMutation> | null = null;
|
|
710
|
+
let completionRework: ReturnType<typeof reworkIntentMutation> | null = null;
|
|
463
711
|
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
464
712
|
const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
465
713
|
if (receipt && !receipt.ok) {
|
|
466
714
|
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
467
715
|
}
|
|
468
716
|
completionReceipt = receipt?.receipt ?? null;
|
|
717
|
+
try {
|
|
718
|
+
const invalidation = invalidationMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
|
|
719
|
+
if ('error' in invalidation) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: invalidation.error }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
720
|
+
completionInvalidation = invalidation.mutation;
|
|
721
|
+
const rework = reworkIntentMutationForCompletion(content, resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp);
|
|
722
|
+
if ('error' in rework) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: rework.error }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
723
|
+
completionRework = rework.mutation;
|
|
724
|
+
} catch (error) {
|
|
725
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: error instanceof Error ? error.message : String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
726
|
+
}
|
|
469
727
|
}
|
|
470
728
|
const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
|
|
471
729
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
@@ -513,6 +771,8 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
513
771
|
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
514
772
|
}
|
|
515
773
|
}
|
|
774
|
+
if (completionInvalidation) mutations.push(completionInvalidation);
|
|
775
|
+
if (completionRework) mutations.push(completionRework);
|
|
516
776
|
if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
|
|
517
777
|
let implementationRows;
|
|
518
778
|
try {
|
|
@@ -531,7 +791,27 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
531
791
|
});
|
|
532
792
|
}
|
|
533
793
|
mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
|
|
534
|
-
|
|
794
|
+
if (!normalized.dryRun && localFinalizationIntent && completedArtifact && localFinalizationIntent.state === 'passed') {
|
|
795
|
+
try {
|
|
796
|
+
localFinalizationIntent = consumeLocalArtifactFinalizationIntent(resolved.repoRoot, localFinalizationIntent);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
return failed(normalized, {
|
|
799
|
+
code: 'EVENT_ARTIFACT_CONFLICT',
|
|
800
|
+
message: `local finalizer provenance could not be consumed before task write for ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
801
|
+
}, {
|
|
802
|
+
taskId: resolved.taskId,
|
|
803
|
+
taskMdPath: resolved.taskMdPath,
|
|
804
|
+
fromStep: currentStep,
|
|
805
|
+
toStep: step,
|
|
806
|
+
action: eventIdentity.action,
|
|
807
|
+
phase: eventIdentity.phase,
|
|
808
|
+
artifactContext
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
813
|
+
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
814
|
+
const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
|
|
535
815
|
if (result.status === 'failed') return failed(normalized, result.error, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
|
|
536
816
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
537
817
|
try {
|
package/lib/task/finalization.ts
CHANGED
|
@@ -55,6 +55,7 @@ type TaskFinalizationRequest = Readonly<{
|
|
|
55
55
|
|
|
56
56
|
type TaskFinalizationOptions = Readonly<{
|
|
57
57
|
repoRoot: string;
|
|
58
|
+
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
58
59
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
59
60
|
lifecycle?: typeof applyTaskLifecycle;
|
|
60
61
|
commentSync?: typeof syncPlatformComment;
|
|
@@ -73,6 +74,7 @@ type TaskFinalizationReceipt = Readonly<{
|
|
|
73
74
|
verification: FinalizationStepState;
|
|
74
75
|
warningProjection: WarningProjectionState;
|
|
75
76
|
warnings: readonly FinalizationWarning[];
|
|
77
|
+
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
76
78
|
updatedAt: string;
|
|
77
79
|
lastError: FinalizationError | null;
|
|
78
80
|
}>;
|
|
@@ -146,7 +148,7 @@ function failed(
|
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
function emptyReceipt(taskId: string): TaskFinalizationReceipt {
|
|
151
|
+
function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: string; requestId: string }>): TaskFinalizationReceipt {
|
|
150
152
|
return {
|
|
151
153
|
version: RECEIPT_VERSION,
|
|
152
154
|
taskId,
|
|
@@ -158,6 +160,7 @@ function emptyReceipt(taskId: string): TaskFinalizationReceipt {
|
|
|
158
160
|
verification: 'pending',
|
|
159
161
|
warningProjection: 'done',
|
|
160
162
|
warnings: [],
|
|
163
|
+
...(controlBinding ? { controlBinding } : {}),
|
|
161
164
|
updatedAt: now(),
|
|
162
165
|
lastError: null
|
|
163
166
|
};
|
|
@@ -182,6 +185,7 @@ function validWarning(value: unknown): value is FinalizationWarning {
|
|
|
182
185
|
function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceipt {
|
|
183
186
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('receipt must be an object');
|
|
184
187
|
const receipt = value as Record<string, unknown>;
|
|
188
|
+
const controlBinding = receipt.controlBinding as Record<string, unknown> | null | undefined;
|
|
185
189
|
const states = ['pending', 'done', 'skipped'];
|
|
186
190
|
if (
|
|
187
191
|
receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
|
|
@@ -194,6 +198,10 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
|
|
|
194
198
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
195
199
|
|| typeof receipt.updatedAt !== 'string'
|
|
196
200
|
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
|
|
201
|
+
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
202
|
+
|| Array.isArray(controlBinding)
|
|
203
|
+
|| typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
|
|
204
|
+
|| typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)))
|
|
197
205
|
) throw new Error('receipt schema is invalid');
|
|
198
206
|
return receipt as TaskFinalizationReceipt;
|
|
199
207
|
}
|
|
@@ -205,6 +213,18 @@ function readReceipt(repoRoot: string, taskId: string): TaskFinalizationReceipt
|
|
|
205
213
|
return validateReceipt(value, taskId);
|
|
206
214
|
}
|
|
207
215
|
|
|
216
|
+
function readTaskFinalizationReceipt(repoRoot: string, taskId: string): TaskFinalizationReceipt | null {
|
|
217
|
+
return readReceipt(path.resolve(repoRoot), taskId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function controlBindingMatches(
|
|
221
|
+
receipt: TaskFinalizationReceipt,
|
|
222
|
+
binding: Readonly<{ generation: string; requestId: string }>
|
|
223
|
+
): boolean {
|
|
224
|
+
return receipt.controlBinding?.generation === binding.generation
|
|
225
|
+
&& receipt.controlBinding.requestId === binding.requestId;
|
|
226
|
+
}
|
|
227
|
+
|
|
208
228
|
function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void {
|
|
209
229
|
const directory = finalizationRoot(repoRoot);
|
|
210
230
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -486,14 +506,14 @@ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: T
|
|
|
486
506
|
catch { return receipt; }
|
|
487
507
|
}
|
|
488
508
|
|
|
489
|
-
function syncPendingTaskComment(input: {
|
|
509
|
+
async function syncPendingTaskComment(input: {
|
|
490
510
|
repoRoot: string;
|
|
491
511
|
taskId: string;
|
|
492
512
|
agent: string;
|
|
493
513
|
receipt: TaskFinalizationReceipt;
|
|
494
514
|
commentSync: typeof syncPlatformComment;
|
|
495
515
|
consumedCapabilities: Set<string>;
|
|
496
|
-
}): TaskCommentSyncOutcome {
|
|
516
|
+
}): Promise<TaskCommentSyncOutcome> {
|
|
497
517
|
const { repoRoot, taskId, agent, commentSync, consumedCapabilities } = input;
|
|
498
518
|
let receipt = input.receipt;
|
|
499
519
|
if (receipt.taskComment !== 'pending') {
|
|
@@ -505,7 +525,7 @@ function syncPendingTaskComment(input: {
|
|
|
505
525
|
};
|
|
506
526
|
}
|
|
507
527
|
try {
|
|
508
|
-
const result = commentSync(taskId, { kind: 'task', agent, cwd: repoRoot });
|
|
528
|
+
const result = await commentSync(taskId, { kind: 'task', agent, cwd: repoRoot });
|
|
509
529
|
const step = commentStep(result);
|
|
510
530
|
if (result.status === 'applied' || result.status === 'no-op') {
|
|
511
531
|
const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
|
|
@@ -569,11 +589,11 @@ function terminalResult(
|
|
|
569
589
|
};
|
|
570
590
|
}
|
|
571
591
|
|
|
572
|
-
function applyUnderLock(
|
|
592
|
+
async function applyUnderLock(
|
|
573
593
|
request: TaskFinalizationRequest,
|
|
574
594
|
taskId: string,
|
|
575
595
|
options: TaskFinalizationOptions
|
|
576
|
-
): TaskFinalizationResult {
|
|
596
|
+
): Promise<TaskFinalizationResult> {
|
|
577
597
|
const repoRoot = path.resolve(options.repoRoot);
|
|
578
598
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
579
599
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
@@ -583,16 +603,27 @@ function applyUnderLock(
|
|
|
583
603
|
try {
|
|
584
604
|
const file = receiptPath(repoRoot, taskId);
|
|
585
605
|
const existed = fs.existsSync(file);
|
|
586
|
-
receipt = readReceipt(repoRoot, taskId) ?? emptyReceipt(taskId);
|
|
606
|
+
receipt = readReceipt(repoRoot, taskId) ?? emptyReceipt(taskId, options.controlBinding);
|
|
587
607
|
if (!existed) writeReceipt(repoRoot, receipt);
|
|
588
608
|
} catch (error) {
|
|
589
609
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
|
|
590
610
|
}
|
|
611
|
+
if (options.controlBinding && !controlBindingMatches(receipt, options.controlBinding)) {
|
|
612
|
+
const error: FinalizationError = {
|
|
613
|
+
code: 'TASK_FINALIZATION_CONTROL_BINDING_CONFLICT',
|
|
614
|
+
message: 'finalization receipt belongs to a different sandbox request or generation',
|
|
615
|
+
retryable: false
|
|
616
|
+
};
|
|
617
|
+
return failed(taskId, error, {
|
|
618
|
+
completedSteps: completedSteps(receipt),
|
|
619
|
+
pendingSteps: pendingSteps(receipt)
|
|
620
|
+
});
|
|
621
|
+
}
|
|
591
622
|
|
|
592
623
|
const preflightState = resolveTaskRef(taskId, { repoRoot });
|
|
593
624
|
if (options.preflight && preflightState.ok && preflightState.state === 'active') {
|
|
594
625
|
try {
|
|
595
|
-
const preflight = options.preflight(
|
|
626
|
+
const preflight = await options.preflight(
|
|
596
627
|
{ taskRef: taskId, event: 'complete-task.preflight' },
|
|
597
628
|
{ repoRoot }
|
|
598
629
|
);
|
|
@@ -668,7 +699,7 @@ function applyUnderLock(
|
|
|
668
699
|
|
|
669
700
|
let taskComment: TaskFinalizationStep | null = null;
|
|
670
701
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
671
|
-
const initialComment = syncPendingTaskComment({
|
|
702
|
+
const initialComment = await syncPendingTaskComment({
|
|
672
703
|
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
673
704
|
});
|
|
674
705
|
receipt = initialComment.receipt;
|
|
@@ -684,7 +715,7 @@ function applyUnderLock(
|
|
|
684
715
|
if (receipt.verification !== 'pending') {
|
|
685
716
|
verification = { status: 'no-op', changed: false, error: null };
|
|
686
717
|
} else {
|
|
687
|
-
const result = verify(
|
|
718
|
+
const result = await verify(
|
|
688
719
|
{ taskRef: taskId, event: 'complete-task.completed' },
|
|
689
720
|
{ repoRoot }
|
|
690
721
|
);
|
|
@@ -695,7 +726,7 @@ function applyUnderLock(
|
|
|
695
726
|
scope: 'verification', operation: 'succeeded'
|
|
696
727
|
}, consumedCapabilities);
|
|
697
728
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
698
|
-
const finalComment = syncPendingTaskComment({
|
|
729
|
+
const finalComment = await syncPendingTaskComment({
|
|
699
730
|
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
700
731
|
});
|
|
701
732
|
receipt = finalComment.receipt;
|
|
@@ -729,7 +760,7 @@ function applyUnderLock(
|
|
|
729
760
|
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed);
|
|
730
761
|
}
|
|
731
762
|
|
|
732
|
-
function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): TaskFinalizationResult {
|
|
763
|
+
async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
|
733
764
|
if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
|
|
734
765
|
return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
|
|
735
766
|
}
|
|
@@ -737,7 +768,7 @@ function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFi
|
|
|
737
768
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
738
769
|
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
739
770
|
try {
|
|
740
|
-
return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
|
|
771
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
|
|
741
772
|
} catch (error) {
|
|
742
773
|
const detail = error instanceof TaskExecutionLockError
|
|
743
774
|
? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
|
|
@@ -746,7 +777,12 @@ function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFi
|
|
|
746
777
|
}
|
|
747
778
|
}
|
|
748
779
|
|
|
749
|
-
export {
|
|
780
|
+
export {
|
|
781
|
+
applyFinalizationReceiptMutation,
|
|
782
|
+
applyTaskFinalization,
|
|
783
|
+
issueCapability as createFinalizationCapability,
|
|
784
|
+
readTaskFinalizationReceipt
|
|
785
|
+
};
|
|
750
786
|
export type {
|
|
751
787
|
FinalizationError,
|
|
752
788
|
FinalizationCapability,
|