code-foundry 1.9.13 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -162,7 +183,7 @@ Keep pull requests focused and reviewable. Include screenshots or recordings for
162
183
  | Push to a working branch | Draft PR workflow |
163
184
  | Push to `main` | Release workflow; canonical validation already 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
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. Both callers use the mode-aware orchestrator, which fans out only the required jobs and concludes with the stable aggregate gate.
168
189
 
@@ -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."
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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.13...v1.10.0) (2026-09-08)
4
+
5
+
6
+ ### Features
7
+
8
+ * **ci:** enforce draft pull request readiness ([#530](https://github.com/0xPlayerOne/code-foundry/issues/530)) ([fe62147](https://github.com/0xPlayerOne/code-foundry/commit/fe621477a46b49dee69a4c74c482e815a3e60b8d))
9
+
3
10
  ## [1.9.13](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.12...v1.9.13) (2026-09-08)
4
11
 
5
12
 
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
@@ -75,9 +75,12 @@ The standard workflow triggers are:
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
@@ -14,10 +14,15 @@ pull_request:
14
14
 
15
15
  The generated validation caller listens only for `ready_for_review`, so draft
16
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.
17
+ A separate lightweight Draft Guard runs from the trusted base branch on
18
+ `opened`, `reopened`, and `synchronize`; it converts ordinary ready pull
19
+ requests back to draft without checking out pull-request code. It rechecks the
20
+ current head and update timestamp before mutating state, so a stale event
21
+ cannot undo a later draft or ready transition. Release Please version heads are
22
+ excluded because the release workflow owns their state. A separate draft-control
23
+ caller listens for `converted_to_draft` and cancels queued or running
24
+ pull-request workflows without creating skipped validation jobs. Marking a pull
25
+ request ready again starts validation for the current head.
21
26
 
22
27
  The separate `validation-audit.yml` caller is pinned to the configured released
23
28
  runtime and handles scheduled and manual audits:
@@ -91,15 +96,16 @@ opt in or out without a code change.
91
96
 
92
97
  ## Standard workflow responsibilities
93
98
 
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 |
99
+ | Workflow | Responsibility |
100
+ | ----------- | ----------------------------------------------------------------- |
101
+ | CI | Format, lint, type-check, and build |
102
+ | Test | Unit, standardized performance, integration, E2E, and smoke tests |
103
+ | Security | Profile, audits, and public-only Dependency Review |
104
+ | CodeQL | GitHub-native code scanning, kept separate from CI |
105
+ | Draft PR | Create/update development pull requests |
106
+ | Draft Guard | Keep ordinary PRs draft until `ready_for_review` |
107
+ | Release PR | Promote `staging` into `main` (staging-release topology only) |
108
+ | Release | Release Please, GitHub release, and optional npm publication |
103
109
 
104
110
  Use concise job names such as `CI / Format`, `Test / Unit`, and
105
111
  `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.0",
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": {
@@ -150,9 +150,19 @@ export function doctor(root, options = {}) {
150
150
  const validationEnabled =
151
151
  includesValue(features, 'validation') ||
152
152
  ['ci', 'test', 'security', 'codeql'].some((legacy) => includesValue(features, legacy))
153
- for (const name of ['validation', 'draft-control', 'draft-pr', 'release-pr', 'release']) {
153
+ for (const name of [
154
+ 'validation',
155
+ 'draft-control',
156
+ 'draft-enforcement',
157
+ 'draft-pr',
158
+ 'release-pr',
159
+ 'release',
160
+ ]) {
154
161
  if (name === 'release-pr' && workflow !== 'staging-release') continue
155
- const enabled = name === 'draft-control' ? validationEnabled : includesValue(features, name)
162
+ const enabled =
163
+ name === 'draft-control' || name === 'draft-enforcement'
164
+ ? validationEnabled
165
+ : includesValue(features, name)
156
166
  const candidates = [
157
167
  join(target, `.github/workflows/${name}.yml`),
158
168
  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
@@ -719,8 +734,16 @@ const DIRECT_DOC_REPLACEMENTS = {
719
734
  '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
735
  '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
736
  ],
737
+ [
738
+ 'This repository uses the `staging-release` workflow. Topic pull requests target `staging`; promotion pull requests target `main`.',
739
+ 'This repository uses the `direct` workflow. Topic pull requests target `main`.',
740
+ ],
722
741
  ],
723
742
  '.github/CONTRIBUTING.md': [
743
+ [
744
+ 'This repository uses the `staging-release` workflow. Topic pull requests target `staging`; promotion pull requests target `main`.',
745
+ 'This repository uses the `direct` workflow. Topic pull requests target `main`.',
746
+ ],
724
747
  [
725
748
  '4. Branch from `staging` and target pull requests at `staging`; do not work directly on `main`.',
726
749
  '4. Branch from `main` and target pull requests at `main`; do not push directly to `main`.',
@@ -764,8 +787,8 @@ const DIRECT_DOC_REPLACEMENTS = {
764
787
  '| 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
788
  ],
766
789
  [
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.',
790
+ '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.',
791
+ '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
792
  ],
770
793
  ['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
771
794
  ],
@@ -1116,9 +1139,12 @@ function isLegacyManagedDoc(file, content) {
1116
1139
 
1117
1140
  /**
1118
1141
  * 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.
1142
+ * refresh generated copies after branch-topology or validation-policy edits.
1143
+ * Unmarked consumer-owned documents receive only the explicitly marked policy
1144
+ * blocks, so another agent's initializer cannot be overwritten. The top-level
1145
+ * marker owns generated copies. Exact scaffold signatures migrate older
1146
+ * generated copies without treating arbitrary repository documentation as
1147
+ * managed.
1122
1148
  * @param {string} file
1123
1149
  * @param {string} content
1124
1150
  */
@@ -1157,6 +1183,45 @@ function git(target, args) {
1157
1183
  spawnSync('git', args, { cwd: target, stdio: 'ignore' })
1158
1184
  }
1159
1185
 
1186
+ /**
1187
+ * Merge the marked policy blocks from a rendered baseline into a consumer-owned
1188
+ * policy document. Unmarked AGENTS.md/CONTRIBUTING.md files are commonly
1189
+ * generated by another agent initializer, so sync must add and refresh the
1190
+ * Code Foundry contract without replacing the user's surrounding instructions.
1191
+ * @param {string} existing
1192
+ * @param {string} baseline
1193
+ * @returns {string}
1194
+ */
1195
+ function mergeManagedPolicyBlocks(existing, baseline) {
1196
+ const blockPattern =
1197
+ /<!-- code-foundry-managed: ([A-Za-z0-9_-]+) -->[\s\S]*?<!-- \/code-foundry-managed: \1 -->/g
1198
+ const blocks = [...baseline.matchAll(blockPattern)]
1199
+ if (!blocks.length) return existing
1200
+
1201
+ let merged = existing
1202
+ for (const match of blocks) {
1203
+ const id = match[1]
1204
+ const block = match[0]
1205
+ const start = `<!-- code-foundry-managed: ${id} -->`
1206
+ const end = `<!-- /code-foundry-managed: ${id} -->`
1207
+ const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, 'g')
1208
+ const occurrences = merged.match(pattern) ?? []
1209
+ if (occurrences.length > 1) {
1210
+ throw new Error(`Managed policy block appears more than once in consumer document: ${id}`)
1211
+ }
1212
+ if (occurrences.length === 1) {
1213
+ merged = merged.replace(pattern, block)
1214
+ continue
1215
+ }
1216
+ if (merged.includes(start) || merged.includes(end)) {
1217
+ throw new Error(`Managed policy block is incomplete in consumer document: ${id}`)
1218
+ }
1219
+ const separator = merged.endsWith('\n\n') ? '' : merged.endsWith('\n') ? '\n' : '\n\n'
1220
+ merged = `${merged}${separator}${block}\n`
1221
+ }
1222
+ return merged
1223
+ }
1224
+
1160
1225
  /** @param {string} file @param {Buffer|string} content @param {boolean} dryRun */
1161
1226
  function writeOrReport(file, content, dryRun) {
1162
1227
  if (dryRun) {