@erclx/canon 5.25.0 → 5.26.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 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canvas/SKILL.md +1 -1
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +8 -6
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +4 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +21 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/draft-identity/SKILL.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +9 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +31 -20
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/references/evidence.md +8 -0
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-split/SKILL.md +19 -8
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +3 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/write-human/SKILL.md +1 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/docs/agents/canvas.md +3 -1
- package/docs/agents/capture.md +1 -1
- package/docs/agents/commands.md +3 -3
- package/docs/agents/index.md +2 -2
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/skills-audit.md +10 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +1 -34
- package/package.json +1 -1
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/sandbox-hook.sh +99 -0
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/color-field.tsx +368 -0
- package/src/canvas/client/inspector/fields.css +163 -22
- package/src/canvas/client/inspector/values.ts +123 -0
- package/src/canvas/client/inspector.tsx +46 -69
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +1 -1
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +68 -14
- package/src/client-commands.ts +1 -1
- package/src/commands/claude.ts +27 -2
- package/src/commands/feedback.ts +3 -3
- package/src/commands/records.ts +88 -6
- package/src/commands/sandbox.ts +50 -10
- package/src/commands/slides.ts +31 -77
- package/src/context/audit.ts +1 -1
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +1 -1
- package/src/gate/measures.ts +6 -4
- package/src/gov/citations.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +2 -2
- package/src/markdown/ceiling.ts +1 -1
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +1 -0
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +596 -0
- package/src/sandbox/tree.ts +103 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/standards/read.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +1 -1
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +1 -0
- package/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { relative } from 'node:path'
|
|
5
|
+
import { gitEnv } from '@/git-env'
|
|
6
|
+
import { readReviewed } from '@/records/stale'
|
|
7
|
+
import { surfaceDir } from '@/surface-root'
|
|
8
|
+
|
|
9
|
+
/** The two records whose drift is identity rather than a stale path. */
|
|
10
|
+
const CANONICAL_DOCS = ['REQUIREMENTS.md', 'ARCHITECTURE.md'] as const
|
|
11
|
+
|
|
12
|
+
export interface CanonicalDoc {
|
|
13
|
+
/** Relative to the root, at whichever surface root carries the doc. */
|
|
14
|
+
readonly path: string
|
|
15
|
+
/** `YYYY-MM-DD`, or null when the doc carries no readable review date. */
|
|
16
|
+
readonly reviewed: string | null
|
|
17
|
+
/** The raw value of a `reviewed` field that is not a date, present only then. */
|
|
18
|
+
readonly invalidReviewed?: string
|
|
19
|
+
/** Release tags merged into HEAD after the reviewed day, or null when never reviewed. */
|
|
20
|
+
readonly releasesSince: number | null
|
|
21
|
+
/** The newest release tag merged into HEAD, or null when there is none. */
|
|
22
|
+
readonly latestRelease: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CanonicalReport {
|
|
26
|
+
readonly ok: true
|
|
27
|
+
readonly root: string
|
|
28
|
+
/** False when HEAD has merged no tag, so a zero count measured nothing. */
|
|
29
|
+
readonly tagged: boolean
|
|
30
|
+
readonly docs: readonly CanonicalDoc[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CanonicalRefused {
|
|
34
|
+
readonly ok: false
|
|
35
|
+
readonly reason: 'unreadable-tags'
|
|
36
|
+
readonly message: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type CanonicalOutcome = CanonicalReport | CanonicalRefused
|
|
40
|
+
|
|
41
|
+
interface Release {
|
|
42
|
+
readonly tag: string
|
|
43
|
+
readonly day: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A tag ending in a dotted version, bare or behind a component prefix such as
|
|
48
|
+
* `canon-v2.1.0`. An eval, deploy, or snapshot tag on the trunk names no
|
|
49
|
+
* release and would otherwise inflate the count.
|
|
50
|
+
*/
|
|
51
|
+
const RELEASE_TAG = /(?:^|[-/@_])v?\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?$/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A repository before its first commit has no HEAD for `--merged` to resolve,
|
|
55
|
+
* which git reports as a failed read. It has no tags either, so it reads as an
|
|
56
|
+
* empty history rather than an unreadable one.
|
|
57
|
+
*/
|
|
58
|
+
async function isUnborn(root: string): Promise<boolean> {
|
|
59
|
+
const [gitDir, head] = await Promise.all([
|
|
60
|
+
$`git -C ${root} rev-parse --git-dir`.env(gitEnv()).quiet().nothrow(),
|
|
61
|
+
$`git -C ${root} rev-parse --verify -q HEAD`
|
|
62
|
+
.env(gitEnv())
|
|
63
|
+
.quiet()
|
|
64
|
+
.nothrow(),
|
|
65
|
+
])
|
|
66
|
+
return gitDir.exitCode === 0 && head.exitCode !== 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every release tag HEAD has merged, newest first, with the day it was created.
|
|
71
|
+
*
|
|
72
|
+
* The creator date is the tagging date on an annotated tag and the commit date
|
|
73
|
+
* on a lightweight one, so a history mixing both counts by two clocks.
|
|
74
|
+
*/
|
|
75
|
+
async function mergedReleases(root: string): Promise<Release[] | undefined> {
|
|
76
|
+
// Interpolated rather than inline, since Bun's shell parses the bare
|
|
77
|
+
// parentheses in `%(refname)` as syntax of its own.
|
|
78
|
+
const args = [
|
|
79
|
+
'--merged',
|
|
80
|
+
'HEAD',
|
|
81
|
+
'--sort=-creatordate',
|
|
82
|
+
'--format=%(creatordate:short) %(refname:short)',
|
|
83
|
+
'refs/tags',
|
|
84
|
+
]
|
|
85
|
+
const result = await $`git -C ${root} for-each-ref ${args}`
|
|
86
|
+
.env(gitEnv())
|
|
87
|
+
.quiet()
|
|
88
|
+
.nothrow()
|
|
89
|
+
if (result.exitCode !== 0) return (await isUnborn(root)) ? [] : undefined
|
|
90
|
+
|
|
91
|
+
return result.stdout
|
|
92
|
+
.toString()
|
|
93
|
+
.split('\n')
|
|
94
|
+
.filter((line) => line.trim().length > 0)
|
|
95
|
+
.map((line) => {
|
|
96
|
+
const [day, tag] = line.trim().split(' ')
|
|
97
|
+
return { day, tag }
|
|
98
|
+
})
|
|
99
|
+
.filter((release) => RELEASE_TAG.test(release.tag))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reports when each canonical doc was last reviewed and how many releases
|
|
104
|
+
* have shipped since, read from an optional `reviewed:` frontmatter field.
|
|
105
|
+
*
|
|
106
|
+
* The review point is a date rather than a commit, since a commit written on a
|
|
107
|
+
* feature branch never reaches a trunk that squash-merges. A release tagged on
|
|
108
|
+
* the reviewed day itself is not counted, as the review is read as covering
|
|
109
|
+
* that day. It reports and never writes.
|
|
110
|
+
*/
|
|
111
|
+
export async function staleCanonical(root: string): Promise<CanonicalOutcome> {
|
|
112
|
+
const releases = await mergedReleases(root)
|
|
113
|
+
|
|
114
|
+
if (releases === undefined) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
reason: 'unreadable-tags',
|
|
118
|
+
message: `Could not read the release tags at ${root}, so no count can be taken.`,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const latestRelease = releases[0]?.tag ?? null
|
|
123
|
+
const present = CANONICAL_DOCS.map((entry) => surfaceDir(root, entry)).filter(
|
|
124
|
+
(path) => existsSync(path),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const docs = await Promise.all(
|
|
128
|
+
present.map(async (path): Promise<CanonicalDoc> => {
|
|
129
|
+
const text = (await readFile(path, 'utf8')).replaceAll('\r\n', '\n')
|
|
130
|
+
const review = readReviewed(text)
|
|
131
|
+
const { reviewed } = review
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
path: relative(root, path),
|
|
135
|
+
...review,
|
|
136
|
+
releasesSince:
|
|
137
|
+
reviewed === null
|
|
138
|
+
? null
|
|
139
|
+
: releases.filter((release) => release.day > reviewed).length,
|
|
140
|
+
latestRelease,
|
|
141
|
+
}
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return { ok: true, root, tagged: releases.length > 0, docs }
|
|
146
|
+
}
|
package/src/records/stale.ts
CHANGED
|
@@ -65,7 +65,7 @@ const RAW_FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
|
65
65
|
* A value that is not a calendar date reads as never reviewed and is carried
|
|
66
66
|
* back so the review can see it, rather than throwing over one hand edit.
|
|
67
67
|
*/
|
|
68
|
-
function readReviewed(
|
|
68
|
+
export function readReviewed(
|
|
69
69
|
text: string,
|
|
70
70
|
): Pick<StaleEntry, 'reviewed' | 'invalidReviewed'> {
|
|
71
71
|
const block = RAW_FRONTMATTER.exec(text)?.[1]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { PROJECT_ROOT } from '@/project-root'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The bash entry a scenario's hooks run through. Scenarios and the library they
|
|
9
|
+
* call stay bash, so this contract is the whole of what the TypeScript harness
|
|
10
|
+
* knows about one: which hooks it declares and what `use_config` exports.
|
|
11
|
+
*/
|
|
12
|
+
const HOOK_SCRIPT = join(PROJECT_ROOT, 'scripts', 'sandbox-hook.sh')
|
|
13
|
+
|
|
14
|
+
const ANCHOR_RECORD = '@anchor='
|
|
15
|
+
|
|
16
|
+
export interface Probe {
|
|
17
|
+
readonly exports: Readonly<Record<string, string>>
|
|
18
|
+
readonly isAnchor: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ProbeResult =
|
|
22
|
+
| ({ readonly ok: true } & Probe)
|
|
23
|
+
| { readonly ok: false; readonly status: number }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reads the NUL-separated `NAME=value` pairs the probe prints. NUL rather than a
|
|
27
|
+
* newline, since an exported value can hold one. The anchor record closes every
|
|
28
|
+
* well-formed probe, so output missing it is a probe that did not finish.
|
|
29
|
+
*/
|
|
30
|
+
export function parseProbe(stdout: string): Probe {
|
|
31
|
+
const { exports, anchor } = parsePairs(stdout)
|
|
32
|
+
if (anchor === undefined)
|
|
33
|
+
throw new Error('the probe printed no anchor record')
|
|
34
|
+
|
|
35
|
+
return { exports, isAnchor: anchor === '1' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parsePairs(text: string): {
|
|
39
|
+
exports: Record<string, string>
|
|
40
|
+
anchor: string | undefined
|
|
41
|
+
} {
|
|
42
|
+
const exports: Record<string, string> = {}
|
|
43
|
+
let anchor: string | undefined
|
|
44
|
+
|
|
45
|
+
for (const record of text.split('\0')) {
|
|
46
|
+
if (record.startsWith(ANCHOR_RECORD)) {
|
|
47
|
+
anchor = record.slice(ANCHOR_RECORD.length)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
const split = record.indexOf('=')
|
|
51
|
+
if (split > 0) exports[record.slice(0, split)] = record.slice(split + 1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { exports, anchor }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Runs `use_config` and `use_anchor` in a child and returns what they exported.
|
|
59
|
+
* Stderr is inherited by default, so a hook that fails narrates into the same
|
|
60
|
+
* frame the harness writes. A non-zero status passes through rather than being
|
|
61
|
+
* flattened, since the harness exits with it.
|
|
62
|
+
*/
|
|
63
|
+
export function probeScenario(
|
|
64
|
+
file: string,
|
|
65
|
+
env: NodeJS.ProcessEnv,
|
|
66
|
+
stderr: 'inherit' | 'pipe' = 'inherit',
|
|
67
|
+
): ProbeResult {
|
|
68
|
+
const result = spawnSync('bash', [HOOK_SCRIPT, 'probe', file], {
|
|
69
|
+
env,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
stdio: ['ignore', 'pipe', stderr],
|
|
72
|
+
})
|
|
73
|
+
const status = result.status ?? 1
|
|
74
|
+
if (status !== 0) return { ok: false, status }
|
|
75
|
+
|
|
76
|
+
return { ok: true, ...parseProbe(result.stdout) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How `stage_setup` ended. The retired dispatcher ran it in its own shell, so
|
|
81
|
+
* an `exit` there ended provisioning through the frame-closing trap, and an
|
|
82
|
+
* `exec` replaced the harness outright, leaving its program's frame as the last
|
|
83
|
+
* one written. The harness reproduces both from this.
|
|
84
|
+
*/
|
|
85
|
+
export type StageEnding = 'returned' | 'exited' | 'replaced'
|
|
86
|
+
|
|
87
|
+
export interface StageResult {
|
|
88
|
+
readonly status: number
|
|
89
|
+
readonly ending: StageEnding
|
|
90
|
+
readonly exports: Readonly<Record<string, string>>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const EXITED_RECORD = '@exited'
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Runs `stage_setup` inside the tree with every stream inherited, so a scenario
|
|
97
|
+
* prompting through `select_or_route_scenario` still reaches a terminal and its
|
|
98
|
+
* narration lands in order with the harness's own.
|
|
99
|
+
*
|
|
100
|
+
* The exports `stage_setup` changed come back through a file rather than a
|
|
101
|
+
* pipe. Stdout belongs to the scenario, and a background process a scenario
|
|
102
|
+
* starts would hold an extra pipe open past the child's exit.
|
|
103
|
+
*/
|
|
104
|
+
export function stageScenario(
|
|
105
|
+
file: string,
|
|
106
|
+
env: NodeJS.ProcessEnv,
|
|
107
|
+
stdio: 'inherit' | 'ignore' = 'inherit',
|
|
108
|
+
): StageResult {
|
|
109
|
+
const scratch = mkdtempSync(join(tmpdir(), 'canon-sandbox-hook-'))
|
|
110
|
+
const report = join(scratch, 'exports')
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const result = spawnSync('bash', [HOOK_SCRIPT, 'stage', file, report], {
|
|
114
|
+
env,
|
|
115
|
+
stdio,
|
|
116
|
+
})
|
|
117
|
+
const status = result.status ?? 1
|
|
118
|
+
if (!existsSync(report)) return { status, ending: 'replaced', exports: {} }
|
|
119
|
+
|
|
120
|
+
const text = readFileSync(report, 'utf8')
|
|
121
|
+
if (text.split('\0').includes(EXITED_RECORD))
|
|
122
|
+
return { status, ending: 'exited', exports: {} }
|
|
123
|
+
|
|
124
|
+
return { status, ending: 'returned', exports: parsePairs(text).exports }
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
127
|
+
}
|
|
128
|
+
}
|