@erclx/canon 5.13.0 → 5.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +3 -2
- package/claude/skills/deprecation-migration/REQUIREMENT.md +62 -0
- package/claude/skills/deprecation-migration/SKILL.md +115 -0
- package/claude/skills/deprecation-migration/references/adopted.md +50 -0
- package/claude/skills/git-followup/REQUIREMENT.md +1 -0
- package/claude/skills/git-followup/SKILL.md +3 -0
- package/claude/skills/git-pr/REQUIREMENT.md +1 -0
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-ship/REQUIREMENT.md +3 -0
- package/claude/skills/git-ship/SKILL.md +12 -1
- package/claude/skills/review-pr/SKILL.md +1 -1
- package/claude/skills/review-pr/references/close-out.md +1 -1
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/review-ui/REQUIREMENT.md +5 -2
- package/claude/skills/review-ui/SKILL.md +12 -4
- package/claude/skills/review-ui/references/driving.md +5 -3
- package/claude/skills/review-ui/references/post.md +3 -2
- package/claude/skills/role-orchestrator/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +10 -10
- package/claude/skills/role-orchestrator/scripts/poll.ts +628 -0
- package/claude/skills/role-orchestrator/scripts/repo.ts +69 -0
- package/claude/skills/role-orchestrator/scripts/watch.ts +267 -0
- package/docs/agents/commands.md +1 -1
- package/docs/agents/index.md +1 -0
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/pr-evidence.md +5 -0
- package/docs/agents/pr-frames.md +112 -0
- package/docs/agents/pr-reads.md +1 -1
- package/docs/agents/sandbox.md +3 -3
- package/docs/agents/state-scoped-risk.md +2 -2
- package/docs/workflow/skill-map.md +15 -14
- package/package.json +4 -5
- package/scripts/core/check-color-source.sh +11 -1
- package/scripts/core/install-check.sh +13 -4
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-fixtures.sh +2 -2
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/manage-sandbox.sh +2 -2
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +1 -0
- package/src/commands/pr.ts +452 -0
- package/src/commands/sandbox.ts +2 -2
- package/src/context/architecture.ts +5 -2
- package/src/context/citations.ts +1 -1
- package/src/gate/boundaries.ts +509 -0
- package/src/gate/measures.ts +8 -19
- package/src/gate/stages.ts +12 -28
- package/src/pr/frames.ts +323 -0
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/coverage.ts +1 -1
- package/src/sandbox/expect.ts +0 -1
- package/src/shipped/references.ts +8 -10
- package/src/targets/pulls.ts +2 -2
- package/src/tooling/manifest.ts +2 -3
- package/src/tooling/seeds.ts +37 -0
- package/tooling/claude/manifest.toml +3 -3
- package/tsconfig.json +1 -1
- package/claude/skills/role-orchestrator/scripts/poll.sh +0 -504
- package/claude/skills/role-orchestrator/scripts/watch.sh +0 -181
- package/scripts/core/check-capability-seeding.sh +0 -134
- package/scripts/core/check-ignore-parity.sh +0 -141
- package/scripts/core/check-plugin-boundary.sh +0 -45
- package/scripts/core/check-seed-independence.sh +0 -62
- package/scripts/core/check-skill-paths.sh +0 -24
- package/scripts/core/list-seed-roots.sh +0 -18
- package/scripts/lib/tooling.sh +0 -37
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Reports worker and pull request transitions as they happen. Reads only.
|
|
3
|
-
#
|
|
4
|
-
# The poll answers "what moved since the last run" on a schedule the session
|
|
5
|
-
# holds. This answers "what just changed" on a loop of its own, and it exists
|
|
6
|
-
# because the two readings a dispatch needs are not one reading: a worker that
|
|
7
|
-
# finishes goes idle and a worker that crashes vanishes, so a watch matching
|
|
8
|
-
# only the pull request list stays silent through the second. A third case is
|
|
9
|
-
# neither: a worker that stops on a question or a prompt neither finishes nor
|
|
10
|
-
# crashes, and sits reading "waiting" beside the two statuses that resolve on
|
|
11
|
-
# their own until something reports it, which is the WORKER-STOPPED line
|
|
12
|
-
# below, or WORKER-UNMEASURABLE for a "waiting" row too old to carry a stamp
|
|
13
|
-
# this reads.
|
|
14
|
-
#
|
|
15
|
-
# `set -e` is deliberately not set. This runs for hours, and one transient `gh`
|
|
16
|
-
# failure aborting the loop is a silent stop rather than a reported one. Every
|
|
17
|
-
# read below states its own failure instead.
|
|
18
|
-
set -uo pipefail
|
|
19
|
-
|
|
20
|
-
# Seconds between passes. A pass costs two cheap reads and the transitions it
|
|
21
|
-
# watches take minutes, so the number trades staleness against little. Raise it
|
|
22
|
-
# in a project whose workers run long.
|
|
23
|
-
INTERVAL=60
|
|
24
|
-
|
|
25
|
-
# Seconds a worker can sit in "waiting" before this reports it as stopped
|
|
26
|
-
# rather than folding it into the ordinary status-change lines below. "busy"
|
|
27
|
-
# and "idle" resolve on their own; "waiting" does not, so a dwell that keeps
|
|
28
|
-
# growing there is a session blocked on something outside itself.
|
|
29
|
-
#
|
|
30
|
-
# Measured against the live session registry: 341 usable records carried a
|
|
31
|
-
# status at all, and exactly one carried "waiting", too sparse a sample to fit
|
|
32
|
-
# a distribution. The number is picked from the two bounds the measurement can
|
|
33
|
-
# still name rather than from a round guess: well above INTERVAL, so a handful
|
|
34
|
-
# of passes confirm the row before it reports rather than one slow tool call
|
|
35
|
-
# tripping it, and well inside the ten-to-thirty-minute span a dispatched build
|
|
36
|
-
# ordinarily runs, so a real stall is caught with most of that window still
|
|
37
|
-
# open to act on it.
|
|
38
|
-
#
|
|
39
|
-
# That one record carried `waitingFor: "approve Bash"`, a permission prompt
|
|
40
|
-
# rather than a question, and the two clear on different schedules: a prompt
|
|
41
|
-
# resolves the moment a person approves it, where a question can sit
|
|
42
|
-
# legitimately while a controller finishes a turn. This number is defensible
|
|
43
|
-
# for the second and generous for the first, and the gap stays open rather
|
|
44
|
-
# than splitting the constant on a sample of one.
|
|
45
|
-
STALL_THRESHOLD_S=300
|
|
46
|
-
|
|
47
|
-
# Resolving the main worktree root rather than this file's folder keeps a watch
|
|
48
|
-
# started from a linked worktree reading the same repository as one started from
|
|
49
|
-
# main. The repository field on a roster row is that root's `.git`.
|
|
50
|
-
MAIN_ROOT="$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')"
|
|
51
|
-
if [ -z "$MAIN_ROOT" ]; then
|
|
52
|
-
echo "watch: not a git repository, so nothing is watched" >&2
|
|
53
|
-
exit 1
|
|
54
|
-
fi
|
|
55
|
-
REPOSITORY="$MAIN_ROOT/.git"
|
|
56
|
-
|
|
57
|
-
# The base branch is read rather than assumed, since this ships to projects that
|
|
58
|
-
# do not all call it `main`. It is what separates a worker from the controlling
|
|
59
|
-
# session, which sits on the base branch in the main worktree.
|
|
60
|
-
BASE_REF="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"
|
|
61
|
-
if [ -z "$BASE_REF" ]; then
|
|
62
|
-
BASE_REF="origin/$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null || echo main)"
|
|
63
|
-
fi
|
|
64
|
-
BASE_BRANCH="${BASE_REF#origin/}"
|
|
65
|
-
|
|
66
|
-
# Every session in this repository holding a branch other than the base one is a
|
|
67
|
-
# worker, whoever launched it. The prototype matched the `orchestrator-` prefix
|
|
68
|
-
# instead, which reads a dispatched worker and misses every hand-launched one,
|
|
69
|
-
# and those are the ordinary shape whenever the operator is launching.
|
|
70
|
-
#
|
|
71
|
-
# The row goes out tab-separated and comes back read with IFS set to a tab. A
|
|
72
|
-
# name this client actually writes carries spaces, `Update session markdown
|
|
73
|
-
# and check runnable commands (3)` among them, and splitting on whitespace
|
|
74
|
-
# took that one apart into a wrong name, branch, status, and dwell.
|
|
75
|
-
read_workers() {
|
|
76
|
-
canon sessions list --json 2>/dev/null | tail -1 | jq -r \
|
|
77
|
-
--arg repository "$REPOSITORY" --arg base "$BASE_BRANCH" '
|
|
78
|
-
.sessions[]?
|
|
79
|
-
| select(.repository == $repository)
|
|
80
|
-
| select(.branch != null and .branch != $base)
|
|
81
|
-
| [.name, .branch, .status, (.statusDwellMs // -1 | tostring)]
|
|
82
|
-
| @tsv
|
|
83
|
-
' 2>/dev/null | sort
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
read_pulls() {
|
|
87
|
-
gh pr list --state open --json number,headRefName,title --jq \
|
|
88
|
-
'.[] | "PR-OPEN #\(.number) \(.headRefName) \(.title)"' 2>/dev/null | sort
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# A failed read is reported and the previous baseline kept. Reading an empty
|
|
92
|
-
# result as the current state would report every worker gone and every pull
|
|
93
|
-
# request new on the pass after, which is the one failure that makes a detection
|
|
94
|
-
# tool worth less than no tool.
|
|
95
|
-
#
|
|
96
|
-
# The two sources carry a baseline flag apiece rather than sharing one. A read
|
|
97
|
-
# that fails on the first pass leaves its baseline empty while the other source
|
|
98
|
-
# establishes one, and a single flag would then report that source's whole list
|
|
99
|
-
# as new on the pass after it recovers.
|
|
100
|
-
prev_pulls=""
|
|
101
|
-
prev_workers=""
|
|
102
|
-
prev_names=""
|
|
103
|
-
pulls_seen=0
|
|
104
|
-
workers_seen=0
|
|
105
|
-
|
|
106
|
-
# One row per worker name currently reported, waiting past STALL_THRESHOLD_S
|
|
107
|
-
# or waiting with no stamp to measure, so a reader watching the log is told
|
|
108
|
-
# once rather than on every remaining pass. The prototype for that shape is
|
|
109
|
-
# the WORKER-GONE/WORKER pair above, tracked in the same state a status change
|
|
110
|
-
# is: a name drops out the pass it stops reading "waiting", which lets the
|
|
111
|
-
# same worker report a second stall later in its life rather than being
|
|
112
|
-
# marked forever by its first one.
|
|
113
|
-
declare -A stalled
|
|
114
|
-
|
|
115
|
-
while true; do
|
|
116
|
-
pulls="$(read_pulls)"
|
|
117
|
-
pulls_read=$?
|
|
118
|
-
workers="$(read_workers)"
|
|
119
|
-
workers_read=$?
|
|
120
|
-
|
|
121
|
-
if [ "$pulls_read" -ne 0 ]; then
|
|
122
|
-
echo "watch: the open pull request list failed to load, so none is classified this pass"
|
|
123
|
-
fi
|
|
124
|
-
if [ "$workers_read" -ne 0 ]; then
|
|
125
|
-
echo "watch: the session roster failed to load, so no worker is classified this pass"
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
names="$(printf '%s\n' "$workers" | awk -F'\t' 'NF {print $1}' | sort -u)"
|
|
129
|
-
|
|
130
|
-
if [ "$pulls_seen" -eq 1 ] && [ "$pulls_read" -eq 0 ]; then
|
|
131
|
-
comm -13 <(printf '%s\n' "$prev_pulls") <(printf '%s\n' "$pulls") | grep . || true
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
if [ "$workers_seen" -eq 1 ] && [ "$workers_read" -eq 0 ]; then
|
|
135
|
-
comm -13 <(printf '%s\n' "$prev_workers") <(printf '%s\n' "$workers") |
|
|
136
|
-
grep . | sed 's/^/WORKER /' || true
|
|
137
|
-
comm -23 <(printf '%s\n' "$prev_names") <(printf '%s\n' "$names") |
|
|
138
|
-
grep . | sed 's/^/WORKER-GONE /' || true
|
|
139
|
-
fi
|
|
140
|
-
|
|
141
|
-
if [ "$workers_read" -eq 0 ]; then
|
|
142
|
-
while IFS=$'\t' read -r w_name w_branch w_status w_dwell_ms; do
|
|
143
|
-
[ -z "$w_name" ] && continue
|
|
144
|
-
|
|
145
|
-
if [ "$w_status" != "waiting" ]; then
|
|
146
|
-
unset "stalled[$w_name]"
|
|
147
|
-
continue
|
|
148
|
-
fi
|
|
149
|
-
|
|
150
|
-
if [ "$w_dwell_ms" = "-1" ]; then
|
|
151
|
-
if [ -z "${stalled[$w_name]:-}" ]; then
|
|
152
|
-
echo "WORKER-UNMEASURABLE $w_name $w_branch"
|
|
153
|
-
stalled[$w_name]=1
|
|
154
|
-
fi
|
|
155
|
-
continue
|
|
156
|
-
fi
|
|
157
|
-
|
|
158
|
-
w_dwell_s=$((w_dwell_ms / 1000))
|
|
159
|
-
if [ "$w_dwell_s" -ge "$STALL_THRESHOLD_S" ]; then
|
|
160
|
-
if [ -z "${stalled[$w_name]:-}" ]; then
|
|
161
|
-
echo "WORKER-STOPPED $w_name $w_branch ${w_dwell_s}s"
|
|
162
|
-
stalled[$w_name]=1
|
|
163
|
-
fi
|
|
164
|
-
else
|
|
165
|
-
unset "stalled[$w_name]"
|
|
166
|
-
fi
|
|
167
|
-
done <<<"$workers"
|
|
168
|
-
fi
|
|
169
|
-
|
|
170
|
-
if [ "$pulls_read" -eq 0 ]; then
|
|
171
|
-
prev_pulls="$pulls"
|
|
172
|
-
pulls_seen=1
|
|
173
|
-
fi
|
|
174
|
-
if [ "$workers_read" -eq 0 ]; then
|
|
175
|
-
prev_workers="$workers"
|
|
176
|
-
prev_names="$names"
|
|
177
|
-
workers_seen=1
|
|
178
|
-
fi
|
|
179
|
-
|
|
180
|
-
sleep "$INTERVAL"
|
|
181
|
-
done
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
shopt -s nullglob
|
|
5
|
-
|
|
6
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
8
|
-
|
|
9
|
-
TOOLING_ROOT="$PROJECT_ROOT/tooling"
|
|
10
|
-
MARKER="canon-no-seed:"
|
|
11
|
-
|
|
12
|
-
# `find` writes to stderr and returns non-zero for a missing root, inside a
|
|
13
|
-
# process substitution whose status nothing reads. Without this the walk covers
|
|
14
|
-
# nothing and the check reports every capability seeded having never read one.
|
|
15
|
-
if [ ! -d "$TOOLING_ROOT" ]; then
|
|
16
|
-
echo "No tooling root at ${TOOLING_ROOT#"$PROJECT_ROOT/"}, capability seeding unverifiable." >&2
|
|
17
|
-
exit 1
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
failures=""
|
|
21
|
-
|
|
22
|
-
# A comment naming the marker anywhere in the source file is the recorded
|
|
23
|
-
# reason a capability withholds itself from every destination it was compared
|
|
24
|
-
# against. The reason's own prose is free-form, so only the marker is tested.
|
|
25
|
-
has_reason() {
|
|
26
|
-
grep -q "$MARKER" "$1"
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
# Compares one source directory's files against the basenames already
|
|
30
|
-
# expanded from a set of destination paths. Presence on either side clears a
|
|
31
|
-
# name; a source file reaching neither the destination nor a marked reason is
|
|
32
|
-
# a capability answered on one side of the seed or config boundary and not
|
|
33
|
-
# the other.
|
|
34
|
-
check_capability() {
|
|
35
|
-
local label="$1" src_dir="$2"
|
|
36
|
-
shift 2
|
|
37
|
-
|
|
38
|
-
[ -d "$src_dir" ] || return 0
|
|
39
|
-
|
|
40
|
-
local dest_names=" "
|
|
41
|
-
local dest
|
|
42
|
-
for dest in "$@"; do
|
|
43
|
-
[ -f "$dest" ] || continue
|
|
44
|
-
dest_names="$dest_names$(basename "$dest") "
|
|
45
|
-
done
|
|
46
|
-
|
|
47
|
-
local src_file name
|
|
48
|
-
for src_file in "$src_dir"/*; do
|
|
49
|
-
[ -f "$src_file" ] || continue
|
|
50
|
-
name=$(basename "$src_file")
|
|
51
|
-
case "$dest_names" in
|
|
52
|
-
*" $name "*) continue ;;
|
|
53
|
-
esac
|
|
54
|
-
has_reason "$src_file" && continue
|
|
55
|
-
failures="$failures $label: ${src_file#"$PROJECT_ROOT/"} reaches no seed or config and carries no $MARKER reason"$'\n'
|
|
56
|
-
done
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
check_capability "Hooks" "$PROJECT_ROOT/.claude/hooks" \
|
|
60
|
-
"$PROJECT_ROOT"/tooling/claude/seeds/.claude/hooks/*
|
|
61
|
-
|
|
62
|
-
check_capability "Workflows" "$PROJECT_ROOT/.github/workflows" \
|
|
63
|
-
"$PROJECT_ROOT"/tooling/*/configs/.github/workflows/* \
|
|
64
|
-
"$PROJECT_ROOT"/tooling/*/seeds/.github/workflows/*
|
|
65
|
-
|
|
66
|
-
check_capability "Husky" "$PROJECT_ROOT/.husky" \
|
|
67
|
-
"$PROJECT_ROOT"/tooling/base/configs/.husky/*
|
|
68
|
-
|
|
69
|
-
# The reverse direction: a destination file whose source here is gone reaches
|
|
70
|
-
# neither check_capability above nor the wiring pass below, both of which read
|
|
71
|
-
# forward from the source, and it ships a target a hook, workflow, or husky
|
|
72
|
-
# script this repository has already deleted. A destination carrying the same
|
|
73
|
-
# marker clears just as a source does, which is the escape hatch a target-only
|
|
74
|
-
# capability needs, such as a stack-specific workflow with no root counterpart
|
|
75
|
-
# by design.
|
|
76
|
-
check_orphans() {
|
|
77
|
-
local label="$1" src_dir="$2" dest_dir="$3"
|
|
78
|
-
|
|
79
|
-
[ -d "$dest_dir" ] || return 0
|
|
80
|
-
|
|
81
|
-
local dest_file name
|
|
82
|
-
for dest_file in "$dest_dir"/*; do
|
|
83
|
-
[ -f "$dest_file" ] || continue
|
|
84
|
-
name=$(basename "$dest_file")
|
|
85
|
-
[ -f "$src_dir/$name" ] && continue
|
|
86
|
-
has_reason "$dest_file" && continue
|
|
87
|
-
failures="$failures $label: ${dest_file#"$PROJECT_ROOT/"} is seeded or configured with no source at ${src_dir#"$PROJECT_ROOT/"}/$name"$'\n'
|
|
88
|
-
done
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
check_orphans "Hooks" "$PROJECT_ROOT/.claude/hooks" \
|
|
92
|
-
"$PROJECT_ROOT/tooling/claude/seeds/.claude/hooks"
|
|
93
|
-
|
|
94
|
-
for dir in "$PROJECT_ROOT"/tooling/*/configs/.github/workflows \
|
|
95
|
-
"$PROJECT_ROOT"/tooling/*/seeds/.github/workflows; do
|
|
96
|
-
check_orphans "Workflows" "$PROJECT_ROOT/.github/workflows" "$dir"
|
|
97
|
-
done
|
|
98
|
-
|
|
99
|
-
check_orphans "Husky" "$PROJECT_ROOT/.husky" \
|
|
100
|
-
"$PROJECT_ROOT/tooling/base/configs/.husky"
|
|
101
|
-
|
|
102
|
-
# A hook that reached the seed tree with no wiring in the seeded settings.json
|
|
103
|
-
# is installed dead, which fails the same way an unseeded hook does.
|
|
104
|
-
SEED_HOOKS_DIR="$PROJECT_ROOT/tooling/claude/seeds/.claude/hooks"
|
|
105
|
-
SEED_SETTINGS="$PROJECT_ROOT/tooling/claude/seeds/.claude/settings.json"
|
|
106
|
-
|
|
107
|
-
if [ -d "$SEED_HOOKS_DIR" ]; then
|
|
108
|
-
if [ ! -f "$SEED_SETTINGS" ]; then
|
|
109
|
-
failures="$failures Seed settings: no settings.json at ${SEED_SETTINGS#"$PROJECT_ROOT/"} to confirm a seeded hook is wired"$'\n'
|
|
110
|
-
elif ! command -v jq >/dev/null 2>&1; then
|
|
111
|
-
failures="$failures Seed settings: jq is not installed, seeded-hook wiring unverifiable"$'\n'
|
|
112
|
-
else
|
|
113
|
-
# Every wired command as its own path segment set, rather than a substring
|
|
114
|
-
# search over the raw file, so a hook name that is a substring of another
|
|
115
|
-
# hook's filename (`log.sh` inside `pr-create-log.sh`) cannot pass on a
|
|
116
|
-
# neighbor's wiring.
|
|
117
|
-
wired=" $(jq -r '.. | .command? // empty' "$SEED_SETTINGS" | tr '/\n' ' ') "
|
|
118
|
-
for hook_file in "$SEED_HOOKS_DIR"/*.sh; do
|
|
119
|
-
[ -f "$hook_file" ] || continue
|
|
120
|
-
name=$(basename "$hook_file")
|
|
121
|
-
case "$wired" in
|
|
122
|
-
*" $name "*) continue ;;
|
|
123
|
-
esac
|
|
124
|
-
failures="$failures Seed settings: $name is seeded and wired into no command in ${SEED_SETTINGS#"$PROJECT_ROOT/"}"$'\n'
|
|
125
|
-
done
|
|
126
|
-
fi
|
|
127
|
-
fi
|
|
128
|
-
|
|
129
|
-
if [ -n "$failures" ]; then
|
|
130
|
-
echo "A capability reaches one side of the seed or config boundary and not the other:" >&2
|
|
131
|
-
printf '%s' "$failures" >&2
|
|
132
|
-
echo "Seed or configure the capability, or mark the source line with # $MARKER <reason>." >&2
|
|
133
|
-
exit 1
|
|
134
|
-
fi
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
7
|
-
|
|
8
|
-
GITIGNORE="$PROJECT_ROOT/.gitignore"
|
|
9
|
-
MANIFEST="$PROJECT_ROOT/tooling/claude/manifest.toml"
|
|
10
|
-
SECTION="# Claude"
|
|
11
|
-
|
|
12
|
-
# Parity is exact, with no exception list behind it, and the move to `.canon/`
|
|
13
|
-
# is what removed the one there was. Its two members each named a path inside
|
|
14
|
-
# `.claude/` that this repository ignored and the manifest withheld, so a target
|
|
15
|
-
# kept tracking it. A single root entry can withhold nothing inside itself: git
|
|
16
|
-
# does not descend into an excluded directory, so a re-inclusion like
|
|
17
|
-
# `!.canon/diagrams/` matches nothing. Retiring both is what buys the one line.
|
|
18
|
-
#
|
|
19
|
-
# The diagrams divergence is the one that cost something. A target used to track
|
|
20
|
-
# its diagrams and keep the context audit's default coverage of them, and it now
|
|
21
|
-
# ignores them with the rest of its records, so that coverage is gone unless the
|
|
22
|
-
# project names the folder itself. `canon/context/tooling/manifests.md` carries the
|
|
23
|
-
# narrative.
|
|
24
|
-
#
|
|
25
|
-
# An exception mechanism is not kept against a future divergence, because an
|
|
26
|
-
# empty one cannot be exercised and a check nobody can test is a check nobody
|
|
27
|
-
# should trust. It comes back with its first real member or not at all.
|
|
28
|
-
|
|
29
|
-
if [ ! -f "$GITIGNORE" ]; then
|
|
30
|
-
echo "No .gitignore at ${GITIGNORE#"$PROJECT_ROOT/"}, ignore parity unverifiable." >&2
|
|
31
|
-
exit 1
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
if [ ! -f "$MANIFEST" ]; then
|
|
35
|
-
echo "No claude manifest at ${MANIFEST#"$PROJECT_ROOT/"}, ignore parity unverifiable." >&2
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
# A gitignore pattern and a manifest entry describe the same folder whether or
|
|
40
|
-
# not either spells the trailing slash, so presence is compared with it dropped.
|
|
41
|
-
# No live pair differs that way now that both lists carry the same two entries,
|
|
42
|
-
# and the normalization stays because either side may be written without one.
|
|
43
|
-
normalize() {
|
|
44
|
-
printf '%s\n' "${1%/}"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# Every pattern the file carries, comments and blank lines dropped. The whole
|
|
48
|
-
# file rather than one header, since a claude-scoped entry filed under a header
|
|
49
|
-
# of its own would otherwise read as missing from a list that carries it.
|
|
50
|
-
gitignore_patterns() {
|
|
51
|
-
awk '{ sub(/[[:space:]]+$/, "") } $0 ~ /^[[:space:]]*#/ || $0 == "" { next } { print }' "$GITIGNORE"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# Every entry of the named array in the manifest's `[gitignore]` table. The
|
|
55
|
-
# array is accumulated to its closing bracket before the quoted strings are read
|
|
56
|
-
# out, so a formatter wrapping it across lines does not silently empty this.
|
|
57
|
-
manifest_entries() {
|
|
58
|
-
awk -v key="\"$SECTION\"" '
|
|
59
|
-
/^\[/ { table = $0 }
|
|
60
|
-
table != "[gitignore]" { next }
|
|
61
|
-
index($0, key) == 1 { collecting = 1; buf = "" }
|
|
62
|
-
collecting { buf = buf $0 }
|
|
63
|
-
collecting && index($0, "]") > 0 {
|
|
64
|
-
collecting = 0
|
|
65
|
-
buf = substr(buf, index(buf, "["))
|
|
66
|
-
count = split(buf, parts, "\"")
|
|
67
|
-
for (i = 2; i <= count; i += 2) print parts[i]
|
|
68
|
-
}
|
|
69
|
-
' "$MANIFEST"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
contains() {
|
|
73
|
-
local needle=$1
|
|
74
|
-
shift
|
|
75
|
-
local candidate
|
|
76
|
-
for candidate in "$@"; do
|
|
77
|
-
[ "$candidate" = "$needle" ] || continue
|
|
78
|
-
return 0
|
|
79
|
-
done
|
|
80
|
-
return 1
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
ignored=()
|
|
84
|
-
while IFS= read -r pattern; do
|
|
85
|
-
[ -n "$pattern" ] || continue
|
|
86
|
-
ignored+=("$(normalize "$pattern")")
|
|
87
|
-
done < <(gitignore_patterns)
|
|
88
|
-
|
|
89
|
-
shipped=()
|
|
90
|
-
while IFS= read -r entry; do
|
|
91
|
-
[ -n "$entry" ] || continue
|
|
92
|
-
shipped+=("$(normalize "$entry")")
|
|
93
|
-
done < <(manifest_entries)
|
|
94
|
-
|
|
95
|
-
# An empty read on either side is a parse that failed rather than a repository
|
|
96
|
-
# ignoring nothing, and reporting parity off it would say the two lists agree
|
|
97
|
-
# having compared none of their entries.
|
|
98
|
-
if [ ${#ignored[@]} -eq 0 ]; then
|
|
99
|
-
echo "No patterns read from .gitignore, ignore parity unverifiable." >&2
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
if [ ${#shipped[@]} -eq 0 ]; then
|
|
104
|
-
echo "No entries read from the \"$SECTION\" array in ${MANIFEST#"$PROJECT_ROOT/"}, ignore parity unverifiable." >&2
|
|
105
|
-
exit 1
|
|
106
|
-
fi
|
|
107
|
-
|
|
108
|
-
failures=""
|
|
109
|
-
|
|
110
|
-
# Entries the manifest ships that this repository does not ignore. A target is
|
|
111
|
-
# told to ignore a folder the toolkit itself tracks, which no decision sanctions,
|
|
112
|
-
# so this direction carries no exception list.
|
|
113
|
-
for entry in "${shipped[@]}"; do
|
|
114
|
-
contains "$entry" "${ignored[@]}" && continue
|
|
115
|
-
failures="$failures $entry is shipped by the manifest and absent from .gitignore"$'\n'
|
|
116
|
-
done
|
|
117
|
-
|
|
118
|
-
# Claude-scoped patterns this repository ignores that the manifest does not
|
|
119
|
-
# ship. Scoped to the two record roots because the manifest is the claude stack
|
|
120
|
-
# and says nothing about `node_modules/` or `.env`.
|
|
121
|
-
#
|
|
122
|
-
# `.canon` is read as a bare root as well as a prefix, since it is one line
|
|
123
|
-
# covering a whole tree where `.claude/` names a folder inside a root that also
|
|
124
|
-
# holds tracked content. Leaving it out is what would let a new entry sit outside
|
|
125
|
-
# the only stage comparing the two lists, which is the direction that goes
|
|
126
|
-
# silently blind.
|
|
127
|
-
for pattern in "${ignored[@]}"; do
|
|
128
|
-
case "$pattern" in
|
|
129
|
-
.claude/* | .canon | .canon/*) ;;
|
|
130
|
-
*) continue ;;
|
|
131
|
-
esac
|
|
132
|
-
contains "$pattern" "${shipped[@]}" && continue
|
|
133
|
-
failures="$failures $pattern is ignored here and absent from the manifest"$'\n'
|
|
134
|
-
done
|
|
135
|
-
|
|
136
|
-
if [ -n "$failures" ]; then
|
|
137
|
-
echo "The ignore set a target receives disagrees with this repository's own:" >&2
|
|
138
|
-
printf '%s' "$failures" >&2
|
|
139
|
-
echo "Add the entry to the \"$SECTION\" array in ${MANIFEST#"$PROJECT_ROOT/"} and to .gitignore. The two lists are compared exactly, and nothing here records an exception." >&2
|
|
140
|
-
exit 1
|
|
141
|
-
fi
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
7
|
-
|
|
8
|
-
INTERNAL_ROOT="$PROJECT_ROOT/internal"
|
|
9
|
-
PLUGIN_ROOT="$PROJECT_ROOT/claude"
|
|
10
|
-
|
|
11
|
-
# `find` writes to stderr and returns non-zero for a missing root, inside a
|
|
12
|
-
# process substitution whose status nothing reads. Without this the walk covers
|
|
13
|
-
# nothing and the check reports the boundary clean having never measured it.
|
|
14
|
-
if [ ! -d "$PLUGIN_ROOT" ]; then
|
|
15
|
-
echo "No plugin root at ${PLUGIN_ROOT#"$PROJECT_ROOT/"}, boundary unverifiable." >&2
|
|
16
|
-
exit 1
|
|
17
|
-
fi
|
|
18
|
-
|
|
19
|
-
# `realpath` resolves each walked path below. Absent, `set -e` aborts on the
|
|
20
|
-
# substitution and the stage reports a leak for a tool that is not installed,
|
|
21
|
-
# which is the same unmeasured-tree-with-a-verdict shape the guard above closes.
|
|
22
|
-
if ! command -v realpath >/dev/null 2>&1; then
|
|
23
|
-
echo "realpath is not installed, boundary unverifiable." >&2
|
|
24
|
-
exit 1
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
# The plugin reaches `standards/` through a symlink, which an
|
|
28
|
-
# installer dereferences with no code in the path to filter. Walking the plugin
|
|
29
|
-
# tree with symlinks followed is what an install actually copies, so resolving
|
|
30
|
-
# each file and rejecting anything under `internal/` asserts the boundary at the
|
|
31
|
-
# only place it can be measured.
|
|
32
|
-
leaked=""
|
|
33
|
-
while IFS= read -r file; do
|
|
34
|
-
resolved=$(realpath "$file")
|
|
35
|
-
case "$resolved" in
|
|
36
|
-
"$INTERNAL_ROOT"/*) leaked="$leaked ${file#"$PROJECT_ROOT/"} -> ${resolved#"$PROJECT_ROOT/"}"$'\n' ;;
|
|
37
|
-
esac
|
|
38
|
-
done < <(find -L "$PLUGIN_ROOT" -type f)
|
|
39
|
-
|
|
40
|
-
if [ -n "$leaked" ]; then
|
|
41
|
-
echo "Plugin ships toolkit-internal content:" >&2
|
|
42
|
-
printf '%s' "$leaked" >&2
|
|
43
|
-
echo "Author internal content under internal/, which nothing under claude/ reaches." >&2
|
|
44
|
-
exit 1
|
|
45
|
-
fi
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
7
|
-
|
|
8
|
-
source "$PROJECT_ROOT/scripts/lib/tooling.sh"
|
|
9
|
-
|
|
10
|
-
TOOLKIT_TOKEN="canon"
|
|
11
|
-
TOOLING_ROOT="$PROJECT_ROOT/tooling"
|
|
12
|
-
|
|
13
|
-
# `find` writes to stderr and returns non-zero for a missing root, inside a
|
|
14
|
-
# process substitution whose status nothing reads. Without this the walk covers
|
|
15
|
-
# nothing and the check reports the seeds independent having never read one.
|
|
16
|
-
if [ ! -d "$TOOLING_ROOT" ]; then
|
|
17
|
-
echo "No tooling root at ${TOOLING_ROOT#"$PROJECT_ROOT/"}, seed independence unverifiable." >&2
|
|
18
|
-
exit 1
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
# An empty discovery is a real state rather than a broken walk, and the Seed
|
|
22
|
-
# standards stage already reports it as a skip. Failing here would have the two
|
|
23
|
-
# stages disagree about what the same condition means.
|
|
24
|
-
seed_roots=$(collect_seed_roots)
|
|
25
|
-
if [ -z "$seed_roots" ]; then
|
|
26
|
-
echo "No seed root carries .claude/ or canon/, nothing to check." >&2
|
|
27
|
-
exit 0
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
# Markdown alone. The seed tree also ships hooks that call the toolkit CLI on
|
|
31
|
-
# purpose, each reporting a named stale-index warning when the binary is absent,
|
|
32
|
-
# and scoping by extension leaves them outside this walk without an exemption
|
|
33
|
-
# list that would have to be maintained against them.
|
|
34
|
-
cited=""
|
|
35
|
-
measured=0
|
|
36
|
-
while IFS= read -r seed_root; do
|
|
37
|
-
while IFS= read -r file; do
|
|
38
|
-
measured=$((measured + 1))
|
|
39
|
-
# A token followed by `/` is the tracked surface root, a folder the seed
|
|
40
|
-
# itself installs, rather than the binary. Every other spelling still
|
|
41
|
-
# reports, which keeps a verb, a plugin skill prefix, and a bare mention in.
|
|
42
|
-
while IFS= read -r hit; do
|
|
43
|
-
cited="$cited ${file#"$PROJECT_ROOT/"}:$hit"$'\n'
|
|
44
|
-
done < <(grep -nE "${TOOLKIT_TOKEN}([^/]|$)" "$file" || true)
|
|
45
|
-
done < <(find "$PROJECT_ROOT/$seed_root" -type f -name '*.md')
|
|
46
|
-
done <<<"$seed_roots"
|
|
47
|
-
|
|
48
|
-
# Roots resolved and no markdown under any of them is a walk that covered
|
|
49
|
-
# nothing, which is the verdict-without-a-measurement the guards above refuse for
|
|
50
|
-
# a missing tree. Reporting a pass here would say the seeds cite no CLI on the
|
|
51
|
-
# strength of having read no prose.
|
|
52
|
-
if [ "$measured" -eq 0 ]; then
|
|
53
|
-
echo "Seed roots resolved but carry no markdown, seed independence unverifiable." >&2
|
|
54
|
-
exit 1
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
if [ -n "$cited" ]; then
|
|
58
|
-
echo "Seed prose cites the toolkit CLI:" >&2
|
|
59
|
-
printf '%s' "$cited" >&2
|
|
60
|
-
echo "A scaffolded project may not have $TOOLKIT_TOKEN installed. State the capability the line needs rather than the binary that supplies it." >&2
|
|
61
|
-
exit 1
|
|
62
|
-
fi
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -e
|
|
3
|
-
set -o pipefail
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
7
|
-
|
|
8
|
-
SKILLS_DIR="$PROJECT_ROOT/claude/skills"
|
|
9
|
-
|
|
10
|
-
# A shipped skill runs from a target project, where the toolkit's own wiki/ does
|
|
11
|
-
# not exist. Anchoring on a non-path character keeps a target's .claude/wiki/ legal.
|
|
12
|
-
BANNED_PATH_PATTERN='(^|[^[:alnum:]._/-])wiki/'
|
|
13
|
-
|
|
14
|
-
[ -d "$SKILLS_DIR" ] || exit 0
|
|
15
|
-
|
|
16
|
-
matches=$(grep -rnE "$BANNED_PATH_PATTERN" "$SKILLS_DIR" || true)
|
|
17
|
-
|
|
18
|
-
[ -z "$matches" ] && exit 0
|
|
19
|
-
|
|
20
|
-
echo "Shipped skills reference a repo-local path that does not exist in a target project:"
|
|
21
|
-
echo "$matches" | sed "s|^$PROJECT_ROOT/||"
|
|
22
|
-
echo
|
|
23
|
-
echo "Reach supporting prose through a canon docs command, a standard cited at the flat root, or inlined text."
|
|
24
|
-
exit 1
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Prints every seed root carrying a `.claude/` or a `canon/`, one per line and
|
|
3
|
-
# relative to the project root.
|
|
4
|
-
#
|
|
5
|
-
# The discovery rule has one definition, `collect_seed_roots` in
|
|
6
|
-
# `scripts/lib/tooling.sh`, which `check-seed-independence.sh` already reads.
|
|
7
|
-
# This file is the route a caller outside bash takes to that same answer, so a
|
|
8
|
-
# stack seeding either root later reaches both readers and the two stages
|
|
9
|
-
# measuring seed content cannot disagree about which roots exist.
|
|
10
|
-
set -e
|
|
11
|
-
set -o pipefail
|
|
12
|
-
|
|
13
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
15
|
-
|
|
16
|
-
source "$PROJECT_ROOT/scripts/lib/tooling.sh"
|
|
17
|
-
|
|
18
|
-
collect_seed_roots
|
package/scripts/lib/tooling.sh
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
TOOLING_STACK_EXCLUDE=("claude")
|
|
4
|
-
|
|
5
|
-
is_tooling_stack_excluded() {
|
|
6
|
-
local name="$1"
|
|
7
|
-
local excluded
|
|
8
|
-
for excluded in "${TOOLING_STACK_EXCLUDE[@]}"; do
|
|
9
|
-
if [ "$name" = "$excluded" ]; then
|
|
10
|
-
return 0
|
|
11
|
-
fi
|
|
12
|
-
done
|
|
13
|
-
return 1
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
list_tooling_stacks() {
|
|
17
|
-
local tooling_dir="${1:-$PROJECT_ROOT/tooling}"
|
|
18
|
-
local name
|
|
19
|
-
while IFS= read -r entry; do
|
|
20
|
-
name=$(basename "$entry")
|
|
21
|
-
is_tooling_stack_excluded "$name" && continue
|
|
22
|
-
echo "$name"
|
|
23
|
-
done < <(find "$tooling_dir" -mindepth 1 -maxdepth 1 -type d | sort)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
# Seed roots that carry a `.claude/` or a `canon/`, emitted relative to
|
|
27
|
-
# `PROJECT_ROOT`. Every stage measuring seed content discovers through this
|
|
28
|
-
# rather than naming a stack, so a stack seeding either root later arrives
|
|
29
|
-
# covered with no edit to any caller. A stack seeding only tracked surfaces
|
|
30
|
-
# carries `canon/` alone, which a `.claude/` test would drop from both stages.
|
|
31
|
-
collect_seed_roots() {
|
|
32
|
-
local dir
|
|
33
|
-
for dir in "$PROJECT_ROOT"/tooling/*/seeds; do
|
|
34
|
-
[ -d "$dir/.claude" ] || [ -d "$dir/canon" ] || continue
|
|
35
|
-
printf '%s\n' "${dir#"$PROJECT_ROOT"/}"
|
|
36
|
-
done
|
|
37
|
-
}
|