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.
- package/AGENTS.md +61 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/bin/nova-spec.js +8 -0
- package/lib/cli.js +24 -0
- package/lib/installer.js +243 -0
- package/lib/sync.js +147 -0
- package/novaspec/agents/context-loader.md +65 -0
- package/novaspec/agents/nova-review-agent.md +73 -0
- package/novaspec/commands/nova-build.md +70 -0
- package/novaspec/commands/nova-diff.md +64 -0
- package/novaspec/commands/nova-plan.md +41 -0
- package/novaspec/commands/nova-review.md +35 -0
- package/novaspec/commands/nova-spec.md +41 -0
- package/novaspec/commands/nova-start.md +82 -0
- package/novaspec/commands/nova-status.md +82 -0
- package/novaspec/commands/nova-sync.md +46 -0
- package/novaspec/commands/nova-wrap.md +94 -0
- package/novaspec/config.example.yml +24 -0
- package/novaspec/custom/agents/.gitkeep +0 -0
- package/novaspec/custom/commands/.gitkeep +0 -0
- package/novaspec/custom/skills/.gitkeep +0 -0
- package/novaspec/guardrails/checklist.md +34 -0
- package/novaspec/skills/close-requirement/SKILL.md +56 -0
- package/novaspec/skills/jira-integration/SKILL.md +96 -0
- package/novaspec/skills/update-service-context/SKILL.md +60 -0
- package/novaspec/skills/write-decision/SKILL.md +69 -0
- package/novaspec/templates/commit.md +6 -0
- package/novaspec/templates/pr-body.md +20 -0
- package/novaspec/templates/proposal.md +49 -0
- package/novaspec/templates/review.md +22 -0
- package/novaspec/templates/status-report.md +8 -0
- package/novaspec/templates/tasks.md +24 -0
- 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).
|
package/bin/nova-spec.js
ADDED
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 };
|
package/lib/installer.js
ADDED
|
@@ -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.
|