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
13 changes: 13 additions & 0 deletions PLATFORMCLAW.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ These areas are candidates for future evaluation and migration. They are not an

Bring upstream changes from `upstream/main` into a sync branch, validate them there, and merge them into `main` through a PR.

## Development resource lifecycle

Keep one primary checkout per developer. Create task-owned linked worktrees with
`node scripts/platformclaw-dev-task.mjs create <task> --branch fix/<topic>` instead
of making another independent clone for each task. Use `run` for bounded commands,
`image` for disposable validation images, and `finish <task> --apply` when the work
is complete. The [developer workflow](scripts/README.md#platformclaw-development-resources)
includes dependency reuse, interrupted-task recovery, and maintenance setup.

Managed cleanup never adopts old folders by name or age. Audit existing clones
and worktrees before removing them. Product data, volumes, active tasks, and
uncommitted or unrecognized files remain protected; a blocked cleanup reports why.

## Current Phase

Production Docker deployment migration and validation remains in progress.
Expand Down
21 changes: 21 additions & 0 deletions scripts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
- If a script writes generated artifacts, keep the source-of-truth generator, the package script, and the matching verification/check command aligned.
- Prefer additive generator/check pairs like `*:gen` and `*:check` over one-off undocumented scripts.

## PlatformClaw Development Resources

- Keep one primary clone. Prefer `platformclaw-dev-task.mjs create` for task
worktrees over independent clones or ad hoc source copies. Use the managed
task's scratch directory for disposable inputs, not unrelated host folders.
- Run bounded commands through `platformclaw-dev-task.mjs run`; build disposable
proof images through its `image` command. An exported task prefix alone does
not make arbitrary Docker resources managed. Do not daemonize child commands.
- Finish tasks explicitly with `finish <task> --apply`, including failed work.
Periodic `platformclaw-dev-cleanup.mjs` sweeps only completed managed tasks.
Unknown or interrupted ownership is a visible stop condition, not an age-based
deletion opportunity. Confirm the exact task and its descendants stopped
before using `recover <task> --confirm-stopped`.
- Never force-remove dirty, untracked, ignored, or container-referenced resources.
Keep the PR wrapper's worktree GC and operation locks under `scripts/pr`;
do not bypass them through the managed-task cleanup path.
- Reuse repository toolchain wrappers for compatible focused host checks. Never
install dependencies in Windows linked worktrees. Shared download caches and
junctions are not equivalent to duplicate physical dependency trees; isolated
Linux checks keep dependencies on Linux-owned filesystems.

## Scope

- Keep script-runner behavior, wrapper expectations, and generated-artifact guidance here.
Expand Down
124 changes: 124 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,128 @@ new directory taxonomy.
- Add or update focused tests under `test/scripts/` when changing script
behavior.

## PlatformClaw Development Resources

Use one primary checkout per developer and Node 24.15 or newer. Managed task
commands run on Windows or Linux; on other hosts use a Linux development
environment for those commands. The resource
tool itself needs only Node and Git; it works before package installation.
Install the
repository's pinned dependencies once in the primary checkout when host
installation is permitted. Never run `pnpm install` in a Windows linked worktree.
Focused host checks use existing repository wrappers to reuse a compatible
primary toolchain; heavy checks use local Linux Docker or healthy WSL with
Linux-owned source and dependencies, following the root validation policy.
Share package download caches, not arbitrary `node_modules` directories across
different lockfiles or runtimes. A junction points at an existing installation;
counting its target again overstates physical disk use.

From the primary checkout, create a task instead of another independent clone:

```sh
git fetch origin main
node scripts/platformclaw-dev-task.mjs create login-fix --branch fix/login-fix --ref origin/main
node scripts/platformclaw-dev-task.mjs list
```

`create` prints the worktree path under `.worktrees/`; open that path in your
editor. The task has its own scratch directory under `.artifacts/dev-tasks/`.
All linked invocations share one development-only SQLite registry in the Git
common directory. No Gateway or agent database is used.

Run a bounded command from the primary checkout against that task's source:

```sh
node scripts/platformclaw-dev-task.mjs run login-fix -- node scripts/run-vitest.mjs test/scripts/platformclaw-dev-task.test.ts
```

The command receives task-local `TMP`, `TEMP`, and `TMPDIR`. Normal command
success and failure both join the child and clean its disposable scratch files;
the source worktree remains for further edits. Do not use `run` to launch
detached servers or other background work. Keep any result you need outside the
temporary directory and exclude private data from committed evidence.

Build disposable Docker images through the task owner:

```sh
node scripts/platformclaw-dev-task.mjs image login-fix proof -- -f Dockerfile.proof .
```

Replace the Dockerfile and context with your actual validation inputs; pass
short flags separately (`-f Dockerfile`, not grouped or attached forms). `image`
prints its task-specific tag and adds ownership metadata. Direct `docker build`,
independent clones, arbitrary temporary folders, and preexisting resources are
not automatically adopted. Release images and release archives remain under
the release builder's retention policy, not the disposable-proof lifecycle.
Use `docker run --rm` for bounded proof containers. The task tool never stops
containers or deletes volumes; a remaining container reference blocks cleanup.

After committing or discarding your intended edits, finish from the primary
checkout. Preview first, then apply:

```sh
node scripts/platformclaw-dev-task.mjs finish login-fix
node scripts/platformclaw-dev-task.mjs finish login-fix --apply
node scripts/platformclaw-dev-task.mjs list
```

Finishing deletes only the task's disposable resources and a clean worktree;
the Git branch remains. Dirty, untracked, ignored, or container-referenced
resources are retained with an explanation. Remove or relocate those files
deliberately, then retry. Do not use force deletion to make cleanup appear clean.
Periodic sweeping retries finished tasks; it never completes active tasks for
you or removes product volumes and databases.

A killed wrapper or uncertain child outcome is not normal completion. First
verify the original command and all of its descendants stopped, then recover
that exact task and finish it:

```sh
node scripts/platformclaw-dev-task.mjs recover login-fix --confirm-stopped
node scripts/platformclaw-dev-task.mjs finish login-fix --apply
```

Inspect legacy independent clones and unmanaged worktrees without deleting them:

```sh
node scripts/platformclaw-dev-task.mjs audit
node scripts/platformclaw-dev-cleanup.mjs --skip-final-images --skip-archives
```

Classify legacy folders separately using their Git state, merged status, recent
changes, and actual running consumers. An old timestamp or a matching folder
name alone is not deletion authority.

### Windows maintenance setup

After updating the primary checkout to include this tooling, install or refresh
the per-user task there; installation from a removable worktree is rejected:

```powershell
.\scripts\platformclaw-dev-maintenance.ps1 -Action Install
.\scripts\platformclaw-dev-maintenance.ps1 -Action Status
```

The task runs every four hours while that user is signed in, using the same
bounded options as `-Action Run`: managed-task sweeping, release intermediates,
and a BuildKit cache target of 20 GB with 5 GB reserved and a 20 GB free-space
target. Final release images and archives are skipped; their lifecycle remains
with the release builder. `Status` shows the last result and next run. Read
cleanup output for skipped or blocked work; a scheduled-task result alone does
not prove every resource was reclaimed.

For manual maintenance or hosts without Windows Task Scheduler:

```sh
node scripts/platformclaw-dev-cleanup.mjs --apply --skip-final-images --skip-archives --cache-max 20gb --cache-reserved 5gb --cache-min-free 20gb
```

The 20 GB target applies to BuildKit cache, not total Docker storage. Images,
container writable layers, volumes, and exported archives are separate. Docker
internal free space also does not necessarily shrink Docker Desktop's VHDX.
Compaction is a separate, elevated, explicitly scheduled downtime operation:
only after Docker and WSL work has stopped, run
`./scripts/platformclaw-docker-vhdx-compact.ps1 -Apply`. It stops Docker Desktop
and WSL; routine maintenance never does this automatically.

See also [Scripts](https://docs.openclaw.ai/help/scripts) for public-facing script guidance.
4 changes: 4 additions & 0 deletions scripts/platformclaw-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ const SURFACE_COMMANDS = {
"scripts/platformclaw-check.mjs",
"scripts/platformclaw-ci-plan.d.mts",
"scripts/platformclaw-ci-plan.mjs",
"scripts/platformclaw-dev-task.mjs",
"scripts/platformclaw-dev-cleanup.mjs",
"test/scripts/platformclaw-dev-task.test.ts",
"test/scripts/platformclaw-check.test.ts",
"test/scripts/platformclaw-ci-plan.test.ts",
]),
Expand All @@ -120,6 +123,7 @@ const SURFACE_COMMANDS = {
"test/scripts/platformclaw-check.test.ts",
"test/scripts/platformclaw-ci-plan.test.ts",
"test/scripts/platformclaw-runtime-docker.test.ts",
"test/scripts/platformclaw-dev-task.test.ts",
]),
],
ui: [
Expand Down
3 changes: 3 additions & 0 deletions scripts/platformclaw-ci-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export function classifyPlatformClawChanges(inputFiles) {
file === "scripts/platformclaw-ci-plan.d.mts" ||
file === "scripts/platformclaw-check.mjs" ||
file === "scripts/platformclaw-check.d.mts" ||
file === "scripts/platformclaw-dev-task.mjs" ||
file === "scripts/platformclaw-dev-cleanup.mjs" ||
file === "scripts/platformclaw-dev-maintenance.ps1" ||
file.startsWith("test/scripts/platformclaw-"),
);
const hasWorkflowChanges = files.some((file) =>
Expand Down
25 changes: 20 additions & 5 deletions scripts/platformclaw-dev-cleanup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function docker(args, { allowFailure = false } = {}) {
}

function restoreImageTag(tag, imageId) {
const args = imageId ? ["image", "tag", imageId, tag] : ["image", "rm", tag];
const args = imageId ? ["image", "tag", imageId, tag] : ["image", "rm", "--no-prune", tag];
const result = docker(args, { allowFailure: true });
if (result.status !== 0 && !/No such image/u.test(result.stderr)) {
fail(result.stderr.trim() || `Failed to restore image tag ${tag}`);
Expand Down Expand Up @@ -291,7 +291,7 @@ function removeIntermediateImages(options, used) {
}
const tag = `${row.Repository}:${row.Tag}`;
announce(options, `remove intermediate image ${tag}`, () => {
docker(["image", "rm", tag]);
docker(["image", "rm", "--no-prune", tag]);
});
}
}
Expand All @@ -311,7 +311,7 @@ function removeFailedFinalImages(options, used) {
}
const tag = `${row.Repository}:${row.Tag}`;
announce(options, `remove unvalidated final image ${tag}`, () => {
docker(["image", "rm", tag]);
docker(["image", "rm", "--no-prune", tag]);
});
}
}
Expand Down Expand Up @@ -349,7 +349,7 @@ function pruneRepository(options, repository, used) {
}
const tag = `${row.Repository}:${row.Tag}`;
announce(options, `remove old rollback tag ${tag}`, () => {
docker(["image", "rm", tag]);
docker(["image", "rm", "--no-prune", tag]);
});
}
}
Expand Down Expand Up @@ -522,10 +522,25 @@ function pruneBuildCache(options) {
}

const options = readArgs(process.argv.slice(2));
// Host task ownership is independent of Docker availability. Sweep before the
// engine lock, so a stopped Desktop does not strand completed source worktrees.
const taskSweep = spawnSync(
process.execPath,
[
resolve(repoRoot, "scripts", "platformclaw-dev-task.mjs"),
"sweep",
...(options.apply ? ["--apply"] : []),
],
{ cwd: repoRoot, stdio: "inherit" },
);
if (taskSweep.error || taskSweep.status !== 0) {
console.error("Managed task cleanup is blocked; inspect platformclaw-dev-task.mjs list");
process.exitCode = 1;
}
const cleanupLock = await acquireCleanupLock(options);
if (!cleanupLock) {
console.log("SKIP: a PlatformClaw build is active");
process.exit(0);
process.exit(process.exitCode ?? 0);
}
const outputDirectoryLock = await acquireOutputDirectoryLock(options);
try {
Expand Down
39 changes: 31 additions & 8 deletions scripts/platformclaw-dev-maintenance.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,32 @@ $ErrorActionPreference = "Stop"
$taskName = "PlatformClaw Home Dev Cleanup"
$repoRoot = Split-Path -Parent $PSScriptRoot
$cleanupScript = Join-Path $PSScriptRoot "platformclaw-dev-cleanup.mjs"
$cleanupArgs = @(
"--apply", "--skip-final-images", "--skip-archives",
"--cache-max", "20gb", "--cache-reserved", "5gb", "--cache-min-free", "20gb"
)

if ($Action -eq "Run") {
& node $cleanupScript --apply
& node $cleanupScript @cleanupArgs
if ($LASTEXITCODE -ne 0) { throw "PlatformClaw cleanup failed ($LASTEXITCODE)" }
exit 0
}

if ($Action -eq "Install") {
if ((Get-Item -LiteralPath (Join-Path $repoRoot ".git") -Force).PSIsContainer -eq $false) {
throw "Install maintenance from the primary checkout after updating it, not from a removable linked worktree."
}
$node = (Get-Command node -ErrorAction Stop).Source
$arguments = '"{0}" --apply' -f $cleanupScript
$arguments = '"{0}" {1}' -f $cleanupScript, ($cleanupArgs -join " ")
$taskAction = New-ScheduledTaskAction -Execute $node -Argument $arguments -WorkingDirectory $repoRoot
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 3am
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 2)
Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $trigger -Settings $settings -Description "Weekly bounded cleanup for PlatformClaw home development Docker data" -Force | Out-Null
Write-Host "Installed weekly task: $taskName"
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
# Daily triggers do not expose repetition parameters, but accept the same
# scheduler repetition pattern as a once trigger. Renew it every day.
$repeating = New-ScheduledTaskTrigger -Once -At 3am -RepetitionInterval (New-TimeSpan -Hours 4) -RepetitionDuration (New-TimeSpan -Days 1)
$trigger.Repetition = $repeating.Repetition
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 2)
Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $trigger -Settings $settings -Description "Every-four-hour bounded cleanup for PlatformClaw development resources" -Force | Out-Null
Write-Host "Installed every-four-hour task: $taskName"
exit 0
}

Expand All @@ -31,5 +42,17 @@ if ($Action -eq "Uninstall") {
exit 0
}

Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue |
Select-Object TaskName, State, Description
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($null -eq $task) {
Write-Host "Not installed: $taskName"
exit 0
}
$info = $task | Get-ScheduledTaskInfo
[pscustomobject]@{
TaskName = $task.TaskName
State = $task.State
LastRunTime = $info.LastRunTime
LastTaskResult = $info.LastTaskResult
NextRunTime = $info.NextRunTime
Description = $task.Description
}
Loading