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 +83 -20
- package/commands/verity/architect.md +24 -4
- package/package.json +1 -1
- package/verity/bin/lib/deployment.cjs +230 -0
- package/verity/bin/lib/install.cjs +20 -4
- package/verity/bin/lib/release.cjs +43 -7
- package/verity/bin/lib/smoke.cjs +29 -2
- package/verity/bin/verity.cjs +4 -0
- package/verity/templates/deploy-access-pointer.md.tmpl +13 -0
- package/verity/templates/deployment-methods.md.tmpl +32 -0
- package/verity/templates/gitignore.tmpl +3 -0
package/README.md
CHANGED
|
@@ -2,29 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
**Prompt to production, proven.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
9
|
+
> 📦 On npm as [`verity-framework`](https://www.npmjs.com/package/verity-framework) · works with **Claude Code** and **OpenCode**.
|
|
10
10
|
|
|
11
|
-
##
|
|
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
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
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
|
-
##
|
|
27
|
-
`verity-framework` (npm) · CLI binary: `verity` · Node ≥16 · host deps: `git`, `gh`
|
|
18
|
+
## How it works
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 {
|
|
140
|
+
return { ...result, applied: true };
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
function current(cwd) {
|
package/verity/bin/lib/smoke.cjs
CHANGED
|
@@ -78,9 +78,36 @@ function buildScript(baseUrl, flow) {
|
|
|
78
78
|
return lines.join('\n');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function
|
|
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 (
|
|
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
|
}
|
package/verity/bin/verity.cjs
CHANGED
|
@@ -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
|