minimal-vibe-coding-kit 0.4.2 → 0.5.1
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/.agents/skills/claim/SKILL.md +82 -0
- package/.agents/skills/tutien/SKILL.md +97 -0
- package/.agents/skills/tutien/references/adaptive-response.md +68 -0
- package/.agents/skills/tutien/references/classification.md +64 -0
- package/.agents/skills/tutien/references/lore-sources.md +43 -0
- package/.agents/skills/tutien/references/privacy.md +45 -0
- package/.agents/skills/tutien/references/schema.md +106 -0
- package/.agents/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.agents/skills/tutien/references/story-system.md +176 -0
- package/.agents/skills/tutien/references/vi-style-guide.md +89 -0
- package/.agents/skills/tutien/references/voice-and-mode.md +59 -0
- package/.agents/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.agents/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.agents/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.agents/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.agents/skills/tutien/scripts/catalog.mjs +130 -0
- package/.agents/skills/tutien/scripts/classify.mjs +338 -0
- package/.agents/skills/tutien/scripts/command.mjs +126 -0
- package/.agents/skills/tutien/scripts/compare.mjs +41 -0
- package/.agents/skills/tutien/scripts/metrics.mjs +306 -0
- package/.agents/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.agents/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.agents/skills/tutien/scripts/redact.mjs +30 -0
- package/.agents/skills/tutien/scripts/render-report.mjs +479 -0
- package/.agents/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.agents/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.agents/skills/tutien/scripts/score.mjs +97 -0
- package/.agents/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.agents/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.agents/skills/tutien/scripts/villains.mjs +196 -0
- package/.claude/skills/claim/SKILL.md +82 -0
- package/.claude/skills/tutien/SKILL.md +97 -0
- package/.claude/skills/tutien/references/adaptive-response.md +68 -0
- package/.claude/skills/tutien/references/classification.md +64 -0
- package/.claude/skills/tutien/references/lore-sources.md +43 -0
- package/.claude/skills/tutien/references/privacy.md +45 -0
- package/.claude/skills/tutien/references/schema.md +106 -0
- package/.claude/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.claude/skills/tutien/references/story-system.md +176 -0
- package/.claude/skills/tutien/references/vi-style-guide.md +89 -0
- package/.claude/skills/tutien/references/voice-and-mode.md +59 -0
- package/.claude/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.claude/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.claude/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.claude/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.claude/skills/tutien/scripts/catalog.mjs +130 -0
- package/.claude/skills/tutien/scripts/classify.mjs +338 -0
- package/.claude/skills/tutien/scripts/command.mjs +126 -0
- package/.claude/skills/tutien/scripts/compare.mjs +41 -0
- package/.claude/skills/tutien/scripts/metrics.mjs +306 -0
- package/.claude/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.claude/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.claude/skills/tutien/scripts/redact.mjs +30 -0
- package/.claude/skills/tutien/scripts/render-report.mjs +479 -0
- package/.claude/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.claude/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.claude/skills/tutien/scripts/score.mjs +97 -0
- package/.claude/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.claude/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.claude/skills/tutien/scripts/villains.mjs +196 -0
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/skills/claim/SKILL.md +82 -0
- package/.cursor/skills/tutien/SKILL.md +97 -0
- package/.cursor/skills/tutien/references/adaptive-response.md +68 -0
- package/.cursor/skills/tutien/references/classification.md +64 -0
- package/.cursor/skills/tutien/references/lore-sources.md +43 -0
- package/.cursor/skills/tutien/references/privacy.md +45 -0
- package/.cursor/skills/tutien/references/schema.md +106 -0
- package/.cursor/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.cursor/skills/tutien/references/story-system.md +176 -0
- package/.cursor/skills/tutien/references/vi-style-guide.md +89 -0
- package/.cursor/skills/tutien/references/voice-and-mode.md +59 -0
- package/.cursor/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.cursor/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.cursor/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.cursor/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.cursor/skills/tutien/scripts/catalog.mjs +130 -0
- package/.cursor/skills/tutien/scripts/classify.mjs +338 -0
- package/.cursor/skills/tutien/scripts/command.mjs +126 -0
- package/.cursor/skills/tutien/scripts/compare.mjs +41 -0
- package/.cursor/skills/tutien/scripts/metrics.mjs +306 -0
- package/.cursor/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.cursor/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.cursor/skills/tutien/scripts/redact.mjs +30 -0
- package/.cursor/skills/tutien/scripts/render-report.mjs +479 -0
- package/.cursor/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.cursor/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.cursor/skills/tutien/scripts/score.mjs +97 -0
- package/.cursor/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.cursor/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.cursor/skills/tutien/scripts/villains.mjs +196 -0
- package/.grok/skills/claim/SKILL.md +82 -0
- package/.grok/skills/tutien/SKILL.md +97 -0
- package/.grok/skills/tutien/references/adaptive-response.md +68 -0
- package/.grok/skills/tutien/references/classification.md +64 -0
- package/.grok/skills/tutien/references/lore-sources.md +43 -0
- package/.grok/skills/tutien/references/privacy.md +45 -0
- package/.grok/skills/tutien/references/schema.md +106 -0
- package/.grok/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.grok/skills/tutien/references/story-system.md +176 -0
- package/.grok/skills/tutien/references/vi-style-guide.md +89 -0
- package/.grok/skills/tutien/references/voice-and-mode.md +59 -0
- package/.grok/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.grok/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.grok/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.grok/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.grok/skills/tutien/scripts/catalog.mjs +130 -0
- package/.grok/skills/tutien/scripts/classify.mjs +338 -0
- package/.grok/skills/tutien/scripts/command.mjs +126 -0
- package/.grok/skills/tutien/scripts/compare.mjs +41 -0
- package/.grok/skills/tutien/scripts/metrics.mjs +306 -0
- package/.grok/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.grok/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.grok/skills/tutien/scripts/redact.mjs +30 -0
- package/.grok/skills/tutien/scripts/render-report.mjs +479 -0
- package/.grok/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.grok/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.grok/skills/tutien/scripts/score.mjs +97 -0
- package/.grok/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.grok/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.grok/skills/tutien/scripts/villains.mjs +196 -0
- package/.vibekit/docs/BACKBONE_REFERENCE.md +2 -0
- package/.vibekit/docs/INSTALL.md +3 -1
- package/.vibekit/init/CLAUDE-template.md +2 -0
- package/.vibekit/scripts/mvck.mjs +5 -5
- package/.vibekit/scripts/pack-dry-run.mjs +38 -3
- package/.vibekit/scripts/validate-kit.mjs +34 -24
- package/.vibekit/skills/claim/SKILL.md +82 -0
- package/.vibekit/skills/skills-manifest.json +27 -0
- package/.vibekit/skills/tutien/SKILL.md +97 -0
- package/.vibekit/skills/tutien/references/adaptive-response.md +68 -0
- package/.vibekit/skills/tutien/references/classification.md +64 -0
- package/.vibekit/skills/tutien/references/lore-sources.md +43 -0
- package/.vibekit/skills/tutien/references/privacy.md +45 -0
- package/.vibekit/skills/tutien/references/schema.md +106 -0
- package/.vibekit/skills/tutien/references/scoring-and-realms.md +37 -0
- package/.vibekit/skills/tutien/references/story-system.md +176 -0
- package/.vibekit/skills/tutien/references/vi-style-guide.md +89 -0
- package/.vibekit/skills/tutien/references/voice-and-mode.md +59 -0
- package/.vibekit/skills/tutien/scripts/adapters/generic-jsonl.mjs +82 -0
- package/.vibekit/skills/tutien/scripts/adapters/git.mjs +48 -0
- package/.vibekit/skills/tutien/scripts/adapters/plain-transcript.mjs +37 -0
- package/.vibekit/skills/tutien/scripts/analyze-history.mjs +84 -0
- package/.vibekit/skills/tutien/scripts/catalog.mjs +130 -0
- package/.vibekit/skills/tutien/scripts/classify.mjs +338 -0
- package/.vibekit/skills/tutien/scripts/command.mjs +126 -0
- package/.vibekit/skills/tutien/scripts/compare.mjs +41 -0
- package/.vibekit/skills/tutien/scripts/metrics.mjs +306 -0
- package/.vibekit/skills/tutien/scripts/normalize-events.mjs +68 -0
- package/.vibekit/skills/tutien/scripts/project-profile.mjs +202 -0
- package/.vibekit/skills/tutien/scripts/redact.mjs +30 -0
- package/.vibekit/skills/tutien/scripts/render-report.mjs +479 -0
- package/.vibekit/skills/tutien/scripts/response-brief.mjs +103 -0
- package/.vibekit/skills/tutien/scripts/run-tutien.mjs +350 -0
- package/.vibekit/skills/tutien/scripts/score.mjs +97 -0
- package/.vibekit/skills/tutien/scripts/snapshot.mjs +76 -0
- package/.vibekit/skills/tutien/scripts/story-ledger.mjs +241 -0
- package/.vibekit/skills/tutien/scripts/villains.mjs +196 -0
- package/CHANGELOG.md +51 -0
- package/README.md +53 -47
- package/docs/README.vi.md +14 -8
- package/docs/README.zh-CN.md +279 -0
- package/package.json +9 -3
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic storage helpers for the agent-authored living chronicle.
|
|
3
|
+
// This module never generates story prose. It only builds an aggregate-only
|
|
4
|
+
// evidence packet and validates the plot/chapter ledger written by the agent.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
export const STORY_SCHEMA = 1;
|
|
13
|
+
export const STORY_CONTEXT_SCHEMA = 'tutien-story-context-v1';
|
|
14
|
+
export const STORY_STATE_SCHEMA = 'tutien-story-state-v1';
|
|
15
|
+
export const STORY_CHAPTER_SCHEMA = 'tutien-story-chapter-v1';
|
|
16
|
+
|
|
17
|
+
const sha16 = (value) => crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 16);
|
|
18
|
+
|
|
19
|
+
export function storyPaths(root = process.cwd()) {
|
|
20
|
+
const base = path.join(path.resolve(root), '.vibekit', 'reports', 'tutien', 'story');
|
|
21
|
+
return {
|
|
22
|
+
base,
|
|
23
|
+
plot: path.join(base, 'plot.md'),
|
|
24
|
+
state: path.join(base, 'story-state.json'),
|
|
25
|
+
context: path.join(base, 'latest-context.json'),
|
|
26
|
+
chapters: path.join(base, 'chapters')
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Preserve letters from every language, including Han characters. Latin
|
|
31
|
+
// scripts are normalized so Vietnamese titles remain readable in filenames.
|
|
32
|
+
export function slugifyChapterTitle(title) {
|
|
33
|
+
const raw = String(title ?? '').trim();
|
|
34
|
+
if (!raw) throw new Error('chapter title is required');
|
|
35
|
+
const slug = raw
|
|
36
|
+
.normalize('NFD')
|
|
37
|
+
.replace(/\p{M}+/gu, '')
|
|
38
|
+
.toLocaleLowerCase('und')
|
|
39
|
+
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
40
|
+
.replace(/^-+|-+$/g, '')
|
|
41
|
+
.slice(0, 80)
|
|
42
|
+
.replace(/-+$/g, '');
|
|
43
|
+
if (!slug || slug === '.' || slug === '..') throw new Error('chapter title does not produce a safe filename');
|
|
44
|
+
return slug;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function chapterFilename(number, title) {
|
|
48
|
+
if (!Number.isInteger(number) || number < 1 || number > 9999) {
|
|
49
|
+
throw new Error('chapter number must be an integer from 1 to 9999');
|
|
50
|
+
}
|
|
51
|
+
return `${String(number).padStart(4, '0')}-${slugifyChapterTitle(title)}.md`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listChapters(root = process.cwd()) {
|
|
55
|
+
const dir = storyPaths(root).chapters;
|
|
56
|
+
if (!fs.existsSync(dir)) return [];
|
|
57
|
+
return fs.readdirSync(dir)
|
|
58
|
+
.filter((file) => /^\d{4}-[\p{L}\p{N}][\p{L}\p{N}-]*\.md$/u.test(file))
|
|
59
|
+
.map((file) => ({ file, number: Number(file.slice(0, 4)), path: path.join(dir, file) }))
|
|
60
|
+
.sort((a, b) => a.number - b.number || a.file.localeCompare(b.file));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function nextChapterNumber(root = process.cwd()) {
|
|
64
|
+
const chapters = listChapters(root);
|
|
65
|
+
return chapters.length ? chapters[chapters.length - 1].number + 1 : 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveStoryLanguage(requested = 'auto', invocationText = '', fallback = 'en') {
|
|
69
|
+
const explicit = String(requested).toLowerCase();
|
|
70
|
+
if (['vi', 'en', 'zh'].includes(explicit)) return explicit;
|
|
71
|
+
if (/\p{Script=Han}/u.test(invocationText)) return 'zh';
|
|
72
|
+
if (/[À-ỹĐđ]/u.test(invocationText) || /\b(hãy|giúp|của|và|không|chương|tu tiên)\b/iu.test(invocationText)) return 'vi';
|
|
73
|
+
return ['vi', 'en', 'zh'].includes(fallback) ? fallback : 'en';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function safeSlug(value, fallback = null) {
|
|
77
|
+
const text = String(value ?? '');
|
|
78
|
+
return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(text) ? text : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function safeValidationCommand(value) {
|
|
82
|
+
const text = String(value ?? '');
|
|
83
|
+
return /^(?:npm run|yarn|pnpm|bun run) [A-Za-z0-9][A-Za-z0-9:_-]{0,63}$/.test(text) ? text : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function storyFacts(model, profile = {}) {
|
|
87
|
+
const cultivation = model.cultivation ?? null;
|
|
88
|
+
const progression = cultivation?.progression ?? null;
|
|
89
|
+
return {
|
|
90
|
+
project: {
|
|
91
|
+
id: safeSlug(profile.projectId, 'repo'),
|
|
92
|
+
type: safeSlug(profile.projectType),
|
|
93
|
+
primaryLanguage: safeSlug(profile.primaryLanguage),
|
|
94
|
+
domains: (profile.domains ?? []).map((v) => safeSlug(v)).filter(Boolean).slice(0, 12),
|
|
95
|
+
stack: (profile.stack ?? []).map((v) => safeSlug(v)).filter(Boolean).slice(0, 12),
|
|
96
|
+
validationCommands: (profile.validationCommands ?? []).map((v) => safeValidationCommand(v)).filter(Boolean).slice(0, 3),
|
|
97
|
+
metadataSources: (profile.metadataSources ?? []).map((v) => safeSlug(String(v).replace(/\.[^.]+$/, ''))).filter(Boolean).slice(0, 12),
|
|
98
|
+
authorsCount: Number.isInteger(profile.authorsCount) && profile.authorsCount >= 0 ? profile.authorsCount : null
|
|
99
|
+
},
|
|
100
|
+
evidence: {
|
|
101
|
+
window: model.coverage?.window ?? { start: null, end: null },
|
|
102
|
+
sessions: model.coverage?.sessions ?? 0,
|
|
103
|
+
prompts: model.coverage?.userPrompts ?? 0,
|
|
104
|
+
commits: model.coverage?.commits ?? 0,
|
|
105
|
+
confidence: model.coverage?.confidence ?? 'low',
|
|
106
|
+
tokens: {
|
|
107
|
+
reported: model.tokens?.reportedTotal ?? 0,
|
|
108
|
+
estimated: model.tokens?.estimatedTotal ?? 0,
|
|
109
|
+
unknownTurns: model.tokens?.unknownTurns ?? 0
|
|
110
|
+
},
|
|
111
|
+
realm: model.realm ? { name: model.realm.name, score: model.score } : null,
|
|
112
|
+
problems: (model.problems ?? []).map((p) => ({
|
|
113
|
+
type: p.problemType,
|
|
114
|
+
confidence: p.confidence,
|
|
115
|
+
priority: p.priority
|
|
116
|
+
})),
|
|
117
|
+
classification: cultivation ? {
|
|
118
|
+
faction: cultivation.classification?.faction?.id ?? null,
|
|
119
|
+
affiliation: cultivation.classification?.affiliation?.id ?? null,
|
|
120
|
+
paths: (cultivation.classification?.paths ?? []).map((p) => p.id)
|
|
121
|
+
} : null,
|
|
122
|
+
progression: progression ? {
|
|
123
|
+
tuVi: progression.tuVi?.window ?? 0,
|
|
124
|
+
congDuc: progression.congDuc?.window ?? 0,
|
|
125
|
+
nghiepLuc: progression.nghiepLuc?.window ?? 0,
|
|
126
|
+
daoHanhWindows: progression.daoHanh?.windows ?? 0,
|
|
127
|
+
overlap: progression.overlap ?? 0
|
|
128
|
+
} : null
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildStoryContext(model, options = {}) {
|
|
134
|
+
const facts = storyFacts(model, options.profile ?? {});
|
|
135
|
+
const evidenceKey = sha16(JSON.stringify({
|
|
136
|
+
storySchema: STORY_SCHEMA,
|
|
137
|
+
project: facts.project.id,
|
|
138
|
+
range: options.range ?? 'all',
|
|
139
|
+
evidence: facts.evidence
|
|
140
|
+
}));
|
|
141
|
+
return {
|
|
142
|
+
schema: STORY_CONTEXT_SCHEMA,
|
|
143
|
+
storySchema: STORY_SCHEMA,
|
|
144
|
+
evidenceKey,
|
|
145
|
+
language: resolveStoryLanguage(options.language, options.invocationText, options.conversationLanguage),
|
|
146
|
+
style: safeSlug(options.style, 'auto'),
|
|
147
|
+
focus: safeSlug(options.focus, 'balanced'),
|
|
148
|
+
policyState: model.policyState ?? 'clear',
|
|
149
|
+
canWriteChapter: model.suppressGamification !== true,
|
|
150
|
+
...facts
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function writeStoryContext(root, context) {
|
|
155
|
+
if (context?.schema !== STORY_CONTEXT_SCHEMA) throw new Error(`story context must use ${STORY_CONTEXT_SCHEMA}`);
|
|
156
|
+
const paths = storyPaths(root);
|
|
157
|
+
fs.mkdirSync(paths.base, { recursive: true });
|
|
158
|
+
fs.writeFileSync(paths.context, `${JSON.stringify(context, null, 2)}\n`);
|
|
159
|
+
return paths.context;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function chapterFrontmatter(text) {
|
|
163
|
+
const match = String(text).match(/^---\n([\s\S]*?)\n---\n/);
|
|
164
|
+
if (!match) return null;
|
|
165
|
+
const get = (key) => match[1].match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim().replace(/^['"]|['"]$/g, '') ?? null;
|
|
166
|
+
return {
|
|
167
|
+
schema: get('schema'),
|
|
168
|
+
chapter: Number(get('chapter')),
|
|
169
|
+
title: get('title'),
|
|
170
|
+
evidenceKey: get('evidence_key')
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function validateStoryTree(root = process.cwd()) {
|
|
175
|
+
const paths = storyPaths(root);
|
|
176
|
+
const chapters = listChapters(root);
|
|
177
|
+
const errors = [];
|
|
178
|
+
if (chapters.length && !fs.existsSync(paths.plot)) errors.push('plot.md is required when chapters exist');
|
|
179
|
+
|
|
180
|
+
let state = null;
|
|
181
|
+
if (fs.existsSync(paths.state)) {
|
|
182
|
+
try {
|
|
183
|
+
state = JSON.parse(fs.readFileSync(paths.state, 'utf8'));
|
|
184
|
+
if (state.schema !== STORY_STATE_SCHEMA) errors.push(`story-state.json must use ${STORY_STATE_SCHEMA}`);
|
|
185
|
+
} catch {
|
|
186
|
+
errors.push('story-state.json must contain valid JSON');
|
|
187
|
+
}
|
|
188
|
+
} else if (chapters.length) {
|
|
189
|
+
errors.push('story-state.json is required when chapters exist');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const evidenceKeys = new Set();
|
|
193
|
+
for (let i = 0; i < chapters.length; i += 1) {
|
|
194
|
+
const expected = i + 1;
|
|
195
|
+
const item = chapters[i];
|
|
196
|
+
if (item.number !== expected) errors.push(`chapter sequence must be contiguous: expected ${expected}, found ${item.number}`);
|
|
197
|
+
const fm = chapterFrontmatter(fs.readFileSync(item.path, 'utf8'));
|
|
198
|
+
if (!fm) {
|
|
199
|
+
errors.push(`${item.file} is missing YAML frontmatter`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (fm.schema !== STORY_CHAPTER_SCHEMA) errors.push(`${item.file} must use ${STORY_CHAPTER_SCHEMA}`);
|
|
203
|
+
if (fm.chapter !== item.number) errors.push(`${item.file} frontmatter chapter must equal ${item.number}`);
|
|
204
|
+
if (!fm.title) errors.push(`${item.file} requires a title`);
|
|
205
|
+
if (!fm.evidenceKey || !/^[0-9a-f]{16}$/.test(fm.evidenceKey)) errors.push(`${item.file} requires a 16-hex evidence_key`);
|
|
206
|
+
else if (evidenceKeys.has(fm.evidenceKey)) errors.push(`${item.file} repeats evidence_key ${fm.evidenceKey}`);
|
|
207
|
+
else evidenceKeys.add(fm.evidenceKey);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (state) {
|
|
211
|
+
if (state.lastChapter !== chapters.length) errors.push(`story-state.json lastChapter must equal ${chapters.length}`);
|
|
212
|
+
const consumed = state.consumedEvidenceKeys ?? [];
|
|
213
|
+
if (!Array.isArray(consumed) || consumed.some((key) => !/^[0-9a-f]{16}$/.test(key))) {
|
|
214
|
+
errors.push('story-state.json consumedEvidenceKeys must be an array of 16-hex keys');
|
|
215
|
+
}
|
|
216
|
+
for (const key of evidenceKeys) {
|
|
217
|
+
if (!consumed.includes(key)) errors.push(`story-state.json is missing consumed evidence key ${key}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
ok: errors.length === 0,
|
|
223
|
+
errors,
|
|
224
|
+
chapters: chapters.length,
|
|
225
|
+
nextChapter: chapters.length + 1,
|
|
226
|
+
paths: Object.fromEntries(Object.entries(paths).map(([key, value]) => [key, path.relative(path.resolve(root), value)]))
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
231
|
+
if (isMain) {
|
|
232
|
+
const command = process.argv[2] ?? 'status';
|
|
233
|
+
const root = path.resolve(process.argv[3] ?? process.cwd());
|
|
234
|
+
if (!['status', 'validate'].includes(command)) {
|
|
235
|
+
console.error('usage: story-ledger.mjs [status|validate] [repo-root]');
|
|
236
|
+
process.exit(2);
|
|
237
|
+
}
|
|
238
|
+
const result = validateStoryTree(root);
|
|
239
|
+
console.log(JSON.stringify(result, null, 2));
|
|
240
|
+
if (command === 'validate' && !result.ok) process.exit(1);
|
|
241
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Phase 3 villain engine. Evidence chooses the problem and the
|
|
2
|
+
// counter-technique; a deterministic seed chooses only the villain's name and
|
|
3
|
+
// wording. Nothing here can change a count, severity, evidence, or advice.
|
|
4
|
+
//
|
|
5
|
+
// Safety rails (all enforced, not optional):
|
|
6
|
+
// - a villain appears only when its problem confidence >= threshold (0.80);
|
|
7
|
+
// - at most one boss and two minor villains per report;
|
|
8
|
+
// - villains=off, tone=neutral, or any emergency signal -> no villains;
|
|
9
|
+
// - challenge lines are authored safe and re-checked against banned
|
|
10
|
+
// categories before emission;
|
|
11
|
+
// - origin stories use only an allowlist of sanitized context slugs — never
|
|
12
|
+
// raw prompts, secrets, names, emails, or identity-bearing paths.
|
|
13
|
+
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import { normalizeTone } from './command.mjs';
|
|
16
|
+
|
|
17
|
+
export const VILLAIN_THRESHOLD = 0.8;
|
|
18
|
+
export const MAX_BOSS = 1;
|
|
19
|
+
export const MAX_MINOR = 2;
|
|
20
|
+
export const DEFAULT_COOLDOWN_WINDOWS = 2;
|
|
21
|
+
|
|
22
|
+
// Defense-in-depth: challenge lines are written to avoid these, but every
|
|
23
|
+
// emitted line is re-scanned so a future edit cannot slip an attack through.
|
|
24
|
+
const BANNED_CATEGORY = [
|
|
25
|
+
/\b(stupid|idiot|idiotic|incompetent|dumb|moron|useless|worthless|pathetic|loser|failure of a)\b/i,
|
|
26
|
+
/\b(insane|crazy|psycho|mental|deranged|unhinged|tẩu hỏa nhập ma)\b/i,
|
|
27
|
+
/\b(retard|cripple|disabled|handicapped)\b/i,
|
|
28
|
+
/\b(sin|heretic against god|infidel|kafir|damned to hell)\b/i,
|
|
29
|
+
/\b(man up|like a girl|woman driver|too emotional for a)\b/i,
|
|
30
|
+
/\b(race|racial|ethnic slur|your people)\b/i,
|
|
31
|
+
/\b(broke|poor|can't afford|worthless salary|financial failure)\b/i,
|
|
32
|
+
/\b(kill|die|hurt you|destroy you|i will end)\b/i,
|
|
33
|
+
/\b(f[u*]ck|sh[i*]t|damn you|bastard)\b/i,
|
|
34
|
+
/\b(humiliat|shame on you|you should be ashamed|embarrass yourself)\b/i
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function containsBannedCategory(text) {
|
|
38
|
+
return BANNED_CATEGORY.some((re) => re.test(String(text)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Only these context fields may shape an origin story, and each must be a
|
|
42
|
+
// strict slug: no slashes, dots, at-signs, spaces, or traversal — so neither
|
|
43
|
+
// free text nor path-like or identity-bearing values can ride along.
|
|
44
|
+
const CONTEXT_ALLOWLIST = ['projectType', 'primaryLanguage', 'fileCategory', 'validationLabel', 'metricCategory'];
|
|
45
|
+
const SLUG = /^[a-z0-9][a-z0-9_-]{0,39}$/i;
|
|
46
|
+
|
|
47
|
+
export function sanitizeContext(ctx = {}) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const key of CONTEXT_ALLOWLIST) {
|
|
50
|
+
const v = ctx[key];
|
|
51
|
+
if (typeof v === 'string' && SLUG.test(v)) out[key] = v;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Two authored variants per tone per archetype. The seed picks the index, so
|
|
57
|
+
// changing the seed changes wording only. The serene/spirited vocabulary is
|
|
58
|
+
// local to this coding-reflection game and always targets the workflow pattern,
|
|
59
|
+
// never the person or any unrelated support/companion persona.
|
|
60
|
+
const LINES = {
|
|
61
|
+
'repeated-failure': {
|
|
62
|
+
serene: [
|
|
63
|
+
{ vi: 'Giữa làn sương mỏng, Luân Hồi Ma Ảnh hiện bóng khi lỗi cũ quay thêm một vòng.', en: 'In the thin mountain mist, the Error-Cycle Wraith appears as the old error completes another circle.' },
|
|
64
|
+
{ vi: 'Một lần thử giống hệt khẽ lay chuông, gọi Luân Hồi Ma Ảnh trở lại đạo lộ.', en: 'One unchanged retry stirs the bell that calls the Error-Cycle Wraith back to the path.' }
|
|
65
|
+
],
|
|
66
|
+
spirited: [
|
|
67
|
+
{ vi: 'Luân Hồi Ma Ảnh lại gõ chuông: “Không đổi giả thuyết, sao đổi được kết cục?”', en: 'The Error-Cycle Wraith rings its bell again: “Without a new hypothesis, what new ending could appear?”' },
|
|
68
|
+
{ vi: 'Vòng lặp cũ vừa khép, Luân Hồi Ma Ảnh đã bày sẵn bàn trà chờ vòng kế tiếp.', en: 'The old loop has barely closed, and the Error-Cycle Wraith has already set tea for the next.' }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
'too-many-prompts': {
|
|
72
|
+
serene: [
|
|
73
|
+
{ vi: 'Cửu Hoàn Tâm Ma dệt một nhiệm vụ thành nhiều vòng sương khi cột mốc chưa hiện rõ.', en: 'The Ninefold Loop Heart-Shadow weaves one task into rings of mist when its checkpoints remain unclear.' },
|
|
74
|
+
{ vi: 'Nơi ranh giới nhiệm vụ nhạt dần, Cửu Hoàn Tâm Ma lặng lẽ nối thêm một vòng.', en: 'Where a task boundary fades, the Ninefold Loop Heart-Shadow quietly adds another circle.' }
|
|
75
|
+
],
|
|
76
|
+
spirited: [
|
|
77
|
+
{ vi: 'Cửu Hoàn Tâm Ma đếm lượt yêu cầu thay cột mốc; đã đến lúc đổi chiếc vòng kế tiếp thành một tiêu chí hoàn thành.', en: 'The Ninefold Loop Heart-Shadow is counting prompts instead of checkpoints; turn the next circle into a done-criterion.' },
|
|
78
|
+
{ vi: 'Lượt yêu cầu càng tản như lá thu, Cửu Hoàn Tâm Ma càng dễ giấu mất lối ra.', en: 'The more prompts scatter like autumn leaves, the easier the Ninefold Loop Heart-Shadow hides the exit.' }
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
'conflicting-instructions': {
|
|
82
|
+
serene: [
|
|
83
|
+
{ vi: 'Nghịch Lệnh Ma Quân hiện giữa hai đạo lệnh không thể cùng đứng vững.', en: 'The Lord of Clashing Edicts appears between two instructions that cannot stand together.' },
|
|
84
|
+
{ vi: 'Hai đạo lệnh ngược chiều làm mặt hồ dậy sóng, và Nghịch Lệnh Ma Quân bước ra.', en: 'Two opposing edicts ripple the still lake, and the Lord of Clashing Edicts steps forth.' }
|
|
85
|
+
],
|
|
86
|
+
spirited: [
|
|
87
|
+
{ vi: 'Nghịch Lệnh Ma Quân giơ hai cuộn lệnh và hỏi: “Cuộn nào mới thật sự dẫn đường?”', en: 'The Lord of Clashing Edicts raises two scrolls and asks: “Which one truly leads the way?”' },
|
|
88
|
+
{ vi: 'Hai lệnh cùng tranh ngôi; Nghịch Lệnh Ma Quân chỉ cần ta chưa lập thứ tự ưu tiên.', en: 'Two edicts contest the throne; the Lord of Clashing Edicts needs only an absent precedence rule.' }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
'unrecovered-failure': {
|
|
92
|
+
serene: [
|
|
93
|
+
{ vi: 'Vô Nghiệm Ảnh Quân ẩn sau một thất bại chưa được soi lại bằng lần kiểm chứng mới.', en: 'The Unverified Trial Wraith rests behind a failure that has not yet met a fresh validation.' },
|
|
94
|
+
{ vi: 'Một lỗi bị bỏ lại làm bóng Vô Nghiệm Ảnh Quân dài thêm trên bậc đá.', en: 'A failure left behind lengthens the Unverified Trial Wraith’s shadow across the stone steps.' }
|
|
95
|
+
],
|
|
96
|
+
spirited: [
|
|
97
|
+
{ vi: 'Vô Nghiệm Ảnh Quân phủi bụi trên ghế đá: một lần kiểm chứng thành công là đủ để mời nó rời đi.', en: 'The Unverified Trial Wraith dusts its stone seat; one passing validation is enough to send it on its way.' },
|
|
98
|
+
{ vi: 'Chưa có lần chạy lại, Vô Nghiệm Ảnh Quân vẫn ung dung giữ chỗ bên cổng hoàn thành.', en: 'Without a rerun, the Unverified Trial Wraith calmly keeps its seat beside the completion gate.' }
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
'work-without-proof': {
|
|
102
|
+
serene: [
|
|
103
|
+
{ vi: 'Vô Chứng Đan Ảnh lảng vảng bên lò luyện khi lời tuyên bố hoàn thành chưa có bằng chứng đi cùng.', en: 'The Proofless Elixir Shade lingers by the furnace when a claim of completion carries no evidence.' },
|
|
104
|
+
{ vi: 'Một bản ghi Git thiếu kết quả kiểm chứng để lại làn khói mỏng cho Vô Chứng Đan Ảnh nương thân.', en: 'A commit without validation leaves a thread of smoke where the Proofless Elixir Shade can dwell.' }
|
|
105
|
+
],
|
|
106
|
+
spirited: [
|
|
107
|
+
{ vi: 'Vô Chứng Đan Ảnh nâng chén trà bên viên đan chưa thử; một dòng kiểm chứng sẽ làm màn khói tan ngay.', en: 'The Proofless Elixir Shade raises tea beside an untested pill; one validation line will clear the smoke.' },
|
|
108
|
+
{ vi: 'Không có biên nhận kiểm chứng, Vô Chứng Đan Ảnh cứ ngỡ lò luyện thuộc về mình.', en: 'Without a validation receipt, the Proofless Elixir Shade starts to think the furnace is its own.' }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Roll the villain cooldown forward one reporting window. Villains shown this
|
|
114
|
+
// run reset to a full cooldown; villains from prior runs that were not shown
|
|
115
|
+
// decrement and drop at zero. Feeds the next run's `priorVillains`.
|
|
116
|
+
export function advanceVillainState(prevState = [], shownVillains = []) {
|
|
117
|
+
const next = new Map();
|
|
118
|
+
for (const p of prevState) {
|
|
119
|
+
const remaining = Math.max(0, (p.windowsRemaining ?? 0) - 1);
|
|
120
|
+
if (remaining > 0) next.set(p.problemId, { problemId: p.problemId, windowsRemaining: remaining, priority: p.priority });
|
|
121
|
+
}
|
|
122
|
+
for (const v of shownVillains) {
|
|
123
|
+
next.set(v.problemId, { problemId: v.problemId, windowsRemaining: v.cooldownWindows ?? DEFAULT_COOLDOWN_WINDOWS, priority: v.priorityAtShow ?? v.priority ?? 0 });
|
|
124
|
+
}
|
|
125
|
+
return [...next.values()].sort((a, b) => a.problemId.localeCompare(b.problemId));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function villainSeed({ projectId = 'repo', window = 'all', schemaVersion = 1 } = {}) {
|
|
129
|
+
return crypto.createHash('sha256').update(`${projectId}${window}${schemaVersion}`).digest('hex');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Deterministic non-negative index from the seed and a per-villain salt.
|
|
133
|
+
function seedIndex(seed, salt, n) {
|
|
134
|
+
if (n <= 1) return 0;
|
|
135
|
+
const h = crypto.createHash('sha256').update(`${seed}${salt}`).digest();
|
|
136
|
+
return h[0] % n;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// problems: the raw output of detectProblems(analysis).
|
|
140
|
+
// options: { tone, villains, threshold, emergency, seedInputs, context,
|
|
141
|
+
// priorVillains: [{ problemId, windowsRemaining, priority }] }.
|
|
142
|
+
export function buildVillains(problems, options = {}) {
|
|
143
|
+
const tone = normalizeTone(options.tone);
|
|
144
|
+
if (options.villains === 'off') return { villains: [], suppressed: 'villains-off' };
|
|
145
|
+
if (tone === 'neutral' || options.emergency) return { villains: [], suppressed: 'neutral-context' };
|
|
146
|
+
|
|
147
|
+
const threshold = options.threshold ?? VILLAIN_THRESHOLD;
|
|
148
|
+
const seed = villainSeed(options.seedInputs);
|
|
149
|
+
const context = sanitizeContext(options.context);
|
|
150
|
+
const prior = new Map((options.priorVillains ?? []).map((p) => [p.problemId, p]));
|
|
151
|
+
|
|
152
|
+
const eligible = problems
|
|
153
|
+
.filter((p) => p.confidence >= threshold && LINES[p.problemId])
|
|
154
|
+
.sort((a, b) => b.priority - a.priority || a.problemId.localeCompare(b.problemId));
|
|
155
|
+
|
|
156
|
+
const cards = [];
|
|
157
|
+
let bosses = 0;
|
|
158
|
+
let minors = 0;
|
|
159
|
+
for (const p of eligible) {
|
|
160
|
+
// Cooldown: skip a villain still cooling down unless its problem worsened.
|
|
161
|
+
const was = prior.get(p.problemId);
|
|
162
|
+
if (was && was.windowsRemaining > 0 && p.priority <= was.priority) continue;
|
|
163
|
+
|
|
164
|
+
let role;
|
|
165
|
+
if (bosses < MAX_BOSS) { role = 'boss'; bosses += 1; }
|
|
166
|
+
else if (minors < MAX_MINOR) { role = 'minor'; minors += 1; }
|
|
167
|
+
else break;
|
|
168
|
+
|
|
169
|
+
const toneLines = LINES[p.problemId][tone] ?? LINES[p.problemId].serene;
|
|
170
|
+
const idx = seedIndex(seed, p.problemId, toneLines.length);
|
|
171
|
+
const challenge = toneLines[idx];
|
|
172
|
+
|
|
173
|
+
if (containsBannedCategory(challenge.vi) || containsBannedCategory(challenge.en)) {
|
|
174
|
+
// Fail safe: drop the theatrics, keep the finding (rendered plainly).
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
cards.push({
|
|
179
|
+
problemId: p.problemId,
|
|
180
|
+
role,
|
|
181
|
+
priority: p.priority,
|
|
182
|
+
archetype: p.meta.villain,
|
|
183
|
+
tone,
|
|
184
|
+
challenge,
|
|
185
|
+
originSignals: [`metric:${p.meta.problemType}`, ...Object.values(context)],
|
|
186
|
+
evidenceRefs: p.evidence.eventIds ?? [],
|
|
187
|
+
confidence: p.confidence,
|
|
188
|
+
rebuttal: p.meta.counterTechnique,
|
|
189
|
+
microQuest: p.meta.microQuest,
|
|
190
|
+
victory: p.meta.victory,
|
|
191
|
+
cooldownWindows: DEFAULT_COOLDOWN_WINDOWS,
|
|
192
|
+
safetyFlags: ['failure-mode-not-person', 'evidence-bound']
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return { villains: cards, suppressed: null };
|
|
196
|
+
}
|
|
@@ -40,6 +40,8 @@ If no command is known, leave `validate` as a safe echo and ask the user to fill
|
|
|
40
40
|
|
|
41
41
|
The `conventions` section is created during first-time init and must be reviewed before writing. It should capture repo-specific rules with evidence, not a fixed framework template.
|
|
42
42
|
|
|
43
|
+
`conventions.review_required_before_write: true` means: before writing to agent surfaces or instruction files — rules, skills, workflows, commands, root instruction files, and `backbone.yml` itself — the agent must show the proposed diff and wait for explicit approval. It does not gate ordinary code edits inside `policy.editable_paths`. Skills that offer a "clean, proceed in the same turn" path (for example `claim`) must downgrade to propose-and-wait for writes this flag covers.
|
|
44
|
+
|
|
43
45
|
Include:
|
|
44
46
|
|
|
45
47
|
- Naming style for files, directories, and symbols.
|
package/.vibekit/docs/INSTALL.md
CHANGED
|
@@ -127,8 +127,10 @@ The installer also includes `visual-design-loop` for Claude, Codex, and Grok sur
|
|
|
127
127
|
|
|
128
128
|
## User-invoked utility skills
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
Five user-invoked skills install across Claude, Codex, Cursor, and Grok surfaces:
|
|
131
131
|
|
|
132
132
|
- `memento`: write a `MEMENTO.md` working note before closing a multi-day task (`/memento`), then resume from it in the next session (`/memento resume`).
|
|
133
133
|
- `coding-level`: set the explanation register from 0 (ELI5) to 5 (expert peer) with `/coding-level N`; stays active until reinvoked.
|
|
134
134
|
- `prompt-sharpener`: sharpen a rough prompt into a precise one with `/prompt-sharpener <rough prompt>`, then execute the sharpened version immediately in the same turn.
|
|
135
|
+
- `claim`: vet a request to bring something new into the repo with `/claim <request>` — validate URLs and references against official sources, check fit with existing rules and skills, confirm anything unclear, then integrate and document it.
|
|
136
|
+
- `tutien`: turn Git history and explicitly supplied AI-chat exports into a private, funny xianxia "cultivation" report with `/tutien` (realm, token use, workflow habits, evidence-bound suggestions); read-only, aggregate-only by default, and `/tutien off` disables the mode.
|
|
@@ -18,6 +18,8 @@ This file is intentionally small. Shared rules live in `AGENTS.md`, project fact
|
|
|
18
18
|
- `/daily-enhance` - propose rule, skill, and workflow improvements.
|
|
19
19
|
- `/coding-level N` - set explanation depth 0-5; the project default lives in `backbone.yml` `conventions.custom_rules`.
|
|
20
20
|
- `/prompt-sharpener <rough prompt>` - sharpen a rough prompt into a precise one, then execute it in the same turn.
|
|
21
|
+
- `/claim <request>` - vet a request to bring something new into the repo (validate sources, fit-check, confirm, integrate, document).
|
|
22
|
+
- `/tutien` - private, funny xianxia "cultivation" progress report from Git history + supplied AI-chat exports; `/tutien off` disables it.
|
|
21
23
|
|
|
22
24
|
## Hard rules
|
|
23
25
|
|
|
@@ -168,12 +168,12 @@ function parseProfiles(profileRaw) {
|
|
|
168
168
|
return profiles;
|
|
169
169
|
}
|
|
170
170
|
const CLAUDE_DIRS = ['.claude/agents', '.claude/commands', '.claude/rules'];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
// Skill registries come from the central distribution manifest so the
|
|
172
|
+
// installer and the validator cannot drift apart.
|
|
173
|
+
const skillsManifest = JSON.parse(fs.readFileSync(path.join(kitRoot, '.vibekit/skills/skills-manifest.json'), 'utf8'));
|
|
174
|
+
const CLAUDE_SKILLS = skillsManifest.skills.map((s) => s.name);
|
|
175
175
|
const CURSOR_DIRS = ['.cursor/rules', '.cursor/commands'];
|
|
176
|
-
const CURSOR_SKILLS =
|
|
176
|
+
const CURSOR_SKILLS = skillsManifest.skills.filter((s) => (s.surfaces || []).includes('cursor')).map((s) => s.name);
|
|
177
177
|
const CODEX_DIRS = ['.agents', '.codex', '.codex-plugin'];
|
|
178
178
|
const GROK_DIRS = ['.grok'];
|
|
179
179
|
const GITIGNORE_BLOCK = `# BEGIN: minimal-vibe-coding-kit\n.autoresearch/\nresults.tsv\n.vibekit/INIT_DONE\n.vibekit/FINALIZE_DONE\n.vibekit/reports/\n.vibekit/update-backup/\n_vibekit-cleanup/\nCLAUDE.local.md\n# END: minimal-vibe-coding-kit`;
|
|
@@ -1,16 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { spawnSync } from 'node:child_process';
|
|
5
6
|
|
|
6
7
|
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
7
8
|
const cache = process.env.MVCK_NPM_CACHE || path.join(os.tmpdir(), 'mvck-npm-cache');
|
|
8
|
-
const result = spawnSync(npm, ['pack', '--dry-run'], {
|
|
9
|
-
|
|
9
|
+
const result = spawnSync(npm, ['pack', '--dry-run', '--json'], {
|
|
10
|
+
encoding: 'utf8',
|
|
10
11
|
env: {
|
|
11
12
|
...process.env,
|
|
12
13
|
npm_config_cache: cache
|
|
13
14
|
}
|
|
14
15
|
});
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
if (result.status !== 0) {
|
|
18
|
+
process.stderr.write(result.stderr || '');
|
|
19
|
+
process.exit(result.status ?? 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let packed;
|
|
23
|
+
try {
|
|
24
|
+
packed = new Set(JSON.parse(result.stdout)[0].files.map((file) => file.path));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error(`FAIL could not parse npm pack --dry-run --json output: ${err.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Every skill file present on disk must ship in the tarball. A skill directory
|
|
31
|
+
// added without its package.json `files` entry fails here with the missing path.
|
|
32
|
+
const skillRoots = ['.vibekit/skills', '.claude/skills', '.cursor/skills', '.agents/skills', '.grok/skills'];
|
|
33
|
+
const expected = [];
|
|
34
|
+
function walk(dir) {
|
|
35
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
const rel = path.posix.join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) walk(rel);
|
|
38
|
+
else expected.push(rel);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const root of skillRoots) {
|
|
42
|
+
if (fs.existsSync(root)) walk(root);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const missing = expected.filter((rel) => !packed.has(rel));
|
|
46
|
+
for (const rel of missing) console.error(`FAIL packaged tarball is missing ${rel}`);
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
console.error(`Pack dry-run: ${missing.length} of ${expected.length} on-disk skill files are not packaged.`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.log(`PASS pack dry-run: all ${expected.length} on-disk skill files are packaged (${packed.size} files in tarball)`);
|
|
@@ -47,15 +47,31 @@ for (const [surface, present] of Object.entries(surfacePresent)) {
|
|
|
47
47
|
if (!present) console.log(`INFO surface ${surface} not installed; skipping its checks`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
// Skill registries come from the central distribution manifest shared with
|
|
51
|
+
// the installer; validation fails closed when the manifest is missing.
|
|
52
|
+
const MANIFEST_REL = '.vibekit/skills/skills-manifest.json';
|
|
53
|
+
const skillsManifest = readJson(MANIFEST_REL);
|
|
54
|
+
if (!skillsManifest || !Array.isArray(skillsManifest.skills)) {
|
|
55
|
+
fail(`missing or invalid ${MANIFEST_REL}; installer and validator registries derive from it`);
|
|
56
|
+
}
|
|
57
|
+
const manifestSkills = skillsManifest?.skills ?? [];
|
|
58
|
+
const KIT_SKILLS = manifestSkills.map((s) => s.name);
|
|
59
|
+
const CURSOR_KIT_SKILLS = manifestSkills.filter((s) => (s.surfaces || []).includes('cursor')).map((s) => s.name);
|
|
60
|
+
|
|
61
|
+
// Fail closed: a canonical skill directory that is not in the manifest would
|
|
62
|
+
// otherwise escape mirror, package, and install validation entirely.
|
|
63
|
+
const canonicalSkillDirs = exists('.vibekit/skills')
|
|
64
|
+
? fs.readdirSync(path.join(root, '.vibekit/skills'), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
|
|
65
|
+
: [];
|
|
66
|
+
for (const dir of canonicalSkillDirs) {
|
|
67
|
+
if (KIT_SKILLS.includes(dir)) continue;
|
|
68
|
+
const msg = `canonical skill dir .vibekit/skills/${dir} is not registered in ${MANIFEST_REL}`;
|
|
69
|
+
if (isKitSourceRepo) fail(msg);
|
|
70
|
+
else warn(`${msg}; register it to get mirror, package, and install validation`);
|
|
71
|
+
}
|
|
72
|
+
for (const skill of KIT_SKILLS) {
|
|
73
|
+
if (!canonicalSkillDirs.includes(skill)) fail(`manifest skill ${skill} has no canonical dir .vibekit/skills/${skill}`);
|
|
74
|
+
}
|
|
59
75
|
|
|
60
76
|
const required = [
|
|
61
77
|
'AGENTS.md', '.vibekit/init/CLAUDE-template.md', '.vibekit/init/FIRST_TIME_INIT.md', '.vibekit/init/FIRST_PROMPT.md', 'backbone.yml',
|
|
@@ -142,21 +158,15 @@ if (exists('.vibekit/docs/AUTORESEARCH_LEDGER.md')) {
|
|
|
142
158
|
stalePhrase ? fail(`.vibekit/docs/AUTORESEARCH_LEDGER.md contains stale phrase: ${stalePhrase}`) : ok('autoresearch ledger has no stale build placeholders');
|
|
143
159
|
}
|
|
144
160
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
'visual-design-loop': ['.claude/skills/visual-design-loop', '.agents/skills/visual-design-loop', '.grok/skills/visual-design-loop'],
|
|
155
|
-
'memento': ['.claude/skills/memento', '.cursor/skills/memento', '.agents/skills/memento', '.grok/skills/memento'],
|
|
156
|
-
'coding-level': ['.claude/skills/coding-level', '.cursor/skills/coding-level', '.agents/skills/coding-level', '.grok/skills/coding-level'],
|
|
157
|
-
'parallel-analysis': ['.claude/skills/parallel-analysis', '.cursor/skills/parallel-analysis', '.agents/skills/parallel-analysis', '.grok/skills/parallel-analysis'],
|
|
158
|
-
'prompt-sharpener': ['.claude/skills/prompt-sharpener', '.cursor/skills/prompt-sharpener', '.agents/skills/prompt-sharpener', '.grok/skills/prompt-sharpener']
|
|
159
|
-
};
|
|
161
|
+
// Mirror registry is derived from the manifest so a skill cannot be
|
|
162
|
+
// registered for install yet skipped by parity validation.
|
|
163
|
+
const MANIFEST_SURFACE_DIRS = { claude: '.claude/skills', cursor: '.cursor/skills', codex: '.agents/skills', grok: '.grok/skills' };
|
|
164
|
+
const skillMirrors = {};
|
|
165
|
+
for (const skill of manifestSkills) {
|
|
166
|
+
skillMirrors[skill.name] = (skill.surfaces || [])
|
|
167
|
+
.filter((surface) => MANIFEST_SURFACE_DIRS[surface])
|
|
168
|
+
.map((surface) => `${MANIFEST_SURFACE_DIRS[surface]}/${skill.name}`);
|
|
169
|
+
}
|
|
160
170
|
|
|
161
171
|
function validateSkillMirror(sourceRel, mirrorRel) {
|
|
162
172
|
if (!exists(sourceRel)) { fail(`missing canonical skill dir ${sourceRel}`); return; }
|