@erclx/canon 5.10.0 → 5.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  3. package/claude/skills/auto-ship/SKILL.md +5 -8
  4. package/claude/skills/auto-ship/references/failure-recovery.md +0 -1
  5. package/claude/skills/auto-ship/references/ui-checklist.md +21 -0
  6. package/claude/skills/git-pr/references/evidence.md +7 -7
  7. package/claude/skills/git-ship/REQUIREMENT.md +1 -1
  8. package/claude/skills/git-ship/SKILL.md +1 -1
  9. package/claude/skills/review-craft/references/ui.md +1 -2
  10. package/claude/skills/review-pr/SKILL.md +1 -1
  11. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  12. package/claude/skills/ui-checklist/SKILL.md +1 -18
  13. package/docs/agents/gate.md +1 -1
  14. package/docs/agents/hooks.md +2 -0
  15. package/docs/agents/index.md +2 -2
  16. package/docs/agents/install-and-sync.md +2 -2
  17. package/docs/agents/pr-evidence.md +70 -22
  18. package/docs/agents/tasks-validate.md +3 -3
  19. package/docs/agents/worktrees.md +16 -1
  20. package/docs/target/migrations.md +1 -1
  21. package/docs/workflow/ai-workflow.md +2 -1
  22. package/docs/workflow/parallel-features.md +3 -3
  23. package/docs/workflow/skill-map.md +1 -1
  24. package/package.json +3 -4
  25. package/scripts/lib/sandbox-path.sh +1 -1
  26. package/src/commands/gate.ts +3 -2
  27. package/src/commands/pr.ts +93 -19
  28. package/src/commands/worktrees.ts +37 -0
  29. package/src/gate/sequencer.ts +0 -20
  30. package/src/gate/stages.ts +10 -3
  31. package/src/pr/evidence.ts +35 -0
  32. package/src/tasks/validate.ts +37 -27
  33. package/src/worktrees/bare-flag.ts +83 -0
  34. package/standards/tasks.md +1 -1
  35. package/tooling/base/configs/.husky/post-merge +30 -77
  36. package/tooling/base/configs/.husky/post-rewrite +3 -3
  37. package/tooling/base/reference.md +1 -1
  38. package/scripts/core/clean.sh +0 -36
  39. package/scripts/core/regen-agent-fixture.sh +0 -235
  40. package/scripts/core/regen-claude-copies.sh +0 -10
  41. package/scripts/core/regen-indexes.sh +0 -10
  42. package/scripts/core/repair-bare-flag.sh +0 -19
  43. package/scripts/core/snapshot.sh +0 -134
  44. package/scripts/core/update.sh +0 -35
  45. package/scripts/lib/worktree.sh +0 -20
@@ -1,92 +1,45 @@
1
1
  #!/usr/bin/env sh
2
2
 
3
- # Names archive candidates after a pull merges work in.
3
+ # Runs the post-merge steps through `canon hooks post-merge`: archive the task a
4
+ # merged pull request closed, push the records, reclaim merged worktrees,
5
+ # reinstall the CLI, and update the plugin cache. The verb owns the order and
6
+ # each line it prints.
4
7
  #
5
- # Every step before the merge fires from a chain and the archive fires when a
6
- # person remembers, so shipped tasks accumulate on a board that reads as ready
7
- # to hand a worker. This is the only event that lands after a merge.
8
+ # CANON_SKIP_RECLAIM=1 turns off the worktree reclaim.
9
+ # CANON_SKIP_UPGRADE=1 turns off the CLI reinstall.
10
+ # CANON_SKIP_PLUGIN_UPDATE=1 turns off the plugin cache update.
8
11
  #
9
- # Candidates rather than closed tasks, because an all-`[x]` board entry does not
10
- # mean the work merged. `context-fold` marks outcomes on the branch as step 1 of
11
- # the ship chain, so a task can read closed while its pull request is still open
12
- # or already abandoned. The board is gitignored and an archived task has nothing
13
- # to recover it, so `task-board` confirms the work is on `main` before moving
14
- # anything and this output says so rather than implying the check is done.
12
+ # The reclaim reads merge state through `gh`, so a machine without it on PATH
13
+ # reclaims nothing and says nothing. Install `gh` to turn it on.
15
14
  #
16
- # It announces and never moves a file. The board is gitignored, so a shell-side
17
- # archive leaves no diff to review with nobody watching, and `index.md` is
18
- # regenerated by a session-scoped hook that a `mv` from a shell never fires.
19
- #
20
- # Husky runs this as `sh -e`, so the shebang is advisory and any bare command
21
- # returning non-zero aborts the hook and prints a husky failure. Keep every
22
- # test inside an `if` condition, which errexit exempts.
15
+ # Husky runs this as `sh -e`, and `post-rewrite` sources it on a rebase, so
16
+ # every test sits inside an `if` or an `||` and nothing here exits non-zero.
23
17
 
24
18
  root=$(git worktree list --porcelain 2>/dev/null | grep -m 1 '^worktree ' | cut -d' ' -f2-) || root=''
25
19
  [ -n "$root" ] || exit 0
26
20
 
27
21
  # The board is shared scratch at the main worktree root, and `git worktree
28
- # list` puts that root first. A pull inside a linked worktree fires this same
29
- # hook, so trusting the working directory would read a board that is not there.
30
- board="$root/.canon/tasks"
31
- [ -d "$board" ] || exit 0
32
-
33
- closed=""
34
- count=0
35
-
36
- for file in "$board"/*.md; do
37
- [ -f "$file" ] || continue
22
+ # list` puts that root first. A checkout with no board runs none of the steps.
23
+ [ -d "$root/.canon/tasks" ] || exit 0
38
24
 
39
- case "$file" in
40
- */index.md | */priority.md) continue ;;
41
- esac
42
-
43
- # Gate on the same condition both archive verbs gate on: at least one closed
44
- # outcome and none open. A file carrying no outcomes at all falls out here,
45
- # which is what keeps notes on the board from being announced as tasks.
46
- if grep -q '^- \[ \]' "$file"; then
47
- continue
48
- fi
25
+ if ! command -v canon >/dev/null 2>&1; then
26
+ exit 0
27
+ fi
49
28
 
50
- if grep -q '^- \[x\]' "$file"; then
51
- closed="$closed $(basename "$file" .md)
52
- "
53
- count=$((count + 1))
29
+ # A global binary older than the verb cannot run it, and the verb carries the
30
+ # only step that would update it, so reinstall here and probe again. The probe
31
+ # reads the verb's own usage line rather than the exit, since an older binary
32
+ # answers an unknown command's `--help` with its root help and exits zero.
33
+ has_verb() {
34
+ canon hooks post-merge --help 2>/dev/null | grep -q '^Usage: canon hooks post-merge'
35
+ }
36
+
37
+ if ! has_verb; then
38
+ if [ -z "$CANON_SKIP_UPGRADE" ]; then
39
+ CANON_NON_INTERACTIVE=1 canon upgrade >/dev/null 2>&1 || true
54
40
  fi
55
- done
56
-
57
- [ "$count" -gt 0 ] || exit 0
58
-
59
- printf '\n📋 %s archive candidate(s) on the board:\n%s\n' "$count" "$closed"
60
- printf 'Outcomes are marked on the branch, so each still needs its work confirmed\n'
61
- printf 'on main. Run /task-board, which checks that and sweeps the plan first.\n\n'
62
-
63
- # The gitignored record folders live on one disk and nowhere else, and a merge
64
- # is the event that changes them most. This runs on every merge rather than
65
- # only on one that closes a task, since a review report and a memory entry
66
- # both land on runs that archive nothing.
67
- #
68
- # Sits last so a slow or unreachable remote delays no candidate announcement,
69
- # and inside an `if` so a push failing offline, or a machine that never ran
70
- # the one-time setup, still leaves this hook exiting zero. Guarded by its own
71
- # `command -v canon` check, since this file carries none above it.
72
- if command -v canon >/dev/null 2>&1; then
73
- if backup=$(canon records push --root "$root" --json 2>/dev/null); then
74
- changed=$(printf '%s' "$backup" | sed -n 's/.*"changed":\([0-9][0-9]*\).*/\1/p')
75
- if [ -n "$changed" ] && [ "$changed" != "0" ]; then
76
- printf '\n🗄️ Backed up %s record path(s).\n\n' "$changed"
77
- fi
78
- else
79
- reason=$(printf '%s' "$backup" | sed -n 's/.*"reason":"\([^"]*\)".*/\1/p')
41
+ fi
80
42
 
81
- # No records history on this machine, which is every checkout that never
82
- # ran the one-time setup. Nothing to report.
83
- if [ "$reason" != "no-repository" ]; then
84
- printf '\n🗄️ Records not backed up: %s\n' "${reason:-unknown}"
85
- if [ "$reason" = "unsafe-payload" ]; then
86
- printf 'Run canon records push to see the blocked paths.\n\n'
87
- else
88
- printf 'Run canon records push when the remote is reachable.\n\n'
89
- fi
90
- fi
91
- fi
43
+ if has_verb; then
44
+ canon hooks post-merge --root "$root" || true
92
45
  fi
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env sh
2
2
 
3
- # Runs the `post-merge` board check for anyone pulling with rebase.
3
+ # Runs the `post-merge` steps for anyone pulling with rebase.
4
4
  #
5
5
  # `git pull` under `pull.rebase=true` runs `git rebase`, which fires this event
6
6
  # and never `post-merge`. Without this the trigger is a silent no-op on that
@@ -9,8 +9,8 @@
9
9
  # target, where the pull style is not something this repository controls.
10
10
  #
11
11
  # Only the `rebase` argument delegates. `post-rewrite` also fires on
12
- # `commit --amend`, which rewrites nothing on the board and would print the
13
- # candidate list on an ordinary amend.
12
+ # `commit --amend`, which rewrites nothing on the board and would run the
13
+ # archive, reclaim, and upgrade steps on an ordinary amend.
14
14
 
15
15
  [ "$1" = "rebase" ] || exit 0
16
16
 
@@ -52,7 +52,7 @@ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and n
52
52
  - `pre-commit` runs `bunx lint-staged`.
53
53
  - `commit-msg` runs `bunx commitlint --edit "$1"`.
54
54
  - `pre-push` runs `bun run check`.
55
- - `post-merge` names `.canon/tasks/` archive candidates, staying silent otherwise and when the board is absent.
55
+ - `post-merge` runs `canon hooks post-merge`: archive the task a merged pull request closed, push the records, reclaim merged worktrees, reinstall the CLI, and update the plugin. It does nothing when the board is absent. `CANON_SKIP_RECLAIM`, `CANON_SKIP_UPGRADE`, and `CANON_SKIP_PLUGIN_UPDATE` turn off the three steps that act on the machine. Read `canon docs hooks` for the contract.
56
56
  - `post-rewrite` delegates to `post-merge` on `rebase`, so a `pull.rebase=true` machine still gets the check.
57
57
 
58
58
  ## lint-staged
@@ -1,36 +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/ui.sh"
9
-
10
- check_dependencies() {
11
- command -v bun >/dev/null 2>&1 || log_error "bun is not installed"
12
- }
13
-
14
- main() {
15
- check_dependencies
16
-
17
- echo -e "${GREY}┌${NC}"
18
-
19
- echo -e "${GREY}├${NC} ${WHITE}Cleaning artifacts${NC}"
20
- rm -rf node_modules
21
-
22
- log_rem "node_modules/"
23
-
24
- log_step "Cleaning cache"
25
- bun pm cache rm
26
- log_info "Package manager cache cleared"
27
-
28
- log_step "Rehydrating project"
29
- bun install
30
- log_info "Dependencies installed"
31
-
32
- echo -e "${GREY}└${NC}\n"
33
- echo -e "${GREEN}✓ Clean complete${NC}"
34
- }
35
-
36
- main "$@"
@@ -1,235 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Writes web/src/fixtures/agent-view.json from a real `canon sessions list
3
- # --json` read of this repository's own sessions.
4
- #
5
- # The page it feeds re-creates an orchestrator dispatching workers, which is the
6
- # one thing this toolkit does that no other surface here shows. A re-creation
7
- # earns nothing if the rows behind it are invented, so the session half is a
8
- # live listing and never a literal.
9
- #
10
- # Every role in the roster appears, not the workers alone. The controlling
11
- # session is pinned above the bands and planners sit in them beside workers,
12
- # which is what the surface being re-created does. An earlier version of this
13
- # script filtered the listing down to sessions holding a feature branch, and
14
- # that dropped the orchestrator and every planner, since a controlling session
15
- # and a planning session both sit on the default branch. A page about a dispatch
16
- # that excludes the dispatcher is the defect that filter shipped.
17
- #
18
- # Two fields cannot come from the listing and are transcribed by hand into the
19
- # table below. The activity text is written by Claude Code into its own status
20
- # line and no verb here reports it. The pull request number sits on a task file
21
- # with nothing joining a session record to it, which the plan behind this script
22
- # took as a trade rather than building the join. Both are recorded as
23
- # transcribed in the fixture's own header, so a reader of the committed file can
24
- # tell which half was measured and which was typed.
25
- #
26
- # The honesty cost this leaves open is the one assets/captures/install.html.tmpl
27
- # already documents for the terminal frames: a hand-edit after generation
28
- # defeats the discipline silently, and nothing here detects one.
29
- #
30
- # Clone-only. It reads this repository's own sessions, and a registry install
31
- # carries no such listing.
32
- set -e
33
- set -o pipefail
34
-
35
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
- PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
37
-
38
- OUT="$PROJECT_ROOT/web/src/fixtures/agent-view.json"
39
-
40
- # Transcribed half, keyed by session name. The name is the only key that reaches
41
- # every row: a controlling session and a planning session both sit on the
42
- # default branch, so a branch cannot tell them apart or from each other. A
43
- # dispatched session is named `<role>-<project>-<slug>` after the work it was
44
- # given rather than after what it turned out to be doing, which is what makes
45
- # the name stable enough to key on here.
46
- #
47
- # Each row is `name|activity|pullRequest|state|age`. The state is `pinned`,
48
- # `working`, or `completed`, and it is where the row renders. An empty pull
49
- # request field is a session that had not reached its ship step when the
50
- # snapshot was taken.
51
- #
52
- # A pinned or working row has to appear in the live listing or this refuses, and
53
- # its age comes from the listing rather than the table, so the trailing field is
54
- # empty for those. A completed row names a session that has already exited, so
55
- # no listing carries it, its age is transcribed with the rest of the row, and
56
- # leaving that field blank would print an empty column on half the list.
57
- # Re-transcribe this table against a fresh listing rather than editing the
58
- # generated file.
59
- #
60
- # A planner belongs in this table as much as a worker does. `role-planner`
61
- # forbids entering a worktree, so a planner registers on the default branch at
62
- # launch and stays there for its whole life, which is why the branch filter this
63
- # script used to carry dropped every one of them permanently rather than
64
- # occasionally.
65
- ROWS=(
66
- "orchestrator-canon-lead|dispatching the planners that need a plan||pinned|"
67
- "worker-canon-agent-view-and-deploy|PR #1510 open (landing page)|1510|working|" # canon-allow-reference: transcribed status text
68
- "planner-canon-context-wireframe-draft|drafting the context and wireframe plan||working|"
69
- "planner-canon-skill-coverage-pointers|reading the skill coverage pointers||working|"
70
- "planner-canon-screenshot-trap-guard|planning the screenshot trap guard||working|"
71
- "planner-canon-write-route-hook-bypass|plan written; tested write probe||completed|32m"
72
- "planner-canon-indexes-list-lookup|plan and task board consistent||completed|13m"
73
- "worker-canon-labels-scan-body-file|address-review pass finished|1508|completed|21m"
74
- "worker-canon-web-context-entry|PR #1507 addressed with commit|1507|completed|20m" # canon-allow-reference: transcribed status text
75
- )
76
-
77
- # The listing reports a session's repository as the main `.git` directory, which
78
- # is what this resolves whether the script runs from the main checkout or from a
79
- # linked worktree. Comparing against PROJECT_ROOT instead would match nothing
80
- # from a worktree, where the two differ.
81
- MAIN_GIT_DIR="$(cd "$PROJECT_ROOT" && git rev-parse --path-format=absolute --git-common-dir)"
82
-
83
- # `bun src/cli.ts` rather than `canon`, since a globally linked binary resolves
84
- # to the main checkout no matter which worktree is running, and this script is
85
- # run from a worktree as often as not.
86
- SESSIONS_JSON="$(cd "$PROJECT_ROOT" && CANON_NON_INTERACTIVE=1 bun src/cli.ts sessions list --json 2>/dev/null)"
87
-
88
- if [ -z "$SESSIONS_JSON" ]; then
89
- echo "regen-agent-fixture: the session listing returned nothing, refusing to write an empty fixture" >&2
90
- exit 1
91
- fi
92
-
93
- # The listing crosses into the eval as a file rather than an environment entry,
94
- # since Linux caps a single env string at 128KB and a machine running many
95
- # sessions can cross it, which fails the exec with E2BIG before any check can
96
- # report a thing.
97
- SESSIONS_FILE="$(mktemp)"
98
- trap 'rm -f "$SESSIONS_FILE"' EXIT
99
- printf '%s' "$SESSIONS_JSON" >"$SESSIONS_FILE"
100
-
101
- TRANSCRIBED_ROWS="$(printf '%s\n' "${ROWS[@]}")"
102
- READ_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
103
-
104
- export SESSIONS_FILE OUT PROJECT_ROOT READ_AT TRANSCRIBED_ROWS MAIN_GIT_DIR
105
-
106
- bun --eval '
107
- const { readFileSync } = require("node:fs")
108
-
109
- const { SESSIONS_FILE, OUT, PROJECT_ROOT, READ_AT, TRANSCRIBED_ROWS, MAIN_GIT_DIR } =
110
- process.env
111
-
112
- const listing = JSON.parse(readFileSync(SESSIONS_FILE, "utf8"))
113
-
114
- const rows = TRANSCRIBED_ROWS.split("\n")
115
- .map((line) => line.replace(/\s*#\s*canon-allow-reference:.*$/, "").trim())
116
- .filter((line) => line !== "")
117
- .map((line) => {
118
- const [name, activity, pullRequest, state, age] = line.split("|")
119
- return {
120
- name,
121
- activity,
122
- pullRequest: pullRequest ? Number(pullRequest) : null,
123
- state,
124
- age: age || null,
125
- }
126
- })
127
-
128
- // Narrowed to this repository, and the reason is disclosure rather than scope.
129
- // `canon sessions list` is machine-wide, so an unfiltered read carries the
130
- // session names and worktree paths of every other project on the machine, which
131
- // here includes paths under a folder named `private` and several client names.
132
- // This fixture renders on a public page, so the filter is what keeps them off
133
- // it. The cost is a fidelity gap stated plainly: the real surface is
134
- // machine-wide and the depiction is one repository, and that is a deliberate
135
- // narrowing rather than an oversight.
136
- //
137
- // Keyed by name for the reason the shell comment gives: a session on the
138
- // default branch has no other identity, and the orchestrator and every planner
139
- // sit there.
140
- const live = new Map(
141
- listing.sessions
142
- .filter((session) => session.repository === MAIN_GIT_DIR)
143
- .map((session) => [session.name, session]),
144
- )
145
-
146
- // The real surface shows an age per row, so the fixture carries one. It is
147
- // frozen at the read rather than live, which the rendered provenance line dates
148
- // so a reader is never told a stale figure is current.
149
- const ageLabel = (startedAt) => {
150
- const minutes = Math.round(
151
- (Date.parse(READ_AT) - Date.parse(startedAt)) / 60000,
152
- )
153
- if (!Number.isFinite(minutes) || minutes < 1) return "now"
154
- if (minutes < 60) return `${minutes}m`
155
- const hours = Math.round(minutes / 60)
156
- return hours < 24 ? `${hours}h` : `${Math.round(hours / 24)}d`
157
- }
158
-
159
- // A row the page shows as running has to be running. Transcribing one that is
160
- // not is the failure the live half exists to prevent, so it refuses rather than
161
- // rendering.
162
- const absent = rows.filter(
163
- (row) => row.state !== "completed" && !live.has(row.name),
164
- )
165
- if (absent.length > 0) {
166
- console.error(
167
- "regen-agent-fixture: no live session for " +
168
- absent.map((row) => row.name).join(", ") +
169
- ". Re-transcribe the table in this script against a fresh listing rather than editing the generated fixture.",
170
- )
171
- process.exit(1)
172
- }
173
-
174
- const sessions = rows.map((row) => {
175
- const session = live.get(row.name)
176
- return {
177
- name: row.name,
178
- branch: session ? session.branch : null,
179
- state: row.state,
180
- activity: row.activity,
181
- pullRequest: row.pullRequest,
182
- age: session ? ageLabel(session.startedAt) : row.age,
183
- }
184
- })
185
-
186
- const ageless = sessions.filter((entry) => !entry.age)
187
- if (ageless.length > 0) {
188
- console.error(
189
- "regen-agent-fixture: no age for " +
190
- ageless.map((entry) => entry.name).join(", ") +
191
- ". A completed row carries its age in the table, since the session has exited and no listing reports one.",
192
- )
193
- process.exit(1)
194
- }
195
-
196
- // What the real surface prints above its own list. The remainder is every
197
- // session on the machine this page has no room to show, which is a real count.
198
- const summary = {
199
- working: sessions.filter((entry) => entry.state === "working").length,
200
- completed: sessions.filter((entry) => entry.state === "completed").length,
201
- more: Math.max(0, listing.sessions.length - live.size),
202
- }
203
-
204
- const fixture = {
205
- // Read by web/src/components/agent-view.astro. Every field below is either a
206
- // live read or a hand transcription, and this header is what says which.
207
- generatedBy: "scripts/core/regen-agent-fixture.sh",
208
- readAt: READ_AT,
209
- source: {
210
- command: "canon sessions list --json",
211
- live: [
212
- "name",
213
- "branch",
214
- "age",
215
- "that every pinned and working row is running",
216
- "the remainder count",
217
- ],
218
- transcribed: {
219
- activity:
220
- "written by Claude Code into its own status line, which no verb here reports",
221
- pullRequest:
222
- "read off a task file by hand, since nothing joins a session record to one",
223
- state:
224
- "a completed session has already exited, so no listing carries the row at all",
225
- },
226
- },
227
- summary,
228
- sessions,
229
- }
230
-
231
- await Bun.write(OUT, JSON.stringify(fixture, null, 2) + "\n")
232
- console.error(
233
- `regen-agent-fixture: wrote ${sessions.length} session rows to ${OUT.replace(PROJECT_ROOT + "/", "")}`,
234
- )
235
- '
@@ -1,10 +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
- # `.claude/rules/` is a subset rather than a mirror, so it resolves through the
9
- # stack machinery. The record naming the subset is `internal/governance.toml`.
10
- bun "$PROJECT_ROOT/src/cli.ts" gov regen --root "$PROJECT_ROOT"
@@ -1,10 +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/ui.sh"
9
-
10
- bun "$PROJECT_ROOT/src/cli.ts" indexes regen --root "$PROJECT_ROOT" --json >/dev/null
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Repairs `core.bare`, which Claude Code's worktree entry leaves set in the
3
- # shared config with nothing restoring it. The flag strands the main worktree
4
- # and breaks the git reads that scope a verification run, so this runs ahead of
5
- # every stage rather than as one of them.
6
- #
7
- # The rule itself lives in `scripts/lib/worktree.sh`, the one bash function in
8
- # this repository under test, so this file is the invocation and never a second
9
- # copy of the guard that spares a genuinely bare repository.
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/ui.sh"
17
- source "$PROJECT_ROOT/scripts/lib/worktree.sh"
18
-
19
- repair_bare_flag "$PROJECT_ROOT"
@@ -1,134 +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
- # shellcheck source=/dev/null
9
- source "$PROJECT_ROOT/scripts/lib/ui.sh"
10
-
11
- NESTED="${VERIFY_NESTED:-false}"
12
-
13
- OUTPUT_FILE=".canon/tmp/project/PROJECT-SNAPSHOT.md"
14
-
15
- check_dependencies() {
16
- command -v find >/dev/null 2>&1 || log_error "find not installed"
17
- }
18
-
19
- load_gitignore_patterns() {
20
- if [ ! -f ".gitignore" ]; then
21
- echo ""
22
- return
23
- fi
24
- grep -v '^\s*#' .gitignore | grep -v '^\s*$' | sed 's|/$||'
25
- }
26
-
27
- is_ignored() {
28
- local name="$1"
29
- local patterns="$2"
30
-
31
- [ "$name" = ".git" ] && return 0
32
- [ "$name" = "_" ] && return 0
33
-
34
- while IFS= read -r pattern; do
35
- [ -z "$pattern" ] && continue
36
- # shellcheck disable=SC2053
37
- if [[ "$name" == $pattern ]]; then
38
- return 0
39
- fi
40
- done <<<"$patterns"
41
-
42
- return 1
43
- }
44
-
45
- build_tree() {
46
- local dir="${1:-.}"
47
- local prefix="${2:-}"
48
- local patterns="${3:-}"
49
- local entries=()
50
-
51
- while IFS= read -r entry; do
52
- local name
53
- name=$(basename "$entry")
54
- is_ignored "$name" "$patterns" && continue
55
- entries+=("$entry")
56
- done < <(find "$dir" -maxdepth 1 -mindepth 1 | sort)
57
-
58
- local count=${#entries[@]}
59
- local i=0
60
-
61
- for entry in "${entries[@]}"; do
62
- i=$((i + 1))
63
- local name
64
- name=$(basename "$entry")
65
- local connector="├──"
66
- local child_prefix="${prefix}│ "
67
-
68
- if [ "$i" -eq "$count" ]; then
69
- connector="└──"
70
- child_prefix="${prefix} "
71
- fi
72
-
73
- if [ -d "$entry" ]; then
74
- echo "${prefix}${connector} ${name}/"
75
- build_tree "$entry" "$child_prefix" "$patterns"
76
- else
77
- echo "${prefix}${connector} ${name}"
78
- fi
79
- done
80
- }
81
-
82
- write_snapshot() {
83
- local project_name
84
- project_name=$(basename "$PWD")
85
- local patterns
86
- patterns=$(load_gitignore_patterns)
87
-
88
- mkdir -p "$(dirname "$OUTPUT_FILE")"
89
-
90
- {
91
- echo "# Project Snapshot: $project_name"
92
- echo ""
93
- echo "## Structure"
94
- echo ""
95
- echo '```'
96
- build_tree "." "" "$patterns"
97
- echo '```'
98
- echo ""
99
-
100
- if [ -f "package.json" ]; then
101
- echo "## package.json"
102
- echo ""
103
- echo '```json'
104
- cat package.json
105
- echo '```'
106
- echo ""
107
- fi
108
- } >"$OUTPUT_FILE"
109
- }
110
-
111
- main() {
112
- check_dependencies
113
-
114
- if [ "$NESTED" = false ]; then open_timeline; fi
115
-
116
- log_step "Snapshot"
117
- write_snapshot
118
- log_info "Written to $OUTPUT_FILE"
119
-
120
- # The whole frame goes to stderr, since the run writes its document to a file
121
- # and leaves stdout carrying nothing. Edges on stdout with the body on stderr
122
- # would capture as a frame with no body inside it.
123
- if [ "$NESTED" = false ]; then
124
- close_timeline
125
- # All six are declared to scope what `set_palette` assigns, so the four this
126
- # line never spells stay out of the globals a sourcing script reads.
127
- # shellcheck disable=SC2034
128
- local GREEN RED YELLOW WHITE GREY NC
129
- set_palette 2
130
- echo -e "\n${GREEN}✓ Snapshot complete${NC}" >&2
131
- fi
132
- }
133
-
134
- main "$@"
@@ -1,35 +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/ui.sh"
9
-
10
- check_dependencies() {
11
- command -v bun >/dev/null 2>&1 || log_error "bun is not installed"
12
- }
13
-
14
- main() {
15
- check_dependencies
16
-
17
- echo -e "${GREY}┌${NC}"
18
-
19
- echo -e "${GREY}├${NC} ${WHITE}Interactive dependency update${NC}"
20
- echo -e "${GREY}│${NC}"
21
- bun update --interactive
22
-
23
- log_step "Verifying project health"
24
- if [ -f "$PROJECT_ROOT/src/cli.ts" ]; then
25
- bun "$PROJECT_ROOT/src/cli.ts" gate run --nested
26
- log_info "All checks passed"
27
- else
28
- log_warn "Verification entry point not found, skipping."
29
- fi
30
-
31
- echo -e "${GREY}└${NC}\n"
32
- echo -e "${GREEN}✓ Update complete${NC}"
33
- }
34
-
35
- main "$@"
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- # Claude Code's worktree entry writes core.bare into the parent repository's
4
- # shared config and its exit never restores it, stranding every later command in
5
- # the main worktree. Sourced by verify.sh, which runs the repair before any stage
6
- # because the flag breaks the git reads that scope the run.
7
-
8
- repair_bare_flag() {
9
- local root="${1:-$PROJECT_ROOT}"
10
- local common_dir
11
- [ "$(git -C "$root" config --get core.bare 2>/dev/null || echo false)" = true ] || return 0
12
-
13
- common_dir=$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 0
14
- # A genuinely bare repository keeps its objects at the root and has no .git
15
- # directory, so it fails this test and keeps its flag.
16
- [ "$(basename "$common_dir")" = ".git" ] || return 0
17
-
18
- git -C "$root" config core.bare false
19
- log_warn "Repaired core.bare, which worktree entry left set. Recovery is 'git config core.bare false'."
20
- }