@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.
Files changed (193) hide show
  1. package/README.md +6 -6
  2. package/README.zh-CN.md +6 -6
  3. package/dist/bin/internal-cli.js +5 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
  5. package/dist/lib/defaults.json +4 -0
  6. package/dist/lib/git/workflow.js +43 -1
  7. package/dist/lib/init.js +1 -0
  8. package/dist/lib/internal/git-workflow.js +8 -1
  9. package/dist/lib/internal/platform-pr.js +8 -2
  10. package/dist/lib/internal/task-delivery.js +59 -0
  11. package/dist/lib/platform/github-client.js +7 -1
  12. package/dist/lib/platform/pr-checks.js +8 -5
  13. package/dist/lib/platform/pr-summary.js +12 -8
  14. package/dist/lib/platform/pull-requests.js +393 -91
  15. package/dist/lib/platform/verification-required.js +13 -2
  16. package/dist/lib/platform/verification-sync.js +9 -4
  17. package/dist/lib/pr-review/evidence-grading.js +12 -4
  18. package/dist/lib/sandbox/config.js +5 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +29 -7
  20. package/dist/lib/server/process-state.js +2 -2
  21. package/dist/lib/task/artifact-lifecycle.js +2 -2
  22. package/dist/lib/task/commit-intent.js +100 -0
  23. package/dist/lib/task/commit-operation.js +195 -101
  24. package/dist/lib/task/create.js +33 -7
  25. package/dist/lib/task/decision-details.js +3 -48
  26. package/dist/lib/task/delivery-target.js +97 -0
  27. package/dist/lib/task/delivery.js +153 -0
  28. package/dist/lib/task/human-override.js +5 -2
  29. package/dist/lib/task/lifecycle.js +106 -9
  30. package/dist/lib/task/orchestration.js +55 -62
  31. package/dist/lib/task/pr-delivery-fact.js +182 -0
  32. package/dist/lib/task/review-finalization.js +4 -8
  33. package/dist/lib/task/review-fingerprint.js +8 -0
  34. package/dist/lib/task/verification-engine.js +48 -24
  35. package/dist/lib/task/verification-review.js +1 -1
  36. package/dist/lib/update.js +9 -0
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
  39. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
  40. package/lib/defaults.json +4 -0
  41. package/lib/git/workflow.ts +38 -1
  42. package/lib/init.ts +3 -0
  43. package/lib/internal/git-workflow.ts +7 -1
  44. package/lib/internal/platform-pr.ts +8 -1
  45. package/lib/internal/task-delivery.ts +53 -0
  46. package/lib/platform/github-client.ts +8 -1
  47. package/lib/platform/pr-checks.ts +7 -5
  48. package/lib/platform/pr-summary.ts +10 -7
  49. package/lib/platform/pull-requests.ts +425 -87
  50. package/lib/platform/verification-required.ts +12 -2
  51. package/lib/platform/verification-sync.ts +9 -4
  52. package/lib/pr-review/evidence-grading.ts +7 -4
  53. package/lib/sandbox/config.ts +7 -0
  54. package/lib/sandbox/control/lifecycle.ts +30 -8
  55. package/lib/server/process-state.ts +2 -2
  56. package/lib/task/artifact-lifecycle.ts +2 -2
  57. package/lib/task/commit-intent.ts +137 -0
  58. package/lib/task/commit-operation.ts +174 -87
  59. package/lib/task/create.ts +34 -9
  60. package/lib/task/decision-details.ts +2 -62
  61. package/lib/task/delivery-target.ts +130 -0
  62. package/lib/task/delivery.ts +160 -0
  63. package/lib/task/human-override.ts +5 -2
  64. package/lib/task/lifecycle.ts +93 -4
  65. package/lib/task/orchestration.ts +62 -65
  66. package/lib/task/pr-delivery-fact.ts +254 -0
  67. package/lib/task/review-finalization.ts +4 -14
  68. package/lib/task/review-fingerprint.ts +10 -0
  69. package/lib/task/verification-engine.ts +45 -28
  70. package/lib/task/verification-review.ts +2 -0
  71. package/lib/update.ts +10 -0
  72. package/package.json +1 -1
  73. package/templates/.agents/rules/README.en.md +4 -2
  74. package/templates/.agents/rules/README.zh-CN.md +4 -2
  75. package/templates/.agents/rules/create-issue.en.md +11 -2
  76. package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
  77. package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
  78. package/templates/.agents/rules/issue-sync.en.md +26 -5
  79. package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
  80. package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
  81. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
  82. package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
  83. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
  84. package/templates/.agents/rules/milestone-inference.en.md +12 -2
  85. package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
  86. package/templates/.agents/rules/pr-sync.en.md +22 -13
  87. package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
  88. package/templates/.agents/rules/release-commands.en.md +42 -3
  89. package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
  90. package/templates/.agents/rules/security-alerts.en.md +17 -2
  91. package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
  92. package/templates/.agents/rules/sync-content-generation.en.md +8 -0
  93. package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
  94. package/templates/.agents/rules/task-management.en.md +1 -1
  95. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  96. package/templates/.agents/scripts/security-alerts.github.sh +151 -0
  97. package/templates/.agents/scripts/security-alerts.sh +31 -0
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  100. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  101. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  102. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
  106. package/templates/.agents/skills/commit/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/commit/config/verify.json +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
  115. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
  116. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
  117. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  118. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  119. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  120. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  121. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
  122. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
  123. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
  124. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
  125. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  126. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  127. package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
  128. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
  129. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
  134. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
  135. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
  136. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
  137. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
  138. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
  139. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
  140. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
  141. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
  142. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
  143. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
  144. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
  145. package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
  146. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
  147. package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
  148. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
  149. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
  150. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
  151. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
  152. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
  153. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
  154. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
  155. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
  156. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
  157. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
  158. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
  159. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
  160. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  163. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
  164. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  165. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  166. package/templates/.agents/templates/task.en.md +5 -1
  167. package/templates/.agents/templates/task.zh-CN.md +5 -1
  168. package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
  169. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
  170. package/templates/.agents/workflows/feature-development.en.yaml +9 -8
  171. package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
  172. package/templates/.agents/workflows/refactoring.en.yaml +9 -8
  173. package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
  174. package/templates/.agents/rules/create-issue.github.en.md +0 -14
  175. package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
  176. package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
  177. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
  178. package/templates/.agents/rules/issue-sync.github.en.md +0 -40
  179. package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
  180. package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
  181. package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
  182. package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
  183. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
  184. package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
  185. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
  186. package/templates/.agents/rules/pr-sync.github.en.md +0 -43
  187. package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
  188. package/templates/.agents/rules/release-commands.github.en.md +0 -44
  189. package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
  190. package/templates/.agents/rules/security-alerts.github.en.md +0 -43
  191. package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
  192. /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
  193. /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, repairDecisionDetailDuplicates } from './decision-details.js';
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 repaired = repairDecisionDetailDuplicates(artifactContent);
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(repaired.content, stageStatus.unresolvedFindingCounts);
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 (!repaired.changed && !transformed.changed) {
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.prNumber <= 0
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' && (run.receipts.length === 0
255
- || run.receipts.at(-1)?.stage !== 'commit'
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 status = String(task.metadata.pr_status || 'pending');
438
- const prNumber = parsePrNumber(task.metadata.pr_number);
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 !== 'created' || prNumber === null)) {
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 === 'pending') {
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 = Number(task.metadata.pr_number) > 0;
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 reviewBaseline = extractReviewBaseline(content);
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
- baseline = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewBaseline}^{commit}`], { encoding: "utf8" }).trim();
1142
- diffBase = execFileSync("git", ["-C", gitRoot, "rev-parse", `${reviewDiffBase || reviewBaseline}^{commit}`], { encoding: "utf8" }).trim();
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 baseline '${reviewBaseline}' in the task repository; re-run review-code`);
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", `Review baseline ${baseline.slice(0, 8)} does not match current HEAD ${head.slice(0, 8)}; re-run review-code`);
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 baseline ${baseline.slice(0, 8)}`);
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
@@ -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.11",
4
- "sourceInputHash": "00e07caf1313615df10d46ae620767d7d15c3cc59712c1da581623057b87e0a6",
5
- "internalExecutableBuildHash": "8a481b1d79da5e5f31ef79cd3428f1b51df2b859e70724e50dcc5910f8ee40c6"
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(exactText)
72
- || !value.contractFiles.every(exactText)
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: Object.freeze([...value.executableFiles] as string[]),
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 hashFiles(root: string, files: readonly string[]): string {
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
- hash.update(fs.readFileSync(file));
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(executableRoot, resolveLifecycleExecutableFiles(executableRoot, compiledFiles)) !== manifest.internalExecutableBuildHash) {
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(executableRoot, resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles)) !== manifest.sourceInputHash) {
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
  }