@erclx/aitk 0.27.0 → 0.29.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/README.md CHANGED
@@ -6,7 +6,9 @@
6
6
 
7
7
  One source for your repos' AI conventions. Install once, sync everywhere.
8
8
 
9
- ![Adding the aitk marketplace and installing the plugin in Claude Code](assets/install.png)
9
+ ![The aitk catalog, listing skills, governance rules, and standards with the count each ships and a sample of their names](assets/hero.png)
10
+
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.
10
12
 
11
13
  ## Install
12
14
 
@@ -17,6 +19,8 @@ claude plugin marketplace add https://github.com/erclx/aitk
17
19
  claude plugin install aitk@aitk
18
20
  ```
19
21
 
22
+ ![Adding the aitk marketplace and installing the plugin in Claude Code](assets/install.png)
23
+
20
24
  The skills land as `/aitk:<name>`. If your session was already open, run `/reload-plugins` to pick them up. Updates arrive on release, so a push to this repo won't reach your installed copy.
21
25
 
22
26
  Several skills call the `aitk` CLI to read catalogs and run installs, and the plugin doesn't put it on your path. Install it from the registry.
@@ -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.27.0",
4
+ "version": "0.29.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
package/docs/agents.md CHANGED
@@ -93,7 +93,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
93
93
  | `snippets` | `list`, `install`, `sync`, `create` |
94
94
  | `standards` | `list`, `install`, `sync` |
95
95
  | `gov` | `list`, `install`, `sync`, `build` |
96
- | `claude` | `init`, `sync`, `seeds list`, `setup [dest]` |
96
+ | `claude` | `init`, `sync`, `seeds list`, `skills list`, `setup [dest]` |
97
97
  | `wiki` | `init` |
98
98
  | `design` | `render` |
99
99
  | `slides` | `render`, `list` |
@@ -327,6 +327,8 @@ aitk capture assets/install.html
327
327
  aitk capture assets --out .claude/review/captures
328
328
  ```
329
329
 
330
+ `assets/` now holds two sources, so a bare `aitk capture` rebuilds both. `install.html` is hand-authored and its text came from a real run. `hero.html` is generated by `scripts/core/regen-hero.sh` from the CLI catalogs and must not be edited by hand, since `bun run check` regenerates it and fails on the difference. Only the HTML is asserted for drift. The PNG is a chromium render whose bytes move with the browser version, so rebuild it with `aitk capture assets/hero.html` when the check reports the HTML changed.
331
+
330
332
  | Option | Behavior |
331
333
  | ------------------ | ------------------------------------------------- |
332
334
  | `--out <dir>` | Write every PNG here instead of beside its source |
@@ -407,14 +409,15 @@ What remains is a sentence naming a hypothetical entry to show the shape of a na
407
409
 
408
410
  Use these to discover what's available instead of hardcoding names.
409
411
 
410
- | Command | Returns |
411
- | ------------------------------- | -------------------------------------------- |
412
- | `aitk tooling list --json` | Stacks, extends chain, dep and script counts |
413
- | `aitk snippets list --json` | Presets and categories with their slugs |
414
- | `aitk standards list --json` | Standards docs and the paths each governs |
415
- | `aitk gov list --json` | Governance stacks and rule sets |
416
- | `aitk claude seeds list --json` | Seed doc sources with content |
417
- | `aitk docs list --json` | Consumer docs plus per-domain context |
412
+ | Command | Returns |
413
+ | -------------------------------- | -------------------------------------------- |
414
+ | `aitk tooling list --json` | Stacks, extends chain, dep and script counts |
415
+ | `aitk snippets list --json` | Presets and categories with their slugs |
416
+ | `aitk standards list --json` | Standards docs and the paths each governs |
417
+ | `aitk gov list --json` | Governance stacks and rule sets |
418
+ | `aitk claude seeds list --json` | Seed doc sources with content |
419
+ | `aitk claude skills list --json` | Plugin skills with their descriptions |
420
+ | `aitk docs list --json` | Consumer docs plus per-domain context |
418
421
 
419
422
  Every catalog serializes through `JSON.stringify`, so a name carrying a quote
420
423
  emits valid JSON. `aitk tooling list` and `aitk snippets list` previously built
@@ -433,6 +436,14 @@ listing and the install cannot disagree. It now reports
433
436
  `.claude/context/index.md`, which `init` has always installed and the listing
434
437
  never named, and it emits the project-level `CLAUDE.md` last rather than first.
435
438
 
439
+ `aitk claude skills list` reads `claude/skills/*/SKILL.md` and reports the folder
440
+ name with the frontmatter description, sorted by name. Internal skills under
441
+ `.claude/skills/` are excluded, since they never install into a target and a
442
+ count spanning both overstates what ships. A skill whose frontmatter is missing
443
+ or unparseable returns an empty description rather than failing the listing, so
444
+ one malformed file cannot hide the rest of the catalog. `--names` emits skill
445
+ names one per line.
446
+
436
447
  ## Non-interactive examples
437
448
 
438
449
  ```bash
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.27.0",
4
+ "version": "0.29.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bash
2
+ # Fills assets/hero.html.tmpl from the CLI catalogs and writes assets/hero.html.
3
+ #
4
+ # Only the HTML regenerates here. The PNG beside it is a chromium render whose
5
+ # bytes move with the browser version, so asserting it in verify.sh would fail
6
+ # on a machine whose chromium differs rather than on a stale count. Rebuild the
7
+ # image with `aitk capture assets/hero.html` after this script reports a change.
8
+ # The frame carries no version. `package.json` is bumped on main by the release
9
+ # tooling, so embedding it drifts every open branch on the next release and the
10
+ # stage then fails for work that touched nothing.
11
+ #
12
+ # Clone-only. `src/capture` is excluded from the published tarball, and this
13
+ # script reads the repository's own catalogs, so a registry install has neither.
14
+ set -e
15
+ set -o pipefail
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
19
+
20
+ TEMPLATE="$PROJECT_ROOT/assets/hero.html.tmpl"
21
+ OUTPUT="$PROJECT_ROOT/assets/hero.html"
22
+ LISTED=10
23
+
24
+ # `bun src/cli.ts` rather than `aitk`, since a globally linked binary resolves to
25
+ # the main checkout no matter which worktree is running.
26
+ catalog() {
27
+ (cd "$PROJECT_ROOT" && AITK_NON_INTERACTIVE=1 bun src/cli.ts "$@" --json 2>/dev/null)
28
+ }
29
+
30
+ if [ ! -f "$TEMPLATE" ]; then
31
+ echo "regen-hero: missing template at $TEMPLATE" >&2
32
+ exit 1
33
+ fi
34
+
35
+ SKILLS_JSON="$(catalog claude skills list)"
36
+ GOV_JSON="$(catalog gov list)"
37
+ STANDARDS_JSON="$(catalog standards list)"
38
+ SNIPPETS_JSON="$(catalog snippets list)"
39
+ TOOLING_JSON="$(catalog tooling list)"
40
+
41
+ for payload in "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$SNIPPETS_JSON" "$TOOLING_JSON"; do
42
+ if [ -z "$payload" ]; then
43
+ echo "regen-hero: a catalog returned nothing, refusing to write a zeroed hero" >&2
44
+ exit 1
45
+ fi
46
+ done
47
+
48
+ export SKILLS_JSON GOV_JSON STANDARDS_JSON SNIPPETS_JSON TOOLING_JSON
49
+ export TEMPLATE OUTPUT LISTED PROJECT_ROOT
50
+
51
+ bun --eval '
52
+ const {
53
+ SKILLS_JSON, GOV_JSON, STANDARDS_JSON, SNIPPETS_JSON, TOOLING_JSON,
54
+ TEMPLATE, OUTPUT, LISTED, PROJECT_ROOT,
55
+ } = process.env
56
+
57
+ const listed = Number(LISTED)
58
+ const skills = JSON.parse(SKILLS_JSON).skills.map((entry) => entry.name)
59
+ const gov = JSON.parse(GOV_JSON)
60
+ // Rule names carry a numeric prefix that orders the load, not the identity a
61
+ // reader knows them by, so the frame shows the slug alone.
62
+ const rules = gov.rules.map((entry) => entry.name.replace(/^\d+-/, ""))
63
+ const standards = JSON.parse(STANDARDS_JSON).standards.map((entry) => entry.name)
64
+ const snippets = new Set(
65
+ JSON.parse(SNIPPETS_JSON).categories.flatMap((category) => category.entries),
66
+ )
67
+ const toolingStacks = JSON.parse(TOOLING_JSON).stacks
68
+
69
+ const escape = (value) =>
70
+ value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
71
+
72
+ // Even spacing across the sorted catalog rather than its first N. The skill
73
+ // names are prefixed by domain, so an alphabetical head returns eight
74
+ // `claude-*` entries and reads as a narrower catalog than the one that ships.
75
+ const sample = (names) => {
76
+ if (names.length <= listed) return names
77
+ const step = names.length / listed
78
+ return Array.from({ length: listed }, (_, i) => names[Math.floor(i * step)])
79
+ }
80
+
81
+ const entries = (names) =>
82
+ sample(names)
83
+ .map((name) => ` <div class="entry">${escape(name)}</div>`)
84
+ .join("\n")
85
+
86
+ const remaining = (names) => String(Math.max(0, names.length - listed))
87
+
88
+ // An empty array is well-formed JSON, so the shell guard on an empty payload
89
+ // does not catch a catalog whose tree went missing. A zeroed count would ship a
90
+ // frame claiming the domain has nothing in it.
91
+ for (const [label, list] of [
92
+ ["skills", skills], ["rules", rules], ["standards", standards],
93
+ ["snippets", [...snippets]], ["tooling stacks", toolingStacks], ["gov stacks", gov.stacks],
94
+ ]) {
95
+ if (list.length === 0) {
96
+ console.error(`regen-hero: the ${label} catalog is empty, refusing to write a zeroed hero`)
97
+ process.exit(1)
98
+ }
99
+ }
100
+
101
+ const values = {
102
+ SKILL_COUNT: String(skills.length),
103
+ RULE_COUNT: String(rules.length),
104
+ STANDARD_COUNT: String(standards.length),
105
+ SNIPPET_COUNT: String(snippets.size),
106
+ GOV_STACK_COUNT: String(gov.stacks.length),
107
+ TOOLING_STACK_COUNT: String(toolingStacks.length),
108
+ SKILL_ENTRIES: entries(skills),
109
+ RULE_ENTRIES: entries(rules),
110
+ STANDARD_ENTRIES: entries(standards),
111
+ SKILL_MORE: remaining(skills),
112
+ RULE_MORE: remaining(rules),
113
+ STANDARD_MORE: remaining(standards),
114
+ }
115
+
116
+ let html = await Bun.file(TEMPLATE).text()
117
+ for (const [key, value] of Object.entries(values)) {
118
+ html = html.replaceAll(`{{${key}}}`, value)
119
+ }
120
+
121
+ const unresolved = html.match(/{{[A-Z_]+}}/g)
122
+ if (unresolved) {
123
+ console.error(`regen-hero: unresolved placeholders ${[...new Set(unresolved)].join(", ")}`)
124
+ process.exit(1)
125
+ }
126
+
127
+ await Bun.write(OUTPUT, html)
128
+ '
@@ -99,6 +99,23 @@ collect_plugin_manifests() {
99
99
  } | sort -u
100
100
  }
101
101
 
102
+ # The drift assert covers the HTML because the PNG is a chromium render whose
103
+ # bytes move with the browser. That leaves the artifact a visitor actually sees
104
+ # asserted nowhere, so a branch that regenerates the HTML and never runs the
105
+ # capture passes every stage while shipping an image with the old counts.
106
+ #
107
+ # The two files move together or the image is stale, so their last-touching
108
+ # commit is the same commit. Comparing the branch's file list instead would pass
109
+ # any branch that touched both anywhere, including one that regenerated the HTML
110
+ # alone in a later commit. Both absent resolves to two empty strings and passes,
111
+ # which is correct for a tree that carries neither.
112
+ assert_hero_pair() {
113
+ local html_commit png_commit
114
+ html_commit=$(git -C "$PROJECT_ROOT" log -1 --format=%H -- assets/hero.html)
115
+ png_commit=$(git -C "$PROJECT_ROOT" log -1 --format=%H -- assets/hero.png)
116
+ [ "$html_commit" = "$png_commit" ]
117
+ }
118
+
102
119
  assert_no_drift() {
103
120
  local paths=$1
104
121
  local err_msg=$2
@@ -135,6 +152,15 @@ main() {
135
152
  assert_no_drift ".claude/standards .claude/snippets .claude/internal .claude/rules" "Consumed copies drifted. Run bun run check and commit .claude/standards, .claude/snippets, .claude/internal, and .claude/rules."
136
153
  log_info "Consumed copies clean"
137
154
 
155
+ # Only the HTML is asserted. The PNG beside it is a chromium render whose bytes
156
+ # move with the browser version, so a drift check over it would fail on a
157
+ # machine whose chromium differs rather than on a stale count.
158
+ log_step "Hero"
159
+ run_check "bash $PROJECT_ROOT/scripts/core/regen-hero.sh" "Hero regen failed"
160
+ assert_no_drift "assets/hero.html" "Hero counts drifted. Run bun run check, then aitk capture assets/hero.html, and commit assets/hero.html with assets/hero.png."
161
+ run_check "assert_hero_pair" "Hero HTML and image last moved in different commits, so the image may be stale. Run aitk capture assets/hero.html and commit both files together."
162
+ log_info "Hero clean"
163
+
138
164
  log_step "Skill references"
139
165
  run_check "bash $PROJECT_ROOT/scripts/core/regen-skill-references.sh" "Skill-reference regen failed"
140
166
  assert_no_drift "claude/skills/*/references" "Skill references drifted. Run bun run check and commit the updated reference files."
@@ -0,0 +1,53 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---/
5
+
6
+ export interface SkillListing {
7
+ readonly name: string
8
+ readonly description: string
9
+ }
10
+
11
+ /**
12
+ * Enumerates the plugin skill catalog, which is the corpus under `claude/`
13
+ * rather than the internal skills under `.claude/`. Only the former installs
14
+ * into a target, so a count taken across both overstates what ships.
15
+ *
16
+ * The folder name wins over the frontmatter `name` when they disagree, because
17
+ * Claude Code invokes a skill by its directory.
18
+ */
19
+ export function listSkills(root: string): SkillListing[] {
20
+ const skillsRoot = join(root, 'claude', 'skills')
21
+ if (!existsSync(skillsRoot)) return []
22
+
23
+ const paths = [
24
+ ...new Bun.Glob('*/SKILL.md').scanSync({
25
+ cwd: skillsRoot,
26
+ onlyFiles: true,
27
+ }),
28
+ ].sort()
29
+
30
+ return paths.map((path) => ({
31
+ name: dirname(path),
32
+ description: readDescription(join(skillsRoot, path)),
33
+ }))
34
+ }
35
+
36
+ /**
37
+ * Returns an empty description rather than throwing on a skill whose
38
+ * frontmatter is missing or unparseable, so one malformed file does not hide
39
+ * the rest of the catalog from a caller counting it.
40
+ */
41
+ function readDescription(path: string): string {
42
+ const match = FRONTMATTER.exec(readFileSync(path, 'utf8'))
43
+ if (!match) return ''
44
+
45
+ try {
46
+ const parsed = Bun.YAML.parse(match[1])
47
+ if (typeof parsed !== 'object' || parsed === null) return ''
48
+ const { description } = parsed as Record<string, unknown>
49
+ return typeof description === 'string' ? description : ''
50
+ } catch {
51
+ return ''
52
+ }
53
+ }
@@ -12,6 +12,7 @@ import {
12
12
  type Seed,
13
13
  } from '@/claude/seeds'
14
14
  import { listSeeds, readSeedContents } from '@/claude/seeds-list'
15
+ import { listSkills } from '@/claude/skills-list'
15
16
  import {
16
17
  planSettings,
17
18
  readSettings,
@@ -43,6 +44,11 @@ interface SeedsListOptions {
43
44
  readonly names?: boolean
44
45
  }
45
46
 
47
+ interface SkillsListOptions {
48
+ readonly json?: boolean
49
+ readonly names?: boolean
50
+ }
51
+
46
52
  const SEEDED_FILES: readonly string[] = [
47
53
  'ARCHITECTURE.md',
48
54
  'REQUIREMENTS.md',
@@ -130,6 +136,42 @@ export function register(program: Command): void {
130
136
  .action(async (opts: SeedsListOptions) => {
131
137
  process.exitCode = await runSeedsList(opts)
132
138
  })
139
+
140
+ const skills = claude
141
+ .command('skills')
142
+ .description('Plugin skill catalog (list)')
143
+ .argument('[subcommand]', "Only 'list' is supported")
144
+ .helpOption('-h, --help', 'Show this help message')
145
+ .action((subcommand: string | undefined) => {
146
+ intro('aitk claude')
147
+ logError(
148
+ subcommand === undefined
149
+ ? "Missing subcommand. Use 'list'."
150
+ : `Unknown subcommand: ${subcommand}. Use 'list'.`,
151
+ )
152
+ outro()
153
+ process.exitCode = 1
154
+ })
155
+
156
+ skills
157
+ .command('list')
158
+ .description('List the plugin skills shipped under claude/skills/')
159
+ .helpOption('-h, --help', 'Show this help message')
160
+ .option('--json', 'Emit JSON with name and description')
161
+ .option('--names', 'Only list skill names, one per line')
162
+ .addHelpText(
163
+ 'after',
164
+ [
165
+ '',
166
+ 'Notes:',
167
+ ' Internal skills under .claude/skills/ are excluded, since they',
168
+ ' never install into a target project.',
169
+ '',
170
+ ].join('\n'),
171
+ )
172
+ .action((opts: SkillsListOptions) => {
173
+ process.exitCode = runSkillsList(opts)
174
+ })
133
175
  }
134
176
 
135
177
  function succeed(message: string): number {
@@ -347,3 +389,27 @@ async function runSeedsList(opts: SeedsListOptions): Promise<number> {
347
389
  outro()
348
390
  return 0
349
391
  }
392
+
393
+ function runSkillsList(opts: SkillsListOptions): number {
394
+ const listings = listSkills(PROJECT_ROOT)
395
+
396
+ if (opts.json) {
397
+ process.stdout.write(`${JSON.stringify({ skills: listings })}\n`)
398
+ return 0
399
+ }
400
+
401
+ if (opts.names) {
402
+ process.stdout.write(
403
+ listings.map((listing) => listing.name).join('\n') + '\n',
404
+ )
405
+ return 0
406
+ }
407
+
408
+ intro('aitk claude')
409
+ logStep('Plugin skills')
410
+ for (const listing of listings) {
411
+ logInfo(listing.name)
412
+ }
413
+ outro()
414
+ return 0
415
+ }
@@ -19,6 +19,6 @@ Reference docs for consistent authoring across the toolkit and target projects.
19
19
  - [Claude skill reference](skill.md): Claude skill structure and authoring rules
20
20
  - [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
21
21
  - [Standard reference](standard.md): Shape and content rules for authoring a standard
22
- - [Tasks reference](tasks.md): Folder layout, filename convention, and content rules for .claude/tasks/
22
+ - [Tasks reference](tasks.md): Folder layout, filename convention, readiness groups, and content rules for .claude/tasks/
23
23
  - [Versioning reference](versioning.md): Phase label vs semver discipline across tasks, PRs, reviews, issues, commits, and tags
24
24
  - [Wireframe reference](wireframes.md): Shape and content rules for .claude/wireframes/<surface>.md files
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Tasks reference
3
- description: Folder layout, filename convention, and content rules for .claude/tasks/
3
+ description: Folder layout, filename convention, readiness groups, and content rules for .claude/tasks/
4
4
  ---
5
5
 
6
6
  # Tasks reference
@@ -11,7 +11,7 @@ The folder is gitignored. Board state changes when work ships rather than when a
11
11
 
12
12
  ## Scope
13
13
 
14
- Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, and archiving.
14
+ Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, execution ordering, and archiving.
15
15
 
16
16
  Does not govern:
17
17
 
@@ -34,11 +34,23 @@ One file per task is what keeps the board safe under parallel sessions. Two sess
34
34
 
35
35
  `index.md` is generated from sibling frontmatter. The folder is gitignored, so the whole-repo index walk skips it and a hook passing the changed path regenerates it instead. Never hand-edit it.
36
36
 
37
+ The `claude-tasks` skill creates and archives task files. `claude-docs` marks outcomes `[x]` in an existing file and sweeps the plans those tasks cite. Neither does the other's job.
38
+
39
+ ## Ordering
40
+
37
41
  `priority.md` carries execution order and what each task is waiting on. The generated index sorts by filename and says nothing about order, so without this file board state gets reconstructed by hand every session. Why the order is what it is belongs in `.claude/ROADMAP.md`, which is committed because that rationale has no substitute record.
38
42
 
39
- Group tasks by readiness rather than by status, one row per task, with a stated column for what each is blocked on. Keep it to links and blockers: tables, plus at most one sentence per section. A paragraph in `priority.md` is a defect whatever it says. Stating the shape this way is what lets a single diff fail, since a size cap only trips after the fact and every addition looks defensible on its own.
43
+ Group tasks by readiness rather than by status, one row per task, under the columns each group fixes below. Keep it to links and blockers: tables, plus at most one sentence per section. A paragraph in `priority.md` is a defect whatever it says. Stating the shape this way is what lets a single diff fail, since a size cap only trips after the fact and every addition looks defensible on its own.
40
44
 
41
- The `claude-tasks` skill creates and archives task files. `claude-docs` marks outcomes `[x]` in an existing file and sweeps the plans those tasks cite. Neither does the other's job.
45
+ Readiness is three groups under fixed headings, `## Run now`, `## Up next`, and `## Needs a plan`, in that order. The names are the contract rather than a suggestion, because a board grouped by readiness under names of its own satisfies every other rule here and still reads as empty to anything counting rows under a heading. Add no fourth group. A task belongs to exactly one, and the tests are read in order.
46
+
47
+ - `## Run now`: a written plan covers every open outcome, and the files the task touches collide with nothing already running. A worker is handed a task from this group alone.
48
+ - `## Up next`: a written plan exists, and the task either collides with something running or waits on another task to land. The blocker column names which.
49
+ - `## Needs a plan`: everything else. The task has no plan, or the plan it carries no longer describes the work.
50
+
51
+ Each group fixes its own columns, which follow from the test above it rather than from preference. `## Run now` carries `Task`, `Touches`, and `Plan`, since neither half of its test is checkable without the last two. `## Up next` carries `Task`, `Touches`, and `Waiting on`, the last naming whether a collision or a dependency holds it. `## Needs a plan` carries `Task` and `Waiting on` alone, because a task with no plan has no bounded file set to state.
52
+
53
+ The tests live here so the board does not carry them. Writing them as a sentence under each heading produces the paragraph the rule above deletes, and a criterion with no home gets restated from memory every time the board is touched.
42
54
 
43
55
  ## Filenames
44
56