nubos-pilot 1.3.2 → 1.3.3
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/CHANGELOG.md +5 -2
- package/agents/np-critic-economy.md +103 -0
- package/agents/np-critic.md +11 -10
- package/agents/np-executor.md +14 -0
- package/agents/np-simplifier.md +83 -0
- package/bin/install.js +30 -1
- package/bin/np-tools/_commands.cjs +2 -0
- package/bin/np-tools/doctor.cjs +1 -0
- package/bin/np-tools/economy-mode.cjs +47 -0
- package/bin/np-tools/loop-run-round.cjs +1 -1
- package/bin/np-tools/resolve-model.cjs +1 -0
- package/bin/np-tools/simplify-debt.cjs +91 -0
- package/bin/np-tools/simplify-debt.test.cjs +99 -0
- package/lib/agents-registry.cjs +2 -1
- package/lib/agents.test.cjs +2 -0
- package/lib/config-defaults.cjs +14 -1
- package/lib/config-defaults.test.cjs +9 -0
- package/lib/config-schema.cjs +4 -0
- package/lib/economy-debt.cjs +235 -0
- package/lib/economy-debt.test.cjs +131 -0
- package/lib/economy-mode.cjs +66 -0
- package/lib/economy-mode.test.cjs +85 -0
- package/lib/nubosloop.cjs +4 -0
- package/lib/nubosloop.test.cjs +1 -0
- package/np-tools.cjs +2 -0
- package/package.json +1 -1
- package/workflows/execute-phase.md +16 -0
- package/workflows/simplify-debt.md +93 -0
- package/workflows/simplify-review.md +103 -0
package/lib/agents.test.cjs
CHANGED
|
@@ -248,6 +248,7 @@ const NP_AGENTS = [
|
|
|
248
248
|
{ file: 'np-sc-extractor', expected_tier: 'haiku' },
|
|
249
249
|
{ file: 'np-critic', expected_tier: 'sonnet' },
|
|
250
250
|
{ file: 'np-learnings-extractor', expected_tier: 'haiku' },
|
|
251
|
+
{ file: 'np-simplifier', expected_tier: 'sonnet' },
|
|
251
252
|
];
|
|
252
253
|
|
|
253
254
|
// Audit-surface modules — files in agents/ that carry agent-shaped frontmatter
|
|
@@ -257,6 +258,7 @@ const NP_AGENT_MODULES = [
|
|
|
257
258
|
{ file: 'np-critic-style', parent: 'np-critic' },
|
|
258
259
|
{ file: 'np-critic-tests', parent: 'np-critic' },
|
|
259
260
|
{ file: 'np-critic-acceptance', parent: 'np-critic' },
|
|
261
|
+
{ file: 'np-critic-economy', parent: 'np-critic' },
|
|
260
262
|
];
|
|
261
263
|
|
|
262
264
|
for (let i = 0; i < NP_AGENTS.length; i += 1) {
|
package/lib/config-defaults.cjs
CHANGED
|
@@ -18,6 +18,10 @@ const DEFAULT_AGENTS = Object.freeze({
|
|
|
18
18
|
research: true,
|
|
19
19
|
plan_checker: true,
|
|
20
20
|
verifier: true,
|
|
21
|
+
// Economy axis level (off|lite|full|ultra). Default `lite` = prevention-first:
|
|
22
|
+
// the climb-the-ladder discipline is on, the in-loop critic is opt-in (full/ultra).
|
|
23
|
+
// Resolved via lib/economy-mode.cjs; legacy `economy_critic` bool still honoured.
|
|
24
|
+
economy: 'lite',
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
const DEFAULT_LOOP = Object.freeze({
|
|
@@ -35,6 +39,7 @@ const DEFAULT_SWARM_CRITIC = Object.freeze({
|
|
|
35
39
|
style_tier: 'haiku',
|
|
36
40
|
tests_tier: 'sonnet',
|
|
37
41
|
acceptance_tier: 'sonnet',
|
|
42
|
+
economy_tier: 'haiku',
|
|
38
43
|
});
|
|
39
44
|
|
|
40
45
|
const DEFAULT_SWARM = Object.freeze({
|
|
@@ -118,6 +123,13 @@ const DEFAULT_MODEL_PROFILE = 'frontier';
|
|
|
118
123
|
const DEFAULT_SCOPE = 'local';
|
|
119
124
|
const DEFAULT_RESPONSE_LANGUAGE = 'en';
|
|
120
125
|
|
|
126
|
+
// Install/update ships the most aggressive Economy level by default. This is the
|
|
127
|
+
// value written into a fresh config.json (and backfilled into keyless configs on
|
|
128
|
+
// update — see bin/install.js). It deliberately differs from the *resolved*
|
|
129
|
+
// fallback in DEFAULT_AGENTS.economy (`lite`): a config with the key absent
|
|
130
|
+
// entirely stays conservative, but anything nubos-pilot writes opts into ultra.
|
|
131
|
+
const INSTALL_ECONOMY_MODE = 'ultra';
|
|
132
|
+
|
|
121
133
|
const DEFAULT_CONFIG_TREE = Object.freeze({
|
|
122
134
|
scope: DEFAULT_SCOPE,
|
|
123
135
|
model_profile: DEFAULT_MODEL_PROFILE,
|
|
@@ -147,7 +159,7 @@ function buildInstallConfig(answers) {
|
|
|
147
159
|
model_profile: a.model_profile || DEFAULT_MODEL_PROFILE,
|
|
148
160
|
response_language: a.response_language || DEFAULT_RESPONSE_LANGUAGE,
|
|
149
161
|
workflow: workflowOverride,
|
|
150
|
-
agents: { ...DEFAULT_AGENTS },
|
|
162
|
+
agents: { ...DEFAULT_AGENTS, economy: INSTALL_ECONOMY_MODE },
|
|
151
163
|
loop: { ...DEFAULT_LOOP },
|
|
152
164
|
swarm: {
|
|
153
165
|
research: { ...DEFAULT_SWARM_RESEARCH },
|
|
@@ -191,6 +203,7 @@ module.exports = {
|
|
|
191
203
|
DEFAULT_MODEL_PROFILE,
|
|
192
204
|
DEFAULT_SCOPE,
|
|
193
205
|
DEFAULT_RESPONSE_LANGUAGE,
|
|
206
|
+
INSTALL_ECONOMY_MODE,
|
|
194
207
|
DEFAULT_CONFIG_TREE,
|
|
195
208
|
buildInstallConfig,
|
|
196
209
|
};
|
|
@@ -6,8 +6,17 @@ const {
|
|
|
6
6
|
DEFAULT_WORKFLOW,
|
|
7
7
|
DEFAULT_MODEL_PROFILE,
|
|
8
8
|
DEFAULT_SCOPE,
|
|
9
|
+
DEFAULT_CONFIG_TREE,
|
|
10
|
+
INSTALL_ECONOMY_MODE,
|
|
9
11
|
} = require('./config-defaults.cjs');
|
|
10
12
|
|
|
13
|
+
test('CFD-economy: install writes economy=ultra, but the resolved fallback stays lite', () => {
|
|
14
|
+
assert.equal(INSTALL_ECONOMY_MODE, 'ultra');
|
|
15
|
+
assert.equal(buildInstallConfig({ runtime: 'claude' }).agents.economy, 'ultra');
|
|
16
|
+
// The keyless resolved fallback is intentionally conservative.
|
|
17
|
+
assert.equal(DEFAULT_CONFIG_TREE.agents.economy, 'lite');
|
|
18
|
+
});
|
|
19
|
+
|
|
11
20
|
test('CFD-1: buildInstallConfig defaults preserve commit_artifacts:true (back-compat)', () => {
|
|
12
21
|
const cfg = buildInstallConfig({ runtime: 'claude' });
|
|
13
22
|
assert.equal(cfg.workflow.commit_artifacts, true);
|
package/lib/config-schema.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const VALID_SCOPES = Object.freeze(['local', 'global']);
|
|
|
5
5
|
const VALID_MODEL_PROFILES = Object.freeze(['frontier', 'quality', 'balanced', 'budget', 'inherit']);
|
|
6
6
|
const VALID_KNOWLEDGE_ADAPTERS = Object.freeze(['local']);
|
|
7
7
|
const VALID_TIERS = Object.freeze(['haiku', 'sonnet', 'opus']);
|
|
8
|
+
const { VALID_ECONOMY_MODES } = require('./economy-mode.cjs');
|
|
8
9
|
|
|
9
10
|
const SCHEMA = Object.freeze({
|
|
10
11
|
scope: { type: 'enum', values: VALID_SCOPES, optional: true },
|
|
@@ -32,6 +33,8 @@ const SCHEMA = Object.freeze({
|
|
|
32
33
|
research: { type: 'boolean', optional: true },
|
|
33
34
|
plan_checker: { type: 'boolean', optional: true },
|
|
34
35
|
verifier: { type: 'boolean', optional: true },
|
|
36
|
+
economy: { type: 'enum', values: VALID_ECONOMY_MODES, optional: true },
|
|
37
|
+
economy_critic: { type: 'boolean', optional: true },
|
|
35
38
|
},
|
|
36
39
|
},
|
|
37
40
|
loop: {
|
|
@@ -54,6 +57,7 @@ const SCHEMA = Object.freeze({
|
|
|
54
57
|
style_tier: { type: 'enum', values: VALID_TIERS, optional: true },
|
|
55
58
|
tests_tier: { type: 'enum', values: VALID_TIERS, optional: true },
|
|
56
59
|
acceptance_tier: { type: 'enum', values: VALID_TIERS, optional: true },
|
|
60
|
+
economy_tier: { type: 'enum', values: VALID_TIERS, optional: true },
|
|
57
61
|
},
|
|
58
62
|
},
|
|
59
63
|
knowledge_adapter: { type: 'enum', values: VALID_KNOWLEDGE_ADAPTERS, optional: true },
|
|
@@ -0,0 +1,235 @@
|
|
|
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, NubosPilotError } = require('./core.cjs');
|
|
8
|
+
const { slugify } = require('./layout.cjs');
|
|
9
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
10
|
+
|
|
11
|
+
// The economy-debt ledger records simplifications a reviewer chose to DEFER
|
|
12
|
+
// rather than fix now — the manual twin of the in-loop Economy critic
|
|
13
|
+
// (agents.economy ∈ {full,ultra}). It exists so "later" does not become "never":
|
|
14
|
+
// /np:simplify-review surfaces over-build, and what is not fixed this round is
|
|
15
|
+
// harvested here. Categories mirror the canonical four economy routes in
|
|
16
|
+
// lib/nubosloop.cjs::ROUTE_TABLE and agents/np-critic-economy.md — keep in sync.
|
|
17
|
+
const ECONOMY_CATEGORIES = Object.freeze([
|
|
18
|
+
'over-engineering',
|
|
19
|
+
'stdlib-reinvention',
|
|
20
|
+
'native-duplication',
|
|
21
|
+
'shrinkable',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const STATUSES = Object.freeze(['open', 'resolved']);
|
|
25
|
+
const MAX_NOTE_LENGTH = 500;
|
|
26
|
+
const ID_LENGTH = 7;
|
|
27
|
+
|
|
28
|
+
function debtRoot(cwd) {
|
|
29
|
+
return path.join(projectStateDir(cwd || process.cwd()), 'economy-debt');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statusDir(status, cwd) {
|
|
33
|
+
return path.join(debtRoot(cwd), status);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _entryId(file, line, category, note) {
|
|
37
|
+
const key = String(file) + ':' + String(line) + ':' + String(category) + ':' + String(note);
|
|
38
|
+
return crypto.createHash('sha1').update(key, 'utf-8').digest('hex').slice(0, ID_LENGTH);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _composeMd(entry) {
|
|
42
|
+
const fm = [
|
|
43
|
+
'---',
|
|
44
|
+
'id: ' + entry.id,
|
|
45
|
+
'category: ' + entry.category,
|
|
46
|
+
'file: ' + entry.file,
|
|
47
|
+
'line: ' + entry.line,
|
|
48
|
+
'created: ' + entry.created,
|
|
49
|
+
'status: ' + entry.status,
|
|
50
|
+
];
|
|
51
|
+
if (entry.resolved) fm.push('resolved: ' + entry.resolved);
|
|
52
|
+
fm.push('---');
|
|
53
|
+
return fm.join('\n') + '\n' + entry.note + '\n';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _parseEntry(filePath) {
|
|
57
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
58
|
+
const { frontmatter, body } = extractFrontmatter(raw);
|
|
59
|
+
const lineRaw = frontmatter.line;
|
|
60
|
+
const lineNum = Number.parseInt(lineRaw, 10);
|
|
61
|
+
return {
|
|
62
|
+
id: frontmatter.id != null ? String(frontmatter.id) : '',
|
|
63
|
+
category: frontmatter.category != null ? String(frontmatter.category) : '',
|
|
64
|
+
file: frontmatter.file != null ? String(frontmatter.file) : '',
|
|
65
|
+
line: Number.isFinite(lineNum) ? lineNum : 0,
|
|
66
|
+
created: frontmatter.created != null ? String(frontmatter.created) : '',
|
|
67
|
+
resolved: frontmatter.resolved != null ? String(frontmatter.resolved) : '',
|
|
68
|
+
status: frontmatter.status != null ? String(frontmatter.status) : '',
|
|
69
|
+
note: String(body || '').trim(),
|
|
70
|
+
path: filePath,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _validateInput(input) {
|
|
75
|
+
const file = input && input.file != null ? String(input.file).trim() : '';
|
|
76
|
+
const note = input && input.note != null ? String(input.note).trim() : '';
|
|
77
|
+
const category = input && input.category != null ? String(input.category).trim() : '';
|
|
78
|
+
if (!note) {
|
|
79
|
+
throw new NubosPilotError(
|
|
80
|
+
'economy-debt-missing-note',
|
|
81
|
+
'economy-debt entry requires a non-empty note',
|
|
82
|
+
{},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (note.length > MAX_NOTE_LENGTH) {
|
|
86
|
+
throw new NubosPilotError(
|
|
87
|
+
'economy-debt-note-too-long',
|
|
88
|
+
'economy-debt note must be <= ' + MAX_NOTE_LENGTH + ' chars',
|
|
89
|
+
{ length: note.length },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!ECONOMY_CATEGORIES.includes(category)) {
|
|
93
|
+
throw new NubosPilotError(
|
|
94
|
+
'economy-debt-invalid-category',
|
|
95
|
+
'economy-debt category must be one of: ' + ECONOMY_CATEGORIES.join(', '),
|
|
96
|
+
{ category, valid: ECONOMY_CATEGORIES.slice() },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
let line = 0;
|
|
100
|
+
if (input && input.line != null && String(input.line).trim() !== '') {
|
|
101
|
+
line = Number.parseInt(input.line, 10);
|
|
102
|
+
if (!Number.isFinite(line) || line < 0) {
|
|
103
|
+
throw new NubosPilotError(
|
|
104
|
+
'economy-debt-invalid-line',
|
|
105
|
+
'economy-debt line must be a non-negative integer',
|
|
106
|
+
{ line: input.line },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { file, note, category, line };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Append a deferred-simplification entry to the open ledger. Idempotent: an
|
|
115
|
+
* identical (file, line, category, note) maps to the same id and is not
|
|
116
|
+
* duplicated — a re-harvest of the same finding is a no-op, returning the
|
|
117
|
+
* existing entry with `created: false`.
|
|
118
|
+
* @returns {{id, category, file, line, created, status, path, was_new: boolean}}
|
|
119
|
+
*/
|
|
120
|
+
function addEntry(input, cwd) {
|
|
121
|
+
const { file, note, category, line } = _validateInput(input);
|
|
122
|
+
const id = _entryId(file, line, category, note);
|
|
123
|
+
const openDir = statusDir('open', cwd);
|
|
124
|
+
const resolvedDir = statusDir('resolved', cwd);
|
|
125
|
+
const slug = slugify(note).slice(0, 48) || 'entry';
|
|
126
|
+
const fileName = id + '-' + slug + '.md';
|
|
127
|
+
|
|
128
|
+
// already open, or already resolved — either way this finding is on record
|
|
129
|
+
const existingOpen = _findById(id, 'open', cwd);
|
|
130
|
+
if (existingOpen) return Object.assign({}, _parseEntry(existingOpen), { was_new: false });
|
|
131
|
+
const existingResolved = _findById(id, 'resolved', cwd);
|
|
132
|
+
if (existingResolved) return Object.assign({}, _parseEntry(existingResolved), { was_new: false });
|
|
133
|
+
|
|
134
|
+
fs.mkdirSync(openDir, { recursive: true });
|
|
135
|
+
const entry = {
|
|
136
|
+
id,
|
|
137
|
+
category,
|
|
138
|
+
file,
|
|
139
|
+
line,
|
|
140
|
+
created: new Date().toISOString(),
|
|
141
|
+
status: 'open',
|
|
142
|
+
note,
|
|
143
|
+
};
|
|
144
|
+
const target = path.join(openDir, fileName);
|
|
145
|
+
fs.writeFileSync(target, _composeMd(entry), 'utf-8');
|
|
146
|
+
void resolvedDir;
|
|
147
|
+
return Object.assign({}, entry, { path: target, was_new: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _findById(id, status, cwd) {
|
|
151
|
+
const dir = statusDir(status, cwd);
|
|
152
|
+
let names;
|
|
153
|
+
try {
|
|
154
|
+
names = fs.readdirSync(dir);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const hit = names.find((n) => n.endsWith('.md') && n.slice(0, ID_LENGTH) === id);
|
|
159
|
+
return hit ? path.join(dir, hit) : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* List ledger entries. status: 'open' | 'resolved' | 'all'. Sorted by created
|
|
164
|
+
* ascending (oldest debt first — the longest-deferred is the most urgent).
|
|
165
|
+
*/
|
|
166
|
+
function listEntries(status, cwd) {
|
|
167
|
+
const want = status || 'open';
|
|
168
|
+
const dirs = want === 'all' ? STATUSES.slice() : [want];
|
|
169
|
+
if (want !== 'all' && !STATUSES.includes(want)) {
|
|
170
|
+
throw new NubosPilotError(
|
|
171
|
+
'economy-debt-invalid-status',
|
|
172
|
+
"economy-debt status must be 'open', 'resolved', or 'all'",
|
|
173
|
+
{ status: want },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const d of dirs) {
|
|
178
|
+
const dir = statusDir(d, cwd);
|
|
179
|
+
let names;
|
|
180
|
+
try {
|
|
181
|
+
names = fs.readdirSync(dir);
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
for (const n of names) {
|
|
186
|
+
if (!n.endsWith('.md')) continue;
|
|
187
|
+
out.push(_parseEntry(path.join(dir, n)));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
out.sort((a, b) => (a.created < b.created ? -1 : a.created > b.created ? 1 : 0));
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Mark an open entry resolved: stamp `resolved` + flip status, then move the
|
|
196
|
+
* file from open/ to resolved/. Throws economy-debt-not-found if no open entry
|
|
197
|
+
* carries the id.
|
|
198
|
+
*/
|
|
199
|
+
function resolveEntry(id, cwd) {
|
|
200
|
+
const wanted = String(id || '').trim();
|
|
201
|
+
if (!wanted) {
|
|
202
|
+
throw new NubosPilotError('economy-debt-missing-id', 'resolve requires an entry id', {});
|
|
203
|
+
}
|
|
204
|
+
const src = _findById(wanted, 'open', cwd);
|
|
205
|
+
if (!src) {
|
|
206
|
+
throw new NubosPilotError(
|
|
207
|
+
'economy-debt-not-found',
|
|
208
|
+
'no open economy-debt entry with id: ' + wanted,
|
|
209
|
+
{ id: wanted },
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const entry = _parseEntry(src);
|
|
213
|
+
entry.resolved = new Date().toISOString();
|
|
214
|
+
entry.status = 'resolved';
|
|
215
|
+
const resolvedDir = statusDir('resolved', cwd);
|
|
216
|
+
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
217
|
+
const target = path.join(resolvedDir, path.basename(src));
|
|
218
|
+
fs.writeFileSync(target, _composeMd(entry), 'utf-8');
|
|
219
|
+
fs.rmSync(src);
|
|
220
|
+
return Object.assign({}, entry, { path: target });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
ECONOMY_CATEGORIES,
|
|
225
|
+
STATUSES,
|
|
226
|
+
MAX_NOTE_LENGTH,
|
|
227
|
+
debtRoot,
|
|
228
|
+
statusDir,
|
|
229
|
+
addEntry,
|
|
230
|
+
listEntries,
|
|
231
|
+
resolveEntry,
|
|
232
|
+
_entryId,
|
|
233
|
+
_parseEntry,
|
|
234
|
+
_composeMd,
|
|
235
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { test, afterEach } = require('node:test');
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
|
|
9
|
+
const debt = require('./economy-debt.cjs');
|
|
10
|
+
|
|
11
|
+
const _sandboxes = [];
|
|
12
|
+
|
|
13
|
+
function makeSandbox() {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-economy-debt-'));
|
|
15
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
16
|
+
_sandboxes.push(root);
|
|
17
|
+
return root;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
while (_sandboxes.length) {
|
|
22
|
+
try { fs.rmSync(_sandboxes.pop(), { recursive: true, force: true }); } catch { /* best effort */ }
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('ED-1: addEntry writes an open entry and returns was_new=true', () => {
|
|
27
|
+
const cwd = makeSandbox();
|
|
28
|
+
const e = debt.addEntry(
|
|
29
|
+
{ file: 'src/foo.ts', line: 42, category: 'over-engineering', note: 'Single-use factory — inline it.' },
|
|
30
|
+
cwd,
|
|
31
|
+
);
|
|
32
|
+
assert.equal(e.was_new, true);
|
|
33
|
+
assert.equal(e.status, 'open');
|
|
34
|
+
assert.equal(e.category, 'over-engineering');
|
|
35
|
+
assert.equal(e.file, 'src/foo.ts');
|
|
36
|
+
assert.equal(e.line, 42);
|
|
37
|
+
assert.match(e.id, /^[0-9a-f]{7}$/);
|
|
38
|
+
assert.ok(fs.existsSync(e.path));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('ED-2: addEntry is idempotent — identical input does not duplicate', () => {
|
|
42
|
+
const cwd = makeSandbox();
|
|
43
|
+
const first = debt.addEntry(
|
|
44
|
+
{ file: 'a.ts', line: 1, category: 'shrinkable', note: 'manual reduce -> Array.reduce' },
|
|
45
|
+
cwd,
|
|
46
|
+
);
|
|
47
|
+
const second = debt.addEntry(
|
|
48
|
+
{ file: 'a.ts', line: 1, category: 'shrinkable', note: 'manual reduce -> Array.reduce' },
|
|
49
|
+
cwd,
|
|
50
|
+
);
|
|
51
|
+
assert.equal(first.id, second.id);
|
|
52
|
+
assert.equal(second.was_new, false);
|
|
53
|
+
assert.equal(debt.listEntries('open', cwd).length, 1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('ED-3: addEntry rejects a category outside the four economy routes', () => {
|
|
57
|
+
const cwd = makeSandbox();
|
|
58
|
+
assert.throws(
|
|
59
|
+
() => debt.addEntry({ file: 'a.ts', category: 'security', note: 'x' }, cwd),
|
|
60
|
+
(err) => err && err.name === 'NubosPilotError' && err.code === 'economy-debt-invalid-category',
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('ED-4: addEntry rejects an empty note', () => {
|
|
65
|
+
const cwd = makeSandbox();
|
|
66
|
+
assert.throws(
|
|
67
|
+
() => debt.addEntry({ file: 'a.ts', category: 'shrinkable', note: ' ' }, cwd),
|
|
68
|
+
(err) => err && err.name === 'NubosPilotError' && err.code === 'economy-debt-missing-note',
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('ED-5: line defaults to 0 (file-level) when omitted', () => {
|
|
73
|
+
const cwd = makeSandbox();
|
|
74
|
+
const e = debt.addEntry({ file: 'a.ts', category: 'native-duplication', note: 'reimplements framework helper' }, cwd);
|
|
75
|
+
assert.equal(e.line, 0);
|
|
76
|
+
const parsed = debt.listEntries('open', cwd)[0];
|
|
77
|
+
assert.equal(parsed.line, 0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('ED-6: listEntries sorts oldest-first and round-trips note + fields', () => {
|
|
81
|
+
const cwd = makeSandbox();
|
|
82
|
+
debt.addEntry({ file: 'a.ts', line: 5, category: 'shrinkable', note: 'first' }, cwd);
|
|
83
|
+
debt.addEntry({ file: 'b.ts', line: 9, category: 'over-engineering', note: 'second' }, cwd);
|
|
84
|
+
const list = debt.listEntries('open', cwd);
|
|
85
|
+
assert.equal(list.length, 2);
|
|
86
|
+
assert.equal(list[0].note, 'first');
|
|
87
|
+
assert.equal(list[1].note, 'second');
|
|
88
|
+
assert.equal(list[1].category, 'over-engineering');
|
|
89
|
+
assert.equal(list[1].line, 9);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('ED-7: resolveEntry moves open -> resolved and stamps resolved time', () => {
|
|
93
|
+
const cwd = makeSandbox();
|
|
94
|
+
const e = debt.addEntry({ file: 'a.ts', line: 1, category: 'stdlib-reinvention', note: 'hand-rolled clamp' }, cwd);
|
|
95
|
+
const r = debt.resolveEntry(e.id, cwd);
|
|
96
|
+
assert.equal(r.status, 'resolved');
|
|
97
|
+
assert.match(r.resolved, /^\d{4}-\d{2}-\d{2}T/);
|
|
98
|
+
assert.equal(debt.listEntries('open', cwd).length, 0);
|
|
99
|
+
assert.equal(debt.listEntries('resolved', cwd).length, 1);
|
|
100
|
+
assert.equal(debt.listEntries('all', cwd).length, 1);
|
|
101
|
+
assert.ok(!fs.existsSync(e.path));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('ED-8: resolveEntry throws economy-debt-not-found for an unknown id', () => {
|
|
105
|
+
const cwd = makeSandbox();
|
|
106
|
+
assert.throws(
|
|
107
|
+
() => debt.resolveEntry('deadbee', cwd),
|
|
108
|
+
(err) => err && err.name === 'NubosPilotError' && err.code === 'economy-debt-not-found',
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('ED-9: listEntries rejects an invalid status', () => {
|
|
113
|
+
const cwd = makeSandbox();
|
|
114
|
+
assert.throws(
|
|
115
|
+
() => debt.listEntries('bogus', cwd),
|
|
116
|
+
(err) => err && err.name === 'NubosPilotError' && err.code === 'economy-debt-invalid-status',
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('ED-10: empty ledger lists as []', () => {
|
|
121
|
+
const cwd = makeSandbox();
|
|
122
|
+
assert.deepEqual(debt.listEntries('open', cwd), []);
|
|
123
|
+
assert.deepEqual(debt.listEntries('all', cwd), []);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('ED-11: ECONOMY_CATEGORIES matches the four canonical economy routes', () => {
|
|
127
|
+
assert.deepEqual(
|
|
128
|
+
debt.ECONOMY_CATEGORIES.slice().sort(),
|
|
129
|
+
['native-duplication', 'over-engineering', 'shrinkable', 'stdlib-reinvention'],
|
|
130
|
+
);
|
|
131
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Single source of truth for the Economy axis activation level (Ponytail-style
|
|
4
|
+
// graduated modes). The Economy schicht has two mechanisms — the prevention
|
|
5
|
+
// ladder in agents/np-executor.md (guidance BEFORE writing) and the in-loop
|
|
6
|
+
// Economy critic (agents/np-critic-economy.md, audits the diff AFTER). One
|
|
7
|
+
// enum dials both:
|
|
8
|
+
//
|
|
9
|
+
// off prevention OFF, critic OFF — no economy pressure at all
|
|
10
|
+
// lite prevention ON, critic OFF — prevention-first DEFAULT (advisory only)
|
|
11
|
+
// full prevention ON, critic ON — standard critic rubric
|
|
12
|
+
// ultra prevention ON, critic ON — aggressive critic (lowered shrinkable bar)
|
|
13
|
+
//
|
|
14
|
+
// Default is `lite`: the climb-the-ladder discipline is on, but nothing bounces
|
|
15
|
+
// work back. This makes prevention-first the documented default philosophy
|
|
16
|
+
// while keeping the costlier critic opt-in (full/ultra).
|
|
17
|
+
//
|
|
18
|
+
// Backward-compat: the legacy boolean `agents.economy_critic` is honoured when
|
|
19
|
+
// `agents.economy` is absent — true→full, false→lite — so a pre-existing
|
|
20
|
+
// gitignored config keeps its behaviour. The resolver is LOUD: an explicit but
|
|
21
|
+
// invalid `agents.economy` string throws rather than silently defaulting.
|
|
22
|
+
|
|
23
|
+
const { NubosPilotError } = require('./core.cjs');
|
|
24
|
+
|
|
25
|
+
const VALID_ECONOMY_MODES = Object.freeze(['off', 'lite', 'full', 'ultra']);
|
|
26
|
+
const DEFAULT_ECONOMY_MODE = 'lite';
|
|
27
|
+
|
|
28
|
+
function resolveEconomyMode(config) {
|
|
29
|
+
const agents = config && typeof config === 'object' ? config.agents : null;
|
|
30
|
+
if (agents && typeof agents === 'object') {
|
|
31
|
+
if (agents.economy !== undefined) {
|
|
32
|
+
const explicit = agents.economy;
|
|
33
|
+
if (typeof explicit !== 'string' || !VALID_ECONOMY_MODES.includes(explicit)) {
|
|
34
|
+
throw new NubosPilotError(
|
|
35
|
+
'config-invalid-economy-mode',
|
|
36
|
+
'agents.economy must be one of ' + VALID_ECONOMY_MODES.join('|') + ' (got: ' + JSON.stringify(explicit) + ')',
|
|
37
|
+
{ value: explicit, valid: VALID_ECONOMY_MODES },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return explicit;
|
|
41
|
+
}
|
|
42
|
+
if (typeof agents.economy_critic === 'boolean') {
|
|
43
|
+
return agents.economy_critic ? 'full' : 'lite';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return DEFAULT_ECONOMY_MODE;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function preventionOn(mode) { return mode !== 'off'; }
|
|
50
|
+
function criticOn(mode) { return mode === 'full' || mode === 'ultra'; }
|
|
51
|
+
function isUltra(mode) { return mode === 'ultra'; }
|
|
52
|
+
|
|
53
|
+
function economyFlags(config) {
|
|
54
|
+
const mode = resolveEconomyMode(config);
|
|
55
|
+
return { mode, prevention: preventionOn(mode), critic: criticOn(mode), ultra: isUltra(mode) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
VALID_ECONOMY_MODES,
|
|
60
|
+
DEFAULT_ECONOMY_MODE,
|
|
61
|
+
resolveEconomyMode,
|
|
62
|
+
preventionOn,
|
|
63
|
+
criticOn,
|
|
64
|
+
isUltra,
|
|
65
|
+
economyFlags,
|
|
66
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
VALID_ECONOMY_MODES,
|
|
8
|
+
DEFAULT_ECONOMY_MODE,
|
|
9
|
+
resolveEconomyMode,
|
|
10
|
+
preventionOn,
|
|
11
|
+
criticOn,
|
|
12
|
+
isUltra,
|
|
13
|
+
economyFlags,
|
|
14
|
+
} = require('./economy-mode.cjs');
|
|
15
|
+
|
|
16
|
+
test('default is lite (prevention-first) when nothing is set', () => {
|
|
17
|
+
assert.equal(DEFAULT_ECONOMY_MODE, 'lite');
|
|
18
|
+
assert.equal(resolveEconomyMode({}), 'lite');
|
|
19
|
+
assert.equal(resolveEconomyMode({ agents: {} }), 'lite');
|
|
20
|
+
assert.equal(resolveEconomyMode(null), 'lite');
|
|
21
|
+
assert.equal(resolveEconomyMode(undefined), 'lite');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('explicit agents.economy wins for every valid mode', () => {
|
|
25
|
+
for (const mode of VALID_ECONOMY_MODES) {
|
|
26
|
+
assert.equal(resolveEconomyMode({ agents: { economy: mode } }), mode);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('legacy agents.economy_critic maps true→full, false→lite', () => {
|
|
31
|
+
assert.equal(resolveEconomyMode({ agents: { economy_critic: true } }), 'full');
|
|
32
|
+
assert.equal(resolveEconomyMode({ agents: { economy_critic: false } }), 'lite');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('non-boolean legacy economy_critic falls back to the lite default (schema warns separately)', () => {
|
|
36
|
+
assert.equal(resolveEconomyMode({ agents: { economy_critic: 'true' } }), 'lite');
|
|
37
|
+
assert.equal(resolveEconomyMode({ agents: { economy_critic: 1 } }), 'lite');
|
|
38
|
+
assert.equal(resolveEconomyMode({ agents: { economy_critic: null } }), 'lite');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('explicit economy overrides the legacy bool', () => {
|
|
42
|
+
assert.equal(resolveEconomyMode({ agents: { economy: 'off', economy_critic: true } }), 'off');
|
|
43
|
+
assert.equal(resolveEconomyMode({ agents: { economy: 'ultra', economy_critic: false } }), 'ultra');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('invalid explicit economy throws loud (no silent default)', () => {
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => resolveEconomyMode({ agents: { economy: 'banana' } }),
|
|
49
|
+
(err) => err.code === 'config-invalid-economy-mode',
|
|
50
|
+
);
|
|
51
|
+
assert.throws(
|
|
52
|
+
() => resolveEconomyMode({ agents: { economy: 42 } }),
|
|
53
|
+
(err) => err.code === 'config-invalid-economy-mode',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('flag helpers gate prevention/critic/ultra correctly', () => {
|
|
58
|
+
assert.equal(preventionOn('off'), false);
|
|
59
|
+
assert.equal(preventionOn('lite'), true);
|
|
60
|
+
assert.equal(preventionOn('full'), true);
|
|
61
|
+
assert.equal(preventionOn('ultra'), true);
|
|
62
|
+
|
|
63
|
+
assert.equal(criticOn('off'), false);
|
|
64
|
+
assert.equal(criticOn('lite'), false);
|
|
65
|
+
assert.equal(criticOn('full'), true);
|
|
66
|
+
assert.equal(criticOn('ultra'), true);
|
|
67
|
+
|
|
68
|
+
assert.equal(isUltra('ultra'), true);
|
|
69
|
+
assert.equal(isUltra('full'), false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('economyFlags bundles the resolved mode with its gates', () => {
|
|
73
|
+
assert.deepEqual(economyFlags({ agents: { economy: 'off' } }), {
|
|
74
|
+
mode: 'off', prevention: false, critic: false, ultra: false,
|
|
75
|
+
});
|
|
76
|
+
assert.deepEqual(economyFlags({}), {
|
|
77
|
+
mode: 'lite', prevention: true, critic: false, ultra: false,
|
|
78
|
+
});
|
|
79
|
+
assert.deepEqual(economyFlags({ agents: { economy: 'full' } }), {
|
|
80
|
+
mode: 'full', prevention: true, critic: true, ultra: false,
|
|
81
|
+
});
|
|
82
|
+
assert.deepEqual(economyFlags({ agents: { economy: 'ultra' } }), {
|
|
83
|
+
mode: 'ultra', prevention: true, critic: true, ultra: true,
|
|
84
|
+
});
|
|
85
|
+
});
|
package/lib/nubosloop.cjs
CHANGED
|
@@ -34,6 +34,10 @@ const ROUTE_TABLE = {
|
|
|
34
34
|
'verify-mismatch': 'executor',
|
|
35
35
|
'unmet-criterion': 'executor',
|
|
36
36
|
'scope-creep': 'executor',
|
|
37
|
+
'over-engineering': 'executor',
|
|
38
|
+
'stdlib-reinvention': 'executor',
|
|
39
|
+
'native-duplication': 'executor',
|
|
40
|
+
'shrinkable': 'executor',
|
|
37
41
|
'information-missing': 'researcher',
|
|
38
42
|
'question-to-user': 'askuser',
|
|
39
43
|
'locked-decision-violation': 'plan-checker',
|
package/lib/nubosloop.test.cjs
CHANGED
|
@@ -191,6 +191,7 @@ test('NL-17: ROUTE_TABLE covers every documented finding category', () => {
|
|
|
191
191
|
'missing-test', 'edge-case-gap',
|
|
192
192
|
'weak-assertion', 'silenced-failure', 'test-naming', 'non-deterministic',
|
|
193
193
|
'verify-mismatch', 'unmet-criterion', 'scope-creep', 'information-missing',
|
|
194
|
+
'over-engineering', 'stdlib-reinvention', 'native-duplication', 'shrinkable',
|
|
194
195
|
'infrastructure-mismatch',
|
|
195
196
|
'question-to-user', 'locked-decision-violation', 'stuck-detected',
|
|
196
197
|
];
|
package/np-tools.cjs
CHANGED
|
@@ -44,6 +44,7 @@ const topLevelCommands = {
|
|
|
44
44
|
'askuser': require('./bin/np-tools/askuser.cjs'),
|
|
45
45
|
'commit': require('./bin/np-tools/commit.cjs'),
|
|
46
46
|
'config-get': require('./bin/np-tools/config.cjs'),
|
|
47
|
+
'economy-mode': require('./bin/np-tools/economy-mode.cjs'),
|
|
47
48
|
'scan-codebase': require('./bin/np-tools/scan-codebase.cjs'),
|
|
48
49
|
'update-docs': require('./bin/np-tools/update-docs.cjs'),
|
|
49
50
|
'graph-impact': require('./bin/np-tools/graph-impact.cjs'),
|
|
@@ -79,6 +80,7 @@ const topLevelCommands = {
|
|
|
79
80
|
'worktree-list': require('./bin/np-tools/worktree-list.cjs'),
|
|
80
81
|
'worktree-ff-merge': require('./bin/np-tools/worktree-ff-merge.cjs'),
|
|
81
82
|
'dashboard': require('./bin/np-tools/dashboard.cjs'),
|
|
83
|
+
'simplify-debt': require('./bin/np-tools/simplify-debt.cjs'),
|
|
82
84
|
'archive-project': require('./bin/np-tools/archive-project.cjs'),
|
|
83
85
|
|
|
84
86
|
...initWorkflows,
|
package/package.json
CHANGED