crewforth 3.4.0 → 3.5.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +273 -0
- package/README.md +3 -2
- package/VERSION +1 -1
- package/adopt.sh +40 -5
- package/bin/cli.js +138 -2
- package/kit/AGENT_TEMPLATE.md +15 -0
- package/kit/CLAUDE.md +50 -18
- package/kit/agents/crew-backend-expert.md +4 -3
- package/kit/agents/crew-commit-agent.md +2 -0
- package/kit/agents/crew-database-expert.md +2 -1
- package/kit/agents/crew-devops-expert.md +1 -0
- package/kit/agents/crew-docs-agent.md +3 -1
- package/kit/agents/crew-frontend-expert.md +2 -0
- package/kit/agents/crew-performance-expert.md +7 -2
- package/kit/agents/crew-planner.md +15 -3
- package/kit/agents/crew-privacy-agent.md +2 -0
- package/kit/agents/crew-review-agent.md +8 -1
- package/kit/agents/crew-security-expert.md +4 -3
- package/kit/agents/crew-test-expert.md +6 -2
- package/kit/eval/doctor.sh +201 -1
- package/kit/eval/gate-report.sh +28 -0
- package/kit/eval/golden-holdout.txt +2 -0
- package/kit/eval/golden-routing.txt +17 -0
- package/kit/eval/lib/crew-map/contracts.mjs +77 -0
- package/kit/eval/lib/crew-map/crew-map.sh +2 -1
- package/kit/eval/lib/crew-map/detect.mjs +2 -1
- package/kit/eval/lib/crew-map/narrative.mjs +14 -3
- package/kit/eval/lib/crew-map/project.mjs +45 -6
- package/kit/eval/lib/crew-map/scan.mjs +153 -14
- package/kit/eval/lib/findings/findings.schema.json +11 -1
- package/kit/eval/lib/findings/findings.sh +24 -1
- package/kit/eval/lib/hook-inject.sh +159 -0
- package/kit/eval/lib/lockfiles.sh +71 -0
- package/kit/eval/lib/payload-check.sh +29 -0
- package/kit/eval/lib/preview.sh +3 -3
- package/kit/eval/lib/review-bench/bench.mjs +14 -2
- package/kit/eval/lib/review-prepass/prepass.mjs +32 -3
- package/kit/eval/lib/settings-json.awk +18 -0
- package/kit/eval/lib/statusline.sh +22 -5
- package/kit/eval/lib/todo.awk +167 -0
- package/kit/eval/lib/todo.sh +120 -0
- package/kit/eval/lib/untrusted-text.awk +38 -0
- package/kit/eval/scan-skill.sh +67 -20
- package/kit/eval/smoke-test.sh +1759 -41
- package/kit/eval/utilization.sh +77 -3
- package/kit/hooks/agent-outcome.sh +9 -1
- package/kit/hooks/board-sync.sh +49 -3
- package/kit/hooks/board.sh +8 -8
- package/kit/hooks/branch-fresh.sh +101 -0
- package/kit/hooks/floor-blocklist.txt +19 -1
- package/kit/hooks/guard-bash.sh +92 -8
- package/kit/hooks/guard-question.sh +2 -1
- package/kit/hooks/loop-guard.sh +445 -0
- package/kit/hooks/map-stale.sh +437 -0
- package/kit/hooks/pre-commit +48 -7
- package/kit/hooks/precompact-snapshot.sh +102 -6
- package/kit/hooks/prompt-approval.sh +50 -14
- package/kit/hooks/route-hint.sh +36 -7
- package/kit/hooks/secret-blocklist.txt +55 -1
- package/kit/hooks/session-guard.sh +45 -15
- package/kit/hooks/session-rehydrate.sh +34 -11
- package/kit/hooks/session-stats.sh +169 -8
- package/kit/hooks/session-update-check.sh +5 -1
- package/kit/hooks/skill-trust.sh +23 -14
- package/kit/hooks/turn-check.sh +190 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +64 -0
- package/kit/skills/a11y/SKILL.md +50 -3
- package/kit/skills/a11y/references/recipes.md +41 -0
- package/kit/skills/adr/SKILL.md +14 -2
- package/kit/skills/api-design/SKILL.md +6 -1
- package/kit/skills/api-design/references/idempotency.md +39 -0
- package/kit/skills/brainstorm/SKILL.md +4 -2
- package/kit/skills/brainstorm/references/asking.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +11 -1
- package/kit/skills/ci-pipeline/references/agent-job.md +33 -0
- package/kit/skills/commit-message/SKILL.md +9 -3
- package/kit/skills/confidence-check/SKILL.md +9 -7
- package/kit/skills/confidence-check/references/source-check.md +37 -0
- package/kit/skills/crew-approve/SKILL.md +3 -0
- package/kit/skills/crew-audit/SKILL.md +63 -0
- package/kit/skills/crew-board/SKILL.md +2 -2
- package/kit/skills/crew-code-review/SKILL.md +27 -4
- package/kit/skills/crew-code-review/references/panel-mode.md +9 -9
- package/kit/skills/crew-code-review/references/simplify.md +28 -0
- package/kit/skills/crew-doctor/SKILL.md +3 -2
- package/kit/skills/crew-gates/SKILL.md +3 -0
- package/kit/skills/crew-gates/references/enforcers.md +30 -0
- package/kit/skills/crew-interview/SKILL.md +42 -0
- package/kit/skills/crew-map/SKILL.md +26 -6
- package/kit/skills/crew-plan/SKILL.md +10 -2
- package/kit/skills/crew-pr/SKILL.md +57 -0
- package/kit/skills/crew-pr/references/checks.md +35 -0
- package/kit/skills/crew-pr/references/threads.md +23 -0
- package/kit/skills/crew-review/SKILL.md +34 -1
- package/kit/skills/crew-ship/SKILL.md +5 -2
- package/kit/skills/crew-skill/SKILL.md +26 -1
- package/kit/skills/crew-todo/SKILL.md +31 -0
- package/kit/skills/crew-update/SKILL.md +4 -1
- package/kit/skills/db-migration/SKILL.md +15 -0
- package/kit/skills/db-migration/references/expand-contract.md +52 -0
- package/kit/skills/dependency-audit/SKILL.md +34 -1
- package/kit/skills/dependency-audit/scripts/record.sh +37 -0
- package/kit/skills/dependency-upgrade/SKILL.md +29 -1
- package/kit/skills/deploy/SKILL.md +18 -0
- package/kit/skills/deploy/references/topology-a.md +7 -0
- package/kit/skills/deploy/references/topology-b.md +2 -2
- package/kit/skills/docs-writer/SKILL.md +37 -2
- package/kit/skills/eval-grader/SKILL.md +9 -0
- package/kit/skills/eval-grader/references/method.md +26 -0
- package/kit/skills/frontend/SKILL.md +5 -2
- package/kit/skills/frontend/references/from-design.md +29 -0
- package/kit/skills/frontend/references/page-metadata.md +17 -0
- package/kit/skills/frontend/references/verify-contract.md +18 -1
- package/kit/skills/frontend/references/worst-data.md +35 -0
- package/kit/skills/frontend-design/SKILL.md +20 -4
- package/kit/skills/frontend-design/references/design-review.md +21 -0
- package/kit/skills/frontend-design/references/detection.md +44 -0
- package/kit/skills/frontend-design/references/motion.md +54 -0
- package/kit/skills/frontend-design/references/ui-copy.md +35 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
- package/kit/skills/handoff/SKILL.md +28 -3
- package/kit/skills/handoff/references/cold-reader.md +13 -6
- package/kit/skills/i18n-integrity/SKILL.md +3 -0
- package/kit/skills/iterate/SKILL.md +7 -2
- package/kit/skills/mcp-builder/SKILL.md +21 -4
- package/kit/skills/mcp-builder/references/building.md +74 -13
- package/kit/skills/mcp-builder/references/cli-for-agents.md +26 -0
- package/kit/skills/observability/SKILL.md +13 -1
- package/kit/skills/performance/SKILL.md +25 -1
- package/kit/skills/performance/references/bottlenecks.md +11 -0
- package/kit/skills/performance/references/web-vitals.md +31 -0
- package/kit/skills/red-team/SKILL.md +25 -3
- package/kit/skills/reflect/SKILL.md +7 -2
- package/kit/skills/release/SKILL.md +13 -0
- package/kit/skills/security-scan/SKILL.md +6 -2
- package/kit/skills/security-scan/references/ai-agents.md +16 -0
- package/kit/skills/security-scan/references/fronts.md +59 -2
- package/kit/skills/security-scan/references/reporting.md +13 -0
- package/kit/skills/security-scan/references/verify.md +9 -0
- package/kit/skills/sonarqube-check/SKILL.md +32 -2
- package/kit/skills/sonarqube-check/references/ratchet.md +32 -0
- package/kit/skills/spec-planning/SKILL.md +32 -8
- package/kit/skills/systematic-debugging/SKILL.md +15 -7
- package/kit/skills/systematic-debugging/references/techniques.md +5 -0
- package/kit/skills/teamboard/SKILL.md +3 -3
- package/kit/skills/testing/SKILL.md +18 -3
- package/kit/skills/testing/references/browser-headless.md +21 -0
- package/kit/skills/token-budget/SKILL.md +26 -5
- package/kit/skills/worktree/SKILL.md +13 -5
- package/kit/studio/server/lib/session.js +23 -2
- package/kit/studio/server/lib/worktree-changes.js +64 -0
- package/kit/studio/web/agent-spend.js +32 -0
- package/kit/studio/web/app.js +245 -236
- package/kit/studio/web/approvals.js +14 -13
- package/kit/studio/web/ask.js +10 -9
- package/kit/studio/web/canvas.js +56 -54
- package/kit/studio/web/chat.js +41 -41
- package/kit/studio/web/convo.js +33 -16
- package/kit/studio/web/dock.js +20 -19
- package/kit/studio/web/graph-plan.js +62 -22
- package/kit/studio/web/i18n-tr.js +760 -0
- package/kit/studio/web/i18n.js +79 -0
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +27 -24
- package/kit/studio/web/list-plan.js +16 -15
- package/kit/studio/web/list.js +6 -5
- package/kit/studio/web/liveness.js +11 -9
- package/kit/studio/web/map-plan.js +86 -36
- package/kit/studio/web/map.js +85 -72
- package/kit/studio/web/models-plan.js +47 -42
- package/kit/studio/web/models.js +21 -20
- package/kit/studio/web/names.js +13 -10
- package/kit/studio/web/nav.js +34 -33
- package/kit/studio/web/newsession.js +25 -24
- package/kit/studio/web/style.css +2 -0
- package/kit/studio/web/theme.js +5 -2
- package/kit/studio/web/timeline-plan.js +19 -18
- package/kit/studio/web/timeline.js +23 -22
- package/kit/studio/web/usage.js +36 -32
- package/package.json +5 -4
- package/start.sh +11 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Sourced, not run. The ONE list of lockfiles, and the ONE way to find the staged ones, for the two sides of §4.7:
|
|
2
|
+
# guard-bash.sh (reads the audit record back) and skills/dependency-audit/scripts/record.sh (writes it). Both hash
|
|
3
|
+
# what crew_staged_lockfiles returns, so a second copy of the list would let them disagree on which files were
|
|
4
|
+
# audited and turn a correct record into a refusal (or the reverse).
|
|
5
|
+
#
|
|
6
|
+
# Lockfiles, not manifests: a lockfile changes when the RESOLVED set changes, which is what an audit judges. A
|
|
7
|
+
# manifest also changes for a script, a description or a version field, and gating those would ask for an audit
|
|
8
|
+
# that has nothing new to read. (npm's and uv's lockfiles carry the project's own version, so a release bump does
|
|
9
|
+
# trip the gate — the one moment an audit is due anyway.) Left out on purpose: flake.lock and .terraform.lock.hcl,
|
|
10
|
+
# which pin no package ecosystem dependency-audit can read, so every commit touching them would be `unverified`.
|
|
11
|
+
#
|
|
12
|
+
# Every function takes the WORK-TREE TOP. git prints staged paths from the top, and they are handed back to git as
|
|
13
|
+
# pathspecs, so the two must agree: run from a subfolder, the pathspecs named nothing and the diff hashed to the
|
|
14
|
+
# empty blob (found in review: a valid record refused).
|
|
15
|
+
|
|
16
|
+
# rc 0 when the path's file name is a lockfile.
|
|
17
|
+
crew_is_lockfile(){
|
|
18
|
+
case "${1##*/}" in
|
|
19
|
+
package-lock.json|npm-shrinkwrap.json|yarn.lock|pnpm-lock.yaml|bun.lock|bun.lockb|deno.lock) return 0 ;;
|
|
20
|
+
uv.lock|poetry.lock|Pipfile.lock|pdm.lock|pixi.lock|renv.lock|conan.lock) return 0 ;;
|
|
21
|
+
Cargo.lock|go.sum|Gemfile.lock|composer.lock|mix.lock|pubspec.lock) return 0 ;;
|
|
22
|
+
packages.lock.json|paket.lock|*gradle.lockfile|Package.resolved|Podfile.lock|Cartfile.resolved) return 0 ;;
|
|
23
|
+
esac
|
|
24
|
+
return 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# crew_staged_lockfiles <top> -> CREW_LOCKS: the staged lockfiles, an array in git's order. One git process.
|
|
28
|
+
# -z, because without it git QUOTES a path with a non-ASCII byte in it ("\305\237/package-lock.json"): the file name
|
|
29
|
+
# then ends in `"` and is not a lockfile, so a lockfile in a folder named in Turkish passed the gate unaudited.
|
|
30
|
+
# --diff-filter=d: a deleted lockfile has nothing left to audit (deleting one is §4.5's business).
|
|
31
|
+
crew_staged_lockfiles(){
|
|
32
|
+
local p
|
|
33
|
+
CREW_LOCKS=()
|
|
34
|
+
while IFS= read -r -d '' p; do
|
|
35
|
+
crew_is_lockfile "$p" && CREW_LOCKS+=("$p")
|
|
36
|
+
done < <(git -C "${1:-.}" -c core.fsmonitor= diff --cached --name-only --no-renames --diff-filter=d -z 2>/dev/null)
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# crew_lockfile_pending <top> -> rc 0 when a lockfile differs from HEAD anywhere: staged, changed in the work tree,
|
|
41
|
+
# or new and untracked. For a command that stages AND commits in one call (`git add -A && git commit`): the hook
|
|
42
|
+
# reads the index before the command runs, so what is staged then is not what lands. One git process.
|
|
43
|
+
crew_lockfile_pending(){
|
|
44
|
+
local e
|
|
45
|
+
while IFS= read -r -d '' e; do
|
|
46
|
+
# A porcelain entry is "XY path"; a rename's second field is the bare original path. Both are tested.
|
|
47
|
+
crew_is_lockfile "${e:3}" && return 0
|
|
48
|
+
crew_is_lockfile "$e" && return 0
|
|
49
|
+
done < <(git -C "${1:-.}" -c core.fsmonitor= status --porcelain=v1 -z --untracked-files=all 2>/dev/null)
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# crew_lock_oid <top> -> CREW_LOCK_OID: the object id of the staged diff of CREW_LOCKS alone, or "" when there is no
|
|
54
|
+
# such diff to hash. Code staged beside the lockfiles does not change it, so an audit stays valid while the rest of
|
|
55
|
+
# the commit is still being edited.
|
|
56
|
+
# --literal-pathspecs: a lockfile under a folder named `[x]` is a path, not a glob.
|
|
57
|
+
# --full-index: the `index` lines carry full object ids, so core.abbrev or the repository's size cannot change the
|
|
58
|
+
# text, and with it the id, between the audit and the commit.
|
|
59
|
+
# pipefail and the empty blob: a diff that FAILS still hands hash-object empty input, and both sides would then
|
|
60
|
+
# agree on e69de29… (found in review). A staged lockfile change is never an empty diff, so that id means "none".
|
|
61
|
+
crew_lock_oid(){
|
|
62
|
+
CREW_LOCK_OID="$( set -o pipefail
|
|
63
|
+
git -C "${1:-.}" -c core.fsmonitor= --literal-pathspecs diff --cached --full-index --no-ext-diff --no-textconv -- "${CREW_LOCKS[@]}" 2>/dev/null \
|
|
64
|
+
| git hash-object --stdin 2>/dev/null )" || CREW_LOCK_OID=""
|
|
65
|
+
case "$CREW_LOCK_OID" in e69de29bb2d1d6434b8b29ae775ad8c2e48c5391|473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813) CREW_LOCK_OID="" ;; esac # the empty blob, SHA-1 and SHA-256
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# The record's file: crew_git_dir (gate-log.sh) must be loaded. Inside the git directory, so it is never committed,
|
|
70
|
+
# needs no .gitignore line on any install, and each linked worktree keeps its own.
|
|
71
|
+
CREW_DEPS_RECORD=crewforth-deps-audit.json
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# payload-check.sh KIT_DIR — is this payload whole enough to install? start.sh and adopt.sh run it before their first
|
|
3
|
+
# write, so a broken download (a truncated archive, a copy that lost a folder, a hook settings.json names but the
|
|
4
|
+
# payload does not carry) stops with "nothing installed" instead of leaving a half install whose gates are missing.
|
|
5
|
+
# Read-only and quick: file tests and one read of settings.json, no node, no network. Prints one problem per line
|
|
6
|
+
# (paths relative to KIT_DIR) and exits 1 when there is any, 0 when there is none, 2 on bad usage.
|
|
7
|
+
# CREW_PAYLOAD_UNCHECKED=1 skips it (the installers say so); it exists for a payload someone trimmed on purpose.
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
K="${1:-}"
|
|
10
|
+
[ -n "$K" ] && [ -d "$K" ] || { echo "usage: payload-check.sh KIT_DIR" >&2; exit 2; }
|
|
11
|
+
P=""
|
|
12
|
+
# The files every install copies and every gate depends on.
|
|
13
|
+
for f in CLAUDE.md settings.json hooks/pre-commit hooks/commit-msg hooks/guard-bash.sh hooks/guard-write.sh \
|
|
14
|
+
eval/doctor.sh eval/lib/settings-json.awk; do
|
|
15
|
+
[ -s "$K/$f" ] || P="$P"$'\n'"missing or empty: $f"
|
|
16
|
+
done
|
|
17
|
+
for d in agents skills hooks eval; do [ -d "$K/$d" ] || P="$P"$'\n'"missing folder: $d/"; done
|
|
18
|
+
# Every hook settings.json wires is in the payload: a missing one is a gate that fails on every tool call.
|
|
19
|
+
if [ -f "$K/settings.json" ]; then
|
|
20
|
+
while IFS= read -r h; do
|
|
21
|
+
[ -s "$K/hooks/$h" ] || P="$P"$'\n'"settings.json names hooks/$h, which the payload does not carry"
|
|
22
|
+
done < <(grep -o '\.claude/hooks/[A-Za-z0-9._-]*' "$K/settings.json" | sed 's|^\.claude/hooks/||' | LC_ALL=C sort -u)
|
|
23
|
+
fi
|
|
24
|
+
# A skill folder without its SKILL.md and an empty agent file load as nothing, silently.
|
|
25
|
+
for s in "$K"/skills/*/; do [ -d "$s" ] || continue; [ -s "${s}SKILL.md" ] || P="$P"$'\n'"skills/$(basename "$s")/SKILL.md is missing or empty"; done
|
|
26
|
+
for a in "$K"/agents/*.md; do [ -e "$a" ] || continue; [ -s "$a" ] || P="$P"$'\n'"agents/$(basename "$a") is empty"; done
|
|
27
|
+
[ -z "$P" ] && exit 0
|
|
28
|
+
printf '%s\n' "${P#$'\n'}"
|
|
29
|
+
exit 1
|
package/kit/eval/lib/preview.sh
CHANGED
|
@@ -66,7 +66,7 @@ _crew_preview(){
|
|
|
66
66
|
esac
|
|
67
67
|
case "$kind" in
|
|
68
68
|
reset)
|
|
69
|
-
n="$(git status --porcelain --untracked-files=no 2>/dev/null | wc -l)"; n="${n//[!0-9]/}"
|
|
69
|
+
n="$(git -c core.fsmonitor= status --porcelain --untracked-files=no 2>/dev/null | wc -l)"; n="${n//[!0-9]/}"
|
|
70
70
|
_pv_words "$cmd" reset; t=""
|
|
71
71
|
for w in $_PV_W; do case "$w" in -*) ;; *) t="$w"; break ;; esac; done
|
|
72
72
|
a=""
|
|
@@ -82,7 +82,7 @@ _crew_preview(){
|
|
|
82
82
|
-*) case "$w" in --*) ;; *) case "$w" in *d*) flags="$flags -d" ;; esac; case "$w" in *x*) flags="$flags -x" ;; *X*) flags="$flags -X" ;; esac ;; esac ;;
|
|
83
83
|
esac; done
|
|
84
84
|
# shellcheck disable=SC2086 # the flags are words of our own
|
|
85
|
-
n="$(git clean $flags 2>/dev/null | wc -l)"; n="${n//[!0-9]/}"
|
|
85
|
+
n="$(git -c core.fsmonitor= clean $flags 2>/dev/null | wc -l)"; n="${n//[!0-9]/}"
|
|
86
86
|
_CREW_PV="Preview: a dry run (git clean $flags) lists ${n:-0} path(s) it would delete."
|
|
87
87
|
;;
|
|
88
88
|
push)
|
|
@@ -118,7 +118,7 @@ _crew_preview(){
|
|
|
118
118
|
[ -n "$out" ] && _CREW_PV="Preview: $out."
|
|
119
119
|
;;
|
|
120
120
|
commit)
|
|
121
|
-
case " $cmd " in *' -a'*|*' --all'*|*' -[A-Za-z]*a'*) line="$(git diff HEAD --shortstat 2>/dev/null)" ;; *) line="$(git diff --cached --shortstat 2>/dev/null)" ;; esac
|
|
121
|
+
case " $cmd " in *' -a'*|*' --all'*|*' -[A-Za-z]*a'*) line="$(git -c core.fsmonitor= diff --no-ext-diff --no-textconv HEAD --shortstat 2>/dev/null)" ;; *) line="$(git -c core.fsmonitor= diff --no-ext-diff --no-textconv --cached --shortstat 2>/dev/null)" ;; esac
|
|
122
122
|
line="${line# }"
|
|
123
123
|
_CREW_PV="Preview: ${line:-nothing staged}."
|
|
124
124
|
;;
|
|
@@ -209,7 +209,7 @@ export function run(o) {
|
|
|
209
209
|
const rows = []; let spent = 0;
|
|
210
210
|
try {
|
|
211
211
|
for (const c of cases) {
|
|
212
|
-
if (o.arm === 'agent' && spent + o.caseBudget > o.maxCost) { rows.push({ id: c.id, skipped: `the run's cap ($${o.maxCost}) would be passed` }); continue; }
|
|
212
|
+
if (o.arm === 'agent' && spent + o.caseBudget > o.maxCost) { rows.push({ id: c.id, kind: c.kind, stratum: c.stratum, skipped: `the run's cap ($${o.maxCost}) would be passed` }); continue; }
|
|
213
213
|
const b = build(c, tmp);
|
|
214
214
|
let out;
|
|
215
215
|
if (c.kind === 'task') {
|
|
@@ -236,6 +236,10 @@ export function summarise(rows, o, spent = 0) {
|
|
|
236
236
|
const sum = (k) => done.reduce((n, r) => n + (Number(r[k]) || 0), 0);
|
|
237
237
|
return { arm: o.arm, model: o.arm === 'agent' ? o.model : null, advisor: o.arm === 'agent' ? o.advisor ?? null : null, split: o.split, cases: rows.length, ran: done.length,
|
|
238
238
|
tasks: { done: tasks.filter((r) => r.done).length, of: tasks.length }, ms: sum('ms'), advisorCalls: sum('advisorCalls'),
|
|
239
|
+
// Worst case: a defect row that did not run counts as missed. Recall over the rows that ran can clear a floor the
|
|
240
|
+
// whole set would miss, because the cap decides which rows run; this is the figure that cannot.
|
|
241
|
+
recallWorstCase: rec([...defects, ...rows.filter((r) => r.skipped && r.kind === 'defect')]),
|
|
242
|
+
byStratumWorstCase: Object.fromEntries(['pattern', 'judgement'].map((s) => [s, rec([...defects, ...rows.filter((r) => r.skipped && r.kind === 'defect')].filter((r) => r.stratum === s))])),
|
|
239
243
|
recall: rec(defects), recallNoHint: rec(defects.filter((r) => !r.hint)), recallHint: rec(defects.filter((r) => r.hint)),
|
|
240
244
|
byStratum: Object.fromEntries(['pattern', 'judgement'].map((s) => [s, rec(defects.filter((r) => r.stratum === s))])),
|
|
241
245
|
precision: { matched, of: claims, pct: pct(matched, claims) },
|
|
@@ -249,6 +253,7 @@ function main(argv) {
|
|
|
249
253
|
const line = (n, x) => `${n} ${x.caught}/${x.of}${x.pct === null ? '' : ` (${x.pct}%)`}`;
|
|
250
254
|
console.log(`review-bench · arm ${s.arm}${s.model ? ` (${s.model}${s.advisor ? ` + ${s.advisor} advisor` : ''})` : ''} · split ${s.split} · ${s.ran}/${s.cases} cases`);
|
|
251
255
|
console.log(` ${line('recall', s.recall)} · ${line('without a hint', s.recallNoHint)} · ${line('with one', s.recallHint)}`);
|
|
256
|
+
if (s.recallWorstCase.of > s.recall.of) console.log(` ${line('recall counting the defect rows that did not run as missed', s.recallWorstCase)}`);
|
|
252
257
|
console.log(` ${line('pattern', s.byStratum.pattern)} · ${line('judgement', s.byStratum.judgement)}`);
|
|
253
258
|
console.log(` precision ${s.precision.matched}/${s.precision.of}${s.precision.pct === null ? ' (no claim made)' : ` (${s.precision.pct}%)`} · ${s.falseAlarms.length} false alarm(s)${s.arm === 'agent' ? ` · cost $${s.cost}` : ''}`);
|
|
254
259
|
if (s.tasks.of) console.log(` tasks done ${s.tasks.done}/${s.tasks.of}`);
|
|
@@ -260,12 +265,19 @@ function main(argv) {
|
|
|
260
265
|
let rc = s.errors.length && o.arm === 'agent' ? 1 : 0;
|
|
261
266
|
if (o.floors) {
|
|
262
267
|
const fl = JSON.parse(fs.readFileSync(o.floors, 'utf8'))[o.arm] ?? {};
|
|
268
|
+
let skippedPass = false;
|
|
263
269
|
for (const [k, v] of Object.entries(fl)) {
|
|
264
270
|
const got = k === 'precision' ? s.precision.pct : k === 'falseAlarms' ? s.falseAlarms.length : (k === 'recall' ? s.recall : s.byStratum[k.replace(/^recall-/, '')])?.pct;
|
|
265
271
|
const ok = k === 'falseAlarms' ? got <= v : got !== null && got !== undefined && got >= v;
|
|
266
|
-
|
|
272
|
+
// A recall floor that holds only because rows were skipped is not a pass: the worst case is checked beside it.
|
|
273
|
+
const worst = k === 'recall' ? s.recallWorstCase.pct : k.startsWith('recall-') ? s.byStratumWorstCase[k.replace(/^recall-/, '')]?.pct : null;
|
|
274
|
+
const onlySkipped = ok && worst !== null && worst !== undefined && worst < v;
|
|
275
|
+
console.log(` floor ${k}: ${got} ${k === 'falseAlarms' ? '≤' : '≥'} ${v} ${!ok ? 'BELOW THE FLOOR' : onlySkipped ? `NOT A PASS — met only because rows did not run (worst case ${worst})` : 'ok'}`);
|
|
267
276
|
if (!ok) rc = 1;
|
|
277
|
+
if (onlySkipped) skippedPass = true;
|
|
268
278
|
}
|
|
279
|
+
// 3: no floor failed on what ran, but one held only because the cap skipped rows. Not 0, and not a measured 1.
|
|
280
|
+
if (rc === 0 && skippedPass) rc = 3;
|
|
269
281
|
}
|
|
270
282
|
return rc;
|
|
271
283
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// node prepass.mjs [--root DIR] [--base REF] [--stdout]
|
|
6
6
|
// default the staged change (git diff --cached), what the next commit will hold
|
|
7
|
-
// --base the working tree against REF (a branch under review: --base origin/next)
|
|
7
|
+
// --base the working tree against REF (a branch under review: --base origin/next), and every untracked,
|
|
8
|
+
// not ignored file as added in full: git diff never lists a file git does not track
|
|
8
9
|
// --stdout print the document, write nothing; otherwise .crewforth/review-prepass.json, through findings.mjs
|
|
9
10
|
// rc 0 = written (or printed), whatever it found · 1 = the document did not verify, nothing written ·
|
|
10
11
|
// 2 = usage, or not a git repository
|
|
@@ -41,6 +42,7 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
|
41
42
|
export const NAME = 'review-prepass';
|
|
42
43
|
const LARGE = 500;
|
|
43
44
|
const QUOTE = 200;
|
|
45
|
+
const MAX_UNTRACKED = 2 * 1024 * 1024; // the size crew-map stops reading at, too
|
|
44
46
|
|
|
45
47
|
const DEBUG = [
|
|
46
48
|
{ files: /\.(m?[jt]sx?|cjs|cts|vue|svelte)$/, re: /\bconsole\.(log|debug|trace)\(|^\s*debugger\s*;?\s*$/, what: 'a console call or debugger statement' },
|
|
@@ -63,7 +65,8 @@ const LOCKS = {
|
|
|
63
65
|
const GENERATED = /(^|\/)(dist|build|vendor|node_modules|__snapshots__)\/|\.(g|freezed|gr|pb)\.dart$|\.min\.(js|css)$|\.snap$|\.lock$|-lock\.(json|yaml)$/;
|
|
64
66
|
|
|
65
67
|
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
66
|
-
|
|
68
|
+
// core.fsmonitor= : a review of someone else's branch must not start the program its .git/config names.
|
|
69
|
+
const git = (root, args) => spawnSync('git', ['-C', root, '-c', 'core.fsmonitor=', '-c', 'core.quotePath=false', ...args], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
67
70
|
|
|
68
71
|
// The added lines of a unified diff with no context: { file, line, text }[] and the files it touches.
|
|
69
72
|
export function addedLines(diff) {
|
|
@@ -95,6 +98,29 @@ export function prepass(root = '.', { base = null } = {}) {
|
|
|
95
98
|
const d = git(rootReal, base ? [...diffArgs, base, '--'] : [...diffArgs, '--cached', '--']);
|
|
96
99
|
if (d.status !== 0) throw new Error(`git diff failed: ${(d.stderr || '').trim().split('\n')[0]}`);
|
|
97
100
|
const { added, files, binary } = addedLines(d.stdout);
|
|
101
|
+
// Under --base the change is the working tree, and a new file nobody has `git add`ed yet is part of it.
|
|
102
|
+
let untracked = 0; const unread = [];
|
|
103
|
+
if (base) {
|
|
104
|
+
// Paths relative to the root, the way every citation here is read back; a subdirectory root lists its own.
|
|
105
|
+
const u = git(rootReal, ['ls-files', '--others', '--exclude-standard', '-z']);
|
|
106
|
+
if (u.status !== 0) throw new Error(`git ls-files failed: ${(u.stderr || '').trim().split('\n')[0]}`);
|
|
107
|
+
// .crewforth/ holds the tools' own documents (this one included), not the change under review.
|
|
108
|
+
for (const f of u.stdout.split('\0').filter((x) => x && !x.startsWith('.crewforth/')).sort()) {
|
|
109
|
+
// Never followed, never read whole when large, never dropped without a word.
|
|
110
|
+
const skip = (reason) => { untracked++; unread.push({ what: f, reason }); };
|
|
111
|
+
let st; try { st = fs.lstatSync(path.join(rootReal, f)); } catch { skip('untracked, could not be read'); continue; }
|
|
112
|
+
if (st.isSymbolicLink()) { skip('an untracked symbolic link: not followed'); continue; }
|
|
113
|
+
if (!st.isFile()) { skip('untracked, not a regular file'); continue; }
|
|
114
|
+
if (st.size > MAX_UNTRACKED) { skip('untracked, over 2 MiB: not read'); continue; }
|
|
115
|
+
let buf; try { buf = fs.readFileSync(path.join(rootReal, f)); } catch { skip('untracked, could not be read'); continue; }
|
|
116
|
+
untracked++; files.push(f);
|
|
117
|
+
if (buf.subarray(0, 8000).includes(0)) { binary.push(f); continue; }
|
|
118
|
+
const text = buf.toString('utf8').split('\n');
|
|
119
|
+
if (text.length > 1 && text[text.length - 1] === '') text.pop();
|
|
120
|
+
text.forEach((t, i) => added.push({ file: f, line: i + 1, text: t.replace(/\r$/, '') }));
|
|
121
|
+
}
|
|
122
|
+
files.sort(); binary.sort();
|
|
123
|
+
}
|
|
98
124
|
|
|
99
125
|
const doc = { schema: SCHEMA_ID, producer: { name: NAME, version: '1' }, findings: [], notMeasured: [] };
|
|
100
126
|
const seen = new Set();
|
|
@@ -114,6 +140,7 @@ export function prepass(root = '.', { base = null } = {}) {
|
|
|
114
140
|
const lines = added.filter((a) => { const t = linesOf(a.file); if (t && t[a.line - 1] === a.text) return true; stale.add(a.file); return false; });
|
|
115
141
|
for (const f of [...stale].sort()) doc.notMeasured.push({ what: f, reason: 'edited again after it was staged: its staged lines are not the ones on disk, so they were not read' });
|
|
116
142
|
for (const f of binary) doc.notMeasured.push({ what: f, reason: 'a binary file: no line to read' });
|
|
143
|
+
doc.notMeasured.push(...unread);
|
|
117
144
|
|
|
118
145
|
// ---- secrets: the pre-commit gate's list, minus the project's allowlist ----
|
|
119
146
|
let allow = new Set();
|
|
@@ -172,6 +199,8 @@ export function prepass(root = '.', { base = null } = {}) {
|
|
|
172
199
|
}
|
|
173
200
|
}
|
|
174
201
|
doc.findings.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
202
|
+
// Reported on the summary line, not in the document: the findings schema has no field for it.
|
|
203
|
+
Object.defineProperty(doc, 'untracked', { value: untracked, enumerable: false });
|
|
175
204
|
return doc;
|
|
176
205
|
}
|
|
177
206
|
|
|
@@ -188,7 +217,7 @@ function main(argv) {
|
|
|
188
217
|
let doc;
|
|
189
218
|
try { doc = prepass(root, { base }); } catch (e) { console.error(`prepass: ${e.message}`); return 2; }
|
|
190
219
|
const count = (s) => doc.findings.filter((f) => f.severity === s).length;
|
|
191
|
-
const line = `review-prepass: ${doc.findings.length} finding(s) · high ${count('high')} · medium ${count('medium')} · low ${count('low')} · info ${count('info')} · ${doc.notMeasured.length} not measured`;
|
|
220
|
+
const line = `review-prepass: ${doc.findings.length} finding(s) · high ${count('high')} · medium ${count('medium')} · low ${count('low')} · info ${count('info')} · ${doc.notMeasured.length} not measured${base ? ` · ${doc.untracked} untracked read` : ''}`;
|
|
192
221
|
if (stdout) {
|
|
193
222
|
const r = validate(doc, root);
|
|
194
223
|
if (!r.ok) { for (const p of r.problems) console.error(`prepass: ${p.at} ${p.code}: ${p.message}`); return 1; }
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
# awk -v op=overlay -v key=K [-v setk=M -v setv=RAWJSON] -f settings-json.awk FILE1 FILE2
|
|
18
18
|
# FILE1 with FILE1.K shallow-merged from FILE2.K (FILE2 wins, FILE1's other members and order kept), then
|
|
19
19
|
# member M set to RAWJSON. rc 10 = FILE1 (or its K) is not an object, 11 = FILE2 has no object at K.
|
|
20
|
+
# awk -v op=hookcheck -f settings-json.awk FILE the "hooks" block holds the shape Claude Code accepts: an object of
|
|
21
|
+
# arrays, each entry an object whose "matcher" (when present) is a string and whose "hooks" is an array of
|
|
22
|
+
# objects with a string "type", and a string "command" when the type is "command". Claude Code discards the
|
|
23
|
+
# WHOLE file on a mismatch, every gate with it. rc 0 = fine (or no "hooks"), 1 = one problem per line on
|
|
24
|
+
# stdout, as hooks.EVENT.N[.hooks.M]: what is wrong, 10 = FILE is not a JSON object.
|
|
20
25
|
# awk -v op=merge -v retired='R1|R2' -f settings-json.awk KIT PROJECT > OUT
|
|
21
26
|
# rc 0 = merged JSON on stdout · 10 = PROJECT is not a JSON object · 11 = KIT is not · 12 = "hooks" has a
|
|
22
27
|
# shape the merge cannot place (not an object of arrays). Nothing is printed on a non-zero rc. The codes sit
|
|
@@ -134,6 +139,19 @@ END{
|
|
|
134
139
|
print (it && !isnull(it) ? V[it] : 0) + V[cr] + (cc && !isnull(cc) ? V[cc] : 0); exit 0 }
|
|
135
140
|
exit 0 }
|
|
136
141
|
if(op=="validate") exit !doc(1)
|
|
142
|
+
if(op=="hookcheck"){ if(!(r=doc(1))) exit 10; h=get(r,"\"hooks\""); if(!h) exit 0; bad=0
|
|
143
|
+
if(T[h]!="o"){ print "hooks: not an object"; exit 1 }
|
|
144
|
+
for(i=1;i<=N[h];i++){ ev=substr(K[h,i],2,length(K[h,i])-2); a=C[h,i]
|
|
145
|
+
if(T[a]!="a"){ print "hooks." ev ": not an array"; bad=1; continue }
|
|
146
|
+
for(j=1;j<=N[a];j++){ e=C[a,j]; w="hooks." ev "." (j-1)
|
|
147
|
+
if(T[e]!="o"){ print w ": not an object"; bad=1; continue }
|
|
148
|
+
m=get(e,"\"matcher\""); if(m && !(T[m]=="s" && substr(V[m],1,1)=="\"")){ print w ": matcher is not a string"; bad=1 }
|
|
149
|
+
hs=get(e,"\"hooks\""); if(!hs || T[hs]!="a"){ print w ": no \"hooks\" array"; bad=1; continue }
|
|
150
|
+
for(k=1;k<=N[hs];k++){ o=C[hs,k]; ww=w ".hooks." (k-1)
|
|
151
|
+
if(T[o]!="o"){ print ww ": not an object"; bad=1; continue }
|
|
152
|
+
t=get(o,"\"type\""); if(!t || !(T[t]=="s" && substr(V[t],1,1)=="\"")){ print ww ": no string \"type\""; bad=1; continue }
|
|
153
|
+
if(V[t]=="\"command\""){ c=get(o,"\"command\""); if(!c || !(T[c]=="s" && substr(V[c],1,1)=="\"")){ print ww ": type command without a string \"command\""; bad=1 } } } } }
|
|
154
|
+
exit bad }
|
|
137
155
|
# gets: several paths in one read (path = "a.b|c.d"), their values on one line, tab-separated; an absent one is
|
|
138
156
|
# empty. One process for what a hot caller (the status line) would otherwise read with one awk per field.
|
|
139
157
|
if(op=="gets"){ if(!(r=doc(1))) exit 10; n=split(path,pp,"|"); o=""
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
# ⚙ crew agents started in this session that have not stopped yet (model routing's records)
|
|
13
13
|
# 🔋 the context fill Claude Code measured (context_window.used_percentage), never an estimate
|
|
14
14
|
# ~$ what Claude Code puts the session's cost at (cost.total_cost_usd), at API list price
|
|
15
|
+
# 📋 the plan's count of done tasks (docs/PLAN.md, todo.sh); with the panel open (/crew-todo), the panel itself is
|
|
16
|
+
# drawn under the line instead: phases and tasks, ✓ done, ☐ open, ▸ the phase and the task in work
|
|
15
17
|
#
|
|
16
18
|
# Wired by the installers only where no status line is set already (theirs is never replaced), from
|
|
17
19
|
# statusline.settings.json beside this file.
|
|
18
20
|
# COST. Claude Code runs this after every change, debounced at 300 ms. Measured: awk (the stdin JSON) and tail (the
|
|
19
|
-
# gate log), and on bash older than 4.2 one date; the rest is builtins. No git, no jq, no python.
|
|
21
|
+
# gate log), and on bash older than 4.2 one date; the rest is builtins. No git, no jq, no python. The plan panel
|
|
22
|
+
# adds one awk only when the plan (or its drawing code) changed since the last line; otherwise it reads its cache.
|
|
20
23
|
set -u
|
|
21
24
|
_here="${BASH_SOURCE%[/\\]*}"; [ "$_here" = "${BASH_SOURCE}" ] && _here=.
|
|
22
25
|
|
|
@@ -93,8 +96,22 @@ case "${COST:-}" in ''|null|*[!0-9.]*) ;; *)
|
|
|
93
96
|
printf -v c '%.2f' "$COST" 2>/dev/null && OUT+=("~\$$c") ;;
|
|
94
97
|
esac
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
# 📋 The plan: its count on the line, or with the panel open (/crew-todo), the panel under it.
|
|
100
|
+
PANEL=""
|
|
101
|
+
if [ -d .claude ] && [ -f "$_here/todo.sh" ]; then
|
|
102
|
+
. "$_here/todo.sh"
|
|
103
|
+
if [ -e .claude/state/todo/open ]; then
|
|
104
|
+
col=1; [ -n "${NO_COLOR:-}" ] && col=0
|
|
105
|
+
crew_todo_read "$col"; PANEL="$_CREW_TODO_PANEL"
|
|
106
|
+
elif [ -f docs/PLAN.md ] || [ -s .claude/state/todo/path ]; then
|
|
107
|
+
crew_todo_read 1; [ -n "$_CREW_TODO_HINT" ] && OUT+=("$_CREW_TODO_HINT")
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
if [ "${#OUT[@]}" -gt 0 ]; then
|
|
112
|
+
line="${OUT[0]}"
|
|
113
|
+
for p in "${OUT[@]:1}"; do line="$line · $p"; done
|
|
114
|
+
printf '%s\n' "$line"
|
|
115
|
+
fi
|
|
116
|
+
[ -n "$PANEL" ] && printf '%s\n' "$PANEL"
|
|
100
117
|
exit 0
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# todo.awk — reads a plan (docs/PLAN.md by default) and draws its to-do panel: the phases and the tasks, a tick on
|
|
2
|
+
# what is done, an empty box on what is not, and the phase and the task in work marked.
|
|
3
|
+
#
|
|
4
|
+
# LC_ALL=C awk -v color=1 -v maxl=14 -v name=docs/PLAN.md -f todo.awk docs/PLAN.md
|
|
5
|
+
#
|
|
6
|
+
# Run it under LC_ALL=C: lengths are counted in bytes, so a cut never depends on the locale (and is mended so it never
|
|
7
|
+
# splits a UTF-8 character).
|
|
8
|
+
#
|
|
9
|
+
# What it reads, and nothing else:
|
|
10
|
+
# a task a list item whose text starts with a box: `- [ ] …` `- [x] …` `1. [ ] …` (at the margin)
|
|
11
|
+
# [ ] open · [x] done · [~] or [>] in work. A box indented under a task is a sub-step, not a task.
|
|
12
|
+
# a phase a heading (## or deeper) with at least one task under it.
|
|
13
|
+
# the scope when the plan has a `## Tasks` heading (or Görevler, İşler, To-do), only what is under it; else the
|
|
14
|
+
# whole file. An `Acceptance criteria` section is never a phase: its boxes are counted as AC n/m.
|
|
15
|
+
# code blocks are skipped (a template quoted in the plan is not the plan).
|
|
16
|
+
# The task in work is the first one marked [~]/[>]; with none marked, the first open one. Its phase is the active one.
|
|
17
|
+
#
|
|
18
|
+
# Output: line 1 the short count for the status line ("📋 7/12"), or nothing when the plan has no task; then the panel.
|
|
19
|
+
# It decides nothing and changes nothing.
|
|
20
|
+
BEGIN {
|
|
21
|
+
if (maxl + 0 < 6) maxl = 14
|
|
22
|
+
wmax = 64
|
|
23
|
+
E = sprintf("%c", 27)
|
|
24
|
+
if (color + 0) { DIM = E "[2m"; BOLD = E "[1m"; ACT = E "[1;36m"; RST = E "[0m" } else { DIM = BOLD = ACT = RST = "" }
|
|
25
|
+
for (i = 128; i < 192; i++) CONT = CONT sprintf("%c", i)
|
|
26
|
+
for (i = 192; i < 256; i++) LEAD = LEAD sprintf("%c", i)
|
|
27
|
+
TICK = "✓"; BOX = "☐"; HERE = "▸"
|
|
28
|
+
nev = 0; fence = ""
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# A fenced code block: skipped whole.
|
|
32
|
+
/^[ ]*(```|~~~)/ {
|
|
33
|
+
f = $0; sub(/^[ ]*/, "", f); f = substr(f, 1, 3)
|
|
34
|
+
if (fence == "") fence = f; else if (f == fence) fence = ""
|
|
35
|
+
next
|
|
36
|
+
}
|
|
37
|
+
fence != "" { next }
|
|
38
|
+
|
|
39
|
+
/^#+[ \t]/ {
|
|
40
|
+
t = $0; match(t, /^#+/); lv = RLENGTH
|
|
41
|
+
t = substr(t, lv + 1); sub(/^[ \t]+/, "", t); sub(/[ \t]+#*[ \t]*$/, "", t); sub(/\r$/, "", t)
|
|
42
|
+
nev++; EV[nev] = "H"; LV[nev] = lv; TX[nev] = t
|
|
43
|
+
next
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
line = $0; sub(/\r$/, "", line)
|
|
48
|
+
match(line, /^ */); ind = RLENGTH
|
|
49
|
+
if (ind > 1) next
|
|
50
|
+
rest = substr(line, ind + 1)
|
|
51
|
+
if (match(rest, /^[-*+][ \t]+\[[ xX~>]\][ \t]+/)) num = ""
|
|
52
|
+
else if (match(rest, /^[0-9]+[.)][ \t]+\[[ xX~>]\][ \t]+/)) { num = rest; sub(/[.)].*/, "", num) }
|
|
53
|
+
else next
|
|
54
|
+
head = substr(rest, 1, RLENGTH); txt = substr(rest, RLENGTH + 1)
|
|
55
|
+
b = head; sub(/^[^[]*\[/, "", b); b = substr(b, 1, 1)
|
|
56
|
+
st = (b == "x" || b == "X") ? "d" : (b == "~" || b == ">") ? "w" : "o"
|
|
57
|
+
# The task's name is what comes before its first " — " (the plan's fields: satisfies, size, risk…).
|
|
58
|
+
k = index(txt, " — "); if (k > 0) txt = substr(txt, 1, k - 1)
|
|
59
|
+
sub(/[ \t]+$/, "", txt)
|
|
60
|
+
if (num != "") txt = num ". " txt
|
|
61
|
+
nev++; EV[nev] = "I"; ST[nev] = st; TX[nev] = txt
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isac(t) { return t ~ /^([Aa]cceptance [Cc]riteria|[Kk]abul [Kk]riter)/ }
|
|
65
|
+
function istasks(t) { return t ~ /^([Tt]asks|TASKS|[Gg]örevler|GÖREVLER|[İi]şler|İŞLER|[Tt]o-?[Dd]o|TODO)([^A-Za-z]|$)/ }
|
|
66
|
+
# A cut in bytes that never leaves half a UTF-8 character: drop trailing continuation bytes, then their lead byte.
|
|
67
|
+
function cut(s, n, r) {
|
|
68
|
+
if (length(s) <= n) return s
|
|
69
|
+
r = substr(s, 1, n - 1)
|
|
70
|
+
while (length(r) && index(CONT, substr(r, length(r), 1))) r = substr(r, 1, length(r) - 1)
|
|
71
|
+
if (length(r) && index(LEAD, substr(r, length(r), 1))) r = substr(r, 1, length(r) - 1)
|
|
72
|
+
return r "…"
|
|
73
|
+
}
|
|
74
|
+
function mark(st) { return st == "d" ? TICK : st == "w" ? HERE : BOX }
|
|
75
|
+
function out(s) { L[++nl] = s }
|
|
76
|
+
|
|
77
|
+
END {
|
|
78
|
+
# The scope: under a Tasks heading when the plan has one.
|
|
79
|
+
s0 = 1; s1 = nev; base = 1; title = ""
|
|
80
|
+
for (i = 1; i <= nev; i++) if (EV[i] == "H" && LV[i] == 1 && title == "") title = TX[i]
|
|
81
|
+
for (i = 1; i <= nev; i++) if (EV[i] == "H" && LV[i] >= 2 && istasks(TX[i])) {
|
|
82
|
+
s0 = i + 1; s1 = nev; base = LV[i]
|
|
83
|
+
for (j = i + 1; j <= nev; j++) if (EV[j] == "H" && LV[j] <= LV[i]) { s1 = j - 1; break }
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
# Acceptance criteria, wherever they are: counted, never shown as a phase.
|
|
87
|
+
ac = 0; acd = 0; inac = 0
|
|
88
|
+
for (i = 1; i <= nev; i++) {
|
|
89
|
+
if (EV[i] == "H") { if (inac && LV[i] <= aclv) inac = 0; if (isac(TX[i])) { inac = 1; aclv = LV[i] } }
|
|
90
|
+
else if (inac) { ac++; if (ST[i] == "d") acd++ }
|
|
91
|
+
}
|
|
92
|
+
# Phases and tasks inside the scope. A heading opens a phase; tasks before any heading sit in an unnamed one.
|
|
93
|
+
np = 0; cur = 0; skip = 0; skiplv = 0; nt = 0; done = 0
|
|
94
|
+
for (i = s0; i <= s1; i++) {
|
|
95
|
+
if (EV[i] == "H") {
|
|
96
|
+
if (skip && LV[i] <= skiplv) skip = 0
|
|
97
|
+
if (LV[i] <= base && base > 1) continue
|
|
98
|
+
if (isac(TX[i]) || (LV[i] == 1 && base == 1)) { if (isac(TX[i])) { skip = 1; skiplv = LV[i] }; cur = 0; continue }
|
|
99
|
+
if (skip) continue
|
|
100
|
+
np++; PN[np] = TX[i]; PT[np] = 0; PD[np] = 0; cur = np
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (skip) continue
|
|
104
|
+
if (cur == 0) { np++; PN[np] = ""; PT[np] = 0; PD[np] = 0; cur = np }
|
|
105
|
+
PT[cur]++; nt++; T[cur, PT[cur]] = i
|
|
106
|
+
if (ST[i] == "d") { PD[cur]++; done++ }
|
|
107
|
+
}
|
|
108
|
+
if (nt == 0) { print ""; print "📋 " cut(name != "" ? name : FILENAME, wmax) ": no task with a box yet ( - [ ] task · - [x] done · - [~] in work )"; exit }
|
|
109
|
+
|
|
110
|
+
# The task in work: the first marked [~]/[>], else the first open one.
|
|
111
|
+
ap = 0; at = 0
|
|
112
|
+
for (p = 1; p <= np && !ap; p++) for (t = 1; t <= PT[p]; t++) if (ST[T[p, t]] == "w") { ap = p; at = t; break }
|
|
113
|
+
for (p = 1; p <= np && !ap; p++) for (t = 1; t <= PT[p]; t++) if (ST[T[p, t]] == "o") { ap = p; at = t; break }
|
|
114
|
+
|
|
115
|
+
# Phases with no task are not phases.
|
|
116
|
+
m = 0
|
|
117
|
+
for (p = 1; p <= np; p++) if (PT[p] > 0) { m++; PN[m] = PN[p]; PT[m] = PT[p]; PD[m] = PD[p]; for (t = 1; t <= PT[p]; t++) T[m, t] = T[p, t]; if (p == ap) ap = m }
|
|
118
|
+
np = m
|
|
119
|
+
phased = (np > 1 || PN[1] != "")
|
|
120
|
+
|
|
121
|
+
print "📋 " done "/" nt
|
|
122
|
+
hd = BOLD "📋 " (title != "" ? cut(title, wmax) : "Plan") RST " · " done "/" nt " done"
|
|
123
|
+
if (phased && ap) hd = hd " · phase " ap "/" np
|
|
124
|
+
if (ac) hd = hd " · AC " acd "/" ac
|
|
125
|
+
if (!ap) hd = hd " · all done"
|
|
126
|
+
out(hd)
|
|
127
|
+
|
|
128
|
+
# Which phases get a line: all of them when they fit, else the active one with its neighbours and a count of the rest.
|
|
129
|
+
budget = maxl - 1
|
|
130
|
+
pa = 1; pb = np
|
|
131
|
+
if (phased && np > budget - 3) {
|
|
132
|
+
c = (ap ? ap : np); pa = c - 1; if (pa < 1) pa = 1; pb = pa + (budget - 6); if (pb > np) pb = np
|
|
133
|
+
if (pb - pa < budget - 6) { pa = pb - (budget - 6); if (pa < 1) pa = 1 }
|
|
134
|
+
}
|
|
135
|
+
# The tasks of the active phase (or of the plan, when it has no phases) get what is left.
|
|
136
|
+
room = budget - (phased ? (pb - pa + 1) + (pa > 1) + (pb < np) : 0)
|
|
137
|
+
if (room < 3) room = 3
|
|
138
|
+
|
|
139
|
+
if (phased && pa > 1) { k = 0; for (p = 1; p < pa; p++) k += (PD[p] == PT[p]); out(DIM "… " (pa - 1) " phase" (pa > 2 ? "s" : "") " before (" k " done)" RST) }
|
|
140
|
+
for (p = (phased ? pa : 1); p <= (phased ? pb : 1); p++) {
|
|
141
|
+
if (phased) {
|
|
142
|
+
nm = (PN[p] != "" ? cut(PN[p], wmax) : "(no phase)") " " PD[p] "/" PT[p]
|
|
143
|
+
if (p == ap) out(ACT HERE " " nm RST)
|
|
144
|
+
else if (PD[p] == PT[p]) out(DIM TICK " " nm RST)
|
|
145
|
+
else out(BOX " " nm)
|
|
146
|
+
}
|
|
147
|
+
if (p != ap && !(phased == 0 && !ap)) continue
|
|
148
|
+
# A window over the tasks that keeps the one in work in sight: one before it, the rest after.
|
|
149
|
+
n = PT[p]; lo = 1; hi = n
|
|
150
|
+
if (n > room) {
|
|
151
|
+
lo = (at > 1 ? at - 1 : 1)
|
|
152
|
+
hi = (lo == 1) ? room - 1 : lo + room - 3
|
|
153
|
+
if (hi >= n) { hi = n; lo = n - room + 2; if (lo < 1) lo = 1 }
|
|
154
|
+
}
|
|
155
|
+
ind = (phased ? " " : "")
|
|
156
|
+
if (lo > 1) out(ind DIM "… " (lo - 1) " above" RST)
|
|
157
|
+
for (t = lo; t <= hi; t++) {
|
|
158
|
+
e = T[p, t]; s = mark(ST[e]) " " cut(TX[e], wmax)
|
|
159
|
+
if (t == at && p == ap) out(ind ACT HERE " " cut(TX[e], wmax) RST)
|
|
160
|
+
else if (ST[e] == "d") out(ind DIM s RST)
|
|
161
|
+
else out(ind s)
|
|
162
|
+
}
|
|
163
|
+
if (hi < n) out(ind DIM "… " (n - hi) " more" RST)
|
|
164
|
+
}
|
|
165
|
+
if (phased && pb < np) { k = 0; for (p = pb + 1; p <= np; p++) k += PT[p] - PD[p]; out(BOX " … " (np - pb) " more phase" (np - pb > 1 ? "s" : "") " (" k " open)") }
|
|
166
|
+
for (i = 1; i <= nl; i++) print L[i]
|
|
167
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# todo.sh — the plan's to-do panel: what is done (✓), what is not (☐), and the phase and the task in work (▸), read
|
|
3
|
+
# from the plan file (docs/PLAN.md unless /crew-todo named another). The drawing is todo.awk; this file keeps the
|
|
4
|
+
# panel's two bits of state and serves both its readers.
|
|
5
|
+
#
|
|
6
|
+
# bash .claude/eval/lib/todo.sh open the panel if it is closed, close it if it is open
|
|
7
|
+
# bash .claude/eval/lib/todo.sh on [FILE] open it (and read FILE from now on) · off: close it
|
|
8
|
+
# bash .claude/eval/lib/todo.sh FILE the same as `on FILE`
|
|
9
|
+
# bash .claude/eval/lib/todo.sh show [FILE] print the panel here, once (no status line needed)
|
|
10
|
+
# bash .claude/eval/lib/todo.sh status one line: open or closed, which file, how far
|
|
11
|
+
#
|
|
12
|
+
# The status line sources this file and calls crew_todo_read: open, the panel is drawn under the line; closed, the
|
|
13
|
+
# line carries only the count ("📋 7/12"). Nothing here decides or blocks anything, and the plan is never written:
|
|
14
|
+
# the plan's own boxes are the record ([ ] open · [x] done · [~] in work), ticked by whoever does the work.
|
|
15
|
+
#
|
|
16
|
+
# State, per project, under .claude/state/todo/: `open` (present = the panel is open) and `path` (the plan file, when
|
|
17
|
+
# it is not docs/PLAN.md); `view` is a cache of the drawing, made again only when the plan, todo.awk or the settings
|
|
18
|
+
# changed, so a status line refresh costs no process at all while the plan stands still.
|
|
19
|
+
# Settings: CREW_TODO_LINES (the panel's height, default 14) · NO_COLOR (set: no colours).
|
|
20
|
+
|
|
21
|
+
_CREW_TODO_LIB="${BASH_SOURCE%[/\\]*}"; [ "$_CREW_TODO_LIB" = "${BASH_SOURCE}" ] && _CREW_TODO_LIB=.
|
|
22
|
+
_CREW_TODO_DIR=.claude/state/todo
|
|
23
|
+
|
|
24
|
+
# The plan file this project's panel reads: _CREW_TODO_PLAN. Run from the project root.
|
|
25
|
+
crew_todo_plan(){
|
|
26
|
+
_CREW_TODO_PLAN=""
|
|
27
|
+
[ -s "$_CREW_TODO_DIR/path" ] && { IFS= read -r _CREW_TODO_PLAN < "$_CREW_TODO_DIR/path"; _CREW_TODO_PLAN="${_CREW_TODO_PLAN%$'\r'}"; }
|
|
28
|
+
[ -n "$_CREW_TODO_PLAN" ] || _CREW_TODO_PLAN=docs/PLAN.md
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# The drawing: _CREW_TODO_HINT (the count, "" when the plan has no task) and _CREW_TODO_PANEL (its lines, joined with
|
|
32
|
+
# newlines). $1 = color (1/0). From the cache when the cache still matches; one awk when it does not.
|
|
33
|
+
crew_todo_read(){
|
|
34
|
+
local col="${1:-1}" maxl="${CREW_TODO_LINES:-14}" v="$_CREW_TODO_DIR/view" key l first=1
|
|
35
|
+
_CREW_TODO_HINT=""; _CREW_TODO_PANEL=""
|
|
36
|
+
case "$maxl" in ''|*[!0-9]*) maxl=14 ;; esac
|
|
37
|
+
crew_todo_plan
|
|
38
|
+
if [ ! -f "$_CREW_TODO_PLAN" ]; then
|
|
39
|
+
_CREW_TODO_PANEL="📋 no plan at $_CREW_TODO_PLAN yet: /crew-plan writes one, /crew-todo <file> reads another"
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
key="k=$col/$maxl/$_CREW_TODO_PLAN"
|
|
43
|
+
# The cache holds when it is newer than the plan and than the drawing code, and was drawn with these settings.
|
|
44
|
+
if [ -f "$v" ] && [ "$v" -nt "$_CREW_TODO_PLAN" ] && ! [ "$_CREW_TODO_LIB/todo.awk" -nt "$v" ]; then
|
|
45
|
+
IFS= read -r l < "$v" 2>/dev/null || l=""
|
|
46
|
+
[ "$l" = "$key" ] || l=""
|
|
47
|
+
else l=""; fi
|
|
48
|
+
if [ -z "$l" ]; then
|
|
49
|
+
[ -d "$_CREW_TODO_DIR" ] || mkdir -p "$_CREW_TODO_DIR" 2>/dev/null
|
|
50
|
+
{ printf '%s\n' "$key"; LC_ALL=C awk -v color="$col" -v maxl="$maxl" -v name="$_CREW_TODO_PLAN" -f "$_CREW_TODO_LIB/todo.awk" "$_CREW_TODO_PLAN" 2>/dev/null; } > "$v" 2>/dev/null || return 0
|
|
51
|
+
fi
|
|
52
|
+
{
|
|
53
|
+
IFS= read -r l # the key
|
|
54
|
+
IFS= read -r _CREW_TODO_HINT
|
|
55
|
+
while IFS= read -r l; do
|
|
56
|
+
if [ "$first" = 1 ]; then _CREW_TODO_PANEL="$l"; first=0; else _CREW_TODO_PANEL="$_CREW_TODO_PANEL"$'\n'"$l"; fi
|
|
57
|
+
done
|
|
58
|
+
} < "$v"
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Sourced: the functions only.
|
|
63
|
+
[ "${BASH_SOURCE[0]}" = "$0" ] || return 0
|
|
64
|
+
|
|
65
|
+
set -u
|
|
66
|
+
[ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "$CLAUDE_PROJECT_DIR/.claude" ] && cd "$CLAUDE_PROJECT_DIR" 2>/dev/null
|
|
67
|
+
if [ ! -d .claude ]; then echo "todo: no .claude/ here: run it from the project root of a Crewforth install" >&2; exit 1; fi
|
|
68
|
+
|
|
69
|
+
cmd="${1:-toggle}"; file="${2:-}"
|
|
70
|
+
case "$cmd" in on|off|toggle|show|status) ;; -h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
71
|
+
-*) echo "todo: unknown option $cmd (on · off · show · status · a plan file)" >&2; exit 2 ;;
|
|
72
|
+
*) file="$cmd"; cmd=on ;;
|
|
73
|
+
esac
|
|
74
|
+
case "$file" in *$'\n'*|*$'\r'*) echo "todo: a file name with a line break is not read" >&2; exit 2 ;; esac
|
|
75
|
+
|
|
76
|
+
if [ "$cmd" = show ]; then
|
|
77
|
+
# Drawn here, not from the cache, and coloured only on a terminal: this may go to a chat that does not take colour.
|
|
78
|
+
crew_todo_plan; [ -n "$file" ] && _CREW_TODO_PLAN="$file"
|
|
79
|
+
if [ ! -f "$_CREW_TODO_PLAN" ]; then echo "📋 no plan at $_CREW_TODO_PLAN yet: /crew-plan writes one"; exit 0; fi
|
|
80
|
+
c=0; [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && c=1
|
|
81
|
+
LC_ALL=C awk -v color="$c" -v maxl=999 -v name="$_CREW_TODO_PLAN" -f "$_CREW_TODO_LIB/todo.awk" "$_CREW_TODO_PLAN" | sed 1d
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
mkdir -p "$_CREW_TODO_DIR" 2>/dev/null || { echo "todo: cannot write $_CREW_TODO_DIR" >&2; exit 1; }
|
|
86
|
+
if [ -n "$file" ]; then
|
|
87
|
+
[ -f "$file" ] || echo "todo: $file does not exist yet; the panel will read it once it does"
|
|
88
|
+
if [ "$file" = docs/PLAN.md ]; then rm -f "$_CREW_TODO_DIR/path"; else printf '%s\n' "$file" > "$_CREW_TODO_DIR/path"; fi
|
|
89
|
+
fi
|
|
90
|
+
[ "$cmd" = toggle ] && { if [ -e "$_CREW_TODO_DIR/open" ]; then cmd=off; else cmd=on; fi; }
|
|
91
|
+
case "$cmd" in
|
|
92
|
+
on) : > "$_CREW_TODO_DIR/open" ;;
|
|
93
|
+
off) rm -f "$_CREW_TODO_DIR/open" ;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
crew_todo_read 0
|
|
97
|
+
st=closed; [ -e "$_CREW_TODO_DIR/open" ] && st=open
|
|
98
|
+
how=""
|
|
99
|
+
if [ -n "$_CREW_TODO_HINT" ]; then
|
|
100
|
+
how="${_CREW_TODO_HINT#📋 } done"
|
|
101
|
+
inwork="$(printf '%s\n' "$_CREW_TODO_PANEL" | grep '▸ ' | tail -n 1 | sed 's/^ *▸ //')"
|
|
102
|
+
[ -n "$inwork" ] && how="$how, in work: $inwork"
|
|
103
|
+
elif [ -f "$_CREW_TODO_PLAN" ]; then how="no task with a box in it yet"
|
|
104
|
+
else how="the file does not exist yet"; fi
|
|
105
|
+
if [ "$st" = open ]; then echo "plan panel: open · $_CREW_TODO_PLAN · $how"
|
|
106
|
+
else echo "plan panel: closed · $_CREW_TODO_PLAN · $how (the status line keeps the count)"; fi
|
|
107
|
+
|
|
108
|
+
# Shown under Crewforth's status line only. Say so when this project's status line is another one, or none.
|
|
109
|
+
sl=""
|
|
110
|
+
for f in .claude/settings.local.json .claude/settings.json "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"; do
|
|
111
|
+
[ -f "$f" ] && [ -f "$_CREW_TODO_LIB/settings-json.awk" ] || continue
|
|
112
|
+
sl="$(awk -v op=get -v path=statusLine.command -f "$_CREW_TODO_LIB/settings-json.awk" "$f" 2>/dev/null)" || sl=""
|
|
113
|
+
[ -n "$sl" ] && break
|
|
114
|
+
done
|
|
115
|
+
case "$sl" in
|
|
116
|
+
*eval/lib/statusline.sh*) ;;
|
|
117
|
+
?*) [ "$st" = open ] && echo "note: this project's status line is not Crewforth's, so the panel does not show there; /crew-todo show prints it here" ;;
|
|
118
|
+
*) [ "$st" = open ] && echo "note: no status line is set; bash .claude/eval/lib/statusline.sh --install (Bash tool, not PowerShell) sets Crewforth's (only where none is set), or /crew-todo show prints the panel here" ;;
|
|
119
|
+
esac
|
|
120
|
+
exit 0
|