Skip to content

[bug-report] The custom-detector loader walks the project tree and des… #526

Description

@jessegall

Report:
The custom-detector loader walks the project tree and descends into nested git worktrees, so a worktree under .claude/worktrees/ carries a second copy of every .commandments/custom/ class under the same Commandments\ namespace and PHP fatals: 'Cannot redeclare trait Commandments\Naming\ReadsAsProse'. It is non-blocking but prints on every hook while a worktree exists. A nested worktree is a checkout of the same repo, so its custom detectors are always duplicates by construction; the walk should skip a directory that git reports as a separate worktree, or skip .claude entirely.

Where: .commandments/custom/Naming/ReadsAsProse.php:13

Code (.commandments/custom/Naming/ReadsAsProse.php:13):

  10   * ask this the same way — a name is judged by the words in it, and words are the same in either
  11   * language.
  12   */
→ 13  trait ReadsAsProse
  14  {
  15  
  16      /**
  17       * @var list<string>
  18       */
  19      private const array CLAUSE_WORDS = [
  20          'where', 'when', 'whether', 'why', 'who',
  21          'nothing', 'everything', 'anything', 'something',
  22          'whatever', 'wherever', 'whenever', 'however', 'itself', 'themselves',
  23      ];
  24  
  25      /**
  26       * An article or a relative pronoun is English at ANY length, so these are judged however short the
  27       * name is. `TheTarget` is two words and still a phrase — and while a name like it passes, it is the
  28       * exemplar the next one is written from, which is how `CalledOnTheFirst` came to be written at all.
  29       *
  30       * The conditional words are NOT here: `when()` is an ordinary fluent-API verb on its own, and only
  31       * reads as prose once a name is long enough to be a sentence.
  32       *
  33       * @var list<string>
  34       */
  35      private const array ALWAYS_PROSE = ['the', 'that', 'which', 'what', 'whom', 'whose'];
  36  
  37      /**

Where: .commandments/config.php:275

Code (.commandments/config.php:275):

  272          'resources/js/realtime',
  273          'resources/js/types',
  274      );
→ 275  
  276      $config->exclude('tests-legacy', '.claude');
  277  
  278      $config->package(WorkflowsPackage::class);
  279  
  280      $config->planExecution(
  281          fn(PlanExecution $plan) => $plan
  282              ->pushEachPhase()
  283              ->trackWorkingState()
  284              ->keepGoing()
  285              ->onComplete('composer test')
  286      );
  287  
  288      $config->detector(\Commandments\CapitalisedEmphasisDetector::class, \Commandments\RawReflectionDetector::class, \Commandments\MeasuredStylingDetector::class, \Commandments\ContainerInDisguiseDetector::class, \Commandments\ProseDocblock\ProseDocblockDetector::class, \Commandments\Naming\SentenceIdentifierDetector::class, \Commandments\Naming\ProseFileNameDetector::class, \Commandments\Naming\TypeScriptSentenceIdentifierDetector::class, \Commandments\UndocumentedType\UndocumentedTypeDetector::class, \Commandments\UndocumentedType\ContractNamesItsMethodsDetector::class, \Commandments\InlineComment\InlineCommentDetector::class, \Commandments\PoeticComment\PoeticCommentDetector::class);
  289  
  290      $config->hook(\Commandments\UnboundedMonitorHook::class);
  291  
  292  
  293  
  294      $disabledSkills($config);
  295      $disabledSins($config);
  296      $disabledHooks($config);
  297      $disabledAgents($config);
  298  };

Filed via commandments report from a consumer project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions