@novare-technologies/nvaid 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -2
- package/dist/cli.js +476 -0
- package/package.json +46 -4
- package/templates/commands/apply.md +28 -0
- package/templates/commands/breakdown.md +27 -0
- package/templates/commands/contracts.md +35 -0
- package/templates/commands/intent.md +32 -0
- package/templates/nvaid/DESIGN-CODE-DICTIONARY.md +90 -0
- package/templates/nvaid/PROJECT-RULES.md +93 -0
- package/templates/nvaid/config.yml +74 -0
- package/templates/nvaid/intents/.gitkeep +0 -0
- package/templates/rules/ISSUE-TRACKER-TEMPLATE.md +221 -0
- package/templates/rules/backend-architecture-contract-generation-rules.md +56 -0
- package/templates/rules/customer-experience-contract-generation-rules.md +51 -0
- package/templates/rules/frontend-architecture-contract-generation-rules.md +56 -0
- package/templates/rules/quality-architecture-contract-generation-rules.md +86 -0
- package/templates/rules/solutions-architecture-contract-generation-rules.md +59 -0
- package/templates/rules/unit-breakdown-rules.md +121 -0
- package/templates/skills/backend/SKILL.md +34 -0
- package/templates/skills/customer-experience/SKILL.md +26 -0
- package/templates/skills/daily-construction-update/SKILL.md +27 -0
- package/templates/skills/frontend/SKILL.md +30 -0
- package/templates/skills/github/SKILL.md +142 -0
- package/templates/skills/pr-review/SKILL.md +119 -0
- package/templates/skills/project-ticketing/SKILL.md +26 -0
- package/templates/skills/quality/SKILL.md +31 -0
- package/templates/skills/r3-analytics/SKILL.md +26 -0
- package/templates/skills/release-plan/SKILL.md +27 -0
- package/templates/skills/solutions/SKILL.md +29 -0
- package/templates/skills/unit-breakdown/SKILL.md +31 -0
package/README.md
CHANGED
|
@@ -1,3 +1,138 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nvaid
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One CLI that installs an **AI-DLC** (AI Development Lifecycle) skills-and-workflow toolkit
|
|
4
|
+
into **any** coding agent, and houses a repo's intents, config, and design-code dictionary
|
|
5
|
+
in a single owned `nvaid/` folder.
|
|
6
|
+
|
|
7
|
+
Edit a skill once here, publish, and every consuming repo re-syncs with `nvaid update` —
|
|
8
|
+
no more hand-copying skills between repositories, or between one agent's config and
|
|
9
|
+
another's (`.claude/`, `.kiro/`, `.cursor/`, …).
|
|
10
|
+
|
|
11
|
+
## Install & use
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -g @novare-technologies/nvaid # or on demand: npx @novare-technologies/nvaid <command>
|
|
15
|
+
|
|
16
|
+
cd your-repo
|
|
17
|
+
nvaid init # pick coding agents, inject skills + slash commands, drop nvaid/
|
|
18
|
+
nvaid update # re-sync every configured agent to the installed nvaid version
|
|
19
|
+
nvaid validate # CI gate: fail if generated files are stale or hand-edited
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> Published under the `@novare-technologies` org scope as a **public** package, so anyone
|
|
23
|
+
> can install it. The typed command stays `nvaid` (that's the `bin`, independent of the
|
|
24
|
+
> package name).
|
|
25
|
+
|
|
26
|
+
`init` flags:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
nvaid init --agents claude,kiro,cursor # skip the prompt, name agents directly
|
|
30
|
+
nvaid init --owning-repo frontend # preset the config's owning_repo
|
|
31
|
+
nvaid init --yes # non-interactive (defaults to the AGENTS.md catch-all)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
- **Full skill set, no profiles.** `init` injects every skill into each selected agent. A
|
|
37
|
+
repo declares what it *is* in `nvaid/config.yml` (`owning_repo`), rather than receiving a
|
|
38
|
+
trimmed-down skill set.
|
|
39
|
+
- **Any coding agent.** `init` shows a multi-select. Each agent is a *path + format adapter*
|
|
40
|
+
(`src/agents.ts`) over one neutral source — adding a new agent is a new registry entry,
|
|
41
|
+
not new content. An `AGENTS.md` catch-all covers any tool that reads the cross-agent
|
|
42
|
+
standard but has no bespoke adapter.
|
|
43
|
+
- **The `nvaid/` folder is hand-owned, content only** — `config.yml`, `PROJECT-RULES.md`,
|
|
44
|
+
`DESIGN-CODE-DICTIONARY.md`, and `intents/`. `update` never touches these. The *generated*
|
|
45
|
+
skill/command files live in each agent's own directory and carry a "do not edit" header.
|
|
46
|
+
- **`PROJECT-RULES.md` and `DESIGN-CODE-DICTIONARY.md` are per-project templates**, not
|
|
47
|
+
shipped content: `init` drops fill-in skeletons for a developer to own. nvaid ships only
|
|
48
|
+
the universal skills and the contract/unit-decomposition rules.
|
|
49
|
+
- **Drift is caught, not just avoided.** `validate` fails (exit 1) if a repo's generated
|
|
50
|
+
files are stale (behind the installed version), missing, or hand-edited — turning silent
|
|
51
|
+
divergence into a failing CI check.
|
|
52
|
+
|
|
53
|
+
## The workflow (slash commands)
|
|
54
|
+
|
|
55
|
+
`init` installs four gated commands. They run in order, with a human gate after the intent:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
/nvaid:intent draft the intent — business need + success criteria only, then STOP
|
|
59
|
+
⏸ stakeholder approval
|
|
60
|
+
/nvaid:contracts draft the per-role architecture contracts
|
|
61
|
+
/nvaid:breakdown cut into units (each tagged with its owning repo) — the final cut
|
|
62
|
+
/nvaid:apply generate per-repo unit docs + open construction branches
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Contracts precede the breakdown deliberately (the contract skills run across the
|
|
66
|
+
elaboration phases; the unit-breakdown is their final output).
|
|
67
|
+
|
|
68
|
+
## Repository layout
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
nvaid/
|
|
72
|
+
package.json # bin: nvaid; ships dist/ + templates/
|
|
73
|
+
tsconfig.json
|
|
74
|
+
src/
|
|
75
|
+
cli.ts # command wiring (commander)
|
|
76
|
+
agents.ts # the agent registry — 9 bespoke + an AGENTS.md catch-all
|
|
77
|
+
paths.ts # resolves the bundled templates/
|
|
78
|
+
config.ts # read/patch a repo's nvaid/config.yml
|
|
79
|
+
scaffold.ts # drop the nvaid/ folder + sync nvaid/rules
|
|
80
|
+
generate.ts # neutral source -> per-agent files (+ drift diff)
|
|
81
|
+
commands/
|
|
82
|
+
init.ts # agent multi-select + inject + scaffold nvaid/
|
|
83
|
+
update.ts # re-sync configured agents
|
|
84
|
+
validate.ts # drift gate
|
|
85
|
+
templates/ # the payload shipped in the package
|
|
86
|
+
skills/ # 12 skills (6 ceremony + frontend + backend + 4 tool)
|
|
87
|
+
rules/ # 7 flow/rule files (contract-gen, unit-breakdown, tracker)
|
|
88
|
+
commands/ # the 4 slash-command defs
|
|
89
|
+
nvaid/ # the folder init drops into a repo (all hand-owned)
|
|
90
|
+
config.yml
|
|
91
|
+
PROJECT-RULES.md # fill-in template
|
|
92
|
+
DESIGN-CODE-DICTIONARY.md # fill-in template (a technical README for the code)
|
|
93
|
+
intents/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Skills included (`templates/skills/`)
|
|
97
|
+
|
|
98
|
+
- **Ceremony:** `customer-experience`, `solutions`, `quality`, `unit-breakdown`,
|
|
99
|
+
`r3-analytics`, `release-plan`
|
|
100
|
+
- **Tier:** `frontend`, `backend`
|
|
101
|
+
- **Tool:** `github`, `project-ticketing`, `pr-review`, `daily-construction-update`
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install
|
|
107
|
+
npm run typecheck
|
|
108
|
+
npm run dev -- init --agents claude # run the CLI from source (tsx)
|
|
109
|
+
npm run build # bundle src/ -> dist/cli.js (tsup)
|
|
110
|
+
npm test # vitest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Status & next steps
|
|
114
|
+
|
|
115
|
+
The CLI (`init` / `update` / `validate`) is implemented and runs end-to-end. Still open:
|
|
116
|
+
|
|
117
|
+
- Tests (`vitest`) for the generator and the agent adapters.
|
|
118
|
+
- Confirm each agent's target path against its current docs before relying on its adapter
|
|
119
|
+
(agent conventions change often; `AGENTS.md` is the safe default).
|
|
120
|
+
### Non-JS repos (Python, Go, etc.)
|
|
121
|
+
|
|
122
|
+
No special delivery path needed. nvaid only *writes files* — skills, the guardrail
|
|
123
|
+
templates, and the `nvaid/` folder — and exits; the output has no runtime dependency on
|
|
124
|
+
Node. A Python repo just runs the command once:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx @novare-technologies/nvaid init
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Nothing is added to the target project itself (no `devDependency`, no Node toolchain in
|
|
131
|
+
the repo). The only requirement is that Node is available on the developer's machine or CI
|
|
132
|
+
to invoke the CLI — a one-time "have Node installed to run a scaffolding tool" ask, not an
|
|
133
|
+
ongoing footprint. (A prebuilt binary or pipx wrapper was considered and dropped as
|
|
134
|
+
unnecessary.)
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
Authored and maintained by **Novare SQE Armory**.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/agents.ts
|
|
11
|
+
var AGENTS = [
|
|
12
|
+
{
|
|
13
|
+
id: "claude",
|
|
14
|
+
label: "Claude Code",
|
|
15
|
+
skillsPath: ".claude/skills",
|
|
16
|
+
instructionsFile: "CLAUDE.md",
|
|
17
|
+
format: "markdown",
|
|
18
|
+
bespoke: true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "kiro",
|
|
22
|
+
label: "Kiro",
|
|
23
|
+
skillsPath: ".kiro/skills",
|
|
24
|
+
instructionsFile: ".kiro/steering/nvaid.md",
|
|
25
|
+
format: "markdown",
|
|
26
|
+
bespoke: true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "cursor",
|
|
30
|
+
label: "Cursor",
|
|
31
|
+
skillsPath: ".cursor/rules",
|
|
32
|
+
format: "mdc",
|
|
33
|
+
bespoke: true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "copilot",
|
|
37
|
+
label: "GitHub Copilot",
|
|
38
|
+
skillsPath: ".github",
|
|
39
|
+
instructionsFile: ".github/copilot-instructions.md",
|
|
40
|
+
format: "markdown",
|
|
41
|
+
bespoke: true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "windsurf",
|
|
45
|
+
label: "Windsurf",
|
|
46
|
+
skillsPath: ".windsurf/rules",
|
|
47
|
+
format: "markdown",
|
|
48
|
+
bespoke: true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "amazonq",
|
|
52
|
+
label: "Amazon Q",
|
|
53
|
+
skillsPath: ".amazonq/rules",
|
|
54
|
+
format: "markdown",
|
|
55
|
+
bespoke: true
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "cline",
|
|
59
|
+
label: "Cline",
|
|
60
|
+
skillsPath: ".cline/rules",
|
|
61
|
+
instructionsFile: ".clinerules",
|
|
62
|
+
format: "markdown",
|
|
63
|
+
bespoke: true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "gemini",
|
|
67
|
+
label: "Gemini Code Assist",
|
|
68
|
+
skillsPath: ".gemini",
|
|
69
|
+
instructionsFile: ".gemini/styleguide.md",
|
|
70
|
+
format: "markdown",
|
|
71
|
+
bespoke: true
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "aider",
|
|
75
|
+
label: "Aider",
|
|
76
|
+
skillsPath: ".nvaid/skills",
|
|
77
|
+
instructionsFile: "AGENTS.md",
|
|
78
|
+
format: "markdown",
|
|
79
|
+
bespoke: true
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "agents-md",
|
|
83
|
+
label: "Any AGENTS.md-compatible tool (catch-all)",
|
|
84
|
+
skillsPath: ".nvaid/skills",
|
|
85
|
+
instructionsFile: "AGENTS.md",
|
|
86
|
+
format: "markdown",
|
|
87
|
+
bespoke: false
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
function getAgent(id) {
|
|
91
|
+
return AGENTS.find((a) => a.id === id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/generate.ts
|
|
95
|
+
import { readFile, writeFile, mkdir, readdir, rm } from "fs/promises";
|
|
96
|
+
import { existsSync } from "fs";
|
|
97
|
+
import { join, relative } from "path";
|
|
98
|
+
|
|
99
|
+
// src/paths.ts
|
|
100
|
+
import { fileURLToPath } from "url";
|
|
101
|
+
import { dirname, resolve } from "path";
|
|
102
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
103
|
+
var TEMPLATES_DIR = resolve(here, "..", "templates");
|
|
104
|
+
var SKILLS_SRC = resolve(TEMPLATES_DIR, "skills");
|
|
105
|
+
var RULES_SRC = resolve(TEMPLATES_DIR, "rules");
|
|
106
|
+
var COMMANDS_SRC = resolve(TEMPLATES_DIR, "commands");
|
|
107
|
+
var NVAID_FOLDER_SRC = resolve(TEMPLATES_DIR, "nvaid");
|
|
108
|
+
var NVAID_DIR_NAME = "nvaid";
|
|
109
|
+
var CONFIG_FILE = "config.yml";
|
|
110
|
+
var AIDLC_ROOT_TOKEN = "{{AIDLC_ROOT}}";
|
|
111
|
+
async function nvaidVersion() {
|
|
112
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
113
|
+
const pkg = JSON.parse(await readFile3(resolve(TEMPLATES_DIR, "..", "package.json"), "utf8"));
|
|
114
|
+
return pkg.version;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/generate.ts
|
|
118
|
+
var DO_NOT_EDIT = "<!-- generated by nvaid \u2014 do not edit. Run `nvaid update` to regenerate. -->";
|
|
119
|
+
function aidlcRootFor(agent) {
|
|
120
|
+
void agent;
|
|
121
|
+
return "nvaid/rules";
|
|
122
|
+
}
|
|
123
|
+
function withHeader(body, format) {
|
|
124
|
+
if (format === "mdc") {
|
|
125
|
+
return `${DO_NOT_EDIT}
|
|
126
|
+
${body}`;
|
|
127
|
+
}
|
|
128
|
+
return `${DO_NOT_EDIT}
|
|
129
|
+
|
|
130
|
+
${body}`;
|
|
131
|
+
}
|
|
132
|
+
async function listSkillDirs() {
|
|
133
|
+
const entries = await readdir(SKILLS_SRC, { withFileTypes: true });
|
|
134
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
135
|
+
}
|
|
136
|
+
async function listCommandFiles() {
|
|
137
|
+
const entries = await readdir(COMMANDS_SRC, { withFileTypes: true });
|
|
138
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
139
|
+
}
|
|
140
|
+
async function renderTemplate(srcFile, agent) {
|
|
141
|
+
const raw = await readFile(srcFile, "utf8");
|
|
142
|
+
const substituted = raw.split(AIDLC_ROOT_TOKEN).join(aidlcRootFor(agent));
|
|
143
|
+
return withHeader(substituted, agent.format);
|
|
144
|
+
}
|
|
145
|
+
function skillTarget(projectRoot, agent, skill) {
|
|
146
|
+
const ext = agent.format === "mdc" ? "mdc" : "md";
|
|
147
|
+
return join(projectRoot, agent.skillsPath, `${skill}.${ext}`);
|
|
148
|
+
}
|
|
149
|
+
function commandTarget(projectRoot, agent, cmdFile) {
|
|
150
|
+
const base = cmdFile.replace(/\.md$/, "");
|
|
151
|
+
const ext = agent.format === "mdc" ? "mdc" : "md";
|
|
152
|
+
return join(projectRoot, agent.skillsPath, `commands`, `nvaid-${base}.${ext}`);
|
|
153
|
+
}
|
|
154
|
+
async function writeFileEnsuringDir(path, body, force, result) {
|
|
155
|
+
if (!force && existsSync(path)) {
|
|
156
|
+
result.skipped.push(path);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
160
|
+
await writeFile(path, body, "utf8");
|
|
161
|
+
result.written.push(path);
|
|
162
|
+
}
|
|
163
|
+
async function generateAgentFiles(opts) {
|
|
164
|
+
const result = { written: [], skipped: [] };
|
|
165
|
+
const skills = await listSkillDirs();
|
|
166
|
+
const commands = await listCommandFiles();
|
|
167
|
+
for (const agent of opts.agents) {
|
|
168
|
+
for (const skill of skills) {
|
|
169
|
+
const src = join(SKILLS_SRC, skill, "SKILL.md");
|
|
170
|
+
if (!existsSync(src)) continue;
|
|
171
|
+
const body = await renderTemplate(src, agent);
|
|
172
|
+
await writeFileEnsuringDir(skillTarget(opts.projectRoot, agent, skill), body, opts.force, result);
|
|
173
|
+
}
|
|
174
|
+
for (const cmd of commands) {
|
|
175
|
+
const body = await renderTemplate(join(COMMANDS_SRC, cmd), agent);
|
|
176
|
+
await writeFileEnsuringDir(commandTarget(opts.projectRoot, agent, cmd), body, opts.force, result);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
async function diffAgentFiles(opts) {
|
|
182
|
+
const drift = [];
|
|
183
|
+
const skills = await listSkillDirs();
|
|
184
|
+
const commands = await listCommandFiles();
|
|
185
|
+
const checks = [];
|
|
186
|
+
for (const agent of opts.agents) {
|
|
187
|
+
for (const skill of skills) {
|
|
188
|
+
const src = join(SKILLS_SRC, skill, "SKILL.md");
|
|
189
|
+
if (existsSync(src)) checks.push({ target: skillTarget(opts.projectRoot, agent, skill), src });
|
|
190
|
+
}
|
|
191
|
+
for (const cmd of commands) {
|
|
192
|
+
checks.push({ target: commandTarget(opts.projectRoot, agent, cmd), src: join(COMMANDS_SRC, cmd) });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const { target, src } of checks) {
|
|
196
|
+
const agent = opts.agents.find((a) => target.includes(a.skillsPath.replace(/^\.\//, "")));
|
|
197
|
+
const expected = await renderTemplate(src, agent ?? opts.agents[0]);
|
|
198
|
+
if (!existsSync(target)) {
|
|
199
|
+
drift.push(`${relative(opts.projectRoot, target)} (missing)`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const actual = await readFile(target, "utf8");
|
|
203
|
+
if (actual !== expected) drift.push(relative(opts.projectRoot, target));
|
|
204
|
+
}
|
|
205
|
+
return drift;
|
|
206
|
+
}
|
|
207
|
+
async function cleanAgentDirs(projectRoot, agents) {
|
|
208
|
+
for (const agent of agents) {
|
|
209
|
+
const dir = join(projectRoot, agent.skillsPath);
|
|
210
|
+
const commandsDir = join(dir, "commands");
|
|
211
|
+
if (existsSync(commandsDir)) await rm(commandsDir, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/scaffold.ts
|
|
216
|
+
import { cp, mkdir as mkdir2, readdir as readdir2 } from "fs/promises";
|
|
217
|
+
import { existsSync as existsSync2 } from "fs";
|
|
218
|
+
import { join as join2 } from "path";
|
|
219
|
+
async function scaffoldNvaidFolder(projectRoot) {
|
|
220
|
+
const dest = join2(projectRoot, NVAID_DIR_NAME);
|
|
221
|
+
await mkdir2(dest, { recursive: true });
|
|
222
|
+
const written = [];
|
|
223
|
+
await copyTreeSkipExisting(NVAID_FOLDER_SRC, dest, written);
|
|
224
|
+
return written;
|
|
225
|
+
}
|
|
226
|
+
async function syncRules(projectRoot) {
|
|
227
|
+
const dest = join2(projectRoot, NVAID_DIR_NAME, "rules");
|
|
228
|
+
await mkdir2(dest, { recursive: true });
|
|
229
|
+
await cp(RULES_SRC, dest, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
async function copyTreeSkipExisting(src, dest, written) {
|
|
232
|
+
const entries = await readdir2(src, { withFileTypes: true });
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
const s = join2(src, entry.name);
|
|
235
|
+
const d = join2(dest, entry.name);
|
|
236
|
+
if (entry.isDirectory()) {
|
|
237
|
+
await mkdir2(d, { recursive: true });
|
|
238
|
+
await copyTreeSkipExisting(s, d, written);
|
|
239
|
+
} else {
|
|
240
|
+
if (existsSync2(d)) continue;
|
|
241
|
+
await cp(s, d);
|
|
242
|
+
written.push(d);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/config.ts
|
|
248
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
249
|
+
import { existsSync as existsSync3 } from "fs";
|
|
250
|
+
import { join as join3 } from "path";
|
|
251
|
+
import { parse } from "yaml";
|
|
252
|
+
var EMPTY_STACK = {
|
|
253
|
+
languages: [],
|
|
254
|
+
runtime: "",
|
|
255
|
+
frameworks: [],
|
|
256
|
+
package_manager: "",
|
|
257
|
+
build: "",
|
|
258
|
+
test: "",
|
|
259
|
+
lint_format: "",
|
|
260
|
+
database: "",
|
|
261
|
+
deploy: "",
|
|
262
|
+
additional: {}
|
|
263
|
+
};
|
|
264
|
+
function configPath(projectRoot) {
|
|
265
|
+
return join3(projectRoot, NVAID_DIR_NAME, CONFIG_FILE);
|
|
266
|
+
}
|
|
267
|
+
function hasConfig(projectRoot) {
|
|
268
|
+
return existsSync3(configPath(projectRoot));
|
|
269
|
+
}
|
|
270
|
+
async function readConfig(projectRoot) {
|
|
271
|
+
const raw = await readFile2(configPath(projectRoot), "utf8");
|
|
272
|
+
const parsed = parse(raw) ?? {};
|
|
273
|
+
return {
|
|
274
|
+
schema: parsed.schema ?? "nvaid/v1",
|
|
275
|
+
project_name: parsed.project_name ?? "",
|
|
276
|
+
owning_repo: parsed.owning_repo ?? "",
|
|
277
|
+
tech_stack: { ...EMPTY_STACK, ...parsed.tech_stack ?? {} },
|
|
278
|
+
agents: Array.isArray(parsed.agents) ? parsed.agents : [],
|
|
279
|
+
context: parsed.context ?? "",
|
|
280
|
+
generated_with: parsed.generated_with ?? ""
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function setConfigFields(projectRoot, fields) {
|
|
284
|
+
const path = configPath(projectRoot);
|
|
285
|
+
let text = await readFile2(path, "utf8");
|
|
286
|
+
if (fields.project_name !== void 0) {
|
|
287
|
+
text = replaceScalar(text, "project_name", `"${fields.project_name}"`);
|
|
288
|
+
}
|
|
289
|
+
if (fields.owning_repo !== void 0) {
|
|
290
|
+
text = replaceScalar(text, "owning_repo", `"${fields.owning_repo}"`);
|
|
291
|
+
}
|
|
292
|
+
if (fields.generated_with !== void 0) {
|
|
293
|
+
text = replaceScalar(text, "generated_with", `"${fields.generated_with}"`);
|
|
294
|
+
}
|
|
295
|
+
if (fields.agents !== void 0) {
|
|
296
|
+
text = replaceAgents(text, fields.agents);
|
|
297
|
+
}
|
|
298
|
+
await writeFile2(path, text, "utf8");
|
|
299
|
+
}
|
|
300
|
+
function replaceScalar(text, key, value) {
|
|
301
|
+
const re = new RegExp(`^(${key}:).*$`, "m");
|
|
302
|
+
return re.test(text) ? text.replace(re, `$1 ${value}`) : `${text}
|
|
303
|
+
${key}: ${value}
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
function replaceAgents(text, agents) {
|
|
307
|
+
const flow = agents.length ? `[${agents.map((a) => `"${a}"`).join(", ")}]` : "[]";
|
|
308
|
+
const re = /^agents:.*(?:\n[ \t]*-.*)*$/m;
|
|
309
|
+
const line = `agents: ${flow}`;
|
|
310
|
+
return re.test(text) ? text.replace(re, line) : `${text}
|
|
311
|
+
${line}
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/commands/init.ts
|
|
316
|
+
async function runInit(opts) {
|
|
317
|
+
const projectRoot = process.cwd();
|
|
318
|
+
if (hasConfig(projectRoot) && !opts.yes) {
|
|
319
|
+
const proceed = await p.confirm({
|
|
320
|
+
message: "This repo already has nvaid/config.yml. Re-run init anyway (update is usually what you want)?",
|
|
321
|
+
initialValue: false
|
|
322
|
+
});
|
|
323
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
324
|
+
p.cancel("Nothing changed. Run `nvaid update` to re-sync agents.");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
let selected;
|
|
329
|
+
if (opts.agents?.length) {
|
|
330
|
+
selected = opts.agents.map(resolveAgentOrThrow);
|
|
331
|
+
} else if (opts.yes) {
|
|
332
|
+
selected = [getAgent("agents-md")];
|
|
333
|
+
} else {
|
|
334
|
+
p.intro(pc.bold("nvaid init"));
|
|
335
|
+
const choice = await p.multiselect({
|
|
336
|
+
message: "Select the coding agents to set up (space to toggle, enter to confirm):",
|
|
337
|
+
options: AGENTS.map((a) => ({ value: a.id, label: a.label, hint: a.skillsPath })),
|
|
338
|
+
required: true
|
|
339
|
+
});
|
|
340
|
+
if (p.isCancel(choice)) {
|
|
341
|
+
p.cancel("Cancelled.");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
selected = choice.map(resolveAgentOrThrow);
|
|
345
|
+
}
|
|
346
|
+
const gen = await generateAgentFiles({ projectRoot, agents: selected, force: false });
|
|
347
|
+
const scaffolded = await scaffoldNvaidFolder(projectRoot);
|
|
348
|
+
await syncRules(projectRoot);
|
|
349
|
+
await setConfigFields(projectRoot, {
|
|
350
|
+
agents: selected.map((a) => a.id),
|
|
351
|
+
owning_repo: opts.owningRepo ?? "<set-me>",
|
|
352
|
+
...opts.projectName ? { project_name: opts.projectName } : {},
|
|
353
|
+
generated_with: await nvaidVersion()
|
|
354
|
+
});
|
|
355
|
+
p.note(
|
|
356
|
+
[
|
|
357
|
+
`agents: ${selected.map((a) => a.label).join(", ")}`,
|
|
358
|
+
`written: ${gen.written.length} agent files` + (gen.skipped.length ? `, ${gen.skipped.length} skipped (existing)` : ""),
|
|
359
|
+
`nvaid/: ${scaffolded.length ? scaffolded.length + " files created" : "already present"}`
|
|
360
|
+
].join("\n"),
|
|
361
|
+
"Done"
|
|
362
|
+
);
|
|
363
|
+
p.outro(
|
|
364
|
+
[
|
|
365
|
+
"Next, in your AI chat:",
|
|
366
|
+
' /nvaid:intent "..." draft the intent, then STOP for stakeholder approval',
|
|
367
|
+
" /nvaid:contracts after approval \u2014 draft the per-role contracts",
|
|
368
|
+
" /nvaid:breakdown cut into units (tagged by owning repo)",
|
|
369
|
+
" /nvaid:apply generate per-repo unit docs + branches",
|
|
370
|
+
"",
|
|
371
|
+
"Fill in nvaid/PROJECT-RULES.md and set owning_repo in nvaid/config.yml."
|
|
372
|
+
].join("\n")
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
function resolveAgentOrThrow(id) {
|
|
376
|
+
const a = getAgent(id);
|
|
377
|
+
if (!a) throw new Error(`Unknown agent id: "${id}". Known: ${AGENTS.map((x) => x.id).join(", ")}`);
|
|
378
|
+
return a;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/commands/update.ts
|
|
382
|
+
import * as p2 from "@clack/prompts";
|
|
383
|
+
import pc2 from "picocolors";
|
|
384
|
+
async function runUpdate() {
|
|
385
|
+
const projectRoot = process.cwd();
|
|
386
|
+
if (!hasConfig(projectRoot)) {
|
|
387
|
+
throw new Error("No nvaid/config.yml here. Run `nvaid init` first.");
|
|
388
|
+
}
|
|
389
|
+
const cfg = await readConfig(projectRoot);
|
|
390
|
+
if (!cfg.agents.length) {
|
|
391
|
+
throw new Error("config.yml lists no agents. Run `nvaid init` to select some.");
|
|
392
|
+
}
|
|
393
|
+
const agents = cfg.agents.map((id) => {
|
|
394
|
+
const a = getAgent(id);
|
|
395
|
+
if (!a) throw new Error(`config.yml references unknown agent "${id}".`);
|
|
396
|
+
return a;
|
|
397
|
+
});
|
|
398
|
+
const version = await nvaidVersion();
|
|
399
|
+
p2.intro(pc2.bold(`nvaid update \u2192 ${version}`));
|
|
400
|
+
await cleanAgentDirs(projectRoot, agents);
|
|
401
|
+
const gen = await generateAgentFiles({ projectRoot, agents, force: true });
|
|
402
|
+
await syncRules(projectRoot);
|
|
403
|
+
await setConfigFields(projectRoot, { generated_with: version });
|
|
404
|
+
p2.note(
|
|
405
|
+
[
|
|
406
|
+
`agents: ${agents.map((a) => a.label).join(", ")}`,
|
|
407
|
+
`rewrote: ${gen.written.length} agent files`,
|
|
408
|
+
`rules: refreshed nvaid/rules/`,
|
|
409
|
+
`was: ${cfg.generated_with || "unknown"} \u2192 now: ${version}`
|
|
410
|
+
].join("\n"),
|
|
411
|
+
"Updated"
|
|
412
|
+
);
|
|
413
|
+
p2.outro("Hand-owned files (config.yml, PROJECT-RULES.md, DESIGN-CODE-DICTIONARY.md, intents/) untouched.");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/commands/validate.ts
|
|
417
|
+
import pc3 from "picocolors";
|
|
418
|
+
async function runValidate() {
|
|
419
|
+
const projectRoot = process.cwd();
|
|
420
|
+
if (!hasConfig(projectRoot)) {
|
|
421
|
+
console.error(pc3.red("\u2717 No nvaid/config.yml here. Run `nvaid init` first."));
|
|
422
|
+
return 1;
|
|
423
|
+
}
|
|
424
|
+
const cfg = await readConfig(projectRoot);
|
|
425
|
+
const agents = [];
|
|
426
|
+
for (const id of cfg.agents) {
|
|
427
|
+
const a = getAgent(id);
|
|
428
|
+
if (!a) {
|
|
429
|
+
console.error(pc3.red(`\u2717 config.yml references unknown agent "${id}".`));
|
|
430
|
+
return 1;
|
|
431
|
+
}
|
|
432
|
+
agents.push(a);
|
|
433
|
+
}
|
|
434
|
+
const version = await nvaidVersion();
|
|
435
|
+
let failed = false;
|
|
436
|
+
if (cfg.generated_with && cfg.generated_with !== version) {
|
|
437
|
+
console.error(
|
|
438
|
+
pc3.yellow(
|
|
439
|
+
`\u26A0 generated with nvaid ${cfg.generated_with}, installed is ${version}. Run \`nvaid update\`.`
|
|
440
|
+
)
|
|
441
|
+
);
|
|
442
|
+
failed = true;
|
|
443
|
+
}
|
|
444
|
+
const drift = await diffAgentFiles({ projectRoot, agents, force: true });
|
|
445
|
+
if (drift.length) {
|
|
446
|
+
console.error(pc3.red(`\u2717 ${drift.length} generated file(s) stale or hand-edited:`));
|
|
447
|
+
for (const f of drift) console.error(pc3.red(` ${f}`));
|
|
448
|
+
console.error(pc3.dim(" Run `nvaid update` to regenerate (hand edits belong in the nvaid source, not here)."));
|
|
449
|
+
failed = true;
|
|
450
|
+
}
|
|
451
|
+
if (failed) return 1;
|
|
452
|
+
console.log(pc3.green(`\u2713 nvaid ${version}: ${agents.length} agent(s) in sync, no drift.`));
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/cli.ts
|
|
457
|
+
var program = new Command();
|
|
458
|
+
program.name("nvaid").description("nvaid \u2014 install an AI-DLC skills + workflow toolkit into any coding agent").version("0.1.0");
|
|
459
|
+
program.command("init").description("Onboard this repo: select coding agents, inject skills, create the nvaid/ folder").option("--agents <ids>", "comma-separated agent ids (skips the prompt)", (v) => v.split(",")).option("--owning-repo <tier>", "owning_repo value written to config.yml").option("--project-name <name>", "project_name value written to config.yml").option("-y, --yes", "accept defaults, no prompts (CI)").action(async (opts) => {
|
|
460
|
+
await runInit({
|
|
461
|
+
agents: opts.agents,
|
|
462
|
+
owningRepo: opts.owningRepo,
|
|
463
|
+
projectName: opts.projectName,
|
|
464
|
+
yes: opts.yes
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
program.command("update").description("Re-sync agent files to the installed nvaid version").action(async () => {
|
|
468
|
+
await runUpdate();
|
|
469
|
+
});
|
|
470
|
+
program.command("validate").description("Fail if generated agent files are stale or hand-edited").action(async () => {
|
|
471
|
+
process.exit(await runValidate());
|
|
472
|
+
});
|
|
473
|
+
program.parseAsync().catch((err) => {
|
|
474
|
+
console.error(err instanceof Error ? err.message : err);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novare-technologies/nvaid",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "nvaid — one CLI that installs an AI-DLC skills-and-workflow toolkit into any coding agent, and houses intents, config, and the design-code dictionary in a single nvaid/ folder.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nvaid": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/cli.ts --format esm --clean",
|
|
18
|
+
"dev": "tsx src/cli.ts",
|
|
19
|
+
"test": "vitest run --passWithNoTests",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai-dlc",
|
|
25
|
+
"coding-agent",
|
|
26
|
+
"skills",
|
|
27
|
+
"scaffold",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"author": "Novare SQE Armory",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@clack/prompts": "^0.7.0",
|
|
37
|
+
"commander": "^12.0.0",
|
|
38
|
+
"picocolors": "^1.0.0",
|
|
39
|
+
"yaml": "^2.4.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.0.0",
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"tsx": "^4.7.0",
|
|
45
|
+
"typescript": "^5.4.0",
|
|
46
|
+
"vitest": "^1.4.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
command: nvaid:apply
|
|
3
|
+
summary: Generate per-repo unit design docs and open construction branches for an approved breakdown.
|
|
4
|
+
stage: "Fig. 2 Stage 4+ — Per-Role Authoring / Construction"
|
|
5
|
+
precondition: unit-breakdown approved (Decomposition-Approved gate green)
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /nvaid:apply
|
|
9
|
+
|
|
10
|
+
Turn an **approved** unit-breakdown into construction-ready work: for each Unit, generate
|
|
11
|
+
its design doc in the Owning Repo named by the breakdown, and open the construction branch
|
|
12
|
+
per the `github` Skill.
|
|
13
|
+
|
|
14
|
+
## Precondition
|
|
15
|
+
|
|
16
|
+
Only run once the unit-breakdown is approved. If it's still awaiting the gate, stop.
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
1. Read the approved `unit-breakdown.md` and its Units + Owning Repo tags.
|
|
21
|
+
2. For each Unit, in its Owning Repo:
|
|
22
|
+
- generate `nvaid/intents/<INTENT-ID>-<slug>/units/<UNIT-ID>-design.md` with the Unit's
|
|
23
|
+
own Acceptance Criteria, referencing the relevant contracts (`covers_units`).
|
|
24
|
+
- cut the construction branch (`feature/<story-id>-<slug>` or `bugfix/...`) per the
|
|
25
|
+
`github` Skill.
|
|
26
|
+
3. Create/update the GitHub Project items per `ISSUE-TRACKER-TEMPLATE.md` (Epic per Intent,
|
|
27
|
+
Story/Bug per Unit) — links, not pasted content.
|
|
28
|
+
4. Hand off to Construction. Do not implement code here.
|