diff --git a/resources/AGENTS.md b/resources/AGENTS.md index 0af1106..ba61bd3 100644 --- a/resources/AGENTS.md +++ b/resources/AGENTS.md @@ -1,39 +1,35 @@ # PHPForge Agent Notes -- Applies to PHP projects using `infocyph/phpforge`. -- First inspect with `composer ic:doctor` and `composer ic:list-config` (`--json` is available). -- Do not edit `vendor/` or commit cache/coverage/benchmark/generated output unless tracked. -- Keep edits scoped to the request and existing project architecture. -- Project config always overrides PHPForge defaults. - -## Commands - -- `composer ic:process` - fixes Composer Normalize, Rector, Pint, PHPCBF issues. -- `composer ic:tests:details` - detailed step-by-step errors. -- `composer ic:tests` - full quality suite. -- `composer ic:tests:parallel` - syntax preflight plus bounded parallel quality checks. -- `composer ic:test:architecture` - Deptrac architecture boundary checks. -- `composer ic:release:guard` - release gate. -- `composer ic:ci` / `composer ic:ci --prefer-lowest` - CI parity. -- `composer ic:init` / `composer ic:hooks` - project setup and hooks. -- `composer ic:publish-config phpprobe.json` - customize syntax/duplicate scan policy. - -## Resolution Flow - -- Run `composer ic:process` first unless the task is read-only. -- Run `composer ic:tests:details` and use its output for remaining fixes. -- Re-run `composer ic:tests:details` after edits. -- Finish with `composer ic:tests` or `composer ic:release:guard` when relevant. -- If blocked, report the failing command and key error. - -## Config And CI - -- Config priority: project root config first, then `vendor/infocyph/phpforge/resources`, then source-tree `resources/` only when the current project is `infocyph/phpforge`; otherwise missing bundled configs hard fail. -- `phpprobe.json` controls PHPProbe syntax and duplicate paths/excludes; empty `paths` means project-root discovery through Git-aware PHP file finding. -- Syntax and duplicate scans run through `vendor/bin/phpprobe` and respect Git ignores plus configured `exclude`/`exclude_paths` entries. -- Checker CLI paths override configured `paths`; CLI `--exclude` values are added to configured excludes. -- `deptrac.yaml` controls architecture boundary checks. -- Pre-commit runs `composer validate --strict`, `composer normalize --dry-run`, `composer ic:release:audit`, `composer ic:ci`. -- `IC_HOOKS_STRICT=1` is default; use `IC_HOOKS_STRICT=0 composer install` only for best-effort hook install. -- Workflow: `infocyph/phpforge/.github/workflows/security-standards.yml@main`. -- Workflow inputs: `php_versions`, `dependency_versions`, `php_extensions`, `coverage`, `composer_flags`, `phpstan_memory_limit`, `psalm_threads`, `run_analysis`, `run_svg_report`, `artifact_retention_days`. +- For projects using `infocyph/phpforge`. +- Run `composer ic:doctor` and `composer ic:list-config` first. +- Keep changes scoped; do not edit `vendor/`. + +## Core Flow + +- `composer ic:process` (unless read-only task). +- `composer ic:tests:details`, fix issues, then re-run. +- Final check: `composer ic:tests` or `composer ic:release:guard`. +- If blocked, report the exact failing command + key error. + +## Agent Behavior + +- Execute the flow by default; do not ask for routine step confirmation. +- Ask only for destructive/risky actions, unclear product decisions, or missing secrets. +- Run routine commands directly and report concise results. + +## Key Commands + +- `composer ic:process` +- `composer ic:tests:details` +- `composer ic:tests` +- `composer ic:tests:parallel` +- `composer ic:ci` / `composer ic:ci --prefer-lowest` +- `composer ic:release:guard` +- `composer ic:init`, `composer ic:hooks` + +## CI Notes + +- Config precedence: project root -> `vendor/infocyph/phpforge/resources` -> source `resources/` (only in `infocyph/phpforge` repo). +- Pest parallel is on by default for `ic:tests`/`ic:ci`. +- Use `IC_PEST_PARALLEL=0` to disable Pest parallel in unstable CI. +- Optional tuning: `IC_PEST_PROCESSES`, `IC_PSALM_THREADS`, `IC_PHPSTAN_MEMORY_LIMIT`. diff --git a/src/Composer/TaskCatalog.php b/src/Composer/TaskCatalog.php index 7c4bdd7..ff663c0 100644 --- a/src/Composer/TaskCatalog.php +++ b/src/Composer/TaskCatalog.php @@ -200,7 +200,7 @@ public static function testAll(): array { return [ ...self::syntax(), - [Paths::php(), Paths::bin('pest'), ...self::pestConfigArgs(), '--parallel', '--processes=' . self::pestProcesses()], + [Paths::php(), Paths::bin('pest'), ...self::pestConfigArgs(), ...self::pestParallelArgs()], [Paths::php(), Paths::bin('pint'), '--test', '--config', Paths::config('pint.json')], [Paths::php(), Paths::bin('phpcs'), '--standard=' . Paths::config('phpcs.xml.dist'), '--report=summary', '.'], ...self::duplicates(), @@ -400,6 +400,29 @@ private static function pestConfigArgs(): array return [...$args, ...Paths::existingProjectPaths('tests')]; } + /** + * @return list + */ + private static function pestParallelArgs(): array + { + if (!self::pestParallelEnabled()) { + return []; + } + + return ['--parallel', '--processes=' . self::pestProcesses()]; + } + + private static function pestParallelEnabled(): bool + { + $value = getenv('IC_PEST_PARALLEL'); + + if (!is_string($value) || $value === '') { + return true; + } + + return !in_array(strtolower(trim($value)), ['0', 'false', 'off', 'no'], true); + } + private static function pestProcesses(): string { return self::envInt('IC_PEST_PROCESSES', 10, 1, 64); diff --git a/tests/Composer/TaskCatalogTest.php b/tests/Composer/TaskCatalogTest.php index c2d1a75..df96901 100644 --- a/tests/Composer/TaskCatalogTest.php +++ b/tests/Composer/TaskCatalogTest.php @@ -44,6 +44,29 @@ function removeTaskCatalogTree(string $path): void rmdir($path); } +function withTaskCatalogEnv(string $name, ?string $value, callable $callback): void +{ + $previous = getenv($name); + + if ($value === null) { + putenv($name); + } else { + putenv($name.'='.$value); + } + + try { + $callback(); + } finally { + if ($previous === false) { + putenv($name); + + return; + } + + putenv($name.'='.$previous); + } +} + it('runs composer normalize as part of process all', function (): void { expect(TaskCatalog::processAll()[0])->toBe(['composer', 'normalize']); }); @@ -82,6 +105,38 @@ function removeTaskCatalogTree(string $path): void ->and(TaskDisplay::heading(TaskCatalog::testParallel()[0]))->toStartWith('Pest'); }); +it('runs pest in parallel by default for full test suites', function (): void { + withTaskCatalogEnv('IC_PEST_PARALLEL', null, function (): void { + withTaskCatalogEnv('IC_PEST_PROCESSES', null, function (): void { + $command = TaskCatalog::testAll()[1]; + + expect($command)->toContain('--parallel') + ->and($command)->toContain('--processes=10'); + }); + }); +}); + +it('allows disabling pest parallel in full test suites', function (): void { + withTaskCatalogEnv('IC_PEST_PARALLEL', '0', function (): void { + withTaskCatalogEnv('IC_PEST_PROCESSES', '7', function (): void { + $command = TaskCatalog::testAll()[1]; + + $hasProcesses = false; + + foreach ($command as $argument) { + if (str_starts_with($argument, '--processes=')) { + $hasProcesses = true; + + break; + } + } + + expect($command)->not()->toContain('--parallel') + ->and($hasProcesses)->toBeFalse(); + }); + }); +}); + it('uses the bundled phpbench config directly for consuming projects', function (): void { $originalCwd = getcwd(); $projectRoot = sys_get_temp_dir().DIRECTORY_SEPARATOR.'phpforge-task-catalog-'.uniqid('', true);