diff --git a/lib/homeboy.sh b/lib/homeboy.sh index b8e1451..7357e9d 100644 --- a/lib/homeboy.sh +++ b/lib/homeboy.sh @@ -37,6 +37,10 @@ homeboy_run() { [ -n "${SERVICE_HOME:-}" ] && \ { [ "${WP_CODING_AGENTS_TEST_ASSUME_ROOT:-false}" = true ] || [ "$(id -u)" -eq 0 ]; } && \ command -v sudo >/dev/null 2>&1; then + if [ -n "${HOMEBOY_DATA_DIR:-}" ]; then + sudo -n -H -u "$SERVICE_USER" env HOME="$SERVICE_HOME" PATH="$PATH" HOMEBOY_DATA_DIR="$HOMEBOY_DATA_DIR" homeboy "$@" + return $? + fi sudo -n -H -u "$SERVICE_USER" env HOME="$SERVICE_HOME" PATH="$PATH" homeboy "$@" return $? fi @@ -126,6 +130,12 @@ homeboy_dmc_command_json() { # probing its safety fields; the adapter refuses any continuation. homeboy_json_array php "$SCRIPT_DIR/scripts/homeboy-dmc-provider.php" resolve_task '{task_url}' "${wp_argv[@]}" datamachine-code workspace worktree list '--task-ref={task_url}' --with-status --limit=200 --envelope --format=json "${wp_flags[@]}" ;; + task_attachment_preview) + homeboy_json_array php "$SCRIPT_DIR/scripts/homeboy-dmc-provider.php" task_attachment_preview "$provider" "$DM_WORKSPACE_DIR" '{handle}' '{task_url}' + ;; + task_attachment_apply) + homeboy_json_array php "$SCRIPT_DIR/scripts/homeboy-dmc-provider.php" task_attachment_apply '{handle}' '{task_url}' "${wp_argv[@]}" datamachine-code workspace worktree attach-tracker '{handle}' '--task-url={task_url}' --format=json "${wp_flags[@]}" + ;; ensure) # Homeboy owns each fanout worktree lifecycle. DMC verifies this complete # contract on creation and on owner-identical retries. @@ -150,12 +160,19 @@ homeboy_dmc_command_json() { homeboy_dmc_worktree_provider_json() { local provider="$1" - printf '{"enabled":true,"kind":"command","apply_enabled":true,"lookup_timeout_ms":12000,"lookup_output_limit_bytes":262144,"mutation_timeout_ms":120000,"list_result_mapping":{"items":"$","handle":"$.handle","path":"$.path","branch":"$.branch","task_url":"$.task_url","dirty":"$.safety.dirty","unpushed":"$.safety.unpushed","primary":"$.safety.primary"},"commands":{"resolve_identity":%s,"attest_safety":%s,"resolve":%s,"resolve_path":%s,"resolve_task":%s,"resolve_not_found_exit_codes":[42],"resolve_task_not_found_exit_codes":[42],"ensure":%s,"converge":%s,"plan":%s,"cleanup_preview":%s,"cleanup_apply":%s}}' \ + local task_attachment_supported="${2:-false}" + printf '{"enabled":true,"kind":"command","apply_enabled":true,"lookup_timeout_ms":12000,"lookup_output_limit_bytes":262144,"mutation_timeout_ms":120000,"list_result_mapping":{"items":"$","handle":"$.handle","path":"$.path","branch":"$.branch","task_url":"$.task_url","dirty":"$.safety.dirty","unpushed":"$.safety.unpushed","primary":"$.safety.primary"},"commands":{"resolve_identity":%s,"attest_safety":%s,"resolve":%s,"resolve_path":%s,"resolve_task":%s' \ "$(homeboy_dmc_command_json resolve_identity "$provider")" \ "$(homeboy_dmc_command_json attest_safety "$provider")" \ "$(homeboy_dmc_command_json resolve "$provider")" \ "$(homeboy_dmc_command_json resolve_path "$provider")" \ - "$(homeboy_dmc_command_json resolve_task "$provider")" \ + "$(homeboy_dmc_command_json resolve_task "$provider")" + if [ "$task_attachment_supported" = true ]; then + printf ',"task_attachment_preview":%s,"task_attachment_apply":%s' \ + "$(homeboy_dmc_command_json task_attachment_preview "$provider")" \ + "$(homeboy_dmc_command_json task_attachment_apply "$provider")" + fi + printf ',"resolve_not_found_exit_codes":[42],"resolve_task_not_found_exit_codes":[42],"ensure":%s,"converge":%s,"plan":%s,"cleanup_preview":%s,"cleanup_apply":%s}}' \ "$(homeboy_dmc_command_json ensure "$provider")" \ "$(homeboy_dmc_command_json converge "$provider")" \ "$(homeboy_dmc_command_json plan "$provider")" \ @@ -163,6 +180,41 @@ homeboy_dmc_worktree_provider_json() { "$(homeboy_dmc_command_json cleanup_apply "$provider")" } +homeboy_dmc_task_attachment_capable() { + local provider="$1" response + response="$(php "$provider" capabilities 2>/dev/null)" || return 1 + printf '%s' "$response" | python3 -c ' +import json +import sys + +try: + payload = json.load(sys.stdin) +except Exception: + sys.exit(1) + +sys.exit(0 if ( + payload.get("schema") == "datamachine-code/worktree-provider-capabilities/v1" + and "task_url" in payload.get("tracker_fields", []) + and payload.get("attachment_operation") == "datamachine-code/workspace-worktree-attach-tracker" + and payload.get("attachment_standalone") is False +) else 1) +' >/dev/null 2>&1 +} + +homeboy_task_attachment_commands_supported() { + local probe_root probe_provider + probe_root="$(mktemp -d)" || return 1 + probe_provider='{"enabled":false,"kind":"command","apply_enabled":false,"commands":{"task_attachment_preview":["true"],"task_attachment_apply":["true"]}}' + if HOMEBOY_DATA_DIR="$probe_root" homeboy_run config set /worktree_providers/wpca-task-attachment-probe "$probe_provider" >/dev/null 2>&1 && + HOMEBOY_DATA_DIR="$probe_root" homeboy_run config show /worktree_providers/wpca-task-attachment-probe/commands/task_attachment_preview >/dev/null 2>&1 && + HOMEBOY_DATA_DIR="$probe_root" homeboy_run config show /worktree_providers/wpca-task-attachment-probe/commands/task_attachment_apply >/dev/null 2>&1; then + rm -rf "$probe_root" + return 0 + fi + rm -rf "$probe_root" + return 1 +} + homeboy_dmc_worktree_provider_ready() { local provider="$1" response @@ -551,12 +603,15 @@ configure_homeboy_dmc_worktree_provider() { return 0 fi - local provider provider_json + local provider provider_json task_attachment_supported=false provider="$(homeboy_dmc_worktree_provider_executable)" || { homeboy_handle_failure "Data Machine Code standalone worktree provider source contract is unavailable; skipping Homeboy DMC worktree provider setup." return 0 } - provider_json="$(homeboy_dmc_worktree_provider_json "$provider")" + if homeboy_dmc_task_attachment_capable "$provider" && homeboy_task_attachment_commands_supported; then + task_attachment_supported=true + fi + provider_json="$(homeboy_dmc_worktree_provider_json "$provider" "$task_attachment_supported")" if [ "${DRY_RUN:-false}" = true ]; then echo -e "${BLUE}[dry-run]${NC} homeboy config set /worktree_providers/dmc '$provider_json'" diff --git a/scripts/homeboy-dmc-provider.php b/scripts/homeboy-dmc-provider.php index b14d9e7..fa8cced 100644 --- a/scripts/homeboy-dmc-provider.php +++ b/scripts/homeboy-dmc-provider.php @@ -13,6 +13,7 @@ // and report a typed adapter failure instead of being terminated externally. const HOMEBOY_DMC_TASK_LOOKUP_TIMEOUT_SECONDS = 8; const HOMEBOY_DMC_TASK_TERMINATION_GRACE_SECONDS = 1; +const HOMEBOY_DMC_ATTACHMENT_TIMEOUT_SECONDS = 30; if ( '_session_exec' === $operation ) { $command = array_slice($argv, 2); @@ -43,7 +44,7 @@ } /** @return array{status:int,stdout:string,stderr:string,failure:?string} */ -$run_bounded_command = static function ( array $command, int $stdout_limit, int $stderr_limit ): array { +$run_bounded_command = static function ( array $command, int $stdout_limit, int $stderr_limit, int $timeout_seconds = HOMEBOY_DMC_TASK_LOOKUP_TIMEOUT_SECONDS ): array { $launcher = array_merge(array( PHP_BINARY, __FILE__, '_session_exec' ), $command); $process = proc_open($launcher, array( 1 => array( 'pipe', 'w' ), 2 => array( 'pipe', 'w' ), 3 => array( 'pipe', 'w' ) ), $pipes); if ( ! is_resource($process) ) { @@ -69,7 +70,7 @@ $failure = 'session'; $termination_started_at = microtime(true); } - if ( null === $failure && microtime(true) - $started_at >= HOMEBOY_DMC_TASK_LOOKUP_TIMEOUT_SECONDS ) { + if ( null === $failure && microtime(true) - $started_at >= $timeout_seconds ) { $failure = 'timeout'; $termination_started_at = microtime(true); } @@ -134,7 +135,7 @@ // Reap the leader before probing its group so a Linux zombie cannot keep kill(-pgid, 0) true. $status = proc_close($process); while ( $session_ready && null !== $pid && $pid > 0 && @posix_kill(-$pid, 0) ) { - if ( null === $failure && microtime(true) - $started_at >= HOMEBOY_DMC_TASK_LOOKUP_TIMEOUT_SECONDS ) { + if ( null === $failure && microtime(true) - $started_at >= $timeout_seconds ) { $failure = 'timeout'; $termination_started_at = microtime(true); } @@ -172,6 +173,137 @@ static function ( array $matches ): string { ) ?? ''; }; +$task_attachment_result = static function ( array $identity, string $task_url, string $status ): array { + return array( + 'schema' => 'homeboy/worktree-provider-task-attachment/v1', + 'provider_id' => 'dmc', + 'handle' => $identity['handle'], + 'task_url' => $task_url, + 'path' => $identity['path'], + 'branch' => $identity['branch'], + 'primary' => false, + 'status' => $status, + ); +}; + +if ( 'task_attachment_preview' === $operation ) { + $provider = (string) ( $argv[2] ?? '' ); + $workspace = (string) ( $argv[3] ?? '' ); + $handle = (string) ( $argv[4] ?? '' ); + $task_url = $canonical_task_url((string) ( $argv[5] ?? '' )); + if ( '' === $provider || '' === $workspace || '' === $handle || '' === $task_url ) { + fwrite(STDERR, "Usage: homeboy-dmc-provider.php task_attachment_preview \n"); + exit(2); + } + try { + $identity_capture = $run_bounded_command(array( PHP_BINARY, $provider, 'identity', $workspace, $handle ), HOMEBOY_DMC_TASK_MAX_OUTPUT_BYTES, HOMEBOY_DMC_TASK_MAX_DMC_STDERR_BYTES); + } catch (Throwable $error) { + fwrite(STDERR, $error->getMessage() . "\n"); + exit(1); + } + if ( null !== $identity_capture['failure'] ) { + fwrite(STDERR, "DMC tracker-attachment preview exceeded its bounded execution or capture.\n"); + exit(1); + } + if ( 0 !== $identity_capture['status'] ) { + fwrite(STDERR, $identity_capture['stderr']); + exit($identity_capture['status'] > 0 && $identity_capture['status'] < 256 ? $identity_capture['status'] : 1); + } + try { + $identity = json_decode($identity_capture['stdout'], true, 512, JSON_THROW_ON_ERROR); + } catch (Throwable $error) { + fwrite(STDERR, "DMC tracker-attachment preview returned invalid identity JSON.\n"); + exit(1); + } + if ( is_array($identity) && in_array((string) ( $identity['status'] ?? '' ), array( 'not_owned', 'not_found' ), true) ) { + fwrite(STDOUT, "{\"status\":\"not_owned\"}\n"); + exit(0); + } + if ( + ! is_array($identity) || 'datamachine-code/worktree-identity/v1' !== ( $identity['schema'] ?? null ) + || $handle !== ( $identity['handle'] ?? null ) || ! is_string($identity['path'] ?? null) || '' === $identity['path'] + || ! is_string($identity['branch'] ?? null) || '' === $identity['branch'] || false !== ( $identity['primary'] ?? null ) + ) { + fwrite(STDERR, "DMC tracker-attachment preview returned an incomplete exact identity.\n"); + exit(1); + } + $existing_task_url = $canonical_task_url((string) ( $identity['task_url'] ?? '' )); + if ( '' !== $existing_task_url && $task_url !== $existing_task_url ) { + fwrite(STDERR, "DMC worktree already has conflicting tracker ownership.\n"); + exit(1); + } + if ( '' === $existing_task_url ) { + try { + $safety_capture = $run_bounded_command(array( PHP_BINARY, $provider, 'safety', $workspace, (string) ( $identity['token'] ?? '' ) ), HOMEBOY_DMC_TASK_MAX_OUTPUT_BYTES, HOMEBOY_DMC_TASK_MAX_DMC_STDERR_BYTES); + } catch (Throwable $error) { + fwrite(STDERR, $error->getMessage() . "\n"); + exit(1); + } + if ( null !== $safety_capture['failure'] ) { + fwrite(STDERR, "DMC tracker-attachment preview exceeded its bounded execution or capture.\n"); + exit(1); + } + if ( 0 !== $safety_capture['status'] ) { + fwrite(STDERR, $safety_capture['stderr']); + exit($safety_capture['status'] > 0 && $safety_capture['status'] < 256 ? $safety_capture['status'] : 1); + } + $safety = json_decode($safety_capture['stdout'], true); + if ( ! is_array($safety) || 'datamachine-code/worktree-safety/v1' !== ( $safety['schema'] ?? null ) || true !== ( $safety['fresh'] ?? null ) || false !== ( $safety['dirty'] ?? null ) ) { + fwrite(STDERR, "DMC tracker-attachment preview requires a fresh clean worktree.\n"); + exit(1); + } + } + fwrite(STDOUT, json_encode($task_attachment_result($identity, $task_url, '' === $existing_task_url ? 'eligible' : 'already_attached'), JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n"); + exit(0); +} + +if ( 'task_attachment_apply' === $operation ) { + $handle = (string) ( $argv[2] ?? '' ); + $task_url = $canonical_task_url((string) ( $argv[3] ?? '' )); + $command = array_slice($argv, 4); + if ( '' === $handle || '' === $task_url || array() === $command ) { + fwrite(STDERR, "Usage: homeboy-dmc-provider.php task_attachment_apply \n"); + exit(2); + } + foreach ( $command as $index => $argument ) { + $command[ $index ] = str_replace(array( '{handle}', '{task_url}' ), array( $handle, $task_url ), $argument); + } + try { + $capture = $run_bounded_command($command, HOMEBOY_DMC_TASK_MAX_DMC_STDOUT_BYTES, HOMEBOY_DMC_TASK_MAX_DMC_STDERR_BYTES, HOMEBOY_DMC_ATTACHMENT_TIMEOUT_SECONDS); + } catch (Throwable $error) { + fwrite(STDERR, $error->getMessage() . "\n"); + exit(1); + } + if ( null !== $capture['failure'] ) { + fwrite(STDERR, "DMC tracker attachment exceeded its bounded execution or capture.\n"); + exit(1); + } + if ( 0 !== $capture['status'] ) { + fwrite(STDERR, $capture['stderr']); + exit($capture['status'] > 0 && $capture['status'] < 256 ? $capture['status'] : 1); + } + try { + $attached = json_decode($capture['stdout'], true, 512, JSON_THROW_ON_ERROR); + } catch (Throwable $error) { + fwrite(STDERR, "DMC tracker attachment returned invalid JSON.\n"); + exit(1); + } + $identity = is_array($attached) && is_array($attached['provider_resolution'] ?? null) ? $attached['provider_resolution'] : null; + $status = is_array($attached) ? (string) ( $attached['status'] ?? '' ) : ''; + if ( + ! is_array($identity) || ! in_array($status, array( 'attached', 'already_attached' ), true) + || $handle !== ( $attached['handle'] ?? null ) || $handle !== ( $identity['handle'] ?? null ) + || $task_url !== $canonical_task_url((string) ( $identity['task_url'] ?? '' )) + || ! is_string($identity['path'] ?? null) || '' === $identity['path'] || ! is_string($identity['branch'] ?? null) || '' === $identity['branch'] + || false !== ( $identity['primary'] ?? null ) + ) { + fwrite(STDERR, "DMC tracker attachment returned incomplete or mismatched exact evidence.\n"); + exit(1); + } + fwrite(STDOUT, json_encode($task_attachment_result($identity, $task_url, $status), JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n"); + exit(0); +} + if ( 'resolve_task' === $operation ) { $task_url = $canonical_task_url((string) ( $argv[2] ?? '' )); $command = array_slice($argv, 3); diff --git a/tests/homeboy-dmc-provider.sh b/tests/homeboy-dmc-provider.sh index 9871486..40ca62f 100644 --- a/tests/homeboy-dmc-provider.sh +++ b/tests/homeboy-dmc-provider.sh @@ -77,6 +77,8 @@ expected_plan = ["php", f"{sys.argv[2]}/scripts/homeboy-dmc-provider.php", "plan expected_identity = ["php", f"{sys.argv[2]}/scripts/homeboy-dmc-provider.php", "identity", sys.argv[5], sys.argv[4], "{handle}"] expected_safety = ["php", f"{sys.argv[2]}/scripts/homeboy-dmc-provider.php", "safety", sys.argv[5], sys.argv[4], "{identity}"] expected_converge = ["php", f"{sys.argv[2]}/scripts/homeboy-dmc-provider.php", "converge", sys.argv[5], sys.argv[4], "{identity}", "{base}"] +expected_attachment_preview = ["php", f"{sys.argv[2]}/scripts/homeboy-dmc-provider.php", "task_attachment_preview", sys.argv[5], sys.argv[4], "{handle}", "{task_url}"] +expected_attachment_apply = ["php", f"{sys.argv[2]}/scripts/homeboy-dmc-provider.php", "task_attachment_apply", "{handle}", "{task_url}", "studio", "wp", "datamachine-code", "workspace", "worktree", "attach-tracker", "{handle}", "--task-url={task_url}", "--format=json", f"--path={sys.argv[3]}"] if provider.get("lookup_timeout_ms") != 12000: raise SystemExit("FAIL: Homeboy must reserve a supervision margin beyond the DMC adapter budget") adapter = open(commands["resolve_task"][1], encoding="utf-8").read() @@ -111,6 +113,10 @@ if commands.get("attest_safety") != expected_safety: raise SystemExit(f"FAIL: DMC standalone safety mapping mismatch: {commands.get('attest_safety')!r}") if commands.get("converge") != expected_converge: raise SystemExit(f"FAIL: DMC convergence mapping must bind the opaque identity and pinned base: {commands.get('converge')!r}") +if commands.get("task_attachment_preview") != expected_attachment_preview: + raise SystemExit(f"FAIL: DMC task-attachment preview mapping mismatch: {commands.get('task_attachment_preview')!r}") +if commands.get("task_attachment_apply") != expected_attachment_apply: + raise SystemExit(f"FAIL: DMC task-attachment apply mapping mismatch: {commands.get('task_attachment_apply')!r}") if "list" in commands: raise SystemExit("FAIL: DMC provider must not advertise unsupported generic list capability") PY @@ -351,6 +357,77 @@ assert_descendant_stopped("descendant_silent", "execution exceeded the adapter b PY } +assert_task_attachment_contract() { + python3 - "$1" <<'PY' +import json +import os +import subprocess +import sys + +_, payload = open(sys.argv[1], encoding="utf-8").read().strip().split("|", 1) +commands = json.loads(payload)["commands"] +handle = "static-site-importer@refactor-1306-runtime-entity-form-materialization" +task_url = "https://github.com/Automattic/static-site-importer/issues/1306" +values = {"handle": handle, "task_url": task_url} + +def run(name, env=None): + command = [part.format(**values) for part in commands[name]] + return subprocess.run(command, text=True, capture_output=True, env=env or os.environ.copy()) + +preview = run("task_attachment_preview") +expected = { + "schema": "homeboy/worktree-provider-task-attachment/v1", + "provider_id": "dmc", + "handle": handle, + "task_url": task_url, + "path": os.environ["DMC_ATTACHMENT_PATH"], + "branch": "refactor-1306-runtime-entity-form-materialization", + "primary": False, + "status": "eligible", +} +if preview.returncode or json.loads(preview.stdout) != expected: + raise SystemExit(f"FAIL: read-only attachment preview did not return exact eligible evidence: {preview!r}") +if os.path.exists(os.environ["DMC_ATTACHMENT_STATE"]): + raise SystemExit("FAIL: task-attachment preview mutated tracker metadata") + +applied = run("task_attachment_apply") +if applied.returncode or json.loads(applied.stdout) != {**expected, "status": "attached"}: + raise SystemExit(f"FAIL: task-attachment apply did not adapt DMC attached evidence: {applied!r}") +if not os.path.exists(os.environ["DMC_ATTACHMENT_STATE"]): + raise SystemExit("FAIL: task-attachment apply did not invoke authorized DMC attachment") + +resolved = subprocess.run( + [part.replace("{handle}", handle) for part in commands["resolve"]], + text=True, + capture_output=True, + env=os.environ.copy(), +) +resolved_expected = [{ + "handle": handle, + "path": os.environ["DMC_ATTACHMENT_PATH"], + "branch": "refactor-1306-runtime-entity-form-materialization", + "task_url": task_url, + "safety": {"dirty": False, "unpushed": False, "primary": False}, +}] +if resolved.returncode or json.loads(resolved.stdout) != resolved_expected: + raise SystemExit(f"FAIL: attached #1306 worktree did not resolve normally: {resolved!r}") + +replay_preview = run("task_attachment_preview") +if replay_preview.returncode or json.loads(replay_preview.stdout) != {**expected, "status": "already_attached"}: + raise SystemExit(f"FAIL: attachment replay preview was not idempotent: {replay_preview!r}") +replay_apply = run("task_attachment_apply") +if replay_apply.returncode or json.loads(replay_apply.stdout) != {**expected, "status": "already_attached"}: + raise SystemExit(f"FAIL: DMC already-attached apply result was not preserved: {replay_apply!r}") + +for mode in ("conflict", "dirty", "foreign", "ambiguous", "inactive"): + env = os.environ.copy() + env["DMC_ATTACHMENT_ERROR"] = mode + failure = run("task_attachment_apply", env) + if failure.returncode != 17 or failure.stdout or failure.stderr != f"dmc-{mode}\n": + raise SystemExit(f"FAIL: bounded DMC {mode} stderr/exit failure was not preserved: {failure!r}") +PY +} + FAKE_BIN="$TMP/bin" mkdir -p "$FAKE_BIN" @@ -362,6 +439,22 @@ $value = $argv[3] ?? ''; $base = $argv[4] ?? ''; file_put_contents(getenv('DMC_PROVIDER_LOG'), $operation . "|" . $value . ('' === $base ? '' : "|" . $base) . "\n", FILE_APPEND); if ('identity' === $operation) { + $attachment_handle = 'static-site-importer@refactor-1306-runtime-entity-form-materialization'; + if ($attachment_handle === $value) { + $task_url = file_exists(getenv('DMC_ATTACHMENT_STATE')) ? 'https://github.com/Automattic/static-site-importer/issues/1306' : null; + echo json_encode(array( + 'schema' => 'datamachine-code/worktree-identity/v1', + 'status' => 'owned', + 'token' => 'attachment-token', + 'handle' => $value, + 'path' => getenv('DMC_ATTACHMENT_PATH'), + 'branch' => 'refactor-1306-runtime-entity-form-materialization', + 'primary' => false, + 'task_url' => $task_url, + 'latency_ms' => 1, + )) . "\n"; + exit(0); + } if (!file_exists(getenv('DMC_STATE'))) { echo json_encode(array('schema' => 'datamachine-code/worktree-identity/v1', 'status' => 'not_owned', 'ownership' => 'not_owned')) . "\n"; exit(0); @@ -381,6 +474,19 @@ if ('identity' === $operation) { )) . "\n"; exit(0); } +if ('capabilities' === $operation) { + if ('legacy' === getenv('DMC_CAPABILITIES_MODE')) { + fwrite(STDERR, "unsupported fixture request\n"); + exit(2); + } + echo json_encode(array( + 'schema' => 'datamachine-code/worktree-provider-capabilities/v1', + 'tracker_fields' => array('task_url', 'task_ref'), + 'attachment_operation' => 'datamachine-code/workspace-worktree-attach-tracker', + 'attachment_standalone' => false, + )) . "\n"; + exit(0); +} if ('safety' === $operation && 'fixture-token' === $value) { echo json_encode(array( 'schema' => 'datamachine-code/worktree-safety/v1', @@ -394,6 +500,19 @@ if ('safety' === $operation && 'fixture-token' === $value) { )) . "\n"; exit(0); } +if ('safety' === $operation && 'attachment-token' === $value) { + echo json_encode(array( + 'schema' => 'datamachine-code/worktree-safety/v1', + 'status' => 'attested', + 'identity_token' => $value, + 'observed_at' => '2026-08-25T00:00:00Z', + 'dirty' => false, + 'unpushed' => false, + 'fresh' => true, + 'latency_ms' => 2, + )) . "\n"; + exit(0); +} if ('converge' === $operation && 'fixture-token' === $value) { $mode = getenv('DMC_CONVERGENCE_MODE'); if ('success' === $mode) { @@ -423,9 +542,17 @@ chmod +x "$TMP/dmc-source/bin/dmc-worktree-provider" cat > "$FAKE_BIN/homeboy" <<'SH' #!/bin/sh if [ "$1 $2" = "config set" ]; then + if [ "$3" = "/worktree_providers/wpca-task-attachment-probe" ]; then + [ "${HOMEBOY_ATTACHMENT_COMMANDS:-true}" = true ] && : > "$HOMEBOY_DATA_DIR/task-attachment-supported" + exit 0 + fi printf '%s|%s\n' "$3" "$4" >> "$HOMEBOY_CONFIG_LOG" exit 0 fi +if [ "$1 $2" = "config show" ]; then + [ "${HOMEBOY_ATTACHMENT_COMMANDS:-true}" = true ] && [ -f "$HOMEBOY_DATA_DIR/task-attachment-supported" ] && exit 0 + exit 2 +fi exit 2 SH chmod +x "$FAKE_BIN/homeboy" @@ -504,6 +631,20 @@ if [ "$1 $2 $3 $4 $5" = "wp datamachine-code workspace worktree provider" ]; the printf '{"schema":"datamachine-code/standalone-worktree-provider-command/v1","executable":"%s"}\n' "$DMC_PROVIDER_EXECUTABLE" exit 0 fi +if [ "$1 $2 $3 $4 $5" = "wp datamachine-code workspace worktree attach-tracker" ]; then + [ "$6" = "static-site-importer@refactor-1306-runtime-entity-form-materialization" ] || exit 2 + case "$*" in + *--task-url=https://github.com/Automattic/static-site-importer/issues/1306*--format=json*"--path=$SITE_PATH"*) ;; + *) exit 2 ;; + esac + if [ -n "${DMC_ATTACHMENT_ERROR:-}" ]; then + printf 'dmc-%s\n' "$DMC_ATTACHMENT_ERROR" >&2 + exit 17 + fi + if [ -f "$DMC_ATTACHMENT_STATE" ]; then status=already_attached; else status=attached; : > "$DMC_ATTACHMENT_STATE"; fi + printf '{"success":true,"status":"%s","handle":"%s","provider_resolution":{"schema":"datamachine-code/worktree-identity/v1","status":"owned","handle":"%s","path":"%s","branch":"refactor-1306-runtime-entity-form-materialization","primary":false,"task_url":"https://github.com/Automattic/static-site-importer/issues/1306"}}\n' "$status" "$6" "$6" "$DMC_ATTACHMENT_PATH" + exit 0 +fi if [ "$1 $2 $3 $4 $5" = "wp datamachine-code workspace worktree plan" ]; then [ "$6" = "blocks-engine" ] && [ "$7" = "fix/406-dmc-provider-plan" ] || exit 2 case "$*" in @@ -543,9 +684,11 @@ DMC_PROVIDER_LOG="$TMP/dmc-provider.log" DMC_PROVIDER_EXECUTABLE="$SITE_PATH/wp-content/plugins/data-machine-code/bin/dmc-worktree-provider" DMC_CHILD_PID="$TMP/dmc-child.pid" DMC_DESCENDANT_PID="$TMP/dmc-descendant.pid" +DMC_ATTACHMENT_STATE="$TMP/dmc-attachment-state" +DMC_ATTACHMENT_PATH="$DM_WORKSPACE_DIR/static-site-importer@refactor-1306-runtime-entity-form-materialization" : > "$DMC_ENSURE_LOG" : > "$STUDIO_LOG" -export HOMEBOY_CONFIG_LOG STUDIO_LOG DMC_STATE DMC_ENSURE_LOG DMC_PLAN_PATH DMC_PROVIDER_LOG DMC_PROVIDER_EXECUTABLE DMC_CHILD_PID DMC_DESCENDANT_PID +export HOMEBOY_CONFIG_LOG STUDIO_LOG DMC_STATE DMC_ENSURE_LOG DMC_PLAN_PATH DMC_PROVIDER_LOG DMC_PROVIDER_EXECUTABLE DMC_CHILD_PID DMC_DESCENDANT_PID DMC_ATTACHMENT_STATE DMC_ATTACHMENT_PATH SITE_PATH # macOS ships Bash 3.2, which has no mapfile/readarray builtin. Disable it # when the test runs under newer Bash so this path stays portable. @@ -561,6 +704,8 @@ assert_contains "\"converge\":[\"php\",\"$SCRIPT_DIR/scripts/homeboy-dmc-provide assert_contains "\"resolve\":[\"php\",\"$SCRIPT_DIR/scripts/homeboy-dmc-provider.php\",\"resolve\",\"$DMC_PROVIDER_EXECUTABLE\",\"$DM_WORKSPACE_DIR\",\"{handle}\"]" "$TMP/dry-run.log" assert_contains "\"resolve_path\":[\"php\",\"$SCRIPT_DIR/scripts/homeboy-dmc-provider.php\",\"resolve\",\"$DMC_PROVIDER_EXECUTABLE\",\"$DM_WORKSPACE_DIR\",\"{path}\"]" "$TMP/dry-run.log" assert_contains "\"resolve_task\":[\"php\",\"$SCRIPT_DIR/scripts/homeboy-dmc-provider.php\",\"resolve_task\",\"{task_url}\",\"studio\",\"wp\",\"datamachine-code\",\"workspace\",\"worktree\",\"list\",\"--task-ref={task_url}\",\"--with-status\",\"--limit=200\",\"--envelope\",\"--format=json\",\"--path=$SITE_PATH\"]" "$TMP/dry-run.log" +assert_contains "\"task_attachment_preview\":[\"php\",\"$SCRIPT_DIR/scripts/homeboy-dmc-provider.php\",\"task_attachment_preview\",\"$DMC_PROVIDER_EXECUTABLE\",\"$DM_WORKSPACE_DIR\",\"{handle}\",\"{task_url}\"]" "$TMP/dry-run.log" +assert_contains "\"task_attachment_apply\":[\"php\",\"$SCRIPT_DIR/scripts/homeboy-dmc-provider.php\",\"task_attachment_apply\",\"{handle}\",\"{task_url}\",\"studio\",\"wp\",\"datamachine-code\",\"workspace\",\"worktree\",\"attach-tracker\",\"{handle}\",\"--task-url={task_url}\",\"--format=json\",\"--path=$SITE_PATH\"]" "$TMP/dry-run.log" assert_contains "\"resolve_not_found_exit_codes\":[42]" "$TMP/dry-run.log" assert_contains "\"resolve_task_not_found_exit_codes\":[42]" "$TMP/dry-run.log" assert_contains "\"ensure\":[\"studio\",\"wp\",\"datamachine-code\",\"workspace\",\"worktree\",\"add\",\"{repo}\",\"{head}\",\"--from={base}\",\"--task-url={task_url}\",\"--reuse-policy=isolated\",\"--purpose={purpose}\",\"--owner-run-ref={owner_run_ref}\",\"--cleanup-policy={cleanup_policy}\",\"--format=json\",\"--path=$SITE_PATH\"]" "$TMP/dry-run.log" @@ -585,6 +730,7 @@ assert_provisioning_contract "$HOMEBOY_CONFIG_LOG" assert_convergence_contract "$HOMEBOY_CONFIG_LOG" assert_resolution_contract "$HOMEBOY_CONFIG_LOG" assert_task_resolution_contract "$HOMEBOY_CONFIG_LOG" +assert_task_attachment_contract "$HOMEBOY_CONFIG_LOG" assert_not_contains 'wp datamachine-code workspace worktree list fixture --all --full --format=json' "$STUDIO_LOG" assert_contains "\"cleanup_apply\":[\"studio\",\"wp\",\"datamachine-code\",\"workspace\",\"cleanup\",\"safe\",\"--format=json\",\"--path=$SITE_PATH\"]" "$HOMEBOY_CONFIG_LOG" @@ -596,6 +742,24 @@ rm -f "$DMC_STATE" : > "$HOMEBOY_CONFIG_LOG" configure_homeboy_dmc_worktree_provider > "$TMP/source-contract.log" assert_provider_contract "$HOMEBOY_CONFIG_LOG" "$SCRIPT_DIR" "$SITE_PATH" + +# Either missing owning contract keeps the legacy provider JSON byte shape: no +# half-configured attachment key is written. +DMC_CAPABILITIES_MODE=legacy +export DMC_CAPABILITIES_MODE +: > "$HOMEBOY_CONFIG_LOG" +configure_homeboy_dmc_worktree_provider > "$TMP/legacy-dmc.log" +assert_not_contains 'task_attachment_preview' "$HOMEBOY_CONFIG_LOG" +assert_not_contains 'task_attachment_apply' "$HOMEBOY_CONFIG_LOG" +unset DMC_CAPABILITIES_MODE + +HOMEBOY_ATTACHMENT_COMMANDS=false +export HOMEBOY_ATTACHMENT_COMMANDS +: > "$HOMEBOY_CONFIG_LOG" +configure_homeboy_dmc_worktree_provider > "$TMP/legacy-homeboy.log" +assert_not_contains 'task_attachment_preview' "$HOMEBOY_CONFIG_LOG" +assert_not_contains 'task_attachment_apply' "$HOMEBOY_CONFIG_LOG" +unset HOMEBOY_ATTACHMENT_COMMANDS assert_contains "$DMC_PROVIDER_EXECUTABLE" "$HOMEBOY_CONFIG_LOG" assert_not_contains "$SITE_PATH/wp-content/plugins/data-machine-code/bin/dmc-worktree-provider" "$HOMEBOY_CONFIG_LOG" : > "$DMC_STATE"