agentic-sdd-framework 1.4.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/.agents/AGENTS.template.md +59 -0
- package/.agents/CONTEXT.template.md +41 -0
- package/.agents/ENTRYPOINT.template.md +31 -0
- package/.agents/skills/ast-navigator/SKILL.md +31 -0
- package/.agents/skills/ast-navigator/adapters/ast-grep.md +18 -0
- package/.agents/skills/ast-navigator/adapters/graphify.md +19 -0
- package/.agents/skills/ast-navigator/adapters/lsp.md +16 -0
- package/.agents/skills/ast-navigator/adapters/ripgrep.md +19 -0
- package/.agents/skills/auditor-executor-protocol/SKILL.md +410 -0
- package/.agents/skills/auditor-executor-protocol/references/autonomous-mode.md +144 -0
- package/.agents/skills/auditor-executor-protocol/references/failure-modes-and-example.md +103 -0
- package/.agents/skills/auditor-executor-protocol/references/handoffs.md +133 -0
- package/.agents/skills/auditor-executor-protocol/references/tasks-and-gates.md +81 -0
- package/.agents/skills/no-ai-slop/LICENSE +21 -0
- package/.agents/skills/no-ai-slop/SKILL.md +52 -0
- package/.agents/skills/strategic-cto/SKILL.md +54 -0
- package/CHANGELOG.md +117 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/docs/SPEC_TEMPLATE.md +78 -0
- package/docs/decisions/ADR_TEMPLATE.md +49 -0
- package/docs/guidelines/AST_NAVIGATION.md +51 -0
- package/docs/guides/AGENT_CREDENTIALS.md +75 -0
- package/docs/guides/GITHUB_CLI_SETUP.md +74 -0
- package/docs/incidents/0000-00-00-incident-template.md +35 -0
- package/docs/roadmap/templates/compliance-log.md +37 -0
- package/docs/roadmap/templates/execution-guide.md +75 -0
- package/docs/roadmap/templates/plan-of-record.md +49 -0
- package/package.json +49 -0
- package/scripts/check-copy-slop.js +120 -0
- package/scripts/check-file-size.js +66 -0
- package/scripts/check-spec.js +201 -0
- package/scripts/check-system-prerequisites.js +133 -0
- package/scripts/check-versions.js +50 -0
- package/scripts/dev/fuzz-spec-markup.js +123 -0
- package/scripts/dev/set-npm-publish-token.sh +40 -0
- package/scripts/dev/sync-vendored.js +94 -0
- package/scripts/install-git-hooks.js +103 -0
- package/scripts/lib/cli.js +60 -0
- package/scripts/lib/config.js +111 -0
- package/scripts/lib/git.js +211 -0
- package/scripts/lib/markdown.js +46 -0
- package/scripts/lib/provision.js +323 -0
- package/scripts/lib/runner.js +70 -0
- package/scripts/lib/sdd.config.schema.json +213 -0
- package/scripts/lib/slop-patterns.js +57 -0
- package/scripts/lib/spec-markup.js +346 -0
- package/scripts/lib/spec.js +226 -0
- package/scripts/lib/state.js +107 -0
- package/scripts/lib/vendor/README.md +11 -0
- package/scripts/lib/vendor/markdown-it.LICENSE +22 -0
- package/scripts/lib/vendor/markdown-it.min.js +3 -0
- package/scripts/quality-gate.js +151 -0
- package/scripts/sdd-init.js +245 -0
- package/scripts/sdd-verify.js +176 -0
- package/scripts/verify-no-secrets.js +216 -0
- package/sdd.config.json +33 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/lib/provision.js
|
|
3
|
+
*
|
|
4
|
+
* Writes the SDD governance structure into a target project. Every step is
|
|
5
|
+
* idempotent: documents that already exist are kept, sdd.config.json is merged
|
|
6
|
+
* (unknown keys survive), and generated entry points are only rewritten while they
|
|
7
|
+
* still carry the sdd:managed marker.
|
|
8
|
+
*
|
|
9
|
+
* Clone mode: target is the framework checkout itself; templates are used in place.
|
|
10
|
+
* Install mode: target is another project; templates, skills and the quality gate
|
|
11
|
+
* tooling (.sdd/scripts/) are copied into it first.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { install: installHook } = require('../install-git-hooks');
|
|
17
|
+
const { validateConfig, getIn } = require('./config');
|
|
18
|
+
|
|
19
|
+
const FRAMEWORK_ROOT = path.resolve(__dirname, '..', '..');
|
|
20
|
+
const MANAGED_MARKER = 'sdd:managed';
|
|
21
|
+
// Marks a .claude/skills/<name> directory as a copy made by sdd-init (where symlinks are
|
|
22
|
+
// unavailable), so --force refreshes it and never touches a directory the user created.
|
|
23
|
+
const COPY_MARKER = '.sdd-managed-copy';
|
|
24
|
+
const SPEC_MODES = ['lite', 'rigor'];
|
|
25
|
+
const AST_ADAPTERS = ['ast-grep', 'graphify', 'ripgrep', 'lsp'];
|
|
26
|
+
const TOOL_DIR = '.sdd/scripts';
|
|
27
|
+
const TOOL_FILES = [
|
|
28
|
+
'quality-gate.js', 'verify-no-secrets.js', 'check-copy-slop.js', 'check-file-size.js', 'check-spec.js',
|
|
29
|
+
'check-versions.js', 'check-system-prerequisites.js', 'install-git-hooks.js', 'sdd-verify.js', 'lib'
|
|
30
|
+
];
|
|
31
|
+
const TEMPLATES = [
|
|
32
|
+
'.agents/AGENTS.template.md',
|
|
33
|
+
'.agents/CONTEXT.template.md',
|
|
34
|
+
'docs/SPEC_TEMPLATE.md',
|
|
35
|
+
'docs/decisions/ADR_TEMPLATE.md',
|
|
36
|
+
'docs/incidents/0000-00-00-incident-template.md',
|
|
37
|
+
'docs/roadmap/templates/plan-of-record.md',
|
|
38
|
+
'docs/roadmap/templates/execution-guide.md',
|
|
39
|
+
'docs/roadmap/templates/compliance-log.md',
|
|
40
|
+
'docs/guides/AGENT_CREDENTIALS.md',
|
|
41
|
+
'docs/guides/GITHUB_CLI_SETUP.md',
|
|
42
|
+
'docs/guidelines/AST_NAVIGATION.md'
|
|
43
|
+
];
|
|
44
|
+
// Rigor documents use the auditkit format (tBeltty/auditor-executor-protocol) so `auditkit lint` can check them.
|
|
45
|
+
const RIGOR_DOCS = ['plan-of-record', 'execution-guide', 'compliance-log'];
|
|
46
|
+
const ICONS = { created: '✅', updated: '🔄', linked: '🔗', copied: '📄', kept: '⏭️ ', skipped: '⚠️ ', unchanged: '⏭️ ' };
|
|
47
|
+
|
|
48
|
+
// String.prototype.replace would interpret "$5" in answers like "$5 VPS".
|
|
49
|
+
const replaceLiteral = (text, from, to) => text.split(from).join(to);
|
|
50
|
+
|
|
51
|
+
function readJson(file) {
|
|
52
|
+
if (!fs.existsSync(file)) return null;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`${path.basename(file)} is not valid JSON: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isPlainObject(value) {
|
|
61
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function deepMerge(base, override) {
|
|
65
|
+
const result = { ...base };
|
|
66
|
+
for (const [key, value] of Object.entries(override || {})) {
|
|
67
|
+
result[key] = isPlainObject(value) && isPlainObject(result[key]) ? deepMerge(result[key], value) : value;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sameDir(a, b) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.realpathSync(a) === fs.realpathSync(b);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeAnswers(answers) {
|
|
81
|
+
const specMode = answers.specMode === 'strict' ? 'rigor' : answers.specMode;
|
|
82
|
+
if (!SPEC_MODES.includes(specMode)) {
|
|
83
|
+
throw new Error(`Unknown mode "${answers.specMode}". Valid: ${SPEC_MODES.join(', ')}.`);
|
|
84
|
+
}
|
|
85
|
+
if (!AST_ADAPTERS.includes(answers.astAdapter)) {
|
|
86
|
+
throw new Error(`Unknown AST adapter "${answers.astAdapter}". Valid: ${AST_ADAPTERS.join(', ')}.`);
|
|
87
|
+
}
|
|
88
|
+
return { ...answers, specMode };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildConfig(answers, existing) {
|
|
92
|
+
const defaults = {
|
|
93
|
+
architecture: { style: 'clean-architecture', maxLocPerFile: 400 },
|
|
94
|
+
capabilities: { noAiSlop: { enabled: true } }
|
|
95
|
+
};
|
|
96
|
+
const generated = {
|
|
97
|
+
$schema: `https://raw.githubusercontent.com/tBeltty/agentic-sdd-framework/v${frameworkVersion()}/scripts/lib/sdd.config.schema.json`,
|
|
98
|
+
version: frameworkVersion(),
|
|
99
|
+
project: { name: answers.projectName, type: 'application', runtime: answers.runtime },
|
|
100
|
+
specification: { mode: answers.specMode, allowedModes: SPEC_MODES },
|
|
101
|
+
discovery: { concurrency: answers.concurrency, hardware: answers.hardware, workload: answers.workload },
|
|
102
|
+
capabilities: {
|
|
103
|
+
astNavigation: { adapter: answers.astAdapter },
|
|
104
|
+
i18n: { enabled: answers.i18n },
|
|
105
|
+
pwa: { enabled: answers.pwa }
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const merged = deepMerge(deepMerge(defaults, existing || {}), generated);
|
|
109
|
+
const order = ['$schema', 'version', 'project', 'specification', 'discovery', 'architecture', 'capabilities'];
|
|
110
|
+
const config = Object.fromEntries([
|
|
111
|
+
...order.filter(k => k in merged).map(k => [k, merged[k]]),
|
|
112
|
+
...Object.entries(merged).filter(([k]) => !order.includes(k))
|
|
113
|
+
]);
|
|
114
|
+
const errors = validateConfig(config);
|
|
115
|
+
if (errors.length > 0) {
|
|
116
|
+
throw new Error(`sdd.config.json is invalid; fix it and rerun:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
117
|
+
}
|
|
118
|
+
return config;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function frameworkVersion() {
|
|
122
|
+
return readJson(path.join(FRAMEWORK_ROOT, 'package.json')).version;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fillContext(text, answers, config) {
|
|
126
|
+
const onOff = flag => (flag ? 'Enabled' : 'Disabled');
|
|
127
|
+
const pairs = [
|
|
128
|
+
['[Project Name]', answers.projectName],
|
|
129
|
+
['Lite | Rigor', answers.specMode === 'lite' ? 'Lite' : 'Rigor'],
|
|
130
|
+
['[e.g. Node.js 24 LTS, Go 1.23, Python 3.12]', answers.runtime],
|
|
131
|
+
['Clean Architecture | Standard MVC | Modular Monolith', config.architecture.style],
|
|
132
|
+
['No-AI-Slop Linter: [Enabled / Disabled]', `No-AI-Slop Linter: ${onOff(config.capabilities.noAiSlop.enabled !== false)}`],
|
|
133
|
+
['[graphify / ast-grep / ripgrep / lsp]', answers.astAdapter],
|
|
134
|
+
['Internationalization (i18n): [Enabled / Disabled]', `Internationalization (i18n): ${onOff(answers.i18n)}`],
|
|
135
|
+
['Progressive Web App (PWA): [Enabled / Disabled]', `Progressive Web App (PWA): ${onOff(answers.pwa)}`]
|
|
136
|
+
];
|
|
137
|
+
return pairs.reduce((out, [from, to]) => replaceLiteral(out, from, to), text);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function fillAdr(text, answers, date) {
|
|
141
|
+
const required = flag => (flag ? 'required' : 'not required');
|
|
142
|
+
const recorded = [
|
|
143
|
+
'',
|
|
144
|
+
`**Recorded answers (sdd-init, ${date}):**`,
|
|
145
|
+
'',
|
|
146
|
+
`* **Scale and Concurrency:** ${answers.concurrency}`,
|
|
147
|
+
`* **Hardware and Deployment:** ${answers.hardware}`,
|
|
148
|
+
`* **Data and Workload:** ${answers.workload}`,
|
|
149
|
+
`* **Modularity:** i18n ${required(answers.i18n)}, PWA ${required(answers.pwa)}`,
|
|
150
|
+
`* **Runtime Preference:** ${answers.runtime}`,
|
|
151
|
+
''
|
|
152
|
+
].join('\n');
|
|
153
|
+
let out = replaceLiteral(text, '# ADR-[NNNN]: [Decision Title]', '# ADR-0001: Technology Stack and Architecture Selection');
|
|
154
|
+
out = replaceLiteral(out, '**Status:** Accepted | Proposed | Superseded', '**Status:** Proposed');
|
|
155
|
+
out = replaceLiteral(out, '**Date:** [YYYY-MM-DD]', `**Date:** ${date}`);
|
|
156
|
+
return out.replace(/(4\. \*\*Modularity and Localization:\*\*[^\n]*\n)/, match => match + recorded);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderEntrypoint(answers, gateCommand, config) {
|
|
160
|
+
const specFile = getIn(config, 'specification.specFile', 'docs/SPEC.md');
|
|
161
|
+
const roadmapDir = getIn(config, 'specification.roadmapDir', 'docs/roadmap');
|
|
162
|
+
const specLocation = answers.specMode === 'lite'
|
|
163
|
+
? `\`${specFile}\`: the active specification (Lite mode).`
|
|
164
|
+
: `\`${roadmapDir}/\`: \`plan-of-record.md\`, \`execution-guide.md\`, \`compliance-log.md\`, and \`annexes/\` (Rigor mode).`;
|
|
165
|
+
const toolDir = gateCommand.replace(/^node /, '').replace(/\/quality-gate\.js$/, '');
|
|
166
|
+
const specCheck = answers.specMode === 'lite'
|
|
167
|
+
? `Record task evidence with \`node ${toolDir}/sdd-verify.js --task <ID> -- <command>\` and close \`${specFile}\` with \`node ${toolDir}/sdd-verify.js --record\`; the gate rejects a \`Completed\` spec whose recorded PASS does not match the committed content.`
|
|
168
|
+
: `The gate runs \`auditkit lint ${roadmapDir}\`; a \`DONE\` report without pasted verify output fails it.`;
|
|
169
|
+
const values = {
|
|
170
|
+
SPEC_CHECK: specCheck,
|
|
171
|
+
PROJECT_NAME: answers.projectName,
|
|
172
|
+
SPEC_LOCATION: specLocation,
|
|
173
|
+
AST_ADAPTER: answers.astAdapter,
|
|
174
|
+
RUNTIME: answers.runtime,
|
|
175
|
+
SPEC_MODE: answers.specMode,
|
|
176
|
+
GATE_COMMAND: gateCommand
|
|
177
|
+
};
|
|
178
|
+
const template = fs.readFileSync(path.join(FRAMEWORK_ROOT, '.agents/ENTRYPOINT.template.md'), 'utf8');
|
|
179
|
+
return Object.entries(values).reduce((out, [key, value]) => replaceLiteral(out, `{{${key}}}`, value), template);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const CLAUDE_ENTRYPOINT = `<!-- ${MANAGED_MARKER}. Generated by sdd-init; rerun it to refresh. Put project rules in .agents/AGENTS.md. -->
|
|
183
|
+
@AGENTS.md
|
|
184
|
+
@.agents/AGENTS.md
|
|
185
|
+
@.agents/CONTEXT.md
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
function provision(rawAnswers, { target = process.cwd(), force = false, log = console.log, date } = {}) {
|
|
189
|
+
const answers = normalizeAnswers(rawAnswers);
|
|
190
|
+
const root = path.resolve(target);
|
|
191
|
+
const installMode = !sameDir(root, FRAMEWORK_ROOT);
|
|
192
|
+
const today = date || new Date().toISOString().slice(0, 10);
|
|
193
|
+
const at = rel => path.join(root, rel);
|
|
194
|
+
const record = (status, what) => log(` ${ICONS[status]} ${what} (${status})`);
|
|
195
|
+
const skippedEntrypoints = [];
|
|
196
|
+
|
|
197
|
+
const write = (rel, content) => {
|
|
198
|
+
fs.mkdirSync(path.dirname(at(rel)), { recursive: true });
|
|
199
|
+
fs.writeFileSync(at(rel), content);
|
|
200
|
+
};
|
|
201
|
+
const createFromTemplate = (templateRel, destRel, transform = t => t) => {
|
|
202
|
+
if (fs.existsSync(at(destRel))) return record('kept', destRel);
|
|
203
|
+
write(destRel, transform(fs.readFileSync(at(templateRel), 'utf8')));
|
|
204
|
+
record('created', destRel);
|
|
205
|
+
};
|
|
206
|
+
const writeManaged = (rel, content) => {
|
|
207
|
+
if (fs.existsSync(at(rel))) {
|
|
208
|
+
const current = fs.readFileSync(at(rel), 'utf8');
|
|
209
|
+
if (!current.includes(MANAGED_MARKER)) {
|
|
210
|
+
skippedEntrypoints.push(rel);
|
|
211
|
+
return record('skipped', `${rel} exists and is not managed by sdd-init`);
|
|
212
|
+
}
|
|
213
|
+
if (current === content) return record('unchanged', rel);
|
|
214
|
+
write(rel, content);
|
|
215
|
+
return record('updated', rel);
|
|
216
|
+
}
|
|
217
|
+
write(rel, content);
|
|
218
|
+
record('created', rel);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// 1. Framework assets (install mode only). Tooling is framework-owned and always
|
|
222
|
+
// refreshed; skills and templates are refreshed only with --force.
|
|
223
|
+
if (installMode) {
|
|
224
|
+
const versionFile = at('.sdd/VERSION');
|
|
225
|
+
const previous = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : null;
|
|
226
|
+
for (const file of TOOL_FILES) {
|
|
227
|
+
fs.cpSync(path.join(FRAMEWORK_ROOT, 'scripts', file), at(path.join(TOOL_DIR, file)), { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
write('.sdd/VERSION', `${frameworkVersion()}\n`);
|
|
230
|
+
const change = previous && previous !== frameworkVersion() ? ` from ${previous} to ${frameworkVersion()}` : ` at ${frameworkVersion()}`;
|
|
231
|
+
record('updated', `${TOOL_DIR}/ (quality gate tooling${change})`);
|
|
232
|
+
const skillsRoot = path.join(FRAMEWORK_ROOT, '.agents/skills');
|
|
233
|
+
for (const skill of fs.readdirSync(skillsRoot, { withFileTypes: true }).filter(d => d.isDirectory())) {
|
|
234
|
+
const rel = `.agents/skills/${skill.name}`;
|
|
235
|
+
if (fs.existsSync(at(rel)) && !force) { record('kept', rel); continue; }
|
|
236
|
+
fs.cpSync(path.join(skillsRoot, skill.name), at(rel), { recursive: true });
|
|
237
|
+
record('copied', rel);
|
|
238
|
+
}
|
|
239
|
+
for (const rel of TEMPLATES) {
|
|
240
|
+
if (fs.existsSync(at(rel)) && !force) continue;
|
|
241
|
+
fs.mkdirSync(path.dirname(at(rel)), { recursive: true });
|
|
242
|
+
fs.copyFileSync(path.join(FRAMEWORK_ROOT, rel), at(rel));
|
|
243
|
+
}
|
|
244
|
+
record('copied', 'templates and guides referenced by the skills');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 2. Capability manifest.
|
|
248
|
+
const configPath = at('sdd.config.json');
|
|
249
|
+
const existingConfig = readJson(configPath);
|
|
250
|
+
const config = buildConfig(answers, existingConfig);
|
|
251
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
252
|
+
record(existingConfig ? 'updated' : 'created', 'sdd.config.json');
|
|
253
|
+
|
|
254
|
+
// 3. Governance documents.
|
|
255
|
+
createFromTemplate('.agents/AGENTS.template.md', '.agents/AGENTS.md');
|
|
256
|
+
createFromTemplate('.agents/CONTEXT.template.md', '.agents/CONTEXT.md', t => fillContext(t, answers, config));
|
|
257
|
+
// Documents go where the config says, so the gate checks the files sdd-init created.
|
|
258
|
+
const specFile = getIn(config, 'specification.specFile', 'docs/SPEC.md');
|
|
259
|
+
const roadmapDir = getIn(config, 'specification.roadmapDir', 'docs/roadmap');
|
|
260
|
+
if (answers.specMode === 'lite') {
|
|
261
|
+
createFromTemplate('docs/SPEC_TEMPLATE.md', specFile);
|
|
262
|
+
} else {
|
|
263
|
+
for (const doc of RIGOR_DOCS) {
|
|
264
|
+
createFromTemplate(`docs/roadmap/templates/${doc}.md`, `${roadmapDir}/${doc}.md`,
|
|
265
|
+
t => replaceLiteral(t, '{{PROJECT_NAME}}', answers.projectName));
|
|
266
|
+
}
|
|
267
|
+
if (!fs.existsSync(at(`${roadmapDir}/annexes/.gitkeep`))) write(`${roadmapDir}/annexes/.gitkeep`, '');
|
|
268
|
+
}
|
|
269
|
+
createFromTemplate('docs/decisions/ADR_TEMPLATE.md', 'docs/decisions/ADR-0001-stack-and-architecture.md',
|
|
270
|
+
t => fillAdr(t, answers, today));
|
|
271
|
+
|
|
272
|
+
// 4. Agent entry points: AGENTS.md (Codex, Cursor, others), CLAUDE.md and
|
|
273
|
+
// .claude/skills/ (Claude Code) so the rules load without a manual prompt.
|
|
274
|
+
const gateScript = installMode ? at(`${TOOL_DIR}/quality-gate.js`) : path.join(FRAMEWORK_ROOT, 'scripts/quality-gate.js');
|
|
275
|
+
const gateCommand = `node ${path.relative(root, gateScript).split(path.sep).join('/')}`;
|
|
276
|
+
writeManaged('AGENTS.md', renderEntrypoint(answers, gateCommand, config));
|
|
277
|
+
writeManaged('CLAUDE.md', CLAUDE_ENTRYPOINT);
|
|
278
|
+
for (const skill of fs.readdirSync(at('.agents/skills'), { withFileTypes: true }).filter(d => d.isDirectory())) {
|
|
279
|
+
const rel = `.claude/skills/${skill.name}`;
|
|
280
|
+
if (isSymlink(at(rel))) { record('kept', rel); continue; }
|
|
281
|
+
const copySkill = () => {
|
|
282
|
+
fs.cpSync(at(`.agents/skills/${skill.name}`), at(rel), { recursive: true });
|
|
283
|
+
fs.writeFileSync(at(`${rel}/${COPY_MARKER}`), 'Copied by sdd-init; rerun with --force to refresh.\n');
|
|
284
|
+
record('copied', rel);
|
|
285
|
+
};
|
|
286
|
+
if (fs.existsSync(at(rel))) {
|
|
287
|
+
// A copy made by sdd-init goes stale; --force refreshes it. Anything else is the user's.
|
|
288
|
+
if (!force || !fs.existsSync(at(`${rel}/${COPY_MARKER}`))) { record('kept', rel); continue; }
|
|
289
|
+
fs.rmSync(at(rel), { recursive: true, force: true });
|
|
290
|
+
copySkill();
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
fs.mkdirSync(path.dirname(at(rel)), { recursive: true });
|
|
294
|
+
try {
|
|
295
|
+
fs.symlinkSync(path.join('..', '..', '.agents', 'skills', skill.name), at(rel), 'dir');
|
|
296
|
+
record('linked', rel);
|
|
297
|
+
} catch {
|
|
298
|
+
copySkill();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 5. Pre-push hook.
|
|
303
|
+
let hookInstalled = false;
|
|
304
|
+
try {
|
|
305
|
+
const { preserved } = installHook({ root, gateScript });
|
|
306
|
+
record('created', `pre-push quality gate hook${preserved ? ' (previous hook kept as pre-push.local)' : ''}`);
|
|
307
|
+
hookInstalled = true;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
record('skipped', `pre-push hook: ${error.message}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { installMode, root, gateCommand, hookInstalled, config, skippedEntrypoints };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isSymlink(file) {
|
|
316
|
+
try {
|
|
317
|
+
return fs.lstatSync(file).isSymbolicLink();
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = { FRAMEWORK_ROOT, SPEC_MODES, AST_ADAPTERS, provision, buildConfig, deepMerge };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/lib/runner.js
|
|
3
|
+
*
|
|
4
|
+
* Runs a verification command through a shell with a time limit and captures its output.
|
|
5
|
+
* The shell is specification.verifyShell when set, otherwise the platform default
|
|
6
|
+
* (/bin/sh on macOS and Linux, cmd.exe on Windows). On timeout the whole process tree is
|
|
7
|
+
* killed (a process group on macOS/Linux, `taskkill /T` on Windows), so background
|
|
8
|
+
* processes started by the command do not keep running.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawn, spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TIMEOUT_SECONDS = 900;
|
|
14
|
+
const MAX_OUTPUT = 64 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
function killTree(pid) {
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
process.kill(-pid, 'SIGKILL');
|
|
23
|
+
} catch {
|
|
24
|
+
// Already gone.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runCommand(command, { cwd, timeoutSeconds = DEFAULT_TIMEOUT_SECONDS, shell } = {}) {
|
|
29
|
+
return new Promise(resolve => {
|
|
30
|
+
const child = spawn(command, {
|
|
31
|
+
cwd,
|
|
32
|
+
shell: shell || true,
|
|
33
|
+
detached: process.platform !== 'win32', // own process group, killed as a whole
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
windowsHide: true
|
|
36
|
+
});
|
|
37
|
+
let output = '';
|
|
38
|
+
const append = chunk => {
|
|
39
|
+
if (output.length < MAX_OUTPUT) output += chunk;
|
|
40
|
+
};
|
|
41
|
+
// Decode as UTF-8 streams so a multibyte character split across chunks survives.
|
|
42
|
+
child.stdout.setEncoding('utf8');
|
|
43
|
+
child.stderr.setEncoding('utf8');
|
|
44
|
+
child.stdout.on('data', append);
|
|
45
|
+
child.stderr.on('data', append);
|
|
46
|
+
let timedOut = false;
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
timedOut = true;
|
|
49
|
+
killTree(child.pid);
|
|
50
|
+
}, timeoutSeconds * 1000);
|
|
51
|
+
child.on('error', error => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
resolve({ exit: 'error', output: `${output}\n(${error.message})` });
|
|
54
|
+
});
|
|
55
|
+
child.on('close', (code, signal) => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
if (timedOut) resolve({ exit: 'timeout', output: `${output}\n(timed out after ${timeoutSeconds}s)` });
|
|
58
|
+
else if (code === null) resolve({ exit: `signal ${signal}`, output });
|
|
59
|
+
else resolve({ exit: String(code), output });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Quotes argv tokens back into one command line (used for `sdd-verify --task ID -- cmd ...`).
|
|
65
|
+
function joinCommand(tokens) {
|
|
66
|
+
if (tokens.length === 1) return tokens[0];
|
|
67
|
+
return tokens.map(t => (/[\s"'$`\\]/.test(t) ? `"${t.replace(/(["\\$`])/g, '\\$1')}"` : t)).join(' ');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { DEFAULT_TIMEOUT_SECONDS, runCommand, joinCommand };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Agentic SDD Framework configuration (sdd.config.json)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"patternProperties": {
|
|
7
|
+
"^x-": {}
|
|
8
|
+
},
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"version": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Framework version that generated this file."
|
|
16
|
+
},
|
|
17
|
+
"project": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"patternProperties": {
|
|
21
|
+
"^x-": {}
|
|
22
|
+
},
|
|
23
|
+
"properties": {
|
|
24
|
+
"name": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"type": {
|
|
28
|
+
"enum": [
|
|
29
|
+
"application",
|
|
30
|
+
"framework"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"runtime": {
|
|
34
|
+
"type": "string"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"specification": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"patternProperties": {
|
|
42
|
+
"^x-": {}
|
|
43
|
+
},
|
|
44
|
+
"properties": {
|
|
45
|
+
"mode": {
|
|
46
|
+
"enum": [
|
|
47
|
+
"lite",
|
|
48
|
+
"rigor"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"allowedModes": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {
|
|
54
|
+
"enum": [
|
|
55
|
+
"lite",
|
|
56
|
+
"rigor"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"specFile": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"x-relative-path": true,
|
|
63
|
+
"description": "Lite spec path (default docs/SPEC.md).",
|
|
64
|
+
"minLength": 1,
|
|
65
|
+
"x-file-path": true
|
|
66
|
+
},
|
|
67
|
+
"roadmapDir": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"x-relative-path": true,
|
|
70
|
+
"description": "Rigor document directory (default docs/roadmap).",
|
|
71
|
+
"minLength": 1,
|
|
72
|
+
"x-file-path": true
|
|
73
|
+
},
|
|
74
|
+
"verifyTimeoutSeconds": {
|
|
75
|
+
"type": "integer",
|
|
76
|
+
"minimum": 1,
|
|
77
|
+
"description": "sdd-verify limit per command (default 900)."
|
|
78
|
+
},
|
|
79
|
+
"verifyShell": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Shell for sdd-verify commands (default: sh on macOS/Linux, cmd.exe on Windows)."
|
|
82
|
+
},
|
|
83
|
+
"requireRecordedEvidence": {
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"description": "Reject checked tasks whose evidence was not recorded by sdd-verify --task (default false)."
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"discovery": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": false,
|
|
92
|
+
"patternProperties": {
|
|
93
|
+
"^x-": {}
|
|
94
|
+
},
|
|
95
|
+
"properties": {
|
|
96
|
+
"concurrency": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
},
|
|
99
|
+
"hardware": {
|
|
100
|
+
"type": "string"
|
|
101
|
+
},
|
|
102
|
+
"workload": {
|
|
103
|
+
"type": "string"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"architecture": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"additionalProperties": false,
|
|
110
|
+
"patternProperties": {
|
|
111
|
+
"^x-": {}
|
|
112
|
+
},
|
|
113
|
+
"properties": {
|
|
114
|
+
"style": {
|
|
115
|
+
"type": "string"
|
|
116
|
+
},
|
|
117
|
+
"maxLocPerFile": {
|
|
118
|
+
"type": "integer",
|
|
119
|
+
"minimum": 0,
|
|
120
|
+
"description": "0 disables the check (default 400)."
|
|
121
|
+
},
|
|
122
|
+
"maxLocExclude": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"items": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"x-relative-path": true,
|
|
127
|
+
"minLength": 1
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"security": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"additionalProperties": false,
|
|
135
|
+
"patternProperties": {
|
|
136
|
+
"^x-": {}
|
|
137
|
+
},
|
|
138
|
+
"properties": {
|
|
139
|
+
"allowFiles": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"items": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"x-relative-path": true,
|
|
144
|
+
"minLength": 1
|
|
145
|
+
},
|
|
146
|
+
"description": "Tracked paths allowed despite a sensitive name (.env, *.key, ...)."
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"capabilities": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"additionalProperties": false,
|
|
153
|
+
"patternProperties": {
|
|
154
|
+
"^x-": {}
|
|
155
|
+
},
|
|
156
|
+
"properties": {
|
|
157
|
+
"astNavigation": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"properties": {
|
|
161
|
+
"adapter": {
|
|
162
|
+
"enum": [
|
|
163
|
+
"ast-grep",
|
|
164
|
+
"graphify",
|
|
165
|
+
"ripgrep",
|
|
166
|
+
"lsp"
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"noAiSlop": {
|
|
172
|
+
"type": "object",
|
|
173
|
+
"additionalProperties": false,
|
|
174
|
+
"properties": {
|
|
175
|
+
"enabled": {
|
|
176
|
+
"type": "boolean"
|
|
177
|
+
},
|
|
178
|
+
"exclude": {
|
|
179
|
+
"type": "array",
|
|
180
|
+
"items": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"x-relative-path": true,
|
|
183
|
+
"minLength": 1
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"maxEmDashes": {
|
|
187
|
+
"type": "integer",
|
|
188
|
+
"minimum": 0
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"i18n": {
|
|
193
|
+
"type": "object",
|
|
194
|
+
"additionalProperties": false,
|
|
195
|
+
"properties": {
|
|
196
|
+
"enabled": {
|
|
197
|
+
"type": "boolean"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"pwa": {
|
|
202
|
+
"type": "object",
|
|
203
|
+
"additionalProperties": false,
|
|
204
|
+
"properties": {
|
|
205
|
+
"enabled": {
|
|
206
|
+
"type": "boolean"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/lib/slop-patterns.js
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the patterns enforced by check-copy-slop.js.
|
|
5
|
+
* .agents/skills/no-ai-slop/SKILL.md documents the same rules for agents; keep both in sync.
|
|
6
|
+
* Inspired by https://github.com/petergyang/no-ai-slop (MIT License).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Words banned outright. Context-dependent words from the skill (harness, simply,
|
|
10
|
+
// actually, literally, honestly) stay as writing guidance only: they have legitimate
|
|
11
|
+
// technical uses and would produce false positives.
|
|
12
|
+
const BANNED_WORDS = [
|
|
13
|
+
'delve', 'delves', 'delving',
|
|
14
|
+
'foster', 'fosters', 'fostering',
|
|
15
|
+
'leverage', 'leverages', 'leveraged', 'leveraging',
|
|
16
|
+
'utilize', 'utilizes', 'utilized', 'utilizing',
|
|
17
|
+
'empower', 'empowers', 'empowering',
|
|
18
|
+
'streamline', 'streamlines', 'streamlined', 'streamlining',
|
|
19
|
+
'supercharge', 'supercharged', 'supercharging',
|
|
20
|
+
'revolutionize', 'revolutionizes', 'revolutionizing',
|
|
21
|
+
'embark', 'embarking',
|
|
22
|
+
'robust', 'cutting-edge', 'game-changing', 'game changer', 'game-changer',
|
|
23
|
+
'transformative', 'multifaceted', 'meticulous', 'meticulously', 'paramount',
|
|
24
|
+
'ever-evolving', 'world-class', 'seamless', 'seamlessly', 'groundbreaking',
|
|
25
|
+
'tapestry', 'beacon', 'paradigm shift'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// A clause boundary (comma, semicolon or dash) must separate both halves, so ordinary
|
|
29
|
+
// sentences like "it is not cached because it is generated per request" are not reported.
|
|
30
|
+
const BANNED_PATTERNS = [
|
|
31
|
+
{
|
|
32
|
+
name: 'Binary Contrast ("it is not X, it is Y")',
|
|
33
|
+
regex: /\b(?:it['’]?s|it is|this is|that['’]?s|that is)\s+not\b[^.;:!?]{1,60}?[,;—–]\s*(?:but\s+)?(?:it['’]?s|it is|this is|that['’]?s|that is|rather)\b/i
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'Throat-Clearing Opener',
|
|
37
|
+
regex: /\b(?:here['’]?s the thing|it['’]?s worth noting that|it is worth noting that|it is important to (?:note|remember) that)\b/i
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Colon Reveal Hook',
|
|
41
|
+
regex: /\b(?:the best part|here['’]?s the kicker)\s*:/i
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Faux-Insight Hook',
|
|
45
|
+
regex: /\b(?:what nobody tells you)\b/i
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Fake-Profound Conclusion',
|
|
49
|
+
regex: /\b(?:the future is already here|in conclusion)\b/i
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'AI Buzzword',
|
|
53
|
+
regex: new RegExp(`\\b(?:${BANNED_WORDS.map(w => w.replace(/[-\s]/g, '[-\\s]')).join('|')})\\b`, 'i')
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
module.exports = { BANNED_WORDS, BANNED_PATTERNS };
|