@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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { declareImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
4
5
|
import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.js';
|
|
5
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
6
7
|
import { writeTask } from './write.js';
|
|
7
8
|
import { allowsManualOverride } from './guard-override.js';
|
|
9
|
+
import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
|
|
8
10
|
const PREFIX = { analysis: 'AN', plan: 'PL', code: 'CD' };
|
|
9
11
|
const RESPONSE = new Set(['accepted', 'adjusted', 'refuted', 'cannot-judge']);
|
|
10
12
|
const DISPOSITION = new Set(['confirmed', 'closed', 'open', 'needs-human-decision']);
|
|
@@ -102,6 +104,41 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
102
104
|
if (intent.kind === 'decision-next-id') {
|
|
103
105
|
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: nextHdId(rows), before: null, after: null, operations: [], error: null };
|
|
104
106
|
}
|
|
107
|
+
if (intent.kind === 'rework-intent-upsert') {
|
|
108
|
+
if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !['analysis', 'plan', 'code'].includes(intent.target) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
|
|
109
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, target, or source hash is invalid', resolved.taskId, intent.intentId);
|
|
110
|
+
}
|
|
111
|
+
const finding = rows.find((row) => row.id === intent.findingId);
|
|
112
|
+
if (!finding || finding.severity === 'decision')
|
|
113
|
+
return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
|
|
114
|
+
const sourceFromEvidence = finding.evidence.split('#')[0];
|
|
115
|
+
if (sourceFromEvidence !== intent.sourceArtifact || !REVIEW_ARTIFACT[finding.stage]?.test(intent.sourceArtifact)) {
|
|
116
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const actualHash = createHash('sha256').update(fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact))).digest('hex');
|
|
120
|
+
if (actualHash !== intent.sourceSha256)
|
|
121
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact hash does not match', resolved.taskId, intent.intentId);
|
|
122
|
+
const parsed = parseReworkIntentDocument(content);
|
|
123
|
+
if (!parsed.ok)
|
|
124
|
+
return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
|
|
125
|
+
const nextIntent = {
|
|
126
|
+
intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
|
|
127
|
+
sourceSha256: intent.sourceSha256, target: intent.target, status: 'pending',
|
|
128
|
+
declaredAt: new Date().toISOString(), consumedAt: ''
|
|
129
|
+
};
|
|
130
|
+
const next = upsertReworkIntent(parsed.intents, nextIntent);
|
|
131
|
+
if (!next.changed)
|
|
132
|
+
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
133
|
+
const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next.intents)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
134
|
+
if (result.status === 'failed')
|
|
135
|
+
return failed(intent, result.error.code, result.error.message, result.taskId, intent.intentId);
|
|
136
|
+
return { status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId: intent.intentId, before: null, after: null, operations: result.operations, error: null };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', error instanceof Error ? error.message : String(error), resolved.taskId, intent.intentId);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
105
142
|
let before = null;
|
|
106
143
|
let after;
|
|
107
144
|
if (intent.kind === 'finding-upsert') {
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { inspectDecisionDetailDuplicates, scanVisibleMarkdown } from './decision-details.js';
|
|
5
|
+
import { parseArtifactName, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
6
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
7
|
+
const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
|
|
8
|
+
analysis: [
|
|
9
|
+
'需求来源', '需求理解', '相关文件', '影响评估', '技术风险',
|
|
10
|
+
'工作量和复杂度评估', '状态核对'
|
|
11
|
+
],
|
|
12
|
+
plan: [
|
|
13
|
+
'问题理解', '约束条件', '方案对比', '技术方法', '实施步骤',
|
|
14
|
+
'文件清单', '验证策略', '状态核对'
|
|
15
|
+
],
|
|
16
|
+
code: [
|
|
17
|
+
'实现输入', '变更文件', '关键代码说明', '测试结果', '与方案的差异',
|
|
18
|
+
'供审查关注的内容', '状态核对', '证据原文'
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
const LOCAL_ARTIFACT_REQUIRED_PATTERNS = ['^\\$ '];
|
|
22
|
+
function finalizationIntentRoot(repoRoot) {
|
|
23
|
+
return path.join(repoRoot, '.agents', 'workspace', '.local-artifact-finalization-intents');
|
|
24
|
+
}
|
|
25
|
+
function finalizationIntentPath(repoRoot, taskId, family, artifact) {
|
|
26
|
+
return path.join(finalizationIntentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
|
|
27
|
+
}
|
|
28
|
+
function isFinalizationIntent(value) {
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
30
|
+
return false;
|
|
31
|
+
const intent = value;
|
|
32
|
+
return intent.version === 1
|
|
33
|
+
&& typeof intent.taskId === 'string' && intent.taskId.length > 0
|
|
34
|
+
&& (intent.family === 'analysis' || intent.family === 'plan' || intent.family === 'code')
|
|
35
|
+
&& typeof intent.artifact === 'string' && intent.artifact.length > 0
|
|
36
|
+
&& (intent.state === 'awaiting-repair' || intent.state === 'passed' || intent.state === 'consumed')
|
|
37
|
+
&& (intent.baselineSemanticDigest === null || (typeof intent.baselineSemanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.baselineSemanticDigest)))
|
|
38
|
+
&& typeof intent.artifactSha256 === 'string' && /^[a-f0-9]{64}$/.test(intent.artifactSha256)
|
|
39
|
+
&& typeof intent.semanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.semanticDigest)
|
|
40
|
+
&& (intent.state === 'awaiting-repair' ? intent.baselineSemanticDigest === intent.semanticDigest : true);
|
|
41
|
+
}
|
|
42
|
+
function readLocalArtifactFinalizationIntent(repoRoot, taskId, family, artifact) {
|
|
43
|
+
const target = finalizationIntentPath(repoRoot, taskId, family, artifact);
|
|
44
|
+
if (!fs.existsSync(target))
|
|
45
|
+
return null;
|
|
46
|
+
const value = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
47
|
+
if (!isFinalizationIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
|
|
48
|
+
throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: finalization provenance schema is invalid');
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function writeLocalArtifactFinalizationIntent(repoRoot, value) {
|
|
53
|
+
if (!isFinalizationIntent(value))
|
|
54
|
+
throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: finalization provenance schema is invalid');
|
|
55
|
+
const directory = finalizationIntentRoot(repoRoot);
|
|
56
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
57
|
+
const target = finalizationIntentPath(repoRoot, value.taskId, value.family, value.artifact);
|
|
58
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
59
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
60
|
+
try {
|
|
61
|
+
fs.renameSync(temporary, target);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(temporary);
|
|
66
|
+
}
|
|
67
|
+
catch { /* preserve primary error */ }
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function consumeLocalArtifactFinalizationIntent(repoRoot, intent) {
|
|
72
|
+
if (intent.state === 'consumed')
|
|
73
|
+
return intent;
|
|
74
|
+
if (intent.state !== 'passed')
|
|
75
|
+
throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: only passed provenance can be consumed');
|
|
76
|
+
const consumed = { ...intent, state: 'consumed' };
|
|
77
|
+
writeLocalArtifactFinalizationIntent(repoRoot, consumed);
|
|
78
|
+
return consumed;
|
|
79
|
+
}
|
|
80
|
+
function requiredSections(family) {
|
|
81
|
+
return LOCAL_ARTIFACT_REQUIRED_SECTIONS[family];
|
|
82
|
+
}
|
|
83
|
+
function lineNumber(content, offset) {
|
|
84
|
+
return content.slice(0, offset).split('\n').length;
|
|
85
|
+
}
|
|
86
|
+
function sectionBody(content, heading) {
|
|
87
|
+
const next = scanVisibleMarkdown(content).headings.find((candidate) => (candidate.start > heading.start && candidate.level <= 2));
|
|
88
|
+
return content.slice(heading.end, next?.start ?? content.length);
|
|
89
|
+
}
|
|
90
|
+
function semanticDigest(content, candidate) {
|
|
91
|
+
let normalized = content;
|
|
92
|
+
if (candidate) {
|
|
93
|
+
const raw = content.slice(candidate.start, candidate.end);
|
|
94
|
+
const marker = raw.match(/([::])\s*$/);
|
|
95
|
+
if (marker?.index !== undefined) {
|
|
96
|
+
const canonical = raw.slice(0, marker.index) + raw.slice(marker.index + 1);
|
|
97
|
+
normalized = `${content.slice(0, candidate.start)}${canonical}${content.slice(candidate.end)}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return createHash('sha256').update(normalized, 'utf8').digest('hex');
|
|
101
|
+
}
|
|
102
|
+
function sha256Content(content) {
|
|
103
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
104
|
+
}
|
|
105
|
+
function visibleSectionHeadings(content, section) {
|
|
106
|
+
return scanVisibleMarkdown(content).headings.filter((heading) => (heading.level === 2 && heading.text === section));
|
|
107
|
+
}
|
|
108
|
+
function trailingPunctuationCandidates(content, section) {
|
|
109
|
+
return scanVisibleMarkdown(content).headings.filter((heading) => (heading.level === 2 && (heading.text === `${section}:` || heading.text === `${section}:`)));
|
|
110
|
+
}
|
|
111
|
+
function diagnostic(code, message, content, heading, extra = {}) {
|
|
112
|
+
return {
|
|
113
|
+
code, message, repairable: false,
|
|
114
|
+
line: heading ? lineNumber(content, heading.start) : null,
|
|
115
|
+
...extra
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function isStatusPattern(pattern) {
|
|
119
|
+
return pattern === '^\\$ ';
|
|
120
|
+
}
|
|
121
|
+
function validateLocalArtifact(content, options) {
|
|
122
|
+
const sections = options.requiredSections ?? requiredSections(options.family);
|
|
123
|
+
const patterns = options.requiredPatterns ?? LOCAL_ARTIFACT_REQUIRED_PATTERNS;
|
|
124
|
+
const diagnostics = [];
|
|
125
|
+
const candidates = [];
|
|
126
|
+
const scanned = scanVisibleMarkdown(content);
|
|
127
|
+
if (!content.trim()) {
|
|
128
|
+
diagnostics.push(diagnostic('LOCAL_ARTIFACT_EMPTY', 'artifact is empty', content));
|
|
129
|
+
}
|
|
130
|
+
for (const section of sections) {
|
|
131
|
+
const exact = scanned.headings.filter((heading) => heading.level === 2 && heading.text === section);
|
|
132
|
+
const punctuated = trailingPunctuationCandidates(content, section);
|
|
133
|
+
if (exact.length > 1 || punctuated.length > 0 && exact.length > 0 || punctuated.length > 1) {
|
|
134
|
+
const first = exact[1] ?? punctuated[0] ?? exact[0];
|
|
135
|
+
diagnostics.push(diagnostic('LOCAL_ARTIFACT_DUPLICATE_SECTION', `required section '${section}' is duplicated or has an ambiguous punctuation variant`, content, first));
|
|
136
|
+
}
|
|
137
|
+
else if (exact.length === 0 && punctuated.length === 1) {
|
|
138
|
+
candidates.push(punctuated[0]);
|
|
139
|
+
}
|
|
140
|
+
else if (exact.length === 0) {
|
|
141
|
+
diagnostics.push(diagnostic('LOCAL_ARTIFACT_MISSING_SECTION', `required section '${section}' is missing`, content));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const statusSection = sections.find((section) => section === '状态核对' || section.toLowerCase() === 'state check');
|
|
145
|
+
if (statusSection) {
|
|
146
|
+
const statusHeading = visibleSectionHeadings(content, statusSection)[0]
|
|
147
|
+
?? candidates.find((heading) => heading.text === `${statusSection}:` || heading.text === `${statusSection}:`);
|
|
148
|
+
if (statusHeading) {
|
|
149
|
+
const body = sectionBody(content, statusHeading);
|
|
150
|
+
for (const pattern of patterns.filter(isStatusPattern)) {
|
|
151
|
+
if (!new RegExp(pattern, 'm').test(body)) {
|
|
152
|
+
diagnostics.push(diagnostic('LOCAL_STATUS_COMMAND_MISSING', `status section '${statusSection}' is missing required command output`, content, statusHeading));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const pattern of patterns.filter((item) => !isStatusPattern(item))) {
|
|
158
|
+
if (!new RegExp(pattern, 'm').test(content)) {
|
|
159
|
+
diagnostics.push(diagnostic('LOCAL_REQUIRED_PATTERN_MISSING', `artifact is missing required pattern '${pattern}'`, content));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
163
|
+
if (!decisionDetails.ok) {
|
|
164
|
+
diagnostics.push(diagnostic('LOCAL_DECISION_DETAIL_DUPLICATE', decisionDetails.message, content, decisionDetails.duplicates[0]?.blocks[0]
|
|
165
|
+
? scanned.headings.find((heading) => heading.start === decisionDetails.duplicates[0].blocks[0].start)
|
|
166
|
+
: undefined));
|
|
167
|
+
}
|
|
168
|
+
if (candidates.length === 1 && diagnostics.length === 0) {
|
|
169
|
+
const candidate = candidates[0];
|
|
170
|
+
const section = candidate.text.slice(0, -1);
|
|
171
|
+
const repair = diagnostic('LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION', `visible required H2 '${candidate.text}' may be normalized to '${section}'`, content, candidate, {
|
|
172
|
+
repairable: true,
|
|
173
|
+
from: candidate.text,
|
|
174
|
+
to: section,
|
|
175
|
+
operation: 'replace-line'
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
family: options.family,
|
|
180
|
+
semanticDigest: semanticDigest(content, candidate),
|
|
181
|
+
repairable: true,
|
|
182
|
+
diagnostics: [repair]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (candidates.length > 0) {
|
|
186
|
+
for (const candidate of candidates) {
|
|
187
|
+
const section = candidate.text.slice(0, -1);
|
|
188
|
+
diagnostics.push(diagnostic('LOCAL_ARTIFACT_MISSING_SECTION', `required section '${section}' is missing and cannot be repaired while other structural defects exist`, content, candidate));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
ok: diagnostics.length === 0,
|
|
193
|
+
family: options.family,
|
|
194
|
+
semanticDigest: semanticDigest(content),
|
|
195
|
+
repairable: false,
|
|
196
|
+
diagnostics
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function failedFinalization(request, error, extra = {}) {
|
|
200
|
+
return {
|
|
201
|
+
status: 'failed', changed: false,
|
|
202
|
+
taskId: null, taskDir: null,
|
|
203
|
+
family: request.family, artifact: request.artifact,
|
|
204
|
+
artifactSha256: null, semanticDigest: null,
|
|
205
|
+
repairable: false, diagnostics: [], error, ...extra
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function provenanceFailure(request, resolved, content, artifactSha256, semanticDigestValue, code, message) {
|
|
209
|
+
return failedFinalization(request, { code, message }, {
|
|
210
|
+
taskId: resolved.taskId,
|
|
211
|
+
taskDir: resolved.taskDir,
|
|
212
|
+
artifactSha256,
|
|
213
|
+
semanticDigest: semanticDigestValue,
|
|
214
|
+
diagnostics: [diagnostic(code, message, content)]
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function finalizeLocalArtifact(request) {
|
|
218
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
|
|
219
|
+
if (!resolved.ok)
|
|
220
|
+
return failedFinalization(request, { code: resolved.code, message: resolved.message });
|
|
221
|
+
const parsed = parseArtifactName(request.artifact);
|
|
222
|
+
if (!parsed || parsed.family !== request.family) {
|
|
223
|
+
return failedFinalization(request, {
|
|
224
|
+
code: 'ARTIFACT_IDENTITY_INVALID',
|
|
225
|
+
message: `artifact '${request.artifact}' does not match ${request.family}`
|
|
226
|
+
}, { taskId: resolved.taskId, taskDir: resolved.taskDir });
|
|
227
|
+
}
|
|
228
|
+
const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
|
|
229
|
+
if (!validated.ok) {
|
|
230
|
+
return failedFinalization(request, validated.error, { taskId: resolved.taskId, taskDir: resolved.taskDir });
|
|
231
|
+
}
|
|
232
|
+
let content;
|
|
233
|
+
try {
|
|
234
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return failedFinalization(request, { code: 'ARTIFACT_NOT_READABLE', message: String(error) }, {
|
|
238
|
+
taskId: resolved.taskId, taskDir: resolved.taskDir
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const result = validateLocalArtifact(content, {
|
|
242
|
+
family: request.family,
|
|
243
|
+
requiredSections: request.requiredSections,
|
|
244
|
+
requiredPatterns: request.requiredPatterns
|
|
245
|
+
});
|
|
246
|
+
const artifactSha256 = sha256Content(content);
|
|
247
|
+
let intent;
|
|
248
|
+
try {
|
|
249
|
+
intent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
return failedFinalization(request, {
|
|
253
|
+
code: 'LOCAL_FINALIZATION_INTENT_INVALID',
|
|
254
|
+
message: error instanceof Error ? error.message : String(error)
|
|
255
|
+
}, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
|
|
256
|
+
}
|
|
257
|
+
if (result.repairable) {
|
|
258
|
+
if (intent && (intent.state !== 'awaiting-repair' || intent.baselineSemanticDigest !== result.semanticDigest)) {
|
|
259
|
+
return provenanceFailure(request, resolved, content, artifactSha256, result.semanticDigest, 'LOCAL_REPAIR_PROVENANCE_CONFLICT', 'a different local repair baseline is already recorded for this artifact');
|
|
260
|
+
}
|
|
261
|
+
if (!intent) {
|
|
262
|
+
try {
|
|
263
|
+
writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
|
|
264
|
+
version: 1,
|
|
265
|
+
taskId: resolved.taskId,
|
|
266
|
+
family: request.family,
|
|
267
|
+
artifact: request.artifact,
|
|
268
|
+
state: 'awaiting-repair',
|
|
269
|
+
baselineSemanticDigest: result.semanticDigest,
|
|
270
|
+
artifactSha256,
|
|
271
|
+
semanticDigest: result.semanticDigest
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return failedFinalization(request, {
|
|
276
|
+
code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
|
|
277
|
+
message: error instanceof Error ? error.message : String(error)
|
|
278
|
+
}, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
status: 'failed',
|
|
283
|
+
changed: false,
|
|
284
|
+
taskId: resolved.taskId,
|
|
285
|
+
taskDir: resolved.taskDir,
|
|
286
|
+
family: request.family,
|
|
287
|
+
artifact: request.artifact,
|
|
288
|
+
artifactSha256,
|
|
289
|
+
semanticDigest: result.semanticDigest,
|
|
290
|
+
repairable: true,
|
|
291
|
+
diagnostics: result.diagnostics,
|
|
292
|
+
error: {
|
|
293
|
+
code: 'LOCAL_ARTIFACT_INVALID',
|
|
294
|
+
message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (!result.ok) {
|
|
299
|
+
return {
|
|
300
|
+
status: 'failed',
|
|
301
|
+
changed: false,
|
|
302
|
+
taskId: resolved.taskId,
|
|
303
|
+
taskDir: resolved.taskDir,
|
|
304
|
+
family: request.family,
|
|
305
|
+
artifact: request.artifact,
|
|
306
|
+
artifactSha256,
|
|
307
|
+
semanticDigest: result.semanticDigest,
|
|
308
|
+
repairable: false,
|
|
309
|
+
diagnostics: result.diagnostics,
|
|
310
|
+
error: {
|
|
311
|
+
code: 'LOCAL_ARTIFACT_INVALID',
|
|
312
|
+
message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== result.semanticDigest) {
|
|
317
|
+
return provenanceFailure(request, resolved, content, artifactSha256, result.semanticDigest, 'LOCAL_REPAIR_BASELINE_MISMATCH', 'the repaired artifact semantic digest does not match the recorded repair baseline');
|
|
318
|
+
}
|
|
319
|
+
if ((intent?.state === 'passed' || intent?.state === 'consumed')
|
|
320
|
+
&& (intent.artifactSha256 !== artifactSha256 || intent.semanticDigest !== result.semanticDigest)) {
|
|
321
|
+
return provenanceFailure(request, resolved, content, artifactSha256, result.semanticDigest, 'LOCAL_REPAIR_PROVENANCE_CONFLICT', 'the artifact changed after its finalization provenance was recorded');
|
|
322
|
+
}
|
|
323
|
+
if (intent?.state === 'consumed') {
|
|
324
|
+
return {
|
|
325
|
+
status: 'passed',
|
|
326
|
+
changed: false,
|
|
327
|
+
taskId: resolved.taskId,
|
|
328
|
+
taskDir: resolved.taskDir,
|
|
329
|
+
family: request.family,
|
|
330
|
+
artifact: request.artifact,
|
|
331
|
+
artifactSha256,
|
|
332
|
+
semanticDigest: result.semanticDigest,
|
|
333
|
+
repairable: false,
|
|
334
|
+
diagnostics: result.diagnostics,
|
|
335
|
+
error: null
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
|
|
340
|
+
version: 1,
|
|
341
|
+
taskId: resolved.taskId,
|
|
342
|
+
family: request.family,
|
|
343
|
+
artifact: request.artifact,
|
|
344
|
+
state: 'passed',
|
|
345
|
+
baselineSemanticDigest: intent?.baselineSemanticDigest ?? null,
|
|
346
|
+
artifactSha256,
|
|
347
|
+
semanticDigest: result.semanticDigest
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return failedFinalization(request, {
|
|
352
|
+
code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
|
|
353
|
+
message: error instanceof Error ? error.message : String(error)
|
|
354
|
+
}, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
status: result.ok ? 'passed' : 'failed',
|
|
358
|
+
changed: false,
|
|
359
|
+
taskId: resolved.taskId,
|
|
360
|
+
taskDir: resolved.taskDir,
|
|
361
|
+
family: request.family,
|
|
362
|
+
artifact: request.artifact,
|
|
363
|
+
artifactSha256,
|
|
364
|
+
semanticDigest: result.semanticDigest,
|
|
365
|
+
repairable: result.repairable,
|
|
366
|
+
diagnostics: result.diagnostics,
|
|
367
|
+
error: result.ok ? null : {
|
|
368
|
+
code: 'LOCAL_ARTIFACT_INVALID',
|
|
369
|
+
message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
export { LOCAL_ARTIFACT_REQUIRED_PATTERNS, LOCAL_ARTIFACT_REQUIRED_SECTIONS, consumeLocalArtifactFinalizationIntent, finalizeLocalArtifact, readLocalArtifactFinalizationIntent, semanticDigest, sha256Content, validateLocalArtifact };
|
|
374
|
+
//# sourceMappingURL=local-artifact-finalization.js.map
|
|
@@ -3,8 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
5
5
|
import { validateCurrentTaskContract } from './current-contract.js';
|
|
6
|
-
import { parseReviewSummary
|
|
7
|
-
import { inspectArtifactDirectory } from './artifact-lifecycle.js';
|
|
6
|
+
import { parseReviewSummary } from './review-artifacts.js';
|
|
8
7
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
9
8
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
10
9
|
import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
|
|
@@ -18,6 +17,9 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
18
17
|
import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
19
18
|
import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead } from './review-fingerprint.js';
|
|
20
19
|
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.js';
|
|
20
|
+
import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.js';
|
|
21
|
+
import { hasOpenLifecycleExecution } from './activity-log.js';
|
|
22
|
+
import { invalidationBlocks } from './invalidation.js';
|
|
21
23
|
function supportsLifecycleDelegation(client) {
|
|
22
24
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
23
25
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -377,86 +379,21 @@ function validateSavedCurrentDiffBase(repoRoot, metadata, taskDir, reviewedHead)
|
|
|
377
379
|
}
|
|
378
380
|
return null;
|
|
379
381
|
}
|
|
380
|
-
function
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
catch (error) {
|
|
397
|
-
return { approved: false, error: String(error) };
|
|
398
|
-
}
|
|
399
|
-
const parsed = parseReviewSummary(content);
|
|
400
|
-
if (!parsed.ok)
|
|
401
|
-
return { approved: false, error: `${parsed.code}: ${parsed.message}` };
|
|
402
|
-
const verdict = resolveCanonicalVerdict(parsed.summary);
|
|
403
|
-
if (!verdict.ok)
|
|
404
|
-
return { approved: false, error: `${verdict.code}: ${verdict.message}` };
|
|
405
|
-
return { approved: verdict.verdict === 'Approved', error: null };
|
|
406
|
-
}
|
|
407
|
-
function routeFromFacts(taskDir) {
|
|
408
|
-
const analysisRound = highestRound(taskDir, 'analysis');
|
|
409
|
-
const analysisReviewRound = highestRound(taskDir, 'review-analysis');
|
|
410
|
-
const planRound = highestRound(taskDir, 'plan');
|
|
411
|
-
const planReviewRound = highestRound(taskDir, 'review-plan');
|
|
412
|
-
const codeRound = highestRound(taskDir, 'code');
|
|
413
|
-
const codeReviewRound = highestRound(taskDir, 'review-code');
|
|
414
|
-
if (analysisRound === 0) {
|
|
415
|
-
const round = analysisRound + 1;
|
|
416
|
-
return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
|
|
417
|
-
}
|
|
418
|
-
if (!hasReviewAfterArtifact(taskDir, 'analysis', 'review-analysis')) {
|
|
419
|
-
const round = analysisReviewRound + 1;
|
|
420
|
-
return { action: 'review-analysis', role: 'reviewer', stage: 'review-analysis', round, artifact: artifactName('review-analysis', round) };
|
|
421
|
-
}
|
|
422
|
-
const analysisVerdict = latestReviewApproved(taskDir, 'review-analysis');
|
|
423
|
-
if (analysisVerdict.error)
|
|
424
|
-
return { error: analysisVerdict.error };
|
|
425
|
-
if (!analysisVerdict.approved) {
|
|
426
|
-
const round = analysisRound + 1;
|
|
427
|
-
return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
|
|
428
|
-
}
|
|
429
|
-
if (planRound === 0) {
|
|
430
|
-
const round = planRound + 1;
|
|
431
|
-
return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
|
|
432
|
-
}
|
|
433
|
-
if (!hasReviewAfterArtifact(taskDir, 'plan', 'review-plan')) {
|
|
434
|
-
const round = planReviewRound + 1;
|
|
435
|
-
return { action: 'review-plan', role: 'reviewer', stage: 'review-plan', round, artifact: artifactName('review-plan', round) };
|
|
436
|
-
}
|
|
437
|
-
const planVerdict = latestReviewApproved(taskDir, 'review-plan');
|
|
438
|
-
if (planVerdict.error)
|
|
439
|
-
return { error: planVerdict.error };
|
|
440
|
-
if (!planVerdict.approved) {
|
|
441
|
-
const round = planRound + 1;
|
|
442
|
-
return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
|
|
443
|
-
}
|
|
444
|
-
if (codeRound === 0) {
|
|
445
|
-
const round = codeRound + 1;
|
|
446
|
-
return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
|
|
447
|
-
}
|
|
448
|
-
if (!hasReviewAfterArtifact(taskDir, 'code', 'review-code')) {
|
|
449
|
-
const round = codeReviewRound + 1;
|
|
450
|
-
return { action: 'review-code', role: 'reviewer', stage: 'review-code', round, artifact: artifactName('review-code', round) };
|
|
451
|
-
}
|
|
452
|
-
const codeVerdict = latestReviewApproved(taskDir, 'review-code');
|
|
453
|
-
if (codeVerdict.error)
|
|
454
|
-
return { error: codeVerdict.error };
|
|
455
|
-
if (!codeVerdict.approved) {
|
|
456
|
-
const round = codeRound + 1;
|
|
457
|
-
return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
|
|
458
|
-
}
|
|
459
|
-
return { action: 'commit', role: 'executor', stage: 'commit', round: 1, artifact: 'commit' };
|
|
382
|
+
function routeFromFacts(facts) {
|
|
383
|
+
const recommendation = recommendNext(facts);
|
|
384
|
+
if (!recommendation.action)
|
|
385
|
+
return { completion: true };
|
|
386
|
+
const action = recommendation.action;
|
|
387
|
+
if (action === 'manual-validation' || action === 'validation-run')
|
|
388
|
+
return null;
|
|
389
|
+
const family = action === 'analysis' ? 'analysis' : action;
|
|
390
|
+
const round = Math.max(0, ...[...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])].map((name) => {
|
|
391
|
+
const match = /-r(\d+)\.md$/.exec(name);
|
|
392
|
+
return match ? Number(match[1]) : 1;
|
|
393
|
+
})) + 1;
|
|
394
|
+
const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
|
|
395
|
+
const role = action.startsWith('review-') ? 'reviewer' : 'executor';
|
|
396
|
+
return { action: command, role, stage: action, round, artifact: artifactName(family, round) };
|
|
460
397
|
}
|
|
461
398
|
function routeOrchestration(taskRef, options = {}) {
|
|
462
399
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
@@ -478,12 +415,14 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
478
415
|
}
|
|
479
416
|
let metadata;
|
|
480
417
|
let currentLedger = null;
|
|
418
|
+
let currentInvalidation = { operations: [], targets: [] };
|
|
481
419
|
if (resolved.state === 'active') {
|
|
482
420
|
const contract = validateCurrentTaskContract(content);
|
|
483
421
|
if (!contract.ok)
|
|
484
422
|
return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
|
|
485
423
|
metadata = contract.metadata;
|
|
486
424
|
currentLedger = contract.ledger;
|
|
425
|
+
currentInvalidation = contract.invalidation;
|
|
487
426
|
}
|
|
488
427
|
else {
|
|
489
428
|
try {
|
|
@@ -493,19 +432,23 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
493
432
|
return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
494
433
|
}
|
|
495
434
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
435
|
+
if (invalidationBlocks(currentInvalidation)) {
|
|
436
|
+
return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
|
|
437
|
+
}
|
|
438
|
+
let run;
|
|
439
|
+
try {
|
|
440
|
+
run = readRun(resolved.taskDir);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
444
|
+
}
|
|
445
|
+
const facts = buildLifecycleFacts(resolved.taskDir, content, resolved.state, hasOpenLifecycleExecution(content) || Boolean(run?.pendingDelegation));
|
|
446
|
+
if (!facts.ok)
|
|
447
|
+
return failed('ORCHESTRATION_CAPABILITY_FACTS_INVALID', facts.message, resolved.taskId);
|
|
448
|
+
const routed = routeFromFacts(facts.facts);
|
|
499
449
|
if (!routed)
|
|
500
450
|
return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
|
|
501
|
-
|
|
502
|
-
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
503
|
-
const next = {
|
|
504
|
-
...routed,
|
|
505
|
-
requestedModel: policy?.model ?? null,
|
|
506
|
-
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
507
|
-
};
|
|
508
|
-
if (next.stage === 'commit') {
|
|
451
|
+
if ('completion' in routed) {
|
|
509
452
|
const reviewRound = highestRound(resolved.taskDir, 'review-code');
|
|
510
453
|
const review = parseReviewSummary(fs.readFileSync(path.join(resolved.taskDir, artifactName('review-code', reviewRound)), 'utf8'));
|
|
511
454
|
if (!review.ok || review.summary.manualValidation === null) {
|
|
@@ -581,6 +524,19 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
581
524
|
saveRun(resolved.taskDir, completed);
|
|
582
525
|
return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
|
|
583
526
|
}
|
|
527
|
+
const action = routed.stage;
|
|
528
|
+
const capability = canStart(action, facts.facts, {
|
|
529
|
+
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${routed.round}`,
|
|
530
|
+
requestedAction: action, reasonCode: 'user-request', explicitRequest: true
|
|
531
|
+
});
|
|
532
|
+
if (!capability.allowed)
|
|
533
|
+
return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
|
|
534
|
+
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
535
|
+
const next = {
|
|
536
|
+
...routed,
|
|
537
|
+
requestedModel: policy?.model ?? null,
|
|
538
|
+
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
539
|
+
};
|
|
584
540
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
585
541
|
}
|
|
586
542
|
function statusOrchestration(taskRef, options = {}) {
|