@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
|
@@ -6,13 +6,16 @@ import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
|
|
|
6
6
|
import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
|
|
7
7
|
import { taskIssueIdentity } from '../platform/task-identities.js';
|
|
8
8
|
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
9
|
-
import { applyTaskLifecycle } from './lifecycle.js';
|
|
9
|
+
import { applyTaskLifecycle, inspectTaskLifecycleProgress } from './lifecycle.js';
|
|
10
10
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
11
11
|
import { inspectShortIdRegistry } from './short-id.js';
|
|
12
12
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
13
13
|
import { verifyTaskEvent } from './verification.js';
|
|
14
14
|
import { projectFinalizationWarning } from './workflow-warning-intents.js';
|
|
15
15
|
import { mergeOperationWarnings } from './operation-outcome.js';
|
|
16
|
+
import { readTaskFinalizationHandoff } from './finalization-handoff.js';
|
|
17
|
+
import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
|
|
18
|
+
import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.js';
|
|
16
19
|
const RECEIPT_VERSION = 4;
|
|
17
20
|
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
|
|
18
21
|
function finalizationRoot(repoRoot) {
|
|
@@ -89,19 +92,32 @@ function validateReceipt(value, taskId) {
|
|
|
89
92
|
const receipt = value;
|
|
90
93
|
const controlBinding = receipt.controlBinding;
|
|
91
94
|
const states = ['pending', 'done', 'skipped'];
|
|
92
|
-
|
|
95
|
+
const requiredKeys = [
|
|
96
|
+
'version', 'taskId', 'intent', 'receiptId', 'revision', 'lifecycle', 'taskComment',
|
|
97
|
+
'verification', 'summary', 'warningProjection', 'warnings', 'updatedAt', 'lastError'
|
|
98
|
+
];
|
|
99
|
+
if (receipt.controlBinding !== undefined)
|
|
100
|
+
requiredKeys.push('controlBinding');
|
|
101
|
+
if (Object.keys(receipt).sort().join('\0') !== requiredKeys.sort().join('\0')
|
|
102
|
+
||
|
|
103
|
+
receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
|
|
93
104
|
|| typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
|
|
94
105
|
|| !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
|
|
95
|
-
|| !states.includes(
|
|
96
|
-
|| !states.includes(
|
|
97
|
-
|| !states.includes(
|
|
98
|
-
|| !states.includes(
|
|
99
|
-
|| !['pending', 'done'].includes(
|
|
106
|
+
|| !states.includes(receipt.lifecycle)
|
|
107
|
+
|| !states.includes(receipt.taskComment)
|
|
108
|
+
|| !states.includes(receipt.verification)
|
|
109
|
+
|| !states.includes(receipt.summary)
|
|
110
|
+
|| !['pending', 'done'].includes(receipt.warningProjection)
|
|
100
111
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
101
|
-
|| typeof receipt.updatedAt !== 'string'
|
|
102
|
-
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
112
|
+
|| typeof receipt.updatedAt !== 'string' || receipt.updatedAt.length === 0
|
|
113
|
+
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
|
|
114
|
+
|| Object.keys(receipt.lastError).sort().join('\0') !== ['code', 'message', 'retryable'].join('\0')
|
|
115
|
+
|| typeof receipt.lastError.code !== 'string'
|
|
116
|
+
|| typeof receipt.lastError.message !== 'string'
|
|
117
|
+
|| typeof receipt.lastError.retryable !== 'boolean'))
|
|
103
118
|
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
104
119
|
|| Array.isArray(controlBinding)
|
|
120
|
+
|| Object.keys(controlBinding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
|
|
105
121
|
|| typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
|
|
106
122
|
|| typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId))))
|
|
107
123
|
throw new Error('receipt schema is invalid');
|
|
@@ -134,6 +150,12 @@ function writeReceipt(repoRoot, receipt) {
|
|
|
134
150
|
throw error;
|
|
135
151
|
}
|
|
136
152
|
}
|
|
153
|
+
export function bindTaskFinalizationReceipt(repoRoot, taskId, controlBinding) {
|
|
154
|
+
const receipt = readReceipt(path.resolve(repoRoot), taskId);
|
|
155
|
+
if (!receipt)
|
|
156
|
+
throw new Error('TASK_FINALIZATION_PREPARATION_REQUIRED');
|
|
157
|
+
return updateReceipt(path.resolve(repoRoot), receipt, { controlBinding, lastError: null });
|
|
158
|
+
}
|
|
137
159
|
function updateReceipt(repoRoot, receipt, patch) {
|
|
138
160
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
139
161
|
if (!current || current.receiptId !== receipt.receiptId)
|
|
@@ -629,8 +651,15 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
629
651
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
|
|
630
652
|
}
|
|
631
653
|
if (options.controlBinding && (receipt.controlBinding?.generation !== options.controlBinding.generation
|
|
632
|
-
|| receipt.controlBinding.requestId !== options.controlBinding.requestId))
|
|
633
|
-
|
|
654
|
+
|| receipt.controlBinding.requestId !== options.controlBinding.requestId)) {
|
|
655
|
+
const state = resolveTaskRef(taskId, { repoRoot });
|
|
656
|
+
if (!(receipt.lifecycle === 'done' && state.ok && state.state === 'completed')) {
|
|
657
|
+
return failed(taskId, {
|
|
658
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
659
|
+
message: 'canonical receipt is not bound to the current sandbox request', retryable: false
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
634
663
|
const preflightState = resolveTaskRef(taskId, { repoRoot });
|
|
635
664
|
if (options.preflight && preflightState.ok && preflightState.state === 'active') {
|
|
636
665
|
try {
|
|
@@ -878,7 +907,53 @@ async function commitPreparedTaskFinalization(request, options) {
|
|
|
878
907
|
return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
879
908
|
try {
|
|
880
909
|
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
|
|
881
|
-
|
|
910
|
+
if (options.controlBinding && options.manifestPath) {
|
|
911
|
+
try {
|
|
912
|
+
const manifest = readSandboxControlManifest(options.manifestPath);
|
|
913
|
+
const identity = validateSandboxControlIdentity({
|
|
914
|
+
publicStatusDir: manifest.publicStatusDir,
|
|
915
|
+
root: path.dirname(path.resolve(options.manifestPath)),
|
|
916
|
+
mode: manifest.mode,
|
|
917
|
+
taskId: manifest.taskId,
|
|
918
|
+
generation: manifest.generation,
|
|
919
|
+
controlRootId: manifest.controlRootId
|
|
920
|
+
});
|
|
921
|
+
if (identity.state !== 'valid' || manifest.taskId !== resolved.taskId
|
|
922
|
+
|| manifest.generation !== options.controlBinding.generation
|
|
923
|
+
|| fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
|
|
924
|
+
throw new Error('current sandbox manifest does not match finalization request');
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
return failed(resolved.taskId, {
|
|
929
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
930
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
let receipt = readReceipt(repoRoot, resolved.taskId);
|
|
935
|
+
if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
|
|
936
|
+
try {
|
|
937
|
+
const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
|
|
938
|
+
receipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
939
|
+
if (receipt.lifecycle !== 'pending')
|
|
940
|
+
throw new Error('handoff lifecycle must be pending');
|
|
941
|
+
if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
|
|
942
|
+
throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
|
|
943
|
+
}
|
|
944
|
+
writeReceipt(repoRoot, receipt);
|
|
945
|
+
try {
|
|
946
|
+
handoff.cleanup();
|
|
947
|
+
}
|
|
948
|
+
catch { /* keep the persisted canonical receipt usable */ }
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
return failed(resolved.taskId, {
|
|
952
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
953
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
882
957
|
if (!receipt)
|
|
883
958
|
return failed(resolved.taskId, {
|
|
884
959
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
|
|
@@ -886,6 +961,42 @@ async function commitPreparedTaskFinalization(request, options) {
|
|
|
886
961
|
if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
|
|
887
962
|
return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
|
|
888
963
|
}
|
|
964
|
+
if (options.controlBinding) {
|
|
965
|
+
const proof = inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent);
|
|
966
|
+
const oldBinding = receipt.controlBinding;
|
|
967
|
+
if (!oldBinding || oldBinding.generation !== options.controlBinding.generation || proof === 'unknown') {
|
|
968
|
+
return failed(resolved.taskId, {
|
|
969
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
970
|
+
message: 'canonical receipt has no valid host recovery proof', retryable: false
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (oldBinding.requestId !== options.controlBinding.requestId && proof === 'not-started') {
|
|
974
|
+
if (receipt.lifecycle !== 'pending' || !options.handoffDirectory || !request.handoffSha256) {
|
|
975
|
+
return failed(resolved.taskId, {
|
|
976
|
+
code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
|
|
977
|
+
message: 'pending receipt cannot be rebound without a current handoff', retryable: false
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
|
|
982
|
+
const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
|
|
983
|
+
if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
|
|
984
|
+
|| nextReceipt.lifecycle !== 'pending')
|
|
985
|
+
throw new Error('handoff is not continuous with canonical receipt');
|
|
986
|
+
receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
|
|
987
|
+
try {
|
|
988
|
+
handoff.cleanup();
|
|
989
|
+
}
|
|
990
|
+
catch { /* keep the persisted canonical receipt usable */ }
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
return failed(resolved.taskId, {
|
|
994
|
+
code: 'TASK_FINALIZATION_HANDOFF_INVALID',
|
|
995
|
+
message: error instanceof Error ? error.message : String(error), retryable: false
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
889
1000
|
if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
|
|
890
1001
|
return failed(resolved.taskId, {
|
|
891
1002
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
|
|
@@ -7,17 +7,10 @@ import { checkpointCommitMatches } from './commit-identity.js';
|
|
|
7
7
|
import { readCheckpointIntent } from './commit-intent.js';
|
|
8
8
|
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
9
9
|
import { readTaskFinalizationReceipt } from './finalization.js';
|
|
10
|
-
import { semanticDigest, sha256Content } from './local-artifact-finalization.js';
|
|
11
|
-
import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
12
|
-
import { parseArtifactName } from './artifact-name.js';
|
|
13
10
|
import { enumerateAllTaskDirs } from './resolve-ref.js';
|
|
14
11
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
15
|
-
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
|
|
16
12
|
const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
|
|
17
|
-
const AUXILIARY_ROOTS = [
|
|
18
|
-
'.local-artifact-finalization-intents',
|
|
19
|
-
'.task-commit-intents'
|
|
20
|
-
];
|
|
13
|
+
const AUXILIARY_ROOTS = ['.task-commit-intents'];
|
|
21
14
|
function auxiliaryRoot(repoRoot, name) {
|
|
22
15
|
return path.join(repoRoot, '.agents', 'workspace', name);
|
|
23
16
|
}
|
|
@@ -98,46 +91,6 @@ function receiptGate(repoRoot, task, options) {
|
|
|
98
91
|
}
|
|
99
92
|
return null;
|
|
100
93
|
}
|
|
101
|
-
function canonicalLocalIntentPath(taskId, family, artifact) {
|
|
102
|
-
return `${taskId}-${family}-${artifact}.json`;
|
|
103
|
-
}
|
|
104
|
-
function localIntentCandidate(repoRoot, task, filePath, taskId, family, artifact, identity, options) {
|
|
105
|
-
const protectedReason = taskGate(task);
|
|
106
|
-
if (protectedReason)
|
|
107
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
|
|
108
|
-
if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
|
|
109
|
-
|| !parseArtifactName(artifact)?.family || parseArtifactName(artifact).family !== family) {
|
|
110
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
|
|
111
|
-
}
|
|
112
|
-
let intent;
|
|
113
|
-
try {
|
|
114
|
-
intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity);
|
|
118
|
-
}
|
|
119
|
-
if (!intent || intent.state !== 'consumed') {
|
|
120
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
|
|
121
|
-
}
|
|
122
|
-
const artifactPath = path.join(task.taskDir, artifact);
|
|
123
|
-
if (safeLstat(task.taskDir, artifactPath, 'file') === null) {
|
|
124
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
|
|
125
|
-
}
|
|
126
|
-
let content;
|
|
127
|
-
try {
|
|
128
|
-
content = fs.readFileSync(artifactPath, 'utf8');
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity);
|
|
132
|
-
}
|
|
133
|
-
if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
|
|
134
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
|
|
135
|
-
}
|
|
136
|
-
const receiptReason = receiptGate(repoRoot, task, options);
|
|
137
|
-
if (receiptReason)
|
|
138
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
|
|
139
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
|
|
140
|
-
}
|
|
141
94
|
function commitIntentCandidate(repoRoot, task, filePath, taskId, identity, options) {
|
|
142
95
|
const protectedReason = taskGate(task);
|
|
143
96
|
if (protectedReason)
|
|
@@ -209,24 +162,13 @@ function readAuxiliaryCandidates(repoRoot, tasks, options) {
|
|
|
209
162
|
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
210
163
|
continue;
|
|
211
164
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
candidates.push(localIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], match[2], match[3], identity, options));
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
223
|
-
if (!match || (selected && !selected.has(match[1]))) {
|
|
224
|
-
if (!selected || !match)
|
|
225
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], identity, options));
|
|
165
|
+
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
166
|
+
if (!match || (selected && !selected.has(match[1]))) {
|
|
167
|
+
if (!selected || !match)
|
|
168
|
+
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
169
|
+
continue;
|
|
229
170
|
}
|
|
171
|
+
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], identity, options));
|
|
230
172
|
}
|
|
231
173
|
if (entries.length === 0) {
|
|
232
174
|
candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
|
|
@@ -255,85 +197,7 @@ function scanIntermediateCleanup(repoRoot, options = {}) {
|
|
|
255
197
|
const candidates = scanInternal(repoRoot, options);
|
|
256
198
|
return buildIntermediateCleanupReport(candidates, true);
|
|
257
199
|
}
|
|
258
|
-
function
|
|
259
|
-
if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId)
|
|
260
|
-
return true;
|
|
261
|
-
if (!task)
|
|
262
|
-
return false;
|
|
263
|
-
const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
|
|
264
|
-
if (!match)
|
|
265
|
-
return false;
|
|
266
|
-
let intent;
|
|
267
|
-
try {
|
|
268
|
-
intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2], match[3]);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
if (!intent || intent.state !== 'consumed')
|
|
274
|
-
return false;
|
|
275
|
-
const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
|
|
276
|
-
let stat = null;
|
|
277
|
-
try {
|
|
278
|
-
stat = fs.lstatSync(recovery);
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
if (errorCode(error) === 'ENOENT')
|
|
282
|
-
return true;
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
286
|
-
return false;
|
|
287
|
-
let entries;
|
|
288
|
-
try {
|
|
289
|
-
entries = fs.readdirSync(recovery, { withFileTypes: true });
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
for (const entry of entries) {
|
|
295
|
-
if (entry.isSymbolicLink())
|
|
296
|
-
return false;
|
|
297
|
-
if (entry.isFile()) {
|
|
298
|
-
if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name))
|
|
299
|
-
return false;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (!entry.isDirectory() || entry.name !== 'generations')
|
|
303
|
-
return false;
|
|
304
|
-
let generations;
|
|
305
|
-
try {
|
|
306
|
-
generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true });
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
if (generations.some((generation) => generation.isSymbolicLink()
|
|
312
|
-
|| !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name)))
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
try {
|
|
316
|
-
for (const entry of entries) {
|
|
317
|
-
const target = path.join(recovery, entry.name);
|
|
318
|
-
if (entry.isFile())
|
|
319
|
-
fs.unlinkSync(target);
|
|
320
|
-
else {
|
|
321
|
-
for (const generation of fs.readdirSync(target, { withFileTypes: true }))
|
|
322
|
-
fs.unlinkSync(path.join(target, generation.name));
|
|
323
|
-
fs.rmdirSync(target);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
fs.rmdirSync(recovery);
|
|
327
|
-
const recoveryRoot = path.dirname(recovery);
|
|
328
|
-
if (fs.readdirSync(recoveryRoot).length === 0)
|
|
329
|
-
fs.rmdirSync(recoveryRoot);
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
return errorCode(error) === 'ENOENT';
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
function removeCandidate(repoRoot, candidate, task) {
|
|
200
|
+
function removeCandidate(repoRoot, candidate) {
|
|
337
201
|
if (candidate.disposition !== 'planned')
|
|
338
202
|
return candidate;
|
|
339
203
|
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
@@ -358,9 +222,6 @@ function removeCandidate(repoRoot, candidate, task) {
|
|
|
358
222
|
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
359
223
|
if (!sameFileIdentity(expected, actual))
|
|
360
224
|
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
361
|
-
if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
|
|
362
|
-
return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
|
|
363
|
-
}
|
|
364
225
|
try {
|
|
365
226
|
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
366
227
|
if (fs.readdirSync(candidate.path).length !== 0)
|
|
@@ -413,7 +274,7 @@ function cleanupIntermediateUnderRemovalCoordinator(repoRoot, options = {}) {
|
|
|
413
274
|
continue;
|
|
414
275
|
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
415
276
|
items[index] = current
|
|
416
|
-
? removeCandidate(repoRoot, current
|
|
277
|
+
? removeCandidate(repoRoot, current)
|
|
417
278
|
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
418
279
|
}
|
|
419
280
|
};
|
|
@@ -6,7 +6,6 @@ import { parseArtifactName } from './artifact-name.js';
|
|
|
6
6
|
import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.js';
|
|
7
7
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
8
8
|
import { writeTask } from './write.js';
|
|
9
|
-
import { allowsManualOverride } from './guard-override.js';
|
|
10
9
|
import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
|
|
11
10
|
import { extractSection, extractSubSection } from './sections.js';
|
|
12
11
|
import { parseLifecyclePathDecision } from './lifecycle-path.js';
|
|
@@ -105,8 +104,7 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
105
104
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
106
105
|
if (!resolved.ok)
|
|
107
106
|
return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
108
|
-
|
|
109
|
-
if (resolved.state !== 'active' && !stateOverride)
|
|
107
|
+
if (resolved.state !== 'active')
|
|
110
108
|
return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
111
109
|
let content;
|
|
112
110
|
let rows;
|
|
@@ -341,8 +339,7 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
341
339
|
? new Set(['confirmed', 'open', 'needs-human-decision'])
|
|
342
340
|
: before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set();
|
|
343
341
|
const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
|
|
344
|
-
if (
|
|
345
|
-
&& !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
|
|
342
|
+
if (!sameRoundMinorClose && !allowed.has(intent.status)) {
|
|
346
343
|
return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
|
|
347
344
|
}
|
|
348
345
|
after = { ...before, status: intent.status, evidence };
|
|
@@ -375,7 +372,7 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
375
372
|
}
|
|
376
373
|
}
|
|
377
374
|
const result = writeTask({
|
|
378
|
-
taskRef: intent.taskRef, expectedState:
|
|
375
|
+
taskRef: intent.taskRef, expectedState: 'active',
|
|
379
376
|
mutations: [rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
|
|
380
377
|
dryRun: 'dryRun' in intent ? intent.dryRun : false
|
|
381
378
|
}, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
@@ -43,9 +43,6 @@ const lifecycleProducerCatalog = lifecycleFailureCatalog.map((code) => ({
|
|
|
43
43
|
guardId: 'G-03',
|
|
44
44
|
code
|
|
45
45
|
}));
|
|
46
|
-
function allowsManualOverride(options, code) {
|
|
47
|
-
return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
|
|
48
|
-
}
|
|
49
46
|
const STEPS = ['task-written', 'directory-moved', 'registry-committed'];
|
|
50
47
|
const DEFAULT_IO = {
|
|
51
48
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
@@ -320,6 +317,49 @@ export function readLifecycleJournalEvidence(repoRoot, taskId) {
|
|
|
320
317
|
return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' };
|
|
321
318
|
}
|
|
322
319
|
}
|
|
320
|
+
export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
|
|
321
|
+
try {
|
|
322
|
+
const hot = locateHotTaskDirs(repoRoot, taskId);
|
|
323
|
+
if (hot.length !== 1 || (hot[0].state !== 'active' && hot[0].state !== 'completed'))
|
|
324
|
+
return 'unknown';
|
|
325
|
+
const { state, taskDir } = hot[0];
|
|
326
|
+
const content = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
327
|
+
const frontmatter = parseTypedTaskFrontmatter(content);
|
|
328
|
+
if (frontmatter.id !== taskId)
|
|
329
|
+
return 'unknown';
|
|
330
|
+
const journalPath = path.join(taskDir, '.task-lifecycle.json');
|
|
331
|
+
if (!fs.existsSync(journalPath)) {
|
|
332
|
+
if (state === 'active')
|
|
333
|
+
return frontmatter.status === 'active' ? 'not-started' : 'unknown';
|
|
334
|
+
return matchingCompletion(content, { taskRef: taskId, intent: 'complete', agent })
|
|
335
|
+
? 'started-recoverable' : 'unknown';
|
|
336
|
+
}
|
|
337
|
+
const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
|
|
338
|
+
const request = { taskRef: taskId, intent: 'complete', agent };
|
|
339
|
+
if (journal.taskId !== taskId || journal.intent !== 'complete'
|
|
340
|
+
|| journal.intentDigest !== intentIdentity(request, taskId)
|
|
341
|
+
|| journal.sourceState !== 'active' || journal.targetState !== 'completed')
|
|
342
|
+
return 'unknown';
|
|
343
|
+
if (journal.completedSteps.some((step, index) => STEPS[index] !== step))
|
|
344
|
+
return 'unknown';
|
|
345
|
+
const directoryMoved = journal.completedSteps.includes('directory-moved');
|
|
346
|
+
const registryCommitted = journal.completedSteps.includes('registry-committed');
|
|
347
|
+
const hasShortId = loadShortIdByTaskId(repoRoot).has(taskId);
|
|
348
|
+
if (state === 'active' && (directoryMoved || !hasShortId))
|
|
349
|
+
return 'unknown';
|
|
350
|
+
if (state === 'completed' && (!journal.completedSteps.includes('task-written')
|
|
351
|
+
|| !matchingCompletion(content, request) || (registryCommitted && hasShortId)))
|
|
352
|
+
return 'unknown';
|
|
353
|
+
if (frontmatter.status !== state
|
|
354
|
+
&& !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
|
|
355
|
+
return 'unknown';
|
|
356
|
+
}
|
|
357
|
+
return 'started-recoverable';
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return 'unknown';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
323
363
|
function directoryManifest(root) {
|
|
324
364
|
const entries = [];
|
|
325
365
|
const visit = (current, relative = '') => {
|
|
@@ -440,7 +480,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
440
480
|
}
|
|
441
481
|
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 });
|
|
442
482
|
}
|
|
443
|
-
if (!spec.sources.includes(sourceState) && !existingJournal
|
|
483
|
+
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
444
484
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
445
485
|
}
|
|
446
486
|
if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
|
|
@@ -456,7 +496,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
456
496
|
}
|
|
457
497
|
if (frontmatter.id !== taskId)
|
|
458
498
|
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 });
|
|
459
|
-
if (!locateActivityLog(content)
|
|
499
|
+
if (!locateActivityLog(content))
|
|
460
500
|
return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
461
501
|
if (spec.target === 'active') {
|
|
462
502
|
const contract = validateCurrentTaskContract(content);
|
|
@@ -472,10 +512,10 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
472
512
|
}
|
|
473
513
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}');
|
|
474
514
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
475
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey
|
|
515
|
+
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
476
516
|
return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
477
517
|
}
|
|
478
|
-
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1
|
|
518
|
+
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
479
519
|
return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
480
520
|
}
|
|
481
521
|
const digest = intentIdentity(request, taskId);
|
|
@@ -499,8 +539,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
499
539
|
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 });
|
|
500
540
|
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)) });
|
|
501
541
|
}
|
|
502
|
-
|
|
503
|
-
if (journal.intentDigest !== digest && !overrideResumesRecordedFailure)
|
|
542
|
+
if (journal.intentDigest !== digest)
|
|
504
543
|
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 });
|
|
505
544
|
}
|
|
506
545
|
else {
|
|
@@ -516,7 +555,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
516
555
|
}
|
|
517
556
|
journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
|
|
518
557
|
if (request.dryRun) {
|
|
519
|
-
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles,
|
|
558
|
+
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, false);
|
|
520
559
|
if ('code' in plannedMutations)
|
|
521
560
|
return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
522
561
|
const taskPlan = writeTask({ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true }, {
|
|
@@ -558,7 +597,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
558
597
|
completed.add('task-written');
|
|
559
598
|
if (!completed.has('task-written')) {
|
|
560
599
|
const current = io.readFileSync(path.join(sourcePath, 'task.md'));
|
|
561
|
-
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles,
|
|
600
|
+
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, false);
|
|
562
601
|
if ('code' in mutations)
|
|
563
602
|
return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
|
|
564
603
|
const writeResult = writeTask({ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations }, {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { inspectDecisionDetailDuplicates } from './decision-details.js';
|
|
4
|
-
import { scanVisibleMarkdown } from './markdown.js';
|
|
5
4
|
import { parseArtifactName } from './artifact-name.js';
|
|
5
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.js';
|
|
6
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
7
|
-
import { canonicalSemanticDigest,
|
|
7
|
+
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
8
8
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
9
9
|
const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
|
|
10
10
|
analysis: getArtifactSchema('analysis').sections.map((section) => section.headings.zh),
|
|
@@ -16,45 +16,30 @@ function localDiagnosticCode(code) {
|
|
|
16
16
|
ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
|
|
17
17
|
ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
|
|
18
18
|
ARTIFACT_DUPLICATE_SECTION: 'LOCAL_ARTIFACT_DUPLICATE_SECTION',
|
|
19
|
-
ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
19
|
+
ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION',
|
|
20
|
+
ARTIFACT_STATE_CHECK_PATTERN_MISSING: 'LOCAL_STATUS_COMMAND_MISSING',
|
|
21
|
+
ARTIFACT_REQUIRED_PATTERN_MISSING: 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
20
22
|
};
|
|
21
23
|
return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
|
|
22
24
|
}
|
|
23
25
|
function localDiagnostic(item) {
|
|
24
26
|
return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
|
|
25
27
|
}
|
|
26
|
-
function isStatusPattern(pattern) {
|
|
27
|
-
return pattern === '^\\$ ';
|
|
28
|
-
}
|
|
29
28
|
function validateLocalArtifact(content, options) {
|
|
30
29
|
const schema = getArtifactSchema(options.family);
|
|
31
30
|
const diagnostics = [];
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
for (const item of structure.diagnostics)
|
|
31
|
+
const inspection = inspectArtifactContract(content, schema, { scopeStateCheckSection: true });
|
|
32
|
+
for (const item of inspection.diagnostics)
|
|
35
33
|
diagnostics.push(localDiagnostic(item));
|
|
36
|
-
const statusSection = schema.sections.find((section) => section.id === 'state-check');
|
|
37
|
-
if (statusSection) {
|
|
38
|
-
const statusHeading = scanned.headings.find((heading) => (heading.level === 2 && [statusSection.headings.zh, statusSection.headings.en].some((name) => (heading.text === name || heading.text === `${name}:` || heading.text === `${name}:`))));
|
|
39
|
-
if (statusHeading) {
|
|
40
|
-
const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
|
|
41
|
-
const body = content.slice(statusHeading.end, next?.start ?? content.length);
|
|
42
|
-
for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
|
|
43
|
-
if (!new RegExp(pattern, 'm').test(body)) {
|
|
44
|
-
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 });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const patternInspection = inspectArtifactPatterns(content, {
|
|
50
|
-
...schema,
|
|
51
|
-
requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
|
|
52
|
-
});
|
|
53
|
-
for (const item of patternInspection.diagnostics)
|
|
54
|
-
diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
|
|
55
34
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
56
35
|
if (!decisionDetails.ok)
|
|
57
36
|
diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
37
|
+
if (options.family === 'analysis') {
|
|
38
|
+
const flowDecision = parseLifecyclePathDecision(content);
|
|
39
|
+
if (flowDecision.status !== 'valid') {
|
|
40
|
+
diagnostics.push({ code: 'LOCAL_FLOW_DECISION_INVALID', message: flowDecision.message, line: null });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
58
43
|
return {
|
|
59
44
|
ok: diagnostics.length === 0,
|
|
60
45
|
family: options.family,
|
|
@@ -3,8 +3,7 @@ export function parseReviewCommand(args) {
|
|
|
3
3
|
throw new Error('task ref and a supported review operation are required');
|
|
4
4
|
const values = {};
|
|
5
5
|
const flags = {
|
|
6
|
-
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
|
|
7
|
-
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
6
|
+
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
|
|
8
7
|
};
|
|
9
8
|
for (let index = 2; index < args.length; index += 1) {
|
|
10
9
|
const flag = args[index];
|
|
@@ -9,7 +9,6 @@ import { inspectDecisionDetailDuplicates } from './decision-details.js';
|
|
|
9
9
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
10
10
|
import { validateLifecycleExecution } from './lifecycle-execution.js';
|
|
11
11
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
12
|
-
import { allowsManualOverride } from './guard-override.js';
|
|
13
12
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
14
13
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
15
14
|
const STAGES = {
|
|
@@ -138,7 +137,7 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
138
137
|
taskId = resolved.taskId ?? null;
|
|
139
138
|
if (!resolved.ok)
|
|
140
139
|
return reject(resolved.code, resolved.message);
|
|
141
|
-
if (resolved.state !== 'active'
|
|
140
|
+
if (resolved.state !== 'active') {
|
|
142
141
|
return reject('TASK_STATE_MISMATCH', `task ${taskId} is ${resolved.state}, expected active`);
|
|
143
142
|
}
|
|
144
143
|
const parsed = parseArtifactName(request.artifact);
|