nubos-pilot 0.3.0 → 0.4.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.
@@ -0,0 +1,290 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const crypto = require('node:crypto');
4
+ const { NubosPilotError } = require('./core.cjs');
5
+
6
+ const DEFAULT_IGNORES = Object.freeze(new Set([
7
+ 'node_modules', '.git', '.nubos-pilot', '.planning', '.claude',
8
+ 'vendor', 'target', 'build', 'dist', 'out',
9
+ '.next', '.nuxt', '.svelte-kit', '.astro',
10
+ '.venv', 'venv', 'env', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache',
11
+ 'coverage', '.coverage', '.nyc_output', '.tox',
12
+ '.idea', '.vscode', '.vs',
13
+ '.cache', '.turbo', '.parcel-cache', '.gradle',
14
+ 'Pods', 'DerivedData',
15
+ 'tmp', 'temp', '.tmp',
16
+ ]));
17
+
18
+ const MANIFEST_FILES = Object.freeze(new Set([
19
+ 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'pnpm-workspace.yaml',
20
+ 'tsconfig.json', 'jsconfig.json',
21
+ 'pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile', 'Pipfile.lock', 'poetry.lock',
22
+ 'Cargo.toml', 'Cargo.lock',
23
+ 'go.mod', 'go.sum',
24
+ 'composer.json', 'composer.lock',
25
+ 'Gemfile', 'Gemfile.lock', 'Rakefile',
26
+ 'mix.exs', 'rebar.config',
27
+ 'pom.xml', 'build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts',
28
+ 'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml',
29
+ 'Makefile', 'CMakeLists.txt',
30
+ '.nvmrc', '.tool-versions', '.node-version', '.python-version', '.ruby-version',
31
+ '.env.example', '.env.sample', '.editorconfig',
32
+ ]));
33
+
34
+ const DOC_FILE_PREFIXES = Object.freeze([
35
+ 'README', 'CHANGELOG', 'LICENSE', 'CONTRIBUTING',
36
+ 'ARCHITECTURE', 'ROADMAP', 'SECURITY', 'CODE_OF_CONDUCT',
37
+ 'AUTHORS', 'NOTICE', 'DESIGN',
38
+ ]);
39
+
40
+ const BINARY_EXTS = Object.freeze(new Set([
41
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.avif',
42
+ '.pdf', '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar', '.xz',
43
+ '.mp3', '.mp4', '.mov', '.avi', '.mkv', '.wav', '.flac', '.ogg', '.webm',
44
+ '.ttf', '.otf', '.woff', '.woff2', '.eot',
45
+ '.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a',
46
+ '.class', '.jar', '.war', '.pyc', '.pyo',
47
+ '.db', '.sqlite', '.sqlite3',
48
+ '.node', '.wasm',
49
+ '.psd', '.ai', '.sketch', '.fig',
50
+ ]));
51
+
52
+ const MAX_FILE_HASH_BYTES = 512 * 1024;
53
+ const MAX_FILE_CAPTURE_BYTES = 200 * 1024;
54
+ const MAX_FILES_WALKED = 100000;
55
+ const MAX_DEPTH = 12;
56
+
57
+ function _isDocFile(basename) {
58
+ const up = basename.toUpperCase();
59
+ for (const prefix of DOC_FILE_PREFIXES) {
60
+ if (up === prefix) return true;
61
+ if (up.startsWith(prefix + '.')) return true;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ function _isDotfileAllowed(name) {
67
+ if (name === '.nvmrc' || name === '.tool-versions' || name === '.node-version') return true;
68
+ if (name === '.python-version' || name === '.ruby-version') return true;
69
+ if (name === '.env.example' || name === '.env.sample') return true;
70
+ if (name === '.editorconfig' || name === '.gitignore' || name === '.dockerignore') return true;
71
+ if (name === '.gitattributes') return true;
72
+ return false;
73
+ }
74
+
75
+ function _sha256(buffer) {
76
+ return 'sha256:' + crypto.createHash('sha256').update(buffer).digest('hex');
77
+ }
78
+
79
+ function _readCapture(absPath) {
80
+ try {
81
+ const fd = fs.openSync(absPath, 'r');
82
+ try {
83
+ const stat = fs.fstatSync(fd);
84
+ const toRead = Math.min(stat.size, MAX_FILE_CAPTURE_BYTES);
85
+ const buf = Buffer.alloc(toRead);
86
+ if (toRead > 0) fs.readSync(fd, buf, 0, toRead, 0);
87
+ return {
88
+ content: buf.toString('utf-8'),
89
+ size: stat.size,
90
+ truncated: stat.size > toRead,
91
+ };
92
+ } finally {
93
+ fs.closeSync(fd);
94
+ }
95
+ } catch (err) {
96
+ return { error: err && err.code ? err.code : String(err) };
97
+ }
98
+ }
99
+
100
+ function _walk(root, ignores, opts) {
101
+ const files = [];
102
+ const skipped = [];
103
+ let walked = 0;
104
+
105
+ function visit(abs, rel, depth) {
106
+ if (walked >= opts.maxFiles) return;
107
+ if (depth > opts.maxDepth) return;
108
+ let entries;
109
+ try {
110
+ entries = fs.readdirSync(abs, { withFileTypes: true });
111
+ } catch (err) {
112
+ skipped.push({ path: rel || '.', reason: 'readdir-error', detail: err && err.code });
113
+ return;
114
+ }
115
+ for (const entry of entries) {
116
+ if (walked >= opts.maxFiles) {
117
+ skipped.push({ path: rel, reason: 'max-files-reached' });
118
+ return;
119
+ }
120
+ const name = entry.name;
121
+ if (ignores.has(name)) continue;
122
+ if (name.startsWith('.') && entry.isDirectory()) continue;
123
+ if (name.startsWith('.') && entry.isFile() && !_isDotfileAllowed(name)) continue;
124
+
125
+ const childAbs = path.join(abs, name);
126
+ const childRel = rel === '' ? name : rel + '/' + name;
127
+ if (entry.isSymbolicLink()) continue;
128
+ if (entry.isDirectory()) {
129
+ visit(childAbs, childRel, depth + 1);
130
+ } else if (entry.isFile()) {
131
+ walked++;
132
+ let stat;
133
+ try {
134
+ stat = fs.statSync(childAbs);
135
+ } catch (err) {
136
+ skipped.push({ path: childRel, reason: 'stat-error', detail: err && err.code });
137
+ continue;
138
+ }
139
+ files.push({
140
+ path: childRel,
141
+ absPath: childAbs,
142
+ size: stat.size,
143
+ ext: path.extname(name).toLowerCase(),
144
+ basename: name,
145
+ });
146
+ }
147
+ }
148
+ }
149
+
150
+ visit(root, '', 0);
151
+ return { files, skipped };
152
+ }
153
+
154
+ function _hashFile(file) {
155
+ if (BINARY_EXTS.has(file.ext)) return null;
156
+ if (file.size > MAX_FILE_HASH_BYTES) return null;
157
+ try {
158
+ return _sha256(fs.readFileSync(file.absPath));
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ function scan(opts) {
165
+ const options = opts || {};
166
+ const cwd = path.resolve(options.cwd || process.cwd());
167
+ const ignores = new Set([...DEFAULT_IGNORES, ...(options.additionalIgnores || [])]);
168
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
169
+ const batchSize = options.batchSize > 0 ? options.batchSize : 500;
170
+ const maxFiles = options.maxFiles > 0 ? options.maxFiles : MAX_FILES_WALKED;
171
+ const maxDepth = options.maxDepth > 0 ? options.maxDepth : MAX_DEPTH;
172
+
173
+ let rootStat;
174
+ try {
175
+ rootStat = fs.statSync(cwd);
176
+ } catch (err) {
177
+ throw new NubosPilotError(
178
+ 'scan-cwd-unreadable',
179
+ `cannot stat cwd: ${cwd}`,
180
+ { cwd, cause: err && err.code },
181
+ );
182
+ }
183
+ if (!rootStat.isDirectory()) {
184
+ throw new NubosPilotError(
185
+ 'scan-not-a-directory',
186
+ `cwd is not a directory: ${cwd}`,
187
+ { cwd },
188
+ );
189
+ }
190
+
191
+ onProgress({ phase: 'walk-start', cwd });
192
+ const { files, skipped } = _walk(cwd, ignores, { maxFiles, maxDepth });
193
+ onProgress({ phase: 'walk-complete', file_count: files.length, skipped: skipped.length });
194
+
195
+ const language_distribution = {};
196
+ const manifests = {};
197
+ const docs = {};
198
+ const fileHashes = [];
199
+ let totalBytes = 0;
200
+
201
+ const totalBatches = Math.ceil(files.length / batchSize);
202
+ for (let i = 0; i < files.length; i += batchSize) {
203
+ const batchIndex = Math.floor(i / batchSize);
204
+ const batch = files.slice(i, i + batchSize);
205
+ onProgress({
206
+ phase: 'batch-start',
207
+ index: batchIndex,
208
+ total: totalBatches,
209
+ size: batch.length,
210
+ files_processed: i,
211
+ files_total: files.length,
212
+ });
213
+
214
+ for (const f of batch) {
215
+ totalBytes += f.size;
216
+ const extKey = f.ext || '<no-ext>';
217
+ language_distribution[extKey] = (language_distribution[extKey] || 0) + 1;
218
+
219
+ const isManifest = MANIFEST_FILES.has(f.basename);
220
+ const isDoc = _isDocFile(f.basename);
221
+ if (isManifest || isDoc) {
222
+ const captured = _readCapture(f.absPath);
223
+ const entry = { path: f.path, size: f.size, ...captured };
224
+ if (isManifest) manifests[f.path] = entry;
225
+ else docs[f.path] = entry;
226
+ }
227
+
228
+ const hash = _hashFile(f);
229
+ if (hash) {
230
+ fileHashes.push({
231
+ path: f.path,
232
+ size: f.size,
233
+ sha256: hash,
234
+ ext: f.ext,
235
+ });
236
+ } else {
237
+ skipped.push({
238
+ path: f.path,
239
+ reason: BINARY_EXTS.has(f.ext) ? 'binary' : f.size > MAX_FILE_HASH_BYTES ? 'too-large' : 'hash-error',
240
+ size: f.size,
241
+ });
242
+ }
243
+ }
244
+
245
+ onProgress({
246
+ phase: 'batch-done',
247
+ index: batchIndex,
248
+ total: totalBatches,
249
+ files_processed: Math.min(i + batch.length, files.length),
250
+ files_total: files.length,
251
+ });
252
+ }
253
+
254
+ let git = { is_repo: false };
255
+ if (typeof options.gitInfo === 'function') {
256
+ try { git = options.gitInfo(cwd) || { is_repo: false }; }
257
+ catch { git = { is_repo: false }; }
258
+ }
259
+
260
+ const result = {
261
+ cwd,
262
+ scanned_at: new Date().toISOString(),
263
+ stats: {
264
+ file_count: files.length,
265
+ hashed_count: fileHashes.length,
266
+ manifest_count: Object.keys(manifests).length,
267
+ doc_count: Object.keys(docs).length,
268
+ skipped_count: skipped.length,
269
+ total_bytes: totalBytes,
270
+ },
271
+ files: fileHashes,
272
+ manifests,
273
+ docs,
274
+ git,
275
+ language_distribution,
276
+ skipped,
277
+ };
278
+
279
+ onProgress({ phase: 'complete', stats: result.stats });
280
+ return result;
281
+ }
282
+
283
+ module.exports = {
284
+ scan,
285
+ DEFAULT_IGNORES,
286
+ MANIFEST_FILES,
287
+ BINARY_EXTS,
288
+ MAX_FILE_HASH_BYTES,
289
+ MAX_FILE_CAPTURE_BYTES,
290
+ };
@@ -0,0 +1,212 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const { execFileSync } = require('node:child_process');
7
+
8
+ const { scan, DEFAULT_IGNORES, MANIFEST_FILES } = require('./workspace-scan.cjs');
9
+ const { workspaceGitInfo } = require('./git.cjs');
10
+
11
+ const _sandboxes = [];
12
+
13
+ function makeSandbox() {
14
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-ws-scan-'));
15
+ _sandboxes.push(dir);
16
+ return dir;
17
+ }
18
+
19
+ function write(root, rel, content) {
20
+ const abs = path.join(root, rel);
21
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
22
+ fs.writeFileSync(abs, content);
23
+ }
24
+
25
+ afterEach(() => {
26
+ while (_sandboxes.length) {
27
+ const dir = _sandboxes.pop();
28
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
29
+ }
30
+ });
31
+
32
+ test('WS-1: empty directory returns zero files', () => {
33
+ const root = makeSandbox();
34
+ const result = scan({ cwd: root });
35
+ assert.equal(result.stats.file_count, 0);
36
+ assert.equal(result.stats.hashed_count, 0);
37
+ assert.deepEqual(result.files, []);
38
+ assert.equal(result.git.is_repo, false);
39
+ });
40
+
41
+ test('WS-2: walks regular source files and hashes them', () => {
42
+ const root = makeSandbox();
43
+ write(root, 'src/index.js', 'console.log("hi");\n');
44
+ write(root, 'src/util.js', 'module.exports = {};\n');
45
+ write(root, 'lib/helper.py', 'def f(): pass\n');
46
+
47
+ const result = scan({ cwd: root });
48
+ assert.equal(result.stats.file_count, 3);
49
+ assert.equal(result.stats.hashed_count, 3);
50
+ const paths = result.files.map((f) => f.path).sort();
51
+ assert.deepEqual(paths, ['lib/helper.py', 'src/index.js', 'src/util.js']);
52
+ for (const f of result.files) {
53
+ assert.match(f.sha256, /^sha256:[a-f0-9]{64}$/);
54
+ }
55
+ });
56
+
57
+ test('WS-3: ignores node_modules / .git / vendor / .nubos-pilot', () => {
58
+ const root = makeSandbox();
59
+ write(root, 'src/a.js', 'a');
60
+ write(root, 'node_modules/foo/index.js', 'foo');
61
+ write(root, 'vendor/libx/x.php', 'x');
62
+ write(root, '.git/HEAD', 'ref: refs/heads/main');
63
+ write(root, '.nubos-pilot/PROJECT.md', '# project');
64
+
65
+ const result = scan({ cwd: root });
66
+ const paths = result.files.map((f) => f.path);
67
+ assert.deepEqual(paths, ['src/a.js']);
68
+ });
69
+
70
+ test('WS-4: captures manifest content (package.json) verbatim', () => {
71
+ const root = makeSandbox();
72
+ const pkg = { name: 'demo', version: '1.0.0', dependencies: { express: '^4' } };
73
+ write(root, 'package.json', JSON.stringify(pkg, null, 2));
74
+
75
+ const result = scan({ cwd: root });
76
+ assert.ok(result.manifests['package.json']);
77
+ const parsed = JSON.parse(result.manifests['package.json'].content);
78
+ assert.equal(parsed.name, 'demo');
79
+ assert.equal(parsed.dependencies.express, '^4');
80
+ });
81
+
82
+ test('WS-5: captures README as a doc', () => {
83
+ const root = makeSandbox();
84
+ write(root, 'README.md', '# Demo\n\nHello world.\n');
85
+
86
+ const result = scan({ cwd: root });
87
+ assert.ok(result.docs['README.md']);
88
+ assert.ok(result.docs['README.md'].content.includes('Demo'));
89
+ });
90
+
91
+ test('WS-6: language_distribution counts extensions', () => {
92
+ const root = makeSandbox();
93
+ write(root, 'a.js', 'a');
94
+ write(root, 'b.js', 'b');
95
+ write(root, 'c.py', 'c');
96
+ write(root, 'Makefile', 'all:\n');
97
+
98
+ const result = scan({ cwd: root });
99
+ assert.equal(result.language_distribution['.js'], 2);
100
+ assert.equal(result.language_distribution['.py'], 1);
101
+ assert.equal(result.language_distribution['<no-ext>'], 1);
102
+ });
103
+
104
+ test('WS-7: onProgress emits batch events', () => {
105
+ const root = makeSandbox();
106
+ for (let i = 0; i < 5; i++) write(root, `f${i}.txt`, 'x');
107
+
108
+ const events = [];
109
+ scan({ cwd: root, batchSize: 2, onProgress: (e) => events.push(e.phase) });
110
+ assert.ok(events.includes('walk-start'));
111
+ assert.ok(events.includes('walk-complete'));
112
+ assert.ok(events.includes('batch-start'));
113
+ assert.ok(events.includes('batch-done'));
114
+ assert.equal(events[events.length - 1], 'complete');
115
+ });
116
+
117
+ test('WS-8: binary extensions skip hashing', () => {
118
+ const root = makeSandbox();
119
+ write(root, 'logo.png', Buffer.from([0x89, 0x50, 0x4e, 0x47]));
120
+ write(root, 'code.js', 'x');
121
+
122
+ const result = scan({ cwd: root });
123
+ const hashed = result.files.map((f) => f.path);
124
+ assert.deepEqual(hashed, ['code.js']);
125
+ const skippedPaths = result.skipped.map((s) => s.path);
126
+ assert.ok(skippedPaths.includes('logo.png'));
127
+ });
128
+
129
+ test('WS-9: additionalIgnores extend the default ignore set', () => {
130
+ const root = makeSandbox();
131
+ write(root, 'src/a.js', 'a');
132
+ write(root, 'generated/big.js', 'g');
133
+
134
+ const result = scan({ cwd: root, additionalIgnores: ['generated'] });
135
+ const paths = result.files.map((f) => f.path);
136
+ assert.deepEqual(paths, ['src/a.js']);
137
+ });
138
+
139
+ test('WS-10: detects git repo and captures commits', () => {
140
+ const root = makeSandbox();
141
+ write(root, 'a.txt', 'a');
142
+ try {
143
+ execFileSync('git', ['init', '-q'], { cwd: root, stdio: 'ignore' });
144
+ execFileSync('git', ['config', 'user.email', 't@t.t'], { cwd: root, stdio: 'ignore' });
145
+ execFileSync('git', ['config', 'user.name', 't'], { cwd: root, stdio: 'ignore' });
146
+ execFileSync('git', ['add', 'a.txt'], { cwd: root, stdio: 'ignore' });
147
+ execFileSync('git', ['commit', '-q', '-m', 'init'], { cwd: root, stdio: 'ignore' });
148
+ } catch {
149
+ return;
150
+ }
151
+
152
+ const result = scan({ cwd: root, gitInfo: workspaceGitInfo });
153
+ assert.equal(result.git.is_repo, true);
154
+ assert.ok(Array.isArray(result.git.commits));
155
+ assert.ok(result.git.commits.length >= 1);
156
+ assert.equal(result.git.commits[0].subject, 'init');
157
+ });
158
+
159
+ test('WS-11: throws scan-not-a-directory for file path', () => {
160
+ const root = makeSandbox();
161
+ const filePath = path.join(root, 'not-a-dir.txt');
162
+ fs.writeFileSync(filePath, 'x');
163
+ assert.throws(
164
+ () => scan({ cwd: filePath }),
165
+ (err) => err.code === 'scan-not-a-directory',
166
+ );
167
+ });
168
+
169
+ test('WS-12: manifest file triggers capture even in nested dir', () => {
170
+ const root = makeSandbox();
171
+ write(root, 'services/api/package.json', JSON.stringify({ name: 'api' }));
172
+ write(root, 'services/api/src/main.ts', 'export {};');
173
+
174
+ const result = scan({ cwd: root });
175
+ assert.ok(result.manifests['services/api/package.json']);
176
+ });
177
+
178
+ test('WS-13: stats counts match produced arrays', () => {
179
+ const root = makeSandbox();
180
+ write(root, 'a.js', 'a');
181
+ write(root, 'b.js', 'b');
182
+ write(root, 'README.md', '# x');
183
+ write(root, 'package.json', '{}');
184
+
185
+ const result = scan({ cwd: root });
186
+ assert.equal(result.stats.file_count, 4);
187
+ assert.equal(result.stats.manifest_count, Object.keys(result.manifests).length);
188
+ assert.equal(result.stats.doc_count, Object.keys(result.docs).length);
189
+ assert.equal(result.stats.hashed_count, result.files.length);
190
+ });
191
+
192
+ test('WS-14: dotfiles like .nvmrc are walked but .idea directory is ignored', () => {
193
+ const root = makeSandbox();
194
+ write(root, '.nvmrc', '22');
195
+ write(root, '.idea/workspace.xml', '<x/>');
196
+ write(root, 'src/a.js', 'a');
197
+
198
+ const result = scan({ cwd: root });
199
+ const paths = result.files.map((f) => f.path).sort();
200
+ assert.ok(paths.includes('.nvmrc'));
201
+ assert.ok(paths.includes('src/a.js'));
202
+ assert.ok(!paths.some((p) => p.startsWith('.idea')));
203
+ });
204
+
205
+ test('WS-15: MANIFEST_FILES set is frozen and contains known ones', () => {
206
+ assert.ok(MANIFEST_FILES.has('package.json'));
207
+ assert.ok(MANIFEST_FILES.has('Cargo.toml'));
208
+ assert.ok(MANIFEST_FILES.has('go.mod'));
209
+ assert.ok(MANIFEST_FILES.has('composer.json'));
210
+ assert.ok(DEFAULT_IGNORES.has('node_modules'));
211
+ assert.ok(DEFAULT_IGNORES.has('.git'));
212
+ });
package/np-tools.cjs CHANGED
@@ -15,6 +15,7 @@ const initWorkflows = {
15
15
  'discuss-phase-power': require('./bin/np-tools/discuss-phase-power.cjs'),
16
16
  'research-phase': require('./bin/np-tools/research-phase.cjs'),
17
17
  'new-project': require('./bin/np-tools/new-project.cjs'),
18
+ 'discuss-project': require('./bin/np-tools/discuss-project.cjs'),
18
19
  'new-milestone': require('./bin/np-tools/new-milestone.cjs'),
19
20
  'plan-milestone-gaps': require('./bin/np-tools/plan-milestone-gaps.cjs'),
20
21
 
@@ -52,6 +53,8 @@ const topLevelCommands = {
52
53
  'commit': require('./bin/np-tools/commit.cjs'),
53
54
  'config-get': require('./bin/np-tools/config.cjs'),
54
55
  'dispatch': require('./bin/np-tools/dispatch.cjs'),
56
+ 'scan-codebase': require('./bin/np-tools/scan-codebase.cjs'),
57
+ 'update-docs': require('./bin/np-tools/update-docs.cjs'),
55
58
  'doctor': require('./bin/np-tools/doctor.cjs'),
56
59
  'generate-slug': require('./bin/np-tools/slug.cjs'),
57
60
  'metrics': require('./bin/np-tools/metrics.cjs'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {
@@ -1,4 +1,4 @@
1
- <!-- Placeholders: core_value, created_date, first_milestone_name, first_phase_name, primary_constraints, project_name -->
1
+ <!-- Placeholders: core_value, created_date, domain_text, first_milestone_name, first_phase_name, non_goals_text, primary_constraints, project_description, project_name, strategic_decisions_text, success_criteria_text, target_users_text -->
2
2
  # {{project_name}}
3
3
 
4
4
  ## Project
@@ -7,9 +7,15 @@
7
7
 
8
8
  ## What This Is
9
9
 
10
- {{project_name}} is an early-stage project. Update this section after the first
11
- phase ships with a concrete 2-3 sentence description of what the product does
12
- and who it serves. Use the user's language and framing.
10
+ {{project_description}}
11
+
12
+ ## Domain
13
+
14
+ {{domain_text}}
15
+
16
+ ## Target Users
17
+
18
+ {{target_users_text}}
13
19
 
14
20
  ## Core Value
15
21
 
@@ -18,6 +24,18 @@ and who it serves. Use the user's language and framing.
18
24
  If everything else fails, this one sentence must remain true. It drives
19
25
  prioritization when tradeoffs arise.
20
26
 
27
+ ## Non-Goals
28
+
29
+ {{non_goals_text}}
30
+
31
+ ## Success Criteria
32
+
33
+ {{success_criteria_text}}
34
+
35
+ ## Strategic Decisions
36
+
37
+ {{strategic_decisions_text}}
38
+
21
39
  ## Constraints
22
40
 
23
41
  {{primary_constraints}}
@@ -55,6 +73,10 @@ PROJECT.md evolves throughout the project lifecycle.
55
73
  2. Core Value check — still the right priority?
56
74
  3. Update Current Focus with next milestone/phase
57
75
 
76
+ **When scope or positioning shifts:**
77
+ - Run `np:discuss-project` to refresh Domain, Target Users, Non-Goals,
78
+ Success Criteria, and Strategic Decisions without starting over.
79
+
58
80
  ---
59
81
  *Created: {{created_date}}*
60
82
  *Last updated: {{created_date}} after np:new-project*