@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.1

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 (103) hide show
  1. package/README.md +4 -2
  2. package/actions/promote-buildchain-ref/README.md +10 -0
  3. package/actions/release-tail/README.md +17 -0
  4. package/bin/buildchain.mjs +11 -0
  5. package/bin/internal/command-registry.mjs +2 -0
  6. package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
  7. package/contracts/engineering-housekeeper-v1.schema.json +143 -0
  8. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
  9. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  10. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  11. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  12. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  13. package/dist/site/agent-index.json +3 -0
  14. package/dist/site/artifact-schemas.json +6 -0
  15. package/dist/site/buildchain-contract.json +42 -27
  16. package/dist/site/buildchain-site.json +384 -33
  17. package/dist/site/capability-registry.json +15 -12
  18. package/dist/site/cli-registry.json +102 -0
  19. package/dist/site/controller-registry.json +19 -3
  20. package/dist/site/kfd-claims.json +359 -17
  21. package/dist/site/kfd-upstream-aggregate.json +1 -1
  22. package/dist/site/manual-registry.json +53 -7
  23. package/dist/site/node-api-registry.json +5882 -3605
  24. package/dist/site/page-registry.json +348 -21
  25. package/dist/site/public-surface-audit.json +396 -16
  26. package/dist/site/publication-authority-registry.json +106 -1
  27. package/dist/site/publication-registry.json +4 -4
  28. package/dist/site/release-provenance.json +7 -0
  29. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  30. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  31. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  32. package/dist/site/site-manifest.json +35 -11
  33. package/dist/site/workflow-registry.json +204 -10
  34. package/docs/MAP.md +6 -2
  35. package/docs/auditable-demo.md +2 -2
  36. package/docs/cli-reference.md +136 -0
  37. package/docs/dev-delivery-warrant.md +49 -4
  38. package/docs/engineering-housekeeper.md +138 -0
  39. package/docs/lifecycle-protocol.md +4 -2
  40. package/docs/node-api-reference.md +495 -281
  41. package/docs/publication-rehearsal.md +94 -0
  42. package/docs/release-governance.md +17 -2
  43. package/docs/release-tail-contract.md +160 -0
  44. package/docs/release-tail-provider-plane.md +120 -0
  45. package/docs/reusable-build-surface.md +11 -0
  46. package/package.json +11 -3
  47. package/packages/core/artifact-signing.js +61 -0
  48. package/packages/core/buildchain-agent-manuals.js +3 -0
  49. package/packages/core/buildchain-config.js +66 -6
  50. package/packages/core/buildchain-kfd-claims.js +1 -1
  51. package/packages/core/buildchain-publication-authority.js +5 -0
  52. package/packages/core/controller-evidence.js +2 -1
  53. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  54. package/packages/core/dev-delivery-warrant-shadow.js +502 -0
  55. package/packages/core/dev-delivery-warrant.js +15 -6
  56. package/packages/core/diagnostics.js +8 -3
  57. package/packages/core/engineering-housekeeper-github-client.js +222 -0
  58. package/packages/core/engineering-housekeeper-github.js +501 -0
  59. package/packages/core/engineering-housekeeper.js +259 -0
  60. package/packages/core/index.js +42 -0
  61. package/packages/core/kfd-gate.js +45 -15
  62. package/packages/core/paper-agent-entry.js +11 -5
  63. package/packages/core/paper-repository.js +1 -0
  64. package/packages/core/paper-scaffold-content.js +21 -0
  65. package/packages/core/paper.js +28 -2
  66. package/packages/core/publication-rehearsal-projection.js +173 -0
  67. package/packages/core/publication-rehearsal-runtime.js +921 -0
  68. package/packages/core/release-passport.js +130 -20
  69. package/packages/core/release-tail-compatibility.js +60 -0
  70. package/packages/core/release-tail-provider-adapters.js +461 -0
  71. package/packages/core/release-tail-provider-plane.js +1228 -0
  72. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  73. package/scripts/assemble-self-publication-admission.mjs +2 -2
  74. package/scripts/audit-publication-control-plane.mjs +1 -1
  75. package/scripts/auditable-demo-bundle-verification.mjs +2 -3
  76. package/scripts/auditable-demo-platform.mjs +2 -2
  77. package/scripts/auditable-demo-renditions.mjs +1 -1
  78. package/scripts/auditable-demo.mjs +2 -2
  79. package/scripts/build-contract-core.mjs +8 -3
  80. package/scripts/build-standalone-binary.mjs +14 -3
  81. package/scripts/buildchain-cli-help.mjs +13 -0
  82. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  83. package/scripts/check-inventory.mjs +10 -8
  84. package/scripts/check-maintainability.mjs +9 -2
  85. package/scripts/check-release-tail-contract.mjs +435 -0
  86. package/scripts/dev-delivery-warrant.mjs +31 -4
  87. package/scripts/dev-pr-auto-merge.mjs +30 -4
  88. package/scripts/dev-pr-delivery-warrant.mjs +50 -0
  89. package/scripts/engineering-housekeeper-workflow.mjs +394 -0
  90. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  91. package/scripts/generate-site-bundle.mjs +47 -4
  92. package/scripts/init-repo.mjs +26 -2
  93. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  94. package/scripts/materialize-self-release-candidate-version.mjs +137 -0
  95. package/scripts/publication-commit-evidence.mjs +69 -23
  96. package/scripts/release-candidate-resolver.mjs +16 -10
  97. package/scripts/release-tail.mjs +159 -0
  98. package/scripts/resume-from-candidate-run.mjs +123 -9
  99. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  100. package/scripts/site-capability-metadata.mjs +13 -0
  101. package/scripts/v4-architecture.mjs +600 -0
  102. package/scripts/web-surface-core.mjs +8 -2
  103. package/scripts/workflow-call-contract.mjs +184 -5
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ GitHubHousekeeperClient,
6
+ applyGitHubHousekeeperPlan,
7
+ collectGitHubHousekeeperInventory,
8
+ } from "../packages/core/engineering-housekeeper-github.js";
9
+
10
+ const VALID_MODES = new Set(["report", "apply"]);
11
+ const VALID_SCOPES = new Set(["branches", "pull-requests"]);
12
+ const DEFAULT_OUTPUT_DIRECTORY = ".buildchain/engineering-housekeeper";
13
+
14
+ function requiredString(value, field) {
15
+ const normalized = String(value || "").trim();
16
+ if (!normalized) throw new Error(`${field} is required`);
17
+ return normalized;
18
+ }
19
+
20
+ function boolOption(value, fallback = false) {
21
+ if (value === undefined || value === null || value === "") return fallback;
22
+ if (typeof value === "boolean") return value;
23
+ const normalized = String(value).trim().toLowerCase();
24
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
25
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
26
+ throw new Error(`boolean input must be true or false, got: ${value}`);
27
+ }
28
+
29
+ function positiveInteger(value, fallback, field) {
30
+ const selected =
31
+ value === undefined || value === "" ? fallback : Number(value);
32
+ if (!Number.isInteger(selected) || selected < 1) {
33
+ throw new Error(`${field} must be a positive integer`);
34
+ }
35
+ return selected;
36
+ }
37
+
38
+ function splitPatterns(value, fallback) {
39
+ const normalized = String(value || "").trim();
40
+ if (!normalized) return [...fallback];
41
+ return [
42
+ ...new Set(
43
+ normalized
44
+ .split(/[\n,]+/)
45
+ .map((entry) => entry.trim())
46
+ .filter(Boolean),
47
+ ),
48
+ ].sort();
49
+ }
50
+
51
+ function normalizeRepository(value) {
52
+ const repository = requiredString(value, "repository");
53
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repository)) {
54
+ throw new Error(`repository must be owner/repo, got: ${repository}`);
55
+ }
56
+ return repository;
57
+ }
58
+
59
+ function normalizeMode(value) {
60
+ const mode = String(value || "report")
61
+ .trim()
62
+ .toLowerCase();
63
+ if (!VALID_MODES.has(mode)) {
64
+ throw new Error(`mode must be report or apply, got: ${value || "<empty>"}`);
65
+ }
66
+ return mode;
67
+ }
68
+
69
+ export function normalizeHousekeeperWorkflowOptions(options = {}) {
70
+ const mode = normalizeMode(options.mode ?? process.env.HOUSEKEEPER_MODE);
71
+ const applyEnabled = boolOption(
72
+ options.applyEnabled ?? process.env.HOUSEKEEPER_APPLY_ENABLED,
73
+ false,
74
+ );
75
+ if (mode === "apply" && !applyEnabled) {
76
+ throw new Error("apply mode requires apply-enabled=true");
77
+ }
78
+ if (mode === "report" && applyEnabled) {
79
+ throw new Error("apply-enabled=true is only valid when mode=apply");
80
+ }
81
+ return {
82
+ mode,
83
+ applyEnabled,
84
+ repository: normalizeRepository(
85
+ options.repository ||
86
+ process.env.HOUSEKEEPER_REPOSITORY ||
87
+ process.env.GITHUB_REPOSITORY,
88
+ ),
89
+ targetBranch: requiredString(
90
+ options.targetBranch || process.env.HOUSEKEEPER_TARGET_BRANCH,
91
+ "target-branch",
92
+ ).replace(/^refs\/heads\//, ""),
93
+ staleDays: positiveInteger(
94
+ options.staleDays ?? process.env.HOUSEKEEPER_STALE_DAYS,
95
+ 30,
96
+ "stale-days",
97
+ ),
98
+ maxActions: positiveInteger(
99
+ options.maxActions ?? process.env.HOUSEKEEPER_MAX_ACTIONS,
100
+ 20,
101
+ "max-actions",
102
+ ),
103
+ protectedPatterns: splitPatterns(
104
+ options.protectedPatterns ?? process.env.HOUSEKEEPER_PROTECTED_PATTERNS,
105
+ ["dev/**", "alpha/**", "release/**", "publish-gate/**"],
106
+ ),
107
+ retainedPatterns: splitPatterns(
108
+ options.retainedPatterns ?? process.env.HOUSEKEEPER_RETAINED_PATTERNS,
109
+ ["train/**", "authority/**"],
110
+ ),
111
+ stalePullRequestLabel: String(
112
+ options.stalePullRequestLabel ??
113
+ process.env.HOUSEKEEPER_STALE_PR_LABEL ??
114
+ "",
115
+ ).trim(),
116
+ observedAt: String(
117
+ options.observedAt ||
118
+ process.env.HOUSEKEEPER_OBSERVED_AT ||
119
+ new Date().toISOString(),
120
+ ),
121
+ appliedAt: String(
122
+ options.appliedAt ||
123
+ process.env.HOUSEKEEPER_APPLIED_AT ||
124
+ new Date().toISOString(),
125
+ ),
126
+ outputDirectory: path.resolve(
127
+ String(
128
+ options.outputDirectory ||
129
+ process.env.HOUSEKEEPER_OUTPUT_DIRECTORY ||
130
+ DEFAULT_OUTPUT_DIRECTORY,
131
+ ),
132
+ ),
133
+ };
134
+ }
135
+
136
+ function workflowPolicy(options) {
137
+ return {
138
+ protectedPatterns: options.protectedPatterns,
139
+ retainedPatterns: options.retainedPatterns,
140
+ pullRequests: {
141
+ reportStale: true,
142
+ label: options.stalePullRequestLabel,
143
+ autoClose: false,
144
+ },
145
+ };
146
+ }
147
+
148
+ function actionIdentity(action) {
149
+ return action.name
150
+ ? `${action.kind}:${action.name}`
151
+ : `${action.kind}:#${action.number}`;
152
+ }
153
+
154
+ function decisionFor(entry) {
155
+ if (entry.kind === "branch") return entry.decision;
156
+ return entry.actions.length > 0 ? entry.actions.join(",") : "report-only";
157
+ }
158
+
159
+ export function renderHousekeeperWorkflowReport(
160
+ plan,
161
+ receipt,
162
+ { mode, scope = "all" } = {},
163
+ ) {
164
+ const lines = [
165
+ "## Engineering Housekeeper",
166
+ "",
167
+ `Mode: \`${mode || "report"}\``,
168
+ `Scope: \`${scope}\``,
169
+ `Repository: \`${plan.repository}\``,
170
+ `Target ref: \`${plan.target.name}@${plan.target.headOid}\``,
171
+ `Observed at: \`${plan.observedAt}\``,
172
+ `Plan root: \`${plan.planRoot}\``,
173
+ `Receipt root: \`${receipt.receiptRoot}\``,
174
+ "",
175
+ "### Decisions",
176
+ "",
177
+ "| Subject | Observed ref | Decision | Reason codes |",
178
+ "| --- | --- | --- | --- |",
179
+ ];
180
+ for (const entry of plan.inventory) {
181
+ const subject =
182
+ entry.kind === "branch" ? entry.name : `PR #${entry.number}`;
183
+ lines.push(
184
+ `| \`${subject}\` | \`${entry.headOid}\` | ${decisionFor(entry)} | \`${entry.reasonCodes.join(",")}\` |`,
185
+ );
186
+ }
187
+ if (plan.inventory.length === 0)
188
+ lines.push("| - | - | retain | `inventory.empty` |");
189
+ lines.push(
190
+ "",
191
+ "### Outcomes",
192
+ "",
193
+ "| Action | Outcome | Details |",
194
+ "| --- | --- | --- |",
195
+ );
196
+ for (const outcome of receipt.outcomes) {
197
+ const details =
198
+ outcome.reasonCodes?.join(",") || outcome.providerError?.operation || "-";
199
+ lines.push(
200
+ `| \`${outcome.action}\` | ${outcome.status} | \`${details}\` |`,
201
+ );
202
+ }
203
+ if (receipt.outcomes.length === 0)
204
+ lines.push("| - | no-op | `no-actions-in-scope` |");
205
+ return `${lines.join("\n")}\n`;
206
+ }
207
+
208
+ function writeJson(filePath, value) {
209
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
210
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
211
+ }
212
+
213
+ function writeText(filePath, value) {
214
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
215
+ fs.writeFileSync(filePath, value);
216
+ }
217
+
218
+ function writeOutputs(outputs, outputFile = process.env.GITHUB_OUTPUT) {
219
+ if (!outputFile) return;
220
+ const lines = Object.entries(outputs).map(
221
+ ([key, value]) => `${key}=${String(value).replace(/\n/g, "%0A")}`,
222
+ );
223
+ fs.appendFileSync(outputFile, `${lines.join("\n")}\n`);
224
+ }
225
+
226
+ function appendSummary(
227
+ markdown,
228
+ summaryFile = process.env.GITHUB_STEP_SUMMARY,
229
+ ) {
230
+ if (summaryFile) fs.appendFileSync(summaryFile, markdown);
231
+ else process.stdout.write(markdown);
232
+ }
233
+
234
+ export async function createHousekeeperWorkflowPlan(
235
+ optionsInput = {},
236
+ clientInput,
237
+ ) {
238
+ const options = normalizeHousekeeperWorkflowOptions(optionsInput);
239
+ const client =
240
+ clientInput ||
241
+ new GitHubHousekeeperClient({
242
+ token: requiredString(process.env.GITHUB_TOKEN, "GitHub token"),
243
+ });
244
+ const plan = await collectGitHubHousekeeperInventory({
245
+ client,
246
+ repository: options.repository,
247
+ targetBranch: options.targetBranch,
248
+ observedAt: options.observedAt,
249
+ staleDays: options.staleDays,
250
+ policy: workflowPolicy(options),
251
+ });
252
+ const receipt = await applyGitHubHousekeeperPlan({
253
+ client,
254
+ plan,
255
+ dryRun: true,
256
+ appliedAt: options.appliedAt,
257
+ staleDays: options.staleDays,
258
+ maxActions: options.maxActions,
259
+ });
260
+ return { options, plan, receipt };
261
+ }
262
+
263
+ export function selectHousekeeperActions(plan, scope, maxActions) {
264
+ if (!VALID_SCOPES.has(scope))
265
+ throw new Error(`scope must be branches or pull-requests, got: ${scope}`);
266
+ return plan.actions
267
+ .slice(0, positiveInteger(maxActions, 20, "max-actions"))
268
+ .filter((action) =>
269
+ scope === "branches"
270
+ ? action.kind === "delete-branch"
271
+ : action.kind.endsWith("-pull-request"),
272
+ );
273
+ }
274
+
275
+ export async function applyHousekeeperWorkflowScope({
276
+ options: optionsInput = {},
277
+ plan,
278
+ scope,
279
+ client: clientInput,
280
+ }) {
281
+ const options = normalizeHousekeeperWorkflowOptions(optionsInput);
282
+ if (options.mode !== "apply" || !options.applyEnabled) {
283
+ throw new Error("scope apply requires mode=apply and apply-enabled=true");
284
+ }
285
+ if (
286
+ plan.repository !== options.repository ||
287
+ plan.target.name !== options.targetBranch
288
+ ) {
289
+ throw new Error(
290
+ "plan repository or target branch does not match current workflow inputs",
291
+ );
292
+ }
293
+ const client =
294
+ clientInput ||
295
+ new GitHubHousekeeperClient({
296
+ token: requiredString(process.env.GITHUB_TOKEN, "GitHub token"),
297
+ });
298
+ const scopedPlan = {
299
+ ...plan,
300
+ actions: selectHousekeeperActions(plan, scope, options.maxActions),
301
+ };
302
+ const receipt = await applyGitHubHousekeeperPlan({
303
+ client,
304
+ plan: scopedPlan,
305
+ dryRun: false,
306
+ appliedAt: options.appliedAt,
307
+ staleDays: options.staleDays,
308
+ maxActions: Math.max(1, scopedPlan.actions.length),
309
+ });
310
+ return { options, plan, scopedPlan, receipt };
311
+ }
312
+
313
+ async function planCommand() {
314
+ const result = await createHousekeeperWorkflowPlan();
315
+ const planPath = path.join(result.options.outputDirectory, "plan.json");
316
+ const reportPath = path.join(result.options.outputDirectory, "report.md");
317
+ const receiptPath = path.join(
318
+ result.options.outputDirectory,
319
+ "report-receipt.json",
320
+ );
321
+ const report = renderHousekeeperWorkflowReport(result.plan, result.receipt, {
322
+ mode: result.options.mode,
323
+ });
324
+ writeJson(planPath, result.plan);
325
+ writeJson(receiptPath, result.receipt);
326
+ writeText(reportPath, report);
327
+ appendSummary(report);
328
+ writeOutputs({
329
+ "plan-path": planPath,
330
+ "report-path": reportPath,
331
+ "report-receipt-path": receiptPath,
332
+ "plan-root": result.plan.planRoot,
333
+ "report-receipt-root": result.receipt.receiptRoot,
334
+ "action-count": result.plan.actions.length,
335
+ "branch-action-count": result.plan.actions.filter(
336
+ (action) => action.kind === "delete-branch",
337
+ ).length,
338
+ "pull-request-action-count": result.plan.actions.filter((action) =>
339
+ action.kind.endsWith("-pull-request"),
340
+ ).length,
341
+ outcome:
342
+ result.plan.actions.length === 0
343
+ ? "no-actions"
344
+ : `${result.options.mode}-ready`,
345
+ });
346
+ }
347
+
348
+ async function applyCommand(scope) {
349
+ const planPath = path.resolve(
350
+ requiredString(process.env.HOUSEKEEPER_PLAN_PATH, "HOUSEKEEPER_PLAN_PATH"),
351
+ );
352
+ const plan = JSON.parse(fs.readFileSync(planPath, "utf8"));
353
+ const result = await applyHousekeeperWorkflowScope({ plan, scope });
354
+ const receiptPath = path.join(
355
+ result.options.outputDirectory,
356
+ `${scope}-receipt.json`,
357
+ );
358
+ const reportPath = path.join(
359
+ result.options.outputDirectory,
360
+ `${scope}-report.md`,
361
+ );
362
+ const report = renderHousekeeperWorkflowReport(result.plan, result.receipt, {
363
+ mode: result.options.mode,
364
+ scope,
365
+ });
366
+ writeJson(receiptPath, result.receipt);
367
+ writeText(reportPath, report);
368
+ appendSummary(report);
369
+ writeOutputs({
370
+ "receipt-path": receiptPath,
371
+ "report-path": reportPath,
372
+ "receipt-root": result.receipt.receiptRoot,
373
+ "outcome-count": result.receipt.outcomes.length,
374
+ "selected-action-count": result.scopedPlan.actions.length,
375
+ outcome: result.scopedPlan.actions.length === 0 ? "no-actions" : "applied",
376
+ });
377
+ }
378
+
379
+ async function main() {
380
+ const command = process.argv[2];
381
+ if (command === "plan") return planCommand();
382
+ if (command === "apply")
383
+ return applyCommand(requiredString(process.argv[3], "scope"));
384
+ throw new Error(
385
+ "usage: engineering-housekeeper-workflow.mjs plan | apply <branches|pull-requests>",
386
+ );
387
+ }
388
+
389
+ if (import.meta.url === `file://${process.argv[1]}`) {
390
+ main().catch((error) => {
391
+ console.error(error.stack || error.message);
392
+ process.exit(1);
393
+ });
394
+ }
@@ -20,6 +20,7 @@ const internalInputs = new Set([
20
20
  "promotion-publication-channel",
21
21
  "promotion-target-ref",
22
22
  "promotion-override-used",
23
+ "publication-authority-workflow-path",
23
24
  ]);
24
25
 
25
26
  function blockBetween(source, start, end) {
@@ -116,6 +117,9 @@ function forwardedInputs(inputNames, { includeInternal = true, unsupportedInputs
116
117
  if (name === "promotion-override-used") {
117
118
  return ` ${name}: \${{ needs.resolve-promotion.outputs.override-used == 'true' }}`;
118
119
  }
120
+ if (name === "publication-authority-workflow-path") {
121
+ return " publication-authority-workflow-path: .github/workflows/.release-candidate-promote.yml";
122
+ }
119
123
  return routed
120
124
  ? ` ${name}: \${{ needs.resolve-promotion.outputs.${routed} }}`
121
125
  : ` ${name}: \${{ inputs.${name} }}`;
@@ -255,15 +259,13 @@ jobs:
255
259
  exit 1
256
260
  fi
257
261
 
258
- remote_url="https://github.com/\${repository}.git"
259
- refs="$(git ls-remote "\${remote_url}" "refs/heads/\${ref}" "refs/tags/\${ref}" "refs/tags/\${ref}^{}")"
260
- head_sha="$(printf '%s\\n' "\${refs}" | awk -v name="refs/heads/\${ref}" '$2 == name { print tolower($1) }')"
261
- tag_sha="$(printf '%s\\n' "\${refs}" | awk -v peeled="refs/tags/\${ref}^{}" -v name="refs/tags/\${ref}" '$2 == peeled { print tolower($1); found=1 } $2 == name && !found { fallback=tolower($1) } END { if (!found && fallback != "") print fallback }')"
262
- if [[ -n "\${head_sha}" && -n "\${tag_sha}" && "\${head_sha}" != "\${tag_sha}" ]]; then
263
- echo "::error::Promotion router ref is ambiguous between branch and tag"
264
- exit 1
262
+ if [[ "\${ref}" =~ ^[0-9A-Fa-f]{40}$ ]]; then sha="\${ref,,}"; else
263
+ remote_url="https://github.com/\${repository}.git"; refs="$(git ls-remote "\${remote_url}" "refs/heads/\${ref}" "refs/tags/\${ref}" "refs/tags/\${ref}^{}")"
264
+ head_sha="$(printf '%s\\n' "\${refs}" | awk -v name="refs/heads/\${ref}" '$2 == name { print tolower($1) }')"
265
+ tag_sha="$(printf '%s\\n' "\${refs}" | awk -v peeled="refs/tags/\${ref}^{}" -v name="refs/tags/\${ref}" '$2 == peeled { print tolower($1); found=1 } $2 == name && !found { fallback=tolower($1) } END { if (!found && fallback != "") print fallback }')"
266
+ if [[ -n "\${head_sha}" && -n "\${tag_sha}" && "\${head_sha}" != "\${tag_sha}" ]]; then echo "::error::Promotion router ref is ambiguous between branch and tag"; exit 1; fi
267
+ sha="\${head_sha:-\${tag_sha}}"
265
268
  fi
266
- sha="\${head_sha:-\${tag_sha}}"
267
269
  if [[ ! "\${sha}" =~ ^[0-9a-f]{40}$ ]]; then
268
270
  echo "::error::Promotion router ref did not resolve to one exact commit SHA"
269
271
  exit 1
@@ -332,11 +332,16 @@ function capabilityGroup(id) {
332
332
  return id;
333
333
  }
334
334
 
335
- function publicSurfaceLifecycle({ owner, maturity, nonDuplicationRationale }) {
335
+ function publicSurfaceLifecycle({
336
+ owner,
337
+ maturity,
338
+ nonDuplicationRationale,
339
+ introducedVersion = "pre-3.0.2-alpha.4",
340
+ }) {
336
341
  return {
337
342
  owner,
338
343
  maturity,
339
- introducedVersion: "pre-3.0.2-alpha.4",
344
+ introducedVersion,
340
345
  compatibilityPromise: "preserved-through-the-v3-major-line",
341
346
  deprecationReplacement: "",
342
347
  sunsetCondition: "explicit-breaking-change-review-in-a-future-major-line",
@@ -360,12 +365,16 @@ const manualMetaById = new Map(Object.entries({
360
365
  "auditable-demo": { capabilityGroup: "reusable-build", audience: ["consumer", "agent"], maturity: "preview", order: 207 },
361
366
  "binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
362
367
  "publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
368
+ "release-tail-contract": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "draft", order: 122 },
369
+ "release-tail-provider-plane": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "preview", order: 123 },
370
+ "publication-rehearsal": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "preview", order: 124 },
363
371
  "release-activation-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 125 },
364
372
  "release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
365
373
  "stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
366
374
  "dev-qualification-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 136 },
367
375
  "dev-alpha-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 137 },
368
376
  "observed-evidence-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 140 },
377
+ "engineering-housekeeper": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer", "agent"], maturity: "preview", order: 145 },
369
378
  "reusable-build-surface": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator"], maturity: "stable", order: 200 },
370
379
  "lifecycle-protocol": { capabilityGroup: "reusable-build", audience: ["consumer", "developer"], maturity: "stable", order: 210 },
371
380
  "runtime-train-validation": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer"], maturity: "stable", order: 220 },
@@ -514,16 +523,18 @@ function buildCapabilityRegistry({ docs, pages, cliRegistry, manualRegistry, nod
514
523
 
515
524
  function workflowCapabilityGroup(entry) {
516
525
  if (entry.id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
526
+ if (entry.id === "release-tail") return capabilityGroup("release-passport-trust");
517
527
  if (["web-surface", "release-propagation"].includes(entry.id)) return capabilityGroup("site-and-propagation");
518
528
  if (["build", "release-candidate-promote", "publication-artifact", "paper-release"].includes(entry.id)) return capabilityGroup("reusable-build");
519
529
  if (["buildchain-ref-promotion", "release-line-bootstrap"].includes(entry.id)) return capabilityGroup("release-passport-trust");
520
- if (entry.id.includes("patrol") || entry.id.includes("dev-pr-auto-merge") || entry.id.includes("dev-delivery-warrant") || entry.id.includes("buildchain-dev-delivery")) return capabilityGroup("governance-versioning");
530
+ if (entry.id.includes("patrol") || entry.id.includes("housekeeper") || entry.id.includes("dev-pr-auto-merge") || entry.id.includes("dev-delivery-warrant") || entry.id.includes("buildchain-dev-delivery")) return capabilityGroup("governance-versioning");
521
531
  if (entry.status === "repository-internal" || entry.status === "compatibility-fixture") return capabilityGroup("api-cli-reference");
522
532
  return capabilityGroup("api-cli-reference");
523
533
  }
524
534
 
525
535
  function actionCapabilityGroup(id) {
526
536
  if (id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
537
+ if (id === "release-tail") return capabilityGroup("release-passport-trust");
527
538
  if (id === "promote-buildchain-ref") return capabilityGroup("release-passport-trust");
528
539
  if (id === "run-lifecycle" || id === "validate-config") return capabilityGroup("reusable-build");
529
540
  if (id === "report-buildchain-issue") return capabilityGroup("observability-diagnostics");
@@ -727,6 +738,7 @@ function buildSiteBundle() {
727
738
  "docs/dev-qualification-patrol.md",
728
739
  "docs/dev-alpha-candidate-patrol.md",
729
740
  "docs/observed-evidence-patrol.md",
741
+ "docs/engineering-housekeeper.md",
730
742
  "docs/release-governance.md",
731
743
  "docs/release-passport.md",
732
744
  "docs/controller-evidence.md",
@@ -776,6 +788,7 @@ function buildSiteBundle() {
776
788
  ["release-candidate-promote", "release-governance"],
777
789
  ["paper-release", "reusable-build"],
778
790
  ["release-propagation", "release-propagation"],
791
+ ["release-tail", "release-tail-provider-plane"],
779
792
  ["dev-pr-auto-merge", "dev-governance"],
780
793
  ["buildchain-dev-delivery", "dev-governance"],
781
794
  ["github-governance-audit", "dev-governance"],
@@ -785,6 +798,10 @@ function buildSiteBundle() {
785
798
  ["buildchain-patrol-daily", "repository-patrol"],
786
799
  ["buildchain-patrol-weekly", "repository-patrol"],
787
800
  ["buildchain-patrol-monthly", "repository-patrol"],
801
+ ["engineering-housekeeper", "repository-patrol"],
802
+ ["engineering-housekeeper-daily", "repository-patrol"],
803
+ ["engineering-housekeeper-weekly", "repository-patrol"],
804
+ ["engineering-housekeeper-monthly", "repository-patrol"],
788
805
  ["stable-candidate-patrol", "repository-patrol"],
789
806
  ["dev-qualification-patrol", "repository-patrol"],
790
807
  ["dev-alpha-candidate-patrol", "repository-patrol"],
@@ -802,8 +819,13 @@ function buildSiteBundle() {
802
819
  ["candidate-lab", "repository-internal"],
803
820
  ["build-surface-fixture", "repository-internal"],
804
821
  ["buildchain-stable-candidate-qualification", "repository-internal"],
822
+ ["engineering-housekeeper", "preview"],
823
+ ["engineering-housekeeper-daily", "preview"],
824
+ ["engineering-housekeeper-weekly", "preview"],
825
+ ["engineering-housekeeper-monthly", "preview"],
805
826
  ["self-hosted-runner-smoke", "compatibility-fixture"],
806
827
  ]);
828
+ const engineeringHousekeeper = entry.id.startsWith("engineering-housekeeper");
807
829
  return {
808
830
  ...entry,
809
831
  surface: surfaceById.get(entry.id) || (entry.path.includes("/.") ? "reusable-workflow" : "repository-workflow"),
@@ -815,7 +837,10 @@ function buildSiteBundle() {
815
837
  ...publicSurfaceLifecycle({
816
838
  owner: "buildchain-workflows",
817
839
  maturity: statusById.get(entry.id) || "active",
818
- nonDuplicationRationale: "Existing workflow identity retained for caller compatibility and repository orchestration.",
840
+ introducedVersion: engineeringHousekeeper ? packageJson.version : undefined,
841
+ nonDuplicationRationale: engineeringHousekeeper
842
+ ? "One reusable policy and evidence boundary owns Engineering Housekeeper execution; scheduled callers contain cadence values only."
843
+ : "Existing workflow identity retained for caller compatibility and repository orchestration.",
819
844
  }),
820
845
  };
821
846
  }),
@@ -901,6 +926,9 @@ function buildSiteBundle() {
901
926
  "buildchain.release.json",
902
927
  "release-passport-check-manifest.json",
903
928
  "schemas/release-passport-v1.schema.json",
929
+ "schemas/publication-rehearsal-capsule-v1.schema.json",
930
+ "schemas/release-tail-capabilities-v1.schema.json",
931
+ "schemas/release-tail-provider-bindings-v1.schema.json",
904
932
  "schemas/kfd-agent-hub-adoption.schema.json",
905
933
  "schemas/kfd-product-gate-input-v1.schema.json",
906
934
  "schemas/kfd-support-projection-v1.schema.json",
@@ -936,6 +964,9 @@ function buildSiteBundle() {
936
964
  "artifact-schemas.json",
937
965
  "release-passport-check-manifest.json",
938
966
  "schemas/release-passport-v1.schema.json",
967
+ "schemas/publication-rehearsal-capsule-v1.schema.json",
968
+ "schemas/release-tail-capabilities-v1.schema.json",
969
+ "schemas/release-tail-provider-bindings-v1.schema.json",
939
970
  "schemas/kfd-agent-hub-adoption.schema.json",
940
971
  "schemas/kfd-product-gate-input-v1.schema.json",
941
972
  "schemas/kfd-support-projection-v1.schema.json",
@@ -1067,6 +1098,9 @@ function buildSiteBundle() {
1067
1098
  "artifact-schemas.json",
1068
1099
  "release-passport-check-manifest.json",
1069
1100
  "schemas/release-passport-v1.schema.json",
1101
+ "schemas/publication-rehearsal-capsule-v1.schema.json",
1102
+ "schemas/release-tail-capabilities-v1.schema.json",
1103
+ "schemas/release-tail-provider-bindings-v1.schema.json",
1070
1104
  "schemas/kfd-agent-hub-adoption.schema.json",
1071
1105
  "schemas/kfd-product-gate-input-v1.schema.json",
1072
1106
  "schemas/kfd-support-projection-v1.schema.json",
@@ -1242,6 +1276,15 @@ function buildSiteBundle() {
1242
1276
  "artifact-schemas.json": artifactSchemas,
1243
1277
  "release-passport-check-manifest.json": createReleasePassportCheckManifest(),
1244
1278
  "schemas/release-passport-v1.schema.json": RELEASE_PASSPORT_SCHEMA,
1279
+ "schemas/publication-rehearsal-capsule-v1.schema.json": readJson(
1280
+ "contracts/publication-rehearsal-capsule-v1.schema.json",
1281
+ ),
1282
+ "schemas/release-tail-capabilities-v1.schema.json": readJson(
1283
+ "contracts/release-tail-capabilities-v1.schema.json",
1284
+ ),
1285
+ "schemas/release-tail-provider-bindings-v1.schema.json": readJson(
1286
+ "contracts/release-tail-provider-bindings-v1.schema.json",
1287
+ ),
1245
1288
  "schemas/kfd-agent-hub-adoption.schema.json": KFD_AGENT_HUB_ADOPTION_SCHEMA,
1246
1289
  "schemas/kfd-product-gate-input-v1.schema.json": KFD_PRODUCT_GATE_INPUT_SCHEMA,
1247
1290
  "schemas/kfd-support-projection-v1.schema.json": KFD_SUPPORT_PROJECTION_SCHEMA,
@@ -4,6 +4,12 @@ import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { BUILDCHAIN_CONFIG_PATH } from "../packages/core/buildchain-layout.js";
6
6
  import { detectPackageManager, assertPackageManager } from "../packages/core/package-manager.js";
7
+ import {
8
+ PUBLICATION_REHEARSAL_WORKFLOW_PATH,
9
+ appendPublicationRehearsalToml,
10
+ mergePublicationRehearsalAgentInstructions,
11
+ publicationRehearsalWorkflow,
12
+ } from "../packages/core/publication-rehearsal-projection.js";
7
13
 
8
14
  const BUILDCHAIN_WORKFLOW_REF = "kungfu-systems/buildchain/.github/workflows/.build.yml@v3";
9
15
  const DEFAULT_PUBLICATION_LATEX_IMAGE = "ghcr.io/kungfu-systems/build-images/latex-pdf-builder";
@@ -401,6 +407,13 @@ function writeIfAllowed(filePath, content, { force }) {
401
407
  return filePath;
402
408
  }
403
409
 
410
+ function writeManagedAgentEntry(filePath, current) {
411
+ const content = mergePublicationRehearsalAgentInstructions(current);
412
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
413
+ fs.writeFileSync(filePath, content);
414
+ return filePath;
415
+ }
416
+
404
417
  export function initBuildchainRepo({
405
418
  cwd = process.cwd(),
406
419
  type = "package",
@@ -411,7 +424,7 @@ export function initBuildchainRepo({
411
424
  } = {}) {
412
425
  const resolvedCwd = path.resolve(cwd);
413
426
  const manager = detectOrDefaultPackageManager(resolvedCwd, packageManager);
414
- const toml = (() => {
427
+ const toml = appendPublicationRehearsalToml((() => {
415
428
  if (type === "package") {
416
429
  return packageToml(resolvedCwd, manager);
417
430
  }
@@ -431,7 +444,12 @@ export function initBuildchainRepo({
431
444
  return anchoredPackageToml(resolvedCwd, manager);
432
445
  }
433
446
  throw new Error("init --type must be one of package, native, web-surface, infra-contract, publication-artifact, or anchored-package");
434
- })();
447
+ })());
448
+
449
+ const agentsPath = path.join(resolvedCwd, "AGENTS.md");
450
+ const currentAgents = fs.existsSync(agentsPath)
451
+ ? fs.readFileSync(agentsPath, "utf8")
452
+ : "";
435
453
 
436
454
  const written = [
437
455
  writeIfAllowed(path.join(resolvedCwd, BUILDCHAIN_CONFIG_PATH), toml, { force }),
@@ -446,6 +464,12 @@ export function initBuildchainRepo({
446
464
  }),
447
465
  { force },
448
466
  ),
467
+ writeIfAllowed(
468
+ path.join(resolvedCwd, PUBLICATION_REHEARSAL_WORKFLOW_PATH),
469
+ publicationRehearsalWorkflow("v3"),
470
+ { force },
471
+ ),
472
+ writeManagedAgentEntry(agentsPath, currentAgents),
449
473
  ];
450
474
 
451
475
  if (type === "anchored-package" && !fs.existsSync(path.join(resolvedCwd, "release.json"))) {
@@ -82,6 +82,10 @@ export function inspectArtifactSigningRequests({
82
82
  if (seen.has(key))
83
83
  throw new Error(`duplicate artifact signing request: ${key}`);
84
84
  seen.add(key);
85
+ const {
86
+ entitlementsProfile = "none",
87
+ entitlementsPaths = [],
88
+ } = request.signature;
85
89
  const item = {
86
90
  id: request.artifact.id,
87
91
  slug: safeId(request.artifact.id),
@@ -104,6 +108,8 @@ export function inspectArtifactSigningRequests({
104
108
  sourceSha: request.source.sha,
105
109
  sourceTreeSha: request.source.treeSha,
106
110
  transportFormat: request.artifact.transport?.format || "",
111
+ entitlementsProfile,
112
+ entitlementsPaths: entitlementsPaths.join(","),
107
113
  };
108
114
  if (request.signature.profile === "detached-signature-v1")
109
115
  matrices.detached.push(item);