fix(coderabbit): resolve all 12 findings on PR #3152
MAJOR (security/correctness): - commands/gsd/debug.md: add Write to allowed-tools (session file creation requires it — workflow explicitly says 'use Write tool, never heredoc') - workflows/debug.md: add SLUG sanitization guard to steps 1b+1c (status/ continue subcommands used raw user input in file paths — path traversal) - workflows/thread.md: sanitize $ARGUMENTS in RESUME mode before file path construction (was bypassing the sanitization guard in CLOSE/STATUS modes) MINOR (consistency/correctness): - docs/INVENTORY-MANIFEST.json: remove stale top-level 'workflows' array (duplicate of families.workflows introduced in earlier update) - commands/gsd/resume-work.md: normalize process to 'Execute end-to-end.' - commands/gsd/settings.md: normalize process to 'Execute end-to-end.' - commands/gsd/update.md: normalize otherwise branch to 'execute end-to-end.' - docs/adr/0002: add Status: Accepted + Date header (ADR convention) - workflows/extract-learnings.md: rename step extract_learnings → extract-learnings - tests/extract-learnings.test.cjs: tighten step-name assertion to exact name ARCHITECTURE: - scripts/command-contract-helpers.cjs: extract CANONICAL_TOOLS, parseFrontmatter, executionContextRefs as shared module — single source of truth consumed by both lint script and test suite (prevents silent lint/test disagreement) - scripts/lint-command-contract.cjs: require() helpers instead of duplicating - tests/command-contract.test.cjs: require() helpers; move readFileSync calls inside test() callbacks (registration-time throws surface as named failures)
This commit is contained in:
@@ -27,53 +27,11 @@ const ROOT = path.join(__dirname, '..');
|
||||
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
|
||||
const GSD_ROOT = path.join(ROOT, 'get-shit-done');
|
||||
|
||||
const CANONICAL_TOOLS = new Set([
|
||||
'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep',
|
||||
'Task', 'Agent', 'Skill', 'SlashCommand',
|
||||
'AskUserQuestion', 'WebFetch', 'WebSearch', 'TodoWrite',
|
||||
'mcp__context7__resolve-library-id',
|
||||
'mcp__context7__query-docs',
|
||||
'mcp__context7__*',
|
||||
]);
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function parseFrontmatter(content) {
|
||||
const lines = content.split('\n');
|
||||
if (lines[0].trim() !== '---') return {};
|
||||
const end = lines.indexOf('---', 1);
|
||||
if (end === -1) return {};
|
||||
const fm = {};
|
||||
let key = null;
|
||||
for (const line of lines.slice(1, end)) {
|
||||
const kv = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)/);
|
||||
if (kv) { key = kv[1]; fm[key] = kv[2].trim(); }
|
||||
else if (key && line.match(/^\s+-\s+/)) {
|
||||
const val = line.replace(/^\s+-\s+/, '').trim();
|
||||
fm[key] = fm[key] ? fm[key] + '\n' + val : val;
|
||||
}
|
||||
}
|
||||
return fm;
|
||||
}
|
||||
|
||||
function executionContextRefs(content) {
|
||||
const refs = [];
|
||||
const re = /<execution_context(?:_extended)?>([\s\S]*?)<\/execution_context(?:_extended)?>/g;
|
||||
let m;
|
||||
while ((m = re.exec(content)) !== null) {
|
||||
for (const rawLine of m[1].split('\n')) {
|
||||
const line = rawLine.trim();
|
||||
if (!line.startsWith('@')) continue;
|
||||
const token = line.split(/\s+/)[0];
|
||||
const trailingProse = line.length > token.length;
|
||||
const normalized = token
|
||||
.replace(/^@(?:~|\$HOME)\//, '')
|
||||
.replace(/^(?:\.claude\/)?(?:get-shit-done\/)?/, '');
|
||||
refs.push({ token, normalized, trailingProse });
|
||||
}
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
const {
|
||||
CANONICAL_TOOLS,
|
||||
parseFrontmatter,
|
||||
executionContextRefs,
|
||||
} = require('../scripts/command-contract-helpers.cjs');
|
||||
|
||||
const commandFiles = fs
|
||||
.readdirSync(COMMANDS_DIR)
|
||||
@@ -128,27 +86,23 @@ describe('command contract: allowed-tools (ADR-0002)', () => {
|
||||
|
||||
describe('command contract: execution_context @-refs resolve (ADR-0002)', () => {
|
||||
for (const { name, full } of commandFiles) {
|
||||
const content = fs.readFileSync(full, 'utf-8');
|
||||
const refs = executionContextRefs(content);
|
||||
if (refs.length === 0) continue;
|
||||
for (const { token, normalized } of refs) {
|
||||
test(`${name}: @-ref "${normalized}" exists on disk`, () => {
|
||||
test(`${name}: all execution_context @-refs exist on disk`, () => {
|
||||
const refs = executionContextRefs(fs.readFileSync(full, 'utf-8'));
|
||||
for (const { normalized } of refs) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(GSD_ROOT, normalized)),
|
||||
`${name}: execution_context @-ref "${normalized}" does not exist — ` +
|
||||
'create the file or remove the reference',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('command contract: execution_context @-refs on own line (ADR-0002)', () => {
|
||||
for (const { name, full } of commandFiles) {
|
||||
const content = fs.readFileSync(full, 'utf-8');
|
||||
const refs = executionContextRefs(content);
|
||||
if (refs.length === 0) continue;
|
||||
test(`${name}: no @-refs with trailing prose in execution_context`, () => {
|
||||
const refs = executionContextRefs(fs.readFileSync(full, 'utf-8'));
|
||||
const bad = refs.filter(r => r.trailingProse);
|
||||
assert.equal(
|
||||
bad.length, 0,
|
||||
|
||||
Reference in New Issue
Block a user