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
2 changes: 2 additions & 0 deletions data-machine-code.php
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ function datamachine_code_load_chat_tools() {
$tasks['worktree_cleanup'] = \DataMachineCode\Tasks\WorktreeCleanupTask::class;
$tasks['workspace_disk_emergency_cleanup'] = \DataMachineCode\Tasks\WorkspaceDiskEmergencyCleanupTask::class;
$tasks['workspace_retention_cleanup'] = \DataMachineCode\Tasks\WorkspaceRetentionCleanupTask::class;
$tasks['workspace_safe_cleanup'] = \DataMachineCode\Tasks\WorkspaceSafeCleanupTask::class;
$tasks['workspace_hygiene_report'] = \DataMachineCode\Tasks\WorkspaceHygieneReportTask::class;
return $tasks;
}
Expand Down Expand Up @@ -452,6 +453,7 @@ function datamachine_code_load_chat_tools() {
'artifact_chunk_size' => 10,
),
);
$schedules['workspace_safe_cleanup'] = \DataMachineCode\Tasks\WorkspaceSafeCleanupTask::recurringSchedule();
$schedules['workspace_hygiene_report'] = array(
'task_type' => 'workspace_hygiene_report',
'interval' => 'weekly',
Expand Down
107 changes: 107 additions & 0 deletions inc/Tasks/WorkspaceSafeCleanupTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
/**
* Safe Workspace Cleanup System Task.
*
* @package DataMachineCode\Tasks
*/

namespace DataMachineCode\Tasks;

use DataMachine\Core\PluginSettings;
use DataMachine\Engine\AI\System\Tasks\SystemTask;
use DataMachineCode\Workspace\WorkspaceSafeCleanupOrchestrator;

defined('ABSPATH') || exit;

/**
* Runs the bounded safe-cleanup orchestration on the shared maintenance schedule.
*/
class WorkspaceSafeCleanupTask extends SystemTask {

public const SETTING_KEY = 'workspace_safe_cleanup_enabled';

public function getTaskType(): string {
return 'workspace_safe_cleanup';
}

public function requiresAgentContext(): bool {
return false;
}

/** @return array<string,mixed> */
public static function getTaskMeta(): array {
return array(
'label' => 'Safe Workspace Cleanup',
'description' => 'Bounded recurring cleanup of DMC workspaces. Revalidates every candidate and preserves dirty, unpushed, and live worktrees.',
'setting_key' => self::SETTING_KEY,
'default_enabled' => true,
'supports_run' => true,
);
}

/** @return array<string,mixed> */
public static function recurringSchedule(): array {
return array(
'task_type' => 'workspace_safe_cleanup',
'interval' => 'hourly',
'enabled_setting' => self::SETTING_KEY,
'default_enabled' => true,
'label' => 'Hourly — applies bounded safe workspace cleanup',
'task_params' => array(
'source' => 'recurring_schedule',
'limit' => 25,
'passes' => 5,
'cycles' => 5,
'until_budget' => '45s',
),
);
}

/**
* Execute the canonical safe cleanup flow.
*
* Schedule parameters bound execution only. Force and unpushed-discard are
* intentionally not forwarded, so maintenance cannot weaken safety policy.
*
* @param int $jobId Job ID.
* @param array $params Task parameters.
*/
public function executeTask( int $jobId, array $params ): void {
if ( ! PluginSettings::get(self::SETTING_KEY, true) ) {
$this->completeJob(
$jobId,
array(
'skipped' => true,
'reason' => sprintf('Safe workspace cleanup disabled (PluginSettings: %s=false).', self::SETTING_KEY),
)
);
return;
}

$input = array(
'source' => (string) ( $params['source'] ?? 'system_task' ),
'limit' => isset($params['limit']) ? (int) $params['limit'] : 25,
'passes' => isset($params['passes']) ? (int) $params['passes'] : 5,
'cycles' => isset($params['cycles']) ? (int) $params['cycles'] : 5,
'until_budget' => isset($params['until_budget']) ? (string) $params['until_budget'] : '45s',
'dry_run' => ! empty($params['dry_run']),
'force' => false,
'discard_unpushed' => false,
);
$result = $this->run_safe_cleanup($input);

if ( $result instanceof \WP_Error ) {
do_action('datamachine_log', 'error', 'Safe workspace cleanup failed', array( 'task' => $this->getTaskType(), 'jobId' => $jobId, 'error' => $result->get_error_message(), 'code' => $result->get_error_code() ));
$this->failJob($jobId, $result->get_error_message());
return;
}

do_action('datamachine_log', 'info', 'Safe workspace cleanup completed.', array( 'task' => $this->getTaskType(), 'jobId' => $jobId, 'result' => $result ));
$this->completeJob($jobId, $result);
}

/** @return array<string,mixed>|\WP_Error */
protected function run_safe_cleanup( array $input ): array|\WP_Error {
return ( new WorkspaceSafeCleanupOrchestrator() )->run($input);
}
}
85 changes: 85 additions & 0 deletions tests/workspace-safe-cleanup-schedule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace DataMachine\Core {
final class PluginSettings {
public static array $settings = array();

public static function get( string $key, mixed $default = null ): mixed {
return self::$settings[ $key ] ?? $default;
}
}
}

namespace DataMachine\Engine\AI\System\Tasks {
abstract class SystemTask {
public array $completed = array();
public array $failed = array();

protected function completeJob( int $job_id, array $result ): void {
$this->completed[] = array( $job_id, $result );
}

protected function failJob( int $job_id, string $message ): void {
$this->failed[] = array( $job_id, $message );
}
}
}

namespace {
define('ABSPATH', dirname(__DIR__) . '/');
function do_action( string $hook, mixed ...$args ): void {}

final class WP_Error {
public function __construct( private string $code, private string $message ) {}
public function get_error_code(): string { return $this->code; }
public function get_error_message(): string { return $this->message; }
}

function workspace_safe_schedule_assert( bool $condition, string $message ): void {
if ( ! $condition ) {
throw new \RuntimeException($message);
}
}

require_once dirname(__DIR__) . '/inc/Tasks/WorkspaceSafeCleanupTask.php';

final class TestWorkspaceSafeCleanupTask extends \DataMachineCode\Tasks\WorkspaceSafeCleanupTask {
public array $inputs = array();
protected function run_safe_cleanup( array $input ): array|\WP_Error {
$this->inputs[] = $input;
return array( 'success' => true, 'run_id' => 'cleanup-run-safe-task', 'summary' => array( 'removed' => 1 ) );
}
}

$meta = \DataMachineCode\Tasks\WorkspaceSafeCleanupTask::getTaskMeta();
workspace_safe_schedule_assert('workspace_safe_cleanup_enabled' === $meta['setting_key'], 'task exposes its owned setting key');
workspace_safe_schedule_assert(true === $meta['default_enabled'], 'safe maintenance is enabled by default');

\DataMachine\Core\PluginSettings::$settings = array( 'workspace_safe_cleanup_enabled' => false );
$task = new TestWorkspaceSafeCleanupTask();
$task->executeTask(10, array());
workspace_safe_schedule_assert(array() === $task->inputs, 'disabled task does not invoke cleanup');
workspace_safe_schedule_assert(true === ($task->completed[0][1]['skipped'] ?? false), 'disabled task reports visible skipped state');

\DataMachine\Core\PluginSettings::$settings = array( 'workspace_safe_cleanup_enabled' => true );
$task = new TestWorkspaceSafeCleanupTask();
$task->executeTask(11, array( 'source' => 'recurring_schedule', 'limit' => 9, 'passes' => 2, 'cycles' => 3, 'until_budget' => '20s', 'force' => true, 'discard_unpushed' => true ));
$input = $task->inputs[0] ?? array();
workspace_safe_schedule_assert(false === ($input['force'] ?? true), 'scheduled cleanup cannot enable force');
workspace_safe_schedule_assert(false === ($input['discard_unpushed'] ?? true), 'scheduled cleanup cannot discard unpushed work');
workspace_safe_schedule_assert(9 === ($input['limit'] ?? 0) && 2 === ($input['passes'] ?? 0) && 3 === ($input['cycles'] ?? 0), 'schedule bounds are forwarded');
workspace_safe_schedule_assert('20s' === ($input['until_budget'] ?? ''), 'schedule time budget is forwarded');
workspace_safe_schedule_assert('cleanup-run-safe-task' === ($task->completed[0][1]['run_id'] ?? ''), 'task persists cleanup evidence in its completed result');
$schedule = \DataMachineCode\Tasks\WorkspaceSafeCleanupTask::recurringSchedule();
workspace_safe_schedule_assert('hourly' === ($schedule['interval'] ?? ''), 'maintenance schedule is hourly');
workspace_safe_schedule_assert(true === ($schedule['default_enabled'] ?? false), 'maintenance schedule defaults to enabled');
workspace_safe_schedule_assert(\DataMachineCode\Tasks\WorkspaceSafeCleanupTask::SETTING_KEY === ($schedule['enabled_setting'] ?? ''), 'maintenance schedule uses the task setting');
workspace_safe_schedule_assert(array( 'source' => 'recurring_schedule', 'limit' => 25, 'passes' => 5, 'cycles' => 5, 'until_budget' => '45s' ) === ($schedule['task_params'] ?? null), 'maintenance schedule declares bounded invocation parameters');
$source = file_get_contents(dirname(__DIR__) . '/data-machine-code.php');
workspace_safe_schedule_assert(false !== strpos($source, '$tasks[\'workspace_safe_cleanup\'] = \\DataMachineCode\\Tasks\\WorkspaceSafeCleanupTask::class'), 'task is registered with DMC');
workspace_safe_schedule_assert(false !== strpos($source, '$schedules[\'workspace_safe_cleanup\'] = \\DataMachineCode\\Tasks\\WorkspaceSafeCleanupTask::recurringSchedule()'), 'DMC binds the owned recurring schedule');

echo "workspace safe cleanup schedule test passed.\n";
}