@zenera/cli 1.1.10 → 1.1.12
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 +69 -15
- package/dist/banner.js +69 -47
- package/dist/cache.d.ts +98 -0
- package/dist/cache.js +301 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +35 -11
- package/dist/commands/cache.d.ts +7 -0
- package/dist/commands/cache.js +245 -0
- package/dist/commands/check.js +6 -3
- package/dist/commands/index.js +5 -0
- package/dist/commands/init.js +8 -4
- package/dist/commands/key.js +68 -14
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.js +375 -0
- package/dist/commands/models.js +17 -1
- package/dist/commands/run.js +11 -3
- package/dist/commands/sandbox.js +70 -23
- package/dist/engine.js +14 -9
- package/dist/home.d.ts +2 -2
- package/dist/home.js +2 -2
- package/dist/keys.d.ts +22 -0
- package/dist/keys.js +105 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +1 -0
- package/dist/liveness.js +11 -0
- package/dist/main.js +9 -0
- package/dist/podman.js +43 -9
- package/dist/projects.js +10 -2
- package/dist/resolve.d.ts +4 -0
- package/dist/resolve.js +43 -17
- package/dist/scaffold.js +22 -1
- package/dist/term.d.ts +23 -2
- package/dist/term.js +215 -8
- package/dist/tui/app.d.ts +10 -0
- package/dist/tui/app.js +478 -54
- package/dist/tui/theme.d.ts +13 -2
- package/dist/tui/theme.js +26 -13
- package/dist/tui/wrap.d.ts +92 -0
- package/dist/tui/wrap.js +147 -2
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +87 -2
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +200 -55
- package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
- package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +11 -4
- package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
- package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
- package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
- package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
- package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
- package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +4 -4
- package/templates/editor/.vscode/settings.json +4 -2
- package/templates/parts/exa.prompt.md.tmpl +4 -0
- package/templates/parts/exa.spec.md.tmpl +2 -0
- package/templates/project/INSTRUCTIONS.md +8 -0
- package/templates/project/SPECIFICATION.md.tmpl +47 -0
- package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
- package/templates/project/agents.yaml.tmpl +4 -0
- package/templates/project/gitignore +7 -1
- package/templates/project/scripts/_setup.sh +101 -0
- package/templates/project/SPECIFICATION.md +0 -6
package/dist/tui/theme.d.ts
CHANGED
|
@@ -13,8 +13,19 @@ export interface Theme {
|
|
|
13
13
|
readonly accent: string;
|
|
14
14
|
/** Read-only badge, busy label. */
|
|
15
15
|
readonly warn: string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Reasoning. It is not a tool call and it is not the answer, and drawn in
|
|
18
|
+
* the same dim foreground as the rows around it there was nothing to say
|
|
19
|
+
* which — a wall of one weight reads as one thing. Its own hue is what
|
|
20
|
+
* separates the model's thinking from the work it went on to do.
|
|
21
|
+
*/
|
|
22
|
+
readonly thinking: LineStyle;
|
|
23
|
+
/**
|
|
24
|
+
* Branch colours, cycled in order of first sight. A fan-out is the one
|
|
25
|
+
* place where colour carries information rather than decoration: eight
|
|
26
|
+
* branches reporting at once are only separable if they are told apart.
|
|
27
|
+
*/
|
|
28
|
+
readonly lanes: readonly string[];
|
|
18
29
|
}
|
|
19
30
|
export declare const THEMES: Record<Appearance, Theme>;
|
|
20
31
|
export type ThemeChoice = Appearance | 'auto';
|
package/dist/tui/theme.js
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
// A terminal already has a colour scheme, and it is not ours to replace. The
|
|
5
5
|
// rule here is to name as few colours as possible and to name them by role:
|
|
6
6
|
// the answer is drawn in the terminal's *own* foreground (no colour at all),
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// and only the few things that must stand out — the person's own turn, the
|
|
8
|
+
// agent name, what it is reasoning about, a warning, an error — take a colour.
|
|
9
9
|
//
|
|
10
10
|
// That alone fixes most of it. `white` was the bug: it is legible on exactly
|
|
11
|
-
// one kind of background, and half the world runs the other kind.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// one kind of background, and half the world runs the other kind. `gray` was
|
|
12
|
+
// the same bug wearing a hat — bright black, dimmed again, is a step from
|
|
13
|
+
// unreadable on dark and invisible on paper. What is left is the handful of
|
|
14
|
+
// accents that ANSI *does* let a light theme get wrong, so those swap.
|
|
15
|
+
//
|
|
16
|
+
// Dim is spent on *chrome* — box rules, clocks, the footer's numbers — and no
|
|
17
|
+
// longer on content. Tool rows are most of what is on screen, and dimming them
|
|
18
|
+
// put the bulk of the transcript one step from unreadable: the whole thing read
|
|
19
|
+
// as too dark. They are content, so they get the foreground like content does.
|
|
14
20
|
//
|
|
15
21
|
// Note what is *not* here: no hex, no 256-colour ramps, no attempt at a brand.
|
|
16
22
|
// A palette that ignores the user's scheme is worse on both schemes than one
|
|
@@ -21,28 +27,35 @@ const DARK = {
|
|
|
21
27
|
line: {
|
|
22
28
|
you: { color: 'cyan' },
|
|
23
29
|
agent: {},
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
// Never dim, and never `gray`: bright black is already the faintest
|
|
31
|
+
// colour a terminal has, and either one puts the bulk of the
|
|
32
|
+
// transcript a step from unreadable.
|
|
33
|
+
tool: {},
|
|
34
|
+
note: { color: 'cyan' },
|
|
26
35
|
error: { color: 'red' },
|
|
27
36
|
},
|
|
28
37
|
accent: 'cyan',
|
|
29
38
|
warn: 'yellow',
|
|
30
|
-
|
|
39
|
+
thinking: { color: 'magenta' },
|
|
40
|
+
lanes: ['cyan', 'green', 'yellow', 'blue', 'red', 'magenta'],
|
|
31
41
|
};
|
|
32
|
-
// On a light background `
|
|
33
|
-
//
|
|
34
|
-
//
|
|
42
|
+
// On a light background `cyan` and `yellow` are barely darker than the paper.
|
|
43
|
+
// Dimmed default foreground and `blue`/`magenta` are the same information,
|
|
44
|
+
// still legible.
|
|
35
45
|
const LIGHT = {
|
|
36
46
|
appearance: 'light',
|
|
37
47
|
line: {
|
|
38
48
|
you: { color: 'blue' },
|
|
39
49
|
agent: {},
|
|
40
|
-
tool: {
|
|
41
|
-
note: { color: 'blue'
|
|
50
|
+
tool: {},
|
|
51
|
+
note: { color: 'blue' },
|
|
42
52
|
error: { color: 'red' },
|
|
43
53
|
},
|
|
44
54
|
accent: 'blue',
|
|
45
55
|
warn: 'magenta',
|
|
56
|
+
thinking: { color: 'magenta' },
|
|
57
|
+
// No cyan or yellow: on paper they are barely darker than the paper.
|
|
58
|
+
lanes: ['blue', 'green', 'red', 'magenta'],
|
|
46
59
|
};
|
|
47
60
|
export const THEMES = { dark: DARK, light: LIGHT };
|
|
48
61
|
export function parseChoice(value) {
|
package/dist/tui/wrap.d.ts
CHANGED
|
@@ -7,6 +7,98 @@
|
|
|
7
7
|
* and bounds the work per keystroke on a stream that never stops growing.
|
|
8
8
|
*/
|
|
9
9
|
export declare function windowOf(text: string, width: number, rows: number): string[];
|
|
10
|
+
/**
|
|
11
|
+
* `text` flattened onto one row of at most `width` columns, ellipsized when it
|
|
12
|
+
* did not fit. Newlines and runs of space collapse: a row is a row, and a tool
|
|
13
|
+
* argument or a result preview arrives with whatever shape it happened to have.
|
|
14
|
+
*/
|
|
15
|
+
export declare function clip(text: string, width: number): string;
|
|
10
16
|
/** Word wrap. Every returned row is at most `width` columns wide. */
|
|
11
17
|
export declare function wrap(text: string, width: number): string[];
|
|
18
|
+
/** The three footer rows, its margin, the prompt, and a row in hand. */
|
|
19
|
+
export declare const CHROME_ROWS = 7;
|
|
20
|
+
/** How much of the reasoning stream is worth showing. It is a progress bar. */
|
|
21
|
+
export declare const THINKING_ROWS = 6;
|
|
22
|
+
/** How much of the frame the work in flight may take. A branch is a box now,
|
|
23
|
+
* not a row, so a fan-out of two costs fourteen of these. */
|
|
24
|
+
export declare const ACTIVITY_ROWS = 16;
|
|
25
|
+
/** The rule that opens a reasoning block and the one that closes it. */
|
|
26
|
+
export declare const THINKING_CHROME = 2;
|
|
27
|
+
export interface Budget {
|
|
28
|
+
/** rows for the list of what is in flight */
|
|
29
|
+
activity: number;
|
|
30
|
+
/** rows for the tail of the reasoning stream */
|
|
31
|
+
thinking: number;
|
|
32
|
+
/** rows for the answer as it arrives */
|
|
33
|
+
live: number;
|
|
34
|
+
/**
|
|
35
|
+
* What the three of them occupy together, rules included — a constant for
|
|
36
|
+
* a given terminal, whatever is happening inside it. The region grows to
|
|
37
|
+
* this and stops, and never shrinks back while the turn runs, which is what
|
|
38
|
+
* keeps the footer on one row instead of riding up and down on the
|
|
39
|
+
* reasoning block.
|
|
40
|
+
*/
|
|
41
|
+
total: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* How many rows each repainting block may draw in a terminal `rows` tall.
|
|
45
|
+
*
|
|
46
|
+
* Activity is capped first and never takes the last two rows: the answer as it
|
|
47
|
+
* arrives matters more than the machinery producing it. Reasoning yields to it
|
|
48
|
+
* in turn, because it is a progress indicator and the answer is the point.
|
|
49
|
+
*
|
|
50
|
+
* `thinking` is how many rows of reasoning are wanted, counting text only: none
|
|
51
|
+
* when there is no reasoning, fewer than `THINKING_ROWS` once the stream has
|
|
52
|
+
* settled and its tail is history rather than progress. The two rules are
|
|
53
|
+
* charged on top, so a block too cramped to be worth boxing is not drawn at all
|
|
54
|
+
* rather than drawn as a border with a line in it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function budgetOf(rows: number, activity: number, thinking: number): Budget;
|
|
57
|
+
/** The rows a branch box may spend on its own calls and its reasoning. */
|
|
58
|
+
export declare const BRANCH_ROWS = 5;
|
|
59
|
+
/** Rows a branch box spends on chrome: the title rule and the closing one. */
|
|
60
|
+
export declare const BOX_CHROME = 2;
|
|
61
|
+
/**
|
|
62
|
+
* How many call rows each of `count` branch boxes may draw, given the rows the
|
|
63
|
+
* activity region has to divide between them.
|
|
64
|
+
*
|
|
65
|
+
* Every branch gets the same number, because they are the same kind of thing
|
|
66
|
+
* and a fan-out is read across, not down. A wide fork spends its rows on being
|
|
67
|
+
* complete rather than on being detailed: eight branches showing one call each
|
|
68
|
+
* is a picture of the fork, eight rows of one branch is not.
|
|
69
|
+
*/
|
|
70
|
+
export declare function branchRows(count: number, allowance: number): number;
|
|
71
|
+
/**
|
|
72
|
+
* How wide the answer is drawn. A line of prose spanning a 200-column terminal
|
|
73
|
+
* is measurably harder to read than one that stops, which is why every demo in
|
|
74
|
+
* `examples/` puts its answer in a box of bounded width — the terminal is the
|
|
75
|
+
* page, not the paragraph.
|
|
76
|
+
*/
|
|
77
|
+
export declare function answerWidth(columns: number): number;
|
|
78
|
+
/**
|
|
79
|
+
* A tool payload as a person would read it, rather than as it was serialised.
|
|
80
|
+
*
|
|
81
|
+
* A lone field is printed as its bare value, because the name of a generic tool
|
|
82
|
+
* says almost nothing on its own — `run_command` is every shell command there
|
|
83
|
+
* is, and the argument is the part that identifies THIS call.
|
|
84
|
+
*
|
|
85
|
+
* It scans rather than parses: previews are cut to a length, so the JSON very
|
|
86
|
+
* often does not close, and precisely the calls worth reading are the long ones
|
|
87
|
+
* that got cut.
|
|
88
|
+
*/
|
|
89
|
+
export declare function readable(preview: string): string;
|
|
90
|
+
/** A run of lines from an answer, and whether it was fenced as code. */
|
|
91
|
+
export interface Segment {
|
|
92
|
+
code: boolean;
|
|
93
|
+
/** the fence's info string, when it had one */
|
|
94
|
+
title?: string;
|
|
95
|
+
lines: string[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Splits an answer on ``` fences. Prose is left exactly as it was — the common
|
|
99
|
+
* answer has no fence in it and comes back in one piece — but a fenced block is
|
|
100
|
+
* the one thing a terminal must not reflow: its indentation is its meaning, and
|
|
101
|
+
* wrapping it as prose destroys it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function segmentsOf(text: string): Segment[];
|
|
12
104
|
//# sourceMappingURL=wrap.d.ts.map
|
package/dist/tui/wrap.js
CHANGED
|
@@ -23,8 +23,23 @@
|
|
|
23
23
|
export function windowOf(text, width, rows) {
|
|
24
24
|
const w = Math.max(8, width);
|
|
25
25
|
const n = Math.max(1, rows);
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Trailing blank rows are never information, and a stream that ends on a
|
|
27
|
+
// paragraph break would spend one of the few rows it has on nothing.
|
|
28
|
+
const tail = text
|
|
29
|
+
.slice(-w * n * 2)
|
|
30
|
+
.replace(/\n{2,}/g, '\n')
|
|
31
|
+
.trimEnd();
|
|
32
|
+
return wrap(tail, w).slice(-n);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `text` flattened onto one row of at most `width` columns, ellipsized when it
|
|
36
|
+
* did not fit. Newlines and runs of space collapse: a row is a row, and a tool
|
|
37
|
+
* argument or a result preview arrives with whatever shape it happened to have.
|
|
38
|
+
*/
|
|
39
|
+
export function clip(text, width) {
|
|
40
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
41
|
+
const w = Math.max(1, width);
|
|
42
|
+
return flat.length <= w ? flat : `${flat.slice(0, w - 1)}…`;
|
|
28
43
|
}
|
|
29
44
|
/** Word wrap. Every returned row is at most `width` columns wide. */
|
|
30
45
|
export function wrap(text, width) {
|
|
@@ -59,4 +74,134 @@ export function wrap(text, width) {
|
|
|
59
74
|
}
|
|
60
75
|
return out;
|
|
61
76
|
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Dividing the frame
|
|
79
|
+
//
|
|
80
|
+
// The same invariant, stated as arithmetic: every repainting block gets its
|
|
81
|
+
// rows from here, and the sum is never more than the viewport has to give.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
/** The three footer rows, its margin, the prompt, and a row in hand. */
|
|
84
|
+
export const CHROME_ROWS = 7;
|
|
85
|
+
/** How much of the reasoning stream is worth showing. It is a progress bar. */
|
|
86
|
+
export const THINKING_ROWS = 6;
|
|
87
|
+
/** How much of the frame the work in flight may take. A branch is a box now,
|
|
88
|
+
* not a row, so a fan-out of two costs fourteen of these. */
|
|
89
|
+
export const ACTIVITY_ROWS = 16;
|
|
90
|
+
/** The rule that opens a reasoning block and the one that closes it. */
|
|
91
|
+
export const THINKING_CHROME = 2;
|
|
92
|
+
/**
|
|
93
|
+
* How many rows each repainting block may draw in a terminal `rows` tall.
|
|
94
|
+
*
|
|
95
|
+
* Activity is capped first and never takes the last two rows: the answer as it
|
|
96
|
+
* arrives matters more than the machinery producing it. Reasoning yields to it
|
|
97
|
+
* in turn, because it is a progress indicator and the answer is the point.
|
|
98
|
+
*
|
|
99
|
+
* `thinking` is how many rows of reasoning are wanted, counting text only: none
|
|
100
|
+
* when there is no reasoning, fewer than `THINKING_ROWS` once the stream has
|
|
101
|
+
* settled and its tail is history rather than progress. The two rules are
|
|
102
|
+
* charged on top, so a block too cramped to be worth boxing is not drawn at all
|
|
103
|
+
* rather than drawn as a border with a line in it.
|
|
104
|
+
*/
|
|
105
|
+
export function budgetOf(rows, activity, thinking) {
|
|
106
|
+
const total = Math.max(2, rows - CHROME_ROWS);
|
|
107
|
+
const shown = Math.min(Math.max(0, activity), ACTIVITY_ROWS, Math.max(0, total - 2));
|
|
108
|
+
const rest = total - shown;
|
|
109
|
+
const room = rest - 1 - THINKING_CHROME;
|
|
110
|
+
const tail = room >= 1 ? Math.min(Math.max(0, thinking), THINKING_ROWS, room) : 0;
|
|
111
|
+
return {
|
|
112
|
+
activity: shown,
|
|
113
|
+
thinking: tail,
|
|
114
|
+
live: rest - tail - (tail ? THINKING_CHROME : 0),
|
|
115
|
+
total,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** The rows a branch box may spend on its own calls and its reasoning. */
|
|
119
|
+
export const BRANCH_ROWS = 5;
|
|
120
|
+
/** Rows a branch box spends on chrome: the title rule and the closing one. */
|
|
121
|
+
export const BOX_CHROME = 2;
|
|
122
|
+
/**
|
|
123
|
+
* How many call rows each of `count` branch boxes may draw, given the rows the
|
|
124
|
+
* activity region has to divide between them.
|
|
125
|
+
*
|
|
126
|
+
* Every branch gets the same number, because they are the same kind of thing
|
|
127
|
+
* and a fan-out is read across, not down. A wide fork spends its rows on being
|
|
128
|
+
* complete rather than on being detailed: eight branches showing one call each
|
|
129
|
+
* is a picture of the fork, eight rows of one branch is not.
|
|
130
|
+
*/
|
|
131
|
+
export function branchRows(count, allowance) {
|
|
132
|
+
if (count <= 0) {
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
const each = Math.floor(Math.max(0, allowance) / count) - BOX_CHROME;
|
|
136
|
+
return Math.max(1, Math.min(BRANCH_ROWS, each));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* How wide the answer is drawn. A line of prose spanning a 200-column terminal
|
|
140
|
+
* is measurably harder to read than one that stops, which is why every demo in
|
|
141
|
+
* `examples/` puts its answer in a box of bounded width — the terminal is the
|
|
142
|
+
* page, not the paragraph.
|
|
143
|
+
*/
|
|
144
|
+
export function answerWidth(columns) {
|
|
145
|
+
return Math.max(24, Math.min(columns - 4, 96));
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Reading a payload
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
/** One field of a preview: a JSON string, or a bare token when it was cut. */
|
|
151
|
+
const FIELD = /"([A-Za-z_][\w-]*)"\s*:\s*("(?:[^"\\]|\\.)*"?|[^,}\s]+)/g;
|
|
152
|
+
/**
|
|
153
|
+
* A tool payload as a person would read it, rather than as it was serialised.
|
|
154
|
+
*
|
|
155
|
+
* A lone field is printed as its bare value, because the name of a generic tool
|
|
156
|
+
* says almost nothing on its own — `run_command` is every shell command there
|
|
157
|
+
* is, and the argument is the part that identifies THIS call.
|
|
158
|
+
*
|
|
159
|
+
* It scans rather than parses: previews are cut to a length, so the JSON very
|
|
160
|
+
* often does not close, and precisely the calls worth reading are the long ones
|
|
161
|
+
* that got cut.
|
|
162
|
+
*/
|
|
163
|
+
export function readable(preview) {
|
|
164
|
+
const found = [...preview.matchAll(FIELD)];
|
|
165
|
+
if (!found.length) {
|
|
166
|
+
return preview.trim();
|
|
167
|
+
}
|
|
168
|
+
const fields = found.map((m) => [m[1], unquote(m[2])]);
|
|
169
|
+
const one = fields.length === 1 ? fields[0] : undefined;
|
|
170
|
+
return one ? one[1] : fields.map(([k, v]) => `${k}=${v}`).join(' ');
|
|
171
|
+
}
|
|
172
|
+
function unquote(raw) {
|
|
173
|
+
if (!raw.startsWith('"')) {
|
|
174
|
+
return raw;
|
|
175
|
+
}
|
|
176
|
+
const body = raw.length > 1 && raw.endsWith('"') ? raw.slice(1, -1) : raw.slice(1);
|
|
177
|
+
return body.replace(/\\[nrt]/g, ' ').replace(/\\(["\\/])/g, '$1');
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Splits an answer on ``` fences. Prose is left exactly as it was — the common
|
|
181
|
+
* answer has no fence in it and comes back in one piece — but a fenced block is
|
|
182
|
+
* the one thing a terminal must not reflow: its indentation is its meaning, and
|
|
183
|
+
* wrapping it as prose destroys it.
|
|
184
|
+
*/
|
|
185
|
+
export function segmentsOf(text) {
|
|
186
|
+
const out = [];
|
|
187
|
+
let current = { code: false, lines: [] };
|
|
188
|
+
const flush = () => {
|
|
189
|
+
if (current.lines.length) {
|
|
190
|
+
out.push(current);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
for (const raw of text.split('\n')) {
|
|
194
|
+
const fence = /^\s*```+\s*(\S*)/.exec(raw);
|
|
195
|
+
if (!fence) {
|
|
196
|
+
current.lines.push(raw);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
flush();
|
|
200
|
+
current = current.code
|
|
201
|
+
? { code: false, lines: [] }
|
|
202
|
+
: { code: true, title: fence[1] || undefined, lines: [] };
|
|
203
|
+
}
|
|
204
|
+
flush();
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
62
207
|
//# sourceMappingURL=wrap.js.map
|
package/dist/validate.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface SkillReport {
|
|
|
73
73
|
export interface ModelReport {
|
|
74
74
|
/** the alias it is declared under, or the reference itself */
|
|
75
75
|
name: string;
|
|
76
|
+
/** what `name` resolves to, when it is an alias rather than a reference */
|
|
77
|
+
ref?: string;
|
|
76
78
|
/** what the config declared it for */
|
|
77
79
|
role: DeclaredRole;
|
|
78
80
|
/** the provider it resolves to */
|
package/dist/validate.js
CHANGED
|
@@ -181,6 +181,7 @@ export async function validateProject(opts) {
|
|
|
181
181
|
for (const spec of config.agents) {
|
|
182
182
|
agents.push(checkAgent(root, config, spec, entry, available, record, add));
|
|
183
183
|
}
|
|
184
|
+
checkReturnPaths(config, add);
|
|
184
185
|
// -----------------------------------------------------------------------
|
|
185
186
|
// Skills
|
|
186
187
|
// -----------------------------------------------------------------------
|
|
@@ -244,7 +245,7 @@ async function askModels(targets, opts, add) {
|
|
|
244
245
|
if (probe.check.state === 'live') {
|
|
245
246
|
return;
|
|
246
247
|
}
|
|
247
|
-
const where =
|
|
248
|
+
const where = named(report);
|
|
248
249
|
if (probe.check.state === 'blocked') {
|
|
249
250
|
const pick = report.role === 'embedding' ? '--embedding' : '--chat';
|
|
250
251
|
add({
|
|
@@ -576,6 +577,75 @@ function checkAgent(root, config, spec, entry, available, record, add) {
|
|
|
576
577
|
ownSandbox: Boolean(spec.sandbox),
|
|
577
578
|
};
|
|
578
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Every hand-off must be returnable.
|
|
582
|
+
*
|
|
583
|
+
* A hand-off is a one-way door: control moves to the other agent and stays
|
|
584
|
+
* there. Nothing hands it back on its own, so an agent reached by an edge that
|
|
585
|
+
* has no way home owns the conversation for the rest of the session — the next
|
|
586
|
+
* question, whatever it is about, is answered by the specialist the router sent
|
|
587
|
+
* the user to. That is not a runtime error; it is a system that quietly stops
|
|
588
|
+
* being the system that was drawn.
|
|
589
|
+
*
|
|
590
|
+
* So for every `A → B` there must be a path from B back to A. Indirect counts:
|
|
591
|
+
* `B → C → A` is a way home, which is the same as saying every hand-off edge
|
|
592
|
+
* lies on a cycle.
|
|
593
|
+
*
|
|
594
|
+
* Forks are exempt, and always will be. A branch runs, answers, and control
|
|
595
|
+
* returns to the agent that forked it — the return is the mechanism, not an
|
|
596
|
+
* edge someone has to remember to declare. An agent that wants an answer rather
|
|
597
|
+
* than to give up the conversation should fork, not hand off.
|
|
598
|
+
*
|
|
599
|
+
* A warning rather than an error: the project loads, runs, and answers. What it
|
|
600
|
+
* cannot do is come back. `--strict` is for the repository that wants that to
|
|
601
|
+
* fail the build.
|
|
602
|
+
*/
|
|
603
|
+
function checkReturnPaths(config, add) {
|
|
604
|
+
const known = new Set(config.agents.map((a) => a.name));
|
|
605
|
+
// Unknown names and self-hand-offs are already errors of their own; walking
|
|
606
|
+
// them here would only report the same mistake in a second vocabulary.
|
|
607
|
+
const edges = new Map(config.agents.map((a) => [
|
|
608
|
+
a.name,
|
|
609
|
+
(a.handoffs ?? []).filter((to) => to !== a.name && known.has(to)),
|
|
610
|
+
]));
|
|
611
|
+
const reaches = (from, to) => {
|
|
612
|
+
const seen = new Set([from]);
|
|
613
|
+
const queue = [from];
|
|
614
|
+
while (queue.length) {
|
|
615
|
+
for (const next of edges.get(queue.shift()) ?? []) {
|
|
616
|
+
if (next === to) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
if (!seen.has(next)) {
|
|
620
|
+
seen.add(next);
|
|
621
|
+
queue.push(next);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return false;
|
|
626
|
+
};
|
|
627
|
+
for (const [from, targets] of edges) {
|
|
628
|
+
for (const to of targets) {
|
|
629
|
+
if (reaches(to, from)) {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
add({
|
|
633
|
+
severity: 'warning',
|
|
634
|
+
code: 'handoff.one-way',
|
|
635
|
+
where: `agents.${from}.handoffs`,
|
|
636
|
+
message: `"${to}" has no way back to "${from}": nothing it can hand off to, ` +
|
|
637
|
+
`directly or through another agent, reaches "${from}" again. A hand-off ` +
|
|
638
|
+
`does not return by itself, so once control moves to "${to}" it stays ` +
|
|
639
|
+
`there — every later question in the session is answered by "${to}", ` +
|
|
640
|
+
'whether or not it is that agent’s job',
|
|
641
|
+
fix: `add "${from}" to agents.${to}.handoffs (or to the handoffs of an agent ` +
|
|
642
|
+
`"${to}" can reach), or — if "${from}" wants an answer rather than to give ` +
|
|
643
|
+
`up the conversation — drop the edge and declare fork: { agents: [${to}] } ` +
|
|
644
|
+
`on "${from}", because a branch returns on its own`,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
579
649
|
async function checkSkills(root, config, available, record, add) {
|
|
580
650
|
const declared = config.skills
|
|
581
651
|
? Array.isArray(config.skills)
|
|
@@ -1183,6 +1253,15 @@ function checkModels(root, config, keys, add, probe = false) {
|
|
|
1183
1253
|
if (keys) {
|
|
1184
1254
|
report.credential = held.present ? 'present' : 'missing';
|
|
1185
1255
|
}
|
|
1256
|
+
// An alias is the one name the provider has never heard of, so the
|
|
1257
|
+
// report says what it stands for — otherwise `main was refused`
|
|
1258
|
+
// names nothing anyone can look up or retype.
|
|
1259
|
+
const spec = typeof ref === 'string' ? registry.parse(ref) : ref;
|
|
1260
|
+
const api = 'api' in spec && spec.api ? `/${spec.api}` : '';
|
|
1261
|
+
const resolved = `${need.provider}${api}:${spec.model}`;
|
|
1262
|
+
if (resolved !== name) {
|
|
1263
|
+
report.ref = resolved;
|
|
1264
|
+
}
|
|
1186
1265
|
}
|
|
1187
1266
|
catch (err) {
|
|
1188
1267
|
add({
|
|
@@ -1208,7 +1287,7 @@ function checkModels(root, config, keys, add, probe = false) {
|
|
|
1208
1287
|
add({
|
|
1209
1288
|
severity: 'warning',
|
|
1210
1289
|
code: `credential.${issue.reason}`,
|
|
1211
|
-
where:
|
|
1290
|
+
where: named(report, issue.provider),
|
|
1212
1291
|
message: issue.reason === 'missing'
|
|
1213
1292
|
? `nothing to authenticate with — ${issue.env} is not set, and the ` +
|
|
1214
1293
|
'keyring holds no key for this provider'
|
|
@@ -1257,6 +1336,12 @@ function checkModels(root, config, keys, add, probe = false) {
|
|
|
1257
1336
|
}
|
|
1258
1337
|
return { providers: registry.names(), models, targets };
|
|
1259
1338
|
}
|
|
1339
|
+
/** How a finding names a model: as written, and as resolved when the two differ. */
|
|
1340
|
+
function named(report, provider = report.provider) {
|
|
1341
|
+
return (`${report.role} "${report.name}"` +
|
|
1342
|
+
(report.ref ? ` = ${report.ref}` : '') +
|
|
1343
|
+
(provider ? ` (${provider})` : ''));
|
|
1344
|
+
}
|
|
1260
1345
|
/** The config key a bad reference was written under. */
|
|
1261
1346
|
function whereFor(config, role, name, usedBy) {
|
|
1262
1347
|
if (role === 'embedding') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@inkjs/ui": "^2.0.0",
|
|
52
52
|
"ink": "^7.1.1",
|
|
53
53
|
"react": "^19.2.8",
|
|
54
|
-
"@zenera/neo": "^1.1.
|
|
54
|
+
"@zenera/neo": "^1.1.12",
|
|
55
55
|
"@anthropic-ai/sdk": "^0.120.0",
|
|
56
56
|
"@google/genai": "^2.18.0",
|
|
57
57
|
"@openrouter/sdk": "^1.2.80",
|