@zenera/cli 1.1.24 → 1.1.26
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 +65 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +31 -1
- package/dist/catalog.js +140 -7
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +52 -2
- package/dist/commands/inspect.js +220 -31
- package/dist/commands/memory.js +156 -32
- package/dist/commands/meta.js +333 -24
- package/dist/commands/models.js +97 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +148 -16
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +8 -1
- package/dist/engine.js +7 -0
- package/dist/eventlog.d.ts +64 -0
- package/dist/eventlog.js +170 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +35 -3
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +43 -0
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -0
- package/dist/meta/finetune/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +516 -0
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +127 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +57 -0
- package/dist/meta/finetune/feedback.js +104 -0
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +98 -0
- package/dist/meta/finetune/improvements.js +501 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +84 -0
- package/dist/meta/finetune/live.js +272 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +163 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +40 -0
- package/dist/meta/finetune/report.js +1397 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +47 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +43 -0
- package/dist/meta/finetune/train.js +425 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +316 -0
- package/dist/meta/finetune/tuning.d.ts +193 -0
- package/dist/meta/finetune/tuning.js +308 -0
- package/dist/meta/finetune/usage.d.ts +66 -0
- package/dist/meta/finetune/usage.js +260 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +15 -0
- package/dist/meta/host.js +21 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/meta.d.ts +71 -1
- package/dist/meta.js +303 -6
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -7
- package/dist/scaffold.js +24 -67
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +107 -7
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +35 -0
- package/dist/tui/models.js +447 -0
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +4 -2
- package/templates/editor/.github/copilot-instructions.md +60 -145
- package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -494
- package/templates/project/agents/tools-instructions.md +14 -4
- package/templates/project/gitignore +2 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { writeApplyBrief } from "./apply-brief.js";
|
|
5
|
+
import { costChanges, limits, measure, overLimits } from "./cost.js";
|
|
6
|
+
import { errorIn, sizeOf } from "./failure.js";
|
|
7
|
+
import { forProse } from "./feedback.js";
|
|
8
|
+
import { patchFiles, writeApply } from "./report.js";
|
|
9
|
+
import { readJson, Stopped } from "./tuning.js";
|
|
10
|
+
import { applyTokens, spent } from "./usage.js";
|
|
11
|
+
const pad = (n) => String(n).padStart(3, '0');
|
|
12
|
+
const kept = (d) => d?.decision === 'applied' || d?.decision === 'merged';
|
|
13
|
+
const DECIDED = new Set(['applied', 'merged', 'rejected', 'deferred', 'structural']);
|
|
14
|
+
/** `<request> <improvement>` for each improvement of inputs.json that decisions.json gives no valid decision. */
|
|
15
|
+
export function undecided(dir) {
|
|
16
|
+
const inputs = readJson(join(dir, 'inputs.json')) ?? [];
|
|
17
|
+
const decided = readJson(join(dir, 'decisions.json')) ?? {};
|
|
18
|
+
return inputs.flatMap((f) => forProse(f)
|
|
19
|
+
.filter((i) => !DECIDED.has(decided[f.id]?.[i.id]?.decision ?? ''))
|
|
20
|
+
.map((i) => `${f.id} ${i.id}`));
|
|
21
|
+
}
|
|
22
|
+
export class Improvements {
|
|
23
|
+
#t;
|
|
24
|
+
pending = [];
|
|
25
|
+
/** the batch an apply is working on */
|
|
26
|
+
inApply = [];
|
|
27
|
+
#applied = new Set();
|
|
28
|
+
/** cases parked right now */
|
|
29
|
+
parked = 0;
|
|
30
|
+
/** `zen run`s in flight */
|
|
31
|
+
runs = 0;
|
|
32
|
+
applying = false;
|
|
33
|
+
/** the apply under way; `name` once it has a folder, before that it waits for runs */
|
|
34
|
+
current;
|
|
35
|
+
/** applies started in this process */
|
|
36
|
+
started = 0;
|
|
37
|
+
/** applies finished in this process - an analysis spanning one may see edits */
|
|
38
|
+
count = 0;
|
|
39
|
+
constructor(t) {
|
|
40
|
+
this.#t = t;
|
|
41
|
+
this.reload();
|
|
42
|
+
}
|
|
43
|
+
/** Re-reads which requests the applies took - a recovery may have removed one. */
|
|
44
|
+
reload() {
|
|
45
|
+
this.#applied.clear();
|
|
46
|
+
for (const name of this.#names()) {
|
|
47
|
+
for (const id of readJson(join(this.dir, name, 'applied.json'))?.ids ??
|
|
48
|
+
[]) {
|
|
49
|
+
this.#applied.add(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
get dir() {
|
|
54
|
+
return join(this.#t.dir, 'applies');
|
|
55
|
+
}
|
|
56
|
+
#names() {
|
|
57
|
+
try {
|
|
58
|
+
return readdirSync(this.dir)
|
|
59
|
+
.filter((n) => /^\d{3}$/.test(n))
|
|
60
|
+
.sort();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
isApplied(id) {
|
|
67
|
+
return this.#applied.has(id);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Every apply's decisions, later ones over earlier, per improvement. An edit
|
|
71
|
+
* that was undone kept nothing, so what it applied or merged reads `discarded`.
|
|
72
|
+
*/
|
|
73
|
+
decisions() {
|
|
74
|
+
const all = {};
|
|
75
|
+
for (const a of this.applies()) {
|
|
76
|
+
const undone = a.applied?.discarded;
|
|
77
|
+
for (const [id, byImprovement] of Object.entries(readJson(join(a.dir, 'decisions.json')) ?? {})) {
|
|
78
|
+
for (const [i, d] of Object.entries(byImprovement ?? {})) {
|
|
79
|
+
(all[id] ??= {})[i] =
|
|
80
|
+
undone && kept(d)
|
|
81
|
+
? {
|
|
82
|
+
...d,
|
|
83
|
+
decision: 'discarded',
|
|
84
|
+
note: `${d.note ?? ''} - undone: ${undone}`.trim(),
|
|
85
|
+
}
|
|
86
|
+
: d;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return all;
|
|
91
|
+
}
|
|
92
|
+
/** Improvements held back for want of a second case, and not taken up since. */
|
|
93
|
+
deferred() {
|
|
94
|
+
const open = new Map();
|
|
95
|
+
for (const a of this.applies()) {
|
|
96
|
+
const inputs = new Map((a.inputs ?? []).map((f) => [f.id, f]));
|
|
97
|
+
const undone = Boolean(a.applied?.discarded);
|
|
98
|
+
for (const [id, byImprovement] of Object.entries(readJson(join(a.dir, 'decisions.json')) ?? {})) {
|
|
99
|
+
for (const [i, d] of Object.entries(byImprovement ?? {})) {
|
|
100
|
+
const key = `${id} ${i}`;
|
|
101
|
+
if (d?.decision === 'deferred') {
|
|
102
|
+
const improvement = inputs.get(id)?.improvements.find((x) => x.id === i) ??
|
|
103
|
+
open.get(key)?.improvement;
|
|
104
|
+
if (improvement) {
|
|
105
|
+
open.set(key, {
|
|
106
|
+
request: id,
|
|
107
|
+
case: inputs.get(id)?.case ?? open.get(key)?.case ?? id.split('@')[0],
|
|
108
|
+
apply: a.name,
|
|
109
|
+
improvement,
|
|
110
|
+
...(d.note ? { note: d.note } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (!undone) {
|
|
115
|
+
open.delete(key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...open.values()];
|
|
121
|
+
}
|
|
122
|
+
/** The skills an apply made or changed for this request: the case's next run should load them. */
|
|
123
|
+
skillsFor(id) {
|
|
124
|
+
const mine = this.decisions()[id] ?? {};
|
|
125
|
+
return [
|
|
126
|
+
...new Set(Object.values(mine)
|
|
127
|
+
.filter((d) => kept(d) && d.skill)
|
|
128
|
+
.map((d) => d.skill)),
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
/** Nothing of this request was applied, merged or deferred, and some of it is structural: prose cannot fix it. */
|
|
132
|
+
#structural(id) {
|
|
133
|
+
const mine = Object.values(this.decisions()[id] ?? {});
|
|
134
|
+
const structural = mine.filter((d) => d.decision === 'structural');
|
|
135
|
+
if (structural.length === 0 ||
|
|
136
|
+
mine.some((d) => kept(d) || d.decision === 'deferred' || d.decision === 'discarded')) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return structural.map((d) => d.note || 'structural').join('; ');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parks until an apply has taken this feedback, the worker free meanwhile. Taken already - after a restart - returns at once.
|
|
143
|
+
* `structural` when the apply found nothing prose can fix.
|
|
144
|
+
*/
|
|
145
|
+
async apply(feedback, s) {
|
|
146
|
+
if (this.#applied.has(feedback.id)) {
|
|
147
|
+
return this.#outcome(feedback.id);
|
|
148
|
+
}
|
|
149
|
+
if (!this.pending.some((f) => f.id === feedback.id)) {
|
|
150
|
+
this.pending.push(feedback);
|
|
151
|
+
}
|
|
152
|
+
this.parked++;
|
|
153
|
+
this.#t.event({
|
|
154
|
+
what: 'parked',
|
|
155
|
+
worker: s.worker?.slot,
|
|
156
|
+
case: feedback.case,
|
|
157
|
+
phase: feedback.phase,
|
|
158
|
+
attempt: feedback.attempt,
|
|
159
|
+
detail: `${forProse(feedback).length} improvement(s), worker freed`,
|
|
160
|
+
});
|
|
161
|
+
this.#t.unseat(s);
|
|
162
|
+
try {
|
|
163
|
+
while (!this.#applied.has(feedback.id)) {
|
|
164
|
+
if (this.#t.stopping) {
|
|
165
|
+
throw new Stopped();
|
|
166
|
+
}
|
|
167
|
+
await this.#t.tick();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
this.parked--;
|
|
172
|
+
}
|
|
173
|
+
const outcome = this.#outcome(feedback.id);
|
|
174
|
+
if (outcome.structural) {
|
|
175
|
+
return outcome;
|
|
176
|
+
}
|
|
177
|
+
await this.#t.seat(s, true);
|
|
178
|
+
return outcome;
|
|
179
|
+
}
|
|
180
|
+
#outcome(id) {
|
|
181
|
+
const structural = this.#structural(id);
|
|
182
|
+
return structural ? { structural } : {};
|
|
183
|
+
}
|
|
184
|
+
/** A recheck's request: queued like a parked case's, with no case waiting on it. */
|
|
185
|
+
regressed(feedback) {
|
|
186
|
+
if (!this.#applied.has(feedback.id) &&
|
|
187
|
+
!this.pending.some((f) => f.id === feedback.id) &&
|
|
188
|
+
!this.inApply.some((f) => f.id === feedback.id)) {
|
|
189
|
+
this.pending.push(feedback);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Before a run starts: wait out an apply, or stop. */
|
|
193
|
+
async notApplying() {
|
|
194
|
+
while (this.applying) {
|
|
195
|
+
this.#t.checkStopping();
|
|
196
|
+
await this.#t.tick();
|
|
197
|
+
}
|
|
198
|
+
this.#t.checkStopping();
|
|
199
|
+
}
|
|
200
|
+
/** The applier: runs until the tuning is finished. */
|
|
201
|
+
async loop() {
|
|
202
|
+
while (!this.#t.finished) {
|
|
203
|
+
await this.#t.tick();
|
|
204
|
+
if (this.#t.stopping) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (this.pending.length === 0) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (this.pending.length < this.#t.config.applyAt &&
|
|
211
|
+
(this.#t.busy() > 0 || !this.#t.drained)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await this.#applyNow();
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
this.#t.fatal = err;
|
|
219
|
+
this.#t.stop(`apply failed: ${err.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** What the agents read when the tuning began - its first version, even for a tuning begun before this was kept. */
|
|
224
|
+
async baseline() {
|
|
225
|
+
const file = join(this.#t.dir, 'baseline.json');
|
|
226
|
+
const saved = readJson(file)?.cost;
|
|
227
|
+
if (saved) {
|
|
228
|
+
return saved;
|
|
229
|
+
}
|
|
230
|
+
const first = this.#t.system.rows()[0];
|
|
231
|
+
const at = first ? this.#t.system.snapshotDir(first.v) : undefined;
|
|
232
|
+
const cost = await measure(at && existsSync(at) ? at : this.#t.root);
|
|
233
|
+
mkdirSync(this.#t.dir, { recursive: true });
|
|
234
|
+
writeFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), version: first?.v ?? 0, cost }, null, 2)}\n`);
|
|
235
|
+
return cost;
|
|
236
|
+
}
|
|
237
|
+
async #applyNow() {
|
|
238
|
+
const t = this.#t;
|
|
239
|
+
this.applying = true;
|
|
240
|
+
this.current = { since: Date.now() };
|
|
241
|
+
this.started++;
|
|
242
|
+
t.event({ what: 'apply waiting', detail: `${this.runs} run(s) to finish` });
|
|
243
|
+
try {
|
|
244
|
+
while (this.runs > 0) {
|
|
245
|
+
await t.tick();
|
|
246
|
+
}
|
|
247
|
+
const batch = this.pending.splice(0);
|
|
248
|
+
this.inApply = batch;
|
|
249
|
+
const name = pad(Math.max(0, ...this.#names().map(Number)) + 1);
|
|
250
|
+
const dir = join(this.dir, name);
|
|
251
|
+
const from = t.system.record('external').v;
|
|
252
|
+
await t.journal.step({ kind: 'apply', dir, from }, () => this.#applyBatch(batch, name, dir, from));
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
this.applying = false;
|
|
256
|
+
this.current = undefined;
|
|
257
|
+
this.inApply = [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async #applyBatch(batch, name, dir, from) {
|
|
261
|
+
const t = this.#t;
|
|
262
|
+
const startedAt = new Date().toISOString();
|
|
263
|
+
mkdirSync(dir, { recursive: true });
|
|
264
|
+
writeFileSync(join(dir, 'inputs.json'), `${JSON.stringify(batch.map((f) => {
|
|
265
|
+
const at = f.dir ?? t.attemptDir({ id: f.case, rev: f.caseRev }, f.phase, f.attempt);
|
|
266
|
+
const missed = readJson(join(at, 'run.json'))?.missedSkills;
|
|
267
|
+
const { dir: _, ...rest } = f;
|
|
268
|
+
return {
|
|
269
|
+
...rest,
|
|
270
|
+
improvements: forProse(f),
|
|
271
|
+
file: join(at, 'feedback.json'),
|
|
272
|
+
...(missed?.length ? { missedSkills: missed } : {}),
|
|
273
|
+
};
|
|
274
|
+
}), null, 2)}\n`);
|
|
275
|
+
writeFileSync(join(dir, 'deferred.json'), `${JSON.stringify(this.deferred(), null, 2)}\n`);
|
|
276
|
+
const total = batch.reduce((n, f) => n + forProse(f).length, 0);
|
|
277
|
+
this.current = { since: Date.now(), name, dir, requests: batch.length };
|
|
278
|
+
let to = from;
|
|
279
|
+
let discarded;
|
|
280
|
+
let bytes;
|
|
281
|
+
if (total > 0) {
|
|
282
|
+
t.event({
|
|
283
|
+
what: 'apply',
|
|
284
|
+
detail: `${name}: ${batch.length} request(s), ${total} improvement(s)`,
|
|
285
|
+
});
|
|
286
|
+
writeApply(t, dir);
|
|
287
|
+
try {
|
|
288
|
+
const done = await this.#edit(dir, from);
|
|
289
|
+
bytes = done.bytes;
|
|
290
|
+
if ('discarded' in done) {
|
|
291
|
+
discarded = done.discarded;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
to = done.to;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
writeFileSync(join(dir, 'failed.json'), `${JSON.stringify({ at: new Date().toISOString(), startedAt, error: err.message }, null, 2)}\n`);
|
|
299
|
+
writeApply(t, dir);
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const applied = {
|
|
304
|
+
ids: batch.map((f) => f.id),
|
|
305
|
+
from,
|
|
306
|
+
to,
|
|
307
|
+
at: new Date().toISOString(),
|
|
308
|
+
startedAt,
|
|
309
|
+
...(total === 0 ? { skipped: true } : {}),
|
|
310
|
+
...(discarded ? { discarded } : {}),
|
|
311
|
+
...(bytes ? { bytes } : {}),
|
|
312
|
+
};
|
|
313
|
+
writeFileSync(join(dir, 'applied.json'), `${JSON.stringify(applied, null, 2)}\n`);
|
|
314
|
+
writeApply(t, dir);
|
|
315
|
+
for (const f of batch) {
|
|
316
|
+
this.#applied.add(f.id);
|
|
317
|
+
}
|
|
318
|
+
this.count++;
|
|
319
|
+
this.current = undefined;
|
|
320
|
+
t.event({
|
|
321
|
+
what: discarded ? 'apply undone' : 'applied',
|
|
322
|
+
detail: `${name}: ${discarded ? `undone - ${discarded}` : `v${from} -> v${to}`}, see applies/${name}/APPLY.md`,
|
|
323
|
+
result: total === 0 ? 'nothing to change' : discarded ? 'undone' : `v${from} → v${to}`,
|
|
324
|
+
took: Date.now() - Date.parse(startedAt),
|
|
325
|
+
tokens: spent(applyTokens(dir)),
|
|
326
|
+
});
|
|
327
|
+
if (!discarded && to !== from) {
|
|
328
|
+
t.rechecks.queue(name);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** What an apply is handed about the prose: sizes, limits, and which files grew. */
|
|
332
|
+
async #writeBudget(dir, now) {
|
|
333
|
+
const start = await this.baseline();
|
|
334
|
+
const { files: _, ...rest } = now;
|
|
335
|
+
writeFileSync(join(dir, 'budget.json'), `${JSON.stringify({
|
|
336
|
+
...rest,
|
|
337
|
+
start: { total: start.total, alwaysRead: start.alwaysRead },
|
|
338
|
+
...limits(start, this.#t.config),
|
|
339
|
+
}, null, 2)}\n`);
|
|
340
|
+
const touched = new Map();
|
|
341
|
+
for (const a of this.applies()) {
|
|
342
|
+
if (!a.applied || a.applied.discarded || !existsSync(join(a.dir, 'diff.patch'))) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
for (const f of patchFiles(readFileSync(join(a.dir, 'diff.patch'), 'utf8'))) {
|
|
346
|
+
touched.set(f.file, [...(touched.get(f.file) ?? []), a.name]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const growth = Object.keys(now.files)
|
|
350
|
+
.map((file) => ({
|
|
351
|
+
file,
|
|
352
|
+
start: start.files[file] ?? 0,
|
|
353
|
+
now: now.files[file],
|
|
354
|
+
changes: touched.get(file)?.length ?? 0,
|
|
355
|
+
recent: (touched.get(file) ?? []).slice(-5),
|
|
356
|
+
}))
|
|
357
|
+
.sort((a, b) => b.now - b.start - (a.now - a.start));
|
|
358
|
+
writeFileSync(join(dir, 'growth.json'), `${JSON.stringify(growth, null, 2)}\n`);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The meta agent edits, then two gates: `zen check` and the budget. A failed
|
|
362
|
+
* gate undoes the edit and the agent tries once more, told why. `zen check`
|
|
363
|
+
* failing twice is fatal - the agent cannot leave the project loadable; the
|
|
364
|
+
* budget failing twice only undoes it. What the edit broke elsewhere is the
|
|
365
|
+
* rechecks' to find, after it.
|
|
366
|
+
*/
|
|
367
|
+
async #edit(dir, from) {
|
|
368
|
+
const t = this.#t;
|
|
369
|
+
const name = basename(dir);
|
|
370
|
+
const before = await measure(t.root);
|
|
371
|
+
await this.#writeBudget(dir, before);
|
|
372
|
+
const bound = limits(await this.baseline(), t.config);
|
|
373
|
+
const gate = { before, limits: bound };
|
|
374
|
+
writeFileSync(join(dir, 'gate.json'), `${JSON.stringify(gate)}\n`);
|
|
375
|
+
const unchanged = { before: before.total, after: before.total };
|
|
376
|
+
const at = (step) => {
|
|
377
|
+
if (this.current) {
|
|
378
|
+
this.current.step = step;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
for (let attempt = 1;; attempt++) {
|
|
382
|
+
at('edit');
|
|
383
|
+
const session = randomUUID();
|
|
384
|
+
writeFileSync(join(dir, 'session'), `${session}\n`);
|
|
385
|
+
const applyLog = join(dir, 'apply.log');
|
|
386
|
+
const checkLog = join(dir, 'check.log');
|
|
387
|
+
const offset = { apply: sizeOf(applyLog), check: sizeOf(checkLog) };
|
|
388
|
+
// An undone attempt's record must not pass for this one's.
|
|
389
|
+
for (const f of ['decisions.json', 'changes.md']) {
|
|
390
|
+
rmSync(join(dir, f), { force: true });
|
|
391
|
+
}
|
|
392
|
+
const brief = await writeApplyBrief(t, dir, attempt);
|
|
393
|
+
const res = await t.zen([
|
|
394
|
+
'meta',
|
|
395
|
+
'run',
|
|
396
|
+
'--no-refresh',
|
|
397
|
+
'--json',
|
|
398
|
+
'--session-id',
|
|
399
|
+
session,
|
|
400
|
+
'--events',
|
|
401
|
+
join(dir, 'apply.events.jsonl'),
|
|
402
|
+
'--prompt-file',
|
|
403
|
+
brief,
|
|
404
|
+
'/finetune-apply',
|
|
405
|
+
dir,
|
|
406
|
+
], applyLog);
|
|
407
|
+
writeFileSync(join(dir, attempt === 1 ? 'apply.json' : `apply-${attempt}.json`), res.stdout);
|
|
408
|
+
// An agent cut off by its provider can still exit 0, having decided nothing.
|
|
409
|
+
const missing = res.code === 0 ? undecided(dir) : [];
|
|
410
|
+
if (missing.length > 0) {
|
|
411
|
+
writeFileSync(join(dir, 'missing.md'), [
|
|
412
|
+
`# Attempt ${attempt} was undone: decisions missing`,
|
|
413
|
+
'',
|
|
414
|
+
'Written by the tuning loop: the apply ended without a decision for these improvements, so the project was put back. This attempt must write `decisions.json` with every one of them, and `changes.md`.',
|
|
415
|
+
'',
|
|
416
|
+
...missing.map((m) => `- ${m}`),
|
|
417
|
+
'',
|
|
418
|
+
].join('\n'));
|
|
419
|
+
t.error(`apply ${name}${attempt > 1 ? ` try ${attempt}` : ''}`, `the apply ended without a decision for ${missing.length} improvement(s)`, applyLog);
|
|
420
|
+
t.system.restore(from);
|
|
421
|
+
if (attempt === 2) {
|
|
422
|
+
throw new Error(`apply ${dir} ended twice without deciding every improvement - see missing.md`);
|
|
423
|
+
}
|
|
424
|
+
t.event({
|
|
425
|
+
what: 'apply retry',
|
|
426
|
+
detail: `no decision for ${missing.length} improvement(s)`,
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
at('check');
|
|
431
|
+
const check = await t.zen(['check', '--no-models', '--no-sandbox'], checkLog);
|
|
432
|
+
if (res.code !== 0 || check.code !== 0) {
|
|
433
|
+
const e = res.code !== 0
|
|
434
|
+
? errorIn(applyLog, offset.apply)
|
|
435
|
+
: errorIn(checkLog, offset.check);
|
|
436
|
+
t.error(`apply ${name}${attempt > 1 ? ` try ${attempt}` : ''}`, `${res.code !== 0 ? 'the apply failed' : 'zen check failed after the apply'}${e.message ? `: ${e.message}` : ''}`, e.log);
|
|
437
|
+
t.system.restore(from);
|
|
438
|
+
if (attempt === 2) {
|
|
439
|
+
throw new Error(`apply ${dir} left the project broken twice - see check.log`);
|
|
440
|
+
}
|
|
441
|
+
t.event({
|
|
442
|
+
what: 'apply retry',
|
|
443
|
+
detail: res.code !== 0 ? 'the apply failed' : 'zen check failed',
|
|
444
|
+
});
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const after = await measure(t.root);
|
|
448
|
+
const over = overLimits(before, after, bound);
|
|
449
|
+
if (over.length > 0) {
|
|
450
|
+
writeFileSync(join(dir, 'budget.md'), [
|
|
451
|
+
`# Attempt ${attempt} was undone: over the limits`,
|
|
452
|
+
'',
|
|
453
|
+
'Written by the tuning loop: this edit grew what the agents read past a limit, so the project was put back. The next attempt reads this file to know what to cut.',
|
|
454
|
+
'',
|
|
455
|
+
...over.map((o) => `- ${o}`),
|
|
456
|
+
'',
|
|
457
|
+
'## What moved',
|
|
458
|
+
'',
|
|
459
|
+
...costChanges(before, after),
|
|
460
|
+
'',
|
|
461
|
+
].join('\n'));
|
|
462
|
+
t.system.restore(from);
|
|
463
|
+
if (attempt === 2) {
|
|
464
|
+
return { discarded: `over the limits: ${over[0]}`, bytes: unchanged };
|
|
465
|
+
}
|
|
466
|
+
t.event({ what: 'apply retry', detail: `over the limits: ${over[0]}` });
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const to = t.system.record('apply', name).v;
|
|
470
|
+
if (to === from) {
|
|
471
|
+
return { to, bytes: unchanged };
|
|
472
|
+
}
|
|
473
|
+
writeFileSync(join(dir, 'diff.patch'), t.system.diff(from, to));
|
|
474
|
+
const moved = costChanges(before, after);
|
|
475
|
+
writeFileSync(join(dir, 'cost.md'), [
|
|
476
|
+
'# What moved',
|
|
477
|
+
'',
|
|
478
|
+
'Written by the tuning loop once this apply passed its gates: bytes before and after it - what each agent reads on every call, each skill body, each file under agents/.',
|
|
479
|
+
'',
|
|
480
|
+
...(moved.length > 0 ? moved : ['Nothing the agents read changed size.']),
|
|
481
|
+
'',
|
|
482
|
+
].join('\n'));
|
|
483
|
+
return { to, bytes: { before: before.total, after: after.total } };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Requests every apply so far took, for the report. */
|
|
487
|
+
applies() {
|
|
488
|
+
return this.#names().map((name) => {
|
|
489
|
+
const dir = join(this.dir, name);
|
|
490
|
+
return {
|
|
491
|
+
name,
|
|
492
|
+
dir,
|
|
493
|
+
applied: readJson(join(dir, 'applied.json')),
|
|
494
|
+
inputs: existsSync(join(dir, 'inputs.json'))
|
|
495
|
+
? readJson(join(dir, 'inputs.json'))
|
|
496
|
+
: undefined,
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
//# sourceMappingURL=improvements.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Tuning } from './tuning.ts';
|
|
2
|
+
export type Tx = {
|
|
3
|
+
kind: 'run';
|
|
4
|
+
dir: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'analyze';
|
|
7
|
+
dir: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'apply';
|
|
10
|
+
dir: string;
|
|
11
|
+
from: number;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'merge';
|
|
14
|
+
dir: string;
|
|
15
|
+
log: string;
|
|
16
|
+
};
|
|
17
|
+
export declare class Journal {
|
|
18
|
+
#private;
|
|
19
|
+
readonly file: string;
|
|
20
|
+
constructor(file: string);
|
|
21
|
+
begin(tx: Tx): string;
|
|
22
|
+
end(id: string, op: 'commit' | 'failed' | 'rollback'): void;
|
|
23
|
+
/** Runs `fn` as one transaction. */
|
|
24
|
+
step<T>(tx: Tx, fn: () => Promise<T>): Promise<T>;
|
|
25
|
+
/** Transactions begun and never closed, oldest first. */
|
|
26
|
+
open(): {
|
|
27
|
+
id: string;
|
|
28
|
+
tx: Tx;
|
|
29
|
+
}[];
|
|
30
|
+
/** Every transaction closed: the history can go. */
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
|
33
|
+
/** Rolls back what a kill cut off. Only under the loop's lock: a live step looks the same. */
|
|
34
|
+
export declare function recover(t: Tuning): Promise<void>;
|
|
35
|
+
//# sourceMappingURL=journal.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { appendFileSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
4
|
+
import { reap } from "./children.js";
|
|
5
|
+
/** What a step leaves in its attempt folder; the logs stay, they only grow. */
|
|
6
|
+
const OUTPUTS = {
|
|
7
|
+
run: ['run.json', 'envelope.json', 'request.json', 'events.jsonl', 'memory', 'workspace'],
|
|
8
|
+
analyze: [
|
|
9
|
+
'feedback.json',
|
|
10
|
+
'analysis.md',
|
|
11
|
+
'analyze.json',
|
|
12
|
+
'analyze-2.json',
|
|
13
|
+
'analyze.events.jsonl',
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
export class Journal {
|
|
17
|
+
file;
|
|
18
|
+
#base;
|
|
19
|
+
constructor(file) {
|
|
20
|
+
this.file = file;
|
|
21
|
+
this.#base = dirname(file);
|
|
22
|
+
}
|
|
23
|
+
#append(row) {
|
|
24
|
+
mkdirSync(this.#base, { recursive: true });
|
|
25
|
+
appendFileSync(this.file, `${JSON.stringify(row)}\n`);
|
|
26
|
+
}
|
|
27
|
+
// Paths are kept relative, so a project moved between sessions still recovers.
|
|
28
|
+
#paths(tx, to) {
|
|
29
|
+
return { ...tx, dir: to(tx.dir), ...('log' in tx ? { log: to(tx.log) } : {}) };
|
|
30
|
+
}
|
|
31
|
+
begin(tx) {
|
|
32
|
+
const id = randomUUID().slice(0, 8);
|
|
33
|
+
this.#append({
|
|
34
|
+
id,
|
|
35
|
+
at: new Date().toISOString(),
|
|
36
|
+
op: 'begin',
|
|
37
|
+
tx: this.#paths(tx, (p) => relative(this.#base, p)),
|
|
38
|
+
});
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
end(id, op) {
|
|
42
|
+
this.#append({ id, at: new Date().toISOString(), op });
|
|
43
|
+
}
|
|
44
|
+
/** Runs `fn` as one transaction. */
|
|
45
|
+
async step(tx, fn) {
|
|
46
|
+
const id = this.begin(tx);
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
result = await fn();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
this.end(id, 'failed');
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
this.end(id, 'commit');
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/** Transactions begun and never closed, oldest first. */
|
|
59
|
+
open() {
|
|
60
|
+
let text = '';
|
|
61
|
+
try {
|
|
62
|
+
text = readFileSync(this.file, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const begun = new Map();
|
|
68
|
+
for (const line of text.split('\n')) {
|
|
69
|
+
let row;
|
|
70
|
+
try {
|
|
71
|
+
row = JSON.parse(line);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A line torn by the kill itself.
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (row.op === 'begin') {
|
|
78
|
+
begun.set(row.id, row.tx);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
begun.delete(row.id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...begun].map(([id, tx]) => ({
|
|
85
|
+
id,
|
|
86
|
+
tx: this.#paths(tx, (p) => (isAbsolute(p) ? p : join(this.#base, p))),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
/** Every transaction closed: the history can go. */
|
|
90
|
+
clear() {
|
|
91
|
+
rmSync(this.file, { force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Rolls back what a kill cut off. Only under the loop's lock: a live step looks the same. */
|
|
95
|
+
export async function recover(t) {
|
|
96
|
+
// Before anything is rolled back: a child still running would write into it again.
|
|
97
|
+
const reaped = await reap(t.dir);
|
|
98
|
+
if (reaped.length > 0) {
|
|
99
|
+
t.event({
|
|
100
|
+
what: 'stopped leftovers',
|
|
101
|
+
detail: `${reaped.length} process(es) the last loop left running`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const open = t.journal.open().reverse();
|
|
105
|
+
for (const { id, tx } of open) {
|
|
106
|
+
rollback(t, tx);
|
|
107
|
+
t.journal.end(id, 'rollback');
|
|
108
|
+
t.event({ what: 'rolled back', detail: `${tx.kind} ${relative(t.dir, tx.dir)}` });
|
|
109
|
+
}
|
|
110
|
+
if (open.some(({ tx }) => tx.kind === 'apply')) {
|
|
111
|
+
t.improvements.reload();
|
|
112
|
+
}
|
|
113
|
+
t.journal.clear();
|
|
114
|
+
}
|
|
115
|
+
function rollback(t, tx) {
|
|
116
|
+
switch (tx.kind) {
|
|
117
|
+
case 'run':
|
|
118
|
+
case 'analyze':
|
|
119
|
+
for (const name of OUTPUTS[tx.kind]) {
|
|
120
|
+
rmSync(join(tx.dir, name), { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
case 'apply':
|
|
124
|
+
// Its edits never passed `zen check`: put back the version it started from.
|
|
125
|
+
t.system.restore(tx.from);
|
|
126
|
+
t.system.drop(basename(tx.dir));
|
|
127
|
+
rmSync(tx.dir, { recursive: true, force: true });
|
|
128
|
+
return;
|
|
129
|
+
case 'merge':
|
|
130
|
+
rmSync(tx.dir, { recursive: true, force: true });
|
|
131
|
+
rmSync(tx.log, { force: true });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=journal.js.map
|