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.
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:13Code (
.commandments/custom/Naming/ReadsAsProse.php:13):Where:
.commandments/config.php:275Code (
.commandments/config.php:275):Filed via
commandments reportfrom a consumer project.