@xhulz/nina 0.28.18 → 0.28.19
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/package.json +1 -1
- package/releases/0.28.19/core/GAPS.md +125 -0
- package/releases/0.28.19/core/answered.json +29 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.28.19/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.28.19/core/tree/.claude/agents/architect.md +125 -0
- package/releases/0.28.19/core/tree/.claude/agents/dba.md +107 -0
- package/releases/0.28.19/core/tree/.claude/agents/devops.md +113 -0
- package/releases/0.28.19/core/tree/.claude/agents/implementer.md +155 -0
- package/releases/0.28.19/core/tree/.claude/agents/integration-tester.md +160 -0
- package/releases/0.28.19/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.28.19/core/tree/.claude/agents/qa.md +146 -0
- package/releases/0.28.19/core/tree/.claude/agents/reviewer.md +180 -0
- package/releases/0.28.19/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.28.19/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.28.19/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.28.19/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.28.19/core/tree/.claude/graph.md +73 -0
- package/releases/0.28.19/core/tree/.claude/patterns.md +212 -0
- package/releases/0.28.19/core/tree/.claude/pills/README.md +145 -0
- package/releases/0.28.19/core/tree/.claude/pipeline.md +102 -0
- package/releases/0.28.19/core/tree/.claude/retrieval.md +104 -0
- package/releases/0.28.19/core/tree/.claude/router.md +201 -0
- package/releases/0.28.19/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.28.19/core/tree/CLAUDE.md +155 -0
- package/releases/0.28.19/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.28.19/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.28.19/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.28.19/core/vocabulary.json +12 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.28.19/surfaces/blockchain/tree/.claude/router.md +9 -0
- package/releases/0.28.19/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/architect.md +8 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/dba.md +84 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/devops.md +14 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/implementer.md +5 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/integration-tester.md +13 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/reviewer.md +25 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/patterns.md +29 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.28.19/surfaces/db/tree/.claude/router.md +10 -0
- package/releases/0.28.19/surfaces/db/tree/CLAUDE.md +23 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/devops.md +31 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/.claude/router.md +21 -0
- package/releases/0.28.19/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/agents/reviewer.md +62 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/patterns.md +15 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/retrieval.md +5 -0
- package/releases/0.28.19/surfaces/frontend/tree/.claude/router.md +17 -0
- package/releases/0.28.19/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/graph.md +9 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.28.19/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.28.19/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.28.19/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.28.19/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.28.19/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.28.19/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.28.19/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.28.19/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.28.19/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.28.19/surfaces/pii/tree/CLAUDE.md +5 -0
- package/src/commands/pipeline.mjs +190 -55
- package/src/graph.mjs +16 -1
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `nina pipeline` — draws a project's agent chain: the line work moves along, the gates it passes on the
|
|
3
|
-
* way, where each stage sends work back and how many rounds
|
|
4
|
-
* and
|
|
3
|
+
* way, the stages that run as several agents at once, where each stage sends work back and how many rounds
|
|
4
|
+
* it may, each stage's model, effort and skills with what it did over the last thirty days, and which chain
|
|
5
|
+
* a task of each shape takes. `--for` draws one shape's chain alone.
|
|
5
6
|
*
|
|
6
7
|
* Opening a project, the pipeline was a graph, a router, ten specs and a table in CLAUDE.md to read before
|
|
7
8
|
* anyone could say which stage came after which, on which model, with which skills. Every fact here is
|
|
8
|
-
* already composed, and `nina check` already holds it to the graph; this only reads it
|
|
9
|
+
* already composed, and `nina check` already holds it to the graph, or already measured; this only reads it
|
|
10
|
+
* back and draws it.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
12
14
|
import { basename, join, resolve } from 'node:path';
|
|
13
15
|
import { PINK, useColor } from '../banner.mjs';
|
|
14
|
-
import { HARNESS } from '../paths.mjs';
|
|
16
|
+
import { HARNESS, slugFor, snapshotsDir } from '../paths.mjs';
|
|
15
17
|
import { TERMINALS, parseGraph } from '../graph.mjs';
|
|
16
18
|
import { required } from '../tools.mjs';
|
|
17
19
|
import { isLoopBack } from '../transcripts.mjs';
|
|
@@ -20,6 +22,9 @@ import { frontmatter } from './pills.mjs';
|
|
|
20
22
|
/** A stage line under "## Stages": "- `name` — what it does". */
|
|
21
23
|
const STAGE_LINE = /^- `([a-z][a-z-]*)` — (.*)$/;
|
|
22
24
|
|
|
25
|
+
/** How far back the history beside each stage looks, unless `--since` says. */
|
|
26
|
+
const HISTORY_DAYS = 30;
|
|
27
|
+
|
|
23
28
|
/**
|
|
24
29
|
* What each stage does, in the graph's own words: shorter than a spec's `description:`, which is written
|
|
25
30
|
* for Claude Code to choose an agent by.
|
|
@@ -48,7 +53,8 @@ export function stageWords(text) {
|
|
|
48
53
|
* - The gates: a stage off the line that a line stage sends work to on a condition, and that hands it on
|
|
49
54
|
* to the next stage of the line.
|
|
50
55
|
* - The ends: where the line's last stage sends work on a condition, followed forward to a terminal.
|
|
51
|
-
* - The loop-backs: every edge on a verdict that sends work back, to a stage or to the owner
|
|
56
|
+
* - The loop-backs: every edge on a verdict that sends work back, to a stage or to the owner, grouped by
|
|
57
|
+
* the stage that sends it.
|
|
52
58
|
*
|
|
53
59
|
* @param {{stages: Set<string>, edges: object[]}} graph - A parsed graph.
|
|
54
60
|
* @returns {{line: string[], gates: object[], ends: object[], back: object[]}}
|
|
@@ -81,17 +87,18 @@ export function shapeOf(graph) {
|
|
|
81
87
|
}
|
|
82
88
|
return { path, when: e.when };
|
|
83
89
|
});
|
|
84
|
-
// Grouped by the stage that sends the work back, in the order the stages are listed.
|
|
85
90
|
const back = graph.edges.filter((e) => isLoopBack(e.token)).sort((a, b) => order.indexOf(a.from) - order.indexOf(b.from));
|
|
86
91
|
return { line, gates, ends, back };
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
/**
|
|
90
95
|
* The rows of the "Required chain by task shape" table in a composed CLAUDE.md: the shape, and the chain it
|
|
91
|
-
* takes — the part the table sets in bold, where it does.
|
|
96
|
+
* takes — the part the table sets in bold, where it does. A row whose chain runs stage → stage, or is
|
|
97
|
+
* "none", is a chain of its own; any other (`+ dba before reviewer`) is an add-on to whichever chain the
|
|
98
|
+
* task takes.
|
|
92
99
|
*
|
|
93
100
|
* @param {string} text - The composed CLAUDE.md.
|
|
94
|
-
* @returns {{shape: string, chain: string}[]}
|
|
101
|
+
* @returns {{shape: string, chain: string, addOn: boolean}[]}
|
|
95
102
|
*/
|
|
96
103
|
export function chainsByShape(text) {
|
|
97
104
|
const at = text.indexOf('| Task shape |');
|
|
@@ -102,27 +109,116 @@ export function chainsByShape(text) {
|
|
|
102
109
|
const [shape = '', chain = ''] = row.split('|').slice(1).map((c) => c.trim());
|
|
103
110
|
const bold = /\*\*(.+?)\*\*/.exec(chain)?.[1];
|
|
104
111
|
const plain = (s) => s.replace(/\*\*/g, '').replace(/`/g, '').trim();
|
|
105
|
-
|
|
112
|
+
const taken = plain(bold ?? chain);
|
|
113
|
+
rows.push({ shape: plain(shape), chain: taken, addOn: !taken.includes('→') && !/^none\b/i.test(taken) });
|
|
106
114
|
}
|
|
107
115
|
return rows;
|
|
108
116
|
}
|
|
109
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The task shapes a `--for` names: by its number in the list, or by every word it gives.
|
|
120
|
+
*
|
|
121
|
+
* @param {{shape: string}[]} shapes - The chain table's rows.
|
|
122
|
+
* @param {string} query - A number, or words.
|
|
123
|
+
* @returns {object[]}
|
|
124
|
+
*/
|
|
125
|
+
export function pickShape(shapes, query) {
|
|
126
|
+
if (/^\d+$/.test(query.trim())) {
|
|
127
|
+
const row = shapes[Number(query) - 1];
|
|
128
|
+
return row ? [row] : [];
|
|
129
|
+
}
|
|
130
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
131
|
+
if (words.length === 0) return [];
|
|
132
|
+
return shapes.filter((r) => words.every((w) => r.shape.toLowerCase().includes(w)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What each stage did since a date, from the measurement store: its runs, the verdicts that could be read,
|
|
137
|
+
* and how many of those sent work back. A run a hook denied never ran, and is not counted.
|
|
138
|
+
*
|
|
139
|
+
* @param {object[]} records - The project's records.
|
|
140
|
+
* @param {string} since - An ISO date.
|
|
141
|
+
* @returns {Map<string, {runs: number, read: number, back: number}>}
|
|
142
|
+
*/
|
|
143
|
+
export function historyOf(records, since) {
|
|
144
|
+
const byRole = new Map();
|
|
145
|
+
for (const r of records) {
|
|
146
|
+
if (!r.role || r.status === 'denied' || String(r.ts ?? '') < since) continue;
|
|
147
|
+
const s = byRole.get(r.role) ?? { runs: 0, read: 0, back: 0 };
|
|
148
|
+
s.runs += 1;
|
|
149
|
+
if (r.verdict && r.verdict !== 'UNCLEAR' && r.verdict !== 'NONE') {
|
|
150
|
+
s.read += 1;
|
|
151
|
+
if (isLoopBack(r.verdict)) s.back += 1;
|
|
152
|
+
}
|
|
153
|
+
byRole.set(r.role, s);
|
|
154
|
+
}
|
|
155
|
+
return byRole;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The project's records in the measurement store, under the name Claude Code gives its directory — as
|
|
160
|
+
* given, or with its links resolved, since either may be the one the sessions ran in.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} dir - The project.
|
|
163
|
+
* @returns {object[]|null} Null when the store has nothing for it.
|
|
164
|
+
*/
|
|
165
|
+
function storedRuns(dir) {
|
|
166
|
+
const names = new Set([slugFor(dir)]);
|
|
167
|
+
try {
|
|
168
|
+
names.add(slugFor(realpathSync(dir)));
|
|
169
|
+
} catch {
|
|
170
|
+
// A directory that cannot be resolved is looked up as given.
|
|
171
|
+
}
|
|
172
|
+
for (const name of names) {
|
|
173
|
+
const file = join(snapshotsDir(), `${name}.jsonl`);
|
|
174
|
+
if (!existsSync(file)) continue;
|
|
175
|
+
return readFileSync(file, 'utf8')
|
|
176
|
+
.split('\n')
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.flatMap((l) => {
|
|
179
|
+
try {
|
|
180
|
+
return [JSON.parse(l)];
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
110
189
|
/**
|
|
111
190
|
* Runs the command.
|
|
112
191
|
*
|
|
113
|
-
* @param {string[]} argv - `[--project <dir>]`.
|
|
192
|
+
* @param {string[]} argv - `[--project <dir>] [--for <number or words>] [--since <YYYY-MM-DD>]`.
|
|
114
193
|
* @returns {Promise<number>} Process exit code.
|
|
115
194
|
*/
|
|
116
195
|
export async function pipeline(argv) {
|
|
117
|
-
const
|
|
196
|
+
const arg = (flag) => (argv.includes(flag) ? argv[argv.indexOf(flag) + 1] ?? '' : null);
|
|
197
|
+
const dir = resolve(arg('--project') ?? process.cwd());
|
|
118
198
|
const graphPath = join(dir, '.claude', 'graph.md');
|
|
119
199
|
if (!existsSync(graphPath)) {
|
|
120
200
|
console.error(` no .claude/graph.md in ${dir} — this draws a composed pipeline: \`nina compose\` writes one`);
|
|
121
201
|
return 1;
|
|
122
202
|
}
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
203
|
+
const graphText = readFileSync(graphPath, 'utf8');
|
|
204
|
+
const graph = parseGraph(graphText);
|
|
205
|
+
const words = stageWords(graphText);
|
|
206
|
+
const shape = shapeOf(graph);
|
|
207
|
+
const claude = join(dir, 'CLAUDE.md');
|
|
208
|
+
const shapes = existsSync(claude) ? chainsByShape(readFileSync(claude, 'utf8')) : [];
|
|
209
|
+
|
|
210
|
+
// `--for`: one shape's chain, and only the stages on it.
|
|
211
|
+
const query = arg('--for');
|
|
212
|
+
let chosen = null;
|
|
213
|
+
if (query !== null) {
|
|
214
|
+
const picked = pickShape(shapes, query);
|
|
215
|
+
if (picked.length !== 1) {
|
|
216
|
+
console.error(picked.length === 0 ? ` no task shape matches "${query}" — by number or by words:` : ` "${query}" matches ${picked.length} task shapes — narrow it, or give the number:`);
|
|
217
|
+
for (const [i, r] of shapes.entries()) if (picked.length === 0 || picked.includes(r)) console.error(` ${String(i + 1).padStart(2)} ${r.shape}`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
chosen = picked[0];
|
|
221
|
+
}
|
|
126
222
|
|
|
127
223
|
const color = useColor();
|
|
128
224
|
const name = (s) => (color && !TERMINALS.has(s) ? `\x1b[38;2;${PINK.join(';')}m${s}\x1b[0m` : s);
|
|
@@ -130,6 +226,12 @@ export async function pipeline(argv) {
|
|
|
130
226
|
const width = Math.max(80, Math.min(process.stdout.columns ?? 110, 140));
|
|
131
227
|
const fit = (s, room) => (s.length > room ? `${s.slice(0, Math.max(room - 1, 1))}…` : s);
|
|
132
228
|
const arrow = ' ─▶ ';
|
|
229
|
+
const many = graph.many ?? new Map();
|
|
230
|
+
// A stage that may run as several agents at once carries ×n wherever the line draws it. Alignment is
|
|
231
|
+
// counted on the plain text, and the names are painted after.
|
|
232
|
+
const plainStage = (s) => `${s}${many.has(s) ? ' ×n' : ''}`;
|
|
233
|
+
const shownStage = (s) => `${name(s)}${many.has(s) ? ' ×n' : ''}`;
|
|
234
|
+
const padName = (s, n) => `${name(s)}${' '.repeat(Math.max(n - s.length, 1))}`;
|
|
133
235
|
|
|
134
236
|
let profile = {};
|
|
135
237
|
try {
|
|
@@ -138,32 +240,54 @@ export async function pipeline(argv) {
|
|
|
138
240
|
// A pipeline composed by hand draws all the same; only the header has less to say.
|
|
139
241
|
}
|
|
140
242
|
const surfaces = (profile.surfaces ?? []).slice().sort();
|
|
141
|
-
console.log(` pipeline · ${basename(dir)}${profile.core ? ` · core ${profile.core}` : ''}${surfaces.length ? ` · surfaces ${surfaces.join(', ')}` : ''}
|
|
243
|
+
console.log(` pipeline · ${basename(dir)}${profile.core ? ` · core ${profile.core}` : ''}${surfaces.length ? ` · surfaces ${surfaces.join(', ')}` : ''}`);
|
|
142
244
|
|
|
143
|
-
//
|
|
144
|
-
const
|
|
245
|
+
// The stages this drawing covers: all of them, or those the chosen shape's chain names.
|
|
246
|
+
const onChain = chosen ? (chosen.chain.match(/[a-z][a-z-]*/g) ?? []).filter((w) => graph.stages.has(w)) : null;
|
|
247
|
+
const covered = new Set(onChain ?? graph.stages);
|
|
145
248
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
249
|
+
if (chosen) {
|
|
250
|
+
console.log(` for: ${chosen.shape}\n`);
|
|
251
|
+
if (onChain.length === 0) console.log(` ${chosen.chain}`);
|
|
252
|
+
else console.log(` ${onChain.map(shownStage).join(arrow)}`);
|
|
253
|
+
const addOns = shapes.filter((r) => r.addOn && r !== chosen);
|
|
254
|
+
if (addOns.length > 0 && onChain.length > 0) {
|
|
255
|
+
console.log(`\n ${dim('and, when the change calls for them')}`);
|
|
256
|
+
const col = Math.min(Math.max(...addOns.map((r) => r.chain.length)), 44) + 4;
|
|
257
|
+
for (const r of addOns) console.log(` ${fit(r.chain, col - 4).padEnd(col)}${dim(fit(r.shape, width - col - 6))}`);
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
console.log('');
|
|
261
|
+
// The line, and where its last stage sends work.
|
|
262
|
+
const plainHead = ` ${shape.line.map(plainStage).join(arrow)}`;
|
|
263
|
+
const head = ` ${shape.line.map(shownStage).join(arrow)}`;
|
|
264
|
+
if (shape.ends.length === 0) console.log(head);
|
|
265
|
+
const lead = ' '.repeat(plainHead.length + 1);
|
|
266
|
+
const joint = (i) => (shape.ends.length === 1 ? '──▶ ' : i === 0 ? '─┬─▶ ' : i === shape.ends.length - 1 ? ' └─▶ ' : ' ├─▶ ');
|
|
267
|
+
const endCol = Math.max(0, ...shape.ends.map((end, i) => `${lead}${joint(i)}${end.path.join(arrow)}`.length)) + 3;
|
|
268
|
+
shape.ends.forEach((end, i) => {
|
|
269
|
+
const plain = `${lead}${joint(i)}${end.path.join(arrow)}`;
|
|
270
|
+
console.log(`${i === 0 ? `${head} ` : lead}${joint(i)}${end.path.map(name).join(arrow)}${' '.repeat(endCol - plain.length)}${dim(fit(end.when, width - endCol))}`);
|
|
271
|
+
});
|
|
157
272
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
273
|
+
if (shape.gates.length > 0) {
|
|
274
|
+
console.log(`\n ${dim('gates on the way, when the diff calls for them')}`);
|
|
275
|
+
const chain = (g) => `${g.from}${arrow}${g.gate}${arrow}${g.to}`;
|
|
276
|
+
const col = Math.max(...shape.gates.map((g) => chain(g).length)) + 4;
|
|
277
|
+
for (const g of shape.gates) {
|
|
278
|
+
console.log(` ${name(g.from)}${arrow}${name(g.gate)}${arrow}${name(g.to)}${' '.repeat(col - chain(g).length)}${dim(fit(g.when, width - col - 6))}`);
|
|
279
|
+
}
|
|
164
280
|
}
|
|
165
281
|
}
|
|
166
282
|
|
|
283
|
+
const drawnMany = [...many].filter(([s]) => covered.has(s));
|
|
284
|
+
if (drawnMany.length > 0) {
|
|
285
|
+
console.log(`\n ${dim('×n runs as several agents at once, each on its own share of the work')}`);
|
|
286
|
+
const col = Math.max(...drawnMany.map(([s]) => s.length)) + 3;
|
|
287
|
+
for (const [s, { when }] of drawnMany) console.log(` ${padName(s, col)}${dim(fit(when, width - col - 8))}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const back = shape.back.filter((e) => covered.has(e.from) && (covered.has(e.to) || TERMINALS.has(e.to)));
|
|
167
291
|
if (back.length > 0) {
|
|
168
292
|
console.log(`\n ${dim('sent back: to whom, on which verdict, and how many rounds per issue')}`);
|
|
169
293
|
const fromCol = Math.max(...back.map((e) => e.from.length)) + 2;
|
|
@@ -180,20 +304,30 @@ export async function pipeline(argv) {
|
|
|
180
304
|
}
|
|
181
305
|
}
|
|
182
306
|
|
|
183
|
-
// Each stage: what it does, on which model and effort, with which skills.
|
|
307
|
+
// Each stage: what it does, on which model and effort, with which skills, and what it did lately.
|
|
184
308
|
const agentsDir = join(dir, '.claude', 'agents');
|
|
185
309
|
const specs = new Map();
|
|
186
310
|
for (const file of existsSync(agentsDir) ? readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort() : []) {
|
|
187
311
|
specs.set(file.replace(/\.md$/, ''), readFileSync(join(agentsDir, file), 'utf8'));
|
|
188
312
|
}
|
|
189
|
-
const
|
|
313
|
+
const since = arg('--since') || new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString().slice(0, 10);
|
|
314
|
+
const runs = storedRuns(dir);
|
|
315
|
+
const history = runs ? historyOf(runs, since) : null;
|
|
316
|
+
const stages = [...graph.stages].filter((s) => covered.has(s));
|
|
190
317
|
const col = Math.max(...stages.map((s) => s.length), 10) + 2;
|
|
191
|
-
console.log(`\n ${dim('stages')}`);
|
|
318
|
+
console.log(`\n ${dim(history ? `stages · what each ran since ${since}, from the measured history` : 'stages · no measured history for this project yet')}`);
|
|
192
319
|
for (const stage of stages) {
|
|
193
320
|
const spec = specs.get(stage) ?? '';
|
|
194
321
|
const front = frontmatter(spec) ?? {};
|
|
195
|
-
const
|
|
196
|
-
console.log(` ${padName(stage, col)}${
|
|
322
|
+
const on = [front.model, front.effort].filter(Boolean).join(' · ') || 'no spec';
|
|
323
|
+
console.log(` ${padName(stage, col)}${on.padEnd(26)}${dim(fit(words.get(stage) ?? '', width - col - 32))}`);
|
|
324
|
+
if (history) {
|
|
325
|
+
const h = history.get(stage);
|
|
326
|
+
const said = !h
|
|
327
|
+
? 'no run'
|
|
328
|
+
: `${h.runs} run(s)${h.read === 0 ? ', no verdict read' : `, sent back ${h.back} of ${h.read} (${Math.round((100 * h.back) / h.read)}%)`}`;
|
|
329
|
+
console.log(` ${' '.repeat(col)}${dim('ran ')}${said}`);
|
|
330
|
+
}
|
|
197
331
|
const skills = [...required(spec).skills].sort();
|
|
198
332
|
if (skills.length > 0) {
|
|
199
333
|
const lines = [];
|
|
@@ -210,22 +344,23 @@ export async function pipeline(argv) {
|
|
|
210
344
|
}
|
|
211
345
|
}
|
|
212
346
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
347
|
+
if (!chosen) {
|
|
348
|
+
// An agent the project wrote for itself is not a stage, and the graph does not route to it.
|
|
349
|
+
const own = [...specs.keys()].filter((role) => !graph.stages.has(role));
|
|
350
|
+
if (own.length > 0) {
|
|
351
|
+
console.log(`\n ${dim("the project's own agents, outside the pipeline")}`);
|
|
352
|
+
for (const role of own) {
|
|
353
|
+
const front = frontmatter(specs.get(role)) ?? {};
|
|
354
|
+
console.log(` ${role}${' '.repeat(Math.max(col - role.length, 2))}${dim(fit(front.description ?? '', width - col - 6))}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (shapes.length > 0) {
|
|
358
|
+
console.log(`\n ${dim('which chain a task takes, by its shape (CLAUDE.md) — `nina pipeline --for <number or words>` draws one')}`);
|
|
359
|
+
const shapeCol = Math.min(Math.max(...shapes.map((r) => r.shape.length)), 56) + 2;
|
|
360
|
+
for (const [i, r] of shapes.entries()) {
|
|
361
|
+
console.log(` ${String(i + 1).padStart(2)} ${fit(r.shape, shapeCol - 2).padEnd(shapeCol)}${fit(r.chain, width - shapeCol - 10)}`);
|
|
362
|
+
}
|
|
220
363
|
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const claude = join(dir, 'CLAUDE.md');
|
|
224
|
-
const shapes = existsSync(claude) ? chainsByShape(readFileSync(claude, 'utf8')) : [];
|
|
225
|
-
if (shapes.length > 0) {
|
|
226
|
-
console.log(`\n ${dim('which chain a task takes, by its shape (CLAUDE.md)')}`);
|
|
227
|
-
const shapeCol = Math.min(Math.max(...shapes.map((r) => r.shape.length)), 58) + 2;
|
|
228
|
-
for (const r of shapes) console.log(` ${fit(r.shape, shapeCol - 2).padEnd(shapeCol)}${fit(r.chain, width - shapeCol - 6)}`);
|
|
229
364
|
}
|
|
230
365
|
console.log('');
|
|
231
366
|
return 0;
|
package/src/graph.mjs
CHANGED
|
@@ -24,6 +24,13 @@ const STAGE = /^- `([a-z][a-z-]*)` — /;
|
|
|
24
24
|
*/
|
|
25
25
|
const EDGE = /^- `([a-z][a-z-]*)` → `([a-z][a-z-]*)` on `([A-Z][A-Z-]*)`(?: — (.*?))?(?: · max (\d+))?\s*$/;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A line under "## Concurrency" naming a stage that may run as several agents at once, each on a share of
|
|
29
|
+
* the work: "- `name` × many — when". Declared rather than left to the prose, so a drawing of the pipeline
|
|
30
|
+
* can show it and `check` can hold it to the stages.
|
|
31
|
+
*/
|
|
32
|
+
const MANY = /^- `([a-z][a-z-]*)` × many — (.*)$/;
|
|
33
|
+
|
|
27
34
|
/** Where work may end: finished, or handed to the owner. Neither needs a spec. */
|
|
28
35
|
export const TERMINALS = new Set(['done', 'human']);
|
|
29
36
|
|
|
@@ -40,6 +47,8 @@ export function parseGraph(text) {
|
|
|
40
47
|
const stages = new Set();
|
|
41
48
|
const edges = [];
|
|
42
49
|
const stray = [];
|
|
50
|
+
/** @type {Map<string, {when: string, line: number}>} */
|
|
51
|
+
const many = new Map();
|
|
43
52
|
const sections = new Set();
|
|
44
53
|
const twice = [];
|
|
45
54
|
let section = '';
|
|
@@ -61,9 +70,12 @@ export function parseGraph(text) {
|
|
|
61
70
|
return;
|
|
62
71
|
}
|
|
63
72
|
edges.push({ from: m[1], to: m[2], token: m[3], when: m[4] ?? '', max: m[5] ? Number(m[5]) : null, line: i + 1 });
|
|
73
|
+
} else if (section === 'concurrency') {
|
|
74
|
+
const m = MANY.exec(line);
|
|
75
|
+
if (m) many.set(m[1], { when: m[2].trim(), line: i + 1 });
|
|
64
76
|
}
|
|
65
77
|
});
|
|
66
|
-
return { stages, edges, stray, sections, twice };
|
|
78
|
+
return { stages, edges, stray, sections, twice, many };
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/**
|
|
@@ -122,6 +134,9 @@ export function validateGraph(graph, specs, roles = new Set()) {
|
|
|
122
134
|
for (const s of graph.twice ?? []) problems.push(`stage \`${s}\` is listed more than once`);
|
|
123
135
|
|
|
124
136
|
for (const s of stray) problems.push(`graph.md:${s.line} is under "## Edges" but is not an edge: ${s.text}`);
|
|
137
|
+
for (const [s, { line }] of graph.many ?? []) {
|
|
138
|
+
if (!stages.has(s)) problems.push(`graph.md:${line} says \`${s}\` runs as several agents, and it is not a stage here`);
|
|
139
|
+
}
|
|
125
140
|
|
|
126
141
|
// Every stage is a spec on disk and every spec on disk is a stage — the profile decides both, so
|
|
127
142
|
// a mismatch means a layer named a stage the profile does not compose, or composed one no edge
|