@erclx/canon 5.4.0 → 5.6.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/bash-cli-script/REQUIREMENT.md +10 -9
- package/claude/skills/bash-cli-script/SKILL.md +10 -7
- package/claude/skills/bash-cli-script/references/template.md +4 -28
- package/claude/skills/codebase-layout/REQUIREMENT.md +3 -0
- package/claude/skills/codebase-layout/SKILL.md +28 -0
- package/claude/skills/codebase-layout/references/adopted.md +6 -0
- package/claude/skills/plan-feature/REQUIREMENT.md +4 -0
- package/claude/skills/plan-feature/SKILL.md +4 -0
- package/claude/skills/read-frames/REQUIREMENT.md +1 -0
- package/claude/skills/read-frames/SKILL.md +1 -0
- package/claude/skills/teach-workspace/REQUIREMENT.md +3 -0
- package/claude/skills/teach-workspace/SKILL.md +6 -5
- package/claude/skills/teach-workspace/references/lesson-craft.md +8 -1
- package/claude/skills/test-craft/REQUIREMENT.md +4 -1
- package/claude/skills/test-craft/SKILL.md +20 -2
- package/claude/skills/test-craft/references/adopted.md +3 -1
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -0
- package/claude/skills/ui-checklist/SKILL.md +11 -1
- package/claude/skills/ux-measure/REQUIREMENT.md +2 -1
- package/claude/skills/ux-measure/SKILL.md +3 -1
- package/claude/skills/ux-walkthrough/REQUIREMENT.md +3 -0
- package/claude/skills/ux-walkthrough/SKILL.md +1 -0
- package/docs/agents/architecture-record.md +5 -3
- package/docs/agents/context-audit-checks.md +1 -1
- package/docs/agents/context-audit.md +3 -3
- package/docs/agents/index.md +3 -3
- package/docs/agents/skills-audit.md +6 -3
- package/docs/agents/teach-authoring.md +3 -3
- package/docs/agents/teach.md +11 -5
- package/docs/workflow/skill-map.md +1 -2
- package/governance/rules/lang/120-bash.md +4 -5
- package/package.json +1 -1
- package/src/audits/catalog.ts +7 -1
- package/src/claude/cases/authoring.ts +0 -5
- package/src/claude/skills-audit.ts +81 -1
- package/src/commands/claude.ts +33 -0
- package/src/commands/context.ts +25 -2
- package/src/commands/serve.ts +2 -92
- package/src/commands/teach.ts +142 -17
- package/src/context/architecture.ts +82 -5
- package/src/context/gate.ts +10 -3
- package/src/gate/measures.ts +52 -5
- package/src/gate/stages.ts +10 -1
- package/src/serve/report.ts +110 -0
- package/src/teach/nav.ts +12 -1
- package/standards/architecture.md +14 -3
- package/standards/index.md +1 -0
- package/standards/skill-practice.md +51 -0
- package/standards/skill.md +3 -1
- package/claude/skills/bash-script/REQUIREMENT.md +0 -36
- package/claude/skills/bash-script/SKILL.md +0 -100
- package/claude/skills/bash-script/references/patterns.md +0 -250
- package/claude/skills/bash-script/references/prompts.md +0 -100
|
@@ -65,7 +65,7 @@ Fenced blocks are excluded, which keeps a pinned version in an install command f
|
|
|
65
65
|
|
|
66
66
|
## The architecture record
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Five findings read `canon/ARCHITECTURE.md` rather than a folder: an entry cap, a revisit sentence closing each decision, and a length ceiling, each gating only where the record states it for itself, a claim coverage report, and a word count. What each reads and why the last two gate nothing are in `architecture-record.md`.
|
|
69
69
|
|
|
70
70
|
## Wireframe states
|
|
71
71
|
|
|
@@ -5,7 +5,7 @@ description: Running the audit, its flags and folder scope, the exit codes, the
|
|
|
5
5
|
|
|
6
6
|
# Context audit
|
|
7
7
|
|
|
8
|
-
`canon context audit [path]` reports the structural state of the folders following the index-plus-entry contract, meaning a generated `index.md` beside entries carrying frontmatter, and it measures `canon/ARCHITECTURE.md` beside them. It reads and reports. Fixing what it finds is separate work. What each finding means is in `context-audit-checks.md`, and what the architecture record's
|
|
8
|
+
`canon context audit [path]` reports the structural state of the folders following the index-plus-entry contract, meaning a generated `index.md` beside entries carrying frontmatter, and it measures `canon/ARCHITECTURE.md` beside them. It reads and reports. Fixing what it finds is separate work. What each finding means is in `context-audit-checks.md`, and what the architecture record's five findings mean is in `architecture-record.md`.
|
|
9
9
|
|
|
10
10
|
Findings stated over every markdown file rather than over a context entry are measured by `canon markdown audit`, described in `markdown-audit.md`. That command resolves no folder, so it reaches trees this one refuses.
|
|
11
11
|
|
|
@@ -37,9 +37,9 @@ A run where no requested name resolves refuses, whichever list it read. Naming t
|
|
|
37
37
|
|
|
38
38
|
## Exit codes
|
|
39
39
|
|
|
40
|
-
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only`. A record that states an entry cap gates the same way when it holds more decisions than the cap, and a record stating
|
|
40
|
+
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only`. A record that states an entry cap gates the same way when it holds more decisions than the cap, and one stating that every decision closes with a revisit sentence gates when a decision lacks it. A record stating none of the three is reported and never gated.
|
|
41
41
|
|
|
42
|
-
`bun run check` in the toolkit repository reads
|
|
42
|
+
`bun run check` in the toolkit repository reads all three of the record's rules through its own Architecture record stage rather than through this verb, since the verb's only gating stage there runs `--citations-only` and never opens the record.
|
|
43
43
|
|
|
44
44
|
Entry length, reference form, table, provenance, the record's claim classification, and every word figure print and return `0` under every mode, because each is a judgment or a weight read alongside one, and failing a push on either would make the check something to route around.
|
|
45
45
|
|
package/docs/agents/index.md
CHANGED
|
@@ -8,7 +8,7 @@ category: Agent surface
|
|
|
8
8
|
|
|
9
9
|
CLI catalog and invocation rules for agents, split by command domain. Start with overview.
|
|
10
10
|
|
|
11
|
-
- [Architecture record](architecture-record.md): The
|
|
11
|
+
- [Architecture record](architecture-record.md): The five findings canon context audit reads off the architecture record, being the entry cap, revisit sentence, and length gates the record states for itself, the claim coverage report, and the word counts
|
|
12
12
|
- [Audits](audits.md): Running every health check as one set, what the single verdict means, the exit code each outcome takes, the retained baseline and the delta it reports, and which corpora are kept out of the record
|
|
13
13
|
- [Capture](capture.md): Rendering HTML sources to PNG, what the command asserts about fonts, and why the selector has no default
|
|
14
14
|
- [Census](census.md): Tracked-plus-untracked file count, a breakdown by extension, and a line total that skips whatever reads as binary
|
|
@@ -50,7 +50,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
50
50
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
|
51
51
|
- [Sessions transfer](sessions-transfer.md): Moving a session to another machine, the export bundle and its manifest, placing it with import, the refusal reasons, and what a resume on the target depends on
|
|
52
52
|
- [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, reading which row is the caller, the liveness confidence field, what each session surface can see, and where moving a session to another machine is described
|
|
53
|
-
- [Skill audit](skills-audit.md): Measuring both skill corpora against standards
|
|
53
|
+
- [Skill audit](skills-audit.md): Measuring both skill corpora against the skill standards, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
|
|
54
54
|
- [Citation reach](skills-reach.md): Reporting the skill bodies that cite a path no target project receives, which corpus the verb reads, the ownership key that decides what counts, the one-word qualifier that marks a citation as decided, and why the verb reports instead of gating
|
|
55
55
|
- [Standard success criteria](standards-audit.md): Reading the corpus against the success-criterion gate, the two headings it accepts, why the check scopes to arrival rather than the whole corpus, and the exit codes it sets
|
|
56
56
|
- [State-scoped risk](state-scoped-risk.md): Reading committed state rather than an arriving change, the shipped-tree corpus the secret scan reads, what it keys on and how a deliberate value is exempted, the advisory check and its network failure mode, and why one gates while the other reports
|
|
@@ -61,6 +61,6 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
61
61
|
- [Tasks validate](tasks-validate.md): Validating the board against the tree, the seven checks, the untested, claims, and unplaced arrays, the exit codes, and the refusal reasons
|
|
62
62
|
- [Tasks](tasks.md): Claiming the next phase label, recording a pull request number and closing an outcome, listing each task's readiness, and where archiving, the plan verbs, and board validation are described
|
|
63
63
|
- [Teach authoring](teach-authoring.md): Resolving what the next lesson needs before it is written, rendering a lesson body's blocks to HTML, and rewriting the root listing, a contents page, and each lesson's chrome and quiz stepper from its marker regions
|
|
64
|
-
- [Teach](teach.md): Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, where the lesson authoring verbs are described, the refusal reasons, and why every write here runs through a verb
|
|
64
|
+
- [Teach](teach.md): Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, viewing one with its chrome refreshed, where the lesson authoring verbs are described, the refusal reasons, and why every write here runs through a verb
|
|
65
65
|
- [Test order](test-order.md): Reading where an implementation reached history ahead of its test, how a pair is decided, the three verdicts, the coverage the pairing cannot reach, and why the check reports rather than gates
|
|
66
66
|
- [Worktrees](worktrees.md): Reporting which worktrees are reclaimable, removing the ones that are, the record a hook reads back, why the reading keys on the pull request rather than on git ancestry, the refusals it names, and the two removal shapes
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Skill audit
|
|
3
|
-
description: Measuring both skill corpora against standards
|
|
3
|
+
description: Measuring both skill corpora against the skill standards, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Skill audit
|
|
7
7
|
|
|
8
|
-
`canon claude skills audit [path]` reports both skill corpora against the rules `standards/skill.md` and `standards/skill-
|
|
8
|
+
`canon claude skills audit [path]` reports both skill corpora against the rules `standards/skill.md`, `standards/skill-requirement.md`, and `standards/skill-practice.md` state mechanically. It reads and reports. Fixing what it finds is separate work.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
canon claude skills audit
|
|
@@ -36,6 +36,7 @@ Each check traces to a line in the standard.
|
|
|
36
36
|
- Folder name in kebab-case
|
|
37
37
|
- Each `REQUIREMENT.md` declaring `Gap` and `Must`, matched at any heading level
|
|
38
38
|
- No ISO date in `SKILL.md` or any file under `references/`, reported as `datedProvenance` in the JSON record's `findings`
|
|
39
|
+
- Each listed practice skill closing with `## Excuses and rebuttals`, `## Red flags`, and `## Before handing over`, and carrying `references/adopted.md`, reported as `practiceShape`
|
|
39
40
|
|
|
40
41
|
A body whose frontmatter does not parse reports as declaring neither field rather than ending the run, so one malformed file cannot hide the corpus behind it. A key present with an empty value reads as absent, since a blank `name` would otherwise report as a name disagreeing with every folder. A folder carrying no requirement is reported once under presence rather than counted again for the sections it therefore lacks.
|
|
41
42
|
|
|
@@ -43,6 +44,8 @@ The date check reads prose alone. A date inside a fence or a code span is exampl
|
|
|
43
44
|
|
|
44
45
|
Each finding names the file and the line, such as `claude/skills/<name>/references/<file>.md line 12: <date>`. A date after "measured" or "verified" reports like any other, unlike the context audit, because a skill body states the rule a session follows today rather than a reading of the tree.
|
|
45
46
|
|
|
47
|
+
The practice check reads only the skills the audit lists, which the JSON record carries as `checkpoints.practiceSkills`. The list names folders under the toolkit's shipped `claude/skills/` corpus alone, so a project's own skill is never read as a practice skill and a same-named folder under `.claude/skills/` produces no finding. Where `claude/skills/` resolves, a listed folder it does not hold reports as a missing skill, so a renamed or misspelled entry cannot pass unread. Each closing section matches only as an exact H2 outside a fence, so an H3 or a heading carrying more words reports as missing.
|
|
48
|
+
|
|
46
49
|
## What it leaves alone
|
|
47
50
|
|
|
48
51
|
The report names its own blind spots on every run, including the run where everything passed. Whether each `Must` traces to a stated gap is the rule in the requirement standard worth the most and no parser reads it. Whether a gap states an observed failure rather than an intent, and whether a description routes, are the same kind of judgment. The 150-line body checkpoint is the one mechanical rule still absent here, and adding it would print a count rather than a defect, since the standard makes it a prompt to look with nothing enforcing it.
|
|
@@ -53,7 +56,7 @@ A check with no rule behind it prints an opinion as a defect, which is where the
|
|
|
53
56
|
|
|
54
57
|
## Exit codes
|
|
55
58
|
|
|
56
|
-
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a skill folder carrying no `REQUIREMENT.md`. Only presence sets a failing code. Name, description, folder, requirement-section,
|
|
59
|
+
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a skill folder carrying no `REQUIREMENT.md`. Only presence sets a failing code. Name, description, folder, requirement-section, dated-provenance, and practice shape findings print and return `0`, because each is a judgment a reader settles and failing a push on one would make the check something to route around. In the toolkit, two `bun run check` stages read the same report in-process and fail a push on dated provenance and on practice shape, since its own corpus is held at zero on both.
|
|
57
60
|
|
|
58
61
|
## The requirement gate
|
|
59
62
|
|
|
@@ -30,7 +30,7 @@ It reports three things. `lesson` is the numbered path the lesson takes, derived
|
|
|
30
30
|
|
|
31
31
|
A lesson written against this record carries its chrome as four empty marker pairs rather than hand-composed markup: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. `canon teach nav`, below, fills them from what the workspace holds on disk.
|
|
32
32
|
|
|
33
|
-
`canon teach stylesheet <topic>` writes the pair every lesson in the workspace renders through, seeded from the design source so a workspace renders in the system every other surface does. The two halves are written on different terms. `assets/base.css` holds the generated design layer and is rewritten on every run,
|
|
33
|
+
`canon teach stylesheet <topic>` writes the pair every lesson in the workspace renders through, seeded from the design source so a workspace renders in the system every other surface does. The two halves are written on different terms. `assets/base.css` holds the generated design layer and is rewritten on every run, so a change to the shared chrome reaches a workspace that already exists. `canon teach nav` and `canon teach up` rewrite it too. `assets/course.css` is the half a workspace owns, so it refuses to overwrite, reporting `written` as false where the workspace already carries one, and `--force` takes the seed back over it. Each workspace used to carry a hand-authored palette, which is how the course styling forked once per workspace, so a lesson adds its own rules under the seed and reaches a value through its custom property rather than restating the hex.
|
|
34
34
|
|
|
35
35
|
Each `quiz` entry carries `order`, the authored option indices in presentation order where index `0` is the correct answer, and `answer`, the one-based position that answer lands in. Both travel together because a caller deriving the position itself is a caller that can derive it wrongly.
|
|
36
36
|
|
|
@@ -50,7 +50,7 @@ echo '[{"type":"heading","level":1,"text":"Compass bearings"}]' | canon teach re
|
|
|
50
50
|
|
|
51
51
|
It reads a JSON array of blocks from stdin, each a `heading`, `paragraph`, `list`, `refs`, `code`, or `raw` block, and reports `{ ok: true, html }` on `--json` or the bare rendered HTML on stdout otherwise. Content none of the components can express takes a `raw` block, carrying its own HTML verbatim and unescaped, which is the shape the quiz and the teach-back block travel in.
|
|
52
52
|
|
|
53
|
-
A `code` block carries `text` and an optional `lang`, a highlight.js name or alias. The source is highlighted at render time into `hljs-*` class spans the workspace stylesheet colors in both themes, so the page carries no script. A `lang` the renderer does not register renders as plain escaped code, and one that does not match `^[a-z0-9+#-]+$` is refused. A workspace seeded before the syntax colors existed shows the spans uncolored until `canon teach stylesheet <topic>` rewrites its base stylesheet.
|
|
53
|
+
A `code` block carries `text` and an optional `lang`, a highlight.js name or alias. The source is highlighted at render time into `hljs-*` class spans the workspace stylesheet colors in both themes, so the page carries no script. A `lang` the renderer does not register renders as plain escaped code, and one that does not match `^[a-z0-9+#-]+$` is refused. A workspace seeded before the syntax colors existed shows the spans uncolored until `canon teach nav`, `canon teach up`, or `canon teach stylesheet <topic>` rewrites its base stylesheet.
|
|
54
54
|
|
|
55
55
|
```json
|
|
56
56
|
[{ "type": "code", "lang": "ts", "text": "const a: number = 1" }]
|
|
@@ -97,7 +97,7 @@ It also deletes two links a session used to write by hand into a lesson's `<head
|
|
|
97
97
|
|
|
98
98
|
A workspace carrying no `course.css` gets its stylesheet pair seeded first, so the contents page it links is never unstyled, and an existing `course.css` is never overwritten. The `style` region links `assets/base.css` ahead of an embedded copy of `course.css`, so the workspace's own rules still win the cascade. The embedded copy drops the seed's `@import` line, which resolved against the lesson's own folder and never loaded anything, and the link is what reaches the base sheet in its place. A workspace with no `assets/base.css`, one holding a hand-authored `course.css` from before the seed, gets no link rather than one pointing at a missing file.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Every run also rewrites `assets/base.css` wherever the workspace already holds one, so a change to the shared chrome reaches an existing workspace through this verb as well as through `canon teach stylesheet <topic>`. `course.css` is still never rewritten. A legacy workspace holding only a `course.css` is not handed a `base.css`, since its lessons were written without the link. `canon teach up` runs this pass before serving, which is the route that keeps a viewed workspace current.
|
|
101
101
|
|
|
102
102
|
A lesson missing one of the four marker pairs is refused by name rather than rewritten, and every other lesson in the run still rewrites. The record's `skipped` list carries the refused files and which marker each is missing.
|
|
103
103
|
|
package/docs/agents/teach.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Teach
|
|
3
|
-
description: Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, where the lesson authoring verbs are described, the refusal reasons, and why every write here runs through a verb
|
|
3
|
+
description: Listing learning workspaces with what their records schedule next, opening one with its required files, recording sources and glossary terms, viewing one with its chrome refreshed, where the lesson authoring verbs are described, the refusal reasons, and why every write here runs through a verb
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Teach
|
|
@@ -116,15 +116,19 @@ The entry lands as the standard's shape, leading with the term as a bolded span.
|
|
|
116
116
|
|
|
117
117
|
## Opening a workspace
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
`canon teach up` runs the `nav` pass and then serves the teach root, so the page a reader opens carries the chrome the installed CLI generates:
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
canon
|
|
122
|
+
canon teach up 03-fde-system-design --json
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
The link opens the named workspace's contents page, or the root listing when no topic is named. The verb serves the teach folder at its absolute path, so a run from a linked worktree still reaches the main worktree root. `--port` sets the first port tried and `--root` the teach root, as on every other verb. It runs until interrupted, so a session starts it in the background and reads the record off stdout.
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
The JSON record is one line carrying the `nav` fields, then `served`, the absolute folder, and the server's `host`, `port`, `entry`, `url`, and `entryExists`. A lesson `nav` skips lands in `skipped` and the server still starts, since a partial refresh is still worth viewing. A `nav` refusal starts no server.
|
|
128
|
+
|
|
129
|
+
Viewing writes. Every page and generated stylesheet `nav` owns is rewritten first, which on a committed fixture shows as a diff. To serve without rewriting anything, `canon teach list <topic>` ends its human output with a `canon serve` line for that workspace, with the teach folder written relative to the cwd, or absolute when it sits outside it. The JSON record of `list` carries no such field.
|
|
130
|
+
|
|
131
|
+
`canon serve` itself stays general, since nothing about serving a directory is specific to a learning workspace and the same verb carries a slide render and a design preview. What `up` adds in front of it is the refresh, which is teach-specific, and it serves through the same server rather than a second one.
|
|
128
132
|
|
|
129
133
|
Read `url` off the record rather than building one from the port that was asked for. The verb walks forward past a port already in use, which is routine when a second workspace is already open, and the port it took is the one thing a composed URL gets wrong.
|
|
130
134
|
|
|
@@ -142,4 +146,6 @@ Read `url` off the record rather than building one from the port that was asked
|
|
|
142
146
|
| `defined` | A term already carries a glossary entry |
|
|
143
147
|
| `bad-input` | The command line is malformed, before any folder is read |
|
|
144
148
|
|
|
149
|
+
`up` also passes through the refusals `canon serve` raises once `nav` has run, `no-port` when every port in reach is taken and `bind-failed` for any other bind error, such as a privileged port, in the same record shape with the server's detail as `message`.
|
|
150
|
+
|
|
145
151
|
A `bad-input` refusal reports the working directory as its root rather than the resolved one, since the command line is rejected before the root is worth resolving. `lesson` raises it for a slug that is not kebab-case, for a quiz carrying no question, and for a question carrying fewer than two options, since a question with one option has nothing to confuse the right answer with.
|
|
@@ -125,8 +125,7 @@ This page is the corpus the coverage claim is measured against: every name `cano
|
|
|
125
125
|
| `canon:draft-wiki` | For a brand-new wiki reference page on a subject owned outside the project, drafted against `standards/wiki.md` |
|
|
126
126
|
| `canon:draft-figure` | For a hand-drawn figure inside an existing doc, drafted against `standards/figures.md` in Mermaid or freehand SVG |
|
|
127
127
|
| `canon:draft-readme` | For a project's `README.md`, drafted against `standards/readme.md` |
|
|
128
|
-
| `canon:bash-script`
|
|
129
|
-
| `canon:bash-cli-script` | For a non-interactive automation, CI, or pipeline script |
|
|
128
|
+
| `canon:bash-cli-script` | For a small non-interactive shell wrapper, stopping at 100 lines in favor of TypeScript on Bun |
|
|
130
129
|
| `canon:ci-workflow` | For a GitHub Actions workflow file |
|
|
131
130
|
| `canon:draft-slides` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
|
|
132
131
|
| `canon:draft-screencast` | For a recording script with beats and defaults already seeded |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Route bash script authoring to the
|
|
2
|
+
description: Route bash script authoring to the wrapper skill, and name the lint gate
|
|
3
3
|
paths:
|
|
4
4
|
- '**/*.sh'
|
|
5
5
|
---
|
|
@@ -8,10 +8,9 @@ paths:
|
|
|
8
8
|
|
|
9
9
|
## Skill routing
|
|
10
10
|
|
|
11
|
-
- Use `canon:bash-script`
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- Report it rather than proceeding silently when the matched skill does not resolve. Both ship with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have them.
|
|
11
|
+
- Use `canon:bash-cli-script` before writing a script, and follow its guards on what a shell script may be.
|
|
12
|
+
- Load the skill's own reference template rather than hand-rolling logging patterns outside it.
|
|
13
|
+
- Report it rather than proceeding silently when the skill does not resolve. It ships with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have it.
|
|
15
14
|
|
|
16
15
|
## Lint gate
|
|
17
16
|
|
package/package.json
CHANGED
package/src/audits/catalog.ts
CHANGED
|
@@ -140,7 +140,7 @@ function allOf(
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* Reads the architecture record's
|
|
143
|
+
* Reads the architecture record's measures, or nothing when the project
|
|
144
144
|
* carries no record.
|
|
145
145
|
*
|
|
146
146
|
* Three states rather than two, matching what the verb publishes. The key
|
|
@@ -172,8 +172,11 @@ function architectureCounts(
|
|
|
172
172
|
const entryCap =
|
|
173
173
|
typeof record.entryCap === 'number' ? record.entryCap : undefined
|
|
174
174
|
|
|
175
|
+
const revisitRequired = record.revisitRequired === true
|
|
176
|
+
|
|
175
177
|
let unverifiable = 0
|
|
176
178
|
let unchecked = 0
|
|
179
|
+
let lackingRevisit = 0
|
|
177
180
|
for (const raw of decisions) {
|
|
178
181
|
const entry = asObject(raw)
|
|
179
182
|
const claim = entry?.claim
|
|
@@ -182,6 +185,7 @@ function architectureCounts(
|
|
|
182
185
|
|
|
183
186
|
if (claim === 'neither') unverifiable += 1
|
|
184
187
|
else if (checks === 0) unchecked += 1
|
|
188
|
+
if (revisitRequired && entry?.revisit !== true) lackingRevisit += 1
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
return {
|
|
@@ -195,6 +199,8 @@ function architectureCounts(
|
|
|
195
199
|
...(entryCap !== undefined && {
|
|
196
200
|
recordOverCount: decisions.length > entryCap ? 1 : 0,
|
|
197
201
|
}),
|
|
202
|
+
// Absent on a record stating no revisit clause, for the same reason.
|
|
203
|
+
...(revisitRequired && { recordMissingRevisit: lackingRevisit }),
|
|
198
204
|
recordUnverifiable: unverifiable,
|
|
199
205
|
recordUnchecked: unchecked,
|
|
200
206
|
}
|
|
@@ -5,11 +5,6 @@ import type { SkillCase } from '@/claude/skills-rank'
|
|
|
5
5
|
* skills that revise rather than generate.
|
|
6
6
|
*/
|
|
7
7
|
export const AUTHORING_CASES: readonly SkillCase[] = [
|
|
8
|
-
{
|
|
9
|
-
prompt:
|
|
10
|
-
'I need a shell tool with prompts and a nice terminal UI for people to run by hand.',
|
|
11
|
-
expect: 'bash-script',
|
|
12
|
-
},
|
|
13
8
|
{
|
|
14
9
|
prompt:
|
|
15
10
|
'Wire up a GitHub Actions pipeline with parallel jobs for this repo.',
|
|
@@ -12,16 +12,40 @@ export const DESCRIPTION_LIMIT = 1024
|
|
|
12
12
|
/** The two headings the requirement template declares, matched at any level. */
|
|
13
13
|
export const REQUIREMENT_SECTIONS: readonly string[] = ['Gap', 'Must']
|
|
14
14
|
|
|
15
|
+
/** The one tree that ships to a target, and the only one the practice list names. */
|
|
16
|
+
export const SHIPPED_CORPUS = join('claude', 'skills')
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Both trees the standard governs. `claude/skills/` ships to a target and
|
|
17
20
|
* `.claude/skills/` stays here, and every rule measured below applies to each,
|
|
18
21
|
* so a corpus reading one of them reports a pass over half the subject.
|
|
19
22
|
*/
|
|
20
23
|
export const CORPORA: readonly string[] = [
|
|
21
|
-
|
|
24
|
+
SHIPPED_CORPUS,
|
|
22
25
|
join('.claude', 'skills'),
|
|
23
26
|
]
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The practice skills `standards/skill-practice.md` describes, keyed by corpus-relative
|
|
30
|
+
* folder rather than by name, so a target's own `.claude/skills/` folder that
|
|
31
|
+
* shares a name is never swept in. The list lives here rather than in a skill's
|
|
32
|
+
* frontmatter because a skill declaring its own kind could exempt itself.
|
|
33
|
+
*/
|
|
34
|
+
export const PRACTICE_SKILLS: readonly string[] = [
|
|
35
|
+
join(SHIPPED_CORPUS, 'codebase-layout'),
|
|
36
|
+
join(SHIPPED_CORPUS, 'test-craft'),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
|
40
|
+
export const PRACTICE_SECTIONS: readonly string[] = [
|
|
41
|
+
'Excuses and rebuttals',
|
|
42
|
+
'Red flags',
|
|
43
|
+
'Before handing over',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
/** The source ledger every practice skill carries beside its body. */
|
|
47
|
+
export const PRACTICE_LEDGER = join('references', 'adopted.md')
|
|
48
|
+
|
|
25
49
|
/**
|
|
26
50
|
* The one reason this audit refuses. A project carrying neither corpus is the
|
|
27
51
|
* ordinary state of a target that has not adopted either skills convention,
|
|
@@ -34,6 +58,8 @@ const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
|
34
58
|
|
|
35
59
|
const HEADING = /^#{1,6}\s+(.+?)\s*$/
|
|
36
60
|
|
|
61
|
+
const SECTION = /^##\s+(.+?)\s*$/
|
|
62
|
+
|
|
37
63
|
/**
|
|
38
64
|
* An ISO date, read as provenance wherever a reader is told it. The context
|
|
39
65
|
* audit's detector clears a date after "measured" or "verified", and a skill
|
|
@@ -69,6 +95,7 @@ export interface SkillsAudit {
|
|
|
69
95
|
readonly longDescription: readonly SkillFinding[]
|
|
70
96
|
readonly requirementSections: readonly SkillFinding[]
|
|
71
97
|
readonly datedProvenance: readonly SkillFinding[]
|
|
98
|
+
readonly practiceShape: readonly SkillFinding[]
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
interface SkillSource {
|
|
@@ -79,6 +106,9 @@ interface SkillSource {
|
|
|
79
106
|
readonly description: string | undefined
|
|
80
107
|
/** Undefined when the folder carries no `REQUIREMENT.md` at all. */
|
|
81
108
|
readonly requirementHeadings: readonly string[] | undefined
|
|
109
|
+
/** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
|
|
110
|
+
readonly sections: readonly string[]
|
|
111
|
+
readonly hasLedger: boolean
|
|
82
112
|
/**
|
|
83
113
|
* `SKILL.md` and every reference below the folder, keyed by the path a
|
|
84
114
|
* finding names. `REQUIREMENT.md` and `EVAL.md` are left out, since the
|
|
@@ -134,6 +164,13 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
134
164
|
datedProvenance: sources.flatMap((source) =>
|
|
135
165
|
source.bodies.flatMap(dateFindings),
|
|
136
166
|
),
|
|
167
|
+
practiceShape: [
|
|
168
|
+
...missingPracticeSkills(
|
|
169
|
+
present.some((corpus) => corpus.rel === SHIPPED_CORPUS),
|
|
170
|
+
sources,
|
|
171
|
+
),
|
|
172
|
+
...sources.flatMap(practiceFindings),
|
|
173
|
+
],
|
|
137
174
|
}
|
|
138
175
|
}
|
|
139
176
|
|
|
@@ -190,6 +227,11 @@ async function readSkill(
|
|
|
190
227
|
description: declared(readField(fields, 'description')),
|
|
191
228
|
requirementHeadings:
|
|
192
229
|
requirement === undefined ? undefined : headings(requirement),
|
|
230
|
+
sections: bodyLines(body)
|
|
231
|
+
.filter((line) => !line.fenced)
|
|
232
|
+
.map((line) => SECTION.exec(line.text)?.[1])
|
|
233
|
+
.filter((text): text is string => text !== undefined),
|
|
234
|
+
hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
|
|
193
235
|
bodies: [
|
|
194
236
|
{ rel: join(skillRel, 'SKILL.md'), text: body },
|
|
195
237
|
...references.map((path, index) => ({
|
|
@@ -252,6 +294,44 @@ function sectionFindings(source: SkillSource): SkillFinding[] {
|
|
|
252
294
|
]
|
|
253
295
|
}
|
|
254
296
|
|
|
297
|
+
/**
|
|
298
|
+
* A listed folder the shipped corpus does not hold, such as a renamed skill or
|
|
299
|
+
* a misspelled append, would otherwise read as a pass. Silent where the
|
|
300
|
+
* shipped corpus is absent, since a target carries none of the listed skills.
|
|
301
|
+
*/
|
|
302
|
+
function missingPracticeSkills(
|
|
303
|
+
hasShippedCorpus: boolean,
|
|
304
|
+
sources: readonly SkillSource[],
|
|
305
|
+
): SkillFinding[] {
|
|
306
|
+
if (!hasShippedCorpus) return []
|
|
307
|
+
const found = new Set(sources.map((source) => source.rel))
|
|
308
|
+
return PRACTICE_SKILLS.filter((rel) => !found.has(rel)).map((rel) => ({
|
|
309
|
+
rel,
|
|
310
|
+
detail: 'missing skill: no folder under the shipped corpus',
|
|
311
|
+
}))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Matches each section as an exact H2, so a heading carrying trailing text or
|
|
316
|
+
* sitting at another level reads as missing rather than as a near match the
|
|
317
|
+
* standard never names.
|
|
318
|
+
*/
|
|
319
|
+
function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
320
|
+
if (!PRACTICE_SKILLS.includes(source.rel)) return []
|
|
321
|
+
|
|
322
|
+
const sections = PRACTICE_SECTIONS.filter(
|
|
323
|
+
(section) => !source.sections.includes(section),
|
|
324
|
+
).map((section) => ({
|
|
325
|
+
rel: source.rel,
|
|
326
|
+
detail: `missing section: ${section}`,
|
|
327
|
+
}))
|
|
328
|
+
const ledger = source.hasLedger
|
|
329
|
+
? []
|
|
330
|
+
: [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
|
|
331
|
+
|
|
332
|
+
return [...sections, ...ledger]
|
|
333
|
+
}
|
|
334
|
+
|
|
255
335
|
/**
|
|
256
336
|
* A date inside a fence or a code span is example data a reader is shown, such
|
|
257
337
|
* as a sample frontmatter block, so only prose outside both is read.
|
package/src/commands/claude.ts
CHANGED
|
@@ -26,7 +26,11 @@ import {
|
|
|
26
26
|
auditSkills,
|
|
27
27
|
CORPORA,
|
|
28
28
|
DESCRIPTION_LIMIT,
|
|
29
|
+
PRACTICE_LEDGER,
|
|
30
|
+
PRACTICE_SECTIONS,
|
|
31
|
+
PRACTICE_SKILLS,
|
|
29
32
|
REQUIREMENT_SECTIONS,
|
|
33
|
+
SHIPPED_CORPUS,
|
|
30
34
|
type SkillFinding,
|
|
31
35
|
type SkillsAudit,
|
|
32
36
|
type SkillsAuditRefusal,
|
|
@@ -1398,6 +1402,7 @@ async function runSkillsAudit(
|
|
|
1398
1402
|
reportFolder(report)
|
|
1399
1403
|
reportRequirementShape(report)
|
|
1400
1404
|
reportProvenance(report)
|
|
1405
|
+
reportPracticeShape(report)
|
|
1401
1406
|
reportUnmeasured()
|
|
1402
1407
|
outro()
|
|
1403
1408
|
}
|
|
@@ -1420,11 +1425,14 @@ async function runSkillsAudit(
|
|
|
1420
1425
|
folderName: report.folderName,
|
|
1421
1426
|
requirementSections: report.requirementSections,
|
|
1422
1427
|
datedProvenance: report.datedProvenance,
|
|
1428
|
+
practiceShape: report.practiceShape,
|
|
1423
1429
|
},
|
|
1424
1430
|
checkpoints: {
|
|
1425
1431
|
descriptionLimit: DESCRIPTION_LIMIT,
|
|
1426
1432
|
requirementSections: REQUIREMENT_SECTIONS,
|
|
1427
1433
|
corpora: CORPORA,
|
|
1434
|
+
practiceSkills: PRACTICE_SKILLS,
|
|
1435
|
+
practiceSections: PRACTICE_SECTIONS,
|
|
1428
1436
|
},
|
|
1429
1437
|
})}\n`,
|
|
1430
1438
|
)
|
|
@@ -1615,6 +1623,31 @@ function reportProvenance(report: SkillsAudit): void {
|
|
|
1615
1623
|
reportFindings(report.datedProvenance)
|
|
1616
1624
|
}
|
|
1617
1625
|
|
|
1626
|
+
/**
|
|
1627
|
+
* Reports without failing the run for the reason provenance does. The list
|
|
1628
|
+
* names shipped folders alone, which the step says so a target reading a pass
|
|
1629
|
+
* knows its own skills were never in scope.
|
|
1630
|
+
*/
|
|
1631
|
+
function reportPracticeShape(report: SkillsAudit): void {
|
|
1632
|
+
logStep('Practice shape')
|
|
1633
|
+
logInfo(
|
|
1634
|
+
`A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and carries ${PRACTICE_LEDGER}.`,
|
|
1635
|
+
)
|
|
1636
|
+
logInfo(
|
|
1637
|
+
`The list is kept here and names ${plural(PRACTICE_SKILLS.length, 'shipped skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
if (report.practiceShape.length === 0) {
|
|
1641
|
+
logInfo('Every listed practice skill carries its shape.')
|
|
1642
|
+
return
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
logWarn(
|
|
1646
|
+
`${plural(report.practiceShape.length, 'missing part')} across the listed practice skills`,
|
|
1647
|
+
)
|
|
1648
|
+
reportFindings(report.practiceShape)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1618
1651
|
/**
|
|
1619
1652
|
* Stated on every run, including the run where everything above passed. A
|
|
1620
1653
|
* report that lists only what it measured reads as a verdict on the standard
|
package/src/commands/context.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
isOverCount,
|
|
26
26
|
isOverLength,
|
|
27
27
|
measureArchitecture,
|
|
28
|
+
missingRevisit,
|
|
28
29
|
testableCount,
|
|
29
30
|
} from '@/context/architecture'
|
|
30
31
|
import { auditCitations, type CitationReport } from '@/context/citations'
|
|
@@ -122,8 +123,10 @@ export function register(program: Command): void {
|
|
|
122
123
|
'states its own line allowances gates when it is past the ceiling',
|
|
123
124
|
'those derive, on any run except --citations-only, which never',
|
|
124
125
|
'measures it. A record stating an entry cap gates the same way when',
|
|
125
|
-
'it holds more decisions than the cap
|
|
126
|
-
'
|
|
126
|
+
'it holds more decisions than the cap, and a record stating that',
|
|
127
|
+
'every decision closes with a revisit sentence gates when one lacks',
|
|
128
|
+
'it. A record stating none of the three is reported and never',
|
|
129
|
+
'gated. --gate widens the gate to the other two',
|
|
127
130
|
'findings that are facts rather than judgments: a missing required',
|
|
128
131
|
'section and index drift. A context entry requires Overview and',
|
|
129
132
|
'Layout, and a wireframe requires Regions, States, Copy, and Not on',
|
|
@@ -739,6 +742,8 @@ async function runAudit(
|
|
|
739
742
|
unresolvedCitations: citations.unresolved.length,
|
|
740
743
|
recordOverLength: record !== undefined && isOverLength(record),
|
|
741
744
|
recordOverCount: record !== undefined && isOverCount(record),
|
|
745
|
+
recordMissingRevisit:
|
|
746
|
+
record !== undefined && missingRevisit(record).length > 0,
|
|
742
747
|
sections,
|
|
743
748
|
drift,
|
|
744
749
|
wireframes,
|
|
@@ -1194,6 +1199,24 @@ function reportRecord(
|
|
|
1194
1199
|
)
|
|
1195
1200
|
}
|
|
1196
1201
|
|
|
1202
|
+
const revisited = report.decisions.filter((entry) => entry.revisit).length
|
|
1203
|
+
const lackingRevisit = missingRevisit(report)
|
|
1204
|
+
if (!report.revisitRequired) {
|
|
1205
|
+
logInfo(
|
|
1206
|
+
`${revisited} of ${plural(decisions, 'decision')} close with a revisit sentence, and the record states no revisit clause, so this is reported and never gated.`,
|
|
1207
|
+
)
|
|
1208
|
+
} else if (lackingRevisit.length > 0) {
|
|
1209
|
+
for (const heading of lackingRevisit) {
|
|
1210
|
+
logError(
|
|
1211
|
+
`"${heading}" carries no sentence opening "Revisit when", which the record requires of every decision.`,
|
|
1212
|
+
)
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
logInfo(
|
|
1216
|
+
`Every one of ${plural(decisions, 'decision')} closes with the revisit sentence the record requires.`,
|
|
1217
|
+
)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1197
1220
|
if (decisions === 0) {
|
|
1198
1221
|
logWarn('The record declares no decision, so nothing was classified.')
|
|
1199
1222
|
return
|
package/src/commands/serve.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
|
-
import {
|
|
3
|
-
import { DEFAULT_PORT,
|
|
4
|
-
import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
|
|
2
|
+
import { parsePort, report, waitForInterrupt } from '@/serve/report'
|
|
3
|
+
import { DEFAULT_PORT, startServer } from '@/serve/static'
|
|
5
4
|
|
|
6
5
|
const DEFAULT_DIR = '.'
|
|
7
6
|
|
|
@@ -78,92 +77,3 @@ async function runServe(
|
|
|
78
77
|
await waitForInterrupt(outcome.stop)
|
|
79
78
|
return 0
|
|
80
79
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The relative path where it stays inside the working directory, and the
|
|
84
|
-
* absolute one where it climbs out. A run from a linked worktree serving the
|
|
85
|
-
* main root reports `../../teach`, which names the directory without giving a
|
|
86
|
-
* reader anything they can open.
|
|
87
|
-
*/
|
|
88
|
-
function displayPath(root: string): string {
|
|
89
|
-
const near = relative(process.cwd(), root)
|
|
90
|
-
if (near === '') return '.'
|
|
91
|
-
return near.startsWith('..') ? root : near
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Undefined for a value that is not a port, distinct from an absent flag,
|
|
96
|
-
* which takes the default. A `Number` of a typo is `NaN`, and passing that on
|
|
97
|
-
* asks the runtime to bind a port nobody named.
|
|
98
|
-
*/
|
|
99
|
-
function parsePort(raw: string | undefined): number | undefined {
|
|
100
|
-
if (raw === undefined) return DEFAULT_PORT
|
|
101
|
-
const value = Number(raw)
|
|
102
|
-
if (!Number.isInteger(value) || value < 0 || value > 65535) return undefined
|
|
103
|
-
return value
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function report(outcome: ServeOutcome, emitJson: boolean): number {
|
|
107
|
-
if (!outcome.ok) {
|
|
108
|
-
if (emitJson) {
|
|
109
|
-
process.stdout.write(
|
|
110
|
-
`${JSON.stringify({ ok: false, reason: outcome.reason, detail: outcome.detail })}\n`,
|
|
111
|
-
)
|
|
112
|
-
return 1
|
|
113
|
-
}
|
|
114
|
-
intro('canon serve')
|
|
115
|
-
logError(outcome.detail)
|
|
116
|
-
outro()
|
|
117
|
-
return 1
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (emitJson) {
|
|
121
|
-
process.stdout.write(
|
|
122
|
-
`${JSON.stringify({
|
|
123
|
-
ok: true,
|
|
124
|
-
root: outcome.root,
|
|
125
|
-
host: outcome.host,
|
|
126
|
-
port: outcome.port,
|
|
127
|
-
entry: outcome.entry,
|
|
128
|
-
url: outcome.url,
|
|
129
|
-
entryExists: outcome.entryExists,
|
|
130
|
-
})}\n`,
|
|
131
|
-
)
|
|
132
|
-
return 0
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
intro('canon serve')
|
|
136
|
-
logStep('Serving')
|
|
137
|
-
logInfo(displayPath(outcome.root))
|
|
138
|
-
logStep('Open')
|
|
139
|
-
logInfo(outcome.url)
|
|
140
|
-
|
|
141
|
-
if (!outcome.entryExists) {
|
|
142
|
-
logStep('No entry page')
|
|
143
|
-
logWarn(
|
|
144
|
-
`${outcome.entry} is not in that directory, so the link opens a 404`,
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
logStep('Stop')
|
|
149
|
-
logInfo('Ctrl-C')
|
|
150
|
-
return 0
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Resolves when the process is asked to stop. The frame is closed here rather
|
|
155
|
-
* than in the reporter, because the run is the serving rather than the start,
|
|
156
|
-
* and closing at start would print the frame's end while the server ran on.
|
|
157
|
-
*/
|
|
158
|
-
function waitForInterrupt(stop: () => Promise<void>): Promise<void> {
|
|
159
|
-
return new Promise((settle) => {
|
|
160
|
-
const finish = () => {
|
|
161
|
-
void stop().then(() => {
|
|
162
|
-
outro()
|
|
163
|
-
settle()
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
process.once('SIGINT', finish)
|
|
167
|
-
process.once('SIGTERM', finish)
|
|
168
|
-
})
|
|
169
|
-
}
|