@yadurajfleetos/cli 0.10.0 → 0.10.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/dist/commands/services.js +14 -4
- package/dist/repomap.js +75 -30
- package/package.json +1 -1
|
@@ -465,14 +465,22 @@ async function theOnlyNode(flags) {
|
|
|
465
465
|
async function reviewed(draft, flags) {
|
|
466
466
|
const { repoMap } = await import('../repomap.js');
|
|
467
467
|
const fleetId = await requireFleet(typeof flags.fleet === 'string' ? flags.fleet : undefined);
|
|
468
|
-
|
|
469
|
-
|
|
468
|
+
// The second pass is a different, smaller question.
|
|
469
|
+
//
|
|
470
|
+
// It used to resend the original draft and the whole repository map, which
|
|
471
|
+
// on this project meant ~6,000 tokens seconds after the first call had spent
|
|
472
|
+
// ~3,000 — straight through a per-minute budget, and the answer the user had
|
|
473
|
+
// just given was discarded. Applying an answer needs the manifest it applies
|
|
474
|
+
// to and the answer, not the evidence that produced it: the model has
|
|
475
|
+
// already read the repository and written its conclusions down.
|
|
476
|
+
const review = (base, map, answers) => task(answers ? 'applying your answers' : 'reading the repository for a second opinion', async () => request('POST', `/fleets/${fleetId}/manifest/assist`, {
|
|
477
|
+
body: { draft: base, repoMap: map, ...(answers ? { answers } : {}) },
|
|
470
478
|
}), { done: () => (answers ? 'done' : 'reviewed') });
|
|
471
479
|
let map;
|
|
472
480
|
let out;
|
|
473
481
|
try {
|
|
474
482
|
map = await repoMap();
|
|
475
|
-
out = (await review(map)).body;
|
|
483
|
+
out = (await review(draft, map)).body;
|
|
476
484
|
}
|
|
477
485
|
catch (err) {
|
|
478
486
|
console.log(`${glyph.warn} ${c.yellow('review skipped')} ${err instanceof Error ? err.message : 'the control plane could not be reached'}`);
|
|
@@ -506,7 +514,9 @@ async function reviewed(draft, flags) {
|
|
|
506
514
|
const answered = await answerQuestions(out.questions, flags);
|
|
507
515
|
if (answered) {
|
|
508
516
|
try {
|
|
509
|
-
|
|
517
|
+
// The reviewed manifest is what the answer applies to, and the tree
|
|
518
|
+
// alone is enough context to keep names straight.
|
|
519
|
+
const second = (await review(out.manifest, map.split('\n## ')[0] ?? map, answered)).body;
|
|
510
520
|
if (second.status === 'ok') {
|
|
511
521
|
for (const note of second.notes)
|
|
512
522
|
console.log(c.dim(` · ${note}`));
|
package/dist/repomap.js
CHANGED
|
@@ -27,20 +27,34 @@ const SKIP = new Set([
|
|
|
27
27
|
* the middle of a server file is business logic nobody needs to see.
|
|
28
28
|
*/
|
|
29
29
|
const EVIDENCE = [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{ name: /^
|
|
33
|
-
{ name: /^
|
|
34
|
-
{ name: /^(
|
|
35
|
-
{ name:
|
|
36
|
-
{ name: /^(
|
|
37
|
-
|
|
38
|
-
{ name: /^
|
|
30
|
+
// Tier 1 answers "what is this and what port does it serve" — the two
|
|
31
|
+
// questions the manifest is actually made of.
|
|
32
|
+
{ name: /^package\.json$/, lines: 45, tier: 1 },
|
|
33
|
+
{ name: /^(requirements|requirements-prod)\.txt$/, lines: 30, tier: 1 },
|
|
34
|
+
{ name: /^(pyproject\.toml|Pipfile|go\.mod|Cargo\.toml|Gemfile)$/, lines: 30, tier: 1 },
|
|
35
|
+
{ name: /^Dockerfile(\..+)?$/, lines: 30, tier: 1 },
|
|
36
|
+
{ name: /^(docker-)?compose\.ya?ml$/, lines: 50, tier: 1 },
|
|
37
|
+
// Tier 2 is where a route prefix or a listen() hides.
|
|
38
|
+
{ name: /^(main|server|app|index)\.(js|ts|mjs|py|go|rb)$/, lines: 30, tier: 2 },
|
|
39
|
+
{ name: /^(vite|next|nuxt|astro|svelte)\.config\.(js|ts|mjs)$/, lines: 20, tier: 2 },
|
|
40
|
+
{ name: /^\.env\.(example|sample|template)$/, lines: 25, tier: 2 },
|
|
41
|
+
// Tier 3 is context, and the first thing to go.
|
|
42
|
+
{ name: /^README(\.md)?$/, lines: 15, tier: 3 },
|
|
39
43
|
];
|
|
40
44
|
const MAX_DEPTH = 3;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Sized for a model's context, not for the endpoint's limit.
|
|
47
|
+
*
|
|
48
|
+
* A free Groq tier allows 8000 tokens a minute for the whole request. This
|
|
49
|
+
* repository's map came to 25kB — about 6,300 tokens — and with the system
|
|
50
|
+
* prompt and the draft on top the request was 8,901 and refused. Roughly four
|
|
51
|
+
* characters to the token, so 14kB of evidence leaves room for the prompt, the
|
|
52
|
+
* draft, and a reply containing a whole manifest.
|
|
53
|
+
*/
|
|
54
|
+
const MAX_TOTAL_CHARS = 14_000;
|
|
55
|
+
/** The tree is orientation, not evidence, and it was a fifth of the budget. */
|
|
56
|
+
const MAX_TREE_ENTRIES = 120;
|
|
57
|
+
const MAX_TREE_CHARS = 2_500;
|
|
44
58
|
function windowOf(text, lines) {
|
|
45
59
|
const all = text.split('\n');
|
|
46
60
|
if (all.length <= lines)
|
|
@@ -86,35 +100,66 @@ async function tree(root) {
|
|
|
86
100
|
/** Build the evidence bundle for a repository root. */
|
|
87
101
|
export async function repoMap(root = process.cwd()) {
|
|
88
102
|
const paths = await tree(root);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
let treeText = paths.join('\n');
|
|
104
|
+
if (treeText.length > MAX_TREE_CHARS) {
|
|
105
|
+
treeText = treeText.slice(0, MAX_TREE_CHARS).split('\n').slice(0, -1).join('\n') + '\n…';
|
|
106
|
+
}
|
|
107
|
+
const sections = ['## Tree', treeText];
|
|
93
108
|
let budget = MAX_TOTAL_CHARS - sections.join('\n').length;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
// Candidates, tagged with which service they belong to.
|
|
110
|
+
//
|
|
111
|
+
// The directory is the unit that matters: a monorepo is several services,
|
|
112
|
+
// and one of them having a README is worth less than another having its
|
|
113
|
+
// Dockerfile. Reading files in path order spent the whole budget inside the
|
|
114
|
+
// first two directories and left the rest of the repository undescribed.
|
|
115
|
+
const candidates = paths
|
|
116
|
+
.filter((rel) => !rel.endsWith('/'))
|
|
117
|
+
.map((rel) => {
|
|
97
118
|
const base = rel.split('/').pop() ?? rel;
|
|
98
119
|
const rule = EVIDENCE.find((e) => e.name.test(base));
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
return rule ? { rel, rule, dir: rel.includes('/') ? rel.split('/')[0] : '.' } : null;
|
|
121
|
+
})
|
|
122
|
+
.filter((x) => x !== null);
|
|
123
|
+
const read = async (c) => {
|
|
102
124
|
try {
|
|
125
|
+
const full = join(root, c.rel);
|
|
103
126
|
const info = await stat(full);
|
|
104
127
|
// A megabyte of lockfile-shaped JSON is not evidence.
|
|
105
128
|
if (info.size > 512 * 1024)
|
|
106
|
-
|
|
129
|
+
return null;
|
|
107
130
|
const text = await readFile(full, 'utf8');
|
|
108
|
-
|
|
109
|
-
// Stop cleanly at the budget rather than sending a truncated file that
|
|
110
|
-
// reads as though the repository itself is malformed.
|
|
111
|
-
if (block.length > budget)
|
|
112
|
-
break;
|
|
113
|
-
sections.push(block);
|
|
114
|
-
budget -= block.length;
|
|
131
|
+
return `\n## ${c.rel}\n${windowOf(text, c.rule.lines)}`;
|
|
115
132
|
}
|
|
116
133
|
catch {
|
|
117
134
|
// Unreadable is not fatal; it is simply not evidence.
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
// Tier by tier, and within a tier one file per directory before any
|
|
139
|
+
// directory gets a second. Every service is described before any service is
|
|
140
|
+
// described twice, so trimming costs depth rather than whole services.
|
|
141
|
+
for (const tier of [1, 2, 3]) {
|
|
142
|
+
const inTier = candidates.filter((c) => c.rule.tier === tier);
|
|
143
|
+
const byDir = new Map();
|
|
144
|
+
for (const c of inTier) {
|
|
145
|
+
byDir.set(c.dir, [...(byDir.get(c.dir) ?? []), c]);
|
|
146
|
+
}
|
|
147
|
+
let round = 0;
|
|
148
|
+
let placed = true;
|
|
149
|
+
while (placed && budget > 0) {
|
|
150
|
+
placed = false;
|
|
151
|
+
for (const list of byDir.values()) {
|
|
152
|
+
const c = list[round];
|
|
153
|
+
if (!c)
|
|
154
|
+
continue;
|
|
155
|
+
placed = true;
|
|
156
|
+
const block = await read(c);
|
|
157
|
+
if (!block || block.length > budget)
|
|
158
|
+
continue;
|
|
159
|
+
sections.push(block);
|
|
160
|
+
budget -= block.length;
|
|
161
|
+
}
|
|
162
|
+
round++;
|
|
118
163
|
}
|
|
119
164
|
}
|
|
120
165
|
return sections.join('\n');
|