@fitlab-ai/agent-infra 0.9.14 → 0.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/internal-cli.js +93 -30
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +37 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/cli-route-inventory.js +3 -1
- package/dist/lib/internal/codex-lifecycle.js +6 -6
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +322 -0
- package/dist/lib/internal/platform-pr.js +18 -4
- package/dist/lib/internal/release-workflow.js +63 -84
- package/dist/lib/internal/sandbox-control.js +149 -2
- package/dist/lib/internal/task-artifact.js +15 -262
- package/dist/lib/internal/task-create.js +6 -5
- package/dist/lib/internal/task-event.js +37 -37
- package/dist/lib/internal/task-operation-registry.js +101 -3
- package/dist/lib/internal/task-review.js +8 -53
- package/dist/lib/internal/task-validate.js +30 -10
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +4 -1
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +15 -20
- package/dist/lib/platform/issue-comments.js +2 -2
- package/dist/lib/platform/issues.js +1 -179
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +55 -3
- package/dist/lib/platform/pull-requests.js +2 -105
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/verification-sync.js +127 -142
- package/dist/lib/release/workflow.js +1 -1
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/rm.js +3 -1514
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +93 -54
- package/dist/lib/sandbox/control/executor.js +129 -65
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +36 -2
- package/dist/lib/sandbox/control/native-file-lock.js +6 -98
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +491 -120
- package/dist/lib/sandbox/control/state.js +58 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +99 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/recovery.js +28 -4
- package/dist/lib/sandbox/removal.js +1717 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-view.js +13 -14
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +101 -0
- package/dist/lib/task/artifact-lifecycle.js +30 -28
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +23 -69
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-repair-intent.js +87 -16
- package/dist/lib/task/artifact-schema.js +6 -14
- package/dist/lib/task/capabilities.js +12 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +2 -16
- package/dist/lib/task/control-authority.js +371 -5
- package/dist/lib/task/control-recovery.js +128 -0
- package/dist/lib/task/create.js +62 -9
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/events.js +120 -9
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +3 -6
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/intermediate-cleanup.js +383 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle.js +19 -1
- package/dist/lib/task/local-artifact-finalization.js +132 -157
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +101 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +14 -17
- package/dist/lib/task/qualification-audit.js +20 -34
- package/dist/lib/task/review-artifacts.js +2 -18
- package/dist/lib/task/review-command.js +25 -0
- package/dist/lib/task/review-finalization.js +125 -193
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -56
- package/dist/lib/task/verification-engine.js +116 -323
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +17 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
- package/lib/fs/durable-write.ts +29 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/cli-route-inventory.ts +3 -1
- package/lib/internal/codex-lifecycle.ts +6 -6
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +306 -0
- package/lib/internal/platform-pr.ts +15 -4
- package/lib/internal/release-workflow.ts +74 -82
- package/lib/internal/sandbox-control.ts +162 -1
- package/lib/internal/task-artifact.ts +13 -216
- package/lib/internal/task-create.ts +5 -5
- package/lib/internal/task-event.ts +20 -11
- package/lib/internal/task-operation-registry.ts +111 -3
- package/lib/internal/task-review.ts +6 -51
- package/lib/internal/task-validate.ts +26 -10
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +5 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +15 -20
- package/lib/platform/issue-comments.ts +3 -4
- package/lib/platform/issues.ts +4 -245
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +61 -5
- package/lib/platform/pull-requests.ts +3 -170
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/verification-sync.ts +145 -155
- package/lib/release/workflow.ts +3 -3
- package/lib/sandbox/commands/create.ts +7 -6
- package/lib/sandbox/commands/rm.ts +3 -1909
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +109 -66
- package/lib/sandbox/control/executor.ts +134 -74
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +45 -3
- package/lib/sandbox/control/native-file-lock.ts +6 -107
- package/lib/sandbox/control/protocol.ts +75 -6
- package/lib/sandbox/control/server.ts +538 -123
- package/lib/sandbox/control/state.ts +78 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +102 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/recovery.ts +28 -5
- package/lib/sandbox/removal.ts +2144 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-view.ts +17 -15
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +104 -0
- package/lib/task/artifact-lifecycle.ts +31 -26
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +22 -68
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-repair-intent.ts +99 -18
- package/lib/task/artifact-schema.ts +5 -34
- package/lib/task/capabilities.ts +13 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +2 -16
- package/lib/task/control-authority.ts +502 -5
- package/lib/task/control-recovery.ts +174 -0
- package/lib/task/create.ts +63 -8
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/events.ts +131 -10
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +3 -8
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/intermediate-cleanup.ts +455 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle.ts +17 -1
- package/lib/task/local-artifact-finalization.ts +148 -182
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +152 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +19 -19
- package/lib/task/qualification-audit.ts +22 -33
- package/lib/task/review-artifacts.ts +1 -20
- package/lib/task/review-command.ts +23 -0
- package/lib/task/review-finalization.ts +131 -259
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -54
- package/lib/task/verification-engine.ts +110 -366
- package/lib/task/verification-types.ts +17 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +18 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/package.json +6 -6
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +17 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { inspectOwnedPath } from '../owned-path.ts';
|
|
2
|
+
import { taskFinalizationReceiptState } from './finalization-state.ts';
|
|
3
|
+
import { withTaskExecutionLock } from './task-execution-lock.ts';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { checkpointCommitMatches } from './commit-identity.ts';
|
|
8
|
+
import {
|
|
9
|
+
readCheckpointIntent,
|
|
10
|
+
type CheckpointIntent
|
|
11
|
+
} from './commit-intent.ts';
|
|
12
|
+
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
13
|
+
import {
|
|
14
|
+
readTaskFinalizationReceipt,
|
|
15
|
+
type TaskFinalizationReceipt
|
|
16
|
+
} from './finalization.ts';
|
|
17
|
+
import {
|
|
18
|
+
readLocalArtifactFinalizationIntent,
|
|
19
|
+
semanticDigest,
|
|
20
|
+
sha256Content,
|
|
21
|
+
type LocalArtifactFinalizationIntent
|
|
22
|
+
} from './local-artifact-finalization.ts';
|
|
23
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
24
|
+
import { enumerateAllTaskDirs, type TaskWorkspaceState } from './resolve-ref.ts';
|
|
25
|
+
import type { TaskControlBindingEvidence } from './finalization-state.ts';
|
|
26
|
+
|
|
27
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
28
|
+
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|plan|code)-(.+\.md)\.json$/;
|
|
29
|
+
const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
|
|
30
|
+
const AUXILIARY_ROOTS = [
|
|
31
|
+
'.local-artifact-finalization-intents',
|
|
32
|
+
'.task-commit-intents'
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
type CleanupKind = 'LFAI-CONSUMED' | 'COMMIT-SYNCED' | 'EMPTY-AUX-PARENT';
|
|
36
|
+
type CleanupDisposition = 'planned' | 'protected' | 'skipped' | 'deleted' | 'failed';
|
|
37
|
+
type FileIdentity = Readonly<{ dev: string; ino: string; size: number; mtimeMs: number }>;
|
|
38
|
+
|
|
39
|
+
type IntermediateCleanupItem = Readonly<{
|
|
40
|
+
kind: CleanupKind | 'AUXILIARY-UNKNOWN';
|
|
41
|
+
taskId: string | null;
|
|
42
|
+
path: string;
|
|
43
|
+
disposition: CleanupDisposition;
|
|
44
|
+
reason: string;
|
|
45
|
+
identity: FileIdentity | null;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
type IntermediateCleanupReport = Readonly<{
|
|
49
|
+
status: 'dry-run' | 'completed' | 'partial';
|
|
50
|
+
items: readonly IntermediateCleanupItem[];
|
|
51
|
+
remaining: readonly IntermediateCleanupItem[];
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
type IntermediateCleanupOptions = Readonly<{
|
|
55
|
+
taskIds?: readonly string[];
|
|
56
|
+
controlBindingEvidence?: TaskControlBindingEvidence;
|
|
57
|
+
}>;
|
|
58
|
+
|
|
59
|
+
type ScanOptions = IntermediateCleanupOptions & Readonly<{ preflight?: boolean }>;
|
|
60
|
+
|
|
61
|
+
type TaskRecord = Readonly<{
|
|
62
|
+
taskId: string;
|
|
63
|
+
taskDir: string;
|
|
64
|
+
state: TaskWorkspaceState;
|
|
65
|
+
frontmatter: Record<string, string>;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
function auxiliaryRoot(repoRoot: string, name: typeof AUXILIARY_ROOTS[number]): string {
|
|
69
|
+
return path.join(repoRoot, '.agents', 'workspace', name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function taskRoot(repoRoot: string): string {
|
|
73
|
+
return path.join(repoRoot, '.agents', 'workspace');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fileIdentity(stat: fs.Stats): FileIdentity {
|
|
77
|
+
return {
|
|
78
|
+
dev: String(stat.dev),
|
|
79
|
+
ino: String(stat.ino),
|
|
80
|
+
size: stat.size,
|
|
81
|
+
mtimeMs: stat.mtimeMs
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sameFileIdentity(left: FileIdentity, right: FileIdentity): boolean {
|
|
86
|
+
return left.dev === right.dev && left.ino === right.ino
|
|
87
|
+
&& left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function errorCode(error: unknown): string | null {
|
|
91
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
92
|
+
return typeof code === 'string' ? code : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function safeLstat(root: string, target: string, expect: 'file' | 'directory'): FileIdentity | null {
|
|
96
|
+
const stat = inspectOwnedPath(root, target, expect);
|
|
97
|
+
return stat ? fileIdentity(stat) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function inspectTaskRecords(repoRoot: string, taskIds?: ReadonlySet<string>): Map<string, TaskRecord> {
|
|
101
|
+
const records = new Map<string, TaskRecord>();
|
|
102
|
+
for (const entry of enumerateAllTaskDirs(repoRoot)) {
|
|
103
|
+
if (taskIds && !taskIds.has(entry.taskId)) continue;
|
|
104
|
+
const taskMdPath = path.join(entry.taskDir, 'task.md');
|
|
105
|
+
if (safeLstat(taskRoot(repoRoot), taskMdPath, 'file') === null) continue;
|
|
106
|
+
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
107
|
+
records.set(entry.taskId, {
|
|
108
|
+
taskId: entry.taskId,
|
|
109
|
+
taskDir: entry.taskDir,
|
|
110
|
+
state: entry.state,
|
|
111
|
+
frontmatter: parseTaskFrontmatter(content)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return records;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function item(
|
|
118
|
+
kind: CleanupKind | 'AUXILIARY-UNKNOWN',
|
|
119
|
+
taskId: string | null,
|
|
120
|
+
filePath: string,
|
|
121
|
+
disposition: CleanupDisposition,
|
|
122
|
+
reason: string,
|
|
123
|
+
identity: FileIdentity | null = null
|
|
124
|
+
): IntermediateCleanupItem {
|
|
125
|
+
return { kind, taskId, path: path.resolve(filePath), disposition, reason, identity };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function taskGate(task: TaskRecord | undefined): string | null {
|
|
129
|
+
if (!task) return 'TASK_NOT_FOUND';
|
|
130
|
+
if (task.state !== 'completed' && task.state !== 'archive') return 'TASK_STATE_PROTECTED';
|
|
131
|
+
if (task.frontmatter.id !== task.taskId || task.frontmatter.status !== 'completed') return 'TASK_STATE_PROTECTED';
|
|
132
|
+
if (!task.frontmatter.branch) return 'TASK_IDENTITY_INVALID';
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function receiptGate(
|
|
137
|
+
repoRoot: string,
|
|
138
|
+
task: TaskRecord,
|
|
139
|
+
options: ScanOptions
|
|
140
|
+
): string | null {
|
|
141
|
+
let receipt: TaskFinalizationReceipt | null;
|
|
142
|
+
try { receipt = readTaskFinalizationReceipt(repoRoot, task.taskId); }
|
|
143
|
+
catch { return 'FINALIZATION_RECEIPT_INVALID'; }
|
|
144
|
+
if (!receipt) return 'FINALIZATION_RECEIPT_MISSING';
|
|
145
|
+
const state = taskFinalizationReceiptState(receipt);
|
|
146
|
+
if (state === 'pending') return 'FINALIZATION_RECEIPT_PENDING';
|
|
147
|
+
if (state === 'unresolved') return 'LFAI_RETRY_OR_WARNING_OPEN';
|
|
148
|
+
if (receipt.controlBinding) {
|
|
149
|
+
const evidence = options.controlBindingEvidence?.(task.taskId, receipt.controlBinding);
|
|
150
|
+
if (evidence !== 'terminal' && !(options.preflight && evidence === 'pending')) return 'CONTROL_BINDING_MISMATCH';
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function canonicalLocalIntentPath(taskId: string, family: string, artifact: string): string {
|
|
156
|
+
return `${taskId}-${family}-${artifact}.json`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function localIntentCandidate(
|
|
160
|
+
repoRoot: string,
|
|
161
|
+
task: TaskRecord | undefined,
|
|
162
|
+
filePath: string,
|
|
163
|
+
taskId: string,
|
|
164
|
+
family: 'analysis' | 'plan' | 'code',
|
|
165
|
+
artifact: string,
|
|
166
|
+
identity: FileIdentity,
|
|
167
|
+
options: ScanOptions
|
|
168
|
+
): IntermediateCleanupItem {
|
|
169
|
+
const protectedReason = taskGate(task);
|
|
170
|
+
if (protectedReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
|
|
171
|
+
if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
|
|
172
|
+
|| !parseArtifactName(artifact)?.family || parseArtifactName(artifact)!.family !== family) {
|
|
173
|
+
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
|
|
174
|
+
}
|
|
175
|
+
let intent: LocalArtifactFinalizationIntent | null;
|
|
176
|
+
try { intent = readLocalArtifactFinalizationIntent(repoRoot, taskId, family, artifact); }
|
|
177
|
+
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity); }
|
|
178
|
+
if (!intent || intent.state !== 'consumed') {
|
|
179
|
+
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
|
|
180
|
+
}
|
|
181
|
+
const artifactPath = path.join(task!.taskDir, artifact);
|
|
182
|
+
if (safeLstat(task!.taskDir, artifactPath, 'file') === null) {
|
|
183
|
+
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
|
|
184
|
+
}
|
|
185
|
+
let content: string;
|
|
186
|
+
try { content = fs.readFileSync(artifactPath, 'utf8'); }
|
|
187
|
+
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity); }
|
|
188
|
+
if (intent.artifactSha256 !== sha256Content(content) || intent.semanticDigest !== semanticDigest(content)) {
|
|
189
|
+
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
|
|
190
|
+
}
|
|
191
|
+
const receiptReason = receiptGate(repoRoot, task!, options);
|
|
192
|
+
if (receiptReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
|
|
193
|
+
return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function commitIntentCandidate(
|
|
197
|
+
repoRoot: string,
|
|
198
|
+
task: TaskRecord | undefined,
|
|
199
|
+
filePath: string,
|
|
200
|
+
taskId: string,
|
|
201
|
+
identity: FileIdentity,
|
|
202
|
+
options: ScanOptions
|
|
203
|
+
): IntermediateCleanupItem {
|
|
204
|
+
const protectedReason = taskGate(task);
|
|
205
|
+
if (protectedReason) return item('COMMIT-SYNCED', taskId, filePath, 'protected', protectedReason, identity);
|
|
206
|
+
let intent: CheckpointIntent | null;
|
|
207
|
+
try { intent = readCheckpointIntent(repoRoot, taskId); }
|
|
208
|
+
catch { return item('COMMIT-SYNCED', taskId, filePath, 'protected', 'COMMIT_INTENT_INVALID', identity); }
|
|
209
|
+
if (!intent || intent.state !== 'synced' || intent.taskId !== taskId
|
|
210
|
+
|| intent.branch !== task!.frontmatter.branch || !intent.committedHead
|
|
211
|
+
|| task!.frontmatter.checkpoint_commit !== intent.committedHead
|
|
212
|
+
|| !checkpointCommitMatches(repoRoot, intent, intent.committedHead)) {
|
|
213
|
+
return item('COMMIT-SYNCED', taskId, filePath, 'protected', 'COMMIT_EVIDENCE_MISMATCH', identity);
|
|
214
|
+
}
|
|
215
|
+
const receiptReason = receiptGate(repoRoot, task!, options);
|
|
216
|
+
if (receiptReason) return item('COMMIT-SYNCED', taskId, filePath, 'protected', receiptReason, identity);
|
|
217
|
+
return item('COMMIT-SYNCED', taskId, filePath, 'planned', 'COMMIT_SYNCED_AND_VERIFIED', identity);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readAuxiliaryCandidates(
|
|
221
|
+
repoRoot: string,
|
|
222
|
+
tasks: Map<string, TaskRecord>,
|
|
223
|
+
options: ScanOptions
|
|
224
|
+
): IntermediateCleanupItem[] {
|
|
225
|
+
const candidates: IntermediateCleanupItem[] = [];
|
|
226
|
+
const selected = options.taskIds ? new Set(options.taskIds) : null;
|
|
227
|
+
for (const rootName of AUXILIARY_ROOTS) {
|
|
228
|
+
const root = auxiliaryRoot(repoRoot, rootName);
|
|
229
|
+
let rootIdentity: FileIdentity | null;
|
|
230
|
+
try { rootIdentity = safeLstat(taskRoot(repoRoot), root, 'directory'); }
|
|
231
|
+
catch { rootIdentity = null; }
|
|
232
|
+
if (rootIdentity === null) {
|
|
233
|
+
let stat: fs.Stats | null = null;
|
|
234
|
+
try { stat = fs.lstatSync(root); } catch { stat = null; }
|
|
235
|
+
if (stat?.isSymbolicLink()) candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'PATH_SYMLINK'));
|
|
236
|
+
else if (stat) candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
let entries: fs.Dirent[];
|
|
240
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); }
|
|
241
|
+
catch {
|
|
242
|
+
candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'AUXILIARY_ROOT_UNREADABLE', rootIdentity));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
246
|
+
const filePath = path.join(root, entry.name);
|
|
247
|
+
if (entry.isSymbolicLink()) {
|
|
248
|
+
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_SYMLINK'));
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!entry.isFile()) {
|
|
252
|
+
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const identity = safeLstat(root, filePath, 'file');
|
|
256
|
+
if (!identity) {
|
|
257
|
+
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (rootName === '.local-artifact-finalization-intents') {
|
|
261
|
+
const match = LOCAL_INTENT_RE.exec(entry.name);
|
|
262
|
+
if (!match || (selected && !selected.has(match[1]!))) {
|
|
263
|
+
if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
candidates.push(localIntentCandidate(
|
|
267
|
+
repoRoot, tasks.get(match[1]!), filePath, match[1]!, match[2] as 'analysis' | 'plan' | 'code', match[3]!, identity, options
|
|
268
|
+
));
|
|
269
|
+
} else {
|
|
270
|
+
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
271
|
+
if (!match || (selected && !selected.has(match[1]!))) {
|
|
272
|
+
if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (entries.length === 0) {
|
|
279
|
+
candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return candidates;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildIntermediateCleanupReport(items: readonly IntermediateCleanupItem[], dryRun = false, partial = false): IntermediateCleanupReport {
|
|
286
|
+
const remaining = items.filter((candidate) => candidate.disposition !== 'deleted' && candidate.disposition !== 'skipped');
|
|
287
|
+
return {
|
|
288
|
+
status: dryRun ? 'dry-run' : partial || remaining.some((candidate) => candidate.disposition === 'failed') ? 'partial' : 'completed',
|
|
289
|
+
items,
|
|
290
|
+
remaining
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function scanInternal(repoRootInput: string, options: ScanOptions): IntermediateCleanupItem[] {
|
|
295
|
+
const repoRoot = path.resolve(repoRootInput);
|
|
296
|
+
if (safeLstat(path.dirname(repoRoot), repoRoot, 'directory') === null) {
|
|
297
|
+
throw new Error('INTERMEDIATE_CLEANUP_REPOSITORY_INVALID');
|
|
298
|
+
}
|
|
299
|
+
const selected = options.taskIds ? new Set(options.taskIds.filter((taskId) => TASK_ID_RE.test(taskId))) : undefined;
|
|
300
|
+
const tasks = inspectTaskRecords(repoRoot, selected);
|
|
301
|
+
return readAuxiliaryCandidates(repoRoot, tasks, options);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function scanIntermediateCleanup(repoRoot: string, options: ScanOptions = {}): IntermediateCleanupReport {
|
|
305
|
+
const candidates = scanInternal(repoRoot, options);
|
|
306
|
+
return buildIntermediateCleanupReport(candidates, true);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function removeCandidate(candidate: IntermediateCleanupItem): IntermediateCleanupItem {
|
|
310
|
+
if (candidate.disposition !== 'planned') return candidate;
|
|
311
|
+
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
312
|
+
? path.dirname(path.dirname(candidate.path))
|
|
313
|
+
: path.dirname(candidate.path);
|
|
314
|
+
const expected = candidate.identity;
|
|
315
|
+
if (!expected) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_MISSING' };
|
|
316
|
+
let raw: fs.Stats;
|
|
317
|
+
try { raw = fs.lstatSync(candidate.path); }
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (errorCode(error) === 'ENOENT') return { ...candidate, disposition: 'skipped', reason: 'ALREADY_GONE' };
|
|
320
|
+
return { ...candidate, disposition: 'failed', reason: 'CLEANUP_RECHECK_FAILED' };
|
|
321
|
+
}
|
|
322
|
+
if (raw.isSymbolicLink()) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
323
|
+
const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
|
|
324
|
+
if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
325
|
+
if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
326
|
+
try {
|
|
327
|
+
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
328
|
+
if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
|
|
329
|
+
fs.rmdirSync(candidate.path);
|
|
330
|
+
} else {
|
|
331
|
+
fs.unlinkSync(candidate.path);
|
|
332
|
+
}
|
|
333
|
+
return { ...candidate, disposition: 'deleted', reason: 'DELETED_AFTER_LOCKED_RECHECK' };
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (errorCode(error) === 'ENOENT') return { ...candidate, disposition: 'skipped', reason: 'ALREADY_GONE' };
|
|
336
|
+
return { ...candidate, disposition: 'failed', reason: 'CLEANUP_DELETE_FAILED' };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
type IntermediateCleanupCoordinatorOptions = IntermediateCleanupOptions & Readonly<{
|
|
342
|
+
dryRun?: boolean;
|
|
343
|
+
lockedTaskIds?: ReadonlySet<string>;
|
|
344
|
+
}>;
|
|
345
|
+
|
|
346
|
+
function intermediateCleanupKey(item: IntermediateCleanupItem): string {
|
|
347
|
+
return `${item.kind}\0${item.path}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function cleanupIntermediateUnderRemovalCoordinator(
|
|
351
|
+
repoRoot: string,
|
|
352
|
+
options: IntermediateCleanupCoordinatorOptions = {}
|
|
353
|
+
): IntermediateCleanupReport {
|
|
354
|
+
const scanOptions: IntermediateCleanupOptions = {
|
|
355
|
+
...(options.taskIds === undefined ? {} : { taskIds: options.taskIds }),
|
|
356
|
+
...(options.controlBindingEvidence === undefined ? {} : { controlBindingEvidence: options.controlBindingEvidence })
|
|
357
|
+
};
|
|
358
|
+
const initial = scanIntermediateCleanup(repoRoot, scanOptions);
|
|
359
|
+
if (options.dryRun) return initial;
|
|
360
|
+
|
|
361
|
+
const items = [...initial.items];
|
|
362
|
+
const lockedTaskIds = options.lockedTaskIds ?? new Set<string>();
|
|
363
|
+
const byTask = new Map<string, number[]>();
|
|
364
|
+
for (const [index, candidate] of items.entries()) {
|
|
365
|
+
if (!candidate.taskId || candidate.disposition !== 'planned') continue;
|
|
366
|
+
const indexes = byTask.get(candidate.taskId) ?? [];
|
|
367
|
+
indexes.push(index);
|
|
368
|
+
byTask.set(candidate.taskId, indexes);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const taskId of [...byTask.keys()].sort()) {
|
|
372
|
+
const execute = (): void => {
|
|
373
|
+
const refreshed = scanIntermediateCleanup(repoRoot, {
|
|
374
|
+
...scanOptions,
|
|
375
|
+
taskIds: [taskId]
|
|
376
|
+
});
|
|
377
|
+
const refreshedByKey = new Map(refreshed.items.map((candidate) => [intermediateCleanupKey(candidate), candidate]));
|
|
378
|
+
for (const index of byTask.get(taskId) ?? []) {
|
|
379
|
+
const initialCandidate = items[index]!;
|
|
380
|
+
if (initialCandidate.disposition !== 'planned') continue;
|
|
381
|
+
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
382
|
+
items[index] = current
|
|
383
|
+
? removeCandidate(current)
|
|
384
|
+
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
if (lockedTaskIds.has(taskId)) execute();
|
|
389
|
+
else withTaskExecutionLock(repoRoot, taskId, 'sandbox-removal', execute);
|
|
390
|
+
} catch {
|
|
391
|
+
for (const index of byTask.get(taskId) ?? []) {
|
|
392
|
+
const candidate = items[index]!;
|
|
393
|
+
if (candidate.disposition === 'planned') items[index] = { ...candidate, disposition: 'protected', reason: 'TASK_LOCK_BUSY' };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const [index, candidate] of items.entries()) {
|
|
399
|
+
if (candidate.taskId || candidate.disposition !== 'planned') continue;
|
|
400
|
+
items[index] = removeCandidate(candidate);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const knownPaths = new Set(items.map((candidate) => intermediateCleanupKey(candidate)));
|
|
404
|
+
for (const candidate of scanIntermediateCleanup(repoRoot, scanOptions).items) {
|
|
405
|
+
if (candidate.kind !== 'EMPTY-AUX-PARENT' || candidate.disposition !== 'planned'
|
|
406
|
+
|| knownPaths.has(intermediateCleanupKey(candidate))) continue;
|
|
407
|
+
items.push(removeCandidate(candidate));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return buildIntermediateCleanupReport(items);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function protectIntermediateCleanupReport(
|
|
414
|
+
report: IntermediateCleanupReport,
|
|
415
|
+
reason: string
|
|
416
|
+
): IntermediateCleanupReport {
|
|
417
|
+
const items = report.items.map((item) => item.disposition === 'planned'
|
|
418
|
+
? { ...item, disposition: 'protected' as const, reason }
|
|
419
|
+
: item
|
|
420
|
+
);
|
|
421
|
+
return buildIntermediateCleanupReport(items, false, true);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function mergeIntermediateCleanupReports(
|
|
425
|
+
reports: readonly IntermediateCleanupReport[]
|
|
426
|
+
): IntermediateCleanupReport {
|
|
427
|
+
const items = reports.flatMap((report) => report.items);
|
|
428
|
+
return buildIntermediateCleanupReport(items, false, reports.some((report) => report.status === 'partial'));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function formatIntermediateCleanupReport(report: IntermediateCleanupReport): string[] {
|
|
432
|
+
return [
|
|
433
|
+
`remaining ${report.remaining.length} item(s)`,
|
|
434
|
+
...report.items.map((candidate) => (
|
|
435
|
+
`${candidate.disposition} ${candidate.kind} ${candidate.path}`
|
|
436
|
+
+ ` (${candidate.taskId ?? 'unbound'}; ${candidate.reason})`
|
|
437
|
+
))
|
|
438
|
+
];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export {
|
|
442
|
+
formatIntermediateCleanupReport,
|
|
443
|
+
cleanupIntermediateUnderRemovalCoordinator,
|
|
444
|
+
protectIntermediateCleanupReport,
|
|
445
|
+
mergeIntermediateCleanupReports,
|
|
446
|
+
scanIntermediateCleanup
|
|
447
|
+
};
|
|
448
|
+
export type {
|
|
449
|
+
CleanupDisposition,
|
|
450
|
+
CleanupKind,
|
|
451
|
+
FileIdentity,
|
|
452
|
+
IntermediateCleanupItem,
|
|
453
|
+
IntermediateCleanupOptions,
|
|
454
|
+
IntermediateCleanupReport
|
|
455
|
+
};
|
package/lib/task/invalidation.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { findSectionRange, parseTable } from './sections.ts';
|
|
3
|
+
import { scanVisibleMarkdown } from './markdown.ts';
|
|
2
4
|
|
|
3
5
|
const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'] as const;
|
|
4
6
|
const OPERATION_COLUMNS = [
|
|
@@ -84,58 +86,14 @@ function createInvalidationOperation(
|
|
|
84
86
|
};
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let start = 0;
|
|
94
|
-
let escaped = false;
|
|
95
|
-
for (let index = 0; index < inner.length; index += 1) {
|
|
96
|
-
const char = inner[index]!;
|
|
97
|
-
if (char === '|' && !escaped) {
|
|
98
|
-
cells.push(inner.slice(start, index));
|
|
99
|
-
start = index + 1;
|
|
100
|
-
}
|
|
101
|
-
escaped = char === '\\' && !escaped;
|
|
102
|
-
if (char !== '\\') escaped = false;
|
|
89
|
+
function parseInvalidationTable(body: string, heading: string, columns: readonly string[]) {
|
|
90
|
+
const section = findSectionRange(body, [heading], 3);
|
|
91
|
+
const lines = scanVisibleMarkdown(body).lines.filter((line) => line.text.trim());
|
|
92
|
+
const table = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
|
|
93
|
+
if (!section || !table || lines.filter((line) => line.start >= section.bodyStart && line.start < section.end).length !== table.rows.length + 2) {
|
|
94
|
+
throw new Error(`invalidation ${heading} section must contain only its canonical table`);
|
|
103
95
|
}
|
|
104
|
-
|
|
105
|
-
return cells.map((cell) => cell.replace(/\\([\\|])/g, '$1').trim());
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isSeparator(cells: readonly string[], count: number): boolean {
|
|
109
|
-
return cells.length === count && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function sectionBody(content: string): string | null {
|
|
113
|
-
const heading = INVALIDATION_HEADINGS.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
114
|
-
const match = new RegExp(`^##\\s+(${heading})\\s*$`, 'm').exec(content);
|
|
115
|
-
if (!match) return null;
|
|
116
|
-
const start = (match.index ?? 0) + match[0].length;
|
|
117
|
-
const rest = content.slice(start);
|
|
118
|
-
const end = rest.search(/^##\s+/m);
|
|
119
|
-
return rest.slice(0, end < 0 ? rest.length : end);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function parseTable(body: string, heading: string, columns: readonly string[]): Record<string, string>[] | null | false {
|
|
123
|
-
const marker = new RegExp(`^###\\s+${heading}\\s*$`, 'm').exec(body);
|
|
124
|
-
if (!marker) return null;
|
|
125
|
-
const start = (marker.index ?? 0) + marker[0].length;
|
|
126
|
-
const rest = body.slice(start);
|
|
127
|
-
const end = rest.search(/^###\s+/m);
|
|
128
|
-
const lines = rest.slice(0, end < 0 ? rest.length : end).split(/\r?\n/).filter((line) => line.trim());
|
|
129
|
-
const header = lines.length > 0 ? lineCells(lines[0]!) : null;
|
|
130
|
-
const separator = lines.length > 1 ? lineCells(lines[1]!) : null;
|
|
131
|
-
if (!header || !separator || header.length !== columns.length || !header.every((value, index) => value === columns[index]) || !isSeparator(separator, columns.length)) return false;
|
|
132
|
-
const rows: Record<string, string>[] = [];
|
|
133
|
-
for (const line of lines.slice(2)) {
|
|
134
|
-
const cells = lineCells(line);
|
|
135
|
-
if (!cells || cells.length !== columns.length) return false;
|
|
136
|
-
rows.push(Object.fromEntries(columns.map((column, index) => [column, cells[index]!])));
|
|
137
|
-
}
|
|
138
|
-
return rows;
|
|
96
|
+
return table.rows;
|
|
139
97
|
}
|
|
140
98
|
|
|
141
99
|
function required(value: string, field: string): string {
|
|
@@ -155,15 +113,13 @@ function numberValue(value: string, field: string, allowZero = true): number {
|
|
|
155
113
|
}
|
|
156
114
|
|
|
157
115
|
function parseInvalidationDocument(content: string): InvalidationParseResult {
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
116
|
+
const section = findSectionRange(content, INVALIDATION_HEADINGS);
|
|
117
|
+
if (section === null) return { ok: true, present: false, document: { operations: [], targets: [] } };
|
|
160
118
|
try {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
const operations = operationRows.map((row) => ({
|
|
119
|
+
const body = content.slice(section.bodyStart, section.end);
|
|
120
|
+
const operationRows = parseInvalidationTable(body, 'Operations', OPERATION_COLUMNS);
|
|
121
|
+
const targetRows = parseInvalidationTable(body, 'Targets', TARGET_COLUMNS);
|
|
122
|
+
const operations = operationRows.map(({ values: row }) => ({
|
|
167
123
|
operationId: required(row.operation_id!, 'operation_id'), sourceFamily: required(row.source_family!, 'source_family'),
|
|
168
124
|
sourceArtifact: required(row.source_artifact!, 'source_artifact'), sourceRound: numberValue(row.source_round!, 'source_round', false),
|
|
169
125
|
sourceSha256: required(row.source_sha256!, 'source_sha256'), status: status(row.status!, 'operation status'),
|
|
@@ -171,7 +127,7 @@ function parseInvalidationDocument(content: string): InvalidationParseResult {
|
|
|
171
127
|
createdAt: required(row.created_at!, 'created_at'), updatedAt: required(row.updated_at!, 'updated_at'),
|
|
172
128
|
completedAt: row.completed_at ?? '', error: row.error ?? ''
|
|
173
129
|
} satisfies InvalidationOperation));
|
|
174
|
-
const targets = targetRows.map((row) => ({
|
|
130
|
+
const targets = targetRows.map(({ values: row }) => ({
|
|
175
131
|
targetId: required(row.target_id!, 'target_id'), operationId: required(row.operation_id!, 'operation_id'),
|
|
176
132
|
targetKind: row.target_kind as InvalidationTargetKind, targetFamily: required(row.target_family!, 'target_family'),
|
|
177
133
|
targetArtifact: required(row.target_artifact!, 'target_artifact'), targetRound: numberValue(row.target_round!, 'target_round', false),
|
|
@@ -297,11 +253,11 @@ function isArtifactInvalidated(document: InvalidationDocument, family: string, a
|
|
|
297
253
|
}
|
|
298
254
|
|
|
299
255
|
function invalidationMutation(content: string, document: InvalidationDocument) {
|
|
300
|
-
const existing =
|
|
256
|
+
const existing = findSectionRange(content, INVALIDATION_HEADINGS);
|
|
301
257
|
return {
|
|
302
258
|
kind: 'section' as const,
|
|
303
259
|
aliases: INVALIDATION_HEADINGS,
|
|
304
|
-
heading: existing
|
|
260
|
+
heading: existing?.heading ?? INVALIDATION_HEADINGS[0],
|
|
305
261
|
body: renderInvalidation(document)
|
|
306
262
|
};
|
|
307
263
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
parseImplementationInputs,
|
|
9
9
|
renderImplementationInputs
|
|
10
10
|
} from './implementation-inputs.ts';
|
|
11
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
11
12
|
import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.ts';
|
|
12
13
|
import type { LedgerRow, ReviewStage } from './ledger.ts';
|
|
13
14
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
@@ -44,16 +45,6 @@ type LedgerIntentResult = {
|
|
|
44
45
|
const PREFIX: Record<ReviewStage, string> = { analysis: 'AN', plan: 'PL', code: 'CD' };
|
|
45
46
|
const RESPONSE = new Set<ExecutorResponse>(['accepted', 'adjusted', 'refuted', 'cannot-judge']);
|
|
46
47
|
const DISPOSITION = new Set<ReviewDisposition>(['confirmed', 'closed', 'open', 'needs-human-decision']);
|
|
47
|
-
const REVIEW_ARTIFACT: Record<ReviewStage, RegExp> = {
|
|
48
|
-
analysis: /^review-analysis(?:-r(?:[2-9]|[1-9]\d+))?\.md$/,
|
|
49
|
-
plan: /^review-plan(?:-r(?:[2-9]|[1-9]\d+))?\.md$/,
|
|
50
|
-
code: /^review-code(?:-r(?:[2-9]|[1-9]\d+))?\.md$/
|
|
51
|
-
};
|
|
52
|
-
const ARTIFACT: Record<ReviewStage, RegExp> = {
|
|
53
|
-
analysis: /^analysis(?:-r(?:[2-9]|[1-9]\d+))?\.md$/,
|
|
54
|
-
plan: /^plan(?:-r(?:[2-9]|[1-9]\d+))?\.md$/,
|
|
55
|
-
code: /^code(?:-r(?:[2-9]|[1-9]\d+))?\.md$/
|
|
56
|
-
};
|
|
57
48
|
|
|
58
49
|
function failed(intent: LedgerIntent, code: string, message: string, taskId: string | null = null, entityId: string | null = null): LedgerIntentResult {
|
|
59
50
|
return { status: 'failed', changed: false, intent: intent.kind, taskId, entityId, before: null, after: null, operations: [], error: { code, message } };
|
|
@@ -87,11 +78,6 @@ function nextFindingId(rows: readonly LedgerRow[], stage: ReviewStage): string {
|
|
|
87
78
|
return `${prefix}-${max + 1}`;
|
|
88
79
|
}
|
|
89
80
|
|
|
90
|
-
function reviewArtifactRound(reviewArtifact: string): number {
|
|
91
|
-
const match = /-r([1-9]\d*)\.md$/.exec(reviewArtifact);
|
|
92
|
-
return match ? Number(match[1]) : 1;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
81
|
function rowMutation(row: LedgerRow): TaskMutation {
|
|
96
82
|
return {
|
|
97
83
|
kind: 'table-row', action: 'upsert', sectionAliases: LEDGER_HEADINGS,
|
|
@@ -100,10 +86,6 @@ function rowMutation(row: LedgerRow): TaskMutation {
|
|
|
100
86
|
};
|
|
101
87
|
}
|
|
102
88
|
|
|
103
|
-
function ledgerSectionMutation(content: string): TaskMutation[] {
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
89
|
function implementationInputMutation(content: string, rows: Parameters<typeof renderImplementationInputs>[0]): TaskMutation {
|
|
108
90
|
const english = /^##\s+Activity Log\s*$/m.test(content);
|
|
109
91
|
return {
|
|
@@ -147,7 +129,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
147
129
|
const finding = rows.find((row) => row.id === intent.findingId);
|
|
148
130
|
if (!finding || finding.severity === 'decision') return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
|
|
149
131
|
const sourceFromEvidence = finding.evidence.split('#')[0];
|
|
150
|
-
if (sourceFromEvidence !== intent.sourceArtifact ||
|
|
132
|
+
if (sourceFromEvidence !== intent.sourceArtifact || parseArtifactName(intent.sourceArtifact)?.family !== `review-${finding.stage}`) {
|
|
151
133
|
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
|
|
152
134
|
}
|
|
153
135
|
try {
|
|
@@ -173,7 +155,8 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
173
155
|
let before: LedgerRow | null = null;
|
|
174
156
|
let after: LedgerRow;
|
|
175
157
|
if (intent.kind === 'finding-upsert') {
|
|
176
|
-
|
|
158
|
+
const identity = parseArtifactName(intent.reviewArtifact);
|
|
159
|
+
if (identity?.family !== `review-${intent.stage}` || !Number.isInteger(intent.ordinal) || intent.ordinal < 1 || !['blocker', 'major', 'minor'].includes(intent.severity)) {
|
|
177
160
|
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'finding identity, stage, ordinal, or severity is invalid', resolved.taskId);
|
|
178
161
|
}
|
|
179
162
|
const evidence = normalizedEvidence(intent.evidence);
|
|
@@ -191,12 +174,12 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
191
174
|
} else {
|
|
192
175
|
const id = nextFindingId(rows, intent.stage);
|
|
193
176
|
after = {
|
|
194
|
-
id, stage: intent.stage, round: String(
|
|
177
|
+
id, stage: intent.stage, round: String(identity.round),
|
|
195
178
|
severity: intent.severity, status: 'open', evidence, sourceLine: -1
|
|
196
179
|
};
|
|
197
180
|
}
|
|
198
181
|
} else if (intent.kind === 'decision-upsert') {
|
|
199
|
-
if (!/^HD-[1-9]\d*$/.test(intent.id) || !
|
|
182
|
+
if (!/^HD-[1-9]\d*$/.test(intent.id) || !['analysis', 'plan', 'code'].includes(intent.stage) || parseArtifactName(intent.artifact)?.family !== intent.stage) {
|
|
200
183
|
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'decision id, stage, or artifact is invalid', resolved.taskId, intent.id);
|
|
201
184
|
}
|
|
202
185
|
const evidence = `${intent.artifact}#${intent.id}`;
|
|
@@ -266,7 +249,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
266
249
|
|
|
267
250
|
const result = writeTask({
|
|
268
251
|
taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active',
|
|
269
|
-
mutations: [
|
|
252
|
+
mutations: [rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
|
|
270
253
|
dryRun: 'dryRun' in intent ? intent.dryRun : false
|
|
271
254
|
}, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
272
255
|
return mapWrite(intent, after.id, before, after, result);
|
package/lib/task/ledger.ts
CHANGED
|
@@ -51,9 +51,7 @@ function isReviewStage(stage: string): stage is ReviewStage {
|
|
|
51
51
|
return stage === 'analysis' || stage === 'plan' || stage === 'code';
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Parse
|
|
55
|
-
// header row (`| id | ... |`) and the `|---|` separator; ignores non-`|` lines.
|
|
56
|
-
// Rows with fewer than 6 columns are skipped (mirrors the JS gate parser).
|
|
54
|
+
// Parse ledger rows using the shared strict table structure contract.
|
|
57
55
|
function parseLedgerDocument(content: string): LedgerDocument {
|
|
58
56
|
const table = parseTable(content, { sectionAliases: LEDGER_HEADINGS, columns: LEDGER_COLUMNS });
|
|
59
57
|
if (!table) return { present: false, rows: [] };
|