@msn-control/liftoff 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +13 -1
  2. package/assets/governance/single-maintainer-gitflow/policy.md +705 -0
  3. package/assets/locks/frontend/package-lock.json +1012 -1694
  4. package/assets/locks/frontend/package.json +5 -6
  5. package/assets/locks/go-backend/go.mod +17 -0
  6. package/assets/locks/go-backend/go.sum +159 -0
  7. package/assets/locks/node-backend/package-lock.json +1473 -2320
  8. package/assets/locks/node-backend/package.json +8 -7
  9. package/assets/locks/opentofu-azure/.terraform.lock.hcl +39 -0
  10. package/assets/locks/opentofu-azure/versions.tf +10 -0
  11. package/assets/locks/python-genai/function-requirements.txt +1734 -0
  12. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/PKG-INFO +25 -0
  13. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/SOURCES.txt +6 -0
  14. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/dependency_links.txt +1 -0
  15. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/requires.txt +23 -0
  16. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/top_level.txt +1 -0
  17. package/assets/locks/python-genai/pyproject.toml +42 -0
  18. package/assets/locks/python-genai/uv.lock +2536 -0
  19. package/assets/locks/python-standard/pyproject.toml +36 -0
  20. package/assets/locks/python-standard/uv.lock +1780 -0
  21. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +4 -4
  22. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +667 -1108
  23. package/assets/supported-stack.json +711 -0
  24. package/dist/args.js +14 -0
  25. package/dist/args.js.map +1 -1
  26. package/dist/catalogs.d.ts +3 -1
  27. package/dist/catalogs.js +29 -7
  28. package/dist/catalogs.js.map +1 -1
  29. package/dist/cli.js +2 -1
  30. package/dist/cli.js.map +1 -1
  31. package/dist/commands.d.ts +6 -0
  32. package/dist/commands.js +331 -41
  33. package/dist/commands.js.map +1 -1
  34. package/dist/container-validation.d.ts +2 -0
  35. package/dist/container-validation.js +26 -0
  36. package/dist/container-validation.js.map +1 -0
  37. package/dist/file-system.js +130 -5
  38. package/dist/file-system.js.map +1 -1
  39. package/dist/framework-adapters.d.ts +1 -1
  40. package/dist/framework-adapters.js +10 -3
  41. package/dist/framework-adapters.js.map +1 -1
  42. package/dist/framework-validation.d.ts +1 -0
  43. package/dist/framework-validation.js +26 -0
  44. package/dist/framework-validation.js.map +1 -1
  45. package/dist/go-template-assets.d.ts +2 -0
  46. package/dist/go-template-assets.js +14 -0
  47. package/dist/go-template-assets.js.map +1 -0
  48. package/dist/init-filesystem.js +5 -4
  49. package/dist/init-filesystem.js.map +1 -1
  50. package/dist/interactive.d.ts +13 -0
  51. package/dist/interactive.js +25 -7
  52. package/dist/interactive.js.map +1 -1
  53. package/dist/openspec-profile.d.ts +28 -0
  54. package/dist/openspec-profile.js +172 -0
  55. package/dist/openspec-profile.js.map +1 -0
  56. package/dist/opentofu-template-assets.d.ts +2 -0
  57. package/dist/opentofu-template-assets.js +10 -0
  58. package/dist/opentofu-template-assets.js.map +1 -0
  59. package/dist/package-identity.d.ts +8 -0
  60. package/dist/package-identity.js +16 -0
  61. package/dist/package-identity.js.map +1 -0
  62. package/dist/planner.js +61 -4
  63. package/dist/planner.js.map +1 -1
  64. package/dist/power-apps-templates.js +18 -0
  65. package/dist/power-apps-templates.js.map +1 -1
  66. package/dist/project-dependencies.d.ts +1 -1
  67. package/dist/project-dependencies.js +15 -34
  68. package/dist/project-dependencies.js.map +1 -1
  69. package/dist/published-verifier.js +11 -1
  70. package/dist/published-verifier.js.map +1 -1
  71. package/dist/python-template-assets.d.ts +4 -0
  72. package/dist/python-template-assets.js +34 -0
  73. package/dist/python-template-assets.js.map +1 -0
  74. package/dist/repository-governance.d.ts +17 -0
  75. package/dist/repository-governance.js +507 -0
  76. package/dist/repository-governance.js.map +1 -0
  77. package/dist/runtime.d.ts +1 -1
  78. package/dist/runtime.js +3 -1
  79. package/dist/runtime.js.map +1 -1
  80. package/dist/self-upgrade.d.ts +77 -0
  81. package/dist/self-upgrade.js +499 -0
  82. package/dist/self-upgrade.js.map +1 -0
  83. package/dist/stable-release.d.ts +18 -0
  84. package/dist/stable-release.js +66 -0
  85. package/dist/stable-release.js.map +1 -0
  86. package/dist/standard-templates.js +28 -97
  87. package/dist/standard-templates.js.map +1 -1
  88. package/dist/supported-stack.d.ts +115 -0
  89. package/dist/supported-stack.js +439 -0
  90. package/dist/supported-stack.js.map +1 -0
  91. package/dist/telemetry/contract.d.ts +1 -1
  92. package/dist/telemetry/contract.js +1 -0
  93. package/dist/telemetry/contract.js.map +1 -1
  94. package/dist/templates.js +238 -127
  95. package/dist/templates.js.map +1 -1
  96. package/dist/types.d.ts +21 -1
  97. package/dist/workstation-catalog.js +15 -12
  98. package/dist/workstation-catalog.js.map +1 -1
  99. package/dist/workstation.js +16 -6
  100. package/dist/workstation.js.map +1 -1
  101. package/docs/cli-reference.md +72 -2
  102. package/docs/configuration-and-manifests.md +13 -5
  103. package/docs/existing-repositories.md +44 -8
  104. package/docs/getting-started.md +34 -9
  105. package/docs/prerequisites.md +47 -4
  106. package/docs/project-structure.md +27 -1
  107. package/docs/repository-governance.md +91 -0
  108. package/docs/safety-and-consent.md +50 -13
  109. package/docs/spec-workflows-and-agents.md +61 -9
  110. package/docs/supported-stack.md +89 -0
  111. package/docs/telemetry.md +6 -0
  112. package/docs/troubleshooting.md +115 -0
  113. package/docs/workloads.md +14 -5
  114. package/package.json +14 -6
package/dist/templates.js CHANGED
@@ -1,10 +1,16 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { addGenAiExtensionArtifacts } from './genai-templates.js';
3
+ import { assertImmutableGeneratedContainerReferences } from './container-validation.js';
3
4
  import { addPowerAppsCodeAppArtifacts } from './power-apps-templates.js';
4
5
  import { addStandardStackArtifacts, renderStandardDockerfile, renderStandardEnv } from './standard-templates.js';
5
6
  import { liftoffVersion } from './version.js';
6
7
  import { renderNpmLock, renderNpmPackage } from './npm-template-assets.js';
8
+ import { renderOpenTofuProviderLock, renderOpenTofuVersions } from './opentofu-template-assets.js';
7
9
  import { formatCommand } from './process-runner.js';
10
+ import { formatContainerImage, supportedStack } from './supported-stack.js';
11
+ import { buildRepositoryGovernanceArtifacts, governancePolicyVersion } from './repository-governance.js';
12
+ import { OPEN_SPEC_DELIVERY, OPEN_SPEC_PROFILE, OPEN_SPEC_WORKFLOW_IDS } from './openspec-profile.js';
13
+ import { renderFunctionRequirementsAsset, renderPythonLock, renderPythonPyprojectAsset } from './python-template-assets.js';
8
14
  import { workstationRequirementCatalog } from './workstation-catalog.js';
9
15
  const contentHash = (content) => `sha256:${createHash('sha256').update(content, 'utf8').digest('hex')}`;
10
16
  const DEFAULT_FUNCTION_WORKER_QUEUE_NAME = 'events';
@@ -77,10 +83,14 @@ export function buildArtifacts(plan) {
77
83
  addPowerAppsWorkloadArtifacts(add, plan);
78
84
  break;
79
85
  }
80
- addGovernanceArtifacts(add, plan);
86
+ addSpecWorkflowArtifacts(add, plan);
87
+ for (const artifact of buildRepositoryGovernanceArtifacts(plan)) {
88
+ add(artifact.logicalName, artifact.category, artifact.pathParts, artifact.content);
89
+ }
81
90
  if (plan.workload !== 'power-apps-code-app' && plan.includeFrontend) {
82
91
  addFrontendArtifacts(add, plan);
83
92
  }
93
+ assertImmutableGeneratedContainerReferences(artifacts);
84
94
  const manifest = buildManifest(plan, artifacts);
85
95
  artifacts.push({
86
96
  logicalName: 'manifest',
@@ -154,7 +164,7 @@ export function buildManifest(plan, artifacts, options = {}) {
154
164
  environments: plan.environments.map((environment) => environment.id)
155
165
  };
156
166
  return {
157
- artifactVersion: 4,
167
+ artifactVersion: 5,
158
168
  generatedBy: 'Mission Control Liftoff',
159
169
  liftoffVersion,
160
170
  project: {
@@ -169,6 +179,16 @@ export function buildManifest(plan, artifacts, options = {}) {
169
179
  adapter: plan.framework.id,
170
180
  ...(frameworkState === 'initialized' ? { contractVersion: plan.framework.version } : {})
171
181
  },
182
+ governance: plan.governanceProfile.id === 'none'
183
+ ? {
184
+ profile: 'none',
185
+ state: 'disabled'
186
+ }
187
+ : {
188
+ profile: plan.governanceProfile.id,
189
+ policyVersion: governancePolicyVersion,
190
+ state: 'handoff-generated'
191
+ },
172
192
  artifacts: artifacts
173
193
  .filter((artifact) => artifact.category !== 'seed' && artifact.category !== 'framework')
174
194
  .map((artifact) => ({
@@ -193,7 +213,8 @@ function addBaseArtifacts(add, plan) {
193
213
  environments: plan.environments.map((environment) => environment.id),
194
214
  specWorkflow: plan.specWorkflow.id,
195
215
  agents: plan.agents.map((agent) => agent.id),
196
- ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {})
216
+ ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
217
+ governanceProfile: plan.governanceProfile.id
197
218
  }, null, 2));
198
219
  add('env-example', 'configuration', ['.env.example'], renderEnvExample(plan));
199
220
  add('backend-dockerfile', 'runtime', ['Dockerfile'], plan.workload === 'genai' ? renderBackendDockerfile() : renderStandardDockerfile(plan));
@@ -205,12 +226,14 @@ function addPowerAppsProjectConfig(add, plan) {
205
226
  specWorkflow: plan.specWorkflow.id,
206
227
  agents: plan.agents.map((agent) => agent.id),
207
228
  ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
208
- codeAppsPlugin: plan.codeAppsPlugin
229
+ codeAppsPlugin: plan.codeAppsPlugin,
230
+ governanceProfile: plan.governanceProfile.id
209
231
  }, null, 2));
210
232
  }
211
233
  function addBackendArtifacts(add, plan) {
212
234
  const routeModule = pyModule(genAiPattern(plan).id);
213
235
  add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderBackendPyproject(plan));
236
+ add('backend-uv-lock', 'backend', ['backend', 'uv.lock'], renderPythonLock('genai', `${plan.safeProjectName}-backend`));
214
237
  add('backend-package', 'backend', ['backend', '__init__.py'], '');
215
238
  add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
216
239
  add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderFastApiMain(plan, routeModule));
@@ -285,7 +308,8 @@ function addDockerArtifacts(add, plan) {
285
308
  }
286
309
  function addInfrastructureArtifacts(add, plan) {
287
310
  const base = ['infrastructure', 'opentofu', 'azure'];
288
- add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderTofuVersions());
311
+ add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderOpenTofuVersions());
312
+ add('opentofu-provider-lock', 'infrastructure', [...base, '.terraform.lock.hcl'], renderOpenTofuProviderLock());
289
313
  add('opentofu-providers', 'infrastructure', [...base, 'providers.tf'], renderTofuProviders());
290
314
  add('opentofu-variables', 'infrastructure', [...base, 'variables.tf'], renderTofuVariables(plan));
291
315
  add('opentofu-main', 'infrastructure', [...base, 'main.tf'], renderTofuMain(plan));
@@ -297,7 +321,7 @@ function addInfrastructureArtifacts(add, plan) {
297
321
  add(`opentofu-${environment.id}-tfvars`, 'infrastructure', [...base, 'environments', `${environment.id}.tfvars`], renderTofuTfvars(plan, environment.id));
298
322
  }
299
323
  }
300
- function addGovernanceArtifacts(add, plan) {
324
+ function addSpecWorkflowArtifacts(add, plan) {
301
325
  if (plan.workload === 'power-apps-code-app') {
302
326
  if (plan.specWorkflow.id === 'openspec') {
303
327
  const changeName = `bootstrap-${plan.safeProjectName}`;
@@ -333,7 +357,7 @@ function addGovernanceArtifacts(add, plan) {
333
357
  }
334
358
  }
335
359
  function renderPowerAppsOpenSpecConfig(plan) {
336
- return `schema: spec-driven
360
+ return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
337
361
  context: |
338
362
  Project: ${plan.projectName}
339
363
  Workload: Microsoft Power Apps code app
@@ -351,6 +375,14 @@ rules:
351
375
  - Include npm lint and build verification.
352
376
  `;
353
377
  }
378
+ function renderOpenSpecCopilotConfig(plan) {
379
+ return plan.agents.some((agent) => agent.id === 'github-copilot')
380
+ ? `
381
+ githubCopilot:
382
+ cloudAgent: ${plan.copilotCloud}
383
+ `
384
+ : '\n';
385
+ }
354
386
  function renderPowerAppsSeedProposal(plan) {
355
387
  return `# Proposal: bootstrap-${plan.safeProjectName}
356
388
 
@@ -425,10 +457,11 @@ function addFrontendArtifacts(add, plan) {
425
457
  function renderDirectBuildAndTestGuide(plan) {
426
458
  let backendCommands;
427
459
  if (plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi') {
428
- backendCommands = `# macOS and Linux
429
- python3 -m venv .venv
430
- .venv/bin/python -m pip install -e "./backend[test]"
431
- (cd backend && python -m pytest -q)`;
460
+ const extras = plan.workload === 'genai' && hasFunctionWorker(plan)
461
+ ? ' --extra functions'
462
+ : '';
463
+ backendCommands = `uv sync --frozen --project backend --extra test${extras}
464
+ uv run --project backend python -m pytest -q backend/tests`;
432
465
  }
433
466
  else if (plan.apiStack.id === 'node-fastify') {
434
467
  backendCommands = `cd backend
@@ -452,24 +485,12 @@ npm ci
452
485
  npm run build
453
486
  \`\`\`
454
487
  ` : '';
455
- const windowsPythonCommands = plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi'
456
- ? `
457
-
458
- Equivalent Windows dependency commands:
459
-
460
- \`\`\`powershell
461
- py -3 -m venv .venv
462
- .venv\\Scripts\\python.exe -m pip install -e "./backend[test]"
463
- \`\`\`
464
- `
465
- : '';
466
488
  const functionCommands = plan.workload === 'genai' && hasFunctionWorker(plan) ? `
467
489
 
468
- Run the Function worker unit tests from the same Python virtual environment:
490
+ Run the Function worker unit tests from the same locked Python environment:
469
491
 
470
492
  \`\`\`bash
471
- .venv/bin/python -m pip install -r functions/${functionWorkerName(plan)}/requirements.txt
472
- (cd functions/${functionWorkerName(plan)} && ../../.venv/bin/python -m pytest -q)
493
+ (cd functions/${functionWorkerName(plan)} && uv run --project ../../backend --directory . python -m pytest -q)
473
494
  \`\`\`
474
495
  ` : '';
475
496
  return `## Project Dependencies, Build, And Test
@@ -480,7 +501,7 @@ Run the Function worker unit tests from the same Python virtual environment:
480
501
  ${backendCommands}
481
502
  \`\`\`
482
503
 
483
- ${windowsPythonCommands}${frontendCommands}${functionCommands}`;
504
+ ${frontendCommands}${functionCommands}`;
484
505
  }
485
506
  function renderAdvisoryReadinessGuide(plan) {
486
507
  const selected = [
@@ -508,12 +529,21 @@ function renderAdvisoryReadinessGuide(plan) {
508
529
  function renderSpecWorkflowGuide(plan) {
509
530
  const agents = plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default integration)' : ''}`).join(', ');
510
531
  const ownership = plan.specWorkflow.id === 'openspec'
511
- ? 'OpenSpec core files and the selected Copilot or Claude skill integrations'
532
+ ? 'OpenSpec workflow skills, commands, configuration, and optional cloud-agent files'
512
533
  : 'Spec Kit core files, integration state, and the selected Copilot or Claude skill integrations';
534
+ const openSpecDetails = plan.specWorkflow.id === 'openspec'
535
+ ? [
536
+ `- OpenSpec profile: ${OPEN_SPEC_PROFILE}; delivery: ${OPEN_SPEC_DELIVERY}; workflows: ${OPEN_SPEC_WORKFLOW_IDS.join(', ')}`,
537
+ ...(plan.agents.some((agent) => agent.id === 'github-copilot')
538
+ ? ['- GitHub Copilot cloud agent: controlled by `githubCopilot.cloudAgent` in `openspec/config.yaml`']
539
+ : [])
540
+ ].join('\n')
541
+ : '';
513
542
  return `## Spec-Driven Workflow And Validation
514
543
 
515
544
  - Workflow: ${plan.specWorkflow.label} ${plan.framework.version}
516
545
  - AI coding agents: ${agents}
546
+ ${openSpecDetails ? `${openSpecDetails}\n` : ''}
517
547
  - Framework ownership: the official initializer owns ${ownership}. Liftoff validates these files but excludes framework-owned output and one-time seed content from durable manifest hashes.
518
548
  - Deferred tools: advisory workstation checks may be deferred. Liftoff never claims they are installed and never installs them without \`--install-tools\`.
519
549
 
@@ -527,6 +557,8 @@ Validate the scaffold and workstation after setup:
527
557
  liftoff validate
528
558
  liftoff doctor
529
559
  \`\`\`
560
+
561
+ For an existing OpenSpec project, change workflow delivery with \`openspec config profile\` and refresh framework-owned files with \`openspec update\`. Plain \`liftoff update\` does not regenerate OpenSpec integrations.
530
562
  `;
531
563
  }
532
564
  function renderGeneratedConfigurationGuide(plan) {
@@ -553,6 +585,10 @@ Copy \`.env.example\` to \`.env\`, then configure only the integrations you use:
553
585
  function renderGeneratedUpdateGuide() {
554
586
  return `## Safe Liftoff Updates
555
587
 
588
+ \`liftoff upgrade\` replaces a supported global Liftoff CLI installation; it does not inspect or modify this project. Check and apply CLI replacement separately with \`liftoff upgrade --check\` and \`liftoff upgrade\`.
589
+
590
+ \`single-maintainer-gitflow\` repository governance generates a local handoff only. Review \`.liftoff/governance/README.md\`, commit and push the project, then run a selected-agent launcher for read-only Phase 0. Live enforcement requires a separately approved governance change.
591
+
556
592
  \`liftoff update\` immediately applies safe managed changes without prompting in interactive, redirected, and automated environments. Use \`liftoff update --check\` for a read-only human drift report, or \`liftoff update --check --json\` as an automation gate that exits 0 when clean and 2 when drift exists.
557
593
 
558
594
  Local or user-owned conflicts are skipped by default. Review the reported paths, commit or copy local work, and use \`liftoff update --force\` only when every listed overwrite is intended. An occupied destination with different user bytes remains preserved without force, while an identical destination is adopted without rewriting it. Update never deletes orphans or installs dependencies. A failed transaction is rolled back, but Liftoff retains no backup after a successful overwrite.
@@ -694,16 +730,26 @@ LANGFUSE_SECRET_KEY=
694
730
  `;
695
731
  }
696
732
  function renderBackendDockerfile() {
697
- return `FROM python:3.12-slim
733
+ return `FROM ${formatContainerImage(supportedStack.containers['uv-tool'])} AS uv
734
+ FROM ${formatContainerImage(supportedStack.containers['python-runtime'])}
698
735
 
699
736
  WORKDIR /app
700
737
 
701
738
  ENV PYTHONDONTWRITEBYTECODE=1
702
739
  ENV PYTHONUNBUFFERED=1
703
740
  ENV PYTHONPATH=/app
741
+ ENV PATH=/app/backend/.venv/bin:$PATH
742
+ COPY --from=uv /uv /uvx /bin/
704
743
 
744
+ ARG UV_DEFAULT_INDEX=https://pypi.org/simple
705
745
  COPY backend/pyproject.toml /app/backend/pyproject.toml
706
- RUN pip install --no-cache-dir /app/backend
746
+ COPY backend/uv.lock /app/backend/uv.lock
747
+ RUN --mount=type=cache,target=/root/.cache/uv \\
748
+ uv export --frozen --no-dev --no-emit-project --project /app/backend --output-file /tmp/requirements.txt \\
749
+ && uv venv /app/backend/.venv \\
750
+ && uv pip install --python /app/backend/.venv/bin/python --require-hashes \\
751
+ --default-index "$UV_DEFAULT_INDEX" --requirements /tmp/requirements.txt \\
752
+ && rm /tmp/requirements.txt
707
753
 
708
754
  COPY backend /app/backend
709
755
  COPY database /app/database
@@ -713,43 +759,7 @@ CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
713
759
  `;
714
760
  }
715
761
  function renderBackendPyproject(plan) {
716
- return `[project]
717
- name = "${plan.safeProjectName}-backend"
718
- version = "0.1.0"
719
- requires-python = ">=3.12"
720
- dependencies = [
721
- "fastapi>=0.111",
722
- "uvicorn[standard]>=0.30",
723
- "pydantic>=2.7",
724
- "pydantic-settings>=2.3",
725
- "pydantic-ai-slim[openai]==1.107.1",
726
- "scalar-fastapi>=1.0",
727
- "sqlalchemy[asyncio]>=2.0",
728
- "asyncpg>=0.29",
729
- "psycopg[binary]>=3.2",
730
- "alembic>=1.13",
731
- "redis>=5.0",
732
- "langfuse==2.60.10",
733
- "azure-servicebus>=7.12",
734
- "azure-identity>=1.17",
735
- "azure-storage-blob>=12.20",
736
- "azure-communication-email>=1.0"
737
- ]
738
-
739
- [project.optional-dependencies]
740
- test = ["pytest>=8.2", "httpx>=0.27"]
741
-
742
- [build-system]
743
- requires = ["setuptools>=70"]
744
- build-backend = "setuptools.build_meta"
745
-
746
- [tool.setuptools]
747
- packages = []
748
-
749
- [tool.pytest.ini_options]
750
- pythonpath = [".."]
751
- testpaths = ["tests"]
752
- `;
762
+ return renderPythonPyprojectAsset('genai', `${plan.safeProjectName}-backend`);
753
763
  }
754
764
  function renderFastApiMain(plan, routeModule) {
755
765
  return `from fastapi import FastAPI
@@ -1136,8 +1146,12 @@ class LangfuseTracer:
1136
1146
 
1137
1147
  @asynccontextmanager
1138
1148
  async def trace(self, name: str, input_data: Any = None):
1139
- remote_trace = self._client.trace(name=name, input=input_data)
1140
- remote_id = getattr(remote_trace, "id", None)
1149
+ remote_trace = self._client.start_observation(
1150
+ name=name,
1151
+ as_type="span",
1152
+ input=input_data,
1153
+ )
1154
+ remote_id = getattr(remote_trace, "trace_id", None)
1141
1155
  handle = TraceHandle(
1142
1156
  enabled=True,
1143
1157
  trace_id=str(remote_id) if remote_id is not None else None,
@@ -1149,6 +1163,8 @@ class LangfuseTracer:
1149
1163
  raise
1150
1164
  else:
1151
1165
  remote_trace.update(output=handle.output)
1166
+ finally:
1167
+ remote_trace.end()
1152
1168
 
1153
1169
 
1154
1170
  def build_tracer(
@@ -1293,21 +1309,25 @@ from backend.observability.tracing import (
1293
1309
 
1294
1310
 
1295
1311
  class FakeRemoteTrace:
1296
- id = "trace-123"
1312
+ trace_id = "trace-123"
1297
1313
 
1298
1314
  def __init__(self):
1299
1315
  self.updates = []
1316
+ self.ended = False
1300
1317
 
1301
1318
  def update(self, **values):
1302
1319
  self.updates.append(values)
1303
1320
 
1321
+ def end(self):
1322
+ self.ended = True
1323
+
1304
1324
 
1305
1325
  class FakeLangfuse:
1306
1326
  def __init__(self):
1307
1327
  self.calls = []
1308
1328
  self.remote_trace = FakeRemoteTrace()
1309
1329
 
1310
- def trace(self, **values):
1330
+ def start_observation(self, **values):
1311
1331
  self.calls.append(values)
1312
1332
  return self.remote_trace
1313
1333
 
@@ -1337,8 +1357,11 @@ def test_configured_tracing_updates_langfuse_operation():
1337
1357
  trace.set_output({"answer": "world"})
1338
1358
 
1339
1359
  asyncio.run(scenario())
1340
- assert client.calls == [{"name": "agent.run", "input": {"prompt": "hello"}}]
1360
+ assert client.calls == [
1361
+ {"name": "agent.run", "as_type": "span", "input": {"prompt": "hello"}}
1362
+ ]
1341
1363
  assert client.remote_trace.updates == [{"output": {"answer": "world"}}]
1364
+ assert client.remote_trace.ended is True
1342
1365
 
1343
1366
 
1344
1367
  def test_partial_langfuse_configuration_fails(monkeypatch):
@@ -1712,15 +1735,11 @@ Deployed triggers use \`ServiceBusConnection__fullyQualifiedNamespace\` and \`Se
1712
1735
  ## Local Development
1713
1736
 
1714
1737
  \`\`\`bash
1715
- python -m venv .venv
1716
- source .venv/bin/activate
1717
- pip install -r requirements.txt
1738
+ uv sync --frozen --project ../../backend --extra test --extra functions
1718
1739
  cp local.settings.example.json local.settings.json
1719
- python -m pytest -q
1740
+ uv run --project ../../backend --directory . python -m pytest -q
1720
1741
  func start
1721
1742
  \`\`\`
1722
-
1723
- On Windows, activate the virtual environment with \`.venv\\Scripts\\activate\`.
1724
1743
  `;
1725
1744
  }
1726
1745
  function renderFunctionHostJson() {
@@ -1747,9 +1766,7 @@ function renderFunctionLocalSettings(plan) {
1747
1766
  }, null, 2);
1748
1767
  }
1749
1768
  function renderFunctionRequirements() {
1750
- return `azure-functions>=1.21.0
1751
- pytest>=8.2
1752
- `;
1769
+ return renderFunctionRequirementsAsset();
1753
1770
  }
1754
1771
  function renderFunctionApp(plan) {
1755
1772
  const pattern = genAiPattern(plan);
@@ -1861,7 +1878,9 @@ function renderDockerCompose(plan) {
1861
1878
  depends_on:
1862
1879
  - backend
1863
1880
  ` : '';
1864
- const postgresImage = plan.workload === 'genai' ? 'pgvector/pgvector:pg16' : 'postgres:16-alpine';
1881
+ const postgresImage = plan.workload === 'genai'
1882
+ ? formatContainerImage(supportedStack.containers.pgvector)
1883
+ : formatContainerImage(supportedStack.containers.postgres);
1865
1884
  return `services:
1866
1885
  backend:
1867
1886
  build:
@@ -1888,46 +1907,136 @@ ${frontendService}
1888
1907
  POSTGRES_DB: ${plan.safeProjectName.replace(/-/g, '_')}
1889
1908
  ports:
1890
1909
  - "5432:5432"
1910
+ healthcheck:
1911
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
1912
+ interval: 3s
1913
+ timeout: 3s
1914
+ retries: 10
1891
1915
 
1892
1916
  redis:
1893
- image: redis:7-alpine
1917
+ image: ${formatContainerImage(supportedStack.containers.redis)}
1894
1918
  ports:
1895
1919
  - "6379:6379"
1896
1920
 
1897
1921
  azurite:
1898
- image: mcr.microsoft.com/azure-storage/azurite
1922
+ image: ${formatContainerImage(supportedStack.containers.azurite)}
1899
1923
  command: azurite --blobHost 0.0.0.0
1900
1924
  ports:
1901
1925
  - "10000:10000"
1902
1926
 
1903
1927
  mailpit:
1904
- image: axllent/mailpit:latest
1928
+ image: ${formatContainerImage(supportedStack.containers.mailpit)}
1905
1929
  ports:
1906
1930
  - "8025:8025"
1907
1931
 
1908
- ${plan.workload === 'genai' ? ` langfuse:
1909
- image: langfuse/langfuse:2
1932
+ ${plan.workload === 'genai' ? ` langfuse-worker:
1933
+ image: ${formatContainerImage(supportedStack.containers['langfuse-worker'])}
1910
1934
  profiles:
1911
1935
  - observability
1912
- environment:
1936
+ depends_on: &langfuse-dependencies
1937
+ postgres:
1938
+ condition: service_healthy
1939
+ langfuse-redis:
1940
+ condition: service_healthy
1941
+ clickhouse:
1942
+ condition: service_healthy
1943
+ minio:
1944
+ condition: service_healthy
1945
+ environment: &langfuse-environment
1913
1946
  DATABASE_URL: postgresql://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
1914
- NEXTAUTH_SECRET: local-development-placeholder
1915
- SALT: local-development-placeholder
1947
+ NEXTAUTH_URL: http://localhost:3000
1948
+ SALT: local-development-salt
1949
+ ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000
1950
+ TELEMETRY_ENABLED: "false"
1951
+ CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
1952
+ CLICKHOUSE_URL: http://clickhouse:8123
1953
+ CLICKHOUSE_USER: clickhouse
1954
+ CLICKHOUSE_PASSWORD: clickhouse
1955
+ REDIS_HOST: langfuse-redis
1956
+ REDIS_PORT: 6379
1957
+ REDIS_AUTH: langfuse-redis
1958
+ LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
1959
+ LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
1960
+ LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
1961
+ LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: miniosecret
1962
+ LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
1963
+ LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
1964
+ LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
1965
+ LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
1966
+ LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
1967
+ LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: miniosecret
1968
+ LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000
1969
+ LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
1970
+
1971
+ langfuse:
1972
+ image: ${formatContainerImage(supportedStack.containers['langfuse-web'])}
1973
+ profiles:
1974
+ - observability
1975
+ depends_on: *langfuse-dependencies
1976
+ environment:
1977
+ <<: *langfuse-environment
1978
+ NEXTAUTH_SECRET: local-development-secret
1979
+ LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://localhost:9090
1916
1980
  ports:
1917
1981
  - "3000:3000"
1982
+
1983
+ clickhouse:
1984
+ image: ${formatContainerImage(supportedStack.containers.clickhouse)}
1985
+ profiles:
1986
+ - observability
1987
+ user: "101:101"
1988
+ environment:
1989
+ CLICKHOUSE_DB: default
1990
+ CLICKHOUSE_USER: clickhouse
1991
+ CLICKHOUSE_PASSWORD: clickhouse
1992
+ volumes:
1993
+ - langfuse-clickhouse-data:/var/lib/clickhouse
1994
+ - langfuse-clickhouse-logs:/var/log/clickhouse-server
1995
+ healthcheck:
1996
+ test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
1997
+ interval: 5s
1998
+ timeout: 5s
1999
+ retries: 10
2000
+
2001
+ minio:
2002
+ image: ${formatContainerImage(supportedStack.containers.minio)}
2003
+ profiles:
2004
+ - observability
2005
+ entrypoint: sh
2006
+ command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
2007
+ environment:
2008
+ MINIO_ROOT_USER: minio
2009
+ MINIO_ROOT_PASSWORD: miniosecret
2010
+ ports:
2011
+ - "9090:9000"
2012
+ - "127.0.0.1:9091:9001"
2013
+ volumes:
2014
+ - langfuse-minio-data:/data
2015
+ healthcheck:
2016
+ test: ["CMD", "mc", "ready", "local"]
2017
+ interval: 1s
2018
+ timeout: 5s
2019
+ retries: 5
2020
+
2021
+ langfuse-redis:
2022
+ image: ${formatContainerImage(supportedStack.containers.redis)}
2023
+ profiles:
2024
+ - observability
2025
+ command: ["redis-server", "--requirepass", "langfuse-redis", "--maxmemory-policy", "noeviction"]
2026
+ volumes:
2027
+ - langfuse-redis-data:/data
2028
+ healthcheck:
2029
+ test: ["CMD", "redis-cli", "-a", "langfuse-redis", "ping"]
2030
+ interval: 3s
2031
+ timeout: 10s
2032
+ retries: 10
2033
+ ` : ''}
2034
+ ${plan.workload === 'genai' ? `volumes:
2035
+ langfuse-clickhouse-data:
2036
+ langfuse-clickhouse-logs:
2037
+ langfuse-minio-data:
2038
+ langfuse-redis-data:
1918
2039
  ` : ''}
1919
- `;
1920
- }
1921
- function renderTofuVersions() {
1922
- return `terraform {
1923
- required_version = ">= 1.6.0"
1924
- required_providers {
1925
- azurerm = {
1926
- source = "hashicorp/azurerm"
1927
- version = "~> 3.110"
1928
- }
1929
- }
1930
- }
1931
2040
  `;
1932
2041
  }
1933
2042
  function renderTofuProviders() {
@@ -1940,7 +2049,7 @@ function renderTofuVariables(plan) {
1940
2049
  const frontendVariables = plan.includeFrontend ? `
1941
2050
  variable "frontend_image" {
1942
2051
  type = string
1943
- default = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
2052
+ default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
1944
2053
  description = "Frontend image. Replace the bootstrap image with the generated frontend image after pushing it to ACR."
1945
2054
  }
1946
2055
  ` : '';
@@ -1953,7 +2062,7 @@ variable "function_worker_queue_name" {
1953
2062
 
1954
2063
  variable "functions_python_version" {
1955
2064
  type = string
1956
- default = "3.12"
2065
+ default = "${supportedStack.runtimes.python.releaseLine}"
1957
2066
  description = "Python runtime version for the generated Azure Functions worker."
1958
2067
  }
1959
2068
  ` : '';
@@ -1980,7 +2089,7 @@ variable "resource_suffix" {
1980
2089
 
1981
2090
  variable "backend_image" {
1982
2091
  type = string
1983
- default = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
2092
+ default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
1984
2093
  description = "Backend image. Replace the bootstrap image with the generated backend image after pushing it to ACR."
1985
2094
  }
1986
2095
 
@@ -2281,7 +2390,7 @@ resource "azurerm_storage_account" "main" {
2281
2390
 
2282
2391
  resource "azurerm_storage_container" "documents" {
2283
2392
  name = "documents"
2284
- storage_account_name = azurerm_storage_account.main.name
2393
+ storage_account_id = azurerm_storage_account.main.id
2285
2394
  container_access_type = "private"
2286
2395
  }
2287
2396
 
@@ -2305,11 +2414,12 @@ resource "azurerm_communication_service" "main" {
2305
2414
  }
2306
2415
 
2307
2416
  resource "azurerm_key_vault" "main" {
2308
- name = "${names.keyVault}"
2309
- location = azurerm_resource_group.main.location
2310
- resource_group_name = azurerm_resource_group.main.name
2311
- tenant_id = data.azurerm_client_config.current.tenant_id
2312
- sku_name = "standard"
2417
+ name = "${names.keyVault}"
2418
+ location = azurerm_resource_group.main.location
2419
+ resource_group_name = azurerm_resource_group.main.name
2420
+ tenant_id = data.azurerm_client_config.current.tenant_id
2421
+ sku_name = "standard"
2422
+ rbac_authorization_enabled = true
2313
2423
  }
2314
2424
 
2315
2425
  data "azurerm_client_config" "current" {}
@@ -2383,17 +2493,18 @@ Build the generated backend in ACR, then replace the bootstrap image:
2383
2493
 
2384
2494
  \`\`\`bash
2385
2495
  ACR_NAME="$(tofu output -raw container_registry_name)"
2386
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-backend:latest ../../..
2496
+ SOURCE_SHA="$(git rev-parse HEAD)"
2497
+ az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-backend:"$SOURCE_SHA" ../../..
2387
2498
  ${plan.includeFrontend ? `BACKEND_URL="https://$(tofu output -raw backend_url)"
2388
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:latest --build-arg VITE_API_BASE_URL="$BACKEND_URL" ../../../frontend
2499
+ az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:"$SOURCE_SHA" --build-arg VITE_API_BASE_URL="$BACKEND_URL" ../../../frontend
2389
2500
  ` : ''}\`\`\`
2390
2501
 
2391
2502
  Persist the deployed images in \`environments/${env}.tfvars\` so future applies do not restore the bootstrap image:
2392
2503
 
2393
2504
  \`\`\`hcl
2394
- backend_image = "<login-server>/${plan.safeProjectName}-backend:latest"
2505
+ backend_image = "<login-server>/${plan.safeProjectName}-backend@sha256:<manifest-digest>"
2395
2506
  backend_target_port = 8000
2396
- ${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend:latest"
2507
+ ${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend@sha256:<manifest-digest>"
2397
2508
  ` : ''}\`\`\`
2398
2509
 
2399
2510
  \`\`\`bash
@@ -2414,15 +2525,15 @@ function renderTofuTfvars(plan, environment) {
2414
2525
  ['environment', JSON.stringify(environment)],
2415
2526
  ['location', JSON.stringify(plan.region.slug)],
2416
2527
  ['resource_suffix', JSON.stringify(stableResourceSuffix(plan, environment))],
2417
- ['backend_image', JSON.stringify('mcr.microsoft.com/azuredocs/containerapps-helloworld:latest')],
2528
+ ['backend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))],
2418
2529
  ['backend_target_port', '80'],
2419
2530
  ['enable_private_networking', 'false']
2420
2531
  ];
2421
2532
  if (plan.includeFrontend) {
2422
- values.push(['frontend_image', JSON.stringify('mcr.microsoft.com/azuredocs/containerapps-helloworld:latest')]);
2533
+ values.push(['frontend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))]);
2423
2534
  }
2424
2535
  if (hasFunctionWorker(plan)) {
2425
- values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify('3.12')]);
2536
+ values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify(supportedStack.runtimes.python.releaseLine)]);
2426
2537
  }
2427
2538
  const width = Math.max(...values.map(([key]) => key.length));
2428
2539
  return values.map(([key, value]) => `${key.padEnd(width)} = ${value}`).join('\n');
@@ -2435,7 +2546,7 @@ function renderOpenSpecConfig(plan) {
2435
2546
  : plan.apiStack.id === 'node-fastify'
2436
2547
  ? 'Keep backend API code under backend/src.'
2437
2548
  : 'Keep the Go entrypoint under backend/cmd/api and reusable code under backend/internal.';
2438
- return `schema: spec-driven
2549
+ return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
2439
2550
 
2440
2551
  context: |
2441
2552
  Project generated by Mission Control Liftoff.
@@ -2471,7 +2582,7 @@ rules:
2471
2582
  const functionsRule = hasFunctionWorker(plan) ? `
2472
2583
  - Keep Azure Functions trigger adapters under functions/${functionWorkerName(plan)}.
2473
2584
  - Keep reusable GenAI orchestration under backend/orchestration.` : '';
2474
- return `schema: spec-driven
2585
+ return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
2475
2586
 
2476
2587
  context: |
2477
2588
  Project generated by Mission Control Liftoff.
@@ -2804,16 +2915,15 @@ async function submit(): Promise<void> {
2804
2915
  `;
2805
2916
  }
2806
2917
  function renderFrontendStyles() {
2807
- return `@tailwind base;
2808
- @tailwind components;
2809
- @tailwind utilities;
2918
+ return `@import "tailwindcss";
2810
2919
  `;
2811
2920
  }
2812
2921
  function renderFrontendViteConfig() {
2813
2922
  return `import { defineConfig } from 'vite';
2814
- import vue from '@vitejs/plugin-vue';
2923
+ import tailwindcss from '@tailwindcss/vite';
2924
+ import vue from '@vitejs/plugin-vue';
2815
2925
 
2816
- export default defineConfig({ plugins: [vue()] });
2926
+ export default defineConfig({ plugins: [vue(), tailwindcss()] });
2817
2927
  `;
2818
2928
  }
2819
2929
  function renderFrontendTailwindConfig() {
@@ -2827,16 +2937,17 @@ export default {
2827
2937
  `;
2828
2938
  }
2829
2939
  function renderFrontendDockerfile() {
2830
- return `FROM node:20-alpine AS build
2940
+ return `FROM ${formatContainerImage(supportedStack.containers['node-runtime'])} AS build
2831
2941
  WORKDIR /app
2942
+ ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
2832
2943
  COPY package.json package-lock.json* ./
2833
- RUN npm install
2944
+ RUN npm ci --ignore-scripts --no-audit --no-fund --registry="$NPM_CONFIG_REGISTRY"
2834
2945
  COPY . .
2835
2946
  ARG VITE_API_BASE_URL=http://localhost:8000
2836
2947
  ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
2837
2948
  RUN npm run build
2838
2949
 
2839
- FROM nginx:1.27-alpine
2950
+ FROM ${formatContainerImage(supportedStack.containers['nginx-runtime'])}
2840
2951
  COPY --from=build /app/dist /usr/share/nginx/html
2841
2952
  `;
2842
2953
  }