Skip to content

REQ-C-017: Commands Register cleanup() Hook

Tier: Command Contract | Priority: P1

Source: §16 Signal Handling & Graceful Cancellation

Addresses: Severity: High / Token Spend: Medium / Time: Medium / Context: Low


Description

Every command that acquires resources (locks, temp files, network connections, child processes) MUST register a cleanup() hook with the framework. The framework invokes this hook on normal exit, SIGTERM, and timeout. The cleanup() hook MUST be idempotent (safe to call multiple times). Command authors MUST use the framework's resource acquisition APIs, which automatically register cleanup handlers.

Acceptance Criteria

  • A command that acquires a lock has the lock released on normal exit, SIGTERM, and timeout
  • A cleanup() hook called twice produces the same end state as calling it once
  • A command that does not acquire any resources MAY register a no-op cleanup hook
  • The framework warns at development time if a command acquires a tracked resource without a cleanup hook

Schema

No dedicated schema type — this requirement governs cleanup hook registration behavior without adding new wire-format fields


Wire Format

Cleanup hook declarations appear in the command's registration metadata. No additional --schema fields are exposed, but the side_effects field in exit_codes reflects whether cleanup has been completed:

$ tool deploy --schema
{
  "exit_codes": {
    "0":  { "name": "SUCCESS", "description": "Deployment completed and all resources released", "retryable": false, "side_effects": "complete" },
    "10": { "name": "TIMEOUT", "description": "Deployment timed out — cleanup attempted",        "retryable": false, "side_effects": "partial"  }
  }
}

Example

Command authors register cleanup hooks via the framework's resource acquisition APIs:

register command "deploy":
  on_execute: |
    lock = framework.acquire_lock("deploy-lock")
    # framework automatically registers: cleanup() { lock.release() }

    tmp = framework.temp_file("deploy-payload")
    # framework automatically registers: cleanup() { tmp.delete() }

    child = framework.subprocess("kubectl apply ...")
    # framework automatically registers: cleanup() { child.terminate() }

    # cleanup() is called on: normal exit, SIGTERM, timeout
    # cleanup() is idempotent: safe to call multiple times

Requirement Tier Relationship
REQ-F-013 F Enforces: SIGTERM handler invokes registered cleanup() hooks
REQ-F-012 F Enforces: timeout handler invokes registered cleanup() hooks before exiting
REQ-F-030 F Composes: child process tracking is one category of resource that cleanup() must release
REQ-F-032 F Composes: temp files are framework-tracked resources cleaned up via the hook mechanism
REQ-C-001 C Composes: side_effects in exit code declarations reflects whether cleanup was completed