Files
get-shit-done/scripts/strip-prose-atrefs.cjs
Tom Boucher a411e08e88 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)
2026-05-05 16:06:29 -04:00

107 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
/**
* strip-prose-atrefs.cjs
*
* Removes redundant @~/.claude/get-shit-done/ path tokens from prose lines
* in <process> and <context> blocks. The path is already declared in
* <execution_context> where it actually loads the file. Prose copies are
* inert and add ~900 tokens/invocation of dead weight.
*
* Transformation rules (applied per matching line):
* - "Execute the X workflow from @PATH end-to-end." → "Execute end-to-end."
* - "Execute @PATH end-to-end." → "Execute end-to-end."
* - "Read and execute the X workflow from @PATH end-to-end." → "Execute end-to-end."
* - "Follow the X workflow at @PATH." → "Execute end-to-end."
* - "Output the X reference from @PATH." → "Execute end-to-end."
* - "**Follow the X** from `@PATH`." → "**Follow the X.**"
* - "- If it is '...': ... from @PATH end-to-end." → strip path token only
* - "- Otherwise: ... from @PATH end-to-end." → strip path token only
* - "- @PATH (label)" → "- (label)"
*
* Run with --dry-run to preview without writing.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const DRY_RUN = process.argv.includes('--dry-run');
const ROOT = path.join(__dirname, '..');
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
const AT_PATH_PATTERN = /@(?:~|\$HOME)\/.+?get-shit-done\/[^\s`\)]+/;
const mkAtRe = () => new RegExp(AT_PATH_PATTERN.source, 'g');
function transformLine(line) {
if (!AT_PATH_PATTERN.test(line)) return line;
const trimmed = line.trim();
// "- @PATH (label)" → "- (label)"
if (/^- @(?:~|\$HOME)\//.test(trimmed)) {
return line.replace(/^(\s*- )@(?:~|\$HOME)\/[^\s(]+\s*/, '$1');
}
// "**Follow the X workflow** from `@PATH`." → "**Follow the X workflow.**"
// "**Follow the X workflow** from `@PATH`" → "**Follow the X workflow.**"
if (/\*\*Follow the .+ workflow\*\* from `@/.test(trimmed)) {
return line.replace(/\s+from `@(?:~|\$HOME)\/[^`]+`\.?/, '.');
}
// Routing bullet: keep everything except "from @PATH" or bare "@PATH"
// "- If …: … from @PATH end-to-end." → strip path, keep bullet
// "- Otherwise: … from @PATH end-to-end." → strip path, keep bullet
if (/^- (?:If |Otherwise:|pass all)/.test(trimmed)) {
return line
.replace(/\s+from\s+@(?:~|\$HOME)\/\S+/g, '')
.replace(/@(?:~|\$HOME)\/\S+/g, '');
}
// "Execute [the X workflow] [from] @PATH [end-to-end]."
// "Read and execute …" / "Follow …" / "Output …"
// → collapse to leading indent + "Execute end-to-end."
const indent = line.match(/^(\s*)/)[1];
return `${indent}Execute end-to-end.`;
}
function processFile(filePath) {
const original = fs.readFileSync(filePath, 'utf-8');
const lines = original.split('\n');
const out = [];
let inProse = false; // true when inside <process> or <context> (not execution_context)
for (const line of lines) {
const t = line.trim();
if (/<(process|context)>/.test(t) && !t.includes('execution_context')) inProse = true;
if (/<\/(process|context)>/.test(t) && !t.includes('execution_context')) inProse = false;
if (inProse && AT_PATH_PATTERN.test(line)) {
const re = mkAtRe();
re.lastIndex = 0;
out.push(transformLine(line));
} else {
out.push(line);
}
}
const result = out.join('\n');
if (result === original) return false; // no change
if (!DRY_RUN) fs.writeFileSync(filePath, result, 'utf-8');
return true;
}
const files = fs.readdirSync(COMMANDS_DIR)
.filter(f => f.endsWith('.md'))
.map(f => path.join(COMMANDS_DIR, f));
let changed = 0;
for (const f of files) {
if (processFile(f)) {
console.log(`${DRY_RUN ? '[dry]' : 'fixed'}: ${path.basename(f)}`);
changed++;
}
}
console.log(`\n${changed} file(s) ${DRY_RUN ? 'would be' : 'were'} modified.`);