crewforth 3.4.0 → 3.5.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +273 -0
- package/README.md +3 -2
- package/VERSION +1 -1
- package/adopt.sh +40 -5
- package/bin/cli.js +138 -2
- package/kit/AGENT_TEMPLATE.md +15 -0
- package/kit/CLAUDE.md +50 -18
- package/kit/agents/crew-backend-expert.md +4 -3
- package/kit/agents/crew-commit-agent.md +2 -0
- package/kit/agents/crew-database-expert.md +2 -1
- package/kit/agents/crew-devops-expert.md +1 -0
- package/kit/agents/crew-docs-agent.md +3 -1
- package/kit/agents/crew-frontend-expert.md +2 -0
- package/kit/agents/crew-performance-expert.md +7 -2
- package/kit/agents/crew-planner.md +15 -3
- package/kit/agents/crew-privacy-agent.md +2 -0
- package/kit/agents/crew-review-agent.md +8 -1
- package/kit/agents/crew-security-expert.md +4 -3
- package/kit/agents/crew-test-expert.md +6 -2
- package/kit/eval/doctor.sh +201 -1
- package/kit/eval/gate-report.sh +28 -0
- package/kit/eval/golden-holdout.txt +2 -0
- package/kit/eval/golden-routing.txt +17 -0
- package/kit/eval/lib/crew-map/contracts.mjs +77 -0
- package/kit/eval/lib/crew-map/crew-map.sh +2 -1
- package/kit/eval/lib/crew-map/detect.mjs +2 -1
- package/kit/eval/lib/crew-map/narrative.mjs +14 -3
- package/kit/eval/lib/crew-map/project.mjs +45 -6
- package/kit/eval/lib/crew-map/scan.mjs +153 -14
- package/kit/eval/lib/findings/findings.schema.json +11 -1
- package/kit/eval/lib/findings/findings.sh +24 -1
- package/kit/eval/lib/hook-inject.sh +159 -0
- package/kit/eval/lib/lockfiles.sh +71 -0
- package/kit/eval/lib/payload-check.sh +29 -0
- package/kit/eval/lib/preview.sh +3 -3
- package/kit/eval/lib/review-bench/bench.mjs +14 -2
- package/kit/eval/lib/review-prepass/prepass.mjs +32 -3
- package/kit/eval/lib/settings-json.awk +18 -0
- package/kit/eval/lib/statusline.sh +22 -5
- package/kit/eval/lib/todo.awk +167 -0
- package/kit/eval/lib/todo.sh +120 -0
- package/kit/eval/lib/untrusted-text.awk +38 -0
- package/kit/eval/scan-skill.sh +67 -20
- package/kit/eval/smoke-test.sh +1759 -41
- package/kit/eval/utilization.sh +77 -3
- package/kit/hooks/agent-outcome.sh +9 -1
- package/kit/hooks/board-sync.sh +49 -3
- package/kit/hooks/board.sh +8 -8
- package/kit/hooks/branch-fresh.sh +101 -0
- package/kit/hooks/floor-blocklist.txt +19 -1
- package/kit/hooks/guard-bash.sh +92 -8
- package/kit/hooks/guard-question.sh +2 -1
- package/kit/hooks/loop-guard.sh +445 -0
- package/kit/hooks/map-stale.sh +437 -0
- package/kit/hooks/pre-commit +48 -7
- package/kit/hooks/precompact-snapshot.sh +102 -6
- package/kit/hooks/prompt-approval.sh +50 -14
- package/kit/hooks/route-hint.sh +36 -7
- package/kit/hooks/secret-blocklist.txt +55 -1
- package/kit/hooks/session-guard.sh +45 -15
- package/kit/hooks/session-rehydrate.sh +34 -11
- package/kit/hooks/session-stats.sh +169 -8
- package/kit/hooks/session-update-check.sh +5 -1
- package/kit/hooks/skill-trust.sh +23 -14
- package/kit/hooks/turn-check.sh +190 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +64 -0
- package/kit/skills/a11y/SKILL.md +50 -3
- package/kit/skills/a11y/references/recipes.md +41 -0
- package/kit/skills/adr/SKILL.md +14 -2
- package/kit/skills/api-design/SKILL.md +6 -1
- package/kit/skills/api-design/references/idempotency.md +39 -0
- package/kit/skills/brainstorm/SKILL.md +4 -2
- package/kit/skills/brainstorm/references/asking.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +11 -1
- package/kit/skills/ci-pipeline/references/agent-job.md +33 -0
- package/kit/skills/commit-message/SKILL.md +9 -3
- package/kit/skills/confidence-check/SKILL.md +9 -7
- package/kit/skills/confidence-check/references/source-check.md +37 -0
- package/kit/skills/crew-approve/SKILL.md +3 -0
- package/kit/skills/crew-audit/SKILL.md +63 -0
- package/kit/skills/crew-board/SKILL.md +2 -2
- package/kit/skills/crew-code-review/SKILL.md +27 -4
- package/kit/skills/crew-code-review/references/panel-mode.md +9 -9
- package/kit/skills/crew-code-review/references/simplify.md +28 -0
- package/kit/skills/crew-doctor/SKILL.md +3 -2
- package/kit/skills/crew-gates/SKILL.md +3 -0
- package/kit/skills/crew-gates/references/enforcers.md +30 -0
- package/kit/skills/crew-interview/SKILL.md +42 -0
- package/kit/skills/crew-map/SKILL.md +26 -6
- package/kit/skills/crew-plan/SKILL.md +10 -2
- package/kit/skills/crew-pr/SKILL.md +57 -0
- package/kit/skills/crew-pr/references/checks.md +35 -0
- package/kit/skills/crew-pr/references/threads.md +23 -0
- package/kit/skills/crew-review/SKILL.md +34 -1
- package/kit/skills/crew-ship/SKILL.md +5 -2
- package/kit/skills/crew-skill/SKILL.md +26 -1
- package/kit/skills/crew-todo/SKILL.md +31 -0
- package/kit/skills/crew-update/SKILL.md +4 -1
- package/kit/skills/db-migration/SKILL.md +15 -0
- package/kit/skills/db-migration/references/expand-contract.md +52 -0
- package/kit/skills/dependency-audit/SKILL.md +34 -1
- package/kit/skills/dependency-audit/scripts/record.sh +37 -0
- package/kit/skills/dependency-upgrade/SKILL.md +29 -1
- package/kit/skills/deploy/SKILL.md +18 -0
- package/kit/skills/deploy/references/topology-a.md +7 -0
- package/kit/skills/deploy/references/topology-b.md +2 -2
- package/kit/skills/docs-writer/SKILL.md +37 -2
- package/kit/skills/eval-grader/SKILL.md +9 -0
- package/kit/skills/eval-grader/references/method.md +26 -0
- package/kit/skills/frontend/SKILL.md +5 -2
- package/kit/skills/frontend/references/from-design.md +29 -0
- package/kit/skills/frontend/references/page-metadata.md +17 -0
- package/kit/skills/frontend/references/verify-contract.md +18 -1
- package/kit/skills/frontend/references/worst-data.md +35 -0
- package/kit/skills/frontend-design/SKILL.md +20 -4
- package/kit/skills/frontend-design/references/design-review.md +21 -0
- package/kit/skills/frontend-design/references/detection.md +44 -0
- package/kit/skills/frontend-design/references/motion.md +54 -0
- package/kit/skills/frontend-design/references/ui-copy.md +35 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
- package/kit/skills/handoff/SKILL.md +28 -3
- package/kit/skills/handoff/references/cold-reader.md +13 -6
- package/kit/skills/i18n-integrity/SKILL.md +3 -0
- package/kit/skills/iterate/SKILL.md +7 -2
- package/kit/skills/mcp-builder/SKILL.md +21 -4
- package/kit/skills/mcp-builder/references/building.md +74 -13
- package/kit/skills/mcp-builder/references/cli-for-agents.md +26 -0
- package/kit/skills/observability/SKILL.md +13 -1
- package/kit/skills/performance/SKILL.md +25 -1
- package/kit/skills/performance/references/bottlenecks.md +11 -0
- package/kit/skills/performance/references/web-vitals.md +31 -0
- package/kit/skills/red-team/SKILL.md +25 -3
- package/kit/skills/reflect/SKILL.md +7 -2
- package/kit/skills/release/SKILL.md +13 -0
- package/kit/skills/security-scan/SKILL.md +6 -2
- package/kit/skills/security-scan/references/ai-agents.md +16 -0
- package/kit/skills/security-scan/references/fronts.md +59 -2
- package/kit/skills/security-scan/references/reporting.md +13 -0
- package/kit/skills/security-scan/references/verify.md +9 -0
- package/kit/skills/sonarqube-check/SKILL.md +32 -2
- package/kit/skills/sonarqube-check/references/ratchet.md +32 -0
- package/kit/skills/spec-planning/SKILL.md +32 -8
- package/kit/skills/systematic-debugging/SKILL.md +15 -7
- package/kit/skills/systematic-debugging/references/techniques.md +5 -0
- package/kit/skills/teamboard/SKILL.md +3 -3
- package/kit/skills/testing/SKILL.md +18 -3
- package/kit/skills/testing/references/browser-headless.md +21 -0
- package/kit/skills/token-budget/SKILL.md +26 -5
- package/kit/skills/worktree/SKILL.md +13 -5
- package/kit/studio/server/lib/session.js +23 -2
- package/kit/studio/server/lib/worktree-changes.js +64 -0
- package/kit/studio/web/agent-spend.js +32 -0
- package/kit/studio/web/app.js +245 -236
- package/kit/studio/web/approvals.js +14 -13
- package/kit/studio/web/ask.js +10 -9
- package/kit/studio/web/canvas.js +56 -54
- package/kit/studio/web/chat.js +41 -41
- package/kit/studio/web/convo.js +33 -16
- package/kit/studio/web/dock.js +20 -19
- package/kit/studio/web/graph-plan.js +62 -22
- package/kit/studio/web/i18n-tr.js +760 -0
- package/kit/studio/web/i18n.js +79 -0
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +27 -24
- package/kit/studio/web/list-plan.js +16 -15
- package/kit/studio/web/list.js +6 -5
- package/kit/studio/web/liveness.js +11 -9
- package/kit/studio/web/map-plan.js +86 -36
- package/kit/studio/web/map.js +85 -72
- package/kit/studio/web/models-plan.js +47 -42
- package/kit/studio/web/models.js +21 -20
- package/kit/studio/web/names.js +13 -10
- package/kit/studio/web/nav.js +34 -33
- package/kit/studio/web/newsession.js +25 -24
- package/kit/studio/web/style.css +2 -0
- package/kit/studio/web/theme.js +5 -2
- package/kit/studio/web/timeline-plan.js +19 -18
- package/kit/studio/web/timeline.js +23 -22
- package/kit/studio/web/usage.js +36 -32
- package/package.json +5 -4
- package/start.sh +11 -2
package/kit/studio/web/map.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
// nothing; what leaves the page is copied text: a finding as a task, or a file:line.
|
|
11
11
|
import {
|
|
12
12
|
nodesOf, edgesOf, edgeKey, litEdges, findingsOf, findingCounts, cardSub, summaryOf, sourceOf, inspectOf, taskOf,
|
|
13
|
-
emptyNote, layoutOf,
|
|
13
|
+
emptyNote, layoutOf, routesOf, chipsOf, inferredOnly, groupsOf, foldView, placed, BANDS, cardOf, tourOf, searchOf, resourceWord,
|
|
14
14
|
} from './map-plan.js';
|
|
15
15
|
import { Viewport, draggable, swallowClick } from './viewport.js';
|
|
16
|
+
import { t } from './i18n.js';
|
|
16
17
|
|
|
17
18
|
const SVG = 'http://www.w3.org/2000/svg';
|
|
18
19
|
export const LAYOUT_KEY = 'crewforth-studio-map-layout:';
|
|
@@ -31,9 +32,9 @@ function svg(tag, attrs = {}) {
|
|
|
31
32
|
const BAND_ORDER = Object.fromEntries(BANDS.map(([k], i) => [k, i]));
|
|
32
33
|
const BAND_WORD = Object.fromEntries(BANDS);
|
|
33
34
|
|
|
34
|
-
const LAYERS = [['modules', 'Modules'], ['external', 'External'], ['endpoints', 'Endpoints'], ['findings', 'Findings']];
|
|
35
|
+
const LAYERS = [['modules', t('Modules')], ['external', t('External')], ['endpoints', t('Endpoints')], ['findings', t('Findings')]];
|
|
35
36
|
// How a line between two modules is named on the map.
|
|
36
|
-
const EDGE_WORD = { references: 'uses', calls: 'calls', builds: 'builds' };
|
|
37
|
+
const EDGE_WORD = { references: t('uses'), calls: t('calls'), builds: t('builds') };
|
|
37
38
|
|
|
38
39
|
export class MapView {
|
|
39
40
|
/**
|
|
@@ -76,9 +77,9 @@ export class MapView {
|
|
|
76
77
|
});
|
|
77
78
|
this.below = mk('div', 'mp-below');
|
|
78
79
|
this.list = mk('section', 'mp-modlist');
|
|
79
|
-
this.list.setAttribute('aria-label', 'Modules');
|
|
80
|
+
this.list.setAttribute('aria-label', t('Modules'));
|
|
80
81
|
this.side = mk('aside', 'mp-inspect');
|
|
81
|
-
this.side.setAttribute('aria-label', 'Selected node');
|
|
82
|
+
this.side.setAttribute('aria-label', t('Selected node'));
|
|
82
83
|
const main = mk('div', 'mp-main');
|
|
83
84
|
main.append(this.pane, this.below, this.list);
|
|
84
85
|
this.body = mk('div', 'mp-body');
|
|
@@ -162,7 +163,7 @@ export class MapView {
|
|
|
162
163
|
const note = emptyNote(this.data);
|
|
163
164
|
if (note) {
|
|
164
165
|
this.empty.replaceChildren(mk('p', null, note));
|
|
165
|
-
if (this.data?.missing) this.empty.append(mk('p', 'sub', 'Studio only reads the map: nothing is scanned from the panel.'));
|
|
166
|
+
if (this.data?.missing) this.empty.append(mk('p', 'sub', t('Studio only reads the map: nothing is scanned from the panel.')));
|
|
166
167
|
this.root.replaceChildren(this.empty);
|
|
167
168
|
return;
|
|
168
169
|
}
|
|
@@ -222,17 +223,18 @@ export class MapView {
|
|
|
222
223
|
const frame = mk('div', 'mp-group');
|
|
223
224
|
frame.dataset.group = g.id;
|
|
224
225
|
frame.style.left = `${x}px`; frame.style.top = `${y}px`; frame.style.width = `${w}px`; frame.style.height = `${h}px`;
|
|
225
|
-
const fold = mk('button', 'mp-group-fold', `${g.folder}/ · fold`);
|
|
226
|
+
const fold = mk('button', 'mp-group-fold', `${g.folder}/ · ${t('fold')}`);
|
|
226
227
|
fold.type = 'button';
|
|
227
|
-
fold.title =
|
|
228
|
+
fold.title = t('Fold the {n} modules under {folder}/ into one card', { n: g.members.length, folder: g.folder });
|
|
228
229
|
fold.addEventListener('click', () => this.toggleGroup(g.id));
|
|
229
230
|
frame.append(fold);
|
|
230
231
|
parts.push(frame);
|
|
231
232
|
}
|
|
232
233
|
const lines = svg('svg', { class: 'mp-lines', width: lay.width, height: lay.height, 'aria-hidden': 'true' });
|
|
233
234
|
const labels = [];
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
const routes = routesOf(edges, lay.pos);
|
|
236
|
+
for (const [ei, e] of edges.entries()) {
|
|
237
|
+
const p = routes[ei];
|
|
236
238
|
if (!p) continue;
|
|
237
239
|
const path = svg('path', { d: p.d, class: 'mp-line' });
|
|
238
240
|
path.dataset.solid = String(e.solid);
|
|
@@ -243,7 +245,7 @@ export class MapView {
|
|
|
243
245
|
if (EDGE_WORD[e.kind] && !e.calls) tags.push(mk('span', 'mp-badge', EDGE_WORD[e.kind]));
|
|
244
246
|
if (e.line === 'model') { const m = mk('span', 'mp-pill', 'model'); m.dataset.tone = 'model'; m.title = 'Proposed by the model, not shown by a file'; tags.push(m); }
|
|
245
247
|
if (e.calls) {
|
|
246
|
-
tags.push(mk('span', 'mp-pill',
|
|
248
|
+
tags.push(mk('span', 'mp-pill', e.calls === 1 ? t('{n} call', { n: 1 }) : t('{n} calls', { n: e.calls })));
|
|
247
249
|
if (e.missing) { const m = mk('span', 'mp-pill', `${e.missing} missing`); m.dataset.tone = 'missing'; tags.push(m); }
|
|
248
250
|
}
|
|
249
251
|
if (tags.length) {
|
|
@@ -271,14 +273,14 @@ export class MapView {
|
|
|
271
273
|
|
|
272
274
|
#head(doc, findings) {
|
|
273
275
|
const head = mk('header', 'mp-head');
|
|
274
|
-
head.append(mk('h2', 'mp-title', 'Map'), mk('span', 'row-fill'));
|
|
276
|
+
head.append(mk('h2', 'mp-title', t('Map')), mk('span', 'row-fill'));
|
|
275
277
|
for (const s of summaryOf(doc, findings)) head.append(mk('span', 'mp-pill', s));
|
|
276
278
|
return head;
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
#toolbar(d) {
|
|
280
282
|
const bar = mk('div', 'mp-toolbar');
|
|
281
|
-
bar.append(mk('span', 'lbl', 'Layers'));
|
|
283
|
+
bar.append(mk('span', 'lbl', t('Layers')));
|
|
282
284
|
for (const [k, label] of LAYERS) {
|
|
283
285
|
const b = mk('button', 'mp-tog', label);
|
|
284
286
|
b.type = 'button';
|
|
@@ -289,8 +291,8 @@ export class MapView {
|
|
|
289
291
|
bar.append(mk('span', 'row-fill'));
|
|
290
292
|
const search = mk('input', 'mp-search');
|
|
291
293
|
search.type = 'search';
|
|
292
|
-
search.placeholder = 'Search files and modules';
|
|
293
|
-
search.setAttribute('aria-label', 'Search files and modules');
|
|
294
|
+
search.placeholder = t('Search files and modules');
|
|
295
|
+
search.setAttribute('aria-label', t('Search files and modules'));
|
|
294
296
|
search.value = this.query;
|
|
295
297
|
search.addEventListener('input', () => { this.query = search.value; this.render(); this.top.querySelector('.mp-search')?.focus(); });
|
|
296
298
|
search.addEventListener('keydown', (e) => {
|
|
@@ -300,16 +302,16 @@ export class MapView {
|
|
|
300
302
|
});
|
|
301
303
|
bar.append(search);
|
|
302
304
|
if (tourOf(d.doc).length) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
bar.append(
|
|
305
|
+
const tb = mk('button', 'mp-tog', t('Tour'));
|
|
306
|
+
tb.type = 'button';
|
|
307
|
+
tb.setAttribute('aria-pressed', String(this.tourAt >= 0));
|
|
308
|
+
tb.title = t('The stops /crew-map wrote for someone new to the project, in order');
|
|
309
|
+
tb.addEventListener('click', () => this.tour(this.tourAt >= 0 ? -1 : 0));
|
|
310
|
+
bar.append(tb);
|
|
309
311
|
}
|
|
310
312
|
const pick = mk('select', 'mp-compare');
|
|
311
|
-
pick.setAttribute('aria-label', 'Compare with');
|
|
312
|
-
const none = mk('option', null, d.others?.length ? 'Compare with…' : 'Compare with: no other project has a map');
|
|
313
|
+
pick.setAttribute('aria-label', t('Compare with'));
|
|
314
|
+
const none = mk('option', null, d.others?.length ? t('Compare with…') : t('Compare with: no other project has a map'));
|
|
313
315
|
none.value = '';
|
|
314
316
|
pick.append(none);
|
|
315
317
|
for (const o of d.others ?? []) { const opt = mk('option', null, o.label); opt.value = o.cwd; pick.append(opt); }
|
|
@@ -317,20 +319,20 @@ export class MapView {
|
|
|
317
319
|
pick.disabled = !d.others?.length;
|
|
318
320
|
pick.addEventListener('change', () => this.hooks.onCompare?.(pick.value || null));
|
|
319
321
|
const legend = mk('div', 'mp-legend');
|
|
320
|
-
const lb = mk('button', 'btn sm ghost', 'Legend');
|
|
322
|
+
const lb = mk('button', 'btn sm ghost', t('Legend'));
|
|
321
323
|
lb.type = 'button';
|
|
322
324
|
lb.setAttribute('aria-expanded', String(this.legendOpen));
|
|
323
325
|
lb.addEventListener('click', () => { this.legendOpen = !this.legendOpen; this.render(); });
|
|
324
326
|
legend.append(lb);
|
|
325
327
|
if (this.legendOpen) {
|
|
326
328
|
const lg = mk('div', 'mp-legend-box');
|
|
327
|
-
for (const [kind, text] of [['evidence', 'A file proves this line'], ['inferred', 'Inferred, not proven'], ['model', 'Proposed by the model'], ['lit', 'A line of the selected node']]) {
|
|
329
|
+
for (const [kind, text] of [['evidence', t('A file proves this line')], ['inferred', t('Inferred, not proven')], ['model', t('Proposed by the model')], ['lit', t('A line of the selected node')]]) {
|
|
328
330
|
const row = mk('div', 'mp-legend-row');
|
|
329
331
|
const s = mk('span', 'mp-legend-line'); s.dataset.kind = kind;
|
|
330
332
|
row.append(s, mk('span', 'sub', text));
|
|
331
333
|
lg.append(row);
|
|
332
334
|
}
|
|
333
|
-
for (const [kind, text] of [['solid', 'A resource a file shows'], ['dashed', 'A resource only inferred']]) {
|
|
335
|
+
for (const [kind, text] of [['solid', t('A resource a file shows')], ['dashed', t('A resource only inferred')]]) {
|
|
334
336
|
const row = mk('div', 'mp-legend-row');
|
|
335
337
|
const s = mk('span', 'mp-legend-card'); s.dataset.kind = kind;
|
|
336
338
|
row.append(s, mk('span', 'sub', text));
|
|
@@ -348,7 +350,7 @@ export class MapView {
|
|
|
348
350
|
strip.append(mk('code', 'mp-file', s.file), mk('span', 'sub', `· ${s.at}`));
|
|
349
351
|
if (s.behind) { const b = mk('span', 'mp-pill', s.behind); b.dataset.tone = 'behind'; strip.append(b); }
|
|
350
352
|
if (s.behindNote) strip.append(mk('span', 'sub', s.behindNote));
|
|
351
|
-
if (d.compare?.reason) strip.append(mk('span', 'sub',
|
|
353
|
+
if (d.compare?.reason) strip.append(mk('span', 'sub', t('Compare: {reason}', { reason: d.compare.reason })));
|
|
352
354
|
strip.append(mk('span', 'row-fill'), mk('span', 'sub', s.rule));
|
|
353
355
|
return strip;
|
|
354
356
|
}
|
|
@@ -371,7 +373,7 @@ export class MapView {
|
|
|
371
373
|
const name = mk('span', 'mp-name', n.name ?? n.id);
|
|
372
374
|
name.title = n.name ?? n.id;
|
|
373
375
|
top.append(name);
|
|
374
|
-
if (count) { const f = mk('span', 'mp-fb', String(count)); f.title =
|
|
376
|
+
if (count) { const f = mk('span', 'mp-fb', String(count)); f.title = count === 1 ? t('1 finding') : t('{n} findings', { n: count }); top.append(f); }
|
|
375
377
|
top.append(mk('span', 'row-fill'));
|
|
376
378
|
if (n.kind === 'group') { card.append(top, mk('span', 'sub', `${n.members.length} modules · click to open`)); }
|
|
377
379
|
else if (n.kind !== 'module') { card.append(top, mk('span', 'sub', cardSub(n, doc))); }
|
|
@@ -386,7 +388,7 @@ export class MapView {
|
|
|
386
388
|
card.append(row);
|
|
387
389
|
}
|
|
388
390
|
if (this.layers.external) {
|
|
389
|
-
const fold = mk('span', `mp-fold${c.outside.length ? '' : ' none'}`, c.outside.length ? `▸ ${c.outside.join(' · ')}` : 'no outside calls');
|
|
391
|
+
const fold = mk('span', `mp-fold${c.outside.length ? '' : ' none'}`, c.outside.length ? `▸ ${c.outside.join(' · ')}` : t('no outside calls'));
|
|
390
392
|
if (c.outside.length) fold.title = c.outside.join('\n');
|
|
391
393
|
card.append(fold);
|
|
392
394
|
}
|
|
@@ -407,26 +409,26 @@ export class MapView {
|
|
|
407
409
|
|
|
408
410
|
#findings(f) {
|
|
409
411
|
const box = mk('section', 'mp-findings');
|
|
410
|
-
box.setAttribute('aria-label', 'Findings');
|
|
412
|
+
box.setAttribute('aria-label', t('Findings'));
|
|
411
413
|
const h = mk('div', 'mp-row');
|
|
412
|
-
h.append(mk('span', 'lbl',
|
|
414
|
+
h.append(mk('span', 'lbl', `${t('Findings')} · ${f.shown.length}`), mk('span', 'sub', t('{n} proven by a file · {m} inferred', { n: f.proven, m: f.shown.length - f.proven })), mk('span', 'row-fill'));
|
|
413
415
|
if (f.hidden) {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
h.append(
|
|
416
|
+
const tb = mk('button', 'btn sm ghost', this.showHidden ? t('Hide {n} hidden by default', { n: f.hidden }) : t('Show {n} hidden by default', { n: f.hidden }));
|
|
417
|
+
tb.type = 'button';
|
|
418
|
+
tb.title = t('Rules wrong too often to show unasked. Their findings are kept, not deleted.');
|
|
419
|
+
tb.addEventListener('click', () => { this.showHidden = !this.showHidden; this.render(); });
|
|
420
|
+
h.append(tb);
|
|
419
421
|
}
|
|
420
|
-
h.append(mk('span', 'sub mp-hint', 'Click a row to show it on the map'));
|
|
422
|
+
h.append(mk('span', 'sub mp-hint', t('Click a row to show it on the map')));
|
|
421
423
|
box.append(h);
|
|
422
|
-
if (!f.shown.length) box.append(mk('p', 'sub', f.total ? 'Every finding of this map is hidden by default.' : 'No finding in this map.'));
|
|
424
|
+
if (!f.shown.length) box.append(mk('p', 'sub', f.total ? t('Every finding of this map is hidden by default.') : t('No finding in this map.')));
|
|
423
425
|
const list = mk('div', 'mp-flist');
|
|
424
426
|
for (const x of f.shown) {
|
|
425
427
|
const row = mk('button', 'mp-frow');
|
|
426
428
|
row.type = 'button';
|
|
427
429
|
const dot = mk('span', 'mp-dot'); dot.dataset.proven = String(x.proven);
|
|
428
|
-
row.append(dot, mk('span', 'mp-ftitle', x.title), mk('span', 'mp-fwhere', x.where), mk('span', 'row-fill'), mk('span', 'mp-badge', x.proven ? 'proven' : 'inferred'));
|
|
429
|
-
if (x.hidden) row.title = x.hiddenReason ?? 'Hidden by default';
|
|
430
|
+
row.append(dot, mk('span', 'mp-ftitle', x.title), mk('span', 'mp-fwhere', x.where), mk('span', 'row-fill'), mk('span', 'mp-badge', x.proven ? t('proven') : t('inferred')));
|
|
431
|
+
if (x.hidden) row.title = x.hiddenReason ?? t('Hidden by default');
|
|
430
432
|
row.addEventListener('click', () => { if (x.modules[0]) this.select(x.modules[0], { reveal: true }); });
|
|
431
433
|
list.append(row);
|
|
432
434
|
}
|
|
@@ -446,7 +448,7 @@ export class MapView {
|
|
|
446
448
|
r.addEventListener('click', () => this.select(n.id));
|
|
447
449
|
return r;
|
|
448
450
|
});
|
|
449
|
-
this.list.replaceChildren(mk('span', 'lbl',
|
|
451
|
+
this.list.replaceChildren(mk('span', 'lbl', `${t('Modules')} · ${rows.length}`), ...items);
|
|
450
452
|
}
|
|
451
453
|
|
|
452
454
|
#inspector(node, doc, edges, findings) {
|
|
@@ -455,71 +457,82 @@ export class MapView {
|
|
|
455
457
|
// The project's overview, when /crew-map wrote one: what it says, and the lines it rests on.
|
|
456
458
|
const ov = doc?.overview;
|
|
457
459
|
if (typeof ov?.text === 'string' && ov.text.trim()) {
|
|
458
|
-
parts.push(mk('span', 'lbl', 'Overview'), mk('p', 'mp-overview', ov.text));
|
|
460
|
+
parts.push(mk('span', 'lbl', t('Overview')), mk('p', 'mp-overview', ov.text));
|
|
459
461
|
if (Array.isArray(ov.cites) && ov.cites.length) parts.push(mk('p', 'sub mp-cites', `From ${ov.cites.filter((c) => typeof c === 'string').join(', ')}`));
|
|
460
462
|
}
|
|
461
|
-
parts.push(mk('p', 'sub', 'Select a module to see what it connects to and what was found in it. Esc lets it go.'));
|
|
463
|
+
parts.push(mk('p', 'sub', t('Select a module to see what it connects to and what was found in it. Esc lets it go.')));
|
|
462
464
|
} else {
|
|
463
465
|
const v = inspectOf(node, doc, edges, findings);
|
|
464
466
|
const top = mk('div', 'mp-row');
|
|
465
467
|
const close = mk('button', 'btn icon sm ghost', '×');
|
|
466
|
-
close.type = 'button'; close.setAttribute('aria-label', 'Close inspector');
|
|
468
|
+
close.type = 'button'; close.setAttribute('aria-label', t('Close inspector'));
|
|
467
469
|
close.addEventListener('click', () => this.select(null));
|
|
468
470
|
top.append(mk('span', 'mp-name', v.title), mk('span', 'row-fill'), close);
|
|
469
471
|
parts.push(top);
|
|
470
472
|
if (v.layer || v.domain) {
|
|
471
473
|
const pills = mk('div', 'mp-row');
|
|
472
|
-
if (v.layer) pills.append(mk('span', 'mp-pill',
|
|
473
|
-
if (v.domain) pills.append(mk('span', 'mp-pill',
|
|
474
|
+
if (v.layer) pills.append(mk('span', 'mp-pill', `${t('Layer')} · ${v.layer}`));
|
|
475
|
+
if (v.domain) pills.append(mk('span', 'mp-pill', `${t('Domain')} · ${v.domain}`));
|
|
474
476
|
parts.push(pills);
|
|
475
477
|
}
|
|
476
478
|
if (v.path) parts.push(mk('code', 'sub', v.path));
|
|
477
479
|
if (v.chips.length) { const c = mk('div', 'mp-chips'); for (const t of v.chips) c.append(mk('span', 'mp-chip', t)); parts.push(c); }
|
|
478
480
|
const tabs = mk('nav', 'mp-tabs');
|
|
479
|
-
for (const [k, label] of [['overview', 'Overview'], ['connections',
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
tabs.append(
|
|
481
|
+
for (const [k, label] of [['overview', t('Overview')], ['connections', `${t('Connections')} ${v.connections.out.length + v.connections.in.length}`], ['findings', `${t('Findings')} ${v.findings.length}`]]) {
|
|
482
|
+
const tb = mk('button', 'mp-tab', label);
|
|
483
|
+
tb.type = 'button';
|
|
484
|
+
tb.setAttribute('aria-selected', String(this.tab === k));
|
|
485
|
+
tb.addEventListener('click', () => { this.tab = k; this.render(); });
|
|
486
|
+
tabs.append(tb);
|
|
485
487
|
}
|
|
486
488
|
parts.push(tabs);
|
|
487
489
|
const sec = mk('div', 'mp-isec');
|
|
488
490
|
if (this.tab === 'overview') {
|
|
489
|
-
sec.append(mk('span', 'lbl', 'What it is for'), mk('p', v.narrative ? null : 'sub', v.narrative ?? v.narrativeNote));
|
|
490
|
-
if (v.narrative && v.narrativeCites.length) sec.append(mk('p', 'sub mp-cites',
|
|
491
|
+
sec.append(mk('span', 'lbl', t('What it is for')), mk('p', v.narrative ? null : 'sub', v.narrative ?? v.narrativeNote));
|
|
492
|
+
if (v.narrative && v.narrativeCites.length) sec.append(mk('p', 'sub mp-cites', t('From {files}', { files: v.narrativeCites.join(', ') })));
|
|
491
493
|
if (v.inside && (v.inside.entry || v.inside.folders.length)) {
|
|
492
|
-
sec.append(mk('span', 'lbl', 'Inside'));
|
|
494
|
+
sec.append(mk('span', 'lbl', t('Inside')));
|
|
493
495
|
if (v.inside.entry) { const r = mk('div', 'mp-conn mp-inside'); r.append(this.#cite(v.inside.entry), mk('span', 'mp-ev', 'entry point')); sec.append(r); }
|
|
494
496
|
for (const f of v.inside.folders) { const r = mk('div', 'mp-conn mp-inside'); r.append(mk('span', 'mp-file', `${f.folder === '.' ? '(root)' : `${f.folder}/`}`), mk('span', 'mp-ev', `${f.files} ${f.files === 1 ? 'file' : 'files'}`)); sec.append(r); }
|
|
495
|
-
if (v.inside.other) sec.append(mk('p', 'sub',
|
|
496
|
-
if (v.inside.tests) sec.append(mk('p', 'sub',
|
|
497
|
+
if (v.inside.other) sec.append(mk('p', 'sub', t('{n} more files in other folders', { n: v.inside.other })));
|
|
498
|
+
if (v.inside.tests) sec.append(mk('p', 'sub', v.inside.tests === 1 ? t('1 test file') : t('{n} test files', { n: v.inside.tests })));
|
|
497
499
|
}
|
|
498
|
-
sec.append(mk('
|
|
499
|
-
if (
|
|
500
|
+
if (v.changed) sec.append(mk('p', 'sub', v.changed));
|
|
501
|
+
if (v.testFiles.length) {
|
|
502
|
+
sec.append(mk('span', 'lbl', `${t('Tests')} · ${v.testTotal}`));
|
|
503
|
+
for (const f of v.testFiles) { const r = mk('div', 'mp-conn mp-inside'); r.append(mk('span', 'mp-file', f)); sec.append(r); }
|
|
504
|
+
if (v.testTotal > v.testFiles.length) sec.append(mk('p', 'sub', t('{n} more in the map file', { n: v.testTotal - v.testFiles.length })));
|
|
505
|
+
}
|
|
506
|
+
if (v.notes.length) {
|
|
507
|
+
sec.append(mk('span', 'lbl', `${t('Notes in the code')} · ${v.notesTotal}`));
|
|
508
|
+
for (const x of v.notes) { const r = mk('div', 'mp-conn mp-inside'); r.append(this.#cite(x.at), mk('span', 'mp-ev', x.text)); sec.append(r); }
|
|
509
|
+
if (v.notesTotal > v.notes.length) sec.append(mk('p', 'sub', t('{n} more in the map file', { n: v.notesTotal - v.notes.length })));
|
|
510
|
+
}
|
|
511
|
+
sec.append(mk('span', 'lbl', t('Connects to')));
|
|
512
|
+
if (!v.connectsTo.length) sec.append(mk('p', 'sub', t('Nothing on this map.')));
|
|
500
513
|
for (const c of v.connectsTo) sec.append(this.#conn(c));
|
|
501
514
|
if (v.evidence.length) {
|
|
502
|
-
sec.append(mk('span', 'lbl',
|
|
515
|
+
sec.append(mk('span', 'lbl', `${t('Evidence')} · ${v.evidenceTotal}`));
|
|
503
516
|
for (const e of v.evidence) sec.append(this.#cite(e));
|
|
504
|
-
if (v.evidenceTotal > v.evidence.length) sec.append(mk('p', 'sub',
|
|
517
|
+
if (v.evidenceTotal > v.evidence.length) sec.append(mk('p', 'sub', t('{n} more in the map file', { n: v.evidenceTotal - v.evidence.length })));
|
|
505
518
|
}
|
|
506
519
|
} else if (this.tab === 'connections') {
|
|
507
|
-
if (!v.byKind.length) sec.append(mk('p', 'sub', 'Nothing on this map.'));
|
|
520
|
+
if (!v.byKind.length) sec.append(mk('p', 'sub', t('Nothing on this map.')));
|
|
508
521
|
for (const [kind, { out, in: into }] of v.byKind) {
|
|
509
522
|
sec.append(mk('span', 'lbl', `${kind} · ${out.length + into.length}`));
|
|
510
523
|
for (const c of out) sec.append(this.#conn(c, 'out'));
|
|
511
524
|
for (const c of into) sec.append(this.#conn(c, 'in'));
|
|
512
525
|
}
|
|
513
526
|
} else {
|
|
514
|
-
if (!v.findings.length) sec.append(mk('p', 'sub', 'No finding shown for this node.'));
|
|
527
|
+
if (!v.findings.length) sec.append(mk('p', 'sub', t('No finding shown for this node.')));
|
|
515
528
|
for (const f of v.findings) {
|
|
516
529
|
const card = mk('div', 'mp-fcard');
|
|
517
530
|
card.dataset.proven = String(f.proven);
|
|
518
531
|
const r = mk('div', 'mp-row');
|
|
519
|
-
r.append(mk('span', 'mp-ftitle', f.title), mk('span', 'row-fill'), mk('span', 'mp-badge', f.proven ? 'proven' : 'inferred'));
|
|
520
|
-
const b = mk('button', 'btn sm', 'Copy as task');
|
|
532
|
+
r.append(mk('span', 'mp-ftitle', f.title), mk('span', 'row-fill'), mk('span', 'mp-badge', f.proven ? t('proven') : t('inferred')));
|
|
533
|
+
const b = mk('button', 'btn sm', t('Copy as task'));
|
|
521
534
|
b.type = 'button';
|
|
522
|
-
b.title = 'Copies the finding as a task to paste into a session. Nothing is run.';
|
|
535
|
+
b.title = t('Copies the finding as a task to paste into a session. Nothing is run.');
|
|
523
536
|
b.addEventListener('click', () => this.hooks.onCopy?.(taskOf(f), 'task'));
|
|
524
537
|
card.append(r);
|
|
525
538
|
if (f.where) card.append(this.#cite(f.where));
|
|
@@ -530,8 +543,8 @@ export class MapView {
|
|
|
530
543
|
parts.push(sec);
|
|
531
544
|
}
|
|
532
545
|
const nm = mk('div', 'mp-notmeasured');
|
|
533
|
-
nm.append(mk('span', 'lbl', 'Not measured'));
|
|
534
|
-
if (!doc.notMeasured.length) nm.append(mk('p', 'sub', 'Nothing: every file of the project was read.'));
|
|
546
|
+
nm.append(mk('span', 'lbl', t('Not measured')));
|
|
547
|
+
if (!doc.notMeasured.length) nm.append(mk('p', 'sub', t('Nothing: every file of the project was read.')));
|
|
535
548
|
for (const x of doc.notMeasured) nm.append(mk('p', 'sub', `${x.what} — ${x.reason}`));
|
|
536
549
|
parts.push(nm);
|
|
537
550
|
this.side.replaceChildren(...parts);
|
|
@@ -543,11 +556,11 @@ export class MapView {
|
|
|
543
556
|
if (this.tourAt < 0 || !stops.length) { this.tourBox.hidden = true; return; }
|
|
544
557
|
const s = stops[Math.min(this.tourAt, stops.length - 1)];
|
|
545
558
|
const head = mk('div', 'mp-row');
|
|
546
|
-
head.append(mk('span', 'lbl',
|
|
559
|
+
head.append(mk('span', 'lbl', `${t('Tour')} · ${t('{at} of {n}', { at: this.tourAt + 1, n: stops.length })}`), mk('span', 'mp-name', s.name), mk('span', 'row-fill'));
|
|
547
560
|
const nav = (label, at, off) => { const b = mk('button', 'btn sm ghost', label); b.type = 'button'; b.disabled = off; b.addEventListener('click', () => this.tour(at)); return b; };
|
|
548
|
-
head.append(nav('Back', this.tourAt - 1, this.tourAt === 0), nav(this.tourAt === stops.length - 1 ? 'Done' : 'Next', this.tourAt === stops.length - 1 ? -1 : this.tourAt + 1, false));
|
|
561
|
+
head.append(nav(t('Back'), this.tourAt - 1, this.tourAt === 0), nav(this.tourAt === stops.length - 1 ? t('Done') : t('Next'), this.tourAt === stops.length - 1 ? -1 : this.tourAt + 1, false));
|
|
549
562
|
const body = [head, mk('p', null, s.text)];
|
|
550
|
-
if (s.cites.length) body.push(mk('p', 'sub mp-cites',
|
|
563
|
+
if (s.cites.length) body.push(mk('p', 'sub mp-cites', t('From {files}', { files: s.cites.join(', ') })));
|
|
551
564
|
this.tourBox.replaceChildren(...body);
|
|
552
565
|
this.tourBox.hidden = false;
|
|
553
566
|
}
|
|
@@ -556,7 +569,7 @@ export class MapView {
|
|
|
556
569
|
#cite(text) {
|
|
557
570
|
const b = mk('button', 'mp-ev mp-cite', text);
|
|
558
571
|
b.type = 'button';
|
|
559
|
-
b.title = 'Copy the path';
|
|
572
|
+
b.title = t('Copy the path');
|
|
560
573
|
b.addEventListener('click', () => this.hooks.onCopy?.(text, 'path'));
|
|
561
574
|
return b;
|
|
562
575
|
}
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// apart from the ones that passed or failed.
|
|
6
6
|
import { roleName, modelName } from './names.js';
|
|
7
7
|
import { fmtCost, fmtCount } from './usage.js';
|
|
8
|
+
import { t } from './i18n.js';
|
|
8
9
|
|
|
9
10
|
const TIER = { haiku: 'Haiku', sonnet: 'Sonnet', opus: 'Opus', fable: 'Fable' };
|
|
10
|
-
export const tierWord = (
|
|
11
|
+
export const tierWord = (k) => TIER[k] ?? (k ? String(k) : t('no model'));
|
|
11
12
|
const pct = (r) => `${Math.round(r * 100)}%`;
|
|
12
|
-
const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The task card a crew agent's work starts with: three lines at the top of its task text.
|
|
@@ -27,7 +27,7 @@ export function cardOf(prompt) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const VERIFY_TONE = { pass: 'pass', fail: 'fail' };
|
|
30
|
-
const VERIFY_SAYS = { none: 'no verify command', blocked: 'verify not run: the command was not allowed', timeout: 'verify not finished: the command ran out of time' };
|
|
30
|
+
const VERIFY_SAYS = { none: t('no verify command'), blocked: t('verify not run: the command was not allowed'), timeout: t('verify not finished: the command ran out of time') };
|
|
31
31
|
|
|
32
32
|
/** The card as the inspector lists it: the three lines, each kept as it was written. */
|
|
33
33
|
export function cardLines(card, row) {
|
|
@@ -40,11 +40,11 @@ export function cardLines(card, row) {
|
|
|
40
40
|
|
|
41
41
|
function stepOf(r, again) {
|
|
42
42
|
const on = r.ranOn ? modelName(r.ranOn) : tierWord(r.model);
|
|
43
|
-
const bits = [again ?
|
|
44
|
-
if (r.fixes > 0) bits.push(
|
|
43
|
+
const bits = [again ? t('Re-run on {model}', { model: on }) : t('Ran on {model}', { model: on })];
|
|
44
|
+
if (r.fixes > 0) bits.push(r.fixes === 1 ? t('fixed once inside the agent') : t('fixed {n} times inside the agent', { n: r.fixes }));
|
|
45
45
|
// A verdict is a tag (pass, fail); anything else is said in words, because it is no verdict.
|
|
46
46
|
const tone = VERIFY_TONE[r.verify] ?? null;
|
|
47
|
-
if (!tone) bits.push(VERIFY_SAYS[r.verify] ??
|
|
47
|
+
if (!tone) bits.push(VERIFY_SAYS[r.verify] ?? t('verify {verdict}', { verdict: r.verify }));
|
|
48
48
|
return { text: bits.join(' \u00b7 '), verify: tone ? r.verify : null, tone, escalated: again };
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -55,13 +55,13 @@ function stepOf(r, again) {
|
|
|
55
55
|
* @returns { risk, riskWhy, steps: [{ text, verify, tone, escalated }], note }
|
|
56
56
|
*/
|
|
57
57
|
export function workSteps(row) {
|
|
58
|
-
if (!row) return { risk: null, riskWhy: null, steps: [], note: 'Not recorded yet: the line is written when the agent stops.' };
|
|
58
|
+
if (!row) return { risk: null, riskWhy: null, steps: [], note: t('Not recorded yet: the line is written when the agent stops.') };
|
|
59
59
|
const steps = [...(row.before ?? []).map((r) => stepOf(r, false)), stepOf(row, Boolean(row.escalatedFrom))];
|
|
60
60
|
if (row.escalatedFrom && !(row.before ?? []).length) {
|
|
61
|
-
steps.unshift({ text:
|
|
61
|
+
steps.unshift({ text: t('An earlier call on {model} did not pass; its line is not in the record', { model: tierWord(row.escalatedFrom) }), verify: null, tone: null, escalated: false });
|
|
62
62
|
}
|
|
63
63
|
// The reason is the gate's own sentence, kept as it wrote it; a line written before the gate kept one has none.
|
|
64
|
-
return { risk: row.risk ?? null, riskWhy: row.risk === 'critical' ? row.riskWhy ?? null : null, steps, note: row.escalatedFrom ? 'A re-run with the same or a lower model is refused by the gate.' : null };
|
|
64
|
+
return { risk: row.risk ?? null, riskWhy: row.risk === 'critical' ? row.riskWhy ?? null : null, steps, note: row.escalatedFrom ? t('A re-run with the same or a lower model is refused by the gate.') : null };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -70,7 +70,7 @@ export function workSteps(row) {
|
|
|
70
70
|
*/
|
|
71
71
|
export function outsideRouting(agentType) {
|
|
72
72
|
if (typeof agentType !== 'string' || !agentType || agentType.startsWith('crew-')) return null;
|
|
73
|
-
return 'Outside model routing: not a crew agent, so it has no floor and Crewforth keeps no outcome line for it. The gate only holds the expensive end: a call that asks for opus or fable, or names no model and would take the session\u2019s, is held at sonnet unless its task carries an above: <why> line or opens with a card naming critical work. An agent whose own definition names a model keeps it.';
|
|
73
|
+
return t('Outside model routing: not a crew agent, so it has no floor and Crewforth keeps no outcome line for it. The gate only holds the expensive end: a call that asks for opus or fable, or names no model and would take the session\u2019s, is held at sonnet unless its task carries an above: <why> line or opens with a card naming critical work. An agent whose own definition names a model keeps it.');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -80,12 +80,12 @@ export function outsideRouting(agentType) {
|
|
|
80
80
|
*/
|
|
81
81
|
export function floorWords(floor) {
|
|
82
82
|
if (!floor) return { floorTag: null, raised: null, raisedWhy: '' };
|
|
83
|
-
const why =
|
|
83
|
+
const why = t('Raised to {model} automatically: {n} of {calls} calls one model down did not pass first time', { model: tierWord(floor.model), n: floor.notFirstTry, calls: floor.calls ?? '?' });
|
|
84
84
|
if (!floor.lowered) return { floorTag: 'raised', raised: tierWord(floor.model), raisedWhy: why };
|
|
85
85
|
return {
|
|
86
86
|
floorTag: 'lowered',
|
|
87
87
|
raised: null,
|
|
88
|
-
raisedWhy: `${why}. You lowered it to
|
|
88
|
+
raisedWhy: `${why}. ${t('You lowered it to {model} on {date}; its outcomes are counted afresh from then.', { model: tierWord(floor.lowered.to), date: String(floor.lowered.ts).slice(0, 10) })}`,
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -95,15 +95,15 @@ export function floorWords(floor) {
|
|
|
95
95
|
* ("\u2265$0.75 \u00b7 2/7"), because a sum of two calls must not be read as the cost of seven. None: a dash.
|
|
96
96
|
*/
|
|
97
97
|
export function costCell(cost, runs) {
|
|
98
|
-
if (!cost) return { cost: '\u2026', costNote: 'Reading what this class cost\u2026', partial: false, advisor: null };
|
|
99
|
-
if (!cost.costed) return { cost: '\u2014', costNote: 'No agent of this class has a transcript on this machine that could be priced', partial: false, advisor: null };
|
|
98
|
+
if (!cost) return { cost: '\u2026', costNote: t('Reading what this class cost\u2026'), partial: false, advisor: null };
|
|
99
|
+
if (!cost.costed) return { cost: '\u2014', costNote: t('No agent of this class has a transcript on this machine that could be priced'), partial: false, advisor: null };
|
|
100
100
|
const usd = fmtCost(cost.cost);
|
|
101
101
|
const advisor = advisorWords(cost);
|
|
102
102
|
const tail = advisor ? `\n${advisor}` : '';
|
|
103
|
-
if (cost.costed >= runs) return { cost: usd, costNote:
|
|
103
|
+
if (cost.costed >= runs) return { cost: usd, costNote: `${runs === 1 ? t('The 1 task priced, at API list price. An estimate: what a subscription is charged is not known here.') : t('All {n} tasks priced, at API list price. An estimate: what a subscription is charged is not known here.', { n: runs })}${tail}`, partial: false, advisor };
|
|
104
104
|
return {
|
|
105
105
|
cost: `\u2265${usd.replace(/^~/, '')} \u00b7 ${cost.costed}/${runs}`,
|
|
106
|
-
costNote:
|
|
106
|
+
costNote: `${t('At least this much: {n} of {runs} tasks could be priced; the others have no transcript on this machine. At API list price.', { n: cost.costed, runs })}${tail}`,
|
|
107
107
|
partial: true,
|
|
108
108
|
advisor,
|
|
109
109
|
};
|
|
@@ -116,17 +116,18 @@ export function costCell(cost, runs) {
|
|
|
116
116
|
export function advisorWords(cost) {
|
|
117
117
|
const n = cost?.advisorCalls ?? 0;
|
|
118
118
|
if (!(n > 0)) return null;
|
|
119
|
-
const share = typeof cost.advisorCost === 'number' && cost.advisorCost > 0 ?
|
|
120
|
-
return
|
|
119
|
+
const share = typeof cost.advisorCost === 'number' && cost.advisorCost > 0 ? t('{cost} of it', { cost: fmtCost(cost.advisorCost) }) : t('its cost not priced');
|
|
120
|
+
return n === 1 ? t('Advisor: 1 call, {share}. A second model the agents consulted, at that model\'s rates; included above.', { share })
|
|
121
|
+
: t('Advisor: {n} calls, {share}. A second model the agents consulted, at that model\'s rates; included above.', { n, share });
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/** One class as the view's row: who and what kind of change, how risky, what ran it, and how it went. */
|
|
124
125
|
export function classRow(c, cost = null) {
|
|
125
|
-
const models = Object.entries(c.models ?? {}).sort((a, b) => b[1].runs - a[1].runs).map(([
|
|
126
|
+
const models = Object.entries(c.models ?? {}).sort((a, b) => b[1].runs - a[1].runs).map(([k, m]) => `${tierWord(k)} ${m.runs}`).join(' \u00b7 ');
|
|
126
127
|
const apart = [
|
|
127
|
-
c.notVerified?.none ?
|
|
128
|
-
c.notVerified?.blocked ?
|
|
129
|
-
c.notVerified?.timeout ?
|
|
128
|
+
c.notVerified?.none ? t('{n} with no verify command', { n: c.notVerified.none }) : null,
|
|
129
|
+
c.notVerified?.blocked ? t('{n} not run', { n: c.notVerified.blocked }) : null,
|
|
130
|
+
c.notVerified?.timeout ? t('{n} timed out', { n: c.notVerified.timeout }) : null,
|
|
130
131
|
].filter(Boolean);
|
|
131
132
|
return {
|
|
132
133
|
key: c.key,
|
|
@@ -141,10 +142,10 @@ export function classRow(c, cost = null) {
|
|
|
141
142
|
tasks: String(c.runs),
|
|
142
143
|
// The rate and what it is a rate of: "83%" of five calls is not "83%" of five hundred.
|
|
143
144
|
firstTry: c.firstTryRate === null ? '\u2014' : pct(c.firstTryRate),
|
|
144
|
-
firstTryOf: c.firstTryRate === null ? 'No call of this class got a verdict from its verify command' :
|
|
145
|
+
firstTryOf: c.firstTryRate === null ? t('No call of this class got a verdict from its verify command') : t('{n} of {of} verified calls passed the first time', { n: c.firstTry, of: c.verified }),
|
|
145
146
|
rate: c.firstTryRate,
|
|
146
147
|
notVerified: apart.join(' \u00b7 '),
|
|
147
|
-
after: [c.fixed ?
|
|
148
|
+
after: [c.fixed ? t('{n} fixed by the agent', { n: c.fixed }) : null, c.failed ? t('{n} failed', { n: c.failed }) : null].filter(Boolean).join(' \u00b7 '),
|
|
148
149
|
escalated: String(c.escalated ?? 0),
|
|
149
150
|
...costCell(cost, c.runs),
|
|
150
151
|
};
|
|
@@ -164,8 +165,8 @@ export function lowerCommand(s) {
|
|
|
164
165
|
export function suggestionText(s) {
|
|
165
166
|
return {
|
|
166
167
|
title: `${roleName(s.agent)} \u00b7 ${s.change}`,
|
|
167
|
-
says:
|
|
168
|
-
how: 'Yours to decide: send the command in your own session. Studio does not write it.',
|
|
168
|
+
says: t('passed {n} of {n} on {from}: try {to} for this class?', { n: s.passed, from: tierWord(s.from), to: tierWord(s.to) }),
|
|
169
|
+
how: t('Yours to decide: send the command in your own session. Studio does not write it.'),
|
|
169
170
|
command: lowerCommand(s),
|
|
170
171
|
};
|
|
171
172
|
}
|
|
@@ -179,43 +180,47 @@ export function suggestionText(s) {
|
|
|
179
180
|
*/
|
|
180
181
|
export function aboveText(a, cost = null) {
|
|
181
182
|
const to = tierWord(a.to);
|
|
182
|
-
const ran = Object.entries(a.models ?? {}).sort((x, y) => y[1] - x[1]).map(([
|
|
183
|
+
const ran = Object.entries(a.models ?? {}).sort((x, y) => y[1] - x[1]).map(([k, n]) => t('{n} on {model}', { n, model: tierWord(k) })).join(', ');
|
|
183
184
|
let costs = null;
|
|
184
185
|
if (cost?.costed) {
|
|
185
186
|
const some = cost.costed < cost.runs;
|
|
186
187
|
const usd = (v) => (some ? `\u2265${fmtCost(v).replace(/^~/, '')}` : fmtCost(v));
|
|
187
|
-
costs =
|
|
188
|
+
costs = some
|
|
189
|
+
? t('{cost} as run; the same tokens at {model}\'s price {normal} ({n} of {runs} priced). API list price; another model would not spend the same tokens.', { cost: usd(cost.cost), model: to, normal: usd(cost.atNormal), n: cost.costed, runs: cost.runs })
|
|
190
|
+
: t('{cost} as run; the same tokens at {model}\'s price {normal}. API list price; another model would not spend the same tokens.', { cost: usd(cost.cost), model: to, normal: usd(cost.atNormal) });
|
|
188
191
|
}
|
|
189
192
|
return {
|
|
190
193
|
title: `${roleName(a.agent)} \u00b7 ${a.change}`,
|
|
191
|
-
says:
|
|
194
|
+
says: a.runs === 1 ? t('normal risk, 1 task above {model}: {ran}.', { model: to, ran }) : t('normal risk, {n} tasks above {model}: {ran}.', { n: a.runs, model: to, ran }),
|
|
192
195
|
verdict: a.passed
|
|
193
|
-
?
|
|
194
|
-
:
|
|
196
|
+
? t('{model} is enough for this class: its latest {n} verified calls on {model} all passed the first time.', { model: to, n: a.passed })
|
|
197
|
+
: a.need != null
|
|
198
|
+
? t('Whether {model} would have done is not measured: this class does not have {n} verified calls on {model} that all passed the first time.', { model: to, n: a.need })
|
|
199
|
+
: t('Whether {model} would have done is not measured: this class does not have enough verified calls on {model} that all passed the first time.', { model: to }),
|
|
195
200
|
measured: Boolean(a.passed),
|
|
196
201
|
costs,
|
|
197
202
|
};
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
export const ABOVE_HEAD = 'Above the table \u00b7 nothing is blocked';
|
|
201
|
-
export const ABOVE_RULE = 'The gate holds a floor, not a ceiling: which model runs normal work is yours to choose.';
|
|
205
|
+
export const ABOVE_HEAD = t('Above the table \u00b7 nothing is blocked');
|
|
206
|
+
export const ABOVE_RULE = t('The gate holds a floor, not a ceiling: which model runs normal work is yours to choose.');
|
|
202
207
|
|
|
203
208
|
/** What the view says when there is nothing to list. */
|
|
204
209
|
export function emptyNote(data) {
|
|
205
|
-
if (!data) return 'Reading the record…';
|
|
206
|
-
if (data.measured === false) return
|
|
207
|
-
if (!data.classes?.length) return 'The record is there and has no line yet.';
|
|
210
|
+
if (!data) return t('Reading the record…');
|
|
211
|
+
if (data.measured === false) return t('Not measured — {reason}', { reason: data.reason });
|
|
212
|
+
if (!data.classes?.length) return t('The record is there and has no line yet.');
|
|
208
213
|
return null;
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
export const SOURCE = 'Source: .claude/state/model-outcomes.tsv';
|
|
212
|
-
export const LOWER_RULE = 'Lowering a floor is never automatic. Raising it is.';
|
|
216
|
+
export const SOURCE = t('Source: .claude/state/model-outcomes.tsv');
|
|
217
|
+
export const LOWER_RULE = t('Lowering a floor is never automatic. Raising it is.');
|
|
213
218
|
|
|
214
219
|
/** What was read for cost, for the header's tooltip; null before it has been. */
|
|
215
|
-
export const costLine = (costs) => (costs?.measured ?
|
|
220
|
+
export const costLine = (costs) => (costs?.measured ? (costs.sessionsRead === 1 ? t('1 session read for cost') : t('{n} sessions read for cost', { n: fmtCount(costs.sessionsRead ?? 0) })) : null);
|
|
216
221
|
|
|
217
222
|
export const totalsLine = (data) => [
|
|
218
|
-
`${
|
|
219
|
-
data.dropped ? `${data.dropped
|
|
220
|
-
data.truncated ? 'only the end of a long record was read' : null,
|
|
223
|
+
`${(data.rows ?? 0) === 1 ? t('1 task') : t('{n} tasks', { n: data.rows ?? 0 })} ${(data.classes?.length ?? 0) === 1 ? t('in 1 class') : t('in {n} classes', { n: data.classes?.length ?? 0 })}`,
|
|
224
|
+
data.dropped ? `${data.dropped === 1 ? t('1 line not read') : t('{n} lines not read', { n: data.dropped })} (${Object.entries(data.droppedWhy ?? {}).map(([why, n]) => `${n}: ${why}`).join('; ') || t('no reason kept')})` : null,
|
|
225
|
+
data.truncated ? t('only the end of a long record was read') : null,
|
|
221
226
|
].filter(Boolean).join(' · ');
|