From 1f2fb48085228b4eccded389b612c7182c542aee Mon Sep 17 00:00:00 2001 From: John Osumi <931193+sumitake@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:47:52 -0700 Subject: [PATCH] runtime: separate process activity from persistence --- .claude-plugin/marketplace.base.json | 2 +- .claude-plugin/marketplace.json | 4 +- README.md | 22 +- .../2026-07-28-dispatcher-status-semantics.md | 30 ++ .../agent-collab/.claude-plugin/plugin.json | 2 +- .../agent-collab/.codex-plugin/plugin.json | 2 +- plugins/agent-collab/README.md | 27 +- plugins/agent-collab/runtime_client.py | 180 ++++++- .../skills/agent-readiness/SKILL.md | 2 +- .../skills/agent-runtime-status/SKILL.md | 2 +- .../skills/ai-writing-auditor/SKILL.md | 2 +- .../agent-collab/skills/architect/SKILL.md | 2 +- .../skills/autonomy-readiness/SKILL.md | 2 +- .../agent-collab/skills/brainstorm/SKILL.md | 2 +- .../skills/chain-configurator/SKILL.md | 2 +- plugins/agent-collab/skills/chain/SKILL.md | 2 +- .../agent-collab/skills/code-review/SKILL.md | 2 +- .../skills/compose-skills/SKILL.md | 2 +- .../skills/data-engineer/SKILL.md | 2 +- plugins/agent-collab/skills/debate/SKILL.md | 2 +- plugins/agent-collab/skills/delegate/SKILL.md | 2 +- .../agent-collab/skills/dev-delegate/SKILL.md | 2 +- .../skills/elixir-engineer/SKILL.md | 2 +- .../skills/eval-engineer/SKILL.md | 2 +- .../agent-collab/skills/go-engineer/SKILL.md | 2 +- .../skills/governance-review/SKILL.md | 2 +- .../hallucination-investigator/SKILL.md | 2 +- .../skills/incident-responder/SKILL.md | 2 +- .../agent-collab/skills/intent-check/SKILL.md | 2 +- .../skills/knowledge-compile/SKILL.md | 2 +- .../skills/kubernetes-specialist/SKILL.md | 2 +- .../skills/llm-architect/SKILL.md | 2 +- .../agent-collab/skills/logic-check/SKILL.md | 2 +- .../agent-collab/skills/long-context/SKILL.md | 2 +- .../skills/merge-resolve/SKILL.md | 2 +- .../skills/migration-doctor/SKILL.md | 2 +- .../skills/mlops-engineer/SKILL.md | 2 +- .../agent-collab/skills/orchestrate/SKILL.md | 2 +- .../skills/postgres-engineer/SKILL.md | 2 +- .../skills/prompt-regression-tester/SKILL.md | 2 +- .../agent-collab/skills/qa-verify/SKILL.md | 2 +- plugins/agent-collab/skills/red-team/SKILL.md | 2 +- plugins/agent-collab/skills/route/SKILL.md | 2 +- .../skills/rust-engineer/SKILL.md | 2 +- .../skills/second-opinion/SKILL.md | 2 +- .../skills/simulate-user/SKILL.md | 2 +- .../agent-collab/skills/sql-engineer/SKILL.md | 2 +- .../agent-collab/skills/sre-engineer/SKILL.md | 2 +- .../skills/start-inbox-monitor/SKILL.md | 2 +- plugins/agent-collab/skills/teamwork/SKILL.md | 2 +- .../skills/terraform-engineer/SKILL.md | 2 +- .../agent-collab/skills/ui-to-code/SKILL.md | 2 +- .../skills/untrusted-audit/SKILL.md | 2 +- .../skills/visual-review/SKILL.md | 2 +- plugins/agent-collab/skills/worker/SKILL.md | 2 +- scripts/skill-build-config.json | 2 +- tests/fixtures/launchctl_print_keepalive.txt | 14 + .../launchctl_print_keepalive_dictionary.txt | 28 ++ .../launchctl_print_nonpersistent.txt | 26 + tests/test_agent_collab_migration.py | 25 +- tests/test_agent_collab_runtime_client.py | 455 +++++++++++++++++- 61 files changed, 825 insertions(+), 88 deletions(-) create mode 100644 changelog.d/2026-07-28-dispatcher-status-semantics.md create mode 100644 tests/fixtures/launchctl_print_keepalive.txt create mode 100644 tests/fixtures/launchctl_print_keepalive_dictionary.txt create mode 100644 tests/fixtures/launchctl_print_nonpersistent.txt diff --git a/.claude-plugin/marketplace.base.json b/.claude-plugin/marketplace.base.json index c0189af..df997dd 100644 --- a/.claude-plugin/marketplace.base.json +++ b/.claude-plugin/marketplace.base.json @@ -9,7 +9,7 @@ "plugins": [], "metadata": { "description": "Agent collaboration plugin marketplace", - "version": "4.5.1", + "version": "4.5.2", "repository": "https://github.com/sumitake/agent-collab" } } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6eee178..73be139 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ { "name": "agent-collab", "description": "Unified dynamic-host collaboration package. Centralized skills and async coordination work without legacy packages; every model-execution route requires the verified signed plugin artifact.", - "version": "4.5.1", + "version": "4.5.2", "author": { "name": "John Osumi" }, @@ -32,7 +32,7 @@ ], "metadata": { "description": "Agent collaboration plugin marketplace", - "version": "4.5.1", + "version": "4.5.2", "repository": "https://github.com/sumitake/agent-collab" } } diff --git a/README.md b/README.md index 39ee6ae..245bd65 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ trust: primary's identity, model, and session dynamically — no per-host forks to drift out of sync. -This public repository distributes that one package, **agent-collab** (v4.5.1), and is +This public repository distributes that one package, **agent-collab** (v4.5.2), and is the source of truth for the coordinator policy, skills, migration tooling, the fail-closed runtime client, contribution governance, and release-safety checks. The signed and notarized darwin-arm64 native runtime is committed in this @@ -129,7 +129,25 @@ Contributors need no access to the private build/sign system. See | Package | Version | Role | |---|---:|---| -| `agent-collab` | 4.5.1 | Unified skills, dynamic host policy, migration preflight, and verified native-runtime client | +| `agent-collab` | 4.5.2 | Unified skills, dynamic host policy, migration preflight, and verified native-runtime client | + +## What's new - v4.5.2 + +- **Broker status separates process configuration from momentary activity.** + Successful status reports `persistent_process=false` only after the exact + live launchd transcript proves that neither top-level + `KeepAlive`/`RunAtLoad` properties nor structured event triggers are + configured. Any event-trigger block is intentionally persistence-like: + ambiguity blocks readiness instead of being mislabeled socket-only. + `persistence_state` preserves the full `nonpersistent` / `persistent` / + `unproven` result; an unproven live format yields + `persistent_process=null` and fails readiness closed. A separate, optional + `process_idle` boolean-or-null reports bounded point-in-time quiescence only + when that idle probe actually ran; null means unmeasured, never idle. An + in-flight request or post-request grace therefore cannot make an otherwise + proven callable lane unavailable. Both new status observations are additive + and optional for rolling-upgrade consumers. Mutating lifecycle operations + never use either observation and still require their full idle proof. ## What's new - v4.5.1 diff --git a/changelog.d/2026-07-28-dispatcher-status-semantics.md b/changelog.d/2026-07-28-dispatcher-status-semantics.md new file mode 100644 index 0000000..5aff804 --- /dev/null +++ b/changelog.d/2026-07-28-dispatcher-status-semantics.md @@ -0,0 +1,30 @@ +### agent-collab 4.5.2 — 2026-07-28 + +### Fixed + +- Bind `broker-status`'s `persistent_process` field to an exact live launchd + properties proof instead of deriving it from a one-second point-in-time idle + observation or trusting only the mutable on-disk plist. +- Add `persistence_state=nonpersistent|persistent|unproven`; report the matching + `persistent_process=false|true|null`, fail readiness closed on unproven + diagnostic output, and never conflate unknown configuration with observed + persistence. +- Report that observation separately as `process_idle=true|false|null`; null + means the idle probe did not run, while a measured active process does not + make authoritative status reject a callable dispatcher during bounded + post-request grace. +- Keep both new status observations optional for rolling-upgrade consumers and + keep `process_idle` independent from the selected-lane liveness verdict when + present. +- Pin sanitized golden transcripts captured from real nonpersistent, + KeepAlive, and conditional-KeepAlive launchd jobs; closed brace structure, + top-level property tokens, and structured event-trigger evidence fail closed + on malformed or ambiguous format drift. Any event-trigger block is + intentionally persistence-like rather than eligible for socket-only status. +- Leave every mutating lifecycle command on the existing full idle-proof + boundary; a failed full-idle proof restores the pre-mutation selector. + +### Cross-check + +- Companion to workspace PR #2438. The revised cross-repo design is pending an + exact-head distinct-family review before either PR may merge. diff --git a/plugins/agent-collab/.claude-plugin/plugin.json b/plugins/agent-collab/.claude-plugin/plugin.json index b8e44b3..43994f9 100644 --- a/plugins/agent-collab/.claude-plugin/plugin.json +++ b/plugins/agent-collab/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-collab", - "version": "4.5.1", + "version": "4.5.2", "description": "Unified dynamic-host collaboration package with centralized skills, migration preflight, and a verified plugin-relative native runtime boundary. The signed runtime artifact is intentionally absent until the private build/sign integration completes.", "author": { "name": "John Osumi" diff --git a/plugins/agent-collab/.codex-plugin/plugin.json b/plugins/agent-collab/.codex-plugin/plugin.json index 4fa2914..c0f7936 100644 --- a/plugins/agent-collab/.codex-plugin/plugin.json +++ b/plugins/agent-collab/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-collab", - "version": "4.5.1", + "version": "4.5.2", "description": "Unified dynamic-host collaboration package with centralized skills, migration preflight, and a verified plugin-relative native runtime boundary. The signed runtime artifact is intentionally absent until the private build/sign integration completes.", "author": { "name": "John Osumi" diff --git a/plugins/agent-collab/README.md b/plugins/agent-collab/README.md index d4af5b8..551a2bb 100644 --- a/plugins/agent-collab/README.md +++ b/plugins/agent-collab/README.md @@ -7,7 +7,7 @@ verifiable compliance evidence, and operator final-say, delivered as one package for every supported host. This document is the package's technical reference; the repository README carries the purpose and governance narrative. -Current: **4.5.1** +Current: **4.5.2** It resolves `primary_id`, `primary_family`, `active_model`, `host_runtime`, and `session_identifier` from the current host or explicit configuration. ZCode @@ -65,7 +65,7 @@ one macOS `LC_BUILD_VERSION` with minimum macOS 14.0 instead of trusting those manifest labels. The broker transport and provider protocol are both version 2. The package carries both `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`; both -identify this same 4.5.1 package. +identify this same 4.5.2 package. Codex, Gemini, OpenCode, Grok, and Composer are broker-only contracts. Their sealed requests cross a mode-`0600`, digest-bound per-user launchd Unix socket; launchd starts the exact @@ -172,7 +172,7 @@ retained-lane rollback continuity; any received v2 discriminator selects only v2. Dual-version callers use `GEMINI_GOVERNANCE_PROOF_KEYSETS`; the legacy `GEMINI_GOVERNANCE_PROOF_KEYS` alias remains v1-only. -This 4.5.1 source tree carries the rebuilt darwin-arm64 activation artifact +This 4.5.2 source tree retains the rebuilt darwin-arm64 activation artifact from final workspace `1.0.823` commit `d08b6382710d6d5910d64cf011bcac873a2e1c03`. Its manifest pins the complete standalone bundle at SHA-256 @@ -347,11 +347,22 @@ atomically activates a closed plist, proves the job/socket and one-request process exit, and retains one verified prior record. Failed updates restore the complete prior state; same-version reactivation preserves its rollback target, and an unverified version is never recorded as rollback-safe. `broker-status` -is read-only and emits no prompt, credential, provider output, or private path; -an exact selected-lane ping proves callability, while a separate one-second -quiescence observation reports `persistent_process` without making an active -request look unavailable. Lifecycle mutations still require their full idle -proof. Bounded `launchctl` collection failures return a typed lifecycle error. +is read-only and emits no prompt, credential, provider output, or private path. +Before the exact selected-lane ping may prove callability, the live launchd +job's closed transcript must contain one valid top-level `properties =` line +and no `KeepAlive`, `RunAtLoad`, or structured event-trigger evidence. Any +event-trigger block is intentionally persistence-like: ambiguous lifecycle +configuration blocks readiness rather than being mislabeled socket-only. +`persistence_state` reports `nonpersistent`, `persistent`, or `unproven`; the +matching `persistent_process` value is `false`, `true`, or `null`, and an +unproven diagnostic format fails readiness closed. A separate optional +one-second quiescence observation reports `process_idle=true|false` only after +that probe runs; otherwise it is null, meaning unmeasured rather than idle. +Bounded request activity or post-request grace therefore does not masquerade +as configured persistence. Both new status observations are additive and +optional for rolling-upgrade consumers. Lifecycle mutations never use either +observation and still require their full idle proof. Bounded `launchctl` +collection failures remain typed and auditable. Use the closed rollback/removal actions only when needed: diff --git a/plugins/agent-collab/runtime_client.py b/plugins/agent-collab/runtime_client.py index 138dbbb..612c014 100644 --- a/plugins/agent-collab/runtime_client.py +++ b/plugins/agent-collab/runtime_client.py @@ -38,7 +38,7 @@ from dataclasses import dataclass, replace from enum import Enum from pathlib import Path, PurePosixPath -from typing import Any, Callable, Iterator, Mapping +from typing import Any, Callable, Iterator, Mapping, Sequence try: import fcntl as _fcntl @@ -321,6 +321,15 @@ def _read_execute_blank_ranges() -> tuple[tuple[int, int], ...] | None: _JOB_LABEL_RE = re.compile( r"com\.agent-collab\.provider-(?:broker|dispatcher\.[0-9a-f]{32})" ) +_LAUNCHCTL_PROPERTIES_VALUE_RE = re.compile(r"^[A-Za-z0-9_ |\-]*$") +_LAUNCHCTL_KEY_VALUE_LINE_RE = re.compile( + r"^(?P[ \t]+)" + r"(?P[A-Za-z][A-Za-z0-9 _-]*?)" + r"[ \t]*=[ \t]*(?P.*)$" +) +_PERSISTENCE_NONPERSISTENT = "nonpersistent" +_PERSISTENCE_PERSISTENT = "persistent" +_PERSISTENCE_UNPROVEN = "unproven" _TEAM_ID_RE = re.compile(r"^[A-Z0-9]{10}$") _DEVELOPER_ID_RE = re.compile( r"^Developer ID Application: [^\r\n]{1,160} \(([A-Z0-9]{10})\)$" @@ -4435,6 +4444,109 @@ def _job_loaded(label: str, *, deadline: float | None = None) -> bool: ).returncode == 0 +def _job_persistence_state(label: str) -> str: + """Observe one live job's closed launchd persistence projection.""" + + if not isinstance(label, str) or _JOB_LABEL_RE.fullmatch(label) is None: + raise ValueError("provider job label is invalid") + uid = os.getuid() + try: + result = _launchctl(["print", f"gui/{uid}/{label}"]) + except (OSError, RuntimeError, subprocess.SubprocessError): + return _PERSISTENCE_UNPROVEN + if result.returncode != 0 or not isinstance(result.stdout, str): + return _PERSISTENCE_UNPROVEN + lines = result.stdout.splitlines() + expected_header = f"gui/{uid}/{label} = {{" + if ( + not lines + or lines[0] != expected_header + or lines[-1] != "}" + or not _launchctl_transcript_structure_closed(lines) + ): + return _PERSISTENCE_UNPROVEN + + properties: list[str] = [] + event_triggers = 0 + nested_keepalive = 0 + malformed_persistence_shape = False + for line in lines[1:-1]: + match = _LAUNCHCTL_KEY_VALUE_LINE_RE.fullmatch(line) + if match is None: + continue + indent = match.group("indent") + key = " ".join(match.group("key").casefold().split()) + value = match.group("value").strip() + if key == "properties": + if ( + indent != "\t" + or _LAUNCHCTL_PROPERTIES_VALUE_RE.fullmatch(value) is None + ): + malformed_persistence_shape = True + else: + properties.append(value) + elif key == "event triggers": + if indent != "\t" or value != "{": + malformed_persistence_shape = True + else: + event_triggers += 1 + elif key == "keepalive": + if indent != "\t\t\t" or value not in {"0", "1"}: + malformed_persistence_shape = True + else: + nested_keepalive += 1 + elif key == "runatload": + # Current launchctl projects RunAtLoad through the top-level + # properties token. A standalone key is an unknown transcript + # shape, never evidence that the job is nonpersistent. + malformed_persistence_shape = True + + if ( + malformed_persistence_shape + or len(properties) != 1 + or event_triggers > 1 + or (nested_keepalive and event_triggers != 1) + ): + return _PERSISTENCE_UNPROVEN + tokens = { + token + for token in re.split(r"[|\s]+", properties[0].casefold()) + if token + } + if ( + tokens.intersection({"keepalive", "runatload"}) + or event_triggers == 1 + or nested_keepalive + ): + return _PERSISTENCE_PERSISTENT + return _PERSISTENCE_NONPERSISTENT + + +def _launchctl_transcript_structure_closed(lines: Sequence[str]) -> bool: + """Require a complete tab-indented launchctl brace transcript.""" + + depth = 1 + last_index = len(lines) - 1 + for index, line in enumerate(lines[1:], start=1): + stripped = line.lstrip("\t") + indent = len(line) - len(stripped) + if stripped == "}": + if indent != depth - 1: + return False + depth -= 1 + if depth == 0 and index != last_index: + return False + continue + if stripped.endswith(" = {") or stripped.endswith(" => {"): + if indent != depth: + return False + depth += 1 + continue + if stripped and (indent != depth or "{" in stripped or "}" in stripped): + return False + return depth == 0 + + def _broker_job_loaded() -> bool: return _job_loaded(BROKER_LABEL) @@ -4449,8 +4561,9 @@ def _broker_job_loaded() -> bool: # skips the ping and so silently masked the miscalibration. BROKER_COLD_START_TIMEOUT_SECONDS = 30.0 # Status needs only a short point-in-time quiescence observation after a -# successful liveness ping. Lifecycle mutations keep their full idle-proof -# bound and still require ``persistent_process`` to be false. +# successful liveness ping. ``process_idle`` reports that observation; +# ``persistence_state`` separately records the live launchd-properties proof. +# Lifecycle mutations keep their full idle-proof bound. BROKER_STATUS_IDLE_OBSERVATION_SECONDS = 1.0 # Upper bound on how long teardown may poll to reap a SIGKILLed leader when no # caller deadline is supplied (preserves the pre-existing wait bound). When a @@ -6284,26 +6397,37 @@ def broker_status() -> RuntimeResult: lane.socket_path, expected_type=stat.S_IFSOCK, mode=0o600 ) is not None ready = False - persistent_process = False + process_idle: bool | None = None + persistence_state = _PERSISTENCE_UNPROVEN + persistent_process: bool | None = None if loaded and socket_valid: - if lane.transport == "dispatcher": - ping = invoke_dispatcher_ping(lane=lane) - live = ping.status is RuntimeStatus.OK + persistence_state = _job_persistence_state(lane.label) + if persistence_state == _PERSISTENCE_NONPERSISTENT: + persistent_process = False + if lane.transport == "dispatcher": + ping = invoke_dispatcher_ping(lane=lane) + live = ping.status is RuntimeStatus.OK + else: + live = _broker_ping(lane.socket_path) + idle = _wait_for_job_idle( + lane.label, + deadline=( + time.monotonic() + + BROKER_STATUS_IDLE_OBSERVATION_SECONDS + ), + ) + # Liveness proves the selected lane is callable. Idleness + # is a separate lifecycle/quiescence signal: a legitimate + # in-flight request can keep the socket-activated job + # running after this ping without making the lane + # unavailable. Keep the observation even when liveness + # fails; overall status still follows liveness. + ready = live + process_idle = idle + elif persistence_state == _PERSISTENCE_PERSISTENT: + persistent_process = True else: - live = _broker_ping(lane.socket_path) - idle = _wait_for_job_idle( - lane.label, - deadline=( - time.monotonic() - + BROKER_STATUS_IDLE_OBSERVATION_SECONDS - ), - ) - # Liveness proves the selected lane is callable. Idleness is a - # separate lifecycle/quiescence signal: a legitimate in-flight - # provider request can keep the socket-activated job running - # after this ping without making the lane unavailable. - ready = live - persistent_process = not idle + persistence_state = _PERSISTENCE_UNPROVEN rollback_available = False retained = selector.get("retained") @@ -6327,11 +6451,23 @@ def broker_status() -> RuntimeResult: "rollback_available": rollback_available, "dispatcher_ready": ready and lane.transport == "dispatcher", "persistent_process": persistent_process, + "persistence_state": persistence_state, + "process_idle": process_idle, }, error=( "" if status is RuntimeStatus.OK - else "provider selected lane is installed but not executable" + else ( + "provider selected lane is configured for process persistence" + if persistence_state == _PERSISTENCE_PERSISTENT + else ( + "provider selected lane live persistence configuration is unproven" + if loaded + and socket_valid + and persistence_state == _PERSISTENCE_UNPROVEN + else "provider selected lane is installed but not executable" + ) + ) ), ) state = _read_current_broker_state(root) diff --git a/plugins/agent-collab/skills/agent-readiness/SKILL.md b/plugins/agent-collab/skills/agent-readiness/SKILL.md index 0b619df..3ec5d64 100644 --- a/plugins/agent-collab/skills/agent-readiness/SKILL.md +++ b/plugins/agent-collab/skills/agent-readiness/SKILL.md @@ -1,6 +1,6 @@ --- name: agent-readiness -version: 4.5.1 +version: 4.5.2 description: Evaluate whether an agent, model, CLI, plugin, or role is ready for a proposed responsibility. Use when the user says "agent readiness," "is this agent ready," "can Codex be primary," "can Grok handle this role," "promote this agent," "evaluate this worker," "review model readiness," or "/agent-collab:agent-readiness." Also offer this proactively before assigning a new primary, reviewer, worker, delegate, headless, release, or merge-related role to Claude, Codex, Antigravity/Gemini, Grok, or a future agent. --- diff --git a/plugins/agent-collab/skills/agent-runtime-status/SKILL.md b/plugins/agent-collab/skills/agent-runtime-status/SKILL.md index d8dece8..86c1b86 100644 --- a/plugins/agent-collab/skills/agent-runtime-status/SKILL.md +++ b/plugins/agent-collab/skills/agent-runtime-status/SKILL.md @@ -1,6 +1,6 @@ --- name: agent-runtime-status -version: 4.5.1 +version: 4.5.2 defaults: tier: Fast effort: low diff --git a/plugins/agent-collab/skills/ai-writing-auditor/SKILL.md b/plugins/agent-collab/skills/ai-writing-auditor/SKILL.md index 5de1960..82ca318 100644 --- a/plugins/agent-collab/skills/ai-writing-auditor/SKILL.md +++ b/plugins/agent-collab/skills/ai-writing-auditor/SKILL.md @@ -1,6 +1,6 @@ --- name: ai-writing-auditor -version: 4.5.1 +version: 4.5.2 description: Audits prose for the tells of machine-generated writing and rewrites it to read as if a careful human wrote it, without softening the actual content. Use when the user says "de-AI this text", "audit this writing for AI patterns", "make this read human", or "/agent-collab:ai-writing-auditor." Also offer this proactively when a piece of customer-facing or published prose is dense with the stock phrasing and mechanical structure typical of unedited model output. --- diff --git a/plugins/agent-collab/skills/architect/SKILL.md b/plugins/agent-collab/skills/architect/SKILL.md index f46248c..d990eb7 100644 --- a/plugins/agent-collab/skills/architect/SKILL.md +++ b/plugins/agent-collab/skills/architect/SKILL.md @@ -1,6 +1,6 @@ --- name: architect -version: 4.5.1 +version: 4.5.2 description: Request read-only architecture consultation for codebase analysis, system design, implementation planning, decomposition, or long-horizon coding strategy. Use when the user says "ask the architect," "have Grok design this," "architecture consultation," "plan this implementation," "decompose this build," "analyze the system design," or "/agent-collab:architect." Also offer this proactively before a substantial multi-system or long-horizon implementation where an independent architecture pass can reduce rework. This role never edits files, runs shell commands or tests, mutates a worktree, opens PRs, merges, or deploys. --- diff --git a/plugins/agent-collab/skills/autonomy-readiness/SKILL.md b/plugins/agent-collab/skills/autonomy-readiness/SKILL.md index 28e71f9..a8949b2 100644 --- a/plugins/agent-collab/skills/autonomy-readiness/SKILL.md +++ b/plugins/agent-collab/skills/autonomy-readiness/SKILL.md @@ -1,6 +1,6 @@ --- name: autonomy-readiness -version: 4.5.1 +version: 4.5.2 description: Evaluate whether an autonomous, always-on, scheduled, headless, or self-evolving workflow is ready to run safely. Use when the user says "autonomy readiness," "activation gate review," "is this workflow ready to run autonomously," "go/no-go autonomy," "always-on readiness," "headless operation review," or "/agent-collab:autonomy-readiness." Also offer this proactively before enabling background agents, recurring automations, auto-merge/self-evolution, external actions, unattended host runs, or any workflow that can continue without a human watching. --- diff --git a/plugins/agent-collab/skills/brainstorm/SKILL.md b/plugins/agent-collab/skills/brainstorm/SKILL.md index b0b8f2c..a3190c9 100644 --- a/plugins/agent-collab/skills/brainstorm/SKILL.md +++ b/plugins/agent-collab/skills/brainstorm/SKILL.md @@ -1,6 +1,6 @@ --- name: brainstorm -version: 4.5.1 +version: 4.5.2 defaults: tier: Fast effort: low diff --git a/plugins/agent-collab/skills/chain-configurator/SKILL.md b/plugins/agent-collab/skills/chain-configurator/SKILL.md index fb745a4..45a63ef 100644 --- a/plugins/agent-collab/skills/chain-configurator/SKILL.md +++ b/plugins/agent-collab/skills/chain-configurator/SKILL.md @@ -1,6 +1,6 @@ --- name: chain-configurator -version: 4.5.1 +version: 4.5.2 defaults: tier: Standard effort: medium diff --git a/plugins/agent-collab/skills/chain/SKILL.md b/plugins/agent-collab/skills/chain/SKILL.md index dd5c3b1..e27c9a5 100644 --- a/plugins/agent-collab/skills/chain/SKILL.md +++ b/plugins/agent-collab/skills/chain/SKILL.md @@ -1,6 +1,6 @@ --- name: chain -version: 4.5.1 +version: 4.5.2 defaults: tier: Standard effort: medium diff --git a/plugins/agent-collab/skills/code-review/SKILL.md b/plugins/agent-collab/skills/code-review/SKILL.md index 0fdb7c6..fb2d1ee 100644 --- a/plugins/agent-collab/skills/code-review/SKILL.md +++ b/plugins/agent-collab/skills/code-review/SKILL.md @@ -1,6 +1,6 @@ --- name: code-review -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/compose-skills/SKILL.md b/plugins/agent-collab/skills/compose-skills/SKILL.md index 56a5a23..2507798 100644 --- a/plugins/agent-collab/skills/compose-skills/SKILL.md +++ b/plugins/agent-collab/skills/compose-skills/SKILL.md @@ -1,6 +1,6 @@ --- name: compose-skills -version: 4.5.1 +version: 4.5.2 description: Select a bounded, token-aware combination of collaboration skills or task lenses before execution. Use when the user says "compose skills," "which skills should I use," "use skill composition," "select a recipe," "combine these skills," or "/agent-collab:compose-skills." Also offer this proactively when a task plausibly needs multiple lenses, reviewers, or agents and would benefit from progressive disclosure, explicit fan-out limits, and a smallest-useful-skill plan before routing or loading full skill bodies. --- diff --git a/plugins/agent-collab/skills/data-engineer/SKILL.md b/plugins/agent-collab/skills/data-engineer/SKILL.md index c376ee1..f64d611 100644 --- a/plugins/agent-collab/skills/data-engineer/SKILL.md +++ b/plugins/agent-collab/skills/data-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: data-engineer -version: 4.5.1 +version: 4.5.2 description: Builds and hardens the pipelines and warehouse structures that move data from source systems to the people and systems that consume it. Use when the user says "build the ETL pipeline", "design the dbt models", "orchestrate this pipeline", or "design the warehouse schema", or "/agent-collab:data-engineer." Also offer this proactively when a pipeline lacks idempotency, has no data-quality checks, or moves data through undocumented schema contracts. --- diff --git a/plugins/agent-collab/skills/debate/SKILL.md b/plugins/agent-collab/skills/debate/SKILL.md index 221d8b3..29c029e 100644 --- a/plugins/agent-collab/skills/debate/SKILL.md +++ b/plugins/agent-collab/skills/debate/SKILL.md @@ -1,6 +1,6 @@ --- name: debate -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/delegate/SKILL.md b/plugins/agent-collab/skills/delegate/SKILL.md index c95532e..96d2fb0 100644 --- a/plugins/agent-collab/skills/delegate/SKILL.md +++ b/plugins/agent-collab/skills/delegate/SKILL.md @@ -1,6 +1,6 @@ --- name: delegate -version: 4.5.1 +version: 4.5.2 defaults: tier: Fast effort: low diff --git a/plugins/agent-collab/skills/dev-delegate/SKILL.md b/plugins/agent-collab/skills/dev-delegate/SKILL.md index 56b2af1..b4589a9 100644 --- a/plugins/agent-collab/skills/dev-delegate/SKILL.md +++ b/plugins/agent-collab/skills/dev-delegate/SKILL.md @@ -1,6 +1,6 @@ --- name: dev-delegate -version: 4.5.1 +version: 4.5.2 defaults: tier: Standard effort: medium diff --git a/plugins/agent-collab/skills/elixir-engineer/SKILL.md b/plugins/agent-collab/skills/elixir-engineer/SKILL.md index 5b57b6d..31a8cc6 100644 --- a/plugins/agent-collab/skills/elixir-engineer/SKILL.md +++ b/plugins/agent-collab/skills/elixir-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: elixir-engineer -version: 4.5.1 +version: 4.5.2 description: Senior Elixir and OTP engineer for fault-tolerant, concurrent, and Phoenix-based systems. Use when the user says "review this supervision tree", "why did this GenServer crash", "help me design this OTP process architecture", or "/agent-collab:elixir-engineer." Also offer this proactively when a change adds a new process, alters a restart strategy, or touches a Phoenix channel or LiveView boundary. --- diff --git a/plugins/agent-collab/skills/eval-engineer/SKILL.md b/plugins/agent-collab/skills/eval-engineer/SKILL.md index d7fb02b..5aa0fc9 100644 --- a/plugins/agent-collab/skills/eval-engineer/SKILL.md +++ b/plugins/agent-collab/skills/eval-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: eval-engineer -version: 4.5.1 +version: 4.5.2 description: Designs evaluation suites and scoring methods that measure whether an AI-backed workflow is actually good enough to ship. Use when the user says "design evals for this", "build an eval suite", "how should we measure this workflow", or "/agent-collab:eval-engineer." Also offer this proactively when a project ships a prompt, retrieval pipeline, or agent workflow with no structured way to tell whether a change made it better or worse. --- diff --git a/plugins/agent-collab/skills/go-engineer/SKILL.md b/plugins/agent-collab/skills/go-engineer/SKILL.md index 1dea339..b152be8 100644 --- a/plugins/agent-collab/skills/go-engineer/SKILL.md +++ b/plugins/agent-collab/skills/go-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: go-engineer -version: 4.5.1 +version: 4.5.2 description: Senior Go engineer for concurrent, service-oriented, and cloud-native code. Use when the user says "review this Go service for concurrency bugs", "help me design this Go interface", "why is this goroutine leaking", or "/agent-collab:go-engineer." Also offer this proactively when a change adds a goroutine, channel, or context-cancellation path without an obvious termination guarantee. --- diff --git a/plugins/agent-collab/skills/governance-review/SKILL.md b/plugins/agent-collab/skills/governance-review/SKILL.md index 580b482..ae631a4 100644 --- a/plugins/agent-collab/skills/governance-review/SKILL.md +++ b/plugins/agent-collab/skills/governance-review/SKILL.md @@ -1,6 +1,6 @@ --- name: governance-review -version: 4.5.1 +version: 4.5.2 description: Use when the operator says "governance review," "high-stakes review," "tiebreaker," or "second opinion." Also offer this proactively when reviewer-family independence must be enforced. --- diff --git a/plugins/agent-collab/skills/hallucination-investigator/SKILL.md b/plugins/agent-collab/skills/hallucination-investigator/SKILL.md index 1d2e9a8..6a402db 100644 --- a/plugins/agent-collab/skills/hallucination-investigator/SKILL.md +++ b/plugins/agent-collab/skills/hallucination-investigator/SKILL.md @@ -1,6 +1,6 @@ --- name: hallucination-investigator -version: 4.5.1 +version: 4.5.2 description: Traces a specific wrong or fabricated AI output back to its root cause in context, retrieval, prompting, or tool use, and recommends the most targeted fix. Use when the user says "why is it hallucinating", "investigate this fabrication", "trace this wrong answer", or "/agent-collab:hallucination-investigator." Also offer this proactively when a factuality failure has been reported but no one has yet reconstructed why the system produced that specific wrong answer. --- diff --git a/plugins/agent-collab/skills/incident-responder/SKILL.md b/plugins/agent-collab/skills/incident-responder/SKILL.md index 53b0fc9..691ff06 100644 --- a/plugins/agent-collab/skills/incident-responder/SKILL.md +++ b/plugins/agent-collab/skills/incident-responder/SKILL.md @@ -1,6 +1,6 @@ --- name: incident-responder -version: 4.5.1 +version: 4.5.2 description: Leads live incident response — triage, containment, evidence-driven root-cause analysis, and postmortem writeups for active outages or breaches. Use when the user says "we have an incident", "production is down", or "run the postmortem for this outage", or "/agent-collab:incident-responder." Also offer this proactively when the user is trying to write the incident timeline or communicate status while a service disruption is still unresolved. --- diff --git a/plugins/agent-collab/skills/intent-check/SKILL.md b/plugins/agent-collab/skills/intent-check/SKILL.md index 6cf9e33..b9f0b34 100644 --- a/plugins/agent-collab/skills/intent-check/SKILL.md +++ b/plugins/agent-collab/skills/intent-check/SKILL.md @@ -1,6 +1,6 @@ --- name: intent-check -version: 4.5.1 +version: 4.5.2 defaults: tier: Fast effort: low diff --git a/plugins/agent-collab/skills/knowledge-compile/SKILL.md b/plugins/agent-collab/skills/knowledge-compile/SKILL.md index c9f1080..48885fb 100644 --- a/plugins/agent-collab/skills/knowledge-compile/SKILL.md +++ b/plugins/agent-collab/skills/knowledge-compile/SKILL.md @@ -1,6 +1,6 @@ --- name: knowledge-compile -version: 4.5.1 +version: 4.5.2 description: Compile multiple sources into a durable, cited knowledge dossier without mixing claims, assumptions, and decisions. Use when the user says "compile knowledge," "build a dossier," "create a knowledge base," "synthesize these sources," "preserve research context," "make this reviewable later," or "/agent-collab:knowledge-compile." Also offer this proactively when a task spans several repos, PRs, papers, articles, logs, agent messages, or drafts and future agents need source-separated context for independent review. --- diff --git a/plugins/agent-collab/skills/kubernetes-specialist/SKILL.md b/plugins/agent-collab/skills/kubernetes-specialist/SKILL.md index a687853..82aac69 100644 --- a/plugins/agent-collab/skills/kubernetes-specialist/SKILL.md +++ b/plugins/agent-collab/skills/kubernetes-specialist/SKILL.md @@ -1,6 +1,6 @@ --- name: kubernetes-specialist -version: 4.5.1 +version: 4.5.2 description: Designs, hardens, and troubleshoots Kubernetes clusters and the workloads running on them. Use when the user says "review this Kubernetes manifest", "why is my pod crash-looping", or "help me design a multi-tenant cluster", or "/agent-collab:kubernetes-specialist." Also offer this proactively when a change touches Deployment/StatefulSet/DaemonSet specs, RBAC bindings, network policies, or persistent volume claims. --- diff --git a/plugins/agent-collab/skills/llm-architect/SKILL.md b/plugins/agent-collab/skills/llm-architect/SKILL.md index d654d69..6f07373 100644 --- a/plugins/agent-collab/skills/llm-architect/SKILL.md +++ b/plugins/agent-collab/skills/llm-architect/SKILL.md @@ -1,6 +1,6 @@ --- name: llm-architect -version: 4.5.1 +version: 4.5.2 description: Designs the structural shape of an LLM-powered system — how context is assembled, how tools and retrieval are wired in, and how output flows to the caller. Use when the user says "design the RAG pipeline", "plan the agent architecture", or "help me structure the prompt pipeline", or "/agent-collab:llm-architect." Also offer this proactively when a project is wiring multiple prompts, tools, and retrieval steps together without a clear contract between them. --- diff --git a/plugins/agent-collab/skills/logic-check/SKILL.md b/plugins/agent-collab/skills/logic-check/SKILL.md index 61819bd..0c7aa6a 100644 --- a/plugins/agent-collab/skills/logic-check/SKILL.md +++ b/plugins/agent-collab/skills/logic-check/SKILL.md @@ -1,6 +1,6 @@ --- name: logic-check -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: xhigh diff --git a/plugins/agent-collab/skills/long-context/SKILL.md b/plugins/agent-collab/skills/long-context/SKILL.md index a5668e4..0abae23 100644 --- a/plugins/agent-collab/skills/long-context/SKILL.md +++ b/plugins/agent-collab/skills/long-context/SKILL.md @@ -1,6 +1,6 @@ --- name: long-context -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/merge-resolve/SKILL.md b/plugins/agent-collab/skills/merge-resolve/SKILL.md index 7cb565d..325a4e7 100644 --- a/plugins/agent-collab/skills/merge-resolve/SKILL.md +++ b/plugins/agent-collab/skills/merge-resolve/SKILL.md @@ -1,6 +1,6 @@ --- name: merge-resolve -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/migration-doctor/SKILL.md b/plugins/agent-collab/skills/migration-doctor/SKILL.md index 474f057..0c85ef5 100644 --- a/plugins/agent-collab/skills/migration-doctor/SKILL.md +++ b/plugins/agent-collab/skills/migration-doctor/SKILL.md @@ -1,6 +1,6 @@ --- name: migration-doctor -version: 4.5.1 +version: 4.5.2 description: Use when the user says "migration doctor," "check old collaboration plugins," "verify agent-collab migration," or "/agent-collab:migration-doctor." Also offer this proactively after installing or updating agent-collab, when provider routing is blocked, or when a retired package may still be selected from an installed plugin or cache. --- diff --git a/plugins/agent-collab/skills/mlops-engineer/SKILL.md b/plugins/agent-collab/skills/mlops-engineer/SKILL.md index c56fcea..f49d3a1 100644 --- a/plugins/agent-collab/skills/mlops-engineer/SKILL.md +++ b/plugins/agent-collab/skills/mlops-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: mlops-engineer -version: 4.5.1 +version: 4.5.2 description: Designs and hardens the infrastructure that carries models from training through production serving. Use when the user says "set up a model registry", "build the training pipeline", "deploy this model to production", or "/agent-collab:mlops-engineer." Also offer this proactively when a project trains or serves models but has no versioned artifacts, no promotion gate, or no monitoring for prediction quality. --- diff --git a/plugins/agent-collab/skills/orchestrate/SKILL.md b/plugins/agent-collab/skills/orchestrate/SKILL.md index 55078f8..b163ade 100644 --- a/plugins/agent-collab/skills/orchestrate/SKILL.md +++ b/plugins/agent-collab/skills/orchestrate/SKILL.md @@ -1,6 +1,6 @@ --- name: orchestrate -version: 4.5.1 +version: 4.5.2 defaults: tier: Standard effort: medium diff --git a/plugins/agent-collab/skills/postgres-engineer/SKILL.md b/plugins/agent-collab/skills/postgres-engineer/SKILL.md index 3a10b7d..9b28f05 100644 --- a/plugins/agent-collab/skills/postgres-engineer/SKILL.md +++ b/plugins/agent-collab/skills/postgres-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: postgres-engineer -version: 4.5.1 +version: 4.5.2 description: Administers and hardens PostgreSQL at the engine level — configuration, replication, vacuum behavior, and locking under real workloads. Use when the user says "tune postgres for this workload", "design a vacuum strategy", "set up replication", or "fix our connection pooling", or "/agent-collab:postgres-engineer." Also offer this proactively when a PostgreSQL deployment shows replication lag, bloat, connection exhaustion, or a failover plan that has never been tested. --- diff --git a/plugins/agent-collab/skills/prompt-regression-tester/SKILL.md b/plugins/agent-collab/skills/prompt-regression-tester/SKILL.md index e8de222..f624022 100644 --- a/plugins/agent-collab/skills/prompt-regression-tester/SKILL.md +++ b/plugins/agent-collab/skills/prompt-regression-tester/SKILL.md @@ -1,6 +1,6 @@ --- name: prompt-regression-tester -version: 4.5.1 +version: 4.5.2 description: Builds and runs comparison suites that catch behavioral drift when a prompt, model, or workflow configuration changes. Use when the user says "did the prompt change regress anything", "compare these prompt versions", "regression-test this prompt change", or "/agent-collab:prompt-regression-tester." Also offer this proactively when someone is about to swap a prompt, model, or tool wiring in a live workflow without a way to check whether existing behavior held. --- diff --git a/plugins/agent-collab/skills/qa-verify/SKILL.md b/plugins/agent-collab/skills/qa-verify/SKILL.md index 3a8037c..0dcd8ba 100644 --- a/plugins/agent-collab/skills/qa-verify/SKILL.md +++ b/plugins/agent-collab/skills/qa-verify/SKILL.md @@ -1,6 +1,6 @@ --- name: qa-verify -version: 4.5.1 +version: 4.5.2 defaults: tier: Fast effort: low diff --git a/plugins/agent-collab/skills/red-team/SKILL.md b/plugins/agent-collab/skills/red-team/SKILL.md index 96c45a6..56c1543 100644 --- a/plugins/agent-collab/skills/red-team/SKILL.md +++ b/plugins/agent-collab/skills/red-team/SKILL.md @@ -1,6 +1,6 @@ --- name: red-team -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/route/SKILL.md b/plugins/agent-collab/skills/route/SKILL.md index ec709c5..1741901 100644 --- a/plugins/agent-collab/skills/route/SKILL.md +++ b/plugins/agent-collab/skills/route/SKILL.md @@ -1,6 +1,6 @@ --- name: route -version: 4.5.1 +version: 4.5.2 description: Use when the operator says "ask Codex," "target=gemini," "target=grok," "target=composer," or explicitly names a managed backend. Also offer this proactively when routing needs dynamic primary-family exclusion. --- diff --git a/plugins/agent-collab/skills/rust-engineer/SKILL.md b/plugins/agent-collab/skills/rust-engineer/SKILL.md index f8b578b..c900776 100644 --- a/plugins/agent-collab/skills/rust-engineer/SKILL.md +++ b/plugins/agent-collab/skills/rust-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: rust-engineer -version: 4.5.1 +version: 4.5.2 description: Senior Rust engineer for ownership-heavy, performance-sensitive, and systems-level code. Use when the user says "review this Rust code for soundness", "help me fix this borrow checker error", "optimize this Rust hot path", or "/agent-collab:rust-engineer." Also offer this proactively when a diff introduces unsafe blocks, changes lifetime bounds, or touches an async task's cancellation path. --- diff --git a/plugins/agent-collab/skills/second-opinion/SKILL.md b/plugins/agent-collab/skills/second-opinion/SKILL.md index 194e431..997d175 100644 --- a/plugins/agent-collab/skills/second-opinion/SKILL.md +++ b/plugins/agent-collab/skills/second-opinion/SKILL.md @@ -1,6 +1,6 @@ --- name: second-opinion -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/simulate-user/SKILL.md b/plugins/agent-collab/skills/simulate-user/SKILL.md index b43538a..334e104 100644 --- a/plugins/agent-collab/skills/simulate-user/SKILL.md +++ b/plugins/agent-collab/skills/simulate-user/SKILL.md @@ -1,6 +1,6 @@ --- name: simulate-user -version: 4.5.1 +version: 4.5.2 defaults: tier: Fast effort: low diff --git a/plugins/agent-collab/skills/sql-engineer/SKILL.md b/plugins/agent-collab/skills/sql-engineer/SKILL.md index cf69e5b..c1aeed8 100644 --- a/plugins/agent-collab/skills/sql-engineer/SKILL.md +++ b/plugins/agent-collab/skills/sql-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: sql-engineer -version: 4.5.1 +version: 4.5.2 description: Senior SQL specialist for query design, query optimization, and schema-aware performance work across relational databases. Use when the user says "optimize this SQL query", "explain this query plan", "design these indexes", or "/agent-collab:sql-engineer." Also offer this proactively when a diff adds a new query against a large table, changes a join or aggregation, or introduces a schema change that could affect an existing access pattern. --- diff --git a/plugins/agent-collab/skills/sre-engineer/SKILL.md b/plugins/agent-collab/skills/sre-engineer/SKILL.md index e2a03b8..6cc5b63 100644 --- a/plugins/agent-collab/skills/sre-engineer/SKILL.md +++ b/plugins/agent-collab/skills/sre-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: sre-engineer -version: 4.5.1 +version: 4.5.2 description: Builds and improves system reliability through SLO design, error-budget policy, toil reduction, and resilience architecture. Use when the user says "help me define SLOs for this service", "what's our error budget burn rate", or "reduce the toil in this on-call rotation", or "/agent-collab:sre-engineer." Also offer this proactively when a reliability review, capacity plan, or alert-quality audit is warranted ahead of a launch or growth milestone. --- diff --git a/plugins/agent-collab/skills/start-inbox-monitor/SKILL.md b/plugins/agent-collab/skills/start-inbox-monitor/SKILL.md index 48bec72..0783b94 100644 --- a/plugins/agent-collab/skills/start-inbox-monitor/SKILL.md +++ b/plugins/agent-collab/skills/start-inbox-monitor/SKILL.md @@ -1,6 +1,6 @@ --- name: start-inbox-monitor -version: 4.5.1 +version: 4.5.2 description: Use when the user says "start the inbox monitor", "keep monitoring agent messages", or "/agent-collab:start-inbox-monitor", or when an active cross-agent thread needs durable session-scoped inbox monitoring. Select the native Codex, Claude, or Antigravity lifecycle instead of inventing a universal polling loop. --- diff --git a/plugins/agent-collab/skills/teamwork/SKILL.md b/plugins/agent-collab/skills/teamwork/SKILL.md index 703153b..683989d 100644 --- a/plugins/agent-collab/skills/teamwork/SKILL.md +++ b/plugins/agent-collab/skills/teamwork/SKILL.md @@ -1,6 +1,6 @@ --- name: teamwork -version: 4.5.1 +version: 4.5.2 defaults: tier: Standard effort: medium diff --git a/plugins/agent-collab/skills/terraform-engineer/SKILL.md b/plugins/agent-collab/skills/terraform-engineer/SKILL.md index a965960..09acc52 100644 --- a/plugins/agent-collab/skills/terraform-engineer/SKILL.md +++ b/plugins/agent-collab/skills/terraform-engineer/SKILL.md @@ -1,6 +1,6 @@ --- name: terraform-engineer -version: 4.5.1 +version: 4.5.2 description: Designs, refactors, and reviews Terraform infrastructure-as-code across cloud providers, with a focus on module contracts, state safety, and reviewable plans. Use when the user says "review this Terraform plan", "help me design a reusable module", or "why did this apply want to replace my resource", or "/agent-collab:terraform-engineer." Also offer this proactively when a change touches module interfaces, state backend configuration, or resource lifecycle blocks. --- diff --git a/plugins/agent-collab/skills/ui-to-code/SKILL.md b/plugins/agent-collab/skills/ui-to-code/SKILL.md index fa9f996..5a6a899 100644 --- a/plugins/agent-collab/skills/ui-to-code/SKILL.md +++ b/plugins/agent-collab/skills/ui-to-code/SKILL.md @@ -1,6 +1,6 @@ --- name: ui-to-code -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/untrusted-audit/SKILL.md b/plugins/agent-collab/skills/untrusted-audit/SKILL.md index 4597664..09e9413 100644 --- a/plugins/agent-collab/skills/untrusted-audit/SKILL.md +++ b/plugins/agent-collab/skills/untrusted-audit/SKILL.md @@ -1,6 +1,6 @@ --- name: untrusted-audit -version: 4.5.1 +version: 4.5.2 description: Audit an external or untrusted source before using it in code, skills, plugins, workflows, prompts, or operations. Use when the user says "audit this untrusted source," "can we use this repo," "review this gist," "prompt injection audit," "is this plugin safe," "evaluate this methodology," or "/agent-collab:untrusted-audit." Also offer this proactively when a task would incorporate third-party instructions, code, scripts, hooks, generated skills, package manifests, install steps, or auto-updated methodology into the workspace or agent environment. --- diff --git a/plugins/agent-collab/skills/visual-review/SKILL.md b/plugins/agent-collab/skills/visual-review/SKILL.md index 8245827..742a648 100644 --- a/plugins/agent-collab/skills/visual-review/SKILL.md +++ b/plugins/agent-collab/skills/visual-review/SKILL.md @@ -1,6 +1,6 @@ --- name: visual-review -version: 4.5.1 +version: 4.5.2 defaults: tier: Advanced effort: high diff --git a/plugins/agent-collab/skills/worker/SKILL.md b/plugins/agent-collab/skills/worker/SKILL.md index 84ab9d4..b133e07 100644 --- a/plugins/agent-collab/skills/worker/SKILL.md +++ b/plugins/agent-collab/skills/worker/SKILL.md @@ -1,6 +1,6 @@ --- name: worker -version: 4.5.1 +version: 4.5.2 description: Use when the operator says "delegate this implementation," "use Gemini for this corpus," "ask Codex to build," or "use Composer for codegen." Also offer this proactively when a bounded non-governance task benefits from a managed worker. --- diff --git a/scripts/skill-build-config.json b/scripts/skill-build-config.json index 36f0ccd..cab5df0 100644 --- a/scripts/skill-build-config.json +++ b/scripts/skill-build-config.json @@ -20,7 +20,7 @@ "tier_flash_resolves_to_claude": "an asynchronous Anthropic inbox review; never a synchronous invocation", "tier_pro_resolves_to_gemini": "an eligible managed Google-family reviewer at high effort", "tier_flash_resolves_to_gemini": "an eligible managed Google-family reviewer at low effort", - "skill_version": "4.5.1", + "skill_version": "4.5.2", "agent_runtime_status_defaults_block": "defaults:\n tier: Fast\n effort: low\n", "merge_resolve_defaults_block": "defaults:\n tier: Advanced\n effort: high\n", "merge_resolve_call_params": "`effort='high'` in every eligible advisory row and no `tier` request field", diff --git a/tests/fixtures/launchctl_print_keepalive.txt b/tests/fixtures/launchctl_print_keepalive.txt new file mode 100644 index 0000000..9998096 --- /dev/null +++ b/tests/fixtures/launchctl_print_keepalive.txt @@ -0,0 +1,14 @@ +gui/501/com.agent-collab.provider-broker = { + active count = 1 + path = /Library/LaunchAgents/com.example.keepalive.plist + type = LaunchAgent + state = running + + program = /usr/bin/example + minimum runtime = 10 + runs = 1 + pid = 123 + + jetsamproperties category = daemon + properties = keepalive | runatload | inferred program | managed LWCR | has LWCR +} diff --git a/tests/fixtures/launchctl_print_keepalive_dictionary.txt b/tests/fixtures/launchctl_print_keepalive_dictionary.txt new file mode 100644 index 0000000..f62186b --- /dev/null +++ b/tests/fixtures/launchctl_print_keepalive_dictionary.txt @@ -0,0 +1,28 @@ +gui/501/com.agent-collab.provider-broker = { + active count = 0 + path = /Library/LaunchAgents/com.example.conditional.plist + type = LaunchAgent + state = not running + + program = /usr/bin/example + minimum runtime = 1 + runs = 0 + last exit code = (never exited) + + event triggers = { + com.example.launchd.PathState => { + keepalive = 0 + service = com.example.conditional + stream = com.example.events + monitor = com.example.monitor + descriptor = { + "PathState" => { + "/var/run/example.ready" => true + } + } + } + } + + jetsamproperties category = daemon + properties = inferred program +} diff --git a/tests/fixtures/launchctl_print_nonpersistent.txt b/tests/fixtures/launchctl_print_nonpersistent.txt new file mode 100644 index 0000000..03b8da7 --- /dev/null +++ b/tests/fixtures/launchctl_print_nonpersistent.txt @@ -0,0 +1,26 @@ +gui/501/com.agent-collab.provider-broker = { + active count = 0 + path = /Library/Application Support/AgentCollab/provider-dispatcher-0123456789abcdef0123456789abcdef.plist + type = LaunchAgent + state = not running + + program = /Library/Application Support/AgentCollab/versions/example/agent-collab-runtime + arguments = { + /Library/Application Support/AgentCollab/versions/example/agent-collab-runtime + dispatcher + --protocol + 2 + } + + sockets = { + "ProviderBroker" = { + type = stream + path = /Library/Application Support/AgentCollab/provider-dispatcher-0123456789abcdef0123456789abcdef.sock + active = 1 + passive = 1 + } + } + + jetsamproperties category = daemon + properties = +} diff --git a/tests/test_agent_collab_migration.py b/tests/test_agent_collab_migration.py index 6f62f1c..4c61ec3 100644 --- a/tests/test_agent_collab_migration.py +++ b/tests/test_agent_collab_migration.py @@ -977,7 +977,30 @@ def test_doctor_reports_responsive_busy_broker_ready(self) -> None: result={ "active": True, "dispatcher_ready": True, - "persistent_process": True, + "persistent_process": False, + "process_idle": False, + }, + ), + ) + with mock.patch.object( + self.doctor, "_load_runtime_client", return_value=runtime + ): + self.assertEqual(self.doctor._broker_runtime_state(), "ready") + + def test_doctor_accepts_pre_process_idle_broker_status_shape(self) -> None: + statuses = types.SimpleNamespace( + OK="ok", + UNAVAILABLE="unavailable", + INTEGRITY_ERROR="integrity_error", + ) + runtime = types.SimpleNamespace( + RuntimeStatus=statuses, + broker_status=lambda: types.SimpleNamespace( + status=statuses.OK, + result={ + "active": True, + "dispatcher_ready": True, + "persistent_process": False, }, ), ) diff --git a/tests/test_agent_collab_runtime_client.py b/tests/test_agent_collab_runtime_client.py index 7277b88..612d14a 100644 --- a/tests/test_agent_collab_runtime_client.py +++ b/tests/test_agent_collab_runtime_client.py @@ -4190,6 +4190,8 @@ def test_broker_status_accepts_live_v1_selected_green_projection(self) -> None: self.client, "_verify_macos_signature", return_value=(True, "") ), mock.patch.object( self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_job_persistence_state", return_value="nonpersistent" ), mock.patch.object( self.client, "invoke_dispatcher_ping", return_value=ping ), mock.patch.object( @@ -4201,6 +4203,9 @@ def test_broker_status_accepts_live_v1_selected_green_projection(self) -> None: self.assertTrue(result.result["active"]) self.assertTrue(result.result["dispatcher_ready"]) self.assertFalse(result.result["rollback_available"]) + self.assertFalse(result.result["persistent_process"]) + self.assertEqual(result.result["persistence_state"], "nonpersistent") + self.assertTrue(result.result["process_idle"]) self.assertEqual( result.result["selected"]["artifact_sha256"], lane.artifact_digest ) @@ -4241,6 +4246,8 @@ def test_broker_status_requires_stable_selected_dispatcher_ping(self) -> None: self.client, "_verify_macos_signature", return_value=(True, "") ), mock.patch.object( self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_job_persistence_state", return_value="nonpersistent" ), mock.patch.object( self.client, "invoke_dispatcher_ping", return_value=unavailable ), mock.patch.object( @@ -4251,8 +4258,157 @@ def test_broker_status_requires_stable_selected_dispatcher_ping(self) -> None: self.assertEqual(result.status, self.client.RuntimeStatus.UNAVAILABLE) self.assertFalse(result.result["active"]) self.assertFalse(result.result["dispatcher_ready"]) + self.assertFalse(result.result["persistent_process"]) + self.assertTrue( + result.result["process_idle"], + "quiescence is independent from the failed liveness proof", + ) + + def test_broker_status_rejects_observed_live_process_persistence( + self, + ) -> None: + root = self.root / "broker-state" + self._install_modern_selected(root, body="#!/bin/sh\nexit 0\n") + with mock.patch.object( + self.client, "_broker_root", return_value=root + ), mock.patch.object( + self.client, "_verify_macos_signature", return_value=(True, "") + ), mock.patch.object( + self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_job_persistence_state", return_value="persistent" + ), mock.patch.object( + self.client, "invoke_dispatcher_ping" + ) as ping, mock.patch.object( + self.client, "_wait_for_job_idle" + ) as wait_for_idle: + result = self.client.broker_status() + + self.assertEqual(result.status, self.client.RuntimeStatus.UNAVAILABLE) + self.assertFalse(result.result["active"]) + self.assertFalse(result.result["dispatcher_ready"]) + self.assertTrue(result.result["persistent_process"]) + self.assertEqual(result.result["persistence_state"], "persistent") + self.assertIsNone(result.result["process_idle"]) + self.assertEqual( + result.error, + "provider selected lane is configured for process persistence", + ) + ping.assert_not_called() + wait_for_idle.assert_not_called() - def test_broker_status_reports_responsive_dispatcher_ready_while_busy( + def test_broker_status_preserves_unproven_live_persistence_as_unknown( + self, + ) -> None: + root = self.root / "broker-state" + self._install_modern_selected(root, body="#!/bin/sh\nexit 0\n") + with mock.patch.object( + self.client, "_broker_root", return_value=root + ), mock.patch.object( + self.client, "_verify_macos_signature", return_value=(True, "") + ), mock.patch.object( + self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_job_persistence_state", return_value="unproven" + ), mock.patch.object( + self.client, "invoke_dispatcher_ping" + ) as ping, mock.patch.object( + self.client, "_wait_for_job_idle" + ) as wait_for_idle: + result = self.client.broker_status() + + self.assertEqual(result.status, self.client.RuntimeStatus.UNAVAILABLE) + self.assertFalse(result.result["active"]) + self.assertFalse(result.result["dispatcher_ready"]) + self.assertIsNone(result.result["persistent_process"]) + self.assertEqual(result.result["persistence_state"], "unproven") + self.assertIsNone(result.result["process_idle"]) + self.assertEqual( + result.error, + "provider selected lane live persistence configuration is unproven", + ) + ping.assert_not_called() + wait_for_idle.assert_not_called() + + def test_broker_status_reports_idle_unmeasured_when_selected_job_is_unloaded( + self, + ) -> None: + root = self.root / "broker-state" + self._install_modern_selected(root, body="#!/bin/sh\nexit 0\n") + with mock.patch.object( + self.client, "_broker_root", return_value=root + ), mock.patch.object( + self.client, "_verify_macos_signature", return_value=(True, "") + ), mock.patch.object( + self.client, "_job_loaded", return_value=False + ), mock.patch.object( + self.client, "_job_persistence_state" + ) as persistence, mock.patch.object( + self.client, "invoke_dispatcher_ping" + ) as ping, mock.patch.object( + self.client, "_wait_for_job_idle" + ) as wait_for_idle: + result = self.client.broker_status() + + self.assertEqual(result.status, self.client.RuntimeStatus.UNAVAILABLE) + self.assertFalse(result.result["launchd_job"]) + self.assertIsNone(result.result["persistent_process"]) + self.assertEqual(result.result["persistence_state"], "unproven") + self.assertIsNone(result.result["process_idle"]) + persistence.assert_not_called() + ping.assert_not_called() + wait_for_idle.assert_not_called() + + def test_broker_status_reports_idle_unmeasured_when_selected_socket_is_invalid( + self, + ) -> None: + root = self.root / "broker-state" + selector = self._install_modern_selected(root, body="#!/bin/sh\nexit 0\n") + with mock.patch.object( + self.client, "_verify_macos_signature", return_value=(True, "") + ): + lane = self.client._load_selector_v2_lane( + root, selector["selected"], role="selected" + ) + exact_mode = self.client._exact_mode + + def invalidate_socket(path, *args, **kwargs): + if str(path).endswith(".sock"): + return None + return exact_mode(path, *args, **kwargs) + + with mock.patch.object( + self.client, "_broker_root", return_value=root + ), mock.patch.object( + self.client, "_read_broker_selector_view", return_value=selector + ), mock.patch.object( + self.client, "_load_selector_v2_lane", return_value=lane + ), mock.patch.object( + self.client, "_verify_macos_signature", return_value=(True, "") + ), mock.patch.object( + self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_exact_mode", side_effect=invalidate_socket + ), mock.patch.object( + self.client, "_job_persistence_state" + ) as persistence, mock.patch.object( + self.client, "invoke_dispatcher_ping" + ) as ping, mock.patch.object( + self.client, "_wait_for_job_idle" + ) as wait_for_idle: + result = self.client.broker_status() + + self.assertEqual(result.status, self.client.RuntimeStatus.UNAVAILABLE) + self.assertTrue(result.result["launchd_job"]) + self.assertFalse(result.result["socket"]) + self.assertIsNone(result.result["persistent_process"]) + self.assertEqual(result.result["persistence_state"], "unproven") + self.assertIsNone(result.result["process_idle"]) + persistence.assert_not_called() + ping.assert_not_called() + wait_for_idle.assert_not_called() + + def test_broker_status_separates_bounded_process_activity_from_persistence( self, ) -> None: root = self.root / "broker-state" @@ -4269,6 +4425,8 @@ def test_broker_status_reports_responsive_dispatcher_ready_while_busy( self.client, "_verify_macos_signature", return_value=(True, "") ), mock.patch.object( self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_job_persistence_state", return_value="nonpersistent" ), mock.patch.object( self.client, "invoke_dispatcher_ping", return_value=ping ), mock.patch.object( @@ -4279,7 +4437,9 @@ def test_broker_status_reports_responsive_dispatcher_ready_while_busy( self.assertEqual(result.status, self.client.RuntimeStatus.OK) self.assertTrue(result.result["active"]) self.assertTrue(result.result["dispatcher_ready"]) - self.assertTrue(result.result["persistent_process"]) + self.assertFalse(result.result["persistent_process"]) + self.assertEqual(result.result["persistence_state"], "nonpersistent") + self.assertFalse(result.result["process_idle"]) wait_for_idle.assert_called_once() idle_deadline = wait_for_idle.call_args.kwargs["deadline"] self.assertGreater(idle_deadline, started) @@ -4313,6 +4473,8 @@ def test_broker_status_rejects_invalid_retained_lane(self) -> None: side_effect=(selected_lane, ValueError("retained lane is invalid")), ), mock.patch.object( self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_job_persistence_state", return_value="nonpersistent" ), mock.patch.object( self.client, "invoke_dispatcher_ping", return_value=ping ), mock.patch.object( @@ -4358,6 +4520,8 @@ def test_broker_status_does_not_advertise_unloaded_retained_job(self) -> None: ), mock.patch.object( self.client, "_job_loaded", side_effect=(True, False) ) as job_loaded, mock.patch.object( + self.client, "_job_persistence_state", return_value="nonpersistent" + ), mock.patch.object( self.client, "_exact_mode", return_value=object() ), mock.patch.object( self.client, "invoke_dispatcher_ping", return_value=ping @@ -4602,6 +4766,58 @@ def test_stage_dispatcher_is_make_before_break_and_keeps_blue_selected(self) -> self.assertTrue((root / f"provider-dispatcher-{token}.plist").is_file()) self.assertTrue((root / f"provider-dispatcher-{token}.sock").exists()) + def test_stage_dispatcher_restores_baseline_when_full_idle_proof_fails( + self, + ) -> None: + root = self.root / "broker-state" + self._install_modern_selected(root, body="#!/bin/sh\nexit 0\n") + selector_path = root / self.client.BROKER_SELECTOR_V2_FILENAME + selector_before = selector_path.read_bytes() + self._fixture(body="#!/bin/sh\nexit 7\n") + ping = self.client.RuntimeResult( + self.client.RuntimeStatus.OK, + result={"ready": True}, + provenance={"operation": "dispatcher_ping"}, + ) + + with mock.patch.object( + self.client, "_verify_macos_signature", return_value=(True, "") + ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), mock.patch.object( + self.client, "_broker_root", return_value=root + ), mock.patch.object( + self.client, + "_bootstrap_broker", + side_effect=self._dispatcher_bootstrap(root), + ), mock.patch.object( + self.client, "_bootout_broker", return_value=True + ), mock.patch.object( + self.client, "_job_loaded", return_value=True + ), mock.patch.object( + self.client, "_wait_for_job_idle", return_value=False + ), mock.patch.object( + self.client, "invoke_dispatcher_ping", return_value=ping + ): + result = self.client.stage_dispatcher() + + self.assertEqual(result.status, self.client.RuntimeStatus.PROVIDER_ERROR) + self.assertEqual(selector_path.read_bytes(), selector_before) + selector = json.loads(selector_before) + self.assertIsNone(selector["candidate"]) + candidate_files = [ + path + for path in root.glob("provider-dispatcher-*") + if path.suffix in {".json", ".plist", ".sock"} + and path.stem + != ( + "provider-dispatcher-" + + self.client._dispatcher_lane_token( + selector["selected"]["artifact_sha256"], + selector["selected"]["manifest_sha256"], + ) + ) + ] + self.assertEqual(candidate_files, []) + def test_stage_dispatcher_failure_restores_selector_and_blue_byte_for_byte(self) -> None: root = self.root / "broker-state" blue_state_raw, blue_plist_raw, blue = self._install_legacy_blue( @@ -5766,6 +5982,241 @@ def test_job_loaded_default_deadline_is_none(self) -> None: self.client._job_loaded(self.client.BROKER_LABEL) self.assertIsNone(lc.call_args.kwargs.get("deadline")) + def test_job_persistence_state_parses_sanitized_live_golden_transcripts( + self, + ) -> None: + fixtures = ROOT / "tests" / "fixtures" + cases = ( + ("launchctl_print_nonpersistent.txt", "nonpersistent"), + ("launchctl_print_keepalive.txt", "persistent"), + ("launchctl_print_keepalive_dictionary.txt", "persistent"), + ) + for filename, expected in cases: + with self.subTest(filename=filename), mock.patch.object( + self.client, + "_launchctl", + return_value=subprocess.CompletedProcess( + [], + 0, + (fixtures / filename).read_text(encoding="utf-8"), + "", + ), + ), mock.patch.object( + self.client.os, "getuid", return_value=501 + ): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + expected, + ) + + def test_job_persistence_state_uses_exact_top_level_casefolded_tokens( + self, + ) -> None: + for token in ("KeEpAlIvE", "RuNaTlOaD"): + transcript = ( + "gui/501/com.agent-collab.provider-broker = {\n" + f"\tproperties = InFeRrEd PrOgRaM | {token} | BeNiGn\n" + "}\n" + ) + with self.subTest(token=token), mock.patch.object( + self.client, + "_launchctl", + return_value=subprocess.CompletedProcess([], 0, transcript, ""), + ) as launchctl, mock.patch.object( + self.client.os, "getuid", return_value=501 + ): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + "persistent", + ) + launchctl.assert_called_once_with( + ["print", "gui/501/com.agent-collab.provider-broker"] + ) + + def test_job_persistence_state_treats_any_exact_event_trigger_as_persistent( + self, + ) -> None: + transcript = ( + "gui/501/com.agent-collab.provider-broker = {\n" + "\tevent triggers = {\n" + "\t\tcom.example.trigger => {\n" + "\t\t\tservice = com.example.service\n" + "\t\t}\n" + "\t}\n" + "\tproperties = inferred program\n" + "}\n" + ) + with mock.patch.object( + self.client, + "_launchctl", + return_value=subprocess.CompletedProcess([], 0, transcript, ""), + ), mock.patch.object(self.client.os, "getuid", return_value=501): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + "persistent", + ) + + def test_job_persistence_state_accepts_nested_keepalive_one_projection( + self, + ) -> None: + transcript = ( + "gui/501/com.agent-collab.provider-broker = {\n" + "\tevent triggers = {\n" + "\t\tcom.example.trigger => {\n" + "\t\t\tkeepalive = 1\n" + "\t\t}\n" + "\t}\n" + "\tproperties = inferred program\n" + "}\n" + ) + with mock.patch.object( + self.client, + "_launchctl", + return_value=subprocess.CompletedProcess([], 0, transcript, ""), + ), mock.patch.object(self.client.os, "getuid", return_value=501): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + "persistent", + ) + + def test_job_persistence_state_does_not_scan_path_substrings(self) -> None: + transcript = ( + "gui/501/com.agent-collab.provider-broker = {\n" + "\tpath = /Library/LaunchAgents/com.example.keepalive-runatload.plist\n" + "\tprogram = /usr/bin/keepalive-helper\n" + "\tproperties = inferred program\n" + "}\n" + ) + with mock.patch.object( + self.client, + "_launchctl", + return_value=subprocess.CompletedProcess([], 0, transcript, ""), + ), mock.patch.object(self.client.os, "getuid", return_value=501): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + "nonpersistent", + ) + + def test_job_persistence_state_normalizes_insignificant_key_spacing( + self, + ) -> None: + transcript = ( + "gui/501/com.agent-collab.provider-broker = {\n" + "\tevent triggers = {\n" + "\t}\n" + "\tproperties = inferred program\n" + "}\n" + ) + with mock.patch.object( + self.client, + "_launchctl", + return_value=subprocess.CompletedProcess([], 0, transcript, ""), + ), mock.patch.object(self.client.os, "getuid", return_value=501): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + "persistent", + ) + + def test_job_persistence_state_fails_closed_on_unproven_print_shape( + self, + ) -> None: + header = "gui/501/com.agent-collab.provider-broker = {\n" + cases = ( + (1, header + "\tproperties =\n}\n"), + (0, ""), + (0, "gui/501/example = {\n\tproperties =\n}\n"), + (0, header + "}\n"), + (0, header + "\tproperties =\n"), + (0, header + "\tproperties =\n\tproperties = keepalive\n}\n"), + (0, header + "\t\tproperties = keepalive\n}\n"), + (0, header + "\tproperties = keepalive,runatload\n}\n"), + (0, header + "state = running\n\tproperties =\n}\n"), + (0, header + "\tmystery = { trailing\n\tproperties =\n}\n"), + ( + 0, + header + + "\tproperties =\n" + + " event triggers = {\n" + + " }\n" + + "}\n", + ), + ( + 0, + header + + "\tproperties =\n" + + "\tevent triggers = maybe\n" + + "}\n", + ), + ( + 0, + header + + "\tevent triggers = {\n" + + "\t\tcom.example => {\n" + + "\t\t\tkeepalive = maybe\n" + + "\t\t}\n" + + "\t}\n" + + "\tproperties =\n" + + "}\n", + ), + ( + 0, + header + + "\tevent triggers = {\n" + + "\t}\n" + + "\tevent triggers = {\n" + + "\t}\n" + + "\tproperties =\n" + + "}\n", + ), + ( + 0, + header + + "\t\tkeepalive = 1\n" + + "\tproperties =\n" + + "}\n", + ), + ( + 0, + header + + "\trunatload = 1\n" + + "\tproperties =\n" + + "}\n", + ), + ) + for returncode, stdout in cases: + with self.subTest(returncode=returncode, stdout=stdout), mock.patch.object( + self.client, + "_launchctl", + return_value=subprocess.CompletedProcess([], returncode, stdout, ""), + ), mock.patch.object( + self.client.os, "getuid", return_value=501 + ): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + "unproven", + ) + + def test_job_persistence_state_rejects_invalid_label_before_launchctl( + self, + ) -> None: + with mock.patch.object(self.client, "_launchctl") as launchctl: + with self.assertRaises(ValueError): + self.client._job_persistence_state("invalid label") + launchctl.assert_not_called() + + def test_job_persistence_state_fails_closed_on_launchctl_collection_error( + self, + ) -> None: + with mock.patch.object( + self.client, + "_launchctl", + side_effect=RuntimeError("launchctl collection timed out"), + ): + self.assertEqual( + self.client._job_persistence_state(self.client.BROKER_LABEL), + "unproven", + ) + def test_capture_broker_lanes_bounds_retained_probe_by_deadline(self) -> None: # _capture_broker_lanes threads the request deadline into the retained # lane's _job_loaded probe.