nova-spec 1.0.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.
Files changed (34) hide show
  1. package/AGENTS.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/bin/nova-spec.js +8 -0
  5. package/lib/cli.js +24 -0
  6. package/lib/installer.js +243 -0
  7. package/lib/sync.js +147 -0
  8. package/novaspec/agents/context-loader.md +65 -0
  9. package/novaspec/agents/nova-review-agent.md +73 -0
  10. package/novaspec/commands/nova-build.md +70 -0
  11. package/novaspec/commands/nova-diff.md +64 -0
  12. package/novaspec/commands/nova-plan.md +41 -0
  13. package/novaspec/commands/nova-review.md +35 -0
  14. package/novaspec/commands/nova-spec.md +41 -0
  15. package/novaspec/commands/nova-start.md +82 -0
  16. package/novaspec/commands/nova-status.md +82 -0
  17. package/novaspec/commands/nova-sync.md +46 -0
  18. package/novaspec/commands/nova-wrap.md +94 -0
  19. package/novaspec/config.example.yml +24 -0
  20. package/novaspec/custom/agents/.gitkeep +0 -0
  21. package/novaspec/custom/commands/.gitkeep +0 -0
  22. package/novaspec/custom/skills/.gitkeep +0 -0
  23. package/novaspec/guardrails/checklist.md +34 -0
  24. package/novaspec/skills/close-requirement/SKILL.md +56 -0
  25. package/novaspec/skills/jira-integration/SKILL.md +96 -0
  26. package/novaspec/skills/update-service-context/SKILL.md +60 -0
  27. package/novaspec/skills/write-decision/SKILL.md +69 -0
  28. package/novaspec/templates/commit.md +6 -0
  29. package/novaspec/templates/pr-body.md +20 -0
  30. package/novaspec/templates/proposal.md +49 -0
  31. package/novaspec/templates/review.md +22 -0
  32. package/novaspec/templates/status-report.md +8 -0
  33. package/novaspec/templates/tasks.md +24 -0
  34. package/package.json +36 -0
package/AGENTS.md ADDED
@@ -0,0 +1,61 @@
1
+ # nova-spec — framework AGENTS.md
2
+
3
+ > This repo uses CLAUDE.md as a stub → AGENTS.md is the instructions file.
4
+
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
+
7
+ ## What is nova-spec
8
+
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
+
11
+ ## Key commands
12
+
13
+ - `/nova-start <TICKET>` — classify ticket, create branch, load context
14
+ - `/nova-spec` — close requirements, generate spec (uses `close-requirement` skill)
15
+ - `/nova-plan` — create plan + tasks
16
+ - `/nova-build` — execute tasks one-by-one
17
+ - `/nova-review` — final code review
18
+ - `/nova-wrap` — commit, PR, update memory (uses `write-decision`, `update-service-context`)
19
+
20
+ Quick-fixes skip `/nova-spec` and `/nova-plan`.
21
+
22
+ ## Branch config
23
+
24
+ In `novaspec/config.yml`:
25
+ - Pattern: `{type}/{ticket}-{slug}` (e.g., `feature/AGEX-123-new-feature`)
26
+ - Types: bugfix, hotfix, feature, documentation, refactor, chore, architecture
27
+ - Base branch: `main`
28
+
29
+ ## Memory structure
30
+
31
+ ```
32
+ context/
33
+ ├── decisions/ # Why we did X (one file per decision)
34
+ │ └── archived/ # Superseded (never auto-loaded by agents)
35
+ ├── gotchas/ # Non-obvious traps in the code
36
+ ├── services/ # Short map per service (≤80 lines, flat files)
37
+ ├── changes/
38
+ │ ├── active/ # In-progress specs
39
+ │ └── archive/ # Closed specs
40
+ └── backlog/ # Pending proposals
41
+ ```
42
+
43
+ Memory model: one fact = one file, filename = index, explicit supersede via `> Supersedes: <old>.md`. Wisdom about memory itself lives in `decisions/`, not in `AGENTS.md`.
44
+
45
+ ## Symlinks
46
+
47
+ Claude Code discovers commands via `.claude/` symlinks pointing to `novaspec/`.
48
+
49
+ ## Working here
50
+
51
+ This repo uses itself. When modifying nova-spec:
52
+ 1. Test changes in a worktree or sandbox project
53
+ 2. Verify symlinks work: `ls -la .claude/`
54
+ 3. Run through a full ticket cycle
55
+
56
+ ## Reference
57
+
58
+ - Full docs: [README.md](./README.md)
59
+ - Installation: [INSTALL.md](./INSTALL.md)
60
+ - Commands: `novaspec/commands/*.md`
61
+ - Skills: `novaspec/skills/*/SKILL.md`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adán González
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ <p align="center">
2
+ <img src="img/novaspec-logo.svg" alt="nova-spec" width="480">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <strong>Spec-Driven Development on top of Claude Code.</strong><br>
7
+ From a ticket to a merged PR in explicit steps, with architectural memory that doesn't decay.
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
12
+ <img alt="Status: experimental" src="https://img.shields.io/badge/status-experimental-orange.svg">
13
+ <img alt="Built for Claude Code" src="https://img.shields.io/badge/built%20for-Claude%20Code-purple.svg">
14
+ </p>
15
+
16
+ ---
17
+
18
+ ## What it is
19
+
20
+ `nova-spec` adds seven `/nova-*` commands to Claude Code that turn a ticket into a traceable change: classify, close requirements, plan, implement task by task, review, and wrap up with commit + PR + memory update. Memory (`context/decisions/`, `context/gotchas/`, `context/services/`) lives in atomic markdown files that humans edit and `grep` finds.
21
+
22
+ It's not a template or a generator. It's a set of conventions + commands that Claude Code runs as slash commands inside your repo.
23
+
24
+ ## Who is this for
25
+
26
+ - Developers using **Claude Code** (or OpenCode) on real projects, not toy demos.
27
+ - Teams that want their AI agent to follow a **disciplined ticket → PR flow** instead of one-shotting code.
28
+ - Anyone tired of **re-explaining the same architectural context** every new chat.
29
+
30
+ If you only use Claude Code for one-off scripts, this is overkill. If you ship to production with it, read on.
31
+
32
+ ## Why it exists
33
+
34
+ Without discipline, an agent writes code fast and loses the *why*. The next ticket forces you to re-explain the same context. `nova-spec` enforces human checkpoints, separates spec from executable tasks, and leaves a trail in `context/` so the next ticket starts informed.
35
+
36
+ ## Quickstart
37
+
38
+ ```bash
39
+ # 1. Clone nova-spec on your machine (one time only)
40
+ git clone https://github.com/adansuku/nova-spec.git ~/tools/nova-spec
41
+
42
+ # 2. From the repo where you want to use it
43
+ cd /path/to/your-project
44
+ bash ~/tools/nova-spec/install.sh
45
+
46
+ # 3. Open Claude Code and launch your first ticket
47
+ claude
48
+ /nova-start PROJ-123
49
+ ```
50
+
51
+ Full details in [INSTALL.md](./INSTALL.md).
52
+
53
+ ## A taste of it
54
+
55
+ What `/nova-start PROJ-42` actually does:
56
+
57
+ ```text
58
+ > /nova-start PROJ-42
59
+
60
+ Ticket: PROJ-42 — "Add rate limiting to /api/login"
61
+ Classification: feature (2h-3d)
62
+ Affected services: auth-api ✓
63
+
64
+ Branch created: feature/PROJ-42-rate-limit-login (from main)
65
+
66
+ Loaded context:
67
+ Services: auth-api ✓
68
+ Decisions read: throttling-strategy.md, redis-usage.md
69
+ Gaps: none
70
+ Questions: none
71
+
72
+ Next step: /nova-spec
73
+ ```
74
+
75
+ No code yet. The agent has classified the work, created the branch, and pulled in only the architectural decisions that matter for this ticket. From here you'd move on to `/nova-spec` to close requirements, then `/nova-plan`, then `/nova-build`.
76
+
77
+ ## Flow
78
+
79
+ ```
80
+ /nova-start → /nova-spec → /nova-plan → /nova-build → /nova-review → /nova-wrap
81
+ ```
82
+
83
+ | Command | What it does |
84
+ |---|---|
85
+ | `/nova-init` | One-off bootstrap: scans the repo and generates draft `context/services/` files with TODOs |
86
+ | `/nova-start <TICKET>` | Pulls the ticket, classifies it (quick-fix / feature / architecture), creates a branch, loads context |
87
+ | `/nova-spec` | Closes open decisions and writes `proposal.md` |
88
+ | `/nova-plan` | Translates the spec into `tasks.md` (plan + tasks) |
89
+ | `/nova-build` | Executes tasks one by one with incremental review |
90
+ | `/nova-review` | Final code review against spec, conventions and decisions |
91
+ | `/nova-wrap` | Updates memory, archives the spec, creates commit and PR |
92
+ | `/nova-status [TICKET]` | Current status of the ticket (read-only) |
93
+
94
+ `quick-fix` tickets skip `/nova-spec` and `/nova-plan`. `/nova-init` is optional and runs only once when installing nova-spec into an existing repo.
95
+
96
+ ## Principles
97
+
98
+ - **No skipping steps.** Each command has a guardrail that checks preconditions.
99
+ - **No making up context.** If info is missing, the command asks.
100
+ - **Human checkpoints** after `/nova-spec` and before `/nova-wrap`.
101
+ - **Memory that doesn't decay:** one fact = one file, name = index, explicit supersede.
102
+
103
+ ## Documentation
104
+
105
+ - Detailed install: [INSTALL.md](./INSTALL.md)
106
+ - Internal architecture: [novaspec/README.arch.md](./novaspec/README.arch.md)
107
+ - Quick reference: [novaspec/README.quickref.md](./novaspec/README.quickref.md)
108
+ - Contributing: [CONTRIBUTING.md](./CONTRIBUTING.md)
109
+
110
+ ## See also
111
+
112
+ `nova-spec` was built using itself. The full development history — including specs, decisions, gotchas and dogfooding — is preserved in the lab repo: [adansuku/nova-spec-lab](https://github.com/adansuku/nova-spec-lab).
113
+
114
+ ## License
115
+
116
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require('../lib/cli.js');
4
+
5
+ run().catch((err) => {
6
+ console.error(err.message);
7
+ process.exit(1);
8
+ });
package/lib/cli.js ADDED
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { init } = require('./installer.js');
4
+ const { sync } = require('./sync.js');
5
+
6
+ async function run() {
7
+ const command = process.argv[2];
8
+
9
+ switch (command) {
10
+ case 'init':
11
+ case undefined:
12
+ await init();
13
+ break;
14
+ case 'sync':
15
+ await sync();
16
+ break;
17
+ default:
18
+ console.error(`Unknown command: ${command}`);
19
+ console.error('Usage: nova-spec [init|sync]');
20
+ process.exit(1);
21
+ }
22
+ }
23
+
24
+ module.exports = { run };
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { select, input, confirm } = require('@inquirer/prompts');
6
+ const { generateManifest } = require('./sync.js');
7
+
8
+ const PACKAGE_ROOT = path.join(__dirname, '..');
9
+ const NOVASPEC_SRC = path.join(PACKAGE_ROOT, 'novaspec');
10
+ const AGENTS_SRC = path.join(PACKAGE_ROOT, 'AGENTS.md');
11
+ const CLAUDE_MD_SRC = path.join(PACKAGE_ROOT, 'CLAUDE.md');
12
+
13
+ async function init() {
14
+ console.log('\n nova-spec installer\n ───────────────────\n');
15
+
16
+ // 1. Scope: global or project
17
+ const scope = await select({
18
+ message: 'Where do you want to install nova-spec?',
19
+ choices: [
20
+ { name: 'This project only (installs in current directory)', value: 'project' },
21
+ { name: 'Global (works in all your projects via ~/.claude)', value: 'global' },
22
+ { name: 'Update existing installation', value: 'update' },
23
+ ],
24
+ });
25
+
26
+ const destDir = scope === 'global'
27
+ ? path.join(process.env.HOME || process.env.USERPROFILE, '.claude')
28
+ : process.cwd();
29
+
30
+ if (scope === 'update') {
31
+ const { sync } = require('./sync.js');
32
+ await sync(destDir);
33
+ return;
34
+ }
35
+
36
+ // 2. Runtime
37
+ const runtime = await select({
38
+ message: 'Which AI runtime do you use?',
39
+ choices: [
40
+ { name: 'Claude Code', value: 'claude' },
41
+ { name: 'OpenCode', value: 'opencode' },
42
+ { name: 'Both', value: 'both' },
43
+ ],
44
+ });
45
+
46
+ // 3. Jira
47
+ const useJira = await confirm({ message: 'Do you use Jira?', default: true });
48
+
49
+ let jiraConfig = { skill: '', url: '', project: '', email: '', token: '${JIRA_API_TOKEN}', done_transition_id: '41' };
50
+
51
+ if (useJira) {
52
+ jiraConfig.skill = 'jira-integration';
53
+ jiraConfig.url = await input({
54
+ message: 'Jira URL:',
55
+ default: 'https://your-workspace.atlassian.net',
56
+ });
57
+ jiraConfig.project = await input({ message: 'Jira project key:', default: 'PROJ' });
58
+ jiraConfig.email = await input({ message: 'Your Jira email:' });
59
+ jiraConfig.done_transition_id = await input({
60
+ message: 'Jira "Done" transition ID (find it via GET /rest/api/3/issue/<TICKET>/transitions):',
61
+ default: '41',
62
+ });
63
+ console.log('\n Tip: set JIRA_API_TOKEN in your environment.');
64
+ console.log(' Get your token at: https://id.atlassian.com/manage-profile/security/api-tokens\n');
65
+ }
66
+
67
+ // 4. Branch config
68
+ const baseBranch = await input({ message: 'Base branch:', default: 'main' });
69
+
70
+ // 5. Confirm
71
+ console.log('\n Summary:');
72
+ console.log(` → Scope: ${scope === 'global' ? 'Global (~/.claude)' : 'Project (' + destDir + ')'}`);
73
+ console.log(` → Runtime: ${runtime}`);
74
+ console.log(` → Jira: ${useJira ? jiraConfig.url + ' / ' + jiraConfig.project : 'disabled'}`);
75
+ console.log(` → Branch: ${baseBranch}\n`);
76
+
77
+ const ok = await confirm({ message: 'Install with these settings?', default: true });
78
+ if (!ok) {
79
+ console.log(' Cancelled.');
80
+ return;
81
+ }
82
+
83
+ // 6. Install
84
+ installFiles(destDir, runtime, scope);
85
+ writeConfig(destDir, { jiraConfig, baseBranch });
86
+ generateManifest(path.join(destDir, 'novaspec'));
87
+
88
+ console.log('\n ✓ nova-spec installed!\n');
89
+ console.log(' Next step: open Claude Code or OpenCode in this directory and run:');
90
+ console.log(' /nova-start TICKET-123\n');
91
+ }
92
+
93
+ function installFiles(destDir, runtime, scope) {
94
+ // Copy novaspec/
95
+ const destNovaspec = path.join(destDir, 'novaspec');
96
+ const destNovaspecConfig = path.join(destNovaspec, 'config.yml');
97
+
98
+ // Backup existing config.yml
99
+ let configBackup = null;
100
+ if (fs.existsSync(destNovaspecConfig)) {
101
+ configBackup = fs.readFileSync(destNovaspecConfig, 'utf8');
102
+ }
103
+
104
+ copyDir(NOVASPEC_SRC, destNovaspec);
105
+
106
+ // Restore config.yml if it existed (user's config wins)
107
+ if (configBackup) {
108
+ fs.writeFileSync(destNovaspecConfig, configBackup);
109
+ }
110
+
111
+ // Copy AGENTS.md and CLAUDE.md
112
+ if (fs.existsSync(AGENTS_SRC)) fs.copyFileSync(AGENTS_SRC, path.join(destDir, 'AGENTS.md'));
113
+ if (fs.existsSync(CLAUDE_MD_SRC)) fs.copyFileSync(CLAUDE_MD_SRC, path.join(destDir, 'CLAUDE.md'));
114
+
115
+ // Create context/ structure (only for project scope)
116
+ if (scope === 'project') {
117
+ for (const dir of [
118
+ 'context/decisions/archived',
119
+ 'context/gotchas',
120
+ 'context/services',
121
+ 'context/changes/active',
122
+ 'context/changes/archive',
123
+ ]) {
124
+ fs.mkdirSync(path.join(destDir, dir), { recursive: true });
125
+ }
126
+ const gitkeep = path.join(destDir, 'context/changes/active/.gitkeep');
127
+ if (!fs.existsSync(gitkeep)) fs.writeFileSync(gitkeep, '');
128
+
129
+ // notes.md
130
+ const notes = path.join(destDir, 'notes.md');
131
+ if (!fs.existsSync(notes)) fs.writeFileSync(notes, '');
132
+ }
133
+
134
+ // Runtime symlinks / .opencode settings
135
+ if (runtime === 'claude' || runtime === 'both') {
136
+ createSymlinks(destDir, '.claude');
137
+ }
138
+ if (runtime === 'opencode' || runtime === 'both') {
139
+ createSymlinks(destDir, '.opencode');
140
+ writeOpenCodeSettings(path.join(destDir, '.opencode'));
141
+ }
142
+
143
+ // .gitignore
144
+ ensureGitignore(destDir);
145
+ }
146
+
147
+ function createSymlinks(destDir, dotDir) {
148
+ const dir = path.join(destDir, dotDir);
149
+ fs.mkdirSync(dir, { recursive: true });
150
+ for (const name of ['commands', 'skills', 'agents']) {
151
+ const link = path.join(dir, name);
152
+ const target = path.join('..', 'novaspec', name);
153
+ if (fs.existsSync(link) || fs.lstatSync(link).isSymbolicLink?.()) {
154
+ fs.rmSync(link, { recursive: true, force: true });
155
+ }
156
+ fs.symlinkSync(target, link);
157
+ }
158
+ }
159
+
160
+ function writeOpenCodeSettings(opencodeDir) {
161
+ const settingsPath = path.join(opencodeDir, 'settings.local.json');
162
+ if (!fs.existsSync(settingsPath)) {
163
+ fs.writeFileSync(settingsPath, JSON.stringify({
164
+ $schema: 'https://opencode.ai/config.json',
165
+ permission: { skill: { '*': 'allow' } },
166
+ }, null, 2) + '\n');
167
+ }
168
+ }
169
+
170
+ function writeConfig(destDir, { jiraConfig, baseBranch }) {
171
+ const configPath = path.join(destDir, 'novaspec', 'config.yml');
172
+ if (fs.existsSync(configPath)) return; // already restored from backup
173
+
174
+ const content = [
175
+ '# nova-spec — project configuration',
176
+ '# This file is gitignored — do not push it to the repo.',
177
+ '',
178
+ 'branch:',
179
+ ' pattern: "{type}/{ticket}-{slug}"',
180
+ ' types:',
181
+ ' bugfix: bugfix',
182
+ ' hotfix: hotfix',
183
+ ' feature: feature',
184
+ ' documentation: docs',
185
+ ' refactor: refactor',
186
+ ' chore: chore',
187
+ ' architecture: arch',
188
+ ' ticket_case: upper',
189
+ ` base: ${baseBranch}`,
190
+ '',
191
+ 'jira:',
192
+ ` skill: "${jiraConfig.skill}"`,
193
+ ` url: ${jiraConfig.url}`,
194
+ ` project: ${jiraConfig.project}`,
195
+ ` email: ${jiraConfig.email}`,
196
+ ` token: ${jiraConfig.token}`,
197
+ ` done_transition_id: "${jiraConfig.done_transition_id}"`,
198
+ ].join('\n') + '\n';
199
+
200
+ fs.writeFileSync(configPath, content);
201
+ }
202
+
203
+ function ensureGitignore(destDir) {
204
+ const gitignorePath = path.join(destDir, '.gitignore');
205
+ const marker = '# nova-spec (local)';
206
+
207
+ if (fs.existsSync(gitignorePath)) {
208
+ const content = fs.readFileSync(gitignorePath, 'utf8');
209
+ if (content.includes(marker)) return;
210
+ }
211
+
212
+ const block = [
213
+ '',
214
+ '# nova-spec (local)',
215
+ 'novaspec/config.yml',
216
+ 'novaspec/custom/',
217
+ '.env',
218
+ 'notes.md',
219
+ '.opencode/settings.local.json',
220
+ '.opencode/node_modules/',
221
+ '.DS_Store',
222
+ '# /nova-spec',
223
+ '',
224
+ ].join('\n');
225
+
226
+ fs.appendFileSync(gitignorePath, block);
227
+ }
228
+
229
+ function copyDir(src, dest) {
230
+ fs.mkdirSync(dest, { recursive: true });
231
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
232
+ const srcPath = path.join(src, entry.name);
233
+ const destPath = path.join(dest, entry.name);
234
+ if (entry.name === 'config.yml') continue; // never overwrite user config
235
+ if (entry.isDirectory()) {
236
+ copyDir(srcPath, destPath);
237
+ } else {
238
+ fs.copyFileSync(srcPath, destPath);
239
+ }
240
+ }
241
+ }
242
+
243
+ module.exports = { init };
package/lib/sync.js ADDED
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ const MANIFEST_FILE = '.nova-manifest.json';
8
+
9
+ function hashFile(filePath) {
10
+ const content = fs.readFileSync(filePath);
11
+ return crypto.createHash('md5').update(content).digest('hex');
12
+ }
13
+
14
+ function walkDir(dir, base = dir) {
15
+ const results = {};
16
+ if (!fs.existsSync(dir)) return results;
17
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
18
+ const fullPath = path.join(dir, entry.name);
19
+ const relPath = path.relative(base, fullPath);
20
+ if (entry.isDirectory()) {
21
+ Object.assign(results, walkDir(fullPath, base));
22
+ } else {
23
+ results[relPath] = hashFile(fullPath);
24
+ }
25
+ }
26
+ return results;
27
+ }
28
+
29
+ function generateManifest(novaspecDir) {
30
+ const coreDir = novaspecDir;
31
+ const hashes = {};
32
+
33
+ for (const section of ['commands', 'skills', 'agents']) {
34
+ const sectionDir = path.join(coreDir, section);
35
+ if (!fs.existsSync(sectionDir)) continue;
36
+ for (const entry of fs.readdirSync(sectionDir, { withFileTypes: true })) {
37
+ if (!entry.isDirectory() && !entry.name.endsWith('.md')) continue;
38
+ const name = entry.name.replace('.md', '');
39
+ const fullPath = path.join(sectionDir, entry.name);
40
+ if (entry.isDirectory()) {
41
+ hashes[`${section}/${name}`] = walkDir(fullPath);
42
+ } else {
43
+ hashes[`${section}/${name}`] = hashFile(fullPath);
44
+ }
45
+ }
46
+ }
47
+
48
+ const manifest = {
49
+ version: require('../package.json').version,
50
+ generated_at: new Date().toISOString(),
51
+ hashes,
52
+ outdated_customs: [],
53
+ };
54
+
55
+ const manifestPath = path.join(novaspecDir, MANIFEST_FILE);
56
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
57
+ return manifest;
58
+ }
59
+
60
+ async function sync(destDir = process.cwd()) {
61
+ const novaspecDest = path.join(destDir, 'novaspec');
62
+ const manifestPath = path.join(novaspecDest, MANIFEST_FILE);
63
+
64
+ if (!fs.existsSync(novaspecDest)) {
65
+ console.error(' ✗ nova-spec not installed in this directory. Run: npx nova-spec init');
66
+ process.exit(1);
67
+ }
68
+
69
+ // Read existing manifest
70
+ const oldManifest = fs.existsSync(manifestPath)
71
+ ? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
72
+ : { hashes: {} };
73
+
74
+ // Update core files from package
75
+ const PACKAGE_ROOT = path.join(__dirname, '..');
76
+ const NOVASPEC_SRC = path.join(PACKAGE_ROOT, 'novaspec');
77
+
78
+ // Backup config.yml
79
+ const configPath = path.join(novaspecDest, 'config.yml');
80
+ let configBackup = null;
81
+ if (fs.existsSync(configPath)) {
82
+ configBackup = fs.readFileSync(configPath, 'utf8');
83
+ }
84
+
85
+ copyDirSync(NOVASPEC_SRC, novaspecDest);
86
+
87
+ // Restore config
88
+ if (configBackup) fs.writeFileSync(configPath, configBackup);
89
+
90
+ // Regenerate manifest with new hashes
91
+ const newManifest = generateManifest(novaspecDest);
92
+
93
+ // Check custom overrides
94
+ const customDir = path.join(novaspecDest, 'custom');
95
+ const outdated = [];
96
+
97
+ if (fs.existsSync(customDir)) {
98
+ for (const section of ['commands', 'skills', 'agents']) {
99
+ const customSection = path.join(customDir, section);
100
+ if (!fs.existsSync(customSection)) continue;
101
+ for (const entry of fs.readdirSync(customSection, { withFileTypes: true })) {
102
+ const name = entry.name.replace('.md', '');
103
+ const key = `${section}/${name}`;
104
+ const oldHash = oldManifest.hashes[key];
105
+ const newHash = newManifest.hashes[key];
106
+ if (oldHash && newHash && JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
107
+ outdated.push(name);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // Save outdated list in manifest
114
+ newManifest.outdated_customs = outdated;
115
+ fs.writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2) + '\n');
116
+
117
+ // Report
118
+ console.log('\n ✓ nova-spec core updated to v' + newManifest.version + '\n');
119
+
120
+ if (outdated.length > 0) {
121
+ console.log(' ⚠️ Custom overrides with upstream changes:');
122
+ for (const name of outdated) {
123
+ console.log(` - ${name} → run /nova-diff ${name} to review`);
124
+ }
125
+ } else {
126
+ console.log(' ✓ No custom overrides affected.');
127
+ }
128
+ console.log('');
129
+ }
130
+
131
+ function copyDirSync(src, dest) {
132
+ fs.mkdirSync(dest, { recursive: true });
133
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
134
+ if (entry.name === 'config.yml') continue;
135
+ if (entry.name === 'custom') continue; // never overwrite custom/
136
+ if (entry.name === MANIFEST_FILE) continue;
137
+ const srcPath = path.join(src, entry.name);
138
+ const destPath = path.join(dest, entry.name);
139
+ if (entry.isDirectory()) {
140
+ copyDirSync(srcPath, destPath);
141
+ } else {
142
+ fs.copyFileSync(srcPath, destPath);
143
+ }
144
+ }
145
+ }
146
+
147
+ module.exports = { sync, generateManifest };
@@ -0,0 +1,65 @@
1
+ ---
2
+ description: Load architectural context in an isolated context window and return a structured summary
3
+ argument-hint: <service1> [service2 ...]
4
+ ---
5
+
6
+ You are a context-loading agent. Your only job is to read the artifacts of
7
+ the given services and return a structured summary. Don't interact with
8
+ the user beyond the final summary. Don't modify any file.
9
+
10
+ ## Input
11
+
12
+ Affected services: `$ARGUMENTS` (space-separated list)
13
+
14
+ ## Hard rules
15
+
16
+ - **Never read `context/decisions/archived/`**. It's a trash bin; its content is explicitly out of the live scope.
17
+ - **Total budget: ≤3000 tokens**. If summing the chosen files gets close to the cap, trim by relevance. Don't load all of `decisions/` — only the 3-5 files whose names match the ticket scope.
18
+ - Don't write any file.
19
+ - Don't make up context.
20
+
21
+ ## Steps
22
+
23
+ ### 1. Verify `context/`
24
+
25
+ If `context/` doesn't exist, return:
26
+ ```
27
+ ## Loaded context
28
+ **Services**: not documented (context/ missing)
29
+ **Decisions**: none
30
+ **Gaps**: context/ structure not initialized — run install.sh
31
+ **Questions**: none
32
+ ```
33
+ And stop.
34
+
35
+ ### 2. Read each service
36
+
37
+ For each service in `$ARGUMENTS`:
38
+ - Read `context/services/<service>.md` if it exists.
39
+ - If it doesn't exist, note it as a gap.
40
+
41
+ ### 3. Pick relevant decisions and gotchas
42
+
43
+ - `ls context/decisions/` (no `-R`, doesn't enter `archived/`).
44
+ - `ls context/gotchas/`.
45
+ - Pick 3-5 files from each whose name is relevant to the ticket's scope or affected services. Don't force connections.
46
+ - Read the chosen ones.
47
+
48
+ ### 4. Return summary
49
+
50
+ Return exactly this structure, without extra text:
51
+
52
+ ```
53
+ ## Loaded context
54
+
55
+ **Services**: <list with ✓ if services/<svc>.md exists, ✗ otherwise>
56
+ **Decisions read**: <list of files or "none">
57
+ **Gotchas read**: <list of files or "none">
58
+ **Gaps**: <missing files or "none">
59
+ **Questions**: <detected ambiguities or "none">
60
+ ```
61
+
62
+ ## Rules (reminder)
63
+
64
+ - Don't block if documentation is missing; report it under Gaps.
65
+ - Return only the `## Loaded context` block.