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.
Files changed (57) hide show
  1. package/.agents/AGENTS.template.md +59 -0
  2. package/.agents/CONTEXT.template.md +41 -0
  3. package/.agents/ENTRYPOINT.template.md +31 -0
  4. package/.agents/skills/ast-navigator/SKILL.md +31 -0
  5. package/.agents/skills/ast-navigator/adapters/ast-grep.md +18 -0
  6. package/.agents/skills/ast-navigator/adapters/graphify.md +19 -0
  7. package/.agents/skills/ast-navigator/adapters/lsp.md +16 -0
  8. package/.agents/skills/ast-navigator/adapters/ripgrep.md +19 -0
  9. package/.agents/skills/auditor-executor-protocol/SKILL.md +410 -0
  10. package/.agents/skills/auditor-executor-protocol/references/autonomous-mode.md +144 -0
  11. package/.agents/skills/auditor-executor-protocol/references/failure-modes-and-example.md +103 -0
  12. package/.agents/skills/auditor-executor-protocol/references/handoffs.md +133 -0
  13. package/.agents/skills/auditor-executor-protocol/references/tasks-and-gates.md +81 -0
  14. package/.agents/skills/no-ai-slop/LICENSE +21 -0
  15. package/.agents/skills/no-ai-slop/SKILL.md +52 -0
  16. package/.agents/skills/strategic-cto/SKILL.md +54 -0
  17. package/CHANGELOG.md +117 -0
  18. package/LICENSE +21 -0
  19. package/README.md +244 -0
  20. package/docs/SPEC_TEMPLATE.md +78 -0
  21. package/docs/decisions/ADR_TEMPLATE.md +49 -0
  22. package/docs/guidelines/AST_NAVIGATION.md +51 -0
  23. package/docs/guides/AGENT_CREDENTIALS.md +75 -0
  24. package/docs/guides/GITHUB_CLI_SETUP.md +74 -0
  25. package/docs/incidents/0000-00-00-incident-template.md +35 -0
  26. package/docs/roadmap/templates/compliance-log.md +37 -0
  27. package/docs/roadmap/templates/execution-guide.md +75 -0
  28. package/docs/roadmap/templates/plan-of-record.md +49 -0
  29. package/package.json +49 -0
  30. package/scripts/check-copy-slop.js +120 -0
  31. package/scripts/check-file-size.js +66 -0
  32. package/scripts/check-spec.js +201 -0
  33. package/scripts/check-system-prerequisites.js +133 -0
  34. package/scripts/check-versions.js +50 -0
  35. package/scripts/dev/fuzz-spec-markup.js +123 -0
  36. package/scripts/dev/set-npm-publish-token.sh +40 -0
  37. package/scripts/dev/sync-vendored.js +94 -0
  38. package/scripts/install-git-hooks.js +103 -0
  39. package/scripts/lib/cli.js +60 -0
  40. package/scripts/lib/config.js +111 -0
  41. package/scripts/lib/git.js +211 -0
  42. package/scripts/lib/markdown.js +46 -0
  43. package/scripts/lib/provision.js +323 -0
  44. package/scripts/lib/runner.js +70 -0
  45. package/scripts/lib/sdd.config.schema.json +213 -0
  46. package/scripts/lib/slop-patterns.js +57 -0
  47. package/scripts/lib/spec-markup.js +346 -0
  48. package/scripts/lib/spec.js +226 -0
  49. package/scripts/lib/state.js +107 -0
  50. package/scripts/lib/vendor/README.md +11 -0
  51. package/scripts/lib/vendor/markdown-it.LICENSE +22 -0
  52. package/scripts/lib/vendor/markdown-it.min.js +3 -0
  53. package/scripts/quality-gate.js +151 -0
  54. package/scripts/sdd-init.js +245 -0
  55. package/scripts/sdd-verify.js +176 -0
  56. package/scripts/verify-no-secrets.js +216 -0
  57. package/sdd.config.json +33 -0
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * scripts/check-spec.js
5
+ *
6
+ * Verifies that the active specification backs its claims with evidence.
7
+ *
8
+ * Lite mode (docs/SPEC.md):
9
+ * - "**Status:**" is Draft, In Progress, or Completed (the untouched template counts as
10
+ * Draft; anything else is an error).
11
+ * - The "Verification Gate" section exists.
12
+ * - Every checked task has evidence. Evidence recorded by `sdd-verify --task` must be
13
+ * intact (its hash matches) and exit 0. Hand-written evidence is reported as manual,
14
+ * and rejected when specification.requireRecordedEvidence is true.
15
+ * - In Progress / Completed: the verification command and expected output are real.
16
+ * - Completed: every task is checked, and "Last Verified" is a PASS recorded by
17
+ * `sdd-verify --record` whose state fingerprint matches the state the spec was
18
+ * completed in (see lib/state.js).
19
+ *
20
+ * Rigor mode (docs/roadmap/): delegates to `auditkit lint` (>= MIN_AUDITKIT), run on the
21
+ * documents read from the same source as every other check.
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const os = require('os');
26
+ const path = require('path');
27
+ const { spawnSync } = require('child_process');
28
+ const { readFile, readFiles, listTrackedFiles, describeSource, WORKTREE } = require('./lib/git');
29
+ const { loadConfig, getIn } = require('./lib/config');
30
+ const { runCheckCli } = require('./lib/cli');
31
+ const { parseSpec } = require('./lib/spec');
32
+ const { stateOf, referenceSourceFor } = require('./lib/state');
33
+
34
+ const AUDITKIT_INSTALL = 'pipx install git+https://github.com/tBeltty/auditor-executor-protocol';
35
+ const MIN_AUDITKIT = [0, 3, 9];
36
+
37
+ function lintLiteSpec(text, { requireRecordedEvidence = false, expectedState = null, shallow = false } = {}) {
38
+ const spec = parseSpec(text);
39
+ const problems = [];
40
+ const notes = [];
41
+ // An unknown status is reported as a problem and otherwise held to the draft rules.
42
+ const status = spec.status || 'draft';
43
+ const shownStatus = spec.statusProblem && spec.statusProblem.startsWith('Unknown') ? 'unknown' : status;
44
+
45
+ problems.push(...spec.hiddenProblems);
46
+ if (spec.statusProblem) problems.push(spec.statusProblem);
47
+ if (!spec.gate) problems.push('No "Verification Gate" section. Every specification needs one.');
48
+
49
+ let manual = 0;
50
+ for (const task of spec.tasks.filter(t => t.checked)) {
51
+ const { text: evidence, recorded } = task.evidence;
52
+ if (!evidence) {
53
+ const how = task.id.startsWith('line ')
54
+ ? 'Give the task an ID (for example `**T4:**`) and record evidence with sdd-verify --task T4 -- <command>'
55
+ : `Record it with sdd-verify --task ${task.id} -- <command>`;
56
+ problems.push(`${task.id} is checked but has no evidence. ${how}, or paste the command and its output under **Evidence:**.`);
57
+ } else if (recorded) {
58
+ if (!recorded.intact) problems.push(`${task.id}: recorded evidence was edited after sdd-verify wrote it (hash mismatch).`);
59
+ else if (recorded.exit !== '0') problems.push(`${task.id}: recorded evidence shows exit ${recorded.exit}, not 0.`);
60
+ } else {
61
+ manual++;
62
+ if (requireRecordedEvidence) problems.push(`${task.id}: evidence is hand-written; specification.requireRecordedEvidence needs sdd-verify --task ${task.id} -- <command>.`);
63
+ }
64
+ }
65
+ if (manual > 0 && !requireRecordedEvidence) notes.push(`${manual} checked task(s) have hand-written evidence (not recorded by sdd-verify).`);
66
+
67
+ if (spec.gate && (status === 'in progress' || status === 'completed')) {
68
+ if (!spec.gate.command) problems.push(`Status is "${status}" but the verification command is still a placeholder.`);
69
+ if (!spec.gate.expected) problems.push(`Status is "${status}" but the expected output is still a placeholder.`);
70
+ }
71
+ if (status === 'completed') {
72
+ for (const task of spec.tasks.filter(t => !t.checked)) {
73
+ problems.push(`Status is "completed" but ${task.id} is not checked.`);
74
+ }
75
+ const last = spec.gate && spec.gate.lastVerifiedParsed;
76
+ if (!last) {
77
+ problems.push('Status is "completed" but "Last Verified" is missing or was not written by sdd-verify --record.');
78
+ } else if (!last.intact) {
79
+ problems.push('"Last Verified" was edited after sdd-verify wrote it, or the verification command or expected output changed since. Run sdd-verify --record.');
80
+ } else if (last.result !== 'PASS' || last.exit !== '0') {
81
+ problems.push(`Status is "completed" but the last verification is ${last.result} (exit ${last.exit}). Fix it and run sdd-verify --record.`);
82
+ } else if (expectedState && last.state !== expectedState) {
83
+ problems.push(shallow
84
+ ? `The recorded verification (state ${last.state}) does not match the oldest commit in this shallow clone (state ${expectedState}), and the commit that completed the spec may be older. Fetch the full history (git fetch --unshallow; in GitHub Actions, actions/checkout with fetch-depth: 0).`
85
+ : `The recorded verification (state ${last.state}) does not match the content the spec was completed with (state ${expectedState}). Files changed after sdd-verify ran; run it again.`);
86
+ }
87
+ }
88
+ return { status: shownStatus, spec, problems, notes };
89
+ }
90
+
91
+ function parseVersion(text) {
92
+ const match = String(text).match(/(\d+)\.(\d+)\.(\d+)/);
93
+ return match ? match.slice(1).map(Number) : null;
94
+ }
95
+
96
+ function versionAtLeast(actual, minimum) {
97
+ for (let i = 0; i < 3; i++) {
98
+ if (actual[i] !== minimum[i]) return actual[i] > minimum[i];
99
+ }
100
+ return true;
101
+ }
102
+
103
+ function runAuditkit(root, roadmapDir, source) {
104
+ const bin = process.env.SDD_AUDITKIT || 'auditkit';
105
+ const version = spawnSync(bin, ['--version'], { encoding: 'utf8' });
106
+ if (version.error) {
107
+ return { ok: false, report: `❌ Rigor mode is checked by auditkit, which is not installed.\nInstall it with: ${AUDITKIT_INSTALL}` };
108
+ }
109
+ const actual = parseVersion(version.stdout);
110
+ if (!actual || !versionAtLeast(actual, MIN_AUDITKIT)) {
111
+ return {
112
+ ok: false,
113
+ report: `❌ auditkit ${actual ? actual.join('.') : '(unknown version)'} is older than ${MIN_AUDITKIT.join('.')}, which this gate requires.\nUpgrade with: pipx install --force git+https://github.com/tBeltty/auditor-executor-protocol`
114
+ };
115
+ }
116
+
117
+ // auditkit reads files from disk; for the index or a commit, lint a temporary copy.
118
+ let dir = path.join(root, roadmapDir);
119
+ let tmp = null;
120
+ if (source.kind !== 'worktree') {
121
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sdd-roadmap-'));
122
+ const prefix = `${roadmapDir.replace(/\/+$/, '')}/`;
123
+ const files = listTrackedFiles(root, source).filter(f => f.startsWith(prefix));
124
+ for (const [file, buffer] of readFiles(root, files, source)) {
125
+ if (!buffer) continue;
126
+ const target = path.join(tmp, file.slice(prefix.length));
127
+ fs.mkdirSync(path.dirname(target), { recursive: true });
128
+ fs.writeFileSync(target, buffer);
129
+ }
130
+ dir = tmp;
131
+ }
132
+ try {
133
+ const result = spawnSync(bin, ['lint', dir], { cwd: root, encoding: 'utf8' });
134
+ const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
135
+ return {
136
+ ok: result.status === 0,
137
+ report: `${result.status === 0 ? '✅' : '❌'} auditkit lint ${roadmapDir} (${describeSource(source)})\n${output}`
138
+ };
139
+ } finally {
140
+ if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
141
+ }
142
+ }
143
+
144
+ // The framework's own repository has no project specification. The exemption requires both
145
+ // project.type "framework" and the framework's package name, so it cannot be used as a
146
+ // switch to turn the specification check off in a project.
147
+ function isFrameworkRepository(root, source, config) {
148
+ if (getIn(config, 'project.type', 'application') !== 'framework') return false;
149
+ const pkg = readFile(root, 'package.json', source);
150
+ try {
151
+ return Boolean(pkg) && JSON.parse(pkg.toString('utf8')).name === 'agentic-sdd-framework';
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ function run({ root, source = WORKTREE } = {}) {
158
+ const config = loadConfig(root, source);
159
+ if (isFrameworkRepository(root, source, config)) {
160
+ return { ok: true, report: '⏭️ Not applicable (the framework repository has no project specification).' };
161
+ }
162
+
163
+ if (getIn(config, 'specification.mode', 'lite') === 'rigor') {
164
+ const roadmapDir = getIn(config, 'specification.roadmapDir', 'docs/roadmap');
165
+ if (!readFile(root, `${roadmapDir}/execution-guide.md`, source)) {
166
+ return { ok: false, report: `❌ Rigor mode: ${roadmapDir}/execution-guide.md not found. Run sdd-init --mode=rigor.` };
167
+ }
168
+ return runAuditkit(root, roadmapDir, source);
169
+ }
170
+
171
+ const specFile = getIn(config, 'specification.specFile', 'docs/SPEC.md');
172
+ const buffer = readFile(root, specFile, source);
173
+ if (!buffer) {
174
+ return { ok: false, report: `❌ Lite mode: ${specFile} not found. Run sdd-init or create it from docs/SPEC_TEMPLATE.md.` };
175
+ }
176
+ const text = buffer.toString('utf8');
177
+ const completed = parseSpec(text).status === 'completed';
178
+ const reference = completed ? referenceSourceFor(root, source, specFile) : null;
179
+ const expectedState = reference ? stateOf(root, reference, specFile) : null;
180
+ const { status, spec, problems, notes } = lintLiteSpec(text, {
181
+ requireRecordedEvidence: getIn(config, 'specification.requireRecordedEvidence', false),
182
+ expectedState,
183
+ shallow: Boolean(reference && reference.shallow)
184
+ });
185
+ const done = spec.tasks.filter(t => t.checked).length;
186
+ const summary = `${specFile}: status "${status}", ${done}/${spec.tasks.length} task(s) checked`;
187
+ const noteLines = notes.map(n => ` ℹ️ ${n}`);
188
+ if (problems.length === 0) {
189
+ return { ok: true, report: [`✅ ${summary}.`, ...noteLines].join('\n') };
190
+ }
191
+ return {
192
+ ok: false,
193
+ report: [`❌ ${summary}, ${problems.length} problem(s):\n`, ...problems.map(p => ` - ${p}`), ...noteLines].join('\n')
194
+ };
195
+ }
196
+
197
+ if (require.main === module) {
198
+ runCheckCli('📋 Agentic SDD Framework: Specification Check', run);
199
+ }
200
+
201
+ module.exports = { MIN_AUDITKIT, lintLiteSpec, parseVersion, versionAtLeast, run };
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * scripts/check-system-prerequisites.js
5
+ *
6
+ * Day-0 System Diagnostics Validator
7
+ * Verifies that the local environment meets the minimum prerequisites for
8
+ * Spec-Driven Development (SDD) with AI agents.
9
+ */
10
+
11
+ const { execSync } = require('child_process');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ // Oldest Node.js release line still maintained upstream.
17
+ const MIN_NODE_MAJOR = 22;
18
+
19
+ function runCommand(command) {
20
+ try {
21
+ return { success: true, output: execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() };
22
+ } catch (error) {
23
+ return { success: false, error: error.message, stderr: error.stderr ? error.stderr.toString().trim() : '' };
24
+ }
25
+ }
26
+
27
+ // Collects the diagnostics. env and homedir are injectable for tests.
28
+ function collectChecks({ env = process.env, homedir = os.homedir(), nodeVersion = process.version } = {}) {
29
+ const checks = [];
30
+
31
+ // 1. Check Node.js Version
32
+ const majorVersion = parseInt(nodeVersion.replace(/^v/, '').split('.')[0], 10);
33
+ if (majorVersion >= MIN_NODE_MAJOR) {
34
+ checks.push({ name: 'Node.js Runtime', status: 'PASS', detail: `Version ${nodeVersion} (>= ${MIN_NODE_MAJOR} required)` });
35
+ } else {
36
+ checks.push({ name: 'Node.js Runtime', status: 'FAIL', detail: `Version ${nodeVersion} detected. Node.js ${MIN_NODE_MAJOR} LTS or newer is required.` });
37
+ }
38
+
39
+ // 2. Check Git Installation
40
+ const gitCheck = runCommand('git --version');
41
+ if (gitCheck.success) {
42
+ checks.push({ name: 'Git Binary', status: 'PASS', detail: gitCheck.output });
43
+ } else {
44
+ checks.push({ name: 'Git Binary', status: 'FAIL', detail: 'Git is not installed or not in PATH.' });
45
+ }
46
+
47
+ // 3. Check Git Identity (user.name and user.email)
48
+ const isCI = env.CI === 'true' || env.GITHUB_ACTIONS === 'true';
49
+ const gitNameCheck = runCommand('git config --get user.name');
50
+ const gitEmailCheck = runCommand('git config --get user.email');
51
+ if (gitNameCheck.success && gitEmailCheck.success && gitNameCheck.output && gitEmailCheck.output) {
52
+ checks.push({ name: 'Git Identity', status: 'PASS', detail: `${gitNameCheck.output} <${gitEmailCheck.output}>` });
53
+ } else if (isCI) {
54
+ checks.push({ name: 'Git Identity', status: 'PASS', detail: 'CI Runner environment (committer identity not required for builds)' });
55
+ } else {
56
+ checks.push({
57
+ name: 'Git Identity',
58
+ status: 'FAIL',
59
+ detail: 'git config user.name or user.email is missing. Run: git config --global user.name "Your Name" && git config --global user.email "your@email.com"'
60
+ });
61
+ }
62
+
63
+ // 4. Check GitHub CLI (gh)
64
+ const ghVersionCheck = runCommand('gh --version');
65
+ if (ghVersionCheck.success) {
66
+ const firstLine = ghVersionCheck.output.split('\n')[0];
67
+ const ghAuthCheck = runCommand('gh auth status');
68
+ if (ghAuthCheck.success) {
69
+ checks.push({ name: 'GitHub CLI (gh)', status: 'PASS', detail: `${firstLine} (Authenticated)` });
70
+ } else {
71
+ checks.push({
72
+ name: 'GitHub CLI (gh)',
73
+ status: 'WARN',
74
+ detail: `${firstLine} installed, but not authenticated. Run: gh auth login`
75
+ });
76
+ }
77
+ } else {
78
+ checks.push({
79
+ name: 'GitHub CLI (gh)',
80
+ status: 'WARN',
81
+ detail: 'gh CLI is not installed. Recommended for automated repository provisioning. See: https://cli.github.com'
82
+ });
83
+ }
84
+
85
+ // 5. Check SSH Keys for secure Git operations
86
+ const sshDir = path.join(homedir, '.ssh');
87
+ const commonKeys = ['id_ed25519', 'id_rsa', 'id_ecdsa'];
88
+ const foundKeys = commonKeys.filter(k => fs.existsSync(path.join(sshDir, k)));
89
+ if (foundKeys.length > 0) {
90
+ checks.push({ name: 'SSH Keys', status: 'PASS', detail: `Detected: ${foundKeys.join(', ')} in ~/.ssh/` });
91
+ } else {
92
+ checks.push({
93
+ name: 'SSH Keys',
94
+ status: 'WARN',
95
+ detail: 'No standard SSH private keys found in ~/.ssh/. HTTPS credentials or token auth will be required.'
96
+ });
97
+ }
98
+ return checks;
99
+ }
100
+
101
+ function main() {
102
+ const checks = collectChecks();
103
+ console.log('\n======================================================');
104
+ console.log(' 🔍 Agentic SDD Framework: Day-0 Environment Check');
105
+ console.log('======================================================\n');
106
+
107
+ let hasFailure = false;
108
+ checks.forEach(check => {
109
+ let icon = '✅';
110
+ if (check.status === 'FAIL') {
111
+ icon = '❌';
112
+ hasFailure = true;
113
+ } else if (check.status === 'WARN') {
114
+ icon = '⚠️ ';
115
+ }
116
+ console.log(`${icon} [${check.status}] ${check.name}`);
117
+ console.log(` ${check.detail}\n`);
118
+ });
119
+
120
+ if (hasFailure) {
121
+ console.log('❌ Environment diagnostic FAILED. Please resolve the critical issues above.\n');
122
+ process.exit(1);
123
+ } else {
124
+ console.log('✅ Environment diagnostic PASSED. System is ready for SDD bootstrapping.\n');
125
+ process.exit(0);
126
+ }
127
+ }
128
+
129
+ if (require.main === module) {
130
+ main();
131
+ }
132
+
133
+ module.exports = { MIN_NODE_MAJOR, collectChecks };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * scripts/check-versions.js
5
+ *
6
+ * Version Sync Checker for the framework repository itself.
7
+ * When sdd.config.json declares project.type "framework", package.json and
8
+ * sdd.config.json must carry the same version. In projects bootstrapped by sdd-init,
9
+ * sdd.config.json "version" records the framework version that generated it and is
10
+ * unrelated to the project's own version, so the check does not apply.
11
+ */
12
+
13
+ const { readFile, WORKTREE } = require('./lib/git');
14
+ const { parseConfig, getIn } = require('./lib/config');
15
+ const { runCheckCli } = require('./lib/cli');
16
+
17
+ function run({ root, source = WORKTREE } = {}) {
18
+ const configBuffer = readFile(root, 'sdd.config.json', source);
19
+ if (!configBuffer) {
20
+ return { ok: false, report: '❌ sdd.config.json not found. Run sdd-init first.' };
21
+ }
22
+ const config = parseConfig(configBuffer.toString('utf8'));
23
+ const type = getIn(config, 'project.type', 'application');
24
+ if (type !== 'framework') {
25
+ return { ok: true, report: `⏭️ Not applicable (project.type is "${type}"; only the framework repository syncs versions).` };
26
+ }
27
+
28
+ const packageBuffer = readFile(root, 'package.json', source);
29
+ if (!packageBuffer) {
30
+ return { ok: false, report: '❌ package.json not found.' };
31
+ }
32
+ const pkgVersion = JSON.parse(packageBuffer.toString('utf8')).version;
33
+ const configVersion = config.version;
34
+ const summary = ` - package.json: ${pkgVersion}\n - sdd.config.json: ${configVersion}\n\n`;
35
+
36
+ if (pkgVersion !== configVersion) {
37
+ return {
38
+ ok: false,
39
+ report: summary + `❌ Version mismatch: package.json (${pkgVersion}) !== sdd.config.json (${configVersion}).\n` +
40
+ 'Action: Synchronize version strings before committing.'
41
+ };
42
+ }
43
+ return { ok: true, report: summary + '✅ Version manifests are in sync.' };
44
+ }
45
+
46
+ if (require.main === module) {
47
+ runCheckCli('🏷️ Agentic SDD Framework: Version Sync Check', run);
48
+ }
49
+
50
+ module.exports = { run };
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * scripts/dev/fuzz-spec-markup.js
5
+ *
6
+ * Differential fuzz for the Lite spec parser against cmark-gfm, the renderer GitHub uses.
7
+ * It generates random specs from risky Markdown (fences on list markers, tabs, comments,
8
+ * raw HTML, lazy lines, definitions, entities, look-alike and invisible characters, Status
9
+ * variants), and for every spec the parser accepts (no problems) compares its reading with
10
+ * the cmark-gfm rendering. A bypass is a rendered unchecked task the parser does not see, a
11
+ * rendered Status different from the parsed one, or a gate command different from the
12
+ * rendered one.
13
+ *
14
+ * python3 -m venv /tmp/fuzz-venv && /tmp/fuzz-venv/bin/pip install cmarkgfm
15
+ * FUZZ_PYTHON=/tmp/fuzz-venv/bin/python node scripts/dev/fuzz-spec-markup.js [cases]
16
+ *
17
+ * Exits 1 on any bypass.
18
+ */
19
+
20
+ const { spawnSync } = require('child_process');
21
+
22
+ const PYTHON = process.env.FUZZ_PYTHON || 'python3';
23
+ const RENDER = `
24
+ import json, sys
25
+ import cmarkgfm
26
+ from cmarkgfm.cmark import Options
27
+ opts = Options.CMARK_OPT_UNSAFE | Options.CMARK_OPT_FOOTNOTES
28
+ print(json.dumps([cmarkgfm.github_flavored_markdown_to_html(t, options=opts) for t in json.load(sys.stdin)]))
29
+ `;
30
+ // Renders a batch of documents with cmark-gfm in one Python process.
31
+ function renderAll(texts) {
32
+ const result = spawnSync(PYTHON, ['-c', RENDER], { input: JSON.stringify(texts), encoding: 'utf8', maxBuffer: 1 << 30 });
33
+ if (result.status !== 0) {
34
+ console.error(`cmark-gfm is needed (a dev-only tool, not a framework dependency):\n python3 -m pip install cmarkgfm\n${result.stderr}`);
35
+ process.exit(2);
36
+ }
37
+ return JSON.parse(result.stdout);
38
+ }
39
+ const { parseSpec } = require('../lib/spec');
40
+ const { skeleton } = require('../lib/spec-markup');
41
+ const rnd = n => Math.floor(Math.random() * n);
42
+ const BODY = [
43
+ '**Status:** Draft', '**Status:** Completed', '* [ ] **T2:** open', '* [x] **T3:** done', ' * [ ] **T4:** nested',
44
+ '1. [ ] **T5:** num', ' * **Evidence:** ok', '```', ' ```', '~~~', ' ~~~', ' ~~~', '', '', 'plain text', 'lazy line',
45
+ ' indented two', '> quoted', '<!-- x -->', '<!--', '-->', ' <!--', 'a `b` c', '* ~~~', '1. ~~~', '* ```', '> ~~~',
46
+ '* > ~~~', ' * ~~~', '- ~~~', '+ ```', '> * ~~~', '* [ ] ~~~', ' echo A', ' echo B', 'echo C', ' ```', '````',
47
+ '* [x] **T6:** x\n ~~~', '\t~~~', '* \t~~~', '1) ~~~',
48
+ '[n](https://e.com "t', 'end")', '[n](https://e.com "t")', '**Sta&#116;us:** Completed', '**Sta\u200Btus:** Completed',
49
+ '**\u0405tatus:** Completed', '\u00A0', ' \u00A0', '> x', '> * [ ] **T7:** q', ' > * [ ] **T8:** q', '[d]: https://e.com',
50
+ '"title"', '[ci]: https://e.com "**Verification Command:**"', '**Status**: Completed', '**Status:**Completed', '&nbsp;',
51
+ '<!-->', '<!--->', '\t<!--', '\t-->', 'notes <!--x@y_z>', '<https://e.com>', '<a@b.co>', '[ref', ']: https://e.com',
52
+ '[^n]: note', '[^n]', '1. **Status:** Draft', '1. **Verification Command:**', ' ```bash', ' echo D', ' ```',
53
+ '**Status** Completed', '__Status:__ Draft', 'Status codes follow RFC 9110', ' rm -rf build', '__Verification Command:__',
54
+ '```\f', '~~~\v', ' ```\f', '* [a]: /u "', '1. [a]: /u "', '"', 'Tracking: <https://e.com/x`y> <!-- `', ' npm test',
55
+ '* [^n]: note', '> [^n]: note',
56
+ // Wide markers ("1. ", "10. ") widen the gap between 4 spaces and the item's own
57
+ // content column, where cmark-gfm and markdown-it can read a ">"/"#"/fence line
58
+ // differently (lazy continuation vs. indented code): R45, R46.
59
+ '1. [x] **T9:** wide marker', '10. [ ] **T9:** wide marker', ' > shallow quote', ' # shallow heading',
60
+ ' ```', ' ~~~', ' * [ ] **T9:** deep nested',
61
+ ];
62
+ const SAFE = ['**Status:** Draft', '* [ ] **T2:** open', '* [x] **T3:** done', ' * [ ] **T4:** nested', '', 'plain text',
63
+ ' ```bash', ' echo A', ' ```', ' ~~~', ' echo B', '<!-- x -->', 'a `b` c', ' indented two'];
64
+ const pick = () => (Math.random() < 0.8 ? SAFE[rnd(SAFE.length)] : BODY[rnd(BODY.length)]);
65
+ function renderedView(html) {
66
+ const noCode = html.replace(/<pre>[\s\S]*?<\/pre>/g, '').replace(/<code>[\s\S]*?<\/code>/g, '');
67
+ const tasks = [...noCode.matchAll(/<input type="checkbox"( checked="")? disabled="" \/>/g)].map(m => Boolean(m[1]));
68
+ // Every rendered block that reads as a Status line: "Status" followed by a colon, or
69
+ // emphasized, at the start of the block, however it is spelled.
70
+ const decode = t => t.replace(/<[^>]+>/g, '').replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
71
+ const statuses = [];
72
+ for (const block of noCode.split(/<\/?(?:p|li|h[1-6]|blockquote|ul|ol|td|th)[^>]*>|<br\s*\/?>|\n/)) {
73
+ const clean = block.replace(/^\s*<input[^>]*>\s*/, '');
74
+ const text = skeleton(decode(clean));
75
+ const emphasized = clean.trim().match(/^<(strong|em|del)>([\s\S]*?)<\/\1>/);
76
+ if (text.startsWith('status:') || (emphasized && skeleton(decode(emphasized[2])).startsWith('status'))) {
77
+ statuses.push(text.slice(text.indexOf(':') + 1) || text.slice('status'.length));
78
+ }
79
+ }
80
+ let command = null;
81
+ const label = html.match(/<li>\s*(?:<p>)?<strong>Verification Command:<\/strong>/);
82
+ if (label) {
83
+ const m = html.slice(label.index).match(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
84
+ if (m) command = m[1].replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&').trim();
85
+ }
86
+ return { tasks, statuses, command };
87
+ }
88
+
89
+ const N = Number(process.argv[2] || 100000);
90
+ const BATCH = 5000;
91
+ let accepted = 0;
92
+ let bad = 0;
93
+ for (let done = 0; done < N; done += BATCH) {
94
+ const cases = [];
95
+ for (let k = 0; k < Math.min(BATCH, N - done); k++) {
96
+ const body = Array.from({ length: 3 + rnd(10) }, pick);
97
+ const gate = ['## 4. Verification Gate', '', '* **Verification Command:**', ...Array.from({ length: 2 + rnd(8) }, pick),
98
+ '* **Expected Output:**', ' ```text', ' ok', ' ```'];
99
+ const text = ['# Spec', '', ...body, '', ...gate].join('\n') + '\n';
100
+ const spec = parseSpec(text);
101
+ if (spec.hiddenProblems.length === 0 && !spec.statusProblem) cases.push({ text, spec });
102
+ }
103
+ const html = renderAll(cases.map(c => c.text));
104
+ cases.forEach(({ text, spec }, k) => {
105
+ accepted++;
106
+ const r = renderedView(html[k]);
107
+ const pUnchecked = spec.tasks.filter(t => !t.checked).length;
108
+ const rUnchecked = r.tasks.filter(c => !c).length;
109
+ const rStatus = r.statuses.length === 1 ? r.statuses[0] : null;
110
+ const pStatus = spec.status ? skeleton(spec.status) : null;
111
+ const pCommand = spec.gate && spec.gate.command ? spec.gate.command.trim() : null;
112
+ const problems = [];
113
+ if (rUnchecked > pUnchecked) problems.push('hidden unchecked task');
114
+ if (r.statuses.length > 1 || (rStatus !== null && rStatus !== pStatus)) problems.push(`status p=${pStatus} r=${r.statuses.join('|')}`);
115
+ if (pCommand !== null && pCommand !== r.command) problems.push(`command p=${JSON.stringify(pCommand)} r=${JSON.stringify(r.command)}`);
116
+ if (problems.length) {
117
+ bad++;
118
+ if (bad <= 4) console.log('--- BYPASS', problems.join('; '), '\n' + JSON.stringify(text));
119
+ }
120
+ });
121
+ }
122
+ console.log(`cases ${N}, accepted ${accepted}, bypasses ${bad}`);
123
+ process.exitCode = bad === 0 ? 0 : 1;
@@ -0,0 +1,40 @@
1
+ #!/bin/sh
2
+ # scripts/dev/set-npm-publish-token.sh
3
+ #
4
+ # One-time setup: stores an npm publish token as the GitHub Actions secret
5
+ # NPM_TOKEN on this repository, so the release workflow can publish to npm
6
+ # without anyone's npm password or token ever appearing in chat, a commit,
7
+ # or this terminal's scrollback.
8
+ #
9
+ # The token is read with hidden input and piped straight to `gh secret set`
10
+ # via stdin; it is never written to disk and never echoed back.
11
+ #
12
+ # Prerequisite: create the token yourself at
13
+ # https://www.npmjs.com/settings/<your-username>/tokens
14
+ # Choose "Automation" (Classic Token), or a Granular Access Token scoped to
15
+ # read/write on this package — CI has no interactive 2FA prompt, so a token
16
+ # type that skips it is required.
17
+
18
+ set -eu
19
+
20
+ REPO="tBeltty/agentic-sdd-framework"
21
+
22
+ command -v gh >/dev/null 2>&1 || { echo "GitHub CLI (gh) is required. Install it, then run this again." >&2; exit 1; }
23
+ gh auth status >/dev/null 2>&1 || { echo "Run 'gh auth login' first, then run this again." >&2; exit 1; }
24
+
25
+ echo "This will set the NPM_TOKEN secret on: $REPO"
26
+ printf "npm token (input hidden): "
27
+ stty -echo
28
+ IFS= read -r NPM_TOKEN
29
+ stty echo
30
+ echo
31
+
32
+ if [ -z "$NPM_TOKEN" ]; then
33
+ echo "No token entered; nothing was set." >&2
34
+ exit 1
35
+ fi
36
+
37
+ printf '%s' "$NPM_TOKEN" | gh secret set NPM_TOKEN --repo "$REPO" --body-file -
38
+ unset NPM_TOKEN
39
+
40
+ echo "Done: NPM_TOKEN is set on $REPO. The value was not saved anywhere and was not shown."
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * scripts/dev/sync-vendored.js
5
+ *
6
+ * Keeps the files vendored from tBeltty/auditor-executor-protocol identical to their
7
+ * canonical copies: the protocol skill (SKILL.md and references/) and the Rigor
8
+ * templates used by `auditkit init`. Framework-maintenance only; not installed into
9
+ * projects.
10
+ *
11
+ * Usage:
12
+ * node scripts/dev/sync-vendored.js <canonical-checkout> copy canonical -> framework
13
+ * node scripts/dev/sync-vendored.js <canonical-checkout> --check exit 1 on any difference
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const FRAMEWORK_ROOT = path.resolve(__dirname, '..', '..');
20
+
21
+ // Checkouts may use CRLF (Windows with core.autocrlf); compare and write LF content.
22
+ const readNormalized = file => fs.readFileSync(file, 'utf8').replace(/\r\n?/g, '\n');
23
+ const SKILL_DIR = '.agents/skills/auditor-executor-protocol';
24
+ const TEMPLATE_DIR = 'docs/roadmap/templates';
25
+
26
+ function mappings(canonical) {
27
+ const pairs = [[path.join(canonical, 'SKILL.md'), path.join(SKILL_DIR, 'SKILL.md')]];
28
+ for (const name of fs.readdirSync(path.join(canonical, 'references')).filter(f => f.endsWith('.md')).sort()) {
29
+ pairs.push([path.join(canonical, 'references', name), path.join(SKILL_DIR, 'references', name)]);
30
+ }
31
+ for (const name of ['plan-of-record.md', 'execution-guide.md', 'compliance-log.md']) {
32
+ pairs.push([path.join(canonical, 'src/auditkit/templates', name), path.join(TEMPLATE_DIR, name)]);
33
+ }
34
+ return pairs;
35
+ }
36
+
37
+ // Vendored files that no longer exist upstream.
38
+ function orphans(canonical) {
39
+ const upstream = new Set(fs.readdirSync(path.join(canonical, 'references')));
40
+ const local = path.join(FRAMEWORK_ROOT, SKILL_DIR, 'references');
41
+ return fs.existsSync(local)
42
+ ? fs.readdirSync(local).filter(f => !upstream.has(f)).map(f => path.join(SKILL_DIR, 'references', f))
43
+ : [];
44
+ }
45
+
46
+ function main() {
47
+ const usage = 'Usage: node scripts/dev/sync-vendored.js <canonical-checkout> [--check]';
48
+ const args = process.argv.slice(2);
49
+ const flags = args.filter(a => a.startsWith('-'));
50
+ const positional = args.filter(a => !a.startsWith('-'));
51
+ // A mistyped flag must not turn a check into a write.
52
+ const unknown = flags.filter(f => f !== '--check');
53
+ if (unknown.length > 0 || positional.length !== 1) {
54
+ console.error(unknown.length > 0 ? `Unknown argument "${unknown[0]}". ${usage}` : usage);
55
+ process.exit(2);
56
+ }
57
+ const canonical = path.resolve(positional[0]);
58
+ if (!fs.existsSync(path.join(canonical, 'SKILL.md'))) {
59
+ console.error(`${canonical} is not a checkout of the protocol repository (no SKILL.md). ${usage}`);
60
+ process.exit(2);
61
+ }
62
+ const check = flags.includes('--check');
63
+ const drift = [];
64
+
65
+ for (const [src, rel] of mappings(canonical)) {
66
+ const dest = path.join(FRAMEWORK_ROOT, rel);
67
+ const upstream = readNormalized(src);
68
+ const same = fs.existsSync(dest) && readNormalized(dest) === upstream;
69
+ if (same) continue;
70
+ drift.push(rel.split(path.sep).join('/'));
71
+ if (!check) {
72
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
73
+ fs.writeFileSync(dest, upstream);
74
+ }
75
+ }
76
+ for (const rel of orphans(canonical)) {
77
+ drift.push(`${rel.split(path.sep).join('/')} (removed upstream)`);
78
+ if (!check) fs.rmSync(path.join(FRAMEWORK_ROOT, rel));
79
+ }
80
+
81
+ if (drift.length === 0) {
82
+ console.log('✅ Vendored protocol files match the canonical repository.');
83
+ return;
84
+ }
85
+ const verb = check ? 'differ from' : 'updated from';
86
+ console.log(`${check ? '❌' : '🔄'} ${drift.length} vendored file(s) ${verb} the canonical repository:`);
87
+ for (const rel of drift) console.log(` - ${rel}`);
88
+ if (check) {
89
+ console.log('\nRun: node scripts/dev/sync-vendored.js <canonical-checkout>');
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ main();