nova-spec 1.0.4 → 1.0.6

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/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nova-spec — framework AGENTS.md
2
2
 
3
- > This repo uses CLAUDE.md as a stub → AGENTS.md is the instructions file.
3
+ > This repo uses `CLAUDE.md` as a stub → `AGENTS.md` is the instructions file.
4
4
 
5
5
  This repo is the **nova-spec framework itself** — a Spec-Driven Development (SDD) system for Claude Code. It's designed to be installed into other projects.
6
6
 
@@ -8,6 +8,22 @@ This repo is the **nova-spec framework itself** — a Spec-Driven Development (S
8
8
 
9
9
  Framework that orchestrates ticket-to-PR workflow with 7 slash commands: `/nova-start` → `/nova-spec` → `/nova-plan` → `/nova-build` → `/nova-review` → `/nova-wrap` → `/nova-status`.
10
10
 
11
+ ## Installation (consumer repos)
12
+
13
+ To install nova-spec into another repo, run:
14
+
15
+ ```bash
16
+ npx nova-spec init
17
+ ```
18
+
19
+ That leaves the repo usable without manual steps:
20
+
21
+ - `AGENTS.md` and `CLAUDE.md` (shim that points to `AGENTS.md`)
22
+ - `novaspec/` framework files
23
+ - `.claude/` symlinks (and optionally `.opencode/`)
24
+ - `context/` memory structure
25
+ - `novaspec/config.yml` is **gitignored** (local per-repo configuration)
26
+
11
27
  ## Key commands
12
28
 
13
29
  - `/nova-start <TICKET>` — classify ticket, create branch, load context
@@ -52,8 +68,13 @@ At the start of every session, check if nova-spec has pending updates by running
52
68
 
53
69
  If `novaspec/.nova-manifest.json` exists and the installed version matches the latest, skip it silently. If there are updates or outdated custom overrides, report them to the user before proceeding.
54
70
 
55
- > **Claude Code** handles this automatically via a `SessionStart` hook in `.claude/settings.json`.
56
- > **OpenCode** relies on this instruction always run `/nova-sync` at session start.
71
+ This repo (and consumer repos) use `npx nova-spec sync` to update stock files **idempotently** without clobbering local edits.
72
+ Sync records what was last shipped to the repo in `novaspec/.nova-manifest.json`, and only updates files that are still untouched locally.
73
+
74
+ Hooks are maintained in:
75
+
76
+ - `.claude/settings.local.json` (Claude Code)
77
+ - `.opencode/settings.local.json` (OpenCode)
57
78
 
58
79
  ## Symlinks
59
80
 
@@ -66,6 +87,14 @@ This repo uses itself. When modifying nova-spec:
66
87
  2. Verify symlinks work: `ls -la .claude/`
67
88
  3. Run through a full ticket cycle
68
89
 
90
+ ## Tests
91
+
92
+ Run the smoke test suite with:
93
+
94
+ ```bash
95
+ npm test
96
+ ```
97
+
69
98
  ## Reference
70
99
 
71
100
  - Full docs: [README.md](./README.md)
package/CLAUDE.md ADDED
@@ -0,0 +1,3 @@
1
+ AGENTS.md
2
+
3
+ This repository uses `AGENTS.md` as the single source of truth for working instructions.
package/PHILOSOPHY.md CHANGED
@@ -94,14 +94,14 @@ We accept living without features. We do not accept living with bloat. The cost
94
94
 
95
95
  ## Forking and customization
96
96
 
97
- **Today (known limitation).** The installer does `rm -rf novaspec/` and re-copies from source. Local edits to `novaspec/commands/`, `skills/`, `templates/`, or `agents/` are **lost on re-run**. The installer only preserves `novaspec/config.yml` (via backup) and the consumer's `context/`.
97
+ **Today.** Updates are applied with `npx nova-spec sync`, which is idempotent and will not overwrite local edits to stock framework files. Sync records the last-shipped hashes in `novaspec/.nova-manifest.json` and:
98
98
 
99
- This contradicts principle 5. We acknowledge the gap. Two practical paths today:
99
+ - Updates files that are still untouched locally
100
+ - Skips files you edited and reports them so you can merge intentionally
100
101
 
101
- - **Team fork**: clone the nova-spec repo into your team's space, customize, install from your fork. Your fork is your `SCRIPT_DIR`.
102
- - **Manual updates**: don't re-run the installer; pull updates by hand and merge edits.
102
+ This keeps the framework from being the reason you can't ship: your local edits remain yours.
103
103
 
104
- **Planned (not built).** A `team-overrides/` directory that survives re-installs. Until shipped, the fork is the answer. Do not pretend otherwise to consumers.
104
+ If you need deeper customization, commit it in your repo (or fork nova-spec). When upstream changes touch files you customized, sync will intentionally refuse to overwrite them you choose how to merge.
105
105
 
106
106
  ---
107
107
 
package/lib/cli.js CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const yaml = require('js-yaml');
5
6
  const { init } = require('./installer.js');
6
7
  const { sync } = require('./sync.js');
7
8
  const jira = require('./jira.js');
8
9
 
10
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
11
+
9
12
  async function run() {
10
13
  const command = process.argv[2];
11
14
 
@@ -39,8 +42,15 @@ function runSource(args) {
39
42
  console.error('Usage: nova-spec source <relative-path>');
40
43
  process.exit(2);
41
44
  }
42
- const PACKAGE_ROOT = path.join(__dirname, '..');
43
- const abs = path.join(PACKAGE_ROOT, relPath);
45
+
46
+ // Resolve and CONTAIN within PACKAGE_ROOT. Rejects ../ traversal so prompt
47
+ // injection through $ARGUMENTS can't read arbitrary files via /nova-diff.
48
+ const abs = path.resolve(PACKAGE_ROOT, relPath);
49
+ const rootWithSep = PACKAGE_ROOT + path.sep;
50
+ if (abs !== PACKAGE_ROOT && !abs.startsWith(rootWithSep)) {
51
+ console.error(`✗ Path escapes the nova-spec package: ${relPath}`);
52
+ process.exit(1);
53
+ }
44
54
  if (!fs.existsSync(abs)) {
45
55
  console.error(`✗ ${relPath} is not part of the nova-spec package.`);
46
56
  process.exit(1);
@@ -56,31 +66,34 @@ async function runJira(args) {
56
66
  }
57
67
 
58
68
  const config = readJiraConfig();
59
- if (!config) {
60
- console.error('Jira not configured. Run `npx nova-spec init` and enable Jira.');
69
+ if (!config.ok) {
70
+ console.error(`${config.error}`);
71
+ if (config.hint) console.error(` ${config.hint}`);
61
72
  process.exit(1);
62
73
  }
63
74
 
64
75
  try {
65
76
  let result;
77
+ const baseArgs = { url: config.url, email: config.email, token: config.token };
66
78
  if (subcmd === 'get') {
67
79
  if (!rest[0]) throw new Error('Usage: nova-spec jira get <TICKET>');
68
- result = await jira.getIssueAsync({ ...config, ticket: rest[0] });
80
+ result = await jira.getIssueAsync({ ...baseArgs, ticket: rest[0] });
69
81
  } else if (subcmd === 'transitions') {
70
82
  if (!rest[0]) throw new Error('Usage: nova-spec jira transitions <TICKET>');
71
- result = await jira.listTransitionsAsync({ ...config, ticket: rest[0] });
83
+ result = await jira.listTransitionsAsync({ ...baseArgs, ticket: rest[0] });
72
84
  } else if (subcmd === 'transition') {
73
85
  if (!rest[0] || !rest[1]) throw new Error('Usage: nova-spec jira transition <TICKET> <ID>');
74
- result = await jira.transitionAsync({ ...config, ticket: rest[0], transitionId: rest[1] });
86
+ result = await jira.transitionAsync({ ...baseArgs, ticket: rest[0], transitionId: rest[1] });
75
87
  } else {
76
88
  throw new Error(`Unknown jira subcommand: ${subcmd}`);
77
89
  }
78
90
  if (result != null) console.log(JSON.stringify(result, null, 2));
79
91
  } catch (err) {
80
92
  if (err.status === 401) {
81
- console.error(' ✗ Jira 401: invalid credentials. Regenerate JIRA_API_TOKEN.');
93
+ console.error(' ✗ Jira 401: invalid credentials. Regenerate JIRA_API_TOKEN at');
94
+ console.error(' https://id.atlassian.com/manage-profile/security/api-tokens');
82
95
  } else if (err.status === 404) {
83
- console.error(` ✗ Jira 404: ticket not found.`);
96
+ console.error(` ✗ Jira 404: ${rest[0] || 'ticket'} not found.`);
84
97
  } else {
85
98
  console.error(` ✗ ${err.message}`);
86
99
  }
@@ -89,13 +102,17 @@ async function runJira(args) {
89
102
  }
90
103
 
91
104
  async function runForge(args) {
92
- const { detectForge, buildPrCommand, reviewTerm, checkCliAvailable } = require('./forge.js');
105
+ const { detectForge, buildPrCommand, reviewTerm, checkCliAvailable, pickCli } = require('./forge.js');
93
106
  const [subcmd, ...rest] = args;
94
107
 
95
108
  if (subcmd === 'detect') {
96
109
  const f = detectForge();
97
- if (f) console.log(f);
98
- else process.exit(1);
110
+ if (f) {
111
+ console.log(f);
112
+ } else {
113
+ console.error('✗ No git remote `origin` or unsupported forge.');
114
+ process.exit(1);
115
+ }
99
116
  return;
100
117
  }
101
118
 
@@ -107,6 +124,12 @@ async function runForge(args) {
107
124
  process.exit(2);
108
125
  }
109
126
  const cli = config.cli !== 'auto' ? config.cli : null;
127
+ const resolvedCli = pickCli(forge, cli);
128
+ if (resolvedCli && !checkCliAvailable(resolvedCli)) {
129
+ console.error(` ✗ ${resolvedCli} is not installed or not on PATH.`);
130
+ console.error(` Install: ${resolvedCli === 'gh' ? 'https://cli.github.com/' : 'https://gitlab.com/gitlab-org/cli'}`);
131
+ process.exit(127);
132
+ }
110
133
  const [title, body, base] = rest;
111
134
  if (!title || !body || !base) {
112
135
  console.error('Usage: nova-spec forge pr-command <title> <body> <base>');
@@ -127,50 +150,65 @@ async function runForge(args) {
127
150
  process.exit(2);
128
151
  }
129
152
 
130
- function readJiraConfig() {
153
+ // Parse novaspec/config.yml into a plain JS object. Returns null if missing.
154
+ // Returns null + warns if YAML is malformed (don't crash callers).
155
+ function loadConfig() {
131
156
  const configPath = path.join(process.cwd(), 'novaspec', 'config.yml');
132
157
  if (!fs.existsSync(configPath)) return null;
133
- const text = fs.readFileSync(configPath, 'utf8');
158
+ try {
159
+ return yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
160
+ } catch (err) {
161
+ return { __parseError: err.message };
162
+ }
163
+ }
164
+
165
+ function readJiraConfig() {
166
+ const cfg = loadConfig();
167
+ if (!cfg) {
168
+ return { ok: false, error: 'novaspec/config.yml not found.', hint: 'Run: npx nova-spec init' };
169
+ }
170
+ if (cfg.__parseError) {
171
+ return { ok: false, error: `novaspec/config.yml has invalid YAML: ${cfg.__parseError}` };
172
+ }
134
173
 
135
- const url = extractYamlScalar(text, 'jira', 'url');
136
- const email = extractYamlScalar(text, 'jira', 'email');
137
- let token = extractYamlScalar(text, 'jira', 'token');
174
+ const jiraCfg = cfg.jira || {};
175
+ const { url, email } = jiraCfg;
176
+ let token = jiraCfg.token;
138
177
 
139
- if (token && /^\$\{[A-Z_]+\}$/.test(token)) {
140
- const envName = token.slice(2, -1);
141
- token = process.env[envName];
142
- } else if (token === '${JIRA_API_TOKEN}' || !token) {
143
- token = process.env.JIRA_API_TOKEN;
178
+ // Resolve ${ENV_VAR} reference
179
+ if (typeof token === 'string') {
180
+ const m = token.match(/^\$\{([A-Z_][A-Z0-9_]*)\}$/);
181
+ if (m) token = process.env[m[1]];
182
+ }
183
+ if (!token) token = process.env.JIRA_API_TOKEN;
184
+
185
+ if (!url || !email) {
186
+ return {
187
+ ok: false,
188
+ error: 'Jira not configured.',
189
+ hint: 'Set jira.url and jira.email in novaspec/config.yml, or run: npx nova-spec init',
190
+ };
191
+ }
192
+ if (!token) {
193
+ return {
194
+ ok: false,
195
+ error: 'JIRA_API_TOKEN env var is not set.',
196
+ hint: 'Export it in your shell rc. Get one at https://id.atlassian.com/manage-profile/security/api-tokens',
197
+ };
144
198
  }
145
199
 
146
- if (!url || !email || !token) return null;
147
- return { url, email, token };
200
+ return { ok: true, url, email, token };
148
201
  }
149
202
 
150
203
  function readForgeConfig() {
151
- const configPath = path.join(process.cwd(), 'novaspec', 'config.yml');
204
+ const cfg = loadConfig();
152
205
  const defaults = { type: 'auto', cli: 'auto' };
153
- if (!fs.existsSync(configPath)) return defaults;
154
- const text = fs.readFileSync(configPath, 'utf8');
206
+ if (!cfg || cfg.__parseError) return defaults;
207
+ const forge = cfg.forge || {};
155
208
  return {
156
- type: extractYamlScalar(text, 'forge', 'type') || 'auto',
157
- cli: extractYamlScalar(text, 'forge', 'cli') || 'auto',
209
+ type: forge.type || 'auto',
210
+ cli: forge.cli || 'auto',
158
211
  };
159
212
  }
160
213
 
161
- function extractYamlScalar(text, parent, key) {
162
- // Minimal YAML reader for `parent: \n key: value` blocks. Strips quotes.
163
- const re = new RegExp(`^${parent}:\\s*$([\\s\\S]*?)(?=^\\S|\\Z)`, 'm');
164
- const block = text.match(re);
165
- if (!block) return null;
166
- const lineRe = new RegExp(`^\\s+${key}:\\s*(.*)$`, 'm');
167
- const match = block[1].match(lineRe);
168
- if (!match) return null;
169
- let value = match[1].trim();
170
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
171
- value = value.slice(1, -1);
172
- }
173
- return value;
174
- }
175
-
176
- module.exports = { run };
214
+ module.exports = { run, loadConfig, readJiraConfig, readForgeConfig };
package/lib/forge.js CHANGED
@@ -38,7 +38,9 @@ function buildPrCommand({ forge, cli, title, body, base }) {
38
38
  return `gh pr create --base ${q(base)} --title ${q(title)} --body ${q(body)}`;
39
39
  }
40
40
  if (forge === 'gitlab' || resolvedCli === 'glab') {
41
- return `glab mr create --target-branch ${q(base)} --title ${q(title)} --description ${q(body)} --fill`;
41
+ // NOTE: Avoid `--fill` because it can trigger glab autofill/push behavior depending on user config.
42
+ // We always pass title/body explicitly, and we never implicitly push.
43
+ return `glab mr create --target-branch ${q(base)} --title ${q(title)} --description ${q(body)} --yes`;
42
44
  }
43
45
  throw new Error(`Unknown forge: ${forge}`);
44
46
  }
package/lib/installer.js CHANGED
@@ -7,6 +7,7 @@ const {
7
7
  generateManifest,
8
8
  buildHookCommand,
9
9
  HOOK_MARKER,
10
+ MANIFEST_FILE,
10
11
  } = require('./sync.js');
11
12
  const { detectForge } = require('./forge.js');
12
13
  const { listTransitionsAsync } = require('./jira.js');
@@ -15,11 +16,37 @@ const PACKAGE_ROOT = path.join(__dirname, '..');
15
16
  const NOVASPEC_SRC = path.join(PACKAGE_ROOT, 'novaspec');
16
17
  const AGENTS_SRC = path.join(PACKAGE_ROOT, 'AGENTS.md');
17
18
  const CLAUDE_MD_SRC = path.join(PACKAGE_ROOT, 'CLAUDE.md');
18
- const MANIFEST_FILE = '.nova-manifest.json';
19
19
 
20
20
  async function init() {
21
21
  console.log('\n nova-spec installer\n ───────────────────\n');
22
22
 
23
+ // Detect an existing installation in the current directory or globally.
24
+ // Re-running `init` from scratch would clobber the user's customizations,
25
+ // because the wizard path uses copyTree which doesn't hash-compare like
26
+ // sync does. So we refuse and route to sync instead.
27
+ const home = process.env.HOME || process.env.USERPROFILE;
28
+ const existingHere = fs.existsSync(path.join(process.cwd(), 'novaspec', MANIFEST_FILE));
29
+ const existingGlobal =
30
+ home && fs.existsSync(path.join(home, '.claude', 'novaspec', MANIFEST_FILE));
31
+
32
+ if (existingHere || existingGlobal) {
33
+ const where = existingHere ? `here (${process.cwd()})` : `globally (${home}/.claude)`;
34
+ console.log(` ⚠ nova-spec is already installed ${where}.`);
35
+ console.log(' Running init again would overwrite local customizations.');
36
+ console.log();
37
+ const ok = await confirm({
38
+ message: 'Run `sync` instead (preserves your edits via hash-compare)?',
39
+ default: true,
40
+ });
41
+ if (!ok) {
42
+ console.log(' Cancelled. To reinstall from scratch, remove `novaspec/` manually first.');
43
+ return;
44
+ }
45
+ const { sync } = require('./sync.js');
46
+ await sync(existingHere ? process.cwd() : path.join(home, '.claude'));
47
+ return;
48
+ }
49
+
23
50
  const scope = await select({
24
51
  message: 'Where do you want to install nova-spec?',
25
52
  choices: [
@@ -29,7 +56,6 @@ async function init() {
29
56
  ],
30
57
  });
31
58
 
32
- const home = process.env.HOME || process.env.USERPROFILE;
33
59
  if (scope === 'global' && !home) {
34
60
  console.error(' ✗ HOME / USERPROFILE not set; cannot resolve global install path.');
35
61
  process.exit(1);
package/lib/jira.js CHANGED
@@ -31,6 +31,8 @@ function request({ url, method = 'GET', email, token, body = null }) {
31
31
  options.headers['Content-Type'] = 'application/json';
32
32
  }
33
33
 
34
+ options.timeout = 15000;
35
+
34
36
  const req = https.request(options, (res) => {
35
37
  let data = '';
36
38
  res.setEncoding('utf8');
@@ -38,7 +40,10 @@ function request({ url, method = 'GET', email, token, body = null }) {
38
40
  res.on('end', () => {
39
41
  const status = res.statusCode || 0;
40
42
  if (status >= 400) {
41
- const error = new Error(`Jira HTTP ${status}: ${data || res.statusMessage}`);
43
+ // Never include the response body in the message — it can contain
44
+ // request metadata (echoed headers from misconfigured proxies / WAFs)
45
+ // that leaks Authorization. Surface only status + reason phrase.
46
+ const error = new Error(`Jira HTTP ${status}: ${res.statusMessage || 'request failed'}`);
42
47
  error.status = status;
43
48
  return reject(error);
44
49
  }
@@ -51,6 +56,9 @@ function request({ url, method = 'GET', email, token, body = null }) {
51
56
  });
52
57
  });
53
58
 
59
+ req.on('timeout', () => {
60
+ req.destroy(new Error('Jira request timed out (15s)'));
61
+ });
54
62
  req.on('error', (err) => reject(err));
55
63
  if (body) req.write(JSON.stringify(body));
56
64
  req.end();
@@ -79,58 +87,6 @@ async function transitionAsync({ url, email, token, ticket, transitionId }) {
79
87
  });
80
88
  }
81
89
 
82
- function runCli() {
83
- // CLI mode: node lib/jira.js <command> [args...]
84
- const [, , cmd, ...args] = process.argv;
85
- const { JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN } = process.env;
86
-
87
- if (!JIRA_URL || !JIRA_EMAIL || !JIRA_API_TOKEN) {
88
- console.error('Missing env vars: JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN');
89
- process.exit(2);
90
- }
91
-
92
- const baseArgs = { url: JIRA_URL, email: JIRA_EMAIL, token: JIRA_API_TOKEN };
93
- let promise;
94
-
95
- switch (cmd) {
96
- case 'get':
97
- if (!args[0]) {
98
- console.error('Usage: nova-jira get <TICKET>');
99
- process.exit(2);
100
- }
101
- promise = getIssueAsync({ ...baseArgs, ticket: args[0] });
102
- break;
103
- case 'transitions':
104
- if (!args[0]) {
105
- console.error('Usage: nova-jira transitions <TICKET>');
106
- process.exit(2);
107
- }
108
- promise = listTransitionsAsync({ ...baseArgs, ticket: args[0] });
109
- break;
110
- case 'transition':
111
- if (!args[0] || !args[1]) {
112
- console.error('Usage: nova-jira transition <TICKET> <TRANSITION_ID>');
113
- process.exit(2);
114
- }
115
- promise = transitionAsync({ ...baseArgs, ticket: args[0], transitionId: args[1] });
116
- break;
117
- default:
118
- console.error('Commands: get <TICKET> | transitions <TICKET> | transition <TICKET> <ID>');
119
- process.exit(2);
120
- }
121
-
122
- promise
123
- .then((r) => {
124
- if (r != null) console.log(JSON.stringify(r, null, 2));
125
- })
126
- .catch((err) => {
127
- console.error(err.message);
128
- process.exit(err.status === 401 ? 401 : err.status === 404 ? 404 : 1);
129
- });
130
- }
131
-
132
- if (require.main === module) runCli();
133
-
134
90
  module.exports = {
135
91
  getIssueAsync,
136
92
  listTransitionsAsync,
package/lib/sync.js CHANGED
@@ -3,7 +3,6 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
- const os = require('os');
7
6
 
8
7
  const MANIFEST_FILE = '.nova-manifest.json';
9
8
  const HOOK_MARKER = '# nova-spec auto-sync';
@@ -111,9 +110,13 @@ async function sync(destDir = process.cwd()) {
111
110
  if (currentHash === newStockHash) continue; // already up to date
112
111
 
113
112
  const previousShipped = oldManifest.files[rel];
114
- const isUntouched = !previousShipped || currentHash === previousShipped;
115
113
 
116
- if (isUntouched) {
114
+ // Only overwrite when we KNOW the user didn't touch it: previousShipped
115
+ // must be recorded AND match the current disk hash. If the manifest is
116
+ // missing or doesn't mention this file, treat as user-owned and skip.
117
+ // This preserves edits when the manifest is deleted/corrupt/merged-away,
118
+ // at the cost of needing /nova-diff to apply the new version.
119
+ if (previousShipped && currentHash === previousShipped) {
117
120
  fs.copyFileSync(srcAbs, destAbs);
118
121
  updated.push(rel);
119
122
  } else {
@@ -187,8 +190,10 @@ function printReport({ version, created, updated, skipped, removed, skippedRemov
187
190
  }
188
191
 
189
192
  function buildHookCommand() {
190
- const logPath = path.join(os.homedir(), '.nova-spec.log');
191
- return `npx nova-spec@latest sync >> ${logPath} 2>&1 || true ${HOOK_MARKER}`;
193
+ // Use $HOME so the shell resolves it at run time. Avoids baking the
194
+ // installer's home dir into the consumer's settings and survives paths
195
+ // with spaces / apostrophes (common on macOS and Windows usernames).
196
+ return `npx nova-spec@latest sync >> "$HOME/.nova-spec.log" 2>&1 || true ${HOOK_MARKER}`;
192
197
  }
193
198
 
194
199
  function ensureSessionStartHook(destDir) {
@@ -245,4 +250,5 @@ module.exports = {
245
250
  collectPackageFiles,
246
251
  HOOK_MARKER,
247
252
  FRAMEWORK_FILES,
253
+ MANIFEST_FILE,
248
254
  };
@@ -27,31 +27,41 @@ If `context/` doesn't exist, return:
27
27
  ## Loaded context
28
28
  **Services**: not documented (context/ missing)
29
29
  **Decisions**: none
30
- **Gaps**: context/ structure not initialized — run `npx nova-spec init`
30
+ **Gaps**: context/ structure not initialized — run npx nova-spec init
31
31
  **Questions**: none
32
32
  ```
33
33
  And stop.
34
34
 
35
- ### 2. Read each service
35
+ ### 2. Read stack and conventions (always)
36
+
37
+ Always read these two files when they exist (they're scaffolded by `npx nova-spec init`):
38
+ - `context/stack.md` — tech stack + key dependencies
39
+ - `context/conventions.md` — code style + patterns to follow / avoid
40
+
41
+ If either is missing, note it as a gap but continue.
42
+
43
+ ### 3. Read each service
36
44
 
37
45
  For each service in `$ARGUMENTS`:
38
46
  - Read `context/services/<service>.md` if it exists.
39
47
  - If it doesn't exist, note it as a gap.
40
48
 
41
- ### 3. Pick relevant decisions and gotchas
49
+ ### 4. Pick relevant decisions and gotchas
42
50
 
43
51
  - `ls context/decisions/` (no `-R`, doesn't enter `archived/`).
44
52
  - `ls context/gotchas/`.
45
53
  - Pick 3-5 files from each whose name is relevant to the ticket's scope or affected services. Don't force connections.
46
54
  - Read the chosen ones.
47
55
 
48
- ### 4. Return summary
56
+ ### 5. Return summary
49
57
 
50
58
  Return exactly this structure, without extra text:
51
59
 
52
60
  ```
53
61
  ## Loaded context
54
62
 
63
+ **Stack**: <✓ loaded | ✗ missing>
64
+ **Conventions**: <✓ loaded | ✗ missing>
55
65
  **Services**: <list with ✓ if services/<svc>.md exists, ✗ otherwise>
56
66
  **Decisions read**: <list of files or "none">
57
67
  **Gotchas read**: <list of files or "none">
@@ -16,17 +16,40 @@ the user. Don't make commits. Don't modify code.
16
16
  - `context/changes/active/<ticket-id>/proposal.md`
17
17
  - `context/changes/active/<ticket-id>/tasks.md`
18
18
  - Read live decisions in `context/decisions/` (all relevant ones, **without entering `archived/`**)
19
- - Get the full diff by combining:
20
- - Committed changes on the branch: `git diff <branch.base>...HEAD`
21
- - Uncommitted changes (working tree + staged): `git diff HEAD`
22
- - Read `novaspec/config.yml → branch.base` to determine the base branch (default: `main`)
23
- - If both diffs are empty, warn: "⚠️ Empty diff: no changes on the branch or in the working tree."
19
+ - Read `novaspec/config.yml → branch.base` to determine the base branch (default: `main`)
24
20
 
25
21
  If any artifact is missing, stop with:
26
22
  ```
27
23
  ⛔ Review aborted: missing <file>. Run the corresponding step first.
28
24
  ```
29
25
 
26
+ ### 1b. Run deterministic checks (BLOCKING)
27
+
28
+ Before any LLM-based review, run the deterministic guardrail:
29
+
30
+ ```bash
31
+ bash novaspec/guardrails/review-checks.sh <ticket-id> <base-branch>
32
+ ```
33
+
34
+ This runs (in order): diff non-empty, all `## Files to touch` declared in
35
+ `tasks.md` actually appear in the diff, `npm run lint` if defined, `npm test`
36
+ if defined.
37
+
38
+ If the script exits non-zero, **mark the verdict as `✗ Needs fixes`
39
+ immediately** and write the script's output verbatim into `review.md` under
40
+ a `## Pre-review checks` section. Do NOT proceed to the 4-axis LLM review —
41
+ the verdict is already negative. Skip straight to Step 3 (write the report)
42
+ then Step 4 (terminate).
43
+
44
+ ### 1c. Get the full diff
45
+
46
+ Combine:
47
+ - Committed changes on the branch: `git diff <branch.base>...HEAD`
48
+ - Uncommitted changes (working tree + staged): `git diff HEAD`
49
+
50
+ (If `review-checks.sh` already failed at "Diff non-empty", the verdict is
51
+ already locked — skip the diff fetch.)
52
+
30
53
  ### 2. Review across 4 axes
31
54
 
32
55
  **Spec compliance**
@@ -1,64 +1,80 @@
1
1
  ---
2
- description: Show differences between your custom override and the current core version
3
- argument-hint: <skill|command|agent name>
2
+ description: Show what changed upstream for a framework file you've edited locally
3
+ argument-hint: <relative-path-from-repo-root>
4
4
  ---
5
5
 
6
- You are a **read-only** command. Show what changed between the user's custom
7
- override and the upstream core version for `$ARGUMENTS`.
6
+ You are a **read-only** command. You compare the user's local version of a
7
+ framework file against the version shipped by the installed nova-spec
8
+ package, so they can decide whether to merge upstream changes.
8
9
 
9
- ## Steps
10
-
11
- ### 1. Resolve paths
10
+ ## When this is invoked
12
11
 
13
- - Custom path: `novaspec/custom/<type>/$ARGUMENTS/` (check `skills/`, `commands/`, `agents/` in order)
14
- - Core path: `novaspec/<type>/$ARGUMENTS/`
12
+ `npx nova-spec sync` reports files with local edits under the heading
13
+ `⚠ N file(s) NOT updated (you have local edits)` and points the user at
14
+ this command via:
15
15
 
16
- If the custom path doesn't exist:
17
16
  ```
18
- No custom override found for "$ARGUMENTS".
19
- Nothing to diff.
17
+ /nova-diff <path>
20
18
  ```
21
- Stop here.
22
19
 
23
- ### 2. Check manifest
20
+ For example: `/nova-diff novaspec/templates/pr-body.md`.
21
+
22
+ ## Steps
23
+
24
+ ### 1. Resolve the path
24
25
 
25
- Read `novaspec/.nova-manifest.json`. Look for `$ARGUMENTS` in `outdated_customs`.
26
+ `$ARGUMENTS` is a path relative to the repo root, e.g.
27
+ `novaspec/templates/pr-body.md` or `AGENTS.md`.
26
28
 
27
- If not in `outdated_customs`:
29
+ Verify the local file exists. If not:
28
30
  ```
29
- Your custom "$ARGUMENTS" matches the current core version.
30
- No upstream changes since your override was created.
31
+ No local file at "<path>". Nothing to diff.
31
32
  ```
32
- Stop here.
33
+ Stop.
33
34
 
34
- ### 3. Show diff
35
+ ### 2. Locate the upstream (package) version
35
36
 
36
37
  Run:
38
+
37
39
  ```bash
38
- diff -u novaspec/<type>/$ARGUMENTS/SKILL.md novaspec/custom/<type>/$ARGUMENTS/SKILL.md
40
+ npx nova-spec source "$ARGUMENTS"
39
41
  ```
40
42
 
41
- Present the diff clearly, highlighting:
42
- - Lines added in your custom version (your changes)
43
- - Lines changed in the new core version that your custom doesn't have
43
+ This prints the absolute path to the file **inside the installed nova-spec
44
+ package** (somewhere like `~/.npm/_npx/<hash>/node_modules/nova-spec/<path>`).
45
+ Exit code 1 + `✗ ... is not part of the nova-spec package.` means the path
46
+ is not a framework file — tell the user it isn't tracked by nova-spec.
47
+
48
+ ### 3. Show the diff
49
+
50
+ ```bash
51
+ diff -u "<package-path>" "<local-path>"
52
+ ```
53
+
54
+ Read both files and present the diff clearly:
55
+ - Lines marked `+` in YOUR copy are your edits (keep these unless you want to revert).
56
+ - Lines marked `+` in the package copy are upstream changes (consider merging).
44
57
 
45
58
  ### 4. Decision prompt
46
59
 
60
+ Ask the user:
61
+
47
62
  ```
48
63
  Options:
49
- [K] Keep your version — ignore upstream changes
50
- [M] Merge manually — I'll open both files for you to edit
51
- [R] Replace with core — discard your custom, use new core version
64
+ [K] Keep your version — ignore the upstream change (no-op)
65
+ [M] Merge manually — I'll print both paths so you can edit
66
+ [R] Replace with the package version — discard your local edits
52
67
  ```
53
68
 
54
- Wait for user selection.
69
+ Wait for an explicit selection.
55
70
 
56
- - **[K]**: Update manifest to mark as reviewed. No file changes.
57
- - **[M]**: Show both file paths and remind user to run `/nova-sync` after merging.
58
- - **[R]**: Delete `novaspec/custom/<type>/$ARGUMENTS/` and confirm.
71
+ - **[K]**: do nothing. Next `/nova-sync` will skip the file again with the same warning.
72
+ - **[M]**: print both file paths and remind the user that after they merge, sync may still flag the file as edited (until its hash again matches the shipped version).
73
+ - **[R]**: copy the package version over the local file. **Ask for confirmation before overwriting.** After replacement, the next sync will treat it as up to date.
59
74
 
60
75
  ## Rules
61
76
 
62
- - Never auto-apply changes.
63
- - Always wait for explicit user decision.
64
- - For [R], ask for confirmation before deleting.
77
+ - Never auto-apply. Always wait for explicit selection.
78
+ - For `[R]`, double-confirm before overwriting — destructive.
79
+ - If the user passes a path that includes `..`, the `source` CLI will
80
+ refuse it. Trust that boundary check; don't try to bypass it.
@@ -6,7 +6,21 @@ You translate the spec into an executable plan and tasks.
6
6
 
7
7
  ## Guardrail
8
8
 
9
- `checklist.md` → 1, 2 (branch-pattern, proposal-exists)
9
+ `checklist.md` → 0, 1, 2, 7 (nova-installed, branch-pattern, proposal-exists, proposal-closed)
10
+
11
+ ### Run guardrail #7 before drafting tasks
12
+
13
+ ```bash
14
+ bash novaspec/guardrails/proposal-closed.sh <ticket-id>
15
+ ```
16
+
17
+ This greps `proposal.md` for `TBD`, `TODO`, `FIXME`, `???`, `<placeholder>`,
18
+ `[ ] decision`. If it exits non-zero, **stop immediately** with the script's
19
+ output. Tell the user:
20
+
21
+ > "Proposal has open markers. Re-run `/nova-spec` to close them before planning."
22
+
23
+ Do NOT generate `tasks.md` from an unclosed proposal.
10
24
 
11
25
  ## Precondition
12
26
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Start the nova-spec flow from a Jira ticket
2
+ description: Start the nova-spec flow from a ticket
3
3
  argument-hint: <TICKET-ID>
4
4
  ---
5
5
 
@@ -10,17 +10,45 @@ The user has passed the ticket: **$ARGUMENTS**
10
10
  Your job is to set the stage before any spec or code is written.
11
11
  Don't implement anything. Don't propose a spec. Just orchestrate.
12
12
 
13
+ ## Guardrail
14
+
15
+ `checklist.md` → 0 (nova-installed)
16
+
13
17
  ## Steps
14
18
 
15
19
  ### 1. Get the ticket
16
20
 
17
- Read `novaspec/config.yml` → `jira.skill`.
18
- - If it has a value, invoke that skill to fetch the ticket.
19
- - If it's empty or missing, ask the user to paste:
20
- - title
21
- - description
22
- - acceptance criteria
23
- - relevant comments
21
+ Read `novaspec/config.yml` → `ticket_system`. It's one of:
22
+ - `jira` fetch the ticket from Jira via the `jira-integration` skill
23
+ - `none` (or missing key) no tracker; user pastes content
24
+
25
+ #### If `ticket_system: jira`
26
+
27
+ Validate `$ARGUMENTS` matches `[A-Z][A-Z0-9]+-[0-9]+` (e.g. `PROJ-123`).
28
+ If not, refuse and ask for a properly-formatted ticket key.
29
+
30
+ Invoke the `jira-integration` skill, which runs `npx nova-spec jira get <TICKET>`. Error handling by exit code:
31
+
32
+ - **Exit 401** — invalid credentials. Tell the user:
33
+ > "Jira returned 401 Unauthorized. Regenerate your API token at https://id.atlassian.com/manage-profile/security/api-tokens and update `JIRA_API_TOKEN`."
34
+ Do NOT retry. Stop.
35
+ - **Exit 404** — ticket not found:
36
+ > "Jira returned 404 for `<TICKET>`. Check the key and `jira.project` in `novaspec/config.yml`."
37
+ Stop.
38
+ - **Network / timeout / other** — fall back to manual paste:
39
+ > "Couldn't reach Jira. Paste the ticket title, description, AC, and relevant comments and I'll continue."
40
+
41
+ #### If `ticket_system: none` (or missing)
42
+
43
+ Skip the ticket-key format check — `$ARGUMENTS` is a free-form identifier.
44
+
45
+ Ask the user to paste:
46
+ - title
47
+ - description
48
+ - acceptance criteria
49
+ - relevant comments
50
+
51
+ Don't make up ticket content.
24
52
 
25
53
  ### 2. Classify the ticket
26
54
 
@@ -7,7 +7,7 @@ This is the step that feeds architectural memory.
7
7
 
8
8
  ## Guardrail
9
9
 
10
- `checklist.md` → 1, 5, 6 (branch-pattern, review-approved, old-decision-archived)
10
+ `checklist.md` → 0, 1, 5, 6 (nova-installed, branch-pattern, review-approved, old-decision-archived)
11
11
 
12
12
  ## Precondition
13
13
 
@@ -52,15 +52,28 @@ Default: don't write. Most tickets don't generate a gotcha.
52
52
  Use the structure of `novaspec/templates/commit.md` as a template.
53
53
  If there are many changes, propose grouping into logical commits.
54
54
 
55
- ### 6. Create PR
55
+ ### 6. Create PR / MR (forge-agnostic)
56
56
 
57
57
  Resolve the base branch the same way `/nova-start` does:
58
58
  - Read `branch.base` from `novaspec/config.yml`.
59
59
  - If the key is missing, try `develop`; if that doesn't exist either, ask
60
60
  the user and recommend setting `branch.base` in `novaspec/config.yml`.
61
61
 
62
- Create the PR with `gh pr create --base <resolved-base> --title "<title>"
63
- --body "<description>"`.
62
+ **Do NOT hardcode `gh`.** Ask the CLI to build the right command for the
63
+ forge (`gh pr create ...` for GitHub, `glab mr create ...` for GitLab):
64
+
65
+ ```bash
66
+ npx nova-spec forge pr-command "<TICKET-ID>: <title>" "<description>" "<base>"
67
+ ```
68
+
69
+ The CLI also verifies the forge binary is installed; if it isn't, it exits
70
+ with code 127 and a clear error. Show the user the command, get confirmation,
71
+ then execute it.
72
+
73
+ For user-facing messages use the right vocabulary (PR vs MR):
74
+ ```bash
75
+ TERM=$(npx nova-spec forge term)
76
+ ```
64
77
 
65
78
  **Title**: `<TICKET-ID>: <title>`
66
79
 
@@ -68,7 +81,9 @@ Create the PR with `gh pr create --base <resolved-base> --title "<title>"
68
81
 
69
82
  ### 7. Close the ticket in Jira
70
83
 
71
- If `novaspec/config.yml` has `jira.skill` set, invoke the `jira-integration` skill to transition the ticket to "Done".
84
+ If `novaspec/config.yml` has `jira.skill` set (and `ticket_system: jira`), invoke the `jira-integration` skill to transition the ticket to "Done".
85
+
86
+ The transition ID to use is `jira.transitions.done` from `novaspec/config.yml`. If that key is missing, fall back to the legacy `jira.done_transition_id`. The skill (or `npx nova-spec jira transition <TICKET> <id>` directly) handles the API call.
72
87
 
73
88
  Confirm to the user: "Ticket <TICKET-ID> marked as Done in Jira ✓"
74
89
 
@@ -1,10 +1,23 @@
1
1
  # Guardrails — Checklist
2
2
 
3
- **Execution order: 1 → 2 → 3 → 4 → 5 → 6**
3
+ **Execution order: 0 → 1 → 2 → 7 → 3 → 4 → 5 → 6**
4
+
5
+ ## 0. nova-installed
6
+ Verify nova-spec is properly installed in this project.
7
+ - Run `bash novaspec/guardrails/nova-installed.sh`.
8
+ - Checks that `novaspec/config.yml` and `context/` exist.
9
+ - ⛔ **Stop.** Run `npx nova-spec init` first.
4
10
 
5
11
  ## 1. branch-pattern
6
12
  Verify the active ticket branch. Extract `<ticket-id>` from the current git branch.
7
- - Must follow the pattern `(feature|fix|arch)/<TICKET>-<slug>` from `novaspec/config.yml`.
13
+ - Read `branch.types` from `novaspec/config.yml` to know which prefixes are
14
+ valid for this project. The defaults shipped by the installer are:
15
+ `feature, fix, arch, bugfix, hotfix, docs, refactor, chore`.
16
+ - The branch must follow `<type>/<TICKET>-<slug>` where `<type>` is any of
17
+ the configured types (matched against the **values** in `branch.types`,
18
+ e.g. `documentation: docs` → `docs` is valid).
19
+ - If `ticket_system: none` in config, the `<TICKET>` part can be any
20
+ identifier the user chose (no enforced format).
8
21
  - ⛔ **Stop.** Run `/nova-start <TICKET>` first.
9
22
 
10
23
  ## 2. proposal-exists
@@ -32,3 +45,9 @@ Verify the review was approved.
32
45
  Validate that superseded decisions are archived.
33
46
  - Files in `context/decisions/*.md` with `> Supersedes: X.md` imply that `X.md` lives in `context/decisions/archived/`, not at the root.
34
47
  - ⛔ **Stop.** Move the file to `archived/` with `git mv` and retry.
48
+
49
+ ## 7. proposal-closed
50
+ Deterministic check: the proposal has no unresolved markers.
51
+ - Run `bash novaspec/guardrails/proposal-closed.sh <ticket-id>`.
52
+ - The script greps for `TBD`, `TODO`, `FIXME`, `???`, `<placeholder>`, `[ ] decision`.
53
+ - ⛔ **Stop.** Re-run `/nova-spec` and close the open requirements before `/nova-plan`.
File without changes
@@ -1,96 +1,81 @@
1
1
  ---
2
2
  name: jira-integration
3
- description: Read and create tasks in Jira via the REST API. Uses the project's config.yml (`jira` section).
3
+ description: Read and transition Jira tickets via the deterministic `npx nova-spec jira` CLI. The CLI handles auth so tokens never appear in shell commands.
4
4
  ---
5
5
 
6
6
  # Jira Integration
7
7
 
8
- Read and create issues in Jira using Atlassian's REST API v3.
8
+ This skill is a thin wrapper around the `npx nova-spec jira` subcommand,
9
+ which is a small Node-based HTTP client. **Never build `curl` calls by hand**
10
+ — the CLI keeps the `JIRA_API_TOKEN` inside the Node process and never
11
+ inlines it on a command line.
9
12
 
10
- ## Required config in config.yml
13
+ ## Required config in `novaspec/config.yml`
11
14
 
12
15
  ```yaml
16
+ ticket_system: jira
17
+
13
18
  jira:
14
19
  skill: jira-integration
15
- url: https://your-org.atlassian.net # no trailing slash
16
- project: PROJ # default project key
17
- email: you@email.com
18
- token: ${JIRA_API_TOKEN} # reference to env variable
19
- done_transition_id: "41" # find it via GET /rest/api/3/issue/<TICKET>/transitions
20
+ url: https://your-workspace.atlassian.net # no trailing slash
21
+ project: PROJ # default project key
22
+ email: you@example.com # tied to the token
23
+ token: ${JIRA_API_TOKEN} # env-var reference
24
+ done_transition_id: "41" # legacy, kept for fallback
25
+ transitions:
26
+ done: "41" # preferred form, used by /nova-wrap
20
27
  ```
21
28
 
22
29
  Get the token at: https://id.atlassian.com/manage-profile/security/api-tokens
23
30
 
24
- ## How to use this skill
25
-
26
- When the user asks to read or create Jira tasks:
27
-
28
- 1. **Read the config**: read the project's `config.yml` and extract the `jira` section.
29
- 2. **Resolve the token**: if the value starts with `${`, read the corresponding env variable (e.g. `$JIRA_API_TOKEN`).
30
- 3. **Build Basic Auth credentials**: `base64(email:token)`.
31
- 4. **Call the API** with `curl` for the requested operation.
32
-
33
31
  ## Operations
34
32
 
35
33
  ### Read a ticket
36
34
 
37
35
  ```bash
38
- curl -s \
39
- -H "Authorization: Basic <BASE64>" \
40
- -H "Accept: application/json" \
41
- "https://<url>/rest/api/3/issue/<TICKET_KEY>"
36
+ npx nova-spec jira get <TICKET-KEY>
42
37
  ```
43
38
 
44
- Show the user: key, summary, status (status.name), description, assignee.
39
+ Output is JSON. Extract: `key`, `fields.summary`, `fields.status.name`,
40
+ `fields.description`, `fields.assignee.displayName`.
45
41
 
46
- ### List tickets in a project (recent open ones)
42
+ ### List transitions for a ticket
47
43
 
48
44
  ```bash
49
- curl -s \
50
- -H "Authorization: Basic <BASE64>" \
51
- -H "Accept: application/json" \
52
- "https://<url>/rest/api/3/search?jql=project=<PROJECT>+AND+statusCategory!=Done+ORDER+BY+created+DESC&maxResults=20&fields=summary,status,assignee,priority"
45
+ npx nova-spec jira transitions <TICKET-KEY>
53
46
  ```
54
47
 
55
- ### Create a ticket
48
+ Use this when `transitions.done` is missing or wrong: it tells you which
49
+ transitions are reachable from the current ticket status, and their IDs.
50
+
51
+ ### Transition a ticket (close as Done from `/nova-wrap`)
56
52
 
57
53
  ```bash
58
- curl -s -X POST \
59
- -H "Authorization: Basic <BASE64>" \
60
- -H "Content-Type: application/json" \
61
- -H "Accept: application/json" \
62
- "https://<url>/rest/api/3/issue" \
63
- -d '{
64
- "fields": {
65
- "project": { "key": "<PROJECT>" },
66
- "summary": "<TITLE>",
67
- "description": {
68
- "type": "doc", "version": 1,
69
- "content": [{"type": "paragraph", "content": [{"type": "text", "text": "<DESCRIPTION>"}]}]
70
- },
71
- "issuetype": { "name": "<TYPE>" }
72
- }
73
- }'
54
+ npx nova-spec jira transition <TICKET-KEY> <TRANSITION-ID>
74
55
  ```
75
56
 
76
- ### Transition ticket to Done
57
+ Read `<TRANSITION-ID>` from `novaspec/config.yml` → `jira.transitions.done`.
58
+ Fall back to `jira.done_transition_id` if the structured form is missing.
77
59
 
78
- ```bash
79
- AUTH=$(echo -n "<email>:<token>" | base64)
80
- curl -s -X POST \
81
- -H "Authorization: Basic $AUTH" \
82
- -H "Content-Type: application/json" \
83
- "https://<url>/rest/api/3/issue/<TICKET-ID>/transitions" \
84
- -d "{\"transition\": {\"id\": \"<done_transition_id>\"}}"
85
- ```
60
+ ## Error handling
86
61
 
87
- Use `done_transition_id` from `config.yml`. To find your transition IDs:
88
- ```bash
89
- curl -s -H "Authorization: Basic <BASE64>" "https://<url>/rest/api/3/issue/<ANY-TICKET>/transitions"
90
- ```
62
+ The CLI exits with:
63
+
64
+ | Code | Meaning | What to do |
65
+ |---|---|---|
66
+ | 0 | Success | Continue |
67
+ | 1 | Generic error (network, parse) | Surface the message; offer manual fallback |
68
+ | 2 | Usage error (missing arg) | Fix the command and retry |
69
+ | 401 | Invalid credentials | Tell user to regenerate `JIRA_API_TOKEN` — **do NOT retry** |
70
+ | 404 | Ticket not found | Confirm the key with the user |
71
+
72
+ When the CLI prints `✗ Jira 401`, never retry — credentials are wrong.
91
73
 
92
- ## Notes
74
+ ## Rules
93
75
 
94
- - If `token` is not in config.yml, ask the user to set it.
95
- - If the project is not specified in the request, use the `project` from config.yml.
96
- - On HTTP errors (401, 403, 404), show Jira's error message but never expose the token.
76
+ - **Never** paste the token into a `curl` command. The CLI reads it from
77
+ `JIRA_API_TOKEN` env var via the `${JIRA_API_TOKEN}` reference in `config.yml`.
78
+ - If `JIRA_API_TOKEN` is missing, the CLI fails fast with `✗ JIRA_API_TOKEN env var is not set.`
79
+ - For debugging only (and never with a real token visible), you can build
80
+ Basic Auth via `AUTH=$(printf '%s' "$JIRA_EMAIL:$JIRA_API_TOKEN" | base64)` —
81
+ but the CLI is always preferable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nova-spec",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Spec-Driven Development framework for Claude Code and OpenCode",
5
5
  "bin": {
6
6
  "nova-spec": "bin/nova-spec.js"
@@ -15,14 +15,19 @@
15
15
  "novaspec/templates/",
16
16
  "novaspec/config.example.yml",
17
17
  "AGENTS.md",
18
+ "CLAUDE.md",
18
19
  "INSTALL.md",
19
20
  "PHILOSOPHY.md"
20
21
  ],
21
22
  "scripts": {
22
- "test": "node test/smoke.test.js"
23
+ "test": "node test/smoke.test.js",
24
+ "docs:dev": "vitepress dev docs",
25
+ "docs:build": "vitepress build docs",
26
+ "docs:preview": "vitepress preview docs"
23
27
  },
24
28
  "dependencies": {
25
- "@inquirer/prompts": "^7.0.0"
29
+ "@inquirer/prompts": "^7.0.0",
30
+ "js-yaml": "^4.1.1"
26
31
  },
27
32
  "engines": {
28
33
  "node": ">=18"
@@ -39,5 +44,8 @@
39
44
  "spec-driven-development",
40
45
  "jira",
41
46
  "workflow"
42
- ]
47
+ ],
48
+ "devDependencies": {
49
+ "vitepress": "^1.6.4"
50
+ }
43
51
  }