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 +32 -3
- package/CLAUDE.md +3 -0
- package/PHILOSOPHY.md +5 -5
- package/lib/cli.js +83 -45
- package/lib/forge.js +3 -1
- package/lib/installer.js +28 -2
- package/lib/jira.js +9 -53
- package/lib/sync.js +11 -5
- package/novaspec/agents/context-loader.md +14 -4
- package/novaspec/agents/nova-review-agent.md +28 -5
- package/novaspec/commands/nova-diff.md +50 -34
- package/novaspec/commands/nova-plan.md +15 -1
- package/novaspec/commands/nova-start.md +36 -8
- package/novaspec/commands/nova-wrap.md +20 -5
- package/novaspec/guardrails/checklist.md +21 -2
- package/novaspec/guardrails/nova-installed.sh +0 -0
- package/novaspec/skills/jira-integration/SKILL.md +46 -61
- package/package.json +12 -4
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
|
-
|
|
56
|
-
|
|
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
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
|
|
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
|
-
|
|
99
|
+
- Updates files that are still untouched locally
|
|
100
|
+
- Skips files you edited and reports them so you can merge intentionally
|
|
100
101
|
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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(
|
|
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({ ...
|
|
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({ ...
|
|
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({ ...
|
|
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)
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
136
|
-
const email =
|
|
137
|
-
let token =
|
|
174
|
+
const jiraCfg = cfg.jira || {};
|
|
175
|
+
const { url, email } = jiraCfg;
|
|
176
|
+
let token = jiraCfg.token;
|
|
138
177
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
return { url, email, token };
|
|
200
|
+
return { ok: true, url, email, token };
|
|
148
201
|
}
|
|
149
202
|
|
|
150
203
|
function readForgeConfig() {
|
|
151
|
-
const
|
|
204
|
+
const cfg = loadConfig();
|
|
152
205
|
const defaults = { type: 'auto', cli: 'auto' };
|
|
153
|
-
if (!
|
|
154
|
-
const
|
|
206
|
+
if (!cfg || cfg.__parseError) return defaults;
|
|
207
|
+
const forge = cfg.forge || {};
|
|
155
208
|
return {
|
|
156
|
-
type:
|
|
157
|
-
cli:
|
|
209
|
+
type: forge.type || 'auto',
|
|
210
|
+
cli: forge.cli || 'auto',
|
|
158
211
|
};
|
|
159
212
|
}
|
|
160
213
|
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
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
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
-
|
|
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
|
|
3
|
-
argument-hint: <
|
|
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.
|
|
7
|
-
|
|
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
|
-
##
|
|
10
|
-
|
|
11
|
-
### 1. Resolve paths
|
|
10
|
+
## When this is invoked
|
|
12
11
|
|
|
13
|
-
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
Nothing to diff.
|
|
17
|
+
→ /nova-diff <path>
|
|
20
18
|
```
|
|
21
|
-
Stop here.
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
For example: `/nova-diff novaspec/templates/pr-body.md`.
|
|
21
|
+
|
|
22
|
+
## Steps
|
|
23
|
+
|
|
24
|
+
### 1. Resolve the path
|
|
24
25
|
|
|
25
|
-
|
|
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
|
|
29
|
+
Verify the local file exists. If not:
|
|
28
30
|
```
|
|
29
|
-
|
|
30
|
-
No upstream changes since your override was created.
|
|
31
|
+
No local file at "<path>". Nothing to diff.
|
|
31
32
|
```
|
|
32
|
-
Stop
|
|
33
|
+
Stop.
|
|
33
34
|
|
|
34
|
-
###
|
|
35
|
+
### 2. Locate the upstream (package) version
|
|
35
36
|
|
|
36
37
|
Run:
|
|
38
|
+
|
|
37
39
|
```bash
|
|
38
|
-
|
|
40
|
+
npx nova-spec source "$ARGUMENTS"
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
50
|
-
[M] Merge manually — I'll
|
|
51
|
-
[R] Replace with
|
|
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
|
|
69
|
+
Wait for an explicit selection.
|
|
55
70
|
|
|
56
|
-
- **[K]**:
|
|
57
|
-
- **[M]**:
|
|
58
|
-
- **[R]**:
|
|
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
|
|
63
|
-
-
|
|
64
|
-
-
|
|
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
|
|
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` → `
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
16
|
-
project: PROJ
|
|
17
|
-
email: you@
|
|
18
|
-
token: ${JIRA_API_TOKEN}
|
|
19
|
-
done_transition_id: "41"
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
Output is JSON. Extract: `key`, `fields.summary`, `fields.status.name`,
|
|
40
|
+
`fields.description`, `fields.assignee.displayName`.
|
|
45
41
|
|
|
46
|
-
### List
|
|
42
|
+
### List transitions for a ticket
|
|
47
43
|
|
|
48
44
|
```bash
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
##
|
|
74
|
+
## Rules
|
|
93
75
|
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
-
|
|
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.
|
|
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
|
}
|