* fix(#683): auto-degrade phase execution to sequential on worktree base mismatch Claude Code forks worktree-isolated executors off the repository default branch (origin/HEAD), not the orchestrator's HEAD. Running /gsd-execute-phase on a branch diverged from the default (unmerged milestone/feature branch) left every executor without the phase's plan files and tripped the worktree-branch-check guard with `exit 42` — 100% reproducible, all OSes. - New module src/worktree-base-ref.cts: HEAD-vs-fork-base drift detection (origin/HEAD with symbolic-ref fallback) and no-clobber worktree.baseRef management, exposed as `worktree base-check` / `worktree set-baseref`. - execute-phase.md: pre-dispatch, for Claude Code with worktrees enabled, auto-degrades the run to sequential on the main tree when a base mismatch is detected, recommending worktree.baseRef:"head". The exit-42 guard stays as a backstop. - Installer: fresh local Claude installs set worktree.baseRef:"head" in .claude/settings.local.json (no-clobber, respecting an explicit shared settings.json value); upgrades print an opt-in notice pointing at `gsd-tools worktree set-baseref`. - Docs: how-to guide, CLI/config reference, planning-config cross-ref. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#683): auto-apply worktree.baseRef on upgrade; gate fresh+upgrade on use_worktrees Per maintainer direction: on a local Claude Code UPGRADE, set worktree.baseRef:"head" automatically (no opt-in notice) when the project's workflow.use_worktrees is enabled, instead of merely printing a remediation notice. For consistency the FRESH path is now gated the same way: both paths compute worktrees-enabled once (bounded walk-up read of .planning/config.json, default enabled unless workflow.use_worktrees === false) and apply the no-clobber baseRef only when enabled — never overwriting an explicit value in settings.local.json or a shared settings.json. gsd-tools worktree set-baseref remains for manual use. Docs + changeset updated; tests hardened (file-exists assertions, fresh+disabled case, upgrade idempotency). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#683): measure workflow byte-budget on LF, fixing Windows-only CI failure The workflow-size-budget test failed only on Windows: git checks out the .md files as CRLF (no eol=lf in .gitattributes) and byteCount used fs.statSync().size (raw on-disk bytes), counting an extra \r per line. That inflated execute-phase.md — the XL high-water-mark file pinned near its ceiling by the tighten-only ratchet — from 88492 LF bytes to ~90245 on Windows, over the 90000 XL ceiling, while passing on the LF-checkout Mac/Linux runners. The ceilings are explicitly "calibrated against raw `wc -c`" on an LF checkout, so the measurement should be LF-based on every platform. byteCount now reads the file and counts Buffer.byteLength after stripping CR, making the budget platform-independent (a no-op on LF checkouts; verified statSync === normalized for all 88 workflow files). No ceilings changed. Added a regression test asserting CRLF and LF content of the same file count identically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#683): make worktree-base-ref test path mocks Windows-safe (path.join) tests/worktree-base-ref.test.cjs keyed its injected readFile/writeFile mocks (and a few expected `file` values) with forward-slash template literals like `${claudeDir}/settings.local.json`. The module composes those paths with path.join(), which emits backslashes on Windows, so the mock keys never matched the module's lookup → readFile returned null → resolveEffectiveBaseRef / cmdWorktreeBaseCheck / cmdWorktreeSetBaseRef (and the JSONC variants) failed on the Windows full-test runner only (they passed on Mac/Linux, and the install tests passed because they use the real filesystem). The module is correct; only the test fixtures hardcoded '/'. All mock keys and path assertions now use path.join(base, ...) mirroring the module, so they match on every platform (no-op on POSIX). 19 path references across 16 lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
232 lines
9.2 KiB
JavaScript
232 lines
9.2 KiB
JavaScript
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 { dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Local plugin with 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';
|
|
import noRawRmsyncInTests from './eslint-rules/no-raw-rmsync-in-tests.cjs';
|
|
|
|
const localPlugin = {
|
|
rules: {
|
|
'no-source-grep': noSourceGrep,
|
|
'no-magic-sleep-in-tests': noMagicSleepInTests,
|
|
'no-elapsed-assertion': noElapsedAssertion,
|
|
'no-raw-rmsync-in-tests': noRawRmsyncInTests,
|
|
},
|
|
};
|
|
|
|
export default tseslint.config(
|
|
// ── Global ignores ─────────────────────────────────────────────────────────
|
|
{
|
|
ignores: [
|
|
'node_modules/**',
|
|
'**/dist/**',
|
|
'.worktrees/**',
|
|
'.claude/**',
|
|
'coverage/**',
|
|
'**/*.generated.cjs',
|
|
// ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs.
|
|
'gsd-core/bin/lib/semver-compare.cjs',
|
|
'gsd-core/bin/lib/cli-exit.cjs',
|
|
'gsd-core/bin/lib/code-review-flags.cjs',
|
|
'gsd-core/bin/lib/context-utilization.cjs',
|
|
'gsd-core/bin/lib/artifacts.cjs',
|
|
'gsd-core/bin/lib/command-arg-projection.cjs',
|
|
'gsd-core/bin/lib/clock.cjs',
|
|
'gsd-core/bin/lib/ui-safety-gate.cjs',
|
|
'gsd-core/bin/lib/review-reviewer-selection.cjs',
|
|
'gsd-core/bin/lib/clusters.cjs',
|
|
'gsd-core/bin/lib/installer-migrations/001-legacy-orphan-files.cjs',
|
|
'gsd-core/bin/lib/observability/redaction.cjs',
|
|
'gsd-core/bin/lib/installer-migration-report.cjs',
|
|
'gsd-core/bin/lib/prompt-budget.cjs',
|
|
'gsd-core/bin/lib/secrets.cjs',
|
|
'gsd-core/bin/lib/phase-lifecycle.cjs',
|
|
'gsd-core/bin/lib/workstream-name-policy.cjs',
|
|
'gsd-core/bin/lib/decisions.cjs',
|
|
'gsd-core/bin/lib/validate.cjs',
|
|
'gsd-core/bin/lib/schema-detect.cjs',
|
|
'gsd-core/bin/lib/runtime-name-policy.cjs',
|
|
'gsd-core/bin/lib/runtime-slash.cjs',
|
|
'gsd-core/bin/lib/observability/event.cjs',
|
|
'gsd-core/bin/lib/workstream-inventory-builder.cjs',
|
|
'gsd-core/bin/lib/plan-scan.cjs',
|
|
'gsd-core/bin/lib/fallow-runner.cjs',
|
|
'gsd-core/bin/lib/project-root.cjs',
|
|
'gsd-core/bin/lib/installer-migration-authoring.cjs',
|
|
'gsd-core/bin/lib/update-context.cjs',
|
|
'gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs',
|
|
'gsd-core/bin/lib/runtime-homes.cjs',
|
|
'gsd-core/bin/lib/model-catalog.cjs',
|
|
'gsd-core/bin/lib/configuration.cjs',
|
|
'gsd-core/bin/lib/state-document.cjs',
|
|
'gsd-core/bin/lib/shell-command-projection.cjs',
|
|
'gsd-core/bin/lib/security.cjs',
|
|
'gsd-core/bin/lib/command-aliases.cjs',
|
|
'gsd-core/bin/lib/config-schema.cjs',
|
|
'gsd-core/bin/lib/model-profiles.cjs',
|
|
'gsd-core/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs',
|
|
'gsd-core/bin/lib/installer-migrations/003-rename-get-shit-done-to-gsd-core.cjs',
|
|
'gsd-core/bin/lib/observability/logger.cjs',
|
|
'gsd-core/bin/lib/active-workstream-store.cjs',
|
|
'gsd-core/bin/lib/adr-parser.cjs',
|
|
'gsd-core/bin/lib/graphify.cjs',
|
|
'gsd-core/bin/lib/install-profiles.cjs',
|
|
'gsd-core/bin/lib/intel.cjs',
|
|
'gsd-core/bin/lib/installer-migrations.cjs',
|
|
'gsd-core/bin/lib/worktree-safety.cjs',
|
|
'gsd-core/bin/lib/worktree-base-ref.cjs',
|
|
'gsd-core/bin/lib/planning-workspace.cjs',
|
|
'gsd-core/bin/lib/runtime-artifact-layout.cjs',
|
|
'gsd-core/bin/lib/command-routing-hub.cjs',
|
|
'gsd-core/bin/lib/core.cjs',
|
|
'gsd-core/bin/lib/drift.cjs',
|
|
'gsd-core/bin/lib/cjs-command-router-adapter.cjs',
|
|
'gsd-core/bin/lib/phase-command-router.cjs',
|
|
'gsd-core/bin/lib/surface.cjs',
|
|
'gsd-core/bin/lib/roadmap-upgrade.cjs',
|
|
'gsd-core/bin/lib/config-types.cjs',
|
|
'gsd-core/bin/lib/phases-command-router.cjs',
|
|
'gsd-core/bin/lib/verify-command-router.cjs',
|
|
'gsd-core/bin/lib/init-command-router.cjs',
|
|
'gsd-core/bin/lib/agent-command-router.cjs',
|
|
'gsd-core/bin/lib/task-command-router.cjs',
|
|
'gsd-core/bin/lib/validate-command-router.cjs',
|
|
'gsd-core/bin/lib/workstream-inventory.cjs',
|
|
'gsd-core/bin/lib/roadmap-command-router.cjs',
|
|
'gsd-core/bin/lib/state-command-router.cjs',
|
|
'gsd-core/bin/lib/gap-checker.cjs',
|
|
'gsd-core/bin/lib/config.cjs',
|
|
'gsd-core/bin/lib/profile-output.cjs',
|
|
'gsd-core/bin/lib/commands.cjs',
|
|
'gsd-core/bin/lib/state.cjs',
|
|
'gsd-core/bin/lib/milestone.cjs',
|
|
'gsd-core/bin/lib/phase.cjs',
|
|
'gsd-core/bin/lib/verify.cjs',
|
|
'gsd-core/bin/lib/init.cjs',
|
|
'gsd-core/bin/lib/docs.cjs',
|
|
'gsd-core/bin/lib/check-command-router.cjs',
|
|
'gsd-core/bin/lib/frontmatter.cjs',
|
|
'gsd-core/bin/lib/learnings.cjs',
|
|
'gsd-core/bin/lib/gsd2-import.cjs',
|
|
'gsd-core/bin/lib/profile-pipeline.cjs',
|
|
'gsd-core/bin/lib/template.cjs',
|
|
'gsd-core/bin/lib/uat.cjs',
|
|
'gsd-core/bin/lib/workstream.cjs',
|
|
'gsd-core/bin/lib/roadmap.cjs',
|
|
'gsd-core/bin/lib/audit.cjs',
|
|
'gsd-core/bin/lib/research-store.cjs',
|
|
'gsd-core/bin/lib/research-provider.cjs',
|
|
'gsd-core/bin/lib/package-legitimacy.cjs',
|
|
],
|
|
},
|
|
|
|
// ── src/**/*.cts — TypeScript runtime sources (ADR-457 build-at-publish) ─────
|
|
// First-class type-aware linting on the migrated source. The TS compiler
|
|
// (`npm run build:lib`, strict + noEmitOnError) is the primary type gate;
|
|
// these rules add lint-level coverage. warn-first per the harness convention.
|
|
{
|
|
files: ['src/**/*.cts'],
|
|
extends: [tseslint.configs.recommendedTypeChecked],
|
|
languageOptions: {
|
|
parserOptions: {
|
|
project: './tsconfig.build.json',
|
|
tsconfigRootDir: __dirname,
|
|
},
|
|
},
|
|
rules: {
|
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
|
},
|
|
},
|
|
|
|
// ── gsd-core/bin/**/*.cjs + scripts/**/*.cjs ───────────────────────────
|
|
// CommonJS Node files: js.recommended + eslint-plugin-n + local plugin rules
|
|
{
|
|
files: ['gsd-core/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': 'error',
|
|
// 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 — ratcheted to error after cleanup (all violations fixed)
|
|
'local/no-magic-sleep-in-tests': 'error',
|
|
'local/no-elapsed-assertion': 'warn',
|
|
// Ban raw fs.rmSync in tests — use helpers.cleanup() for Windows-EBUSY retry budget
|
|
'local/no-raw-rmsync-in-tests': 'error',
|
|
// Ban raw setTimeout sync + elapsed/duration-style assertions via no-restricted-syntax
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
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': 'error',
|
|
'no-irregular-whitespace': 'warn',
|
|
},
|
|
},
|
|
);
|