@friedbotstudio/create-baseline 0.1.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/LICENSE +202 -0
- package/README.md +222 -0
- package/bin/cli.js +247 -0
- package/obj/template/.claude/agents/swarm-worker.md +52 -0
- package/obj/template/.claude/bin/LICENSE +201 -0
- package/obj/template/.claude/bin/NOTICE +48 -0
- package/obj/template/.claude/commands/approve-spec.md +29 -0
- package/obj/template/.claude/commands/approve-swarm.md +27 -0
- package/obj/template/.claude/commands/grant-commit.md +19 -0
- package/obj/template/.claude/commands/init-project.md +191 -0
- package/obj/template/.claude/hooks/artifact_template_guard.sh +141 -0
- package/obj/template/.claude/hooks/consent_gate_grant.sh +89 -0
- package/obj/template/.claude/hooks/destructive_cmd_guard.sh +42 -0
- package/obj/template/.claude/hooks/env_guard.sh +36 -0
- package/obj/template/.claude/hooks/git_commit_guard.sh +93 -0
- package/obj/template/.claude/hooks/harness_continuation.sh +121 -0
- package/obj/template/.claude/hooks/lib/__pycache__/resume_writer.cpython-314.pyc +0 -0
- package/obj/template/.claude/hooks/lib/common.sh +328 -0
- package/obj/template/.claude/hooks/lib/resume_writer.py +341 -0
- package/obj/template/.claude/hooks/lint_runner.sh +55 -0
- package/obj/template/.claude/hooks/memory_pre_compact.sh +36 -0
- package/obj/template/.claude/hooks/memory_session_start.sh +244 -0
- package/obj/template/.claude/hooks/memory_stop.sh +173 -0
- package/obj/template/.claude/hooks/plantuml_syntax_guard.sh +161 -0
- package/obj/template/.claude/hooks/process_lifecycle_guard.sh +89 -0
- package/obj/template/.claude/hooks/setup_guard.sh +50 -0
- package/obj/template/.claude/hooks/spec_approval_guard.sh +81 -0
- package/obj/template/.claude/hooks/spec_design_calls_guard.sh +183 -0
- package/obj/template/.claude/hooks/spec_diagram_presence_guard.sh +141 -0
- package/obj/template/.claude/hooks/swarm_approval_guard.sh +39 -0
- package/obj/template/.claude/hooks/swarm_boundary_guard.sh +136 -0
- package/obj/template/.claude/hooks/tdd_order_guard.sh +176 -0
- package/obj/template/.claude/hooks/test_runner.sh +75 -0
- package/obj/template/.claude/hooks/tests/fixtures/ac008_byte_equal_reference.txt +12 -0
- package/obj/template/.claude/hooks/tests/memory_session_start_test.sh +285 -0
- package/obj/template/.claude/hooks/track_guard.sh +127 -0
- package/obj/template/.claude/hooks/verify_pass_guard.sh +88 -0
- package/obj/template/.claude/memory/README.md +108 -0
- package/obj/template/.claude/memory/_pending.md +15 -0
- package/obj/template/.claude/memory/_resume.md +12 -0
- package/obj/template/.claude/memory/conventions.md +26 -0
- package/obj/template/.claude/memory/decisions.md +29 -0
- package/obj/template/.claude/memory/landmarks.md +26 -0
- package/obj/template/.claude/memory/landmines.md +27 -0
- package/obj/template/.claude/memory/libraries.md +27 -0
- package/obj/template/.claude/memory/pending-questions.md +28 -0
- package/obj/template/.claude/project.json +221 -0
- package/obj/template/.claude/settings.json +110 -0
- package/obj/template/.claude/skills/archive/SKILL.md +48 -0
- package/obj/template/.claude/skills/archive/archive.sh +145 -0
- package/obj/template/.claude/skills/audit-baseline/SKILL.md +80 -0
- package/obj/template/.claude/skills/audit-baseline/audit.sh +919 -0
- package/obj/template/.claude/skills/brd/SKILL.md +44 -0
- package/obj/template/.claude/skills/brd/template.md +83 -0
- package/obj/template/.claude/skills/chore/SKILL.md +99 -0
- package/obj/template/.claude/skills/claude-automation-recommender/LICENSE +202 -0
- package/obj/template/.claude/skills/claude-automation-recommender/NOTICE +69 -0
- package/obj/template/.claude/skills/claude-automation-recommender/SKILL.md +358 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/hooks-patterns.md +226 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/mcp-servers.md +263 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/plugins-reference.md +98 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/skills-reference.md +408 -0
- package/obj/template/.claude/skills/claude-automation-recommender/references/subagent-templates.md +181 -0
- package/obj/template/.claude/skills/code-structure/SKILL.md +204 -0
- package/obj/template/.claude/skills/commit/SKILL.md +21 -0
- package/obj/template/.claude/skills/copywriting/SKILL.md +252 -0
- package/obj/template/.claude/skills/copywriting/evals/evals.json +111 -0
- package/obj/template/.claude/skills/copywriting/references/ai-writing-detection.md +200 -0
- package/obj/template/.claude/skills/copywriting/references/copy-frameworks.md +344 -0
- package/obj/template/.claude/skills/copywriting/references/natural-transitions.md +272 -0
- package/obj/template/.claude/skills/design-ui/SKILL.md +175 -0
- package/obj/template/.claude/skills/design-ui/references/design-vs-development.md +89 -0
- package/obj/template/.claude/skills/design-ui/references/intent-table.md +64 -0
- package/obj/template/.claude/skills/design-ui/references/orchestration.md +121 -0
- package/obj/template/.claude/skills/design-ui/references/state-machine.md +125 -0
- package/obj/template/.claude/skills/document/SKILL.md +66 -0
- package/obj/template/.claude/skills/documentation/SKILL.md +50 -0
- package/obj/template/.claude/skills/harness/SKILL.md +169 -0
- package/obj/template/.claude/skills/humanizer/SKILL.md +489 -0
- package/obj/template/.claude/skills/humanizer/references/ai-writing-detection.md +208 -0
- package/obj/template/.claude/skills/impeccable/PROJECT_NOTES.md +22 -0
- package/obj/template/.claude/skills/impeccable/SKILL.md +153 -0
- package/obj/template/.claude/skills/impeccable/agents/openai.yaml +4 -0
- package/obj/template/.claude/skills/impeccable/reference/adapt.md +190 -0
- package/obj/template/.claude/skills/impeccable/reference/animate.md +173 -0
- package/obj/template/.claude/skills/impeccable/reference/audit.md +134 -0
- package/obj/template/.claude/skills/impeccable/reference/bolder.md +113 -0
- package/obj/template/.claude/skills/impeccable/reference/brand.md +104 -0
- package/obj/template/.claude/skills/impeccable/reference/clarify.md +174 -0
- package/obj/template/.claude/skills/impeccable/reference/cognitive-load.md +106 -0
- package/obj/template/.claude/skills/impeccable/reference/color-and-contrast.md +105 -0
- package/obj/template/.claude/skills/impeccable/reference/colorize.md +154 -0
- package/obj/template/.claude/skills/impeccable/reference/craft.md +138 -0
- package/obj/template/.claude/skills/impeccable/reference/critique.md +213 -0
- package/obj/template/.claude/skills/impeccable/reference/delight.md +302 -0
- package/obj/template/.claude/skills/impeccable/reference/distill.md +111 -0
- package/obj/template/.claude/skills/impeccable/reference/document.md +427 -0
- package/obj/template/.claude/skills/impeccable/reference/extract.md +70 -0
- package/obj/template/.claude/skills/impeccable/reference/harden.md +347 -0
- package/obj/template/.claude/skills/impeccable/reference/heuristics-scoring.md +234 -0
- package/obj/template/.claude/skills/impeccable/reference/interaction-design.md +195 -0
- package/obj/template/.claude/skills/impeccable/reference/layout.md +141 -0
- package/obj/template/.claude/skills/impeccable/reference/live.md +513 -0
- package/obj/template/.claude/skills/impeccable/reference/motion-design.md +99 -0
- package/obj/template/.claude/skills/impeccable/reference/onboard.md +234 -0
- package/obj/template/.claude/skills/impeccable/reference/optimize.md +258 -0
- package/obj/template/.claude/skills/impeccable/reference/overdrive.md +130 -0
- package/obj/template/.claude/skills/impeccable/reference/personas.md +178 -0
- package/obj/template/.claude/skills/impeccable/reference/polish.md +232 -0
- package/obj/template/.claude/skills/impeccable/reference/product.md +62 -0
- package/obj/template/.claude/skills/impeccable/reference/quieter.md +99 -0
- package/obj/template/.claude/skills/impeccable/reference/responsive-design.md +114 -0
- package/obj/template/.claude/skills/impeccable/reference/shape.md +136 -0
- package/obj/template/.claude/skills/impeccable/reference/spatial-design.md +100 -0
- package/obj/template/.claude/skills/impeccable/reference/teach.md +137 -0
- package/obj/template/.claude/skills/impeccable/reference/typeset.md +124 -0
- package/obj/template/.claude/skills/impeccable/reference/typography.md +159 -0
- package/obj/template/.claude/skills/impeccable/reference/ux-writing.md +107 -0
- package/obj/template/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
- package/obj/template/.claude/skills/impeccable/scripts/command-metadata.json +94 -0
- package/obj/template/.claude/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/obj/template/.claude/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/obj/template/.claude/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-accept.mjs +465 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-browser.js +4684 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-inject.mjs +436 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-poll.mjs +187 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-server.mjs +679 -0
- package/obj/template/.claude/skills/impeccable/scripts/live-wrap.mjs +395 -0
- package/obj/template/.claude/skills/impeccable/scripts/live.mjs +247 -0
- package/obj/template/.claude/skills/impeccable/scripts/load-context.mjs +93 -0
- package/obj/template/.claude/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/obj/template/.claude/skills/impeccable/scripts/pin.mjs +214 -0
- package/obj/template/.claude/skills/implement/SKILL.md +83 -0
- package/obj/template/.claude/skills/intake/SKILL.md +46 -0
- package/obj/template/.claude/skills/intake/template.md +61 -0
- package/obj/template/.claude/skills/integrate/SKILL.md +62 -0
- package/obj/template/.claude/skills/memory-flush/SKILL.md +172 -0
- package/obj/template/.claude/skills/memory-flush/sweep.py +286 -0
- package/obj/template/.claude/skills/memory-flush/tests/run.sh +327 -0
- package/obj/template/.claude/skills/prose/SKILL.md +119 -0
- package/obj/template/.claude/skills/rca/SKILL.md +42 -0
- package/obj/template/.claude/skills/rca/template.md +83 -0
- package/obj/template/.claude/skills/research/SKILL.md +75 -0
- package/obj/template/.claude/skills/scenario/SKILL.md +64 -0
- package/obj/template/.claude/skills/scout/SKILL.md +72 -0
- package/obj/template/.claude/skills/security/SKILL.md +75 -0
- package/obj/template/.claude/skills/simplify/SKILL.md +67 -0
- package/obj/template/.claude/skills/spec/SKILL.md +69 -0
- package/obj/template/.claude/skills/spec/template.md +274 -0
- package/obj/template/.claude/skills/spec-diagram-review/SKILL.md +81 -0
- package/obj/template/.claude/skills/spec-lint/SKILL.md +55 -0
- package/obj/template/.claude/skills/spec-lint/lint.sh +218 -0
- package/obj/template/.claude/skills/spec-render/SKILL.md +45 -0
- package/obj/template/.claude/skills/spec-render/render.sh +109 -0
- package/obj/template/.claude/skills/spec-traceability-review/SKILL.md +72 -0
- package/obj/template/.claude/skills/swarm-dispatch/SKILL.md +212 -0
- package/obj/template/.claude/skills/swarm-dispatch/swarm_merge.sh +154 -0
- package/obj/template/.claude/skills/swarm-plan/SKILL.md +90 -0
- package/obj/template/.claude/skills/swarm-plan/validate.sh +181 -0
- package/obj/template/.claude/skills/tdd/SKILL.md +100 -0
- package/obj/template/.claude/skills/technical-tutorials/SKILL.md +569 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-context-README.md +53 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-context.md +246 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-example.md +175 -0
- package/obj/template/.claude/skills/technical-tutorials/references/audience-template.md +152 -0
- package/obj/template/.claude/skills/triage/SKILL.md +55 -0
- package/obj/template/.claude/skills/verify/SKILL.md +74 -0
- package/obj/template/.mcp.json +24 -0
- package/obj/template/CLAUDE.md +327 -0
- package/obj/template/docs/init/seed.md +585 -0
- package/obj/template/manifest.json +214 -0
- package/package.json +48 -0
- package/src/.mcp.template.json +24 -0
- package/src/.npmrc.template +2 -0
- package/src/CLAUDE.template.md +327 -0
- package/src/agents/swarm-worker.template.md +51 -0
- package/src/cli/conflict.js +31 -0
- package/src/cli/doctor.js +152 -0
- package/src/cli/install.js +93 -0
- package/src/cli/io.js +27 -0
- package/src/cli/manifest.js +38 -0
- package/src/cli/mcp.js +54 -0
- package/src/cli/merge.js +107 -0
- package/src/cli/plantuml.js +121 -0
- package/src/cli/util.js +10 -0
- package/src/memory/_pending.template.md +15 -0
- package/src/memory/_resume.template.md +12 -0
- package/src/memory/conventions.template.md +26 -0
- package/src/memory/decisions.template.md +29 -0
- package/src/memory/landmarks.template.md +26 -0
- package/src/memory/landmines.template.md +27 -0
- package/src/memory/libraries.template.md +27 -0
- package/src/memory/pending-questions.template.md +28 -0
- package/src/project.template.json +221 -0
- package/src/seed.template.md +585 -0
- package/src/settings.template.json +110 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { buildManifestFromDir, saveManifest } from './manifest.js';
|
|
5
|
+
import { deepMergeMcpServers } from './mcp.js';
|
|
6
|
+
import { pathExists } from './util.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
// Package root contains src/ and obj/template/ as siblings of src/cli/.
|
|
10
|
+
const PACKAGE_ROOT = resolve(__dirname, '../..');
|
|
11
|
+
const NPMRC_TEMPLATE_PATH = join(PACKAGE_ROOT, 'src/.npmrc.template');
|
|
12
|
+
|
|
13
|
+
export const NEVER_TOUCH = Object.freeze(['.claude/project.json']);
|
|
14
|
+
export const SPECIAL_MERGE = Object.freeze(['.mcp.json']);
|
|
15
|
+
|
|
16
|
+
async function listFiles(root, base = root, acc = []) {
|
|
17
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
18
|
+
const full = join(root, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
await listFiles(full, base, acc);
|
|
21
|
+
} else if (entry.isFile()) {
|
|
22
|
+
acc.push(relative(base, full).split(sep).join('/'));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return acc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function writeBaselineManifest(target) {
|
|
29
|
+
const files = await listFiles(target);
|
|
30
|
+
const filtered = files.filter((p) => p !== '.claude/.baseline-manifest.json');
|
|
31
|
+
const m = await buildManifestFromDir(target, filtered);
|
|
32
|
+
await mkdir(join(target, '.claude'), { recursive: true });
|
|
33
|
+
await saveManifest(join(target, '.claude/.baseline-manifest.json'), m);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeFilter(opts) {
|
|
37
|
+
return (src, _dest) => {
|
|
38
|
+
const rel = relative(opts.templateRoot, src).split(sep).join('/');
|
|
39
|
+
if (rel === '') return true;
|
|
40
|
+
if (NEVER_TOUCH.includes(rel) && opts.skipNeverTouch) return false;
|
|
41
|
+
if (SPECIAL_MERGE.includes(rel) && opts.skipSpecialMerge) return false;
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function applySpecialAndNeverTouch(templateDir, target) {
|
|
47
|
+
for (const rel of NEVER_TOUCH) {
|
|
48
|
+
const dst = join(target, rel);
|
|
49
|
+
if (!(await pathExists(dst))) {
|
|
50
|
+
const src = join(templateDir, rel);
|
|
51
|
+
if (await pathExists(src)) {
|
|
52
|
+
await mkdir(join(target, rel.split('/').slice(0, -1).join('/')), { recursive: true });
|
|
53
|
+
await cp(src, dst);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const rel of SPECIAL_MERGE) {
|
|
59
|
+
const src = join(templateDir, rel);
|
|
60
|
+
const dst = join(target, rel);
|
|
61
|
+
if (await pathExists(src)) {
|
|
62
|
+
await deepMergeMcpServers(src, dst);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// npm pack drops `.npmrc` from the published tarball even when listed in
|
|
68
|
+
// package.json files (registry hardening). To ship the hardened operator
|
|
69
|
+
// defaults (`ignore-scripts=true`, `min-release-age=7`) into target projects,
|
|
70
|
+
// install.js overlays target/.npmrc from src/.npmrc.template at install time.
|
|
71
|
+
async function materializeNpmrc(target) {
|
|
72
|
+
const dst = join(target, '.npmrc');
|
|
73
|
+
if (await pathExists(dst)) return; // never overwrite an existing operator config
|
|
74
|
+
if (!(await pathExists(NPMRC_TEMPLATE_PATH))) return; // fixture / dev tree without the template — no-op
|
|
75
|
+
const bytes = await readFile(NPMRC_TEMPLATE_PATH, 'utf8');
|
|
76
|
+
await writeFile(dst, bytes);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function freshInstall(templateDir, target) {
|
|
80
|
+
const filter = makeFilter({ templateRoot: templateDir, skipNeverTouch: false, skipSpecialMerge: true });
|
|
81
|
+
await cp(templateDir, target, { recursive: true, force: false, filter });
|
|
82
|
+
await applySpecialAndNeverTouch(templateDir, target);
|
|
83
|
+
await materializeNpmrc(target);
|
|
84
|
+
await writeBaselineManifest(target);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function forceInstall(templateDir, target) {
|
|
88
|
+
const filter = makeFilter({ templateRoot: templateDir, skipNeverTouch: true, skipSpecialMerge: true });
|
|
89
|
+
await cp(templateDir, target, { recursive: true, force: true, filter });
|
|
90
|
+
await applySpecialAndNeverTouch(templateDir, target);
|
|
91
|
+
await materializeNpmrc(target);
|
|
92
|
+
await writeBaselineManifest(target);
|
|
93
|
+
}
|
package/src/cli/io.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
|
|
3
|
+
export function log(msg) {
|
|
4
|
+
process.stdout.write(msg + '\n');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function warn(msg) {
|
|
8
|
+
process.stderr.write('Warning: ' + msg + '\n');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function error(msg) {
|
|
12
|
+
process.stderr.write('Error: ' + msg + '\n');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const isTTY = !!process.stdin.isTTY;
|
|
16
|
+
|
|
17
|
+
export async function ask(prompt, opts = {}) {
|
|
18
|
+
const input = opts.input ?? process.stdin;
|
|
19
|
+
const output = opts.output ?? process.stdout;
|
|
20
|
+
const rl = createInterface({ input, output });
|
|
21
|
+
try {
|
|
22
|
+
const answer = await rl.question(prompt);
|
|
23
|
+
return answer.replace(/\r?\n$/, '');
|
|
24
|
+
} finally {
|
|
25
|
+
rl.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const MANIFEST_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export async function hashFile(path) {
|
|
8
|
+
const buf = await readFile(path);
|
|
9
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function loadManifest(path) {
|
|
13
|
+
let text;
|
|
14
|
+
try {
|
|
15
|
+
text = await readFile(path, 'utf8');
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err.code === 'ENOENT') return null;
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function saveManifest(path, m) {
|
|
24
|
+
await writeFile(path, JSON.stringify(m, null, 2) + '\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function buildManifestFromDir(rootDir, fileList) {
|
|
28
|
+
const files = {};
|
|
29
|
+
const sorted = [...fileList].sort();
|
|
30
|
+
for (const rel of sorted) {
|
|
31
|
+
files[rel] = await hashFile(join(rootDir, rel));
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
manifest_version: MANIFEST_VERSION,
|
|
35
|
+
generated_at: new Date().toISOString(),
|
|
36
|
+
files,
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/cli/mcp.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFile, writeFile, access } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merge `template/.mcp.json` into the target's `.mcp.json` with **baseline-refresh**
|
|
5
|
+
* semantics:
|
|
6
|
+
*
|
|
7
|
+
* - Servers named in the template are baseline-canonical. The merge refreshes
|
|
8
|
+
* them from the template — so users running `--merge` receive baseline arg
|
|
9
|
+
* and env updates (e.g., the `--browser chrome --isolated` flags on
|
|
10
|
+
* playwright). A user who customized a baseline-named server loses that
|
|
11
|
+
* customization; intentional customizations belong under a non-baseline name.
|
|
12
|
+
* - Servers absent from the template are user-added and are preserved byte-for-byte.
|
|
13
|
+
* - Top-level JSON keys outside `mcpServers` follow the same rule: template
|
|
14
|
+
* keys are added when missing; target's existing keys are preserved.
|
|
15
|
+
* - When the target is absent, the template is written verbatim.
|
|
16
|
+
*
|
|
17
|
+
* Decision recorded in: README "MCP merge semantics"; this comment is the
|
|
18
|
+
* authoritative implementation note.
|
|
19
|
+
*/
|
|
20
|
+
export async function deepMergeMcpServers(templatePath, targetPath) {
|
|
21
|
+
const templateText = await readFile(templatePath, 'utf8');
|
|
22
|
+
const template = JSON.parse(templateText);
|
|
23
|
+
|
|
24
|
+
let target;
|
|
25
|
+
try {
|
|
26
|
+
await access(targetPath);
|
|
27
|
+
const targetText = await readFile(targetPath, 'utf8');
|
|
28
|
+
target = JSON.parse(targetText);
|
|
29
|
+
} catch {
|
|
30
|
+
await writeFile(targetPath, templateText);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tplServers = (template && template.mcpServers) || {};
|
|
35
|
+
const tgtServers = (target && target.mcpServers) || {};
|
|
36
|
+
|
|
37
|
+
// Baseline-canonical names = names that appear in the template. Refresh those
|
|
38
|
+
// from the template; preserve every other server byte-for-byte.
|
|
39
|
+
const mergedServers = { ...tgtServers };
|
|
40
|
+
for (const [name, cfg] of Object.entries(tplServers)) {
|
|
41
|
+
mergedServers[name] = cfg;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const merged = { ...target, mcpServers: mergedServers };
|
|
45
|
+
|
|
46
|
+
for (const [key, value] of Object.entries(template || {})) {
|
|
47
|
+
if (key === 'mcpServers') continue;
|
|
48
|
+
if (!(key in merged)) {
|
|
49
|
+
merged[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await writeFile(targetPath, JSON.stringify(merged, null, 2) + '\n');
|
|
54
|
+
}
|
package/src/cli/merge.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { cp, mkdir, unlink } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { hashFile, saveManifest } from './manifest.js';
|
|
4
|
+
import { deepMergeMcpServers } from './mcp.js';
|
|
5
|
+
import { NEVER_TOUCH, SPECIAL_MERGE } from './install.js';
|
|
6
|
+
import { pathExists } from './util.js';
|
|
7
|
+
|
|
8
|
+
export const ACTION_KINDS = Object.freeze({
|
|
9
|
+
ADD: 'ADD',
|
|
10
|
+
OVERWRITE: 'OVERWRITE',
|
|
11
|
+
NOOP: 'NOOP',
|
|
12
|
+
SKIP_CUSTOMIZED: 'SKIP_CUSTOMIZED',
|
|
13
|
+
PRUNE: 'PRUNE',
|
|
14
|
+
PRUNE_SKIPPED_CUSTOMIZED: 'PRUNE_SKIPPED_CUSTOMIZED',
|
|
15
|
+
NEVER_TOUCH_PRESERVE: 'NEVER_TOUCH_PRESERVE',
|
|
16
|
+
NEVER_TOUCH_ADD: 'NEVER_TOUCH_ADD',
|
|
17
|
+
SPECIAL_MERGE: 'SPECIAL_MERGE',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function copyFile(src, dst) {
|
|
21
|
+
await mkdir(dirname(dst), { recursive: true });
|
|
22
|
+
await cp(src, dst, { force: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function threeWayMerge(templateDir, target, oldManifest, newManifest) {
|
|
26
|
+
const actions = [];
|
|
27
|
+
const oldFiles = oldManifest?.files ?? {};
|
|
28
|
+
const newFiles = newManifest?.files ?? {};
|
|
29
|
+
const allPaths = new Set([...Object.keys(oldFiles), ...Object.keys(newFiles)]);
|
|
30
|
+
|
|
31
|
+
for (const rel of allPaths) {
|
|
32
|
+
const tplPath = join(templateDir, rel);
|
|
33
|
+
const tgtPath = join(target, rel);
|
|
34
|
+
|
|
35
|
+
if (NEVER_TOUCH.includes(rel)) {
|
|
36
|
+
if (await pathExists(tgtPath)) {
|
|
37
|
+
actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_PRESERVE, path: rel, reason: 'NEVER_TOUCH path present in target' });
|
|
38
|
+
} else if (rel in newFiles) {
|
|
39
|
+
await copyFile(tplPath, tgtPath);
|
|
40
|
+
actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_ADD, path: rel, reason: 'NEVER_TOUCH path absent; written from template' });
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (SPECIAL_MERGE.includes(rel)) {
|
|
46
|
+
if (rel in newFiles && await pathExists(tplPath)) {
|
|
47
|
+
await deepMergeMcpServers(tplPath, tgtPath);
|
|
48
|
+
actions.push({ kind: ACTION_KINDS.SPECIAL_MERGE, path: rel, reason: 'additive deep-merge applied' });
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const newHash = newFiles[rel];
|
|
54
|
+
const oldHash = oldFiles[rel];
|
|
55
|
+
const targetExists = await pathExists(tgtPath);
|
|
56
|
+
const tgtHash = targetExists ? await hashFile(tgtPath) : null;
|
|
57
|
+
|
|
58
|
+
if (!targetExists && newHash) {
|
|
59
|
+
await copyFile(tplPath, tgtPath);
|
|
60
|
+
actions.push({ kind: ACTION_KINDS.ADD, path: rel, reason: 'new in template; not present in target' });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (newHash && tgtHash === newHash) {
|
|
65
|
+
actions.push({ kind: ACTION_KINDS.NOOP, path: rel, reason: 'target already matches new template' });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (newHash && oldHash && tgtHash === oldHash) {
|
|
70
|
+
await copyFile(tplPath, tgtPath);
|
|
71
|
+
actions.push({ kind: ACTION_KINDS.OVERWRITE, path: rel, reason: 'target untouched since last install; updated' });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (newHash && tgtHash && tgtHash !== oldHash) {
|
|
76
|
+
actions.push({ kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: 'target customized since last install' });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!newHash && oldHash) {
|
|
81
|
+
// File was part of the baseline at last install but has since been
|
|
82
|
+
// removed upstream. Two cases:
|
|
83
|
+
// - target unchanged since last install (tgtHash == oldHash) → safe to
|
|
84
|
+
// prune. Otherwise the user accumulates stale baseline files forever.
|
|
85
|
+
// - target customized (tgtHash != oldHash) → preserve to avoid
|
|
86
|
+
// destroying user work; report drift via exit 3.
|
|
87
|
+
// Pruning only runs when --merge already applies; there is no separate
|
|
88
|
+
// flag (decision recorded in README).
|
|
89
|
+
if (targetExists && tgtHash === oldHash) {
|
|
90
|
+
await unlink(tgtPath);
|
|
91
|
+
actions.push({ kind: ACTION_KINDS.PRUNE, path: rel, reason: 'removed from new template; target was untouched — deleted' });
|
|
92
|
+
} else if (targetExists) {
|
|
93
|
+
actions.push({ kind: ACTION_KINDS.PRUNE_SKIPPED_CUSTOMIZED, path: rel, reason: 'removed from new template; target customized — preserved' });
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (newManifest) {
|
|
100
|
+
await mkdir(join(target, '.claude'), { recursive: true });
|
|
101
|
+
await saveManifest(join(target, '.claude/.baseline-manifest.json'), newManifest);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const skipKinds = [ACTION_KINDS.SKIP_CUSTOMIZED, ACTION_KINDS.PRUNE_SKIPPED_CUSTOMIZED];
|
|
105
|
+
const exitCode = actions.some((a) => skipKinds.includes(a.kind)) ? 3 : 0;
|
|
106
|
+
return { actions, exitCode };
|
|
107
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, rename, unlink } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join, delimiter } from 'node:path';
|
|
4
|
+
import { get as httpsGet } from 'node:https';
|
|
5
|
+
import { existsSync, statSync } from 'node:fs';
|
|
6
|
+
import { pathExists } from './util.js';
|
|
7
|
+
|
|
8
|
+
export const UPSTREAM_URL = 'https://github.com/plantuml/plantuml/releases/download/v1.2026.2/plantuml-asl-1.2026.2.jar';
|
|
9
|
+
export const PINNED_SHA256 = 'c348f6a26d999f81fd05b5d49834bb70df9cf35fab0939c4edecb0909e64022b';
|
|
10
|
+
export const PINNED_SIZE = 19395808;
|
|
11
|
+
|
|
12
|
+
export const FETCH_OUTCOMES = Object.freeze({
|
|
13
|
+
WROTE: 'WROTE',
|
|
14
|
+
SKIPPED_SYSTEM_PLANTUML: 'SKIPPED_SYSTEM_PLANTUML',
|
|
15
|
+
SKIPPED_ALREADY_PRESENT: 'SKIPPED_ALREADY_PRESENT',
|
|
16
|
+
SKIPPED_NO_PLANTUML_FLAG: 'SKIPPED_NO_PLANTUML_FLAG',
|
|
17
|
+
SKIPPED_DRY_RUN: 'SKIPPED_DRY_RUN',
|
|
18
|
+
WARNED_NETWORK_FAILURE: 'WARNED_NETWORK_FAILURE',
|
|
19
|
+
WARNED_HASH_MISMATCH: 'WARNED_HASH_MISMATCH',
|
|
20
|
+
ERRORED_REQUIRE_PLANTUML: 'ERRORED_REQUIRE_PLANTUML',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function detectSystemPlantuml() {
|
|
24
|
+
const pathEnv = process.env.PATH || '';
|
|
25
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
26
|
+
if (!dir) continue;
|
|
27
|
+
const candidate = join(dir, 'plantuml');
|
|
28
|
+
try {
|
|
29
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sha256Hex(buf) {
|
|
36
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function defaultHttpsFetch(url, maxRedirects = 5) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const follow = (currentUrl, hops) => {
|
|
42
|
+
httpsGet(currentUrl, (res) => {
|
|
43
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
44
|
+
if (hops <= 0) return reject(new Error('Too many redirects'));
|
|
45
|
+
res.resume();
|
|
46
|
+
return follow(res.headers.location, hops - 1);
|
|
47
|
+
}
|
|
48
|
+
if (res.statusCode !== 200) {
|
|
49
|
+
res.resume();
|
|
50
|
+
return reject(new Error(`Non-200 status: ${res.statusCode}`));
|
|
51
|
+
}
|
|
52
|
+
const chunks = [];
|
|
53
|
+
res.on('data', (c) => chunks.push(c));
|
|
54
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
55
|
+
res.on('error', reject);
|
|
56
|
+
}).on('error', reject);
|
|
57
|
+
};
|
|
58
|
+
follow(url, maxRedirects);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function writeJarAtomic(target, buffer) {
|
|
63
|
+
const dir = join(target, '.claude/bin');
|
|
64
|
+
await mkdir(dir, { recursive: true });
|
|
65
|
+
const dst = join(dir, 'plantuml.jar');
|
|
66
|
+
const tmp = dst + '.tmp.' + process.pid;
|
|
67
|
+
try {
|
|
68
|
+
await writeFile(tmp, buffer);
|
|
69
|
+
await rename(tmp, dst);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
try { await unlink(tmp); } catch {}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
return dst;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function fetchPlantumlIfMissing(target, opts = {}) {
|
|
78
|
+
const fetcher = opts.fetch ?? defaultHttpsFetch;
|
|
79
|
+
const systemPath = opts.systemPlantumlPath !== undefined
|
|
80
|
+
? opts.systemPlantumlPath
|
|
81
|
+
: detectSystemPlantuml();
|
|
82
|
+
|
|
83
|
+
if (systemPath) {
|
|
84
|
+
return { outcome: FETCH_OUTCOMES.SKIPPED_SYSTEM_PLANTUML, bytesWritten: 0, reason: `system plantuml at ${systemPath}` };
|
|
85
|
+
}
|
|
86
|
+
if (opts.noPlantuml) {
|
|
87
|
+
return { outcome: FETCH_OUTCOMES.SKIPPED_NO_PLANTUML_FLAG, bytesWritten: 0, reason: '--no-plantuml flag set' };
|
|
88
|
+
}
|
|
89
|
+
if (opts.dryRun) {
|
|
90
|
+
return { outcome: FETCH_OUTCOMES.SKIPPED_DRY_RUN, bytesWritten: 0, reason: `would fetch ${UPSTREAM_URL} (sha256 ${PINNED_SHA256.slice(0, 8)}…)` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const targetJar = join(target, '.claude/bin/plantuml.jar');
|
|
94
|
+
if (await pathExists(targetJar)) {
|
|
95
|
+
const existing = await readFile(targetJar);
|
|
96
|
+
if (sha256Hex(existing) === PINNED_SHA256) {
|
|
97
|
+
return { outcome: FETCH_OUTCOMES.SKIPPED_ALREADY_PRESENT, bytesWritten: 0, reason: 'jar already present with matching sha256' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let bytes;
|
|
102
|
+
try {
|
|
103
|
+
bytes = await fetcher(UPSTREAM_URL);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (opts.requirePlantuml) {
|
|
106
|
+
return { outcome: FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML, bytesWritten: 0, reason: `network failure: ${err.message}` };
|
|
107
|
+
}
|
|
108
|
+
return { outcome: FETCH_OUTCOMES.WARNED_NETWORK_FAILURE, bytesWritten: 0, reason: err.message };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hash = sha256Hex(bytes);
|
|
112
|
+
if (hash !== PINNED_SHA256) {
|
|
113
|
+
if (opts.requirePlantuml) {
|
|
114
|
+
return { outcome: FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML, bytesWritten: 0, reason: `sha256 mismatch: got ${hash.slice(0, 8)}…` };
|
|
115
|
+
}
|
|
116
|
+
return { outcome: FETCH_OUTCOMES.WARNED_HASH_MISMATCH, bytesWritten: 0, reason: `expected ${PINNED_SHA256.slice(0, 8)}… got ${hash.slice(0, 8)}…` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await writeJarAtomic(target, bytes);
|
|
120
|
+
return { outcome: FETCH_OUTCOMES.WROTE, bytesWritten: bytes.length, reason: `wrote ${bytes.length} bytes` };
|
|
121
|
+
}
|
package/src/cli/util.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
owners: [memory_stop.sh writes; /memory-flush clears]
|
|
3
|
+
category: auto-extracted candidates awaiting curation
|
|
4
|
+
verifies-against: none
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Pending memory candidates
|
|
8
|
+
|
|
9
|
+
Auto-extracted by `memory_stop.sh` at end of each turn. Run `/memory-flush` to review and commit keepers to the canonical files.
|
|
10
|
+
|
|
11
|
+
**Content of this file is gitignored.** The file itself (with this header) is committed; everything below the `---` separator below is per-session and not staged.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<!-- candidates appended below this line -->
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resume
|
|
3
|
+
type: continuity
|
|
4
|
+
last-updated: never
|
|
5
|
+
trigger: stop
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Resume snapshot
|
|
9
|
+
|
|
10
|
+
## No prior session
|
|
11
|
+
|
|
12
|
+
This file is overwritten by `memory_stop.sh` at end of each turn and by `memory_pre_compact.sh` before context compaction. The SessionStart hook reads it on the next session start.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
owners: [scenario, implement]
|
|
3
|
+
category: repo-specific test and code conventions
|
|
4
|
+
size-cap: 500
|
|
5
|
+
key: short slug
|
|
6
|
+
verifies-against: codebase
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Conventions
|
|
10
|
+
|
|
11
|
+
Repo-specific patterns the `scenario` and `implement` skills should match. Fixture locations, helper idioms, naming, file layout quirks. Not generic best practices — specific facts about *this* codebase.
|
|
12
|
+
|
|
13
|
+
Each entry's stable key is a short slug.
|
|
14
|
+
|
|
15
|
+
Per-entry shape:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
## <short-slug>
|
|
19
|
+
|
|
20
|
+
- Convention: <pattern, rule, or idiom this repo follows>
|
|
21
|
+
- Why: <reason — historical, tooling-driven, performance, etc.>
|
|
22
|
+
- Verified-at: <commit SHA short>
|
|
23
|
+
- Last-touched: <ISO date>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
owners: [spec, rca]
|
|
3
|
+
category: architectural decisions
|
|
4
|
+
size-cap: 500
|
|
5
|
+
key: short slug
|
|
6
|
+
verifies-against: spec/rca artifact
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Architectural decisions
|
|
10
|
+
|
|
11
|
+
Why this repo took the path it took. Includes rejected alternatives so a future session doesn't re-litigate.
|
|
12
|
+
|
|
13
|
+
Each entry's stable key is a short slug (e.g., `auth-jwt-vs-session`, `worktree-isolation`).
|
|
14
|
+
|
|
15
|
+
Per-entry shape:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
## <short-slug>
|
|
19
|
+
|
|
20
|
+
- Decision: <what was chosen>
|
|
21
|
+
- Rationale: <why — the constraint or evidence that decided it>
|
|
22
|
+
- Rejected alternatives:
|
|
23
|
+
- <alt 1> → <why rejected>
|
|
24
|
+
- <alt 2> → <why rejected>
|
|
25
|
+
- Source: <spec slug / rca slug / conversation>
|
|
26
|
+
- Verified-at: <commit SHA short>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
owners: [scout]
|
|
3
|
+
category: codebase landmarks
|
|
4
|
+
size-cap: 500
|
|
5
|
+
key: path:line
|
|
6
|
+
verifies-against: git
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Codebase landmarks
|
|
10
|
+
|
|
11
|
+
Where things live in this repo. The `scout` skill cites these and re-verifies before use; failed verifications are corrected or deleted in the same run.
|
|
12
|
+
|
|
13
|
+
Each entry's stable key is `path:line`.
|
|
14
|
+
|
|
15
|
+
Per-entry shape:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
## <path:line>
|
|
19
|
+
|
|
20
|
+
- Role: <what lives here, why it matters>
|
|
21
|
+
- Verified-at: <commit SHA short>
|
|
22
|
+
- Last-touched: <ISO date>
|
|
23
|
+
- Caveat: <optional — gotcha, neighbour file that must change with this one, etc.>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
owners: [security, integrate, scout]
|
|
3
|
+
category: gotchas and recurring true positives
|
|
4
|
+
size-cap: 500
|
|
5
|
+
key: path:line or short slug
|
|
6
|
+
verifies-against: git
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Landmines
|
|
10
|
+
|
|
11
|
+
Things that have bitten before and will bite again. "Editing X without also editing Y breaks Z." Recurring true positives from security review. Version skew traps.
|
|
12
|
+
|
|
13
|
+
Each entry's stable key is `path:line` or a short slug.
|
|
14
|
+
|
|
15
|
+
Per-entry shape:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
## <path:line or slug>
|
|
19
|
+
|
|
20
|
+
- Path: <file/symbol involved>
|
|
21
|
+
- Trap: <what goes wrong, plain language>
|
|
22
|
+
- Mitigation: <what to check / do instead>
|
|
23
|
+
- Verified-at: <commit SHA short>
|
|
24
|
+
- Last-touched: <ISO date>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
owners: [research]
|
|
3
|
+
category: validated library APIs
|
|
4
|
+
size-cap: 500
|
|
5
|
+
key: lib@version
|
|
6
|
+
verifies-against: lockfile + context7
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Validated library APIs
|
|
10
|
+
|
|
11
|
+
Library APIs the team has confirmed via `context7` MCP against the version present in this repo's lockfile. Saves a context7 round-trip when a stable choice is referenced again.
|
|
12
|
+
|
|
13
|
+
Each entry's stable key is `<lib>@<version>`. If the lockfile bumps, re-verify and update the version.
|
|
14
|
+
|
|
15
|
+
Per-entry shape:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
## <lib>@<version>
|
|
19
|
+
|
|
20
|
+
- Role: <what this lib is used for in this repo>
|
|
21
|
+
- API: <key symbols / canonical call shape>
|
|
22
|
+
- Verified-at: <commit SHA short>
|
|
23
|
+
- Last-touched: <ISO date>
|
|
24
|
+
- Caveat: <optional — version pin reason, breaking-change notes>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
owners: [any phase]
|
|
3
|
+
category: cross-session open questions
|
|
4
|
+
size-cap: 500
|
|
5
|
+
key: Q-NNN
|
|
6
|
+
verifies-against: none
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Pending questions
|
|
10
|
+
|
|
11
|
+
Questions the current session couldn't resolve. Surfaced at next session start so context isn't lost across yields.
|
|
12
|
+
|
|
13
|
+
Each entry's stable key is auto-numbered `Q-NNN`.
|
|
14
|
+
|
|
15
|
+
Per-entry shape:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
## Q-NNN
|
|
19
|
+
|
|
20
|
+
- Question: <what's open>
|
|
21
|
+
- Raised in: <date / phase / conversation>
|
|
22
|
+
- Blocker for: <what work is gated on this>
|
|
23
|
+
- Options considered: <a> / <b> / <c>
|
|
24
|
+
- Verified-at: <commit SHA short>
|
|
25
|
+
- Last-touched: <ISO date>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|