crewforth 3.1.1 โ 3.2.0-rc.10
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 +323 -0
- package/README.md +6 -3
- package/VERSION +1 -1
- package/adopt.sh +66 -6
- package/bin/cli.js +3 -2
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +15 -14
- package/kit/agents/crew-commit-agent.md +11 -4
- package/kit/agents/crew-docs-agent.md +71 -0
- package/kit/agents/crew-planner.md +2 -0
- package/kit/agents/crew-refuter.md +77 -0
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/agents/crew-security-expert.md +3 -1
- package/kit/agents/crew-session-manager.md +11 -9
- package/kit/eval/doctor.sh +128 -6
- package/kit/eval/gate-report.sh +2 -0
- package/kit/eval/golden-routing.txt +9 -0
- package/kit/eval/lib/crew-map/crew-map.sh +26 -0
- package/kit/eval/lib/crew-map/detect.mjs +238 -0
- package/kit/eval/lib/crew-map/narrative.mjs +132 -0
- package/kit/eval/lib/crew-map/parse.mjs +305 -0
- package/kit/eval/lib/crew-map/project.mjs +164 -0
- package/kit/eval/lib/crew-map/rules/names.json +38 -0
- package/kit/eval/lib/crew-map/rules/packages.json +142 -0
- package/kit/eval/lib/crew-map/rules/python-stdlib.json +197 -0
- package/kit/eval/lib/crew-map/rules/tech.json +35 -0
- package/kit/eval/lib/crew-map/scan.mjs +266 -0
- package/kit/eval/lib/crew-map/stacks.mjs +547 -0
- package/kit/eval/lib/crew-map/surface.mjs +535 -0
- package/kit/eval/lib/findings/findings.mjs +410 -0
- package/kit/eval/lib/findings/findings.schema.json +122 -0
- package/kit/eval/lib/findings/findings.sh +29 -0
- package/kit/eval/lib/gate-log.sh +52 -0
- package/kit/eval/lib/json-string.awk +27 -0
- package/kit/eval/lib/preset.sh +109 -0
- package/kit/eval/lib/presets/default.json +7 -0
- package/kit/eval/lib/presets/solo.json +7 -0
- package/kit/eval/lib/presets/strict.json +11 -0
- package/kit/eval/lib/preview.sh +150 -0
- package/kit/eval/lib/review-bench/bench.mjs +273 -0
- package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
- package/kit/eval/lib/review-prepass/prepass.sh +23 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +19 -4
- package/kit/eval/lib/star.sh +23 -7
- package/kit/eval/lib/statusline.settings.json +6 -0
- package/kit/eval/lib/statusline.sh +100 -0
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +878 -53
- package/kit/hooks/agent-outcome.sh +44 -12
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +177 -24
- package/kit/hooks/guard-bash.sh +295 -204
- package/kit/hooks/guard-commit-scan.sh +10 -2
- package/kit/hooks/guard-question.sh +73 -0
- package/kit/hooks/guard-write.sh +24 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/prompt-approval.sh +389 -103
- package/kit/hooks/refuter-ledger.sh +80 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/hooks/trace-blocklist.txt +9 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +51 -0
- package/kit/skills/crew-approve/SKILL.md +7 -0
- package/kit/skills/crew-code-review/SKILL.md +16 -0
- package/kit/skills/crew-doctor/SKILL.md +5 -1
- package/kit/skills/crew-handoff/SKILL.md +3 -2
- package/kit/skills/crew-map/SKILL.md +55 -0
- package/kit/skills/crew-review/SKILL.md +4 -1
- package/kit/skills/crew-ship/SKILL.md +3 -1
- package/kit/skills/crew-update/SKILL.md +1 -1
- package/kit/skills/incident-runbook/SKILL.md +2 -2
- package/kit/skills/security-scan/references/verify.md +6 -3
- package/kit/studio/README.md +25 -2
- package/kit/studio/server/index.js +187 -21
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/crew-map.js +113 -0
- package/kit/studio/server/lib/graph.js +9 -1
- package/kit/studio/server/lib/kit-telemetry.js +30 -5
- package/kit/studio/server/lib/lan.js +87 -0
- package/kit/studio/server/lib/model-outcomes.js +130 -15
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/pricing.js +3 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/redact.js +60 -0
- package/kit/studio/server/lib/remote.js +185 -0
- package/kit/studio/server/lib/session.js +1 -1
- package/kit/studio/server/lib/terminal.js +51 -13
- package/kit/studio/server/lib/transcript.js +32 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +287 -17
- package/kit/studio/web/canvas.js +58 -122
- package/kit/studio/web/chat.js +114 -24
- package/kit/studio/web/graph-plan.js +13 -6
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +410 -0
- package/kit/studio/web/map.js +596 -0
- package/kit/studio/web/models-plan.js +61 -7
- package/kit/studio/web/models.js +19 -3
- package/kit/studio/web/qr.js +200 -0
- package/kit/studio/web/style.css +165 -0
- package/kit/studio/web/tokens.css +6 -0
- package/kit/studio/web/usage.js +12 -0
- package/kit/studio/web/viewport.js +160 -0
- package/package.json +6 -5
- package/start.sh +24 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// prepass.mjs โ the review's deterministic first pass: cheap checks over the lines a change ADDS, run before the
|
|
3
|
+
// review agent reads the diff, written as a findings document (crewforth.findings/1) the agent starts from.
|
|
4
|
+
//
|
|
5
|
+
// node prepass.mjs [--root DIR] [--base REF] [--stdout]
|
|
6
|
+
// default the staged change (git diff --cached), what the next commit will hold
|
|
7
|
+
// --base the working tree against REF (a branch under review: --base origin/next)
|
|
8
|
+
// --stdout print the document, write nothing; otherwise .crewforth/review-prepass.json, through findings.mjs
|
|
9
|
+
// rc 0 = written (or printed), whatever it found ยท 1 = the document did not verify, nothing written ยท
|
|
10
|
+
// 2 = usage, or not a git repository
|
|
11
|
+
//
|
|
12
|
+
// The checks are chosen by the file, and kept narrow on purpose: a first pass that cries wolf trains the reader to
|
|
13
|
+
// skip it. Each one either reads the added line alone (proven) or one fact next to it (inferred):
|
|
14
|
+
//
|
|
15
|
+
// prepass-secret a secret-blocklist.txt pattern on an added line (the pre-commit gate's own list and
|
|
16
|
+
// .secret-allowlist.txt). Cited by sha256 only: the value is never quoted.
|
|
17
|
+
// prepass-debug-call console.log / debugger (JS, TS), breakpoint() / pdb (Python), print( (Dart), outside a
|
|
18
|
+
// test and outside a command-line script (a shebang, or a bin/, scripts/ or tool/ folder).
|
|
19
|
+
// prepass-todo a TODO, FIXME, XXX or HACK opening a comment added in code, outside tests
|
|
20
|
+
// (Markdown is prose, not a promise; the word in a string or a pattern is not a marker).
|
|
21
|
+
// prepass-shellcheck shellcheck's own findings on a shell file, kept to the added lines. Without shellcheck
|
|
22
|
+
// the shell files are listed as not measured: nothing is installed.
|
|
23
|
+
// prepass-lockfile a lock file changed and the manifest beside it did not (inferred: a regenerated lock
|
|
24
|
+
// can be on purpose).
|
|
25
|
+
// prepass-large-change one file gains more than 500 lines (generated files, locks and snapshots aside).
|
|
26
|
+
//
|
|
27
|
+
// What it does not do: judge. A finding here says a line is there; whether it matters is the reviewer's call, and
|
|
28
|
+
// the reviewer writes its own findings beside these (.crewforth/review.json), in the same shape.
|
|
29
|
+
// Host-independent like findings.mjs: git, Node's standard library, and shellcheck when it is there.
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import os from 'node:os';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import crypto from 'node:crypto';
|
|
35
|
+
import { spawnSync } from 'node:child_process';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
import { loadSecretPatterns, isTestPath } from '../crew-map/project.mjs';
|
|
38
|
+
import { SCHEMA_ID, validate, write, serialize } from '../findings/findings.mjs';
|
|
39
|
+
|
|
40
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
export const NAME = 'review-prepass';
|
|
42
|
+
const LARGE = 500;
|
|
43
|
+
const QUOTE = 200;
|
|
44
|
+
|
|
45
|
+
const DEBUG = [
|
|
46
|
+
{ files: /\.(m?[jt]sx?|cjs|cts|vue|svelte)$/, re: /\bconsole\.(log|debug|trace)\(|^\s*debugger\s*;?\s*$/, what: 'a console call or debugger statement' },
|
|
47
|
+
{ files: /\.py$/, re: /\bbreakpoint\(\)|\bpdb\.set_trace\(|^\s*import\s+pdb\b/, what: 'a debugger breakpoint' },
|
|
48
|
+
{ files: /\.dart$/, re: /^\s*print\(/, what: 'a print call' },
|
|
49
|
+
];
|
|
50
|
+
const SCRIPT_DIR = /(^|\/)(bin|scripts?|tools?|tool)\//;
|
|
51
|
+
// A marker opens a comment; the word inside a string, a pattern or prose that names it is not one.
|
|
52
|
+
const TODO = /(?:\/\/|#|\/\*|^\s*\*|--|<!--)\s*(TODO|FIXME|XXX|HACK)\b/;
|
|
53
|
+
const PROSE = /\.(md|markdown|txt|rst|adoc)$/i;
|
|
54
|
+
const SHELL = /\.(sh|bash)$/;
|
|
55
|
+
const SECRET_LISTS = /(^|\/)secret-(block|allow)list\.txt$/;
|
|
56
|
+
// A lock file and the manifest that declares what it pins, in the same folder.
|
|
57
|
+
const LOCKS = {
|
|
58
|
+
'package-lock.json': 'package.json', 'npm-shrinkwrap.json': 'package.json', 'yarn.lock': 'package.json',
|
|
59
|
+
'pnpm-lock.yaml': 'package.json', 'bun.lockb': 'package.json', 'pubspec.lock': 'pubspec.yaml',
|
|
60
|
+
'poetry.lock': 'pyproject.toml', 'uv.lock': 'pyproject.toml', 'Pipfile.lock': 'Pipfile', 'go.sum': 'go.mod',
|
|
61
|
+
'Cargo.lock': 'Cargo.toml', 'composer.lock': 'composer.json', 'Gemfile.lock': 'Gemfile',
|
|
62
|
+
};
|
|
63
|
+
const GENERATED = /(^|\/)(dist|build|vendor|node_modules|__snapshots__)\/|\.(g|freezed|gr|pb)\.dart$|\.min\.(js|css)$|\.snap$|\.lock$|-lock\.(json|yaml)$/;
|
|
64
|
+
|
|
65
|
+
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
66
|
+
const git = (root, args) => spawnSync('git', ['-C', root, '-c', 'core.quotePath=false', ...args], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
67
|
+
|
|
68
|
+
// The added lines of a unified diff with no context: { file, line, text }[] and the files it touches.
|
|
69
|
+
export function addedLines(diff) {
|
|
70
|
+
const added = []; const files = new Set(); const binary = new Set();
|
|
71
|
+
let file = null; let n = 0;
|
|
72
|
+
for (const raw of diff.split('\n')) {
|
|
73
|
+
if (raw.startsWith('diff --git ')) { file = null; continue; }
|
|
74
|
+
if (raw.startsWith('+++ ')) { file = raw === '+++ /dev/null' ? null : raw.slice(6).replace(/\t$/, ''); if (file) files.add(file); continue; }
|
|
75
|
+
if (raw.startsWith('Binary files ')) { const b = / and b\/(.*) differ$/.exec(raw); if (b) binary.add(b[1]); continue; }
|
|
76
|
+
const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
77
|
+
if (h) { n = Number(h[1]); continue; }
|
|
78
|
+
if (file && raw.startsWith('+')) { added.push({ file, line: n, text: raw.slice(1).replace(/\r$/, '') }); n++; }
|
|
79
|
+
}
|
|
80
|
+
return { added, files: [...files].sort(), binary: [...binary].sort() };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A finding id from a rule, a file and a line: lower case, the schema's characters only, unique in the document.
|
|
84
|
+
function idOf(seen, rule, file, line) {
|
|
85
|
+
let id = `${rule}:${file.toLowerCase().replace(/[^a-z0-9._-]+/g, '-')}:${line}`.slice(0, 190);
|
|
86
|
+
if (seen.has(id)) { let k = 2; while (seen.has(`${id}.${k}`)) k++; id = `${id}.${k}`; }
|
|
87
|
+
seen.add(id); return id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function prepass(root = '.', { base = null } = {}) {
|
|
91
|
+
const rootReal = fs.realpathSync(root);
|
|
92
|
+
const top = git(rootReal, ['rev-parse', '--show-toplevel']);
|
|
93
|
+
if (top.status !== 0) throw new Error('not a git repository');
|
|
94
|
+
const diffArgs = ['diff', '--no-ext-diff', '--no-textconv', '--no-color', '-U0', '--diff-filter=AMR'];
|
|
95
|
+
const d = git(rootReal, base ? [...diffArgs, base, '--'] : [...diffArgs, '--cached', '--']);
|
|
96
|
+
if (d.status !== 0) throw new Error(`git diff failed: ${(d.stderr || '').trim().split('\n')[0]}`);
|
|
97
|
+
const { added, files, binary } = addedLines(d.stdout);
|
|
98
|
+
|
|
99
|
+
const doc = { schema: SCHEMA_ID, producer: { name: NAME, version: '1' }, findings: [], notMeasured: [] };
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const machine = [rootReal, os.homedir()].filter((p) => p && p.length > 1);
|
|
102
|
+
const add = (rule, severity, confidence, title, at, detail, quote = true) => {
|
|
103
|
+
// A line that carries this machine's directory, or must not be quoted, is cited by its hash.
|
|
104
|
+
const ev = { file: at.file, line: at.line };
|
|
105
|
+
if (quote && !machine.some((m) => at.text.includes(m)) && at.text.trim()) ev.text = at.text.trim().slice(0, QUOTE);
|
|
106
|
+
else ev.sha256 = sha(at.text);
|
|
107
|
+
doc.findings.push({ id: idOf(seen, rule, at.file, at.line), rule, title, confidence, severity, ...(detail ? { detail } : {}), evidence: [ev] });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// A staged line the working tree no longer has cannot be cited (the citation is checked against the tree).
|
|
111
|
+
const tree = new Map();
|
|
112
|
+
const linesOf = (f) => { if (!tree.has(f)) { let t = null; try { t = fs.readFileSync(path.join(rootReal, f), 'utf8').split('\n').map((l) => l.replace(/\r$/, '')); } catch { /* gone */ } tree.set(f, t); } return tree.get(f); };
|
|
113
|
+
const stale = new Set();
|
|
114
|
+
const lines = added.filter((a) => { const t = linesOf(a.file); if (t && t[a.line - 1] === a.text) return true; stale.add(a.file); return false; });
|
|
115
|
+
for (const f of [...stale].sort()) doc.notMeasured.push({ what: f, reason: 'edited again after it was staged: its staged lines are not the ones on disk, so they were not read' });
|
|
116
|
+
for (const f of binary) doc.notMeasured.push({ what: f, reason: 'a binary file: no line to read' });
|
|
117
|
+
|
|
118
|
+
// ---- secrets: the pre-commit gate's list, minus the project's allowlist ----
|
|
119
|
+
let allow = new Set();
|
|
120
|
+
try { allow = new Set(fs.readFileSync(path.join(rootReal, '.secret-allowlist.txt'), 'utf8').split('\n').map((l) => l.replace(/\r$/, '')).filter(Boolean)); } catch { /* none */ }
|
|
121
|
+
const patterns = loadSecretPatterns(path.join(HERE, '..', 'crew-map'), allow);
|
|
122
|
+
if (!patterns) doc.notMeasured.push({ what: 'prepass-secret', reason: 'secret-blocklist.txt was not found beside the hooks, so no line was checked for a secret' });
|
|
123
|
+
const byFile = new Map();
|
|
124
|
+
for (const a of lines) { if (!byFile.has(a.file)) byFile.set(a.file, []); byFile.get(a.file).push(a); }
|
|
125
|
+
|
|
126
|
+
for (const [f, list] of [...byFile].sort((x, y) => (x[0] < y[0] ? -1 : 1))) {
|
|
127
|
+
const test = isTestPath(f);
|
|
128
|
+
const first = linesOf(f)?.[0] ?? '';
|
|
129
|
+
const script = first.startsWith('#!') || SCRIPT_DIR.test(f);
|
|
130
|
+
const debug = DEBUG.find((x) => x.files.test(f));
|
|
131
|
+
for (const a of list) {
|
|
132
|
+
if (patterns && !SECRET_LISTS.test(f) && patterns.some((re) => re.test(a.text))) {
|
|
133
|
+
add('prepass-secret', 'high', 'proven', 'A secret pattern on an added line', a, 'It matches a pattern of secret-blocklist.txt, the list the pre-commit gate refuses; the value is not quoted here.', false);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (debug && !test && !script && debug.re.test(a.text)) add('prepass-debug-call', 'low', 'proven', `An added ${debug.what}`, a);
|
|
137
|
+
if (!PROSE.test(f) && !test && TODO.test(a.text)) add('prepass-todo', 'info', 'proven', `An added ${TODO.exec(a.text)[1]}`, a);
|
|
138
|
+
}
|
|
139
|
+
if (list.length > LARGE && !GENERATED.test(f)) add('prepass-large-change', 'low', 'proven', `${list.length} lines added to one file`, list[0], 'A change this size is hard to review in one pass; split it, or say in the description what to read first.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- lock files: the manifest beside them should have changed too ----
|
|
143
|
+
const changed = new Set(files);
|
|
144
|
+
for (const f of files) {
|
|
145
|
+
const want = LOCKS[path.posix.basename(f)]; if (!want) continue;
|
|
146
|
+
const dir = path.posix.dirname(f); const manifest = dir === '.' ? want : `${dir}/${want}`;
|
|
147
|
+
if (changed.has(manifest) || !fs.existsSync(path.join(rootReal, manifest))) continue;
|
|
148
|
+
const at = byFile.get(f)?.[0]; if (!at) continue;
|
|
149
|
+
add('prepass-lockfile', 'low', 'inferred', `${path.posix.basename(f)} changed and ${want} did not`, at, 'A lock file that moves on its own pins different versions than the manifest asks for; regenerate it from the manifest, or say why it moved.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- shell: shellcheck's findings on the added lines ----
|
|
153
|
+
const shells = [...byFile.keys()].filter((f) => SHELL.test(f) || /^#!.*\b(ba|z|da)?sh\b/.test(linesOf(f)?.[0] ?? '')).sort();
|
|
154
|
+
if (shells.length) {
|
|
155
|
+
// CREW_SHELLCHECK names another shellcheck. One ending in .mjs is run with this Node: the self-check's stand-in,
|
|
156
|
+
// which a Windows runner could not start as a `#!/bin/sh` script (spawnSync runs no interpreter line there).
|
|
157
|
+
const sc = process.env.CREW_SHELLCHECK || 'shellcheck';
|
|
158
|
+
const run = (args, opts) => (sc.endsWith('.mjs') ? spawnSync(process.execPath, [sc, ...args], opts) : spawnSync(sc, args, opts));
|
|
159
|
+
const v = run(['--version'], { encoding: 'utf8' });
|
|
160
|
+
if (v.status !== 0) doc.notMeasured.push({ what: `prepass-shellcheck (${shells.length} shell file${shells.length === 1 ? '' : 's'})`, reason: 'shellcheck is not installed here, so the shell files were not linted; nothing was installed' });
|
|
161
|
+
else {
|
|
162
|
+
for (const f of shells) {
|
|
163
|
+
const r = run(['-f', 'json1', '-x', f], { cwd: rootReal, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
164
|
+
let out; try { out = JSON.parse(r.stdout || '{}'); } catch { doc.notMeasured.push({ what: f, reason: 'shellcheck gave no output this pass can read' }); continue; }
|
|
165
|
+
const at = new Map(byFile.get(f).map((a) => [a.line, a]));
|
|
166
|
+
for (const c of out.comments ?? []) {
|
|
167
|
+
const a = at.get(c.line); if (!a) continue;
|
|
168
|
+
const sev = { error: 'medium', warning: 'low' }[c.level] ?? 'info';
|
|
169
|
+
add('prepass-shellcheck', sev, 'proven', `SC${c.code}: ${String(c.message).slice(0, 180)}`, a);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
doc.findings.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
175
|
+
return doc;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function main(argv) {
|
|
179
|
+
let root = '.'; let base = null; let stdout = false;
|
|
180
|
+
for (let i = 0; i < argv.length; i++) {
|
|
181
|
+
const a = argv[i];
|
|
182
|
+
if (a === '--root') root = argv[++i];
|
|
183
|
+
else if (a === '--base') base = argv[++i];
|
|
184
|
+
else if (a === '--stdout') stdout = true;
|
|
185
|
+
else { console.error(`prepass: unknown argument ${a}\nusage: prepass.mjs [--root DIR] [--base REF] [--stdout]`); return 2; }
|
|
186
|
+
if (root === undefined || base === undefined) { console.error('prepass: --root and --base need a value'); return 2; }
|
|
187
|
+
}
|
|
188
|
+
let doc;
|
|
189
|
+
try { doc = prepass(root, { base }); } catch (e) { console.error(`prepass: ${e.message}`); return 2; }
|
|
190
|
+
const count = (s) => doc.findings.filter((f) => f.severity === s).length;
|
|
191
|
+
const line = `review-prepass: ${doc.findings.length} finding(s) ยท high ${count('high')} ยท medium ${count('medium')} ยท low ${count('low')} ยท info ${count('info')} ยท ${doc.notMeasured.length} not measured`;
|
|
192
|
+
if (stdout) {
|
|
193
|
+
const r = validate(doc, root);
|
|
194
|
+
if (!r.ok) { for (const p of r.problems) console.error(`prepass: ${p.at} ${p.code}: ${p.message}`); return 1; }
|
|
195
|
+
process.stdout.write(serialize(doc)); console.error(line); return 0;
|
|
196
|
+
}
|
|
197
|
+
const r = write(doc, root, NAME);
|
|
198
|
+
if (!r.ok) { for (const p of r.problems) console.error(`prepass: ${p.at} ${p.code}: ${p.message}`); return 1; }
|
|
199
|
+
console.log(`${line} โ ${r.file}`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# prepass.sh โ the shell entry to prepass.mjs, the review's deterministic first pass. It finds a Node that RUNS
|
|
3
|
+
# and hands over; the arguments and exit codes are prepass.mjs's (0, 1, 2).
|
|
4
|
+
#
|
|
5
|
+
# bash prepass.sh [--root DIR] [--base REF] [--stdout]
|
|
6
|
+
# rc 3 = no usable Node here, so nothing was checked. Not a pass: the review then reads the diff without it.
|
|
7
|
+
#
|
|
8
|
+
# The Node is found the way findings.sh finds it (one rule): asked for its version, never trusted by name.
|
|
9
|
+
set -u
|
|
10
|
+
d="${BASH_SOURCE%[/\\]*}"; [ "$d" = "${BASH_SOURCE}" ] && d=.
|
|
11
|
+
FLOOR=18
|
|
12
|
+
usable(){ local v; v="$("$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null)" || return 1
|
|
13
|
+
case "$v" in ''|*[!0-9]*) return 1 ;; esac; [ "$v" -ge "$FLOOR" ]; }
|
|
14
|
+
NODE=""
|
|
15
|
+
if usable node; then NODE=node
|
|
16
|
+
elif [ -f "$d/../../../studio/ensure-node.sh" ]; then
|
|
17
|
+
n="$(bash "$d/../../../studio/ensure-node.sh" 2>/dev/null)" && [ -n "$n" ] && usable "$n" && NODE="$n"
|
|
18
|
+
fi
|
|
19
|
+
if [ -z "$NODE" ]; then
|
|
20
|
+
echo "review-prepass: no Node $FLOOR+ that runs was found, so nothing was checked (this is not a pass)" >&2
|
|
21
|
+
exit 3
|
|
22
|
+
fi
|
|
23
|
+
exec "$NODE" "$d/prepass.mjs" "$@"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# settings-default.sh โ put one of Crewforth's defaults into the project's .claude/settings.json, only where nothing
|
|
3
|
+
# sets it yet. The project's settings win over the user's, so writing a default over a user's own choice would
|
|
4
|
+
# silently replace it: a value set in the project's settings, its local settings or the user's own is kept.
|
|
5
|
+
#
|
|
6
|
+
# bash settings-default.sh --install <path> <defaults.json> [ENV_VAR] from the project root
|
|
7
|
+
# <path> the setting, as a dot path: statusLine, env.CLAUDE_CODE_SUBAGENT_MODEL
|
|
8
|
+
# <defaults.json> a JSON object holding just that setting (data beside the caller: one definition)
|
|
9
|
+
# ENV_VAR also kept when this variable is set in the environment (a value exported in the shell)
|
|
10
|
+
#
|
|
11
|
+
# Prints one word: installed ยท kept (it is set) ยท none (no settings.json) ยท failed (the file is left as it was).
|
|
12
|
+
# A managed settings layer is not read. The merge is Crewforth's own JSON reader.
|
|
13
|
+
set -u
|
|
14
|
+
_here="${BASH_SOURCE%[/\\]*}"; [ "$_here" = "${BASH_SOURCE}" ] && _here=.
|
|
15
|
+
|
|
16
|
+
[ "${1:-}" = --install ] && [ -n "${2:-}" ] && [ -n "${3:-}" ] || { echo "usage: settings-default.sh --install <path> <defaults.json> [ENV_VAR]" >&2; exit 2; }
|
|
17
|
+
KEY="$2"; K="$3"; VAR="${4:-}"
|
|
18
|
+
P=".claude/settings.json"; SJ="$_here/settings-json.awk"
|
|
19
|
+
[ -f "$P" ] && [ -f "$SJ" ] || { echo none; exit 0; }
|
|
20
|
+
case "$VAR" in '') ;; [A-Za-z_]*) case "$VAR" in *[!A-Za-z0-9_]*) echo failed; exit 0 ;; esac; [ -n "${!VAR:-}" ] && { echo kept; exit 0; } ;; *) echo failed; exit 0 ;; esac
|
|
21
|
+
for f in "$P" ".claude/settings.local.json" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"; do
|
|
22
|
+
[ -f "$f" ] && awk -v op=get -v path="$KEY" -f "$SJ" "$f" >/dev/null 2>&1 && { echo kept; exit 0; }
|
|
23
|
+
done
|
|
24
|
+
[ -f "$K" ] || { echo failed; exit 0; }
|
|
25
|
+
t="$P.default.$$"
|
|
26
|
+
r=failed
|
|
27
|
+
# The project's file goes FIRST: the merge refreshes the first file's .claude/hooks/ entries from the first file,
|
|
28
|
+
# so put second it would lose every Crewforth hook (measured: the installed project's own smoke failed 30 checks).
|
|
29
|
+
# The second file holds only the default, which the first lacks: that is the one change (an object such as env is
|
|
30
|
+
# merged member by member, the project's own members kept). Written through the file, so a symlink and its mode
|
|
31
|
+
# survive.
|
|
32
|
+
if awk -v op=merge -v retired= -f "$SJ" "$P" "$K" > "$t" 2>/dev/null && [ -s "$t" ] \
|
|
33
|
+
&& awk -v op=validate -f "$SJ" "$t" 2>/dev/null && awk -v op=get -v path="$KEY" -f "$SJ" "$t" >/dev/null 2>&1; then
|
|
34
|
+
cat "$t" > "$P" && r=installed
|
|
35
|
+
fi
|
|
36
|
+
rm -f "$t"; echo "$r"
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
# awk -v op=validate -f settings-json.awk FILE rc 0 = FILE is a JSON object, 1 = it is not
|
|
5
5
|
# awk -v op=get -v path=hooks.Stop.0 -f settings-json.awk FILE the value there, as the merge would print it
|
|
6
6
|
# awk -v op=len -v path=hooks.Stop -f settings-json.awk FILE how many entries an array/object holds
|
|
7
|
+
# awk -v op=gets -v path='a.b|c.d' -f settings-json.awk FILE several values in one read, tab-separated, "" if absent
|
|
7
8
|
# awk -v op=strings -v path=permissions.deny -f settings-json.awk FILE each string element of that array, one per
|
|
8
9
|
# line, quotes off, escapes as written (rc 1 = not an array)
|
|
10
|
+
# awk -v op=keys [-v path=P] -f settings-json.awk FILE the member names of the object there, one per line, quotes
|
|
11
|
+
# off, in file order (rc 1 = not an object)
|
|
9
12
|
# path = dot-separated keys, a number indexes an array (0-based), empty = the root. rc 1 = no such path
|
|
10
13
|
# (or, for len, a scalar there), 10 = FILE is not a JSON object.
|
|
14
|
+
# awk -v op=get -v path=P -v setk=M -v setv=RAWJSON -f settings-json.awk FILE the object at P with member M set
|
|
15
|
+
# to RAWJSON (added when absent): a payload rewritten in one field, every other field and escape kept
|
|
16
|
+
# (rc 1 = nothing at P, or not an object)
|
|
11
17
|
# awk -v op=overlay -v key=K [-v setk=M -v setv=RAWJSON] -f settings-json.awk FILE1 FILE2
|
|
12
18
|
# FILE1 with FILE1.K shallow-merged from FILE2.K (FILE2 wins, FILE1's other members and order kept), then
|
|
13
19
|
# member M set to RAWJSON. rc 10 = FILE1 (or its K) is not an object, 11 = FILE2 has no object at K.
|
|
@@ -69,6 +75,10 @@ function at(id,path, n,sg,i){ n=split(path,sg,".")
|
|
|
69
75
|
return id }
|
|
70
76
|
function mk(t){ NN++; T[NN]=t; N[NN]=0; return NN }
|
|
71
77
|
function add(o,k,c){ N[o]++; K[o,N[o]]=k; C[o,N[o]]=c }
|
|
78
|
+
# Member k of object o set to the raw JSON v, added at the end when o has none (overlay and get share it).
|
|
79
|
+
function setm(o,k,v, sn,i,x){ sn=mk("s"); V[sn]=v; x=0
|
|
80
|
+
for(i=1;i<=N[o];i++) if(K[o,i]=="\"" k "\""){ C[o,i]=sn; x=1 }
|
|
81
|
+
if(!x) add(o,"\"" k "\"",sn) }
|
|
72
82
|
function canon(id, i,j,n,s,t,ord){
|
|
73
83
|
if(T[id]=="s") return V[id]
|
|
74
84
|
if(T[id]=="a"){ s="["; for(i=1;i<=N[id];i++) s=s (i>1 ? "," : "") canon(C[id,i]); return s "]" }
|
|
@@ -124,18 +134,23 @@ END{
|
|
|
124
134
|
print (it && !isnull(it) ? V[it] : 0) + V[cr] + (cc && !isnull(cc) ? V[cc] : 0); exit 0 }
|
|
125
135
|
exit 0 }
|
|
126
136
|
if(op=="validate") exit !doc(1)
|
|
137
|
+
# gets: several paths in one read (path = "a.b|c.d"), their values on one line, tab-separated; an absent one is
|
|
138
|
+
# empty. One process for what a hot caller (the status line) would otherwise read with one awk per field.
|
|
139
|
+
if(op=="gets"){ if(!(r=doc(1))) exit 10; n=split(path,pp,"|"); o=""
|
|
140
|
+
for(i=1;i<=n;i++){ x=at(r,pp[i]); o=o (i>1?"\t":"") (x ? put(x,"") : "") } print o; exit 0 }
|
|
127
141
|
if(op=="get" || op=="len"){ if(!(r=doc(1))) exit 10; if(!(r=at(r,path))) exit 1
|
|
128
|
-
if(op=="get"){
|
|
142
|
+
if(op=="get"){ if(setk!=""){ if(T[r]!="o") exit 1; setm(r,setk,setv) } print put(r,""); exit 0 }
|
|
143
|
+
if(T[r]=="s") exit 1; print N[r]; exit 0 }
|
|
129
144
|
if(op=="strings"){ if(!(r=doc(1))) exit 10; if(!(r=at(r,path))) exit 1; if(T[r]!="a") exit 1
|
|
130
145
|
for(i=1;i<=N[r];i++){ c=C[r,i]; if(T[c]=="s" && substr(V[c],1,1)=="\"") print substr(V[c],2,length(V[c])-2) }
|
|
131
146
|
exit 0 }
|
|
147
|
+
if(op=="keys"){ if(!(r=doc(1))) exit 10; if(!(r=at(r,path))) exit 1; if(T[r]!="o") exit 1
|
|
148
|
+
for(i=1;i<=N[r];i++){ k=K[r,i]; print substr(k,2,length(k)-2) } exit 0 }
|
|
132
149
|
if(op=="overlay"){ if(!(t=doc(1))) exit 10; if(!(q=doc(2))) exit 11; kk="\"" key "\""
|
|
133
150
|
qv=get(q,kk); if(T[qv]!="o") exit 11; tv=get(t,kk); if(tv && T[tv]!="o") exit 10
|
|
134
151
|
nv=mk("o"); for(i=1;i<=N[tv];i++){ x=get(qv,K[tv,i]); add(nv,K[tv,i], x ? x : C[tv,i]) }
|
|
135
152
|
for(i=1;i<=N[qv];i++) if(!get(nv,K[qv,i])) add(nv,K[qv,i],C[qv,i])
|
|
136
|
-
if(setk!="")
|
|
137
|
-
for(i=1;i<=N[nv];i++) if(K[nv,i]=="\"" setk "\""){ C[nv,i]=sn; x=1 }
|
|
138
|
-
if(!x) add(nv,"\"" setk "\"",sn) }
|
|
153
|
+
if(setk!="") setm(nv,setk,setv)
|
|
139
154
|
x=0; for(i=1;i<=N[t];i++) if(K[t,i]==kk){ C[t,i]=nv; x=1 }
|
|
140
155
|
if(!x) add(t,kk,nv)
|
|
141
156
|
print put(t,""); exit 0 }
|
package/kit/eval/lib/star.sh
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# The one "leave a star" line. Single source of its URL, its text in both languages, and when it stays quiet.
|
|
3
3
|
#
|
|
4
|
-
# star.sh --once <project>
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# star.sh --once <project> [doctor]
|
|
5
|
+
# ONCE PER KIT VERSION per clone and per caller. The installers (first install, and
|
|
6
|
+
# every update) share one marker; doctor (doctor.sh on a healthy verdict, and the
|
|
7
|
+
# plugin's /crew-doctor) keeps its own, `doctor`. So a version shows the line at most
|
|
8
|
+
# twice, once where it was installed and once where it was checked, and never again
|
|
9
|
+
# until the Crewforth version changes. A marker holds the version it was shown for.
|
|
10
|
+
# /crew-update runs its doctor with CREW_NO_STAR=1, so one command never shows it twice.
|
|
8
11
|
# star.sh unconditional (tests, and anyone who wants to see it).
|
|
9
12
|
#
|
|
10
13
|
# Nothing else prints it: not a hook, not a session start. Cost: one line (~30 tokens) per version, and only when
|
|
@@ -31,11 +34,21 @@ if [ "${1:-}" = --once ]; then
|
|
|
31
34
|
# Builtin reads, not `head | tr` in a $( ): on Git Bash those two pipelines were 4 of ~11 processes and ~250 ms
|
|
32
35
|
# of ~395 ms per call (measured on stock Windows). `read` takes the first line; the CR is stripped by expansion.
|
|
33
36
|
# The braces matter: a failed `<` reports before a trailing 2>/dev/null applies (measured: 450 bytes of stderr).
|
|
34
|
-
VER=""; { IFS= read -r VER < "$_dir/.claude/VERSION"; } 2>/dev/null; VER="${VER%$'\r'}"
|
|
37
|
+
VER=""; { IFS= read -r VER < "$_dir/.claude/VERSION"; } 2>/dev/null; VER="${VER%$'\r'}"
|
|
38
|
+
# The plugin edition has no .claude/VERSION: its version is the manifest's, beside this file at ../../.
|
|
39
|
+
if [ -z "$VER" ]; then
|
|
40
|
+
_here="${BASH_SOURCE%[/\\]*}"; [ "$_here" = "${BASH_SOURCE}" ] && _here=.
|
|
41
|
+
[ -f "$_here/../../.claude-plugin/plugin.json" ] && [ -f "$_here/settings-json.awk" ] && \
|
|
42
|
+
VER="$(awk -v op=get -v path=version -f "$_here/settings-json.awk" "$_here/../../.claude-plugin/plugin.json" 2>/dev/null)"
|
|
43
|
+
VER="${VER#\"}"; VER="${VER%\"}" # the reader prints a JSON string with its quotes
|
|
44
|
+
case "$VER" in *[!0-9A-Za-z.-]*) VER="" ;; esac
|
|
45
|
+
fi
|
|
46
|
+
VER="${VER:-unknown}"
|
|
47
|
+
_key=crewforth-star; [ "${3:-}" = doctor ] && _key=crewforth-star-doctor
|
|
35
48
|
# GIT_DIR/GIT_WORK_TREE dropped: exported by a caller (a hook), they would point the marker at another repo.
|
|
36
|
-
MARK="$(cd "$_dir" 2>/dev/null && env -u GIT_DIR -u GIT_WORK_TREE git rev-parse --git-path
|
|
49
|
+
MARK="$(cd "$_dir" 2>/dev/null && env -u GIT_DIR -u GIT_WORK_TREE git rev-parse --git-path "$_key" 2>/dev/null)" || MARK=""
|
|
37
50
|
case "$MARK" in
|
|
38
|
-
'') MARK="$_dir/.claude/star-shown" ;;
|
|
51
|
+
'') MARK="$_dir/.claude/star-shown"; [ "$_key" = crewforth-star ] || MARK="$MARK-doctor" ;;
|
|
39
52
|
/*|[A-Za-z]:*) ;; # absolute (POSIX or a Windows drive path from git)
|
|
40
53
|
*) MARK="$_dir/$MARK" ;; # git answers relative to the directory it was asked in
|
|
41
54
|
esac
|
|
@@ -65,6 +78,9 @@ _mt() {
|
|
|
65
78
|
_mt 'โญ If Crewforth saves you a review round, a star helps others find it: %s' "$CREW_REPO_URL"
|
|
66
79
|
# The marker only after the line actually went out: a closed stdout must not record "shown".
|
|
67
80
|
if printf '%s\n' "$_M" && [ -n "$MARK" ]; then
|
|
81
|
+
# A plugin project may have no .claude/ yet: without one the marker would never be written and the line would
|
|
82
|
+
# come back on every run.
|
|
83
|
+
[ -d "${MARK%/*}" ] || mkdir -p "${MARK%/*}" 2>/dev/null
|
|
68
84
|
{ printf '%s\n' "$VER" > "$MARK"; } 2>/dev/null
|
|
69
85
|
fi
|
|
70
86
|
exit 0
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# statusline.sh โ Crewforth's status line: one line, from what Claude Code hands it on stdin and what the hooks
|
|
3
|
+
# have already written. It decides nothing and blocks nothing; it only says.
|
|
4
|
+
#
|
|
5
|
+
# ๐ก BLOCK ยง4.2 ยท โ commit approved 24m ยท โ 2 crew agents ยท ๐ 34% ยท ~$1.20
|
|
6
|
+
#
|
|
7
|
+
# Each part is shown only when there is something to say, and only for THIS session:
|
|
8
|
+
# ๐ก the gate's last word in this session: the last line of the gate log, if it was written after the session's
|
|
9
|
+
# first status line (the log carries no time, so a line from last week must not read as now)
|
|
10
|
+
# โ an approval the user gave for a commit or a push that still holds (the record prompt-approval.sh keeps in
|
|
11
|
+
# the git directory, 30 minutes, emptied at the user's next message)
|
|
12
|
+
# โ crew agents started in this session that have not stopped yet (model routing's records)
|
|
13
|
+
# ๐ the context fill Claude Code measured (context_window.used_percentage), never an estimate
|
|
14
|
+
# ~$ what Claude Code puts the session's cost at (cost.total_cost_usd), at API list price
|
|
15
|
+
#
|
|
16
|
+
# Wired by the installers only where no status line is set already (theirs is never replaced), from
|
|
17
|
+
# statusline.settings.json beside this file.
|
|
18
|
+
# COST. Claude Code runs this after every change, debounced at 300 ms. Measured: awk (the stdin JSON) and tail (the
|
|
19
|
+
# gate log), and on bash older than 4.2 one date; the rest is builtins. No git, no jq, no python.
|
|
20
|
+
set -u
|
|
21
|
+
_here="${BASH_SOURCE%[/\\]*}"; [ "$_here" = "${BASH_SOURCE}" ] && _here=.
|
|
22
|
+
|
|
23
|
+
# --install, from the project root: put this status line into .claude/settings.json only where NO status line is
|
|
24
|
+
# set (the project's settings, its local settings, or the user's own); a user's line is never replaced, because
|
|
25
|
+
# the project's setting would win over it. Prints one word: installed ยท kept (one is set) ยท none (no settings.json)
|
|
26
|
+
# ยท failed (the file is left as it was). A managed settings layer is not read. The merge is Crewforth's own JSON reader.
|
|
27
|
+
if [ "${1:-}" = --install ]; then
|
|
28
|
+
# The setting itself is data beside this file (statusline.settings.json); the rule for putting a default in is
|
|
29
|
+
# one helper, shared with the subagent model's default.
|
|
30
|
+
exec bash "$_here/settings-default.sh" --install statusLine "$_here/statusline.settings.json"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
IFS= read -r -d '' IN || true
|
|
34
|
+
[ -n "$IN" ] || exit 0
|
|
35
|
+
# One read of the four values the line needs.
|
|
36
|
+
F="$(printf '%s' "$IN" | awk -v op=gets -v 'path=session_id|context_window.used_percentage|cost.total_cost_usd|workspace.project_dir' -f "$_here/settings-json.awk" - 2>/dev/null)" || F=""
|
|
37
|
+
IFS=$'\t' read -r SID PCT COST PDIR <<< "$F"
|
|
38
|
+
# The reader prints a string with its quotes; taken off by expansion, not a subshell (this runs every few seconds).
|
|
39
|
+
SID="${SID:-}"; SID="${SID#\"}"; SID="${SID%\"}"; PDIR="${PDIR:-}"; PDIR="${PDIR#\"}"; PDIR="${PDIR%\"}"
|
|
40
|
+
case "$SID" in ''|*[!A-Za-z0-9._-]*) SID="" ;; esac
|
|
41
|
+
[ -n "$PDIR" ] && [ -d "$PDIR/.claude" ] && cd "$PDIR" 2>/dev/null
|
|
42
|
+
|
|
43
|
+
OUT=()
|
|
44
|
+
# The session's baseline: made on its first status line. What is newer than it happened in this session.
|
|
45
|
+
BASE=""
|
|
46
|
+
if [ -n "$SID" ] && [ -d .claude ]; then
|
|
47
|
+
BASE=".claude/state/statusline/$SID"
|
|
48
|
+
if [ ! -e "$BASE" ]; then
|
|
49
|
+
mkdir -p .claude/state/statusline 2>/dev/null
|
|
50
|
+
: > "$BASE" 2>/dev/null || BASE=""
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ๐ก The gate's last word in this session.
|
|
55
|
+
GL=""; [ -f "$_here/gate-log.sh" ] && { . "$_here/gate-log.sh"; crew_gate_log_read; GL="$_CREW_GL"; } # one rule (gate-log.sh)
|
|
56
|
+
if [ -n "$BASE" ] && [ -f "$GL" ] && [ "$GL" -nt "$BASE" ]; then
|
|
57
|
+
last="$(tail -n 1 "$GL" 2>/dev/null)"
|
|
58
|
+
IFS=$'\t' read -r verdict section _ <<< "$last"
|
|
59
|
+
case "$verdict" in BLOCK|ASK|ALLOW|NOTE) OUT+=("๐ก $verdict ${section:-}") ;; esac
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# โ An approval that still holds: the record is in the git directory (a worktree's .git is a file naming it).
|
|
63
|
+
G=""
|
|
64
|
+
if [ -d .git ]; then G=".git"
|
|
65
|
+
elif [ -f .git ]; then IFS= read -r l < .git 2>/dev/null; case "$l" in "gitdir: "*) G="${l#gitdir: }"; G="${G%$'\r'}" ;; esac; fi
|
|
66
|
+
if [ -n "$G" ] && [ -s "$G/crewforth-approval" ]; then
|
|
67
|
+
op=""; ts=""
|
|
68
|
+
while IFS='=' read -r k v; do v="${v%$'\r'}"; case "$k" in op) op="$v" ;; ts) ts="$v" ;; esac; done < "$G/crewforth-approval"
|
|
69
|
+
if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v now '%(%s)T' -1; else now="$(date +%s)"; fi
|
|
70
|
+
case "$ts" in ''|*[!0-9]*) ;; *)
|
|
71
|
+
left=$(( (1800 - (now - ts)) / 60 ))
|
|
72
|
+
case "$op" in commit|push|commit+push) [ "$left" -ge 0 ] && [ "$((now - ts))" -le 1800 ] && OUT+=("โ $op approved ${left}m") ;; esac ;;
|
|
73
|
+
esac
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# โ Crew agents started in this session and not stopped: a record under agents/ with no result beside it.
|
|
77
|
+
if [ -n "$BASE" ] && [ -d .claude/state/crew-model/agents ]; then
|
|
78
|
+
n=0
|
|
79
|
+
for a in .claude/state/crew-model/agents/*; do
|
|
80
|
+
[ -f "$a" ] || continue
|
|
81
|
+
case "$a" in *.claim|*.used) continue ;; esac
|
|
82
|
+
[ "$a" -nt "$BASE" ] || continue
|
|
83
|
+
[ -e ".claude/state/crew-model/results/${a##*/}" ] && continue
|
|
84
|
+
n=$((n + 1))
|
|
85
|
+
done
|
|
86
|
+
pl=s; [ "$n" -eq 1 ] && pl=""
|
|
87
|
+
[ "$n" -gt 0 ] && OUT+=("โ $n crew agent$pl")
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ๐ and ~$: Claude Code's own numbers, shown as they are.
|
|
91
|
+
case "${PCT:-}" in ''|null|*[!0-9.]*) ;; *) OUT+=("๐ ${PCT%%.*}%") ;; esac
|
|
92
|
+
case "${COST:-}" in ''|null|*[!0-9.]*) ;; *)
|
|
93
|
+
printf -v c '%.2f' "$COST" 2>/dev/null && OUT+=("~\$$c") ;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
[ "${#OUT[@]}" -gt 0 ] || exit 0
|
|
97
|
+
line="${OUT[0]}"
|
|
98
|
+
for p in "${OUT[@]:1}"; do line="$line ยท $p"; done
|
|
99
|
+
printf '%s\n' "$line"
|
|
100
|
+
exit 0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Sourced, not run. How a card's verify command is read: one definition, for the gate that runs it
|
|
2
|
+
# (hooks/guard-agent-model.sh) and for doctor, which offers the line that would allow it.
|
|
3
|
+
#
|
|
4
|
+
# A verify command in a sub-folder: `cd apps/mobile && flutter test`, the usual shape in a repository of several
|
|
5
|
+
# projects (measured on one: 41 of 71 refused verify commands had it). Split into the folder and the one command,
|
|
6
|
+
# so the command is judged alone and run in that folder with no chain; a `verify <command>` rule names the command
|
|
7
|
+
# alone. Only a relative folder inside the project, with no .. step, that exists; anything else is left whole, and
|
|
8
|
+
# is refused as a chain.
|
|
9
|
+
crew_verify_split(){ # $1 = the command, $2 = the project root -> _AMVD the folder ("" = the root), _AMVC the command
|
|
10
|
+
local c="$1" d r root="${2:-.}"
|
|
11
|
+
_AMVD=""; _AMVC="$c"
|
|
12
|
+
case "$c" in 'cd '*' && '*) ;; *) return 0 ;; esac
|
|
13
|
+
d="${c#cd }"; d="${d%% && *}"; r="${c#cd "$d" && }"
|
|
14
|
+
case "$d" in ''|/*|~*|*[!A-Za-z0-9._/-]*|..|../*|*/..|*/../*) return 0 ;; esac
|
|
15
|
+
[ -n "$r" ] && [ -d "$root/$d" ] || return 0
|
|
16
|
+
_AMVD="$d"; _AMVC="$r"
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Sourced, not run. The ONE definition of how Crewforth reads and orders its own version numbers.
|
|
2
|
+
#
|
|
3
|
+
# An install's VERSION holds X.Y.Z, or X.Y.Z-rc.N when it came from a release candidate. An rc comes before its
|
|
4
|
+
# final (3.2.0-rc.1 < 3.2.0), so someone who tried the rc is offered the final when it ships. Read by the update
|
|
5
|
+
# check (hooks/session-update-check.sh), doctor and adopt.sh; each used to split on "." and read 3.2.0-rc.1 as 3.2.0.
|
|
6
|
+
#
|
|
7
|
+
# Builtins only: sourcing this costs no process, which matters in hooks on Git Bash.
|
|
8
|
+
|
|
9
|
+
# The version an install may hold in its own VERSION file: X.Y.Z or X.Y.Z-rc.N, nothing else; printed, or rc 1.
|
|
10
|
+
# A value off the network is not read with this: the update check holds that to X.Y.Z alone.
|
|
11
|
+
crew_local_version(){
|
|
12
|
+
local v="${1:-}" base n
|
|
13
|
+
v="${v//[!0-9A-Za-z.-]/}"
|
|
14
|
+
base="${v%%-rc.*}"
|
|
15
|
+
case "$base" in
|
|
16
|
+
*[!0-9.]*|'') return 1 ;;
|
|
17
|
+
[0-9]*.[0-9]*.[0-9]*.*) return 1 ;;
|
|
18
|
+
[0-9]*.[0-9]*.[0-9]*) ;;
|
|
19
|
+
*) return 1 ;;
|
|
20
|
+
esac
|
|
21
|
+
if [ "$base" != "$v" ]; then
|
|
22
|
+
n="${v#"$base"-rc.}"
|
|
23
|
+
case "$n" in ''|*[!0-9]*) return 1 ;; esac
|
|
24
|
+
fi
|
|
25
|
+
printf '%s' "$v"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# vcmp(a, b): 1 when a is newer, -1 when older, 0 when the same. Prepended to an awk program:
|
|
29
|
+
# awk -v a="$X" -v b="$Y" "$CREW_VCMP_AWK"' BEGIN { exit !(vcmp(a, b) > 0) }'
|
|
30
|
+
CREW_VCMP_AWK='
|
|
31
|
+
function vrc(v) { return match(v, /-rc\.[0-9]+$/) ? substr(v, RSTART + 4) + 0 : -1 }
|
|
32
|
+
function vcmp(a, b, x, y, i, ra, rb) {
|
|
33
|
+
ra = vrc(a); rb = vrc(b); sub(/-.*/, "", a); sub(/-.*/, "", b)
|
|
34
|
+
split(a, x, "."); split(b, y, ".")
|
|
35
|
+
for (i = 1; i <= 3; i++) { if (x[i] + 0 > y[i] + 0) return 1; if (x[i] + 0 < y[i] + 0) return -1 }
|
|
36
|
+
if (ra == rb) return 0
|
|
37
|
+
if (ra < 0) return 1
|
|
38
|
+
if (rb < 0) return -1
|
|
39
|
+
return ra > rb ? 1 : -1
|
|
40
|
+
}
|
|
41
|
+
'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-bench.sh โ how right the review is, on labelled cases: precision, recall and every false alarm by name.
|
|
3
|
+
# The shell entry to eval/lib/review-bench/bench.mjs; it finds a Node that RUNS and hands over.
|
|
4
|
+
#
|
|
5
|
+
# bash review-bench.sh --cases DIR [--arm prepass|agent] [--split all|train|holdout] [--only id,id]
|
|
6
|
+
# [--model sonnet] [--advisor opus] [--case-budget 0.75] [--max-cost 12] [--floors FILE]
|
|
7
|
+
# [--out FILE] [--no-tools]
|
|
8
|
+
#
|
|
9
|
+
# --arm prepass (the default) runs no model and costs nothing. --arm agent runs Claude Code headless as
|
|
10
|
+
# crew-review-agent on every case and SPENDS TOKENS: each case is capped by --case-budget (US dollars) and the run
|
|
11
|
+
# by --max-cost; ask the person paying before running it. rc 3 = no usable Node here (not a pass).
|
|
12
|
+
set -u
|
|
13
|
+
d="${BASH_SOURCE%[/\\]*}"; [ "$d" = "${BASH_SOURCE}" ] && d=.
|
|
14
|
+
FLOOR=18
|
|
15
|
+
usable(){ local v; v="$("$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null)" || return 1
|
|
16
|
+
case "$v" in ''|*[!0-9]*) return 1 ;; esac; [ "$v" -ge "$FLOOR" ]; }
|
|
17
|
+
NODE=""
|
|
18
|
+
if usable node; then NODE=node
|
|
19
|
+
elif [ -f "$d/../studio/ensure-node.sh" ]; then
|
|
20
|
+
n="$(bash "$d/../studio/ensure-node.sh" 2>/dev/null)" && [ -n "$n" ] && usable "$n" && NODE="$n"
|
|
21
|
+
fi
|
|
22
|
+
if [ -z "$NODE" ]; then
|
|
23
|
+
echo "review-bench: no Node $FLOOR+ that runs was found, so nothing was measured (this is not a pass)" >&2
|
|
24
|
+
exit 3
|
|
25
|
+
fi
|
|
26
|
+
exec "$NODE" "$d/lib/review-bench/bench.mjs" "$@"
|