code-foundry 1.9.13 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,27 @@ Agents must not:
32
32
  - Change branch protections, secrets, deployments, or external systems unless that action is explicitly in scope.
33
33
  - Claim completion when tests, deployment checks, or required reviews are still pending.
34
34
 
35
+ <!-- code-foundry-managed: pull-request-policy -->
36
+
37
+ ### Pull request readiness (mandatory)
38
+
39
+ This repository uses the `direct` workflow. Topic pull requests target `main`.
40
+
41
+ - Open every ordinary pull request as a draft. Use `gh pr create --draft` or
42
+ set `draft: true` in the GitHub API; never create a ready ordinary pull
43
+ request as a shortcut.
44
+ - Keep ordinary pull requests in draft while making commits. The generated
45
+ Draft Guard resets a ready ordinary pull request to draft on creation,
46
+ reopening, or a new commit, and runner-heavy validation starts only after an
47
+ explicit `ready_for_review` transition.
48
+ - Run local validation and finish review preparation before marking an ordinary
49
+ pull request ready. After any later commit, mark it ready again only after
50
+ the current head is ready for review.
51
+ - Release Please version pull requests are managed by the Code Foundry release
52
+ workflow; do not manually change their draft state unless the workflow asks.
53
+
54
+ <!-- /code-foundry-managed: pull-request-policy -->
55
+
35
56
  ## Branching model
36
57
 
37
58
  ```text
@@ -160,11 +181,11 @@ Keep pull requests focused and reviewable. Include screenshots or recordings for
160
181
  | Exact Release Please pull request targeting `main` | Full validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |
161
182
  | Scheduled or manual validation | Full audit tier |
162
183
  | Push to a working branch | Draft PR workflow |
163
- | Push to `main` | Release workflow; canonical validation already ran on the merged PR |
184
+ | Push to `main` | Release workflow plus default-branch CodeQL scan; validation ran on the merged PR |
164
185
 
165
- Draft pull requests do not start validation. Marking a pull request ready for review starts the applicable validation tier. Convert it back to draft after an update, then mark it ready again after every update so the required checks attach to the current head. Converting it to draft runs only the lightweight cancellation control.
186
+ Draft pull requests do not start validation. The lightweight Draft Guard also converts ordinary pull requests opened, reopened, or updated while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier. Convert it back to draft after an update, then mark it ready again after every update so the required checks attach to the current head. Converting it to draft runs only the lightweight cancellation control.
166
187
 
167
- Pull-request validation keys concurrency by event and pull-request head, so a newer update cancels its superseded run. Scheduled and manual audits use a separate caller pinned to the protected default branch; this prevents caller-selected runtime code from executing with default-branch cache access. Both callers use the mode-aware orchestrator, which fans out only the required jobs and concludes with the stable aggregate gate.
188
+ Pull-request validation keys concurrency by event and pull-request head, so a newer update cancels its superseded run. Scheduled and manual audits use a separate caller pinned to the protected default branch; this prevents caller-selected runtime code from executing with default-branch cache access. Pull-request and scheduled/manual callers use the mode-aware orchestrator, which fans out only the required jobs and concludes with the stable aggregate gate. Main pushes run the default-branch CodeQL lane separately.
168
189
 
169
190
  Required checks are enforced by branch protection rulesets/branch protection. Do not duplicate their checklists in the pull request description; document validation commands and results instead.
170
191
 
@@ -0,0 +1,79 @@
1
+ name: Code Foundry Draft Guard
2
+
3
+ on:
4
+ # This workflow runs from the trusted base branch and only reads pull-request
5
+ # metadata before changing draft state. It never checks out or executes PR code.
6
+ pull_request_target:
7
+ branches: [main, staging]
8
+ types:
9
+ - opened
10
+ - reopened
11
+ - synchronize
12
+
13
+ permissions:
14
+ pull-requests: write
15
+
16
+ concurrency:
17
+ group: code-foundry-draft-guard-${{ github.repository }}-${{ github.event.pull_request.number }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ enforce:
22
+ name: Draft Guard / Enforce
23
+ if: vars.CI_BILLING_PAUSED != 'true'
24
+ runs-on: ubuntu-slim
25
+ timeout-minutes: 5
26
+ steps:
27
+ - name: Keep ordinary pull requests in draft
28
+ env:
29
+ GH_TOKEN: ${{ github.token }}
30
+ EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
31
+ EVENT_UPDATED_AT: ${{ github.event.pull_request.updated_at }}
32
+ PR_NUMBER: ${{ github.event.pull_request.number }}
33
+ run: |
34
+ set -euo pipefail
35
+ if [ -z "$EVENT_HEAD_SHA" ] || [ -z "$EVENT_UPDATED_AT" ] || [ -z "$PR_NUMBER" ]; then
36
+ echo '::error title=Draft guard cannot verify event freshness::The pull-request event did not include the required immutable metadata.'
37
+ exit 1
38
+ fi
39
+
40
+ read -r draft head_ref head_sha updated_at < <(
41
+ gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
42
+ --jq '[.draft, .head.ref, .head.sha, .updated_at] | @tsv'
43
+ )
44
+ if [ -z "$draft" ] || [ -z "$head_ref" ] || [ -z "$head_sha" ] || [ -z "$updated_at" ]; then
45
+ echo '::error title=Draft guard cannot verify current state::The GitHub API response did not include the required pull-request metadata.'
46
+ exit 1
47
+ fi
48
+
49
+ # Release Please owns the state of generated version PRs. Its release
50
+ # workflow deliberately resets and readies these PRs before merging.
51
+ case "$head_ref" in
52
+ release-please--branches--main|release-please--branches--main--*)
53
+ echo "Release Please head $head_ref is managed by the release workflow; leaving its draft state unchanged."
54
+ exit 0
55
+ ;;
56
+ esac
57
+
58
+ # A newer state transition or commit superseded this event. Do not
59
+ # undo a user's later explicit readiness decision with a stale event.
60
+ if [ "$head_sha" != "$EVENT_HEAD_SHA" ] || [ "$updated_at" != "$EVENT_UPDATED_AT" ]; then
61
+ echo 'A newer pull-request state or head commit superseded this draft-guard event; leaving the current state unchanged.'
62
+ exit 0
63
+ fi
64
+
65
+ if [ "$draft" = true ]; then
66
+ echo "Pull request #$PR_NUMBER is already a draft; no validation state change is needed."
67
+ exit 0
68
+ fi
69
+ if [ "$draft" != false ]; then
70
+ echo "::error title=Draft guard cannot verify current state::Unexpected draft value: $draft"
71
+ exit 1
72
+ fi
73
+
74
+ gh api \
75
+ --method PATCH \
76
+ "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
77
+ -F draft=true \
78
+ --silent >/dev/null
79
+ echo "Converted ordinary pull request #$PR_NUMBER to draft; runner-heavy validation waits for ready_for_review."
@@ -1,6 +1,8 @@
1
1
  name: Code Foundry
2
2
 
3
3
  on:
4
+ push:
5
+ branches: [main]
4
6
  pull_request:
5
7
  branches: [main, staging]
6
8
  types:
@@ -18,9 +20,9 @@ concurrency:
18
20
  jobs:
19
21
  mode:
20
22
  name: Mode
21
- # The workflow is emitted only for ready_for_review, so draft pull requests
22
- # register no skipped validation checks and allocate no validation runner.
23
- if: vars.CI_BILLING_PAUSED != 'true'
23
+ # The mode classifier is only needed for pull requests. Main pushes use
24
+ # the dedicated default-branch CodeQL lane below.
25
+ if: vars.CI_BILLING_PAUSED != 'true' && github.event_name == 'pull_request'
24
26
  runs-on: ubuntu-slim
25
27
  timeout-minutes: 10
26
28
  permissions:
@@ -59,7 +61,7 @@ jobs:
59
61
  validation:
60
62
  name: Validation
61
63
  needs: mode
62
- if: vars.CI_BILLING_PAUSED != 'true'
64
+ if: vars.CI_BILLING_PAUSED != 'true' && github.event_name == 'pull_request'
63
65
  # Reusable workflows can only maintain or reduce the caller job's scopes.
64
66
  # The audit tier needs security-events: write for CodeQL uploads.
65
67
  permissions:
@@ -87,3 +89,23 @@ jobs:
87
89
  secrets:
88
90
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
89
91
  NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
92
+
93
+ # CodeQL requires a push analysis to populate default-branch code-scanning
94
+ # alerts. Keep the full validation gate pull-request-only so main pushes do
95
+ # not duplicate CI, test, or security work.
96
+ default-branch-codeql:
97
+ name: CodeQL (default branch)
98
+ if: vars.CI_BILLING_PAUSED != 'true' && github.event_name == 'push'
99
+ permissions:
100
+ actions: read
101
+ contents: read
102
+ packages: read
103
+ security-events: write
104
+ uses: ./.github/workflows/codeql.yml
105
+ with:
106
+ runtime-repository: 0xPlayerOne/code-foundry
107
+ runtime-ref: ${{ github.sha }}
108
+ runner: ubuntu-latest
109
+ rust-shards: '["all"]'
110
+ rust-threads: '1'
111
+ rust-max-parallel: 1
package/.oxfmtrc.json CHANGED
@@ -3,5 +3,5 @@
3
3
  "singleQuote": true,
4
4
  "trailingComma": "es5",
5
5
  "printWidth": 100,
6
- "ignorePatterns": ["CHANGELOG.md", ".github/.code-foundry", ".github/actions/"]
6
+ "ignorePatterns": ["CHANGELOG.md", ".github/.code-foundry", ".github/actions/", "plugin.json"]
7
7
  }
package/AGENTS.md CHANGED
@@ -94,6 +94,29 @@ them:
94
94
  - Keep pull requests focused; merge with the canonical method only after
95
95
  required checks pass.
96
96
 
97
+ <!-- code-foundry-managed: pull-request-policy -->
98
+
99
+ ## Code Foundry workflow policy (mandatory)
100
+
101
+ This repository uses the `direct` workflow. Topic pull requests target `main`.
102
+
103
+ - Open every ordinary pull request as a draft. Use `gh pr create --draft` or
104
+ set `draft: true` in the GitHub API; never create a ready ordinary pull
105
+ request as a shortcut.
106
+ - Keep ordinary pull requests in draft while making commits. The generated
107
+ Draft Guard resets a ready ordinary pull request to draft on creation,
108
+ reopening, or a new commit, and runner-heavy validation starts only after an
109
+ explicit `ready_for_review` transition.
110
+ - Run local validation and finish review preparation before marking an ordinary
111
+ pull request ready. After any later commit, mark it ready again only after
112
+ the current head is ready for review.
113
+ - This contract is mandatory for every agent scope. Nested `AGENTS.md` files
114
+ may add stricter rules but must not weaken or replace it.
115
+ - Release Please version pull requests are managed by the Code Foundry release
116
+ workflow; do not manually change their draft state unless the workflow asks.
117
+
118
+ <!-- /code-foundry-managed: pull-request-policy -->
119
+
97
120
  ## Toolchain and dependencies
98
121
 
99
122
  - Follow `toolchain: auto` in `.github/code-foundry.yml`; use native tools by
@@ -154,7 +177,7 @@ If a check cannot run, state the exact reason. A skipped check is not a passing
154
177
  - Use per-workflow concurrency groups that cancel superseded runs while allowing independent workflows to run in parallel.
155
178
  - Keep setup language-aware and cache dependency downloads by lockfile; do not cache secrets, `node_modules`, virtual environments, or broad build output without a measured reason.
156
179
  - Use least-privilege permissions and pin action versions consistently with the template.
157
- - Keep CI, Test, Security, CodeQL, Draft PR, Release PR, and Release concerns separated.
180
+ - Keep CI, Test, Security, CodeQL, Draft Guard, Draft PR, Release PR, and Release concerns separated.
158
181
  - Security and CodeQL may skip when repository visibility or GitHub plan support does not permit them. Do not make an unavailable check required.
159
182
  - Optional Turborepo Remote Caching uses `TURBO_TOKEN` and `TURBO_TEAM`; do not add Vercel deployment behavior just to enable caching.
160
183
  - Update branch protection when adding or renaming required job checks; verify the actual GitHub status context.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.10.0...v1.10.1) (2026-09-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **doctor:** honor shared config and correctly identify npm/Yarn lockfiles ([#533](https://github.com/0xPlayerOne/code-foundry/issues/533)) ([6464409](https://github.com/0xPlayerOne/code-foundry/commit/646440907ad3fc47f63e27b9621c8e0025459964))
9
+ * **validation:** scan default branch with CodeQL on push ([#540](https://github.com/0xPlayerOne/code-foundry/issues/540)) ([98d8241](https://github.com/0xPlayerOne/code-foundry/commit/98d8241522c78e1b6d3f4f29a44053d33888edba))
10
+
11
+ ## [1.10.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.13...v1.10.0) (2026-09-08)
12
+
13
+
14
+ ### Features
15
+
16
+ * **ci:** enforce draft pull request readiness ([#530](https://github.com/0xPlayerOne/code-foundry/issues/530)) ([fe62147](https://github.com/0xPlayerOne/code-foundry/commit/fe621477a46b49dee69a4c74c482e815a3e60b8d))
17
+
3
18
  ## [1.9.13](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.12...v1.9.13) (2026-09-08)
4
19
 
5
20
 
package/README.md CHANGED
@@ -36,7 +36,7 @@ contract. For normal updates, edit that file and run `npx code-foundry sync`.
36
36
  ## What it installs
37
37
 
38
38
  - Short workflow callers for pull-request validation, protected scheduled/manual
39
- audits, Draft PR, Release PR, and Release.
39
+ audits, Draft Guard, Draft PR, Release PR, and Release.
40
40
  - A deterministic performance lane with ordered command support, stable result
41
41
  artifacts, and an optional shared Node package budget profile.
42
42
  - A small `.githooks/pre-commit` launcher with language-aware formatting and
@@ -70,14 +70,17 @@ when present and otherwise uses native language tooling; choose `native` or
70
70
 
71
71
  The standard workflow triggers are:
72
72
 
73
- - Pushes to `main` (and `staging` when `git_workflow: staging-release` is configured).
73
+ - Pushes to `main` (and `staging` when `git_workflow: staging-release` is configured); main pushes also run the default-branch CodeQL scan.
74
74
  - Pull requests targeting `main` (and `staging` in the staging-release topology).
75
75
  - Draft PR automation for supported feature/fix branches.
76
76
 
77
77
  Automated feature/fix and staging-promotion pull requests open as drafts.
78
- Pull-request validation runs on the ready-for-review transition. After later
79
- commits, convert the PR to draft and mark it ready again so checks attach to
80
- the current head. Converting a PR to draft runs only the lightweight
78
+ The trusted Draft Guard is a fallback for manually created or reopened ready
79
+ PRs and for new commits to ordinary ready PRs: it converts them to drafts
80
+ without checking out PR code. It verifies the event head and update timestamp
81
+ before changing state, and excludes Release Please version PRs whose release
82
+ workflow owns readiness. Pull-request validation runs only on the
83
+ ready-for-review transition. Converting a PR to draft runs only the lightweight
81
84
  cancellation control. Scheduled and manually dispatched audits are unaffected.
82
85
 
83
86
  Jobs are language-aware and skip irrelevant setup inside the applicable
@@ -38,7 +38,16 @@ runtime, then run sync.
38
38
 
39
39
  Sync updates standard Code Foundry files only. It preserves application code,
40
40
  authored documentation, existing `.mise.toml` selections, and custom workflows
41
- such as deployment, search, Slither, or monitoring workflows.
41
+ such as deployment, search, Slither, or monitoring workflows. A missing
42
+ `AGENTS.md` receives the baseline agent contract. If another initializer has
43
+ created an unmarked `AGENTS.md` or `.github/CONTRIBUTING.md`, sync preserves its
44
+ surrounding text and adds or refreshes only the marked Code Foundry policy block;
45
+ this lets later syncs restore mandatory pull-request rules without replacing
46
+ agent-specific instructions. Marked generated policy documents continue to
47
+ receive topology-aware baseline updates, and missing configuration keys are
48
+ added without changing existing values. The Oxfmt baseline also ignores
49
+ `plugin.json`, whose serialization is owned by Release Please; plugin manifest
50
+ semantics remain covered by repository tests rather than a formatter rewrite.
42
51
 
43
52
  The environment bootstrap enables repository hooks and uses mise only when an
44
53
  existing `.mise.toml` is present or `toolchain: mise` is selected. Otherwise it
package/docs/WORKFLOWS.md CHANGED
@@ -4,20 +4,30 @@
4
4
 
5
5
  Validation uses two callers so pull-request code and default-branch-capable
6
6
  audit events never share a caller-selected runtime ref. The canonical
7
- `validation.yml` caller handles pull requests only:
7
+ `validation.yml` caller handles pull requests and a lightweight default-branch
8
+ CodeQL lane:
8
9
 
9
10
  ```yaml
11
+ push:
12
+ branches: [main] # CodeQL only
10
13
  pull_request:
11
14
  branches: [main, staging] # staging-release topology
12
15
  # direct topology: branches: [main]
13
16
  ```
14
17
 
15
- The generated validation caller listens only for `ready_for_review`, so draft
16
- pull requests register no validation checks and allocate no validation runner.
17
- After any later update, convert the pull request to draft and mark it ready
18
- again so required checks attach to the current head. A separate lightweight
19
- draft-control caller listens for `converted_to_draft` and cancels queued or
20
- running pull-request workflows without creating skipped validation jobs.
18
+ The generated validation caller listens for `ready_for_review` and `push` to
19
+ `main`. Draft pull requests register no validation checks and allocate no
20
+ validation runner; main pushes run only the default-branch CodeQL lane, while
21
+ full validation remains pull-request-only.
22
+ A separate lightweight Draft Guard runs from the trusted base branch on
23
+ `opened`, `reopened`, and `synchronize`; it converts ordinary ready pull
24
+ requests back to draft without checking out pull-request code. It rechecks the
25
+ current head and update timestamp before mutating state, so a stale event
26
+ cannot undo a later draft or ready transition. Release Please version heads are
27
+ excluded because the release workflow owns their state. A separate draft-control
28
+ caller listens for `converted_to_draft` and cancels queued or running
29
+ pull-request workflows without creating skipped validation jobs. Marking a pull
30
+ request ready again starts validation for the current head.
21
31
 
22
32
  The separate `validation-audit.yml` caller is pinned to the configured released
23
33
  runtime and handles scheduled and manual audits:
@@ -39,7 +49,7 @@ Scheduled and manual runs select the audit tier in both topologies. Draft PR
39
49
  automation separately listens to supported topic-branch pushes and always
40
50
  opens those PRs as drafts. Promotion automation listens to `staging` pushes
41
51
  (staging-release only) and also always opens its PR as a draft. Release
42
- automation listens to `main` pushes.
52
+ automation and default-branch CodeQL listen to `main` pushes.
43
53
  Custom deployment, indexing, search, Slither, or other workflows are
44
54
  repository-owned extensions and should use the same ready-transition policy.
45
55
 
@@ -91,15 +101,16 @@ opt in or out without a code change.
91
101
 
92
102
  ## Standard workflow responsibilities
93
103
 
94
- | Workflow | Responsibility |
95
- | ---------- | ----------------------------------------------------------------- |
96
- | CI | Format, lint, type-check, and build |
97
- | Test | Unit, standardized performance, integration, E2E, and smoke tests |
98
- | Security | Profile, audits, and public-only Dependency Review |
99
- | CodeQL | GitHub-native code scanning, kept separate from CI |
100
- | Draft PR | Create/update development pull requests |
101
- | Release PR | Promote `staging` into `main` (staging-release topology only) |
102
- | Release | Release Please, GitHub release, and optional npm publication |
104
+ | Workflow | Responsibility |
105
+ | ----------- | ----------------------------------------------------------------- |
106
+ | CI | Format, lint, type-check, and build |
107
+ | Test | Unit, standardized performance, integration, E2E, and smoke tests |
108
+ | Security | Profile, audits, and public-only Dependency Review |
109
+ | CodeQL | GitHub-native code scanning, kept separate from CI |
110
+ | Draft PR | Create/update development pull requests |
111
+ | Draft Guard | Keep ordinary PRs draft until `ready_for_review` |
112
+ | Release PR | Promote `staging` into `main` (staging-release topology only) |
113
+ | Release | Release Please, GitHub release, and optional npm publication |
103
114
 
104
115
  Use concise job names such as `CI / Format`, `Test / Unit`, and
105
116
  `CodeQL / Analyze (Python)`. Per-language CodeQL analyzers (Rust shards
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.9.13",
3
+ "version": "1.10.1",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
6
6
  "bugs": {
@@ -5,6 +5,7 @@ import { join, resolve } from 'node:path'
5
5
  import { spawnSync } from 'node:child_process'
6
6
  import { includesValue, readConfig } from '../lib/config.mjs'
7
7
  import { recommendRunners, resolveProfile } from '../lib/profile.mjs'
8
+ import { packageManagerForLockfile } from '../lib/lockfiles.mjs'
8
9
  import { doctorGithub } from '../lib/github-doctor.mjs'
9
10
  import { isGeneratedEventCaller } from './sync.mjs'
10
11
 
@@ -69,9 +70,7 @@ export function doctor(root, options = {}) {
69
70
  if (lockfiles.length > 1) error('multiple JavaScript lockfiles found; keep one package manager')
70
71
  if (packageJson.packageManager && lockfiles.length) {
71
72
  const declared = String(packageJson.packageManager).split('@')[0]
72
- const actual = lockfiles[0]?.startsWith('bun')
73
- ? 'bun'
74
- : lockfiles[0]?.split('-')[0].replace('.yaml', '')
73
+ const actual = packageManagerForLockfile(lockfiles[0])
75
74
  if (actual && declared !== actual)
76
75
  error(`packageManager (${declared}) does not match ${actual} lockfile`)
77
76
  }
@@ -150,9 +149,19 @@ export function doctor(root, options = {}) {
150
149
  const validationEnabled =
151
150
  includesValue(features, 'validation') ||
152
151
  ['ci', 'test', 'security', 'codeql'].some((legacy) => includesValue(features, legacy))
153
- for (const name of ['validation', 'draft-control', 'draft-pr', 'release-pr', 'release']) {
152
+ for (const name of [
153
+ 'validation',
154
+ 'draft-control',
155
+ 'draft-enforcement',
156
+ 'draft-pr',
157
+ 'release-pr',
158
+ 'release',
159
+ ]) {
154
160
  if (name === 'release-pr' && workflow !== 'staging-release') continue
155
- const enabled = name === 'draft-control' ? validationEnabled : includesValue(features, name)
161
+ const enabled =
162
+ name === 'draft-control' || name === 'draft-enforcement'
163
+ ? validationEnabled
164
+ : includesValue(features, name)
156
165
  const candidates = [
157
166
  join(target, `.github/workflows/${name}.yml`),
158
167
  join(target, `.github/workflows/${name}_self-ci.yml`),
@@ -371,6 +371,7 @@ function deliverReconciliationPullRequest(target, base, head, state, targetSha,
371
371
  title,
372
372
  '--body',
373
373
  body,
374
+ '--draft',
374
375
  ])
375
376
  if (created.status !== 0) {
376
377
  // A concurrent run may have created the pull request between our list
@@ -44,6 +44,7 @@ const standardFiles = [
44
44
  '.github/workflows/validation.yml',
45
45
  '.github/workflows/validation-audit.yml',
46
46
  '.github/workflows/draft-control.yml',
47
+ '.github/workflows/draft-enforcement.yml',
47
48
  '.github/workflows/draft-pr.yml',
48
49
  '.github/workflows/release-pr.yml',
49
50
  '.github/workflows/release.yml',
@@ -221,10 +222,6 @@ export function syncRepository(options) {
221
222
  const sourceFile = sourcePath(source, file)
222
223
  if (!existsSync(sourceFile)) throw new Error(`Template file missing: ${file}`)
223
224
  const destination = join(target, file)
224
- if (!force && protectedFiles.has(file) && existsSync(destination)) {
225
- const existing = readFileSync(destination, 'utf8')
226
- if (!isLegacyManagedDoc(file, existing) && !isManagedConfigPolicy(file, existing)) continue
227
- }
228
225
  if (
229
226
  (file === 'LICENSE' || file === 'NOTICE') &&
230
227
  license === 'preserve' &&
@@ -267,6 +264,19 @@ export function syncRepository(options) {
267
264
  mergeIgnorePatternsConfig(content.toString('utf8'), readFileSync(destination, 'utf8'))
268
265
  )
269
266
  }
267
+ if (!force && protectedFiles.has(file) && existsSync(destination)) {
268
+ const existing = readFileSync(destination, 'utf8')
269
+ if (!isLegacyManagedDoc(file, existing) && !isManagedConfigPolicy(file, existing)) {
270
+ if (configAwarePolicyFiles.has(file)) {
271
+ const merged = mergeManagedPolicyBlocks(existing, content.toString('utf8'))
272
+ if (merged !== existing) {
273
+ changed.push(file)
274
+ writeOrReport(destination, merged, dryRun)
275
+ }
276
+ }
277
+ continue
278
+ }
279
+ }
270
280
  if (!existsSync(destination) || !buffersEqual(content, readFileSync(destination))) {
271
281
  changed.push(file)
272
282
  writeOrReport(destination, content, dryRun)
@@ -478,7 +488,12 @@ function shouldInclude(file, languages, features, config) {
478
488
  // the configuration or the variable enables it and the API key exists.
479
489
  if (file === '.github/workflows/opencode-security.yml') return true
480
490
  const workflow = file.match(/^\.github\/workflows\/([^/]+)\.yml$/)?.[1]
481
- if (workflow === 'draft-control') return true
491
+ if (workflow === 'draft-control' || workflow === 'draft-enforcement') {
492
+ return (
493
+ includesValue(features, 'validation') ||
494
+ LEGACY_GENERATED_CALLERS.some((legacy) => includesValue(features, legacy))
495
+ )
496
+ }
482
497
  // The staging promotion caller only exists in the staging-release topology;
483
498
  // direct repositories open feature branches into main and need no promotion.
484
499
  if (workflow === 'release-pr' && !isStagingRelease(config.git_workflow)) return false
@@ -522,12 +537,15 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
522
537
  let rendered = content.replaceAll(localPrefix, remotePrefix)
523
538
  // An explicitly unavailable CodeQL capability selects an orchestrator that
524
539
  // omits the job entirely, so GitHub does not register a misleading skipped
525
- // check on every pull request. `auto` retains runtime capability detection.
540
+ // check on every pull request or an unused main-push run. `auto` retains
541
+ // runtime capability detection.
526
542
  if (configured(config.codeql, 'auto') === 'false') {
527
543
  rendered = rendered.replaceAll(
528
544
  `${remotePrefix}validation.yml`,
529
545
  `${remotePrefix}validation-no-codeql.yml`
530
546
  )
547
+ rendered = removeWorkflowBlock(rendered, 'default-branch-codeql')
548
+ rendered = removeWorkflowBlock(rendered, 'push')
531
549
  }
532
550
  rendered = rendered.replace(
533
551
  new RegExp(`${escapeRegExp(remotePrefix)}([^\\s@]+)`, 'g'),
@@ -625,6 +643,25 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
625
643
  return rendered
626
644
  }
627
645
 
646
+ /**
647
+ * Remove a root-level YAML block by id while preserving the surrounding file.
648
+ * This is used for workflow jobs and triggers whose feature is disabled by
649
+ * configuration.
650
+ * @param {string} content
651
+ * @param {string} blockId
652
+ * @returns {string}
653
+ */
654
+ function removeWorkflowBlock(content, blockId) {
655
+ const lines = content.split('\n')
656
+ const start = lines.findIndex((line) => line === ` ${blockId}:`)
657
+ if (start === -1) return content
658
+ let end = start + 1
659
+ while (end < lines.length && !/^ [A-Za-z0-9_-]+:\s*$/.test(lines[end])) end += 1
660
+ lines.splice(start, end - start)
661
+ if (content.endsWith('\n') && lines.at(-1) !== '') lines.push('')
662
+ return lines.join('\n')
663
+ }
664
+
628
665
  /**
629
666
  * Dependabot updates land on the repository's integration branch. Direct
630
667
  * repositories have no staging branch, so every update targets main. Cargo,
@@ -719,8 +756,16 @@ const DIRECT_DOC_REPLACEMENTS = {
719
756
  'This repository uses the `staging-release` workflow: topic branches **squash** into `staging`, a promotion PR **rebases** validated changes into `main` (`merge_strategy: rebase`), and the Release Please version PR **rebases** into `main` (`release_merge_strategy: rebase`). Feature PRs land on `staging` with squash merges; promotion and release PRs land on `main` with rebase merges. Re-align `staging` with `main` after a release when needed.',
720
757
  'This repository uses the `direct` workflow: topic branches **squash** directly into `main`, and the Release Please version PR **squashes** into `main` (`release_merge_strategy: squash`). Feature and release PRs land on `main` with squash merges. No integration branch exists; all pull requests target `main`.',
721
758
  ],
759
+ [
760
+ 'This repository uses the `staging-release` workflow. Topic pull requests target `staging`; promotion pull requests target `main`.',
761
+ 'This repository uses the `direct` workflow. Topic pull requests target `main`.',
762
+ ],
722
763
  ],
723
764
  '.github/CONTRIBUTING.md': [
765
+ [
766
+ 'This repository uses the `staging-release` workflow. Topic pull requests target `staging`; promotion pull requests target `main`.',
767
+ 'This repository uses the `direct` workflow. Topic pull requests target `main`.',
768
+ ],
724
769
  [
725
770
  '4. Branch from `staging` and target pull requests at `staging`; do not work directly on `main`.',
726
771
  '4. Branch from `main` and target pull requests at `main`; do not push directly to `main`.',
@@ -756,16 +801,16 @@ const DIRECT_DOC_REPLACEMENTS = {
756
801
  '7. Push to the fork and open a pull request targeting `main`.',
757
802
  ],
758
803
  [
759
- '| Event | Expected automation |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| Draft pull request targeting `staging` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `staging` | Fast validation: CI plus unit tests, ending in `Validation / Gate` |\n| Draft ordinary pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready ordinary pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Full validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `staging` | Promotion PR workflow; canonical validation waits for the PR event |\n| Push to `main` | Release workflow; canonical validation already ran on the merged PR |\n',
760
- '| Event | Expected automation |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| Draft pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Full validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `main` | Release workflow; canonical validation already ran on the merged PR |\n',
804
+ '| Event | Expected automation |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| Draft pull request targeting `staging` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `staging` | Fast validation: CI plus unit tests, ending in `Validation / Gate` |\n| Draft ordinary pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready ordinary pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Full validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `staging` | Promotion PR workflow; canonical validation waits for the PR event |\n| Push to `main` | Release workflow plus default-branch CodeQL scan; validation ran on the merged PR |\n',
805
+ '| Event | Expected automation |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- |\n| Draft pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please pull request targeting `main` | Full validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `main` | Release workflow plus default-branch CodeQL scan; validation ran on the merged PR |\n',
761
806
  ],
762
807
  [
763
808
  '| Change | Target | Merge method | Merge gate |\n| ---------------------------- | --------- | ----------------------------------------------- | --------------------------------------------------------- |\n| Working branch | `staging` | Squash | All applicable required checks pass |\n| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |\n',
764
809
  '| Change | Target | Merge method | Merge gate |\n| ------------------------- | ------ | --------------------------------- | --------------------------------------- |\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` | Squash (`release_merge_strategy`) | Validation gate and release policy pass |\n',
765
810
  ],
766
811
  [
767
- 'Draft pull requests do not start runner-heavy validation. Marking a pull request ready for review starts the applicable validation tier; converting it back to draft cancels in-flight validation, and no replacement starts until it is ready again.',
768
- 'Draft pull requests do not start validation. Marking a pull request ready for review starts the applicable validation tier. Convert it back to draft after an update, then mark it ready again after every update so the required checks attach to the current head. Converting it to draft runs only the lightweight cancellation control.',
812
+ 'Draft pull requests do not start runner-heavy validation. The lightweight Draft Guard also converts ordinary pull requests opened, reopened, or updated while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier. Convert it back to draft after an update, then mark it ready again after every update so the required checks attach to the current head; converting it back to draft cancels in-flight validation, and no replacement starts until it is ready again.',
813
+ 'Draft pull requests do not start validation. The lightweight Draft Guard also converts ordinary pull requests opened, reopened, or updated while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier. Convert it back to draft after an update, then mark it ready again after every update so the required checks attach to the current head. Converting it to draft runs only the lightweight cancellation control.',
769
814
  ],
770
815
  ['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
771
816
  ],
@@ -1116,9 +1161,12 @@ function isLegacyManagedDoc(file, content) {
1116
1161
 
1117
1162
  /**
1118
1163
  * Config-aware policy documents are generated contracts: a normal sync must
1119
- * refresh them after branch-topology or validation-policy edits. The marker
1120
- * owns future copies. Exact scaffold signatures migrate older generated
1121
- * copies without treating arbitrary repository documentation as managed.
1164
+ * refresh generated copies after branch-topology or validation-policy edits.
1165
+ * Unmarked consumer-owned documents receive only the explicitly marked policy
1166
+ * blocks, so another agent's initializer cannot be overwritten. The top-level
1167
+ * marker owns generated copies. Exact scaffold signatures migrate older
1168
+ * generated copies without treating arbitrary repository documentation as
1169
+ * managed.
1122
1170
  * @param {string} file
1123
1171
  * @param {string} content
1124
1172
  */
@@ -1157,6 +1205,45 @@ function git(target, args) {
1157
1205
  spawnSync('git', args, { cwd: target, stdio: 'ignore' })
1158
1206
  }
1159
1207
 
1208
+ /**
1209
+ * Merge the marked policy blocks from a rendered baseline into a consumer-owned
1210
+ * policy document. Unmarked AGENTS.md/CONTRIBUTING.md files are commonly
1211
+ * generated by another agent initializer, so sync must add and refresh the
1212
+ * Code Foundry contract without replacing the user's surrounding instructions.
1213
+ * @param {string} existing
1214
+ * @param {string} baseline
1215
+ * @returns {string}
1216
+ */
1217
+ function mergeManagedPolicyBlocks(existing, baseline) {
1218
+ const blockPattern =
1219
+ /<!-- code-foundry-managed: ([A-Za-z0-9_-]+) -->[\s\S]*?<!-- \/code-foundry-managed: \1 -->/g
1220
+ const blocks = [...baseline.matchAll(blockPattern)]
1221
+ if (!blocks.length) return existing
1222
+
1223
+ let merged = existing
1224
+ for (const match of blocks) {
1225
+ const id = match[1]
1226
+ const block = match[0]
1227
+ const start = `<!-- code-foundry-managed: ${id} -->`
1228
+ const end = `<!-- /code-foundry-managed: ${id} -->`
1229
+ const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, 'g')
1230
+ const occurrences = merged.match(pattern) ?? []
1231
+ if (occurrences.length > 1) {
1232
+ throw new Error(`Managed policy block appears more than once in consumer document: ${id}`)
1233
+ }
1234
+ if (occurrences.length === 1) {
1235
+ merged = merged.replace(pattern, block)
1236
+ continue
1237
+ }
1238
+ if (merged.includes(start) || merged.includes(end)) {
1239
+ throw new Error(`Managed policy block is incomplete in consumer document: ${id}`)
1240
+ }
1241
+ const separator = merged.endsWith('\n\n') ? '' : merged.endsWith('\n') ? '\n' : '\n\n'
1242
+ merged = `${merged}${separator}${block}\n`
1243
+ }
1244
+ return merged
1245
+ }
1246
+
1160
1247
  /** @param {string} file @param {Buffer|string} content @param {boolean} dryRun */
1161
1248
  function writeOrReport(file, content, dryRun) {
1162
1249
  if (dryRun) {
@@ -3,6 +3,7 @@
3
3
  import { readFileSync, readdirSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { spawnSync } from 'node:child_process'
6
+ import { readConfig } from './config.mjs'
6
7
 
7
8
  /** @param {string} root @returns {{ errors: string[], warnings: string[], details: Record<string, unknown> }} */
8
9
  export function doctorGithub(root) {
@@ -306,22 +307,3 @@ function ghJson(args) {
306
307
  function commandExists(command) {
307
308
  return spawnSync(command, ['--version'], { stdio: 'ignore' }).status === 0
308
309
  }
309
-
310
- /** @param {string} root @returns {Record<string, string>} */
311
- function readConfig(root) {
312
- const file = join(root, '.github/code-foundry.yml')
313
- try {
314
- return Object.fromEntries(
315
- readFileSync(file, 'utf8')
316
- .split(/\r?\n/)
317
- .flatMap((line) => {
318
- const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/)
319
- return match
320
- ? [[match[1], match[2].replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '')]]
321
- : []
322
- })
323
- )
324
- } catch {
325
- return {}
326
- }
327
- }
@@ -0,0 +1,15 @@
1
+ // @ts-check
2
+
3
+ /** @type {Readonly<Record<string, string>>} */
4
+ const managers = Object.freeze({
5
+ 'bun.lock': 'bun',
6
+ 'bun.lockb': 'bun',
7
+ 'pnpm-lock.yaml': 'pnpm',
8
+ 'yarn.lock': 'yarn',
9
+ 'package-lock.json': 'npm',
10
+ })
11
+
12
+ /** @param {string | undefined} file @returns {string | undefined} */
13
+ export function packageManagerForLockfile(file) {
14
+ return file && Object.hasOwn(managers, file) ? managers[file] : undefined
15
+ }