Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 33 additions & 37 deletions resources/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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`.
25 changes: 24 additions & 1 deletion src/Composer/TaskCatalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -400,6 +400,29 @@ private static function pestConfigArgs(): array
return [...$args, ...Paths::existingProjectPaths('tests')];
}

/**
* @return list<string>
*/
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);
Expand Down
55 changes: 55 additions & 0 deletions tests/Composer/TaskCatalogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand Down Expand Up @@ -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);
Expand Down