@empire-builder-kit/nx 1.0.0-beta.4 → 1.0.0-beta.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.
Files changed (72) hide show
  1. package/README.md +13 -4
  2. package/executors.json +20 -0
  3. package/generators.json +5 -0
  4. package/package.json +15 -1
  5. package/src/executors/deployment-plan/deployment-plan.d.ts +9 -0
  6. package/src/executors/deployment-plan/deployment-plan.js +50 -0
  7. package/src/executors/deployment-plan/deployment-plan.js.map +1 -0
  8. package/src/executors/deployment-plan/schema.json +24 -0
  9. package/src/executors/foundation-preflight/foundation-preflight.js +22 -0
  10. package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -1
  11. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +8 -0
  12. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +41 -0
  13. package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -0
  14. package/src/executors/foundation-profile-preflight/schema.json +21 -0
  15. package/src/executors/github-bootstrap/github-bootstrap.d.ts +8 -0
  16. package/src/executors/github-bootstrap/github-bootstrap.js +27 -0
  17. package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -0
  18. package/src/executors/github-bootstrap/schema.json +25 -0
  19. package/src/executors/sst-lifecycle/run-sst-command.d.ts +2 -0
  20. package/src/executors/sst-lifecycle/run-sst-command.js +58 -5
  21. package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
  22. package/src/executors/work-markers/schema.json +14 -0
  23. package/src/executors/work-markers/work-markers.d.ts +6 -0
  24. package/src/executors/work-markers/work-markers.js +20 -0
  25. package/src/executors/work-markers/work-markers.js.map +1 -0
  26. package/src/generators/preset/workspace-files.d.ts +1 -1
  27. package/src/generators/preset/workspace-files.js +702 -38
  28. package/src/generators/preset/workspace-files.js.map +1 -1
  29. package/src/generators/repository-policy/repository-policy.d.ts +4 -0
  30. package/src/generators/repository-policy/repository-policy.js +55 -0
  31. package/src/generators/repository-policy/repository-policy.js.map +1 -0
  32. package/src/generators/repository-policy/schema.d.ts +16 -0
  33. package/src/generators/repository-policy/schema.json +41 -0
  34. package/src/generators/slice/slice.js +22 -0
  35. package/src/generators/slice/slice.js.map +1 -1
  36. package/src/git-policy/action-pins.d.ts +28 -0
  37. package/src/git-policy/action-pins.js +35 -0
  38. package/src/git-policy/action-pins.js.map +1 -0
  39. package/src/git-policy/github-bootstrap.d.ts +20 -0
  40. package/src/git-policy/github-bootstrap.js +676 -0
  41. package/src/git-policy/github-bootstrap.js.map +1 -0
  42. package/src/git-policy/index.d.ts +3 -0
  43. package/src/git-policy/index.js +7 -0
  44. package/src/git-policy/index.js.map +1 -0
  45. package/src/git-policy/routing.d.ts +15 -0
  46. package/src/git-policy/routing.js +77 -0
  47. package/src/git-policy/routing.js.map +1 -0
  48. package/src/index.d.ts +3 -0
  49. package/src/index.js +3 -0
  50. package/src/index.js.map +1 -1
  51. package/src/repository-policy/aurora-preflight.d.ts +15 -0
  52. package/src/repository-policy/aurora-preflight.js +45 -0
  53. package/src/repository-policy/aurora-preflight.js.map +1 -0
  54. package/src/repository-policy/deployment-plan.d.ts +24 -0
  55. package/src/repository-policy/deployment-plan.js +124 -0
  56. package/src/repository-policy/deployment-plan.js.map +1 -0
  57. package/src/repository-policy/foundation-profile.d.ts +20 -0
  58. package/src/repository-policy/foundation-profile.js +45 -0
  59. package/src/repository-policy/foundation-profile.js.map +1 -0
  60. package/src/repository-policy/index.d.ts +4 -0
  61. package/src/repository-policy/index.js +8 -0
  62. package/src/repository-policy/index.js.map +1 -0
  63. package/src/repository-policy/policy.d.ts +53 -0
  64. package/src/repository-policy/policy.js +250 -0
  65. package/src/repository-policy/policy.js.map +1 -0
  66. package/src/schemas/repository-policy-v1.json +42 -0
  67. package/src/work-markers/index.d.ts +1 -0
  68. package/src/work-markers/index.js +5 -0
  69. package/src/work-markers/index.js.map +1 -0
  70. package/src/work-markers/work-markers.d.ts +39 -0
  71. package/src/work-markers/work-markers.js +359 -0
  72. package/src/work-markers/work-markers.js.map +1 -0
@@ -5,6 +5,8 @@ exports.renderWorkspaceFiles = renderWorkspaceFiles;
5
5
  exports.renderWorkspaceRootFilePlan = renderWorkspaceRootFilePlan;
6
6
  exports.renderFoundationFilePlan = renderFoundationFilePlan;
7
7
  const index_js_1 = require("../../ownership/index.js");
8
+ const index_js_2 = require("../../git-policy/index.js");
9
+ const index_js_3 = require("../../repository-policy/index.js");
8
10
  const TOOL_VERSIONS = {
9
11
  eslint: '^9.8.0',
10
12
  eslintJs: '^9.8.0',
@@ -25,8 +27,15 @@ const FOUNDATION_OWNERSHIP_PATH = `${FOUNDATION_ROOT}.ebk/ownership.json`;
25
27
  const FOUNDATION_TOOL_OWNED_PATHS = new Set(['sst-env.d.ts']);
26
28
  const WORKSPACE_OWNERSHIP_PATH = '.ebk/ownership.json';
27
29
  const WORKSPACE_SEEDED_PATHS = new Set([
30
+ '.github/CODEOWNERS',
31
+ 'AGENTS.md',
32
+ 'CLAUDE.md',
28
33
  'README.md',
29
34
  'docs/architecture.md',
35
+ 'docs/git-workflow.md',
36
+ 'docs/repository-operating-policy.md',
37
+ 'docs/work-markers.md',
38
+ 'ebk.repository-policy.json',
30
39
  'package.json',
31
40
  ]);
32
41
  exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
@@ -34,10 +43,14 @@ exports.WORKSPACE_PROJECT_MANAGED_JSON_POINTERS = [
34
43
  '/$schema',
35
44
  '/projectType',
36
45
  '/tags',
46
+ '/targets/deployment-plan',
37
47
  '/targets/dev',
38
48
  '/targets/dev-doctor',
39
49
  '/targets/dev-setup',
50
+ '/targets/github-bootstrap',
51
+ '/targets/install-git-hooks',
40
52
  '/targets/verify',
53
+ '/targets/work-markers',
41
54
  '/targets/workspace-health',
42
55
  ];
43
56
  const FOUNDATION_SEEDED_PATHS = new Set([
@@ -101,13 +114,24 @@ function createWorkspaceOwnership(files, frameworkVersion) {
101
114
  }
102
115
  function renderWorkspaceFiles(options) {
103
116
  const { frameworkVersion, name } = options;
117
+ const checkoutAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.checkout);
118
+ const configureAwsAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.configureAwsCredentials);
119
+ const pnpmAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.pnpmSetup);
120
+ const setupNodeAction = (0, index_js_2.pinnedAction)(index_js_2.GITHUB_ACTION_PINS.setupNode);
104
121
  const files = {
122
+ '.github/CODEOWNERS': `# Replace this placeholder with the GitHub user or team that owns protected source.
123
+ # GitHub bootstrap remains blocked until a non-placeholder catch-all owner is committed.
124
+ * @REPLACE_WITH_GITHUB_CODE_OWNER
125
+ `,
126
+ '.github/actions-pins.json': json(index_js_2.GITHUB_ACTION_PINS),
105
127
  '.github/workflows/ci.yml': `name: Verify
106
128
 
107
129
  on:
108
130
  pull_request:
131
+ branches: [develop, main]
109
132
  push:
110
- branches: [main]
133
+ branches: [develop, main]
134
+ workflow_dispatch:
111
135
 
112
136
  concurrency:
113
137
  group: verify-\${{ github.ref }}
@@ -117,12 +141,29 @@ permissions:
117
141
  contents: read
118
142
 
119
143
  jobs:
144
+ policy:
145
+ name: policy
146
+ runs-on: ubuntu-latest
147
+ steps:
148
+ - uses: ${checkoutAction}
149
+ with:
150
+ fetch-depth: 0
151
+ ref: \${{ github.event.pull_request.head.sha || github.sha }}
152
+ - uses: ${pnpmAction}
153
+ - uses: ${setupNodeAction}
154
+ with:
155
+ node-version: 22
156
+ cache: pnpm
157
+ - run: pnpm install --frozen-lockfile
158
+ - run: node tools/ebk-git-policy.mjs
159
+
120
160
  verify:
161
+ name: verify
121
162
  runs-on: ubuntu-latest
122
163
  steps:
123
- - uses: actions/checkout@v6
124
- - uses: pnpm/action-setup@v5
125
- - uses: actions/setup-node@v6
164
+ - uses: ${checkoutAction}
165
+ - uses: ${pnpmAction}
166
+ - uses: ${setupNodeAction}
126
167
  with:
127
168
  node-version: 22
128
169
  cache: pnpm
@@ -133,49 +174,289 @@ jobs:
133
174
  '.github/workflows/deploy.yml': `name: Deploy
134
175
 
135
176
  on:
177
+ push:
178
+ branches: [develop, main]
136
179
  workflow_dispatch:
137
180
  inputs:
138
- stage:
139
- description: Deployment stage
140
- required: true
141
- default: development
142
- type: choice
143
- options: [development, staging, production]
144
181
  action:
145
182
  description: Lifecycle action
146
183
  required: true
147
184
  default: deploy
148
185
  type: choice
149
186
  options: [deploy, refresh, unlock, remove]
187
+ project:
188
+ description: Required registered project for explicit unlock or remove
189
+ required: false
190
+ type: string
150
191
 
151
192
  concurrency:
152
- group: deploy-\${{ inputs.stage }}
193
+ group: deploy-\${{ github.ref }}
153
194
  cancel-in-progress: false
154
195
 
155
196
  permissions:
156
197
  contents: read
157
- id-token: write
158
198
 
159
199
  jobs:
160
- deploy:
200
+ route:
201
+ runs-on: ubuntu-latest
202
+ steps:
203
+ - name: Validate protected ref
204
+ env:
205
+ EBK_PROTECTED_REF: \${{ github.ref }}
206
+ run: |
207
+ case "$EBK_PROTECTED_REF" in
208
+ refs/heads/develop|refs/heads/main) ;;
209
+ *) echo 'Protected lifecycle must be dispatched from develop or main'; exit 1 ;;
210
+ esac
211
+
212
+ plan-staging:
213
+ needs: route
214
+ if: github.ref == 'refs/heads/develop'
215
+ runs-on: ubuntu-latest
216
+ outputs:
217
+ foundation_project: \${{ steps.plan.outputs.foundation_project }}
218
+ has_slices: \${{ steps.plan.outputs.has_slices }}
219
+ ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
220
+ slice_projects: \${{ steps.plan.outputs.slice_projects }}
221
+ steps:
222
+ - uses: ${checkoutAction}
223
+ - uses: ${pnpmAction}
224
+ - uses: ${setupNodeAction}
225
+ with:
226
+ node-version: 22
227
+ cache: pnpm
228
+ - run: pnpm install --frozen-lockfile
229
+ - id: plan
230
+ env:
231
+ EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
232
+ EBK_PROJECT: \${{ inputs.project || '' }}
233
+ run: |
234
+ args=("--action=$EBK_ACTION" "--stage=staging")
235
+ if [ -n "$EBK_PROJECT" ]; then args+=("--project=$EBK_PROJECT"); fi
236
+ pnpm nx run ${name}:deployment-plan -- "\${args[@]}"
237
+
238
+ plan-production:
239
+ needs: route
161
240
  if: github.ref == 'refs/heads/main'
162
- environment: \${{ inputs.stage }}
241
+ runs-on: ubuntu-latest
242
+ outputs:
243
+ foundation_project: \${{ steps.plan.outputs.foundation_project }}
244
+ foundation_profile: \${{ steps.plan.outputs.foundation_profile }}
245
+ has_slices: \${{ steps.plan.outputs.has_slices }}
246
+ ordered_projects: \${{ steps.plan.outputs.ordered_projects }}
247
+ slice_projects: \${{ steps.plan.outputs.slice_projects }}
248
+ steps:
249
+ - uses: ${checkoutAction}
250
+ - uses: ${pnpmAction}
251
+ - uses: ${setupNodeAction}
252
+ with:
253
+ node-version: 22
254
+ cache: pnpm
255
+ - run: pnpm install --frozen-lockfile
256
+ - id: plan
257
+ env:
258
+ EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
259
+ EBK_PROJECT: \${{ inputs.project || '' }}
260
+ run: |
261
+ args=("--action=$EBK_ACTION" "--stage=production")
262
+ if [ -n "$EBK_PROJECT" ]; then args+=("--project=$EBK_PROJECT"); fi
263
+ pnpm nx run ${name}:deployment-plan -- "\${args[@]}"
264
+
265
+ deploy-staging:
266
+ name: deploy staging
267
+ needs: plan-staging
268
+ environment: staging
269
+ permissions:
270
+ contents: read
271
+ id-token: write
272
+ env:
273
+ AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
274
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
275
+ AWS_REGION: \${{ vars.AWS_REGION }}
276
+ EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
277
+ EBK_STAGE: staging
278
+ FOUNDATION_PROJECT: \${{ needs.plan-staging.outputs.foundation_project }}
279
+ ORDERED_PROJECTS: \${{ needs.plan-staging.outputs.ordered_projects }}
280
+ SLICE_PROJECTS: \${{ needs.plan-staging.outputs.slice_projects }}
281
+ runs-on: ubuntu-latest
282
+ steps:
283
+ - uses: ${checkoutAction}
284
+ - name: Validate environment configuration
285
+ run: |
286
+ test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'staging AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
287
+ test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'staging AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
288
+ test -n "$AWS_REGION" || { echo 'staging AWS_REGION is required'; exit 1; }
289
+ case "$AWS_DEPLOY_ROLE_ARN" in
290
+ arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
291
+ *) echo 'Deployment role does not belong to the expected staging account'; exit 1 ;;
292
+ esac
293
+ - uses: ${configureAwsAction}
294
+ with:
295
+ role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
296
+ role-session-name: ebk-\${{ github.run_id }}-\${{ github.run_attempt }}
297
+ aws-region: \${{ vars.AWS_REGION }}
298
+ - name: Verify assumed account
299
+ run: test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID"
300
+ - uses: ${pnpmAction}
301
+ - uses: ${setupNodeAction}
302
+ with:
303
+ node-version: 22
304
+ cache: pnpm
305
+ - run: pnpm install --frozen-lockfile
306
+ - name: Run ordered lifecycle
307
+ run: |
308
+ if [ -n "$FOUNDATION_PROJECT" ]; then
309
+ pnpm nx run "$FOUNDATION_PROJECT:$EBK_ACTION" --stage="$EBK_STAGE"
310
+ fi
311
+ if [ -n "$SLICE_PROJECTS" ]; then
312
+ pnpm nx run-many -t "$EBK_ACTION" --projects="$SLICE_PROJECTS" --stage="$EBK_STAGE" --parallel=1
313
+ fi
314
+
315
+ deploy-production:
316
+ name: deploy production
317
+ needs: plan-production
318
+ environment: production
319
+ permissions:
320
+ contents: read
321
+ id-token: write
163
322
  env:
323
+ AWS_DEPLOY_ROLE_ARN: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
324
+ AWS_EXPECTED_ACCOUNT_ID: \${{ vars.AWS_EXPECTED_ACCOUNT_ID }}
325
+ AWS_REGION: \${{ vars.AWS_REGION }}
326
+ EBK_ACTION: \${{ github.event_name == 'push' && 'deploy' || inputs.action }}
164
327
  EBK_ALLOW_PRODUCTION_REMOVE: \${{ vars.EBK_ALLOW_PRODUCTION_REMOVE || 'false' }}
328
+ EBK_STAGE: production
329
+ FOUNDATION_PROJECT: \${{ needs.plan-production.outputs.foundation_project }}
330
+ ORDERED_PROJECTS: \${{ needs.plan-production.outputs.ordered_projects }}
331
+ SLICE_PROJECTS: \${{ needs.plan-production.outputs.slice_projects }}
165
332
  runs-on: ubuntu-latest
166
333
  steps:
167
- - uses: actions/checkout@v6
168
- - uses: aws-actions/configure-aws-credentials@v6
334
+ - uses: ${checkoutAction}
335
+ - name: Validate environment configuration
336
+ run: |
337
+ test -n "$AWS_DEPLOY_ROLE_ARN" || { echo 'production AWS_DEPLOY_ROLE_ARN is required'; exit 1; }
338
+ test -n "$AWS_EXPECTED_ACCOUNT_ID" || { echo 'production AWS_EXPECTED_ACCOUNT_ID is required'; exit 1; }
339
+ test -n "$AWS_REGION" || { echo 'production AWS_REGION is required'; exit 1; }
340
+ case "$AWS_DEPLOY_ROLE_ARN" in
341
+ arn:aws:iam::$AWS_EXPECTED_ACCOUNT_ID:role/*) ;;
342
+ *) echo 'Deployment role does not belong to the expected production account'; exit 1 ;;
343
+ esac
344
+ - uses: ${configureAwsAction}
169
345
  with:
170
346
  role-to-assume: \${{ secrets.AWS_DEPLOY_ROLE_ARN }}
171
- aws-region: \${{ vars.AWS_REGION || 'us-east-1' }}
172
- - uses: pnpm/action-setup@v5
173
- - uses: actions/setup-node@v6
347
+ role-session-name: ebk-\${{ github.run_id }}-\${{ github.run_attempt }}
348
+ aws-region: \${{ vars.AWS_REGION }}
349
+ - name: Verify assumed account
350
+ run: test "$(aws sts get-caller-identity --query Account --output text)" = "$AWS_EXPECTED_ACCOUNT_ID"
351
+ - uses: ${pnpmAction}
352
+ - uses: ${setupNodeAction}
174
353
  with:
175
354
  node-version: 22
176
355
  cache: pnpm
177
356
  - run: pnpm install --frozen-lockfile
178
- - run: pnpm nx run-many -t \${{ inputs.action }} --stage=\${{ inputs.stage }} --parallel=1
357
+ - name: Run ordered lifecycle
358
+ run: |
359
+ if [ -n "$FOUNDATION_PROJECT" ]; then
360
+ pnpm nx run "$FOUNDATION_PROJECT:$EBK_ACTION" --stage="$EBK_STAGE"
361
+ fi
362
+ if [ -n "$SLICE_PROJECTS" ]; then
363
+ pnpm nx run-many -t "$EBK_ACTION" --projects="$SLICE_PROJECTS" --stage="$EBK_STAGE" --parallel=1
364
+ fi
365
+ `,
366
+ '.github/workflows/sync-back.yml': `name: Sync production history
367
+
368
+ on:
369
+ workflow_run:
370
+ workflows: [Deploy]
371
+ types: [completed]
372
+
373
+ permissions:
374
+ contents: read
375
+ pull-requests: write
376
+
377
+ jobs:
378
+ sync-back:
379
+ if: >-
380
+ github.event.workflow_run.conclusion == 'success' &&
381
+ github.event.workflow_run.head_branch == 'main'
382
+ runs-on: ubuntu-latest
383
+ steps:
384
+ - name: Open or locate main-to-develop pull request
385
+ env:
386
+ GH_TOKEN: \${{ github.token }}
387
+ GH_REPO: \${{ github.repository }}
388
+ run: |
389
+ number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number // empty')
390
+ if [ -z "$number" ]; then
391
+ gh pr create \
392
+ --head main \
393
+ --base develop \
394
+ --title 'chore: sync production history to develop' \
395
+ --body 'Managed EBK sync-back. Review and merge; this workflow never pushes or auto-merges.'
396
+ number=$(gh pr list --state open --head main --base develop --json number --jq '.[0].number')
397
+ fi
398
+ echo "Sync-back pull request #$number is ready for review."
399
+
400
+ head_sha=$(gh pr view "$number" --json headRefOid --jq '.headRefOid')
401
+ for attempt in 1 2 3 4 5 6; do
402
+ runs=$(gh api \
403
+ -H 'Accept: application/vnd.github+json' \
404
+ "/repos/$GH_REPO/actions/workflows/ci.yml/runs?event=pull_request&head_sha=$head_sha" \
405
+ --jq '.total_count')
406
+ if [ "$runs" -gt 0 ]; then
407
+ echo 'Pull-request verification run materialized; approve it if GitHub marks it approval-required.'
408
+ exit 0
409
+ fi
410
+ sleep 5
411
+ done
412
+ echo 'No pull-request verification run materialized. Close and reopen the sync pull request as a human, then approve its workflows.'
413
+ exit 1
414
+ `,
415
+ 'tools/ebk-pre-push.mjs': `import { createInterface } from 'node:readline';
416
+
417
+ const protectedRefs = new Set(['refs/heads/develop', 'refs/heads/main']);
418
+ const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
419
+ let rejected = false;
420
+
421
+ for await (const line of lines) {
422
+ const [, , remoteRef] = line.trim().split(/\\s+/);
423
+ if (!protectedRefs.has(remoteRef)) continue;
424
+ rejected = true;
425
+ const branch = remoteRef.slice('refs/heads/'.length);
426
+ const route = branch === 'main'
427
+ ? 'develop -> main promotion'
428
+ : 'feature -> develop or main -> develop sync-back';
429
+ console.error('Direct push to ' + branch + ' rejected. Use a reviewed ' + route + ' pull request.');
430
+ }
431
+
432
+ if (rejected) process.exitCode = 1;
433
+ `,
434
+ 'tools/install-ebk-hooks.mjs': `import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
435
+ import { spawnSync } from 'node:child_process';
436
+
437
+ const marker = '# Empire Builder Kit managed pre-push hook';
438
+ const resolved = spawnSync('git', ['rev-parse', '--git-path', 'hooks/pre-push'], {
439
+ encoding: 'utf8',
440
+ shell: false,
441
+ });
442
+ if (resolved.status !== 0 || !resolved.stdout.trim()) {
443
+ console.error(resolved.stderr.trim() || 'Unable to resolve the repository-local Git hook path.');
444
+ process.exit(1);
445
+ }
446
+
447
+ const hookPath = resolved.stdout.trim();
448
+ if (existsSync(hookPath) && !readFileSync(hookPath, 'utf8').includes(marker)) {
449
+ console.error('A non-EBK pre-push hook already exists at ' + hookPath + '; preserve or compose it manually.');
450
+ process.exit(1);
451
+ }
452
+
453
+ writeFileSync(
454
+ hookPath,
455
+ '#!/bin/sh\\n' + marker + '\\nnode tools/ebk-pre-push.mjs\\n',
456
+ 'utf8',
457
+ );
458
+ chmodSync(hookPath, 0o755);
459
+ console.log('Installed the repository-local EBK pre-push hook at ' + hookPath + '.');
179
460
  `,
180
461
  '.gitignore': `.nx/cache
181
462
  .nx/workspace-data
@@ -200,6 +481,36 @@ dist
200
481
  pnpm-lock.yaml
201
482
  `,
202
483
  '.prettierrc.json': json({ singleQuote: true }),
484
+ 'AGENTS.md': `# Coding agent instructions
485
+
486
+ Use \`pnpm nx\` for workspace tasks and read the repository runbooks before changing
487
+ infrastructure or protected deployment policy. Preserve the ownership boundary recorded in
488
+ \`.ebk/ownership.json\`: managed files belong to EBK, seeded files are generated starting
489
+ points that adopters may change, and unrecorded files are adopter-owned.
490
+
491
+ ## Work markers
492
+
493
+ Run \`pnpm nx run ${name}:work-markers\` when beginning or handing off implementation work.
494
+ Search results are durable pointers to custom or nondeterministic work that the deterministic
495
+ generator cannot complete safely. Read the marker's self-contained description and its
496
+ \`GH:<number>\` or \`PLAN:<phase>/<slug>\` reference before acting.
497
+
498
+ Blueprint and repository upgrades may also add a marker to an editable surface when preserved
499
+ custom code must be reconciled with the new contract. Treat that marker as incomplete upgrade
500
+ work: adapt the custom code, run the relevant verification, retain evidence, and then remove it.
501
+
502
+ Generators and agents may add markers only to seeded or adopter-owned source files. Never add
503
+ one to a managed file or edit managed output to resolve one. Complete the referenced work,
504
+ preserve relevant tests and evidence, then remove the marker. \`EBK-SECURITY\` and
505
+ \`EBK-BLOCKER\` prevent staging and production deploy or refresh; remove and unlock recovery
506
+ remain available. See \`docs/work-markers.md\` for the fixed vocabulary and syntax.
507
+ `,
508
+ 'CLAUDE.md': `# Claude workspace guidance
509
+
510
+ Read and follow \`AGENTS.md\`, including its generated-file ownership rules and work-marker
511
+ workflow. Before implementing or handing off work, run
512
+ \`pnpm nx run ${name}:work-markers\` and inspect every reported reference.
513
+ `,
203
514
  'eslint.config.mjs': `import eslint from '@eslint/js';
204
515
  import globals from 'globals';
205
516
  import tseslint from 'typescript-eslint';
@@ -223,6 +534,8 @@ This workspace was created with Empire Builder Kit v1.
223
534
  1. Review \`docs/architecture.md\`.
224
535
  2. Review the generated Foundation before deploying it.
225
536
  3. Add product slices through the Empire Builder Kit Nx plugin.
537
+ 4. Review \`docs/repository-operating-policy.md\` and \`docs/git-workflow.md\` before enabling production.
538
+ 5. Read \`AGENTS.md\` and \`docs/work-markers.md\` before assigning generated customization work to agents.
226
539
 
227
540
  For one-origin fleet development, first run \`pnpm nx run ${name}:dev-setup\`, inspect the
228
541
  generated root certificate, then explicitly approve \`--trust --portless\`. Diagnose without
@@ -246,9 +559,150 @@ Foundation implementation files.
246
559
  Canonical consumer stages use the matching \`development\`, \`staging\`, or \`production\`
247
560
  Foundation. Personal and preview consumer stages may use only the \`development\` Foundation.
248
561
  `,
562
+ 'docs/git-workflow.md': `# Protected Git workflow
563
+
564
+ Ordinary work starts from \`develop\` and returns through a reviewed pull request to
565
+ \`develop\`. A production promotion is exactly \`develop\` to \`main\`. After production,
566
+ review and merge the managed \`main\` to \`develop\` sync-back pull request before ordinary
567
+ work resumes.
568
+
569
+ Both protected branches require the stable \`policy\` and \`verify\` checks, one approving
570
+ review including an approval from the committed catch-all CODEOWNER, dismissed stale reviews,
571
+ resolved conversations, and merge commits. Replace the placeholder in \`.github/CODEOWNERS\`
572
+ with a GitHub user or team that has repository access before bootstrap. This prevents an
573
+ approval from \`github-actions[bot]\` from satisfying the independent code-owner gate even
574
+ though Actions must be allowed to create and approve the managed sync-back pull request.
575
+ Direct updates, force pushes, deletion, squash merging, and rebase merging are prohibited.
576
+ GitHub rulesets, environments, and AWS IAM are the authorization boundaries; local hooks are
577
+ only defense in depth.
578
+
579
+ The \`staging\` environment accepts only \`develop\`; \`production\` accepts only \`main\`
580
+ and requires an independent approval when configured. Each environment owns its own
581
+ \`AWS_DEPLOY_ROLE_ARN\`, \`AWS_EXPECTED_ACCOUNT_ID\`, and \`AWS_REGION\`; production also
582
+ defines \`EBK_ALLOW_PRODUCTION_REMOVE\`, normally as \`false\`. Never add repository- or
583
+ organization-visible fallbacks for these protected values. Keep repository default workflow
584
+ permissions read-only; the generated jobs request only the additional permissions they need.
585
+
586
+ Run the GitHub bootstrap in report mode first. Source upgrades never alter remote GitHub
587
+ settings. Apply a reviewed report only after \`develop\` exists and the candidate's exact
588
+ \`policy\` and \`verify\` check contexts have run.
589
+
590
+ Set \`GH_REPO=<owner>/<repository>\` and a suitable \`GH_TOKEN\`, then run
591
+ \`pnpm nx run ${name}:github-bootstrap\`. Review the deterministic report before rerunning
592
+ with \`--apply\`. The command never creates branches, credentials, secrets, or AWS resources.
593
+
594
+ Install the optional defense-in-depth pre-push hook explicitly with
595
+ \`pnpm nx run ${name}:install-git-hooks\`. It refuses to overwrite an unrelated existing hook.
596
+
597
+ The sync-back workflow uses only \`GITHUB_TOKEN\` and never pushes or auto-merges. GitHub may
598
+ place its pull-request workflows in an approval-required state. Approve them; if no run
599
+ materializes, close and reopen the pull request as a human and then approve it.
600
+ `,
601
+ 'docs/repository-operating-policy.md': `# Repository Operating Policy
602
+
603
+ \`ebk.repository-policy.json\` is the reviewed source of truth for production access,
604
+ per-Slice readiness, and the one shared production Foundation profile. New workspaces start
605
+ with production disabled. A missing Slice entry is always blocked.
606
+
607
+ The named repository modes are source-writing presets, not runtime state. Report a change
608
+ first and inspect the complete current/next policy:
609
+
610
+ \`\`\`bash
611
+ pnpm nx g @empire-builder-kit/nx:repository-policy \\
612
+ --preset=controlled-production --slices=billing
613
+ \`\`\`
614
+
615
+ Apply the reviewed source change separately with \`--apply\`. This command never contacts AWS
616
+ or GitHub. Use \`--status=blocked|pilot|ready\` for explicit Slice transitions and
617
+ \`--profile=cost-optimized|standard\` for an explicit shared Foundation decision.
618
+
619
+ - \`blocked\` prohibits production create, update, and migration.
620
+ - \`pilot\` permits protected evidence gathering without claiming normal latency or maturity.
621
+ - \`ready\` records an adopter decision after its evidence and review; it is not inferred.
622
+ - removal and unlock remain explicit protected recovery paths so disabled policy cannot strand
623
+ resources.
624
+
625
+ Protected production deploy and refresh validate the complete Foundation/Slice set before AWS
626
+ access. The Foundation is always ordered first. The set is derived from committed policy and
627
+ cannot be narrowed with an ad hoc project argument.
628
+
629
+ \`standard\` keeps Aurora warm at 2–16 ACU. \`cost-optimized\` is a prerelease candidate at
630
+ 0–4 ACU with a five-minute auto-pause and potential cold-resume latency. Both keep managed NAT,
631
+ 30-day backups, deletion protection, retained data, and the protected GitHub/OIDC/AWS path.
632
+ Changing policy affects only later lifecycle requests; it never deploys or removes resources.
633
+ Changing profile requires a separate reviewed Foundation deployment and retained operational
634
+ evidence.
635
+
636
+ \`EBK_PRODUCTION_FOUNDATION_PROFILE\` is reserved managed executor input. The lifecycle
637
+ executor derives it from validated committed policy immediately before SST and removes any
638
+ ambient shell value. Do not set it as a local, repository, organization, or environment
639
+ variable. Incubation keeps the last configured profile, or \`standard\` for legacy/default
640
+ policy, solely so protected remove and unlock can still render existing production resources;
641
+ production deploy remains disabled.
642
+ `,
643
+ 'docs/work-markers.md': `# Agent work markers
644
+
645
+ EBK work markers let deterministic generators identify places where an agent or human must
646
+ make a contextual product decision. They are searchable pointers to durable work, not a
647
+ replacement for the referenced issue or plan.
648
+
649
+ ## Syntax and vocabulary
650
+
651
+ Use one full-line source comment with a self-contained description. Examples include
652
+ \`// EBK-TODO(GH:123): Replace the sample authorization decision with the approved policy.\`
653
+ and
654
+ \`# EBK-ASSUMPTION(PLAN:1/operator-proof): Confirm this timeout during the operator pilot.\`
655
+
656
+ The fixed vocabulary is:
657
+
658
+ - \`EBK-TODO\` — known implementation work;
659
+ - \`EBK-ASSUMPTION\` — a decision that needs validation;
660
+ - \`EBK-QUESTION\` — an unresolved design or product question;
661
+ - \`EBK-SECURITY\` — security work that blocks protected promotion; and
662
+ - \`EBK-BLOCKER\` — any other work that blocks protected promotion.
663
+
664
+ Vocabulary names used in comment prose are inert. The scanner treats only a kind immediately
665
+ followed by \`(\` or \`:\` as a marker attempt, so malformed marker-shaped comments still fail
666
+ validation.
667
+
668
+ Until Work Orchestration imports these markers, references must be an existing GitHub issue
669
+ (\`GH:<positive-number>\`) or an append-only roadmap key
670
+ (\`PLAN:<phase>/<slug>\`). The description must remain useful without opening the reference.
671
+ The scanner validates syntax and ownership now; it intentionally does not claim that a
672
+ reference exists. Phase 2 owns reference import, retained old-to-new ID mapping, and later
673
+ orphan checks. Do not invent temporary Work Orchestration numbers.
674
+
675
+ ## Generator and agent workflow
676
+
677
+ Generators may emit a marker only into a \`seeded\` customization surface. They must never put
678
+ one in a \`managed\` file: managed output stays deterministic, and a needed change there is a
679
+ framework defect to report outside the generated file. Adopter-owned files may also contain
680
+ markers. Framework upgrades preserve seeded source and its unresolved markers.
681
+
682
+ Blueprint and repository update tooling may create a deterministic reconciliation marker in a
683
+ seeded or adopter-owned file when a preserved customization needs human or agent judgment. The
684
+ update must preserve the custom code and identify the changed contract in the description; a
685
+ successful update command alone does not resolve that marker. Reconcile the code, run the
686
+ relevant checks, retain evidence at the referenced work item, and only then remove the marker.
687
+ Automatic insertion requires an explicit publisher migration or other deterministic anchor;
688
+ the generic upgrade command does not guess where to edit preserved custom code.
689
+
690
+ At the start and handoff of agent work, run:
691
+
692
+ \`pnpm nx run ${name}:work-markers\`
693
+
694
+ The same ordinary validation runs through workspace health and CI. Complete the referenced
695
+ custom work and remove the marker; do not merely delete it while leaving the requirement
696
+ unresolved. Open \`TODO\`, \`ASSUMPTION\`, and \`QUESTION\` markers are reported but do not block
697
+ deployment by themselves. Open \`SECURITY\` and \`BLOCKER\` markers fail staging and production
698
+ deploy or refresh before AWS access. Protected remove and unlock remain usable for recovery.
699
+ There is no local or silent bypass in this prerelease contract; any future waiver mechanism
700
+ must be committed, reviewable, and auditable.
701
+ `,
702
+ 'ebk.repository-policy.json': json((0, index_js_3.createIncubationRepositoryPolicy)()),
249
703
  'nx.json': json({
250
704
  $schema: './node_modules/nx/schemas/nx-schema.json',
251
- defaultBase: 'main',
705
+ defaultBase: 'develop',
252
706
  namedInputs: {
253
707
  default: ['{projectRoot}/**/*', 'sharedGlobals'],
254
708
  production: [
@@ -324,6 +778,13 @@ Foundation. Personal and preview consumer stages may use only the \`development\
324
778
  projectType: 'application',
325
779
  tags: ['scope:workspace', 'layer:governance'],
326
780
  targets: {
781
+ 'deployment-plan': {
782
+ executor: '@empire-builder-kit/nx:deployment-plan',
783
+ options: {
784
+ action: 'deploy',
785
+ stage: 'staging',
786
+ },
787
+ },
327
788
  dev: {
328
789
  continuous: true,
329
790
  executor: '@empire-builder-kit/nx:fleet',
@@ -339,6 +800,18 @@ Foundation. Personal and preview consumer stages may use only the \`development\
339
800
  executor: '@empire-builder-kit/nx:dev-setup',
340
801
  options: {},
341
802
  },
803
+ 'github-bootstrap': {
804
+ executor: '@empire-builder-kit/nx:github-bootstrap',
805
+ options: {},
806
+ },
807
+ 'install-git-hooks': {
808
+ executor: 'nx:run-commands',
809
+ options: { command: 'node tools/install-ebk-hooks.mjs' },
810
+ },
811
+ 'work-markers': {
812
+ executor: '@empire-builder-kit/nx:work-markers',
813
+ options: {},
814
+ },
342
815
  verify: {
343
816
  executor: 'nx:run-commands',
344
817
  options: { command: 'node tools/workspace-health.mjs' },
@@ -380,7 +853,13 @@ domain. Without it, the shared Router uses its functional provider URL.
380
853
  ],
381
854
  }),
382
855
  'packages/foundation/contracts/costs.json': json({
383
- schemaVersion: 1,
856
+ schemaVersion: 2,
857
+ aurora: {
858
+ engine: index_js_3.FOUNDATION_AURORA_ENGINE_FAMILY,
859
+ engineVersion: index_js_3.FOUNDATION_AURORA_ENGINE_VERSION,
860
+ },
861
+ productionProfiles: index_js_3.PRODUCTION_FOUNDATION_PROFILE_CONTRACTS,
862
+ productionProfileSource: 'ebk.repository-policy.json',
384
863
  stages: {
385
864
  development: {
386
865
  aurora: 'scale-to-zero',
@@ -393,7 +872,7 @@ domain. Without it, the shared Router uses its functional provider URL.
393
872
  nat: 'ec2',
394
873
  },
395
874
  production: {
396
- aurora: 'always-warm',
875
+ aurora: 'repository-policy',
397
876
  dataRetention: 'retained-and-protected',
398
877
  nat: 'managed',
399
878
  },
@@ -415,7 +894,9 @@ seeded decision record aligned with organization-specific budgets and alerts.
415
894
 
416
895
  - Development uses EC2 NAT, a development bastion, and Aurora scale-to-zero.
417
896
  - Staging uses EC2 NAT and Aurora scale-to-zero by default.
418
- - Production uses managed NAT and an always-warm Aurora baseline.
897
+ - Production always uses managed NAT and retains protected data. Its explicit Repository
898
+ Operating Policy profile selects either the standard always-warm Aurora floor or the
899
+ prerelease cost-optimized auto-pause candidate.
419
900
  - ECS, EventBridge, Cognito, SNS, SSM, and CloudWatch are primarily usage-priced; logs,
420
901
  metrics, alarms, and retained data still require active cost review.
421
902
 
@@ -474,6 +955,11 @@ declare const $app: {
474
955
  foundationSsmPath,
475
956
  type FoundationSsmKey,
476
957
  } from '@empire-builder-kit/nx/foundation';
958
+ import {
959
+ FOUNDATION_AURORA_ENGINE,
960
+ FOUNDATION_AURORA_ENGINE_VERSION,
961
+ productionFoundationProfileContract,
962
+ } from '@empire-builder-kit/nx/repository-policy';
477
963
 
478
964
  function publishFoundation(values: Record<FoundationSsmKey, any>) {
479
965
  return Object.fromEntries(
@@ -492,11 +978,16 @@ export function foundation() {
492
978
  const isProduction = $app.stage === 'production';
493
979
  const isStaging = $app.stage === 'staging';
494
980
  const rootDomain = process.env.EBK_ROOT_DOMAIN?.trim();
981
+ const productionConfiguration = isProduction
982
+ ? productionFoundationProfileContract(
983
+ process.env.EBK_PRODUCTION_FOUNDATION_PROFILE,
984
+ )
985
+ : undefined;
495
986
 
496
987
  const vpc = new sst.aws.Vpc('FoundationVpc', {
497
988
  az: 2,
498
989
  bastion: $app.stage === 'development',
499
- nat: isProduction ? 'managed' : 'ec2',
990
+ nat: productionConfiguration?.contract.nat ?? 'ec2',
500
991
  });
501
992
  const cluster = new sst.aws.Cluster('FoundationCluster', { vpc });
502
993
  const router = new sst.aws.Router('FoundationRouter', {
@@ -512,16 +1003,19 @@ export function foundation() {
512
1003
  });
513
1004
  const database = new sst.aws.Aurora('FoundationDatabase', {
514
1005
  dataApi: true,
515
- engine: 'postgres',
516
- scaling: isProduction
517
- ? { min: '2 ACU', max: '16 ACU' }
518
- : isStaging
1006
+ engine: FOUNDATION_AURORA_ENGINE,
1007
+ version: FOUNDATION_AURORA_ENGINE_VERSION,
1008
+ scaling:
1009
+ productionConfiguration?.contract.aurora ??
1010
+ (isStaging
519
1011
  ? { min: '0 ACU', max: '4 ACU', pauseAfter: '5 minutes' }
520
- : { min: '0 ACU', max: '2 ACU', pauseAfter: '5 minutes' },
1012
+ : { min: '0 ACU', max: '2 ACU', pauseAfter: '5 minutes' }),
521
1013
  transform: {
522
1014
  cluster: {
523
- backupRetentionPeriod: isProduction ? 30 : 7,
524
- deletionProtection: isProduction,
1015
+ backupRetentionPeriod:
1016
+ productionConfiguration?.contract.backupRetentionDays ?? 7,
1017
+ deletionProtection:
1018
+ productionConfiguration?.contract.deletionProtection ?? false,
525
1019
  },
526
1020
  },
527
1021
  vpc,
@@ -531,9 +1025,39 @@ export function foundation() {
531
1025
  });
532
1026
  const eventBus = new sst.aws.Bus('FoundationEventBus');
533
1027
  const alertTopic = new sst.aws.SnsTopic('FoundationAlerts');
1028
+ const alertTopicPolicy = new aws.sns.TopicPolicy(
1029
+ 'FoundationAlertTopicPolicy',
1030
+ {
1031
+ arn: alertTopic.arn,
1032
+ policy: aws.iam.getPolicyDocumentOutput({
1033
+ statements: [
1034
+ {
1035
+ actions: ['sns:Publish'],
1036
+ principals: [
1037
+ { identifiers: ['events.rds.amazonaws.com'], type: 'Service' },
1038
+ ],
1039
+ resources: [alertTopic.arn],
1040
+ },
1041
+ ],
1042
+ }).json,
1043
+ },
1044
+ );
1045
+ const databaseInstanceId = database.nodes[0].identifier;
1046
+ const databaseEvents = isProduction
1047
+ ? new aws.rds.EventSubscription(
1048
+ 'FoundationDatabaseEvents',
1049
+ {
1050
+ eventCategories: ['failure', 'notification', 'serverless'],
1051
+ snsTopic: alertTopic.arn,
1052
+ sourceIds: database.nodes.map((node: any) => node.identifier),
1053
+ sourceType: 'db-instance',
1054
+ },
1055
+ { dependsOn: [alertTopicPolicy] },
1056
+ )
1057
+ : undefined;
534
1058
  const dashboard = new aws.cloudwatch.Dashboard('FoundationDashboard', {
535
1059
  dashboardName: 'ebk-' + $app.stage + '-foundation',
536
- dashboardBody: JSON.stringify({
1060
+ dashboardBody: databaseInstanceId.apply((identifier: string) => JSON.stringify({
537
1061
  widgets: [
538
1062
  {
539
1063
  type: 'text',
@@ -542,12 +1066,64 @@ export function foundation() {
542
1066
  width: 24,
543
1067
  height: 3,
544
1068
  properties: {
545
- markdown: '# Empire Builder Kit Foundation\\nStage: ' + $app.stage,
1069
+ markdown:
1070
+ '# Empire Builder Kit Foundation\\nStage: ' +
1071
+ $app.stage +
1072
+ '\\nProduction profile: ' +
1073
+ (productionConfiguration?.profile ?? 'not-applicable'),
1074
+ },
1075
+ },
1076
+ {
1077
+ type: 'metric',
1078
+ x: 0,
1079
+ y: 3,
1080
+ width: 12,
1081
+ height: 6,
1082
+ properties: {
1083
+ title: 'Aurora capacity and pause state',
1084
+ period: 60,
1085
+ stat: 'Minimum',
1086
+ metrics: [
1087
+ ['AWS/RDS', 'ServerlessDatabaseCapacity', 'DBInstanceIdentifier', identifier],
1088
+ ['.', 'ACUUtilization', '.', '.'],
1089
+ ],
1090
+ },
1091
+ },
1092
+ {
1093
+ type: 'metric',
1094
+ x: 12,
1095
+ y: 3,
1096
+ width: 12,
1097
+ height: 6,
1098
+ properties: {
1099
+ title: 'Aurora connections',
1100
+ period: 60,
1101
+ stat: 'Maximum',
1102
+ metrics: [
1103
+ ['AWS/RDS', 'DatabaseConnections', 'DBInstanceIdentifier', identifier],
1104
+ ],
546
1105
  },
547
1106
  },
548
1107
  ],
549
- }),
1108
+ })),
550
1109
  });
1110
+ const databaseCapacityAlarm = new aws.cloudwatch.MetricAlarm(
1111
+ 'FoundationDatabaseCapacityAlarm',
1112
+ {
1113
+ alarmActions: [alertTopic.arn],
1114
+ alarmDescription:
1115
+ 'Aurora Serverless capacity utilization stayed above 90 percent.',
1116
+ comparisonOperator: 'GreaterThanThreshold',
1117
+ dimensions: { DBInstanceIdentifier: databaseInstanceId },
1118
+ evaluationPeriods: 5,
1119
+ metricName: 'ACUUtilization',
1120
+ namespace: 'AWS/RDS',
1121
+ period: 60,
1122
+ statistic: 'Average',
1123
+ threshold: 90,
1124
+ treatMissingData: 'notBreaching',
1125
+ },
1126
+ );
551
1127
 
552
1128
  if (new Set(['development', 'staging', 'production']).has($app.stage)) {
553
1129
  new aws.cloudwatch.EventArchive('FoundationEventArchive', {
@@ -581,11 +1157,14 @@ export function foundation() {
581
1157
  capabilities: STANDARD_FOUNDATION_CAPABILITIES,
582
1158
  clusterId: cluster.id,
583
1159
  contractVersion: '1.0.0',
1160
+ databaseCapacityAlarmName: databaseCapacityAlarm.name,
584
1161
  dashboardName: dashboard.dashboardName,
585
1162
  databaseClusterId: database.id,
1163
+ databaseEventSubscriptionId: databaseEvents?.id,
586
1164
  eventBusName: eventBus.name,
587
1165
  identityUserPoolId: userPool.id,
588
1166
  name: 'foundation',
1167
+ productionFoundationProfile: productionConfiguration?.profile,
589
1168
  rootDomain,
590
1169
  routerUrl: router.url,
591
1170
  vpcId: vpc.id,
@@ -613,7 +1192,10 @@ export function foundation() {
613
1192
  targets: {
614
1193
  deploy: {
615
1194
  executor: '@empire-builder-kit/nx:deploy',
616
- dependsOn: ['verify'],
1195
+ dependsOn: [
1196
+ 'verify',
1197
+ { target: 'profile-preflight', params: 'forward' },
1198
+ ],
617
1199
  options: {
618
1200
  cwd: 'packages/foundation',
619
1201
  stage: 'development',
@@ -638,6 +1220,7 @@ export function foundation() {
638
1220
  },
639
1221
  refresh: {
640
1222
  executor: '@empire-builder-kit/nx:refresh',
1223
+ dependsOn: [{ target: 'profile-preflight', params: 'forward' }],
641
1224
  options: {
642
1225
  cwd: 'packages/foundation',
643
1226
  stage: 'development',
@@ -657,6 +1240,10 @@ export function foundation() {
657
1240
  executor: '@empire-builder-kit/nx:foundation-remove-preflight',
658
1241
  options: { stage: 'development' },
659
1242
  },
1243
+ 'profile-preflight': {
1244
+ executor: '@empire-builder-kit/nx:foundation-profile-preflight',
1245
+ options: { stage: 'development' },
1246
+ },
660
1247
  typecheck: {
661
1248
  executor: 'nx:run-commands',
662
1249
  options: {
@@ -709,7 +1296,11 @@ if (JSON.stringify(descriptor.capabilities) !== JSON.stringify(expectedCapabilit
709
1296
  throw new Error('Foundation capability descriptor does not match the v1 standard.');
710
1297
  }
711
1298
  if (
712
- costs.schemaVersion !== 1 ||
1299
+ costs.schemaVersion !== 2 ||
1300
+ costs.aurora?.engineVersion !== '${index_js_3.FOUNDATION_AURORA_ENGINE_VERSION}' ||
1301
+ costs.productionProfileSource !== 'ebk.repository-policy.json' ||
1302
+ costs.productionProfiles?.standard?.aurora?.min !== '2 ACU' ||
1303
+ costs.productionProfiles?.['cost-optimized']?.aurora?.min !== '0 ACU' ||
713
1304
  !costs.stages?.development ||
714
1305
  !costs.stages?.staging ||
715
1306
  costs.stages?.production?.dataRetention !== 'retained-and-protected'
@@ -792,7 +1383,69 @@ export default $config({
792
1383
  files: [],
793
1384
  references: [],
794
1385
  }),
795
- 'tools/workspace-health.mjs': `import { createHash } from 'node:crypto';
1386
+ 'tools/ebk-git-policy.mjs': `import { evaluateGitPolicy } from '@empire-builder-kit/nx/git-policy';
1387
+ import { spawnSync } from 'node:child_process';
1388
+
1389
+ const git = (...args) => {
1390
+ const result = spawnSync('git', args, { encoding: 'utf8', shell: false });
1391
+ if (result.status !== 0) {
1392
+ throw new Error('git ' + args.join(' ') + ' failed: ' + (result.stderr || result.stdout).trim());
1393
+ }
1394
+ return result.stdout.trim();
1395
+ };
1396
+ const isAncestor = (ancestor, descendant) =>
1397
+ spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
1398
+ encoding: 'utf8',
1399
+ shell: false,
1400
+ }).status === 0;
1401
+
1402
+ const eventName = process.env.GITHUB_EVENT_NAME;
1403
+ if (!['pull_request', 'push', 'workflow_dispatch'].includes(eventName)) {
1404
+ throw new Error('Unsupported or missing GITHUB_EVENT_NAME; Git policy fails closed.');
1405
+ }
1406
+
1407
+ const request = { eventName, ref: process.env.GITHUB_REF };
1408
+ if (eventName === 'pull_request' || eventName === 'push') {
1409
+ git(
1410
+ 'fetch',
1411
+ '--no-tags',
1412
+ 'origin',
1413
+ 'main:refs/remotes/origin/main',
1414
+ 'develop:refs/remotes/origin/develop',
1415
+ );
1416
+ }
1417
+ if (eventName === 'pull_request') {
1418
+ const baseRef = process.env.GITHUB_BASE_REF;
1419
+ const headRef = process.env.GITHUB_HEAD_REF;
1420
+ if (!baseRef || !headRef) {
1421
+ throw new Error('Pull-request branch metadata is missing.');
1422
+ }
1423
+ Object.assign(request, {
1424
+ baseIsAncestorOfHead: isAncestor('refs/remotes/origin/' + baseRef, 'HEAD'),
1425
+ baseRef,
1426
+ headRef,
1427
+ mainIsAncestorOfDevelop: isAncestor(
1428
+ 'refs/remotes/origin/main',
1429
+ 'refs/remotes/origin/develop',
1430
+ ),
1431
+ });
1432
+ } else if (eventName === 'push') {
1433
+ Object.assign(request, {
1434
+ baseIsAncestorOfHead: isAncestor('refs/remotes/origin/develop', 'HEAD'),
1435
+ mainIsAncestorOfDevelop: isAncestor(
1436
+ 'refs/remotes/origin/main',
1437
+ 'refs/remotes/origin/develop',
1438
+ ),
1439
+ });
1440
+ }
1441
+
1442
+ const decision = evaluateGitPolicy(request);
1443
+ process.stdout.write(JSON.stringify(decision, null, 2) + '\\n');
1444
+ if (!decision.allowed) process.exitCode = 1;
1445
+ `,
1446
+ 'tools/workspace-health.mjs': `import { validateRepositoryOperatingPolicy } from '@empire-builder-kit/nx/repository-policy';
1447
+ import { assertWorkMarkerReport, formatWorkMarkerReport, scanWorkMarkers } from '@empire-builder-kit/nx/work-markers';
1448
+ import { createHash } from 'node:crypto';
796
1449
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
797
1450
  import { join } from 'node:path';
798
1451
 
@@ -853,6 +1506,14 @@ if (
853
1506
  validateOwnership('.', workspace, 'Workspace');
854
1507
  validateOwnership('packages/foundation', foundation, 'Foundation');
855
1508
 
1509
+ const policy = validateRepositoryOperatingPolicy(readJson('ebk.repository-policy.json'));
1510
+ if (!policy.valid) {
1511
+ throw new Error(
1512
+ 'Repository Operating Policy is invalid:\\n' +
1513
+ policy.issues.map(({ message, path }) => '- ' + path + ': ' + message).join('\\n'),
1514
+ );
1515
+ }
1516
+
856
1517
  for (const name of readdirSync('packages')) {
857
1518
  if (name === 'foundation') continue;
858
1519
  const ownershipPath = join('packages', name, '.ebk/ownership.json');
@@ -863,6 +1524,9 @@ for (const name of readdirSync('packages')) {
863
1524
  }
864
1525
  validateOwnership(join('packages', name), ownership, 'Product Slice ' + name);
865
1526
  }
1527
+ const markerReport = scanWorkMarkers(process.cwd());
1528
+ process.stdout.write(formatWorkMarkerReport(markerReport) + '\\n');
1529
+ assertWorkMarkerReport(markerReport);
866
1530
  process.stdout.write('Empire Builder Kit workspace source health is green.\\n');
867
1531
  `,
868
1532
  };