@erclx/aitk 0.62.1 → 0.63.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  One source for your repos' AI conventions. Install once, sync everywhere.
8
8
 
9
- ![The aitk catalog, listing skills, governance rules, and standards with the count each ships and a sample of their names](assets/hero.png)
9
+ ![The aitk catalog, listing skills, governance rules, and standards with the count each ships, the workflow skills named, and a sample of the rule and standard names](assets/hero.png)
10
10
 
11
11
  If you work across more than one repository and your AI setup has started to drift between them, this is for you. The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today.
12
12
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.62.1",
4
+ "version": "0.63.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -77,7 +77,7 @@ Run one orchestrator at a time. The board is gitignored, so a second session rea
77
77
 
78
78
  Before a handoff, the orchestrator checks the plan against the tree rather than reading it: grep each construct it names and count the sites, confirm every phase label it cites is still open, and open each file it describes. A plan goes stale from whatever merged after it was written, and reading cannot catch that.
79
79
 
80
- `.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
80
+ `.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. A session inside a worktree reads them directly and writes them through the shell, since the file-editing tools refuse a main-root path. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
81
81
 
82
82
  The plan's shape is fixed by `.claude/standards/plan.md`: the section list, the filename, the lifecycle, and the contract its questions keep. Every question carries a `- Suggested:` line and an empty `- Answer:` slot, and a blank answer accepts the suggestion at execution time. That default is what makes a plan decision-ready in one pass, and it is the opposite of the contract an intake folder keeps, where an empty slot means nobody reached the item.
83
83
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.62.1",
4
+ "version": "0.63.1",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -19,6 +19,7 @@ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
19
19
 
20
20
  TEMPLATE="$PROJECT_ROOT/assets/hero.html.tmpl"
21
21
  OUTPUT="$PROJECT_ROOT/assets/hero.html"
22
+ CLI_ENTRY="$PROJECT_ROOT/src/cli.ts"
22
23
  LISTED=10
23
24
 
24
25
  # `bun src/cli.ts` rather than `aitk`, since a globally linked binary resolves to
@@ -32,6 +33,20 @@ if [ ! -f "$TEMPLATE" ]; then
32
33
  exit 1
33
34
  fi
34
35
 
36
+ # The commands have no `--json` catalog to read, so the count comes from the
37
+ # registration block in the CLI entry point, which is the one place a command is
38
+ # added. `--help` is hand-authored ASCII and would drift from what is registered.
39
+ if [ ! -f "$CLI_ENTRY" ]; then
40
+ echo "regen-hero: missing CLI entry point at $CLI_ENTRY" >&2
41
+ exit 1
42
+ fi
43
+
44
+ COMMAND_COUNT="$(grep -c '^import { register as ' "$CLI_ENTRY" || true)"
45
+ if [ "${COMMAND_COUNT:-0}" -eq 0 ]; then
46
+ echo "regen-hero: no command registrations found in $CLI_ENTRY, refusing to write a zeroed hero" >&2
47
+ exit 1
48
+ fi
49
+
35
50
  SKILLS_JSON="$(catalog claude skills list)"
36
51
  GOV_JSON="$(catalog gov list)"
37
52
  STANDARDS_JSON="$(catalog standards list)"
@@ -59,12 +74,12 @@ printf '%s' "$SNIPPETS_JSON" >"$PAYLOAD_DIR/snippets.json"
59
74
  printf '%s' "$TOOLING_JSON" >"$PAYLOAD_DIR/tooling.json"
60
75
 
61
76
  export PAYLOAD_DIR
62
- export TEMPLATE OUTPUT LISTED PROJECT_ROOT
77
+ export TEMPLATE OUTPUT LISTED PROJECT_ROOT COMMAND_COUNT
63
78
 
64
79
  bun --eval '
65
80
  const { readFileSync } = require("node:fs")
66
81
 
67
- const { PAYLOAD_DIR, TEMPLATE, OUTPUT, LISTED, PROJECT_ROOT } = process.env
82
+ const { PAYLOAD_DIR, TEMPLATE, OUTPUT, LISTED, PROJECT_ROOT, COMMAND_COUNT } = process.env
68
83
 
69
84
  const payload = (name) => readFileSync(PAYLOAD_DIR + "/" + name + ".json", "utf8")
70
85
 
@@ -79,7 +94,13 @@ const skills = JSON.parse(SKILLS_JSON).skills.map((entry) => entry.name)
79
94
  const gov = JSON.parse(GOV_JSON)
80
95
  // Rule names carry a numeric prefix that orders the load, not the identity a
81
96
  // reader knows them by, so the frame shows the slug alone.
82
- const rules = gov.rules.map((entry) => entry.name.replace(/^\d+-/, ""))
97
+ const slug = (entry) => entry.name.replace(/^\d+-/, "")
98
+ const rules = gov.rules.map(slug)
99
+ // A rule no stack names is opt-in behind `--add`, so featuring one advertises
100
+ // an entry no ordinary `aitk gov install` delivers. The column samples the
101
+ // stack-reached subset while its count and remainder describe the whole catalog.
102
+ const stacked = new Set(gov.stacks.flatMap((stack) => stack.rules))
103
+ const deliveredRules = gov.rules.filter((entry) => stacked.has(entry.name)).map(slug)
83
104
  const standards = JSON.parse(STANDARDS_JSON).standards.map((entry) => entry.name)
84
105
  const snippets = new Set(
85
106
  JSON.parse(SNIPPETS_JSON).categories.flatMap((category) => category.entries),
@@ -89,20 +110,62 @@ const toolingStacks = JSON.parse(TOOLING_JSON).stacks
89
110
  const escape = (value) =>
90
111
  value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
91
112
 
92
- // Even spacing across the sorted catalog rather than its first N. The skill
93
- // names are prefixed by domain, so an alphabetical head returns eight
94
- // `claude-*` entries and reads as a narrower catalog than the one that ships.
113
+ // Even spacing across the sorted catalog rather than its first N, so a column
114
+ // whose names share a prefix does not read as a narrower catalog than the one
115
+ // that ships. Rules and standards sample. Skills do not, because the even step
116
+ // lands on the entries closest to commodity and on none of the workflow the
117
+ // toolkit exists to carry.
95
118
  const sample = (names) => {
96
119
  if (names.length <= listed) return names
97
120
  const step = names.length / listed
98
121
  return Array.from({ length: listed }, (_, i) => names[Math.floor(i * step)])
99
122
  }
100
123
 
101
- const entries = (names) =>
102
- sample(names)
124
+ // The first seven are the method and exist nowhere else. The last three are
125
+ // recognizable on sight and are what keeps a column of chosen names from
126
+ // reading as an all-`claude-` catalog, which is the failure the sampler above
127
+ // was written against.
128
+ const FEATURED_SKILLS = [
129
+ "claude-feature",
130
+ "claude-groundwork",
131
+ "claude-intake",
132
+ "claude-orchestrate",
133
+ "claude-autoship",
134
+ "claude-pr-review",
135
+ "claude-tasks",
136
+ "git-ship",
137
+ "setup-init",
138
+ "systematic-debugging",
139
+ ]
140
+
141
+ // A chosen name is maintained by hand where a sample never goes stale, so a
142
+ // rename has to fail the run rather than quietly shrink the column. This is
143
+ // what makes the trade acceptable.
144
+ //
145
+ // The length is asserted because the `+N more` figure below counts down from
146
+ // `listed` rather than from what this returns, so a list of any other length
147
+ // renders a remainder that is wrong by the difference.
148
+ const featured = (names, chosen) => {
149
+ if (chosen.length !== listed) {
150
+ console.error(`regen-hero: ${chosen.length} featured skills, expected ${listed}`)
151
+ process.exit(1)
152
+ }
153
+ const catalog = new Set(names)
154
+ const missing = chosen.filter((name) => !catalog.has(name))
155
+ if (missing.length > 0) {
156
+ console.error(`regen-hero: featured skills missing from the catalog: ${missing.join(", ")}`)
157
+ process.exit(1)
158
+ }
159
+ return chosen
160
+ }
161
+
162
+ const markup = (names) =>
163
+ names
103
164
  .map((name) => ` <div class="entry">${escape(name)}</div>`)
104
165
  .join("\n")
105
166
 
167
+ const entries = (names) => markup(sample(names))
168
+
106
169
  const remaining = (names) => String(Math.max(0, names.length - listed))
107
170
 
108
171
  // An empty array is well-formed JSON, so the shell guard on an empty payload
@@ -111,6 +174,7 @@ const remaining = (names) => String(Math.max(0, names.length - listed))
111
174
  for (const [label, list] of [
112
175
  ["skills", skills], ["rules", rules], ["standards", standards],
113
176
  ["snippets", [...snippets]], ["tooling stacks", toolingStacks], ["gov stacks", gov.stacks],
177
+ ["stack-delivered rules", deliveredRules],
114
178
  ]) {
115
179
  if (list.length === 0) {
116
180
  console.error(`regen-hero: the ${label} catalog is empty, refusing to write a zeroed hero`)
@@ -125,8 +189,9 @@ const values = {
125
189
  SNIPPET_COUNT: String(snippets.size),
126
190
  GOV_STACK_COUNT: String(gov.stacks.length),
127
191
  TOOLING_STACK_COUNT: String(toolingStacks.length),
128
- SKILL_ENTRIES: entries(skills),
129
- RULE_ENTRIES: entries(rules),
192
+ COMMAND_COUNT: String(Number(COMMAND_COUNT)),
193
+ SKILL_ENTRIES: markup(featured(skills, FEATURED_SKILLS)),
194
+ RULE_ENTRIES: entries(deliveredRules),
130
195
  STANDARD_ENTRIES: entries(standards),
131
196
  SKILL_MORE: remaining(skills),
132
197
  RULE_MORE: remaining(rules),
@@ -84,4 +84,6 @@
84
84
 
85
85
  - Implementation work runs in a linked worktree. From the main worktree, enter one with `/claude-worktree` before editing tracked files for a feature.
86
86
  - Shared session scratch (`.claude/plans/`, `.claude/review/`, `.claude/memory/`, `.claude/tasks/`) lives at the main worktree root, not inside a linked worktree. From a linked worktree, resolve these paths against the main root via `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`. Fall back to `pwd` if not a git repo.
87
- - From a linked worktree, every `Edit` or `Write` to a tracked file (source, docs) must use a path starting with `pwd`. Only shared session scratch (`.claude/plans/`, `.claude/review/`, `.claude/memory/`, `.claude/tasks/`) resolves to the main worktree root.
87
+ - From a linked worktree, every `Edit` or `Write` to a tracked file (source, docs) must use a path starting with `pwd`.
88
+ - From a linked worktree, `Edit` and `Write` are refused for every main-root path, session scratch included. The refusal names session isolation and points at the worktree copy, which is a second gitignored file no later session reads, so never take that redirect.
89
+ - `Read` resolves against the main root normally from a linked worktree. A main-root write reaches it only through `Bash`, as one plain command rather than a compound one, which is refused for complexity.