@fitlab-ai/agent-infra 0.9.11 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/git/workflow.js +43 -1
- package/dist/lib/init.js +1 -0
- package/dist/lib/internal/git-workflow.js +8 -1
- package/dist/lib/internal/platform-pr.js +8 -2
- package/dist/lib/internal/task-delivery.js +59 -0
- package/dist/lib/platform/github-client.js +7 -1
- package/dist/lib/platform/pr-checks.js +8 -5
- package/dist/lib/platform/pr-summary.js +12 -8
- package/dist/lib/platform/pull-requests.js +393 -91
- package/dist/lib/platform/verification-required.js +13 -2
- package/dist/lib/platform/verification-sync.js +9 -4
- package/dist/lib/pr-review/evidence-grading.js +12 -4
- package/dist/lib/sandbox/config.js +5 -0
- package/dist/lib/sandbox/control/lifecycle.js +29 -7
- package/dist/lib/server/process-state.js +2 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -2
- package/dist/lib/task/commit-intent.js +100 -0
- package/dist/lib/task/commit-operation.js +195 -101
- package/dist/lib/task/create.js +33 -7
- package/dist/lib/task/decision-details.js +3 -48
- package/dist/lib/task/delivery-target.js +97 -0
- package/dist/lib/task/delivery.js +153 -0
- package/dist/lib/task/human-override.js +5 -2
- package/dist/lib/task/lifecycle.js +106 -9
- package/dist/lib/task/orchestration.js +55 -62
- package/dist/lib/task/pr-delivery-fact.js +182 -0
- package/dist/lib/task/review-finalization.js +4 -8
- package/dist/lib/task/review-fingerprint.js +8 -0
- package/dist/lib/task/verification-engine.js +48 -24
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/update.js +9 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
- package/lib/defaults.json +4 -0
- package/lib/git/workflow.ts +38 -1
- package/lib/init.ts +3 -0
- package/lib/internal/git-workflow.ts +7 -1
- package/lib/internal/platform-pr.ts +8 -1
- package/lib/internal/task-delivery.ts +53 -0
- package/lib/platform/github-client.ts +8 -1
- package/lib/platform/pr-checks.ts +7 -5
- package/lib/platform/pr-summary.ts +10 -7
- package/lib/platform/pull-requests.ts +425 -87
- package/lib/platform/verification-required.ts +12 -2
- package/lib/platform/verification-sync.ts +9 -4
- package/lib/pr-review/evidence-grading.ts +7 -4
- package/lib/sandbox/config.ts +7 -0
- package/lib/sandbox/control/lifecycle.ts +30 -8
- package/lib/server/process-state.ts +2 -2
- package/lib/task/artifact-lifecycle.ts +2 -2
- package/lib/task/commit-intent.ts +137 -0
- package/lib/task/commit-operation.ts +174 -87
- package/lib/task/create.ts +34 -9
- package/lib/task/decision-details.ts +2 -62
- package/lib/task/delivery-target.ts +130 -0
- package/lib/task/delivery.ts +160 -0
- package/lib/task/human-override.ts +5 -2
- package/lib/task/lifecycle.ts +93 -4
- package/lib/task/orchestration.ts +62 -65
- package/lib/task/pr-delivery-fact.ts +254 -0
- package/lib/task/review-finalization.ts +4 -14
- package/lib/task/review-fingerprint.ts +10 -0
- package/lib/task/verification-engine.ts +45 -28
- package/lib/task/verification-review.ts +2 -0
- package/lib/update.ts +10 -0
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +4 -2
- package/templates/.agents/rules/README.zh-CN.md +4 -2
- package/templates/.agents/rules/create-issue.en.md +11 -2
- package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
- package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
- package/templates/.agents/rules/issue-sync.en.md +26 -5
- package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
- package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
- package/templates/.agents/rules/milestone-inference.en.md +12 -2
- package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
- package/templates/.agents/rules/pr-sync.en.md +22 -13
- package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
- package/templates/.agents/rules/release-commands.en.md +42 -3
- package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
- package/templates/.agents/rules/security-alerts.en.md +17 -2
- package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
- package/templates/.agents/rules/sync-content-generation.en.md +8 -0
- package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/scripts/security-alerts.github.sh +151 -0
- package/templates/.agents/scripts/security-alerts.sh +31 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/commit/SKILL.en.md +3 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/config/verify.json +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/templates/task.en.md +5 -1
- package/templates/.agents/templates/task.zh-CN.md +5 -1
- package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/feature-development.en.yaml +9 -8
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
- package/templates/.agents/workflows/refactoring.en.yaml +9 -8
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
- package/templates/.agents/rules/create-issue.github.en.md +0 -14
- package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
- package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
- package/templates/.agents/rules/issue-sync.github.en.md +0 -40
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
- package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
- package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
- package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
- package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
- package/templates/.agents/rules/pr-sync.github.en.md +0 -43
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
- package/templates/.agents/rules/release-commands.github.en.md +0 -44
- package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
- package/templates/.agents/rules/security-alerts.github.en.md +0 -43
- package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
- /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
- /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const PR_DELIVERY_FACT_KEY = 'pr_delivery_fact';
|
|
2
|
+
function factError(message) {
|
|
3
|
+
return Object.assign(new Error(`PR_DELIVERY_FACT_INVALID: ${message}`), { code: 'PR_DELIVERY_FACT_INVALID' });
|
|
4
|
+
}
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function exactKeys(value, keys, label) {
|
|
9
|
+
const actual = Object.keys(value).sort();
|
|
10
|
+
const expected = [...keys].sort();
|
|
11
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
12
|
+
throw factError(`${label} has an invalid field set`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function text(value, label) {
|
|
16
|
+
if (typeof value !== 'string' || !value || value.trim() !== value)
|
|
17
|
+
throw factError(`${label} must be a non-empty string`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function nullableText(value, label) {
|
|
21
|
+
return value === null ? null : text(value, label);
|
|
22
|
+
}
|
|
23
|
+
function positiveNumber(value, label) {
|
|
24
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
25
|
+
throw factError(`${label} must be a positive integer`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function optionalTimestamp(value, label) {
|
|
29
|
+
const result = nullableText(value, label);
|
|
30
|
+
if (result !== null && Number.isNaN(Date.parse(result)))
|
|
31
|
+
throw factError(`${label} must be a valid timestamp`);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function parseIdentity(value) {
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
throw factError('identity must be an object');
|
|
37
|
+
exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'identity');
|
|
38
|
+
const head = parseRef(value.head, 'head');
|
|
39
|
+
const base = parseRef(value.base, 'base');
|
|
40
|
+
return {
|
|
41
|
+
repository: text(value.repository, 'identity.repository'),
|
|
42
|
+
number: positiveNumber(value.number, 'identity.number'),
|
|
43
|
+
nodeId: text(value.nodeId, 'identity.nodeId'),
|
|
44
|
+
url: text(value.url, 'identity.url'),
|
|
45
|
+
head,
|
|
46
|
+
base
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function parseRef(value, label) {
|
|
50
|
+
if (!isRecord(value))
|
|
51
|
+
throw factError(`${label} must be an object`);
|
|
52
|
+
exactKeys(value, ['repository', 'ref', 'sha'], label);
|
|
53
|
+
return {
|
|
54
|
+
repository: text(value.repository, `${label}.repository`),
|
|
55
|
+
ref: text(value.ref, `${label}.ref`),
|
|
56
|
+
sha: text(value.sha, `${label}.sha`)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function parseFact(value) {
|
|
60
|
+
if (!isRecord(value))
|
|
61
|
+
throw factError('fact must be an object');
|
|
62
|
+
if (value.version !== 1 || typeof value.state !== 'string')
|
|
63
|
+
throw factError('version or state is invalid');
|
|
64
|
+
if (value.state === 'unbound') {
|
|
65
|
+
exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
|
|
66
|
+
if (value.reason !== 'initial')
|
|
67
|
+
throw factError('unbound reason is invalid');
|
|
68
|
+
return { version: 1, state: 'unbound', reason: value.reason };
|
|
69
|
+
}
|
|
70
|
+
if (value.state === 'skipped') {
|
|
71
|
+
exactKeys(value, ['version', 'state', 'reason', 'decidedAt'], 'skipped fact');
|
|
72
|
+
if (value.reason !== 'explicit')
|
|
73
|
+
throw factError('skipped reason is invalid');
|
|
74
|
+
const decidedAt = text(value.decidedAt, 'skipped.decidedAt');
|
|
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 };
|
|
78
|
+
}
|
|
79
|
+
if (value.state !== 'bound')
|
|
80
|
+
throw factError('state is invalid');
|
|
81
|
+
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'bound fact');
|
|
82
|
+
if (!isRecord(value.binding))
|
|
83
|
+
throw factError('binding must be an object');
|
|
84
|
+
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'binding');
|
|
85
|
+
if (value.binding.status !== 'verified')
|
|
86
|
+
throw factError('binding status is invalid');
|
|
87
|
+
const sources = ['created', 'reused', 'explicit-bind', 'external-unique', 'external-explicit'];
|
|
88
|
+
if (!sources.includes(value.binding.source))
|
|
89
|
+
throw factError('binding source is invalid');
|
|
90
|
+
const verifiedAt = text(value.binding.verifiedAt, 'binding.verifiedAt');
|
|
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');
|
|
94
|
+
if (value.binding.remoteState !== 'open' && value.binding.remoteState !== 'closed')
|
|
95
|
+
throw factError('binding.remoteState is invalid');
|
|
96
|
+
const mergedAt = optionalTimestamp(value.binding.mergedAt, 'binding.mergedAt');
|
|
97
|
+
const mergeCommitSha = nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha');
|
|
98
|
+
if (Boolean(mergedAt) !== Boolean(mergeCommitSha))
|
|
99
|
+
throw factError('mergedAt and mergeCommitSha must be paired');
|
|
100
|
+
if (!isRecord(value.provenance))
|
|
101
|
+
throw factError('provenance must be an object');
|
|
102
|
+
exactKeys(value.provenance, ['establishedBy'], 'provenance');
|
|
103
|
+
const provenance = ['create-post', 'reuse', 'explicit-bind', 'external-unique', 'external-explicit'];
|
|
104
|
+
if (!provenance.includes(value.provenance.establishedBy))
|
|
105
|
+
throw factError('provenance.establishedBy is invalid');
|
|
106
|
+
if (value.provenance.establishedBy !== provenanceForSource(value.binding.source)) {
|
|
107
|
+
throw factError('binding.source and provenance.establishedBy do not match');
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
version: 1,
|
|
111
|
+
state: 'bound',
|
|
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
|
+
},
|
|
122
|
+
provenance: { establishedBy: value.provenance.establishedBy }
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function decodePrDeliveryFact(value) {
|
|
126
|
+
if (typeof value === 'string') {
|
|
127
|
+
try {
|
|
128
|
+
return parseFact(JSON.parse(value));
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw error instanceof Error && error.message.startsWith('PR_DELIVERY_FACT_INVALID:') ? error : factError('value is not valid JSON');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return parseFact(value);
|
|
135
|
+
}
|
|
136
|
+
function encodePrDeliveryFact(fact) {
|
|
137
|
+
return JSON.stringify(parseFact(fact));
|
|
138
|
+
}
|
|
139
|
+
function readPrDeliveryFact(metadata) {
|
|
140
|
+
if (!Object.hasOwn(metadata, PR_DELIVERY_FACT_KEY) || metadata[PR_DELIVERY_FACT_KEY] === '')
|
|
141
|
+
return { status: 'missing', fact: null };
|
|
142
|
+
try {
|
|
143
|
+
return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]) };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return { status: 'invalid', fact: null, error: error instanceof Error ? error : factError(String(error)) };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function buildUnboundFact() {
|
|
150
|
+
return { version: 1, state: 'unbound', reason: 'initial' };
|
|
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
|
+
}
|
|
158
|
+
function buildBoundFact(input) {
|
|
159
|
+
return parseFact({
|
|
160
|
+
version: 1,
|
|
161
|
+
state: 'bound',
|
|
162
|
+
identity: input.identity,
|
|
163
|
+
binding: {
|
|
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) } };
|
|
177
|
+
}
|
|
178
|
+
function identityFromPullRequest(pullRequest) {
|
|
179
|
+
return parseIdentity(pullRequest);
|
|
180
|
+
}
|
|
181
|
+
export { PR_DELIVERY_FACT_KEY, buildBoundFact, buildSkippedFact, buildUnboundFact, decodePrDeliveryFact, encodePrDeliveryFact, factFrontmatterMutation, identityFromPullRequest, readPrDeliveryFact };
|
|
182
|
+
//# sourceMappingURL=pr-delivery-fact.js.map
|
|
@@ -5,7 +5,7 @@ import { locateActivityLog, pairEntries, startedBackedRows } from './activity-lo
|
|
|
5
5
|
import { parseArtifactName, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
6
6
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
7
7
|
import { finalizeReviewSummaryContent } from './review-artifacts.js';
|
|
8
|
-
import { inspectDecisionDetailDuplicates
|
|
8
|
+
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';
|
|
@@ -118,19 +118,15 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
|
|
|
118
118
|
return failed(request, 'REVIEW_LEDGER_INVALID', `${ledgerError.code}: ${ledgerError.message}`, resolved.taskId);
|
|
119
119
|
}
|
|
120
120
|
const stageStatus = summarizeLedgerStage(rows, stage);
|
|
121
|
-
const
|
|
122
|
-
if (!repaired.ok) {
|
|
123
|
-
return failed(request, 'REVIEW_DECISION_DETAIL_INVALID', `${repaired.code}: ${repaired.message}`, resolved.taskId, stageStatus);
|
|
124
|
-
}
|
|
125
|
-
const detailInspection = inspectDecisionDetailDuplicates(repaired.content);
|
|
121
|
+
const detailInspection = inspectDecisionDetailDuplicates(artifactContent);
|
|
126
122
|
if (!detailInspection.ok) {
|
|
127
123
|
return failed(request, 'REVIEW_DECISION_DETAIL_INVALID', `${detailInspection.code}: ${detailInspection.message}`, resolved.taskId, stageStatus);
|
|
128
124
|
}
|
|
129
|
-
const transformed = finalizeReviewSummaryContent(
|
|
125
|
+
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts);
|
|
130
126
|
if (!transformed.ok) {
|
|
131
127
|
return failed(request, transformed.code, transformed.message, resolved.taskId, stageStatus);
|
|
132
128
|
}
|
|
133
|
-
if (!
|
|
129
|
+
if (!transformed.changed) {
|
|
134
130
|
return {
|
|
135
131
|
...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, stageStatus),
|
|
136
132
|
status: 'no-op',
|
|
@@ -44,6 +44,14 @@ export function extractReviewBaseline(content) {
|
|
|
44
44
|
const match = String(content).match(/^[-*]?\s*\*\*(?:审查基线提交|Review Baseline Commit)\*\*[::]\s*(.*?)\s*$/m);
|
|
45
45
|
return match ? match[1].trim().replace(/`/g, "") : "";
|
|
46
46
|
}
|
|
47
|
+
export function extractReviewTargetHead(content) {
|
|
48
|
+
const match = String(content).match(/^[-*]?\s*\*\*(?:审查目标提交|审查目标 HEAD|Review Target Commit|Review Target Head)\*\*[::]\s*(.*?)\s*$/m);
|
|
49
|
+
return match ? match[1].trim().replace(/`/g, "") : "";
|
|
50
|
+
}
|
|
51
|
+
export function extractReviewedHead(content) {
|
|
52
|
+
const match = String(content).match(/^[-*]?\s*\*\*(?:审查已检视提交|已检视提交|Reviewed Head Commit|Reviewed Head)\*\*[::]\s*(.*?)\s*$/m);
|
|
53
|
+
return match ? match[1].trim().replace(/`/g, "") : "";
|
|
54
|
+
}
|
|
47
55
|
export function extractReviewDiffBase(content) {
|
|
48
56
|
const match = String(content).match(/^[-*]?\s*\*\*(?:审查差异基线|Reviewed Diff Base)\*\*[::]\s*(.*?)\s*$/m);
|
|
49
57
|
return match ? match[1].trim().replace(/`/g, "") : "";
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { extractReviewBaseline, extractReviewDiffBase, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from "./verification-review.js";
|
|
7
|
+
import { extractReviewBaseline, extractReviewTargetHead, extractReviewedHead, extractReviewDiffBase, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from "./verification-review.js";
|
|
8
8
|
import { check as checkPlatformSync } from "../platform/verification-sync.js";
|
|
9
9
|
import { check as checkRequiredChecks } from "../platform/verification-required.js";
|
|
10
10
|
import { inspectPlatformPullRequest } from "../platform/pull-requests.js";
|
|
@@ -19,6 +19,8 @@ import { inspectDecisionDetailDuplicates } from "./decision-details.js";
|
|
|
19
19
|
import { loadVerificationConfig } from "./verification-config.js";
|
|
20
20
|
import { snapshotReview } from "../git/review-snapshot.js";
|
|
21
21
|
import { OrchestrationStateError, readRun } from "./orchestration.js";
|
|
22
|
+
import { resolveDeliveryTarget } from "./delivery-target.js";
|
|
23
|
+
import { readPrDeliveryFact } from "./pr-delivery-fact.js";
|
|
22
24
|
const TASK_ENUMS = {
|
|
23
25
|
type: ["feature", "bugfix", "refactor", "docs", "chore"],
|
|
24
26
|
workflow: ["feature-development", "bug-fix", "refactoring"],
|
|
@@ -176,13 +178,12 @@ function validateCleanCompletionEvidence(run) {
|
|
|
176
178
|
|| !shaPattern.test(evidence.headTree)
|
|
177
179
|
|| !shaPattern.test(evidence.worktreeTree)
|
|
178
180
|
|| !shaPattern.test(evidence.lastReviewedCommit)
|
|
179
|
-
|| !Number.isInteger(evidence.prNumber)
|
|
180
|
-
|| evidence.
|
|
181
|
-
|| !shaPattern.test(evidence.prHead)) {
|
|
181
|
+
|| !(evidence.prNumber === null || (Number.isInteger(evidence.prNumber) && evidence.prNumber > 0))
|
|
182
|
+
|| !(evidence.prHead === null || shaPattern.test(evidence.prHead))) {
|
|
182
183
|
return 'Clean completion evidence has an invalid structure';
|
|
183
184
|
}
|
|
184
185
|
if (evidence.head !== evidence.lastReviewedCommit
|
|
185
|
-
|| evidence.head !== evidence.prHead
|
|
186
|
+
|| (evidence.prHead !== null && evidence.head !== evidence.prHead)
|
|
186
187
|
|| evidence.headTree !== evidence.worktreeTree) {
|
|
187
188
|
return 'Clean completion evidence does not bind matching commit and tree identities';
|
|
188
189
|
}
|
|
@@ -251,10 +252,8 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
251
252
|
if (evidenceError)
|
|
252
253
|
return failResult('orchestration-evidence', evidenceError);
|
|
253
254
|
}
|
|
254
|
-
else if (run.status === 'completed'
|
|
255
|
-
|
|
256
|
-
|| run.receipts.at(-1)?.status !== 'consumed')) {
|
|
257
|
-
return failResult('orchestration-evidence', 'Completed run requires a final consumed commit receipt');
|
|
255
|
+
else if (run.status === 'completed') {
|
|
256
|
+
return failResult('orchestration-evidence', 'Completed run requires reviewed-head-clean completion evidence');
|
|
258
257
|
}
|
|
259
258
|
if (run.pendingDelegation && !['prepared', 'activated', 'stage-completed'].includes(run.pendingDelegation.status)) {
|
|
260
259
|
return failResult('orchestration-evidence', `Pending receipt has unsafe status '${run.pendingDelegation.status}'`);
|
|
@@ -434,19 +433,20 @@ function checkRequiredPrDelivery({ taskDir, repositoryRoot, mode }) {
|
|
|
434
433
|
return failResult('required-pr-delivery', 'Project configuration is invalid');
|
|
435
434
|
}
|
|
436
435
|
const flow = projectConfig.prFlow;
|
|
437
|
-
const
|
|
438
|
-
|
|
436
|
+
const factRead = readPrDeliveryFact(task.metadata);
|
|
437
|
+
if (factRead.status === 'invalid')
|
|
438
|
+
return failResult('required-pr-delivery', factRead.error.message);
|
|
439
|
+
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
440
|
+
const status = fact?.state || 'unbound';
|
|
441
|
+
const prNumber = fact?.state === 'bound' ? fact.identity.number : null;
|
|
439
442
|
if (flow === 'disabled')
|
|
440
443
|
return passResult('required-pr-delivery', 'Pull request delivery is disabled by project policy');
|
|
441
|
-
if (flow === 'required' && (status !== '
|
|
444
|
+
if (flow === 'required' && (status !== 'bound' || prNumber === null)) {
|
|
442
445
|
return failResult('required-pr-delivery', 'Project policy requires a bound pull request before completion');
|
|
443
446
|
}
|
|
444
|
-
if (flow !== 'required' && status === '
|
|
447
|
+
if (flow !== 'required' && status === 'unbound') {
|
|
445
448
|
return failResult('required-pr-delivery', 'Pull request delivery is pending; create a PR or explicitly skip it');
|
|
446
449
|
}
|
|
447
|
-
if (status === 'created' && prNumber === null) {
|
|
448
|
-
return failResult('required-pr-delivery', 'Task claims a created pull request without a valid pr_number');
|
|
449
|
-
}
|
|
450
450
|
if (flow === 'required') {
|
|
451
451
|
if (mode === 'gate') {
|
|
452
452
|
return passResult('required-pr-delivery', 'Bound pull request shape is valid; merged-state inspection is reserved for hard preflight');
|
|
@@ -458,13 +458,15 @@ function checkRequiredPrDelivery({ taskDir, repositoryRoot, mode }) {
|
|
|
458
458
|
const pullRequest = inspected.pullRequest;
|
|
459
459
|
const repository = inspected.platform.repository?.toLowerCase() || '';
|
|
460
460
|
const taskBranch = String(task.metadata.branch || '').trim();
|
|
461
|
+
const deliveryBaseRef = String(task.metadata.delivery_base_ref || projectConfig.delivery?.baseRef || '').trim();
|
|
461
462
|
if (inspected.status !== 'no-op' || !pullRequest || pullRequest.number !== prNumber) {
|
|
462
463
|
return failResult('required-pr-delivery', 'Bound pull request identity could not be verified');
|
|
463
464
|
}
|
|
464
465
|
if (!repository
|
|
465
466
|
|| pullRequest.repository.toLowerCase() !== repository
|
|
466
467
|
|| pullRequest.base.repository.toLowerCase() !== repository
|
|
467
|
-
|| (taskBranch && pullRequest.head.ref !== taskBranch)
|
|
468
|
+
|| (taskBranch && pullRequest.head.ref !== taskBranch)
|
|
469
|
+
|| (deliveryBaseRef && pullRequest.base.ref !== deliveryBaseRef)) {
|
|
468
470
|
return failResult('required-pr-delivery', 'Bound pull request identity does not match the task');
|
|
469
471
|
}
|
|
470
472
|
if (pullRequest.state !== 'closed' || !pullRequest.mergedAt || !pullRequest.mergeCommitSha) {
|
|
@@ -966,9 +968,13 @@ function checkPostReviewCommit({ taskDir, config }) {
|
|
|
966
968
|
const task = loadTask(taskDir);
|
|
967
969
|
if (!task.ok)
|
|
968
970
|
return failResult("post-review-commit", task.message);
|
|
971
|
+
const factRead = readPrDeliveryFact(task.metadata);
|
|
972
|
+
if (factRead.status === 'invalid')
|
|
973
|
+
return failResult("post-review-commit", factRead.error.message);
|
|
974
|
+
const fact = factRead.status === 'valid' ? factRead.fact : null;
|
|
969
975
|
const lastReviewedCommit = task.ok ? (task.metadata.last_reviewed_commit || "").trim() : "";
|
|
970
976
|
const globs = resolvePostReviewGlobs(config, loadPostReviewConfig(repoRoot));
|
|
971
|
-
const hasPullRequest =
|
|
977
|
+
const hasPullRequest = fact?.state === 'bound';
|
|
972
978
|
let inspected = null;
|
|
973
979
|
if (hasPullRequest) {
|
|
974
980
|
inspected = inspectPlatformPullRequest(task.metadata.id, { cwd: gitRoot });
|
|
@@ -1124,28 +1130,46 @@ function checkReviewFact({ taskDir, artifactFile }) {
|
|
|
1124
1130
|
}
|
|
1125
1131
|
const content = fs.readFileSync(resolvedArtifact.path, "utf8");
|
|
1126
1132
|
const verdict = parseReviewVerdict(content);
|
|
1127
|
-
const
|
|
1133
|
+
const reviewTargetHead = extractReviewTargetHead(content);
|
|
1134
|
+
const reviewReviewedHead = extractReviewedHead(content);
|
|
1128
1135
|
const reviewDiffBase = extractReviewDiffBase(content);
|
|
1129
1136
|
const reviewedFingerprint = extractReviewDiffFingerprint(content);
|
|
1130
1137
|
const reviewedTree = extractReviewedSnapshotTree(content);
|
|
1131
1138
|
if (!["通过", "需要修改", "拒绝", "Approved", "Changes Requested", "Rejected"].includes(verdict)) {
|
|
1132
1139
|
return failResult("review-fact", `Unsupported review verdict '${verdict}'`);
|
|
1133
1140
|
}
|
|
1141
|
+
const deliveryRemote = String(task.metadata.delivery_remote || "").trim();
|
|
1142
|
+
const deliveryBaseRef = String(task.metadata.delivery_base_ref || "").trim();
|
|
1143
|
+
if (!deliveryRemote || !deliveryBaseRef) {
|
|
1144
|
+
return failResult("review-fact", "Task delivery target is not bound; re-run task branch preparation before review-code");
|
|
1145
|
+
}
|
|
1146
|
+
if (!reviewTargetHead || !reviewReviewedHead || !reviewDiffBase) {
|
|
1147
|
+
return failResult("review-fact", "Review fact must record target head, reviewed head, and diff base");
|
|
1148
|
+
}
|
|
1134
1149
|
let gitRoot;
|
|
1135
1150
|
let head;
|
|
1136
1151
|
let baseline;
|
|
1152
|
+
let targetHead;
|
|
1137
1153
|
let diffBase;
|
|
1138
1154
|
try {
|
|
1139
1155
|
gitRoot = execFileSync("git", ["-C", taskDir, "rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
1140
1156
|
head = execFileSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
1141
|
-
|
|
1142
|
-
|
|
1157
|
+
const target = resolveDeliveryTarget(gitRoot, { remote: deliveryRemote, baseRef: deliveryBaseRef });
|
|
1158
|
+
if (!target.ok)
|
|
1159
|
+
throw new Error(target.message);
|
|
1160
|
+
const reviewedRef = reviewReviewedHead;
|
|
1161
|
+
baseline = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewedRef}^{commit}`], { encoding: "utf8" }).trim();
|
|
1162
|
+
targetHead = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewTargetHead}^{commit}`], { encoding: "utf8" }).trim();
|
|
1163
|
+
const computed = execFileSync("git", ["-C", gitRoot, "merge-base", baseline, targetHead], { encoding: "utf8" }).trim();
|
|
1164
|
+
diffBase = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewDiffBase}^{commit}`], { encoding: "utf8" }).trim();
|
|
1165
|
+
if (diffBase !== computed)
|
|
1166
|
+
throw new Error('saved review diff base does not match merge-base(reviewed head, target head)');
|
|
1143
1167
|
}
|
|
1144
1168
|
catch {
|
|
1145
|
-
return blockedResult("review-fact", `Unable to resolve review
|
|
1169
|
+
return blockedResult("review-fact", `Unable to resolve saved review commits in the task repository; re-run review-code`);
|
|
1146
1170
|
}
|
|
1147
1171
|
if (baseline !== head) {
|
|
1148
|
-
return failResult("review-fact", `
|
|
1172
|
+
return failResult("review-fact", `Reviewed head ${baseline.slice(0, 8)} does not match current HEAD ${head.slice(0, 8)}; re-run review-code`);
|
|
1149
1173
|
}
|
|
1150
1174
|
let actualSnapshot;
|
|
1151
1175
|
try {
|
|
@@ -1170,7 +1194,7 @@ function checkReviewFact({ taskDir, artifactFile }) {
|
|
|
1170
1194
|
const lastReviewedCommit = String(task.metadata.last_reviewed_commit || "").trim();
|
|
1171
1195
|
const cleanSnapshot = actualSnapshot.tree === execFileSync("git", ["-C", gitRoot, "rev-parse", `${baseline}^{tree}`], { encoding: "utf8" }).trim();
|
|
1172
1196
|
if (cleanSnapshot && lastReviewedCommit !== baseline) {
|
|
1173
|
-
return failResult("review-fact", `Approved clean review must set task last_reviewed_commit to
|
|
1197
|
+
return failResult("review-fact", `Approved clean review must set task last_reviewed_commit to reviewed head ${baseline.slice(0, 8)}`);
|
|
1174
1198
|
}
|
|
1175
1199
|
if (!cleanSnapshot && lastReviewedCommit) {
|
|
1176
1200
|
return failResult("review-fact", "Approved review with uncommitted changes must remain unanchored until commit");
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { extractReviewBaseline, extractReviewDiffBase, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from './review-fingerprint.js';
|
|
1
|
+
export { extractReviewBaseline, extractReviewTargetHead, extractReviewedHead, extractReviewDiffBase, extractReviewDiffFingerprint, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, loadPostReviewConfig, parseReviewVerdict, resolvePostReviewGlobs } from './review-fingerprint.js';
|
|
2
2
|
export { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
3
3
|
//# sourceMappingURL=verification-review.js.map
|
package/dist/lib/update.js
CHANGED
|
@@ -43,6 +43,7 @@ async function cmdSync() {
|
|
|
43
43
|
const sandboxAdded = !config.sandbox;
|
|
44
44
|
const taskAdded = !config.task;
|
|
45
45
|
const labelsAdded = !config.labels;
|
|
46
|
+
const deliveryAdded = !config.delivery;
|
|
46
47
|
if (platformAdded)
|
|
47
48
|
config.platform = structuredClone(defaults.platform);
|
|
48
49
|
if (sandboxAdded)
|
|
@@ -51,6 +52,8 @@ async function cmdSync() {
|
|
|
51
52
|
config.task = structuredClone(defaults.task);
|
|
52
53
|
if (labelsAdded)
|
|
53
54
|
config.labels = structuredClone(defaults.labels);
|
|
55
|
+
if (deliveryAdded)
|
|
56
|
+
config.delivery = structuredClone(defaults.delivery);
|
|
54
57
|
const workflowPlan = planAgentClientReconciliation({
|
|
55
58
|
config: config,
|
|
56
59
|
mutation: { type: 'replace-state', state: normalizedOriginal.state },
|
|
@@ -98,6 +101,9 @@ async function cmdSync() {
|
|
|
98
101
|
if (labelsAdded) {
|
|
99
102
|
info(`Default labels.in config added to ${CONFIG_PATH}.`);
|
|
100
103
|
}
|
|
104
|
+
if (deliveryAdded) {
|
|
105
|
+
info(`Default delivery target added to ${CONFIG_PATH}.`);
|
|
106
|
+
}
|
|
101
107
|
}
|
|
102
108
|
else {
|
|
103
109
|
info(`File registry changed in ${CONFIG_PATH}.`);
|
|
@@ -114,6 +120,9 @@ async function cmdSync() {
|
|
|
114
120
|
if (hasNewEntries && platformAdded) {
|
|
115
121
|
info(`Default platform config added to ${CONFIG_PATH}.`);
|
|
116
122
|
}
|
|
123
|
+
if (hasNewEntries && deliveryAdded) {
|
|
124
|
+
info(`Default delivery target added to ${CONFIG_PATH}.`);
|
|
125
|
+
}
|
|
117
126
|
ok(`Updated ${CONFIG_PATH}`);
|
|
118
127
|
}
|
|
119
128
|
// done
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocolVersion": 3,
|
|
3
|
-
"packageVersion": "0.9.
|
|
4
|
-
"sourceInputHash": "
|
|
5
|
-
"internalExecutableBuildHash": "
|
|
3
|
+
"packageVersion": "0.9.12",
|
|
4
|
+
"sourceInputHash": "5e1b341aad5fe825884f4dbdbd01d0752ca66107d1ccb4b0fe4d8d3aae49120d",
|
|
5
|
+
"internalExecutableBuildHash": "39bf2c60e3a9b834d4a24ab3088f108583d15ee2bdc77e6376e517921be47c80"
|
|
6
6
|
}
|
|
@@ -22,6 +22,14 @@ type LifecycleBuildIdentityOptions = Readonly<{
|
|
|
22
22
|
type LifecycleManifestFiles = Readonly<{
|
|
23
23
|
executableFiles: readonly string[];
|
|
24
24
|
contractFiles: readonly string[];
|
|
25
|
+
launcherShebang: LifecycleLauncherShebang;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
type LifecycleLauncherShebang = Readonly<{
|
|
29
|
+
sourceFile: string;
|
|
30
|
+
compiledFile: string;
|
|
31
|
+
canonicalLine: string;
|
|
32
|
+
acceptedLines: readonly string[];
|
|
25
33
|
}>;
|
|
26
34
|
|
|
27
35
|
type IdentityVerification = Readonly<{
|
|
@@ -53,6 +61,56 @@ function normalizeRelativePath(value: string): string {
|
|
|
53
61
|
return value.replaceAll('\\', '/');
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
function isSafeLifecyclePath(value: unknown): value is string {
|
|
65
|
+
return exactText(value)
|
|
66
|
+
&& value === normalizeRelativePath(value)
|
|
67
|
+
&& !path.isAbsolute(value)
|
|
68
|
+
&& !/^[A-Za-z]:[\\/]/u.test(value)
|
|
69
|
+
&& !value.split('/').includes('..');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isLifecycleLauncherLine(value: unknown): value is string {
|
|
73
|
+
return typeof value === 'string'
|
|
74
|
+
&& value.length > 1
|
|
75
|
+
&& value.slice(0, -1).trim() === value.slice(0, -1)
|
|
76
|
+
&& value.startsWith('#!')
|
|
77
|
+
&& value.endsWith('\n')
|
|
78
|
+
&& value.indexOf('\n') === value.length - 1
|
|
79
|
+
&& !value.includes('\r');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readLifecycleLauncherShebang(
|
|
83
|
+
value: unknown,
|
|
84
|
+
executableFiles: readonly string[]
|
|
85
|
+
): LifecycleLauncherShebang {
|
|
86
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
87
|
+
throw new Error('Lifecycle manifest launcher shebang policy is invalid');
|
|
88
|
+
}
|
|
89
|
+
const policy = value as Record<string, unknown>;
|
|
90
|
+
if (!isSafeLifecyclePath(policy.sourceFile)
|
|
91
|
+
|| !isSafeLifecyclePath(policy.compiledFile)
|
|
92
|
+
|| !isLifecycleLauncherLine(policy.canonicalLine)
|
|
93
|
+
|| !Array.isArray(policy.acceptedLines)
|
|
94
|
+
|| !policy.acceptedLines.every(isLifecycleLauncherLine)
|
|
95
|
+
|| new Set(policy.acceptedLines).size !== policy.acceptedLines.length
|
|
96
|
+
|| !policy.acceptedLines.includes(policy.canonicalLine)
|
|
97
|
+
|| !executableFiles.includes(policy.sourceFile)
|
|
98
|
+
|| policy.compiledFile !== normalizeRelativePath(path.join(
|
|
99
|
+
'dist',
|
|
100
|
+
policy.sourceFile.endsWith('.ts')
|
|
101
|
+
? policy.sourceFile.replace(/\.ts$/u, '.js')
|
|
102
|
+
: policy.sourceFile
|
|
103
|
+
))) {
|
|
104
|
+
throw new Error('Lifecycle manifest launcher shebang policy is invalid');
|
|
105
|
+
}
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
sourceFile: policy.sourceFile,
|
|
108
|
+
compiledFile: policy.compiledFile,
|
|
109
|
+
canonicalLine: policy.canonicalLine,
|
|
110
|
+
acceptedLines: Object.freeze([...policy.acceptedLines])
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
function readLifecycleManifestFiles(packageRoot: string): LifecycleManifestFiles {
|
|
57
115
|
const file = path.join(
|
|
58
116
|
packageRoot,
|
|
@@ -65,18 +123,21 @@ function readLifecycleManifestFiles(packageRoot: string): LifecycleManifestFiles
|
|
|
65
123
|
const value = JSON.parse(fs.readFileSync(file, 'utf8')) as {
|
|
66
124
|
executableFiles?: unknown;
|
|
67
125
|
contractFiles?: unknown;
|
|
126
|
+
launcherShebang?: unknown;
|
|
68
127
|
};
|
|
69
128
|
if (!Array.isArray(value.executableFiles)
|
|
70
129
|
|| !Array.isArray(value.contractFiles)
|
|
71
|
-
|| !value.executableFiles.every(
|
|
72
|
-
|| !value.contractFiles.every(
|
|
130
|
+
|| !value.executableFiles.every(isSafeLifecyclePath)
|
|
131
|
+
|| !value.contractFiles.every(isSafeLifecyclePath)
|
|
73
132
|
|| new Set(value.executableFiles).size !== value.executableFiles.length
|
|
74
133
|
|| new Set(value.contractFiles).size !== value.contractFiles.length) {
|
|
75
134
|
throw new Error('Lifecycle manifest file list is invalid');
|
|
76
135
|
}
|
|
136
|
+
const executableFiles = Object.freeze([...value.executableFiles] as string[]);
|
|
77
137
|
return Object.freeze({
|
|
78
|
-
executableFiles
|
|
79
|
-
contractFiles: Object.freeze([...value.contractFiles] as string[])
|
|
138
|
+
executableFiles,
|
|
139
|
+
contractFiles: Object.freeze([...value.contractFiles] as string[]),
|
|
140
|
+
launcherShebang: readLifecycleLauncherShebang(value.launcherShebang, executableFiles)
|
|
80
141
|
});
|
|
81
142
|
}
|
|
82
143
|
|
|
@@ -94,7 +155,28 @@ function executablePackageRoot(): string {
|
|
|
94
155
|
}
|
|
95
156
|
}
|
|
96
157
|
|
|
97
|
-
function
|
|
158
|
+
function normalizeLifecycleLauncherBytes(
|
|
159
|
+
relative: string,
|
|
160
|
+
content: Buffer,
|
|
161
|
+
policy: LifecycleLauncherShebang
|
|
162
|
+
): Buffer {
|
|
163
|
+
const normalized = normalizeRelativePath(relative);
|
|
164
|
+
if (normalized !== policy.sourceFile && normalized !== policy.compiledFile) return content;
|
|
165
|
+
const lineEnd = content.indexOf(0x0a);
|
|
166
|
+
if (lineEnd < 0) return content;
|
|
167
|
+
const firstLine = content.subarray(0, lineEnd + 1);
|
|
168
|
+
if (!policy.acceptedLines.some((line) => Buffer.from(line).equals(firstLine))) return content;
|
|
169
|
+
return Buffer.concat([
|
|
170
|
+
Buffer.from(policy.canonicalLine),
|
|
171
|
+
content.subarray(lineEnd + 1)
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hashFiles(
|
|
176
|
+
root: string,
|
|
177
|
+
files: readonly string[],
|
|
178
|
+
launcherShebang?: LifecycleLauncherShebang
|
|
179
|
+
): string {
|
|
98
180
|
const hash = crypto.createHash('sha256');
|
|
99
181
|
for (const relative of [...new Set(files.map(normalizeRelativePath))].sort()) {
|
|
100
182
|
if (path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..')) {
|
|
@@ -107,7 +189,10 @@ function hashFiles(root: string, files: readonly string[]): string {
|
|
|
107
189
|
}
|
|
108
190
|
hash.update(relative);
|
|
109
191
|
hash.update('\0');
|
|
110
|
-
|
|
192
|
+
const content = fs.readFileSync(file);
|
|
193
|
+
hash.update(launcherShebang
|
|
194
|
+
? normalizeLifecycleLauncherBytes(relative, content, launcherShebang)
|
|
195
|
+
: content);
|
|
111
196
|
hash.update('\0');
|
|
112
197
|
}
|
|
113
198
|
return hash.digest('hex');
|
|
@@ -157,7 +242,7 @@ function computeLifecycleBuildIdentity(
|
|
|
157
242
|
if (!exactText(packageJson.version)) throw new Error('Lifecycle package version is invalid');
|
|
158
243
|
let internalExecutableBuildHash: string;
|
|
159
244
|
if (options.executableFiles) {
|
|
160
|
-
internalExecutableBuildHash = hashFiles(repoRoot, options.executableFiles);
|
|
245
|
+
internalExecutableBuildHash = hashFiles(repoRoot, options.executableFiles, defaults.launcherShebang);
|
|
161
246
|
} else {
|
|
162
247
|
const manifestFile = path.join(executableRoot, 'dist', 'lifecycle-build-manifest.json');
|
|
163
248
|
if (fs.existsSync(manifestFile)) {
|
|
@@ -177,16 +262,25 @@ function computeLifecycleBuildIdentity(
|
|
|
177
262
|
const compiledFiles = defaults.executableFiles.map((file) =>
|
|
178
263
|
file.endsWith('.ts') ? path.join('dist', file.replace(/\.ts$/u, '.js')) : file
|
|
179
264
|
);
|
|
180
|
-
if (hashFiles(
|
|
265
|
+
if (hashFiles(
|
|
266
|
+
executableRoot,
|
|
267
|
+
resolveLifecycleExecutableFiles(executableRoot, compiledFiles),
|
|
268
|
+
defaults.launcherShebang
|
|
269
|
+
) !== manifest.internalExecutableBuildHash) {
|
|
181
270
|
throw new Error('Lifecycle compiled executable build does not match its manifest');
|
|
182
271
|
}
|
|
183
|
-
} else if (hashFiles(
|
|
272
|
+
} else if (hashFiles(
|
|
273
|
+
executableRoot,
|
|
274
|
+
resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles),
|
|
275
|
+
defaults.launcherShebang
|
|
276
|
+
) !== manifest.sourceInputHash) {
|
|
184
277
|
throw new Error('Lifecycle executable build manifest is stale or invalid');
|
|
185
278
|
}
|
|
186
279
|
internalExecutableBuildHash = manifest.internalExecutableBuildHash;
|
|
187
280
|
} else internalExecutableBuildHash = hashFiles(
|
|
188
281
|
executableRoot,
|
|
189
|
-
resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles)
|
|
282
|
+
resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles),
|
|
283
|
+
defaults.launcherShebang
|
|
190
284
|
);
|
|
191
285
|
}
|
|
192
286
|
return Object.freeze({
|
|
@@ -249,6 +343,7 @@ export type {
|
|
|
249
343
|
IdentityVerification,
|
|
250
344
|
LifecycleIdentityWarning,
|
|
251
345
|
LifecycleBuildIdentity,
|
|
346
|
+
LifecycleLauncherShebang,
|
|
252
347
|
LifecycleManifestFiles,
|
|
253
348
|
LifecycleBuildIdentityOptions
|
|
254
349
|
};
|
|
@@ -30,5 +30,16 @@
|
|
|
30
30
|
".agents/hooks/lifecycle-delegation.js",
|
|
31
31
|
".agents/skills/run-task/SKILL.md",
|
|
32
32
|
".agents/rules/lifecycle-orchestration.md"
|
|
33
|
-
]
|
|
33
|
+
],
|
|
34
|
+
"launcherShebang": {
|
|
35
|
+
"sourceFile": "bin/internal-cli.ts",
|
|
36
|
+
"compiledFile": "dist/bin/internal-cli.js",
|
|
37
|
+
"canonicalLine": "#!/usr/bin/env node\n",
|
|
38
|
+
"acceptedLines": [
|
|
39
|
+
"#!/usr/bin/env node\n",
|
|
40
|
+
"#!/opt/homebrew/opt/node/bin/node\n",
|
|
41
|
+
"#!/usr/local/opt/node/bin/node\n",
|
|
42
|
+
"#!/home/linuxbrew/.linuxbrew/opt/node/bin/node\n"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
34
45
|
}
|