Evidence Every claim on this site is backed by a test or a recorded run in the repository. What is proven, and what is still unverified, is listed in docs/VERIFICATION.md.

synchrobuilder.json: setup and CI

The manifest

synchrobuilder.json describes what a project needs to run. This page summarises docs/manifest-spec.md, the normative spec for version 1; ADR-004 holds the rationale. v1

Statusdocs/manifest-spec.md is the normative spec for version 1. init writes the file, and setup, doctor and ci read it through lib/manifest/schema.mjs. The shell-syntax rule below is exercised end to end by tests/unit/setup-plan.test.mjs, which turns a command containing shell syntax into a manual step rather than spawning it. The validator has no dedicated test of its own yet, so docs/VERIFICATION.md is the place to check before relying on a rule stated here.

Purpose

init writes the manifest, setup and doctor read it on another machine, and ci turns it into a GitHub Actions matrix. The brief requires the format to be simple, versioned, documented, and not tied to Claude so other coding agents can adopt it. The plugin has no runtime dependencies, so the format must be parseable with the Node standard library.

Principles

  • JSON only. One file, synchrobuilder.json, at the repository root. UTF-8, LF, two-space indent when the plugin writes it. No comments in the file; explanations go in hint and description fields, which the tooling shows to humans.
  • Integer version. A top-level "version": 1. Readers reject a higher major version with a clear message and accept lower ones with defaults. Additive changes do not bump the version; removals and semantic changes do.
  • Exact runtime versions. runtimes[].version is an exact version string. Setup installs or verifies exactly that version; doctor reports the difference. Ranges are not allowed in v1, because "works on my machine" is the bug being removed. A semver range may be added as allowedRange in v2 without a version bump.
  • Env var names and descriptions only. A value field is not part of the schema, and the writer refuses to serialize one.
  • Commands are plain argument strings without shell syntax. The runner splits them with a small, documented tokenizer (quotes and backslash escapes only) and spawns the program directly. The validator rejects these with a message pointing at the audit rule that explains why: &&, ||, |, ;, >, <, &, a backtick, $VAR, ${VAR}, $(...) and a line break. A project that needs a shell pipeline puts it in a Node script and calls that.
  • Per-OS overrides are limited. os.windows, os.macos and os.linux may override commands and runtimes only. Nothing else is per-OS, on purpose.
  • Extensions carry an x- prefix. Unknown top-level keys are allowed only with that prefix, which keeps the door open for other tools without silently accepting typos.
  • Nothing mentions Claude. The file never names Claude, hooks or the plugin.

Example

The skeleton from docs/manifest-spec.md. The project, versions and service in it are illustrative.

skeleton · docs/manifest-spec.md · validated by lib/manifest/schema.mjs
{
  "$schema": "https://synchrobuilder.dev/schema/manifest-v1.json",
  "version": 1,
  "name": "acme-portal",
  "runtimes": [
    { "name": "node", "version": "20.11.0", "detect": "node --version" }
  ],
  "packageManager": { "name": "pnpm", "version": "9.1.0" },
  "services": [
    { "name": "postgres", "version": "16", "port": 5432,
      "hint": "Any Postgres 16 reachable at DATABASE_URL" }
  ],
  "env": [
    { "name": "DATABASE_URL", "description": "Postgres connection string", "required": true },
    { "name": "DEBUG", "description": "Verbose logging", "required": false }
  ],
  "commands": {
    "install": "pnpm install --frozen-lockfile",
    "build": "pnpm build",
    "migrate": "pnpm prisma migrate deploy",
    "seed": "pnpm run seed",
    "start": "pnpm dev",
    "test": "pnpm test"
  },
  "healthCheck": {
    "type": "http",
    "url": "http://localhost:3000/health",
    "expectStatus": 200,
    "timeoutSeconds": 60
  },
  "os": {
    "windows": { "commands": { "start": "pnpm dev:win" } }
  }
}

Nothing is served at the $schema URL in this example. See the last section on this page.

Fields

The shape and the rules come from the spec. Required fields are as docs/manifest-spec.md states them.

FieldTypeRequiredMeaning
$schemastring (URL)noURL of the JSON schema. Informational: nothing validates against it today.
versionintegeryes1. A reader that meets a higher major version stops with a clear message.
namestringnoHuman name of the project, at most 100 characters.
runtimes[]array of { name, version, detect }no; name and version required in each entryname is node, python, ruby, go, java, dotnet, php or rust. version is an exact version string; a range is rejected. detect is the command that prints the installed version; it defaults to <name> --version.
packageManager{ name, version }noName and version of the package manager, pnpm 9.1.0 in the example. packageManager.version is an exact version too; a range is rejected.
services[]array of { name, version, port, hint }no; name required in each entryA database or other process the project talks to: postgres, redis, mysql, mongodb, or your own name. A major version is usually enough. doctor uses port to see whether it answers. hint is one sentence a human reads when it is missing; redaction runs on it before anything is shared (ADR-005).
env[]array of { name, description, required }no; name required in each entryNames and descriptions only. A value key is invalid and the writer refuses to emit one. required defaults to true. Redaction runs on description before anything is shared.
commandsobject with install, build, migrate, seed, start, testnoEach value is a plain argument string without shell syntax. The validator rejects &&, ||, |, ;, >, <, &, a backtick, $VAR, ${VAR}, $(...) and a line break.
healthCheck{ type, url, expectStatus, command, timeoutSeconds }no; type required, plus url when type is http and command when it is commandtype is http (GET url, expect expectStatus, default 200) or command (run healthCheck.command, exit 0 means healthy). Both use timeoutSeconds, default 60.
os.windows, os.macos, os.linuxobject with commands and/or runtimesnoMay override commands and runtimes only. Nothing else is per-OS.
x-*anynoYour own extensions. They are the only unknown keys a reader accepts; any other unknown key is an error, which catches typos.

Field names, types and rules come from docs/manifest-spec.md; ADR-004 holds the rationale.

How Synchrobuilder uses it

Four commands, all shipped; see the Commands page.

Writes it

  • /synchrobuilder:init inspects the project and writes synchrobuilder.json: runtimes with exact versions, the package manager, services such as a database, env var names with descriptions (never values), the install, build, migrate, seed, start and test commands, and a health check.
  • Exact versions mean the manifest changes whenever the team upgrades Node. /synchrobuilder:init --refresh re-detects and shows a diff.

Reads it

  • /synchrobuilder:setup compares the manifest to what is installed, shows a step-by-step plan for this OS, asks before each install, then runs the health check. It uses the OS's native package manager (winget, Homebrew, apt), pins exact versions and never elevates silently.
  • /synchrobuilder:doctor records a fingerprint of a working machine, or explains why this machine differs from the manifest, most likely cause first.
  • /synchrobuilder:ci generates a GitHub Actions workflow that installs, builds, tests and health-checks the project on ubuntu, macos and windows, plus a README badge. The badge only says "verified" when that workflow passes.

Because the schema forbids values for env vars, an audit rule for "secret in manifest" is structurally unnecessary. Redaction still runs on hint and description text before anything is shared (ADR-005). What the ledger records: the CI generator's output is unit-tested for three runners, exact pinned versions, per-OS overrides as guarded steps and a health check no quoting can break; setup --plan and doctor were run by hand against a copy of the demo repository. A clean-runner run of a generated workflow is not recorded.

Commands on Windows, without a shell

The runner never hands a command string to a shell. On Windows that needs one extra step.

On Windows, npm, pnpm and yarn are installed as .cmd shims, and those cannot be spawned directly without a shell. The Claude Code hooks reference states the same limit for exec-form hooks: the .cmd and .bat shims that tools install "are not executables and can't be spawned without a shell", and the recommended fix is to invoke the underlying script with node, because node.exe is a real binary.

The runner follows that technique. It resolves the package manager to its JavaScript entry point and runs it with node: for npm, <node prefix>/node_modules/npm/bin/npm-cli.js; for Corepack-managed pnpm and yarn, the Corepack shim's JavaScript entry. If resolution fails, setup says so and offers to open a shell command for you to run yourself, rather than running a shell silently.

UnverifiedResolving npm, pnpm and yarn to a JavaScript entry point has never run on a real Windows machine. The code is unit tested against a fake layout; docs/VERIFICATION.md keeps it on the not-verified list until a Windows run covers it.

Schema URL

The schema is not served yet.

The example points $schema at https://synchrobuilder.dev/schema/manifest-v1.json. No schema file is served there. The website is at synchrobuilder.com, so that URL also has the wrong host; $schema is informational and nothing validates against it today. docs/manifest-spec.md is the source for everything on this page.

To read the source, see docs/manifest-spec.md, docs/adr/ADR-004-manifest-format.md and docs/commands.md in the repository at github.com/RyanNorouzi/synchrobuilder.