crewforth 3.1.1 → 3.2.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/README.md +4 -3
- package/VERSION +1 -1
- package/adopt.sh +56 -5
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +14 -13
- 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 +117 -6
- 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-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +15 -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/version.sh +41 -0
- package/kit/eval/smoke-test.sh +576 -41
- package/kit/hooks/agent-outcome.sh +26 -12
- package/kit/hooks/guard-agent-model.sh +109 -9
- 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/hooks/session-update-check.sh +9 -8
- 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 +125 -11
- 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/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/remote.js +99 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +223 -15
- package/kit/studio/web/canvas.js +44 -120
- package/kit/studio/web/chat.js +82 -17
- package/kit/studio/web/graph-plan.js +10 -4
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/liveness.js +14 -0
- 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 +131 -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
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
set -uo pipefail
|
|
45
45
|
# The 2.x names of the variables a user can set still work (one helper: eval/lib/crew-env.sh).
|
|
46
46
|
_crew_d="${BASH_SOURCE%/*}"; [ "$_crew_d" = "${BASH_SOURCE}" ] && _crew_d=.
|
|
47
|
-
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
47
|
+
[ -f "$_crew_d/../eval/lib/crew-env.sh" ] && . "$_crew_d/../eval/lib/crew-env.sh"
|
|
48
|
+
# How an install's own version is read and ordered (an rc before its final): one definition, eval/lib/version.sh.
|
|
49
|
+
[ -f "$_crew_d/../eval/lib/version.sh" ] && . "$_crew_d/../eval/lib/version.sh"; unset _crew_d
|
|
48
50
|
|
|
49
51
|
URL="${CREW_UPDATE_URL:-https://registry.npmjs.org/-/package/crewforth/dist-tags}"
|
|
50
52
|
# plugin-stable, not main: the marketplace installs the plugin from that branch, and only an approved release
|
|
@@ -139,7 +141,10 @@ PJSON="$PR_ROOT/.claude-plugin/plugin.json"
|
|
|
139
141
|
# plugin's copy steps aside whenever a project install is present, instead of asking the project's question too.
|
|
140
142
|
if [ "$IS_PLUGIN" = 1 ] && [ -f "$CL/VERSION" ]; then exit 0; fi
|
|
141
143
|
if [ -f "$CL/VERSION" ]; then
|
|
142
|
-
|
|
144
|
+
# The install's own file, so an rc (X.Y.Z-rc.N) is a version here; what npm answers is still held to X.Y.Z.
|
|
145
|
+
read -r _cv < "$CL/VERSION" 2>/dev/null || _cv=""
|
|
146
|
+
declare -F crew_local_version >/dev/null || exit 0
|
|
147
|
+
CUR="$(crew_local_version "$_cv")" || exit 0
|
|
143
148
|
STATE="$CL/.state" # repo-local: the install lives in the repo
|
|
144
149
|
FEED="${CREW_UPDATE_URL:-$URL}"; KEY=latest; EDITION=project
|
|
145
150
|
ANSWER='bash .claude/hooks/session-update-check.sh --answer'
|
|
@@ -186,12 +191,8 @@ fi
|
|
|
186
191
|
[ -n "$LATEST" ] || exit 0
|
|
187
192
|
LATEST="$(sane_version "$LATEST")" || exit 0
|
|
188
193
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
awk -v a="$LATEST" -v b="$CUR" 'BEGIN{
|
|
192
|
-
split(a,x,"."); split(b,y,".");
|
|
193
|
-
for(i=1;i<=3;i++){ if(x[i]+0 > y[i]+0) exit 0; if(x[i]+0 < y[i]+0) exit 1 }
|
|
194
|
-
exit 1 }' || exit 0
|
|
194
|
+
# One awk, eval/lib/version.sh's order: 3.2.0 is newer than an installed 3.2.0-rc.1, so the rc's user is asked.
|
|
195
|
+
awk -v a="$LATEST" -v b="$CUR" "${CREW_VCMP_AWK:-}"' BEGIN { exit !(vcmp(a, b) > 0) }' 2>/dev/null || exit 0
|
|
195
196
|
|
|
196
197
|
# Skipped for good? Asked (or answered "later") within the last day?
|
|
197
198
|
if [ -f "$STATE/update-skip" ]; then read -r _sk < "$STATE/update-skip" 2>/dev/null || _sk=""; [ "${_sk//[!0-9.]/}" = "$LATEST" ] && exit 0; fi
|
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,10 @@ 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 {
|
|
24
|
+
import { streamOwned } from './lib/owned-stream.js';
|
|
25
|
+
import { stampAtStart } from './lib/code-stamp.js';
|
|
26
|
+
import { readModels, readRecord, readCosts, readAboveCosts } from './lib/model-outcomes.js';
|
|
27
|
+
import { readMap, behindOf, compareMaps, mapSummary, MAP_FILE } from './lib/crew-map.js';
|
|
25
28
|
|
|
26
29
|
const USAGE_BATCH = 12;
|
|
27
30
|
import { palette } from './lib/palette.js';
|
|
@@ -35,11 +38,14 @@ import { open as openTerminal, plan as terminalPlan } from './lib/terminal.js';
|
|
|
35
38
|
import { gateLog, gateReport, sessionStats, board } from './lib/kit-telemetry.js';
|
|
36
39
|
import { writeState, clearStateSync, findRunning } from './lib/instance.js';
|
|
37
40
|
import { remoteRoster } from './lib/roster.js';
|
|
41
|
+
import { addViewer, listViewers, viewerByToken, revokeViewer, noteRequest, viewerMay, MAX_VIEWERS } from './lib/remote.js';
|
|
38
42
|
import { randomUUID } from 'node:crypto';
|
|
39
43
|
import { spawn } from 'node:child_process';
|
|
40
44
|
|
|
41
45
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
42
46
|
const WEB_ROOT = path.resolve(HERE, '..', 'web');
|
|
47
|
+
// The server's own code as it was when it started: an update on disk since then is said to the page (code-stamp.js).
|
|
48
|
+
const codeChanged = stampAtStart(HERE);
|
|
43
49
|
const LOOPBACK = '127.0.0.1';
|
|
44
50
|
|
|
45
51
|
const MIME = {
|
|
@@ -108,15 +114,28 @@ function sendJson(res, status, value) {
|
|
|
108
114
|
const TOKEN = process.env.CREW_STUDIO_TOKEN || randomUUID();
|
|
109
115
|
const TOKEN_GENERATED = !process.env.CREW_STUDIO_TOKEN;
|
|
110
116
|
|
|
117
|
+
/** For the self-check, which asks a real server as the owner and as a viewer. */
|
|
118
|
+
export const _internals = { token: TOKEN };
|
|
119
|
+
|
|
111
120
|
// Peers are other machines running Studio, reached over a forwarded port. This
|
|
112
121
|
// server still listens on loopback only; a peer never widens that.
|
|
113
122
|
let PEERS = [];
|
|
114
123
|
let SELF_NAME = 'this machine';
|
|
115
124
|
|
|
116
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Whose request this is: the owner's, a read-only viewer's (lib/remote.js), or nobody's.
|
|
127
|
+
* The token is read from the same two places for both, so a viewer's link works exactly as the owner's does.
|
|
128
|
+
*/
|
|
129
|
+
function whoAsks(req, url) {
|
|
117
130
|
const header = req.headers['authorization'];
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
const bearer = typeof header === 'string' && header.startsWith('Bearer ') ? header.slice(7) : null;
|
|
132
|
+
const given = [bearer, url.searchParams.get('token')].filter(Boolean);
|
|
133
|
+
if (given.includes(TOKEN)) return { role: 'owner' };
|
|
134
|
+
for (const t of given) {
|
|
135
|
+
const viewer = viewerByToken(t);
|
|
136
|
+
if (viewer) return { role: 'viewer', viewer };
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
/**
|
|
@@ -167,10 +186,16 @@ async function modelCosts(cwd) {
|
|
|
167
186
|
for (const s of (await listSessions(dir)).filter((x) => x.modifiedAt >= since).slice(0, MODELS_SESSIONS_MAX)) {
|
|
168
187
|
const u = await cachedUsage(s);
|
|
169
188
|
read += 1;
|
|
170
|
-
for (const [id, a] of Object.entries(u.agents ?? {})) per.set(id, a
|
|
189
|
+
for (const [id, a] of Object.entries(u.agents ?? {})) per.set(id, a);
|
|
171
190
|
}
|
|
172
191
|
}
|
|
173
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
measured: true,
|
|
194
|
+
costs: readCosts(cwd, (id) => per.get(id)?.cost ?? null,
|
|
195
|
+
(id) => (per.get(id)?.advisorCalls ? { calls: per.get(id).advisorCalls, cost: per.get(id).advisorCost } : null)),
|
|
196
|
+
above: readAboveCosts(cwd, (id) => (per.has(id) ? { cost: per.get(id).cost, atNormal: per.get(id).costAtNormal } : null)),
|
|
197
|
+
sessionsRead: read,
|
|
198
|
+
};
|
|
174
199
|
}
|
|
175
200
|
const MODELS_SESSIONS_MAX = 60;
|
|
176
201
|
|
|
@@ -214,23 +239,35 @@ async function serveStatic(res, urlPath) {
|
|
|
214
239
|
}
|
|
215
240
|
}
|
|
216
241
|
|
|
217
|
-
async function handle(req, res) {
|
|
242
|
+
export async function handle(req, res) {
|
|
218
243
|
const url = new URL(req.url, `http://${LOOPBACK}`);
|
|
219
244
|
|
|
220
245
|
// The token guards data and actions, not the app shell. A browser does not
|
|
221
246
|
// carry the page's query string into its sub-resource requests, so demanding
|
|
222
247
|
// it for /style.css served an unstyled page and protected nothing: the shell
|
|
223
248
|
// holds no secrets, and every /api/ path below is still gated.
|
|
224
|
-
|
|
249
|
+
const who = url.pathname.startsWith('/api/') ? whoAsks(req, url) : null;
|
|
250
|
+
if (url.pathname.startsWith('/api/') && !who) {
|
|
225
251
|
return sendJson(res, 403, { ok: false, reason: 'token required' });
|
|
226
252
|
}
|
|
253
|
+
// A viewer is refused HERE, before any handler: no route below has to remember that a viewer exists, and a
|
|
254
|
+
// route added tomorrow is read-only for a viewer without anyone writing a line for it.
|
|
255
|
+
if (who?.role === 'viewer') {
|
|
256
|
+
const may = viewerMay(req.method, url.pathname);
|
|
257
|
+
if (!may.ok) return sendJson(res, 403, { ok: false, reason: may.reason });
|
|
258
|
+
noteRequest(who.viewer, req, res);
|
|
259
|
+
}
|
|
227
260
|
|
|
228
261
|
if (url.pathname === '/api/health') {
|
|
229
262
|
return sendJson(res, 200, {
|
|
230
263
|
ok: true,
|
|
264
|
+
role: who.role,
|
|
231
265
|
node: process.version,
|
|
232
266
|
pid: process.pid,
|
|
233
267
|
uptimeMs: Math.round(process.uptime() * 1000),
|
|
268
|
+
// `changed`: the server's files on disk are not the ones it started with. A page that finds no `code` at all
|
|
269
|
+
// is talking to a server older than this field, which is the same thing.
|
|
270
|
+
code: codeChanged(),
|
|
234
271
|
});
|
|
235
272
|
}
|
|
236
273
|
|
|
@@ -288,7 +325,12 @@ async function handle(req, res) {
|
|
|
288
325
|
// dead time whenever the feed hung, measured. The refresh runs behind this response.
|
|
289
326
|
const latest = latestVersionCached();
|
|
290
327
|
const projects = await listProjects({ currentCwd: cwd, kitOf: (c) => kitStatus(c, latest) });
|
|
291
|
-
for (const p of projects) {
|
|
328
|
+
for (const p of projects) {
|
|
329
|
+
p.origin = SELF_NAME; p.local = true;
|
|
330
|
+
// The navigator's Map entry and its count: the findings the map shows unasked. A stat per project; the map
|
|
331
|
+
// is parsed again only when its file changes (crew-map.js).
|
|
332
|
+
p.map = p.exists && p.cwd ? mapSummary(p.cwd) : null;
|
|
333
|
+
}
|
|
292
334
|
|
|
293
335
|
const origins = [{ name: SELF_NAME, local: true, ok: true, projects: projects.length }];
|
|
294
336
|
if (PEERS.length) {
|
|
@@ -339,7 +381,7 @@ async function handle(req, res) {
|
|
|
339
381
|
const sid = url.searchParams.get('session');
|
|
340
382
|
const session = sid ? await findSession(sid) : null;
|
|
341
383
|
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: {} });
|
|
384
|
+
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', classes: [], suggestions: [], above: [], byAgent: {} });
|
|
343
385
|
return sendJson(res, 200, readModels(cwd, { agents: await agentIdsOf(session) }));
|
|
344
386
|
}
|
|
345
387
|
|
|
@@ -347,10 +389,29 @@ async function handle(req, res) {
|
|
|
347
389
|
const sid = url.searchParams.get('session');
|
|
348
390
|
const session = sid ? await findSession(sid) : null;
|
|
349
391
|
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 });
|
|
392
|
+
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', costs: {}, above: {}, sessionsRead: 0 });
|
|
351
393
|
return sendJson(res, 200, await modelCosts(cwd));
|
|
352
394
|
}
|
|
353
395
|
|
|
396
|
+
// The project's crew map, as Crewforth's scanner wrote it, and how far HEAD has moved since. Nothing is scanned here.
|
|
397
|
+
// `compare` names another project of this machine by its directory; only a project the panel lists, with a map
|
|
398
|
+
// of its own, is read, so the route cannot be pointed at an arbitrary path.
|
|
399
|
+
if (url.pathname === '/api/map') {
|
|
400
|
+
const sid = url.searchParams.get('session');
|
|
401
|
+
const session = sid ? await findSession(sid) : null;
|
|
402
|
+
const cwd = url.searchParams.get('cwd') || (session ? await sessionCwd(session.file, session.bytes) : null);
|
|
403
|
+
if (!cwd) return sendJson(res, 200, { measured: false, reason: 'no project named', others: [] });
|
|
404
|
+
const map = readMap(cwd);
|
|
405
|
+
const others = (await listProjects({ currentCwd: cwd })).filter((p) => p.cwd && p.cwd !== cwd && readMap(p.cwd).measured)
|
|
406
|
+
.map((p) => ({ label: p.label, cwd: p.cwd }));
|
|
407
|
+
if (!map.measured) return sendJson(res, 200, { ...map, cwd, others });
|
|
408
|
+
const asked = url.searchParams.get('compare');
|
|
409
|
+
const peer = asked ? others.find((o) => o.cwd === asked) : null;
|
|
410
|
+
const compare = peer ? { label: peer.label, cwd: peer.cwd, clients: compareMaps(map.doc, readMap(peer.cwd).doc) }
|
|
411
|
+
: asked ? { label: null, cwd: asked, clients: [], reason: 'not a project of this machine with a crew map' } : null;
|
|
412
|
+
return sendJson(res, 200, { ...map, cwd, behind: await behindOf(cwd, map.doc.source?.commit), others, compare });
|
|
413
|
+
}
|
|
414
|
+
|
|
354
415
|
if (url.pathname === '/api/kit') {
|
|
355
416
|
const cwd = url.searchParams.get('cwd') || process.cwd();
|
|
356
417
|
const sid = url.searchParams.get('session');
|
|
@@ -463,6 +524,30 @@ async function handle(req, res) {
|
|
|
463
524
|
return stream(req, res, url);
|
|
464
525
|
}
|
|
465
526
|
|
|
527
|
+
/* --------------------------------------------------------- remote ---
|
|
528
|
+
Read-only viewers: who holds a link, and the way to take it back. Only
|
|
529
|
+
the owner reaches this far; a viewer was refused above. */
|
|
530
|
+
|
|
531
|
+
if (url.pathname === '/api/remote' && req.method === 'GET') {
|
|
532
|
+
return sendJson(res, 200, { measured: true, viewers: listViewers(), max: MAX_VIEWERS, now: Date.now() });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const remoteMatch = url.pathname.match(/^\/api\/remote(?:\/([^/]+))?$/);
|
|
536
|
+
if (remoteMatch) {
|
|
537
|
+
const gate = writeAllowed(req);
|
|
538
|
+
if (!gate.ok) return sendJson(res, 403, { ok: false, reason: gate.reason });
|
|
539
|
+
if (req.method === 'POST' && !remoteMatch[1]) {
|
|
540
|
+
const body = await readBody(req);
|
|
541
|
+
if (!body) return sendJson(res, 400, { ok: false, reason: 'body was not JSON' });
|
|
542
|
+
const made = addViewer(body.label);
|
|
543
|
+
return sendJson(res, made.ok ? 201 : 409, { ...made, viewers: listViewers() });
|
|
544
|
+
}
|
|
545
|
+
if (req.method === 'DELETE' && remoteMatch[1]) {
|
|
546
|
+
return sendJson(res, 200, { ...revokeViewer(decodeURIComponent(remoteMatch[1])), viewers: listViewers() });
|
|
547
|
+
}
|
|
548
|
+
return sendJson(res, 405, { ok: false, reason: 'POST to add a viewer, or DELETE a viewer id' });
|
|
549
|
+
}
|
|
550
|
+
|
|
466
551
|
/* ---------------------------------------------------------- owned ---
|
|
467
552
|
Sessions the panel started. Their transcripts land in the usual place,
|
|
468
553
|
so the graph endpoints already cover them; only the conversation needs
|
|
@@ -525,6 +610,10 @@ async function handle(req, res) {
|
|
|
525
610
|
return sendJson(res, out.ok ? 200 : 400, out);
|
|
526
611
|
}
|
|
527
612
|
|
|
613
|
+
// Every open conversation on ONE connection (lib/owned-stream.js). Reading, so it sits beside /events, not
|
|
614
|
+
// behind the write gate.
|
|
615
|
+
if (url.pathname === '/api/owned-events') return streamOwned(req, res, url, { getSession, stateAfter: STATE_AFTER });
|
|
616
|
+
|
|
528
617
|
const ownedMatch = url.pathname.match(/^\/api\/owned\/([^/]+)(?:\/(message|stop|events|mode))?$/);
|
|
529
618
|
if (ownedMatch) {
|
|
530
619
|
const s = getSession(decodeURIComponent(ownedMatch[1]));
|
|
@@ -830,6 +919,28 @@ async function selftest() {
|
|
|
830
919
|
fleet.measured ? `${fleet.sessions.length} session(s)` : `not measured: ${fleet.reason}`,
|
|
831
920
|
);
|
|
832
921
|
|
|
922
|
+
// The Map view: its files are installed, and the reader takes a crew map and refuses one of another schema. On a
|
|
923
|
+
// scratch map of its own, so the verdict does not depend on whether this project has one; this project's own map,
|
|
924
|
+
// when there is one, is reported beside it.
|
|
925
|
+
check('map view installed', ['map.js', 'map-plan.js', 'viewport.js'].every((f) => fs.existsSync(path.join(WEB_ROOT, f))), null);
|
|
926
|
+
{
|
|
927
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'crew-studio-map-'));
|
|
928
|
+
try {
|
|
929
|
+
fs.mkdirSync(path.join(dir, '.crewforth'));
|
|
930
|
+
const doc = { schema: 'crewforth.crew-map/1', nodes: [{ id: 'mod:x:a', kind: 'module', name: 'a' }], edges: [], findings: [], notMeasured: [] };
|
|
931
|
+
fs.writeFileSync(path.join(dir, MAP_FILE), JSON.stringify(doc));
|
|
932
|
+
const read = readMap(dir);
|
|
933
|
+
// A different size, not only a different schema: the reader caches on mtime and size, and on Windows both writes
|
|
934
|
+
// land on one timestamp, so a same-size rewrite read back the first map (seen on a CI runner).
|
|
935
|
+
fs.writeFileSync(path.join(dir, MAP_FILE), JSON.stringify({ ...doc, schema: 'crewforth.crew-map/99' }));
|
|
936
|
+
const other = readMap(dir);
|
|
937
|
+
check('map read', read.measured === true && read.doc.nodes.length === 1 && other.measured === false,
|
|
938
|
+
read.measured ? `a scratch map read; one of another schema refused (${other.reason ?? 'read'})` : read.reason);
|
|
939
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
940
|
+
const own = readMap(process.cwd());
|
|
941
|
+
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`);
|
|
942
|
+
}
|
|
943
|
+
|
|
833
944
|
const cost = await measureSpawnCost(3);
|
|
834
945
|
if (cost === null) skip('spawn cost measured', 'claude CLI absent');
|
|
835
946
|
else check('spawn cost measured', true, `min ${cost.minMs}ms · median ${cost.medianMs}ms · max ${cost.maxMs}ms`);
|
|
@@ -877,6 +988,9 @@ async function main() {
|
|
|
877
988
|
' --help this text\n\n' +
|
|
878
989
|
'Set CREW_STUDIO_TOKEN to require a bearer token, CREW_STUDIO_PEERS for a\n' +
|
|
879
990
|
'comma-separated peer list.\n\n' +
|
|
991
|
+
'To watch from another device, add a viewer under Machines in the panel\n' +
|
|
992
|
+
'and open its link through a tunnel of your own. A viewer reads and\n' +
|
|
993
|
+
'changes nothing, and the server stays on loopback.\n\n' +
|
|
880
994
|
'Peers are normally reached over a forwarded port, which keeps every\n' +
|
|
881
995
|
'Studio on loopback:\n' +
|
|
882
996
|
' ssh -N -L 7778:127.0.0.1:7777 other-machine\n' +
|