Skip to content

Prism 0.6.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 12:34
· 7 commits to main since this release

Release date: 2026-10-08

Prism 0.6.0 generates workspaces from an app list, with templates as stack packs. One copier.yml and one template tag cover two layers: the workspace layer (the knowledge base, the guidance and shared/) and one app layer per app, from five packs: a Spring Boot backend, Next.js web apps, Android apps, iOS apps and a Python agent service. Every app is one tested slice with a fail-closed local development identity. The questionnaire is minimal: the project identity and the app list, with ports, packages and workflow names derived. Every skill, command and Cursor rule has one source, template-skills/, rendered into the layers of each tool. A committed golden workspace is built and tested in CI with every version pinned in packs/versions.yml, and automated dependency updates keep the pins current. The release also carries the security hardening from the independent reviews (confined paths and answers files, checked update layers, commit-bound releases, a fail-closed audit gate). Workspaces generated by earlier versions must be recreated.

Added

  • Two-layer generation and five stack packs. The hidden question prism_layer chooses the workspace layer (template/) or one app's pack (packs/<stack>/). Copier applies a pack to the repository root with every path under {{ app_path }}/, plus the app's workflow .github/workflows/<id>.yml and Cursor rule .cursor/rules/<id>.mdc. Each app keeps its own answers in <path>/.copier-answers.yml, so one template applies several times to one project. The packs are:
    • spring-backend: a Spring Boot 4 app with the users entity and its Flyway migration, GET /api/me, the shared error model, an OAuth2 resource-server configuration and the dev identity, with unit tests and integration tests against a PostgreSQL started by Testcontainers (./gradlew test needs Docker and no credentials). task db-up and task db-down start and stop the workspace's PostgreSQL and task <app-id>:dev runs the app with the local profile. A README, docs/entities/, a Dockerfile whose healthcheck needs no tool of the image, and its own schema in the shared development database (DATABASE_SCHEMA), so several backends share it.
    • nextjs-web: a Next.js (App Router, TypeScript) app per audience with a "Local development sign-in" route that keeps the dev-identity token in an httpOnly cookie, one page that shows GET /api/me through a client that openapi-typescript and openapi-fetch generate from shared/api-contracts/openapi.yml, Vitest and Testing Library tests and a committed package-lock.json. Each web app has its own port, package name and session cookie name.
    • android-compose: a Kotlin and Jetpack Compose (MVVM) app with a sign-in screen and a profile screen, a hand-written Retrofit client that ApiContractTest checks against the shared contract, an in-memory session and JVM unit tests (a Robolectric Compose test included) that need no emulator. The app calls localhost, and task <app-id>:reverse forwards the backend's port with adb reverse; a release build refuses an apiBaseUrl that is not https://.
    • ios-swiftui: a SwiftUI app (Swift 6 strict concurrency, @MainActor @Observable view models) with a sign-in screen and a profile screen, a URLSession client kept to the contract, XCTest unit tests, one XCUITest, an XcodeGen project.yml and a Fastlane TestFlight lane. The dev identity works in the simulator only. Only the macOS CI job builds and tests the pack, so its maturity is experimental.
    • python-agent-service: a FastAPI app (uv, committed uv.lock) that assists and never advises: GET /api/health and POST /api/assist, which runs one agent turn through a provider interface (a deterministic fake by default and a Claude API adapter with the model in AGENT_CLAUDE_MODEL and the key only in ANTHROPIC_API_KEY) with one read-only tool that reads the backend's GET /api/me with the caller's own token. It verifies bearer tokens against the key the backend publishes at GET /api/dev-identity/jwks, or against a configured issuer and audience, and has its own openapi.yml, pytest tests and an evaluation harness (python -m evals.run) that runs the fake provider in CI and the live model only on demand (task <app-id>:eval-live). The Claude adapter is tested against a stub only.
  • The local development identity. POST /api/dev-identity/token and GET /api/dev-identity/jwks exist only under the backend's @Profile("local") and answer loopback requests only (a non-loopback peer, a forwarding header or a non-loopback host name is refused). The token is a 15-minute JWT (iss=prism-dev-identity) signed with an RSA key generated in memory at startup, never written to disk. Startup fails when local is active together with a configured identity provider, and with no provider configured the app rejects every token. No JWT secret exists anywhere in the template. The shared contract defines the dev identity (createDevToken, tagged x-prism-dev-only) and GET /api/me (getMe).
  • packs/versions.yml, the one place that pins versions. A pack reads its stack's baseline as versions; the workspace guidance and docker-compose.yml read the same pins, and a test fails when a pack file repeats a pinned version. scripts/read-pins.py <stack> gives the repository workflows the same pins (--select-xcode prints the developer directory of the installed Xcode that matches the pin, and --check-xcode is exact: 26.6 accepts its patch releases and refuses 26.7). The lockfiles are rewritten from the pins by scripts/refresh-nextjs-web-lock.py and scripts/refresh-python-agent-service-lock.py.
  • Derived identifiers, remembered ports and the backend an app calls. From an app's ID the CLI derives its package segment, package, module name, workflow name and paths: filters, and picks the first free port of the stack's range (spring-backend from 8080, nextjs-web from 3000, python-agent-service from 8200), kept in the app's answers. An app of a client stack (web, Android, iOS, agent service) may name its backend (backend: <app id> in the manifest and in an answers file, prism app add --backend, a question per client in the interactive flow when there are several backends); without it a client calls the first scaffolded backend. The CLI renders that backend's loopback address (backend_base_url) into every default of the client, its guidance, the root README, the API conventions and the servers of the shared contract. An unknown backend is unknown-app-backend.
  • prism app add --scaffold. It generates a new app into an existing generated workspace at the template revision the workspace records. With --apply it works on the branch prism-scaffold-<id> with one commit per layer, and it needs the workspace to be its own git repository with a clean tree and --trust-template for a custom template. The manifest field generation is scaffolded (Prism generated the code, so prism update keeps it current) or registered (the default); other apps and apps of external repositories are always registered.
  • agent-service is a choice of the interactive app list and of prism workflow install --app, with the other four generated apps; the full preset is one app of each of the backend, web, Android and iOS stacks.
  • The golden workspace (golden/). A generated workspace in the repository with one app of each of the five stacks, generated by prism new from scripts/golden-answers.yml. python scripts/build-golden.py regenerates it deterministically and --check fails on any difference; the golden-current job and a test run the check. It is excluded from the source archive, the wheel and the staged copy of a local template. scripts/run-golden-workflow.py <app> runs the run: steps of a golden app's own workflow in CI, so a pack workflow cannot change unnoticed, and scripts/sync-golden.py refreshes the lockfiles and golden/ after a pin moves.
  • Automated dependency updates and an advisory gate. renovate.json proposes updates to packs/versions.yml, one pull request per pack, and .github/workflows/dependency-sync.yml pushes the regenerated lockfiles and golden/ to that pull request. scripts/audit-gate.py runs npm audit --audit-level=high over the golden web lockfile and uv audit --locked over the golden agent service lockfile, and fails on any advisory that packs/audit-allowlist.yml does not list with its advisory link and a reason (docs/current-status.md records each entry).
  • A broken slice blocks the release. The first job of release.yml runs scripts/check-ci-green.py, which requires a green run of Template Validation (which builds and tests the golden app of every pack) and of CLI Validation for the tagged commit.
  • A deployment skill in every generated workspace. .claude/skills/deployment/ and .agents/skills/deployment/ explain how to deploy each generated stack and hold worked examples under references/: Azure Container Apps for a backend, Cloudflare Workers through OpenNext for web apps and the tag-triggered store release jobs for the mobile apps. The user and their agent own the cloud choice, the secrets and the deployment.
  • One source for every skill, command and Cursor rule. template-skills/<name>/skill.md holds a skill's name, description, layers, stacks and body, and scripts/build-skill-layers.py renders template/.agents/skills/, template/.claude/commands/, template/.claude/skills/ and template/.cursor/rules/ from it and writes the stack conditions into the generated block of _exclude. --check and a test fail when a generated file differs from its source. The stack skills cite the slice files they teach from in a Slice files section, and tests/test_stack_skill_slice_paths.py fails when a cited path is missing from any app of the skill's stack. Four agent skills (agent-conventions, add-tool, add-evaluation-case, agent-safety) ship with an agent service, and web-conventions with a web app.
  • feature-scope, an explicit scope edit through the board. A feature's apps and its ## App scope section change together in one board-validated proposal that may also create pending requirement pages for the apps the scope gains. It changes no status, owner or other field, refuses a done feature (reopen it first), never adds a retired app (app_retired) and never rewrites an existing requirement page. It is a canonical skill rather than a human action, because the human actions stay po-handoff, design-start and dev-start; it is the way out of app_retired_in_scope for a feature in progress. The board serves 27 canonical skills.
  • A generated workspace's board is writable out of the box. prism new pins the packaged workflow in the manifest (version, board identity and the digest of the packaged assets, workflow.mode: generated) and the generated .gitignore ignores .prism/state/, so prism board grant and prism board serve take a write without prism workflow upgrade. A workspace whose pin is missing or stale stays read-only.
  • Generated workspaces ship a root .gitattributes (* text=auto eol=lf, CRLF only for *.bat and *.cmd), so a generated workspace has the same line endings on every OS and the CLI's scaffold and update commits leave git status clean.
  • Conflicts are reported per layer. After each layer prism update scans for .rej files and conflict markers, and a conflicted update exits with 6.
  • Checks for the pack workflows. Template validation has an agent-service job and generates through the CLI. The macOS ios-build job builds and tests two iOS apps, and the Android and web jobs build two apps each.

Changed

  • prism new collects an app list; Copier no longer asks about platforms. The questionnaire asks for the project identity and the apps (an ID, a stack, a path, an audience, a repository and whether to scaffold or only register each). A preset is an app list, an answers file carries apps (leaving it out is an error; apps: [] generates the workspace layer alone), and the interactive flow asks for the apps and then for further apps. The CLI runs the workspace layer and then each scaffolded app's pack, each from its own answers file. prism presets --json carries each preset's apps. The default web app is web, and a second web app is one more nextjs-web entry.
  • The backend is generated by the spring-backend pack. A backend app gets the pack's slice at its own path, with its own package, port, workflow and Cursor rule, and the workspace's docker-compose.yml has one service for each backend app with the PostgreSQL development database. The backend's check task runs Gradle's check without the tests (no Kotlin style linter is configured), so the root task lint covers the web, Android and agent service apps.
  • The API contract is the auth contract. shared/api-contracts/openapi.yml depends on no answer. Template validation checks it, the web client is type-checked against it, and the Android and iOS clients are hand-written and checked against it (ApiContractTest, the contract paths test).
  • prism update works on a branch, one commit per layer. It creates prism-update-<template tag>, updates the workspace layer and commits, then updates each active scaffolded app from its own answers file and commits after each one, reports every layer and stops before merging when a layer conflicted. Without conflicts the branch stays checked out for you to review and merge. A scaffolded app with no answers file stops the update with the ways out (restore the file, retire the app or drop its entry), Copier's .rej files are never committed, and _exclude restores Copier's default exclusions for __pycache__, *.py[co] and .DS_Store. A recopy follows the same branch flow and keeps the workspace's apps and repositories. For a remote template prism new and prism update leave .copier-answers.yml as Copier wrote it.
  • prism validate reads the manifest. It checks each scaffolded app's directory, its own .copier-answers.yml and .github/workflows/<app id>.yml for any app ID and path, treats the manifest's error diagnostics as errors, and requires prism.workspace.yml.
  • Generated CI builds and tests only, with the same hygiene in every pack. No generated workflow pushes an image, deploys, releases to a store or reads a secret. Every pack workflow has permissions: contents: read, a concurrency group per app and branch, and a display name unique per app (<name> CI (<app id>)). The iOS workflow runs on the pinned macos-26 image, selects the Xcode of the pin (26.6), caches Homebrew's downloads, and picks the simulator by reading xcrun simctl list devices available -j for an iPhone whose runtime equals the selected SDK's.
  • The Android and iOS apps behave alike. Neither sign-in sends an email or a display name, a 401 on the profile screen returns to the sign-in with "Your session ended. Sign in again." on both, and both accept a base URL with or without trailing slashes. The iOS project sets VERSIONING_SYSTEM = apple-generic for Fastlane's increment_build_number. The root task test includes each iOS app's unit tests, which skip off macOS.
  • A skill has the same text in every tool. A workflow skill that is both a Codex skill and a Claude command has one body; the host differences are the invocation ($name in Codex, /name in Claude Code) and a few marked lines. Cursor rules carry a description, and the board review is a skill with no Cursor rule, because Cursor already loads the skill from .agents/skills/ and .claude/skills/ (the two skill folders stay: Claude Code and Codex each load only their own).
  • The clarify skills write complete sentences. po-clarify, design-clarify and dev-clarify turn a short answer such as yes into a complete sentence built from the question's wording, with the answer's words unchanged inside it, and ask and the clarify skills copy every other line of the page exactly as read. The board still requires the answer verbatim in each changed section, and its rejection now asks for the sentence.
  • setup-project and the guidance name the current commands. setup-project runs after prism new or prism workflow install, confirms the apps from the manifest, and the iOS and Android pack guidance states what the packs do (generate-clients validates the contract for a hand-written client, the Android compile SDK is printed as the compile SDK, the iOS fake client has no template suffix).
  • Skills and docs describe the slices. The backend, web, Android, iOS and agent skills cite the slice's files by path and explain how to replace the dev identity with an identity provider; the workspace layer lists each app from the app list in the README, AGENTS.md, Taskfile.yml and the architecture and CI docs; deploy-device ships with an Android app and covers each one; the README, getting-started guide, workspace model and questionnaire match the output of the commands they show.
  • The repository workflows read every pinned version from packs/versions.yml through scripts/read-pins.py, and cli-validation.yml also runs on a manual start.
  • The manifest rejects unknown keys. A misspelled field of an app or a repository (satus: retired) is unknown-app-field or unknown-repository-field instead of being ignored.
  • A retired app frees its path (its ID stays reserved): app-path-conflict compares only active apps, and a scaffolded app still refuses a directory that holds files.
  • dev-start and dev-done on the board enforce the API rule (api_surface_without_api_app), as design-handoff does; the status-board header is exact on the board as in lint; and lint compares a design page's feature-id without regard to case.
  • A fresh apply no longer evaluates the operation twice. The apply that records an operation has just revalidated it against the live files, so its roll-forward skips the second full evaluation (about half of an apply's parses and file opens). Every write still checks the recorded before-state, and recovery and the retry of a pending operation revalidate in full.

Removed

  • The full samples and everything that only served them. template/backend, template/web-user-app, template/web-admin-portal, template/mobile-android and template/mobile-ios are gone with their authentication (passwords, refresh tokens, Google, Apple, Facebook and Microsoft OAuth), transactions, examples, docs, per-sample workflows, Cursor rule sources and _exclude entries; the packs' slices replace them. The workspace layer renders no app code.
  • The platforms question and answer, and the database, supporting_services, use_docker, cloud_provider, web_hosting, auth_methods, github_org, ios_module_name and package_path questions. No generated app reads them. prism new rejects an answers file that sets one with an "Unknown answer(s)" error, and the "Username + Password is required" check and the Apple Sign-In warning are gone with them.
  • The JWT and OAuth variables of .env and .env.example, the OAuth and transactions operations of the API contract, and Redis (its compose service, variables and Azure steps). The template holds no JWT secret.
  • The generated infra/azure/ scripts, docs/deployment/cloudflare-setup.md and the root infra:* tasks. They moved into the deployment skill's references, and prism validate no longer requires the Cloudflare guide.
  • The Hygen generators (template/_templates/) and every mention of them, including the hygen prerequisite.
  • The always-on Cursor rule project.mdc and the Cursor rule advisory-review.mdc. Cursor reads AGENTS.md and the skill folders itself, so a rule whose content is @AGENTS.md or a copy of the board-review skill loaded the same text twice.
  • The per-stack Cursor rule sources and the workspace workflow backend.yml.jinja. A stack's rule and workflow belong to its pack, per app.
  • Presets with their own answers. A preset is an app list only.

Fixed

  • A feature in progress that names a retired app, or a done feature that does, can be repaired through the board. The first leaves app_retired_in_scope through feature-scope; the second can be reopened, because the requirement page of the retired app it lists is valid for the reopen.
  • The history-date lint covers advisory/BOARD.md and advisory/PROJECT_FOUNDATION.md, which SCHEMA.md names as current-state pages; their other exemptions stay.
  • The Adding a feature later example of docs/workspace-model.md uses the release: form that release-evidence-required accepts.
  • A board write no longer ends in a conflict receipt when Windows refuses a replace for a moment. Windows refuses to replace a file that another handle holds open without delete sharing, which includes the board's graph poller reading the pages. A replace of a page, a log or a folder now waits out that momentary refusal (access denied, sharing or lock violation) for up to a second and raises any other error as before.
  • The board no longer publishes a spurious version after a write. The server stores the fingerprint it took under its lock, so a poll that raced a write does not announce a version the write already covered; the browser tests wait until the page has adopted the newest version before they drag or open a dialog.
  • Generated workspaces show no racy modifications in git status on Windows with core.autocrlf=true (see .gitattributes above).

Security

  • One resolver for every wiki path. A link, a sources entry or a repo: link on a page is untrusted text. prism_cli/wiki_paths.py decodes first, refuses UNC, rooted, drive-qualified, backslash, double-encoded and .. paths as text, confines the path lexically and only then looks at the components for symlinks and reparse points, so a path such as %2F%2Fhost%2Fshare%2Fx.md reaches no filesystem call during proposal validation or lint. A refused path is a broken-link finding.
  • Names, paths and audiences cannot become code. An app path is restricted to safe segments ([a-z0-9][a-z0-9._-]*); names, audiences, project names and descriptions are one line of plain characters (no quotes, backticks, $, <, > or ;). The rule is one module (prism_cli/safe_values.py) used by the manifest normalizer, prism new, prism app add and Copier's own validators, so a raw copier copy is covered too. Pack workflows pass the app's path once, as a quoted environment variable (APP_PATH), and templates serialize every user value that enters YAML.
  • prism update checks every layer before it runs Copier with --trust. Each app layer must name the workspace's approved template source, a plain template revision and the identity, port and derived values that the manifest gives it, and hold no answer that no app layer asks. The workspace layer's own answers file must select the workspace layer and carry a safe project identity and an apps list that passes the safe-value rule, with stacks and the package segments re-derived and compared. A tampered file stops the update before Copier starts, and prism app add --scaffold refuses it the same way.
  • Recopy and every answers file are confined. The recopy writes its saved answers to an exclusively created temporary file in a checked directory and replaces the answers file instead of writing through it. Every answers file the update reads or writes, the workspace's own included, is confined to its folder before anything opens or follows it; a symlink, a junction or another reparse point on its path is refused with the way out (restore the regular file from git).
  • Copier gets exactly the answers that were validated. prism update, prism app add --scaffold and recopy read each layer's saved answers once, without following a link, and give Copier exactly what was validated: a private file for a recopy, the same bytes for the manifest render. A smart update starts only while the tracked file still holds the validated bytes, and the file Copier writes must hold exactly the validated answers apart from the template revision and the stacks and apps handed over. When an answers file changes after validation, the command refuses and names the file; nothing that depends on it is moved into place or committed.
  • A write retry is checked like the first attempt. Connected writes recheck the expected content or intake tree, confinement, the participant's grant and the relevant sources before every Windows replacement retry. The relevant sources are bound as a set at validation (which pages and their content), so a page that appears during the wait is a conflict, never an overwrite.
  • The generated web app serves the local development identity to this machine only. next dev and next start listen on 127.0.0.1; POST /api/session answers only in explicit local mode (LOCAL_DEV_SIGNIN=1, set by .env.development, which a production build does not load), only to a request that carries the app's own Origin, and only from this machine. The agent service answers callers on its own machine only under the local profile. A Next.js route cannot see the socket peer, so the loopback bind, the flag and the refusal of a non-loopback bound address are its walls.
  • The generated backend requires an audience. Outside the local profile, a configured identity provider needs spring.security.oauth2.resourceserver.jwt.audiences: startup fails without one, and a token whose aud is missing or names another API is rejected. The agent service requires the matching AGENT_OIDC_AUDIENCE. The backend listens on 127.0.0.1 under its local profile, and the generated docker-compose.yml binds the development database and every other published port to 127.0.0.1.
  • The agent service cannot be overspent concurrently. A turn reserves the most each model call can cost before the call, a user has a bounded number of turns in flight (AGENT_BUDGET_CONCURRENT_TURNS), the usage of every completed call is recorded even when the turn fails later, and the budget answers 429. Tool failures log the exception type only, never its message or traceback.
  • iOS Release builds allow no cleartext. NSAllowsLocalNetworking is in Plists/Info.Debug.plist, which only the Debug configuration uses, and APIURL.make accepts an http URL only in Debug builds.
  • Releases are bound to one commit and to what was built. release.yml and npm-release.yml require refs/tags/<tag> (a branch named like a tag is refused), resolve its commit once and check out that SHA in every later job. The built wheel is smoke-tested before any publication, and the TestPyPI and PyPI copies are downloaded from their own index alone and must be byte-identical to the build before the release goes on.
  • Dependency sync holds its write token away from unreviewed tooling. dependency-sync.yml regenerates the lockfiles and golden/ in a read-only job (persist-credentials: false, pinned to the head SHA) and uploads one patch; a second job holding the write token validates that the patch touches only the pack lockfiles, packs/versions.yml and golden/, then applies and pushes it.
  • The audit gate fails closed. scripts/audit-gate.py rejects an npm audit report without the expected structure, whose exit status disagrees with it, whose metadata.vulnerabilities counts differ from the entries present, whose entries and advisories lack the severity, package, link and title the gate reports, or whose entry does not reach an advisory through the entries it names (an empty via, a self-reference, a loop or a package the report does not list); uv audit with an exit status other than 0 or 1 fails too.
  • Recovery keeps checking the sources a human reviewed. A recovery confirmed by a human binds the review to the current relevant paths and their digests, and the snapshot is checked after the before-state is reconstructed and before every move and write, so an editor's change between the review and the apply stops it. The review is verified against the one snapshot that the recovery then keeps.
  • Ingest cannot write format templates. A skill output whose file name starts with _ (knowledge/wiki/topics/_FORMAT.md and the same in research and plans) is refused, and every output must resolve to a page kind the board validates.