nubos-pilot 0.2.2 → 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.
- package/agents/np-code-fixer.md +22 -4
- package/agents/np-code-reviewer.md +6 -5
- package/agents/np-codebase-documenter.md +176 -0
- package/agents/np-executor.md +54 -7
- package/agents/np-planner.md +1 -0
- package/agents/np-researcher.md +7 -0
- package/bin/install.js +81 -8
- package/bin/np-tools/discuss-project.cjs +377 -0
- package/bin/np-tools/discuss-project.test.cjs +238 -0
- package/bin/np-tools/doctor.cjs +103 -0
- package/bin/np-tools/doctor.test.cjs +112 -0
- package/bin/np-tools/new-project.cjs +6 -0
- package/bin/np-tools/scan-codebase.cjs +204 -0
- package/bin/np-tools/scan-codebase.test.cjs +165 -0
- package/bin/np-tools/update-docs.cjs +216 -0
- package/bin/np-tools/update-docs.test.cjs +130 -0
- package/docs/adr/0007-codebase-docs-layer.md +273 -0
- package/docs/adr/README.md +2 -0
- package/lib/codebase-docs.cjs +450 -0
- package/lib/codebase-docs.test.cjs +266 -0
- package/lib/codebase-manifest.cjs +171 -0
- package/lib/codebase-manifest.test.cjs +156 -0
- package/lib/git.cjs +38 -0
- package/lib/install/runtime-assets.cjs +145 -0
- package/lib/install/runtimes-registry.cjs +4 -0
- package/lib/workspace-scan.cjs +290 -0
- package/lib/workspace-scan.test.cjs +212 -0
- package/np-tools.cjs +3 -0
- package/package.json +1 -1
- package/templates/PROJECT.md +26 -4
- package/workflows/discuss-project.md +177 -0
- package/workflows/new-project.md +141 -94
- package/workflows/scan-codebase.md +155 -0
- package/workflows/update-docs.md +132 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { NubosPilotError, atomicWriteFileSync } = require('../../lib/core.cjs');
|
|
5
|
+
const { scan } = require('../../lib/workspace-scan.cjs');
|
|
6
|
+
const { workspaceGitInfo } = require('../../lib/git.cjs');
|
|
7
|
+
const {
|
|
8
|
+
manifestFromScanFiles,
|
|
9
|
+
writeManifest,
|
|
10
|
+
readManifest,
|
|
11
|
+
diffManifest,
|
|
12
|
+
stalePathsForDocs,
|
|
13
|
+
} = require('../../lib/codebase-manifest.cjs');
|
|
14
|
+
const {
|
|
15
|
+
groupFilesIntoModules,
|
|
16
|
+
buildModuleFacts,
|
|
17
|
+
renderModuleDoc,
|
|
18
|
+
buildIndexDoc,
|
|
19
|
+
buildDocIndexMap,
|
|
20
|
+
moduleDocPath,
|
|
21
|
+
indexDocPath,
|
|
22
|
+
} = require('../../lib/codebase-docs.cjs');
|
|
23
|
+
|
|
24
|
+
function _parseArgs(args) {
|
|
25
|
+
const flags = {
|
|
26
|
+
cwd: null,
|
|
27
|
+
batchSize: 500,
|
|
28
|
+
maxFiles: 0,
|
|
29
|
+
applyProse: false,
|
|
30
|
+
moduleId: null,
|
|
31
|
+
proseFile: null,
|
|
32
|
+
paths: [],
|
|
33
|
+
};
|
|
34
|
+
for (let i = 0; i < (args || []).length; i++) {
|
|
35
|
+
const a = args[i];
|
|
36
|
+
if (a === '--cwd') flags.cwd = args[++i];
|
|
37
|
+
else if (a === '--batch-size') flags.batchSize = parseInt(args[++i], 10);
|
|
38
|
+
else if (a === '--max-files') flags.maxFiles = parseInt(args[++i], 10);
|
|
39
|
+
else if (a === '--apply-prose') flags.applyProse = true;
|
|
40
|
+
else if (a === '--module') flags.moduleId = args[++i];
|
|
41
|
+
else if (a === '--prose-file') flags.proseFile = args[++i];
|
|
42
|
+
else if (a === '--path') flags.paths.push(args[++i]);
|
|
43
|
+
}
|
|
44
|
+
return flags;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _readDocIndex(projectRoot) {
|
|
48
|
+
const p = path.join(projectRoot, '.nubos-pilot', 'codebase', '.doc-index.json');
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _hashesLookupFromManifest(manifest) {
|
|
57
|
+
const lookup = {};
|
|
58
|
+
for (const [p, meta] of Object.entries(manifest.files || {})) {
|
|
59
|
+
lookup[p] = meta.sha256;
|
|
60
|
+
}
|
|
61
|
+
return lookup;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _emitPlan(projectRoot, flags, stdout) {
|
|
65
|
+
const prev = readManifest(projectRoot);
|
|
66
|
+
const scanResult = scan({
|
|
67
|
+
cwd: projectRoot,
|
|
68
|
+
batchSize: flags.batchSize,
|
|
69
|
+
maxFiles: flags.maxFiles > 0 ? flags.maxFiles : undefined,
|
|
70
|
+
gitInfo: workspaceGitInfo,
|
|
71
|
+
});
|
|
72
|
+
const next = manifestFromScanFiles(scanResult.files);
|
|
73
|
+
const diff = diffManifest(prev, next);
|
|
74
|
+
|
|
75
|
+
const docIndex = _readDocIndex(projectRoot);
|
|
76
|
+
const staleInfo = stalePathsForDocs(diff, docIndex);
|
|
77
|
+
|
|
78
|
+
const groups = groupFilesIntoModules(scanResult.files);
|
|
79
|
+
const modulesById = new Map();
|
|
80
|
+
for (const g of groups) modulesById.set(g.id, g);
|
|
81
|
+
|
|
82
|
+
const staleModules = [];
|
|
83
|
+
for (const docPath of staleInfo.stale_docs) {
|
|
84
|
+
const parsedId = path.posix.basename(docPath).replace(/\.md$/, '');
|
|
85
|
+
const group = modulesById.get(parsedId);
|
|
86
|
+
if (!group) continue;
|
|
87
|
+
const facts = buildModuleFacts(group, projectRoot);
|
|
88
|
+
staleModules.push({
|
|
89
|
+
id: group.id,
|
|
90
|
+
directory: group.directory,
|
|
91
|
+
doc_path: docPath,
|
|
92
|
+
facts,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const hashLookup = _hashesLookupFromManifest(next);
|
|
97
|
+
const removedModules = [];
|
|
98
|
+
const currentIds = new Set(groups.map((g) => g.id));
|
|
99
|
+
for (const docRel of Object.keys(docIndex)) {
|
|
100
|
+
const id = path.posix.basename(docRel).replace(/\.md$/, '');
|
|
101
|
+
if (!currentIds.has(id)) removedModules.push({ id, doc_path: docRel });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const addedModules = [];
|
|
105
|
+
for (const g of groups) {
|
|
106
|
+
const relDoc = path.posix.join('modules', g.id + '.md');
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(docIndex, relDoc)) {
|
|
108
|
+
const facts = buildModuleFacts(g, projectRoot);
|
|
109
|
+
addedModules.push({
|
|
110
|
+
id: g.id,
|
|
111
|
+
directory: g.directory,
|
|
112
|
+
doc_path: relDoc,
|
|
113
|
+
facts,
|
|
114
|
+
});
|
|
115
|
+
const absDoc = moduleDocPath(projectRoot, g.id);
|
|
116
|
+
if (!fs.existsSync(absDoc)) {
|
|
117
|
+
fs.mkdirSync(path.dirname(absDoc), { recursive: true });
|
|
118
|
+
atomicWriteFileSync(absDoc, renderModuleDoc(facts, null, hashLookup));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newDocIndex = buildDocIndexMap(groups);
|
|
124
|
+
const indexMapPath = path.join(projectRoot, '.nubos-pilot', 'codebase', '.doc-index.json');
|
|
125
|
+
fs.mkdirSync(path.dirname(indexMapPath), { recursive: true });
|
|
126
|
+
atomicWriteFileSync(indexMapPath, JSON.stringify(newDocIndex, null, 2) + '\n');
|
|
127
|
+
|
|
128
|
+
const indexPath = indexDocPath(projectRoot);
|
|
129
|
+
atomicWriteFileSync(indexPath, buildIndexDoc(groups, {}));
|
|
130
|
+
|
|
131
|
+
writeManifest(projectRoot, next);
|
|
132
|
+
|
|
133
|
+
stdout.write(JSON.stringify({
|
|
134
|
+
mode: 'plan',
|
|
135
|
+
diff_summary: diff.summary,
|
|
136
|
+
touched_paths: staleInfo.touched_paths,
|
|
137
|
+
stale_modules: staleModules,
|
|
138
|
+
added_modules: addedModules,
|
|
139
|
+
removed_modules: removedModules,
|
|
140
|
+
}, null, 2));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _applyProse(projectRoot, flags, stdout) {
|
|
144
|
+
if (!flags.moduleId) {
|
|
145
|
+
throw new NubosPilotError('update-docs-missing-module', '--apply-prose requires --module <id>', {});
|
|
146
|
+
}
|
|
147
|
+
if (!flags.proseFile) {
|
|
148
|
+
throw new NubosPilotError('update-docs-missing-prose', '--apply-prose requires --prose-file <path>', {});
|
|
149
|
+
}
|
|
150
|
+
let prose;
|
|
151
|
+
try {
|
|
152
|
+
prose = JSON.parse(fs.readFileSync(flags.proseFile, 'utf-8'));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
throw new NubosPilotError(
|
|
155
|
+
'update-docs-prose-unreadable',
|
|
156
|
+
'prose file not readable or not valid JSON: ' + flags.proseFile,
|
|
157
|
+
{ path: flags.proseFile, cause: err && err.message },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const scanResult = scan({
|
|
162
|
+
cwd: projectRoot,
|
|
163
|
+
batchSize: flags.batchSize,
|
|
164
|
+
maxFiles: flags.maxFiles > 0 ? flags.maxFiles : undefined,
|
|
165
|
+
gitInfo: workspaceGitInfo,
|
|
166
|
+
});
|
|
167
|
+
const groups = groupFilesIntoModules(scanResult.files);
|
|
168
|
+
const target = groups.find((g) => g.id === flags.moduleId);
|
|
169
|
+
if (!target) {
|
|
170
|
+
throw new NubosPilotError(
|
|
171
|
+
'update-docs-module-not-found',
|
|
172
|
+
`module not found: ${flags.moduleId}`,
|
|
173
|
+
{ moduleId: flags.moduleId },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const facts = buildModuleFacts(target, projectRoot);
|
|
178
|
+
const manifest = manifestFromScanFiles(scanResult.files);
|
|
179
|
+
const hashLookup = _hashesLookupFromManifest(manifest);
|
|
180
|
+
|
|
181
|
+
const docPath = moduleDocPath(projectRoot, target.id);
|
|
182
|
+
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
183
|
+
atomicWriteFileSync(docPath, renderModuleDoc(facts, prose, hashLookup));
|
|
184
|
+
|
|
185
|
+
writeManifest(projectRoot, manifest);
|
|
186
|
+
|
|
187
|
+
stdout.write(JSON.stringify({
|
|
188
|
+
mode: 'apply-prose',
|
|
189
|
+
module_id: target.id,
|
|
190
|
+
doc_path: path.relative(projectRoot, docPath),
|
|
191
|
+
}, null, 2));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function run(args, ctx) {
|
|
195
|
+
const context = ctx || {};
|
|
196
|
+
const stdout = context.stdout || process.stdout;
|
|
197
|
+
const flags = _parseArgs(args);
|
|
198
|
+
const projectRoot = path.resolve(flags.cwd || context.cwd || process.cwd());
|
|
199
|
+
|
|
200
|
+
const stateDir = path.join(projectRoot, '.nubos-pilot');
|
|
201
|
+
if (!fs.existsSync(stateDir)) {
|
|
202
|
+
throw new NubosPilotError(
|
|
203
|
+
'update-docs-not-initialized',
|
|
204
|
+
'.nubos-pilot/ not found — run np:new-project first',
|
|
205
|
+
{ cwd: projectRoot },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (flags.applyProse) {
|
|
210
|
+
_applyProse(projectRoot, flags, stdout);
|
|
211
|
+
} else {
|
|
212
|
+
_emitPlan(projectRoot, flags, stdout);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { run, _parseArgs };
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
|
|
7
|
+
const scanCmd = require('./scan-codebase.cjs');
|
|
8
|
+
const subcmd = require('./update-docs.cjs');
|
|
9
|
+
|
|
10
|
+
const _sandboxes = [];
|
|
11
|
+
|
|
12
|
+
function makeSandbox() {
|
|
13
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-ud-'));
|
|
14
|
+
fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
|
|
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
|
+
function captureStdout() {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
return {
|
|
28
|
+
stub: { write: (s) => chunks.push(String(s)) },
|
|
29
|
+
json: () => JSON.parse(chunks.join('')),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
while (_sandboxes.length) {
|
|
35
|
+
const dir = _sandboxes.pop();
|
|
36
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('UD-1: throws when .nubos-pilot missing', () => {
|
|
41
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-ud-bare-'));
|
|
42
|
+
_sandboxes.push(dir);
|
|
43
|
+
assert.throws(
|
|
44
|
+
() => subcmd.run([], { cwd: dir, stdout: captureStdout().stub }),
|
|
45
|
+
(err) => err.code === 'update-docs-not-initialized',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('UD-2: detects added, changed, removed files against manifest', () => {
|
|
50
|
+
const root = makeSandbox();
|
|
51
|
+
write(root, 'src/auth/login.js', 'export function login(){}');
|
|
52
|
+
write(root, 'src/billing/invoice.js', 'export function invoice(){}');
|
|
53
|
+
scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
|
|
54
|
+
|
|
55
|
+
write(root, 'src/auth/login.js', 'export function login(){ /* v2 */ }');
|
|
56
|
+
write(root, 'src/auth/session.js', 'export class Session {}');
|
|
57
|
+
fs.unlinkSync(path.join(root, 'src/billing/invoice.js'));
|
|
58
|
+
|
|
59
|
+
const cap = captureStdout();
|
|
60
|
+
subcmd.run([], { cwd: root, stdout: cap.stub });
|
|
61
|
+
const out = cap.json();
|
|
62
|
+
|
|
63
|
+
assert.equal(out.mode, 'plan');
|
|
64
|
+
assert.ok(out.diff_summary.changed >= 1);
|
|
65
|
+
assert.ok(out.diff_summary.added >= 1);
|
|
66
|
+
assert.ok(out.diff_summary.removed >= 1);
|
|
67
|
+
|
|
68
|
+
const staleIds = out.stale_modules.map((m) => m.id);
|
|
69
|
+
assert.ok(staleIds.includes('src-auth'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('UD-3: new module appears in added_modules and gets stub', () => {
|
|
73
|
+
const root = makeSandbox();
|
|
74
|
+
write(root, 'src/core/a.js', 'export function a(){}');
|
|
75
|
+
scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
|
|
76
|
+
|
|
77
|
+
write(root, 'src/newmod/x.js', 'export function x(){}');
|
|
78
|
+
|
|
79
|
+
const cap = captureStdout();
|
|
80
|
+
subcmd.run([], { cwd: root, stdout: cap.stub });
|
|
81
|
+
const out = cap.json();
|
|
82
|
+
|
|
83
|
+
const addedIds = out.added_modules.map((m) => m.id);
|
|
84
|
+
assert.ok(addedIds.includes('src-newmod'));
|
|
85
|
+
assert.ok(fs.existsSync(path.join(root, '.nubos-pilot', 'codebase', 'modules', 'src-newmod.md')));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('UD-4: removed module reported in removed_modules', () => {
|
|
89
|
+
const root = makeSandbox();
|
|
90
|
+
write(root, 'src/foo/a.js', 'export function a(){}');
|
|
91
|
+
write(root, 'src/bar/b.js', 'export function b(){}');
|
|
92
|
+
scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
|
|
93
|
+
|
|
94
|
+
fs.rmSync(path.join(root, 'src/bar'), { recursive: true, force: true });
|
|
95
|
+
|
|
96
|
+
const cap = captureStdout();
|
|
97
|
+
subcmd.run([], { cwd: root, stdout: cap.stub });
|
|
98
|
+
const out = cap.json();
|
|
99
|
+
const removedIds = out.removed_modules.map((m) => m.id);
|
|
100
|
+
assert.ok(removedIds.includes('src-bar'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('UD-5: apply-prose writes prose into existing module doc', () => {
|
|
104
|
+
const root = makeSandbox();
|
|
105
|
+
write(root, 'src/auth/login.js', 'export function login(){}');
|
|
106
|
+
scanCmd.run([], { cwd: root, stdout: captureStdout().stub });
|
|
107
|
+
|
|
108
|
+
const proseFile = path.join(root, 'p.json');
|
|
109
|
+
fs.writeFileSync(proseFile, JSON.stringify({
|
|
110
|
+
description: 'Login',
|
|
111
|
+
purpose: 'Auth users.',
|
|
112
|
+
key_concepts: [],
|
|
113
|
+
public_api: '`login()`',
|
|
114
|
+
invariants: [],
|
|
115
|
+
gotchas: [],
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
const cap = captureStdout();
|
|
119
|
+
subcmd.run(['--apply-prose', '--module', 'src-auth', '--prose-file', proseFile], {
|
|
120
|
+
cwd: root, stdout: cap.stub,
|
|
121
|
+
});
|
|
122
|
+
const out = cap.json();
|
|
123
|
+
assert.equal(out.mode, 'apply-prose');
|
|
124
|
+
const doc = fs.readFileSync(
|
|
125
|
+
path.join(root, '.nubos-pilot', 'codebase', 'modules', 'src-auth.md'),
|
|
126
|
+
'utf-8',
|
|
127
|
+
);
|
|
128
|
+
assert.ok(doc.includes('description: Login'));
|
|
129
|
+
assert.ok(doc.includes('Auth users.'));
|
|
130
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# ADR-0007: Codebase Documentation Layer as Shared Agent Memory
|
|
2
|
+
|
|
3
|
+
* Status: Accepted
|
|
4
|
+
* Date: 2026-04-20
|
|
5
|
+
* Supersedes: None
|
|
6
|
+
* Relates-to: [ADR-0001](0001-no-daemon-invariant.md), [ADR-0005](0005-three-orthogonal-file-trees.md)
|
|
7
|
+
|
|
8
|
+
## Context and Problem Statement
|
|
9
|
+
|
|
10
|
+
Every dev-agent that nubos-pilot orchestrates (executor, code-fixer,
|
|
11
|
+
planner, researcher, code-reviewer, plus user-authored custom agents) reads
|
|
12
|
+
project source before it writes source. In practice this means each agent
|
|
13
|
+
re-derives context from raw files on every spawn: it opens the same
|
|
14
|
+
modules, re-discovers the same public APIs, and re-learns the same
|
|
15
|
+
invariants. Three failure modes follow:
|
|
16
|
+
|
|
17
|
+
* **Token spend** — the same module content is repeatedly paid for across
|
|
18
|
+
agent runs. Over a multi-phase project this dominates cost.
|
|
19
|
+
* **Drift** — two agents reading the same file at different times may
|
|
20
|
+
reach different conclusions (one spots an invariant the other misses),
|
|
21
|
+
and downstream decisions diverge.
|
|
22
|
+
* **Loss of hard-won context** — when a bug was fixed two phases ago with
|
|
23
|
+
a subtle timing workaround, the next agent has no way to know the
|
|
24
|
+
workaround exists. It will re-introduce the bug.
|
|
25
|
+
|
|
26
|
+
Nubos-pilot already commits to `.nubos-pilot/` as the Project-State tree
|
|
27
|
+
([ADR-0005](0005-three-orthogonal-file-trees.md)). What is missing is a
|
|
28
|
+
canonical, incrementally-maintained description of the project's own
|
|
29
|
+
source tree that agents treat as ground truth. The planning artifacts
|
|
30
|
+
(PROJECT.md, REQUIREMENTS.md, phase CONTEXT.md files) describe *what* the
|
|
31
|
+
project is and *what* each phase should do — they deliberately do not
|
|
32
|
+
describe *how the code is shaped*.
|
|
33
|
+
|
|
34
|
+
## Decision Drivers
|
|
35
|
+
|
|
36
|
+
* **Runtime agnostic** — the docs layer must function identically whether
|
|
37
|
+
the host is Claude Code, OpenAI Agents, Codex, or any other orchestrator.
|
|
38
|
+
No Claude-specific hooks, no Claude-specific invocation paths.
|
|
39
|
+
* **Language agnostic** — nubos-pilot ships into arbitrary third-party
|
|
40
|
+
projects; the layer must work for Node, Python, Go, Rust, PHP, Ruby,
|
|
41
|
+
Java, Kotlin, C#, Swift, and unknown-language files alike.
|
|
42
|
+
* **Cheap to keep fresh** — stale docs are worse than absent docs. The
|
|
43
|
+
update path must be incremental and must piggyback on the existing
|
|
44
|
+
`np:execute-*` workflow cadence.
|
|
45
|
+
* **Physically separated from source** — docs live under `.nubos-pilot/`
|
|
46
|
+
so they cannot pollute the user's code tree, cannot be mistaken for
|
|
47
|
+
source, and can be fully removed with a single directory deletion.
|
|
48
|
+
* **Inspectable and editable by humans** — plain Markdown with a skill-
|
|
49
|
+
style frontmatter header, not an opaque binary index.
|
|
50
|
+
* **Pluggable where speculative** — a deterministic parser handles
|
|
51
|
+
structure; an agent produces prose. Either component can be swapped
|
|
52
|
+
without invalidating the other.
|
|
53
|
+
|
|
54
|
+
## Considered Options
|
|
55
|
+
|
|
56
|
+
* **Option A — No codebase documentation layer.** Status quo. Every agent
|
|
57
|
+
re-reads raw source. Reject: demonstrably wasteful and drift-prone at
|
|
58
|
+
the sizes nubos-pilot targets.
|
|
59
|
+
* **Option B — Single large CODEBASE.md summary.** One file per project.
|
|
60
|
+
Reject: dev-agents cannot selectively load what they need; file grows
|
|
61
|
+
unboundedly; a single write destroys a single read target.
|
|
62
|
+
* **Option C — Per-file docs mirroring the source tree.** One `.md` per
|
|
63
|
+
source file. Reject: explodes in large repos; does not express module
|
|
64
|
+
boundaries, which is where invariants live.
|
|
65
|
+
* **Option D — Module-level docs for coherent units, manifest-tracked,
|
|
66
|
+
skill-style frontmatter, incremental refresh.** Chosen.
|
|
67
|
+
|
|
68
|
+
## Decision Outcome
|
|
69
|
+
|
|
70
|
+
Chosen: **Option D — Codebase Documentation Layer**. Module-granularity
|
|
71
|
+
docs under `.nubos-pilot/codebase/modules/<id>.md`, indexed by
|
|
72
|
+
`.nubos-pilot/codebase/INDEX.md`, tracked for staleness by
|
|
73
|
+
`.nubos-pilot/codebase/.hashes.json`, and mapped to source paths by
|
|
74
|
+
`.nubos-pilot/codebase/.doc-index.json`. The layer is created and
|
|
75
|
+
maintained by three workflows (`np:scan-codebase`, `np:update-docs`,
|
|
76
|
+
`np:discuss-project`) and is consumed by every dev-agent under a strict
|
|
77
|
+
read-first / write-back protocol.
|
|
78
|
+
|
|
79
|
+
### Layout
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
.nubos-pilot/
|
|
83
|
+
codebase/
|
|
84
|
+
INDEX.md # pointer list, generated
|
|
85
|
+
.hashes.json # per-source-file SHA-256 manifest
|
|
86
|
+
.doc-index.json # doc → source-paths mapping
|
|
87
|
+
modules/
|
|
88
|
+
<module-id>.md # one per coherent unit
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A "module" is a **coherent unit**, not a fixed shape. The initial grouping
|
|
92
|
+
is directory-based (all code files in one directory form one module), but
|
|
93
|
+
the contract is explicit: grouping may be overridden and refined in
|
|
94
|
+
future iterations to express bounded contexts, microservice boundaries,
|
|
95
|
+
or feature-level units without breaking the read-first protocol.
|
|
96
|
+
|
|
97
|
+
### Skill-Style Frontmatter
|
|
98
|
+
|
|
99
|
+
Every module doc carries structured frontmatter that agents (and tooling)
|
|
100
|
+
can read without parsing the body:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
---
|
|
104
|
+
name: <human-readable name>
|
|
105
|
+
description: <one-sentence summary>
|
|
106
|
+
kind: module
|
|
107
|
+
module_id: <id>
|
|
108
|
+
directory: <repo-relative>
|
|
109
|
+
primary_language: <lang>
|
|
110
|
+
file_count: <n>
|
|
111
|
+
source_paths: [ ... ]
|
|
112
|
+
symbols: [ ... ] # exported API surface
|
|
113
|
+
external_deps: [ ... ]
|
|
114
|
+
internal_deps: [ ... ]
|
|
115
|
+
source_hashes:
|
|
116
|
+
<path>: <sha256> # per-file integrity anchor
|
|
117
|
+
last_documented: <date>
|
|
118
|
+
---
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The body is human-readable Markdown with fixed sections: Purpose, Key
|
|
122
|
+
Concepts, Public API, Invariants, Gotchas, Files.
|
|
123
|
+
|
|
124
|
+
### Hybrid Parser + Agent Generation
|
|
125
|
+
|
|
126
|
+
* **Deterministic parser** (`lib/codebase-docs.cjs`) extracts symbols and
|
|
127
|
+
imports from 11 languages via line-based regex patterns (JavaScript,
|
|
128
|
+
TypeScript, Python, Go, Rust, PHP, Ruby, Java, Kotlin, C#, Swift; others
|
|
129
|
+
documented as "unknown" but still scanned).
|
|
130
|
+
* **Agent** (`np-codebase-documenter`) receives the parser's facts and
|
|
131
|
+
produces strict-JSON prose sections. The agent prompt forbids inventing
|
|
132
|
+
symbols or behaviors; it grounds every claim in the facts or the source
|
|
133
|
+
it is allowed to read.
|
|
134
|
+
* **Render** combines both into the final `.md`. The agent never writes
|
|
135
|
+
files directly; the subcommand renders.
|
|
136
|
+
|
|
137
|
+
### Staleness Detection
|
|
138
|
+
|
|
139
|
+
`.hashes.json` is the integrity anchor. On every `np:update-docs` run:
|
|
140
|
+
|
|
141
|
+
1. Rescan the workspace.
|
|
142
|
+
2. Diff the new hashes against `.hashes.json` → added / changed / removed
|
|
143
|
+
files.
|
|
144
|
+
3. Map touched paths to modules via `.doc-index.json` → stale modules.
|
|
145
|
+
4. Refresh only stale modules' prose via the documenter agent.
|
|
146
|
+
5. Write back. Overwrite the manifest as the new baseline.
|
|
147
|
+
|
|
148
|
+
`np:doctor` surfaces three related issues — `codebase-not-scanned`,
|
|
149
|
+
`codebase-manifest-stale`, `codebase-tbd-docs` — with `fixable:
|
|
150
|
+
'run-workflow'` so `--fix` prints a hint and does not prompt (honors
|
|
151
|
+
D-16 whitelist semantics from [ADR-0001](0001-no-daemon-invariant.md)
|
|
152
|
+
adjacent conventions).
|
|
153
|
+
|
|
154
|
+
### Dev-Agent Protocol (runtime-agnostic)
|
|
155
|
+
|
|
156
|
+
**Pre-edit (read-first) — mandatory for every dev-agent:**
|
|
157
|
+
|
|
158
|
+
1. Read `.nubos-pilot/codebase/INDEX.md`.
|
|
159
|
+
2. For every source file the agent will touch, locate and read the
|
|
160
|
+
owning `.nubos-pilot/codebase/modules/<id>.md`.
|
|
161
|
+
3. Respect Invariants and Gotchas as constraints; if a planned change
|
|
162
|
+
would violate an invariant, stop and report.
|
|
163
|
+
|
|
164
|
+
**Post-edit (write-back) — mandatory for every dev-agent that mutates
|
|
165
|
+
source:**
|
|
166
|
+
|
|
167
|
+
1. Run `np:update-docs`.
|
|
168
|
+
2. For each stale module in the diff, dispatch the `np-codebase-
|
|
169
|
+
documenter` agent with the provided facts and apply prose via
|
|
170
|
+
`np:update-docs --apply-prose`.
|
|
171
|
+
|
|
172
|
+
The protocol is deliberately not a runtime hook. Installing a
|
|
173
|
+
`PostToolUse` hook into `.claude/settings.json` would tie correctness to
|
|
174
|
+
a specific host. Keeping the protocol in agent prompts means the same
|
|
175
|
+
contract applies in Claude Code, OpenAI Agents, Codex, or any future
|
|
176
|
+
orchestrator that loads nubos-pilot's agent definitions.
|
|
177
|
+
|
|
178
|
+
### Orthogonality Preservation
|
|
179
|
+
|
|
180
|
+
`.nubos-pilot/codebase/` is a strict sub-tree of the Project-State tree
|
|
181
|
+
([ADR-0005](0005-three-orthogonal-file-trees.md)). It is owned by the
|
|
182
|
+
end user's project, mutated only through nubos-pilot workflows, never
|
|
183
|
+
touches Source or Install-Payload trees. The three-tree invariant holds.
|
|
184
|
+
|
|
185
|
+
### `child_process` Boundary Preservation
|
|
186
|
+
|
|
187
|
+
`lib/workspace-scan.cjs` exposes the scan surface. Surface-audit (ADR-0001
|
|
188
|
+
adjacent) forbids `child_process` in `lib/*.cjs` outside the
|
|
189
|
+
`git.cjs` whitelist. The scanner therefore accepts an optional
|
|
190
|
+
`opts.gitInfo` callback; the git-info implementation lives in
|
|
191
|
+
`lib/git.cjs` and is passed in by the subcommand layer
|
|
192
|
+
(`bin/np-tools/*.cjs`). The `no-daemon` / `lib-is-pure` invariant holds.
|
|
193
|
+
|
|
194
|
+
## Consequences
|
|
195
|
+
|
|
196
|
+
* Good, because every dev-agent starts with a curated summary of the
|
|
197
|
+
code it will touch, lowering token spend and reducing drift.
|
|
198
|
+
* Good, because Invariants and Gotchas persist across phases — a
|
|
199
|
+
workaround documented in module X's Gotchas section is seen by every
|
|
200
|
+
future agent that touches module X.
|
|
201
|
+
* Good, because incremental refresh (`np:update-docs`) costs only the
|
|
202
|
+
modules whose source hashes changed, so the steady-state price of
|
|
203
|
+
keeping docs fresh scales with change volume, not repo size.
|
|
204
|
+
* Good, because the layer is inspectable, editable, removable — a plain
|
|
205
|
+
directory of Markdown files, no database, no daemon.
|
|
206
|
+
* Good, because the split between deterministic parser and agent-
|
|
207
|
+
produced prose preserves ADR-0001's no-daemon invariant: the parser
|
|
208
|
+
is a library function, and the agent runs only inside a workflow the
|
|
209
|
+
user already invoked.
|
|
210
|
+
* Good, because language coverage is extensible — adding a new language
|
|
211
|
+
means adding a regex entry to `SYMBOL_PATTERNS` and `IMPORT_PATTERNS`
|
|
212
|
+
in `lib/codebase-docs.cjs`; no schema change, no manifest migration.
|
|
213
|
+
* Bad, because initial scans of large repos are expensive. Mitigation:
|
|
214
|
+
`np:scan-codebase` batches (user can pause between batches) and the
|
|
215
|
+
workflow shows a progress counter.
|
|
216
|
+
* Bad, because parser-extracted symbols are regex-best-effort, not AST-
|
|
217
|
+
precise. Mitigation: the documenter agent is instructed to omit
|
|
218
|
+
signatures it cannot confirm from source rather than guess, and the
|
|
219
|
+
Gotchas section allows surfacing parser gaps explicitly.
|
|
220
|
+
* Bad, because the protocol is contract-enforced in agent prompts, not
|
|
221
|
+
in the runtime. A custom agent that ignores the protocol can still
|
|
222
|
+
write source without refreshing docs. Mitigation: `np:doctor` reports
|
|
223
|
+
`codebase-manifest-stale` any time post-change refresh was skipped;
|
|
224
|
+
the user sees the drift.
|
|
225
|
+
* Neutral, because ADR-0002 (zero runtime deps) is not challenged —
|
|
226
|
+
the layer uses only Node built-ins plus the already-accepted
|
|
227
|
+
`yaml@^2.8` via `lib/codebase-manifest.cjs` (JSON only — not even
|
|
228
|
+
yaml in practice).
|
|
229
|
+
|
|
230
|
+
## Pattern Conformance
|
|
231
|
+
|
|
232
|
+
* **S-1 atomic write + file lock** — every doc write in the codebase
|
|
233
|
+
layer goes through `atomicWriteFileSync`. No partial files.
|
|
234
|
+
* **S-2 NubosPilotError envelope** — all error paths in
|
|
235
|
+
`scan-codebase` / `update-docs` / `discuss-project` subcommands
|
|
236
|
+
throw typed errors (`scan-codebase-not-initialized`,
|
|
237
|
+
`update-docs-module-not-found`, `discuss-project-missing-field`,
|
|
238
|
+
`proposed-reqs-invalid-id`, etc.).
|
|
239
|
+
* **S-5 sandboxed tests** — every new test (65 across lib + bin)
|
|
240
|
+
creates a fresh tmp directory and tears it down in `afterEach`.
|
|
241
|
+
* **S-6 CJS module footer** — every new `.cjs` file ends with a
|
|
242
|
+
`module.exports = {...}` block.
|
|
243
|
+
|
|
244
|
+
## More Information
|
|
245
|
+
|
|
246
|
+
* **Implementation:**
|
|
247
|
+
* `lib/workspace-scan.cjs` — sprachagnostischer Scanner (15 tests)
|
|
248
|
+
* `lib/codebase-manifest.cjs` — `.hashes.json` read/write/diff (10 tests)
|
|
249
|
+
* `lib/codebase-docs.cjs` — module grouping + symbol/import extraction + render (14 tests)
|
|
250
|
+
* `bin/np-tools/scan-codebase.cjs` — initial scan subcommand (6 tests)
|
|
251
|
+
* `bin/np-tools/update-docs.cjs` — incremental refresh subcommand (5 tests)
|
|
252
|
+
* `bin/np-tools/discuss-project.cjs` — project-level interview subcommand (13 tests)
|
|
253
|
+
* `agents/np-codebase-documenter.md` — runtime-agnostic documenter agent
|
|
254
|
+
* `workflows/scan-codebase.md`, `workflows/update-docs.md`,
|
|
255
|
+
`workflows/discuss-project.md`, `workflows/new-project.md`
|
|
256
|
+
* **Consumer updates:** `np-executor`, `np-code-fixer`, `np-planner`,
|
|
257
|
+
`np-researcher`, `np-code-reviewer` received the read-first / write-
|
|
258
|
+
back protocol in their agent frontmatter-adjacent prose.
|
|
259
|
+
* **Related ADRs:**
|
|
260
|
+
* [ADR-0001](0001-no-daemon-invariant.md) — the runtime-agnostic
|
|
261
|
+
protocol exists to avoid a daemon.
|
|
262
|
+
* [ADR-0002](0002-zero-runtime-dependencies.md) — layer adds no new
|
|
263
|
+
runtime deps.
|
|
264
|
+
* [ADR-0005](0005-three-orthogonal-file-trees.md) — `.nubos-pilot/
|
|
265
|
+
codebase/` is strictly inside the Project-State tree.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
*This ADR describes the source-level design of the Codebase Documentation
|
|
270
|
+
Layer. CI-gate enforcement of the read-first / write-back protocol
|
|
271
|
+
(static analysis of agent prompts) and release/publish of the new
|
|
272
|
+
workflows are deferred to later deploy-phase ADRs per the source-vs-
|
|
273
|
+
deploy separation in [ADR-0005](0005-three-orthogonal-file-trees.md).*
|
package/docs/adr/README.md
CHANGED
|
@@ -9,6 +9,8 @@ This directory contains MADR-full ADRs that codify scope invariants of nubos-pil
|
|
|
9
9
|
- [`0003-max-six-unit-types.md`](0003-max-six-unit-types.md) — Milestone, Phase, Plan, Task, Todo, Backlog — no more (FND-03)
|
|
10
10
|
- [`0004-atomic-commit-per-unit.md`](0004-atomic-commit-per-unit.md) — Every unit-completion = exactly one git commit (FND-04)
|
|
11
11
|
- [`0005-three-orthogonal-file-trees.md`](0005-three-orthogonal-file-trees.md) — Source / Install-Payload / Project-State stay disjoint (FND-05)
|
|
12
|
+
- [`0006-yaml-dependency-amendment.md`](0006-yaml-dependency-amendment.md) — Accept `yaml@^2.8` as first runtime dep (amends ADR-0002)
|
|
13
|
+
- [`0007-codebase-docs-layer.md`](0007-codebase-docs-layer.md) — Skill-style codebase documentation under `.nubos-pilot/codebase/` as shared agent memory
|
|
12
14
|
|
|
13
15
|
## Status Lifecycle
|
|
14
16
|
|