@erclx/canon 4.119.0 → 4.121.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 +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +29 -115
- package/claude/skills/auto-ship/references/draft-mark.md +30 -0
- package/claude/skills/auto-ship/references/failure-recovery.md +27 -0
- package/claude/skills/auto-ship/references/tier-failures.md +26 -0
- package/claude/skills/auto-ship/references/verb-absent.md +37 -0
- package/claude/skills/auto-ship/references/worktree-name.md +22 -0
- package/claude/skills/bash-script/SKILL.md +1 -1
- package/claude/skills/bash-script/references/patterns.md +0 -99
- package/claude/skills/bash-script/references/prompts.md +100 -0
- package/claude/skills/create-skill/SKILL.md +3 -2
- package/claude/skills/create-snippet/SKILL.md +1 -1
- package/claude/skills/create-standard/SKILL.md +1 -1
- package/claude/skills/design-extract/SKILL.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +2 -2
- package/claude/skills/docs-sync/SKILL.md +1 -1
- package/claude/skills/draft-context/SKILL.md +1 -1
- package/claude/skills/draft-docs/SKILL.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +1 -1
- package/claude/skills/draft-readme/SKILL.md +1 -1
- package/claude/skills/draft-wiki/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-issue/references/issue.md +1 -1
- package/claude/skills/git-pr/REQUIREMENT.md +1 -0
- package/claude/skills/git-pr/SKILL.md +35 -5
- package/claude/skills/git-stage/SKILL.md +1 -1
- package/claude/skills/memory-capture/SKILL.md +1 -1
- package/claude/skills/restate-plainly/REQUIREMENT.md +1 -1
- package/claude/skills/review-address/SKILL.md +4 -3
- package/claude/skills/review-branch/REQUIREMENT.md +1 -0
- package/claude/skills/review-branch/SKILL.md +5 -6
- package/claude/skills/review-craft/REQUIREMENT.md +43 -0
- package/claude/skills/review-craft/SKILL.md +50 -0
- package/claude/skills/review-craft/references/security.md +21 -0
- package/claude/skills/review-craft/references/ui.md +72 -0
- package/claude/skills/review-pr/REQUIREMENT.md +1 -0
- package/claude/skills/review-pr/SKILL.md +14 -243
- package/claude/skills/review-pr/references/close-out.md +30 -0
- package/claude/skills/review-pr/references/post.md +145 -0
- package/claude/skills/review-pr/references/scope.md +82 -0
- package/claude/skills/role-orchestrator/REQUIREMENT.md +9 -16
- package/claude/skills/role-orchestrator/SKILL.md +9 -153
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +4 -193
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +22 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +85 -0
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +61 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +56 -0
- package/claude/skills/role-orchestrator/references/orchestrator-review-fallback.md +18 -0
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +1 -1
- package/claude/skills/role-planner/REQUIREMENT.md +2 -2
- package/claude/skills/standards-audit/SKILL.md +1 -1
- package/claude/skills/write-human/REQUIREMENT.md +7 -4
- package/claude/skills/write-human/SKILL.md +4 -2
- package/claude/skills/write-human/references/machine-tells.md +1 -1
- package/claude/skills/write-human/references/source-material.md +5 -2
- package/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +3 -2
- package/docs/agents/counts.md +1 -1
- package/docs/agents/index.md +8 -3
- package/docs/agents/markdown-audit.md +13 -19
- package/docs/agents/pr-evidence.md +19 -3
- package/docs/agents/pr-local.md +105 -0
- package/docs/agents/pr-preview.md +2 -2
- package/docs/agents/records-push.md +61 -0
- package/docs/agents/records-validate.md +73 -0
- package/docs/agents/records.md +3 -119
- package/docs/agents/sessions-transfer.md +91 -0
- package/docs/agents/sessions.md +3 -82
- package/docs/agents/skills-audit.md +2 -2
- package/docs/agents/teach-authoring.md +112 -0
- package/docs/agents/teach.md +3 -103
- package/docs/target-projects.md +2 -2
- package/docs/workflow/ai-workflow.md +2 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/rules/claude/500-prose.md +1 -1
- package/governance/rules/claude/501-markdown.md +3 -3
- package/governance/rules/claude/570-skill.md +2 -1
- package/package.json +1 -1
- package/scripts/core/check-ignore-parity.sh +1 -1
- package/scripts/lib/sandbox-git.sh +1 -1
- package/src/autoship/paths.ts +3 -2
- package/src/claude/cases/workflow.ts +5 -0
- package/src/commands/audits.ts +2 -2
- package/src/commands/markdown.ts +7 -9
- package/src/commands/pr.ts +331 -2
- package/src/context/classify/extract.ts +2 -2
- package/src/counts/scan.ts +1 -1
- package/src/design/tokens.ts +1 -1
- package/src/gate/measures.ts +4 -4
- package/src/gate/stages.ts +2 -3
- package/src/markdown/bans.ts +11 -68
- package/src/markdown/gate.ts +1 -1
- package/src/markdown/scan.ts +7 -88
- package/src/pr/evidence.ts +42 -2
- package/src/pr/local.ts +125 -0
- package/standards/docs.md +1 -1
- package/standards/index.md +2 -1
- package/standards/markdown.md +5 -7
- package/standards/pr.md +1 -1
- package/standards/publish.md +1 -1
- package/standards/readme.md +2 -2
- package/standards/skill-requirement.md +72 -0
- package/standards/skill.md +34 -152
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +32 -0
- package/tooling/base/configs/scripts/verify.sh +2 -2
- package/tooling/base/seeds/canon/context/development.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +5 -6
package/src/markdown/scan.ts
CHANGED
|
@@ -55,20 +55,9 @@ export interface BodyLine {
|
|
|
55
55
|
readonly text: string
|
|
56
56
|
/** True on a fence delimiter and on every line between one pair. */
|
|
57
57
|
readonly fenced: boolean
|
|
58
|
-
/**
|
|
59
|
-
* Which fenced block the line sits in, counted from one, and undefined
|
|
60
|
-
* outside one. Two blocks written with nothing between them are one
|
|
61
|
-
* contiguous run of fenced lines, so this is the only field telling them
|
|
62
|
-
* apart. Counting from one keeps the outside answer from reading as a block.
|
|
63
|
-
*
|
|
64
|
-
* The name carries the fence because `block` alone already names a run of
|
|
65
|
-
* `BodyLine` values in `src/markdown/structure.ts`, and one word meaning both
|
|
66
|
-
* an aggregate of lines and an index on each line reads as a bug.
|
|
67
|
-
*/
|
|
68
|
-
readonly fenceBlock: number | undefined
|
|
69
58
|
}
|
|
70
59
|
|
|
71
|
-
export type BanKind = 'character'
|
|
60
|
+
export type BanKind = 'character'
|
|
72
61
|
|
|
73
62
|
export interface BanFinding {
|
|
74
63
|
readonly line: number
|
|
@@ -80,30 +69,20 @@ export interface BanFinding {
|
|
|
80
69
|
|
|
81
70
|
export interface BanSets {
|
|
82
71
|
readonly characters: readonly string[]
|
|
83
|
-
readonly words: readonly string[]
|
|
84
|
-
readonly spellings: readonly string[]
|
|
85
72
|
}
|
|
86
73
|
|
|
87
74
|
/**
|
|
88
|
-
* Marks which lines sit inside a fence without dropping them
|
|
89
|
-
* each of those lines belongs to.
|
|
75
|
+
* Marks which lines sit inside a fence without dropping them.
|
|
90
76
|
*
|
|
91
77
|
* Each measure excludes a fence for its own reason and needs a different
|
|
92
78
|
* response. The depth measure skips a fenced line so an example cannot break
|
|
93
79
|
* the run around it, bullet folding treats one as a break so a bullet does not
|
|
94
80
|
* absorb the block below it, and the ban scan ignores it outright. Returning
|
|
95
81
|
* the mark rather than a filtered list is what lets one walk serve all three.
|
|
96
|
-
*
|
|
97
|
-
* The block index answers the consumer that has to decide per block rather
|
|
98
|
-
* than per contiguous run, since two adjacent blocks are indistinguishable
|
|
99
|
-
* through the mark alone. It is counted here because the walk already holds
|
|
100
|
-
* the opening delimiter, and a consumer parsing its own would be the second
|
|
101
|
-
* fence walker this repository consolidated away.
|
|
102
82
|
*/
|
|
103
83
|
function markFences(texts: readonly string[], offset: number): BodyLine[] {
|
|
104
84
|
const lines: BodyLine[] = []
|
|
105
85
|
let fence: string | undefined
|
|
106
|
-
let fenceBlock = 0
|
|
107
86
|
|
|
108
87
|
for (const [index, text] of texts.entries()) {
|
|
109
88
|
const match = FENCE.exec(text.trim())
|
|
@@ -117,15 +96,9 @@ function markFences(texts: readonly string[], offset: number): BodyLine[] {
|
|
|
117
96
|
} else if (match) {
|
|
118
97
|
fenced = true
|
|
119
98
|
fence = match[1]
|
|
120
|
-
fenceBlock += 1
|
|
121
99
|
}
|
|
122
100
|
|
|
123
|
-
lines.push({
|
|
124
|
-
number: offset + index + 1,
|
|
125
|
-
text,
|
|
126
|
-
fenced,
|
|
127
|
-
fenceBlock: fenced ? fenceBlock : undefined,
|
|
128
|
-
})
|
|
101
|
+
lines.push({ number: offset + index + 1, text, fenced })
|
|
129
102
|
}
|
|
130
103
|
|
|
131
104
|
return lines
|
|
@@ -227,47 +200,12 @@ export function visibleText(text: string): string {
|
|
|
227
200
|
return visible + drop(text.slice(read))
|
|
228
201
|
}
|
|
229
202
|
|
|
230
|
-
function escape(term: string): string {
|
|
231
|
-
return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Bounds a banned word on a word character or a hyphen either side.
|
|
236
|
-
*
|
|
237
|
-
* `\b` sits after a hyphen, so a banned word ending a hyphenated compound
|
|
238
|
-
* reports from inside it: `allows` came back out of `auto-allows`. A compound
|
|
239
|
-
* is one word to a reader, and the ban is on the word rather than on a morpheme
|
|
240
|
-
* of it. Stripping hyphens before matching was the alternative and it joins the
|
|
241
|
-
* compound into a token neither half reaches.
|
|
242
|
-
*
|
|
243
|
-
* A spelling ban takes `wordBoundary` instead. This is not that rule with a
|
|
244
|
-
* wider fence, since the two bans target different things.
|
|
245
|
-
*/
|
|
246
|
-
function bannedWord(term: string): RegExp {
|
|
247
|
-
return new RegExp(`(?<![\\w-])${escape(term)}(?![\\w-])`, 'gi')
|
|
248
|
-
}
|
|
249
|
-
|
|
250
203
|
/**
|
|
251
|
-
*
|
|
204
|
+
* Finds every banned character outside a fence, a code span, and a link.
|
|
252
205
|
*
|
|
253
|
-
*
|
|
254
|
-
* A
|
|
255
|
-
*
|
|
256
|
-
* spelling as plainly as `behaviour` does, and rejecting a hyphen here would
|
|
257
|
-
* leave the usual spelling of that phrase unreported.
|
|
258
|
-
*/
|
|
259
|
-
function bannedSpelling(term: string): RegExp {
|
|
260
|
-
return new RegExp(`\\b${escape(term)}\\b`, 'gi')
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Finds every banned term outside a fence, a code span, and a link.
|
|
265
|
-
*
|
|
266
|
-
* A word ban matches in either casing, since the standard states each in
|
|
267
|
-
* lowercase and bans the word rather than a spelling of it. A closed set of
|
|
268
|
-
* whole words separates the check from the pattern that produced most of the
|
|
269
|
-
* intake's false positives: `exercises` and `promises` end in the banned
|
|
270
|
-
* suffix and are not the banned words, and a closed set never reaches them.
|
|
206
|
+
* Characters are the whole set because a character is the one class a literal
|
|
207
|
+
* match settles. A word carries honest uses no match separates, which is why
|
|
208
|
+
* word choice is guidance a reader applies rather than a finding this reports.
|
|
271
209
|
*/
|
|
272
210
|
export function scanBans(
|
|
273
211
|
lines: readonly BodyLine[],
|
|
@@ -275,19 +213,6 @@ export function scanBans(
|
|
|
275
213
|
): BanFinding[] {
|
|
276
214
|
const found: BanFinding[] = []
|
|
277
215
|
|
|
278
|
-
const patterns: { kind: BanKind; term: string; pattern: RegExp }[] = [
|
|
279
|
-
...bans.words.map((term) => ({
|
|
280
|
-
kind: 'word' as const,
|
|
281
|
-
term,
|
|
282
|
-
pattern: bannedWord(term),
|
|
283
|
-
})),
|
|
284
|
-
...bans.spellings.map((term) => ({
|
|
285
|
-
kind: 'spelling' as const,
|
|
286
|
-
term,
|
|
287
|
-
pattern: bannedSpelling(term),
|
|
288
|
-
})),
|
|
289
|
-
]
|
|
290
|
-
|
|
291
216
|
for (const line of lines) {
|
|
292
217
|
if (line.fenced) continue
|
|
293
218
|
const text = maskDisplayed(line.text)
|
|
@@ -299,12 +224,6 @@ export function scanBans(
|
|
|
299
224
|
column = text.indexOf(term, column + term.length)
|
|
300
225
|
}
|
|
301
226
|
}
|
|
302
|
-
|
|
303
|
-
for (const { kind, term, pattern } of patterns) {
|
|
304
|
-
for (const match of text.matchAll(pattern)) {
|
|
305
|
-
found.push({ line: line.number, column: match.index, kind, term })
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
227
|
}
|
|
309
228
|
|
|
310
229
|
return found.sort((a, b) => a.line - b.line || a.column - b.column)
|
package/src/pr/evidence.ts
CHANGED
|
@@ -7,6 +7,9 @@ const MARKER_PREFIX = '<!-- pr-evidence:'
|
|
|
7
7
|
/** How the first line of a body names the branch's preview deployment. */
|
|
8
8
|
const PREVIEW_PREFIX = '**Preview:**'
|
|
9
9
|
|
|
10
|
+
/** How the opening block names the branch's server on the operator's own machine, under any hosted preview line. */
|
|
11
|
+
export const LOCAL_PREFIX = '**Local preview:**'
|
|
12
|
+
|
|
10
13
|
/** The heading the checklist sits under, printed outside the delimiters so a carried checklist never doubles it. */
|
|
11
14
|
const CHECKLIST_HEADING = '## What to look at'
|
|
12
15
|
|
|
@@ -128,6 +131,9 @@ export function evidenceMarker(head: string): string {
|
|
|
128
131
|
*
|
|
129
132
|
* A checklist closes the body, below the comparison it annotates, so one
|
|
130
133
|
* comment carries the preview address, the screenshots, and what to look at.
|
|
134
|
+
*
|
|
135
|
+
* A local address sits under the hosted one, never above it, so the hosted
|
|
136
|
+
* line keeps the first-line position `findEvidencePreview` reads.
|
|
131
137
|
*/
|
|
132
138
|
export function renderEvidenceBody(
|
|
133
139
|
states: readonly EvidenceState[],
|
|
@@ -136,9 +142,13 @@ export function renderEvidenceBody(
|
|
|
136
142
|
head: string,
|
|
137
143
|
preview?: string,
|
|
138
144
|
checklist?: string,
|
|
145
|
+
local?: string,
|
|
139
146
|
): string {
|
|
140
|
-
const
|
|
141
|
-
preview === undefined ? [] : [`${PREVIEW_PREFIX} ${preview}
|
|
147
|
+
const addresses = [
|
|
148
|
+
...(preview === undefined ? [] : [`${PREVIEW_PREFIX} ${preview}`]),
|
|
149
|
+
...(local === undefined ? [] : [`${LOCAL_PREFIX} ${local}`]),
|
|
150
|
+
]
|
|
151
|
+
const opening = addresses.length === 0 ? [] : [...addresses, '']
|
|
142
152
|
const closing =
|
|
143
153
|
checklist === undefined
|
|
144
154
|
? []
|
|
@@ -223,6 +233,36 @@ export function findEvidencePreview(
|
|
|
223
233
|
return first.slice(PREVIEW_PREFIX.length).trim() || undefined
|
|
224
234
|
}
|
|
225
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Where the local address line sits in a body, searched only through the
|
|
238
|
+
* opening block above the first blank line, so a line quoted further down in
|
|
239
|
+
* a checklist is never taken for it. -1 when the block carries none.
|
|
240
|
+
*/
|
|
241
|
+
export function findLocalLineIndex(body: string): number {
|
|
242
|
+
const lines = body.split('\n')
|
|
243
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
244
|
+
const line = (lines[index] ?? '').trim()
|
|
245
|
+
if (line === '') return -1
|
|
246
|
+
if (line.startsWith(LOCAL_PREFIX)) return index
|
|
247
|
+
}
|
|
248
|
+
return -1
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The local address the marked comment already carries, carried forward on a
|
|
253
|
+
* re-render the way the hosted preview is.
|
|
254
|
+
*/
|
|
255
|
+
export function findEvidenceLocal(
|
|
256
|
+
comments: readonly EvidenceComment[],
|
|
257
|
+
): string | undefined {
|
|
258
|
+
const marked = comments.find((comment) => hasEvidenceMarker(comment.body))
|
|
259
|
+
if (marked === undefined) return undefined
|
|
260
|
+
const index = findLocalLineIndex(marked.body)
|
|
261
|
+
if (index === -1) return undefined
|
|
262
|
+
const line = marked.body.split('\n')[index]?.trim() ?? ''
|
|
263
|
+
return line.slice(LOCAL_PREFIX.length).trim() || undefined
|
|
264
|
+
}
|
|
265
|
+
|
|
226
266
|
/**
|
|
227
267
|
* The checklist the marked comment already carries, read back out of its
|
|
228
268
|
* delimiters. The handoff file is deleted once posted, so every later render
|
package/src/pr/local.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { findLocalLineIndex } from '@/pr/evidence'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The folder linked worktrees sit under. A listener there belongs to another
|
|
5
|
+
* branch even though its cwd sits inside the main checkout's toplevel.
|
|
6
|
+
*/
|
|
7
|
+
const LINKED_WORKTREES = '.claude/worktrees'
|
|
8
|
+
|
|
9
|
+
/** The state `/proc/net/tcp` writes for a socket in LISTEN. */
|
|
10
|
+
const PROC_LISTEN_STATE = '0A'
|
|
11
|
+
|
|
12
|
+
export type LocalRefusal = 'no-server' | 'no-listener-reader'
|
|
13
|
+
|
|
14
|
+
export interface Listener {
|
|
15
|
+
readonly port: number
|
|
16
|
+
readonly pid: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The machine reads a detection makes, injected so it can be driven without binding a socket. */
|
|
20
|
+
export interface LocalRunner {
|
|
21
|
+
/** Every TCP socket in LISTEN with its owning pid, or undefined when this machine offers no way to read them. */
|
|
22
|
+
listListeners(): Promise<readonly Listener[] | undefined>
|
|
23
|
+
cwdOf(pid: number): Promise<string | undefined>
|
|
24
|
+
/** Whether the port serves an HTML page at its root inside the runner's timeout. */
|
|
25
|
+
probe(port: number): Promise<boolean>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type LocalResult =
|
|
29
|
+
| { readonly kind: 'found'; readonly port: number; readonly url: string }
|
|
30
|
+
| { readonly kind: 'refused'; readonly reason: LocalRefusal }
|
|
31
|
+
|
|
32
|
+
function isInside(path: string, root: string): boolean {
|
|
33
|
+
return path === root || path.startsWith(`${root}/`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether a process running in `cwd` serves this worktree. A tool that
|
|
38
|
+
* changes into a subfolder before listening still counts, while a sibling
|
|
39
|
+
* worktree nested under the main checkout's `.claude/worktrees/` does not.
|
|
40
|
+
*/
|
|
41
|
+
function belongsTo(cwd: string, worktreeRoot: string): boolean {
|
|
42
|
+
return (
|
|
43
|
+
isInside(cwd, worktreeRoot) &&
|
|
44
|
+
!isInside(cwd, `${worktreeRoot}/${LINKED_WORKTREES}`)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Finds the server this worktree is running: the lowest port whose listening
|
|
50
|
+
* process runs inside the worktree and answers an HTTP probe. A port rather
|
|
51
|
+
* than a process is the unit, since one dev server often holds a second
|
|
52
|
+
* socket for its reload channel, and the lower one is the page by convention.
|
|
53
|
+
*/
|
|
54
|
+
export async function findLocalServer(
|
|
55
|
+
worktreeRoot: string,
|
|
56
|
+
runner: LocalRunner,
|
|
57
|
+
): Promise<LocalResult> {
|
|
58
|
+
const listeners = await runner.listListeners()
|
|
59
|
+
if (listeners === undefined) {
|
|
60
|
+
return { kind: 'refused', reason: 'no-listener-reader' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cwdByPid = new Map<number, string | undefined>()
|
|
64
|
+
const ports = new Set<number>()
|
|
65
|
+
for (const { pid, port } of listeners) {
|
|
66
|
+
if (!cwdByPid.has(pid)) cwdByPid.set(pid, await runner.cwdOf(pid))
|
|
67
|
+
const cwd = cwdByPid.get(pid)
|
|
68
|
+
if (cwd !== undefined && belongsTo(cwd, worktreeRoot)) ports.add(port)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const port of [...ports].sort((a, b) => a - b)) {
|
|
72
|
+
if (await runner.probe(port)) {
|
|
73
|
+
return { kind: 'found', port, url: `http://localhost:${port}` }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { kind: 'refused', reason: 'no-server' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Reads `lsof -nP -iTCP -sTCP:LISTEN -Fpn` output, where a `p` line opens a process and each `n` line names one of its sockets. */
|
|
80
|
+
export function parseLsofListeners(text: string): Listener[] {
|
|
81
|
+
const listeners: Listener[] = []
|
|
82
|
+
let pid: number | undefined
|
|
83
|
+
for (const line of text.split('\n')) {
|
|
84
|
+
if (line.startsWith('p')) pid = Number(line.slice(1))
|
|
85
|
+
if (!line.startsWith('n') || pid === undefined) continue
|
|
86
|
+
const port = Number(line.slice(line.lastIndexOf(':') + 1))
|
|
87
|
+
if (Number.isInteger(port) && port > 0) listeners.push({ pid, port })
|
|
88
|
+
}
|
|
89
|
+
return listeners
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ProcSocket {
|
|
93
|
+
readonly port: number
|
|
94
|
+
readonly inode: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Reads `/proc/net/tcp` or `tcp6`, keeping each listening socket's port and inode for matching against `/proc/<pid>/fd`. */
|
|
98
|
+
export function parseProcNetTcp(text: string): ProcSocket[] {
|
|
99
|
+
const sockets: ProcSocket[] = []
|
|
100
|
+
for (const line of text.split('\n').slice(1)) {
|
|
101
|
+
const fields = line.trim().split(/\s+/)
|
|
102
|
+
const local = fields[1]
|
|
103
|
+
const state = fields[3]
|
|
104
|
+
const inode = fields[9]
|
|
105
|
+
if (local === undefined || inode === undefined) continue
|
|
106
|
+
if (state !== PROC_LISTEN_STATE) continue
|
|
107
|
+
const port = Number.parseInt(local.slice(local.lastIndexOf(':') + 1), 16)
|
|
108
|
+
if (Number.isInteger(port) && port > 0) sockets.push({ port, inode })
|
|
109
|
+
}
|
|
110
|
+
return sockets
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Replaces the local address line of a marked body with `note`, leaving
|
|
115
|
+
* every other byte in place so the trailing marker, the checklist delimiters,
|
|
116
|
+
* and any ticked box survive. Undefined when the body carries no such line,
|
|
117
|
+
* which is also what a second strip of the same body returns.
|
|
118
|
+
*/
|
|
119
|
+
export function stripLocalLine(body: string, note: string): string | undefined {
|
|
120
|
+
const index = findLocalLineIndex(body)
|
|
121
|
+
if (index === -1) return undefined
|
|
122
|
+
const lines = body.split('\n')
|
|
123
|
+
lines[index] = note
|
|
124
|
+
return lines.join('\n')
|
|
125
|
+
}
|
package/standards/docs.md
CHANGED
|
@@ -19,7 +19,7 @@ Does not govern:
|
|
|
19
19
|
- Per-domain internal narrative written for a reader with the source open: `context.md`
|
|
20
20
|
- The drawing inside a Mermaid fence: `mermaid.md`
|
|
21
21
|
- Voice, rhythm, and sentence construction: the `write-human` skill
|
|
22
|
-
-
|
|
22
|
+
- Punctuation and formatting: `markdown.md`
|
|
23
23
|
|
|
24
24
|
## What a working docs page looks like
|
|
25
25
|
|
package/standards/index.md
CHANGED
|
@@ -19,7 +19,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
19
19
|
- [Glossary reference](glossary.md): Frontmatter, entry shape, ordering, and the rules deciding which terms a glossary carries
|
|
20
20
|
- [Groundwork reference](groundwork.md): Folder layout, ordinal naming, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
|
|
21
21
|
- [Intake reference](intake.md): Folder layout, ordinal naming, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
|
|
22
|
-
- [Markdown reference](markdown.md): Headings, paragraph and list structure, code spans, the date form, punctuation, emphasis, file references,
|
|
22
|
+
- [Markdown reference](markdown.md): Headings, paragraph and list structure, code spans, the date form, punctuation, emphasis, file references, American spelling, and frontmatter wording
|
|
23
23
|
- [Memory reference](memory.md): Filename and type prefix, frontmatter, the body shape per type, links between entries, and the lifecycle from write to retire
|
|
24
24
|
- [Mermaid reference](mermaid.md): Direction and layout, node and edge budgets, accessibility fields, label punctuation, and render verification for a Mermaid diagram
|
|
25
25
|
- [Plan reference](plan.md): Filename and slug, required sections, the suggested-and-answer contract, and the lifecycle from the live folder to the archive
|
|
@@ -30,6 +30,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
30
30
|
- [Requirements reference](requirements.md): Shape and content rules for canon/REQUIREMENTS.md
|
|
31
31
|
- [Governance rule reference](rule.md): Rule frontmatter, body shape, and voice for .claude/rules files
|
|
32
32
|
- [Session map reference](session.md): Filename and location, the sections a handoff carries, the write and read procedures, and how a role extends it
|
|
33
|
+
- [Skill requirement reference](skill-requirement.md): Shape and content rules for the REQUIREMENT.md beside a skill
|
|
33
34
|
- [Claude skill reference](skill.md): Claude skill structure and authoring rules
|
|
34
35
|
- [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
|
|
35
36
|
- [Snippet reference](snippets.md): Snippet reference and authoring conventions
|
package/standards/markdown.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Markdown reference
|
|
3
|
-
description: Headings, paragraph and list structure, code spans, the date form, punctuation, emphasis, file references,
|
|
3
|
+
description: Headings, paragraph and list structure, code spans, the date form, punctuation, emphasis, file references, American spelling, and frontmatter wording
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Markdown reference
|
|
@@ -9,11 +9,11 @@ Applies to markdown reference docs, READMEs, and inline documentation in repos.
|
|
|
9
9
|
|
|
10
10
|
## Scope
|
|
11
11
|
|
|
12
|
-
Governs what a check can decide about a markdown file: headings, paragraph and list structure, code spans and fences, the form a date takes, punctuation, emphasis, file references, the
|
|
12
|
+
Governs what a check can decide about a markdown file: headings, paragraph and list structure, code spans and fences, the form a date takes, punctuation, emphasis, file references, the spelling convention, what prose may claim about its sources, and the wording of a catalog title and description. It is an attribute standard rather than a document-type one, so it applies over documents whose shape another standard sets, and it carries no template because these rules are written across every document and have no shape of their own.
|
|
13
13
|
|
|
14
14
|
The two frontmatter fields it reaches are `title` and `description`. They are named here rather than in the statement above, since a backticked token in a scope statement's first sentence is published as the standard's jurisdiction.
|
|
15
15
|
|
|
16
|
-
The split with the voice guidance is what reads the rule rather than what the rule covers.
|
|
16
|
+
The split with the voice guidance is what reads the rule rather than what the rule covers. The character bans ship as data `canon markdown audit` and the installed audit hook both read, so a violation is measured on every run. Word choice, cadence, rhythm, and information density are judgments a reader settles, and those travel in the `canon:write-human` skill, which a markdown edit routes to.
|
|
17
17
|
|
|
18
18
|
Does not govern:
|
|
19
19
|
|
|
@@ -71,9 +71,7 @@ Does not govern:
|
|
|
71
71
|
|
|
72
72
|
## Language
|
|
73
73
|
|
|
74
|
-
- Use American English spelling
|
|
75
|
-
- Do not use marketing buzzwords (`seamless`, `robust`, `powerful`, `revolutionary`, `enhanced`, `allows`, `leverage`)
|
|
76
|
-
- Do not use vague qualifiers (`simply`, `just`, `easily`, `quickly`, `very`, `really`)
|
|
74
|
+
- Use American English spelling
|
|
77
75
|
- Open a sentence with its subject and action, not filler (`Note that`, `Basically`), a hollow connective (`That being said`, `It's worth noting`), or a gerund windup (`Leveraging the API...`). Substantive transitions that carry a real relationship are fine.
|
|
78
76
|
- Do not use the negative parallelism pattern (`It's not X, it's Y`, `not because X, but because Y`)
|
|
79
77
|
- Do not pad verb phrases or delay the action. Write the shortest form (`in order to` → `to`, `ensure that X is set` → `set X`, `By doing X, you can Y` → state Y directly).
|
|
@@ -84,7 +82,7 @@ Does not govern:
|
|
|
84
82
|
- Do not attribute a claim to an unnamed authority (`experts say`, `studies show`, `it is widely believed`). Name the source or cut the claim.
|
|
85
83
|
- Do not introduce a fact, name, date, or citation the source does not carry when rewriting existing text. A rewrite changes wording and never claims.
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
One command reads the character bans under `## Punctuation`. `canon markdown audit` ships them as package data, so a project that installed no standards is measured the same as one that did, and this file states them for a reader rather than for the parser. The spelling line above states a convention and ships no set, so a project's spell checker is what enforces it.
|
|
88
86
|
|
|
89
87
|
## Frontmatter descriptions
|
|
90
88
|
|
package/standards/pr.md
CHANGED
|
@@ -16,7 +16,7 @@ Does not govern:
|
|
|
16
16
|
- Whether a phase label or a semver tag may appear in a title or body: `versioning.md`
|
|
17
17
|
- Whether a quoted label, a gitignored record path, or a link to one Claude Code session may appear in a title or body, which `canon labels scan` fails on: `publish.md`
|
|
18
18
|
- Voice, rhythm, and sentence construction in pull request prose: the `write-human` skill
|
|
19
|
-
- Punctuation
|
|
19
|
+
- Punctuation and formatting in pull request prose: `markdown.md`
|
|
20
20
|
|
|
21
21
|
## Title
|
|
22
22
|
|
package/standards/publish.md
CHANGED
|
@@ -26,7 +26,7 @@ Scope every check below by destination. Text published to a remote takes all of
|
|
|
26
26
|
|
|
27
27
|
## Banned characters
|
|
28
28
|
|
|
29
|
-
`markdown.md` holds the character bans
|
|
29
|
+
`markdown.md` holds the character bans. Read it at scan time rather than working the sets from memory, then scan the drafted text and rewrite each occurrence.
|
|
30
30
|
|
|
31
31
|
Restructure the sentence rather than substituting the character. A semicolon swapped for a period leaves both clauses in the order the semicolon chose, which is the shape the ban exists to remove.
|
|
32
32
|
|
package/standards/readme.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Readme voice, structure, and content conventions
|
|
|
5
5
|
|
|
6
6
|
# Readme reference
|
|
7
7
|
|
|
8
|
-
Applies to every `README.md`. The `## Voice` section states the voice for a repository's root README, so the `write-human` skill yields to it there. The yield covers voice alone. The rhythm and density rules that skill carries stay in force, as
|
|
8
|
+
Applies to every `README.md`. The `## Voice` section states the voice for a repository's root README, so the `write-human` skill yields to it there. The yield covers voice alone. The rhythm and density rules that skill carries stay in force, as does the word-choice guidance that travels with them, and so do the punctuation and formatting in `markdown.md`, so the warmer register ships with the same hygiene: no em dashes, no semicolons, no marketing words.
|
|
9
9
|
|
|
10
10
|
The reader is what changes. Reference prose serves someone who already committed to the project and is scanning for a fact. A root README meets someone deciding whether to commit at all, and it is often the only file they read.
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ Governs every `README.md`: voice, heading structure, required and optional secti
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
18
18
|
- Rhythm and sentence construction in README prose: the `write-human` skill, which yields the voice and keeps the rest
|
|
19
|
-
-
|
|
19
|
+
- Punctuation and formatting in README prose: `markdown.md`, which yields nothing
|
|
20
20
|
- Product scope and goals: `requirements.md`
|
|
21
21
|
- The consumer-facing reference under `docs/`, whose reader has already committed and is operating the project: `docs.md`
|
|
22
22
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Skill requirement reference
|
|
3
|
+
description: Shape and content rules for the REQUIREMENT.md beside a skill
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill requirement reference
|
|
7
|
+
|
|
8
|
+
Applies to the `REQUIREMENT.md` beside each `SKILL.md`. It states what the skill is for, so a proposed change can be argued against something and the corpus can be read to decide whether a skill should exist at all.
|
|
9
|
+
|
|
10
|
+
## Scope
|
|
11
|
+
|
|
12
|
+
Governs `skills/<name>/REQUIREMENT.md`: why it exists, what it must answer, and its sections.
|
|
13
|
+
|
|
14
|
+
Does not govern:
|
|
15
|
+
|
|
16
|
+
- The skill folder, `SKILL.md`, and the rule that every skill carries a requirement: `skill.md`
|
|
17
|
+
- Voice, rhythm, and sentence construction: the `write-human` skill
|
|
18
|
+
- Punctuation, formatting, and word choice: `markdown.md`
|
|
19
|
+
|
|
20
|
+
## What a working requirement looks like
|
|
21
|
+
|
|
22
|
+
A requirement works when a proposed change to the skill can be settled by reading it alone, without re-deriving what the skill is for:
|
|
23
|
+
|
|
24
|
+
- Does this change close a gap the file states?
|
|
25
|
+
- Does a line already in the body trace to one of those gaps?
|
|
26
|
+
- Does this behavior belong to this skill or to a sibling the file names?
|
|
27
|
+
|
|
28
|
+
A requirement that leaves any of the three open is non-conforming regardless of whether it satisfies every shape rule below. The third question is what makes a requirement worth more than a restated description, because a boundary stated only in prose is never checked against the skill on the other side of it.
|
|
29
|
+
|
|
30
|
+
## Purpose
|
|
31
|
+
|
|
32
|
+
- Treat the file as the compressed statement of what the skill is for. A skill body is procedural by design, so its purpose sits spread across its steps, and a reader gets this file before opening the body whether or not the skill's scope is contested.
|
|
33
|
+
- Read it before editing the skill. When a change closes no gap the requirement states, change the requirement first or drop the change.
|
|
34
|
+
- Write the gaps from what the skill is for, then compare the body against them. A requirement derived from the body records the body's accidents as the requirement, and misinforms a reader who reads it in place of the body.
|
|
35
|
+
|
|
36
|
+
## Content
|
|
37
|
+
|
|
38
|
+
- State each gap as an observed failure, not an intent. "Without this skill a session invents its own filename" can be shown wrong. "This skill helps manage tasks" cannot fail, so it constrains nothing.
|
|
39
|
+
- Trace everything under `## Must` to a stated gap. A `Must` with no gap behind it is the padding the file exists to prevent.
|
|
40
|
+
- Keep it high level. A requirement that outgrows one file has stopped being a requirement, and `references/` already holds detail.
|
|
41
|
+
- Use `name` and `description` frontmatter, matching `SKILL.md` so the pair is consistent.
|
|
42
|
+
|
|
43
|
+
## Template
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
---
|
|
47
|
+
name: <skill-name>
|
|
48
|
+
description: <one line, distinct from the SKILL.md description>
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# <Skill name> requirement
|
|
52
|
+
|
|
53
|
+
## Gap
|
|
54
|
+
|
|
55
|
+
Without this skill, a session <observed failure>, <observed failure>.
|
|
56
|
+
|
|
57
|
+
## Must
|
|
58
|
+
|
|
59
|
+
- <behavior that closes a named gap>
|
|
60
|
+
|
|
61
|
+
## Must not
|
|
62
|
+
|
|
63
|
+
- <behavior that would be wrong even though it closes a gap>
|
|
64
|
+
|
|
65
|
+
## Guards
|
|
66
|
+
|
|
67
|
+
- <the refusal condition and its message>
|
|
68
|
+
|
|
69
|
+
## Out of scope
|
|
70
|
+
|
|
71
|
+
- <the adjacent thing this deliberately does not cover, and what covers it>
|
|
72
|
+
```
|