@monte3l/groundwork 0.0.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 +23 -0
- package/bin/m3l-groundwork.mjs +10 -0
- package/dist/assets.d.ts +20 -0
- package/dist/assets.js +79 -0
- package/dist/caps.d.ts +25 -0
- package/dist/caps.js +69 -0
- package/dist/conflicts.d.ts +12 -0
- package/dist/conflicts.js +77 -0
- package/dist/emit.d.ts +7 -0
- package/dist/emit.js +42 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.js +9 -0
- package/dist/harness/conformance.d.ts +20 -0
- package/dist/harness/conformance.js +18 -0
- package/dist/harness/frontmatter.d.ts +38 -0
- package/dist/harness/frontmatter.js +204 -0
- package/dist/harness/grade.d.ts +4 -0
- package/dist/harness/grade.js +105 -0
- package/dist/harness/rules.d.ts +55 -0
- package/dist/harness/rules.js +580 -0
- package/dist/harness/types.d.ts +32 -0
- package/dist/harness/types.js +9 -0
- package/dist/inventory.d.ts +63 -0
- package/dist/inventory.js +66 -0
- package/dist/jsonc.d.ts +14 -0
- package/dist/jsonc.js +83 -0
- package/dist/main.d.ts +24 -0
- package/dist/main.js +297 -0
- package/dist/merge-json.d.ts +74 -0
- package/dist/merge-json.js +135 -0
- package/dist/mode.d.ts +19 -0
- package/dist/mode.js +53 -0
- package/dist/packs.d.ts +61 -0
- package/dist/packs.js +186 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +79 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.js +323 -0
- package/dist/survey/fs-walk.d.ts +14 -0
- package/dist/survey/fs-walk.js +60 -0
- package/dist/survey/survey-docs.d.ts +4 -0
- package/dist/survey/survey-docs.js +69 -0
- package/dist/survey/survey-harness.d.ts +4 -0
- package/dist/survey/survey-harness.js +121 -0
- package/dist/survey/survey-shape.d.ts +4 -0
- package/dist/survey/survey-shape.js +182 -0
- package/dist/survey/survey-toolchain.d.ts +4 -0
- package/dist/survey/survey-toolchain.js +217 -0
- package/dist/survey/survey.d.ts +5 -0
- package/dist/survey/survey.js +21 -0
- package/dist/survey/types.d.ts +117 -0
- package/dist/survey/types.js +8 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.js +13 -0
- package/dist/toolchain/conformance.d.ts +20 -0
- package/dist/toolchain/conformance.js +30 -0
- package/dist/toolchain/grade.d.ts +4 -0
- package/dist/toolchain/grade.js +244 -0
- package/dist/toolchain/rules.d.ts +118 -0
- package/dist/toolchain/rules.js +706 -0
- package/dist/toolchain/tsconfig-chain.d.ts +36 -0
- package/dist/toolchain/tsconfig-chain.js +116 -0
- package/dist/toolchain/types.d.ts +27 -0
- package/dist/toolchain/types.js +9 -0
- package/package.json +59 -0
- package/plugin/skills/customize/SKILL.md +305 -0
- package/plugin/src/domain-map.ts +134 -0
- package/plugin/src/index.ts +4 -0
- package/plugin/src/kind-facet-map.ts +174 -0
- package/plugin/src/pack-map.ts +65 -0
- package/templates/core/.claude/agents/Explore.md +43 -0
- package/templates/core/.claude/agents/code-implementer.md +258 -0
- package/templates/core/.claude/agents/code-reviewer.md +163 -0
- package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
- package/templates/core/.claude/agents/test-author.md +211 -0
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
- package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
- package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
- package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
- package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
- package/templates/core/.claude/rules/agent-dispatch.md +121 -0
- package/templates/core/.claude/rules/refactoring.md +52 -0
- package/templates/core/.claude/rules/src.md +114 -0
- package/templates/core/.claude/rules/tests.md +129 -0
- package/templates/core/.claude/settings.json +111 -0
- package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
- package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
- package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
- package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
- package/templates/core/.github/workflows/ci.yml +123 -0
- package/templates/core/.github/workflows/dependency-review.yml +26 -0
- package/templates/core/.github/workflows/security-audit.yml +54 -0
- package/templates/core/.node-version +1 -0
- package/templates/core/.prettierignore +5 -0
- package/templates/core/.prettierrc.json +4 -0
- package/templates/core/CLAUDE.md +127 -0
- package/templates/core/README.md +24 -0
- package/templates/core/_gitignore +19 -0
- package/templates/core/_npmrc +1 -0
- package/templates/core/bin/check-exports.mjs +92 -0
- package/templates/core/bin/check-harness.mjs +27 -0
- package/templates/core/bin/check-node-version.mjs +51 -0
- package/templates/core/bin/check-toolchain.mjs +20 -0
- package/templates/core/bin/lib/agent-roster.mjs +8 -0
- package/templates/core/bin/lib/frontmatter.mjs +210 -0
- package/templates/core/bin/lib/harness-rules.mjs +916 -0
- package/templates/core/bin/lib/protected-paths.mjs +23 -0
- package/templates/core/bin/lib/report.mjs +56 -0
- package/templates/core/bin/lib/signed-range.mjs +178 -0
- package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
- package/templates/core/bin/lib/verify-steps.mjs +131 -0
- package/templates/core/bin/lib/verify-steps.packs.json +1 -0
- package/templates/core/bin/lint-commit.mjs +50 -0
- package/templates/core/bin/strip-claude-trailers.mjs +25 -0
- package/templates/core/bin/verify.mjs +64 -0
- package/templates/core/commitlint.config.js +11 -0
- package/templates/core/docs/research/harness-refresh.md +27 -0
- package/templates/core/docs/research/typescript-refresh.md +32 -0
- package/templates/core/eslint.config.js +105 -0
- package/templates/core/knip.json +6 -0
- package/templates/core/lefthook.yml +39 -0
- package/templates/core/package.json +58 -0
- package/templates/core/pnpm-workspace.yaml +13 -0
- package/templates/core/src/index.ts +12 -0
- package/templates/core/tests/index.test.ts +8 -0
- package/templates/core/tsconfig.base.json +36 -0
- package/templates/core/tsconfig.build.json +10 -0
- package/templates/core/tsconfig.json +11 -0
- package/templates/core/vitest.config.ts +32 -0
- package/templates/packs/README.md +81 -0
- package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
- package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
- package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
- package/templates/packs/harness-extras/pack.json +65 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
- package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
- package/templates/packs/statusline/pack.json +31 -0
package/dist/report.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { CAP_LIMITS, countBaselineCaps } from "./caps.js";
|
|
2
|
+
/**
|
|
3
|
+
* Estimates the post-merge total against each cap: the baseline's own count
|
|
4
|
+
* plus whatever the existing project already has, on the (approximate)
|
|
5
|
+
* assumption nothing overlaps by name. This is a decision to surface, not a
|
|
6
|
+
* precise reconciliation -- Step 0's confirmation round settles it for real.
|
|
7
|
+
*/
|
|
8
|
+
function estimatePostMergeCaps(inventory) {
|
|
9
|
+
const baseline = countBaselineCaps(inventory.templateRoot);
|
|
10
|
+
const existing = inventory.survey.harness;
|
|
11
|
+
const workflowsExisting = inventory.survey.toolchain.workflows.files.length;
|
|
12
|
+
const scriptsExisting = Object.keys(inventory.survey.toolchain.scripts).length;
|
|
13
|
+
return {
|
|
14
|
+
baseline,
|
|
15
|
+
postMerge: {
|
|
16
|
+
agents: baseline.agents + existing.agents.length,
|
|
17
|
+
skills: baseline.skills + existing.skills.length,
|
|
18
|
+
hooks: baseline.hooks + existing.hooks.length,
|
|
19
|
+
workflows: baseline.workflows + workflowsExisting,
|
|
20
|
+
scripts: baseline.scripts + scriptsExisting,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Sums every listed pack's declared `budget` -- the "if every pack were installed" delta, not just the ones a user will choose. */
|
|
25
|
+
function sumPackBudgets(packs) {
|
|
26
|
+
const total = {
|
|
27
|
+
agents: 0,
|
|
28
|
+
skills: 0,
|
|
29
|
+
hooks: 0,
|
|
30
|
+
workflows: 0,
|
|
31
|
+
scripts: 0,
|
|
32
|
+
};
|
|
33
|
+
for (const pack of packs) {
|
|
34
|
+
total.agents += pack.budget.agents;
|
|
35
|
+
total.skills += pack.budget.skills;
|
|
36
|
+
total.hooks += pack.budget.hooks;
|
|
37
|
+
total.workflows += pack.budget.workflows;
|
|
38
|
+
total.scripts += pack.budget.scripts;
|
|
39
|
+
}
|
|
40
|
+
return total;
|
|
41
|
+
}
|
|
42
|
+
function renderShapeSection(inventory) {
|
|
43
|
+
const s = inventory.survey.shape;
|
|
44
|
+
const lines = [
|
|
45
|
+
"## Codebase shape",
|
|
46
|
+
"",
|
|
47
|
+
`- Package manager: ${s.packageManager}`,
|
|
48
|
+
`- Monorepo tool: ${s.monorepoTool}${s.workspaceGlobs.length > 0 ? ` (${s.workspaceGlobs.join(", ")})` : ""}`,
|
|
49
|
+
`- Module type: ${s.moduleType}`,
|
|
50
|
+
`- TypeScript: ${s.typescriptVersion ?? "not declared"}`,
|
|
51
|
+
`- Node pin: ${s.nodeVersionPin ? `${s.nodeVersionPin.value} (${s.nodeVersionPin.source})` : "none found"}`,
|
|
52
|
+
`- Source layout: ${s.sourceLayout}`,
|
|
53
|
+
`- Test placement: ${s.testPlacement}`,
|
|
54
|
+
`- Kind evidence: exports map=${s.kindEvidence.hasExportsMap}, bin=${s.kindEvidence.hasBinField}, main=${s.kindEvidence.hasMainField}, framework deps=${s.kindEvidence.frameworkDeps.join(", ") || "none"}`,
|
|
55
|
+
];
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
function renderToolchainSection(inventory) {
|
|
59
|
+
const t = inventory.survey.toolchain;
|
|
60
|
+
const lines = [
|
|
61
|
+
"## Toolchain enforcement in effect",
|
|
62
|
+
"",
|
|
63
|
+
`- tsconfig chain: ${t.tsconfig.files.length > 0 ? t.tsconfig.files.join(" -> ") : "none found"}`,
|
|
64
|
+
`- Effective strict flags: ${Object.keys(t.tsconfig.effectiveFlags).length > 0 ? JSON.stringify(t.tsconfig.effectiveFlags) : "none set"}`,
|
|
65
|
+
`- ESLint: ${t.eslint.configFile ? `${t.eslint.configFile} (${t.eslint.flat ? "flat" : "legacy"} config)` : "not found"}`,
|
|
66
|
+
`- Test runner: ${t.testRunner.tool}${t.testRunner.configFile ? ` (${t.testRunner.configFile})` : ""}`,
|
|
67
|
+
`- Formatter: ${t.formatter.tool}${t.formatter.configFile ? ` (${t.formatter.configFile})` : ""}`,
|
|
68
|
+
`- Git hooks: ${t.gitHooks.manager}${t.gitHooks.configFile ? ` (${t.gitHooks.configFile})` : ""}`,
|
|
69
|
+
`- CI workflows: ${t.workflows.files.length > 0 ? t.workflows.files.join(", ") : "none found"}`,
|
|
70
|
+
`- package.json scripts: ${Object.keys(t.scripts).length > 0 ? Object.keys(t.scripts).join(", ") : "none"}`,
|
|
71
|
+
];
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
function renderHarnessSection(inventory) {
|
|
75
|
+
const h = inventory.survey.harness;
|
|
76
|
+
if (!h.present) {
|
|
77
|
+
return [
|
|
78
|
+
"## Existing Claude Code harness",
|
|
79
|
+
"",
|
|
80
|
+
"No `.claude/` directory found -- the baseline harness would be entirely new.",
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
const lines = [
|
|
84
|
+
"## Existing Claude Code harness",
|
|
85
|
+
"",
|
|
86
|
+
`- Agents: ${h.agents.length > 0 ? h.agents.map((a) => a.name).join(", ") : "none"}`,
|
|
87
|
+
`- Skills: ${h.skills.length > 0 ? h.skills.map((s) => s.name).join(", ") : "none"}`,
|
|
88
|
+
`- Hooks: ${h.hooks.length > 0 ? h.hooks.join(", ") : "none"}`,
|
|
89
|
+
`- Rules: ${h.rules.length > 0 ? h.rules.map((r) => r.name).join(", ") : "none"}`,
|
|
90
|
+
`- Commands: ${h.commands.length > 0 ? h.commands.join(", ") : "none"}`,
|
|
91
|
+
`- settings.local.json present: ${h.hasSettingsLocal}`,
|
|
92
|
+
];
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
function renderDocsSection(inventory) {
|
|
96
|
+
const files = inventory.survey.docs.files;
|
|
97
|
+
const lines = ["## Human-facing docs & guidelines", ""];
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
lines.push("No CONTRIBUTING.md, docs/contributing, ADR directory, or style guide found.");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
lines.push(`- ${file.path}${file.headings.length > 0 ? ` -- ${file.headings.join(" / ")}` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
/** The toolchain grade: wiring integrity, rubric quality, and drift from the baseline -- three separate measurements. */
|
|
109
|
+
function renderToolchainGradeSection(inventory) {
|
|
110
|
+
const { toolchainGrade: grade, toolchainConformance: conformance } = inventory;
|
|
111
|
+
const lines = ["## Toolchain grade", ""];
|
|
112
|
+
const rubricChecked = Object.values(grade.rubric).reduce((sum, tally) => sum + tally.checked, 0);
|
|
113
|
+
if (grade.structural.checked === 0 && rubricChecked === 0) {
|
|
114
|
+
lines.push("No TypeScript toolchain files (tsconfig, ESLint or vitest config, verify steps) found -- nothing to grade.");
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
lines.push(`- **Wiring (structural):** ${grade.structural.checked - grade.structural.failed} of ${grade.structural.checked} checks pass.`, `- **Quality (rubric):** ${Math.round(grade.rubricScore * 100)}% over ${rubricChecked} checks -- advisory, never a blocker.`, `- **Drift from the baseline toolchain:** ${conformance.identical} identical, ${conformance.divergent} divergent, ${conformance.absent} absent. ` +
|
|
118
|
+
"Informational only -- `/customize` rewrites the baseline on purpose, so divergence is not a defect.");
|
|
119
|
+
const structural = grade.findings.filter((f) => f.level === "structural");
|
|
120
|
+
const rubric = grade.findings.filter((f) => f.level === "rubric");
|
|
121
|
+
lines.push("", "### Toolchain wiring findings", "");
|
|
122
|
+
if (structural.length === 0) {
|
|
123
|
+
lines.push("None.");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
for (const finding of structural) {
|
|
127
|
+
lines.push(`- [${finding.ruleId}] ${finding.subject} -- ${finding.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
lines.push("", "### Toolchain quality findings (advisory)", "");
|
|
131
|
+
if (rubric.length === 0) {
|
|
132
|
+
lines.push("None.");
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
for (const finding of rubric) {
|
|
136
|
+
lines.push(`- [${finding.ruleId}] ${finding.subject} -- ${finding.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
/** The harness grade: wiring integrity, rubric quality, and drift from the baseline -- three separate measurements. */
|
|
142
|
+
function renderHarnessGradeSection(inventory) {
|
|
143
|
+
const { harnessGrade: grade, harnessConformance: conformance } = inventory;
|
|
144
|
+
const harness = inventory.survey.harness;
|
|
145
|
+
const lines = ["## Harness grade", ""];
|
|
146
|
+
if (!harness.present && !harness.hasClaudeMd) {
|
|
147
|
+
lines.push("No `.claude/` directory or `CLAUDE.md` found -- nothing to grade.");
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
const rubricChecked = Object.values(grade.rubric).reduce((sum, tally) => sum + tally.checked, 0);
|
|
151
|
+
lines.push(`- **Wiring (structural):** ${grade.structural.checked - grade.structural.failed} of ${grade.structural.checked} checks pass.`, `- **Quality (rubric):** ${Math.round(grade.rubricScore * 100)}% over ${rubricChecked} checks -- advisory, never a blocker.`, `- **Drift from the baseline harness:** ${conformance.identical} identical, ${conformance.divergent} divergent, ${conformance.absent} absent. ` +
|
|
152
|
+
"Informational only -- `/customize` rewrites the baseline on purpose, so divergence is not a defect.");
|
|
153
|
+
const structural = grade.findings.filter((f) => f.level === "structural");
|
|
154
|
+
const rubric = grade.findings.filter((f) => f.level === "rubric");
|
|
155
|
+
lines.push("", "### Wiring findings", "");
|
|
156
|
+
if (structural.length === 0) {
|
|
157
|
+
lines.push("None.");
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
for (const finding of structural) {
|
|
161
|
+
lines.push(`- [${finding.ruleId}] ${finding.subject} -- ${finding.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
lines.push("", "### Quality findings (advisory)", "");
|
|
165
|
+
if (rubric.length === 0) {
|
|
166
|
+
lines.push("None.");
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
for (const finding of rubric) {
|
|
170
|
+
lines.push(`- [${finding.ruleId}] ${finding.subject} -- ${finding.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (harness.present && conformance.divergentFiles.length > 0) {
|
|
174
|
+
lines.push("", "### Baseline harness files this project has changed", "", ...conformance.divergentFiles.map((file) => `- ${file}`));
|
|
175
|
+
}
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
function renderCapsSection(inventory) {
|
|
179
|
+
const { baseline, postMerge } = estimatePostMergeCaps(inventory);
|
|
180
|
+
const hasPacks = inventory.packs.length > 0;
|
|
181
|
+
const packBudget = sumPackBudgets(inventory.packs);
|
|
182
|
+
const overCap = (count, cap) => count > cap ? " ⚠ over cap" : "";
|
|
183
|
+
const header = hasPacks
|
|
184
|
+
? "| Artifact | Baseline | Existing project | + all packs | Post-merge (approx.) | Cap |"
|
|
185
|
+
: "| Artifact | Baseline | Existing project | Post-merge (approx.) | Cap |";
|
|
186
|
+
const divider = hasPacks
|
|
187
|
+
? "| --- | --- | --- | --- | --- | --- |"
|
|
188
|
+
: "| --- | --- | --- | --- | --- |";
|
|
189
|
+
const row = (label, key, existingCount, cap) => {
|
|
190
|
+
const cells = [label, String(baseline[key]), String(existingCount)];
|
|
191
|
+
if (hasPacks)
|
|
192
|
+
cells.push(`+${packBudget[key]}`);
|
|
193
|
+
cells.push(`${postMerge[key]}${overCap(postMerge[key], cap)}`, String(cap));
|
|
194
|
+
return `| ${cells.join(" | ")} |`;
|
|
195
|
+
};
|
|
196
|
+
const lines = [
|
|
197
|
+
"## Baseline caps after a merge (approximate)",
|
|
198
|
+
"",
|
|
199
|
+
header,
|
|
200
|
+
divider,
|
|
201
|
+
row("Agents", "agents", inventory.survey.harness.agents.length, CAP_LIMITS.agents),
|
|
202
|
+
row("Skills", "skills", inventory.survey.harness.skills.length, CAP_LIMITS.skills),
|
|
203
|
+
row("Hooks", "hooks", inventory.survey.harness.hooks.length, CAP_LIMITS.hooks),
|
|
204
|
+
"",
|
|
205
|
+
"This is an approximate count assuming no name overlap; `/customize`'s Step 0 resolves it for real.",
|
|
206
|
+
];
|
|
207
|
+
if (hasPacks) {
|
|
208
|
+
lines.push("", '"+ all packs" sums every pack listed below, not just the ones you choose ' +
|
|
209
|
+
'to install -- see "## Available packs" for per-pack numbers.');
|
|
210
|
+
}
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
function renderConflictTable(conflicts) {
|
|
214
|
+
const divergent = conflicts.filter((c) => c.status === "divergent");
|
|
215
|
+
if (divergent.length === 0) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const lines = ["", "| File | Differing keys |", "| --- | --- |"];
|
|
219
|
+
for (const conflict of divergent) {
|
|
220
|
+
lines.push(`| ${conflict.relPath} | ${conflict.keyDiffs?.join(", ") ?? "(whole file)"} |`);
|
|
221
|
+
}
|
|
222
|
+
return lines;
|
|
223
|
+
}
|
|
224
|
+
function renderPacksSection(inventory) {
|
|
225
|
+
const lines = ["## Available packs", ""];
|
|
226
|
+
if (inventory.packs.length === 0) {
|
|
227
|
+
lines.push("No packs found under `templates/packs/`.");
|
|
228
|
+
return lines.join("\n");
|
|
229
|
+
}
|
|
230
|
+
for (const pack of inventory.packs) {
|
|
231
|
+
const b = pack.budget;
|
|
232
|
+
lines.push(`### ${pack.name}`, "", `- Modes: ${pack.modes.join(", ")}`, `- Budget: ${b.agents} agent(s), ${b.skills} skill(s), ${b.hooks} hook(s), ${b.workflows} workflow(s), ${b.scripts} script(s)`);
|
|
233
|
+
for (const observation of pack.wiringObservations) {
|
|
234
|
+
lines.push(`- ${observation}`);
|
|
235
|
+
}
|
|
236
|
+
if (pack.adoptNotes !== undefined) {
|
|
237
|
+
lines.push(`- Adopt notes: ${pack.adoptNotes}`);
|
|
238
|
+
}
|
|
239
|
+
lines.push(...renderConflictTable(pack.fileConflicts), "");
|
|
240
|
+
}
|
|
241
|
+
return lines
|
|
242
|
+
.join("\n")
|
|
243
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
244
|
+
.trimEnd();
|
|
245
|
+
}
|
|
246
|
+
function renderConflictsSection(inventory) {
|
|
247
|
+
const { conflicts } = inventory;
|
|
248
|
+
const absent = conflicts.filter((c) => c.status === "absent");
|
|
249
|
+
const identical = conflicts.filter((c) => c.status === "identical");
|
|
250
|
+
const divergent = conflicts.filter((c) => c.status === "divergent");
|
|
251
|
+
const lines = [
|
|
252
|
+
"## What groundwork would change",
|
|
253
|
+
"",
|
|
254
|
+
`- ${absent.length} file(s) would be added cleanly (no collision).`,
|
|
255
|
+
`- ${identical.length} file(s) already match the baseline.`,
|
|
256
|
+
`- ${divergent.length} file(s) conflict and need a decision.`,
|
|
257
|
+
];
|
|
258
|
+
if (divergent.length > 0) {
|
|
259
|
+
lines.push(...renderConflictTable(conflicts));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
lines.push("", "No conflicts found.");
|
|
263
|
+
}
|
|
264
|
+
return lines.join("\n");
|
|
265
|
+
}
|
|
266
|
+
function renderUndeterminedSection(inventory) {
|
|
267
|
+
const lines = ["## Could not be determined", ""];
|
|
268
|
+
if (inventory.survey.undetermined.length === 0) {
|
|
269
|
+
lines.push("Nothing -- every file the survey looked at parsed cleanly.");
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
for (const entry of inventory.survey.undetermined) {
|
|
273
|
+
lines.push(`- ${entry}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
/** Renders the full adoption report as Markdown. */
|
|
279
|
+
export function renderReport(inventory) {
|
|
280
|
+
const sections = [
|
|
281
|
+
"# Adoption report",
|
|
282
|
+
"",
|
|
283
|
+
`Generated ${inventory.generatedAt} by m3l-groundwork ${inventory.cliVersion}.`,
|
|
284
|
+
"",
|
|
285
|
+
`**Mode:** adopt -- ${inventory.modeSignal}`,
|
|
286
|
+
"",
|
|
287
|
+
renderShapeSection(inventory),
|
|
288
|
+
"",
|
|
289
|
+
renderToolchainSection(inventory),
|
|
290
|
+
renderToolchainGradeSection(inventory),
|
|
291
|
+
"",
|
|
292
|
+
renderHarnessSection(inventory),
|
|
293
|
+
"",
|
|
294
|
+
renderHarnessGradeSection(inventory),
|
|
295
|
+
"",
|
|
296
|
+
renderDocsSection(inventory),
|
|
297
|
+
"",
|
|
298
|
+
renderCapsSection(inventory),
|
|
299
|
+
"",
|
|
300
|
+
renderPacksSection(inventory),
|
|
301
|
+
"",
|
|
302
|
+
renderConflictsSection(inventory),
|
|
303
|
+
"",
|
|
304
|
+
renderUndeterminedSection(inventory),
|
|
305
|
+
"",
|
|
306
|
+
"## Next step",
|
|
307
|
+
"",
|
|
308
|
+
"Open this project in Claude Code and run `/customize`. It reads this " +
|
|
309
|
+
"report and `.groundwork/inventory.json`, does a deeper read of " +
|
|
310
|
+
"anything above marked as needing one, and asks you to confirm before " +
|
|
311
|
+
"changing anything. Did this report miss something about your " +
|
|
312
|
+
"project? Say so when `/customize` asks -- that confirmation round " +
|
|
313
|
+
"is the point where it's caught.",
|
|
314
|
+
"",
|
|
315
|
+
"`.groundwork/` itself was not added to this project's `.gitignore` -- " +
|
|
316
|
+
"that choice is yours. It's disposable (regenerate it any time by " +
|
|
317
|
+
"re-running the CLI), so most projects gitignore it; some prefer to " +
|
|
318
|
+
"commit it as a record of what the adoption found.",
|
|
319
|
+
"",
|
|
320
|
+
];
|
|
321
|
+
return sections.join("\n");
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface WalkEntry {
|
|
2
|
+
/** Absolute path. */
|
|
3
|
+
path: string;
|
|
4
|
+
/** Path relative to the walk root, using forward slashes. */
|
|
5
|
+
relPath: string;
|
|
6
|
+
isDirectory: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Recursively lists `root`, skipping known dependency/build directories and
|
|
10
|
+
* stopping once a descendant is more than `maxDepth` directories below
|
|
11
|
+
* `root`. Missing or unreadable directories are skipped, not thrown.
|
|
12
|
+
*/
|
|
13
|
+
export declare function walkBounded(root: string, maxDepth: number): WalkEntry[];
|
|
14
|
+
//# sourceMappingURL=fs-walk.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded directory walk shared by every survey collector. Adopt mode may
|
|
3
|
+
* run against an arbitrarily large pre-existing repository (dependency
|
|
4
|
+
* trees, build output, a decade of history), so nothing under this package
|
|
5
|
+
* ever does an unbounded recursive scan -- every walk skips the common
|
|
6
|
+
* dependency/build directories and stops at an explicit depth.
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
const SKIP_DIR_NAMES = new Set([
|
|
11
|
+
"node_modules",
|
|
12
|
+
".git",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
"coverage",
|
|
16
|
+
".next",
|
|
17
|
+
".turbo",
|
|
18
|
+
".cache",
|
|
19
|
+
".pnpm",
|
|
20
|
+
"out",
|
|
21
|
+
".nx",
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Recursively lists `root`, skipping known dependency/build directories and
|
|
25
|
+
* stopping once a descendant is more than `maxDepth` directories below
|
|
26
|
+
* `root`. Missing or unreadable directories are skipped, not thrown.
|
|
27
|
+
*/
|
|
28
|
+
export function walkBounded(root, maxDepth) {
|
|
29
|
+
const results = [];
|
|
30
|
+
const visit = (dir, depth) => {
|
|
31
|
+
if (depth > maxDepth) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.isDirectory() && SKIP_DIR_NAMES.has(entry.name)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const absPath = join(dir, entry.name);
|
|
46
|
+
const relPath = relative(root, absPath).split("\\").join("/");
|
|
47
|
+
results.push({
|
|
48
|
+
path: absPath,
|
|
49
|
+
relPath,
|
|
50
|
+
isDirectory: entry.isDirectory(),
|
|
51
|
+
});
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
visit(absPath, depth + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
visit(root, 0);
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=fs-walk.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indexes human-facing docs and guidelines: `CONTRIBUTING.md`, a
|
|
3
|
+
* `docs/contributing/` tree, ADR/decision/RFC directories, style guides, and
|
|
4
|
+
* the README's own heading outline. This is an index (path, size, headings)
|
|
5
|
+
* so `/customize`'s Step 0 knows what exists and where to read it in full --
|
|
6
|
+
* it never inlines a doc's content itself, which would make the survey's
|
|
7
|
+
* own output as large as the docs it's indexing.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { walkBounded } from "./fs-walk.js";
|
|
12
|
+
const NAMED_ROOT_CANDIDATES = ["README.md", "CONTRIBUTING.md"];
|
|
13
|
+
const NAMED_DIR_CANDIDATES = [
|
|
14
|
+
"docs/contributing",
|
|
15
|
+
"docs/adr",
|
|
16
|
+
"docs/decisions",
|
|
17
|
+
"docs/rfcs",
|
|
18
|
+
"decisions",
|
|
19
|
+
"adr",
|
|
20
|
+
];
|
|
21
|
+
function extractHeadings(content) {
|
|
22
|
+
return content
|
|
23
|
+
.split("\n")
|
|
24
|
+
.filter((line) => /^#{1,3}\s/.test(line))
|
|
25
|
+
.map((line) => line.replace(/^#{1,3}\s*/, "").trim());
|
|
26
|
+
}
|
|
27
|
+
function indexFile(path) {
|
|
28
|
+
const content = readFileSync(path, "utf8");
|
|
29
|
+
return {
|
|
30
|
+
path,
|
|
31
|
+
sizeBytes: statSync(path).size,
|
|
32
|
+
headings: extractHeadings(content),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function collectRootMarkdown(dir) {
|
|
36
|
+
const found = [];
|
|
37
|
+
for (const name of NAMED_ROOT_CANDIDATES) {
|
|
38
|
+
const path = join(dir, name);
|
|
39
|
+
if (existsSync(path))
|
|
40
|
+
found.push(indexFile(path));
|
|
41
|
+
}
|
|
42
|
+
for (const entry of walkBounded(dir, 0)) {
|
|
43
|
+
if (!entry.isDirectory && /^STYLE.*\.md$/i.test(entry.relPath)) {
|
|
44
|
+
found.push(indexFile(entry.path));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
function collectNamedDirectories(dir) {
|
|
50
|
+
const found = [];
|
|
51
|
+
for (const relDir of NAMED_DIR_CANDIDATES) {
|
|
52
|
+
const absDir = join(dir, relDir);
|
|
53
|
+
if (!existsSync(absDir))
|
|
54
|
+
continue;
|
|
55
|
+
for (const entry of walkBounded(absDir, 2)) {
|
|
56
|
+
if (!entry.isDirectory && entry.relPath.endsWith(".md")) {
|
|
57
|
+
found.push(indexFile(entry.path));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
/** Indexes docs/guideline files at `dir`. Offline, read-only, index-only -- no full content. */
|
|
64
|
+
export function surveyDocs(dir) {
|
|
65
|
+
return {
|
|
66
|
+
files: [...collectRootMarkdown(dir), ...collectNamedDirectories(dir)],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=survey-docs.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects the whole `.claude/` surface of an existing project -- hook
|
|
3
|
+
* wiring, agent/skill/rule frontmatter, commands, and `CLAUDE.md`'s heading
|
|
4
|
+
* outline -- so `/customize`'s harness sweep starts from what is actually
|
|
5
|
+
* there instead of assuming the m3l-groundwork baseline.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fieldText, parseFrontmatter } from "../harness/frontmatter.js";
|
|
10
|
+
/**
|
|
11
|
+
* Reads one frontmatter field as text (a list is joined with `, `), or
|
|
12
|
+
* `undefined` when the file has no frontmatter, the field is absent, or its
|
|
13
|
+
* value is empty. Goes through the shared YAML-subset reader so a folded
|
|
14
|
+
* `description: >-` block -- the shape every baseline skill uses -- yields
|
|
15
|
+
* its text rather than the literal `>-`.
|
|
16
|
+
*/
|
|
17
|
+
function extractFrontmatterField(content, field) {
|
|
18
|
+
const parsed = parseFrontmatter(content);
|
|
19
|
+
if (!parsed.ok)
|
|
20
|
+
return undefined;
|
|
21
|
+
const text = fieldText(parsed.fields, field);
|
|
22
|
+
return text === undefined || text === "" ? undefined : text;
|
|
23
|
+
}
|
|
24
|
+
function listMarkdownFiles(dir) {
|
|
25
|
+
if (!existsSync(dir))
|
|
26
|
+
return [];
|
|
27
|
+
return readdirSync(dir).filter((name) => name.endsWith(".md"));
|
|
28
|
+
}
|
|
29
|
+
function surveyAgents(claudeDir) {
|
|
30
|
+
const agentsDir = join(claudeDir, "agents");
|
|
31
|
+
return listMarkdownFiles(agentsDir).map((name) => {
|
|
32
|
+
const content = readFileSync(join(agentsDir, name), "utf8");
|
|
33
|
+
return {
|
|
34
|
+
name: name.replace(/\.md$/, ""),
|
|
35
|
+
model: extractFrontmatterField(content, "model"),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function surveySkills(claudeDir) {
|
|
40
|
+
const skillsDir = join(claudeDir, "skills");
|
|
41
|
+
if (!existsSync(skillsDir))
|
|
42
|
+
return [];
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
45
|
+
if (!entry.isDirectory())
|
|
46
|
+
continue;
|
|
47
|
+
const skillFile = join(skillsDir, entry.name, "SKILL.md");
|
|
48
|
+
if (!existsSync(skillFile))
|
|
49
|
+
continue;
|
|
50
|
+
const content = readFileSync(skillFile, "utf8");
|
|
51
|
+
results.push({
|
|
52
|
+
name: extractFrontmatterField(content, "name") ?? entry.name,
|
|
53
|
+
description: extractFrontmatterField(content, "description"),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
function surveyHooks(claudeDir) {
|
|
59
|
+
const hooksDir = join(claudeDir, "hooks");
|
|
60
|
+
if (!existsSync(hooksDir))
|
|
61
|
+
return [];
|
|
62
|
+
return readdirSync(hooksDir).filter((name) => name.endsWith(".mjs") || name.endsWith(".js"));
|
|
63
|
+
}
|
|
64
|
+
function surveyRules(claudeDir) {
|
|
65
|
+
const rulesDir = join(claudeDir, "rules");
|
|
66
|
+
return listMarkdownFiles(rulesDir).map((name) => {
|
|
67
|
+
const content = readFileSync(join(rulesDir, name), "utf8");
|
|
68
|
+
return {
|
|
69
|
+
name: name.replace(/\.md$/, ""),
|
|
70
|
+
paths: extractFrontmatterField(content, "paths"),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function surveyCommands(claudeDir) {
|
|
75
|
+
const commandsDir = join(claudeDir, "commands");
|
|
76
|
+
return listMarkdownFiles(commandsDir);
|
|
77
|
+
}
|
|
78
|
+
function extractHeadings(content) {
|
|
79
|
+
return content
|
|
80
|
+
.split("\n")
|
|
81
|
+
.filter((line) => /^#{1,3}\s/.test(line))
|
|
82
|
+
.map((line) => line.replace(/^#{1,3}\s*/, "").trim());
|
|
83
|
+
}
|
|
84
|
+
/** Surveys the `.claude/` harness and `CLAUDE.md` at `dir`. Offline, read-only. */
|
|
85
|
+
export function surveyHarness(dir) {
|
|
86
|
+
const claudeDir = join(dir, ".claude");
|
|
87
|
+
const claudeMdPath = join(dir, "CLAUDE.md");
|
|
88
|
+
const hasClaudeMd = existsSync(claudeMdPath);
|
|
89
|
+
if (!existsSync(claudeDir)) {
|
|
90
|
+
return {
|
|
91
|
+
present: false,
|
|
92
|
+
settingsFile: undefined,
|
|
93
|
+
agents: [],
|
|
94
|
+
skills: [],
|
|
95
|
+
hooks: [],
|
|
96
|
+
rules: [],
|
|
97
|
+
commands: [],
|
|
98
|
+
hasSettingsLocal: false,
|
|
99
|
+
hasClaudeMd,
|
|
100
|
+
claudeMdHeadings: hasClaudeMd
|
|
101
|
+
? extractHeadings(readFileSync(claudeMdPath, "utf8"))
|
|
102
|
+
: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
106
|
+
return {
|
|
107
|
+
present: true,
|
|
108
|
+
settingsFile: existsSync(settingsPath) ? "settings.json" : undefined,
|
|
109
|
+
agents: surveyAgents(claudeDir),
|
|
110
|
+
skills: surveySkills(claudeDir),
|
|
111
|
+
hooks: surveyHooks(claudeDir),
|
|
112
|
+
rules: surveyRules(claudeDir),
|
|
113
|
+
commands: surveyCommands(claudeDir),
|
|
114
|
+
hasSettingsLocal: existsSync(join(claudeDir, "settings.local.json")),
|
|
115
|
+
hasClaudeMd,
|
|
116
|
+
claudeMdHeadings: hasClaudeMd
|
|
117
|
+
? extractHeadings(readFileSync(claudeMdPath, "utf8"))
|
|
118
|
+
: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=survey-harness.js.map
|