Skip to content

Commit 6c124a7

Browse files
authored
feat(agents-md): contribute homeboy CLI section when on PATH (#56)
Adds optional Homeboy awareness to data-machine-code. When the `homeboy` Rust CLI (https://github.com/Extra-Chill/homeboy) is callable from this host's shell, AGENTS.md gains a `## Homeboy` section that teaches agents the verb surface and the release-automation rules they must respect inside Homeboy-managed repos. Why this lives here: data-machine-code is the bridge between WordPress and an external coding-agent runtime. Surfacing extra CLI tools the runtime can call (`homeboy`, on top of `git`, `wp-cli`, etc.) is exactly the kind of thing that belongs in the bridge layer. Homeboy itself stays PHP-free. What the section teaches agents (only when `homeboy` is on PATH): - The verb surface — `audit`, `lint`, `test`, `git <verb>`, `deploy`, `component list`, `release` — with `homeboy --help` as the canonical source of truth for the full list. - Release-automation rules for any repo with a `homeboy.json`: - NEVER edit `CHANGELOG.md` — Homeboy generates it from conventional commits at release time. - NEVER hand-bump version strings — Homeboy rewrites version targets declared in `homeboy.json` automatically from `feat:` / `fix:` / `BREAKING CHANGE` commits. - Use conventional commits; `feat:` / `fix:` trigger releases. Mirrors how DMC's existing `## Abilities` section works: teach the discovery primitive (`homeboy --help`), trust the CLI to be its own source of truth. Don't enumerate state in AGENTS.md that the tool can list itself. `DataMachineCode\Homeboy` mirrors the shape of `Environment`: - `is_available()` — single shell probe via `command -v homeboy`, memoized for the request lifecycle. - `reset_cache()` — test-only seam. Hard-gated on `Environment::has_shell()` so hosts with `shell_exec` disabled get a graceful false, never a fatal. Tests: tests/smoke-homeboy.php — 5 assertions on the probe across no-shell / empty-PATH / synthetic-on-PATH / memoization / reset_cache branches. Pure-PHP, no WP bootstrap, deterministic via PATH override.
1 parent c4bf8bf commit 6c124a7

3 files changed

Lines changed: 192 additions & 0 deletions

File tree

data-machine-code.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,37 @@ function datamachine_code_load_chat_tools() {
393393
'description' => 'Pointers to WordPress source directories.',
394394
) );
395395

396+
// Homeboy — conditional, only on hosts where the `homeboy` CLI is
397+
// callable from PATH. Mirrors the house style of other AGENTS.md
398+
// sections: lead with a one-line definition, group with bold
399+
// sub-labels, end with a discoverability hint. `homeboy --help`
400+
// is the canonical verb list; this section just surfaces the
401+
// verbs agents reach for and the repo-level rules.
402+
if ( \DataMachineCode\Homeboy::is_available() ) {
403+
\DataMachine\Engine\AI\SectionRegistry::register( 'AGENTS.md', 'homeboy', 35, function () {
404+
return <<<'MD'
405+
## Homeboy
406+
407+
`homeboy` is a Rust CLI on this host. Every verb runs the same locally as in CI.
408+
409+
**Quality:** `homeboy audit | lint | test | refactor`
410+
411+
**Git:** prefer `homeboy git changes | status | commit | push | pull | tag` — auto-baselines, structured output, `--json` bulk. One-off reads (`git diff`, `git show`, `git blame`) stay on raw `git`.
412+
413+
**Perf + envs:** `homeboy bench` for pinned benchmarks, `homeboy rig` for reproducible dev environments.
414+
415+
**Repo rules** (when `homeboy.json` is present):
416+
- **NEVER edit `CHANGELOG.md`** — generated from conventional commits at release time.
417+
- **NEVER hand-bump version strings** — `feat:`/`fix:`/`BREAKING CHANGE` drive semver; Homeboy rewrites version targets in `homeboy.json`.
418+
419+
Run `homeboy --help` for the full verb list. Operator verbs (`release`, `deploy`, `fleet`, `ssh`) only on explicit ask.
420+
MD;
421+
}, array(
422+
'label' => 'Homeboy',
423+
'description' => 'Homeboy CLI — verbs agents reach for + repo rules.',
424+
) );
425+
}
426+
396427
// Multisite — conditional, only on multisite installs.
397428
if ( is_multisite() ) {
398429
\DataMachine\Engine\AI\SectionRegistry::register( 'AGENTS.md', 'multisite', 40, function () use ( $wp ) {

inc/Homeboy.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
/**
3+
* Homeboy capability helper.
4+
*
5+
* Detects whether the [Homeboy](https://github.com/Extra-Chill/homeboy)
6+
* Rust CLI is callable from this host's shell. Homeboy is a code-factory
7+
* tool that runs release automation, audits, lints, tests, and git
8+
* primitives over local projects. It operates outside WordPress and
9+
* never speaks PHP — the only thing DMC needs to know is whether the
10+
* binary is on PATH so it can teach agents about the verbs.
11+
*
12+
* Detection runs once per request and is memoized. Callers should not
13+
* shell out themselves; use `is_available()` and rely on Data Machine
14+
* Code's `Environment::has_shell()` for the hard gate.
15+
*
16+
* @package DataMachineCode
17+
* @since 0.11.0
18+
*/
19+
20+
namespace DataMachineCode;
21+
22+
if ( ! defined( 'ABSPATH' ) ) {
23+
exit;
24+
}
25+
26+
class Homeboy {
27+
28+
/**
29+
* Memoized detection result. Null until the first probe; bool after.
30+
*
31+
* @var bool|null
32+
*/
33+
private static $available_cache = null;
34+
35+
/**
36+
* Is the `homeboy` CLI on this host's PATH?
37+
*
38+
* Probes via `command -v homeboy` exactly once per request. Returns
39+
* false on hosts that disable shell functions, on hosts that lack
40+
* the binary, and any time `Environment::has_shell()` returns false.
41+
*
42+
* @since 0.11.0
43+
*
44+
* @return bool True when the binary is callable from the current shell.
45+
*/
46+
public static function is_available(): bool {
47+
if ( null !== self::$available_cache ) {
48+
return self::$available_cache;
49+
}
50+
51+
if ( ! Environment::has_shell() ) {
52+
self::$available_cache = false;
53+
return false;
54+
}
55+
56+
$output = @shell_exec( 'command -v homeboy 2>/dev/null' );
57+
self::$available_cache = ! empty( trim( (string) $output ) );
58+
return self::$available_cache;
59+
}
60+
61+
/**
62+
* Reset the detection cache.
63+
*
64+
* Test-only seam — production code never needs to re-probe because
65+
* PATH does not change mid-request.
66+
*
67+
* @since 0.11.0
68+
*/
69+
public static function reset_cache(): void {
70+
self::$available_cache = null;
71+
}
72+
}

tests/smoke-homeboy.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
/**
3+
* Pure-PHP smoke test for the Homeboy capability helper.
4+
*
5+
* Run: php tests/smoke-homeboy.php
6+
*
7+
* Exercises the shell-probe path against a synthetic PATH so the
8+
* result is deterministic regardless of whether the host actually
9+
* has `homeboy` installed. Avoids a WP bootstrap.
10+
*/
11+
12+
declare( strict_types=1 );
13+
14+
if ( ! defined( 'ABSPATH' ) ) {
15+
define( 'ABSPATH', __DIR__ . '/' );
16+
}
17+
18+
require __DIR__ . '/../inc/Environment.php';
19+
require __DIR__ . '/../inc/Homeboy.php';
20+
21+
use DataMachineCode\Environment;
22+
use DataMachineCode\Homeboy;
23+
24+
$failures = array();
25+
$assert = function ( string $label, bool $cond ) use ( &$failures ): void {
26+
if ( $cond ) {
27+
echo "{$label}\n";
28+
return;
29+
}
30+
$failures[] = $label;
31+
echo "{$label}\n";
32+
};
33+
34+
echo "Homeboy capability helper — smoke\n";
35+
36+
// ── Branch 1: shell unavailable ────────────────────────────────
37+
// Skip when the host disabled shell_exec entirely.
38+
if ( Environment::has_shell() ) {
39+
echo " (host has shell — exercising probe paths)\n";
40+
} else {
41+
echo " (host has no shell — only the absent branch is testable)\n";
42+
Homeboy::reset_cache();
43+
$assert( 'no shell: is_available() returns false', false === Homeboy::is_available() );
44+
echo "\nOK\n";
45+
exit( 0 );
46+
}
47+
48+
$original_path = getenv( 'PATH' ) ?: '';
49+
$tmp_root = sys_get_temp_dir() . '/dmc-homeboy-smoke-' . uniqid( '', true );
50+
mkdir( $tmp_root, 0755, true );
51+
52+
// ── Branch 2: empty PATH → binary not found ────────────────────
53+
putenv( 'PATH=' . $tmp_root );
54+
Homeboy::reset_cache();
55+
$assert( 'empty PATH: is_available() returns false', false === Homeboy::is_available() );
56+
$assert( 'absent: result is memoized (second call same as first)', false === Homeboy::is_available() );
57+
58+
// ── Branch 3: synthetic homeboy on PATH → detected ─────────────
59+
$fake_bin = $tmp_root . '/homeboy';
60+
file_put_contents( $fake_bin, "#!/bin/sh\nexit 0\n" );
61+
chmod( $fake_bin, 0755 );
62+
63+
putenv( 'PATH=' . $tmp_root );
64+
Homeboy::reset_cache();
65+
$assert( 'synthetic on PATH: is_available() returns true', true === Homeboy::is_available() );
66+
67+
// Removing the binary mid-request should NOT flip the result —
68+
// detection is memoized exactly once.
69+
unlink( $fake_bin );
70+
$assert( 'memoization: result sticky after PATH change', true === Homeboy::is_available() );
71+
72+
// reset_cache() lets tests re-probe.
73+
Homeboy::reset_cache();
74+
$assert( 'reset_cache: re-probe sees current state', false === Homeboy::is_available() );
75+
76+
// ── Cleanup ────────────────────────────────────────────────────
77+
@rmdir( $tmp_root );
78+
putenv( 'PATH=' . $original_path );
79+
80+
if ( ! empty( $failures ) ) {
81+
echo "\nFAIL: " . count( $failures ) . " assertion(s)\n";
82+
foreach ( $failures as $f ) {
83+
echo " - {$f}\n";
84+
}
85+
exit( 1 );
86+
}
87+
88+
echo "\nOK\n";
89+
exit( 0 );

0 commit comments

Comments
 (0)