verity-framework 0.1.0 → 0.2.1

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.
package/README.md CHANGED
@@ -2,29 +2,92 @@
2
2
 
3
3
  **Prompt to production, proven.**
4
4
 
5
- Verity is a CI/CD-native, GitHub-native, production-lifecycle AI software delivery framework a clean-room successor to [spec-driven-devops](https://www.npmjs.com/package/spec-driven-devops) 1.4 for projects that go *beyond MVP* into real, operated production.
5
+ Most AI coding tools stop when the code is written — leaving you to find out later whether it runs, deploys, or actually works. **Verity keeps going.** It carries a project from an idea all the way to software that is tested, deployed, and **proven working in front of a real user** — and then keeps it running.
6
6
 
7
- Most AI coding tools stop when the code is written. Verity keeps going until the software is tested, deployed, and **proven working in front of a user**. It runs a project as a sequence of specialized AI roles (architect, builder, reviewer, release operator, verifier…) that hand work off through clear contracts, with GitHub as the source of truth.
7
+ It does that by running your project as a sequence of specialized AI roles — a vision assistant, an architect, a builder, a reviewer, a release operator, a verifier, and more — that hand work to each other through clear contracts, with **GitHub as the single source of truth**. It's CI/CD-native and GitHub-native by design, built for projects going *beyond a prototype* into real, operated production. (Verity is a clean-room successor to [spec-driven-devops](https://www.npmjs.com/package/spec-driven-devops) 1.4.)
8
8
 
9
- > **Status: design complete, pre-implementation.** This repo currently tracks the design. Implementation begins with Verity's own *walking skeleton* — see [`docs/walking-skeleton-plan.md`](docs/walking-skeleton-plan.md).
9
+ > 📦 On npm as [`verity-framework`](https://www.npmjs.com/package/verity-framework) · works with **Claude Code** and **OpenCode**.
10
10
 
11
- ## Subsystems
12
- - **Relay** — role orchestration + the stream loop + dependency engine
13
- - **Shipyard** — CI/CD spine + Release/Deploy Operator + runtime truth (`STATUS.md`)
14
- - **Ledger** — GitHub-derived state engine (no stale files)
15
- - **Gate** — review + security + the quality gates
16
- - **Verify** — live "observably-works" verification
11
+ ## What makes it different
17
12
 
18
- ## Docs
19
- - [`docs/framework-spec.md`](docs/framework-spec.md) — the build-ready architecture spec
20
- - [`docs/roles-spec.md`](docs/roles-spec.md) working log + full rationale (all 14 roles)
21
- - [`docs/interview-findings.md`](docs/interview-findings.md) forensic interview of the real build that drove the design
22
- - [`docs/brand.md`](docs/brand.md) — naming / positioning
23
- - [`docs/features/helper-bot.md`](docs/features/helper-bot.md) — drop-in feature #1
24
- - [`docs/walking-skeleton-plan.md`](docs/walking-skeleton-plan.md) — the first implementation slice
13
+ - **"Done" means proven, not written.** Every change is reviewed, deployed, and driven like a real user before it counts as finished.
14
+ - **The AI decides; a deterministic tool records.** The `verity` CLI is the "notebook" that never forgets it holds the rules and the official state, so a forgetful model (or a brand-new chat) can always pick up exactly where things left off.
15
+ - **State is read from GitHub, never a file that drifts.** A merged PR *is* "built"; a tag *is* "released." Nothing to hand-maintain, nothing to lie about.
16
+ - **Specialist roles with real handoffs** beat one mega-prompt trying to do everything.
25
17
 
26
- ## Package
27
- `verity-framework` (npm) · CLI binary: `verity` · Node ≥16 · host deps: `git`, `gh`
18
+ ## How it works
28
19
 
29
- ## License
30
- MIT
20
+ A Verity project moves through three arcs:
21
+
22
+ 1. **Bootstrap** *(once)* — name the project, create the repo, design the architecture, choose where it deploys, and prove the whole build→ship pipeline on a tiny end-to-end "walking skeleton."
23
+ 2. **Stream** *(the loop)* — every feature flows through the same short cycle: **plan → build → review → merge**, riding that proven pipeline.
24
+ 3. **Operate** *(continuous)* — cut releases, deploy, **verify on the live app**, and keep it healthy.
25
+
26
+ Five subsystems carry that out:
27
+
28
+ | Subsystem | Job |
29
+ |---|---|
30
+ | **Relay** | Orchestrates the roles, the stream loop, and the dependency engine |
31
+ | **Shipyard** | The CI/CD spine — releases, deploys, the deployment-methods catalog, and runtime truth (`STATUS.md`) |
32
+ | **Ledger** | Derives state from GitHub, so nothing goes stale |
33
+ | **Gate** | Review, security, and the quality gates |
34
+ | **Verify** | Live "observably-works" verification |
35
+
36
+ ## Install
37
+
38
+ **Prerequisites:** a GitHub account · Node ≥16 · `git` and the GitHub CLI (`gh`) installed **and signed in** (run `gh auth login` once — installing `gh` is not the same as being authenticated).
39
+
40
+ ```bash
41
+ # preflight — all three should answer without error:
42
+ node -v && git --version && gh auth status
43
+
44
+ # install, then connect it to your assistant:
45
+ npm i -g verity-framework
46
+ verity install --claude # or: --opencode
47
+ ```
48
+
49
+ Then, in your AI assistant, start a project with `/verity:vision`. The [interactive guides](#guides) walk through it step by step.
50
+
51
+ ## Deployment methods
52
+
53
+ Verity makes **where your app deploys a deliberate choice**, not an accident. Left alone, an AI agent picks a host by whatever happens to be wired into its environment — suggesting Render.com, say, just because a Render tool is installed. The deployment-methods catalog fixes that: the **Architect** reads your saved targets and works with you to choose one (recording it as an ADR), and if none are configured yet it asks how you want to deploy and offers suggestions.
54
+
55
+ It's **two files, two scopes** — and crucially, no secrets ever touch git:
56
+
57
+ | | Global catalog | Per-app access |
58
+ |---|---|---|
59
+ | **Path** | `~/.verity/deployment-methods.md` | `.verity/deploy-access.md` |
60
+ | **Scope** | You, across *every* project | One app |
61
+ | **Holds** | The deploy targets you *can* use | How to reach *this* app's host |
62
+ | **Created** | At `verity install` — seeded once, never overwritten | Written by the Architect, per app |
63
+ | **In git?** | No (lives in your home dir) | **No — gitignored.** A committed, secret-free pointer tells teammates who lack it to ask the project admin |
64
+
65
+ > 🔒 Both files reference credential **locations** — a key file like `~/.ssh/prod.pem`, an SSO profile, a secret-store entry — never an actual key, password, or token. The per-app file is shared out-of-band, so nothing sensitive lands in git history. (Same rule as `STATUS.md`.)
66
+
67
+ The global catalog ships with two worked examples (AWS EC2 over SSH, and a local-network server). Edit it to describe your real targets:
68
+
69
+ ```bash
70
+ verity deployment list # your targets (this is what the Architect reads)
71
+ verity deployment show aws-ec2 # one target's details
72
+ verity deployment edit # open the catalog in $EDITOR
73
+ verity deployment path # where the catalog lives
74
+ ```
75
+
76
+ When the Architect picks a target it sets up the per-app access file — `verity deployment init-access` gitignores the real file and commits the "ask the admin" pointer — then writes `.verity/deploy-access.md` with how to reach this app's host. From there, `/verity:ship` deploys to that target. (`verity deployment access` tells a teammate whether they have the file, and who to ask if not.)
77
+
78
+ ## Guides
79
+
80
+ Interactive, beginner-friendly, and fully self-contained — clone or download the repo and open them in any browser (no server or internet needed):
81
+
82
+ - [**Overview**](docs/verity-overview.html) — what Verity is and the mental model, no jargon assumed
83
+ - [**Usage**](docs/verity-usage.html) — install + command-by-command recipes + pro tips (Claude Code / OpenCode toggle)
84
+ - [**Flows**](docs/verity-flows.html) — start-from-scratch vs. add-to-an-existing-project, side by side ([editable `.drawio`](docs/verity-flows.drawio))
85
+ - [**Command reference**](docs/commands.md) — all 13 `/verity:*` roles and what each one does
86
+ - [**Explainer kit**](docs/explainer-kit.md) — a briefing for an AI to describe Verity to humans (podcast / deck / talk)
87
+
88
+ ## Reference
89
+
90
+ - **Package:** `verity-framework` · CLI binary `verity` · Node ≥16 · host deps `git`, `gh`
91
+ - **Design docs:** [framework spec](docs/framework-spec.md) · [roles spec](docs/roles-spec.md) · [the interview that drove the design](docs/interview-findings.md) · [brand / positioning](docs/brand.md) · [walking-skeleton plan](docs/walking-skeleton-plan.md)
92
+ - **Contributing:** [`CONTRIBUTING.md`](CONTRIBUTING.md) — local setup, the test/lint checks, project layout, and conventions
93
+ - **License:** MIT
@@ -34,24 +34,44 @@ and the walking-skeleton definition handed to /verity:plan.
34
34
  ```
35
35
  Then fill in Context / Decision / Alternatives considered / Consequences.
36
36
 
37
- 3. **Freeze the core contracts** (wire/JWT/schema between components) additive-only
37
+ 3. **Choose the deployment target.** Read the user's global catalog of where they can
38
+ deploy, and pick a target for THIS app — never assume one (don't reach for a host
39
+ just because its MCP/CLI happens to be installed):
40
+ ```bash
41
+ verity deployment list # ~/.verity/deployment-methods.md (locations, not secrets)
42
+ ```
43
+ - Work with the user to choose a method; record the choice as an ADR.
44
+ - If `hasConfigured` is false (only the shipped examples remain), ASK how they want
45
+ to deploy and offer suggestions (managed PaaS, a VM over SSH, a local/LAN server…),
46
+ then help them add it: `verity deployment edit`.
47
+ - Set up the per-app access file (committed pointer + gitignore — no secrets in git):
48
+ ```bash
49
+ verity deployment init-access
50
+ ```
51
+ Then WRITE `.verity/deploy-access.md` with how to reach this app's host —
52
+ credential **locations** only (key files, SSO profiles, secret-store entries),
53
+ never raw secrets. That file is gitignored and shared out-of-band; teammates who
54
+ lack it are pointed at the project admin. The Release/Deploy Operator (`/verity:ship`)
55
+ consumes this target when it builds `deploy.sh`.
56
+
57
+ 4. **Freeze the core contracts** (wire/JWT/schema between components) — additive-only
38
58
  thereafter; a breaking change is a NEW contract, never an edit:
39
59
  ```bash
40
60
  verity contract new <seam-name>
41
61
  ```
42
62
 
43
- 4. Offer the **drop-in feature catalog**. For each feature the user wants, note that
63
+ 5. Offer the **drop-in feature catalog**. For each feature the user wants, note that
44
64
  its stages will fold into the plan:
45
65
  ```bash
46
66
  verity feature list
47
67
  verity feature show <id>
48
68
  ```
49
69
 
50
- 5. Define the **walking skeleton** (Stage 0): the thinnest end-to-end slice that
70
+ 6. Define the **walking skeleton** (Stage 0): the thinnest end-to-end slice that
51
71
  compiles, runs, passes one real test, goes green in CI, and deploys. This blocks
52
72
  all feature stages and proves the spine.
53
73
 
54
- 6. Hand off to **/verity:plan** (Intake/Planner) to decompose the design + accepted
74
+ 7. Hand off to **/verity:plan** (Intake/Planner) to decompose the design + accepted
55
75
  features into the initial thin backlog of stages.
56
76
 
57
77
  Runtime note: if `verity` is not on PATH, use `node "$HOME/.claude/verity/bin/verity.cjs" ...`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verity-framework",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Verity — a CI/CD-native, GitHub-native, production-lifecycle AI software delivery framework.",
5
5
  "keywords": ["verity", "ai", "ci-cd", "github", "devops", "agent", "framework"],
6
6
  "author": "Sean Mahoney",
@@ -0,0 +1,230 @@
1
+ // Verity deployment methods (framework-spec.md — deployment-target feature).
2
+ //
3
+ // TWO scopes, deliberately separate:
4
+ //
5
+ // 1. GLOBAL catalog — `~/.verity/deployment-methods.md`. Your private inventory
6
+ // of where you CAN deploy (one per machine/user, reused across every project).
7
+ // Seeded at `verity install`, edited with `verity deployment edit`, read by the
8
+ // Architect with `verity deployment list`. Holds credential LOCATIONS, never
9
+ // secret values. NOT in any repo.
10
+ //
11
+ // 2. PER-PROJECT access file — `.verity/deploy-access.md`. Written by the Architect
12
+ // for ONE app: how to reach THAT app's host. Gitignored + shared out-of-band; a
13
+ // secret-free pointer (`.verity/deploy-access.README.md`) IS committed so a
14
+ // teammate without the file is told to get it from the project admin.
15
+ const fs = require('node:fs');
16
+ const os = require('node:os');
17
+ const path = require('node:path');
18
+ const { spawnSync } = require('node:child_process');
19
+
20
+ const identity = require('./identity.cjs');
21
+ const { render } = require('./core.cjs');
22
+
23
+ const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
24
+
25
+ const ACCESS_FILE = '.verity/deploy-access.md'; // gitignored real file
26
+ const ACCESS_POINTER = '.verity/deploy-access.README.md'; // committed pointer
27
+
28
+ function readTemplate(name) {
29
+ return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8');
30
+ }
31
+
32
+ // --- GLOBAL catalog -------------------------------------------------------
33
+
34
+ // User-global, harness-independent, and OUTSIDE the install dir (which `verity
35
+ // install` clobbers on every reinstall). VERITY_HOME / opts.home override for tests.
36
+ function homeDir(opts = {}) {
37
+ return opts.home || process.env.VERITY_HOME || path.join(os.homedir(), '.verity');
38
+ }
39
+
40
+ function globalPath(opts = {}) {
41
+ return path.join(homeDir(opts), 'deployment-methods.md');
42
+ }
43
+
44
+ // Seed the catalog only if absent — NEVER clobber the user's edits.
45
+ function ensure(opts = {}) {
46
+ const p = globalPath(opts);
47
+ if (fs.existsSync(p)) {
48
+ return { created: false, path: p };
49
+ }
50
+ fs.mkdirSync(path.dirname(p), { recursive: true });
51
+ fs.writeFileSync(p, readTemplate('deployment-methods.md.tmpl'));
52
+ return { created: true, path: p };
53
+ }
54
+
55
+ // Each method is a `## <id> — <Title>` section; `- **status:** <x>` tags it.
56
+ // status defaults to `active` when a user adds a method without one; the shipped
57
+ // samples are `example` so the Architect can tell "real option" from "placeholder".
58
+ function parseMethods(text) {
59
+ const methods = [];
60
+ let cur = null;
61
+ for (const line of String(text).split('\n')) {
62
+ const head = line.match(/^##\s+([a-z0-9][a-z0-9-]*)\s+—\s+(.+)$/);
63
+ if (head) {
64
+ if (cur) {
65
+ methods.push(cur);
66
+ }
67
+ cur = { id: head[1], title: head[2].trim(), status: 'active', content: line };
68
+ continue;
69
+ }
70
+ if (cur) {
71
+ cur.content += `\n${line}`;
72
+ const st = line.match(/^-\s+\*\*status:\*\*\s*(.+)$/i);
73
+ if (st) {
74
+ cur.status = st[1].trim().toLowerCase();
75
+ }
76
+ }
77
+ }
78
+ if (cur) {
79
+ methods.push(cur);
80
+ }
81
+ return methods;
82
+ }
83
+
84
+ function list(opts = {}) {
85
+ const { path: p } = ensure(opts);
86
+ const methods = parseMethods(fs.readFileSync(p, 'utf8')).map((m) => ({
87
+ id: m.id,
88
+ title: m.title,
89
+ status: m.status,
90
+ }));
91
+ const configured = methods.filter((m) => m.status !== 'example');
92
+ return { path: p, methods, configured, hasConfigured: configured.length > 0 };
93
+ }
94
+
95
+ function show(id, opts = {}) {
96
+ if (!id) {
97
+ throw new Error('show requires a method id');
98
+ }
99
+ const { path: p } = ensure(opts);
100
+ const method = parseMethods(fs.readFileSync(p, 'utf8')).find((m) => m.id === id);
101
+ if (!method) {
102
+ throw new Error(`no such deployment method: ${id}`);
103
+ }
104
+ return method;
105
+ }
106
+
107
+ // Open the catalog in $EDITOR when run interactively; otherwise just report the
108
+ // path (the agent edits via its own Write tool, not an interactive editor).
109
+ function edit(opts = {}) {
110
+ const { path: p } = ensure(opts);
111
+ const editor = opts.editor || process.env.VISUAL || process.env.EDITOR || 'nano';
112
+ const isTTY = opts.isTTY !== undefined ? opts.isTTY : Boolean(process.stdout.isTTY);
113
+ if (!isTTY) {
114
+ return { path: p, editor, opened: false, hint: `open it: ${editor} ${p}` };
115
+ }
116
+ const run = opts.spawn || ((cmd, a) => spawnSync(cmd, a, { stdio: 'inherit' }));
117
+ run(editor, [p]);
118
+ return { path: p, editor, opened: true };
119
+ }
120
+
121
+ // --- PER-PROJECT access ---------------------------------------------------
122
+
123
+ function adminFor(cwd) {
124
+ try {
125
+ const { manifest } = identity.get(cwd);
126
+ return manifest.owner || manifest.name || manifest.slug || '(project admin)';
127
+ } catch (_e) {
128
+ return '(project admin — lock identity first)';
129
+ }
130
+ }
131
+
132
+ // Add a line to the project .gitignore if not already present. Returns whether the
133
+ // file changed (idempotent).
134
+ function ensureIgnored(cwd, line) {
135
+ const gi = path.join(cwd, '.gitignore');
136
+ const text = fs.existsSync(gi) ? fs.readFileSync(gi, 'utf8') : '';
137
+ if (text.split('\n').some((l) => l.trim() === line)) {
138
+ return false;
139
+ }
140
+ const prefix = text.length && !text.endsWith('\n') ? '\n' : '';
141
+ fs.writeFileSync(gi, `${text}${prefix}${line}\n`);
142
+ return true;
143
+ }
144
+
145
+ // Set up the committed pointer + gitignore for a project's access file. Does NOT
146
+ // write the real access file — the Architect writes that with real host detail.
147
+ function initAccess(cwd) {
148
+ fs.mkdirSync(path.join(cwd, '.verity'), { recursive: true });
149
+ const admin = adminFor(cwd);
150
+ const ignored = ensureIgnored(cwd, ACCESS_FILE);
151
+
152
+ const pointer = path.join(cwd, ACCESS_POINTER);
153
+ let pointerCreated = false;
154
+ if (!fs.existsSync(pointer)) {
155
+ fs.writeFileSync(pointer, render(readTemplate('deploy-access-pointer.md.tmpl'), { admin }));
156
+ pointerCreated = true;
157
+ }
158
+
159
+ return {
160
+ accessFile: ACCESS_FILE,
161
+ accessPath: path.join(cwd, ACCESS_FILE),
162
+ pointer: ACCESS_POINTER,
163
+ pointerCreated,
164
+ gitignoreUpdated: ignored,
165
+ admin,
166
+ next: `Write ${ACCESS_FILE} with how to reach this app's host — credential LOCATIONS only.`,
167
+ };
168
+ }
169
+
170
+ // Report whether THIS machine has the (gitignored) access file; if not, name the
171
+ // admin to request it from.
172
+ function accessStatus(cwd) {
173
+ const present = fs.existsSync(path.join(cwd, ACCESS_FILE));
174
+ const admin = adminFor(cwd);
175
+ return {
176
+ present,
177
+ accessFile: ACCESS_FILE,
178
+ pointerPresent: fs.existsSync(path.join(cwd, ACCESS_POINTER)),
179
+ admin,
180
+ message: present
181
+ ? `${ACCESS_FILE} is present.`
182
+ : `${ACCESS_FILE} not found — request it from the project admin: ${admin}.`,
183
+ };
184
+ }
185
+
186
+ function dispatch(args, flags) {
187
+ const verb = args[0];
188
+ const cwd = flags.cwd || process.cwd();
189
+ const opts = { home: flags.home };
190
+ if (verb === 'list') {
191
+ return list(opts);
192
+ }
193
+ if (verb === 'show') {
194
+ return show(args[1], opts);
195
+ }
196
+ if (verb === 'path') {
197
+ const p = globalPath(opts);
198
+ return { path: p, raw: p };
199
+ }
200
+ if (verb === 'ensure') {
201
+ return ensure(opts);
202
+ }
203
+ if (verb === 'edit') {
204
+ return edit(opts);
205
+ }
206
+ if (verb === 'init-access') {
207
+ return initAccess(cwd);
208
+ }
209
+ if (verb === 'access') {
210
+ return accessStatus(cwd);
211
+ }
212
+ throw new Error(
213
+ `unknown deployment verb: ${verb || '(none)'} — use list|show|path|edit|ensure|init-access|access`,
214
+ );
215
+ }
216
+
217
+ module.exports = {
218
+ homeDir,
219
+ globalPath,
220
+ ensure,
221
+ parseMethods,
222
+ list,
223
+ show,
224
+ edit,
225
+ initAccess,
226
+ accessStatus,
227
+ dispatch,
228
+ ACCESS_FILE,
229
+ ACCESS_POINTER,
230
+ };
@@ -7,8 +7,17 @@ const fs = require('node:fs');
7
7
  const os = require('node:os');
8
8
  const path = require('node:path');
9
9
 
10
+ const deployment = require('./deployment.cjs');
11
+
10
12
  const PKG_ROOT = path.join(__dirname, '..', '..', '..');
11
13
 
14
+ // Part of setup: seed the user-global deployment-methods catalog (NEVER clobbered).
15
+ // It lives in the user's home (~/.verity), independent of the harness target dir.
16
+ function seedDeploymentMethods(opts) {
17
+ const seed = deployment.ensure({ home: opts.home });
18
+ return { ...seed, label: `${seed.path}${seed.created ? '' : ' (existing)'}` };
19
+ }
20
+
12
21
  function commandFiles(srcCommands) {
13
22
  return fs.readdirSync(srcCommands).filter((n) => n.endsWith('.md'));
14
23
  }
@@ -38,7 +47,11 @@ function installClaude(opts = {}) {
38
47
  copyInternals(target);
39
48
  installed.push('verity/');
40
49
 
41
- return { harness: 'claude', target, installed };
50
+ // 3. Seed the global deployment-methods catalog (setup step).
51
+ const deploymentMethods = seedDeploymentMethods(opts);
52
+ installed.push(deploymentMethods.label);
53
+
54
+ return { harness: 'claude', target, installed, deploymentMethods };
42
55
  }
43
56
 
44
57
  // --- OpenCode adapter ---
@@ -85,17 +98,20 @@ function installOpenCode(opts = {}) {
85
98
  copyInternals(target);
86
99
  installed.push('verity/');
87
100
 
88
- return { harness: 'opencode', target, installed };
101
+ const deploymentMethods = seedDeploymentMethods(opts);
102
+ installed.push(deploymentMethods.label);
103
+
104
+ return { harness: 'opencode', target, installed, deploymentMethods };
89
105
  }
90
106
 
91
107
  function dispatch(_args, flags) {
92
108
  if (flags.opencode) {
93
- return installOpenCode({ target: flags.target });
109
+ return installOpenCode({ target: flags.target, home: flags.home });
94
110
  }
95
111
  if (flags.codex || flags.gemini) {
96
112
  throw new Error('only the claude and opencode adapters are implemented so far');
97
113
  }
98
- return installClaude({ target: flags.target });
114
+ return installClaude({ target: flags.target, home: flags.home });
99
115
  }
100
116
 
101
117
  module.exports = {
@@ -75,18 +75,34 @@ function changelogFrom(commits, version) {
75
75
  return lines.join('\n').trim();
76
76
  }
77
77
 
78
+ // Prepend a changelog section, returning a rollback that restores the file to its
79
+ // exact prior state (content, or non-existence) — so a later failure can undo it.
78
80
  function prependChangelog(cwd, section) {
79
81
  const p = path.join(cwd, 'CHANGELOG.md');
82
+ const existedBefore = fs.existsSync(p);
83
+ const before = existedBefore ? fs.readFileSync(p, 'utf8') : null;
80
84
  const header = '# Changelog';
81
- const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8').replace(header, '').trim() : '';
85
+ const existing = before ? before.replace(header, '').trim() : '';
82
86
  const body = `${header}\n\n${section}\n\n${existing}`.trim();
83
87
  fs.writeFileSync(p, `${body}\n`);
88
+ return () => {
89
+ if (existedBefore) {
90
+ fs.writeFileSync(p, before);
91
+ } else {
92
+ fs.rmSync(p, { force: true });
93
+ }
94
+ };
84
95
  }
85
96
 
86
97
  function run(cmd, args) {
87
98
  execFileSync(cmd, args, { stdio: 'inherit' });
88
99
  }
89
100
 
101
+ // A release has three side effects (tag, changelog edit, push) that must be
102
+ // all-or-nothing: a half-done release leaves either a dirty CHANGELOG.md with no
103
+ // tag, or a local tag that never pushed. We order them cheap-and-reversible-first
104
+ // (tag → changelog → push) and roll back the earlier steps if a later one throws.
105
+ // The git runner is injectable (opts.run) so partial failure is unit-testable.
90
106
  function cut(cwd, opts = {}) {
91
107
  const tags = opts.tags || gitTags(cwd);
92
108
  const previous = ledger.latestTag(tags);
@@ -94,14 +110,34 @@ function cut(cwd, opts = {}) {
94
110
  const tag = `v${version}`;
95
111
  const commits = opts.commits || commitsSince(cwd, previous);
96
112
  const changelog = changelogFrom(commits, version);
97
- if (!opts.dryRun) {
98
- prependChangelog(cwd, changelog);
99
- run('git', ['-C', cwd, 'tag', tag]);
100
- if (opts.push !== false) {
101
- run('git', ['-C', cwd, 'push', 'origin', tag]);
113
+ const result = { version, tag, previous, changelog, commitCount: commits.length };
114
+ if (opts.dryRun) {
115
+ return { ...result, applied: false };
116
+ }
117
+
118
+ const exec = opts.run || run;
119
+ exec('git', ['-C', cwd, 'tag', tag]); // step 1 — if this throws, nothing changed yet
120
+
121
+ let restoreChangelog;
122
+ try {
123
+ restoreChangelog = prependChangelog(cwd, changelog); // step 2
124
+ } catch (err) {
125
+ exec('git', ['-C', cwd, 'tag', '-d', tag]); // roll back step 1
126
+ throw err;
127
+ }
128
+
129
+ if (opts.push !== false) {
130
+ try {
131
+ exec('git', ['-C', cwd, 'push', 'origin', tag]); // step 3
132
+ } catch (err) {
133
+ restoreChangelog(); // roll back step 2
134
+ exec('git', ['-C', cwd, 'tag', '-d', tag]); // roll back step 1
135
+ throw new Error(
136
+ `release push failed — rolled back tag ${tag} and CHANGELOG.md, working tree is clean. Original error: ${err.message}`,
137
+ );
102
138
  }
103
139
  }
104
- return { version, tag, previous, changelog, commitCount: commits.length, applied: !opts.dryRun };
140
+ return { ...result, applied: true };
105
141
  }
106
142
 
107
143
  function current(cwd) {
@@ -78,9 +78,36 @@ function buildScript(baseUrl, flow) {
78
78
  return lines.join('\n');
79
79
  }
80
80
 
81
- function defaultProbe(cwd) {
81
+ function resolvableFrom(pkg, cwd) {
82
+ try {
83
+ require.resolve(pkg, { paths: [cwd, process.cwd()] });
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ function onPath(bin, pathDirs) {
91
+ const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : [''];
92
+ return pathDirs.some((d) => exts.some((e) => fs.existsSync(path.join(d, bin + e))));
93
+ }
94
+
95
+ // Capability probe — is a headless browser usable here? Checks three honest sources
96
+ // so we don't false-skip when the tool is installed in a non-local layout:
97
+ // 1. the project's local node_modules/.bin
98
+ // 2. resolvable as a package from cwd (hoisted / monorepo / npm-linked global)
99
+ // 3. a CLI on PATH (a globally-installed playwright)
100
+ // Pure (no execution). If none match it returns unavailable, and runSmoke degrades
101
+ // to a non-pass — never a false green. Callers can bypass detection entirely by
102
+ // injecting opts.probe / opts.driver into runSmoke (e.g. a project-specific runner).
103
+ function defaultProbe(cwd, env = process.env) {
104
+ const pathDirs = (env.PATH || '').split(path.delimiter).filter(Boolean);
82
105
  for (const bin of ['playwright', 'puppeteer']) {
83
- if (fs.existsSync(path.join(cwd, 'node_modules', '.bin', bin))) {
106
+ if (
107
+ fs.existsSync(path.join(cwd, 'node_modules', '.bin', bin)) ||
108
+ resolvableFrom(bin, cwd) ||
109
+ onPath(bin, pathDirs)
110
+ ) {
84
111
  return { available: true, tool: bin };
85
112
  }
86
113
  }
@@ -26,6 +26,7 @@ const map = require('./lib/map.cjs');
26
26
  const recovery = require('./lib/recovery.cjs');
27
27
  const golive = require('./lib/golive.cjs');
28
28
  const smoke = require('./lib/smoke.cjs');
29
+ const deployment = require('./lib/deployment.cjs');
29
30
 
30
31
  function parseArgs(argv) {
31
32
  const positional = [];
@@ -154,6 +155,9 @@ const COMMANDS = {
154
155
  smoke(rest, flags) {
155
156
  return smoke.dispatch(rest, flags);
156
157
  },
158
+ deployment(rest, flags) {
159
+ return deployment.dispatch(rest, flags);
160
+ },
157
161
  };
158
162
 
159
163
  function main() {
@@ -0,0 +1,13 @@
1
+ # Deployment access — obtain from the project admin
2
+
3
+ This project deploys to a specific host. The access details live in
4
+ `.verity/deploy-access.md`, which is **intentionally gitignored** — it is never
5
+ committed, so no host or credential detail ever lands in git history.
6
+
7
+ **If you do not have `.verity/deploy-access.md` on your machine, request it from the
8
+ project admin: {{admin}}.** They maintain it and share it out-of-band (not through
9
+ this repo).
10
+
11
+ Maintainers: write or update the real file with `/verity:architect`, or by hand.
12
+ Keep it to credential *locations* (key files, SSO profiles, secret-store entries) —
13
+ never raw secrets.
@@ -0,0 +1,32 @@
1
+ # Your Verity deployment methods
2
+
3
+ > **Global to YOU, across every project.** This is your private inventory of the
4
+ > places you *can* deploy to. The Architect (`/verity:architect`) reads it with
5
+ > `verity deployment list` and works with you to choose a target for each app.
6
+ >
7
+ > ⚠️ **Reference credential LOCATIONS, never paste secrets.** Point at a key file,
8
+ > an SSO profile, or a secret-store entry — never an actual key, password, or token.
9
+ > This file lives at `~/.verity/deployment-methods.md`; it is NOT in any repo, but
10
+ > treat it as sensitive anyway.
11
+ >
12
+ > Edit it any time with `verity deployment edit` (or open the path printed by
13
+ > `verity deployment path`). Add one `## <id> — <Title>` section per method, and set
14
+ > `- **status:** active` once it is real (the two below are `example` — replace or
15
+ > delete them).
16
+
17
+ ## aws-ec2 — AWS EC2 over SSH (example — edit or delete)
18
+ - **status:** example
19
+ - **provider:** AWS
20
+ - **host:** `<ec2-public-dns-or-elastic-ip>`
21
+ - **user:** `ubuntu`
22
+ - **access:** `ssh -i ~/.ssh/<your-key>.pem ubuntu@<host>` — key *location* only
23
+ - **notes:** region `<e.g. us-east-1>`; the security group must allow your IP on
24
+ ports 22 (SSH) and 443 (HTTPS).
25
+
26
+ ## local-server — Local network server (example — edit or delete)
27
+ - **status:** example
28
+ - **provider:** self-hosted (LAN)
29
+ - **host:** `<hostname-or-10.0.0.x>`
30
+ - **user:** `<you>`
31
+ - **access:** `ssh <user>@<host>` over the local network / VPN
32
+ - **notes:** only reachable on the LAN or while connected to the VPN — not public.
@@ -7,3 +7,6 @@ dist/
7
7
  !.env.*.example
8
8
  # Verity's derived state cache — never authoritative (framework-spec §5)
9
9
  .verity-cache/
10
+ # Per-app deployment access (host + credential locations) — shared out-of-band,
11
+ # never committed. The committed pointer (.verity/deploy-access.README.md) stays.
12
+ .verity/deploy-access.md