@erclx/canon 5.42.0 → 5.44.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/auto-ship/SKILL.md +3 -3
- package/claude/skills/backlog-triage/references/apply.md +6 -6
- package/claude/skills/canon-cli/SKILL.md +4 -4
- package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
- package/claude/skills/canvas-board/SKILL.md +1 -1
- package/claude/skills/ci-workflow/REQUIREMENT.md +2 -1
- package/claude/skills/ci-workflow/SKILL.md +12 -2
- package/claude/skills/ci-workflow/references/workflows.md +12 -9
- package/claude/skills/context-fold/references/context.md +1 -1
- package/claude/skills/create-rule/SKILL.md +1 -0
- package/claude/skills/create-skill/REQUIREMENT.md +1 -1
- package/claude/skills/create-skill/SKILL.md +2 -2
- package/claude/skills/create-skill/references/skill-requirement.md +68 -9
- package/claude/skills/create-skill/references/skill.md +1 -1
- package/claude/skills/decision-escalate/SKILL.md +1 -2
- package/claude/skills/design-identity/REQUIREMENT.md +40 -0
- package/claude/skills/design-identity/SKILL.md +138 -0
- package/claude/skills/{draft-identity → design-identity}/references/card-route.md +2 -2
- package/claude/skills/design-taste/REQUIREMENT.md +4 -4
- package/claude/skills/design-taste/SKILL.md +2 -2
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/preflight.md +1 -1
- package/claude/skills/document-health/references/axes.md +2 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -69
- package/claude/skills/draft-and-pick/SKILL.md +3 -153
- package/claude/skills/draft-doc/REQUIREMENT.md +9 -30
- package/claude/skills/draft-doc/SKILL.md +4 -76
- package/claude/skills/draft-figure/REQUIREMENT.md +8 -42
- package/claude/skills/draft-figure/SKILL.md +3 -77
- package/claude/skills/draft-identity/REQUIREMENT.md +8 -23
- package/claude/skills/draft-identity/SKILL.md +3 -130
- package/claude/skills/draft-ready/REQUIREMENT.md +8 -37
- package/claude/skills/draft-ready/SKILL.md +3 -128
- package/claude/skills/draft-screencast/REQUIREMENT.md +8 -23
- package/claude/skills/draft-screencast/SKILL.md +3 -62
- package/claude/skills/draft-slides/REQUIREMENT.md +8 -27
- package/claude/skills/draft-slides/SKILL.md +3 -72
- package/claude/skills/draw-figure/REQUIREMENT.md +59 -0
- package/claude/skills/draw-figure/SKILL.md +85 -0
- package/claude/skills/{draft-figure → draw-figure}/references/architecture-set.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/hand-drawn.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/mermaid-path.md +2 -2
- package/claude/skills/{draft-figure → draw-figure}/references/mermaid.md +1 -1
- package/claude/skills/git-commit/references/commit.md +1 -5
- package/claude/skills/git-pr/references/pr.md +1 -0
- package/claude/skills/make-deck/REQUIREMENT.md +44 -0
- package/claude/skills/make-deck/SKILL.md +80 -0
- package/claude/skills/markdown-craft/REQUIREMENT.md +1 -1
- package/claude/skills/markdown-craft/SKILL.md +2 -2
- package/claude/skills/pick-by-eye/REQUIREMENT.md +86 -0
- package/claude/skills/pick-by-eye/SKILL.md +161 -0
- package/claude/skills/plan-feature/SKILL.md +3 -2
- package/claude/skills/plan-feature/references/plan.md +25 -11
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/plan-intake/SKILL.md +1 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +54 -0
- package/claude/skills/plan-ready/SKILL.md +136 -0
- package/claude/skills/{draft-ready → plan-ready}/references/assembly.md +1 -1
- package/claude/skills/{draft-ready → plan-ready}/references/ready.md +1 -1
- package/claude/skills/pr-evidence/references/evidence.md +1 -1
- package/claude/skills/record-screencast/REQUIREMENT.md +3 -3
- package/claude/skills/record-screencast/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +5 -2
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +18 -13
- package/claude/skills/role-planner/REQUIREMENT.md +1 -1
- package/claude/skills/role-planner/SKILL.md +5 -4
- package/claude/skills/role-worker/REQUIREMENT.md +2 -0
- package/claude/skills/role-worker/SKILL.md +35 -2
- package/claude/skills/script-screencast/REQUIREMENT.md +40 -0
- package/claude/skills/script-screencast/SKILL.md +70 -0
- package/claude/skills/{draft-screencast → script-screencast}/references/beats-template.md +2 -2
- package/claude/skills/search-craft/REQUIREMENT.md +4 -4
- package/claude/skills/search-craft/SKILL.md +2 -2
- package/claude/skills/target-setup/SKILL.md +1 -0
- package/claude/skills/task-board/SKILL.md +11 -9
- package/claude/skills/task-board/references/board.md +15 -12
- package/claude/skills/teach-workspace/SKILL.md +1 -1
- package/claude/skills/teach-workspace/references/lesson-craft.md +2 -2
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- package/claude/skills/write-doc/REQUIREMENT.md +46 -0
- package/claude/skills/write-doc/SKILL.md +83 -0
- package/claude/skills/{draft-doc → write-doc}/references/context.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/docs.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/readme.md +2 -2
- package/claude/skills/{draft-doc → write-doc}/references/wiki-kind.md +4 -4
- package/claude/skills/{draft-doc → write-doc}/references/wiki.md +1 -1
- package/docs/agents/audits.md +1 -0
- package/docs/agents/markdown-audit.md +4 -2
- package/docs/agents/migrate.md +13 -10
- package/docs/target/migrations.md +28 -4
- package/docs/target/projects.md +1 -1
- package/docs/workflow/parallel-features.md +3 -2
- package/docs/workflow/skill-map-on-request.md +18 -12
- package/docs/workflow/skill-map.md +13 -12
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +4 -0
- package/governance/ledger/framework/astro.md +73 -0
- package/governance/ledger/framework/fastapi.md +76 -0
- package/governance/ledger/framework/nextjs.md +86 -0
- package/governance/ledger/framework/react.md +83 -0
- package/governance/ledger/framework/shadcn.md +60 -0
- package/governance/ledger/framework/tailwind.md +55 -0
- package/governance/rules/canon/context.md +2 -1
- package/governance/rules/canon/plan.md +3 -5
- package/governance/rules/canon/scratch.md +3 -1
- package/governance/rules/canon/task-scope.md +2 -1
- package/governance/rules/canon/tasks.md +2 -1
- package/governance/rules/canon/worktrees.md +5 -2
- package/governance/rules/claude/output.md +7 -4
- package/governance/rules/claude/skill.md +6 -4
- package/governance/rules/framework/astro.md +12 -14
- package/governance/rules/framework/fastapi.md +20 -24
- package/governance/rules/framework/nextjs.md +19 -27
- package/governance/rules/framework/react.md +22 -33
- package/governance/rules/framework/shadcn.md +11 -15
- package/governance/rules/framework/tailwind.md +12 -14
- package/governance/rules/tooling/ci-workflow.md +6 -39
- package/governance/rules/ui/design-reference.md +37 -0
- package/governance/rules/writing/markdown.md +3 -2
- package/package.json +6 -6
- package/src/audits/catalog.ts +42 -0
- package/src/claude/cases/authoring.ts +15 -3
- package/src/claude/cases/misc.ts +12 -0
- package/src/claude/cases/workflow.ts +19 -1
- package/src/claude/settings.ts +8 -4
- package/src/claude/skills-audit.ts +113 -1
- package/src/commands/claude.ts +45 -12
- package/src/commands/demo.ts +1 -1
- package/src/commands/gov.ts +198 -0
- package/src/commands/markdown.ts +1 -1
- package/src/commands/migrate.ts +125 -2
- package/src/commands/pr.ts +34 -83
- package/src/commands/tasks.ts +383 -2
- package/src/commands/tooling.ts +3 -3
- package/src/commands/worktrees.ts +49 -0
- package/src/context/audit.ts +1 -1
- package/src/demo/beats.ts +1 -1
- package/src/gate/stages.ts +15 -0
- package/src/gov/audit.ts +441 -0
- package/src/listeners/belongs.ts +21 -0
- package/src/listeners/machine.ts +93 -0
- package/src/listeners/parse.ts +41 -0
- package/src/markdown/scope.ts +45 -35
- package/src/migrate/record-layout.ts +1 -1
- package/src/migrate/session-defaults.ts +138 -0
- package/src/migrate/skill-names.ts +68 -21
- package/src/pr/frames.ts +14 -3
- package/src/pr/local.ts +4 -69
- package/src/records/validate.ts +31 -0
- package/src/sync/check.ts +15 -0
- package/src/tasks/board.ts +854 -0
- package/src/tasks/record.ts +20 -1
- package/src/tasks/validate.ts +206 -9
- package/src/tooling/stamp.ts +56 -12
- package/src/worktrees/stop.ts +92 -0
- package/tooling/astro/configs/card-src/pages/og-card.astro +1 -1
- package/tooling/astro/reference.md +2 -2
- package/tooling/base/configs/.github/workflows/phase-label-gate.yml +6 -2
- package/tooling/base/configs/commitlint.config.js +0 -20
- package/tooling/base/reference.md +1 -1
- package/tooling/base/seeds/.github/workflows/verify.yml +4 -1
- package/tooling/claude/reference.md +13 -7
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +65 -0
- package/tooling/claude/seeds/.claude/settings.json +8 -0
- package/tooling/claude/user/settings.template.json +0 -4
- package/tooling/cloudflare/configs/.github/workflows/deploy.yml +5 -5
- package/tooling/public/configs/.github/dependabot.yml +12 -0
- package/tooling/public/configs/.github/workflows/release-please.yml +30 -0
- package/tooling/public/manifest.toml +8 -0
- package/tooling/public/reference.md +53 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/bug.yml +48 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/tooling/public/seeds/.github/ISSUE_TEMPLATE/feature.yml +16 -0
- package/tooling/public/seeds/.release-please-manifest.json +3 -0
- package/tooling/public/seeds/CONTRIBUTING.md +50 -0
- package/tooling/public/seeds/LICENSE +21 -0
- package/tooling/public/seeds/SECURITY.md +13 -0
- package/tooling/public/seeds/release-please-config.json +12 -0
- package/tooling/shell/configs/.github/workflows/shell.yml +4 -1
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +6 -3
- package/tooling/web/configs/.github/workflows/verify.yml +15 -12
- /package/claude/skills/{draft-figure → draw-figure}/references/figures.md +0 -0
- /package/claude/skills/{draft-slides → make-deck}/references/deck.md +0 -0
- /package/claude/skills/{draft-and-pick → pick-by-eye}/references/live-arms.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/agent-facing.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/application.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/cli.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/library.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/readme/plugin.md +0 -0
- /package/claude/skills/{draft-doc → write-doc}/references/requirements.md +0 -0
package/src/markdown/scope.ts
CHANGED
|
@@ -8,19 +8,28 @@ const BULLET_RUN_SCOPE = [
|
|
|
8
8
|
/^(?:claude|\.claude)\/skills\/[^/]+\/references\/.+\.md$/,
|
|
9
9
|
]
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The requirement beside each skill. Its findings gate only on a `GATED_PATHS`
|
|
13
|
+
* entry naming the file itself, so the prefix that gates a skill's body never
|
|
14
|
+
* sweeps in a requirement the rewrite has not reached yet.
|
|
15
|
+
*/
|
|
16
|
+
const REQUIREMENT_SCOPE =
|
|
17
|
+
/^(?:claude|\.claude)\/skills\/[^/]+\/REQUIREMENT\.md$/
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* The tracked documents the density limits hold: the context entries, the docs
|
|
13
|
-
* pages, the skill bodies and
|
|
14
|
-
* seeds and stack references, the wiki, and the two root guides.
|
|
21
|
+
* pages, the skill bodies, references, and requirements of both skill trees,
|
|
22
|
+
* the tooling seeds and stack references, the wiki, and the two root guides.
|
|
15
23
|
*
|
|
16
24
|
* Rules stay out, since a rule is one imperative per bullet. So do
|
|
17
|
-
* `CLAUDE.md`, the requirement and design records, a seed carrying one
|
|
18
|
-
* those three names, the changelog, the examples, and every `.canon/`
|
|
19
|
-
* each with a shape and cap of its own. Named here and nowhere else, so
|
|
20
|
-
* audit, the gate stage reading its exit, and their tests read one scope.
|
|
25
|
+
* `CLAUDE.md`, the project requirement and design records, a seed carrying one
|
|
26
|
+
* of those three names, the changelog, the examples, and every `.canon/`
|
|
27
|
+
* record, each with a shape and cap of its own. Named here and nowhere else, so
|
|
28
|
+
* the audit, the gate stage reading its exit, and their tests read one scope.
|
|
21
29
|
*/
|
|
22
30
|
const DENSITY_SCOPE = [
|
|
23
31
|
...BULLET_RUN_SCOPE,
|
|
32
|
+
REQUIREMENT_SCOPE,
|
|
24
33
|
/^tooling\/[^/]+\/seeds\/(?:.+\/)?(?!(?:CLAUDE|REQUIREMENTS|DESIGN)\.md$)[^/]+\.md$/,
|
|
25
34
|
/^tooling\/[^/]+\/reference\.md$/,
|
|
26
35
|
/^wiki\/.+\.md$/,
|
|
@@ -105,35 +114,35 @@ export const GATED_PATHS: readonly string[] = [
|
|
|
105
114
|
'claude/skills/docs-sync/references/readme.md',
|
|
106
115
|
'claude/skills/document-health/SKILL.md',
|
|
107
116
|
'claude/skills/document-health/references/axes.md',
|
|
108
|
-
'claude/skills/
|
|
109
|
-
'claude/skills/
|
|
110
|
-
'claude/skills/
|
|
111
|
-
'claude/skills/
|
|
112
|
-
'claude/skills/
|
|
113
|
-
'claude/skills/
|
|
114
|
-
'claude/skills/
|
|
115
|
-
'claude/skills/
|
|
116
|
-
'claude/skills/
|
|
117
|
-
'claude/skills/
|
|
118
|
-
'claude/skills/
|
|
119
|
-
'claude/skills/
|
|
120
|
-
'claude/skills/
|
|
121
|
-
'claude/skills/
|
|
122
|
-
'claude/skills/
|
|
123
|
-
'claude/skills/
|
|
124
|
-
'claude/skills/
|
|
125
|
-
'claude/skills/
|
|
126
|
-
'claude/skills/
|
|
127
|
-
'claude/skills/
|
|
128
|
-
'claude/skills/
|
|
129
|
-
'claude/skills/
|
|
130
|
-
'claude/skills/
|
|
131
|
-
'claude/skills/
|
|
132
|
-
'claude/skills/
|
|
133
|
-
'claude/skills/
|
|
134
|
-
'claude/skills/
|
|
135
|
-
'claude/skills/
|
|
136
|
-
'claude/skills/
|
|
117
|
+
'claude/skills/pick-by-eye/SKILL.md',
|
|
118
|
+
'claude/skills/pick-by-eye/references/live-arms.md',
|
|
119
|
+
'claude/skills/write-doc/SKILL.md',
|
|
120
|
+
'claude/skills/write-doc/references/context.md',
|
|
121
|
+
'claude/skills/write-doc/references/docs.md',
|
|
122
|
+
'claude/skills/write-doc/references/readme.md',
|
|
123
|
+
'claude/skills/write-doc/references/readme/agent-facing.md',
|
|
124
|
+
'claude/skills/write-doc/references/readme/application.md',
|
|
125
|
+
'claude/skills/write-doc/references/readme/cli.md',
|
|
126
|
+
'claude/skills/write-doc/references/readme/library.md',
|
|
127
|
+
'claude/skills/write-doc/references/readme/plugin.md',
|
|
128
|
+
'claude/skills/write-doc/references/requirements.md',
|
|
129
|
+
'claude/skills/write-doc/references/wiki-kind.md',
|
|
130
|
+
'claude/skills/write-doc/references/wiki.md',
|
|
131
|
+
'claude/skills/draw-figure/SKILL.md',
|
|
132
|
+
'claude/skills/draw-figure/references/architecture-set.md',
|
|
133
|
+
'claude/skills/draw-figure/references/figures.md',
|
|
134
|
+
'claude/skills/draw-figure/references/hand-drawn.md',
|
|
135
|
+
'claude/skills/draw-figure/references/mermaid-path.md',
|
|
136
|
+
'claude/skills/draw-figure/references/mermaid.md',
|
|
137
|
+
'claude/skills/design-identity/SKILL.md',
|
|
138
|
+
'claude/skills/design-identity/references/card-route.md',
|
|
139
|
+
'claude/skills/plan-ready/SKILL.md',
|
|
140
|
+
'claude/skills/plan-ready/references/assembly.md',
|
|
141
|
+
'claude/skills/plan-ready/references/ready.md',
|
|
142
|
+
'claude/skills/script-screencast/SKILL.md',
|
|
143
|
+
'claude/skills/script-screencast/references/beats-template.md',
|
|
144
|
+
'claude/skills/make-deck/SKILL.md',
|
|
145
|
+
'claude/skills/make-deck/references/deck.md',
|
|
137
146
|
'claude/skills/git-branch/SKILL.md',
|
|
138
147
|
'claude/skills/git-branch/references/branch.md',
|
|
139
148
|
'claude/skills/git-branch/references/slug.md',
|
|
@@ -301,6 +310,7 @@ export function densityGates(
|
|
|
301
310
|
gated: readonly string[] = GATED_PATHS,
|
|
302
311
|
): boolean {
|
|
303
312
|
if (!inDensityScope(rel)) return false
|
|
313
|
+
if (REQUIREMENT_SCOPE.test(rel)) return gated.includes(rel)
|
|
304
314
|
if (
|
|
305
315
|
ALWAYS_GATED.includes(kind) &&
|
|
306
316
|
BULLET_RUN_SCOPE.some((pattern) => pattern.test(rel))
|
|
@@ -346,7 +346,7 @@ function entriesOf(dir: string, isDirectory: boolean): string[] {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
/**
|
|
349
|
-
* What `
|
|
349
|
+
* What `pick-by-eye` Step 6 writes into the folder it archives a choice to:
|
|
350
350
|
* a capture per canvas frame, named `arm-<id>`. A design handoff marks a pick
|
|
351
351
|
* folder an earlier skill wrote. A folder carrying neither is evidence.
|
|
352
352
|
*/
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds the session defaults the seeded `.claude/settings.json` carries to a
|
|
3
|
+
* target that was seeded before them.
|
|
4
|
+
*
|
|
5
|
+
* `canon claude init` copies a seed only when the file is absent, so a target
|
|
6
|
+
* holding its own settings never sees a later seed change. This verb adds each
|
|
7
|
+
* default only where the key is absent: a deny entry is never removed, and a
|
|
8
|
+
* person's `autoMemoryEnabled: true` is never overwritten.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import {
|
|
14
|
+
readSettings,
|
|
15
|
+
type SettingsKey,
|
|
16
|
+
type SettingsRecord,
|
|
17
|
+
serializeSettings,
|
|
18
|
+
writeSettings,
|
|
19
|
+
} from '@/claude/settings'
|
|
20
|
+
|
|
21
|
+
/** Both names, since `Task` is the tool's name before Claude Code renamed it. */
|
|
22
|
+
export const DENIED_TOOLS: readonly string[] = ['Agent', 'Task']
|
|
23
|
+
|
|
24
|
+
export interface SessionDefaultsPlan {
|
|
25
|
+
readonly keys: readonly SettingsKey[]
|
|
26
|
+
readonly next: SettingsRecord
|
|
27
|
+
readonly changed: boolean
|
|
28
|
+
/**
|
|
29
|
+
* True when the file holds no default at all. A file holding either one has
|
|
30
|
+
* been through the seed or this verb, so a missing deny there was removed
|
|
31
|
+
* on purpose, which is how an operator opts back into subagents.
|
|
32
|
+
*/
|
|
33
|
+
readonly predates: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type SessionDefaultsRead =
|
|
37
|
+
| { readonly kind: 'refused'; readonly reason: string }
|
|
38
|
+
| { readonly kind: 'absent'; readonly path: string }
|
|
39
|
+
| {
|
|
40
|
+
readonly kind: 'planned'
|
|
41
|
+
readonly path: string
|
|
42
|
+
readonly indent: string
|
|
43
|
+
readonly plan: SessionDefaultsPlan
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function settingsPath(root: string): string {
|
|
47
|
+
return join(root, '.claude', 'settings.json')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is SettingsRecord {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Appends the denied tools a list lacks, keeping the list's own order, so the
|
|
56
|
+
* diff a person reads is the added entries alone.
|
|
57
|
+
*/
|
|
58
|
+
export function planSessionDefaults(
|
|
59
|
+
current: SettingsRecord,
|
|
60
|
+
): SessionDefaultsPlan {
|
|
61
|
+
const next: SettingsRecord = { ...current }
|
|
62
|
+
const permissions = isRecord(current.permissions) ? current.permissions : {}
|
|
63
|
+
const deny = Array.isArray(permissions.deny) ? permissions.deny : []
|
|
64
|
+
|
|
65
|
+
const missing = DENIED_TOOLS.filter((tool) => !deny.includes(tool))
|
|
66
|
+
if (missing.length > 0) {
|
|
67
|
+
next.permissions = { ...permissions, deny: [...deny, ...missing] }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const memoryAbsent = !('autoMemoryEnabled' in current)
|
|
71
|
+
if (memoryAbsent) next.autoMemoryEnabled = false
|
|
72
|
+
|
|
73
|
+
const keys: SettingsKey[] = [
|
|
74
|
+
{ label: 'permissions.deny', changed: missing.length > 0 },
|
|
75
|
+
{ label: 'autoMemoryEnabled', changed: memoryAbsent },
|
|
76
|
+
]
|
|
77
|
+
return {
|
|
78
|
+
keys,
|
|
79
|
+
next,
|
|
80
|
+
changed: keys.some((key) => key.changed),
|
|
81
|
+
predates: memoryAbsent && missing.length === DENIED_TOOLS.length,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Refuses rather than plans whenever a write could replace something the
|
|
87
|
+
* person owns: the toolkit's own settings, a file `JSON.parse` cannot read,
|
|
88
|
+
* or a `permissions` or `deny` value of an unexpected shape.
|
|
89
|
+
*/
|
|
90
|
+
export async function readSessionDefaults(
|
|
91
|
+
root: string,
|
|
92
|
+
protectedRoot: string,
|
|
93
|
+
): Promise<SessionDefaultsRead> {
|
|
94
|
+
if (root === protectedRoot) {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'refused',
|
|
97
|
+
reason:
|
|
98
|
+
'Cannot run against toolkit root. Files here are the source of truth.',
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const path = settingsPath(root)
|
|
103
|
+
if (!existsSync(path)) return { kind: 'absent', path }
|
|
104
|
+
|
|
105
|
+
let read: Awaited<ReturnType<typeof readSettings>>
|
|
106
|
+
try {
|
|
107
|
+
read = await readSettings(path)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'refused',
|
|
111
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { permissions } = read.value
|
|
116
|
+
if (permissions !== undefined && !isRecord(permissions)) {
|
|
117
|
+
return { kind: 'refused', reason: `${path}: permissions is not an object` }
|
|
118
|
+
}
|
|
119
|
+
if (permissions?.deny !== undefined && !Array.isArray(permissions.deny)) {
|
|
120
|
+
return {
|
|
121
|
+
kind: 'refused',
|
|
122
|
+
reason: `${path}: permissions.deny is not a list`,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
kind: 'planned',
|
|
128
|
+
path,
|
|
129
|
+
indent: read.indent,
|
|
130
|
+
plan: planSessionDefaults(read.value),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function applySessionDefaults(
|
|
135
|
+
read: Extract<SessionDefaultsRead, { kind: 'planned' }>,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
await writeSettings(read.path, serializeSettings(read.plan.next, read.indent))
|
|
138
|
+
}
|
|
@@ -13,13 +13,12 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
|
|
|
13
13
|
* session roles together.
|
|
14
14
|
*
|
|
15
15
|
* The four `canon-` rows split the same way rather than sharing one
|
|
16
|
-
* replacement prefix. `canon-screencast` and `canon-slides-draft`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
20
|
-
*
|
|
21
|
-
* reading
|
|
22
|
-
* no prefix forcing that reading.
|
|
16
|
+
* replacement prefix. `canon-screencast` and `canon-slides-draft` each make
|
|
17
|
+
* an artifact, so each takes the verb-first name of what it makes,
|
|
18
|
+
* `script-screencast` and `make-deck`. `canon-record` and `canon-frames-read`
|
|
19
|
+
* take `record-screencast` and `read-frames`, reading in sequence with
|
|
20
|
+
* `script-screencast` as one three-step pipeline with no prefix forcing that
|
|
21
|
+
* reading.
|
|
23
22
|
*
|
|
24
23
|
* Two rows rename a skill rather than retiring a prefix. `ui-test` became
|
|
25
24
|
* `ui-checklist` when the skill stopped writing tests and shrank to the
|
|
@@ -36,15 +35,25 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
|
|
|
36
35
|
* Five rows merge skills rather than rename one. `draft-docs`,
|
|
37
36
|
* `draft-context`, `draft-wireframes`, `draft-readme`, and `draft-wiki` ran
|
|
38
37
|
* one procedure five times over, differing only in the standard each read and
|
|
39
|
-
* the catalog each checked, so they became
|
|
40
|
-
* from where the document lands. Every retired name lands on
|
|
41
|
-
*
|
|
38
|
+
* the catalog each checked, so they became one skill, which picks the kind
|
|
39
|
+
* from where the document lands. Every retired name lands on that skill, so a
|
|
40
|
+
* target citing any of them resolves after a single sweep.
|
|
42
41
|
*
|
|
43
|
-
* `draft-diagram` merged into
|
|
42
|
+
* `draft-diagram` merged into the figure skill the same way, its architecture
|
|
44
43
|
* views becoming one reference the figure skill reads. `claude-diagram` is
|
|
45
|
-
* retargeted onto
|
|
44
|
+
* retargeted onto the figure skill rather than left on a retired name, for the
|
|
46
45
|
* same reason `claude-ui-test` was.
|
|
47
46
|
*
|
|
47
|
+
* The `draft-` family then dissolved. Its seven members shared a verb rather
|
|
48
|
+
* than a phase or a role, so the prefix grouped a deck, a figure, a brand
|
|
49
|
+
* identity, a pick loop, and a thin plan under one word that said nothing
|
|
50
|
+
* about which of them a skill makes. Each took a verb-first name for its
|
|
51
|
+
* output instead: `write-doc`, `draw-figure`, `design-identity`, `make-deck`,
|
|
52
|
+
* `pick-by-eye`, `plan-ready`, and `script-screencast`. Every older key that
|
|
53
|
+
* landed on a `draft-` name is retargeted straight onto its new one, so a
|
|
54
|
+
* target on the oldest spelling lands in one pass rather than two.
|
|
55
|
+
* `draft-ready` alone keys on its namespaced forms below rather than here.
|
|
56
|
+
*
|
|
48
57
|
* Every name takes two words. Ten of these would have landed as a bare single
|
|
49
58
|
* word under a plain strip, and a bare word such as `review` or `docs` is a
|
|
50
59
|
* substring of ordinary prose with no token left for a later sweep to find.
|
|
@@ -63,7 +72,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
63
72
|
'claude-address-review': 'review-address',
|
|
64
73
|
'claude-autoship': 'auto-ship',
|
|
65
74
|
'claude-design-extract': 'design-extract',
|
|
66
|
-
'claude-diagram': '
|
|
75
|
+
'claude-diagram': 'draw-figure',
|
|
67
76
|
'claude-docs': 'context-fold',
|
|
68
77
|
'docs-fold': 'context-fold',
|
|
69
78
|
'claude-feature': 'plan-feature',
|
|
@@ -87,16 +96,22 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
87
96
|
'claude-ux-measure': 'ux-measure',
|
|
88
97
|
'claude-worker': 'role-worker',
|
|
89
98
|
'claude-worktree': 'session-worktree',
|
|
90
|
-
'canon-screencast': '
|
|
91
|
-
'canon-slides-draft': '
|
|
99
|
+
'canon-screencast': 'script-screencast',
|
|
100
|
+
'canon-slides-draft': 'make-deck',
|
|
92
101
|
'canon-record': 'record-screencast',
|
|
93
102
|
'canon-frames-read': 'read-frames',
|
|
94
|
-
'draft-docs': '
|
|
95
|
-
'draft-context': '
|
|
96
|
-
'draft-wireframes': '
|
|
97
|
-
'draft-readme': '
|
|
98
|
-
'draft-wiki': '
|
|
99
|
-
'draft-diagram': '
|
|
103
|
+
'draft-docs': 'write-doc',
|
|
104
|
+
'draft-context': 'write-doc',
|
|
105
|
+
'draft-wireframes': 'write-doc',
|
|
106
|
+
'draft-readme': 'write-doc',
|
|
107
|
+
'draft-wiki': 'write-doc',
|
|
108
|
+
'draft-diagram': 'draw-figure',
|
|
109
|
+
'draft-doc': 'write-doc',
|
|
110
|
+
'draft-figure': 'draw-figure',
|
|
111
|
+
'draft-identity': 'design-identity',
|
|
112
|
+
'draft-slides': 'make-deck',
|
|
113
|
+
'draft-and-pick': 'pick-by-eye',
|
|
114
|
+
'draft-screencast': 'script-screencast',
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
/**
|
|
@@ -110,14 +125,42 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
110
125
|
* guard rejects a following `-`, so a rewritten `canon:canvas-board` is not
|
|
111
126
|
* matched again.
|
|
112
127
|
*
|
|
128
|
+
* `draft-ready` became `plan-ready` and takes the same treatment for a
|
|
129
|
+
* different reason. It is two words, but they are ordinary prose for a pull
|
|
130
|
+
* request moving from draft to ready, and the sweep takes no scope in a
|
|
131
|
+
* target, so a bare key would rewrite that sentence in every tracked file a
|
|
132
|
+
* project wrote.
|
|
133
|
+
*
|
|
113
134
|
* What this leaves behind is a prose mention of the skill by its bare name,
|
|
114
135
|
* which has no token for a sweep to find and is fixed by hand.
|
|
115
136
|
*/
|
|
116
137
|
export const SKILL_CITATION_MAP: Readonly<Record<string, string>> = {
|
|
117
138
|
'canon:canvas': 'canon:canvas-board',
|
|
118
139
|
'skills/canvas': 'skills/canvas-board',
|
|
140
|
+
'canon:draft-ready': 'canon:plan-ready',
|
|
141
|
+
'skills/draft-ready': 'skills/plan-ready',
|
|
119
142
|
}
|
|
120
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The old `draft-` names that still answer as a pointer skill, each naming the
|
|
146
|
+
* skill it became.
|
|
147
|
+
*
|
|
148
|
+
* Each pointer's own files spell the old name on purpose, so the sweep must
|
|
149
|
+
* never reach them. A later run would otherwise move the stub onto the real
|
|
150
|
+
* body at the new name. Their paths join the exclusions only once the sweep
|
|
151
|
+
* that moved the real bodies has run, since excluding them first leaves each
|
|
152
|
+
* body unmoved at its old path.
|
|
153
|
+
*/
|
|
154
|
+
export const RENAMED_DRAFT_POINTERS: readonly string[] = [
|
|
155
|
+
'draft-doc',
|
|
156
|
+
'draft-figure',
|
|
157
|
+
'draft-identity',
|
|
158
|
+
'draft-slides',
|
|
159
|
+
'draft-and-pick',
|
|
160
|
+
'draft-ready',
|
|
161
|
+
'draft-screencast',
|
|
162
|
+
]
|
|
163
|
+
|
|
121
164
|
/**
|
|
122
165
|
* The skill rename.
|
|
123
166
|
*
|
|
@@ -159,5 +202,9 @@ export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
|
|
|
159
202
|
'CHANGELOG.md',
|
|
160
203
|
'src/migrate/skill-names.ts',
|
|
161
204
|
'src/migrate/skill-names.test.ts',
|
|
205
|
+
...RENAMED_DRAFT_POINTERS.flatMap((name) => [
|
|
206
|
+
`claude/skills/${name}/SKILL.md`,
|
|
207
|
+
`claude/skills/${name}/REQUIREMENT.md`,
|
|
208
|
+
]),
|
|
162
209
|
],
|
|
163
210
|
})
|
package/src/pr/frames.ts
CHANGED
|
@@ -22,6 +22,9 @@ const ADD_ATTEMPTS = 3
|
|
|
22
22
|
/** One half of a reference address. It opens on a letter or digit and holds no separator, so `..` and `/` cannot survive it. */
|
|
23
23
|
const REFERENCE_PART = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
24
24
|
|
|
25
|
+
/** A tracked reference, `canon/design/<surface>/<name>`, whose name may carry its `.png`. The halves are validated as a canvas address's are. */
|
|
26
|
+
const DESIGN_REFERENCE = /^canon\/design\/([^/]+)\/([^/]+?)(?:\.png)?$/
|
|
27
|
+
|
|
25
28
|
const FOLDER_PATTERN = /^pr-(\d+)$/
|
|
26
29
|
|
|
27
30
|
const DAY_MS = 86_400_000
|
|
@@ -163,20 +166,28 @@ export function frameLink(repo: string, path: string): string {
|
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
/**
|
|
166
|
-
* The file-name slug of a `<page>/<frame>` address
|
|
169
|
+
* The file-name slug of a `<page>/<frame>` address or a tracked
|
|
170
|
+
* `canon/design/<surface>/<name>` path, or undefined when the
|
|
167
171
|
* address would not round-trip. Each half must open on a letter or digit and
|
|
168
172
|
* hold only letters, digits, dots, underscores, and dashes, and neither may
|
|
169
173
|
* carry `--`, so the joined slug reads back as the same two names and a path
|
|
170
174
|
* never gains a directory or escapes the pass folder.
|
|
171
175
|
*/
|
|
172
176
|
export function referenceSlug(address: string): string | undefined {
|
|
173
|
-
const
|
|
177
|
+
const tracked = DESIGN_REFERENCE.exec(address)
|
|
178
|
+
const parts = tracked ? [tracked[1], tracked[2]] : address.split('/')
|
|
174
179
|
if (parts.length !== 2) return undefined
|
|
175
180
|
const [page, frame] = parts as [string, string]
|
|
176
181
|
const valid = [page, frame].every(
|
|
177
182
|
(part) => REFERENCE_PART.test(part) && !part.includes('--'),
|
|
178
183
|
)
|
|
179
|
-
|
|
184
|
+
if (!valid) return undefined
|
|
185
|
+
return tracked ? `design--${page}--${frame}` : `${page}--${frame}`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Whether a reference names a tracked `canon/design/<surface>/<name>` file rather than a `<page>/<frame>` address. */
|
|
189
|
+
export function isDesignReference(address: string): boolean {
|
|
190
|
+
return DESIGN_REFERENCE.test(address)
|
|
180
191
|
}
|
|
181
192
|
|
|
182
193
|
/**
|
package/src/pr/local.ts
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
|
+
import { belongsTo } from '@/listeners/belongs'
|
|
2
|
+
import type { ListenerReader } from '@/listeners/machine'
|
|
1
3
|
import { readAddresses } from '@/pr/evidence'
|
|
2
4
|
|
|
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
5
|
export type LocalRefusal = 'no-server' | 'no-listener-reader'
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
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>
|
|
7
|
+
/** The listener reads a detection makes plus an HTML probe, injected so it can be driven without binding a socket. */
|
|
8
|
+
export interface LocalRunner extends ListenerReader {
|
|
24
9
|
/** Whether the port serves an HTML page at its root inside the runner's timeout. */
|
|
25
10
|
probe(port: number): Promise<boolean>
|
|
26
11
|
}
|
|
@@ -29,22 +14,6 @@ export type LocalResult =
|
|
|
29
14
|
| { readonly kind: 'found'; readonly port: number; readonly url: string }
|
|
30
15
|
| { readonly kind: 'refused'; readonly reason: LocalRefusal }
|
|
31
16
|
|
|
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
17
|
/**
|
|
49
18
|
* Finds the server this worktree is running: the lowest port whose listening
|
|
50
19
|
* process runs inside the worktree and answers an HTTP probe. A port rather
|
|
@@ -76,40 +45,6 @@ export async function findLocalServer(
|
|
|
76
45
|
return { kind: 'refused', reason: 'no-server' }
|
|
77
46
|
}
|
|
78
47
|
|
|
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
48
|
/**
|
|
114
49
|
* Drops the address line of a marked body, hosted and local segments alike,
|
|
115
50
|
* since the close that runs this also deletes the branch's preview
|
package/src/records/validate.ts
CHANGED
|
@@ -208,6 +208,7 @@ const PLAN_SECTIONS = [
|
|
|
208
208
|
'Summary',
|
|
209
209
|
'Constraints',
|
|
210
210
|
'Files to touch',
|
|
211
|
+
'Design references',
|
|
211
212
|
'Verification',
|
|
212
213
|
'Risks',
|
|
213
214
|
'Review focus',
|
|
@@ -226,6 +227,22 @@ const PLAN_REQUIRED: readonly PlanSection[] = [
|
|
|
226
227
|
'Questions',
|
|
227
228
|
]
|
|
228
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The files whose edit changes what a person sees, the set the `ui/a11y` rule
|
|
232
|
+
* globs plus the other markup and style extensions a plan names. A plan touching
|
|
233
|
+
* one owes a `Design references` section, so a set too wide forces it on every
|
|
234
|
+
* plan and one too narrow lets a UI plan skip it.
|
|
235
|
+
*/
|
|
236
|
+
const RENDERED_SURFACE = /\.(?:tsx|jsx|astro|vue|svelte|html|css|scss)$/
|
|
237
|
+
|
|
238
|
+
export function touchesRenderedSurface(entries: readonly string[]): boolean {
|
|
239
|
+
return entries.some((entry) =>
|
|
240
|
+
[...entry.matchAll(/`([^`]+)`/g)].some((span) =>
|
|
241
|
+
RENDERED_SURFACE.test(span[1]),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
229
246
|
/**
|
|
230
247
|
* A line standing alone as a bold label or an H2, whatever it names. A plan is
|
|
231
248
|
* free to carry a section of its own, so the split has to see one to close the
|
|
@@ -450,6 +467,20 @@ export function checkPlan(name: string, text: string): Finding[] {
|
|
|
450
467
|
}
|
|
451
468
|
}
|
|
452
469
|
|
|
470
|
+
if (
|
|
471
|
+
!sections.has('Design references') &&
|
|
472
|
+
touchesRenderedSurface(sections.get('Files to touch') ?? [])
|
|
473
|
+
) {
|
|
474
|
+
findings.push(
|
|
475
|
+
finding(
|
|
476
|
+
'section-missing',
|
|
477
|
+
name,
|
|
478
|
+
preferredMarker('Design references'),
|
|
479
|
+
'is required when Files to touch names a rendered-surface file. Write None identified. when no reference folder governs it.',
|
|
480
|
+
),
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
453
484
|
for (const line of sections.get('Files to touch') ?? []) {
|
|
454
485
|
const trimmed = line.trim()
|
|
455
486
|
if (!trimmed.startsWith('- ') || trimmed === `- ${NONE_IDENTIFIED}`)
|
package/src/sync/check.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
resolveChain,
|
|
39
39
|
} from '@/tooling/manifest'
|
|
40
40
|
import { scan } from '@/tooling/scan'
|
|
41
|
+
import { isAddOn } from '@/tooling/stamp'
|
|
41
42
|
import { readSkew, type SkewReport } from '@/version/skew'
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -302,6 +303,20 @@ export function buildToolingReport(
|
|
|
302
303
|
function resolveRecorded(
|
|
303
304
|
toolkitRoot: string,
|
|
304
305
|
recorded: readonly string[],
|
|
306
|
+
): Manifest[] {
|
|
307
|
+
const addOns = recorded
|
|
308
|
+
.map((name) => loadManifest(toolkitRoot, name))
|
|
309
|
+
.filter((manifest) => manifest !== undefined)
|
|
310
|
+
.filter(isAddOn)
|
|
311
|
+
const addOnNames = new Set(addOns.map((manifest) => manifest.name))
|
|
312
|
+
const core = recorded.filter((name) => !addOnNames.has(name))
|
|
313
|
+
|
|
314
|
+
return [...resolveCore(toolkitRoot, core), ...addOns]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function resolveCore(
|
|
318
|
+
toolkitRoot: string,
|
|
319
|
+
recorded: readonly string[],
|
|
305
320
|
): Manifest[] {
|
|
306
321
|
const loaded = recorded
|
|
307
322
|
.map((name) => loadManifest(toolkitRoot, name))
|