jkunz 008dd1ed0c fix(scan): classify from 16 MiB and take the failure kind from the parsed command
- collectMeasuredScanReport classifies measured entries from 16 MiB up (was 256 MiB); the 64 MiB noise floor folds findings below it and --all lists them.
- The --json failure envelope takes its kind from the parsed command, so a flag value that names a command (--root cleanup) no longer labels it; parseCliCommand is exported.
- Readme notes that a folded finding inside a larger listed finding appears in both lines.
2026-10-11 21:43:43 +00:00
…
…
…
…

@git.zone/tsdisk

tsdisk keeps disk usage on Linux hosts in check. It measures and explains where the space on each filesystem went, classifies what it finds with a verdict and the evidence behind it, protects what must not be touched, hands cleanup to the tools that own each cache, and watches free-space thresholds on a schedule.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm add --global @git.zone/tsdisk

For one-off usage:

pnpm dlx @git.zone/tsdisk scan

tsdisk is a Linux CLI. It uses du, df, findmnt, GNU timeout and, for alerts, util-linux logger from the host. It bundles @git.zone/tsrust and @git.zone/tsdocker as fallback owner tools and downloads nothing at run time.

Commands

Command What it does --json
tsdisk scan [--root PATH]… Measure, explain and classify the selected roots; never changes anything tsdisk.scan envelope
tsdisk explain PATH Measure and classify one path and show the policy rules that decide it tsdisk.explain envelope
tsdisk cleanup [--only …] [--workspace] [--apply --yes] Preview cleanup through the owning tools; apply owner-tool cleanup tsdisk.cleanup envelope
tsdisk worktrees [--root REPO]… [--workspace] Inventory linked git worktrees; never changes anything tsdisk.worktrees envelope
tsdisk adopt DIR [--apply --yes] Hand an unmarked Cargo target to tsrust tsdisk.adopt envelope
tsdisk policy show|validate|explain Show, validate or explain the layered disk policy tsdisk.policy envelope
tsdisk check [--alert] Compare free space with the policy thresholds; the exit code is the verdict tsdisk.check envelope
tsdisk schedule print|write DIR Generate the hourly check as systemd user units tsdisk.schedule envelope
tsdisk --help, tsdisk --version Help and version —

Bare tsdisk, tsdisk --json and tsdisk --root PATH run scan. A command refuses every flag it does not use: scan --check, or --worktree-idle-hours without the worktree category, is an error instead of being ignored. Invalid arguments print an error on stderr and exit 1; with --json, stdout also carries a failure envelope (see JSON output contract). tsdisk never uses sudo; it runs as its caller. scan and cleanup print live progress every five seconds; with --json the progress goes to stderr, so stdout carries exactly one JSON document.

The audit log and trends commands are pending; their sections describe what is planned.

Scanning

tsdisk scan                                   # the policy's roots, else the current directory
tsdisk scan --root /mnt/data/foss.global --json
tsdisk scan --root /mnt/data/project-a --root /mnt/data/project-b
tsdisk scan --all                             # list findings below the noise floor one by one

scan is read-only. It never removes anything, never asks for sudo and never runs project code.

Roots. Repeat --root to scan several trees. Without --root, the scan uses the host policy's roots, else the current directory. Symlink roots and roots with symlink ancestors are rejected.

Measurement. Each root is measured with one streaming du pass for allocated bytes and one for apparent bytes. Every figure carries its provenance: the source (du, stat, statfs, qemu-img, docker or owner-tool), the time it was measured, and whether it is cached (cached figures keep their original time). Paths inside a protect root declared with measure: false are never read.

Filesystem verdicts. For every filesystem the scan touches, the statfs used bytes are reconciled with what was measured, and the difference is explained by causes:

Cause Meaning
protected-unmeasured Protect roots declared with measure: false
permission-denied Paths the scanning user cannot read
deleted-open-files Deleted files that a process still holds open
other-roots Data on the filesystem outside the scanned roots
fs-metadata Filesystem metadata estimate
budget-exhausted A measurement or classification budget ran out
measure-error A measurement failed; detail names the error and its errno code
changed-during-scan Used bytes changed while the scan ran

Each cause has bytes (or null when only a bound exists), a bound (exact, estimate or remainder), example paths and a count. The filesystem verdict is complete when nothing is unaccounted and no cause was needed, explained when the causes account for the difference and any remainder is under 1 % of the used bytes or under 1 GiB, and incomplete when the remainder exceeds that tolerance or a measurement failed. An incomplete scan never ends in a bare complete: false: it always says which causes are missing how much.

Findings. Entries of at least 16 MiB are classified largest first, without a depth limit. Every finding carries:

  • a verdict: protected or in-use (keep), review or report-only (check), or eligible (safe);
  • reasons in plain language, never a command line;
  • its measurement with provenance, and the bytes this path alone would free (exclusiveBytes) for eligible, review and node_modules findings;
  • its attribution: workspace root, project, repository, worktree, AGL lane (unknown when AGL does not answer) and, for archives, the repository and commit it came from;
  • its use: the processes, containers, VMs and agent sessions holding it, hidden processes, the newest change below it, and whether process visibility is host or sandbox;
  • the policy rule that decided the verdict, and the owner tool when one exists.

The scan never marks a finding eligible on its own. A holder makes it in-use; a protect rule makes it protected; anything without a quiescent use verdict, full host visibility, an owner tool and a complete measurement stays at review or below.

Classifier Finds Verdict
owner-data nosqldb, MongoDB and SQLite stores, backups, .nogit/local, Git LFS, bare repositories, Ghost content, declared protect roots protected; duplicate files inside are reported, never offered
vm-image qcow2 and other VM disk images with their backing chains and snapshots protected, in-use or report-only
standalone-checkout Full git clones below a .nogit report-only, with the proof an owner needs before removing one by hand
trash ~/.local/share/Trash and .Trash-<uid> review
rust-target tsrust-marked targets review; protected when the marker forbids pruning
cargo-target Any Cargo target (Cargo's CACHEDIR.TAG plus build metadata) review; adopt it for owner cleanup
docker-registry tsdocker registry caches and their small stubs review or report-only; protected when the marker forbids pruning
evidence .nogit/debug, evidence, archive and worktree-evidence folders report-only, or protected by policy
gitzone-releases, chromium-build, git-internal Release staging, Chromium build trees, git object storage report-only, or protected by policy
project-build-output Generated project output protected when it holds tracked files, else report-only
node-modules Dependency trees, with install time and owning worktree report-only, with exclusive bytes
cachedir, user-cache CACHEDIR.TAG directories and known caches in ~/.cache report-only

Policy can make a category stricter, never eligible.

Budgets. Usage measurement has 120 seconds, policy discovery 100,000 entries and 45 seconds, classification 50,000 entries and 45 seconds, the AGL lane lookup 10 seconds and the deleted-open-file survey 5 seconds. A spent budget becomes a budget-exhausted cause or a classification gap; it never hides bytes.

Output. The text report prints one line per filesystem (verdict, used, measured and unaccounted bytes with provenance), its causes, then every finding with its size, verdict, path and provenance. Docker registry stubs are folded into one line per filesystem. Warnings come last.

Exit Meaning
0 The scan is complete
2 The scan is incomplete; the causes say why
1 Invalid input or a failure

Old .nogit/worktree-evidence/ folders from tsdisk 1.x are reported as evidence like any other.

Noise floor. The text listing folds findings smaller than the host policy's noiseFloorBytes (default 64 MiB) into one line per project and category, with their count and allocated bytes; a finding inside another member of its group is counted once. --all lists every finding instead. JSON never folds and refuses --all. Paths are classified from 16 MiB up (minimumBytes), so with the default floor findings from 16 to 64 MiB are folded. A folded finding inside a larger listed finding shows up twice, in its group line and within the larger finding's bytes; the lines are not meant to be summed.

Docker engine. After the filesystems, the scan summarizes each Docker context's storage from docker system df (images, containers, volumes and build cache with their reclaimable bytes) and lists restarting and long-exited containers with review-only hints. The summary is report-only: tsdisk never prunes shared engine state. Each Docker call is limited to 15 seconds and the summary to 120 seconds; when Docker is missing or does not answer, the scan adds a warning and stays otherwise complete. JSON carries it as dockerEngine.

Explaining a path

tsdisk explain /mnt/data/project/.nogit/tsrust-target
tsdisk explain /mnt/data/project/node_modules --json

tsdisk explain PATH measures PATH freshly, classifies exactly that path whatever its size, and prints its category, verdict, size, reasons, attribution, use and owner tool, followed by the policy layers and the rules that protect it or decide its categories. A path no classifier recognizes is reported as such; tsdisk never offers it for cleanup. It is read-only like scan, skips the Docker engine summary and exits 2 when the measurement is incomplete.

Cleanup

# Preview Rust, Docker and declared project resources in the current project.
tsdisk cleanup
# Select an individual project's owning tool.
tsdisk cleanup --root /mnt/data/project --only=docker
# pnpm has no prune dry-run: preview identifies its configured store.
tsdisk cleanup --root /mnt/data/project --only=pnpm --json
# Apply owner-tool cleanup after reviewing the preview (needs the audit store, see below).
tsdisk cleanup --root /mnt/data/project --only=rust,docker --apply --yes
# Preview linked worktrees below a workspace (opt-in).
tsdisk cleanup --root /mnt/data --workspace --only=worktree

Cleanup defaults to a preview of rust,docker,project for the current project. --dry-run (-n) explicitly selects preview mode and cannot be combined with --apply. Repeat --root to select multiple projects, and use --only=rust,docker,pnpm,project,worktree to choose categories. pnpm is opt-in because the configured store may be shared; tsdisk never deletes a pnpm store directory. worktree is opt-in as well; see Git worktrees. --days N (default 14) changes only Rust retention.

Rust and Docker cleanup act on marked owner caches in the project's .nogit: tsrust's tsrust-target and tsrust-* directories, and tsdocker's docker-registry when it carries the marker itself, otherwise its marked session directories (never releases). Each needs a valid owner marker and safeToPrune: true. The owning tool is resolved per project: the project's own @git.zone/tsrust (4.1.0 or later) or @git.zone/tsdocker (3.12.0 or later), otherwise the copy bundled with tsdisk. A package qualifies only when its package.json names it, its version is an exact release at or above the minimum, and its bin is a file inside the package; tsdisk runs that file with its own Node.js, never through a shim or PATH. The plan shows which copy was used and why a project copy was rejected. When neither qualifies, the action is blocked; nothing is downloaded. Rust previews pass --days 14 by default.

Owner tools run in targeted mode on exactly one directory: tsrust prune --workspace <project> --target <dir> --days N --json and tsdocker prune --target <dir> --json, each with --apply on execution. Targeted tsdocker never removes containers. A Docker cache is eligible only when tsdocker plans exactly this cache with no root-owned files; root-owned files block with a pointer to tsdocker prune --target <dir> --via-container --apply, because tsdisk never uses sudo or root containers. Bundle, matrix, and legacy registry findings do not become execution targets.

Each owner preview runs through the cleanup executor. It verifies the target's identity and policy, runs the owner's dry run and checks the JSON output against the owner's contract: tsrust's result must name exactly this target as its only candidate, with the planned device and inode, the owner marker recognized and an idle holder verdict; tsdocker's report (schema 1) must name exactly this cache as its only candidate with the planned device and inode and no containers. Anything else blocks the action; a plan that removes nothing, or a holder tsrust reports, skips it. An accepted preview is then checked for current use; a held, recently changed or sandbox-visible cache is skipped. Owner previews time out after 240 seconds, and the owners' own lock waits (TSRUST_LOCK_WAIT_TIMEOUT, TSDOCKER_HOST_LEASE_WAIT_TIMEOUT) end 60 seconds earlier.

Rust rechecks retention, markers, cache identity, and active use at execution time. Active Rust tools or inaccessible process state (including protected same-user processes) block removal. Linux tsrust builds and pruning share a cache guard.

--workspace discovers nested projects and marked Docker/Rust cache owners and reports unsupported owners separately. It never downloads tools. Workspace discovery uses depth 12, 50,000 inspections and 45 seconds per root, and reports incomplete traversal. With --only=pnpm --workspace, each project's store is resolved and each real store is previewed once; older stores are report-only.

Apply. --apply requires --yes; there is no interactive confirmation. It runs owner-tool cleanup only (--only rust, docker or both); pnpm, project and worktree apply are refused by the cleanup command. Pending: apply also needs the audit log, which is not available yet, so it currently refuses before planning. On apply, the executor handles each action on its own: it re-verifies identity and policy, reruns the owner preview, checks use against a fresh holder snapshot, records the intent, runs the owner with --apply, verifies containment and records the outcome. The owner's JSON result decides whether the action applied, was skipped, or failed; the exit code alone never counts as success. A failed or blocked action does not stop the others, and actions whose targets overlap within the run are blocked while the rest run; only a safety breach stops the run. Owner commands time out after 240 seconds, with a five-second termination grace period. Completed actions are not rolled back; review owning-tool state before retrying an interrupted operation.

Exit Meaning
0 The preview or apply succeeded
2 Discovery, the pnpm store resolution, project resources or the worktree inventory is incomplete, or a project resource candidate is blocked
3 Some actions failed or were blocked; the others ran
1 Invalid input or a safety breach

Every cleanup result includes blockedSummary: actionCount, blockedCount and groups of blocked actions by category and reason. Each group lists every project path. When no qualifying owner tool resolved, the group also names the owner package and the minimum version the prune contract requires, and each project carries its installed version (installedVersion: null when the owner package is missing); groups of refused owner previews carry neither. Human output prints the same summary after the plan, so the projects to upgrade are visible at a glance. Tools are never downloaded.

Cleanup runs as the project owner and refuses filesystem roots and symlink project paths. It never uses sudo and has no --sudo or --no-sudo option. A Docker cache with root-owned files is blocked; its owner removes it with tsdocker's --via-container. No plans or application state are persisted.

Adopting Cargo targets

tsdisk adopt /mnt/data/project/rust/target              # preview
tsdisk adopt /mnt/data/project/rust/target --apply --yes

tsdisk adopt DIR hands an unmarked Cargo target (a cargo-target finding) to tsrust (adopt capability, 4.1.0 or later, resolved from the nearest project with a package.json): tsrust verifies that the directory holds only Cargo build profiles and writes its owner marker, after which cleanup --only rust can plan it. DIR must be a real directory, not a symlink or a filesystem root, and adopt refuses to run as root. Adoption runs through the same executor and policy checks; it removes nothing, so tsrust's 24-hour quiescence window does not apply (a cargo-target policy minimum age still does), and a held target is skipped. Without --apply it previews; --apply --yes needs the audit log like cleanup. When no qualifying tsrust resolves, the report is blocked and exits 3.

Git worktrees

tsdisk worktrees --root /mnt/data/project            # one repository
tsdisk worktrees --root /mnt/data --workspace --json # every repository below /mnt/data

tsdisk worktrees and cleanup --only=worktree work with explicit repository roots (each --root must contain a .git directory; without --root, the current directory) and with --workspace, which uses the discovered repositories. Both list every linked worktree with its status, reasons, the exact git commands and the evidence destination; --worktree-idle-hours N (default 24) changes the idle threshold. worktrees exits 2 when the inventory or repository discovery is incomplete. The CLI previews only: applying needs the audit store, which is not configured yet; until then worktree apply is available through the library.

Each entry records repo, path, branch (null when detached), head, locked, missing (directory gone), dirty (git status --porcelain, including untracked files), merged (HEAD is an ancestor of the local main, else master; null when neither exists), activeProcess, activityCheck, lastActivityAt and idleHours (newest mtime of the worktree's gitdir index, HEAD and logs/HEAD), ignoredEvidence, ignoredInventory, measurement, identity, protected, status, reasons, commands and evidenceDestination. Root-owned files are listed in ownershipIssues, with an ownershipRepair hint that points to tsdocker's --via-container.

A worktree is eligible only when it exists, is clean, merged, unlocked, not protected by policy, has no active process, and has been idle for at least the idle threshold; a missing, unlocked registration is prunable-registration; everything else is retained with reasons. activeProcess is true when any readable /proc/*/cwd lies at or under the worktree. Non-dumpable same-user processes such as sshd sessions, gpg-agent or sandboxed agents have unreadable working directories; the report lists them in unreadableProcesses (pid, command from /proc/<pid>/comm, and a boolean cmdlineReadable). For those processes tsdisk reads /proc/<pid>/cmdline only to match worktree paths; command lines can carry credentials and never appear in JSON, human output or reasons: a command line naming the worktree or a path below it retains the worktree, and a process whose command line is unreadable too keeps every worktree retained as unknown. Otherwise the worktree must pass a filesystem quiescence check (activityCheck: "filesystem-quiescence"): nothing below it may have been modified within the idle window. The walk skips node_modules and regenerable caches, never follows symlinks, and stops at a filesystem boundary, depth 32, 100,000 entries or 15 seconds; any of these, or a read error, retains the worktree. Symlinked worktree paths, worktrees whose .git link does not match the repository registration, worktrees with submodules, and repositories not owned by the current user are retained. Git runs with GIT_OPTIONAL_LOCKS=0, so the inventory never rewrites an index; each git command runs in its own process group with a 15-second limit.

ignoredInventory lists every ignored path from git status --ignored, inside and outside .nogit, as evidence or regenerable. Regenerable paths are node_modules, top-level dist_* folders, .nogit/tstest_cache, .nogit/testlogs, .nogit/worktrees, tsrust and tsbundle caches whose marker allows pruning, Cargo targets with build proof, and tsdocker stubs of 64 KiB or less. Everything else is evidence and is listed in ignoredEvidence.

Applying an eligible worktree (applyWorktreeCleanup or removeWorktree with apply and yes, never as root) re-inspects it against every eligibility condition with a fresh process snapshot, checks that branch, HEAD, device and inode are unchanged, and records each step in the audit sink it is given (without one it reports a safety breach and changes nothing). Then:

  1. Evidence moves into <repo>/.nogit/archive/<worktree>-<YYYY-MM-DD>-<commit12>/ (the next free name is claimed; nothing is merged or overwritten), by rename on the same filesystem or by a verified copy across filesystems, with a manifest.json that records the repository, worktree, commit, time and every entry.
  2. When the caller supplies owner tools, they prune the regenerable caches they own; a payload change outside the declared target fails the step.
  3. git -C <repo> worktree remove <path> runs, never with --force.
  4. git -C <repo> branch -d <branch> runs unless the worktree is detached or on main or master. A refused -d is reported, never retried with -D.
  5. git -C <repo> worktree prune runs for repositories whose planned stale registrations are still missing.

A failed step leaves a repair hint; completed work is not rolled back. Worktree cleanup runs as the repository owner and never uses sudo.

Disk policy

Policy decides what no cleanup may touch and how strict each category is. It has four layers: built-in defaults, the host file $XDG_CONFIG_HOME/tsdisk/policy.json (default ~/.config/tsdisk/policy.json), @git.zone/tsdisk.workspace sections in .smartconfig.json files above a project, and the project's own @git.zone/tsdisk section. The most specific layer wins. The host file and the built-in floor are minimums that no project can relax.

tsdisk policy show                      # layers, roots, thresholds, alerts and protected roots
tsdisk policy validate --policy FILE    # exit 1 when FILE or any layer is invalid
tsdisk policy explain PATH --category rust-target   # deciding rule and layer for PATH
{
  "roots": ["/mnt/data/foss.global"],
  "filesystems": { "/": { "warnFreeBytes": "150GiB", "criticalFreeBytes": "50GiB" } },
  "protect": [{ "path": "/mnt/data/books", "reason": "accounting data", "measure": false }],
  "categories": { "rust-target": { "minAgeDays": 14 } },
  "noiseFloorBytes": "64MiB"
}

protect entries refuse the path, everything below it and every directory that contains it. They match by canonical path and by device and inode, so neither a symlink nor a rename evades them. Workspace and project entries are relative to their .smartconfig.json. Mount points, git internals and every git repository root (a directory that contains .git, or a bare repository) are always protected, as are the directories above them; only git-aware removal of a linked worktree passes the repository rule. A directory is checked against the policy of every project below it; if that policy cannot be read completely, the directory is refused. Category verdicts are review, report-only or protected; policy never makes a category eligible. noiseFloorBytes sets the noise floor of the scan's text listing.

The host file must be owned by you or root, and neither the file nor its directory may be writable by group or others. A host file that configures a webhook must not be readable by group or others either (chmod 600). Project .smartconfig.json files must not be symlinks. A policy file that is not valid JSON is reported with its line and column only, never with the surrounding text, which may hold a secret. Any invalid or unreadable layer makes the command exit 1. --policy FILE replaces the default host file for policy, check and schedule; the file must then exist.

Threshold checks and alerts

tsdisk check compares every filesystem in the host policy's filesystems with its thresholds and returns the verdict as its exit code, for systemd timers and CI. It reads statfs once per path and never scans or removes anything. It takes no --root; filesystems come from the policy.

tsdisk check                            # report per filesystem; exit code is the verdict
tsdisk check --json                     # tsdisk.check envelope on stdout, warnings on stderr
tsdisk check --alert                    # also send journald entries and the webhook
tsdisk check --policy FILE              # check against FILE instead of the default host file
{
  "filesystems": {
    "/": { "warnFreeBytes": "150GiB", "criticalFreeBytes": "50GiB" },
    "/mnt/data": { "warnFreePercent": 15, "criticalFreePercent": 5 }
  },
  "alerts": { "webhook": { "url": "https://hooks.example.com/tsdisk/TOKEN", "minSeverity": "warn", "timeoutSeconds": 10 } }
}

Free space is what an unprivileged user can still write (statfs available blocks, the space df shows as Avail); free percent is available ÷ (used + available), so it complements df's Use%. A threshold is breached when free space is below it; exactly at the limit is fine. Percent values lie between 0 and 100 exclusive, and each critical limit must be at or below its warn limit. Every filesystems entry needs at least one threshold. A path is checked on the filesystem that contains it, after resolving symlinks. Pending: warnGrowthPerDay is accepted but not evaluated; it needs the run history described under Trends and growth. A filesystem with only growth thresholds is incomplete.

Exit Meaning
0 every filesystem is above its thresholds
10 at least one warn threshold is breached
11 at least one critical threshold is breached
2 at least one filesystem could not be measured or evaluated (and none is critical or warn)
1 the policy is invalid or configures no filesystem thresholds

The worst filesystem decides: critical beats warn, warn beats incomplete. Failed alert delivery is a warning and does not change the exit code.

Alerts are sent only with --alert. journald always gets one entry per checked filesystem through logger --journald (util-linux), with SYSLOG_IDENTIFIER=tsdisk, PRIORITY 2 for critical, 4 for warn, 3 for incomplete and 6 for ok, MESSAGE_ID ac35ca219c4044be8c7625ea254a99c2 for threshold results and fb44b873de364ad69ee0ad0090849a0a for incomplete ones, and TSDISK_STATUS, TSDISK_PATH, TSDISK_MOUNT, TSDISK_FSTYPE, TSDISK_AVAILABLE_BYTES, TSDISK_TOTAL_BYTES, TSDISK_AVAILABLE_PERCENT and TSDISK_BREACHES fields:

journalctl --user -t tsdisk -p warning                        # warn and critical results
journalctl --user MESSAGE_ID=ac35ca219c4044be8c7625ea254a99c2 -o json

The webhook gets one JSON POST per run (kind: "tsdisk.alert", tool version, host name, check time, overall status, and each filesystem at or above minSeverity with its breaches); when no filesystem reaches minSeverity (default warn; incomplete, warn or critical) nothing is sent. Only https URLs are accepted, or http to localhost, 127.0.0.1 or [::1]. Redirects are refused and the request ends after timeoutSeconds (1 to 60, default 10). Alerts carry no secrets: the URL never appears in output, logs, errors, JSON or the alert itself (policy show prints it as redacted). Pending: every run alerts again while a threshold stays breached; suppressing repeats needs the run history described under Trends and growth.

Scheduling the check

tsdisk schedule generates a systemd user service and timer that run tsdisk check --alert hourly (OnCalendar=hourly, up to 5 minutes randomized delay, Persistent=true). tsdisk never installs, enables or reloads units: it prints them or writes them into an existing directory you name.

tsdisk schedule print                           # both units on stdout (--json for the envelope)
tsdisk schedule write DIR                       # write tsdisk-check.service and tsdisk-check.timer into DIR
tsdisk schedule write DIR --policy FILE         # the units pass --policy FILE to the check
tsdisk schedule write DIR --exec /usr/local/bin/tsdisk   # run this launcher instead of Node + cli.js

Without --exec (an absolute path), the service runs the current Node binary with this package's resolved cli.js, and its PATH starts with Node's directory; after moving or upgrading either, generate the units again. The service runs at Nice=19 with idle I/O priority, CPUQuota=50%, MemoryMax=1G, NoNewPrivileges=yes and a 15-minute limit. Exit codes 10 and 11 fail the unit on purpose, so systemctl --user --failed and OnFailure= see breaches. write refuses a directory that does not exist, a symlink, and any directory systemd loads units from; it never replaces a file: an identical file is left as it is, and a different file or non-file stops the write before anything is created. Install the written units yourself:

cp DIR/tsdisk-check.service DIR/tsdisk-check.timer ~/.config/systemd/user/
systemctl --user daemon-reload && systemctl --user enable --now tsdisk-check.timer

Pending: a nightly scan timer, which records the run history used by Trends and growth.

Audit log

Pending: this section is not available yet. Every removal is designed to be recorded before it runs (intent) and after it finishes (outcome), with the target's identity, the owner tool and the freed bytes. Until the audit store exists, cleanup --apply, adopt --apply and the library's worktree apply refuse to change anything. Planned here:

  • where the audit log is stored and how long it is kept;
  • tsdisk audit [--since TIME] [--json] to list recorded actions (tsdisk.audit envelope);
  • the record format and its fields.

Pending: this section is not available yet. Planned here:

  • run history from scheduled scans and checks, and where it is stored;
  • tsdisk trend [--json] for per-filesystem and per-category growth (tsdisk.trend envelope);
  • evaluation of warnGrowthPerDay thresholds in check;
  • suppressing repeated alerts while a threshold stays breached;
  • cached figures from stored runs in tsdisk explain, which measures freshly today.

Project cleanup resources

Declare generated artifacts under @git.zone/tsdisk in .smartconfig.json. The published JSON Schema validates this namespace while allowing other tools' configuration. With a project-local installation, an editor can use "$schema": "./node_modules/@git.zone/tsdisk/schema/smartconfig.schema.json".

{
  "@git.zone/tsdisk": {
    "schemaVersion": 1,
    "resources": [{
      "id": "guest-test-runs",
      "paths": [".nogit/debug/pallet-cni-*/run.*"],
      "preserve": ["**/result.json", "**/*.log"],
      "retention": { "minAgeDays": 7, "keepNewest": 2 },
      "provider": {
        "module": "./scripts/cleanup.mjs",
        "export": "guestRuns",
        "timeoutSeconds": 30
      }
    }]
  }
}

Resource IDs are unique within a project. paths are project-relative literals or * / ** patterns with a literal directory prefix. * matches within a component; ** matches zero or more complete components. Each match is a retention unit. preserve patterns are relative to each match; preserved directories retain their descendants. retention.keepFilesUnderBytes also preserves every regular file whose apparent size is below that many bytes, including sparse files. minAgeDays uses the newest entry modification time, including preserved evidence. keepNewest retains the newest matched candidates, breaking ties by path. protected: true prevents checks and deletion for that resource. Rules run before provider code and cannot be overridden by it.

A provider is optional for reporting. Without one, candidates remain unchecked once their retention period expires and cannot be deleted. This package's test-log declaration demonstrates that behavior. Age alone is not proof of inactivity.

# Discover and measure declared resources without importing project code.
tsdisk cleanup --root /mnt/data/foss.global --workspace --only=project --json
# Explicitly authorize provider checks in discovered projects.
tsdisk cleanup --root /mnt/data/foss.global --workspace --only=project --check

Only cleanup --check imports providers; scan never does and refuses --check. The cleanup command previews project resources only: applying them needs the audit store, which is not configured yet; until then it is available through applyProjectResources in the library. Providers are trusted project code, running with the current user's permissions and environment. Their subprocess is not a sandbox. They never run as root. Docker and Rust use their owning tools as described above; --only=project selects just declared resources.

The module's named export implements the published ICleanupProvider interface:

interface ICleanupProvider {
  inspect(context: ICleanupProviderContext): Promise<ICleanupDecision[]>;
  withLock<T>(context: ICleanupProviderContext, operation: () => Promise<T>): Promise<T>;
  apply(context: ICleanupProviderContext): Promise<void>;
}

Context contains the project root, declaration, and candidates with project-relative file paths, identities, allocated bytes and preservation flags. inspect returns exactly one decision per candidate, with a reason:

[{ "path": ".nogit/debug/pallet-cni-demo/run.123", "status": "eligible", "reason": "Guest exited; no producer owns this run." }]

Other decisions are retained, protected and blocked. A provider cannot expand the supplied paths. withLock must await the operation exactly once and release the lock in finally; artifact producers must use the same lock. Inspection and apply-time re-inspection run while holding it. Return blocked when inactivity cannot be established. apply receives only eligible candidates with preserved files removed from the file list. Delete only those exact regular files after validating producer state. Recursive candidate deletion is forbidden: candidates can contain retained evidence or new files. Empty directories may remain.

Before apply, tsdisk re-plans, checks configuration/provider-entry digests and candidate/file identities, then verifies them again inside the locked operation. Provider dependencies remain trusted project code; entry hashing does not freeze the dependency graph. Symlinks, mounts, Git metadata and nested project configs block candidates. Hardlinked files, ownership markers and the provider entry module are retained. Overlapping declarations block both candidates.

Resource traversal is bounded to 100,000 entries and 45 seconds per report, 1,024 matches per expansion, and depth 32 for patterns / 64 for contents. Provider checks have a 120-second report budget. Each provider process has a configurable 1–120-second timeout (default 30), an 8 MiB request limit and a 1 MiB output limit. Its private response pipe keeps logging out of JSON stdout. Process groups are terminated on timeout; providers must not leave background work behind.

projectResources JSON includes per-resource candidates, decisions, issues and statistics: discoveredBytes, eligibleBytes, retainedBytes, protectedBytes, blockedBytes, uncheckedBytes, and measured removedBytes. Five-second progress uses these totals. Discovered bytes describe the payload measured before apply; removed bytes are subtracted from remaining status totals. Inodes count once; directory metadata is omitted. Incomplete traversal is explicit in complete, issues and incompleteCount. These totals are separate from heuristic cache estimates, which can cover the same paths; do not add them. Shared extents and open files can make actual filesystem free-space changes differ from payload reduction.

Blocked project resources do not stop unrelated eligible resources, and a blocked candidate makes cleanup exit 2. Already applied work is never rolled back.

JSON output contract

Every command prints exactly one envelope on stdout with --json, on success and on failure; progress and warnings that are not part of the result go to stderr. A command that fails (invalid arguments, a policy that cannot be loaded, check without thresholds, a refused apply) prints a failure envelope of its own kind with error set and empty lists, writes the same message to stderr, and exits 1; arguments without a command count as scan. policy validate reports an invalid policy in its envelope instead. The contract is identified by kind plus the package version; there is no separate schema version field. Additive fields can appear in minor releases; removing or changing a field is a major release.

{
  "kind": "tsdisk.scan",
  "tool": "@git.zone/tsdisk",
  "version": "2.0.0",
  "host": "build-01",
  "startedAt": "2026-10-11T08:00:00.000Z",
  "finishedAt": "2026-10-11T08:01:12.000Z",
  "filesystems": [],
  "findings": [],
  "actions": [],
  "warnings": []
}
Field Type Meaning
kind string The envelope kind, see below
tool, version, host string @git.zone/tsdisk, the package version and the host name
startedAt, finishedAt ISO time When the command started and finished
filesystems IScanVerdict[] Per-filesystem accounting (scan)
findings IFinding[] Classified paths (scan)
actions IActionResult[] Action results (cleanup, adopt)
warnings string[] Non-fatal problems
error string Only in a failure envelope: why the command failed
explain IExplainReport Only in tsdisk.explain
cleanup ICleanupReport Only in tsdisk.cleanup
worktrees IWorktreeReport Only in tsdisk.worktrees
adopt IAdoptReport without results Only in tsdisk.adopt
policy IPolicyReport Only in tsdisk.policy
check ICheckReport Only in tsdisk.check
schedule IScheduleReport Only in tsdisk.schedule
Kind Emitted by Extra fields
tsdisk.scan scan, tsdisk --json complete, usage, apparentUsage, filesystemMeasurements, deletedOpen, classification, dockerEngine
tsdisk.explain explain explain; findings holds the path's finding, if any
tsdisk.cleanup cleanup cleanup; actions holds the executed actions' results
tsdisk.worktrees worktrees worktrees
tsdisk.adopt adopt adopt; actions holds the adoption result
tsdisk.policy policy show, validate, explain policy
tsdisk.check check check
tsdisk.schedule schedule print, write schedule
tsdisk.audit pending, see Audit log —
tsdisk.trend pending, see Trends and growth —

All types below are exported from the package root.

IScanVerdict: filesystem (IFilesystemRef: mount, device, fsType), usedBytes, measuredBytes, explained (IScanExplanation[]: cause, bytes or null, bound exact | estimate | remainder, paths, count, detail?), unaccountedBytes, verdict (complete | explained | incomplete), measuredAt, cached.

IFinding: path, filesystem, category (the classifier id), verdict (protected | in-use | review | eligible | report-only), reasons, measurement, attribution, use, owner?, policy, details? (classifier-specific, for example a qcow2 snapshot list).

  • IMeasurement: apparentBytes, allocatedBytes, exclusiveBytes? (absent when the probe did not finish), sharedHint? (hardlinks | reflinks | none), source (du | stat | statfs | qemu-img | docker | owner-tool), measuredAt, cached.
  • IAttribution: workspaceRoot?, project?, repository?, worktree?, lane? ({ id, ticket?, state, lastActivityAt? } or unknown), archiveOf? ({ repository, commit? }).
  • IUseVerdict: state (in-use | quiescent | unknown), holders ({ pid, name, kind }, kind fd | cwd | exe | map | mount | container | vm | cmdline | session; names, never command lines), hiddenProcesses, quiescentSince?, lane?, checkedAt, visibility (host | sandbox).
  • IOwnerToolRef: tool, command, resolvedVersion?, via (project | bundled).
  • IPolicyRef: rule, source (builtin | host | workspace | project), file?.

IActionResult: actionId, status (applied | skipped | blocked | failed), before?, after?, statfsFreedBytes?, owner?, error? (never a command line), auditRecordIds.

Scan extras. usage and apparentUsage are the allocated and apparent du passes (roots, measuredBytes, entries, records, complete, code, stderr, source, measuredAt, cached, mode, excludedPaths). filesystemMeasurements holds the statfs samples per filesystem at start and end. deletedOpen lists deleted-open files with totals per device and its own complete and issues. classification has complete, visitedEntries and gaps (path, bytes and the reason a subtree could not be classified). dockerEngine (IDockerEngineReport, report-only) has contexts (context, endpoint, sameEngineAs?, usage per Docker type with parsed sizeBytes and reclaimableBytes, totals, restartingContainers, staleExitedContainers, hints, issues), staleExitedDays, issues and complete.

IExplainReport: path, classified (whether a classifier recognized the path), policy (IPolicyExplanation: the protecting rule or the deciding category rules and their layers).

IPolicyReport: action, path, hostFile, hostFileFound, valid, errors, host? (the resolved host policy, webhook URL redacted), sources, protectedRoots, mountPointCount, explanation?.

ICheckReport: hostFile, status (ok | incomplete | warn | critical), exitCode, filesystems (IFilesystemCheck[]: path, filesystem?, totalBytes?, availableBytes?, availablePercent?, measuredAt?, status, thresholds, error?), alerts? (with --alert: sink, status, entries, error?). The webhook body is an IAlertPayload (kind: "tsdisk.alert").

IScheduleReport: action (print | write), units (name, content), directory?, written? (name, path, result written | unchanged).

ICleanupReport and IAdoptReport. The cleanup report has dryRun, actions (ICleanupAction[], each with category, cwd, scope, blocked?, ownerTool?, ownerAction?, preview?, result?, and for pnpm command, previewArgs, applyArgs and pnpmStore), pnpmStores?, results (IActionResult[]), safetyBreach?, projectResources? (its own versioned resource report), worktrees? (IWorktreeReport), blockedSummary and discoveryIssues. The adopt report has dryRun, target, ownerTool, blocked?, safetyBreach? and exitCode; its results are the envelope's actions.

IWorktreeReport: idleHours, repositories, worktrees (IWorktreeEntry[], see Git worktrees), counts per status, unreadableProcesses, issues and complete.

Exit codes are shared by every command:

Code Meaning Commands
0 OK all
1 Invalid input, a failed precondition or a safety breach all
2 Incomplete: something could not be measured or evaluated scan, explain, cleanup, worktrees, check
3 Some isolated actions failed or were blocked; the others ran cleanup, adopt
10 A warn threshold is breached check
11 A critical threshold is breached check

Migrating from 1.6.0

JSON. The 1.x report with schemaVersion: 1 is gone. scan --json and tsdisk --json print the tsdisk.scan envelope; dispatch on kind.

1.x field 2.0
schemaVersion removed; use kind and version
generatedAt startedAt / finishedAt
roots usage.roots
toolCaches findings with a category
usage usage (allocated) and apparentUsage; reconciled per filesystem in filesystems
issues, issueCounts filesystems[].explained, classification.gaps, warnings
reclaim exclusiveBytes on each eligible, review and node-modules finding
worktrees worktrees --json → worktrees, or cleanup --only=worktree --json → cleanup.worktrees
projectResources cleanup --only=project --json → cleanup.projectResources
usagePriorityPaths, gitRepositories not emitted; scanWorkspace() still returns both
dockerEngine dockerEngine in the tsdisk.scan envelope
sections removed with the host report; filesystems are explained in filesystems, deleted-open files in deletedOpen

Scan. Roots come from --root, else the host policy's roots, else the current directory; /mnt/data is no longer added automatically, so set roots in the policy for scheduled or scripted scans. Classification has no depth limit. Bare tsdisk runs scan; the 1.x interactive host report (journal usage, Docker context storage, an ncdu export, sudo) and its environment variables DOCKER_TIMEOUT, DU_TIMEOUT, NCDU_TIMEOUT, TSDISK_NCDU_EXPORT, TSDISK_KEEP_NCDU_EXPORT and TSDISK_INSTALL_NCDU are gone. A command refuses flags it does not use: scan --check is an error (run provider checks with cleanup --only=project --check), and --worktree-idle-hours needs worktrees or cleanup --only=worktree. The text listing folds findings below the noise floor; --all lists them.

Cleanup.

  • No sudo: --sudo and --no-sudo are rejected as unknown arguments. A Docker cache with root-owned files is blocked; its owner runs tsdocker prune --target <dir> --via-container --apply.
  • Owner tools: tsrust 4.1.0 or later (was 1.13.2) and tsdocker 3.12.0 or later (was 3.6.0). A project with an older copy uses tsdisk's bundled copy, and the plan says why. node_modules/.bin shims are never run.
  • Rust and Docker actions target one marked .nogit cache each instead of a whole project. rust/target and ts_rust/target are no longer cleanup targets: run tsdisk adopt DIR, then cleanup --only=rust.
  • Apply always needs --yes; there is no prompt. Apply runs owner-tool cleanup only, and it refuses until the audit log exists. In 1.6.0 the CLI also applied pnpm, project and worktree cleanup; in 2.0 it previews them, and project resources and worktrees can be applied through the library.
  • Each action runs on its own. A failed or blocked action exits 3 instead of 1 or 2, and no longer stops the others; a safety breach exits 1.
  • cleanup --json and adopt --json print the tsdisk.cleanup and tsdisk.adopt envelopes. The cleanup report moves into cleanup, and its results become the envelope's actions; the adopt report moves into adopt, with its results in actions. A refused apply prints a failure envelope. The cleanup report drops previews and worktreeResults; each action carries preview and result; safetyBreach is new.

Worktrees. Evidence moves into <repo>/.nogit/archive/<worktree>-<date>-<commit>/ with a manifest.json instead of being copied into .nogit/worktree-evidence/<name>[-N]/. It now covers every ignored path that is not regenerable, inside and outside .nogit, so ignoredEvidence lists more paths. Existing .nogit/worktree-evidence/ folders are not migrated; scans report them as evidence.

Policy. tsdisk 2.0 reads a host policy and .smartconfig.json policy sections. Rules to know when writing one:

  • Every filesystems entry needs at least one threshold.
  • A host file with a webhook must not be readable by group or others (chmod 600); neither the file nor its directory may be writable by group or others.
  • A policy file that is not valid JSON is reported with its line and column only.
  • --policy FILE works with policy, check and schedule; scan and cleanup always read the default host file.

Library.

  • ICleanupAction: command, previewArgs and applyArgs are pnpm-only and optional; sudoArgs and requiresSudo are gone; ownerAction, preview, result and pnpmStore are new.
  • ICleanupOwnerTool gains capability, via, resolvedVersion and projectIssue; minimumVersion may be null.
  • dockerPreviewRequiresSudo is removed.
  • applyWorktreeCleanup and removeWorktree need an auditSink; without one they report a safety breach and change nothing. IWorktreeEntry gains ignoredInventory, ownershipIssues, ownershipRepair, measurement, identity, protected and evidenceDestination; IWorktreeApplyResult gains evidenceArchivedTo, manifest, statfsFreedBytes, auditRecordIds, removalState, repair and safetyBreach.
  • Removed with the 1.x host report: main, collectDiskReport, collectProjectToolCacheReport, diagnosticCommandComplete, parseDuOutput and the IDiagnosticSection type. Use collectMeasuredScanReport for a scan, scanWorkspace for marked tool caches and measureDiskUsage for usage.
  • IRuntimeContext is { onProgress?, runningAsRoot }, and IRunOptions loses its sudo fields.

Library API

collectMeasuredScanReport({ roots?, policy?, policyFile?, lanes?, attribution?, laneTimeoutSeconds?, minimumBytes?, usageTimeoutSeconds?, policyMaxEntries?, policyTimeoutMs?, classificationMaxEntries?, classificationTimeoutMs?, deletedOpenMaxEntries?, deletedOpenTimeoutMs?, docker?, classifyRootsOnly?, onProgress? }) runs the measured scan and returns the tsdisk.scan envelope with its extras; it classifies entries from minimumBytes up (default 16 MiB); docker replaces the Docker CLI runner (createDockerRunner(budgetMs?) is the default), and classifyRootsOnly classifies each root itself and skips the Docker summary. runMeasuredScanCommand(options) is the CLI command around it, foldNoiseFloor(findings, floorBytes) its text folding, and runExplainCommand(options) and runWorktreesCommand(options) are the explain and worktrees commands. measureFilesystems(roots) and reconcileFilesystem(snapshot, accounting) produce the per-filesystem verdicts, classifyMeasuredTree(usage, options) the findings, and foldDockerRegistryStubs(findings) the folded stub summaries.

createCleanupPlan(options) plans cleanup without executing it; executeOwnerActions(actions, options) runs the planned owner actions through a CleanupExecutor (adapter, safety, use, audit, context, quiescenceHours, timeoutSeconds), and cleanupExitCode(results, incomplete?, safetyBreach?) derives the exit code. resolveOwnerTool(project, capability, options?) resolves a project or bundled owner tool. adoptTarget(options, deps?) runs adopt; apply needs deps.audit, an IAuditSink whose recordIntent and recordOutcome resolve once a record is durable.

scanWorkspace(root, { maxDepth, maxEntries, timeoutMs, onProgress? }) returns findings, issues, issue counts, usage-priority paths, completion status, and the directory/symlink inspection count. measureDiskUsage(roots, { timeoutSeconds, trackedPaths?, priorityPaths?, onProgress? }) performs the standalone native usage pass. Priority paths only reorder descendants on the selected filesystem; they cannot expand scan scope. estimateReclaimableSpace(findings) returns the conservative candidate summary. The existing classification and byte-formatting exports remain available. parseCliOptions(args) exposes validated CLI options, and parseCliCommand(args) the command they name, also when they are otherwise invalid. summarizeBlockedActions(actions) groups blocked owner actions; groups of unresolved owner tools carry installed and minimum versions.

collectWorktreeInventory(repositories, { idleHours?, timeoutMs?, readProcesses?, onProgress? }) classifies linked worktrees; applyWorktreeCleanup(report, options?) re-inspects and applies eligible entries, removeWorktree(repo, worktree, options?) one worktree and archiveWorktreeEvidence(repo, worktree, options?) only its evidence, as described under Git worktrees; all three need options.auditSink. readProcessCwds() is the default /proc working-directory source; its in-memory command lines are matching input only. formatWorktreeReport(report) returns the human worktree section. scanWorkspace also returns gitRepositories. collectDockerEngineReport(run) builds the report-only Docker engine summary that scan includes as dockerEngine from a Docker command runner; parseDockerSystemDf(stdout), parseDockerSize(value) and classifyDockerContainers(inspectJson, nowMs?, staleExitedDays?) expose its parsers.

createReadOnlyInspector({ root?, maxOperations?, matcher? }) gives classifiers bounded read-only access below root: lstat, readdir(path, { limit? }) and readFile(path, maxBytes). It never follows a symbolic link and never leaves root. A missing entry resolves to undefined; anything it cannot read, including a spent operation budget, throws an InspectorError with a reason. Given a ProtectedPathMatcher, it refuses every path in a measure: false protect root (unmeasured), by path and by the identity of each directory up to root, so a renamed root is refused too. The measured scan gives each classified entry its own inspector, rooted at the directory (a file's parent directory) and bound to the scan's matcher, and records an InspectorError as a measure-error gap.

OwnerDataClassifier({ matcher?, duplicates? }) (registered first) marks owner data protected and never descends into it: nosqldb, MongoDB and SQLite (with a -wal, -shm or -journal file) stores, backup/backups directories, archives whose name contains backup or -pre-, .nogit/local, .git/lfs, bare repositories, Ghost content/ and the protect roots declared in policy. Detected roots are added to the given ProtectedPathMatcher, so every removal refuses them and their ancestors. Inside backups, app state, Ghost content and declared roots it reports duplicate files (findDuplicates(root, inspect, maxHashBytes): equal size and SHA-256, sampled to the first and last MiB above 2 MiB, hard links and unmeasured entries skipped); the search has its own inspector, bound to matcher and bounded by duplicates: { maxOperations?, maxHashBytes? } (defaults 100,000 operations and 256 MiB per root), and duplicates are never offered. The measured scan classifies with registeredClassifiersWith(new OwnerDataClassifier({ matcher: policy.matcher }), new VmImageClassifier({ holders, matcher: policy.matcher })), so owner data it finds joins the scan's matcher, VM images report the scan's holders, and a held image and its backing chain join the matcher as floor.held-vm-image. It counts exclusive bytes only for eligible and review findings and for report-only findings whose classification sets countExclusive (node_modules, whose reclaim figure is the report); protected, in-use and other report-only findings are never counted, and the count's limits (reflink upper bound, unfinished probe) become reasons. Every finding carries the attribution chain (workspace root, repository, worktree, project, archive origin and lane) from AttributionResolver with the scan roots as workspace roots and AglLaneSource as lanes (scan option lanes replaces it, attribution replaces the whole chain); the AGL lane lookup starts before measurement and has its own bound (scan option laneTimeoutSeconds, default 10), and when AGL does not answer in time the lane is unknown, which refuses removal. Classifiers get the same chain through input.attribution(), resolved once per path. A node_modules finding records installMetadataModifiedAt from the layout's install metadata (.modules.yaml, .package-lock.json or .bun), assuming pnpm, npm and Bun rewrite that file on every install (not verified against each package manager), and worktree: { path, linked, lane } for the checkout it belongs to (lane is null without one, unknown when AGL does not answer); an archived lane adds a reason. ClassifierRegistry.with(...classifiers) is the copy that replaces registered classifiers by id. A marker the inspector cannot read throws, so the scan records a gap instead of a verdict. StandaloneCheckoutClassifier({ run? }) reports a full git clone below a .nogit as report-only with the proof an owner needs before removing it by hand: no changes or untracked files, every commit on a remote-tracking ref (as last fetched), no stash, no operation in progress, no linked worktrees, plus the ignored-file inventory. tsdisk never removes either.

collectProjectResources(projectRoots, { check?, onProgress?, timeoutMs?, maxEntries? }) reads project declarations and returns a versioned resource report. applyProjectResources(report, onProgress?) revalidates and applies eligible resources. resourceStatistics(resources) computes exclusive payload totals. All configuration, provider, candidate, decision and report interfaces are exported from the package root. scanWorkspace also returns projectRoots.

Attribution and use (read-only): HolderIndex.build({ procRoot?, run?, sessionSources?, sessionLookbackHours?, sessionWindowHours?, ancestry? }) indexes /proc cwd, exe, root, fd and map entries, bind mounts, running containers, qemu images and agent sessions (running claude, codex and opencode processes, unarchived AGL fleet workers, the Codex state store). holdersOf(path) lists them by comm or source name, never by command line; hiddenProcesses(path) counts unreadable processes that name nothing there; visibility is sandbox when detectVisibility() finds a nested pid namespace, a foreign pid 1 or hidepid. A session or lane whose working directory lies at or below a git repository root holds that whole subtree; one above every repository (a workspace root) holds only what contains it. UseProbe({ holders, lanes }).useOf(path, { quiescenceHours }) returns the in-use/quiescent/unknown verdict and isEligibleUse(verdict, hours) the eligibility gate. AglLaneSource reads lanes from agl fleet worker list --archived --json and passes AGL's states through; only a terminal state (isTerminalLaneState: archived) releases a lane, and an unarchived lane covering the path outranks any archived one. AttributionResolver({ workspaceRoots?, lanes? }).attribute(path) resolves workspace root, project, repository, worktree, lane and archive origin. ExclusiveBytesProbe().exclusiveBytes(path) counts inodes whose links all lie in the subtree on its filesystem and, on XFS and btrfs, probes the largest files with filefrag or xfs_io; shared extents make the figure an upper bound with sharedHint: 'reflinks'.

DiskPolicy.load({ hostPolicyPath?, requireHostFile?, mountPoints?, discoveryBudget? }) reads the host policy. policy.check(path, { linkedWorktreeRemoval? }) returns the protection verdict with every refusing rule; only git-aware worktree removal sets linkedWorktreeRemoval, and it exempts nothing but a verified linked worktree's own repository rule. isLinkedWorktree(directory) and repositoryKind(directory) expose that detection; policy.scopeFor(directory) returns the layered category rules; policy.explain(path, categoryIds?) combines both. ProtectedPathMatcher, canonicalPath, defaultHostPolicyPath, parseByteSize and the parseHostPolicy, parseWorkspacePolicy and parseProjectPolicy parsers are exported as well.

checkFilesystems(policy.host, measureOptions?) measures every policy filesystem and evaluateThresholds(path, thresholds, sample) judges one sample; summarizeCheck(hostFile, checks) returns the ICheckReport with status and exit code (checkExitCode(status)). deliverAlerts(report, policy.host.alerts, { runner?, fetch?, checkedAt? }) sends the journald entries (sendJournald, journaldEntry, journaldMessageIds) and the webhook (WebhookTarget.deliver(alertPayload(report, minSeverity, checkedAt))). A WebhookTarget keeps its URL private and serializes and inspects as redacted. generateScheduleUnits({ command, policyFile?, pathDirectories? }) returns the unit files and writeScheduleUnits(directory, units) writes them under the rules above; quoteUnitWord(word, 'command' | 'assignment') (only command lines double $) and unitSearchDirectories are exported as well.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
1.9 MiB
Languages
TypeScript 100%