@fitlab-ai/agent-infra 0.9.12 → 0.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/README.zh-CN.md +11 -11
- package/dist/bin/internal-cli.js +24 -19
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +63 -24
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +1 -1
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +119 -104
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +520 -50
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +783 -537
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +11 -12
- package/dist/lib/platform/verification-sync.js +112 -238
- package/dist/lib/pr-review/evidence-grading.js +19 -6
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +33 -5
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-operation.js +12 -1
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/current-contract.js +5 -1
- package/dist/lib/task/decision-details.js +1 -1
- package/dist/lib/task/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +58 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +49 -93
- package/dist/lib/task/pr-delivery-fact.js +102 -101
- package/dist/lib/task/review-fingerprint.js +24 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +43 -16
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/version.js +11 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/init.ts +1 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +47 -22
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +2 -2
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +126 -106
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +560 -53
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +826 -543
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +11 -13
- package/lib/platform/verification-sync.ts +112 -292
- package/lib/pr-review/evidence-grading.ts +25 -8
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +32 -6
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-operation.ts +12 -1
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +6 -2
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +70 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +52 -85
- package/lib/task/pr-delivery-fact.ts +131 -148
- package/lib/task/review-fingerprint.ts +23 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +52 -17
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +1 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
- package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
- package/templates/.agents/rules/pr-sync.en.md +10 -4
- package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
- package/templates/.agents/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +18 -1
- package/templates/.agents/templates/task.zh-CN.md +18 -1
- package/templates/.github/workflows/pr-label.yml +7 -26
|
@@ -1,182 +1,183 @@
|
|
|
1
|
+
import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.js';
|
|
2
|
+
import { isResourceIdentity, parseResourceIdentity, resourceIdentityNumber, serializeResourceIdentity } from '../platform/resource-identity.js';
|
|
1
3
|
const PR_DELIVERY_FACT_KEY = 'pr_delivery_fact';
|
|
2
4
|
function factError(message) {
|
|
3
5
|
return Object.assign(new Error(`PR_DELIVERY_FACT_INVALID: ${message}`), { code: 'PR_DELIVERY_FACT_INVALID' });
|
|
4
6
|
}
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
+
function asFactError(error) {
|
|
8
|
+
if (error instanceof Error && typeof error.code === 'string')
|
|
9
|
+
return error;
|
|
10
|
+
return factError(error instanceof Error ? error.message : String(error));
|
|
7
11
|
}
|
|
12
|
+
function isRecord(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
|
|
8
13
|
function exactKeys(value, keys, label) {
|
|
9
14
|
const actual = Object.keys(value).sort();
|
|
10
15
|
const expected = [...keys].sort();
|
|
11
|
-
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index]))
|
|
16
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index]))
|
|
12
17
|
throw factError(`${label} has an invalid field set`);
|
|
13
|
-
}
|
|
14
18
|
}
|
|
15
|
-
function text(value, label) {
|
|
16
|
-
if (typeof value !== 'string' || !value || value.trim() !== value)
|
|
17
|
-
throw factError(`${label} must be a
|
|
19
|
+
function text(value, label, allowEmpty = false) {
|
|
20
|
+
if (typeof value !== 'string' || (!allowEmpty && !value) || value.trim() !== value)
|
|
21
|
+
throw factError(`${label} must be a string`);
|
|
18
22
|
return value;
|
|
19
23
|
}
|
|
20
|
-
function nullableText(value, label) {
|
|
21
|
-
|
|
24
|
+
function nullableText(value, label) { return value === null ? null : text(value, label); }
|
|
25
|
+
function timestamp(value, label) {
|
|
26
|
+
const result = text(value, label);
|
|
27
|
+
if (Number.isNaN(Date.parse(result)))
|
|
28
|
+
throw factError(`${label} must be a valid timestamp`);
|
|
29
|
+
return new Date(result).toISOString();
|
|
22
30
|
}
|
|
23
31
|
function positiveNumber(value, label) {
|
|
24
32
|
if (!Number.isSafeInteger(value) || value <= 0)
|
|
25
33
|
throw factError(`${label} must be a positive integer`);
|
|
26
34
|
return value;
|
|
27
35
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
36
|
+
function parseRef(value, label) {
|
|
37
|
+
if (!isRecord(value))
|
|
38
|
+
throw factError(`${label} must be an object`);
|
|
39
|
+
exactKeys(value, ['repository', 'ref', 'sha'], label);
|
|
40
|
+
return { repository: text(value.repository, `${label}.repository`), ref: text(value.ref, `${label}.ref`), sha: text(value.sha, `${label}.sha`) };
|
|
33
41
|
}
|
|
34
|
-
function
|
|
42
|
+
function canonicalIdentity(value) {
|
|
35
43
|
if (!isRecord(value))
|
|
36
44
|
throw factError('identity must be an object');
|
|
37
|
-
exactKeys(value, ['
|
|
38
|
-
const head = parseRef(value.head, 'head');
|
|
39
|
-
const base = parseRef(value.base, 'base');
|
|
45
|
+
exactKeys(value, ['resource', 'repository', 'url', 'head', 'base'], 'identity');
|
|
40
46
|
return {
|
|
47
|
+
resource: parseResourceIdentity(value.resource, 'identity.resource'),
|
|
41
48
|
repository: text(value.repository, 'identity.repository'),
|
|
42
|
-
number: positiveNumber(value.number, 'identity.number'),
|
|
43
|
-
nodeId: text(value.nodeId, 'identity.nodeId'),
|
|
44
49
|
url: text(value.url, 'identity.url'),
|
|
45
|
-
head,
|
|
46
|
-
base
|
|
50
|
+
head: parseRef(value.head, 'identity.head'),
|
|
51
|
+
base: parseRef(value.base, 'identity.base')
|
|
47
52
|
};
|
|
48
53
|
}
|
|
49
|
-
function
|
|
54
|
+
function decodeLegacyIdentity(value) {
|
|
50
55
|
if (!isRecord(value))
|
|
51
|
-
throw factError(
|
|
52
|
-
|
|
56
|
+
throw factError('legacy identity must be an object');
|
|
57
|
+
// TODO(compat): Remove the v1 identity decoder before the first stable v1.0.0 release.
|
|
58
|
+
exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'legacy identity');
|
|
59
|
+
const legacy = {
|
|
60
|
+
number: positiveNumber(value.number, 'legacy identity.number'),
|
|
61
|
+
repository: text(value.repository, 'identity.repository'),
|
|
62
|
+
nodeId: text(value.nodeId, 'legacy identity.nodeId'),
|
|
63
|
+
url: text(value.url, 'identity.url'),
|
|
64
|
+
head: parseRef(value.head, 'identity.head'),
|
|
65
|
+
base: parseRef(value.base, 'identity.base')
|
|
66
|
+
};
|
|
53
67
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
resource: { kind: 'number', value: legacy.number },
|
|
69
|
+
repository: legacy.repository,
|
|
70
|
+
url: legacy.url,
|
|
71
|
+
head: legacy.head,
|
|
72
|
+
base: legacy.base
|
|
57
73
|
};
|
|
58
74
|
}
|
|
59
|
-
function
|
|
75
|
+
function provenanceForSource(source) { return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source; }
|
|
76
|
+
function parseFact(value, runtimeVersion = VERSION) {
|
|
60
77
|
if (!isRecord(value))
|
|
61
78
|
throw factError('fact must be an object');
|
|
62
|
-
if (value.version !== 1 || typeof value.state !== 'string')
|
|
79
|
+
if ((value.version !== 1 && value.version !== 2) || typeof value.state !== 'string')
|
|
63
80
|
throw factError('version or state is invalid');
|
|
81
|
+
if (value.version === 1 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
|
|
82
|
+
throw legacyCompatibilityError('v1 PR delivery fact', runtimeVersion);
|
|
83
|
+
}
|
|
64
84
|
if (value.state === 'unbound') {
|
|
65
85
|
exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
|
|
66
86
|
if (value.reason !== 'initial')
|
|
67
87
|
throw factError('unbound reason is invalid');
|
|
68
|
-
return { version:
|
|
88
|
+
return { version: 2, state: 'unbound', reason: 'initial' };
|
|
69
89
|
}
|
|
70
90
|
if (value.state === 'skipped') {
|
|
71
91
|
exactKeys(value, ['version', 'state', 'reason', 'decidedAt'], 'skipped fact');
|
|
72
92
|
if (value.reason !== 'explicit')
|
|
73
93
|
throw factError('skipped reason is invalid');
|
|
74
|
-
|
|
75
|
-
if (Number.isNaN(Date.parse(decidedAt)))
|
|
76
|
-
throw factError('skipped.decidedAt must be a valid timestamp');
|
|
77
|
-
return { version: 1, state: 'skipped', reason: 'explicit', decidedAt };
|
|
94
|
+
return { version: 2, state: 'skipped', reason: 'explicit', decidedAt: timestamp(value.decidedAt, 'skipped.decidedAt') };
|
|
78
95
|
}
|
|
79
96
|
if (value.state !== 'bound')
|
|
80
97
|
throw factError('state is invalid');
|
|
98
|
+
if (value.version === 1) {
|
|
99
|
+
// TODO(compat): Remove this v1-to-v2 shape conversion before the first stable v1.0.0 release.
|
|
100
|
+
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'legacy bound fact');
|
|
101
|
+
if (!isRecord(value.binding) || !isRecord(value.provenance))
|
|
102
|
+
throw factError('legacy binding or provenance is invalid');
|
|
103
|
+
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'legacy binding');
|
|
104
|
+
const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'legacy binding.issueNumber');
|
|
105
|
+
return parseFact({
|
|
106
|
+
version: 2,
|
|
107
|
+
state: 'bound',
|
|
108
|
+
identity: decodeLegacyIdentity(value.identity),
|
|
109
|
+
binding: {
|
|
110
|
+
status: 'verified', source: value.binding.source, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'),
|
|
111
|
+
issueIdentity: issueNumber === null ? null : { kind: 'number', value: issueNumber }, remoteState: value.binding.remoteState,
|
|
112
|
+
mergedAt: value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt'), mergeCommitSha: nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha')
|
|
113
|
+
},
|
|
114
|
+
provenance: value.provenance
|
|
115
|
+
}, runtimeVersion);
|
|
116
|
+
}
|
|
81
117
|
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'bound fact');
|
|
82
|
-
if (!isRecord(value.binding))
|
|
83
|
-
throw factError('binding
|
|
84
|
-
exactKeys(value.binding, ['status', 'source', 'verifiedAt', '
|
|
118
|
+
if (!isRecord(value.binding) || !isRecord(value.provenance))
|
|
119
|
+
throw factError('binding or provenance is invalid');
|
|
120
|
+
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueIdentity', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'binding');
|
|
85
121
|
if (value.binding.status !== 'verified')
|
|
86
122
|
throw factError('binding status is invalid');
|
|
87
123
|
const sources = ['created', 'reused', 'explicit-bind', 'external-unique', 'external-explicit'];
|
|
88
124
|
if (!sources.includes(value.binding.source))
|
|
89
125
|
throw factError('binding source is invalid');
|
|
90
|
-
const
|
|
91
|
-
if (Number.isNaN(Date.parse(verifiedAt)))
|
|
92
|
-
throw factError('binding.verifiedAt must be a valid timestamp');
|
|
93
|
-
const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'binding.issueNumber');
|
|
126
|
+
const issueIdentity = value.binding.issueIdentity === null ? null : parseResourceIdentity(value.binding.issueIdentity, 'binding.issueIdentity');
|
|
94
127
|
if (value.binding.remoteState !== 'open' && value.binding.remoteState !== 'closed')
|
|
95
128
|
throw factError('binding.remoteState is invalid');
|
|
96
|
-
const mergedAt =
|
|
129
|
+
const mergedAt = value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt');
|
|
97
130
|
const mergeCommitSha = nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha');
|
|
98
131
|
if (Boolean(mergedAt) !== Boolean(mergeCommitSha))
|
|
99
132
|
throw factError('mergedAt and mergeCommitSha must be paired');
|
|
100
|
-
if (!isRecord(value.provenance))
|
|
101
|
-
throw factError('provenance must be an object');
|
|
102
133
|
exactKeys(value.provenance, ['establishedBy'], 'provenance');
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
134
|
+
const provenances = ['create-post', 'reuse', 'explicit-bind', 'external-unique', 'external-explicit'];
|
|
135
|
+
if (!provenances.includes(value.provenance.establishedBy))
|
|
105
136
|
throw factError('provenance.establishedBy is invalid');
|
|
106
|
-
if (value.provenance.establishedBy !== provenanceForSource(value.binding.source))
|
|
137
|
+
if (value.provenance.establishedBy !== provenanceForSource(value.binding.source))
|
|
107
138
|
throw factError('binding.source and provenance.establishedBy do not match');
|
|
108
|
-
}
|
|
109
139
|
return {
|
|
110
|
-
version:
|
|
111
|
-
|
|
112
|
-
identity: parseIdentity(value.identity),
|
|
113
|
-
binding: {
|
|
114
|
-
status: 'verified',
|
|
115
|
-
source: value.binding.source,
|
|
116
|
-
verifiedAt,
|
|
117
|
-
issueNumber,
|
|
118
|
-
remoteState: value.binding.remoteState,
|
|
119
|
-
mergedAt,
|
|
120
|
-
mergeCommitSha
|
|
121
|
-
},
|
|
140
|
+
version: 2, state: 'bound', identity: canonicalIdentity(value.identity),
|
|
141
|
+
binding: { status: 'verified', source: value.binding.source, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'), issueIdentity, remoteState: value.binding.remoteState, mergedAt, mergeCommitSha },
|
|
122
142
|
provenance: { establishedBy: value.provenance.establishedBy }
|
|
123
143
|
};
|
|
124
144
|
}
|
|
125
|
-
function decodePrDeliveryFact(value) {
|
|
145
|
+
function decodePrDeliveryFact(value, runtimeVersion = VERSION) {
|
|
126
146
|
if (typeof value === 'string') {
|
|
147
|
+
let decoded;
|
|
127
148
|
try {
|
|
128
|
-
|
|
149
|
+
decoded = JSON.parse(value);
|
|
129
150
|
}
|
|
130
|
-
catch
|
|
131
|
-
throw
|
|
151
|
+
catch {
|
|
152
|
+
throw factError('value is not valid JSON');
|
|
132
153
|
}
|
|
154
|
+
return parseFact(decoded, runtimeVersion);
|
|
133
155
|
}
|
|
134
|
-
return parseFact(value);
|
|
156
|
+
return parseFact(value, runtimeVersion);
|
|
135
157
|
}
|
|
136
|
-
function encodePrDeliveryFact(fact) {
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
function readPrDeliveryFact(metadata) {
|
|
158
|
+
function encodePrDeliveryFact(fact) { return JSON.stringify(parseFact(fact)); }
|
|
159
|
+
function readPrDeliveryFact(metadata, runtimeVersion = VERSION) {
|
|
140
160
|
if (!Object.hasOwn(metadata, PR_DELIVERY_FACT_KEY) || metadata[PR_DELIVERY_FACT_KEY] === '')
|
|
141
161
|
return { status: 'missing', fact: null };
|
|
142
162
|
try {
|
|
143
|
-
return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]) };
|
|
163
|
+
return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY], runtimeVersion) };
|
|
144
164
|
}
|
|
145
165
|
catch (error) {
|
|
146
|
-
return { status: 'invalid', fact: null, error:
|
|
166
|
+
return { status: 'invalid', fact: null, error: asFactError(error) };
|
|
147
167
|
}
|
|
148
168
|
}
|
|
149
|
-
function buildUnboundFact() {
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
function buildSkippedFact(decidedAt) {
|
|
153
|
-
return parseFact({ version: 1, state: 'skipped', reason: 'explicit', decidedAt });
|
|
154
|
-
}
|
|
155
|
-
function provenanceForSource(source) {
|
|
156
|
-
return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source;
|
|
157
|
-
}
|
|
169
|
+
function buildUnboundFact() { return { version: 2, state: 'unbound', reason: 'initial' }; }
|
|
170
|
+
function buildSkippedFact(decidedAt) { return parseFact({ version: 2, state: 'skipped', reason: 'explicit', decidedAt }); }
|
|
158
171
|
function buildBoundFact(input) {
|
|
159
|
-
return parseFact({
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
status: 'verified',
|
|
165
|
-
source: input.source,
|
|
166
|
-
verifiedAt: input.verifiedAt,
|
|
167
|
-
issueNumber: input.issueNumber ?? null,
|
|
168
|
-
remoteState: input.remoteState,
|
|
169
|
-
mergedAt: input.mergedAt ?? null,
|
|
170
|
-
mergeCommitSha: input.mergeCommitSha ?? null
|
|
171
|
-
},
|
|
172
|
-
provenance: { establishedBy: provenanceForSource(input.source) }
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
function factFrontmatterMutation(fact) {
|
|
176
|
-
return { set: { [PR_DELIVERY_FACT_KEY]: encodePrDeliveryFact(fact) } };
|
|
172
|
+
return parseFact({ version: 2, state: 'bound', identity: input.identity, binding: {
|
|
173
|
+
status: 'verified', source: input.source, verifiedAt: input.verifiedAt,
|
|
174
|
+
issueIdentity: input.issueIdentity === undefined ? null : input.issueIdentity,
|
|
175
|
+
remoteState: input.remoteState, mergedAt: input.mergedAt ?? null, mergeCommitSha: input.mergeCommitSha ?? null
|
|
176
|
+
}, provenance: { establishedBy: provenanceForSource(input.source) } });
|
|
177
177
|
}
|
|
178
|
+
function factFrontmatterMutation(fact) { return { set: { [PR_DELIVERY_FACT_KEY]: encodePrDeliveryFact(fact) } }; }
|
|
178
179
|
function identityFromPullRequest(pullRequest) {
|
|
179
|
-
return
|
|
180
|
+
return canonicalIdentity(pullRequest);
|
|
180
181
|
}
|
|
181
|
-
export { PR_DELIVERY_FACT_KEY, buildBoundFact, buildSkippedFact, buildUnboundFact, decodePrDeliveryFact, encodePrDeliveryFact, factFrontmatterMutation, identityFromPullRequest, readPrDeliveryFact };
|
|
182
|
+
export { PR_DELIVERY_FACT_KEY, buildBoundFact, buildSkippedFact, buildUnboundFact, decodePrDeliveryFact, encodePrDeliveryFact, factFrontmatterMutation, identityFromPullRequest, readPrDeliveryFact, serializeResourceIdentity };
|
|
182
183
|
//# sourceMappingURL=pr-delivery-fact.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { artifactName, maxRound } from "./review-artifacts.js";
|
|
4
|
+
import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from "./invalidation.js";
|
|
4
5
|
const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
|
|
5
6
|
export function parseReviewedGitTree(value) {
|
|
6
7
|
return REVIEWED_TREE_RE.test(value) ? value : null;
|
|
@@ -30,14 +31,35 @@ export function findAuthoritativeReviewCodeArtifact(taskDir) {
|
|
|
30
31
|
const entries = fs.existsSync(taskDir) ? fs.readdirSync(taskDir) : [];
|
|
31
32
|
const round = maxRound(entries, "review-code");
|
|
32
33
|
if (round === 0) {
|
|
33
|
-
return { ok: false, round: 0, fileName: null, path: null };
|
|
34
|
+
return { ok: false, round: 0, fileName: null, path: null, error: null };
|
|
34
35
|
}
|
|
35
36
|
const fileName = artifactName("review-code", round);
|
|
37
|
+
let taskContent;
|
|
38
|
+
try {
|
|
39
|
+
taskContent = fs.readFileSync(path.join(taskDir, "task.md"), "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false, round, fileName, path: null,
|
|
44
|
+
error: error instanceof Error ? error.message : String(error)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const invalidation = parseInvalidationDocument(taskContent);
|
|
48
|
+
if (!invalidation.ok) {
|
|
49
|
+
return { ok: false, round, fileName, path: null, error: invalidation.message };
|
|
50
|
+
}
|
|
51
|
+
if (invalidationBlocks(invalidation.document)) {
|
|
52
|
+
return { ok: false, round, fileName, path: null, error: "artifact invalidation is incomplete; reconcile task.md before using review-code" };
|
|
53
|
+
}
|
|
54
|
+
if (isArtifactInvalidated(invalidation.document, "review-code", fileName)) {
|
|
55
|
+
return { ok: false, round, fileName, path: null, error: `${fileName} is invalidated; re-run review-code` };
|
|
56
|
+
}
|
|
36
57
|
return {
|
|
37
58
|
ok: true,
|
|
38
59
|
round,
|
|
39
60
|
fileName,
|
|
40
|
-
path: path.join(taskDir, fileName)
|
|
61
|
+
path: path.join(taskDir, fileName),
|
|
62
|
+
error: null
|
|
41
63
|
};
|
|
42
64
|
}
|
|
43
65
|
export function extractReviewBaseline(content) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const REWORK_INTENT_HEADINGS = ['返工意图', 'Rework Intent'];
|
|
2
|
+
const REWORK_INTENT_COLUMNS = [
|
|
3
|
+
'intent_id', 'finding_id', 'source_artifact', 'source_sha256', 'target',
|
|
4
|
+
'status', 'declared_at', 'consumed_at'
|
|
5
|
+
];
|
|
6
|
+
function cells(line) {
|
|
7
|
+
const first = line.indexOf('|');
|
|
8
|
+
const last = line.lastIndexOf('|');
|
|
9
|
+
if (first < 0 || first === last || line.slice(0, first).trim() || line.slice(last + 1).trim())
|
|
10
|
+
return null;
|
|
11
|
+
const inner = line.slice(first + 1, last);
|
|
12
|
+
const result = [];
|
|
13
|
+
let start = 0;
|
|
14
|
+
let escaped = false;
|
|
15
|
+
for (let index = 0; index < inner.length; index += 1) {
|
|
16
|
+
const char = inner[index];
|
|
17
|
+
if (char === '|' && !escaped) {
|
|
18
|
+
result.push(inner.slice(start, index));
|
|
19
|
+
start = index + 1;
|
|
20
|
+
}
|
|
21
|
+
escaped = char === '\\' && !escaped;
|
|
22
|
+
if (char !== '\\')
|
|
23
|
+
escaped = false;
|
|
24
|
+
}
|
|
25
|
+
result.push(inner.slice(start));
|
|
26
|
+
return result.map((cell) => cell.replace(/\\([\\|])/g, '$1').trim());
|
|
27
|
+
}
|
|
28
|
+
function sectionBody(content) {
|
|
29
|
+
const heading = REWORK_INTENT_HEADINGS.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
30
|
+
const match = new RegExp(`^##\\s+(${heading})\\s*$`, 'm').exec(content);
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
34
|
+
const rest = content.slice(start);
|
|
35
|
+
const end = rest.search(/^##\s+/m);
|
|
36
|
+
return rest.slice(0, end < 0 ? rest.length : end);
|
|
37
|
+
}
|
|
38
|
+
function parseReworkIntentDocument(content) {
|
|
39
|
+
const body = sectionBody(content);
|
|
40
|
+
if (body === null)
|
|
41
|
+
return { ok: true, present: false, intents: [] };
|
|
42
|
+
const lines = body.split(/\r?\n/).filter((line) => line.trim());
|
|
43
|
+
const header = lines[0] ? cells(lines[0]) : null;
|
|
44
|
+
const separator = lines[1] ? cells(lines[1]) : null;
|
|
45
|
+
if (!header || !separator || header.length !== REWORK_INTENT_COLUMNS.length || !header.every((value, index) => value === REWORK_INTENT_COLUMNS[index]) || !separator.every((value) => /^:?-{3,}:?$/.test(value))) {
|
|
46
|
+
return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: 'rework intent section must contain its canonical table' };
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const intents = lines.slice(2).map((line) => {
|
|
50
|
+
const values = cells(line);
|
|
51
|
+
if (!values || values.length !== REWORK_INTENT_COLUMNS.length)
|
|
52
|
+
throw new Error('rework intent row has the wrong column count');
|
|
53
|
+
const row = Object.fromEntries(REWORK_INTENT_COLUMNS.map((column, index) => [column, values[index]]));
|
|
54
|
+
if (!/^RI-[1-9]\d*$/.test(row.intent_id))
|
|
55
|
+
throw new Error(`invalid intent id '${row.intent_id}'`);
|
|
56
|
+
if (!/^(AN|PL|CD)-[1-9]\d*$/.test(row.finding_id))
|
|
57
|
+
throw new Error(`invalid finding id '${row.finding_id}'`);
|
|
58
|
+
if (!['analysis', 'plan', 'code'].includes(row.target))
|
|
59
|
+
throw new Error(`invalid rework target '${row.target}'`);
|
|
60
|
+
if (!['pending', 'consumed', 'superseded'].includes(row.status))
|
|
61
|
+
throw new Error(`invalid rework status '${row.status}'`);
|
|
62
|
+
if (!/^[a-f0-9]{64}$/.test(row.source_sha256))
|
|
63
|
+
throw new Error(`invalid source hash for '${row.intent_id}'`);
|
|
64
|
+
if (!row.source_artifact || !row.declared_at)
|
|
65
|
+
throw new Error(`rework intent '${row.intent_id}' has invalid required fields`);
|
|
66
|
+
const terminal = row.status === 'consumed' || row.status === 'superseded';
|
|
67
|
+
if (terminal !== Boolean(row.consumed_at))
|
|
68
|
+
throw new Error(`rework intent '${row.intent_id}' has invalid consumed_at`);
|
|
69
|
+
return {
|
|
70
|
+
intentId: row.intent_id, findingId: row.finding_id, sourceArtifact: row.source_artifact,
|
|
71
|
+
sourceSha256: row.source_sha256, target: row.target,
|
|
72
|
+
status: row.status, declaredAt: row.declared_at, consumedAt: row.consumed_at
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
const ids = new Set();
|
|
76
|
+
for (const intent of intents) {
|
|
77
|
+
if (ids.has(intent.intentId))
|
|
78
|
+
throw new Error(`duplicate intent '${intent.intentId}'`);
|
|
79
|
+
ids.add(intent.intentId);
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, present: true, intents };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function renderReworkIntents(intents) {
|
|
88
|
+
const escape = (value) => value.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ');
|
|
89
|
+
return [
|
|
90
|
+
`| ${REWORK_INTENT_COLUMNS.join(' | ')} |`,
|
|
91
|
+
`| ${REWORK_INTENT_COLUMNS.map(() => '---').join(' | ')} |`,
|
|
92
|
+
...intents.map((intent) => `| ${[
|
|
93
|
+
intent.intentId, intent.findingId, intent.sourceArtifact, intent.sourceSha256,
|
|
94
|
+
intent.target, intent.status, intent.declaredAt, intent.consumedAt
|
|
95
|
+
].map(escape).join(' | ')} |`)
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
function upsertReworkIntent(intents, intent) {
|
|
99
|
+
const existing = intents.find((candidate) => candidate.intentId === intent.intentId);
|
|
100
|
+
if (!existing)
|
|
101
|
+
return { changed: true, intents: [...intents, intent] };
|
|
102
|
+
const identity = (value) => [value.intentId, value.findingId, value.sourceArtifact, value.sourceSha256, value.target];
|
|
103
|
+
if (JSON.stringify(identity(existing)) !== JSON.stringify(identity(intent)))
|
|
104
|
+
throw new Error(`intent '${intent.intentId}' identity conflicts with existing state`);
|
|
105
|
+
return { changed: false, intents };
|
|
106
|
+
}
|
|
107
|
+
function consumeReworkIntents(intents, target, sourceHashes, consumedAt) {
|
|
108
|
+
let changed = false;
|
|
109
|
+
const next = intents.map((intent) => {
|
|
110
|
+
if (intent.status !== 'pending'
|
|
111
|
+
|| intent.target !== target
|
|
112
|
+
|| sourceHashes[intent.sourceArtifact] !== intent.sourceSha256)
|
|
113
|
+
return intent;
|
|
114
|
+
changed = true;
|
|
115
|
+
return { ...intent, status: 'consumed', consumedAt };
|
|
116
|
+
});
|
|
117
|
+
return { changed, intents: next };
|
|
118
|
+
}
|
|
119
|
+
function supersedeReworkIntents(intents, sourceArtifact, sourceSha256, supersededAt) {
|
|
120
|
+
let changed = false;
|
|
121
|
+
const next = intents.map((intent) => {
|
|
122
|
+
if (intent.status !== 'pending' || intent.sourceArtifact !== sourceArtifact || intent.sourceSha256 === sourceSha256)
|
|
123
|
+
return intent;
|
|
124
|
+
changed = true;
|
|
125
|
+
return { ...intent, status: 'superseded', consumedAt: supersededAt };
|
|
126
|
+
});
|
|
127
|
+
return { changed, intents: next };
|
|
128
|
+
}
|
|
129
|
+
function reworkIntentMutation(content, intents) {
|
|
130
|
+
return {
|
|
131
|
+
kind: 'section',
|
|
132
|
+
aliases: REWORK_INTENT_HEADINGS,
|
|
133
|
+
heading: /^##\s+Rework Intent\s*$/m.test(content) ? REWORK_INTENT_HEADINGS[1] : REWORK_INTENT_HEADINGS[0],
|
|
134
|
+
body: renderReworkIntents(intents)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export { REWORK_INTENT_COLUMNS, REWORK_INTENT_HEADINGS, parseReworkIntentDocument, renderReworkIntents, reworkIntentMutation, consumeReworkIntents, supersedeReworkIntents, upsertReworkIntent };
|
|
138
|
+
//# sourceMappingURL=rework-intent.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const TASK_SCOPE_SKILLS = new Set([
|
|
2
|
+
'analyze-task',
|
|
3
|
+
'block-task',
|
|
4
|
+
'cancel-task',
|
|
5
|
+
'check-task',
|
|
6
|
+
'code-task',
|
|
7
|
+
'commit',
|
|
8
|
+
'complete-manual-validation',
|
|
9
|
+
'complete-task',
|
|
10
|
+
'create-pr',
|
|
11
|
+
'plan-task',
|
|
12
|
+
'review-analysis',
|
|
13
|
+
'review-code',
|
|
14
|
+
'review-plan',
|
|
15
|
+
'run-manual-validation',
|
|
16
|
+
'run-task',
|
|
17
|
+
'watch-pr'
|
|
18
|
+
]);
|
|
19
|
+
function isTaskScopeSkill(skillName) {
|
|
20
|
+
return TASK_SCOPE_SKILLS.has(skillName);
|
|
21
|
+
}
|
|
22
|
+
export { TASK_SCOPE_SKILLS, isTaskScopeSkill };
|
|
23
|
+
//# sourceMappingURL=skill-scope.js.map
|
|
@@ -160,6 +160,25 @@ function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options =
|
|
|
160
160
|
};
|
|
161
161
|
const serialized = `${JSON.stringify(owner)}\n`;
|
|
162
162
|
let ownsFixed = false;
|
|
163
|
+
let deferRelease = false;
|
|
164
|
+
const release = () => {
|
|
165
|
+
try {
|
|
166
|
+
unlinkIfPresent(candidate);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Candidate cleanup is best-effort and must not replace the primary result.
|
|
170
|
+
}
|
|
171
|
+
if (ownsFixed) {
|
|
172
|
+
try {
|
|
173
|
+
const current = parseOwner(fs.readFileSync(fixed, 'utf8'), key);
|
|
174
|
+
if (current.token === token)
|
|
175
|
+
unlinkIfPresent(fixed);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// A missing or replaced fixed lock is not owned by this invocation.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
163
182
|
try {
|
|
164
183
|
fs.mkdirSync(lockRoot, { recursive: true });
|
|
165
184
|
cleanStaleCandidates(lockRoot, key, options.identityMatches ?? processIdentityMatches);
|
|
@@ -202,7 +221,12 @@ function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options =
|
|
|
202
221
|
throw new TaskExecutionLockError('ORCHESTRATION_LOCK_BUSY', 'lifecycle task lock could not be acquired after stale recovery', { operation: 'acquire', errno: 'EEXIST', key });
|
|
203
222
|
}
|
|
204
223
|
unlinkIfPresent(candidate);
|
|
205
|
-
|
|
224
|
+
const value = callback();
|
|
225
|
+
if (value && typeof value.then === 'function') {
|
|
226
|
+
deferRelease = true;
|
|
227
|
+
return value.finally(release);
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
206
230
|
}
|
|
207
231
|
catch (error) {
|
|
208
232
|
if (error instanceof TaskExecutionLockError)
|
|
@@ -212,22 +236,8 @@ function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options =
|
|
|
212
236
|
throw new TaskExecutionLockError('ORCHESTRATION_LOCK_FAILED', 'lifecycle task lock operation failed', { operation: ownsFixed ? 'callback' : 'acquire', errno: errno(error), key });
|
|
213
237
|
}
|
|
214
238
|
finally {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
// Candidate cleanup is best-effort and must not replace the primary result.
|
|
220
|
-
}
|
|
221
|
-
if (ownsFixed) {
|
|
222
|
-
try {
|
|
223
|
-
const current = parseOwner(fs.readFileSync(fixed, 'utf8'), key);
|
|
224
|
-
if (current.token === token)
|
|
225
|
-
unlinkIfPresent(fixed);
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
// A missing or replaced fixed lock is not owned by this invocation.
|
|
229
|
-
}
|
|
230
|
-
}
|
|
239
|
+
if (!deferRelease)
|
|
240
|
+
release();
|
|
231
241
|
}
|
|
232
242
|
}
|
|
233
243
|
function withRepositoryMutationLock(repoRoot, callback, options = {}) {
|