@fitlab-ai/agent-infra 0.11.4 → 0.11.5
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/dist/bin/internal-cli.js +0 -5
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +9 -2
- package/dist/lib/internal/task-ledger.js +5 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-operations.js +36 -2
- package/dist/lib/task/artifact-schema.js +7 -7
- package/dist/lib/task/control-authority.js +9 -166
- package/dist/lib/task/decision-intents.js +2 -4
- package/dist/lib/task/events.js +7 -28
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +123 -12
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +3 -6
- package/dist/lib/task/lifecycle.js +50 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -28
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +1 -2
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +16 -11
- package/dist/lib/task/verification.js +1 -13
- package/dist/lib/task/workflow-warning-intents.js +2 -4
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +9 -2
- package/lib/internal/task-ledger.ts +5 -38
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +1 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/removal.ts +23 -7
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-operations.ts +58 -2
- package/lib/task/artifact-schema.ts +9 -7
- package/lib/task/control-authority.ts +14 -188
- package/lib/task/decision-intents.ts +2 -4
- package/lib/task/events.ts +8 -27
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +120 -12
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +3 -6
- package/lib/task/lifecycle.ts +45 -13
- package/lib/task/local-artifact-finalization.ts +16 -35
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +1 -4
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +20 -10
- package/lib/task/verification.ts +1 -15
- package/lib/task/workflow-warning-intents.ts +2 -4
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
package/lib/task/finalization.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
|
10
10
|
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
11
11
|
import {
|
|
12
12
|
applyTaskLifecycle,
|
|
13
|
+
inspectTaskLifecycleProgress,
|
|
13
14
|
type TaskLifecycleOptions,
|
|
14
15
|
type TaskLifecycleRequest,
|
|
15
16
|
type TaskLifecycleResult
|
|
@@ -25,6 +26,9 @@ import {
|
|
|
25
26
|
type OperationWarning,
|
|
26
27
|
type OperationWarningSeverity
|
|
27
28
|
} from './operation-outcome.ts';
|
|
29
|
+
import { readTaskFinalizationHandoff } from './finalization-handoff.ts';
|
|
30
|
+
import { readSandboxControlManifest } from '../sandbox/control/lifecycle.ts';
|
|
31
|
+
import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.ts';
|
|
28
32
|
|
|
29
33
|
const RECEIPT_VERSION = 4 as const;
|
|
30
34
|
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
|
|
@@ -55,11 +59,14 @@ type TaskFinalizationRequest = Readonly<{
|
|
|
55
59
|
taskRef: string;
|
|
56
60
|
intent: 'complete';
|
|
57
61
|
agent: string;
|
|
62
|
+
handoffSha256?: string;
|
|
58
63
|
}>;
|
|
59
64
|
|
|
60
65
|
type TaskFinalizationOptions = Readonly<{
|
|
61
66
|
repoRoot: string;
|
|
62
67
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
68
|
+
handoffDirectory?: string;
|
|
69
|
+
manifestPath?: string;
|
|
63
70
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
64
71
|
lifecycle?: typeof applyTaskLifecycle;
|
|
65
72
|
backfill?: typeof backfillCompletionComments;
|
|
@@ -198,20 +205,32 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
|
|
|
198
205
|
const receipt = value as Record<string, unknown>;
|
|
199
206
|
const controlBinding = receipt.controlBinding as Record<string, unknown> | null | undefined;
|
|
200
207
|
const states = ['pending', 'done', 'skipped'];
|
|
208
|
+
const requiredKeys = [
|
|
209
|
+
'version', 'taskId', 'intent', 'receiptId', 'revision', 'lifecycle', 'taskComment',
|
|
210
|
+
'verification', 'summary', 'warningProjection', 'warnings', 'updatedAt', 'lastError'
|
|
211
|
+
];
|
|
212
|
+
if (receipt.controlBinding !== undefined) requiredKeys.push('controlBinding');
|
|
201
213
|
if (
|
|
214
|
+
Object.keys(receipt).sort().join('\0') !== requiredKeys.sort().join('\0')
|
|
215
|
+
||
|
|
202
216
|
receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
|
|
203
217
|
|| typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
|
|
204
218
|
|| !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
|
|
205
|
-
|| !states.includes(
|
|
206
|
-
|| !states.includes(
|
|
207
|
-
|| !states.includes(
|
|
208
|
-
|| !states.includes(
|
|
209
|
-
|| !['pending', 'done'].includes(
|
|
219
|
+
|| !states.includes(receipt.lifecycle as string)
|
|
220
|
+
|| !states.includes(receipt.taskComment as string)
|
|
221
|
+
|| !states.includes(receipt.verification as string)
|
|
222
|
+
|| !states.includes(receipt.summary as string)
|
|
223
|
+
|| !['pending', 'done'].includes(receipt.warningProjection as string)
|
|
210
224
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
211
|
-
|| typeof receipt.updatedAt !== 'string'
|
|
212
|
-
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
225
|
+
|| typeof receipt.updatedAt !== 'string' || receipt.updatedAt.length === 0
|
|
226
|
+
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
227
|
+
|| Object.keys(receipt.lastError).sort().join('\0') !== ['code', 'message', 'retryable'].join('\0')
|
|
228
|
+
|| typeof (receipt.lastError as Record<string, unknown>).code !== 'string'
|
|
229
|
+
|| typeof (receipt.lastError as Record<string, unknown>).message !== 'string'
|
|
230
|
+
|| typeof (receipt.lastError as Record<string, unknown>).retryable !== 'boolean'))
|
|
213
231
|
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
214
232
|
|| Array.isArray(controlBinding)
|
|
233
|
+
|| Object.keys(controlBinding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
|
|
215
234
|
|| typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
|
|
216
235
|
|| typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)))
|
|
217
236
|
) throw new Error('receipt schema is invalid');
|
|
@@ -243,6 +262,14 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
|
|
|
243
262
|
}
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
export function bindTaskFinalizationReceipt(
|
|
266
|
+
repoRoot: string, taskId: string, controlBinding: Readonly<{ generation: string; requestId: string }>
|
|
267
|
+
): TaskFinalizationReceipt {
|
|
268
|
+
const receipt = readReceipt(path.resolve(repoRoot), taskId);
|
|
269
|
+
if (!receipt) throw new Error('TASK_FINALIZATION_PREPARATION_REQUIRED');
|
|
270
|
+
return updateReceipt(path.resolve(repoRoot), receipt, { controlBinding, lastError: null });
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
function updateReceipt(
|
|
247
274
|
repoRoot: string,
|
|
248
275
|
receipt: TaskFinalizationReceipt,
|
|
@@ -798,10 +825,16 @@ async function prepareUnderLock(
|
|
|
798
825
|
} catch (error) {
|
|
799
826
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
|
|
800
827
|
}
|
|
801
|
-
if (options.controlBinding && (
|
|
802
|
-
receipt.controlBinding
|
|
803
|
-
|
|
804
|
-
|
|
828
|
+
if (options.controlBinding && (receipt.controlBinding?.generation !== options.controlBinding.generation
|
|
829
|
+
|| receipt.controlBinding.requestId !== options.controlBinding.requestId)) {
|
|
830
|
+
const state = resolveTaskRef(taskId, { repoRoot });
|
|
831
|
+
if (!(receipt.lifecycle === 'done' && state.ok && state.state === 'completed')) {
|
|
832
|
+
return failed(taskId, {
|
|
833
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
834
|
+
message: 'canonical receipt is not bound to the current sandbox request', retryable: false
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
805
838
|
|
|
806
839
|
const preflightState = resolveTaskRef(taskId, { repoRoot });
|
|
807
840
|
if (options.preflight && preflightState.ok && preflightState.state === 'active') {
|
|
@@ -1043,13 +1076,88 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1043
1076
|
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
1044
1077
|
try {
|
|
1045
1078
|
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
|
|
1046
|
-
|
|
1079
|
+
if (options.controlBinding && options.manifestPath) {
|
|
1080
|
+
try {
|
|
1081
|
+
const manifest = readSandboxControlManifest(options.manifestPath);
|
|
1082
|
+
const identity = validateSandboxControlIdentity({
|
|
1083
|
+
publicStatusDir: manifest.publicStatusDir,
|
|
1084
|
+
root: path.dirname(path.resolve(options.manifestPath)),
|
|
1085
|
+
mode: manifest.mode,
|
|
1086
|
+
taskId: manifest.taskId,
|
|
1087
|
+
generation: manifest.generation,
|
|
1088
|
+
controlRootId: manifest.controlRootId
|
|
1089
|
+
});
|
|
1090
|
+
if (identity.state !== 'valid' || manifest.taskId !== resolved.taskId
|
|
1091
|
+
|| manifest.generation !== options.controlBinding.generation
|
|
1092
|
+
|| fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
|
|
1093
|
+
throw new Error('current sandbox manifest does not match finalization request');
|
|
1094
|
+
}
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
return failed(resolved.taskId, {
|
|
1097
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
1098
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
let receipt = readReceipt(repoRoot, resolved.taskId);
|
|
1103
|
+
if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
|
|
1104
|
+
try {
|
|
1105
|
+
const handoff = readTaskFinalizationHandoff(
|
|
1106
|
+
options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256
|
|
1107
|
+
);
|
|
1108
|
+
receipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
1109
|
+
if (receipt.lifecycle !== 'pending') throw new Error('handoff lifecycle must be pending');
|
|
1110
|
+
if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
|
|
1111
|
+
throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
|
|
1112
|
+
}
|
|
1113
|
+
writeReceipt(repoRoot, receipt);
|
|
1114
|
+
try { handoff.cleanup(); } catch { /* keep the persisted canonical receipt usable */ }
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
return failed(resolved.taskId, {
|
|
1117
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
1118
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1047
1122
|
if (!receipt) return failed(resolved.taskId, {
|
|
1048
1123
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
|
|
1049
1124
|
});
|
|
1050
1125
|
if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
|
|
1051
1126
|
return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
|
|
1052
1127
|
}
|
|
1128
|
+
if (options.controlBinding) {
|
|
1129
|
+
const proof = inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent);
|
|
1130
|
+
const oldBinding = receipt.controlBinding;
|
|
1131
|
+
if (!oldBinding || oldBinding.generation !== options.controlBinding.generation || proof === 'unknown') {
|
|
1132
|
+
return failed(resolved.taskId, {
|
|
1133
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
1134
|
+
message: 'canonical receipt has no valid host recovery proof', retryable: false
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
if (oldBinding.requestId !== options.controlBinding.requestId && proof === 'not-started') {
|
|
1138
|
+
if (receipt.lifecycle !== 'pending' || !options.handoffDirectory || !request.handoffSha256) {
|
|
1139
|
+
return failed(resolved.taskId, {
|
|
1140
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
1141
|
+
message: 'pending receipt cannot be rebound without a current handoff', retryable: false
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
const handoff = readTaskFinalizationHandoff(
|
|
1146
|
+
options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256
|
|
1147
|
+
);
|
|
1148
|
+
const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
1149
|
+
if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
|
|
1150
|
+
|| nextReceipt.lifecycle !== 'pending') throw new Error('handoff is not continuous with canonical receipt');
|
|
1151
|
+
receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
|
|
1152
|
+
try { handoff.cleanup(); } catch { /* keep the persisted canonical receipt usable */ }
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
return failed(resolved.taskId, {
|
|
1155
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
1156
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1053
1161
|
if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
|
|
1054
1162
|
return failed(resolved.taskId, {
|
|
1055
1163
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
|
|
@@ -14,26 +14,14 @@ import {
|
|
|
14
14
|
readTaskFinalizationReceipt,
|
|
15
15
|
type TaskFinalizationReceipt
|
|
16
16
|
} from './finalization.ts';
|
|
17
|
-
import {
|
|
18
|
-
semanticDigest,
|
|
19
|
-
sha256Content
|
|
20
|
-
} from './local-artifact-finalization.ts';
|
|
21
|
-
import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
22
|
-
import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
23
|
-
import { parseArtifactName } from './artifact-name.ts';
|
|
24
|
-
import type { ArtifactSchemaFamily } from './artifact-schema.ts';
|
|
25
17
|
import { enumerateAllTaskDirs, type TaskWorkspaceState } from './resolve-ref.ts';
|
|
26
18
|
import type { TaskControlBindingEvidence } from './finalization-state.ts';
|
|
27
19
|
|
|
28
20
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
29
|
-
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
|
|
30
21
|
const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
|
|
31
|
-
const AUXILIARY_ROOTS = [
|
|
32
|
-
'.local-artifact-finalization-intents',
|
|
33
|
-
'.task-commit-intents'
|
|
34
|
-
] as const;
|
|
22
|
+
const AUXILIARY_ROOTS = ['.task-commit-intents'] as const;
|
|
35
23
|
|
|
36
|
-
type CleanupKind = '
|
|
24
|
+
type CleanupKind = 'COMMIT-SYNCED' | 'EMPTY-AUX-PARENT';
|
|
37
25
|
type CleanupDisposition = 'planned' | 'protected' | 'skipped' | 'deleted' | 'failed';
|
|
38
26
|
type FileIdentity = Readonly<{ dev: string; ino: string; size: number; mtimeMs: number }>;
|
|
39
27
|
|
|
@@ -153,47 +141,6 @@ function receiptGate(
|
|
|
153
141
|
return null;
|
|
154
142
|
}
|
|
155
143
|
|
|
156
|
-
function canonicalLocalIntentPath(taskId: string, family: string, artifact: string): string {
|
|
157
|
-
return `${taskId}-${family}-${artifact}.json`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function localIntentCandidate(
|
|
161
|
-
repoRoot: string,
|
|
162
|
-
task: TaskRecord | undefined,
|
|
163
|
-
filePath: string,
|
|
164
|
-
taskId: string,
|
|
165
|
-
family: ArtifactSchemaFamily,
|
|
166
|
-
artifact: string,
|
|
167
|
-
identity: FileIdentity,
|
|
168
|
-
options: ScanOptions
|
|
169
|
-
): IntermediateCleanupItem {
|
|
170
|
-
const protectedReason = taskGate(task);
|
|
171
|
-
if (protectedReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
|
|
172
|
-
if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
|
|
173
|
-
|| !parseArtifactName(artifact)?.family || parseArtifactName(artifact)!.family !== family) {
|
|
174
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
|
|
175
|
-
}
|
|
176
|
-
let intent: ArtifactRecoveryIntent | null;
|
|
177
|
-
try { intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact); }
|
|
178
|
-
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity); }
|
|
179
|
-
if (!intent || intent.state !== 'consumed') {
|
|
180
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
|
|
181
|
-
}
|
|
182
|
-
const artifactPath = path.join(task!.taskDir, artifact);
|
|
183
|
-
if (safeLstat(task!.taskDir, artifactPath, 'file') === null) {
|
|
184
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
|
|
185
|
-
}
|
|
186
|
-
let content: string;
|
|
187
|
-
try { content = fs.readFileSync(artifactPath, 'utf8'); }
|
|
188
|
-
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity); }
|
|
189
|
-
if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
|
|
190
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
|
|
191
|
-
}
|
|
192
|
-
const receiptReason = receiptGate(repoRoot, task!, options);
|
|
193
|
-
if (receiptReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
|
|
194
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
144
|
function commitIntentCandidate(
|
|
198
145
|
repoRoot: string,
|
|
199
146
|
task: TaskRecord | undefined,
|
|
@@ -258,23 +205,12 @@ function readAuxiliaryCandidates(
|
|
|
258
205
|
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
259
206
|
continue;
|
|
260
207
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (!
|
|
264
|
-
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
candidates.push(localIntentCandidate(
|
|
268
|
-
repoRoot, tasks.get(match[1]!), filePath, match[1]!, match[2] as 'analysis' | 'plan' | 'code', match[3]!, identity, options
|
|
269
|
-
));
|
|
270
|
-
} else {
|
|
271
|
-
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
272
|
-
if (!match || (selected && !selected.has(match[1]!))) {
|
|
273
|
-
if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
|
|
208
|
+
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
209
|
+
if (!match || (selected && !selected.has(match[1]!))) {
|
|
210
|
+
if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
211
|
+
continue;
|
|
277
212
|
}
|
|
213
|
+
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
|
|
278
214
|
}
|
|
279
215
|
if (entries.length === 0) {
|
|
280
216
|
candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
|
|
@@ -307,56 +243,7 @@ function scanIntermediateCleanup(repoRoot: string, options: ScanOptions = {}): I
|
|
|
307
243
|
return buildIntermediateCleanupReport(candidates, true);
|
|
308
244
|
}
|
|
309
245
|
|
|
310
|
-
function
|
|
311
|
-
if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId) return true;
|
|
312
|
-
if (!task) return false;
|
|
313
|
-
const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
|
|
314
|
-
if (!match) return false;
|
|
315
|
-
let intent: ReturnType<typeof readArtifactRecoveryIntent>;
|
|
316
|
-
try { intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2] as ArtifactSchemaFamily, match[3]!); }
|
|
317
|
-
catch { return false; }
|
|
318
|
-
if (!intent || intent.state !== 'consumed') return false;
|
|
319
|
-
|
|
320
|
-
const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
|
|
321
|
-
let stat: fs.Stats | null = null;
|
|
322
|
-
try { stat = fs.lstatSync(recovery); } catch (error) {
|
|
323
|
-
if (errorCode(error) === 'ENOENT') return true;
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
327
|
-
let entries: fs.Dirent[];
|
|
328
|
-
try { entries = fs.readdirSync(recovery, { withFileTypes: true }); } catch { return false; }
|
|
329
|
-
for (const entry of entries) {
|
|
330
|
-
if (entry.isSymbolicLink()) return false;
|
|
331
|
-
if (entry.isFile()) {
|
|
332
|
-
if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (!entry.isDirectory() || entry.name !== 'generations') return false;
|
|
336
|
-
let generations: fs.Dirent[];
|
|
337
|
-
try { generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true }); } catch { return false; }
|
|
338
|
-
if (generations.some((generation) => generation.isSymbolicLink()
|
|
339
|
-
|| !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name))) return false;
|
|
340
|
-
}
|
|
341
|
-
try {
|
|
342
|
-
for (const entry of entries) {
|
|
343
|
-
const target = path.join(recovery, entry.name);
|
|
344
|
-
if (entry.isFile()) fs.unlinkSync(target);
|
|
345
|
-
else {
|
|
346
|
-
for (const generation of fs.readdirSync(target, { withFileTypes: true })) fs.unlinkSync(path.join(target, generation.name));
|
|
347
|
-
fs.rmdirSync(target);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
fs.rmdirSync(recovery);
|
|
351
|
-
const recoveryRoot = path.dirname(recovery);
|
|
352
|
-
if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
|
|
353
|
-
return true;
|
|
354
|
-
} catch (error) {
|
|
355
|
-
return errorCode(error) === 'ENOENT';
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, task?: TaskRecord): IntermediateCleanupItem {
|
|
246
|
+
function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem): IntermediateCleanupItem {
|
|
360
247
|
if (candidate.disposition !== 'planned') return candidate;
|
|
361
248
|
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
362
249
|
? path.dirname(path.dirname(candidate.path))
|
|
@@ -373,9 +260,6 @@ function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, t
|
|
|
373
260
|
const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
|
|
374
261
|
if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
375
262
|
if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
376
|
-
if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
|
|
377
|
-
return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
|
|
378
|
-
}
|
|
379
263
|
try {
|
|
380
264
|
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
381
265
|
if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
|
|
@@ -434,7 +318,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
434
318
|
if (initialCandidate.disposition !== 'planned') continue;
|
|
435
319
|
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
436
320
|
items[index] = current
|
|
437
|
-
? removeCandidate(repoRoot, current
|
|
321
|
+
? removeCandidate(repoRoot, current)
|
|
438
322
|
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
439
323
|
}
|
|
440
324
|
};
|
|
@@ -14,7 +14,6 @@ import type { LedgerRow, ReviewStage } from './ledger.ts';
|
|
|
14
14
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
15
15
|
import { writeTask } from './write.ts';
|
|
16
16
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
17
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
18
17
|
import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
|
|
19
18
|
import type { ReworkClassification, ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
20
19
|
import { extractSection, extractSubSection } from './sections.ts';
|
|
@@ -149,8 +148,7 @@ function mapWrite(intent: LedgerIntent, entityId: string, before: LedgerRow | nu
|
|
|
149
148
|
function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {}): LedgerIntentResult {
|
|
150
149
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
151
150
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
152
|
-
|
|
153
|
-
if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
151
|
+
if (resolved.state !== 'active') return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
154
152
|
let content: string;
|
|
155
153
|
let rows: LedgerRow[];
|
|
156
154
|
try {
|
|
@@ -353,8 +351,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
353
351
|
? new Set(['confirmed', 'open', 'needs-human-decision'])
|
|
354
352
|
: before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set<string>();
|
|
355
353
|
const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
|
|
356
|
-
if (
|
|
357
|
-
&& !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
|
|
354
|
+
if (!sameRoundMinorClose && !allowed.has(intent.status)) {
|
|
358
355
|
return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
|
|
359
356
|
}
|
|
360
357
|
after = { ...before, status: intent.status, evidence };
|
|
@@ -388,7 +385,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
388
385
|
}
|
|
389
386
|
|
|
390
387
|
const result = writeTask({
|
|
391
|
-
taskRef: intent.taskRef, expectedState:
|
|
388
|
+
taskRef: intent.taskRef, expectedState: 'active',
|
|
392
389
|
mutations: [rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
|
|
393
390
|
dryRun: 'dryRun' in intent ? intent.dryRun : false
|
|
394
391
|
}, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -107,16 +107,11 @@ type TaskLifecycleOptions = {
|
|
|
107
107
|
fileSystem?: Partial<LifecycleFileSystem>;
|
|
108
108
|
taskFileSystem?: Partial<TaskFileSystem>;
|
|
109
109
|
directoryRenameSync?: (source: string, target: string) => void;
|
|
110
|
-
manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
|
|
111
110
|
/** Write the terminal task document and durable journal, but leave the move
|
|
112
111
|
* and short-id mutation for a separately authorized commit. */
|
|
113
112
|
prepareOnly?: boolean;
|
|
114
113
|
};
|
|
115
114
|
|
|
116
|
-
function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
|
|
117
|
-
return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
115
|
const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
|
|
121
116
|
const DEFAULT_IO: LifecycleFileSystem = {
|
|
122
117
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
@@ -362,6 +357,44 @@ export function readLifecycleJournalEvidence(repoRoot: string, taskId: string):
|
|
|
362
357
|
} catch { return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' }; }
|
|
363
358
|
}
|
|
364
359
|
|
|
360
|
+
export function inspectTaskLifecycleProgress(
|
|
361
|
+
repoRoot: string, taskId: string, agent: string
|
|
362
|
+
): 'not-started' | 'started-recoverable' | 'unknown' {
|
|
363
|
+
try {
|
|
364
|
+
const hot = locateHotTaskDirs(repoRoot, taskId);
|
|
365
|
+
if (hot.length !== 1 || (hot[0]!.state !== 'active' && hot[0]!.state !== 'completed')) return 'unknown';
|
|
366
|
+
const { state, taskDir } = hot[0]!;
|
|
367
|
+
const content = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
368
|
+
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
369
|
+
if (frontmatter.id !== taskId) return 'unknown';
|
|
370
|
+
const journalPath = path.join(taskDir, '.task-lifecycle.json');
|
|
371
|
+
if (!fs.existsSync(journalPath)) {
|
|
372
|
+
if (state === 'active') return frontmatter.status === 'active' ? 'not-started' : 'unknown';
|
|
373
|
+
return matchingCompletion(content, { taskRef: taskId, intent: 'complete', agent })
|
|
374
|
+
? 'started-recoverable' : 'unknown';
|
|
375
|
+
}
|
|
376
|
+
const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
|
|
377
|
+
const request: TaskLifecycleRequest = { taskRef: taskId, intent: 'complete', agent };
|
|
378
|
+
if (journal.taskId !== taskId || journal.intent !== 'complete'
|
|
379
|
+
|| journal.intentDigest !== intentIdentity(request, taskId)
|
|
380
|
+
|| journal.sourceState !== 'active' || journal.targetState !== 'completed') return 'unknown';
|
|
381
|
+
if (journal.completedSteps.some((step, index) => STEPS[index] !== step)) return 'unknown';
|
|
382
|
+
const directoryMoved = journal.completedSteps.includes('directory-moved');
|
|
383
|
+
const registryCommitted = journal.completedSteps.includes('registry-committed');
|
|
384
|
+
const hasShortId = loadShortIdByTaskId(repoRoot).has(taskId);
|
|
385
|
+
if (state === 'active' && (directoryMoved || !hasShortId)) return 'unknown';
|
|
386
|
+
if (state === 'completed' && (!journal.completedSteps.includes('task-written')
|
|
387
|
+
|| !matchingCompletion(content, request) || (registryCommitted && hasShortId))) return 'unknown';
|
|
388
|
+
if (frontmatter.status !== state
|
|
389
|
+
&& !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
|
|
390
|
+
return 'unknown';
|
|
391
|
+
}
|
|
392
|
+
return 'started-recoverable';
|
|
393
|
+
} catch {
|
|
394
|
+
return 'unknown';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
365
398
|
type DirectoryEntry = Readonly<{ relativePath: string; kind: 'file' | 'directory'; size: number; digest: string }>;
|
|
366
399
|
|
|
367
400
|
function directoryManifest(root: string): DirectoryEntry[] {
|
|
@@ -468,7 +501,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
468
501
|
}
|
|
469
502
|
return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: `task ${taskId} is already ${spec.target} with a different lifecycle intent` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
470
503
|
}
|
|
471
|
-
if (!spec.sources.includes(sourceState) && !existingJournal
|
|
504
|
+
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
472
505
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
473
506
|
}
|
|
474
507
|
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
|
|
@@ -479,7 +512,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
479
512
|
try { frontmatter = parseTypedTaskFrontmatter(content); }
|
|
480
513
|
catch (error) { return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
|
|
481
514
|
if (frontmatter.id !== taskId) return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
482
|
-
if (!locateActivityLog(content)
|
|
515
|
+
if (!locateActivityLog(content)) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
483
516
|
if (spec.target === 'active') {
|
|
484
517
|
const contract = validateCurrentTaskContract(content);
|
|
485
518
|
if (!contract.ok) return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: contract.message }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
@@ -493,10 +526,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
493
526
|
}
|
|
494
527
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
|
|
495
528
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
496
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey
|
|
529
|
+
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
497
530
|
return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
498
531
|
}
|
|
499
|
-
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1
|
|
532
|
+
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
500
533
|
return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
501
534
|
}
|
|
502
535
|
const digest = intentIdentity(request, taskId);
|
|
@@ -514,8 +547,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
514
547
|
if (!journal.failure) return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
515
548
|
return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
|
|
516
549
|
}
|
|
517
|
-
|
|
518
|
-
if (journal.intentDigest !== digest && !overrideResumesRecordedFailure) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
550
|
+
if (journal.intentDigest !== digest) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
519
551
|
} else {
|
|
520
552
|
let metadata: TaskWriteMetadata;
|
|
521
553
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
@@ -525,7 +557,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
525
557
|
}
|
|
526
558
|
journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
|
|
527
559
|
if (request.dryRun) {
|
|
528
|
-
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles,
|
|
560
|
+
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, false);
|
|
529
561
|
if ('code' in plannedMutations) return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
530
562
|
const taskPlan = writeTask(
|
|
531
563
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true },
|
|
@@ -561,7 +593,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
561
593
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
|
|
562
594
|
if (!completed.has('task-written')) {
|
|
563
595
|
const current = io.readFileSync(path.join(sourcePath, 'task.md'));
|
|
564
|
-
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles,
|
|
596
|
+
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, false);
|
|
565
597
|
if ('code' in mutations) return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
|
|
566
598
|
const writeResult = writeTask(
|
|
567
599
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations },
|
|
@@ -2,10 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { inspectDecisionDetailDuplicates } from './decision-details.ts';
|
|
5
|
-
import { scanVisibleMarkdown } from './markdown.ts';
|
|
6
5
|
import { parseArtifactName } from './artifact-name.ts';
|
|
6
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
7
7
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
8
|
-
import { canonicalSemanticDigest,
|
|
8
|
+
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
9
9
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
10
10
|
|
|
11
11
|
type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
|
|
@@ -24,7 +24,8 @@ type LocalArtifactDiagnosticCode =
|
|
|
24
24
|
| 'LOCAL_DECISION_DETAIL_DUPLICATE'
|
|
25
25
|
| 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
26
26
|
| 'LOCAL_STRUCTURAL_INVALID'
|
|
27
|
-
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
27
|
+
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
28
|
+
| 'LOCAL_FLOW_DECISION_INVALID';
|
|
28
29
|
|
|
29
30
|
type LocalArtifactDiagnostic = {
|
|
30
31
|
code: LocalArtifactDiagnosticCode;
|
|
@@ -71,7 +72,9 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
|
|
|
71
72
|
ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
|
|
72
73
|
ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
|
|
73
74
|
ARTIFACT_DUPLICATE_SECTION: 'LOCAL_ARTIFACT_DUPLICATE_SECTION',
|
|
74
|
-
ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
75
|
+
ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION',
|
|
76
|
+
ARTIFACT_STATE_CHECK_PATTERN_MISSING: 'LOCAL_STATUS_COMMAND_MISSING',
|
|
77
|
+
ARTIFACT_REQUIRED_PATTERN_MISSING: 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
75
78
|
};
|
|
76
79
|
return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
|
|
77
80
|
}
|
|
@@ -80,47 +83,25 @@ function localDiagnostic(item: { code: string; message: string; line: number | n
|
|
|
80
83
|
return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
function isStatusPattern(pattern: string): boolean {
|
|
84
|
-
return pattern === '^\\$ ';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
86
|
function validateLocalArtifact(
|
|
88
87
|
content: string,
|
|
89
88
|
options: LocalArtifactValidationOptions
|
|
90
89
|
): LocalArtifactValidationResult {
|
|
91
90
|
const schema = getArtifactSchema(options.family)!;
|
|
92
91
|
const diagnostics: LocalArtifactDiagnostic[] = [];
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
|
|
96
|
-
|
|
97
|
-
const statusSection = schema.sections.find((section) => section.id === 'state-check');
|
|
98
|
-
if (statusSection) {
|
|
99
|
-
const statusHeading = scanned.headings.find((heading) => (
|
|
100
|
-
heading.level === 2 && [statusSection.headings.zh, statusSection.headings.en].some((name) => (
|
|
101
|
-
heading.text === name || heading.text === `${name}:` || heading.text === `${name}:`
|
|
102
|
-
))
|
|
103
|
-
));
|
|
104
|
-
if (statusHeading) {
|
|
105
|
-
const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
|
|
106
|
-
const body = content.slice(statusHeading.end, next?.start ?? content.length);
|
|
107
|
-
for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
|
|
108
|
-
if (!new RegExp(pattern, 'm').test(body)) {
|
|
109
|
-
diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, line: content.slice(0, statusHeading.start).split('\n').length });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const patternInspection = inspectArtifactPatterns(content, {
|
|
116
|
-
...schema,
|
|
117
|
-
requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
|
|
118
|
-
});
|
|
119
|
-
for (const item of patternInspection.diagnostics) diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
|
|
92
|
+
const inspection = inspectArtifactContract(content, schema, { scopeStateCheckSection: true });
|
|
93
|
+
for (const item of inspection.diagnostics) diagnostics.push(localDiagnostic(item));
|
|
120
94
|
|
|
121
95
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
122
96
|
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
123
97
|
|
|
98
|
+
if (options.family === 'analysis') {
|
|
99
|
+
const flowDecision = parseLifecyclePathDecision(content);
|
|
100
|
+
if (flowDecision.status !== 'valid') {
|
|
101
|
+
diagnostics.push({ code: 'LOCAL_FLOW_DECISION_INVALID', message: flowDecision.message, line: null });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
124
105
|
return {
|
|
125
106
|
ok: diagnostics.length === 0,
|
|
126
107
|
family: options.family,
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { ReviewFinalizationRequest } from './review-finalization.ts';
|
|
2
2
|
|
|
3
|
-
export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest
|
|
4
|
-
overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
|
|
5
|
-
} {
|
|
3
|
+
export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest {
|
|
6
4
|
if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? '')) throw new Error('task ref and a supported review operation are required');
|
|
7
5
|
const values: Record<string, string | boolean> = {};
|
|
8
6
|
const flags = {
|
|
9
|
-
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
|
|
10
|
-
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
7
|
+
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
|
|
11
8
|
} as const;
|
|
12
9
|
for (let index = 2; index < args.length; index += 1) {
|
|
13
10
|
const flag = args[index]!;
|