@kungfu-tech/buildchain 3.0.2-alpha.5 → 3.0.2-alpha.6

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.
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "name": "Buildchain",
6
6
  "package": "@kungfu-tech/buildchain",
7
- "version": "3.0.2-alpha.5",
7
+ "version": "3.0.2-alpha.6",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "majorLine": "v3",
@@ -175,7 +175,7 @@
175
175
  "GitHub Release passport and evidence publication is delegated to promote-buildchain-ref after the semver release transaction completes"
176
176
  ],
177
177
  "breakingDigest": "sha256:acd401cfc46450115a3763fd4b679d85f185e8757ebd52510d6262e1533df4cf",
178
- "auditDigest": "sha256:8b1ff8b2487980c879eec4ffbfa2a2207e54fc76c05d010b7eb4d619685caff4"
178
+ "auditDigest": "sha256:df3ae042d9715119eb82d0295ee9a22f1c1fb2e603f96a52ae233db8c628b5a8"
179
179
  },
180
180
  {
181
181
  "contractVersion": 1,
@@ -498,7 +498,7 @@
498
498
  "release-state SHA is recorded as a durable audit entrance"
499
499
  ],
500
500
  "breakingDigest": "sha256:b627e1b2ece9b6049517a942c5139e342e6077cdd3d8962d61cef8481b1f70ad",
501
- "auditDigest": "sha256:09b0794b6d0e4bc3638f52e8d58e2eb6d6ab7efd2a2f90247c48c8fa68beda16"
501
+ "auditDigest": "sha256:aad90c2b39ce3fd7d2d4df5833f388c3090f5eeaecfd97d03276d375eed9459b"
502
502
  },
503
503
  {
504
504
  "contractVersion": 1,
@@ -547,7 +547,7 @@
547
547
  "prose-only public claims downgrade the release trust passport audit"
548
548
  ],
549
549
  "breakingDigest": "sha256:f0b636eb925ddbf45ff1a8872454ab7b4dd98857cac12bb75217830130e62a16",
550
- "auditDigest": "sha256:09b0794b6d0e4bc3638f52e8d58e2eb6d6ab7efd2a2f90247c48c8fa68beda16"
550
+ "auditDigest": "sha256:aad90c2b39ce3fd7d2d4df5833f388c3090f5eeaecfd97d03276d375eed9459b"
551
551
  },
552
552
  {
553
553
  "contractVersion": 1,
@@ -719,7 +719,7 @@
719
719
  "agents can discover supported Node APIs without importing internal file paths"
720
720
  ],
721
721
  "breakingDigest": "sha256:48f925608d3e2131d90936b07dc2a30341204cae3e6e785c0f77d61ad755c945",
722
- "auditDigest": "sha256:c68a2dbb92a25a2a2838f8ff72954cc5898e1383e2bd8fcfe699c2c2563912dd"
722
+ "auditDigest": "sha256:f9d8d42ea1aaa81e127ad0aeaacf846f944369f49d06f5ee749078aae320e0ee"
723
723
  },
724
724
  {
725
725
  "contractVersion": 1,
@@ -3362,5 +3362,5 @@
3362
3362
  }
3363
3363
  ],
3364
3364
  "compatibilityDigest": "sha256:008b1c5a90a787cf30106829ac7aca68ab501f6d9c375e415d9c5c6b89039f46",
3365
- "contractDigest": "sha256:8b3db665d04a98e1c40ff386f164525a63699fd13c345bea2bc3e09f70656a07"
3365
+ "contractDigest": "sha256:741a9262d51445d776077265081fbf7acac549dea1a624e9728861eabf66cc6e"
3366
3366
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-bundle",
4
- "generatedAt": "2026-07-27T06:15:19.437Z",
5
- "publishedAt": "2026-07-27T06:15:19.437Z",
4
+ "generatedAt": "2026-07-27T11:31:51.000Z",
5
+ "publishedAt": "2026-07-27T11:31:51.000Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "edd5b7573394e8724e26a2d47500705dc9b06d9d",
22
+ "sourceRevision": "e48aadba016f39ead070e73eff9a7c4c1a25d08b",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "3.0.2-alpha.5",
40
+ "version": "3.0.2-alpha.6",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "source": {
@@ -662,7 +662,7 @@
662
662
  ],
663
663
  "maturity": "stable",
664
664
  "sourcePath": "docs/dev-alpha-candidate-patrol.md",
665
- "digest": "sha256:ffd96da21ed6a99e01d60b6545ca4a486d6c22dbf053f717a3a2cce5c3970a28",
665
+ "digest": "sha256:2ec29098775cdf7f4eb80014261e437f026e05ea7a2bd6086273cdf0273bf2c2",
666
666
  "headings": [
667
667
  {
668
668
  "level": 1,
@@ -675,7 +675,7 @@
675
675
  "anchor": "reusable-workflow"
676
676
  }
677
677
  ],
678
- "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-26\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-26\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and PR controller for repositories\nthat promote a development branch into a protected Alpha branch. It does not\npublish Alpha. The controller reads the exact heads of both branches, walks the\nbounded development history from newest to oldest (stopping early at the Alpha\nhead), and selects the newest commit that satisfies all of these conditions:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact commit SHA succeeded;\n- the latest completed Alpha preflight for the same commit SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe selected commit can be behind the observed development head when newer\ncommits have not completed both workflows yet. The decision binds the observed\nhead, selected SHA, and count of skipped newer commits. This makes a slow native\nverification lane live under continuous development without silently treating\nan unqualified head as releasable.\n\nHistory discovery is bounded to the newest 1000 development commits. The\ncontroller then compares the selected SHA to the exact Alpha head before it can\nbe eligible, so a bounded scan cannot turn a commit outside the promotion\nancestry into a candidate.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. Once the repository has proven that its\ntwo workflow names and branch topology produce exact same-SHA evidence, it may\nset `create-pull-request: true` and `dry-run: false`.\n\nCandidate mode creates one branch named from the target branch and the first 12\ncharacters of the full source SHA. An existing branch must point to the same\nfull SHA or the run fails. The controller then creates or reuses one open pull\nrequest from that immutable branch to the protected Alpha branch.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
678
+ "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-27\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-27\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and single-flight PR controller for\nrepositories that promote a development branch into a protected Alpha branch.\nIt does not publish Alpha. The read-only observer reads the exact heads of both\nbranches, walks the\nbounded development history from newest to oldest (stopping early at the Alpha\nhead), and selects the newest commit that satisfies all of these conditions:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact commit SHA succeeded;\n- the latest completed Alpha preflight for the same commit SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe selected commit can be behind the observed development head when newer\ncommits have not completed both workflows yet. The decision binds the observed\nhead, selected SHA, and count of skipped newer commits. This makes a slow native\nverification lane live under continuous development without silently treating\nan unqualified head as releasable.\n\nHistory discovery is bounded to the newest 1000 development commits. The\ncontroller then compares the selected SHA to the exact Alpha head before it can\nbe eligible, so a bounded scan cannot turn a commit outside the promotion\nancestry into a candidate.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed as an auditable `blocked` or `stale` observation and cannot enter\nsettlement.\n\nThe companion state is\n`kungfu-buildchain-dev-alpha-candidate-state/v1`. Its current state is one of:\n\n- `observed`: no exact candidate is currently settleable;\n- `eligible-for-settlement`: a qualified candidate exists and no managed Alpha\n candidate PR is active;\n- `active`: exactly one managed candidate PR is open;\n- `retained-next`: an active PR remains authoritative and the newest different\n qualified SHA is retained as `nextCandidate`;\n- `stale`: the available exact-SHA evidence pair is outside policy age; or\n- `blocked`: qualification or reconciliation failed closed.\n\nWhen a newer qualified SHA replaces an earlier `nextCandidate`, the state also\nrecords that earlier SHA as `supersededCandidate`. Every state carries exact\nrepository, source/target refs and SHAs, workflow-run evidence through the\ncandidate decision, and canonical decision/state roots.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. The reusable workflow always runs an\n`observe` job with only Actions/content/pull-request read permissions. Once the\nrepository has proven that its two workflow names and branch topology produce\nexact same-SHA evidence, it may set `settlement-authorized: true` and\n`dry-run: false`. The older `create-pull-request` input remains a compatibility\nalias for settlement authorization.\n\nThe separately permissioned `settle` job re-runs the exact observation before\nany write. With no active managed candidate, it creates one branch named from\nthe target branch and the first 12 characters of the full source SHA. An\nexisting branch must point to the same full SHA or the run fails. With one\nactive managed candidate, it only updates the machine-readable state marker in\nthat PR body so repeated events and rapid dev progress cannot create another\ncandidate PR or another heavy candidate build. Foreign human-authored Alpha PRs\nare ignored. More than one open Buildchain-managed candidate fails closed.\n\nThe PR body is the bounded durable controller state: it preserves the active\ncandidate and newest retained `nextCandidate` without introducing an always-on\nservice. Once the active PR settles or is abandoned, the next execution\nrecomputes current exact-SHA qualification and creates only the newest still\nfresh candidate. It never trusts a `workflow_run` trigger SHA as evidence.\n\nConsumers should invoke this workflow after relevant qualification workflow\ncompletion and from an offset periodic fallback. GitHub may delay scheduled\nruns, so the event path supplies low latency while the fallback supplies\nrecovery. Workflow concurrency plus the server-side open-PR reconciliation\nmakes duplicate or delayed events idempotent.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
679
679
  },
680
680
  {
681
681
  "id": "manual:github-artifact-attestation",
@@ -21,7 +21,7 @@
21
21
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
22
22
  "path": "dist/site/public-surface-audit.json",
23
23
  "status": "passed",
24
- "sha256": "cfabd951fad28fb421abe155733557a50c4ec5fa4ce932b8edb427388596f717",
24
+ "sha256": "6d778d2a4d04a1b93ed21622719e08b3fa1e149af2941e09675c66582a3ebcdd",
25
25
  "summary": {
26
26
  "cliCommandCount": 97,
27
27
  "workflowCount": 55,
@@ -228,7 +228,7 @@
228
228
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
229
229
  "path": "dist/site/public-surface-audit.json",
230
230
  "status": "passed",
231
- "sha256": "cfabd951fad28fb421abe155733557a50c4ec5fa4ce932b8edb427388596f717",
231
+ "sha256": "6d778d2a4d04a1b93ed21622719e08b3fa1e149af2941e09675c66582a3ebcdd",
232
232
  "summary": {
233
233
  "cliCommandCount": 97,
234
234
  "workflowCount": 55,
@@ -3913,7 +3913,7 @@
3913
3913
  "visibility": "public",
3914
3914
  "participantFacing": true,
3915
3915
  "public": true,
3916
- "inputCount": 9,
3916
+ "inputCount": 10,
3917
3917
  "inputs": [
3918
3918
  "alpha-workflow-path",
3919
3919
  "buildchain-ref",
@@ -3922,6 +3922,7 @@
3922
3922
  "dev-workflow-path",
3923
3923
  "dry-run",
3924
3924
  "max-age-seconds",
3925
+ "settlement-authorized",
3925
3926
  "source-branch",
3926
3927
  "target-branch"
3927
3928
  ],
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "id": "kungfu-tech-buildchain",
6
6
  "name": "@kungfu-tech/buildchain",
7
- "version": "3.0.2-alpha.5",
7
+ "version": "3.0.2-alpha.6",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "source": {
@@ -450,7 +450,7 @@
450
450
  "specifier": "@kungfu-tech/buildchain/release-passport",
451
451
  "export": "./release-passport",
452
452
  "target": "./packages/core/release-passport.js",
453
- "digest": "sha256:09b0794b6d0e4bc3638f52e8d58e2eb6d6ab7efd2a2f90247c48c8fa68beda16",
453
+ "digest": "sha256:aad90c2b39ce3fd7d2d4df5833f388c3090f5eeaecfd97d03276d375eed9459b",
454
454
  "summary": "Release passport collection, verification, explanation, and evidence APIs.",
455
455
  "capabilityGroup": "release-passport-trust",
456
456
  "audience": [
@@ -596,7 +596,7 @@
596
596
  ],
597
597
  "maturity": "stable",
598
598
  "sourcePath": "docs/dev-alpha-candidate-patrol.md",
599
- "digest": "sha256:ffd96da21ed6a99e01d60b6545ca4a486d6c22dbf053f717a3a2cce5c3970a28",
599
+ "digest": "sha256:2ec29098775cdf7f4eb80014261e437f026e05ea7a2bd6086273cdf0273bf2c2",
600
600
  "headings": [
601
601
  {
602
602
  "level": 1,
@@ -609,7 +609,7 @@
609
609
  "anchor": "reusable-workflow"
610
610
  }
611
611
  ],
612
- "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-26\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-26\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and PR controller for repositories\nthat promote a development branch into a protected Alpha branch. It does not\npublish Alpha. The controller reads the exact heads of both branches, walks the\nbounded development history from newest to oldest (stopping early at the Alpha\nhead), and selects the newest commit that satisfies all of these conditions:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact commit SHA succeeded;\n- the latest completed Alpha preflight for the same commit SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe selected commit can be behind the observed development head when newer\ncommits have not completed both workflows yet. The decision binds the observed\nhead, selected SHA, and count of skipped newer commits. This makes a slow native\nverification lane live under continuous development without silently treating\nan unqualified head as releasable.\n\nHistory discovery is bounded to the newest 1000 development commits. The\ncontroller then compares the selected SHA to the exact Alpha head before it can\nbe eligible, so a bounded scan cannot turn a commit outside the promotion\nancestry into a candidate.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. Once the repository has proven that its\ntwo workflow names and branch topology produce exact same-SHA evidence, it may\nset `create-pull-request: true` and `dry-run: false`.\n\nCandidate mode creates one branch named from the target branch and the first 12\ncharacters of the full source SHA. An existing branch must point to the same\nfull SHA or the run fails. The controller then creates or reuses one open pull\nrequest from that immutable branch to the protected Alpha branch.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
612
+ "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-27\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-27\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and single-flight PR controller for\nrepositories that promote a development branch into a protected Alpha branch.\nIt does not publish Alpha. The read-only observer reads the exact heads of both\nbranches, walks the\nbounded development history from newest to oldest (stopping early at the Alpha\nhead), and selects the newest commit that satisfies all of these conditions:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact commit SHA succeeded;\n- the latest completed Alpha preflight for the same commit SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe selected commit can be behind the observed development head when newer\ncommits have not completed both workflows yet. The decision binds the observed\nhead, selected SHA, and count of skipped newer commits. This makes a slow native\nverification lane live under continuous development without silently treating\nan unqualified head as releasable.\n\nHistory discovery is bounded to the newest 1000 development commits. The\ncontroller then compares the selected SHA to the exact Alpha head before it can\nbe eligible, so a bounded scan cannot turn a commit outside the promotion\nancestry into a candidate.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed as an auditable `blocked` or `stale` observation and cannot enter\nsettlement.\n\nThe companion state is\n`kungfu-buildchain-dev-alpha-candidate-state/v1`. Its current state is one of:\n\n- `observed`: no exact candidate is currently settleable;\n- `eligible-for-settlement`: a qualified candidate exists and no managed Alpha\n candidate PR is active;\n- `active`: exactly one managed candidate PR is open;\n- `retained-next`: an active PR remains authoritative and the newest different\n qualified SHA is retained as `nextCandidate`;\n- `stale`: the available exact-SHA evidence pair is outside policy age; or\n- `blocked`: qualification or reconciliation failed closed.\n\nWhen a newer qualified SHA replaces an earlier `nextCandidate`, the state also\nrecords that earlier SHA as `supersededCandidate`. Every state carries exact\nrepository, source/target refs and SHAs, workflow-run evidence through the\ncandidate decision, and canonical decision/state roots.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. The reusable workflow always runs an\n`observe` job with only Actions/content/pull-request read permissions. Once the\nrepository has proven that its two workflow names and branch topology produce\nexact same-SHA evidence, it may set `settlement-authorized: true` and\n`dry-run: false`. The older `create-pull-request` input remains a compatibility\nalias for settlement authorization.\n\nThe separately permissioned `settle` job re-runs the exact observation before\nany write. With no active managed candidate, it creates one branch named from\nthe target branch and the first 12 characters of the full source SHA. An\nexisting branch must point to the same full SHA or the run fails. With one\nactive managed candidate, it only updates the machine-readable state marker in\nthat PR body so repeated events and rapid dev progress cannot create another\ncandidate PR or another heavy candidate build. Foreign human-authored Alpha PRs\nare ignored. More than one open Buildchain-managed candidate fails closed.\n\nThe PR body is the bounded durable controller state: it preserves the active\ncandidate and newest retained `nextCandidate` without introducing an always-on\nservice. Once the active PR settles or is abandoned, the next execution\nrecomputes current exact-SHA qualification and creates only the newest still\nfresh candidate. It never trusts a `workflow_run` trigger SHA as evidence.\n\nConsumers should invoke this workflow after relevant qualification workflow\ncompletion and from an offset periodic fallback. GitHub may delay scheduled\nruns, so the event path supplies low latency while the fallback supplies\nrecovery. Workflow concurrency plus the server-side open-PR reconciliation\nmakes duplicate or delayed events idempotent.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
613
613
  },
614
614
  {
615
615
  "id": "manual:github-artifact-attestation",
@@ -1638,22 +1638,27 @@
1638
1638
  "dev-workflow-path",
1639
1639
  "dry-run",
1640
1640
  "max-age-seconds",
1641
+ "settlement-authorized",
1641
1642
  "source-branch",
1642
1643
  "target-branch"
1643
1644
  ],
1644
- "inputCount": 9,
1645
+ "inputCount": 10,
1645
1646
  "secrets": [
1646
1647
  "promotion-token"
1647
1648
  ],
1648
1649
  "secretCount": 1,
1649
1650
  "outputs": [
1651
+ "active-candidate-pr",
1652
+ "controller-state",
1650
1653
  "eligible",
1654
+ "next-candidate-sha",
1651
1655
  "promotion-pr",
1652
1656
  "result-path",
1653
1657
  "selected-sha",
1658
+ "settlement-action",
1654
1659
  "source-lock-ref"
1655
1660
  ],
1656
- "outputCount": 5
1661
+ "outputCount": 9
1657
1662
  },
1658
1663
  {
1659
1664
  "id": "dev-merge-queue-governance",
@@ -4227,8 +4232,8 @@
4227
4232
  "workflowRegistryPath": "dist/site/workflow-registry.json",
4228
4233
  "pageRegistryPath": "dist/site/page-registry.json",
4229
4234
  "cliRegistryDigest": "0be723caa314a36a1a36b03522527eb5b13e9b741e6566ff7f6ee06958df02ca",
4230
- "workflowRegistryDigest": "f199eeb327027cf62d35f6c9591a6dadc16142bbecce14c562b435dc4ff2f47e",
4231
- "pageRegistryDigest": "962ba466e4a22e5113f8f795c76c49da66359a7fdb303ab7f9abb07b865cfa06"
4235
+ "workflowRegistryDigest": "18405fac6ddfebd647c2ba7d3927ec666e6d43dc1913043b0334c91e2bf143f1",
4236
+ "pageRegistryDigest": "b453877a28760e2446d5b58ee2bcbb3edd1059731a4046166ce1a4fc6649624b"
4232
4237
  },
4233
4238
  "comparison": {
4234
4239
  "missingCliRegistry": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-publication-release-registry",
4
- "generatedAt": "2026-07-27T06:15:19.437Z",
5
- "publishedAt": "2026-07-27T06:15:19.437Z",
4
+ "generatedAt": "2026-07-27T11:31:51.000Z",
5
+ "publishedAt": "2026-07-27T11:31:51.000Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "edd5b7573394e8724e26a2d47500705dc9b06d9d",
22
+ "sourceRevision": "e48aadba016f39ead070e73eff9a7c4c1a25d08b",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "3.0.2-alpha.5",
35
+ "version": "3.0.2-alpha.6",
36
36
  "versionSource": "package.json#version"
37
37
  },
38
38
  "sourceKind": "package-site-bundle",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-07-27T06:15:19.437Z",
5
- "publishedAt": "2026-07-27T06:15:19.437Z",
4
+ "generatedAt": "2026-07-27T11:31:51.000Z",
5
+ "publishedAt": "2026-07-27T11:31:51.000Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "edd5b7573394e8724e26a2d47500705dc9b06d9d",
22
+ "sourceRevision": "e48aadba016f39ead070e73eff9a7c4c1a25d08b",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "3.0.2-alpha.5",
40
+ "version": "3.0.2-alpha.6",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -1238,22 +1238,27 @@
1238
1238
  "dev-workflow-path",
1239
1239
  "dry-run",
1240
1240
  "max-age-seconds",
1241
+ "settlement-authorized",
1241
1242
  "source-branch",
1242
1243
  "target-branch"
1243
1244
  ],
1244
- "inputCount": 9,
1245
+ "inputCount": 10,
1245
1246
  "secrets": [
1246
1247
  "promotion-token"
1247
1248
  ],
1248
1249
  "secretCount": 1,
1249
1250
  "outputs": [
1251
+ "active-candidate-pr",
1252
+ "controller-state",
1250
1253
  "eligible",
1254
+ "next-candidate-sha",
1251
1255
  "promotion-pr",
1252
1256
  "result-path",
1253
1257
  "selected-sha",
1258
+ "settlement-action",
1254
1259
  "source-lock-ref"
1255
1260
  ],
1256
- "outputCount": 5,
1261
+ "outputCount": 9,
1257
1262
  "surface": "repository-patrol",
1258
1263
  "capabilityGroup": "governance-versioning",
1259
1264
  "status": "active"
@@ -8,20 +8,21 @@ confidence: high
8
8
  sensitivity: public
9
9
  evidence_grade: A
10
10
  review_state: self-reviewed
11
- last_reviewed: 2026-07-26
11
+ last_reviewed: 2026-07-27
12
12
  ai_provenance:
13
13
  model_family: GPT-5
14
14
  product: Codex
15
- generated_at: 2026-07-26
15
+ generated_at: 2026-07-27
16
16
  visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.
17
17
  invisible_context_boundary: No credentials, private logs, or private configuration were used.
18
18
  ---
19
19
 
20
20
  # Dev to Alpha Candidate Patrol
21
21
 
22
- Buildchain provides a reusable observation and PR controller for repositories
23
- that promote a development branch into a protected Alpha branch. It does not
24
- publish Alpha. The controller reads the exact heads of both branches, walks the
22
+ Buildchain provides a reusable observation and single-flight PR controller for
23
+ repositories that promote a development branch into a protected Alpha branch.
24
+ It does not publish Alpha. The read-only observer reads the exact heads of both
25
+ branches, walks the
25
26
  bounded development history from newest to oldest (stopping early at the Alpha
26
27
  head), and selects the newest commit that satisfies all of these conditions:
27
28
 
@@ -45,19 +46,56 @@ The decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records th
45
46
  source and target branches and SHAs, comparison distance, workflow paths, run
46
47
  identities and attempts, completion times, URLs, policy, and a canonical decision
47
48
  root. Missing, stale, failed, duplicate, or source-mismatched evidence fails
48
- closed.
49
+ closed as an auditable `blocked` or `stale` observation and cannot enter
50
+ settlement.
51
+
52
+ The companion state is
53
+ `kungfu-buildchain-dev-alpha-candidate-state/v1`. Its current state is one of:
54
+
55
+ - `observed`: no exact candidate is currently settleable;
56
+ - `eligible-for-settlement`: a qualified candidate exists and no managed Alpha
57
+ candidate PR is active;
58
+ - `active`: exactly one managed candidate PR is open;
59
+ - `retained-next`: an active PR remains authoritative and the newest different
60
+ qualified SHA is retained as `nextCandidate`;
61
+ - `stale`: the available exact-SHA evidence pair is outside policy age; or
62
+ - `blocked`: qualification or reconciliation failed closed.
63
+
64
+ When a newer qualified SHA replaces an earlier `nextCandidate`, the state also
65
+ records that earlier SHA as `supersededCandidate`. Every state carries exact
66
+ repository, source/target refs and SHAs, workflow-run evidence through the
67
+ candidate decision, and canonical decision/state roots.
49
68
 
50
69
  ## Reusable workflow
51
70
 
52
71
  Call `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository
53
- workflow. Start with `dry-run: true`. Once the repository has proven that its
54
- two workflow names and branch topology produce exact same-SHA evidence, it may
55
- set `create-pull-request: true` and `dry-run: false`.
72
+ workflow. Start with `dry-run: true`. The reusable workflow always runs an
73
+ `observe` job with only Actions/content/pull-request read permissions. Once the
74
+ repository has proven that its two workflow names and branch topology produce
75
+ exact same-SHA evidence, it may set `settlement-authorized: true` and
76
+ `dry-run: false`. The older `create-pull-request` input remains a compatibility
77
+ alias for settlement authorization.
78
+
79
+ The separately permissioned `settle` job re-runs the exact observation before
80
+ any write. With no active managed candidate, it creates one branch named from
81
+ the target branch and the first 12 characters of the full source SHA. An
82
+ existing branch must point to the same full SHA or the run fails. With one
83
+ active managed candidate, it only updates the machine-readable state marker in
84
+ that PR body so repeated events and rapid dev progress cannot create another
85
+ candidate PR or another heavy candidate build. Foreign human-authored Alpha PRs
86
+ are ignored. More than one open Buildchain-managed candidate fails closed.
87
+
88
+ The PR body is the bounded durable controller state: it preserves the active
89
+ candidate and newest retained `nextCandidate` without introducing an always-on
90
+ service. Once the active PR settles or is abandoned, the next execution
91
+ recomputes current exact-SHA qualification and creates only the newest still
92
+ fresh candidate. It never trusts a `workflow_run` trigger SHA as evidence.
56
93
 
57
- Candidate mode creates one branch named from the target branch and the first 12
58
- characters of the full source SHA. An existing branch must point to the same
59
- full SHA or the run fails. The controller then creates or reuses one open pull
60
- request from that immutable branch to the protected Alpha branch.
94
+ Consumers should invoke this workflow after relevant qualification workflow
95
+ completion and from an offset periodic fallback. GitHub may delay scheduled
96
+ runs, so the event path supplies low latency while the fallback supplies
97
+ recovery. Workflow concurrency plus the server-side open-PR reconciliation
98
+ makes duplicate or delayed events idempotent.
61
99
 
62
100
  The workflow never moves the Alpha ref directly, merges or auto-merges the pull
63
101
  request, publishes npm, creates a Git tag or GitHub Release, or changes branch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.2-alpha.5",
3
+ "version": "3.0.2-alpha.6",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -1170,6 +1170,7 @@ export function createReleasePassport({
1170
1170
  controllerReceipts = [],
1171
1171
  controllerReceiptReferences = [],
1172
1172
  githubArtifactAttestations = [],
1173
+ checkedAt = "",
1173
1174
  } = {}) {
1174
1175
  const normalizedTag = nonEmptyString(tag, "tag");
1175
1176
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
@@ -1189,7 +1190,7 @@ export function createReleasePassport({
1189
1190
  ? normalizeEvidenceDocument(distTagPromotionEvidence, "distTagPromotionEvidence")
1190
1191
  : undefined;
1191
1192
  const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
1192
- const generatedAt = nowIso();
1193
+ const generatedAt = optionalString(checkedAt) || nowIso();
1193
1194
  const kfd1Metadata = resolveKfd1Metadata();
1194
1195
  const normalizedKfd1 = kfd1?.passportSection ? kfd1 : undefined;
1195
1196
  const normalizedKfd3 = kfd3?.passportSection ? kfd3 : undefined;
@@ -1465,6 +1466,7 @@ export function collectGitHubReleasePassport({
1465
1466
  releaseJsonExtra = "",
1466
1467
  publishJson = "",
1467
1468
  workflow = {},
1469
+ checkedAt = "",
1468
1470
  } = {}) {
1469
1471
  const release = parseJsonInput(releaseJson, {}, { cwd, label: "releaseJson" });
1470
1472
  const releaseExtra = parseJsonInput(releaseJsonExtra, {}, { cwd, label: "releaseJsonExtra" });
@@ -1558,6 +1560,7 @@ export function collectGitHubReleasePassport({
1558
1560
  ];
1559
1561
  const resolvedTag = tag || release.tag_name || release.name || "";
1560
1562
  const resolvedOutputDir = path.resolve(cwd, outputDir);
1563
+ const resolvedCheckedAt = optionalString(checkedAt) || nowIso();
1561
1564
  const productMechanism = defaultProductMechanism({ repository, productName });
1562
1565
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: resolvedTag, sourceSha, workflow });
1563
1566
  const bundledPublishEvidencePath = publishEvidenceMeta.value ? "evidence.json" : "";
@@ -1585,7 +1588,7 @@ export function collectGitHubReleasePassport({
1585
1588
  matrixRoot: kfdSupportMatrixMeta.sha256 ? `sha256:${kfdSupportMatrixMeta.sha256}` : "",
1586
1589
  gateResults: kfdProductGateMetas.map((meta) => meta.value),
1587
1590
  expectedSourceSha: sourceSha,
1588
- checkedAt: nowIso(),
1591
+ checkedAt: resolvedCheckedAt,
1589
1592
  })
1590
1593
  : undefined;
1591
1594
  const passport = mergeAuthoritativePassportBase(createReleasePassport({
@@ -1644,6 +1647,7 @@ export function collectGitHubReleasePassport({
1644
1647
  publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
1645
1648
  transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
1646
1649
  workflow,
1650
+ checkedAt: resolvedCheckedAt,
1647
1651
  }), basePassportMeta.value, { requireKfd: requireBaseKfd });
1648
1652
  if (bundledPublishEvidencePath) {
1649
1653
  passport.evidence.publishEvidence = bundledPublishEvidencePath;
@@ -1657,7 +1661,7 @@ export function collectGitHubReleasePassport({
1657
1661
  productMechanism,
1658
1662
  kfdAgentHubEvidence: kfdAgentHubEvidenceMeta.value,
1659
1663
  kfdSupportEvidence: kfdSupport,
1660
- checkedAt: nowIso(),
1664
+ checkedAt: resolvedCheckedAt,
1661
1665
  });
1662
1666
  const files = {
1663
1667
  "product-mechanism.json": productMechanism,
@@ -2473,6 +2477,7 @@ export async function verifyReleasePassport({
2473
2477
  productMechanismLocation = "",
2474
2478
  kfdAgentHubEvidenceLocation = "",
2475
2479
  kfdSupportEvidenceLocation = "",
2480
+ checkedAt = nowIso(),
2476
2481
  } = {}) {
2477
2482
  const passport = await readJsonFromLocation(passportLocation);
2478
2483
  const basePath = /^https?:\/\//.test(passportLocation) ? passportLocation : path.resolve(passportLocation);
@@ -2513,6 +2518,7 @@ export async function verifyReleasePassport({
2513
2518
  productMechanism,
2514
2519
  kfdAgentHubEvidence,
2515
2520
  kfdSupportEvidence,
2521
+ checkedAt,
2516
2522
  });
2517
2523
  }
2518
2524
 
@@ -3,8 +3,16 @@
3
3
 
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
+ import crypto from "node:crypto";
6
7
  import { decideChannelCandidate } from "../packages/core/channel-candidate.js";
7
8
 
9
+ export const DEV_ALPHA_CANDIDATE_STATE_SCHEMA =
10
+ "kungfu-buildchain-dev-alpha-candidate-state/v1";
11
+ const STATE_MARKER_START = "<!-- buildchain-dev-alpha-candidate-state";
12
+ const STATE_MARKER_END = "-->";
13
+ const LEGACY_BODY_MARKER = "Buildchain exact-source channel candidate.";
14
+ const EXACT_SHA = /^[0-9a-f]{40}$/u;
15
+
8
16
  function text(value = "") {
9
17
  return String(value ?? "").trim();
10
18
  }
@@ -46,7 +54,127 @@ function integer(value, fallback) {
46
54
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
47
55
  }
48
56
 
57
+ function canonical(value) {
58
+ if (Array.isArray(value)) return value.map(canonical);
59
+ if (value && typeof value === "object") {
60
+ return Object.fromEntries(
61
+ Object.entries(value)
62
+ .sort(([left], [right]) => left.localeCompare(right))
63
+ .map(([key, item]) => [key, canonical(item)]),
64
+ );
65
+ }
66
+ return value;
67
+ }
68
+
69
+ function evidenceRoot(value) {
70
+ return `sha256:${crypto
71
+ .createHash("sha256")
72
+ .update(JSON.stringify(canonical(value)))
73
+ .digest("hex")}`;
74
+ }
75
+
76
+ function candidateStateMarker(state) {
77
+ return `${STATE_MARKER_START}\n${JSON.stringify(state)}\n${STATE_MARKER_END}`;
78
+ }
79
+
80
+ export function parseCandidateStateMarker(bodyInput) {
81
+ const body = String(bodyInput || "");
82
+ const start = body.indexOf(STATE_MARKER_START);
83
+ if (start < 0) return undefined;
84
+ const jsonStart = body.indexOf("\n", start);
85
+ const end = body.indexOf(STATE_MARKER_END, jsonStart + 1);
86
+ if (jsonStart < 0 || end < 0)
87
+ throw new Error("malformed Buildchain candidate state marker");
88
+ const state = JSON.parse(body.slice(jsonStart + 1, end).trim());
89
+ if (state.schema !== DEV_ALPHA_CANDIDATE_STATE_SCHEMA)
90
+ throw new Error(
91
+ `unsupported candidate state schema ${state.schema || "<empty>"}`,
92
+ );
93
+ return state;
94
+ }
95
+
96
+ function replaceCandidateStateMarker(bodyInput, state) {
97
+ const body = String(bodyInput || "").trimEnd();
98
+ const start = body.indexOf(STATE_MARKER_START);
99
+ if (start < 0) return `${body}\n\n${candidateStateMarker(state)}\n`;
100
+ const end = body.indexOf(STATE_MARKER_END, start);
101
+ if (end < 0) throw new Error("malformed Buildchain candidate state marker");
102
+ return `${body.slice(0, start).trimEnd()}\n\n${candidateStateMarker(state)}\n`;
103
+ }
104
+
105
+ function sourceShaFromLegacyBody(bodyInput) {
106
+ return String(bodyInput || "").match(/- Source SHA: `([0-9a-f]{40})`/u)?.[1];
107
+ }
108
+
109
+ function targetSlug(targetBranch) {
110
+ return targetBranch.replace(/[^A-Za-z0-9._-]+/g, "-");
111
+ }
112
+
113
+ export function managedCandidateFromPullRequest(pullRequest, targetBranch) {
114
+ const body = String(pullRequest.body || "");
115
+ const marker = parseCandidateStateMarker(body);
116
+ const expectedPrefix = `buildchain/candidate/${targetSlug(targetBranch)}/`;
117
+ const headRef = text(pullRequest.head?.ref);
118
+ const baseRef = text(pullRequest.base?.ref || targetBranch);
119
+ if (baseRef !== targetBranch) return undefined;
120
+ if (marker) {
121
+ if (marker.targetBranch !== targetBranch)
122
+ throw new Error(
123
+ `candidate PR #${pullRequest.number} state targets ${marker.targetBranch}, not ${targetBranch}`,
124
+ );
125
+ if (!headRef.startsWith(expectedPrefix))
126
+ throw new Error(
127
+ `candidate PR #${pullRequest.number} head ${headRef} is outside ${expectedPrefix}`,
128
+ );
129
+ const sourceSha = text(marker.activeCandidate?.sourceSha);
130
+ if (
131
+ !EXACT_SHA.test(sourceSha) ||
132
+ headRef !== `${expectedPrefix}${sourceSha.slice(0, 12)}`
133
+ ) {
134
+ throw new Error(
135
+ `candidate PR #${pullRequest.number} state does not bind its exact source-lock head`,
136
+ );
137
+ }
138
+ return {
139
+ number: Number(pullRequest.number),
140
+ url: text(pullRequest.html_url),
141
+ body,
142
+ sourceSha,
143
+ sourceLockRef: headRef,
144
+ decisionRoot: text(marker.activeCandidate?.decisionRoot),
145
+ nextCandidate: marker.nextCandidate || null,
146
+ state: marker,
147
+ };
148
+ }
149
+ if (!body.includes(LEGACY_BODY_MARKER) || !headRef.startsWith(expectedPrefix))
150
+ return undefined;
151
+ const sourceSha = sourceShaFromLegacyBody(body);
152
+ if (!sourceSha)
153
+ throw new Error(
154
+ `legacy candidate PR #${pullRequest.number} has no exact source SHA`,
155
+ );
156
+ if (headRef !== `${expectedPrefix}${sourceSha.slice(0, 12)}`)
157
+ throw new Error(
158
+ `legacy candidate PR #${pullRequest.number} does not bind its exact source-lock head`,
159
+ );
160
+ return {
161
+ number: Number(pullRequest.number),
162
+ url: text(pullRequest.html_url),
163
+ body,
164
+ sourceSha,
165
+ sourceLockRef: headRef,
166
+ decisionRoot: "",
167
+ nextCandidate: null,
168
+ state: null,
169
+ };
170
+ }
171
+
49
172
  export function normalizeDevAlphaPatrolOptions(options = {}) {
173
+ const createPullRequest = bool(
174
+ options.createPullRequest ??
175
+ process.env.BUILDCHAIN_CHANNEL_PATROL_CREATE_PR,
176
+ false,
177
+ );
50
178
  return {
51
179
  repository: repository(
52
180
  options.repository ??
@@ -82,10 +210,11 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
82
210
  process.env.BUILDCHAIN_CHANNEL_PATROL_MAX_AGE_SECONDS,
83
211
  7 * 24 * 60 * 60,
84
212
  ),
85
- createPullRequest: bool(
86
- options.createPullRequest ??
87
- process.env.BUILDCHAIN_CHANNEL_PATROL_CREATE_PR,
88
- false,
213
+ createPullRequest,
214
+ settlementAuthorized: bool(
215
+ options.settlementAuthorized ??
216
+ process.env.BUILDCHAIN_CHANNEL_PATROL_SETTLEMENT_AUTHORIZED,
217
+ createPullRequest,
89
218
  ),
90
219
  dryRun: bool(
91
220
  options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN,
@@ -183,11 +312,118 @@ export function selectLatestQualifiedSource({
183
312
  };
184
313
  }
185
314
  }
315
+ const staleSuccessfulPair = sourceHistory.some((sourceSha) => {
316
+ const rows = requiredWorkflowPaths.map((workflow) =>
317
+ latestByPath.get(workflow).get(sourceSha),
318
+ );
319
+ return (
320
+ rows.every(
321
+ (run) =>
322
+ run?.status === "completed" &&
323
+ run?.conclusion === "success" &&
324
+ run?.head_sha === sourceSha,
325
+ ) &&
326
+ rows.some(
327
+ (run) =>
328
+ !workflowEvidenceIsFreshAndSuccessful(run, { now, maxAgeSeconds }),
329
+ )
330
+ );
331
+ });
332
+ if (staleSuccessfulPair)
333
+ throw new Error(
334
+ "same-SHA workflow evidence is stale for every qualified source commit",
335
+ );
186
336
  throw new Error(
187
337
  "no source commit ahead of target has fresh completed successful same-SHA workflow evidence",
188
338
  );
189
339
  }
190
340
 
341
+ function blockedCandidateDecision({
342
+ options,
343
+ sourceSha,
344
+ targetSha,
345
+ comparison,
346
+ reason,
347
+ }) {
348
+ const body = {
349
+ schema: "kungfu-buildchain-channel-candidate-decision/v1",
350
+ eligible: false,
351
+ reason: "qualification-evidence-blocked",
352
+ repository: options.repository,
353
+ source: { branch: options.sourceBranch, sha: sourceSha },
354
+ target: { branch: options.targetBranch, sha: targetSha },
355
+ comparison: {
356
+ status: text(comparison.status || "unknown"),
357
+ aheadBy: Number(comparison.ahead_by || 0),
358
+ },
359
+ blockReason: text(reason),
360
+ decidedAt: options.now,
361
+ };
362
+ return { ...body, decisionRoot: evidenceRoot(body) };
363
+ }
364
+
365
+ function candidateFromDecision(decision) {
366
+ if (!decision.eligible) return null;
367
+ return {
368
+ sourceSha: decision.source.sha,
369
+ sourceLockRef: decision.sourceLockRef,
370
+ decisionRoot: decision.decisionRoot,
371
+ workflowEvidence: decision.workflowEvidence,
372
+ };
373
+ }
374
+
375
+ function candidateStateBody({
376
+ options,
377
+ targetSha,
378
+ decision,
379
+ activeCandidate,
380
+ nextCandidate,
381
+ supersededCandidate,
382
+ }) {
383
+ const body = {
384
+ schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
385
+ repository: options.repository,
386
+ sourceBranch: options.sourceBranch,
387
+ targetBranch: options.targetBranch,
388
+ targetSha,
389
+ activeCandidate,
390
+ nextCandidate,
391
+ observationDecisionRoot: decision.decisionRoot || null,
392
+ observedAt: options.now,
393
+ ...(supersededCandidate ? { supersededCandidate } : {}),
394
+ };
395
+ return { ...body, stateRoot: evidenceRoot(body) };
396
+ }
397
+
398
+ function pullRequestBody({
399
+ options,
400
+ observedSourceHeadSha,
401
+ sourceSha,
402
+ skippedNewerCommitCount,
403
+ targetSha,
404
+ decision,
405
+ state,
406
+ }) {
407
+ return [
408
+ LEGACY_BODY_MARKER,
409
+ "",
410
+ `- Source branch: \`${options.sourceBranch}\``,
411
+ `- Observed source HEAD: \`${observedSourceHeadSha}\``,
412
+ `- Source SHA: \`${sourceSha}\``,
413
+ `- Skipped newer unqualified commits: \`${skippedNewerCommitCount}\``,
414
+ `- Target branch/head: \`${options.targetBranch}\` / \`${targetSha}\``,
415
+ `- Decision root: \`${decision.decisionRoot}\``,
416
+ ...decision.workflowEvidence.map(
417
+ (row) =>
418
+ `- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
419
+ ),
420
+ "",
421
+ "The source-lock branch must continue to point at the exact source SHA. This patrol never merges the PR, publishes a package, creates a tag, or creates a release.",
422
+ "",
423
+ candidateStateMarker(state),
424
+ ].join("\n");
425
+ }
426
+
191
427
  export async function runDevAlphaCandidatePatrol(
192
428
  optionsInput = {},
193
429
  clientInput,
@@ -214,6 +450,7 @@ export async function runDevAlphaCandidatePatrol(
214
450
  let comparison = headComparison;
215
451
  let workflowEvidence = [];
216
452
  let skippedNewerCommitCount = 0;
453
+ let qualificationError;
217
454
  if (
218
455
  headComparison.status === "ahead" &&
219
456
  Number(headComparison.ahead_by) > 0
@@ -224,71 +461,189 @@ export async function runDevAlphaCandidatePatrol(
224
461
  client.listCompletedWorkflowRuns(workflow, options.sourceBranch),
225
462
  ),
226
463
  ]);
227
- const selected = selectLatestQualifiedSource({
228
- sourceHistory,
229
- workflowRunsByPath: new Map(
230
- requiredWorkflowPaths.map((workflow, index) => [
231
- workflow,
232
- workflowRunSets[index],
233
- ]),
234
- ),
235
- requiredWorkflowPaths,
236
- now: options.now,
237
- maxAgeSeconds: options.maxAgeSeconds,
238
- });
239
- sourceSha = selected.sourceSha;
240
- skippedNewerCommitCount = selected.skippedNewerCommitCount;
241
- workflowEvidence = selected.workflowEvidence;
242
- if (sourceSha !== observedSourceHeadSha)
243
- comparison = await client.compare(targetSha, sourceSha);
464
+ try {
465
+ const selected = selectLatestQualifiedSource({
466
+ sourceHistory,
467
+ workflowRunsByPath: new Map(
468
+ requiredWorkflowPaths.map((workflow, index) => [
469
+ workflow,
470
+ workflowRunSets[index],
471
+ ]),
472
+ ),
473
+ requiredWorkflowPaths,
474
+ now: options.now,
475
+ maxAgeSeconds: options.maxAgeSeconds,
476
+ });
477
+ sourceSha = selected.sourceSha;
478
+ skippedNewerCommitCount = selected.skippedNewerCommitCount;
479
+ workflowEvidence = selected.workflowEvidence;
480
+ if (sourceSha !== observedSourceHeadSha)
481
+ comparison = await client.compare(targetSha, sourceSha);
482
+ } catch (error) {
483
+ qualificationError = error;
484
+ }
244
485
  }
245
- const decision = decideChannelCandidate({
246
- repository: options.repository,
247
- sourceBranch: options.sourceBranch,
248
- targetBranch: options.targetBranch,
249
- sourceSha,
486
+ const decision = qualificationError
487
+ ? blockedCandidateDecision({
488
+ options,
489
+ sourceSha: observedSourceHeadSha,
490
+ targetSha,
491
+ comparison: headComparison,
492
+ reason: qualificationError.message,
493
+ })
494
+ : decideChannelCandidate({
495
+ repository: options.repository,
496
+ sourceBranch: options.sourceBranch,
497
+ targetBranch: options.targetBranch,
498
+ sourceSha,
499
+ targetSha,
500
+ comparison: { status: comparison.status, aheadBy: comparison.ahead_by },
501
+ selection: {
502
+ mode: "latest-qualified-source-ancestor",
503
+ observedSourceHeadSha,
504
+ skippedNewerCommitCount,
505
+ },
506
+ workflowEvidence,
507
+ requiredWorkflowPaths,
508
+ maxAgeSeconds: options.maxAgeSeconds,
509
+ now: options.now,
510
+ });
511
+ const openPullRequests = await client.listOpenPullRequests(
512
+ options.targetBranch,
513
+ );
514
+ const managedCandidates = openPullRequests
515
+ .map((pullRequest) =>
516
+ managedCandidateFromPullRequest(pullRequest, options.targetBranch),
517
+ )
518
+ .filter(Boolean);
519
+ for (const candidate of managedCandidates) {
520
+ if (
521
+ candidate.state &&
522
+ (candidate.state.repository !== options.repository ||
523
+ candidate.state.sourceBranch !== options.sourceBranch)
524
+ ) {
525
+ throw new Error(
526
+ `candidate PR #${candidate.number} state does not bind ${options.repository} ${options.sourceBranch}`,
527
+ );
528
+ }
529
+ }
530
+ if (managedCandidates.length > 1) {
531
+ throw new Error(
532
+ `multiple open Buildchain candidate PRs target ${options.targetBranch}: ${managedCandidates
533
+ .map((candidate) => `#${candidate.number}`)
534
+ .join(", ")}`,
535
+ );
536
+ }
537
+ let activeCandidate = managedCandidates[0] || null;
538
+ const observedCandidate = candidateFromDecision(decision);
539
+ let nextCandidate = null;
540
+ let supersededCandidate = null;
541
+ let controllerState = decision.eligible
542
+ ? "eligible-for-settlement"
543
+ : qualificationError
544
+ ? /stale/u.test(qualificationError.message)
545
+ ? "stale"
546
+ : "blocked"
547
+ : "observed";
548
+ if (activeCandidate) {
549
+ controllerState = "active";
550
+ if (
551
+ observedCandidate &&
552
+ observedCandidate.sourceSha !== activeCandidate.sourceSha
553
+ ) {
554
+ nextCandidate = observedCandidate;
555
+ controllerState = "retained-next";
556
+ }
557
+ if (
558
+ activeCandidate.nextCandidate &&
559
+ activeCandidate.nextCandidate.sourceSha !== nextCandidate?.sourceSha
560
+ ) {
561
+ supersededCandidate = activeCandidate.nextCandidate;
562
+ }
563
+ }
564
+ let state = candidateStateBody({
565
+ options,
250
566
  targetSha,
251
- comparison: { status: comparison.status, aheadBy: comparison.ahead_by },
252
- selection: {
253
- mode: "latest-qualified-source-ancestor",
254
- observedSourceHeadSha,
255
- skippedNewerCommitCount,
256
- },
257
- workflowEvidence,
258
- requiredWorkflowPaths,
259
- maxAgeSeconds: options.maxAgeSeconds,
260
- now: options.now,
567
+ decision,
568
+ activeCandidate: activeCandidate
569
+ ? {
570
+ sourceSha: activeCandidate.sourceSha,
571
+ sourceLockRef: activeCandidate.sourceLockRef,
572
+ decisionRoot: activeCandidate.decisionRoot || null,
573
+ pullRequestNumber: activeCandidate.number,
574
+ pullRequestUrl: activeCandidate.url,
575
+ }
576
+ : null,
577
+ nextCandidate,
578
+ supersededCandidate,
261
579
  });
262
580
  let pullRequest;
263
- if (decision.eligible && options.createPullRequest && !options.dryRun) {
264
- await client.ensureImmutableBranch(decision.sourceLockRef, sourceSha);
265
- pullRequest = await client.ensurePullRequest({
266
- head: decision.sourceLockRef,
267
- base: options.targetBranch,
268
- title: `Promote qualified ${options.sourceBranch} candidate ${sourceSha.slice(0, 12)} to ${options.targetBranch}`,
269
- body: [
270
- "Buildchain exact-source channel candidate.",
271
- "",
272
- `- Source branch: \`${options.sourceBranch}\``,
273
- `- Observed source HEAD: \`${observedSourceHeadSha}\``,
274
- `- Source SHA: \`${sourceSha}\``,
275
- `- Skipped newer unqualified commits: \`${skippedNewerCommitCount}\``,
276
- `- Target branch/head: \`${options.targetBranch}\` / \`${targetSha}\``,
277
- `- Decision root: \`${decision.decisionRoot}\``,
278
- ...decision.workflowEvidence.map(
279
- (row) =>
280
- `- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
281
- ),
282
- "",
283
- "The source-lock branch must continue to point at the exact source SHA. This patrol never merges the PR, publishes a package, creates a tag, or creates a release.",
284
- ].join("\n"),
285
- });
581
+ let settlementAction = "none";
582
+ if (options.settlementAuthorized && !options.dryRun) {
583
+ if (activeCandidate) {
584
+ const nextBody = replaceCandidateStateMarker(activeCandidate.body, state);
585
+ if (nextBody !== activeCandidate.body) {
586
+ await client.updatePullRequestBody(activeCandidate.number, nextBody);
587
+ settlementAction = nextCandidate
588
+ ? supersededCandidate
589
+ ? "supersede-next-candidate"
590
+ : "retain-next-candidate"
591
+ : "reconcile-active-candidate";
592
+ }
593
+ pullRequest = {
594
+ number: activeCandidate.number,
595
+ html_url: activeCandidate.url,
596
+ };
597
+ } else if (decision.eligible) {
598
+ await client.ensureImmutableBranch(decision.sourceLockRef, sourceSha);
599
+ state = candidateStateBody({
600
+ options,
601
+ targetSha,
602
+ decision,
603
+ activeCandidate: observedCandidate,
604
+ nextCandidate: null,
605
+ supersededCandidate: null,
606
+ });
607
+ pullRequest = await client.ensurePullRequest({
608
+ head: decision.sourceLockRef,
609
+ base: options.targetBranch,
610
+ title: `Promote qualified ${options.sourceBranch} candidate ${sourceSha.slice(0, 12)} to ${options.targetBranch}`,
611
+ body: pullRequestBody({
612
+ options,
613
+ observedSourceHeadSha,
614
+ sourceSha,
615
+ skippedNewerCommitCount,
616
+ targetSha,
617
+ decision,
618
+ state,
619
+ }),
620
+ });
621
+ settlementAction = "create-active-candidate";
622
+ controllerState = "active";
623
+ activeCandidate = {
624
+ number: Number(pullRequest.number || 0),
625
+ url: text(pullRequest.html_url),
626
+ sourceSha,
627
+ sourceLockRef: decision.sourceLockRef,
628
+ decisionRoot: decision.decisionRoot,
629
+ };
630
+ }
286
631
  }
287
632
  return {
288
633
  schema: "kungfu-buildchain-dev-alpha-candidate-patrol/v1",
289
634
  dryRun: options.dryRun,
290
635
  createPullRequest: options.createPullRequest,
636
+ settlementAuthorized: options.settlementAuthorized,
291
637
  decision,
638
+ controller: {
639
+ schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
640
+ state: controllerState,
641
+ activeCandidate,
642
+ nextCandidate,
643
+ supersededCandidate,
644
+ settlementAction,
645
+ stateRoot: state.stateRoot,
646
+ },
292
647
  pullRequest: pullRequest || null,
293
648
  };
294
649
  }
@@ -301,6 +656,8 @@ export function createGitHubChannelCandidateClient({
301
656
  repository: repositoryInput,
302
657
  token,
303
658
  fetchImpl = globalThis.fetch,
659
+ sleepImpl = (milliseconds) =>
660
+ new Promise((resolve) => setTimeout(resolve, milliseconds)),
304
661
  }) {
305
662
  const [owner, repo] = repository(repositoryInput).split("/");
306
663
  const headers = {
@@ -313,21 +670,37 @@ export function createGitHubChannelCandidateClient({
313
670
  requestPath,
314
671
  { method = "GET", body, allow404 = false } = {},
315
672
  ) {
316
- const response = await fetchImpl(`https://api.github.com${requestPath}`, {
317
- method,
318
- headers: Object.fromEntries(
319
- Object.entries(headers).filter(([, value]) => value),
320
- ),
321
- body: body === undefined ? undefined : JSON.stringify(body),
322
- });
323
- const raw = await response.text();
324
- const payload = raw ? JSON.parse(raw) : undefined;
325
- if (allow404 && response.status === 404) return undefined;
326
- if (!response.ok)
673
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
674
+ const response = await fetchImpl(`https://api.github.com${requestPath}`, {
675
+ method,
676
+ headers: Object.fromEntries(
677
+ Object.entries(headers).filter(([, value]) => value),
678
+ ),
679
+ body: body === undefined ? undefined : JSON.stringify(body),
680
+ });
681
+ const raw = await response.text();
682
+ const payload = raw ? JSON.parse(raw) : undefined;
683
+ if (allow404 && response.status === 404) return undefined;
684
+ if (response.ok) return payload;
685
+ const retryable = response.status === 429 || response.status >= 500;
686
+ if (retryable && attempt < 3) {
687
+ const retryAfterHeader = response.headers?.get?.("retry-after");
688
+ const retryAfter =
689
+ retryAfterHeader === null || retryAfterHeader === undefined
690
+ ? Number.NaN
691
+ : Number(retryAfterHeader);
692
+ await sleepImpl(
693
+ Number.isFinite(retryAfter) && retryAfter >= 0
694
+ ? Math.min(retryAfter * 1000, 10_000)
695
+ : attempt * 250,
696
+ );
697
+ continue;
698
+ }
327
699
  throw new Error(
328
700
  `GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`,
329
701
  );
330
- return payload;
702
+ }
703
+ throw new Error(`GitHub API ${method} ${requestPath} exhausted retries`);
331
704
  }
332
705
  return {
333
706
  async resolveBranch(ref) {
@@ -368,6 +741,19 @@ export function createGitHubChannelCandidateClient({
368
741
  }
369
742
  return commits;
370
743
  },
744
+ async listOpenPullRequests(base) {
745
+ const pullRequests = [];
746
+ for (let page = 1; page <= 10; page += 1) {
747
+ const rows = await api(
748
+ `/repos/${owner}/${repo}/pulls?state=open&base=${encodeURIComponent(base)}&per_page=100&page=${page}`,
749
+ );
750
+ pullRequests.push(...rows);
751
+ if (rows.length < 100) return pullRequests;
752
+ }
753
+ throw new Error(
754
+ `open pull request history for ${base} exceeds 1000 rows`,
755
+ );
756
+ },
371
757
  async ensureImmutableBranch(ref, sourceSha) {
372
758
  const current = await api(
373
759
  `/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`,
@@ -396,6 +782,12 @@ export function createGitHubChannelCandidateClient({
396
782
  })
397
783
  );
398
784
  },
785
+ async updatePullRequestBody(number, body) {
786
+ return api(`/repos/${owner}/${repo}/pulls/${number}`, {
787
+ method: "PATCH",
788
+ body: { body },
789
+ });
790
+ },
399
791
  };
400
792
  }
401
793
 
@@ -406,6 +798,10 @@ function markdown(result) {
406
798
  `Eligible: \`${result.decision.eligible}\` (${result.decision.reason})`,
407
799
  `Source: \`${result.decision.source.branch}@${result.decision.source.sha}\``,
408
800
  `Target: \`${result.decision.target.branch}@${result.decision.target.sha}\``,
801
+ `Controller state: \`${result.controller.state}\``,
802
+ `Active candidate: ${result.controller.activeCandidate?.url || "none"}`,
803
+ `Next candidate: \`${result.controller.nextCandidate?.sourceSha || "none"}\``,
804
+ `Settlement action: \`${result.controller.settlementAction}\``,
409
805
  `Dry run: \`${result.dryRun}\``,
410
806
  `Pull request: ${result.pullRequest?.html_url || "not created"}`,
411
807
  "",
@@ -428,6 +824,10 @@ async function main() {
428
824
  "selected-sha": result.decision.source.sha,
429
825
  "source-lock-ref": result.decision.sourceLockRef || "",
430
826
  "promotion-pr": result.pullRequest?.html_url || "",
827
+ "controller-state": result.controller.state,
828
+ "active-candidate-pr": result.controller.activeCandidate?.url || "",
829
+ "next-candidate-sha": result.controller.nextCandidate?.sourceSha || "",
830
+ "settlement-action": result.controller.settlementAction,
431
831
  };
432
832
  fs.appendFileSync(
433
833
  process.env.GITHUB_OUTPUT,
@@ -205,6 +205,8 @@ ${publicOutputs(outputs)}
205
205
 
206
206
  permissions:
207
207
  actions: write
208
+ artifact-metadata: write
209
+ attestations: write
208
210
  checks: write
209
211
  contents: write
210
212
  id-token: write
@@ -406,6 +408,8 @@ jobs:
406
408
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
407
409
  permissions:
408
410
  actions: write
411
+ artifact-metadata: write
412
+ attestations: write
409
413
  checks: write
410
414
  contents: write
411
415
  id-token: write
@@ -422,6 +426,8 @@ ${alphaForwarded}
422
426
  uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
423
427
  permissions:
424
428
  actions: write
429
+ artifact-metadata: write
430
+ attestations: write
425
431
  checks: write
426
432
  contents: write
427
433
  id-token: write