Install eslint@9 + typescript-eslint@8 + globals@16 + eslint-plugin-n@17 + eslint-plugin-no-only-tests@3 + typescript as devDependencies. eslint.config.mjs (flat config): - Global ignores: node_modules, dist, .worktrees, .claude, coverage, the 12 generated get-shit-done/bin/lib/*.cjs files - Block for get-shit-done/bin/**/*.cjs + scripts/**/*.cjs: js.recommended + eslint-plugin-n + local plugin; generic quality rules (no-var, prefer-const, no-unused-vars, no-empty, n/no-process-exit) - Block for tests/**/*.test.cjs: no-only-tests (error), local timing rules, no-restricted-syntax timing bans eslint-rules/ local plugin (three AST rules, all at warn pending cleanup): - no-source-grep: flag readFileSync on source .cjs/.js/.ts + text methods - no-magic-sleep-in-tests: flag Atomics.wait and await-new-Promise(setTimeout) - no-elapsed-assertion: flag assert*() on timing props (elapsed/duration/took/ms) tsconfig.lint.json: allowJs + checkJs + noEmit for future type-aware passes. tests/eslint-rules.test.cjs: 15 RuleTester unit tests (all pass, 0 fail). package.json: add lint/lint:fix scripts; remove lint:tests (subsumed by ESLint local/no-source-grep). Rules that produced pre-existing errors downgraded to warn: no-useless-escape, no-unsafe-finally, no-regex-spaces, no-control-regex, no-irregular-whitespace. ESLint exits 0 (warnings ok). .github/workflows/test.yml lint-tests job: add npm ci + ESLint step; remove "Lint — no source-grep tests" step (now covered by ESLint); bump timeout 3→5 min. .gitignore: add node_modules/.cache/eslint/ entry. eslint --fix auto-cleaned: no-regex-spaces in tests, prefer-const in state.cjs, redundant eslint-disable-next-line comments. Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
# Runs once, not per matrix lane.
|
||||
lint-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -94,10 +94,12 @@ jobs:
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Install dev dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
- name: Lint — ESLint (source-grep + timing + no-only-tests + quality)
|
||||
run: npx eslint . --cache --cache-location node_modules/.cache/eslint/
|
||||
- name: Lint — skill dependency graph
|
||||
run: npm run lint:skill-deps
|
||||
- name: Lint — no source-grep tests
|
||||
run: node scripts/lint-no-source-grep.cjs
|
||||
- name: Lint — test file count per module
|
||||
run: node scripts/lint-test-file-count.cjs
|
||||
- name: Lint — command contract (ADR-0002)
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
|
||||
# ESLint cache
|
||||
node_modules/.cache/eslint/
|
||||
TO-DOS.md
|
||||
CLAUDE.md
|
||||
/research.claude/
|
||||
|
||||
103
eslint-rules/no-elapsed-assertion.cjs
Normal file
103
eslint-rules/no-elapsed-assertion.cjs
Normal file
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* no-elapsed-assertion
|
||||
*
|
||||
* Flag assert*() calls whose argument reads a property named
|
||||
* /^(elapsed|duration|took|ms)$/ or compares such an identifier.
|
||||
* Timing assertions are flaky and should not be in the test suite.
|
||||
*/
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const rule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow timing assertions (elapsed, duration, took, ms) in assert calls',
|
||||
category: 'Best Practices',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noElapsedAssertion:
|
||||
'Timing assertion detected: assert*() on a timing property (elapsed/duration/took/ms). Timing assertions are flaky — assert on observable behavior instead.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const TIMING_PROPS = /^(elapsed|duration|took|ms)$/;
|
||||
|
||||
function containsTimingRef(node) {
|
||||
if (!node) return false;
|
||||
|
||||
// foo.elapsed, foo.duration, foo.took, foo.ms
|
||||
if (
|
||||
node.type === 'MemberExpression' &&
|
||||
node.property.type === 'Identifier' &&
|
||||
TIMING_PROPS.test(node.property.name)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Identifier directly: elapsed, duration, took, ms
|
||||
if (node.type === 'Identifier' && TIMING_PROPS.test(node.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Binary expression: elapsed > 100, duration <= 500, etc.
|
||||
if (node.type === 'BinaryExpression') {
|
||||
return containsTimingRef(node.left) || containsTimingRef(node.right);
|
||||
}
|
||||
|
||||
// Logical expression: elapsed && elapsed > 0
|
||||
if (node.type === 'LogicalExpression') {
|
||||
return containsTimingRef(node.left) || containsTimingRef(node.right);
|
||||
}
|
||||
|
||||
// UnaryExpression: !elapsed
|
||||
if (node.type === 'UnaryExpression') {
|
||||
return containsTimingRef(node.argument);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isAssertCall(node) {
|
||||
if (node.callee.type === 'Identifier') {
|
||||
return /^assert/.test(node.callee.name);
|
||||
}
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.type === 'Identifier' &&
|
||||
node.callee.object.name === 'assert'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// assert.strict.* or assert/strict
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.type === 'MemberExpression' &&
|
||||
node.callee.object.object.type === 'Identifier' &&
|
||||
node.callee.object.object.name === 'assert'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (!isAssertCall(node)) return;
|
||||
|
||||
// Check all arguments for timing refs
|
||||
for (const arg of node.arguments) {
|
||||
if (containsTimingRef(arg)) {
|
||||
context.report({ node, messageId: 'noElapsedAssertion' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = rule;
|
||||
80
eslint-rules/no-magic-sleep-in-tests.cjs
Normal file
80
eslint-rules/no-magic-sleep-in-tests.cjs
Normal file
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* no-magic-sleep-in-tests
|
||||
*
|
||||
* In *.test.cjs files, flag:
|
||||
* - Atomics.wait used as a sleep
|
||||
* - raw setTimeout used for synchronization (i.e., awaited or used without a callback driven by logic)
|
||||
*/
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const rule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow Atomics.wait sleeps and raw setTimeout synchronization in test files',
|
||||
category: 'Best Practices',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
atomicsWaitSleep:
|
||||
'Atomics.wait() used as a sleep in tests. Use a proper async wait pattern instead.',
|
||||
setTimeoutSync:
|
||||
'Raw setTimeout used for synchronization in tests. Use proper async patterns (promises, events, polling) instead.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
|
||||
// Only applies in test files
|
||||
if (!filename.endsWith('.test.cjs')) return {};
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
// Atomics.wait(...)
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.type === 'Identifier' &&
|
||||
node.callee.object.name === 'Atomics' &&
|
||||
node.callee.property.name === 'wait'
|
||||
) {
|
||||
context.report({ node, messageId: 'atomicsWaitSleep' });
|
||||
}
|
||||
|
||||
// setTimeout(...) used for synchronization:
|
||||
// - await new Promise(resolve => setTimeout(resolve, N))
|
||||
// - setTimeout(() => resolve(...), N) pattern
|
||||
// - setTimeout(cb, N) where N is a numeric literal (magic delay)
|
||||
if (
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'setTimeout'
|
||||
) {
|
||||
const args = node.arguments;
|
||||
// setTimeout(something, numericLiteral) — magic delay
|
||||
if (
|
||||
args.length >= 2 &&
|
||||
args[1].type === 'Literal' &&
|
||||
typeof args[1].value === 'number'
|
||||
) {
|
||||
// Check if it's in an await expression or a Promise constructor
|
||||
const parent = node.parent;
|
||||
const grandParent = parent && parent.parent;
|
||||
const isInPromise =
|
||||
(grandParent &&
|
||||
grandParent.type === 'NewExpression' &&
|
||||
grandParent.callee.type === 'Identifier' &&
|
||||
grandParent.callee.name === 'Promise') ||
|
||||
(parent && parent.type === 'AwaitExpression');
|
||||
if (isInPromise) {
|
||||
context.report({ node, messageId: 'setTimeoutSync' });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = rule;
|
||||
116
eslint-rules/no-source-grep.cjs
Normal file
116
eslint-rules/no-source-grep.cjs
Normal file
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* no-source-grep
|
||||
*
|
||||
* Flags variables bound to readFileSync() of a .cjs/.js/.ts source path that
|
||||
* later have .includes/.match/.startsWith/.indexOf called on them.
|
||||
*
|
||||
* Honor file-level escape comment: // allow-test-rule: <reason>
|
||||
*/
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const rule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow reading source .cjs/.js/.ts files with readFileSync and then doing text search on the result',
|
||||
category: 'Best Practices',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noSourceGrep:
|
||||
'Source-grep test: do not read source .cjs/.js/.ts files with readFileSync and call .includes/.match/.startsWith/.indexOf on the result. Use require() to run the module instead. Add // allow-test-rule: <reason> at the top of the file to suppress.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode
|
||||
? context.getSourceCode()
|
||||
: context.sourceCode;
|
||||
|
||||
// Check for file-level escape comment
|
||||
const comments = sourceCode.getAllComments();
|
||||
const hasAllowAnnotation = comments.some(
|
||||
(c) => /allow-test-rule:\s*\S/.test(c.value)
|
||||
);
|
||||
if (hasAllowAnnotation) return {};
|
||||
|
||||
// Track variable names bound to readFileSync of a source path
|
||||
const sourceGrepVars = new Set();
|
||||
|
||||
// Detect if a node represents a readFileSync call on a source file (.cjs/.js/.ts)
|
||||
// that lives in a source directory (bin, lib, get-shit-done, src).
|
||||
function isSourceReadFileSync(node) {
|
||||
if (node.type !== 'CallExpression') return false;
|
||||
|
||||
// Match: readFileSync(...) or fs.readFileSync(...) or require('fs').readFileSync(...)
|
||||
const callee = node.callee;
|
||||
const isFsRead =
|
||||
(callee.type === 'Identifier' && callee.name === 'readFileSync') ||
|
||||
(callee.type === 'MemberExpression' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
callee.property.name === 'readFileSync');
|
||||
|
||||
if (!isFsRead) return false;
|
||||
|
||||
const args = node.arguments;
|
||||
if (!args || args.length === 0) return false;
|
||||
|
||||
const firstArg = args[0];
|
||||
const fullSrc = sourceCode.getText(firstArg);
|
||||
|
||||
return looksLikeSourcePath(fullSrc);
|
||||
}
|
||||
|
||||
// Given the source text of a path expression, determine if it references
|
||||
// a .cjs/.js/.ts source file in a source directory.
|
||||
function looksLikeSourcePath(src) {
|
||||
// Must end with a .cjs, .js, or .ts extension (in a string)
|
||||
const hasCjsExt = /['"`.][^'"`.]*\.(?:cjs|js|ts)['"`)]/i.test(src);
|
||||
if (!hasCjsExt) return false;
|
||||
|
||||
// Must reference a source directory indicator somewhere in the expression
|
||||
const hasSourceDir = /['"](?:bin|lib|get-shit-done|src)['"]/i.test(src);
|
||||
return hasSourceDir;
|
||||
}
|
||||
|
||||
const TEXT_METHODS = new Set(['includes', 'match', 'startsWith', 'endsWith', 'indexOf', 'search']);
|
||||
|
||||
return {
|
||||
VariableDeclarator(node) {
|
||||
// const varName = readFileSync(...) OR const varName = fs.readFileSync(...)
|
||||
if (node.init && isSourceReadFileSync(node.init)) {
|
||||
if (node.id.type === 'Identifier') {
|
||||
sourceGrepVars.add(node.id.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
AssignmentExpression(node) {
|
||||
if (node.right && isSourceReadFileSync(node.right)) {
|
||||
if (node.left.type === 'Identifier') {
|
||||
sourceGrepVars.add(node.left.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
// varName.includes(...), varName.match(...), etc.
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
TEXT_METHODS.has(node.callee.property.name)
|
||||
) {
|
||||
const obj = node.callee.object;
|
||||
if (obj.type === 'Identifier' && sourceGrepVars.has(obj.name)) {
|
||||
context.report({ node, messageId: 'noSourceGrep' });
|
||||
}
|
||||
// Inline: readFileSync(...).includes(...)
|
||||
if (isSourceReadFileSync(obj)) {
|
||||
context.report({ node, messageId: 'noSourceGrep' });
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = rule;
|
||||
138
eslint.config.mjs
Normal file
138
eslint.config.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import globals from 'globals';
|
||||
import pluginN from 'eslint-plugin-n';
|
||||
import noOnlyTests from 'eslint-plugin-no-only-tests';
|
||||
import { existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Local plugin with three custom AST rules
|
||||
import noSourceGrep from './eslint-rules/no-source-grep.cjs';
|
||||
import noMagicSleepInTests from './eslint-rules/no-magic-sleep-in-tests.cjs';
|
||||
import noElapsedAssertion from './eslint-rules/no-elapsed-assertion.cjs';
|
||||
|
||||
const localPlugin = {
|
||||
rules: {
|
||||
'no-source-grep': noSourceGrep,
|
||||
'no-magic-sleep-in-tests': noMagicSleepInTests,
|
||||
'no-elapsed-assertion': noElapsedAssertion,
|
||||
},
|
||||
};
|
||||
|
||||
// Generated bin/lib files — never lint
|
||||
const GENERATED_CJS_IGNORES = [
|
||||
'get-shit-done/bin/lib/command-aliases.cjs',
|
||||
'get-shit-done/bin/lib/configuration.cjs',
|
||||
'get-shit-done/bin/lib/decisions.cjs',
|
||||
'get-shit-done/bin/lib/phase-lifecycle.cjs',
|
||||
'get-shit-done/bin/lib/plan-scan.cjs',
|
||||
'get-shit-done/bin/lib/project-root.cjs',
|
||||
'get-shit-done/bin/lib/schema-detect.cjs',
|
||||
'get-shit-done/bin/lib/secrets.cjs',
|
||||
'get-shit-done/bin/lib/state-document.cjs',
|
||||
'get-shit-done/bin/lib/validate.cjs',
|
||||
'get-shit-done/bin/lib/workstream-inventory-builder.cjs',
|
||||
'get-shit-done/bin/lib/workstream-name-policy.cjs',
|
||||
];
|
||||
|
||||
const sdkSrcExists = existsSync(join(__dirname, 'sdk', 'src'));
|
||||
|
||||
export default tseslint.config(
|
||||
// ── Global ignores ─────────────────────────────────────────────────────────
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'**/dist/**',
|
||||
'sdk/dist/**',
|
||||
'.worktrees/**',
|
||||
'.claude/**',
|
||||
'coverage/**',
|
||||
'**/*.generated.cjs',
|
||||
...GENERATED_CJS_IGNORES,
|
||||
],
|
||||
},
|
||||
|
||||
// ── get-shit-done/bin/**/*.cjs + scripts/**/*.cjs ───────────────────────────
|
||||
// CommonJS Node files: js.recommended + eslint-plugin-n + local plugin rules
|
||||
// Type-aware via parserOptions.project=tsconfig.lint.json where applicable
|
||||
{
|
||||
files: ['get-shit-done/bin/**/*.cjs', 'scripts/**/*.cjs'],
|
||||
plugins: {
|
||||
n: pluginN,
|
||||
local: localPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
// Generic quality rules
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'warn',
|
||||
'no-unused-vars': ['warn', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'none',
|
||||
}],
|
||||
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||
// Downgraded from recommended error → warn (pre-existing violations; follow-up to fix)
|
||||
'no-useless-escape': 'warn',
|
||||
'no-unsafe-finally': 'warn',
|
||||
// eslint-plugin-n rules
|
||||
'n/no-process-exit': 'warn',
|
||||
// Local rules — warn for now; flip to error after cleanup phases
|
||||
'local/no-source-grep': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// ── tests/**/*.test.cjs ─────────────────────────────────────────────────────
|
||||
{
|
||||
files: ['tests/**/*.test.cjs'],
|
||||
plugins: {
|
||||
'no-only-tests': noOnlyTests,
|
||||
local: localPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
'no-only-tests/no-only-tests': 'error',
|
||||
// Timing anti-patterns — warn for now; flip to error after cleanup
|
||||
'local/no-magic-sleep-in-tests': 'warn',
|
||||
'local/no-elapsed-assertion': 'warn',
|
||||
// Ban raw setTimeout sync + elapsed/duration-style assertions via no-restricted-syntax
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
{
|
||||
selector: 'AwaitExpression > NewExpression[callee.name="Promise"] ArrowFunctionExpression CallExpression[callee.name="setTimeout"]',
|
||||
message: 'Raw setTimeout used for synchronization in tests. Use proper async patterns instead.',
|
||||
},
|
||||
{
|
||||
selector: 'CallExpression[callee.object.name="Atomics"][callee.property.name="wait"]',
|
||||
message: 'Atomics.wait() used as a sleep in tests. Use a proper async wait pattern instead.',
|
||||
},
|
||||
],
|
||||
'no-unused-vars': ['warn', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'none',
|
||||
}],
|
||||
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||
// Downgraded from recommended error → warn (pre-existing violations; follow-up to fix)
|
||||
'no-useless-escape': 'warn',
|
||||
'no-regex-spaces': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'no-irregular-whitespace': 'warn',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -121,7 +121,7 @@ function scanQuickTasks(planDir) {
|
||||
}
|
||||
|
||||
let status = 'missing';
|
||||
let description = '';
|
||||
const description = '';
|
||||
|
||||
if (summaryPath && fs.existsSync(summaryPath)) {
|
||||
let safeSum;
|
||||
|
||||
@@ -53,7 +53,7 @@ function extractFrontmatter(content) {
|
||||
|
||||
// Stack to track nested objects: [{obj, key, indent}]
|
||||
// obj = object to write to, key = current key collecting array items, indent = indentation level
|
||||
let stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
||||
const stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip empty lines
|
||||
|
||||
@@ -985,7 +985,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
||||
// `phases clear` between milestones, on-disk dirs will be a subset of the
|
||||
// roadmap until each phase is materialized; reading from disk causes
|
||||
// `all_phases_complete: true` to fire prematurely.
|
||||
let roadmapPhaseNumbers = [];
|
||||
const roadmapPhaseNumbers = [];
|
||||
try {
|
||||
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
||||
const roadmapRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
|
||||
@@ -256,7 +256,7 @@ function applyBudget({ sections, budget, options = {} }) {
|
||||
const budgetUnderPressure = baseTokens > effectiveBudget;
|
||||
|
||||
// Available for content (reserve note slot when under pressure)
|
||||
let contentBudget = budgetUnderPressure
|
||||
const contentBudget = budgetUnderPressure
|
||||
? effectiveBudget - NOTE_RESERVE_TOKENS
|
||||
: effectiveBudget;
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
||||
// Patch ROADMAP.md
|
||||
let updated = false;
|
||||
withPlanningLock(cwd, () => {
|
||||
let content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
|
||||
// Find the phase section.
|
||||
// #3537: padding-tolerant fragment so the caller's resolved padded id
|
||||
|
||||
@@ -32,7 +32,7 @@ function loadInstallExports() {
|
||||
const savedTestMode = process.env.GSD_TEST_MODE;
|
||||
if (savedTestMode === undefined) process.env.GSD_TEST_MODE = '1';
|
||||
try {
|
||||
// eslint-disable-next-line global-require -- lazy import to avoid running installer at module load
|
||||
|
||||
return require('../../../bin/install.js');
|
||||
} finally {
|
||||
if (savedTestMode === undefined) delete process.env.GSD_TEST_MODE;
|
||||
|
||||
@@ -945,7 +945,7 @@ function stripFrontmatter(content) {
|
||||
// Handles CRLF line endings and multiple stacked blocks (corruption recovery).
|
||||
// Greedy: keeps stripping ---...--- blocks separated by optional whitespace.
|
||||
let result = content;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
|
||||
while (true) {
|
||||
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
||||
if (stripped === result) break;
|
||||
@@ -1001,7 +1001,7 @@ function acquireStateLock(statePath) {
|
||||
const startedAt = Date.now();
|
||||
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4)); // hoisted; value stays 0, pure Atomics.wait timeout target
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
||||
@@ -1391,7 +1391,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
||||
|
||||
// Update Total Plans in Phase
|
||||
if (planCount !== null && planCount !== undefined) {
|
||||
let result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
||||
const result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
||||
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
||||
}
|
||||
|
||||
@@ -1403,7 +1403,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
||||
|
||||
// Update Last Activity Description
|
||||
{
|
||||
let result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
||||
const result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
||||
if (result) { content = result; updated.push('Last Activity Description'); }
|
||||
}
|
||||
|
||||
|
||||
1429
package-lock.json
generated
1429
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -48,8 +48,15 @@
|
||||
"ws": "8.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"c8": "^11.0.0",
|
||||
"js-yaml": "^4.1.1"
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-n": "^17.24.0",
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.60.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fallow": "^2.70.0"
|
||||
@@ -64,9 +71,10 @@
|
||||
"prepublishOnly": "npm run build:hooks",
|
||||
"pretest": "npm run lint:skill-deps",
|
||||
"pretest:coverage": "npm run lint:skill-deps",
|
||||
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
||||
"lint:skill-deps": "node scripts/lint-skill-deps.cjs",
|
||||
"lint:tests": "node scripts/lint-no-source-grep.cjs",
|
||||
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
||||
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
||||
"lint:changeset": "node scripts/changeset/lint.cjs",
|
||||
|
||||
319
tests/eslint-rules.test.cjs
Normal file
319
tests/eslint-rules.test.cjs
Normal file
@@ -0,0 +1,319 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* eslint-rules.test.cjs
|
||||
*
|
||||
* RuleTester unit tests for the three local ESLint rules:
|
||||
* - local/no-source-grep
|
||||
* - local/no-magic-sleep-in-tests
|
||||
* - local/no-elapsed-assertion
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { RuleTester } = require('eslint');
|
||||
|
||||
const noSourceGrep = require('../eslint-rules/no-source-grep.cjs');
|
||||
const noMagicSleepInTests = require('../eslint-rules/no-magic-sleep-in-tests.cjs');
|
||||
const noElapsedAssertion = require('../eslint-rules/no-elapsed-assertion.cjs');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'commonjs',
|
||||
},
|
||||
});
|
||||
|
||||
// ─── no-source-grep ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('no-source-grep rule', () => {
|
||||
test('valid: readFileSync on .md file is allowed', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const content = fs.readFileSync(path.join(__dirname, '..', 'docs', 'readme.md'), 'utf-8');
|
||||
content.includes('hello');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const content = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'workflows', 'config.json'), 'utf-8');
|
||||
content.includes('key');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-source-grep valid cases passed');
|
||||
});
|
||||
|
||||
test('invalid: readFileSync on .cjs source file followed by .includes()', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'core.cjs'), 'utf-8');
|
||||
src.includes('someFunction');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noSourceGrep' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-source-grep invalid case detected');
|
||||
});
|
||||
|
||||
test('invalid: readFileSync on .cjs source file followed by .match()', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'foo.cjs'), 'utf-8');
|
||||
src.match(/pattern/);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noSourceGrep' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-source-grep match case detected');
|
||||
});
|
||||
|
||||
test('valid: file with allow-test-rule annotation is exempt', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
// The allow annotation exempts the whole file
|
||||
code: `
|
||||
// allow-test-rule: pending migration
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'core.cjs'), 'utf-8');
|
||||
src.includes('someFunction');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-source-grep allow-test-rule annotation works');
|
||||
});
|
||||
|
||||
test('valid: require() of a .cjs file is allowed (not readFileSync)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const mod = require('../get-shit-done/bin/lib/core.cjs');
|
||||
mod.someMethod();
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-source-grep require() is allowed');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── no-magic-sleep-in-tests ─────────────────────────────────────────────────
|
||||
|
||||
describe('no-magic-sleep-in-tests rule', () => {
|
||||
test('valid: setTimeout used outside tests (no-op since rule only applies to *.test.cjs)', () => {
|
||||
// Rule only applies to *.test.cjs files; a non-test filename is always valid
|
||||
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const delay = new Promise(resolve => setTimeout(resolve, 100));
|
||||
`,
|
||||
filename: 'scripts/some-script.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-magic-sleep-in-tests does not apply outside test files');
|
||||
});
|
||||
|
||||
test('invalid: Atomics.wait() in test file', () => {
|
||||
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const shared = new SharedArrayBuffer(4);
|
||||
const arr = new Int32Array(shared);
|
||||
Atomics.wait(arr, 0, 0, 100);
|
||||
`,
|
||||
filename: 'tests/some.test.cjs',
|
||||
errors: [{ messageId: 'atomicsWaitSleep' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-magic-sleep-in-tests flags Atomics.wait()');
|
||||
});
|
||||
|
||||
test('invalid: setTimeout used for synchronization in Promise in test file', () => {
|
||||
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
async function waitABit() {
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
`,
|
||||
filename: 'tests/some.test.cjs',
|
||||
errors: [{ messageId: 'setTimeoutSync' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-magic-sleep-in-tests flags setTimeout in Promise');
|
||||
});
|
||||
|
||||
test('valid: setTimeout with callback (not synchronization pattern) in test file', () => {
|
||||
// A setTimeout with no second arg or with a callback that does real work
|
||||
// is allowed. The rule only flags the await-new-Promise(setTimeout) pattern.
|
||||
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
function doSomethingLater(cb) {
|
||||
setTimeout(cb, 100);
|
||||
}
|
||||
`,
|
||||
filename: 'tests/some.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-magic-sleep-in-tests allows simple callback setTimeout');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── no-elapsed-assertion ─────────────────────────────────────────────────────
|
||||
|
||||
describe('no-elapsed-assertion rule', () => {
|
||||
test('valid: assert on non-timing property', () => {
|
||||
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const assert = require('node:assert/strict');
|
||||
const result = { count: 5 };
|
||||
assert.equal(result.count, 5);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const assert = require('node:assert/strict');
|
||||
assert.ok(result.success);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
assert.ok(true, 'no-elapsed-assertion valid cases passed');
|
||||
});
|
||||
|
||||
test('invalid: assert on .elapsed property', () => {
|
||||
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const assert = require('node:assert/strict');
|
||||
const result = { elapsed: 150 };
|
||||
assert.ok(result.elapsed < 200);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-elapsed-assertion flags assert on .elapsed');
|
||||
});
|
||||
|
||||
test('invalid: assert on .duration property', () => {
|
||||
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const assert = require('node:assert/strict');
|
||||
assert.equal(stats.duration, 100);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-elapsed-assertion flags assert on .duration');
|
||||
});
|
||||
|
||||
test('invalid: assert on .took property', () => {
|
||||
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const assert = require('node:assert/strict');
|
||||
assert.ok(result.took < 500);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-elapsed-assertion flags assert on .took');
|
||||
});
|
||||
|
||||
test('invalid: assert on .ms property', () => {
|
||||
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const assert = require('node:assert/strict');
|
||||
assert.ok(result.ms > 0);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-elapsed-assertion flags assert on .ms');
|
||||
});
|
||||
|
||||
test('invalid: assert.equal with timing comparison', () => {
|
||||
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const assert = require('node:assert/strict');
|
||||
assert.equal(result.elapsed > 0, true);
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(true, 'no-elapsed-assertion flags assert.equal with timing comparison');
|
||||
});
|
||||
});
|
||||
@@ -145,8 +145,8 @@ describe('Gemini Markdown Processor', () => {
|
||||
|
||||
const result = convertClaudeToGeminiAgent(input);
|
||||
|
||||
assert.match(result, /^ - read_file$/m, 'Read should still map to Gemini read_file');
|
||||
assert.doesNotMatch(result, /^ - ask_user$/m, 'ask_user must not be emitted as a Gemini tool');
|
||||
assert.match(result, /^ {2}- read_file$/m, 'Read should still map to Gemini read_file');
|
||||
assert.doesNotMatch(result, /^ {2}- ask_user$/m, 'ask_user must not be emitted as a Gemini tool');
|
||||
assert.doesNotMatch(result, /\bAskUserQuestion\b/, 'Claude tool references must be neutralized');
|
||||
assert.doesNotMatch(result, /\bask_user\b/, 'lowercase ask_user references must be neutralized');
|
||||
assert.match(result, /conversational prompting/, 'body should use runtime-neutral wording');
|
||||
|
||||
@@ -64,7 +64,7 @@ const SKILL_RUNTIMES = [
|
||||
// ─── Helper functions ─────────────────────────────────────────────────────────
|
||||
|
||||
function stripAnsi(str) {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
|
||||
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ function parseWorkflowStructure(src) {
|
||||
// A new top-level key ends the `on:` block
|
||||
if (/^[a-zA-Z]/.test(line) && !line.startsWith(' ')) { inOn = false; continue; }
|
||||
// Direct children of `on:` are trigger names at 2-space indent
|
||||
const m = line.match(/^ ([a-zA-Z_][a-zA-Z0-9_]*):/);
|
||||
const m = line.match(/^ {2}([a-zA-Z_][a-zA-Z0-9_]*):/);
|
||||
if (m) onTriggers.push(m[1]);
|
||||
}
|
||||
}
|
||||
@@ -45,17 +45,17 @@ function parseWorkflowStructure(src) {
|
||||
let inPrPaths = false;
|
||||
let inPullRequest = false;
|
||||
for (const line of lines) {
|
||||
if (/^ pull_request:/.test(line)) { inPullRequest = true; continue; }
|
||||
if (/^ {2}pull_request:/.test(line)) { inPullRequest = true; continue; }
|
||||
if (inPullRequest) {
|
||||
if (/^ paths:/.test(line)) { inPrPaths = true; continue; }
|
||||
if (/^ {4}paths:/.test(line)) { inPrPaths = true; continue; }
|
||||
if (inPrPaths) {
|
||||
const m = line.match(/^ - '(.+)'/);
|
||||
const m = line.match(/^ {6}- '(.+)'/);
|
||||
if (m) { prPaths.push(m[1]); continue; }
|
||||
// End of paths list
|
||||
if (/^ [a-zA-Z]/.test(line)) inPrPaths = false;
|
||||
if (/^ {4}[a-zA-Z]/.test(line)) inPrPaths = false;
|
||||
}
|
||||
// End of pull_request block
|
||||
if (/^ [a-zA-Z]/.test(line) && !line.startsWith(' ')) inPullRequest = false;
|
||||
if (/^ {2}[a-zA-Z]/.test(line) && !line.startsWith(' ')) inPullRequest = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,46 +67,46 @@ function parseWorkflowStructure(src) {
|
||||
|
||||
for (const line of lines) {
|
||||
// Step boundary: 6-space "- name:" or "- uses:"
|
||||
if (/^ - name:/.test(line)) {
|
||||
if (/^ {6}- name:/.test(line)) {
|
||||
if (currentStep && runLines.length) {
|
||||
currentStep.run = runLines.join('\n');
|
||||
}
|
||||
if (currentStep) steps.push(currentStep);
|
||||
currentStep = { name: line.replace(/^ - name:\s*/, '').trim() };
|
||||
currentStep = { name: line.replace(/^ {6}- name:\s*/, '').trim() };
|
||||
inRun = false;
|
||||
runLines = [];
|
||||
continue;
|
||||
}
|
||||
if (/^ - uses:/.test(line)) {
|
||||
if (/^ {6}- uses:/.test(line)) {
|
||||
if (currentStep && runLines.length) {
|
||||
currentStep.run = runLines.join('\n');
|
||||
}
|
||||
if (currentStep) steps.push(currentStep);
|
||||
currentStep = { uses: line.replace(/^ - uses:\s*/, '').trim() };
|
||||
currentStep = { uses: line.replace(/^ {6}- uses:\s*/, '').trim() };
|
||||
inRun = false;
|
||||
runLines = [];
|
||||
continue;
|
||||
}
|
||||
// uses: field inside a named step (e.g. "- name: Foo\n uses: actions/...")
|
||||
if (currentStep && /^ uses:\s/.test(line)) {
|
||||
currentStep.uses = line.replace(/^ uses:\s*/, '').trim();
|
||||
if (currentStep && /^ {8}uses:\s/.test(line)) {
|
||||
currentStep.uses = line.replace(/^ {8}uses:\s*/, '').trim();
|
||||
continue;
|
||||
}
|
||||
// run: block inside a step
|
||||
if (currentStep && /^ run:\s*\|/.test(line)) {
|
||||
if (currentStep && /^ {8}run:\s*\|/.test(line)) {
|
||||
inRun = true;
|
||||
runLines = [];
|
||||
continue;
|
||||
}
|
||||
if (currentStep && /^ run:\s*(?!\|)/.test(line)) {
|
||||
if (currentStep && /^ {8}run:\s*(?!\|)/.test(line)) {
|
||||
// Inline run (no |)
|
||||
currentStep.run = line.replace(/^ run:\s*/, '').trim();
|
||||
currentStep.run = line.replace(/^ {8}run:\s*/, '').trim();
|
||||
inRun = false;
|
||||
continue;
|
||||
}
|
||||
if (inRun) {
|
||||
// Lines deeper than 8 spaces belong to the run block
|
||||
if (/^ /.test(line) || line.trim() === '') {
|
||||
if (/^ {10}/.test(line) || line.trim() === '') {
|
||||
runLines.push(line);
|
||||
} else {
|
||||
inRun = false;
|
||||
|
||||
29
tsconfig.lint.json
Normal file
29
tsconfig.lint.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"strict": false
|
||||
},
|
||||
"include": [
|
||||
"get-shit-done/bin/lib/**/*.cjs"
|
||||
],
|
||||
"exclude": [
|
||||
"get-shit-done/bin/lib/command-aliases.cjs",
|
||||
"get-shit-done/bin/lib/configuration.cjs",
|
||||
"get-shit-done/bin/lib/decisions.cjs",
|
||||
"get-shit-done/bin/lib/phase-lifecycle.cjs",
|
||||
"get-shit-done/bin/lib/plan-scan.cjs",
|
||||
"get-shit-done/bin/lib/project-root.cjs",
|
||||
"get-shit-done/bin/lib/schema-detect.cjs",
|
||||
"get-shit-done/bin/lib/secrets.cjs",
|
||||
"get-shit-done/bin/lib/state-document.cjs",
|
||||
"get-shit-done/bin/lib/validate.cjs",
|
||||
"get-shit-done/bin/lib/workstream-inventory-builder.cjs",
|
||||
"get-shit-done/bin/lib/workstream-name-policy.cjs",
|
||||
"tests/**/*",
|
||||
"node_modules/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user