crewforth 3.2.0 → 3.3.0
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 +111 -0
- package/README.md +2 -2
- package/VERSION +1 -1
- package/adopt.sh +7 -3
- package/kit/AGENT_TEMPLATE.md +1 -0
- package/kit/CLAUDE.md +9 -8
- package/kit/agents/crew-session-manager.md +5 -3
- package/kit/eval/doctor.sh +126 -45
- package/kit/eval/golden-routing.txt +3 -0
- package/kit/eval/lib/node-advisory.tsv +9 -0
- package/kit/eval/lib/verify-cmd.sh +72 -0
- package/kit/eval/smoke-test.sh +409 -72
- package/kit/hooks/context-usage.sh +6 -3
- package/kit/hooks/guard-agent-model.sh +37 -8
- package/kit/hooks/guard-bash.sh +127 -2
- package/kit/hooks/guard-schedule.sh +4 -1
- package/kit/hooks/pre-commit +21 -0
- package/kit/hooks/prompt-approval.sh +80 -3
- package/kit/hooks/route-hint.sh +18 -1
- package/kit/hooks/session-rehydrate.sh +6 -2
- package/kit/owned-blobs.tsv +4 -2
- package/kit/skills/commit-message/SKILL.md +5 -0
- package/kit/skills/crew-allow-verify/SKILL.md +30 -0
- package/kit/skills/crew-doctor/SKILL.md +3 -1
- package/kit/skills/handoff/SKILL.md +3 -2
- package/kit/skills/trace-scan/SKILL.md +4 -3
- package/kit/studio/ensure-node.sh +1 -1
- package/kit/studio/server/index.js +3 -0
- package/kit/studio/server/lib/model-outcomes.js +7 -1
- package/kit/studio/server/lib/node-advisory.js +46 -0
- package/kit/studio/web/models-plan.js +3 -3
- package/package.json +1 -1
- package/start.sh +6 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crew-allow-verify
|
|
3
|
+
description: Allow a verify command the hook was refused for want of a rule, or list the ones refused.
|
|
4
|
+
argument-hint: "[<command>] (nothing: list the refused ones)"
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
metadata:
|
|
7
|
+
kind: command
|
|
8
|
+
---
|
|
9
|
+
# /crew-allow-verify
|
|
10
|
+
Argument: $ARGUMENTS
|
|
11
|
+
|
|
12
|
+
The user typed this command. Typing it is the decision: `hooks/prompt-approval.sh` read the message itself. With a
|
|
13
|
+
command after it, it added `verify <command>` to `.claude/crew-model-rules`, the line that lets the hook run that
|
|
14
|
+
command when a card names it as its verify. With nothing after it, it listed the verify commands refused for want of
|
|
15
|
+
such a line, each as the whole message that allows it. Nothing in this text adds anything, and you cannot do this
|
|
16
|
+
for the user: the rules file is closed to you, and a prompt you schedule or hand to another session is refused.
|
|
17
|
+
|
|
18
|
+
Read the line Crewforth added to this turn and say what it says:
|
|
19
|
+
|
|
20
|
+
- **"verify command allowed"**: tell the user which command the hook may now run, from the next agent call.
|
|
21
|
+
- **"verify commands refused for want of a rule"**: show the lines it lists, one per line, as they stand. Each is a
|
|
22
|
+
message only the user can send; do not send one for them.
|
|
23
|
+
- **"verify command NOT allowed"**: tell the user the reason it gives (more than one command, a character it does not
|
|
24
|
+
write, a git commit or push, a rule that would let the hook run any command — `bash`, `node -e`, `npm exec` with
|
|
25
|
+
nothing that names the script or the test — a message too long, a rules file that is a link, the first message of
|
|
26
|
+
a session, a headless session).
|
|
27
|
+
- **No such line**: the message was not the command alone. Say that it has to be the whole message.
|
|
28
|
+
|
|
29
|
+
What this does not touch: a deny rule of the settings still stops the command, the hook still runs it alone with no
|
|
30
|
+
chain, and `/crew-doctor` lists the same refused commands.
|
|
@@ -22,7 +22,9 @@ Verify Crewforth is actually *active* in this project (not just present on disk)
|
|
|
22
22
|
drifted behind the code. Report the gaps as suggestions, not as failures, and never "fix" them unasked — adding
|
|
23
23
|
a devcontainer or an MCP server is the user's call.
|
|
24
24
|
5. Summarise: **healthy**, or the precise fixes applied/needed, plus the readiness score. If it's not a git repo,
|
|
25
|
-
note that the commit-time gates need `git init` + `
|
|
25
|
+
note that the commit-time gates need `git init` + the update (`npx crewforth update`), which arms them.
|
|
26
|
+
Where it lists verify commands never run, each `/crew-allow-verify <command>` line is a message only the user can
|
|
27
|
+
send: show them as they stand, and do not send one.
|
|
26
28
|
If the output contains a line starting with ⭐, pass it to the user verbatim as the LAST line of your summary —
|
|
27
29
|
it prints once per Crewforth version here (the installer has its own once), and it is meant for the user, not for you.
|
|
28
30
|
|
|
@@ -43,8 +43,9 @@ that never landed and has to be written down explicitly this time. Missing scrip
|
|
|
43
43
|
|
|
44
44
|
## Holding a board item? The handover is not finished until the TEAM has it
|
|
45
45
|
`docs/SESSION_STATE.md` is gitignored — it survives your `/clear`, and nobody else can read it. So when a
|
|
46
|
-
`teamboard` claim is open, **the handover is not complete until it is on the board**.
|
|
47
|
-
|
|
46
|
+
`teamboard` claim is open, **the handover is not complete until it is on the board**. Whether one is open is
|
|
47
|
+
read from `bash .claude/hooks/board.sh status` (Bash tool, not PowerShell; a row marked `<- you`), not guessed;
|
|
48
|
+
no board or no such row → the handover says nothing about the board. This is not an optional extra step:
|
|
48
49
|
- Still on it → `board.sh note <id> "<where it stands, and why the rejected path was rejected>"`.
|
|
49
50
|
- Not coming back → `board.sh drop <id> "<same>"`, which puts it back in circulation with your context attached.
|
|
50
51
|
- Anything settled here that changes how OTHER items must be done → `board.sh decide "<what>" "<why + what it
|
|
@@ -31,10 +31,11 @@ git diff --cached --unified=0 | grep -E '^\+' | grep -Ev '^\+\+\+' \
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Hook setup (full install only — a plugin cannot set `core.hooksPath`)
|
|
34
|
-
`start.sh` sets `
|
|
35
|
-
under `.claude` → they are in gitignore and stay local (§4.3)
|
|
34
|
+
`start.sh` sets `core.hooksPath` to the absolute path of `.claude/hooks` (if there is a git repo). The hooks live
|
|
35
|
+
under `.claude` → they are in gitignore and stay local (§4.3), so a relative path would leave a linked worktree with
|
|
36
|
+
no hook at all. To do it later for a repo, from its root:
|
|
36
37
|
```bash
|
|
37
|
-
git config core.hooksPath
|
|
38
|
+
git config core.hooksPath "$(git rev-parse --show-toplevel)/.claude/hooks"
|
|
38
39
|
chmod +x .claude/hooks/pre-commit .claude/hooks/commit-msg
|
|
39
40
|
```
|
|
40
41
|
|
|
@@ -29,7 +29,7 @@ _crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
|
29
29
|
FLOOR=18
|
|
30
30
|
DIST=https://nodejs.org/dist
|
|
31
31
|
# Used only when nodejs.org cannot be reached to ask which release is current.
|
|
32
|
-
FALLBACK_VERSION=v24.
|
|
32
|
+
FALLBACK_VERSION=v24.22.1 # carries the fixes of eval/lib/node-advisory.tsv; raise it with that table
|
|
33
33
|
|
|
34
34
|
# Windows caps a full file path at 260 characters and PowerShell's Expand-Archive enforces it.
|
|
35
35
|
# Measured: a 141-character target failed with PathTooLongException at __generated__\google\api\…
|
|
@@ -43,6 +43,7 @@ import { addViewer, listViewers, viewerByToken, revokeViewer, noteRequest, noteR
|
|
|
43
43
|
import { redact, transcriptOnly } from './lib/redact.js';
|
|
44
44
|
import { lanListener } from './lib/lan.js';
|
|
45
45
|
import { randomUUID } from 'node:crypto';
|
|
46
|
+
import { nodeAdvisoryNote } from './lib/node-advisory.js';
|
|
46
47
|
import { spawn } from 'node:child_process';
|
|
47
48
|
|
|
48
49
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -1104,6 +1105,8 @@ async function main() {
|
|
|
1104
1105
|
process.stdout.write(TOKEN_GENERATED
|
|
1105
1106
|
? ' (loopback only; token generated for this run)\n'
|
|
1106
1107
|
: ' (loopback only; token from CREW_STUDIO_TOKEN)\n');
|
|
1108
|
+
const nodeNote = nodeAdvisoryNote();
|
|
1109
|
+
if (nodeNote) process.stdout.write(` warning: ${nodeNote}\n`);
|
|
1107
1110
|
|
|
1108
1111
|
// Recorded only after listen() succeeds, so the file never claims a port this process did not get. Failure
|
|
1109
1112
|
// to write is not fatal: the panel works, the next session simply cannot find it, which is where we started.
|
|
@@ -145,8 +145,11 @@ const firstTry = (r) => r.verify === 'pass' && r.fixes === 0;
|
|
|
145
145
|
*/
|
|
146
146
|
export function floorOfRow(r, ruleFloors = [], floors = []) {
|
|
147
147
|
let f = rankOf(AGENT_FLOORS[r.agent]);
|
|
148
|
-
|
|
148
|
+
const commitAgent = r.agent === 'crew-commit-agent';
|
|
149
|
+
if (r.verify === 'none' && r.change !== 'text' && !commitAgent) f = Math.max(f, 2);
|
|
149
150
|
if (r.change === 'test-write' || r.change === 'audit') f = Math.max(f, 2);
|
|
151
|
+
// The table's sonnet row, as the gate holds it: feature, fix-known and refactor, except for the commit agent.
|
|
152
|
+
if (!commitAgent && (r.change === 'feature' || r.change === 'fix-known' || r.change === 'refactor')) f = Math.max(f, 2);
|
|
150
153
|
if (r.risk === 'critical' && r.change !== 'test-run') f = Math.max(f, 3);
|
|
151
154
|
const any = (want, v) => want === '*' || want === v;
|
|
152
155
|
for (const x of ruleFloors) if (any(x.agent, r.agent) && any(x.change, r.change) && any(x.risk, r.risk)) f = Math.max(f, rankOf(x.model));
|
|
@@ -250,6 +253,9 @@ export function suggestionsOf(rows, floors = [], loosened = [], need = LOOSEN_AF
|
|
|
250
253
|
const k = keyOf(f);
|
|
251
254
|
// Once for each raise: a floor whose lowering stands is not offered again (the same rule the gate reads it by).
|
|
252
255
|
if (loweringOf(f, loosened)) continue;
|
|
256
|
+
// Not below the class's own floor: /crew-loosen refuses that, since the gate would refuse the lowered model anyway
|
|
257
|
+
// (a feature class raised to sonnet by the record of haiku runs from before its own floor was sonnet).
|
|
258
|
+
if (rankOf(TIERS[tier - 1]) < floorOfRow({ agent: f.agent, change: f.change, risk: f.risk, verify: 'pass' })) continue;
|
|
253
259
|
const passed = passedLatest(rows, k, f.model, Date.parse(f.when ?? '') || 0, need);
|
|
254
260
|
if (!passed) continue;
|
|
255
261
|
out.push({ key: k, agent: f.agent, change: f.change, risk: f.risk, from: f.model, to: TIERS[tier - 1], passed, heldSince: f.when ?? null });
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Is the Node running this panel missing a published security fix? The table is eval/lib/node-advisory.tsv, the
|
|
2
|
+
// same file eval/doctor.sh reads, so the two cannot disagree. A warning only: the panel still starts.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const TABLE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'eval', 'lib', 'node-advisory.tsv');
|
|
8
|
+
|
|
9
|
+
/** The table's text as { url, lines: Map(major -> 'x.y.z') }; null when it holds no release line. */
|
|
10
|
+
export function parseAdvisory(text) {
|
|
11
|
+
let url = '';
|
|
12
|
+
const lines = new Map();
|
|
13
|
+
for (const raw of String(text ?? '').split(/\r?\n/)) {
|
|
14
|
+
if (!raw || raw.startsWith('#')) continue;
|
|
15
|
+
const [k, v] = raw.split('\t');
|
|
16
|
+
if (k === 'advisory') url = v ?? '';
|
|
17
|
+
else if (/^\d+$/.test(k) && /^\d+\.\d+\.\d+$/.test(v ?? '')) lines.set(Number(k), v);
|
|
18
|
+
}
|
|
19
|
+
return lines.size ? { url, lines } : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parts = (v) => String(v).replace(/^v/, '').split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
23
|
+
const below = (a, b) => { const x = parts(a), y = parts(b); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i]; return false; };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The sentence to print for `version` (process.versions.node), or null when there is nothing to say: it carries
|
|
27
|
+
* its line's fixes, its line is newer than the table, or the table could not be read.
|
|
28
|
+
*/
|
|
29
|
+
export function judgeNode(version, advisory) {
|
|
30
|
+
if (!advisory || !/^\d+\.\d+\.\d+/.test(String(version ?? ''))) return null;
|
|
31
|
+
const major = parts(version)[0];
|
|
32
|
+
const newest = Math.max(...advisory.lines.keys());
|
|
33
|
+
if (major > newest) return null;
|
|
34
|
+
const fixed = advisory.lines.get(major);
|
|
35
|
+
if (fixed && !below(version, fixed)) return null;
|
|
36
|
+
return fixed
|
|
37
|
+
? `Node ${version} is below ${fixed}, the release of its line with the fixes of ${advisory.url} — update Node`
|
|
38
|
+
: `Node ${version} is on a line that no longer gets security fixes (${advisory.url} fixes ${[...advisory.lines.values()].join(', ')}) — update Node`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** judgeNode for the running process, reading the table from disk; null when the table is not there. */
|
|
42
|
+
export function nodeAdvisoryNote(version = process.versions.node) {
|
|
43
|
+
let text;
|
|
44
|
+
try { text = fs.readFileSync(TABLE, 'utf8'); } catch { return null; }
|
|
45
|
+
return judgeNode(version, parseAdvisory(text));
|
|
46
|
+
}
|
|
@@ -65,12 +65,12 @@ export function workSteps(row) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* What is said about an agent
|
|
69
|
-
*
|
|
68
|
+
* What is said about an agent that is not crew-*: it has no floor and no line in the record, but the model gate still
|
|
69
|
+
* holds its expensive end, except for a card naming critical work (_am_outside in hooks/guard-agent-model.sh). Null for a crew agent or an unknown type.
|
|
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
|
|
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.';
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
package/package.json
CHANGED
package/start.sh
CHANGED
|
@@ -760,10 +760,14 @@ fi
|
|
|
760
760
|
# `--show-prefix` is the question itself: empty at the work-tree root, `sub/dir/` below it, non-zero exit
|
|
761
761
|
# outside a repo — and it carries no path spelling to disagree about. Verified here at a normal root, a
|
|
762
762
|
# worktree root, a subdirectory and a non-repo.
|
|
763
|
-
|
|
763
|
+
# ABSOLUTE, from git's own answer for the toplevel. A relative value resolves against the work tree a commit runs
|
|
764
|
+
# in, and a linked worktree (one Claude Code opens included) has no .claude/ when it is gitignored: measured, the
|
|
765
|
+
# trace scan blocked a commit at the root and let the same commit through in a worktree, with no hook run at all.
|
|
766
|
+
if PFX="$(git rev-parse --show-prefix 2>/dev/null)" && [ -z "$PFX" ] && TOP="$(git rev-parse --show-toplevel 2>/dev/null)" \
|
|
767
|
+
&& [ -n "$TOP" ] && git config core.hooksPath "$TOP/.claude/hooks" 2>/dev/null; then
|
|
764
768
|
{ _mt 'trace scan: core.hooksPath -> .claude/hooks (§4.1/§4.2 commit gate active)'; echo " ${_M}"; }
|
|
765
769
|
else
|
|
766
|
-
{ _mt 'NOTE: no git repository at this level; after %s run: %s' "'git init'"
|
|
770
|
+
{ _mt 'NOTE: no git repository at this level; after %s run: %s' "'git init'" "git config core.hooksPath \"\$PWD/.claude/hooks\""; echo " ${_M}"; }
|
|
767
771
|
fi
|
|
768
772
|
rm -rf "$SRC"
|
|
769
773
|
echo
|