nubos-pilot 0.8.0 → 0.8.2
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/README.md +14 -14
- package/agents/np-architect.md +132 -0
- package/agents/np-build-fixer.md +98 -0
- package/agents/np-planner.md +1 -1
- package/agents/np-security-reviewer.md +128 -0
- package/bin/np-tools/_commands.cjs +7 -2
- package/bin/np-tools/context-stats.cjs +117 -0
- package/bin/np-tools/knowledge-index.cjs +23 -0
- package/bin/np-tools/knowledge-search.cjs +36 -0
- package/bin/np-tools/knowledge-stats.cjs +19 -0
- package/bin/np-tools/new-project.cjs +9 -0
- package/bin/np-tools/pause-work.cjs +31 -0
- package/bin/np-tools/resume-work.cjs +12 -0
- package/bin/np-tools/session-snapshot-read.cjs +14 -0
- package/bin/np-tools/session-snapshot-write.cjs +44 -0
- package/lib/agents.test.cjs +3 -0
- package/lib/knowledge.cjs +256 -0
- package/lib/knowledge.test.cjs +83 -0
- package/lib/session-snapshot.cjs +93 -0
- package/lib/session-snapshot.test.cjs +73 -0
- package/mcp-configs/README.md +41 -0
- package/mcp-configs/claude-code.example.json +27 -0
- package/mcp-configs/codex.example.toml +17 -0
- package/mcp-configs/nubos-knowledge.notes.md +42 -0
- package/np-tools.cjs +6 -0
- package/package.json +4 -1
- package/templates/RULES.md +95 -0
- package/workflows/add-todo.md +2 -5
- package/workflows/architect-phase.md +107 -0
- package/workflows/context-stats.md +60 -0
- package/workflows/knowledge.md +76 -0
- package/workflows/note.md +2 -2
- package/workflows/research-phase.md +3 -2
- package/workflows/session-report.md +0 -2
- package/workflows/stats.md +2 -1
- package/workflows/thread.md +0 -1
|
@@ -177,6 +177,14 @@ function _apply(answersPath, cwd, stdout) {
|
|
|
177
177
|
_render(_loadTemplate('REQUIREMENTS'), reqVars, 'REQUIREMENTS'),
|
|
178
178
|
);
|
|
179
179
|
|
|
180
|
+
atomicWriteFileSync(
|
|
181
|
+
path.join(stateDir, 'RULES.md'),
|
|
182
|
+
_render(_loadTemplate('RULES'), {
|
|
183
|
+
project_name: answers.project_name,
|
|
184
|
+
created_date: createdDate,
|
|
185
|
+
}, 'RULES'),
|
|
186
|
+
);
|
|
187
|
+
|
|
180
188
|
layout.createMilestoneDir(firstMilestoneNumber, root);
|
|
181
189
|
const msTemplatesDir = path.join(TEMPLATES_DIR, 'milestone');
|
|
182
190
|
const msCtx = _render(
|
|
@@ -261,6 +269,7 @@ function _apply(answersPath, cwd, stdout) {
|
|
|
261
269
|
created: [
|
|
262
270
|
'.nubos-pilot/PROJECT.md',
|
|
263
271
|
'.nubos-pilot/REQUIREMENTS.md',
|
|
272
|
+
'.nubos-pilot/RULES.md',
|
|
264
273
|
'.nubos-pilot/roadmap.yaml',
|
|
265
274
|
'.nubos-pilot/STATE.md',
|
|
266
275
|
path.relative(root, layout.milestoneContextPath(firstMilestoneNumber, root)),
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
+
const { execFileSync } = require('node:child_process');
|
|
2
|
+
|
|
1
3
|
const { mutateState } = require('../../lib/state.cjs');
|
|
4
|
+
const { captureSnapshot, writeSnapshot } = require('../../lib/session-snapshot.cjs');
|
|
5
|
+
|
|
6
|
+
function _gitCommits(cwd, limit) {
|
|
7
|
+
try {
|
|
8
|
+
const out = execFileSync('git', [
|
|
9
|
+
'-C', cwd,
|
|
10
|
+
'log',
|
|
11
|
+
'--no-color',
|
|
12
|
+
'--max-count=' + Number(limit),
|
|
13
|
+
'--pretty=format:%H%x09%s%x09%cI',
|
|
14
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
15
|
+
return out.split('\n').filter(Boolean).map((line) => {
|
|
16
|
+
const [sha, subject, iso] = line.split('\t');
|
|
17
|
+
return { sha, subject, committed_at: iso };
|
|
18
|
+
});
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
2
23
|
|
|
3
24
|
function run(_args, ctx) {
|
|
4
25
|
const context = ctx || {};
|
|
@@ -12,10 +33,20 @@ function run(_args, ctx) {
|
|
|
12
33
|
: null;
|
|
13
34
|
return s;
|
|
14
35
|
}, cwd);
|
|
36
|
+
let snapshotPath = null;
|
|
37
|
+
let snapshotErr = null;
|
|
38
|
+
try {
|
|
39
|
+
const snap = captureSnapshot(cwd, { lastCommits: _gitCommits(cwd, 10) });
|
|
40
|
+
snapshotPath = writeSnapshot(snap, cwd);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
snapshotErr = String((err && err.message) || err);
|
|
43
|
+
}
|
|
15
44
|
const payload = {
|
|
16
45
|
ok: true,
|
|
17
46
|
stopped_at: next.frontmatter.session.stopped_at,
|
|
18
47
|
resume_file: next.frontmatter.session.resume_file,
|
|
48
|
+
snapshot_path: snapshotPath,
|
|
49
|
+
snapshot_error: snapshotErr,
|
|
19
50
|
};
|
|
20
51
|
stdout.write(JSON.stringify(payload));
|
|
21
52
|
return payload;
|
|
@@ -6,6 +6,7 @@ const { readCheckpoint, listCheckpoints } = require('../../lib/checkpoint.cjs');
|
|
|
6
6
|
const { TASK_ID_RE } = require('../../lib/tasks.cjs');
|
|
7
7
|
const textMode = require('../../lib/text-mode.cjs');
|
|
8
8
|
const layout = require('../../lib/layout.cjs');
|
|
9
|
+
const { readSnapshot } = require('../../lib/session-snapshot.cjs');
|
|
9
10
|
const {
|
|
10
11
|
hasSliceWorktree,
|
|
11
12
|
sliceWorktreePath,
|
|
@@ -116,6 +117,17 @@ function run(_args, ctx) {
|
|
|
116
117
|
}));
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
const snap = readSnapshot(cwd);
|
|
121
|
+
if (snap) {
|
|
122
|
+
payload.session_snapshot = {
|
|
123
|
+
captured_at: snap.captured_at,
|
|
124
|
+
milestone: snap.milestone,
|
|
125
|
+
current_task: snap.current_task,
|
|
126
|
+
last_commits: (snap.last_commits || []).slice(0, 5),
|
|
127
|
+
open_handoffs: snap.open_handoffs || [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
stdout.write(JSON.stringify(payload));
|
|
120
132
|
return payload;
|
|
121
133
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { readSnapshot } = require('../../lib/session-snapshot.cjs');
|
|
4
|
+
|
|
5
|
+
function run(args, ctx) {
|
|
6
|
+
const context = ctx || {};
|
|
7
|
+
const cwd = context.cwd || process.cwd();
|
|
8
|
+
const stdout = context.stdout || process.stdout;
|
|
9
|
+
const snap = readSnapshot(cwd);
|
|
10
|
+
stdout.write(JSON.stringify(snap || { ok: false, reason: 'no-snapshot' }));
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { run };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const { captureSnapshot, writeSnapshot } = require('../../lib/session-snapshot.cjs');
|
|
6
|
+
|
|
7
|
+
function _gitCommits(cwd, limit) {
|
|
8
|
+
try {
|
|
9
|
+
const out = execFileSync('git', [
|
|
10
|
+
'-C', cwd,
|
|
11
|
+
'log',
|
|
12
|
+
'--no-color',
|
|
13
|
+
'--max-count=' + Number(limit),
|
|
14
|
+
'--pretty=format:%H%x09%s%x09%cI',
|
|
15
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
16
|
+
return out.split('\n').filter(Boolean).map((line) => {
|
|
17
|
+
const [sha, subject, iso] = line.split('\t');
|
|
18
|
+
return { sha, subject, committed_at: iso };
|
|
19
|
+
});
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function run(args, ctx) {
|
|
26
|
+
const context = ctx || {};
|
|
27
|
+
const cwd = context.cwd || process.cwd();
|
|
28
|
+
const stdout = context.stdout || process.stdout;
|
|
29
|
+
const snap = captureSnapshot(cwd, { lastCommits: _gitCommits(cwd, 10) });
|
|
30
|
+
const dest = writeSnapshot(snap, cwd);
|
|
31
|
+
stdout.write(JSON.stringify({
|
|
32
|
+
ok: true,
|
|
33
|
+
snapshot_path: dest,
|
|
34
|
+
captured_at: snap.captured_at,
|
|
35
|
+
milestone: snap.milestone,
|
|
36
|
+
current_task: snap.current_task,
|
|
37
|
+
last_commit_count: snap.last_commits.length,
|
|
38
|
+
open_handoff_count: snap.open_handoffs.length,
|
|
39
|
+
checkpoint_count: snap.checkpoint_ids.length,
|
|
40
|
+
}));
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { run };
|
package/lib/agents.test.cjs
CHANGED
|
@@ -224,6 +224,9 @@ const NP_AGENTS = [
|
|
|
224
224
|
{ file: 'np-verifier', expected_tier: 'sonnet' },
|
|
225
225
|
{ file: 'np-researcher', expected_tier: 'sonnet' },
|
|
226
226
|
{ file: 'np-codebase-documenter', expected_tier: 'sonnet' },
|
|
227
|
+
{ file: 'np-architect', expected_tier: 'sonnet' },
|
|
228
|
+
{ file: 'np-build-fixer', expected_tier: 'sonnet' },
|
|
229
|
+
{ file: 'np-security-reviewer', expected_tier: 'sonnet' },
|
|
227
230
|
{ file: 'np-nyquist-auditor', expected_tier: 'haiku' },
|
|
228
231
|
{ file: 'np-sc-extractor', expected_tier: 'haiku' },
|
|
229
232
|
];
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
|
|
7
|
+
const { projectStateDir, atomicWriteFileSync, NubosPilotError, withFileLock } = require('./core.cjs');
|
|
8
|
+
|
|
9
|
+
const INDEX_VERSION = 1;
|
|
10
|
+
const CHUNK_LINES = 40;
|
|
11
|
+
const CHUNK_OVERLAP = 8;
|
|
12
|
+
const MAX_RESULTS_DEFAULT = 10;
|
|
13
|
+
const SNIPPET_LINES = 6;
|
|
14
|
+
|
|
15
|
+
const STOPWORDS = new Set([
|
|
16
|
+
'the','a','an','of','to','in','on','for','and','or','is','are','was','were',
|
|
17
|
+
'be','as','by','at','it','this','that','these','those','with','from','into',
|
|
18
|
+
'der','die','das','und','oder','von','zu','im','in','auf','für','nicht',
|
|
19
|
+
'ist','sind','war','waren','sein','als','bei','an','mit','aus','nach',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const INDEXED_GLOBS = [
|
|
23
|
+
'PROJECT.md',
|
|
24
|
+
'REQUIREMENTS.md',
|
|
25
|
+
'RULES.md',
|
|
26
|
+
'STATE.md',
|
|
27
|
+
'codebase/*.md',
|
|
28
|
+
'milestones/M*/*.md',
|
|
29
|
+
'milestones/M*/slices/S*/*.md',
|
|
30
|
+
'milestones/M*/slices/S*/tasks/T*/*.md',
|
|
31
|
+
'todos/**/*.md',
|
|
32
|
+
'threads/**/*.md',
|
|
33
|
+
'notes/**/*.md',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function _stateDir(cwd) {
|
|
37
|
+
return projectStateDir(cwd);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _walkMarkdown(stateDir) {
|
|
41
|
+
const out = [];
|
|
42
|
+
if (!fs.existsSync(stateDir)) return out;
|
|
43
|
+
|
|
44
|
+
function walk(dir, depth) {
|
|
45
|
+
if (depth > 8) return;
|
|
46
|
+
let entries;
|
|
47
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
const full = path.join(dir, e.name);
|
|
50
|
+
const rel = path.relative(stateDir, full);
|
|
51
|
+
if (rel.startsWith('archive' + path.sep)) continue;
|
|
52
|
+
if (rel.startsWith('checkpoints' + path.sep)) continue;
|
|
53
|
+
if (rel.startsWith('worktrees' + path.sep)) continue;
|
|
54
|
+
if (rel.startsWith('reports' + path.sep)) continue;
|
|
55
|
+
if (rel.startsWith('.tmp' + path.sep)) continue;
|
|
56
|
+
if (rel.startsWith('state' + path.sep)) continue;
|
|
57
|
+
if (e.isDirectory()) { walk(full, depth + 1); continue; }
|
|
58
|
+
if (!e.isFile() || !e.name.endsWith('.md')) continue;
|
|
59
|
+
out.push(full);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
walk(stateDir, 0);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _tokenize(text) {
|
|
67
|
+
const lower = String(text).toLowerCase();
|
|
68
|
+
const tokens = lower.match(/[a-z0-9][a-z0-9\-_]{1,}/g) || [];
|
|
69
|
+
return tokens.filter((t) => t.length >= 2 && !STOPWORDS.has(t));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _hash(content) {
|
|
73
|
+
return crypto.createHash('sha1').update(content).digest('hex').slice(0, 16);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _splitChunks(filePath, content) {
|
|
77
|
+
const lines = content.split(/\r?\n/);
|
|
78
|
+
const chunks = [];
|
|
79
|
+
let id = 0;
|
|
80
|
+
for (let start = 0; start < lines.length; start += (CHUNK_LINES - CHUNK_OVERLAP)) {
|
|
81
|
+
const end = Math.min(lines.length, start + CHUNK_LINES);
|
|
82
|
+
const slice = lines.slice(start, end);
|
|
83
|
+
const text = slice.join('\n');
|
|
84
|
+
if (text.trim().length === 0) continue;
|
|
85
|
+
chunks.push({
|
|
86
|
+
chunk_id: id++,
|
|
87
|
+
line_start: start + 1,
|
|
88
|
+
line_end: end,
|
|
89
|
+
text,
|
|
90
|
+
});
|
|
91
|
+
if (end >= lines.length) break;
|
|
92
|
+
}
|
|
93
|
+
return chunks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function _docFromFile(stateDir, filePath) {
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
98
|
+
const stat = fs.statSync(filePath);
|
|
99
|
+
const rel = path.relative(stateDir, filePath);
|
|
100
|
+
const chunks = _splitChunks(filePath, content);
|
|
101
|
+
const indexedChunks = chunks.map((c) => {
|
|
102
|
+
const tokens = _tokenize(c.text);
|
|
103
|
+
const tf = Object.create(null);
|
|
104
|
+
for (const t of tokens) tf[t] = (tf[t] || 0) + 1;
|
|
105
|
+
return {
|
|
106
|
+
chunk_id: c.chunk_id,
|
|
107
|
+
line_start: c.line_start,
|
|
108
|
+
line_end: c.line_end,
|
|
109
|
+
length: tokens.length,
|
|
110
|
+
tf,
|
|
111
|
+
preview: c.text.split('\n').slice(0, SNIPPET_LINES).join('\n'),
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
rel_path: rel,
|
|
116
|
+
mtime_ms: stat.mtimeMs,
|
|
117
|
+
size: stat.size,
|
|
118
|
+
sha: _hash(content),
|
|
119
|
+
chunks: indexedChunks,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildIndex(cwd) {
|
|
124
|
+
const stateDir = _stateDir(cwd);
|
|
125
|
+
const files = _walkMarkdown(stateDir);
|
|
126
|
+
const docs = files.map((f) => _docFromFile(stateDir, f));
|
|
127
|
+
const totalChunks = docs.reduce((n, d) => n + d.chunks.length, 0);
|
|
128
|
+
const avgLen = totalChunks
|
|
129
|
+
? docs.reduce((n, d) => n + d.chunks.reduce((m, c) => m + c.length, 0), 0) / totalChunks
|
|
130
|
+
: 0;
|
|
131
|
+
const df = Object.create(null);
|
|
132
|
+
for (const d of docs) {
|
|
133
|
+
for (const c of d.chunks) {
|
|
134
|
+
for (const term of Object.keys(c.tf)) df[term] = (df[term] || 0) + 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
version: INDEX_VERSION,
|
|
139
|
+
built_at: new Date().toISOString(),
|
|
140
|
+
state_dir: stateDir,
|
|
141
|
+
total_files: docs.length,
|
|
142
|
+
total_chunks: totalChunks,
|
|
143
|
+
avg_chunk_length: Number(avgLen.toFixed(2)),
|
|
144
|
+
df,
|
|
145
|
+
docs,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _indexPath(cwd) {
|
|
150
|
+
return path.join(_stateDir(cwd), 'state', 'knowledge-index.json');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function writeIndex(index, cwd) {
|
|
154
|
+
const dest = _indexPath(cwd);
|
|
155
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
156
|
+
return withFileLock(dest, () => {
|
|
157
|
+
atomicWriteFileSync(dest, JSON.stringify(index));
|
|
158
|
+
return dest;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readIndex(cwd) {
|
|
163
|
+
const dest = _indexPath(cwd);
|
|
164
|
+
if (!fs.existsSync(dest)) return null;
|
|
165
|
+
try {
|
|
166
|
+
const obj = JSON.parse(fs.readFileSync(dest, 'utf-8'));
|
|
167
|
+
if (!obj || obj.version !== INDEX_VERSION) return null;
|
|
168
|
+
return obj;
|
|
169
|
+
} catch { return null; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _bm25Score(terms, chunk, df, totalChunks, avgLen, k1 = 1.4, b = 0.75) {
|
|
173
|
+
let score = 0;
|
|
174
|
+
for (const term of terms) {
|
|
175
|
+
const f = chunk.tf[term] || 0;
|
|
176
|
+
if (f === 0) continue;
|
|
177
|
+
const n = df[term] || 0;
|
|
178
|
+
const idf = Math.log(1 + (totalChunks - n + 0.5) / (n + 0.5));
|
|
179
|
+
const denom = f + k1 * (1 - b + b * (chunk.length / Math.max(1, avgLen)));
|
|
180
|
+
score += idf * (f * (k1 + 1)) / (denom || 1);
|
|
181
|
+
}
|
|
182
|
+
return score;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function search(query, cwd, opts) {
|
|
186
|
+
const o = opts || {};
|
|
187
|
+
const limit = Math.max(1, Math.min(100, o.limit || MAX_RESULTS_DEFAULT));
|
|
188
|
+
let index = readIndex(cwd);
|
|
189
|
+
if (!index) {
|
|
190
|
+
index = buildIndex(cwd);
|
|
191
|
+
writeIndex(index, cwd);
|
|
192
|
+
}
|
|
193
|
+
const terms = _tokenize(query);
|
|
194
|
+
if (terms.length === 0) {
|
|
195
|
+
return { query, terms, total_hits: 0, hits: [], index_built_at: index.built_at };
|
|
196
|
+
}
|
|
197
|
+
const hits = [];
|
|
198
|
+
for (const doc of index.docs) {
|
|
199
|
+
for (const c of doc.chunks) {
|
|
200
|
+
const score = _bm25Score(terms, c, index.df, index.total_chunks, index.avg_chunk_length);
|
|
201
|
+
if (score > 0) {
|
|
202
|
+
hits.push({
|
|
203
|
+
rel_path: doc.rel_path,
|
|
204
|
+
line_start: c.line_start,
|
|
205
|
+
line_end: c.line_end,
|
|
206
|
+
score: Number(score.toFixed(4)),
|
|
207
|
+
preview: c.preview,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
hits.sort((a, b) => b.score - a.score);
|
|
213
|
+
return {
|
|
214
|
+
query,
|
|
215
|
+
terms,
|
|
216
|
+
total_hits: hits.length,
|
|
217
|
+
hits: hits.slice(0, limit),
|
|
218
|
+
index_built_at: index.built_at,
|
|
219
|
+
index_total_files: index.total_files,
|
|
220
|
+
index_total_chunks: index.total_chunks,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function indexStats(cwd) {
|
|
225
|
+
const idx = readIndex(cwd);
|
|
226
|
+
if (!idx) return { exists: false };
|
|
227
|
+
const groups = Object.create(null);
|
|
228
|
+
for (const d of idx.docs) {
|
|
229
|
+
const top = d.rel_path.split(path.sep)[0] || '<root>';
|
|
230
|
+
if (!groups[top]) groups[top] = { files: 0, chunks: 0, bytes: 0 };
|
|
231
|
+
groups[top].files += 1;
|
|
232
|
+
groups[top].chunks += d.chunks.length;
|
|
233
|
+
groups[top].bytes += d.size;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
exists: true,
|
|
237
|
+
built_at: idx.built_at,
|
|
238
|
+
total_files: idx.total_files,
|
|
239
|
+
total_chunks: idx.total_chunks,
|
|
240
|
+
avg_chunk_length: idx.avg_chunk_length,
|
|
241
|
+
unique_terms: Object.keys(idx.df).length,
|
|
242
|
+
groups,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
INDEX_VERSION,
|
|
248
|
+
INDEXED_GLOBS,
|
|
249
|
+
buildIndex,
|
|
250
|
+
writeIndex,
|
|
251
|
+
readIndex,
|
|
252
|
+
search,
|
|
253
|
+
indexStats,
|
|
254
|
+
_tokenize,
|
|
255
|
+
_splitChunks,
|
|
256
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
|
|
9
|
+
const { buildIndex, writeIndex, readIndex, search, indexStats, _tokenize } = require('./knowledge.cjs');
|
|
10
|
+
|
|
11
|
+
function _scratch() {
|
|
12
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-knowledge-'));
|
|
13
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'tasks', 'T0001'), { recursive: true });
|
|
14
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot', 'codebase'), { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'PROJECT.md'),
|
|
16
|
+
'# Project\n\nThe authentication system uses JWT tokens for session security.\n');
|
|
17
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'milestones', 'M001', 'M001-CONTEXT.md'),
|
|
18
|
+
'# M001 Context\n\nLocked: jose@6 for JWT verification, no hand-rolled crypto allowed.\n');
|
|
19
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'milestones', 'M001', 'slices', 'S001', 'S001-PLAN.md'),
|
|
20
|
+
'# S001 Plan\n\nTask 1: install jose. Task 2: verify token signature.\n');
|
|
21
|
+
fs.writeFileSync(path.join(root, '.nubos-pilot', 'codebase', 'auth.md'),
|
|
22
|
+
'# Auth Module\n\nPath: app/Auth/\nExternal Deps: jose, argon2.\n');
|
|
23
|
+
return root;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('tokenize strips stopwords + short tokens', () => {
|
|
27
|
+
const t = _tokenize('The JWT token is verified using jose');
|
|
28
|
+
assert.ok(t.includes('jwt'));
|
|
29
|
+
assert.ok(t.includes('token'));
|
|
30
|
+
assert.ok(t.includes('jose'));
|
|
31
|
+
assert.ok(!t.includes('the'));
|
|
32
|
+
assert.ok(!t.includes('is'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('buildIndex covers PROJECT + milestones + codebase', () => {
|
|
36
|
+
const cwd = _scratch();
|
|
37
|
+
const idx = buildIndex(cwd);
|
|
38
|
+
assert.equal(idx.version, 1);
|
|
39
|
+
assert.equal(idx.total_files, 4);
|
|
40
|
+
assert.ok(idx.total_chunks >= 4);
|
|
41
|
+
assert.ok(idx.df['jose'] >= 2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('writeIndex + readIndex round-trip', () => {
|
|
45
|
+
const cwd = _scratch();
|
|
46
|
+
const idx = buildIndex(cwd);
|
|
47
|
+
const dest = writeIndex(idx, cwd);
|
|
48
|
+
assert.ok(fs.existsSync(dest));
|
|
49
|
+
const back = readIndex(cwd);
|
|
50
|
+
assert.equal(back.version, idx.version);
|
|
51
|
+
assert.equal(back.total_files, idx.total_files);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('search ranks JWT-related chunks high', () => {
|
|
55
|
+
const cwd = _scratch();
|
|
56
|
+
const res = search('jwt jose', cwd);
|
|
57
|
+
assert.ok(res.total_hits >= 2);
|
|
58
|
+
assert.ok(res.hits[0].score > 0);
|
|
59
|
+
assert.match(res.hits[0].rel_path, /M001|PROJECT/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('search auto-builds index if missing', () => {
|
|
63
|
+
const cwd = _scratch();
|
|
64
|
+
const res = search('authentication', cwd);
|
|
65
|
+
assert.ok(res.total_hits >= 1);
|
|
66
|
+
assert.ok(readIndex(cwd) !== null);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('search empty query returns 0 hits without crash', () => {
|
|
70
|
+
const cwd = _scratch();
|
|
71
|
+
const res = search('the', cwd);
|
|
72
|
+
assert.equal(res.total_hits, 0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('indexStats groups by top-level dir', () => {
|
|
76
|
+
const cwd = _scratch();
|
|
77
|
+
buildIndex(cwd);
|
|
78
|
+
writeIndex(buildIndex(cwd), cwd);
|
|
79
|
+
const s = indexStats(cwd);
|
|
80
|
+
assert.equal(s.exists, true);
|
|
81
|
+
assert.ok(s.groups.milestones);
|
|
82
|
+
assert.ok(s.groups.codebase);
|
|
83
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { projectStateDir, atomicWriteFileSync, withFileLock } = require('./core.cjs');
|
|
7
|
+
const { readState } = require('./state.cjs');
|
|
8
|
+
|
|
9
|
+
const SNAPSHOT_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
function _snapshotPath(cwd) {
|
|
12
|
+
return path.join(projectStateDir(cwd), 'state', 'session-snapshot.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function _safeReadState(cwd) {
|
|
16
|
+
try { return readState(cwd); } catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _listOpenHandoffs(cwd) {
|
|
20
|
+
const dir = path.join(projectStateDir(cwd), 'handoffs');
|
|
21
|
+
if (!fs.existsSync(dir)) return [];
|
|
22
|
+
const out = [];
|
|
23
|
+
function walk(d, depth) {
|
|
24
|
+
if (depth > 4) return;
|
|
25
|
+
let entries;
|
|
26
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
const full = path.join(d, e.name);
|
|
29
|
+
if (e.isDirectory()) { walk(full, depth + 1); continue; }
|
|
30
|
+
if (!e.isFile() || !e.name.endsWith('.md')) continue;
|
|
31
|
+
let txt = '';
|
|
32
|
+
try { txt = fs.readFileSync(full, 'utf-8'); } catch { continue; }
|
|
33
|
+
const m = txt.match(/^---[\s\S]*?status:\s*(open|read)[\s\S]*?---/);
|
|
34
|
+
if (!m) continue;
|
|
35
|
+
out.push({
|
|
36
|
+
id: path.basename(e.name, '.md'),
|
|
37
|
+
rel_path: path.relative(projectStateDir(cwd), full),
|
|
38
|
+
status: m[1],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(dir, 0);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _listCheckpoints(cwd) {
|
|
47
|
+
const dir = path.join(projectStateDir(cwd), 'checkpoints');
|
|
48
|
+
if (!fs.existsSync(dir)) return [];
|
|
49
|
+
let entries;
|
|
50
|
+
try { entries = fs.readdirSync(dir); } catch { return []; }
|
|
51
|
+
return entries.filter((f) => f.endsWith('.json')).map((f) => path.basename(f, '.json'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function captureSnapshot(cwd, opts) {
|
|
55
|
+
const root = cwd || process.cwd();
|
|
56
|
+
const state = _safeReadState(root);
|
|
57
|
+
const fm = state && state.frontmatter ? state.frontmatter : {};
|
|
58
|
+
const lastCommits = (opts && Array.isArray(opts.lastCommits)) ? opts.lastCommits : [];
|
|
59
|
+
return {
|
|
60
|
+
version: SNAPSHOT_VERSION,
|
|
61
|
+
captured_at: new Date().toISOString(),
|
|
62
|
+
milestone: fm.milestone || null,
|
|
63
|
+
milestone_name: fm.milestone_name || null,
|
|
64
|
+
current_task: fm.current_task || null,
|
|
65
|
+
progress: fm.progress || null,
|
|
66
|
+
last_commits: lastCommits,
|
|
67
|
+
open_handoffs: _listOpenHandoffs(root),
|
|
68
|
+
checkpoint_ids: _listCheckpoints(root),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeSnapshot(snapshot, cwd) {
|
|
73
|
+
const root = cwd || process.cwd();
|
|
74
|
+
const dest = _snapshotPath(root);
|
|
75
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
76
|
+
return withFileLock(dest, () => {
|
|
77
|
+
atomicWriteFileSync(dest, JSON.stringify(snapshot, null, 2));
|
|
78
|
+
return dest;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readSnapshot(cwd) {
|
|
83
|
+
const root = cwd || process.cwd();
|
|
84
|
+
const dest = _snapshotPath(root);
|
|
85
|
+
if (!fs.existsSync(dest)) return null;
|
|
86
|
+
try {
|
|
87
|
+
const obj = JSON.parse(fs.readFileSync(dest, 'utf-8'));
|
|
88
|
+
if (!obj || obj.version !== SNAPSHOT_VERSION) return null;
|
|
89
|
+
return obj;
|
|
90
|
+
} catch { return null; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { SNAPSHOT_VERSION, captureSnapshot, writeSnapshot, readSnapshot };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const { captureSnapshot, writeSnapshot, readSnapshot } = require('./session-snapshot.cjs');
|
|
11
|
+
const { writeState } = require('./state.cjs');
|
|
12
|
+
|
|
13
|
+
function _scratch() {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-snapshot-'));
|
|
15
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
16
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot', 'handoffs'), { recursive: true });
|
|
17
|
+
execFileSync('git', ['-C', root, 'init', '-q', '-b', 'main'], { stdio: 'ignore' });
|
|
18
|
+
execFileSync('git', ['-C', root, 'config', 'user.email', 'snap@test'], { stdio: 'ignore' });
|
|
19
|
+
execFileSync('git', ['-C', root, 'config', 'user.name', 'Snap'], { stdio: 'ignore' });
|
|
20
|
+
fs.writeFileSync(path.join(root, 'README.md'), 'x\n');
|
|
21
|
+
execFileSync('git', ['-C', root, 'add', '-A'], { stdio: 'ignore' });
|
|
22
|
+
execFileSync('git', ['-C', root, 'commit', '-q', '-m', 'task(M001-S001-T0001): initial'], { stdio: 'ignore' });
|
|
23
|
+
writeState({ frontmatter: {
|
|
24
|
+
schema_version: 2,
|
|
25
|
+
milestone: 'M001',
|
|
26
|
+
milestone_name: 'Test Milestone',
|
|
27
|
+
current_task: 'M001-S001-T0001',
|
|
28
|
+
last_updated: new Date().toISOString(),
|
|
29
|
+
progress: { total_milestones: 1, completed_milestones: 0, total_tasks: 1, completed_tasks: 0, percent: 0 },
|
|
30
|
+
session: { stopped_at: null, resume_file: null, last_activity: null },
|
|
31
|
+
}, body: '\n# State\n' }, root);
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(root, '.nubos-pilot', 'handoffs', 'h1.md'),
|
|
34
|
+
'---\nfrom: a\nto: b\nstatus: open\n---\nbody\n',
|
|
35
|
+
);
|
|
36
|
+
return root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('captureSnapshot pulls state + caller-supplied commits + handoffs', () => {
|
|
40
|
+
const root = _scratch();
|
|
41
|
+
const snap = captureSnapshot(root, {
|
|
42
|
+
lastCommits: [{ sha: 'abc1234', subject: 'task: t1', committed_at: '2026-04-28T00:00:00Z' }],
|
|
43
|
+
});
|
|
44
|
+
assert.equal(snap.version, 1);
|
|
45
|
+
assert.equal(snap.milestone, 'M001');
|
|
46
|
+
assert.equal(snap.current_task, 'M001-S001-T0001');
|
|
47
|
+
assert.equal(snap.last_commits.length, 1);
|
|
48
|
+
assert.equal(snap.last_commits[0].sha, 'abc1234');
|
|
49
|
+
assert.equal(snap.open_handoffs.length, 1);
|
|
50
|
+
assert.equal(snap.open_handoffs[0].status, 'open');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('captureSnapshot defaults last_commits to [] when not supplied', () => {
|
|
54
|
+
const root = _scratch();
|
|
55
|
+
const snap = captureSnapshot(root);
|
|
56
|
+
assert.deepEqual(snap.last_commits, []);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('writeSnapshot + readSnapshot round-trip', () => {
|
|
60
|
+
const root = _scratch();
|
|
61
|
+
const snap = captureSnapshot(root, { lastCommits: [] });
|
|
62
|
+
const dest = writeSnapshot(snap, root);
|
|
63
|
+
assert.ok(fs.existsSync(dest));
|
|
64
|
+
const back = readSnapshot(root);
|
|
65
|
+
assert.equal(back.milestone, snap.milestone);
|
|
66
|
+
assert.equal(back.current_task, snap.current_task);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('readSnapshot returns null when missing', () => {
|
|
70
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-snapshot-empty-'));
|
|
71
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
72
|
+
assert.equal(readSnapshot(root), null);
|
|
73
|
+
});
|