crewforth 3.1.1 → 3.2.0-rc.1
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 +138 -0
- package/README.md +4 -3
- package/VERSION +1 -1
- package/adopt.sh +11 -0
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +9 -8
- package/kit/agents/crew-commit-agent.md +7 -2
- 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-security-expert.md +3 -1
- package/kit/agents/crew-session-manager.md +11 -9
- package/kit/eval/doctor.sh +91 -3
- 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 +98 -0
- package/kit/eval/lib/crew-map/parse.mjs +305 -0
- package/kit/eval/lib/crew-map/project.mjs +163 -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/scan.mjs +214 -0
- package/kit/eval/lib/crew-map/stacks.mjs +441 -0
- package/kit/eval/lib/crew-map/surface.mjs +457 -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/settings-json.awk +5 -0
- package/kit/eval/lib/star.sh +23 -7
- package/kit/eval/lib/statusline.settings.json +6 -0
- package/kit/eval/lib/statusline.sh +115 -0
- package/kit/eval/smoke-test.sh +465 -38
- package/kit/hooks/agent-outcome.sh +6 -3
- package/kit/hooks/guard-agent-model.sh +15 -5
- package/kit/hooks/guard-bash.sh +240 -182
- package/kit/hooks/guard-question.sh +73 -0
- package/kit/hooks/guard-write.sh +21 -0
- package/kit/hooks/prompt-approval.sh +320 -103
- package/kit/hooks/refuter-ledger.sh +80 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +33 -0
- package/kit/skills/crew-approve/SKILL.md +7 -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 +44 -0
- package/kit/skills/crew-review/SKILL.md +2 -0
- 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 +12 -0
- package/kit/studio/server/index.js +114 -11
- package/kit/studio/server/lib/crew-map.js +113 -0
- package/kit/studio/server/lib/model-outcomes.js +130 -15
- package/kit/studio/server/lib/pricing.js +3 -0
- package/kit/studio/server/lib/remote.js +99 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +214 -12
- package/kit/studio/web/canvas.js +44 -120
- package/kit/studio/web/graph-plan.js +10 -4
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/map-plan.js +341 -0
- package/kit/studio/web/map.js +503 -0
- package/kit/studio/web/models-plan.js +61 -7
- package/kit/studio/web/models.js +19 -3
- package/kit/studio/web/style.css +125 -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 +10 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# refuter-ledger.sh - the record of what crew-refuter concluded, written by the hook and not by the session.
|
|
3
|
+
#
|
|
4
|
+
# WHY. A findings document can say a finding was verified. That sentence is the session's own; the ledger is not.
|
|
5
|
+
# When a crew-refuter agent stops, this hook reads the TASK it was given (the first record of its own transcript)
|
|
6
|
+
# and the ANSWER it ended with (last_assistant_message), and adds one line when the two agree on the finding and
|
|
7
|
+
# the lens:
|
|
8
|
+
#
|
|
9
|
+
# ts session agent_id finding lens verdict .claude/state/crew-refuter.tsv
|
|
10
|
+
#
|
|
11
|
+
# kit/eval/lib/findings/findings.mjs `validate --ledger` holds a document to it: a medium-or-higher finding needs
|
|
12
|
+
# verifiers, and every verifier it names must be a line here, with the same finding, lens and verdict.
|
|
13
|
+
#
|
|
14
|
+
# Wired to SubagentStop with a matcher on the agent type, so it starts for crew-refuter only; the type is checked
|
|
15
|
+
# again here. It works whatever CREW_MODEL_ROUTING says: this is not model routing.
|
|
16
|
+
# THIS NEVER BLOCKS. It exits 0 on every path: a stop hook that failed closed would keep an agent running.
|
|
17
|
+
# The ledger is protected from the session the way the routing records are (guard-bash.sh, guard-write.sh): a plain
|
|
18
|
+
# attempt to write it is refused; a determined one (an interpreter, a name built from pieces) is not, and that is
|
|
19
|
+
# the stated scope of those guards.
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
export LC_ALL=C
|
|
22
|
+
_rl_here="${BASH_SOURCE%[/\\]*}"; [ "$_rl_here" = "${BASH_SOURCE}" ] && _rl_here=.
|
|
23
|
+
[ -f "$_rl_here/guard-agent-model.sh" ] || exit 0
|
|
24
|
+
. "$_rl_here/guard-agent-model.sh"
|
|
25
|
+
declare -F _json_slice >/dev/null 2>&1 || exit 0
|
|
26
|
+
|
|
27
|
+
IFS= read -r -d '' INPUT || true
|
|
28
|
+
_json_slice "$INPUT" hook_event_name >/dev/null; [ "$_JS" = SubagentStop ] || exit 0
|
|
29
|
+
_json_slice "$INPUT" agent_type >/dev/null; [ "${_JS##*:}" = crew-refuter ] || exit 0
|
|
30
|
+
_json_slice "$INPUT" agent_id >/dev/null; AID="$_JS"
|
|
31
|
+
case "$AID" in ''|*[!A-Za-z0-9._-]*) exit 0 ;; esac
|
|
32
|
+
_json_slice "$INPUT" session_id >/dev/null; SID="$_JS"
|
|
33
|
+
case "$SID" in ''|*[!A-Za-z0-9._-]*) SID=- ;; esac
|
|
34
|
+
|
|
35
|
+
# One field of a text: the value after "<key>:" at the start of a line, first occurrence, trimmed.
|
|
36
|
+
_rl_field(){ # $1 text, $2 key (any case) -> _RLF
|
|
37
|
+
local line k; _RLF=""
|
|
38
|
+
while IFS= read -r line; do
|
|
39
|
+
line="${line%$'\r'}"; line="${line#"${line%%[![:space:]]*}"}"
|
|
40
|
+
k="${line%%:*}"; [ "$k" = "$line" ] && continue
|
|
41
|
+
shopt -s nocasematch
|
|
42
|
+
if [[ "$k" == "$2" ]]; then shopt -u nocasematch
|
|
43
|
+
_RLF="${line#*:}"; _RLF="${_RLF#"${_RLF%%[![:space:]]*}"}"; _RLF="${_RLF%"${_RLF##*[![:space:]]}"}"; return 0
|
|
44
|
+
fi
|
|
45
|
+
shopt -u nocasematch
|
|
46
|
+
done <<< "$1"
|
|
47
|
+
return 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# The task: the first record of the agent's own transcript (a string, or a list whose first block holds the text).
|
|
51
|
+
_json_slice "$INPUT" agent_transcript_path >/dev/null; _json_unescape "$_JS" >/dev/null; ATP="${_JU//\\//}"
|
|
52
|
+
[ -f "$ATP" ] || exit 0
|
|
53
|
+
L1=""; IFS= read -r L1 < "$ATP" || true
|
|
54
|
+
_json_slice "$L1" content >/dev/null; TXT="$_JS"
|
|
55
|
+
[ -n "$TXT" ] || { _json_slice "$L1" text >/dev/null; TXT="$_JS"; }
|
|
56
|
+
_json_unescape "${TXT:0:8000}" >/dev/null; TASK="$_JU"
|
|
57
|
+
_rl_field "$TASK" finding || exit 0; F_TASK="$_RLF"
|
|
58
|
+
_rl_field "$TASK" lens || exit 0; L_TASK="$_RLF"
|
|
59
|
+
|
|
60
|
+
# The answer, and it must be about the same finding and lens.
|
|
61
|
+
_json_slice "$INPUT" last_assistant_message >/dev/null; _json_unescape "$_JS" >/dev/null; ANS="$_JU"
|
|
62
|
+
_rl_field "$ANS" finding || exit 0; F_ANS="$_RLF"
|
|
63
|
+
_rl_field "$ANS" lens || exit 0; L_ANS="$_RLF"
|
|
64
|
+
_rl_field "$ANS" verdict || exit 0; V="$_RLF"
|
|
65
|
+
[ "$F_TASK" = "$F_ANS" ] && [ "$L_TASK" = "$L_ANS" ] || exit 0
|
|
66
|
+
[[ "$F_TASK" =~ ^[a-z0-9][a-z0-9._:-]*$ ]] && [ "${#F_TASK}" -le 200 ] || exit 0
|
|
67
|
+
case "$L_TASK" in reachability|protection|reproduction|exclusion|blast-radius|claim) ;; *) exit 0 ;; esac
|
|
68
|
+
case "$V" in TRUE_POSITIVE|FALSE_POSITIVE|CANNOT_VERIFY) ;; *) exit 0 ;; esac
|
|
69
|
+
|
|
70
|
+
P="${CLAUDE_PROJECT_DIR:-$PWD}"; P="${P//\\//}"; P="${P%/}"
|
|
71
|
+
D="$P/.claude/state"; T="$D/crew-refuter.tsv"
|
|
72
|
+
mkdir -p "$D" 2>/dev/null || exit 0
|
|
73
|
+
# An agent that is resumed stops again: it answered once, and that answer stands.
|
|
74
|
+
if [ -f "$T" ]; then
|
|
75
|
+
while IFS=$'\t' read -r _ _ a _; do [ "$a" = "$AID" ] && exit 0; done < "$T"
|
|
76
|
+
fi
|
|
77
|
+
[ -s "$T" ] || printf 'ts\tsession\tagent_id\tfinding\tlens\tverdict\n' > "$T" 2>/dev/null
|
|
78
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)"; case "$TS" in [0-9][0-9][0-9][0-9]-*Z) ;; *) TS=- ;; esac
|
|
79
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$TS" "$SID" "$AID" "$F_TASK" "$L_TASK" "$V" >> "$T" 2>/dev/null
|
|
80
|
+
exit 0
|
package/kit/owned-blobs.tsv
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.1.
|
|
1
|
+
# Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.1.1 (below VERSION 3.2.0) — do not edit by hand.
|
|
2
2
|
# The flat files Crewforth owns: a copy whose bytes (CR aside) are one of these ids was never edited.
|
|
3
3
|
# install_path blob_sha1 first_tag last_tag
|
|
4
4
|
.claude/AGENT_TEMPLATE.md 075e2c26706c06f56af01652cd24c36918513bee v3.0.0 v3.0.4
|
|
5
5
|
.claude/AGENT_TEMPLATE.md 3fdc551d221ee3665b21ef8cc3563de861a9abe6 v1.4.0 v1.6.3
|
|
6
6
|
.claude/AGENT_TEMPLATE.md 686fed306c873b45c245c77a95a8e6de6ee383d5 v1.7.0 v2.11.0
|
|
7
|
-
.claude/AGENT_TEMPLATE.md 7ab19417b6ff3ac732e2c894be17ffb3ec091f7d v3.1.0 v3.1.
|
|
7
|
+
.claude/AGENT_TEMPLATE.md 7ab19417b6ff3ac732e2c894be17ffb3ec091f7d v3.1.0 v3.1.1
|
|
8
8
|
.claude/AGENT_TEMPLATE.md 981df9ea27b6f1c5ab5cb509d40875a07603858d v2.12.0 v2.13.0
|
|
9
9
|
.claude/AGENT_TEMPLATE.md b38b9c9b799156e07280ca886230524563acc75c v1.0.5 v1.3.0
|
|
10
10
|
.claude/AGENT_TEMPLATE.md faec09e45543b75763c43bb24114a7b6355649ec v1.0.0 v1.0.4
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
.claude/DISCIPLINE.md 76d73a4e25c559ea361618862cde8480b1a6d8c2 v3.0.0 v3.0.0
|
|
27
27
|
.claude/DISCIPLINE.md b1ab7549b571dd2fc80d5777e0ae3185d29bd7b6 v2.13.0 v2.13.0
|
|
28
28
|
.claude/DISCIPLINE.md b7e1066451a8590e0a2607ad0a6fe80b21599c53 v1.1.3 v1.1.8
|
|
29
|
-
.claude/DISCIPLINE.md b90a067ee323a08146862b889293f8feb49f8e7e v3.1.0 v3.1.
|
|
29
|
+
.claude/DISCIPLINE.md b90a067ee323a08146862b889293f8feb49f8e7e v3.1.0 v3.1.1
|
|
30
30
|
.claude/DISCIPLINE.md bf062c2b51a9cf3cc4d0caac389c148cf6fdc977 v2.12.0 v2.12.0
|
|
31
31
|
.claude/DISCIPLINE.md c693f1bd5832ce570058c6eedcacb5dff13d89e8 v1.10.0 v1.10.0
|
|
32
32
|
.claude/DISCIPLINE.md d63a01a183c9a80d739c840a511d338dc2765820 v1.4.0 v1.4.4
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
.claude/README.md 97d4dd1f7f1695052cc1d06cc43f5bcdc603c989 v1.0.0 v1.0.4
|
|
44
44
|
.claude/README.md a3419891cfda7385564008e4befc1007bc5364cb v1.1.1 v1.7.0
|
|
45
45
|
.claude/README.md a3b76e0fa31267610d28b742c122a962f6db59c4 v3.0.1 v3.0.4
|
|
46
|
-
.claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.1.
|
|
46
|
+
.claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.1.1
|
package/kit/settings.json
CHANGED
|
@@ -108,6 +108,17 @@
|
|
|
108
108
|
"timeout": 600
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"matcher": "AskUserQuestion",
|
|
114
|
+
"hooks": [
|
|
115
|
+
{
|
|
116
|
+
"type": "command",
|
|
117
|
+
"shell": "bash",
|
|
118
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/guard-question.sh",
|
|
119
|
+
"timeout": 600
|
|
120
|
+
}
|
|
121
|
+
]
|
|
111
122
|
}
|
|
112
123
|
],
|
|
113
124
|
"UserPromptSubmit": [
|
|
@@ -168,6 +179,17 @@
|
|
|
168
179
|
"timeout": 600
|
|
169
180
|
}
|
|
170
181
|
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"matcher": "(.*:)?crew-refuter",
|
|
185
|
+
"hooks": [
|
|
186
|
+
{
|
|
187
|
+
"type": "command",
|
|
188
|
+
"shell": "bash",
|
|
189
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/refuter-ledger.sh",
|
|
190
|
+
"timeout": 30
|
|
191
|
+
}
|
|
192
|
+
]
|
|
171
193
|
}
|
|
172
194
|
],
|
|
173
195
|
"PostToolUse": [
|
|
@@ -181,6 +203,17 @@
|
|
|
181
203
|
"timeout": 60
|
|
182
204
|
}
|
|
183
205
|
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"matcher": "AskUserQuestion",
|
|
209
|
+
"hooks": [
|
|
210
|
+
{
|
|
211
|
+
"type": "command",
|
|
212
|
+
"shell": "bash",
|
|
213
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/prompt-approval.sh",
|
|
214
|
+
"timeout": 60
|
|
215
|
+
}
|
|
216
|
+
]
|
|
184
217
|
}
|
|
185
218
|
],
|
|
186
219
|
"SessionStart": [
|
|
@@ -26,4 +26,11 @@ Read the line Crewforth added to this turn and do what it says:
|
|
|
26
26
|
the argument was not `commit`, `push` or `commit+push`. Say which, and in the first case run the command and let
|
|
27
27
|
the gate's own prompt ask.
|
|
28
28
|
|
|
29
|
+
In `auto` the same approval can be given with one click: the gate's refusal prints a fixed AskUserQuestion, and
|
|
30
|
+
the user's answer is recorded exactly like this command. Where a `PermissionRequest` hook may be wired (in the
|
|
31
|
+
user's, the project's or a plugin's settings, or in a skill's or an agent's front matter), that question is closed,
|
|
32
|
+
and so is the gate's own prompt in `default` / `acceptEdits`, because such a hook answers a prompt before the user
|
|
33
|
+
does. This command is then the one approval left, in those modes too. Settings passed with `--settings` and
|
|
34
|
+
managed settings delivered by a server cannot be read, so a hook there is past the check.
|
|
35
|
+
|
|
29
36
|
Force-push and the rest of §4.5 are not opened by this.
|
|
@@ -24,7 +24,7 @@ Verify Crewforth is actually *active* in this project (not just present on disk)
|
|
|
24
24
|
5. Summarise: **healthy**, or the precise fixes applied/needed, plus the readiness score. If it's not a git repo,
|
|
25
25
|
note that the commit-time gates need `git init` + `git config core.hooksPath .claude/hooks`.
|
|
26
26
|
If the output contains a line starting with ⭐, pass it to the user verbatim as the LAST line of your summary —
|
|
27
|
-
it prints once per Crewforth version, and it is meant for the user, not for you.
|
|
27
|
+
it prints once per Crewforth version here (the installer has its own once), and it is meant for the user, not for you.
|
|
28
28
|
|
|
29
29
|
If `.claude/eval/doctor.sh` doesn't exist, this is not a full (start.sh / adopt.sh) install — Crewforth is likely
|
|
30
30
|
running as a **plugin**, whose hooks are managed by Claude Code itself; there's nothing for the doctor to check,
|
|
@@ -52,3 +52,7 @@ found no Git Bash: tell the user the gates do not run and that the fix is Git fo
|
|
|
52
52
|
scripts are developer instruments: they inspect an installation from outside it, and the plugin edition has no
|
|
53
53
|
installation to inspect. Say this plainly when someone asks why `/crew-doctor` reports only the listing (and, on Windows, Git Bash) on a plugin install,
|
|
54
54
|
rather than treating it as a defect. The Studio panel ships in both editions (`/crew-studio`).
|
|
55
|
+
|
|
56
|
+
**On a plugin install, end with the star line** the full install's doctor prints: `bash "${CLAUDE_PLUGIN_ROOT}/eval/lib/star.sh" --once . doctor`
|
|
57
|
+
(Bash tool, not PowerShell). It prints once per plugin version, or nothing; if it prints a line starting with ⭐, pass it to the
|
|
58
|
+
user verbatim as the LAST line of your summary.
|
|
@@ -6,8 +6,9 @@ metadata:
|
|
|
6
6
|
---
|
|
7
7
|
# /crew-handoff
|
|
8
8
|
applies the `handoff` skill:
|
|
9
|
-
1.
|
|
10
|
-
|
|
9
|
+
1. **crew-session-manager** (@agent-crew-session-manager) measures the fill (`context-usage.sh`, never a guess), gives the
|
|
10
|
+
recommendation and drafts the handover. The steps below work from its draft.
|
|
11
|
+
2. Write the handover to `docs/SESSION_STATE.md`: Done · In progress (exactly where) · Next step · Open decisions · File pointers · Blockers.
|
|
11
12
|
3. Preserve decision rationale; the next session should not start from scratch.
|
|
12
13
|
4. **Cold-reader pass** (`handoff/references/cold-reader.md`): the questions are written from the work BEFORE the
|
|
13
14
|
file, then answered from the file alone. Anything unanswerable goes back into the handover and is re-checked.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crew-map
|
|
3
|
+
description: Map the project — modules, what they touch, the gaps between them — and describe each module from the lines the scan cites.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
metadata:
|
|
6
|
+
kind: command
|
|
7
|
+
---
|
|
8
|
+
# /crew-map
|
|
9
|
+
|
|
10
|
+
Write `.crewforth/crew-map.json`, then describe it. The scanner draws the map from the files; you add only what
|
|
11
|
+
it cannot: a sentence or two on what each module is for, and a short overview. Every description points at lines
|
|
12
|
+
the scan itself read, and a gate holds it to that. **Never commit.** Show the diff and stop.
|
|
13
|
+
|
|
14
|
+
1. **Resolve the scanner.** Both editions carry it, in different places:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
PLUGIN_MAP=${CLAUDE_PLUGIN_ROOT}/eval/lib/crew-map/crew-map.sh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Claude Code replaces `${CLAUDE_PLUGIN_ROOT}` in this text before you read it, and only in the plugin. If the
|
|
21
|
+
line above names a real path, that is the scanner; if it still shows `${CLAUDE_PLUGIN_ROOT}`, this is a full
|
|
22
|
+
install and the scanner is `.claude/eval/lib/crew-map/crew-map.sh`. Below, `MAP` is whichever it is.
|
|
23
|
+
|
|
24
|
+
2. **Scan:** `bash MAP` at the project root (Bash tool, not PowerShell). It writes the map and keeps the
|
|
25
|
+
descriptions of an earlier run that still cite only lines this scan cites; it says how many it kept and dropped.
|
|
26
|
+
rc 3 is no usable Node: say so and stop, it is not a pass.
|
|
27
|
+
|
|
28
|
+
3. **Describe.** Read `.crewforth/crew-map.json`, and as much of the cited source as you need. Then edit that
|
|
29
|
+
file, changing ONLY these fields:
|
|
30
|
+
- each `module` node's `narrative`: `{ "text": "…", "cites": ["file:line", …] }`. What the module is for, in
|
|
31
|
+
at most two sentences. Every cite is a `file:line` from THAT node's own `evidence`; cite at least one. Leave a
|
|
32
|
+
node `null` when its evidence does not tell you what it is for: an honest gap beats a guess.
|
|
33
|
+
- the document's `overview`: the same shape, at most 2000 characters, for someone new to the project: the entry
|
|
34
|
+
points, where data goes, the risky places. Its cites may be any node's evidence.
|
|
35
|
+
Describe what the cited lines show. A connection, a purpose or a risk the scan did not find is not added here:
|
|
36
|
+
say it to the user instead.
|
|
37
|
+
|
|
38
|
+
4. **Validate:** `bash MAP --validate` (Bash tool, not PowerShell). It refuses a description that cites a line
|
|
39
|
+
the scan did not read, cites nothing, or runs past two sentences, and a map whose scanner fields were changed.
|
|
40
|
+
Fix what it names, in the descriptions only, until it holds.
|
|
41
|
+
|
|
42
|
+
5. **Hand over:** the scan's summary line, how many modules you described and how many you left `null` and why,
|
|
43
|
+
then `git diff --stat .crewforth/crew-map.json`. Studio's **Map** view (`/crew-studio`) draws the result.
|
|
44
|
+
Committing it is the user's call.
|
|
@@ -9,6 +9,8 @@ Run the change set through the review trio (read-only). **The audits go out in p
|
|
|
9
9
|
1. **At once, in ONE message** — several `Agent` calls, because none of these writes code and so there is
|
|
10
10
|
nothing to serialise:
|
|
11
11
|
- @agent-crew-security-expert (security-scan) — auth/IDOR/injection/secret; findings with severity.
|
|
12
|
+
Each `medium`-or-higher one then goes to **crew-refuter** (@agent-crew-refuter; claim + `file:line`, one call per lens, in
|
|
13
|
+
parallel) before it is reported; `findings.sh write --verified` refuses one its ledger does not back.
|
|
12
14
|
- @agent-crew-performance-expert (`performance`) — hot path, query/loop, render, payload. Reports
|
|
13
15
|
**candidates** (reasoned) and **findings** (measured) separately; an unmeasured claim is never a verdict.
|
|
14
16
|
- @agent-crew-privacy-agent (`privacy-compliance`) — **when the diff touches personal data**: legal basis,
|
|
@@ -32,7 +32,7 @@ Bring the Crewforth install up to the latest published version:
|
|
|
32
32
|
(no flags) in **their own terminal**, where both the npx and the interactive prompts work.
|
|
33
33
|
7. **After:** run `bash .claude/eval/update-guard.sh post` (Bash tool, not PowerShell) and show the user its lists — added, changed, moved,
|
|
34
34
|
removed.
|
|
35
|
-
8. **Verify:** run
|
|
35
|
+
8. **Verify:** run `CREW_NO_STAR=1 bash .claude/eval/doctor.sh` (Bash tool, not PowerShell) so a bad/partial update surfaces immediately; the updater has already shown the star line, if it was due.
|
|
36
36
|
If the updater's or doctor's output contains a line starting with ⭐, pass it to the user verbatim as the LAST
|
|
37
37
|
line of your summary — it prints once per Crewforth version, and it is meant for the user, not for you.
|
|
38
38
|
9. **Report** old → new + the headline changes. Take them from `.claude/.state/whats-new.md` — the updater writes
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: incident-runbook
|
|
3
3
|
description: |
|
|
4
|
-
|
|
4
|
+
Incident response: diagnose → mitigate → resolve, then a blameless postmortem and a repeatable
|
|
5
5
|
runbook. Stop the impact first, root cause second.
|
|
6
|
-
Use when production is degraded or broken,
|
|
6
|
+
Use when production is degraded or broken, an outage keeps recurring, or for the postmortem.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Incident Response & Runbook
|
|
@@ -19,9 +19,12 @@ audit). Each verifier:
|
|
|
19
19
|
Three passes inside one context window are not three verifiers. The second pass has already read the first one's
|
|
20
20
|
reasoning; the blindness above is then a wish, not a property, and the agreement it produces is one argument
|
|
21
21
|
counted three times. So:
|
|
22
|
-
- **Each verifier is its own
|
|
23
|
-
context window: it cannot see what the others concluded, which is the whole point. Read-only work,
|
|
24
|
-
`worktree` is needed — nothing is being written.
|
|
22
|
+
- **Each verifier is its own `crew-refuter` call** (one call per verifier and lens, run in parallel). A subagent
|
|
23
|
+
has its own context window: it cannot see what the others concluded, which is the whole point. Read-only work,
|
|
24
|
+
so no `worktree` is needed — nothing is being written. Its task is the card, then `finding:`, `lens:`, `cite:`
|
|
25
|
+
and `claim:` (its agent file has the exact form); when it stops, `hooks/refuter-ledger.sh` records the verdict
|
|
26
|
+
in the refuter ledger, and `findings.sh write --verified` refuses a `medium`-or-higher finding whose verifiers
|
|
27
|
+
are not there. The ledger is the gate; the prompt that carries no reasoning is still discipline.
|
|
25
28
|
- **Give it the finding and the file:line, never your reasoning.** Not "I think X is exploitable because Y" —
|
|
26
29
|
just "claim: X at file:line. Try to refute it."
|
|
27
30
|
- **If you did not isolate, say so.** Write `verifiers: 1 (single pass, not isolated)` in the verdict block. A
|
package/kit/studio/README.md
CHANGED
|
@@ -198,6 +198,18 @@ reported as unreachable, never as empty.
|
|
|
198
198
|
Nothing listens on a network interface in this arrangement: both Studios stay on
|
|
199
199
|
loopback and the tunnel does the crossing.
|
|
200
200
|
|
|
201
|
+
## Remote viewers
|
|
202
|
+
|
|
203
|
+
A viewer is a second token that reads and changes nothing, for watching the
|
|
204
|
+
panel from another device without handing that device the panel. Add one in the
|
|
205
|
+
Machines tab; its link is shown once. The server refuses a viewer's every
|
|
206
|
+
request that is not a `GET`, and its reads of `/api/remote`, with 403 before any
|
|
207
|
+
handler runs (`server/lib/remote.js`). Revoke ends the token and closes a
|
|
208
|
+
stream it holds open. Viewers live in memory and end with the server.
|
|
209
|
+
|
|
210
|
+
The server still binds to loopback. Reaching it from a phone is a tunnel the
|
|
211
|
+
user runs, an ssh forward or `tailscale serve 7777`; Studio starts none.
|
|
212
|
+
|
|
201
213
|
## What Crewforth already measured
|
|
202
214
|
|
|
203
215
|
The panel does not recompute any of it. Selecting the session node opens what
|
|
@@ -21,7 +21,8 @@ import { projectDir, listSessions, findSession, listProjects, sessionCwd, agentM
|
|
|
21
21
|
import { agentOrigins } from './lib/agent-origin.js';
|
|
22
22
|
import { buildGraph, agentDetail, conversation, STALE_MS } from './lib/graph.js';
|
|
23
23
|
import { cachedUsage } from './lib/usage.js';
|
|
24
|
-
import { readModels, readRecord, readCosts } from './lib/model-outcomes.js';
|
|
24
|
+
import { readModels, readRecord, readCosts, readAboveCosts } from './lib/model-outcomes.js';
|
|
25
|
+
import { readMap, behindOf, compareMaps, mapSummary, MAP_FILE } from './lib/crew-map.js';
|
|
25
26
|
|
|
26
27
|
const USAGE_BATCH = 12;
|
|
27
28
|
import { palette } from './lib/palette.js';
|
|
@@ -35,6 +36,7 @@ import { open as openTerminal, plan as terminalPlan } from './lib/terminal.js';
|
|
|
35
36
|
import { gateLog, gateReport, sessionStats, board } from './lib/kit-telemetry.js';
|
|
36
37
|
import { writeState, clearStateSync, findRunning } from './lib/instance.js';
|
|
37
38
|
import { remoteRoster } from './lib/roster.js';
|
|
39
|
+
import { addViewer, listViewers, viewerByToken, revokeViewer, noteRequest, viewerMay, MAX_VIEWERS } from './lib/remote.js';
|
|
38
40
|
import { randomUUID } from 'node:crypto';
|
|
39
41
|
import { spawn } from 'node:child_process';
|
|
40
42
|
|
|
@@ -108,15 +110,28 @@ function sendJson(res, status, value) {
|
|
|
108
110
|
const TOKEN = process.env.CREW_STUDIO_TOKEN || randomUUID();
|
|
109
111
|
const TOKEN_GENERATED = !process.env.CREW_STUDIO_TOKEN;
|
|
110
112
|
|
|
113
|
+
/** For the self-check, which asks a real server as the owner and as a viewer. */
|
|
114
|
+
export const _internals = { token: TOKEN };
|
|
115
|
+
|
|
111
116
|
// Peers are other machines running Studio, reached over a forwarded port. This
|
|
112
117
|
// server still listens on loopback only; a peer never widens that.
|
|
113
118
|
let PEERS = [];
|
|
114
119
|
let SELF_NAME = 'this machine';
|
|
115
120
|
|
|
116
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Whose request this is: the owner's, a read-only viewer's (lib/remote.js), or nobody's.
|
|
123
|
+
* The token is read from the same two places for both, so a viewer's link works exactly as the owner's does.
|
|
124
|
+
*/
|
|
125
|
+
function whoAsks(req, url) {
|
|
117
126
|
const header = req.headers['authorization'];
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
const bearer = typeof header === 'string' && header.startsWith('Bearer ') ? header.slice(7) : null;
|
|
128
|
+
const given = [bearer, url.searchParams.get('token')].filter(Boolean);
|
|
129
|
+
if (given.includes(TOKEN)) return { role: 'owner' };
|
|
130
|
+
for (const t of given) {
|
|
131
|
+
const viewer = viewerByToken(t);
|
|
132
|
+
if (viewer) return { role: 'viewer', viewer };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
/**
|
|
@@ -167,10 +182,16 @@ async function modelCosts(cwd) {
|
|
|
167
182
|
for (const s of (await listSessions(dir)).filter((x) => x.modifiedAt >= since).slice(0, MODELS_SESSIONS_MAX)) {
|
|
168
183
|
const u = await cachedUsage(s);
|
|
169
184
|
read += 1;
|
|
170
|
-
for (const [id, a] of Object.entries(u.agents ?? {})) per.set(id, a
|
|
185
|
+
for (const [id, a] of Object.entries(u.agents ?? {})) per.set(id, a);
|
|
171
186
|
}
|
|
172
187
|
}
|
|
173
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
measured: true,
|
|
190
|
+
costs: readCosts(cwd, (id) => per.get(id)?.cost ?? null,
|
|
191
|
+
(id) => (per.get(id)?.advisorCalls ? { calls: per.get(id).advisorCalls, cost: per.get(id).advisorCost } : null)),
|
|
192
|
+
above: readAboveCosts(cwd, (id) => (per.has(id) ? { cost: per.get(id).cost, atNormal: per.get(id).costAtNormal } : null)),
|
|
193
|
+
sessionsRead: read,
|
|
194
|
+
};
|
|
174
195
|
}
|
|
175
196
|
const MODELS_SESSIONS_MAX = 60;
|
|
176
197
|
|
|
@@ -214,20 +235,29 @@ async function serveStatic(res, urlPath) {
|
|
|
214
235
|
}
|
|
215
236
|
}
|
|
216
237
|
|
|
217
|
-
async function handle(req, res) {
|
|
238
|
+
export async function handle(req, res) {
|
|
218
239
|
const url = new URL(req.url, `http://${LOOPBACK}`);
|
|
219
240
|
|
|
220
241
|
// The token guards data and actions, not the app shell. A browser does not
|
|
221
242
|
// carry the page's query string into its sub-resource requests, so demanding
|
|
222
243
|
// it for /style.css served an unstyled page and protected nothing: the shell
|
|
223
244
|
// holds no secrets, and every /api/ path below is still gated.
|
|
224
|
-
|
|
245
|
+
const who = url.pathname.startsWith('/api/') ? whoAsks(req, url) : null;
|
|
246
|
+
if (url.pathname.startsWith('/api/') && !who) {
|
|
225
247
|
return sendJson(res, 403, { ok: false, reason: 'token required' });
|
|
226
248
|
}
|
|
249
|
+
// A viewer is refused HERE, before any handler: no route below has to remember that a viewer exists, and a
|
|
250
|
+
// route added tomorrow is read-only for a viewer without anyone writing a line for it.
|
|
251
|
+
if (who?.role === 'viewer') {
|
|
252
|
+
const may = viewerMay(req.method, url.pathname);
|
|
253
|
+
if (!may.ok) return sendJson(res, 403, { ok: false, reason: may.reason });
|
|
254
|
+
noteRequest(who.viewer, req, res);
|
|
255
|
+
}
|
|
227
256
|
|
|
228
257
|
if (url.pathname === '/api/health') {
|
|
229
258
|
return sendJson(res, 200, {
|
|
230
259
|
ok: true,
|
|
260
|
+
role: who.role,
|
|
231
261
|
node: process.version,
|
|
232
262
|
pid: process.pid,
|
|
233
263
|
uptimeMs: Math.round(process.uptime() * 1000),
|
|
@@ -288,7 +318,12 @@ async function handle(req, res) {
|
|
|
288
318
|
// dead time whenever the feed hung, measured. The refresh runs behind this response.
|
|
289
319
|
const latest = latestVersionCached();
|
|
290
320
|
const projects = await listProjects({ currentCwd: cwd, kitOf: (c) => kitStatus(c, latest) });
|
|
291
|
-
for (const p of projects) {
|
|
321
|
+
for (const p of projects) {
|
|
322
|
+
p.origin = SELF_NAME; p.local = true;
|
|
323
|
+
// The navigator's Map entry and its count: the findings the map shows unasked. A stat per project; the map
|
|
324
|
+
// is parsed again only when its file changes (crew-map.js).
|
|
325
|
+
p.map = p.exists && p.cwd ? mapSummary(p.cwd) : null;
|
|
326
|
+
}
|
|
292
327
|
|
|
293
328
|
const origins = [{ name: SELF_NAME, local: true, ok: true, projects: projects.length }];
|
|
294
329
|
if (PEERS.length) {
|
|
@@ -339,7 +374,7 @@ async function handle(req, res) {
|
|
|
339
374
|
const sid = url.searchParams.get('session');
|
|
340
375
|
const session = sid ? await findSession(sid) : null;
|
|
341
376
|
const cwd = url.searchParams.get('cwd') || (session ? await sessionCwd(session.file, session.bytes) : null);
|
|
342
|
-
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', classes: [], suggestions: [], byAgent: {} });
|
|
377
|
+
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', classes: [], suggestions: [], above: [], byAgent: {} });
|
|
343
378
|
return sendJson(res, 200, readModels(cwd, { agents: await agentIdsOf(session) }));
|
|
344
379
|
}
|
|
345
380
|
|
|
@@ -347,10 +382,29 @@ async function handle(req, res) {
|
|
|
347
382
|
const sid = url.searchParams.get('session');
|
|
348
383
|
const session = sid ? await findSession(sid) : null;
|
|
349
384
|
const cwd = url.searchParams.get('cwd') || (session ? await sessionCwd(session.file, session.bytes) : null);
|
|
350
|
-
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', costs: {}, sessionsRead: 0 });
|
|
385
|
+
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', costs: {}, above: {}, sessionsRead: 0 });
|
|
351
386
|
return sendJson(res, 200, await modelCosts(cwd));
|
|
352
387
|
}
|
|
353
388
|
|
|
389
|
+
// The project's crew map, as Crewforth's scanner wrote it, and how far HEAD has moved since. Nothing is scanned here.
|
|
390
|
+
// `compare` names another project of this machine by its directory; only a project the panel lists, with a map
|
|
391
|
+
// of its own, is read, so the route cannot be pointed at an arbitrary path.
|
|
392
|
+
if (url.pathname === '/api/map') {
|
|
393
|
+
const sid = url.searchParams.get('session');
|
|
394
|
+
const session = sid ? await findSession(sid) : null;
|
|
395
|
+
const cwd = url.searchParams.get('cwd') || (session ? await sessionCwd(session.file, session.bytes) : null);
|
|
396
|
+
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', others: [] });
|
|
397
|
+
const map = readMap(cwd);
|
|
398
|
+
const others = (await listProjects({ currentCwd: cwd })).filter((p) => p.cwd && p.cwd !== cwd && readMap(p.cwd).measured)
|
|
399
|
+
.map((p) => ({ label: p.label, cwd: p.cwd }));
|
|
400
|
+
if (!map.measured) return sendJson(res, 200, { ...map, cwd, others });
|
|
401
|
+
const asked = url.searchParams.get('compare');
|
|
402
|
+
const peer = asked ? others.find((o) => o.cwd === asked) : null;
|
|
403
|
+
const compare = peer ? { label: peer.label, cwd: peer.cwd, clients: compareMaps(map.doc, readMap(peer.cwd).doc) }
|
|
404
|
+
: asked ? { label: null, cwd: asked, clients: [], reason: 'not a project of this machine with a crew map' } : null;
|
|
405
|
+
return sendJson(res, 200, { ...map, cwd, behind: await behindOf(cwd, map.doc.source?.commit), others, compare });
|
|
406
|
+
}
|
|
407
|
+
|
|
354
408
|
if (url.pathname === '/api/kit') {
|
|
355
409
|
const cwd = url.searchParams.get('cwd') || process.cwd();
|
|
356
410
|
const sid = url.searchParams.get('session');
|
|
@@ -463,6 +517,30 @@ async function handle(req, res) {
|
|
|
463
517
|
return stream(req, res, url);
|
|
464
518
|
}
|
|
465
519
|
|
|
520
|
+
/* --------------------------------------------------------- remote ---
|
|
521
|
+
Read-only viewers: who holds a link, and the way to take it back. Only
|
|
522
|
+
the owner reaches this far; a viewer was refused above. */
|
|
523
|
+
|
|
524
|
+
if (url.pathname === '/api/remote' && req.method === 'GET') {
|
|
525
|
+
return sendJson(res, 200, { measured: true, viewers: listViewers(), max: MAX_VIEWERS, now: Date.now() });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const remoteMatch = url.pathname.match(/^\/api\/remote(?:\/([^/]+))?$/);
|
|
529
|
+
if (remoteMatch) {
|
|
530
|
+
const gate = writeAllowed(req);
|
|
531
|
+
if (!gate.ok) return sendJson(res, 403, { ok: false, reason: gate.reason });
|
|
532
|
+
if (req.method === 'POST' && !remoteMatch[1]) {
|
|
533
|
+
const body = await readBody(req);
|
|
534
|
+
if (!body) return sendJson(res, 400, { ok: false, reason: 'body was not JSON' });
|
|
535
|
+
const made = addViewer(body.label);
|
|
536
|
+
return sendJson(res, made.ok ? 201 : 409, { ...made, viewers: listViewers() });
|
|
537
|
+
}
|
|
538
|
+
if (req.method === 'DELETE' && remoteMatch[1]) {
|
|
539
|
+
return sendJson(res, 200, { ...revokeViewer(decodeURIComponent(remoteMatch[1])), viewers: listViewers() });
|
|
540
|
+
}
|
|
541
|
+
return sendJson(res, 405, { ok: false, reason: 'POST to add a viewer, or DELETE a viewer id' });
|
|
542
|
+
}
|
|
543
|
+
|
|
466
544
|
/* ---------------------------------------------------------- owned ---
|
|
467
545
|
Sessions the panel started. Their transcripts land in the usual place,
|
|
468
546
|
so the graph endpoints already cover them; only the conversation needs
|
|
@@ -830,6 +908,28 @@ async function selftest() {
|
|
|
830
908
|
fleet.measured ? `${fleet.sessions.length} session(s)` : `not measured: ${fleet.reason}`,
|
|
831
909
|
);
|
|
832
910
|
|
|
911
|
+
// The Map view: its files are installed, and the reader takes a crew map and refuses one of another schema. On a
|
|
912
|
+
// scratch map of its own, so the verdict does not depend on whether this project has one; this project's own map,
|
|
913
|
+
// when there is one, is reported beside it.
|
|
914
|
+
check('map view installed', ['map.js', 'map-plan.js', 'viewport.js'].every((f) => fs.existsSync(path.join(WEB_ROOT, f))), null);
|
|
915
|
+
{
|
|
916
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'crew-studio-map-'));
|
|
917
|
+
try {
|
|
918
|
+
fs.mkdirSync(path.join(dir, '.crewforth'));
|
|
919
|
+
const doc = { schema: 'crewforth.crew-map/1', nodes: [{ id: 'mod:x:a', kind: 'module', name: 'a' }], edges: [], findings: [], notMeasured: [] };
|
|
920
|
+
fs.writeFileSync(path.join(dir, MAP_FILE), JSON.stringify(doc));
|
|
921
|
+
const read = readMap(dir);
|
|
922
|
+
// A different size, not only a different schema: the reader caches on mtime and size, and on Windows both writes
|
|
923
|
+
// land on one timestamp, so a same-size rewrite read back the first map (seen on a CI runner).
|
|
924
|
+
fs.writeFileSync(path.join(dir, MAP_FILE), JSON.stringify({ ...doc, schema: 'crewforth.crew-map/99' }));
|
|
925
|
+
const other = readMap(dir);
|
|
926
|
+
check('map read', read.measured === true && read.doc.nodes.length === 1 && other.measured === false,
|
|
927
|
+
read.measured ? `a scratch map read; one of another schema refused (${other.reason ?? 'read'})` : read.reason);
|
|
928
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
929
|
+
const own = readMap(process.cwd());
|
|
930
|
+
process.stdout.write(`INFO this project's map — ${own.measured ? `${own.doc.nodes.length} node(s), ${own.doc.findings.length} finding(s)` : own.reason}\n`);
|
|
931
|
+
}
|
|
932
|
+
|
|
833
933
|
const cost = await measureSpawnCost(3);
|
|
834
934
|
if (cost === null) skip('spawn cost measured', 'claude CLI absent');
|
|
835
935
|
else check('spawn cost measured', true, `min ${cost.minMs}ms · median ${cost.medianMs}ms · max ${cost.maxMs}ms`);
|
|
@@ -877,6 +977,9 @@ async function main() {
|
|
|
877
977
|
' --help this text\n\n' +
|
|
878
978
|
'Set CREW_STUDIO_TOKEN to require a bearer token, CREW_STUDIO_PEERS for a\n' +
|
|
879
979
|
'comma-separated peer list.\n\n' +
|
|
980
|
+
'To watch from another device, add a viewer under Machines in the panel\n' +
|
|
981
|
+
'and open its link through a tunnel of your own. A viewer reads and\n' +
|
|
982
|
+
'changes nothing, and the server stays on loopback.\n\n' +
|
|
880
983
|
'Peers are normally reached over a forwarded port, which keeps every\n' +
|
|
881
984
|
'Studio on loopback:\n' +
|
|
882
985
|
' ssh -N -L 7778:127.0.0.1:7777 other-machine\n' +
|