@fitlab-ai/agent-infra 0.9.11 → 0.9.12
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 +6 -6
- package/README.zh-CN.md +6 -6
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/git/workflow.js +43 -1
- package/dist/lib/init.js +1 -0
- package/dist/lib/internal/git-workflow.js +8 -1
- package/dist/lib/internal/platform-pr.js +8 -2
- package/dist/lib/internal/task-delivery.js +59 -0
- package/dist/lib/platform/github-client.js +7 -1
- package/dist/lib/platform/pr-checks.js +8 -5
- package/dist/lib/platform/pr-summary.js +12 -8
- package/dist/lib/platform/pull-requests.js +393 -91
- package/dist/lib/platform/verification-required.js +13 -2
- package/dist/lib/platform/verification-sync.js +9 -4
- package/dist/lib/pr-review/evidence-grading.js +12 -4
- package/dist/lib/sandbox/config.js +5 -0
- package/dist/lib/sandbox/control/lifecycle.js +29 -7
- package/dist/lib/server/process-state.js +2 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -2
- package/dist/lib/task/commit-intent.js +100 -0
- package/dist/lib/task/commit-operation.js +195 -101
- package/dist/lib/task/create.js +33 -7
- package/dist/lib/task/decision-details.js +3 -48
- package/dist/lib/task/delivery-target.js +97 -0
- package/dist/lib/task/delivery.js +153 -0
- package/dist/lib/task/human-override.js +5 -2
- package/dist/lib/task/lifecycle.js +106 -9
- package/dist/lib/task/orchestration.js +55 -62
- package/dist/lib/task/pr-delivery-fact.js +182 -0
- package/dist/lib/task/review-finalization.js +4 -8
- package/dist/lib/task/review-fingerprint.js +8 -0
- package/dist/lib/task/verification-engine.js +48 -24
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/update.js +9 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
- package/lib/defaults.json +4 -0
- package/lib/git/workflow.ts +38 -1
- package/lib/init.ts +3 -0
- package/lib/internal/git-workflow.ts +7 -1
- package/lib/internal/platform-pr.ts +8 -1
- package/lib/internal/task-delivery.ts +53 -0
- package/lib/platform/github-client.ts +8 -1
- package/lib/platform/pr-checks.ts +7 -5
- package/lib/platform/pr-summary.ts +10 -7
- package/lib/platform/pull-requests.ts +425 -87
- package/lib/platform/verification-required.ts +12 -2
- package/lib/platform/verification-sync.ts +9 -4
- package/lib/pr-review/evidence-grading.ts +7 -4
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/control/lifecycle.ts +30 -8
- package/lib/server/process-state.ts +2 -2
- package/lib/task/artifact-lifecycle.ts +2 -2
- package/lib/task/commit-intent.ts +137 -0
- package/lib/task/commit-operation.ts +174 -87
- package/lib/task/create.ts +34 -9
- package/lib/task/decision-details.ts +2 -62
- package/lib/task/delivery-target.ts +130 -0
- package/lib/task/delivery.ts +160 -0
- package/lib/task/human-override.ts +5 -2
- package/lib/task/lifecycle.ts +93 -4
- package/lib/task/orchestration.ts +62 -65
- package/lib/task/pr-delivery-fact.ts +254 -0
- package/lib/task/review-finalization.ts +4 -14
- package/lib/task/review-fingerprint.ts +10 -0
- package/lib/task/verification-engine.ts +45 -28
- package/lib/task/verification-review.ts +2 -0
- package/lib/update.ts +10 -0
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +4 -2
- package/templates/.agents/rules/README.zh-CN.md +4 -2
- package/templates/.agents/rules/create-issue.en.md +11 -2
- package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
- package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
- package/templates/.agents/rules/issue-sync.en.md +26 -5
- package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
- package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
- package/templates/.agents/rules/milestone-inference.en.md +12 -2
- package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
- package/templates/.agents/rules/pr-sync.en.md +22 -13
- package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
- package/templates/.agents/rules/release-commands.en.md +42 -3
- package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
- package/templates/.agents/rules/security-alerts.en.md +17 -2
- package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
- package/templates/.agents/rules/sync-content-generation.en.md +8 -0
- package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/security-alerts.github.sh +151 -0
- package/templates/.agents/scripts/security-alerts.sh +31 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/commit/SKILL.en.md +3 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/config/verify.json +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/templates/task.en.md +5 -1
- package/templates/.agents/templates/task.zh-CN.md +5 -1
- package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/feature-development.en.yaml +9 -8
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/refactoring.en.yaml +9 -8
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
- package/templates/.agents/rules/create-issue.github.en.md +0 -14
- package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
- package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
- package/templates/.agents/rules/issue-sync.github.en.md +0 -40
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
- package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
- package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
- package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
- package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
- package/templates/.agents/rules/pr-sync.github.en.md +0 -43
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
- package/templates/.agents/rules/release-commands.github.en.md +0 -44
- package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
- package/templates/.agents/rules/security-alerts.github.en.md +0 -43
- package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
- /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
- /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
|
@@ -11,10 +11,13 @@ import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
|
|
|
11
11
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
12
12
|
import type { TaskMutation } from './write.ts';
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
checkpointIntentDigest,
|
|
15
|
+
readCheckpointIntent,
|
|
16
|
+
removeCheckpointIntent,
|
|
17
|
+
sameCheckpointIntent,
|
|
18
|
+
updateCheckpointIntent,
|
|
19
|
+
writeCheckpointIntent
|
|
20
|
+
} from './commit-intent.ts';
|
|
18
21
|
|
|
19
22
|
type CommitExecutionMode = 'direct' | 'orchestrated';
|
|
20
23
|
|
|
@@ -27,7 +30,13 @@ type CommitOperationInput = Readonly<{
|
|
|
27
30
|
taskRef?: string;
|
|
28
31
|
agent?: string;
|
|
29
32
|
mode?: CommitExecutionMode;
|
|
30
|
-
|
|
33
|
+
round?: number;
|
|
34
|
+
delivery?: Readonly<{
|
|
35
|
+
mode: 'local' | 'push';
|
|
36
|
+
remote: string;
|
|
37
|
+
refs: readonly string[];
|
|
38
|
+
}> | Readonly<{ mode: 'local' }>;
|
|
39
|
+
push?: Readonly<{
|
|
31
40
|
remote: string;
|
|
32
41
|
refs: readonly string[];
|
|
33
42
|
}>;
|
|
@@ -55,6 +64,7 @@ type BoundTask = Readonly<{
|
|
|
55
64
|
}>;
|
|
56
65
|
|
|
57
66
|
const HEAD_REF = /^refs\/heads\/(.+)$/;
|
|
67
|
+
type CommitDelivery = Readonly<{ mode: 'local' } | { mode: 'push'; remote: string; refs: readonly string[] }>;
|
|
58
68
|
|
|
59
69
|
function isCommitExecutionMode(value: unknown): value is CommitExecutionMode {
|
|
60
70
|
return value === 'direct' || value === 'orchestrated';
|
|
@@ -68,6 +78,12 @@ function gitText(repoRoot: string, args: readonly string[]): string {
|
|
|
68
78
|
}).trim();
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
function effectiveDelivery(input: CommitOperationInput): CommitDelivery | null {
|
|
82
|
+
if (input.delivery) return input.delivery;
|
|
83
|
+
if (input.push) return { mode: 'push', remote: input.push.remote, refs: input.push.refs };
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
function canonicalRepositoryRoot(cwd: string): string {
|
|
72
88
|
return gitText(cwd, ['rev-parse', '--show-toplevel']);
|
|
73
89
|
}
|
|
@@ -156,7 +172,8 @@ function validateCommon(input: CommitOperationInput, branch: string | null): Rea
|
|
|
156
172
|
if (!input.message.trim()) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit message is required' };
|
|
157
173
|
if (!/^[a-f0-9]{40,64}$/.test(input.expectedHead)) return { code: 'GIT_HEAD_EXPECTATION_REQUIRED', message: 'A valid expected HEAD is required' };
|
|
158
174
|
if (!/^[a-f0-9]{40,64}$/.test(input.expectedTree)) return { code: 'GIT_TREE_EXPECTATION_REQUIRED', message: 'A valid expected tree is required' };
|
|
159
|
-
|
|
175
|
+
const delivery = effectiveDelivery(input);
|
|
176
|
+
if (!delivery) return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Commit delivery requires a delivery policy' };
|
|
160
177
|
if (input.paths.some((candidate) => !candidate || candidate.startsWith('/') || candidate.split(/[\\/]/).includes('..'))) {
|
|
161
178
|
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit paths must stay inside the repository' };
|
|
162
179
|
}
|
|
@@ -164,42 +181,16 @@ function validateCommon(input: CommitOperationInput, branch: string | null): Rea
|
|
|
164
181
|
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Sensitive paths cannot be committed' };
|
|
165
182
|
}
|
|
166
183
|
if (branch !== null && branch === '') return { code: 'GIT_BRANCH_INVALID', message: 'A named branch is required' };
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
184
|
+
if (delivery.mode === 'push') {
|
|
185
|
+
if (!delivery.remote || delivery.refs.length !== 1) return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push requires one remote and one ref' };
|
|
186
|
+
const ref = delivery.refs[0] ?? '';
|
|
187
|
+
if (!HEAD_REF.test(ref) || HEAD_REF.exec(ref)?.[1] !== branch) {
|
|
188
|
+
return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push ref must be the current full heads ref' };
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
191
|
return null;
|
|
173
192
|
}
|
|
174
193
|
|
|
175
|
-
function validateOrchestrated(
|
|
176
|
-
input: CommitOperationInput,
|
|
177
|
-
task: BoundTask
|
|
178
|
-
): { plan: OrchestrationStageCompletion } | { error: Readonly<{ code: string; message: string }> } {
|
|
179
|
-
if (!input.agent) return { error: { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'orchestrated commit requires an agent' } };
|
|
180
|
-
const planned = planOrchestrationStageCompletion(task.taskId, {
|
|
181
|
-
stage: 'commit',
|
|
182
|
-
round: 1,
|
|
183
|
-
artifact: 'commit',
|
|
184
|
-
role: 'executor',
|
|
185
|
-
agent: input.agent
|
|
186
|
-
}, { repoRoot: task.repoRoot });
|
|
187
|
-
const run = planned.result.run;
|
|
188
|
-
if (
|
|
189
|
-
!planned.plan
|
|
190
|
-
|| !run
|
|
191
|
-
|| run.status !== 'running'
|
|
192
|
-
|| !run.commitAuthorization.issuedAt
|
|
193
|
-
|| run.commitAuthorization.consumedAt
|
|
194
|
-
) return {
|
|
195
|
-
error: {
|
|
196
|
-
code: planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
197
|
-
message: planned.result.error?.message ?? 'orchestrated commit requires one matching activated commit delegation'
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
return { plan: planned.plan };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
194
|
function syncTaskCommit(
|
|
204
195
|
input: CommitOperationInput,
|
|
205
196
|
task: BoundTask,
|
|
@@ -227,7 +218,10 @@ function syncTaskCommit(
|
|
|
227
218
|
heading: activity.heading,
|
|
228
219
|
body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Commit', agent: input.agent!, note: commitNote })
|
|
229
220
|
});
|
|
230
|
-
const frontmatter: Record<string, string> = {
|
|
221
|
+
const frontmatter: Record<string, string> = {
|
|
222
|
+
assigned_to: input.agent!,
|
|
223
|
+
checkpoint_commit: committedHead
|
|
224
|
+
};
|
|
231
225
|
mutations.push({ kind: 'frontmatter', set: frontmatter });
|
|
232
226
|
if (mutations.length === 0) return null;
|
|
233
227
|
const written = writeTask({
|
|
@@ -256,29 +250,133 @@ function syncTaskCommit(
|
|
|
256
250
|
}
|
|
257
251
|
}
|
|
258
252
|
|
|
253
|
+
function checkpointChildMatches(task: BoundTask, intent: NonNullable<ReturnType<typeof readCheckpointIntent>>, head: string): boolean {
|
|
254
|
+
try {
|
|
255
|
+
const parent = gitText(task.repoRoot, ['rev-parse', `${head}^`]);
|
|
256
|
+
const tree = gitText(task.repoRoot, ['rev-parse', `${head}^{tree}`]);
|
|
257
|
+
const message = gitText(task.repoRoot, ['show', '-s', '--format=%s', head]);
|
|
258
|
+
const changed = gitText(task.repoRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', head]).split('\n').filter(Boolean).sort();
|
|
259
|
+
return parent === intent.expectedHead
|
|
260
|
+
&& tree === intent.expectedTree
|
|
261
|
+
&& message === intent.message
|
|
262
|
+
&& JSON.stringify(changed) === JSON.stringify([...intent.paths].sort());
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function checkpointIdentity(input: CommitOperationInput, task: BoundTask, delivery: CommitDelivery): Parameters<typeof checkpointIntentDigest>[0] {
|
|
269
|
+
if (delivery.mode !== 'local') throw new Error('checkpoint identity requires local delivery');
|
|
270
|
+
return {
|
|
271
|
+
taskId: task.taskId,
|
|
272
|
+
branch: task.branch,
|
|
273
|
+
mode: 'local',
|
|
274
|
+
expectedHead: input.expectedHead,
|
|
275
|
+
expectedTree: input.expectedTree,
|
|
276
|
+
paths: [...input.paths],
|
|
277
|
+
message: input.message,
|
|
278
|
+
round: input.round ?? 1
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function checkpointTimestamp(): string {
|
|
283
|
+
return new Date().toISOString();
|
|
284
|
+
}
|
|
285
|
+
|
|
259
286
|
function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
|
|
260
287
|
const inspected = inspectGitWorkflow(input.cwd);
|
|
261
288
|
if (!inspected.snapshot) return failure(input, mode, task?.taskId ?? null, 'GIT_INSPECT_FAILED', 'Unable to inspect Git repository');
|
|
289
|
+
if (mode === 'orchestrated') return failure(
|
|
290
|
+
input,
|
|
291
|
+
mode,
|
|
292
|
+
task?.taskId ?? null,
|
|
293
|
+
'ORCHESTRATED_COMMIT_REMOVED',
|
|
294
|
+
'Orchestrated commit is no longer a lifecycle stage; use local checkpoint delivery from code-task',
|
|
295
|
+
'blocked',
|
|
296
|
+
inspected.snapshot
|
|
297
|
+
);
|
|
262
298
|
const branch = inspected.snapshot.branch;
|
|
263
299
|
const commonError = validateCommon(input, branch);
|
|
264
300
|
if (commonError) return failure(input, mode, task?.taskId ?? null, commonError.code, commonError.message, 'blocked', inspected.snapshot);
|
|
265
301
|
if (task && task.state !== 'active') return failure(input, mode, task.taskId, 'TASK_STATE_INVALID', 'task-bound commit requires an active task', 'blocked', inspected.snapshot);
|
|
266
302
|
if (task && task.branch !== branch) return failure(input, mode, task.taskId, 'GIT_BRANCH_MISMATCH', `Task branch '${task.branch}' does not match current branch '${branch}'`, 'blocked', inspected.snapshot);
|
|
267
303
|
if (task && !input.agent) return failure(input, mode, task.taskId, 'COMMIT_AGENT_REQUIRED', 'task-bound commit requires an agent', 'blocked', inspected.snapshot);
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
304
|
+
const delivery = effectiveDelivery(input)!;
|
|
305
|
+
if (delivery.mode === 'local' && !task) return failure(input, mode, null, 'CHECKPOINT_TASK_REQUIRED', 'local checkpoint delivery requires a task context', 'blocked', inspected.snapshot);
|
|
306
|
+
let pendingIntent: NonNullable<ReturnType<typeof readCheckpointIntent>> | null = null;
|
|
307
|
+
let checkpointCommittedHead: string | null = null;
|
|
308
|
+
const committed = delivery.mode === 'local'
|
|
309
|
+
? (() => {
|
|
310
|
+
const identity = checkpointIdentity(input, task!, delivery);
|
|
311
|
+
try {
|
|
312
|
+
pendingIntent = readCheckpointIntent(task!.repoRoot, task!.taskId);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return {
|
|
315
|
+
status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
|
|
316
|
+
error: { code: 'COMMIT_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) return {
|
|
320
|
+
status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
|
|
321
|
+
error: { code: 'COMMIT_INTENT_CONFLICT', message: 'A different checkpoint intent is still pending for this task' }
|
|
322
|
+
};
|
|
323
|
+
if (!pendingIntent) {
|
|
324
|
+
const timestamp = checkpointTimestamp();
|
|
325
|
+
pendingIntent = {
|
|
326
|
+
version: 1, ...identity, digest: checkpointIntentDigest(identity), state: 'prepared',
|
|
327
|
+
committedHead: null, createdAt: timestamp, updatedAt: timestamp
|
|
328
|
+
};
|
|
329
|
+
try { writeCheckpointIntent(task!.repoRoot, pendingIntent); }
|
|
330
|
+
catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
|
|
333
|
+
error: { code: 'COMMIT_INTENT_WRITE_FAILED', message: error instanceof Error ? error.message : String(error) }
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (pendingIntent.state !== 'prepared') {
|
|
338
|
+
checkpointCommittedHead = pendingIntent.committedHead;
|
|
339
|
+
return {
|
|
340
|
+
status: 'no-op' as const, changed: false, snapshot: inspected.snapshot,
|
|
341
|
+
operations: [{ name: 'commit', status: 'no-op' as const }], error: null
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (inspected.snapshot.head !== identity.expectedHead && !checkpointChildMatches(task!, pendingIntent, inspected.snapshot.head)) return {
|
|
345
|
+
status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
|
|
346
|
+
error: { code: 'COMMIT_INTENT_HEAD_CONFLICT', message: 'Current HEAD does not match the pending checkpoint intent' }
|
|
347
|
+
};
|
|
348
|
+
if (inspected.snapshot.head !== identity.expectedHead) {
|
|
349
|
+
checkpointCommittedHead = inspected.snapshot.head;
|
|
350
|
+
pendingIntent = updateCheckpointIntent(pendingIntent, {
|
|
351
|
+
state: 'committed', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
|
|
352
|
+
});
|
|
353
|
+
writeCheckpointIntent(task!.repoRoot, pendingIntent);
|
|
354
|
+
return {
|
|
355
|
+
status: 'no-op' as const, changed: false, snapshot: inspected.snapshot,
|
|
356
|
+
operations: [{ name: 'commit', status: 'no-op' as const }], error: null
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (input.paths.length === 0) return {
|
|
360
|
+
status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
|
|
361
|
+
error: { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Local checkpoint requires explicit paths' }
|
|
362
|
+
};
|
|
363
|
+
const created = commitExplicitPaths({
|
|
364
|
+
cwd: input.cwd, paths: input.paths, message: input.message,
|
|
365
|
+
expectedHead: identity.expectedHead, expectedTree: identity.expectedTree
|
|
366
|
+
});
|
|
367
|
+
if (created.status === 'applied' && created.snapshot?.head) {
|
|
368
|
+
checkpointCommittedHead = created.snapshot.head;
|
|
369
|
+
pendingIntent = updateCheckpointIntent(pendingIntent, {
|
|
370
|
+
state: 'committed', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
|
|
371
|
+
});
|
|
372
|
+
writeCheckpointIntent(task!.repoRoot, pendingIntent);
|
|
373
|
+
} else if (created.status === 'no-op') {
|
|
374
|
+
removeCheckpointIntent(task!.repoRoot, task!.taskId);
|
|
375
|
+
pendingIntent = null;
|
|
376
|
+
}
|
|
377
|
+
return created;
|
|
378
|
+
})()
|
|
379
|
+
: input.paths.length === 0
|
|
282
380
|
? (() => {
|
|
283
381
|
if (inspected.snapshot.worktree.length > 0 || inspected.snapshot.staged.length > 0) return {
|
|
284
382
|
status: 'failed' as const,
|
|
@@ -327,53 +425,42 @@ function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mo
|
|
|
327
425
|
};
|
|
328
426
|
|
|
329
427
|
const warnings: OperationWarning[] = [];
|
|
330
|
-
if (orchestrationCompletion) {
|
|
331
|
-
try {
|
|
332
|
-
commitOrchestrationStageCompletion(orchestrationCompletion);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
warnings.push(warning(
|
|
335
|
-
'ORCHESTRATION_COMPLETION_FAILED',
|
|
336
|
-
error instanceof Error ? error.message : String(error),
|
|
337
|
-
true,
|
|
338
|
-
'orchestration',
|
|
339
|
-
task?.taskId ?? 'orchestration',
|
|
340
|
-
'ACTION_REQUIRED'
|
|
341
|
-
));
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
428
|
let status: CommitOperationResult['status'] = committed.status;
|
|
345
429
|
let result: CommitOperationResult['result'] = committed.status === 'applied' ? 'committed' : 'no_op';
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
true,
|
|
361
|
-
'push',
|
|
362
|
-
target,
|
|
363
|
-
'ACTION_REQUIRED'
|
|
364
|
-
));
|
|
430
|
+
if (delivery.mode === 'push') {
|
|
431
|
+
const target = `${delivery.remote}:${delivery.refs[0]}`;
|
|
432
|
+
const decision = commitPushDecision({ branch }, target);
|
|
433
|
+
if (!decision.shouldPush) {
|
|
434
|
+
warnings.push(decision.warning!);
|
|
435
|
+
} else {
|
|
436
|
+
const pushed = pushGitRefs({
|
|
437
|
+
cwd: input.cwd, remote: delivery.remote, refs: delivery.refs,
|
|
438
|
+
expectedSha: committed.snapshot?.head ?? gitText(input.cwd, ['rev-parse', 'HEAD'])
|
|
439
|
+
});
|
|
440
|
+
if (pushed.status !== 'applied') warnings.push(warning(
|
|
441
|
+
'COMMIT_PUSH_FAILED', pushed.error?.message ?? 'Git push failed', true, 'push', target, 'ACTION_REQUIRED'
|
|
442
|
+
));
|
|
443
|
+
}
|
|
365
444
|
}
|
|
366
445
|
if (warnings.length > 0) {
|
|
367
446
|
status = 'applied';
|
|
368
447
|
result = 'committed_with_warnings';
|
|
369
448
|
}
|
|
370
449
|
if (task && (committed.status === 'applied' || committed.status === 'no-op')) {
|
|
371
|
-
const
|
|
450
|
+
const committedHead = checkpointCommittedHead ?? committed.snapshot?.head ?? gitText(task.repoRoot, ['rev-parse', 'HEAD']);
|
|
451
|
+
const taskWarning = syncTaskCommit(input, task, committedHead);
|
|
372
452
|
if (taskWarning) {
|
|
373
453
|
warnings.push(taskWarning);
|
|
374
454
|
status = 'applied';
|
|
375
455
|
result = 'committed_with_warnings';
|
|
376
456
|
}
|
|
457
|
+
if (delivery.mode === 'local' && !taskWarning && pendingIntent && checkpointCommittedHead) {
|
|
458
|
+
const synced = updateCheckpointIntent(pendingIntent, {
|
|
459
|
+
state: 'synced', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
|
|
460
|
+
});
|
|
461
|
+
writeCheckpointIntent(task.repoRoot, synced);
|
|
462
|
+
removeCheckpointIntent(task.repoRoot, task.taskId);
|
|
463
|
+
}
|
|
377
464
|
}
|
|
378
465
|
return {
|
|
379
466
|
status,
|
package/lib/task/create.ts
CHANGED
|
@@ -5,6 +5,8 @@ import path from 'node:path';
|
|
|
5
5
|
import { isValidAgentInfraVersion, VERSION } from '../version.ts';
|
|
6
6
|
import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.ts';
|
|
7
7
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
8
|
+
import { readDeliveryDefaults, validateBaseRef, validateRemote } from './delivery-target.ts';
|
|
9
|
+
import { buildUnboundFact, encodePrDeliveryFact } from './pr-delivery-fact.ts';
|
|
8
10
|
|
|
9
11
|
const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'] as const;
|
|
10
12
|
const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'] as const;
|
|
@@ -30,6 +32,8 @@ type TaskCreateCandidateV1 = Readonly<{
|
|
|
30
32
|
priority: TaskPriority;
|
|
31
33
|
effort: TaskEffort;
|
|
32
34
|
description: string;
|
|
35
|
+
deliveryRemote?: string;
|
|
36
|
+
deliveryBaseRef?: string;
|
|
33
37
|
taskInput: Readonly<Record<typeof TASK_INPUT_KEYS[number], readonly string[]>>;
|
|
34
38
|
}>;
|
|
35
39
|
|
|
@@ -49,12 +53,14 @@ function payloadError(message: string): never {
|
|
|
49
53
|
throw new Error(`TASK_CREATE_PAYLOAD_INVALID: ${message}`);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
function exactKeys(value: Record<string, unknown>, expected: readonly string[], label: string): void {
|
|
56
|
+
function exactKeys(value: Record<string, unknown>, expected: readonly string[], label: string, optional: readonly string[] = []): void {
|
|
53
57
|
const actual = Object.keys(value).sort();
|
|
54
58
|
const wanted = [...expected].sort();
|
|
55
|
-
|
|
59
|
+
const allowed = new Set([...expected, ...optional]);
|
|
60
|
+
if (actual.some((key) => !allowed.has(key))) {
|
|
56
61
|
payloadError(`${label} has unknown or missing fields`);
|
|
57
62
|
}
|
|
63
|
+
if (expected.some((key) => !actual.includes(key))) payloadError(`${label} has unknown or missing fields`);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
function cleanString(value: unknown, label: string, maxBytes: number, oneLine = false): string {
|
|
@@ -78,7 +84,7 @@ function validateTaskCreateCandidate(value: unknown): TaskCreateCandidateV1 {
|
|
|
78
84
|
exactKeys(candidate, [
|
|
79
85
|
'version', 'idempotencyKey', 'agent', 'title', 'type', 'branchSlug',
|
|
80
86
|
'priority', 'effort', 'description', 'taskInput'
|
|
81
|
-
], 'candidate');
|
|
87
|
+
], 'candidate', ['deliveryRemote', 'deliveryBaseRef']);
|
|
82
88
|
if (candidate.version !== 1) payloadError('version must be 1');
|
|
83
89
|
const idempotencyKey = cleanString(candidate.idempotencyKey, 'idempotencyKey', 64, true);
|
|
84
90
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(idempotencyKey)) {
|
|
@@ -91,6 +97,14 @@ function validateTaskCreateCandidate(value: unknown): TaskCreateCandidateV1 {
|
|
|
91
97
|
payloadError('branchSlug must be lowercase ASCII kebab-case with length 3-64');
|
|
92
98
|
}
|
|
93
99
|
const description = cleanString(candidate.description, 'description', 8 * 1024);
|
|
100
|
+
const deliveryRemote = candidate.deliveryRemote === undefined
|
|
101
|
+
? undefined
|
|
102
|
+
: cleanString(candidate.deliveryRemote, 'deliveryRemote', 200, true);
|
|
103
|
+
const deliveryBaseRef = candidate.deliveryBaseRef === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: cleanString(candidate.deliveryBaseRef, 'deliveryBaseRef', 200, true);
|
|
106
|
+
if (deliveryRemote !== undefined && !validateRemote(deliveryRemote)) payloadError('deliveryRemote is invalid');
|
|
107
|
+
if (deliveryBaseRef !== undefined && !validateBaseRef(deliveryBaseRef)) payloadError('deliveryBaseRef is invalid');
|
|
94
108
|
if (!candidate.taskInput || typeof candidate.taskInput !== 'object' || Array.isArray(candidate.taskInput)) {
|
|
95
109
|
payloadError('taskInput must be an object');
|
|
96
110
|
}
|
|
@@ -111,6 +125,8 @@ function validateTaskCreateCandidate(value: unknown): TaskCreateCandidateV1 {
|
|
|
111
125
|
priority: enumValue(candidate.priority, PRIORITIES, 'priority'),
|
|
112
126
|
effort: enumValue(candidate.effort, EFFORTS, 'effort'),
|
|
113
127
|
description,
|
|
128
|
+
...(deliveryRemote === undefined ? {} : { deliveryRemote }),
|
|
129
|
+
...(deliveryBaseRef === undefined ? {} : { deliveryBaseRef }),
|
|
114
130
|
taskInput
|
|
115
131
|
};
|
|
116
132
|
}
|
|
@@ -127,6 +143,8 @@ function canonicalTaskCreateCandidate(candidate: TaskCreateCandidateV1): string
|
|
|
127
143
|
priority: valid.priority,
|
|
128
144
|
effort: valid.effort,
|
|
129
145
|
description: valid.description,
|
|
146
|
+
deliveryRemote: valid.deliveryRemote ?? null,
|
|
147
|
+
deliveryBaseRef: valid.deliveryBaseRef ?? null,
|
|
130
148
|
taskInput: Object.fromEntries(TASK_INPUT_KEYS.map((key) => [key, valid.taskInput[key]]))
|
|
131
149
|
});
|
|
132
150
|
}
|
|
@@ -170,6 +188,7 @@ function renderTask(params: Readonly<{
|
|
|
170
188
|
candidate: TaskCreateCandidateV1;
|
|
171
189
|
taskId: string;
|
|
172
190
|
project: string;
|
|
191
|
+
delivery: { remote: string; baseRef: string };
|
|
173
192
|
timestamp: string;
|
|
174
193
|
agentInfraVersion: string;
|
|
175
194
|
keyDigest: string;
|
|
@@ -194,7 +213,11 @@ function renderTask(params: Readonly<{
|
|
|
194
213
|
'target_date:',
|
|
195
214
|
'current_step: requirement-analysis',
|
|
196
215
|
`assigned_to: ${candidate.agent}`,
|
|
197
|
-
|
|
216
|
+
`pr_delivery_fact: ${JSON.stringify(encodePrDeliveryFact(buildUnboundFact()))}`,
|
|
217
|
+
`delivery_remote: ${candidate.deliveryRemote ?? params.delivery.remote}`,
|
|
218
|
+
`delivery_base_ref: ${candidate.deliveryBaseRef ?? params.delivery.baseRef}`,
|
|
219
|
+
'checkpoint_commit:',
|
|
220
|
+
'delivery_remote_head:',
|
|
198
221
|
`task_create_key_digest: ${params.keyDigest}`,
|
|
199
222
|
`task_create_candidate_digest: ${params.candidateDigest}`,
|
|
200
223
|
'---'
|
|
@@ -219,12 +242,14 @@ function renderTask(params: Readonly<{
|
|
|
219
242
|
return `${content.trimEnd()}\n`;
|
|
220
243
|
}
|
|
221
244
|
|
|
222
|
-
function readProject(repoRoot: string): string {
|
|
223
|
-
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
|
|
245
|
+
function readProject(repoRoot: string): { project: string; delivery: { remote: string; baseRef: string } } {
|
|
246
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown; delivery?: { remote?: unknown; baseRef?: unknown } };
|
|
224
247
|
if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
|
|
225
248
|
throw new Error('TASK_CREATE_CONFIG_INVALID: project is missing or invalid');
|
|
226
249
|
}
|
|
227
|
-
|
|
250
|
+
const delivery = readDeliveryDefaults(repoRoot);
|
|
251
|
+
if (!delivery.ok) throw new Error(`TASK_CREATE_CONFIG_INVALID: ${delivery.message}`);
|
|
252
|
+
return { project: config.project, delivery: delivery.value };
|
|
228
253
|
}
|
|
229
254
|
|
|
230
255
|
function assertRealDirectory(directory: string, code: string): void {
|
|
@@ -305,7 +330,7 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
|
|
|
305
330
|
ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
|
|
306
331
|
ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
307
332
|
ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
308
|
-
const
|
|
333
|
+
const projectConfig = readProject(repoRoot);
|
|
309
334
|
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
310
335
|
const candidateDigest = sha256(canonical);
|
|
311
336
|
const keyDigest = sha256(candidate.idempotencyKey);
|
|
@@ -346,7 +371,7 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
|
|
|
346
371
|
const templateStat = fs.lstatSync(templatePath);
|
|
347
372
|
if (!templateStat.isFile() || templateStat.isSymbolicLink()) throw new Error('TASK_CREATE_TEMPLATE_INVALID');
|
|
348
373
|
const rendered = renderTask({
|
|
349
|
-
template: fs.readFileSync(templatePath, 'utf8'), candidate, taskId, project, timestamp,
|
|
374
|
+
template: fs.readFileSync(templatePath, 'utf8'), candidate, taskId, project: projectConfig.project, delivery: projectConfig.delivery, timestamp,
|
|
350
375
|
agentInfraVersion, keyDigest, candidateDigest
|
|
351
376
|
});
|
|
352
377
|
const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
|
|
@@ -36,7 +36,6 @@ type DecisionDetailResolution =
|
|
|
36
36
|
type DecisionDetailDuplicate = {
|
|
37
37
|
id: string;
|
|
38
38
|
blocks: DecisionDetailBlock[];
|
|
39
|
-
repairable: boolean;
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
type DecisionDetailInspection =
|
|
@@ -49,15 +48,6 @@ type DecisionDetailInspection =
|
|
|
49
48
|
duplicates: DecisionDetailDuplicate[];
|
|
50
49
|
};
|
|
51
50
|
|
|
52
|
-
type DecisionDetailRepair =
|
|
53
|
-
| { ok: true; changed: boolean; content: string }
|
|
54
|
-
| {
|
|
55
|
-
ok: false;
|
|
56
|
-
code: 'DECISION_DETAIL_AMBIGUOUS';
|
|
57
|
-
message: string;
|
|
58
|
-
content: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
51
|
const DECISION_ID = /^(AN|PL|CD|HD)-[1-9]\d*(?=$|[\s::])/i;
|
|
62
52
|
const HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/;
|
|
63
53
|
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
@@ -172,32 +162,8 @@ function resolveDecisionDetail(content: string, rowId: string, anchor: string):
|
|
|
172
162
|
return { status: 'found', block: matches[0]! };
|
|
173
163
|
}
|
|
174
164
|
|
|
175
|
-
const INFORMAL_SHORT_HEADING = /^(?:AN|PL|CD|HD)-[1-9]\d*[::]\s*(?:上一轮)?简短复核$/i;
|
|
176
|
-
const INFORMAL_SHORT_FRAGMENT = /^简短(?:结论|说明)$/;
|
|
177
|
-
|
|
178
|
-
function isInformalShortBlock(block: DecisionDetailBlock): boolean {
|
|
179
|
-
if (block.canonical || block.content.length > 600) return false;
|
|
180
|
-
if (!INFORMAL_SHORT_HEADING.test(block.heading)) return false;
|
|
181
|
-
const scanned = scanVisibleMarkdown(block.content);
|
|
182
|
-
const bodyLines = scanned.lines
|
|
183
|
-
.filter((line) => !scanned.headings.some((heading) => heading.start === line.start) && line.text.trim())
|
|
184
|
-
.map((line) => line.text);
|
|
185
|
-
if (bodyLines.length === 0 || bodyLines.length > 2) return false;
|
|
186
|
-
const fragments = bodyLines.map((line) => line.match(/^\s*[-*]\s+([^\s].*?)\s*$/)?.[1] ?? null);
|
|
187
|
-
if (fragments.some((fragment) => fragment === null)) return false;
|
|
188
|
-
return fragments.every((fragment) => INFORMAL_SHORT_FRAGMENT.test(fragment!));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function hasDirectPrecedingAnchor(block: DecisionDetailBlock, scanned: VisibleMarkdown): boolean {
|
|
192
|
-
return scanned.anchors.some((anchor) => (
|
|
193
|
-
anchor.end < block.start
|
|
194
|
-
&& !scanned.lines.some((line) => line.start > anchor.end && line.start < block.start && line.text.trim())
|
|
195
|
-
));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
165
|
function inspectDecisionDetailDuplicates(content: string): DecisionDetailInspection {
|
|
199
166
|
const blocks = parseDecisionDetailBlocks(content);
|
|
200
|
-
const scanned = scanVisibleMarkdown(content);
|
|
201
167
|
const grouped = new Map<string, DecisionDetailBlock[]>();
|
|
202
168
|
for (const block of blocks) {
|
|
203
169
|
const key = block.id.toUpperCase();
|
|
@@ -208,16 +174,12 @@ function inspectDecisionDetailDuplicates(content: string): DecisionDetailInspect
|
|
|
208
174
|
const duplicates: DecisionDetailDuplicate[] = [];
|
|
209
175
|
for (const group of grouped.values()) {
|
|
210
176
|
if (group.length < 2) continue;
|
|
211
|
-
|
|
212
|
-
const repairable = canonical.length === 1 && group.every((block) => (
|
|
213
|
-
block.canonical || (!hasDirectPrecedingAnchor(block, scanned) && isInformalShortBlock(block))
|
|
214
|
-
));
|
|
215
|
-
duplicates.push({ id: canonical[0]?.id ?? group[0]!.id, blocks: group, repairable });
|
|
177
|
+
duplicates.push({ id: group[0]!.id, blocks: group });
|
|
216
178
|
}
|
|
217
179
|
if (duplicates.length === 0) return { ok: true, blocks };
|
|
218
180
|
const summary = duplicates.map((duplicate) => {
|
|
219
181
|
const types = duplicate.blocks.map((block) => block.canonical ? 'canonical' : 'informal').join(', ');
|
|
220
|
-
return `${duplicate.id} (${duplicate.blocks.length} blocks: ${types}
|
|
182
|
+
return `${duplicate.id} (${duplicate.blocks.length} blocks: ${types})`;
|
|
221
183
|
}).join(', ');
|
|
222
184
|
return {
|
|
223
185
|
ok: false,
|
|
@@ -228,26 +190,6 @@ function inspectDecisionDetailDuplicates(content: string): DecisionDetailInspect
|
|
|
228
190
|
};
|
|
229
191
|
}
|
|
230
192
|
|
|
231
|
-
function repairDecisionDetailDuplicates(content: string): DecisionDetailRepair {
|
|
232
|
-
const inspection = inspectDecisionDetailDuplicates(content);
|
|
233
|
-
if (inspection.ok) return { ok: true, changed: false, content };
|
|
234
|
-
const unsafe = inspection.duplicates.filter((duplicate) => !duplicate.repairable);
|
|
235
|
-
if (unsafe.length > 0) {
|
|
236
|
-
return {
|
|
237
|
-
ok: false,
|
|
238
|
-
code: 'DECISION_DETAIL_AMBIGUOUS',
|
|
239
|
-
message: `Cannot safely repair duplicate decision detail ids: ${unsafe.map((duplicate) => duplicate.id).join(', ')}`,
|
|
240
|
-
content
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
const removals = inspection.duplicates.flatMap((duplicate) => duplicate.blocks.filter((block) => !block.canonical));
|
|
244
|
-
let repaired = content;
|
|
245
|
-
for (const block of removals.sort((left, right) => right.start - left.start)) {
|
|
246
|
-
repaired = repaired.slice(0, block.start) + repaired.slice(block.end);
|
|
247
|
-
}
|
|
248
|
-
return { ok: true, changed: repaired !== content, content: repaired };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
193
|
type FieldKey = 'decision' | 'why' | 'impact' | 'risk' | 'analogy' | 'recommend' | 'reason' | 'implementation';
|
|
252
194
|
|
|
253
195
|
const FIELD_KEYS: Record<string, FieldKey> = {
|
|
@@ -328,13 +270,11 @@ export {
|
|
|
328
270
|
normalizeDecisionDetailForDisplay,
|
|
329
271
|
parseDecisionDetailBlocks,
|
|
330
272
|
parseDecisionEvidence,
|
|
331
|
-
repairDecisionDetailDuplicates,
|
|
332
273
|
resolveDecisionDetail
|
|
333
274
|
};
|
|
334
275
|
export type {
|
|
335
276
|
DecisionDetailBlock,
|
|
336
277
|
DecisionDetailDuplicate,
|
|
337
278
|
DecisionDetailInspection,
|
|
338
|
-
DecisionDetailRepair,
|
|
339
279
|
DecisionDetailResolution
|
|
340
280
|
};
|