@erclx/canon 4.3.0 → 4.5.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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-markdown-propose/REQUIREMENT.md +0 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +34 -9
- package/claude/skills/repo-metadata/REQUIREMENT.md +37 -0
- package/claude/skills/repo-metadata/SKILL.md +51 -0
- package/docs/agents/audits.md +6 -6
- package/docs/agents/commands.md +64 -62
- package/docs/agents/context-audit.md +2 -2
- package/docs/agents/index.md +1 -1
- package/docs/agents/records.md +17 -7
- package/docs/agents/sandbox.md +3 -1
- package/docs/agents/tasks.md +36 -1
- package/docs/operating-model.md +1 -1
- package/package.json +1 -1
- package/scripts/core/check-ignore-parity.sh +9 -3
- package/scripts/lib/sandbox-dispatch.sh +182 -0
- package/src/audits/baseline.ts +1 -1
- package/src/claude/cases/misc.ts +4 -0
- package/src/cli.ts +4 -0
- package/src/commands/claude.ts +7 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/design.ts +6 -1
- package/src/commands/feedback.ts +5 -1
- package/src/commands/gov.ts +2 -1
- package/src/commands/repo.ts +393 -0
- package/src/commands/slides.ts +6 -1
- package/src/commands/tasks.ts +100 -0
- package/src/context/citations.ts +16 -5
- package/src/context/folders.ts +18 -8
- package/src/gate/measures.ts +1 -1
- package/src/intake/folder.ts +2 -1
- package/src/paths.ts +16 -0
- package/src/record-root.ts +134 -0
- package/src/records/backup.ts +40 -22
- package/src/records/size.ts +12 -7
- package/src/records/validate.ts +35 -17
- package/src/repo/metadata.ts +206 -0
- package/src/tasks/answers.ts +202 -0
- package/src/tasks/archive.ts +33 -30
- package/src/teach/workspace.ts +2 -1
- package/tooling/claude/manifest.toml +1 -1
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +8 -1
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +28 -11
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +12 -3
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +4 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +28 -10
|
@@ -117,11 +117,17 @@ for entry in "${shipped[@]}"; do
|
|
|
117
117
|
done
|
|
118
118
|
|
|
119
119
|
# Claude-scoped patterns this repository ignores that the manifest does not
|
|
120
|
-
# ship. Scoped to
|
|
121
|
-
# nothing about `node_modules/` or `.env`.
|
|
120
|
+
# ship. Scoped to the two record roots because the manifest is the claude stack
|
|
121
|
+
# and says nothing about `node_modules/` or `.env`.
|
|
122
|
+
#
|
|
123
|
+
# `.canon` is read as a bare root as well as a prefix, since it is one line
|
|
124
|
+
# covering a whole tree where `.claude/` is thirteen lines naming folders inside
|
|
125
|
+
# a root that also holds tracked content. Leaving it out is what would let a new
|
|
126
|
+
# entry sit outside the only stage comparing the two lists, which is the
|
|
127
|
+
# direction that goes silently blind.
|
|
122
128
|
for pattern in "${ignored[@]}"; do
|
|
123
129
|
case "$pattern" in
|
|
124
|
-
.claude/*) ;;
|
|
130
|
+
.claude/* | .canon | .canon/*) ;;
|
|
125
131
|
*) continue ;;
|
|
126
132
|
esac
|
|
127
133
|
contains "$pattern" "${shipped[@]}" && continue
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# The bound on a nested background dispatch, and the recording behind it.
|
|
4
|
+
#
|
|
5
|
+
# An arm invoked without the narration its fixture states dispatched a real
|
|
6
|
+
# `claude --bg` session against the machine's own process table, and nothing in
|
|
7
|
+
# the harness noticed. `snapshot_tree` reads the sandbox and `snapshot_root`
|
|
8
|
+
# reads four scratch directories, and a dispatched session writes into none of
|
|
9
|
+
# them, so the escape watch reported clean throughout and was right to. The run
|
|
10
|
+
# spent real cost until a person found it, and `SIGTERM` alone did not end it.
|
|
11
|
+
#
|
|
12
|
+
# Three mechanisms sit here rather than in `scripts/sandbox/run.sh`, which is the
|
|
13
|
+
# only caller. Each is reachable from a test that never launches a session:
|
|
14
|
+
# `src/sandbox-dispatch.test.ts` drives the shim against a stub binary and the
|
|
15
|
+
# reap against a `sleep`, where proving either through `run.sh` would mean
|
|
16
|
+
# spawning the thing the bound exists to prevent.
|
|
17
|
+
|
|
18
|
+
# The prevention. Writes a `claude` that refuses a background dispatch by flag
|
|
19
|
+
# name and delegates everything else, for `run.sh` to place first on the PATH of
|
|
20
|
+
# the session it spawns.
|
|
21
|
+
#
|
|
22
|
+
# The real binary is baked into the file rather than passed through the
|
|
23
|
+
# environment. An env var carrying it would sit in the spawned session's own
|
|
24
|
+
# environment, which hands any arm the exact string that walks around the shim.
|
|
25
|
+
#
|
|
26
|
+
# It names itself in its refusal. No arm shells to `claude` today, and one
|
|
27
|
+
# written tomorrow would meet this and read it as the harness being broken
|
|
28
|
+
# unless the text says which layer refused and why.
|
|
29
|
+
#
|
|
30
|
+
# It is not the whole bound. A dispatch reaching the binary by an absolute path
|
|
31
|
+
# never resolves through PATH at all, which is what `reap_process_group`
|
|
32
|
+
# backstops.
|
|
33
|
+
install_dispatch_shim() {
|
|
34
|
+
local dir="$1"
|
|
35
|
+
local real="$2"
|
|
36
|
+
|
|
37
|
+
cat >"$dir/claude" <<SHIM
|
|
38
|
+
#!/usr/bin/env bash
|
|
39
|
+
# Generated by install_dispatch_shim in scripts/lib/sandbox-dispatch.sh.
|
|
40
|
+
for arg in "\$@"; do
|
|
41
|
+
case "\$arg" in
|
|
42
|
+
--bg | --background)
|
|
43
|
+
echo "canon sandbox harness: refusing \$arg. A sandbox run may not dispatch a background session, so this shim stands first on PATH in place of the real claude binary. Drive the refusal the fixture asks for, or run this outside the harness." >&2
|
|
44
|
+
exit 64
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
exec "$real" "\$@"
|
|
49
|
+
SHIM
|
|
50
|
+
|
|
51
|
+
chmod +x "$dir/claude"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# True while any process still carries this group id. Reads the whole table
|
|
55
|
+
# rather than `ps -g`, which selects by session on some builds and reports an
|
|
56
|
+
# empty group as a clean exit either way.
|
|
57
|
+
process_group_alive() {
|
|
58
|
+
ps -eo pgid= 2>/dev/null | tr -d ' ' | grep -qx "$1"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# The backstop. Signals a process group `run.sh` created, escalating rather than
|
|
62
|
+
# sending one signal and reporting success, since the dispatch that produced this
|
|
63
|
+
# row took two signals to reap.
|
|
64
|
+
#
|
|
65
|
+
# Prints its outcome and always exits zero. Every caller runs under `set -e`, and
|
|
66
|
+
# a survivor is something to report rather than a reason to abandon the verdict
|
|
67
|
+
# the run already took.
|
|
68
|
+
#
|
|
69
|
+
# The group has to be one the run created, and the refusal below is what makes
|
|
70
|
+
# that property travel with the function rather than living in one caller.
|
|
71
|
+
# `run.sh` reads the harness group once and reports `inherited` when the session
|
|
72
|
+
# failed to lead a group of its own, which is a diagnosis; this is the floor
|
|
73
|
+
# under it, and under every caller after it, because the cost of getting the
|
|
74
|
+
# argument wrong is the operator's own shell.
|
|
75
|
+
#
|
|
76
|
+
# What it cannot reach is a dispatch that called `setsid` on its way out, which
|
|
77
|
+
# leaves the group before this reads it. That is the half the shim covers.
|
|
78
|
+
reap_process_group() {
|
|
79
|
+
local pgid="$1"
|
|
80
|
+
|
|
81
|
+
if [ "$pgid" = "$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')" ]; then
|
|
82
|
+
printf 'refused-own-group\n'
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
if ! process_group_alive "$pgid"; then
|
|
87
|
+
printf 'clear\n'
|
|
88
|
+
return 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
kill -TERM -"$pgid" 2>/dev/null || true
|
|
92
|
+
wait_for_group_exit "$pgid"
|
|
93
|
+
|
|
94
|
+
if ! process_group_alive "$pgid"; then
|
|
95
|
+
printf 'reaped-term\n'
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
kill -KILL -"$pgid" 2>/dev/null || true
|
|
100
|
+
wait_for_group_exit "$pgid"
|
|
101
|
+
|
|
102
|
+
if process_group_alive "$pgid"; then
|
|
103
|
+
printf 'survived\n'
|
|
104
|
+
else
|
|
105
|
+
printf 'reaped-kill\n'
|
|
106
|
+
fi
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Five seconds per signal. A shorter window reports a survivor for a process
|
|
111
|
+
# that was already exiting, and a longer one stalls a run whose verdict is
|
|
112
|
+
# already taken.
|
|
113
|
+
wait_for_group_exit() {
|
|
114
|
+
local pgid="$1"
|
|
115
|
+
local waited=0
|
|
116
|
+
|
|
117
|
+
while [ "$waited" -lt 20 ] && process_group_alive "$pgid"; do
|
|
118
|
+
sleep 0.25
|
|
119
|
+
waited=$((waited + 1))
|
|
120
|
+
done
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# The client's own session registry, which is the only surface a nested dispatch
|
|
124
|
+
# leaves a trace on that this harness can read. It is client-owned and carries no
|
|
125
|
+
# contract, so a client that stops writing a record per session breaks the
|
|
126
|
+
# detection silently. That is why what reads it reports rather than asserts.
|
|
127
|
+
sessions_dir() {
|
|
128
|
+
printf '%s/sessions\n' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Set whenever the registry directory existed at snapshot time. An absent
|
|
132
|
+
# registry produces the same empty manifest a quiet run does, and the two mean
|
|
133
|
+
# opposite things, so the report carries this rather than letting an unwatched
|
|
134
|
+
# run read as a clean one. Same distinction `escape_watched` draws in `run.sh`,
|
|
135
|
+
# which is also the only reader, hence the exemption on the write below.
|
|
136
|
+
sessions_watched=0
|
|
137
|
+
|
|
138
|
+
# Record names only, never hashes. A live session rewrites its own record on
|
|
139
|
+
# every status change, so a content manifest would name every session running
|
|
140
|
+
# beside this one, which is the noise the escape watch was already narrowed to
|
|
141
|
+
# avoid. A record that appears between the two snapshots is a session that
|
|
142
|
+
# started during the run, and that is the whole question.
|
|
143
|
+
snapshot_sessions() {
|
|
144
|
+
local manifest="$1"
|
|
145
|
+
local dir
|
|
146
|
+
dir="$(sessions_dir)"
|
|
147
|
+
|
|
148
|
+
: >"$manifest"
|
|
149
|
+
[ -d "$dir" ] || return 0
|
|
150
|
+
# shellcheck disable=SC2034
|
|
151
|
+
sessions_watched=1
|
|
152
|
+
|
|
153
|
+
find "$dir" -maxdepth 1 -type f -name '*.json' 2>/dev/null |
|
|
154
|
+
sed 's|.*/||' | sort >"$manifest"
|
|
155
|
+
return 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# The new side alone, unlike `writes_between`. A record that vanished is a
|
|
159
|
+
# session that ended and a record that changed is one that was already running,
|
|
160
|
+
# and neither is a dispatch this run made.
|
|
161
|
+
sessions_between() {
|
|
162
|
+
comm -13 "$1" "$2"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Turns a record name into a line a person can act on. The file can be gone by
|
|
166
|
+
# the time this reads it, since a session that started and exited inside the run
|
|
167
|
+
# takes its record with it, so an unreadable record still reports its name rather
|
|
168
|
+
# than dropping out of the list.
|
|
169
|
+
#
|
|
170
|
+
# One record is one line. The name is written by whatever peer claimed the
|
|
171
|
+
# session, so a newline inside it would split one record across two entries in a
|
|
172
|
+
# list every reader takes as one line each.
|
|
173
|
+
describe_session() {
|
|
174
|
+
local record desc
|
|
175
|
+
record="$(sessions_dir)/$1"
|
|
176
|
+
|
|
177
|
+
desc="$(jq -r '"\(.name // "unnamed") in \(.cwd // "an unrecorded directory")"
|
|
178
|
+
| gsub("\\s+"; " ")' "$record" 2>/dev/null || true)"
|
|
179
|
+
[ -z "$desc" ] && desc="record already gone"
|
|
180
|
+
|
|
181
|
+
printf '%s: %s\n' "$1" "$desc"
|
|
182
|
+
}
|
package/src/audits/baseline.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { AuditResult } from '@/audits/catalog'
|
|
|
15
15
|
* that installs the CLI. A baseline in the package would hand a target this
|
|
16
16
|
* repository's counts to measure its own tree against.
|
|
17
17
|
*/
|
|
18
|
-
export const BASELINE_REL = join('.claude', '
|
|
18
|
+
export const BASELINE_REL = join('.claude', 'canon', 'baseline.json')
|
|
19
19
|
|
|
20
20
|
export interface Baseline {
|
|
21
21
|
/** The day the record was taken, as `YYYY-MM-DD`. */
|
package/src/claude/cases/misc.ts
CHANGED
|
@@ -24,4 +24,8 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
24
24
|
prompt: "This test just started failing and I don't know why yet.",
|
|
25
25
|
expect: 'systematic-debugging',
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
prompt: 'Does our github about text still match what the readme says?',
|
|
29
|
+
expect: 'repo-metadata',
|
|
30
|
+
},
|
|
27
31
|
]
|
package/src/cli.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { register as deps } from '@/commands/deps'
|
|
|
37
37
|
import { register as labels } from '@/commands/labels'
|
|
38
38
|
import { register as autoship } from '@/commands/autoship'
|
|
39
39
|
import { register as pr } from '@/commands/pr'
|
|
40
|
+
import { register as repo } from '@/commands/repo'
|
|
40
41
|
import { register as census } from '@/commands/census'
|
|
41
42
|
import { register as targets } from '@/commands/targets'
|
|
42
43
|
import { register as upgrade } from '@/commands/upgrade'
|
|
@@ -86,6 +87,7 @@ function showHelp(): void {
|
|
|
86
87
|
`${GREY}│${NC} labels [cmd] ${GREY}# Read a changed set against the pull request label map (audit)${NC}`,
|
|
87
88
|
`${GREY}│${NC} autoship [cmd] ${GREY}# Decide whether a changed set needs the review pass (classify)${NC}`,
|
|
88
89
|
`${GREY}│${NC} pr [cmd] ${GREY}# Read a pull request body against its own diff (key-changes)${NC}`,
|
|
90
|
+
`${GREY}│${NC} repo [cmd] ${GREY}# This repository's own remote metadata (metadata propose, apply)${NC}`,
|
|
89
91
|
`${GREY}│${NC} census [path] ${GREY}# Report tracked file count, extension breakdown, and line totals${NC}`,
|
|
90
92
|
`${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
|
|
91
93
|
`${GREY}│${NC} gate [cmd] ${GREY}# Run the merge gate stage by stage (run)${NC}`,
|
|
@@ -135,6 +137,7 @@ function showHelp(): void {
|
|
|
135
137
|
`${GREY}│${NC} canon secrets scan --json`,
|
|
136
138
|
`${GREY}│${NC} canon deps audit --json`,
|
|
137
139
|
`${GREY}│${NC} canon labels audit --json`,
|
|
140
|
+
`${GREY}│${NC} canon repo metadata propose --json`,
|
|
138
141
|
`${GREY}│${NC} canon census --json`,
|
|
139
142
|
`${GREY}│${NC} canon audits run --json`,
|
|
140
143
|
`${GREY}│${NC} canon gate run --all --no-write`,
|
|
@@ -192,6 +195,7 @@ deps(program)
|
|
|
192
195
|
labels(program)
|
|
193
196
|
autoship(program)
|
|
194
197
|
pr(program)
|
|
198
|
+
repo(program)
|
|
195
199
|
census(program)
|
|
196
200
|
audits(program)
|
|
197
201
|
gate(program)
|
package/src/commands/claude.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { copyPreservingMode } from '@/copy'
|
|
52
52
|
import { execScript } from '@/exec'
|
|
53
53
|
import { PROJECT_ROOT } from '@/project-root'
|
|
54
|
+
import { recordDir } from '@/record-root'
|
|
54
55
|
import { isDirectory, resolveTarget } from '@/target'
|
|
55
56
|
import { injectGitignore, pruneGitignore } from '@/tooling/inject'
|
|
56
57
|
import {
|
|
@@ -491,13 +492,18 @@ async function runSync(target: string): Promise<number> {
|
|
|
491
492
|
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
492
493
|
if (typeof resolved === 'number') return resolved
|
|
493
494
|
|
|
495
|
+
// A record folder resolves at either root, so a migrated target is reported as
|
|
496
|
+
// seeded rather than sent to `canon claude init` to re-create records it
|
|
497
|
+
// already holds. The three seeded files and `wireframes` are tracked and stay
|
|
498
|
+
// at `.claude/`, which the resolver answers for them anyway, since nothing
|
|
499
|
+
// ever creates a second root copy for a name that does not move.
|
|
494
500
|
logStep('Seeded')
|
|
495
501
|
for (const name of SEEDED_FILES) {
|
|
496
502
|
if (existsSync(join(resolved, '.claude', name))) logInfo(name)
|
|
497
503
|
else logWarn(`${name} missing. Run \`canon claude init\``)
|
|
498
504
|
}
|
|
499
505
|
for (const name of SEEDED_DIRS) {
|
|
500
|
-
if (isDirectory(
|
|
506
|
+
if (isDirectory(recordDir(resolved, name))) logInfo(`${name}/`)
|
|
501
507
|
else logWarn(`${name}/ missing. Run \`canon claude init\``)
|
|
502
508
|
}
|
|
503
509
|
|
package/src/commands/context.ts
CHANGED
|
@@ -180,7 +180,7 @@ async function runAudit(
|
|
|
180
180
|
if (folders.length === 0) {
|
|
181
181
|
return refuse(
|
|
182
182
|
'no-folders',
|
|
183
|
-
`No audited folder found ${named ? 'under
|
|
183
|
+
`No audited folder found ${named ? 'under a record root or the project root' : 'under a record root'}, since resolving one needs its own index.md file. Looked for: ${names.join(', ')}.`,
|
|
184
184
|
gateOnly,
|
|
185
185
|
root,
|
|
186
186
|
opts.json ?? false,
|
|
@@ -200,7 +200,7 @@ async function runAudit(
|
|
|
200
200
|
if (gateOnly && cited.length === 0) {
|
|
201
201
|
return refuse(
|
|
202
202
|
'no-citation-scope',
|
|
203
|
-
`The citation check spells the
|
|
203
|
+
`The citation check spells the record-root prefixes and no audited folder resolved under one. Looked for: ${names.join(', ')}.`,
|
|
204
204
|
gateOnly,
|
|
205
205
|
root,
|
|
206
206
|
opts.json ?? false,
|
|
@@ -414,7 +414,7 @@ function reportCitations(
|
|
|
414
414
|
|
|
415
415
|
if (cited.length === 0) {
|
|
416
416
|
logInfo(
|
|
417
|
-
'Out of scope. The pattern spells the
|
|
417
|
+
'Out of scope. The pattern spells the record-root prefixes, and no audited folder resolved under one.',
|
|
418
418
|
)
|
|
419
419
|
return
|
|
420
420
|
}
|
package/src/commands/design.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
+
import { creationRel } from '@/record-root'
|
|
4
5
|
import { renderDesignDoc } from '@/design/render'
|
|
5
6
|
import { palette } from '@/ui'
|
|
6
7
|
|
|
@@ -13,7 +14,11 @@ export function register(program: Command): void {
|
|
|
13
14
|
.command('render')
|
|
14
15
|
.description('Render DESIGN.md tokens to HTML and CSS preview')
|
|
15
16
|
.option('-s, --source <path>', 'Source DESIGN.md path', '.claude/DESIGN.md')
|
|
16
|
-
.option(
|
|
17
|
+
.option(
|
|
18
|
+
'-o, --out <path>',
|
|
19
|
+
'Output directory',
|
|
20
|
+
creationRel('review', 'design'),
|
|
21
|
+
)
|
|
17
22
|
.action((opts: { source: string; out: string }) => {
|
|
18
23
|
const sourcePath = resolve(process.cwd(), opts.source)
|
|
19
24
|
const outDir = resolve(process.cwd(), opts.out)
|
package/src/commands/feedback.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path'
|
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { deriveSlug, deriveTitle } from '@/commands/feedback-format'
|
|
5
5
|
import { PROJECT_ROOT } from '@/project-root'
|
|
6
|
+
import { creationRel } from '@/record-root'
|
|
6
7
|
import { createGithubIssue } from '@/github'
|
|
7
8
|
import { frameError, frameSuccess, palette } from '@/ui'
|
|
8
9
|
|
|
@@ -39,7 +40,10 @@ function isToolkitSource(): boolean {
|
|
|
39
40
|
* single ignore entry and the single backed-folder entry it already had.
|
|
40
41
|
*/
|
|
41
42
|
function writeLocal(body: string): string {
|
|
42
|
-
|
|
43
|
+
// Creation, so the destination is the creation default rather than the
|
|
44
|
+
// resolved read root. A toolkit checkout that has migrated its records already
|
|
45
|
+
// carries the folder and resolves the same path either way.
|
|
46
|
+
const relativeDir = creationRel('review', 'feedback')
|
|
43
47
|
const reviewDir = join(PROJECT_ROOT, relativeDir)
|
|
44
48
|
mkdirSync(reviewDir, { recursive: true })
|
|
45
49
|
const filename = `feedback-${deriveSlug(body)}-${timestamp()}.md`
|
package/src/commands/gov.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
scanCounts,
|
|
10
10
|
} from '@/counts/scan'
|
|
11
11
|
import { PROJECT_ROOT } from '@/project-root'
|
|
12
|
+
import { creationRel, SCRATCH } from '@/record-root'
|
|
12
13
|
import { createGovAdapter } from '@/gov/adapter'
|
|
13
14
|
import { regenConsumedRules } from '@/gov/consumed'
|
|
14
15
|
import { installRules, lookupRules } from '@/gov/install'
|
|
@@ -65,7 +66,7 @@ import {
|
|
|
65
66
|
select,
|
|
66
67
|
} from '@/ui'
|
|
67
68
|
|
|
68
|
-
const PAYLOAD_REL =
|
|
69
|
+
const PAYLOAD_REL = creationRel(SCRATCH, 'gov', 'rules.md')
|
|
69
70
|
const RULES_REL = join('.claude', 'rules')
|
|
70
71
|
|
|
71
72
|
interface InstallOptions {
|