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

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 (64) hide show
  1. package/README.md +4 -2
  2. package/actions/release-tail/README.md +17 -0
  3. package/bin/buildchain.mjs +11 -0
  4. package/bin/internal/command-registry.mjs +2 -0
  5. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  6. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  7. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  8. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  9. package/dist/site/agent-index.json +3 -0
  10. package/dist/site/artifact-schemas.json +6 -0
  11. package/dist/site/buildchain-contract.json +35 -20
  12. package/dist/site/buildchain-site.json +307 -17
  13. package/dist/site/capability-registry.json +12 -9
  14. package/dist/site/cli-registry.json +102 -0
  15. package/dist/site/controller-registry.json +14 -2
  16. package/dist/site/kfd-claims.json +247 -16
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +48 -3
  19. package/dist/site/node-api-registry.json +3108 -1409
  20. package/dist/site/page-registry.json +275 -9
  21. package/dist/site/public-surface-audit.json +304 -15
  22. package/dist/site/publication-authority-registry.json +26 -1
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/release-provenance.json +5 -0
  25. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  26. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  27. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  28. package/dist/site/site-manifest.json +31 -7
  29. package/dist/site/workflow-registry.json +86 -9
  30. package/docs/MAP.md +5 -2
  31. package/docs/cli-reference.md +136 -0
  32. package/docs/node-api-reference.md +233 -84
  33. package/docs/publication-rehearsal.md +94 -0
  34. package/docs/release-tail-contract.md +160 -0
  35. package/docs/release-tail-provider-plane.md +120 -0
  36. package/package.json +8 -3
  37. package/packages/core/buildchain-agent-manuals.js +3 -0
  38. package/packages/core/buildchain-kfd-claims.js +1 -1
  39. package/packages/core/buildchain-publication-authority.js +1 -0
  40. package/packages/core/index.js +39 -0
  41. package/packages/core/paper-agent-entry.js +11 -5
  42. package/packages/core/paper-repository.js +1 -0
  43. package/packages/core/paper-scaffold-content.js +21 -0
  44. package/packages/core/paper.js +28 -2
  45. package/packages/core/publication-rehearsal-projection.js +173 -0
  46. package/packages/core/publication-rehearsal-runtime.js +909 -0
  47. package/packages/core/release-tail-compatibility.js +60 -0
  48. package/packages/core/release-tail-provider-adapters.js +461 -0
  49. package/packages/core/release-tail-provider-plane.js +1228 -0
  50. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  51. package/scripts/assemble-self-publication-admission.mjs +1 -1
  52. package/scripts/buildchain-cli-help.mjs +13 -0
  53. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  54. package/scripts/check-inventory.mjs +8 -8
  55. package/scripts/check-maintainability.mjs +9 -2
  56. package/scripts/check-release-tail-contract.mjs +435 -0
  57. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  58. package/scripts/generate-site-bundle.mjs +24 -0
  59. package/scripts/init-repo.mjs +26 -2
  60. package/scripts/materialize-self-release-candidate-version.mjs +131 -0
  61. package/scripts/release-tail.mjs +159 -0
  62. package/scripts/site-capability-metadata.mjs +11 -0
  63. package/scripts/v4-architecture.mjs +600 -0
  64. package/scripts/workflow-call-contract.mjs +184 -5
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ const INVENTORY_PATH = "architecture/release-tail-contract-inventory.json";
8
+ const ROOT_PATTERN = /^sha256:[0-9a-f]{64}$/u;
9
+ const SHA_PATTERN = /^[0-9a-f]{40}$/u;
10
+
11
+ function loadJson(root, file) {
12
+ return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
13
+ }
14
+
15
+ function read(root, file) {
16
+ return fs.readFileSync(path.join(root, file), "utf8");
17
+ }
18
+
19
+ function commandInputs(text, indent, stopAt = "") {
20
+ const source = stopAt ? text.split(new RegExp(`^${stopAt}`, "mu"))[0] : text;
21
+ const pattern = new RegExp(`^ {${indent}}([a-z0-9-]+-command):\\s*$`, "gmu");
22
+ return [...source.matchAll(pattern)].map((match) => match[1]);
23
+ }
24
+
25
+ function workflowCoordinates(root, inventory) {
26
+ return (inventory.reverseScan.workflowInputs || []).flatMap((file) =>
27
+ commandInputs(read(root, file), 6, "jobs:").map(
28
+ (name) => `workflow:${file}#${name}`,
29
+ ),
30
+ );
31
+ }
32
+
33
+ function actionCoordinates(root, inventory) {
34
+ return (inventory.reverseScan.actionInputs || []).flatMap((file) =>
35
+ commandInputs(read(root, file).split(/^outputs:/mu)[0], 2).map(
36
+ (name) => `action:${file}#${name}`,
37
+ ),
38
+ );
39
+ }
40
+
41
+ function sortedUnique(values) {
42
+ return [...new Set(values)].sort();
43
+ }
44
+
45
+ function sameSet(left, right) {
46
+ return (
47
+ JSON.stringify(sortedUnique(left)) === JSON.stringify(sortedUnique(right))
48
+ );
49
+ }
50
+
51
+ function validateReverseScan(root, inventory, surfaces, issues) {
52
+ const coordinates = [
53
+ ...workflowCoordinates(root, inventory),
54
+ ...actionCoordinates(root, inventory),
55
+ ...(inventory.reverseScan.configAndCliAliases || []),
56
+ ];
57
+ const owned = new Map();
58
+ for (const surface of surfaces) {
59
+ for (const coordinate of surface.coordinates || []) {
60
+ if (owned.has(coordinate))
61
+ issues.push(
62
+ `${coordinate}: ambiguous surface ownership (${owned.get(coordinate)}, ${surface.id})`,
63
+ );
64
+ owned.set(coordinate, surface.id);
65
+ }
66
+ }
67
+ for (const coordinate of coordinates) {
68
+ if (!owned.has(coordinate))
69
+ issues.push(`unclassified release-tail command surface: ${coordinate}`);
70
+ }
71
+ for (const coordinate of owned.keys()) {
72
+ if (!coordinates.includes(coordinate))
73
+ issues.push(
74
+ `declared release-tail coordinate is not reverse-discovered: ${coordinate}`,
75
+ );
76
+ }
77
+
78
+ const workflowNames = coordinates
79
+ .filter((entry) => entry.startsWith("workflow:"))
80
+ .map((entry) => entry.split("#")[1]);
81
+ const actionNames = coordinates
82
+ .filter((entry) => entry.startsWith("action:"))
83
+ .map((entry) => entry.split("#")[1]);
84
+ if (!sameSet(workflowNames, inventory.reverseScan.workflowCommandNames || []))
85
+ issues.push("reverse scan workflow command-name inventory drifted");
86
+ if (!sameSet(actionNames, inventory.reverseScan.actionCommandNames || []))
87
+ issues.push("reverse scan Action command-name inventory drifted");
88
+
89
+ let executionSites = 0;
90
+ for (const surface of surfaces) {
91
+ for (const site of surface.executionSites || []) {
92
+ const file = path.join(root, site.path || "");
93
+ if (!site.path || !site.marker || !fs.existsSync(file)) {
94
+ issues.push(
95
+ `${surface.id}: execution site is incomplete: ${site.path || "<empty>"}`,
96
+ );
97
+ continue;
98
+ }
99
+ if (!fs.readFileSync(file, "utf8").includes(site.marker))
100
+ issues.push(
101
+ `${surface.id}: execution marker is missing from ${site.path}`,
102
+ );
103
+ executionSites += 1;
104
+ }
105
+ }
106
+ return { coordinates: coordinates.length, executionSites };
107
+ }
108
+
109
+ function validateTransaction(inventory, issues) {
110
+ const transaction = inventory.canonicalTransaction || {};
111
+ const phases = (transaction.phases || []).map((entry) => entry.id);
112
+ if (
113
+ transaction.id !== "buildchain.release-tail/v1" ||
114
+ transaction.singleWriter !== true
115
+ )
116
+ issues.push(
117
+ "canonical release transaction identity or single-writer invariant is invalid",
118
+ );
119
+ if (
120
+ !sameSet(phases, [
121
+ "prepare",
122
+ "publish",
123
+ "commit",
124
+ "activate",
125
+ "readback",
126
+ "settle",
127
+ ])
128
+ )
129
+ issues.push("canonical release transaction must own all six frozen phases");
130
+ if (
131
+ transaction.effectSchema !== "kungfu.buildchain.release-tail.effect/v1" ||
132
+ transaction.observationSchema !==
133
+ "kungfu.buildchain.release-tail.observation/v1" ||
134
+ transaction.receiptSchema !== "kungfu.buildchain.release-tail.receipt/v1"
135
+ )
136
+ issues.push("canonical effect, observation, and receipt schemas drifted");
137
+ if ((transaction.retryClasses || []).some((entry) => entry.localAttempts > 3))
138
+ issues.push("canonical transaction permits unbounded local retry");
139
+ for (const terminal of [
140
+ "complete",
141
+ "blocked",
142
+ "repair-required",
143
+ "terminal-failure",
144
+ ]) {
145
+ if (!(transaction.terminalClasses || []).includes(terminal))
146
+ issues.push(`canonical transaction omits terminal class ${terminal}`);
147
+ }
148
+ for (const forbidden of [
149
+ "choose transaction transitions",
150
+ "execute repository-supplied shell",
151
+ ]) {
152
+ if (!(transaction.adapterBoundary?.mustNot || []).includes(forbidden))
153
+ issues.push(`adapter boundary omits ${forbidden}`);
154
+ }
155
+ }
156
+
157
+ function findForbiddenKey(value, forbidden, at = "$") {
158
+ if (Array.isArray(value)) {
159
+ for (const [index, entry] of value.entries()) {
160
+ const found = findForbiddenKey(entry, forbidden, `${at}[${index}]`);
161
+ if (found) return found;
162
+ }
163
+ return "";
164
+ }
165
+ if (!value || typeof value !== "object") return "";
166
+ for (const [key, entry] of Object.entries(value)) {
167
+ if (forbidden.has(key.toLowerCase())) return `${at}.${key}`;
168
+ const found = findForbiddenKey(entry, forbidden, `${at}.${key}`);
169
+ if (found) return found;
170
+ }
171
+ return "";
172
+ }
173
+
174
+ function validateCapabilityMessages(
175
+ inventory,
176
+ capability,
177
+ fixturePath,
178
+ issues,
179
+ ) {
180
+ if (
181
+ capability.effect?.schema !==
182
+ inventory.canonicalTransaction?.effectSchema ||
183
+ capability.observation?.schema !==
184
+ inventory.canonicalTransaction?.observationSchema ||
185
+ capability.receipt?.schema !== inventory.canonicalTransaction?.receiptSchema
186
+ )
187
+ issues.push(`${fixturePath}: ${capability.id} message schema drifted`);
188
+ }
189
+
190
+ function validateCapabilityOperation(capability, fixturePath, issues) {
191
+ if (
192
+ !ROOT_PATTERN.test(capability.operationIdentity?.transactionRoot || "") ||
193
+ capability.operationIdentity?.capabilityId !== capability.id ||
194
+ !ROOT_PATTERN.test(capability.operationIdentity?.subjectRoot || "") ||
195
+ !ROOT_PATTERN.test(capability.operationIdentity?.targetRoot || "") ||
196
+ !String(capability.operationIdentity?.attemptKey || "").trim()
197
+ )
198
+ issues.push(
199
+ `${fixturePath}: ${capability.id} operation identity is incomplete or inconsistent`,
200
+ );
201
+ if (
202
+ !Number.isInteger(capability.retry?.localAttempts) ||
203
+ capability.retry.localAttempts < 0 ||
204
+ capability.retry.localAttempts > 3
205
+ )
206
+ issues.push(`${fixturePath}: ${capability.id} local retry is invalid`);
207
+ }
208
+
209
+ function validateCapabilityDimensions(
210
+ inventory,
211
+ capability,
212
+ fixturePath,
213
+ issues,
214
+ ) {
215
+ for (const field of inventory.declarativeContract.requiredDimensions || []) {
216
+ if (capability[field] === undefined)
217
+ issues.push(`${fixturePath}: ${capability.id} omits ${field}`);
218
+ }
219
+ if (!(capability.readbackPredicates || []).length)
220
+ issues.push(`${fixturePath}: ${capability.id} has no readback predicate`);
221
+ if (!(capability.evidenceRequirements || []).length)
222
+ issues.push(`${fixturePath}: ${capability.id} has no evidence requirement`);
223
+ }
224
+
225
+ function validateCapabilityFixture(inventory, fixture, fixturePath, issues) {
226
+ const contract = inventory.declarativeContract || {};
227
+ if (
228
+ fixture.contract !== contract.contract ||
229
+ fixture.schemaVersion !== contract.schemaVersion ||
230
+ fixture.transactionPolicy !== inventory.canonicalTransaction?.id
231
+ )
232
+ issues.push(`${fixturePath}: declaration identity is invalid`);
233
+ if (!SHA_PATTERN.test(fixture.subject?.sourceSha || ""))
234
+ issues.push(`${fixturePath}: subject sourceSha is not exact`);
235
+ const forbidden = new Set(contract.forbiddenKeys || []);
236
+ const forbiddenPath = findForbiddenKey(fixture, forbidden);
237
+ if (forbiddenPath)
238
+ issues.push(
239
+ `${fixturePath}: executable key is forbidden at ${forbiddenPath}`,
240
+ );
241
+
242
+ const capabilities = fixture.capabilities || [];
243
+ const ids = capabilities.map((entry) => entry.id);
244
+ if (!sameSet(ids, contract.requiredCapabilityIds || []))
245
+ issues.push(`${fixturePath}: required capability set drifted`);
246
+ if (ids.length !== new Set(ids).size)
247
+ issues.push(`${fixturePath}: capability ids must be unique`);
248
+ for (const capability of capabilities) {
249
+ validateCapabilityDimensions(inventory, capability, fixturePath, issues);
250
+ validateCapabilityMessages(inventory, capability, fixturePath, issues);
251
+ validateCapabilityOperation(capability, fixturePath, issues);
252
+ }
253
+ }
254
+
255
+ function validateContract(root, inventory, issues) {
256
+ const contract = inventory.declarativeContract || {};
257
+ if (!fs.existsSync(path.join(root, contract.schemaPath || "")))
258
+ issues.push("declarative release-tail schema path is missing");
259
+ else {
260
+ const schema = loadJson(root, contract.schemaPath);
261
+ if (
262
+ schema.properties?.contract?.const !== contract.contract ||
263
+ schema.properties?.schemaVersion?.const !== contract.schemaVersion
264
+ )
265
+ issues.push("declarative release-tail JSON Schema identity drifted");
266
+ }
267
+ for (const fixturePath of contract.fixturePaths || []) {
268
+ if (!fs.existsSync(path.join(root, fixturePath))) {
269
+ issues.push(`declarative fixture is missing: ${fixturePath}`);
270
+ continue;
271
+ }
272
+ validateCapabilityFixture(
273
+ inventory,
274
+ loadJson(root, fixturePath),
275
+ fixturePath,
276
+ issues,
277
+ );
278
+ }
279
+ }
280
+
281
+ function validateSurfaceInventory(inventory, issues) {
282
+ const surfaces = inventory.legacyExecutableSurfaces || [];
283
+ const surfaceIds = new Set();
284
+ for (const surface of surfaces) {
285
+ if (!surface.id || surfaceIds.has(surface.id))
286
+ issues.push(
287
+ `release-tail surface id is missing or duplicated: ${surface.id || "<empty>"}`,
288
+ );
289
+ surfaceIds.add(surface.id);
290
+ for (const field of [
291
+ "owner",
292
+ "classification",
293
+ "default",
294
+ "replacement",
295
+ "disposition",
296
+ ]) {
297
+ if (!String(surface[field] || "").trim())
298
+ issues.push(`${surface.id}: ${field} is empty`);
299
+ }
300
+ if (!(surface.publicNames || []).length)
301
+ issues.push(`${surface.id}: publicNames is empty`);
302
+ if (!(surface.coordinates || []).length)
303
+ issues.push(`${surface.id}: coordinates is empty`);
304
+ }
305
+ return { surfaces, surfaceIds };
306
+ }
307
+
308
+ function validateManagedCallers(inventory, surfaceIds, issues) {
309
+ for (const caller of inventory.managedConsumerCallers || []) {
310
+ if (
311
+ !caller.id ||
312
+ !caller.repository ||
313
+ !caller.workflow ||
314
+ !caller.runtimeRef
315
+ )
316
+ issues.push(
317
+ `managed consumer caller is incomplete: ${caller.id || "<empty>"}`,
318
+ );
319
+ if (
320
+ !SHA_PATTERN.test(caller.sourceCommit || "") ||
321
+ !SHA_PATTERN.test(caller.sourceTree || "")
322
+ )
323
+ issues.push(`${caller.id}: managed consumer cut is not exact`);
324
+ for (const surfaceId of caller.executableSurfaceIds || []) {
325
+ if (!surfaceIds.has(surfaceId))
326
+ issues.push(`${caller.id}: unknown executable surface ${surfaceId}`);
327
+ }
328
+ }
329
+ }
330
+
331
+ function validateInventory(inventory, issues) {
332
+ if (
333
+ inventory.schemaVersion !== 1 ||
334
+ inventory.contract !== "kungfu-buildchain-release-tail-contract-inventory"
335
+ )
336
+ issues.push("release-tail inventory identity is invalid");
337
+ if (
338
+ !SHA_PATTERN.test(inventory.baseline?.commit || "") ||
339
+ !SHA_PATTERN.test(inventory.baseline?.tree || "")
340
+ )
341
+ issues.push("release-tail baseline must bind an exact commit and tree");
342
+ const { surfaces, surfaceIds } = validateSurfaceInventory(inventory, issues);
343
+ validateManagedCallers(inventory, surfaceIds, issues);
344
+ return surfaces;
345
+ }
346
+
347
+ function validateMigration(inventory, issues) {
348
+ const migration = inventory.migration || {};
349
+ const window = migration.compatibilityWindow || {};
350
+ if (
351
+ window.maximumDurationDays !== 90 ||
352
+ window.maximumMinorLines !== 2 ||
353
+ window.permanentEscapeHatch !== false
354
+ )
355
+ issues.push(
356
+ "compatibility window is not bounded by time, minor lines, and no-escape policy",
357
+ );
358
+ if ((migration.publishedReleasePreservation || []).length < 3)
359
+ issues.push("published-release preservation rules are incomplete");
360
+ const codes = new Set(
361
+ (migration.rejectionRules || []).map((entry) => entry.code),
362
+ );
363
+ for (const code of [
364
+ "release-tail-command-forbidden",
365
+ "release-tail-alias-collision",
366
+ "release-tail-operation-id-missing",
367
+ "release-tail-readback-missing",
368
+ "release-tail-retry-unbounded",
369
+ ]) {
370
+ if (!codes.has(code)) issues.push(`migration rejection rules omit ${code}`);
371
+ }
372
+ const excepted = new Set(
373
+ (migration.exceptionLedger || []).flatMap(
374
+ (entry) => entry.surfaceIds || [],
375
+ ),
376
+ );
377
+ for (const surface of inventory.legacyExecutableSurfaces || []) {
378
+ if (
379
+ surface.classification !== "adjacent-non-tail" &&
380
+ !excepted.has(surface.id)
381
+ )
382
+ issues.push(
383
+ `${surface.id}: compatibility exception has no owner and sunset test`,
384
+ );
385
+ }
386
+ for (const exception of migration.exceptionLedger || []) {
387
+ if (!exception.owner || !exception.expires || !exception.removalTest)
388
+ issues.push(`${exception.id}: exception ledger entry is incomplete`);
389
+ }
390
+ }
391
+
392
+ function checkReleaseTailContract({
393
+ root = process.cwd(),
394
+ inventory = loadJson(root, INVENTORY_PATH),
395
+ fixtures,
396
+ } = {}) {
397
+ const issues = [];
398
+ const surfaces = validateInventory(inventory, issues);
399
+ validateTransaction(inventory, issues);
400
+ if (fixtures) {
401
+ for (const [fixturePath, fixture] of Object.entries(fixtures))
402
+ validateCapabilityFixture(inventory, fixture, fixturePath, issues);
403
+ } else {
404
+ validateContract(root, inventory, issues);
405
+ }
406
+ const reverseScan = validateReverseScan(root, inventory, surfaces, issues);
407
+ validateMigration(inventory, issues);
408
+ if (issues.length)
409
+ throw new Error(
410
+ `release-tail contract check failed:\n- ${issues.join("\n- ")}`,
411
+ );
412
+ return {
413
+ surfaces: surfaces.length,
414
+ managedCallers: inventory.managedConsumerCallers.length,
415
+ capabilities: inventory.declarativeContract.requiredCapabilityIds.length,
416
+ ...reverseScan,
417
+ };
418
+ }
419
+
420
+ if (
421
+ process.argv[1] &&
422
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
423
+ ) {
424
+ try {
425
+ const report = checkReleaseTailContract();
426
+ console.log(
427
+ `release-tail contract check passed: ${report.surfaces} classified surfaces, ${report.coordinates} reverse-discovered coordinates, ${report.executionSites} execution sites, ${report.capabilities} declarative capabilities, ${report.managedCallers} managed callers`,
428
+ );
429
+ } catch (error) {
430
+ console.error(error instanceof Error ? error.message : String(error));
431
+ process.exitCode = 1;
432
+ }
433
+ }
434
+
435
+ export { checkReleaseTailContract };
@@ -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
@@ -360,6 +360,9 @@ const manualMetaById = new Map(Object.entries({
360
360
  "auditable-demo": { capabilityGroup: "reusable-build", audience: ["consumer", "agent"], maturity: "preview", order: 207 },
361
361
  "binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
362
362
  "publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
363
+ "release-tail-contract": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "draft", order: 122 },
364
+ "release-tail-provider-plane": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "preview", order: 123 },
365
+ "publication-rehearsal": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "preview", order: 124 },
363
366
  "release-activation-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 125 },
364
367
  "release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
365
368
  "stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
@@ -514,6 +517,7 @@ function buildCapabilityRegistry({ docs, pages, cliRegistry, manualRegistry, nod
514
517
 
515
518
  function workflowCapabilityGroup(entry) {
516
519
  if (entry.id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
520
+ if (entry.id === "release-tail") return capabilityGroup("release-passport-trust");
517
521
  if (["web-surface", "release-propagation"].includes(entry.id)) return capabilityGroup("site-and-propagation");
518
522
  if (["build", "release-candidate-promote", "publication-artifact", "paper-release"].includes(entry.id)) return capabilityGroup("reusable-build");
519
523
  if (["buildchain-ref-promotion", "release-line-bootstrap"].includes(entry.id)) return capabilityGroup("release-passport-trust");
@@ -524,6 +528,7 @@ function workflowCapabilityGroup(entry) {
524
528
 
525
529
  function actionCapabilityGroup(id) {
526
530
  if (id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
531
+ if (id === "release-tail") return capabilityGroup("release-passport-trust");
527
532
  if (id === "promote-buildchain-ref") return capabilityGroup("release-passport-trust");
528
533
  if (id === "run-lifecycle" || id === "validate-config") return capabilityGroup("reusable-build");
529
534
  if (id === "report-buildchain-issue") return capabilityGroup("observability-diagnostics");
@@ -776,6 +781,7 @@ function buildSiteBundle() {
776
781
  ["release-candidate-promote", "release-governance"],
777
782
  ["paper-release", "reusable-build"],
778
783
  ["release-propagation", "release-propagation"],
784
+ ["release-tail", "release-tail-provider-plane"],
779
785
  ["dev-pr-auto-merge", "dev-governance"],
780
786
  ["buildchain-dev-delivery", "dev-governance"],
781
787
  ["github-governance-audit", "dev-governance"],
@@ -901,6 +907,9 @@ function buildSiteBundle() {
901
907
  "buildchain.release.json",
902
908
  "release-passport-check-manifest.json",
903
909
  "schemas/release-passport-v1.schema.json",
910
+ "schemas/publication-rehearsal-capsule-v1.schema.json",
911
+ "schemas/release-tail-capabilities-v1.schema.json",
912
+ "schemas/release-tail-provider-bindings-v1.schema.json",
904
913
  "schemas/kfd-agent-hub-adoption.schema.json",
905
914
  "schemas/kfd-product-gate-input-v1.schema.json",
906
915
  "schemas/kfd-support-projection-v1.schema.json",
@@ -936,6 +945,9 @@ function buildSiteBundle() {
936
945
  "artifact-schemas.json",
937
946
  "release-passport-check-manifest.json",
938
947
  "schemas/release-passport-v1.schema.json",
948
+ "schemas/publication-rehearsal-capsule-v1.schema.json",
949
+ "schemas/release-tail-capabilities-v1.schema.json",
950
+ "schemas/release-tail-provider-bindings-v1.schema.json",
939
951
  "schemas/kfd-agent-hub-adoption.schema.json",
940
952
  "schemas/kfd-product-gate-input-v1.schema.json",
941
953
  "schemas/kfd-support-projection-v1.schema.json",
@@ -1067,6 +1079,9 @@ function buildSiteBundle() {
1067
1079
  "artifact-schemas.json",
1068
1080
  "release-passport-check-manifest.json",
1069
1081
  "schemas/release-passport-v1.schema.json",
1082
+ "schemas/publication-rehearsal-capsule-v1.schema.json",
1083
+ "schemas/release-tail-capabilities-v1.schema.json",
1084
+ "schemas/release-tail-provider-bindings-v1.schema.json",
1070
1085
  "schemas/kfd-agent-hub-adoption.schema.json",
1071
1086
  "schemas/kfd-product-gate-input-v1.schema.json",
1072
1087
  "schemas/kfd-support-projection-v1.schema.json",
@@ -1242,6 +1257,15 @@ function buildSiteBundle() {
1242
1257
  "artifact-schemas.json": artifactSchemas,
1243
1258
  "release-passport-check-manifest.json": createReleasePassportCheckManifest(),
1244
1259
  "schemas/release-passport-v1.schema.json": RELEASE_PASSPORT_SCHEMA,
1260
+ "schemas/publication-rehearsal-capsule-v1.schema.json": readJson(
1261
+ "contracts/publication-rehearsal-capsule-v1.schema.json",
1262
+ ),
1263
+ "schemas/release-tail-capabilities-v1.schema.json": readJson(
1264
+ "contracts/release-tail-capabilities-v1.schema.json",
1265
+ ),
1266
+ "schemas/release-tail-provider-bindings-v1.schema.json": readJson(
1267
+ "contracts/release-tail-provider-bindings-v1.schema.json",
1268
+ ),
1245
1269
  "schemas/kfd-agent-hub-adoption.schema.json": KFD_AGENT_HUB_ADOPTION_SCHEMA,
1246
1270
  "schemas/kfd-product-gate-input-v1.schema.json": KFD_PRODUCT_GATE_INPUT_SCHEMA,
1247
1271
  "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"))) {