From 99ac95a7a456b94d709fb7efb1c3dc9874ca3ad0 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:03:12 +0200 Subject: [PATCH 01/48] docs: design automatic watchdog activation --- .../2026-08-11-watchdog-activation-design.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-11-watchdog-activation-design.md diff --git a/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md b/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md new file mode 100644 index 0000000..5a137bb --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md @@ -0,0 +1,143 @@ +# Automatic Progress Watchdog Activation Design + +## Problem + +`github-delivery` v0.2.0 contains a runtime progress watchdog, Codex lifecycle-hook integration, and a Codex App Server streaming proxy, but the normal skill installer does not activate either runtime boundary. Users can therefore install or update the skill successfully and still observe the exact failure the watchdog was built to stop: hundreds of repeated assistant-intent lines such as `Let me check the type` before any tool call occurs. + +The strongest detector already exists. The missing work is activation, capability selection, and end-to-end proof that a supported Codex install actually uses the strongest boundary available. + +## Goal + +Make progress protection effective by default on supported Codex installations without weakening mutation, freshness, review, or evidence gates and without silently rewriting unsupported host configuration. + +Success means a normal supported install/upgrade chooses and activates the strongest watchdog mode it can safely use, records the resulting mode, and an end-to-end incident regression proves that repeated low-novelty narration is interrupted before the configured output budget is exceeded. + +## Non-goals + +- Do not change the mutation-authority model. +- Do not make omitted evidence count as success. +- Do not require App Server streaming on hosts that cannot expose it. +- Do not replace Codex itself or assume an undocumented host interception API. +- Do not silently alter unrelated user hooks or editor configuration. + +## Approaches considered + +### A. Documentation-only activation + +Keep hook/proxy installation separate and improve README/INSTALL instructions. + +Rejected because it preserves the current failure mode: the protection can exist but remain inactive after a successful install. + +### B. Always install lifecycle hooks + +Make `install-skill.mjs` install `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd` hooks automatically for Codex targets. + +Better than today, but still cannot stop tokens already emitted inside a single assistant message. It solves tool-boundary waste but not the incident that motivated this PR. + +### C. Capability-driven activation with strongest-safe mode + +Recommended. During install/upgrade, detect the target host and available watchdog integration surface, select `stream` when an App Server launch boundary is actually controllable, otherwise select `hooks` when Codex lifecycle hooks are supported, otherwise record `none` and surface the degradation clearly. Installation remains idempotent and preserves unrelated configuration. + +This is the only approach that both fixes the activation trap and keeps unsupported hosts safe. + +## Architecture + +### 1. Activation planner + +Add a deterministic activation planner with one input contract: + +- installation target path; +- host hint/detection result; +- available integration capabilities; +- existing Codex hook configuration; +- whether the caller requested apply vs dry-run. + +It returns one of: + +- `stream` — the caller/host can launch Codex through the watchdog App Server proxy; +- `hooks` — lifecycle hooks can be safely installed and used; +- `none` — no enforceable runtime boundary is available. + +The planner must never infer `stream` merely because `codex` exists. Streaming is selected only when the actual launch boundary is under installer/host control. + +### 2. Installer integration + +`install-skill.mjs` remains dry-run by default and continues to own skill files/backups. For Codex targets it also produces a watchdog activation plan. + +On `--apply`: + +- install/upgrade the skill first; +- activate the selected watchdog mode; +- preserve unrelated hooks/configuration; +- back up any modified host configuration before writing; +- emit one structured result containing the installed version, watchdog mode, changed files/configuration, and any degradation reason. + +The existing standalone hook installer remains available for recovery/manual use but is no longer the normal required path after a Codex install. + +### 3. Streaming launcher integration + +When `stream` is selected, installation must create or update a stable launcher/adapter owned by GitHub Delivery rather than asking the user to remember a different command. The launcher delegates to `scripts/codex-app-server-watchdog-proxy.mjs` and is referenced by the supported Codex/App Server integration point. + +If the current Codex surface does not provide a safe way for the installer to replace or configure that launch boundary, the planner must choose `hooks`, not pretend that streaming is active. + +### 4. Runtime capability truth + +`runtime-capabilities.mjs` must report the mode actually activated by installation rather than relying only on an operator-set environment variable. Environment declarations may override or assist probing for controlled CI/fixtures, but a successful normal install should leave machine-readable activation state that runtime inspection can verify. + +Activation state must contain no secrets, prompts, raw tool inputs, or conversation content. + +### 5. Visible degradation + +If only `hooks` is available, the install result must explicitly say that tool-boundary protection is active but in-turn narration cannot be interrupted until `Stop`. + +If mode is `none`, installation must not fail solely because the host lacks a watchdog surface, but it must clearly report `progress_watchdog_unavailable` so users are not told they are protected when they are not. + +## Data flow + +1. User runs the normal skill installer/upgrade. +2. Installer plans skill replacement and watchdog activation together. +3. Host/capability probe selects `stream`, `hooks`, or `none`. +4. Dry-run reports exactly what would change. +5. `--apply` installs the skill and applies only the selected supported activation. +6. Runtime capability inspection reads the persisted activation receipt and confirms the effective mode. +7. A workflow uses the watchdog through that boundary without requiring the user to remember a second installer command. + +## Safety and failure handling + +- Host configuration writes remain atomic, backup-first, and idempotent. +- Malformed or symlinked host configuration fails closed before modification. +- A partial activation failure must not claim the requested watchdog mode. The result records the lower verified mode or `none`. +- Hook installation must preserve all unrelated hook entries. +- Streaming launch integration must never swallow ordinary App Server traffic, mutation prompts, or errors. +- The watchdog remains incapable of granting GitHub write authority. +- Existing `GD-CORE-*`, `GD-AUTH-*`, CI, review, security, and final-evidence rules remain authoritative. + +## Testing strategy + +### RED regression first + +Add an end-to-end installation regression that installs to an isolated temporary Codex home, runs the normal installer, and asserts the expected activation mode without invoking the standalone hook installer. + +Add an incident regression using the real repeated phrase family from the observed trace (`Let me check the type`, `Let me check the NOUS_DEF type`, etc.). Under the `stream` adapter, the generated assistant deltas must trigger one `turn/interrupt` before the configured character budget is exceeded. + +### Additional contracts + +- normal Codex install activates the strongest supported mode automatically; +- upgrade from v0.2.0 does not duplicate hook entries; +- existing unrelated hooks survive byte-for-semantic-content unchanged; +- malformed/symlinked config fails closed; +- `hooks` is selected when streaming cannot be controlled; +- `none` is reported honestly on unsupported hosts; +- runtime capability output matches the persisted effective mode; +- repeated install is idempotent; +- uninstall/restore does not leave a false `stream`/`hooks` receipt; +- existing repository `npm run check`, distribution reproducibility, security, and cross-platform CI remain green. + +## Acceptance criteria + +1. A standard supported Codex install/upgrade no longer requires a second manual watchdog-install command to obtain available protection. +2. The installer chooses the strongest mode it can prove: `stream` > `hooks` > `none`. +3. The exact repeated `Let me check...` incident is interrupted in streaming mode before the configured generation budget is exceeded. +4. Hook-only installations clearly disclose that in-turn token burn cannot be stopped mid-message. +5. Runtime capability reporting reflects the mode actually installed. +6. Existing user configuration and GitHub Delivery safety/authority gates are preserved. From 1be0f523ddb39aeccdd39d1d7c126036adc773aa Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:19:22 +0200 Subject: [PATCH 02/48] docs: plan automatic watchdog activation --- .../plans/2026-08-11-watchdog-activation.md | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-11-watchdog-activation.md diff --git a/docs/superpowers/plans/2026-08-11-watchdog-activation.md b/docs/superpowers/plans/2026-08-11-watchdog-activation.md new file mode 100644 index 0000000..f9db632 --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-watchdog-activation.md @@ -0,0 +1,293 @@ +# Automatic Watchdog Activation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make a standard supported Codex install/upgrade automatically activate the strongest watchdog mode it can prove, persist the effective mode, and prove the observed repeated `Let me check...` narration is interrupted in streaming mode before the configured output budget is exceeded. + +**Architecture:** Add a small deterministic activation module that selects `stream`, `hooks`, or `none` from explicit capabilities and owns a non-sensitive activation receipt. Integrate that module into `install-skill.mjs`, reusing the existing safe hook installer for hook mode and a stable App Server launcher for stream mode. Runtime capability reporting reads the persisted receipt, while tests cover installation, idempotency, truthful degradation, and the real incident phrase family. + +**Tech Stack:** Node.js 22/24, ESM, `node:test`, filesystem primitives, existing Codex hook installer, existing App Server watchdog proxy, GitHub Actions matrix. + +## Global Constraints + +- Normal install/upgrade must no longer require a second manual watchdog-install command to obtain available protection. +- Mode selection must be strongest verified mode: `stream` > `hooks` > `none`. +- Never select `stream` merely because `codex` exists; require a controllable App Server launch boundary. +- Host configuration writes remain backup-first, fail closed on malformed/symlinked configuration, preserve unrelated hooks, and remain idempotent. +- Activation state must contain no secrets, prompts, raw tool inputs, or conversation content. +- Hook-only mode must explicitly disclose that in-turn narration cannot be interrupted until `Stop`. +- `none` must report `progress_watchdog_unavailable` without making installation itself fail solely for lack of a watchdog surface. +- Existing GitHub mutation authority, freshness, review, security, CI, and final-evidence rules remain unchanged. +- Existing Node 22/24 Ubuntu, macOS, and Windows CI must remain green. + +--- + +### Task 1: Add activation selection and receipt contracts + +**Files:** +- Create: `scripts/lib/watchdog-activation.mjs` +- Create: `tests/unit/watchdog-activation.test.mjs` + +**Interfaces:** +- Produces: `selectWatchdogMode({ host, streamLaunchControlled, lifecycleHooksSupported }) -> { mode, degradationReason }` +- Produces: `activationReceiptPath({ codexHome }) -> string` +- Produces: `writeActivationReceipt({ codexHome, mode, degradationReason, launcherPath, apply }) -> { path, changed, applied, receipt }` +- Produces: `readActivationReceipt({ codexHome }) -> object | null` + +- [ ] **Step 1: Write failing selection tests** + +```js +assert.deepEqual( + selectWatchdogMode({ host: "codex", streamLaunchControlled: true, lifecycleHooksSupported: true }), + { mode: "stream", degradationReason: null }, +); +assert.equal( + selectWatchdogMode({ host: "codex", streamLaunchControlled: false, lifecycleHooksSupported: true }).mode, + "hooks", +); +assert.deepEqual( + selectWatchdogMode({ host: "unknown", streamLaunchControlled: false, lifecycleHooksSupported: false }), + { mode: "none", degradationReason: "progress_watchdog_unavailable" }, +); +``` + +- [ ] **Step 2: Write failing receipt tests** + +Assert dry-run never writes, apply writes only schema/version/mode/degradation/launcher metadata, repeated identical apply is idempotent, malformed existing receipt is replaced only through the activation-owned path, and no prompt/tool/conversation fields exist. + +- [ ] **Step 3: Run the targeted test and verify RED** + +Run: `node --test tests/unit/watchdog-activation.test.mjs` +Expected: FAIL because `scripts/lib/watchdog-activation.mjs` does not exist. + +- [ ] **Step 4: Implement the minimal activation module** + +Use explicit booleans only. `stream` requires `host === "codex" && streamLaunchControlled === true`; otherwise `hooks` requires `host === "codex" && lifecycleHooksSupported === true`; otherwise `none`. Store the receipt below Codex home as `github-delivery/watchdog-activation.json` with schema version, mode, degradation reason, launcher path when applicable, and `updatedAt`. + +- [ ] **Step 5: Run the targeted test and verify GREEN** + +Run: `node --test tests/unit/watchdog-activation.test.mjs` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/lib/watchdog-activation.mjs tests/unit/watchdog-activation.test.mjs +git commit -m "feat: add watchdog activation planner" +``` + +### Task 2: Integrate automatic hook activation into the normal installer + +**Files:** +- Modify: `scripts/install-skill.mjs` +- Modify: `scripts/install-codex-watchdog-hooks.mjs` +- Modify: `tests/unit/installer.test.mjs` +- Modify: `tests/unit/install-codex-watchdog-hooks.test.mjs` + +**Interfaces:** +- Consumes: Task 1 `selectWatchdogMode`, `writeActivationReceipt` +- Produces: normal installer result field `watchdog: { mode, degradationReason, receiptPath, hookResult, launcherPath }` +- Refactors: export reusable `defaultHooksPath()` and keep `installCodexWatchdogHooks(...)` semantics unchanged for standalone callers. + +- [ ] **Step 1: Add a failing normal-install regression** + +Create an isolated temporary Codex home and source/target skill fixture. Invoke `install-skill.mjs` through an exported `installSkill(...)` orchestration function with `host: "codex"`, `streamLaunchControlled: false`, and `lifecycleHooksSupported: true`. Assert apply installs the skill and creates exactly one GitHub Delivery hook entry for each lifecycle event without calling the standalone hook installer. + +- [ ] **Step 2: Add failing upgrade/idempotency assertions** + +Seed unrelated hooks, run install twice, and assert unrelated semantic content survives and watchdog entries remain exactly one per event. + +- [ ] **Step 3: Run targeted installer tests and verify RED** + +Run: `node --test tests/unit/installer.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs` +Expected: FAIL because normal installer does not yet orchestrate activation. + +- [ ] **Step 4: Refactor `install-skill.mjs` around `installSkill(options)`** + +Keep CLI argument parsing backwards compatible. Add injectable options used by tests and host integrations: `codexHome`, `host`, `streamLaunchControlled`, and `lifecycleHooksSupported`. Default normal behaviour must remain safe when support cannot be proven. + +- [ ] **Step 5: Reuse the existing hook installer for selected `hooks` mode** + +For `--apply`, install the skill first, then invoke `installCodexWatchdogHooks({ hooksPath, skillDir: target, apply: true })`, then write the activation receipt only after the hook installation succeeds. Dry-run reports the planned hook changes but writes neither target nor hooks nor receipt. + +- [ ] **Step 6: Return truthful degradation** + +For `hooks`, set a machine-readable degradation reason such as `streaming_interruption_unavailable`; for `none`, set `progress_watchdog_unavailable`. Do not fail skill installation solely because mode is `none`. + +- [ ] **Step 7: Run targeted tests and verify GREEN** + +Run: `node --test tests/unit/installer.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs` +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add scripts/install-skill.mjs scripts/install-codex-watchdog-hooks.mjs tests/unit/installer.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs +git commit -m "feat: activate watchdog during Codex install" +``` + +### Task 3: Add a stable streaming launcher and select stream only when controllable + +**Files:** +- Create: `scripts/lib/watchdog-stream-launcher.mjs` +- Create: `scripts/codex-watchdog-app-server.mjs` +- Modify: `scripts/install-skill.mjs` +- Create: `tests/unit/watchdog-stream-launcher.test.mjs` +- Modify: `tests/unit/codex-watchdog-entrypoints.test.mjs` + +**Interfaces:** +- Produces: `installStreamLauncher({ skillDir, launcherPath, apply }) -> { launcherPath, changed, applied }` +- Entry point: `scripts/codex-watchdog-app-server.mjs` delegates to the installed skill's `scripts/codex-app-server-watchdog-proxy.mjs` without changing App Server JSONL semantics. + +- [ ] **Step 1: Add failing stream-mode installer tests** + +With `streamLaunchControlled: true`, assert the normal installer selects `stream`, creates/plans a stable launcher, records its path in the activation receipt, and does not claim stream when `streamLaunchControlled` is false. + +- [ ] **Step 2: Add failing launcher delegation test** + +Inject/spawn a fake Codex binary and assert the stable launcher reaches the existing proxy path and preserves command arguments. + +- [ ] **Step 3: Run targeted tests and verify RED** + +Run: `node --test tests/unit/watchdog-stream-launcher.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs` +Expected: FAIL because the stable launcher does not exist. + +- [ ] **Step 4: Implement launcher installation** + +Write/update only GitHub Delivery-owned launcher material. Do not rewrite editor or unrelated Codex launch configuration. The caller must explicitly prove that this launch boundary is controlled before `stream` can be selected. + +- [ ] **Step 5: Persist stream receipt only after launcher activation succeeds** + +If stream launcher activation fails, do not write a `stream` receipt. Fall back to a verified lower mode only when that lower activation succeeds; otherwise record `none` with a concrete degradation reason. + +- [ ] **Step 6: Run targeted tests and verify GREEN** + +Run the same test command as Step 3 and expect PASS. + +- [ ] **Step 7: Commit** + +```bash +git add scripts/lib/watchdog-stream-launcher.mjs scripts/codex-watchdog-app-server.mjs scripts/install-skill.mjs tests/unit/watchdog-stream-launcher.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs +git commit -m "feat: add verified streaming watchdog launcher" +``` + +### Task 4: Make runtime capability reporting read installed activation truth + +**Files:** +- Modify: `scripts/lib/runtime-capabilities.mjs` +- Modify: `scripts/runtime-capabilities.mjs` +- Modify: `tests/unit/runtime-capabilities.test.mjs` + +**Interfaces:** +- Consumes: Task 1 `readActivationReceipt({ codexHome })` +- Produces: `buildRuntimeCapabilities({ ..., activation })` where persisted activation is preferred over an absent declaration, while explicit test/operator declaration may override for controlled fixtures. + +- [ ] **Step 1: Write failing persisted-mode tests** + +Assert a persisted `stream` receipt yields `runtime.progressWatchdog === "stream"` without `SHIPPING_GITHUB_PROGRESS_WATCHDOG`; persisted `hooks` yields `hooks`; missing receipt yields `none`; explicit declaration remains usable for controlled fixtures. + +- [ ] **Step 2: Run targeted test and verify RED** + +Run: `node --test tests/unit/runtime-capabilities.test.mjs` +Expected: FAIL because runtime capability code does not consume persisted activation. + +- [ ] **Step 3: Implement receipt-aware capability resolution** + +Resolve mode from explicit declaration when present, otherwise from a validated activation receipt, otherwise `none`. Invalid receipt content must never upgrade capability. + +- [ ] **Step 4: Run targeted test and verify GREEN** + +Run: `node --test tests/unit/runtime-capabilities.test.mjs` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/lib/runtime-capabilities.mjs scripts/runtime-capabilities.mjs tests/unit/runtime-capabilities.test.mjs +git commit -m "feat: report installed watchdog capability" +``` + +### Task 5: Add the exact in-turn incident end-to-end regression + +**Files:** +- Modify: `tests/unit/agent-progress-watchdog.test.mjs` +- Modify: `tests/unit/codex-watchdog-entrypoints.test.mjs` +- Modify: `tests/unit/installer.test.mjs` + +**Interfaces:** +- Consumes: existing `createAppServerWatchdogRouter()` and Task 3 installed streaming entry point. +- Produces: regression proving one private `turn/interrupt` is emitted before the configured character budget for the observed phrase family. + +- [ ] **Step 1: Add incident fixture text** + +Use variants from the observed failure, including `Let me check the type.`, `Let me check the NOUS_DEF type.`, `Let me check the live test type.`, and `Let me check the OAuthProviderDef type.` Feed them as realistic incremental `item/agentMessage/delta` messages. + +- [ ] **Step 2: Assert bounded interruption** + +Track emitted assistant characters and assert the first private `turn/interrupt` appears before the watchdog's configured incident budget is exceeded and appears exactly once for the turn. + +- [ ] **Step 3: Prove normal install reaches that boundary** + +Use the installed stream launcher fixture from Task 3 rather than constructing the router directly for the integration assertion. This proves activation, not merely detection. + +- [ ] **Step 4: Run the incident tests** + +Run: `node --test tests/unit/agent-progress-watchdog.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs` +Expected: PASS with the exact trace family bounded. + +- [ ] **Step 5: Commit** + +```bash +git add tests/unit/agent-progress-watchdog.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs +git commit -m "test: prove installed watchdog stops narration stalls" +``` + +### Task 6: Update operator docs and run repository-wide verification + +**Files:** +- Modify: `README.md` +- Modify: `INSTALL.md` +- Modify: `references/agent-progress-watchdog.md` +- Modify: `references/runtime-capabilities.md` +- Modify: PR #213 body + +**Interfaces:** +- Documents: normal install activation, actual `stream`/`hooks`/`none` truth, hook-only limitation, standalone installer as recovery/manual path, and controlled stream-boundary requirement. + +- [ ] **Step 1: Update installation docs** + +Remove wording that makes the standalone hook installer a normal required second step. Keep it documented as manual recovery/repair. Explain that the normal installer activates the strongest verified mode and reports degradation honestly. + +- [ ] **Step 2: Update runtime docs** + +Document persisted activation receipt semantics and that `stream` is claimed only when a controllable App Server launch boundary is installed/selected. + +- [ ] **Step 3: Run targeted watchdog/installer tests** + +Run: + +```bash +node --test tests/unit/watchdog-activation.test.mjs tests/unit/watchdog-stream-launcher.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs tests/unit/installer.test.mjs tests/unit/runtime-capabilities.test.mjs tests/unit/agent-progress-watchdog.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs +``` + +Expected: PASS. + +- [ ] **Step 4: Run aggregate repository verification** + +Run: `npm run check` +Expected: PASS, including syntax, policy validation, pre-open self-test, repository security, reproducible distribution, offline evals, and unit suite. + +- [ ] **Step 5: Verify current-head CI** + +Require CI, CodeQL, Architecture Contracts, and Dependency Review to pass on the exact final PR head across the repository's required matrix. + +- [ ] **Step 6: Update PR #213 from design draft to implementation-ready summary** + +Record RED evidence, implemented activation semantics, exact incident regression, safety invariants, and current-head verification. Mark ready for review only after exact-head required checks are green. + +- [ ] **Step 7: Commit documentation** + +```bash +git add README.md INSTALL.md references/agent-progress-watchdog.md references/runtime-capabilities.md +git commit -m "docs: explain automatic watchdog activation" +``` From ca5546f0f0c06c42aa2ccb14fd62d8f944bb5459 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:20:06 +0200 Subject: [PATCH 03/48] test: define automatic watchdog activation contract --- tests/unit/watchdog-activation.test.mjs | 103 ++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/unit/watchdog-activation.test.mjs diff --git a/tests/unit/watchdog-activation.test.mjs b/tests/unit/watchdog-activation.test.mjs new file mode 100644 index 0000000..6cbfee8 --- /dev/null +++ b/tests/unit/watchdog-activation.test.mjs @@ -0,0 +1,103 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import test from "node:test"; + +const ROOT = resolve(import.meta.dirname, "../.."); +const INSTALL = join(ROOT, "scripts", "install-skill.mjs"); + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "gd-watchdog-activation-")); + const source = join(root, "source"); + const target = join(root, "skills", "github-delivery"); + const codexHome = join(root, ".codex"); + mkdirSync(source, { recursive: true }); + writeFileSync( + join(source, "package.json"), + `${JSON.stringify({ name: "github-delivery", version: "0.2.0" }, null, 2)}\n`, + ); + writeFileSync(join(source, "marker.txt"), "installed\n"); + return { root, source, target, codexHome }; +} + +function runInstall(f, extra = []) { + return spawnSync( + process.execPath, + [ + INSTALL, + "--source", + f.source, + "--target", + f.target, + "--codex-home", + f.codexHome, + "--host", + "codex", + ...extra, + ], + { cwd: ROOT, encoding: "utf8" }, + ); +} + +test("normal Codex install activates lifecycle watchdog without a second installer", () => { + const f = fixture(); + const result = runInstall(f, ["--lifecycle-hooks-supported", "--apply"]); + assert.equal(result.status, 0, result.stderr); + + const receipt = JSON.parse(result.stdout); + assert.equal(receipt.watchdog.mode, "hooks"); + assert.equal(receipt.watchdog.degradationReason, "streaming_interruption_unavailable"); + assert.equal(readFileSync(join(f.target, "marker.txt"), "utf8"), "installed\n"); + + const hooksPath = join(f.codexHome, "hooks.json"); + const hooks = JSON.parse(readFileSync(hooksPath, "utf8")); + for (const event of ["PreToolUse", "PostToolUse", "Stop", "SubagentStop", "SessionEnd"]) { + const commands = (hooks.hooks[event] || []) + .flatMap((entry) => entry.hooks || []) + .map((entry) => entry.command || ""); + assert.equal(commands.filter((command) => command.includes("codex-watchdog-hook.mjs")).length, 1); + } + + assert.equal(existsSync(join(f.codexHome, "github-delivery", "watchdog-activation.json")), true); +}); + +test("stream mode is selected only when the launch boundary is explicitly controllable", () => { + const f = fixture(); + const withoutBoundary = runInstall(f, ["--lifecycle-hooks-supported"]); + assert.equal(withoutBoundary.status, 0, withoutBoundary.stderr); + assert.equal(JSON.parse(withoutBoundary.stdout).watchdog.mode, "hooks"); + + const withBoundary = runInstall(f, [ + "--lifecycle-hooks-supported", + "--stream-launch-controlled", + ]); + assert.equal(withBoundary.status, 0, withBoundary.stderr); + assert.equal(JSON.parse(withBoundary.stdout).watchdog.mode, "stream"); +}); + +test("unsupported hosts report watchdog unavailability without blocking skill installation", () => { + const f = fixture(); + const result = spawnSync( + process.execPath, + [ + INSTALL, + "--source", + f.source, + "--target", + f.target, + "--codex-home", + f.codexHome, + "--host", + "unknown", + "--apply", + ], + { cwd: ROOT, encoding: "utf8" }, + ); + assert.equal(result.status, 0, result.stderr); + const receipt = JSON.parse(result.stdout); + assert.equal(receipt.watchdog.mode, "none"); + assert.equal(receipt.watchdog.degradationReason, "progress_watchdog_unavailable"); + assert.equal(readFileSync(join(f.target, "marker.txt"), "utf8"), "installed\n"); +}); From 0c33c555f0f3f330c4da4e41e78cd9b528c56d7e Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:28:45 +0200 Subject: [PATCH 04/48] feat: add watchdog activation planner --- scripts/lib/watchdog-activation.mjs | 108 ++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 scripts/lib/watchdog-activation.mjs diff --git a/scripts/lib/watchdog-activation.mjs b/scripts/lib/watchdog-activation.mjs new file mode 100644 index 0000000..373e77d --- /dev/null +++ b/scripts/lib/watchdog-activation.mjs @@ -0,0 +1,108 @@ +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +const MODES = new Set(["none", "hooks", "stream"]); + +export function selectWatchdogMode({ + host = "unknown", + streamLaunchControlled = false, + lifecycleHooksSupported = false, +} = {}) { + if (host === "codex" && streamLaunchControlled === true) { + return { mode: "stream", degradationReason: null }; + } + if (host === "codex" && lifecycleHooksSupported === true) { + return { mode: "hooks", degradationReason: "streaming_interruption_unavailable" }; + } + return { mode: "none", degradationReason: "progress_watchdog_unavailable" }; +} + +export function activationReceiptPath({ codexHome } = {}) { + if (!codexHome) throw new Error("codexHome is required"); + return join(resolve(codexHome), "github-delivery", "watchdog-activation.json"); +} + +function normalizeReceipt(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + if (value.schemaVersion !== 1 || !MODES.has(value.mode)) return null; + if (value.degradationReason !== null && typeof value.degradationReason !== "string") return null; + if (value.launcherPath !== null && typeof value.launcherPath !== "string") return null; + return { + schemaVersion: 1, + mode: value.mode, + degradationReason: value.degradationReason, + launcherPath: value.launcherPath, + updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : null, + }; +} + +export function readActivationReceipt({ codexHome } = {}) { + const path = activationReceiptPath({ codexHome }); + if (!existsSync(path)) return null; + try { + return normalizeReceipt(JSON.parse(readFileSync(path, "utf8"))); + } catch { + return null; + } +} + +function semanticReceipt({ mode, degradationReason = null, launcherPath = null }) { + if (!MODES.has(mode)) throw new Error(`invalid watchdog mode: ${mode}`); + if (degradationReason !== null && typeof degradationReason !== "string") { + throw new Error("degradationReason must be a string or null"); + } + if (launcherPath !== null && typeof launcherPath !== "string") { + throw new Error("launcherPath must be a string or null"); + } + return { + schemaVersion: 1, + mode, + degradationReason, + launcherPath: launcherPath ? resolve(launcherPath) : null, + }; +} + +function sameSemantics(left, right) { + return Boolean( + left && + right && + left.schemaVersion === right.schemaVersion && + left.mode === right.mode && + left.degradationReason === right.degradationReason && + left.launcherPath === right.launcherPath, + ); +} + +export function writeActivationReceipt({ + codexHome, + mode, + degradationReason = null, + launcherPath = null, + apply = false, + now = () => new Date(), +} = {}) { + const path = activationReceiptPath({ codexHome }); + const desired = semanticReceipt({ mode, degradationReason, launcherPath }); + const existing = readActivationReceipt({ codexHome }); + const changed = !sameSemantics(existing, desired); + const receipt = changed + ? { ...desired, updatedAt: now().toISOString() } + : existing; + + if (apply && changed) { + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); + writeFileSync(path, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); + } + + return { + path, + changed, + applied: apply && changed, + receipt, + }; +} From c89543afe119aa502b4bc2566d3121f20b0769c1 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:29:50 +0200 Subject: [PATCH 05/48] feat: activate watchdog during Codex install --- scripts/install-skill.mjs | 82 +++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/scripts/install-skill.mjs b/scripts/install-skill.mjs index dd631c1..84347ba 100755 --- a/scripts/install-skill.mjs +++ b/scripts/install-skill.mjs @@ -1,11 +1,26 @@ #!/usr/bin/env node +import { existsSync } from "node:fs"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; +import { installCodexWatchdogHooks } from "./install-codex-watchdog-hooks.mjs"; import { applyInstallation, planInstallation, restoreBackup } from "./lib/distribution.mjs"; +import { + selectWatchdogMode, + writeActivationReceipt, +} from "./lib/watchdog-activation.mjs"; + +function defaultCodexHome() { + return resolve(process.env.CODEX_HOME || join(homedir(), ".codex")); +} + +function inferHost(codexHome) { + return process.env.CODEX_HOME || existsSync(codexHome) ? "codex" : "unknown"; +} export function parseInstallArgs(argv) { + const codexHome = defaultCodexHome(); const options = { source: join(process.cwd(), "dist", "github-delivery"), target: join(homedir(), ".agents", "skills", "github-delivery"), @@ -14,6 +29,10 @@ export function parseInstallArgs(argv) { allowDowngrade: false, force: false, restore: null, + codexHome, + host: inferHost(codexHome), + lifecycleHooksSupported: undefined, + streamLaunchControlled: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; @@ -21,6 +40,11 @@ export function parseInstallArgs(argv) { else if (arg === "--target") options.target = argv[++index]; else if (arg === "--backup-root") options.backupRoot = argv[++index]; else if (arg === "--restore") options.restore = argv[++index]; + else if (arg === "--codex-home") options.codexHome = argv[++index]; + else if (arg === "--host") options.host = argv[++index]; + else if (arg === "--lifecycle-hooks-supported") options.lifecycleHooksSupported = true; + else if (arg === "--no-lifecycle-hooks") options.lifecycleHooksSupported = false; + else if (arg === "--stream-launch-controlled") options.streamLaunchControlled = true; else if (arg === "--apply") options.apply = true; else if (arg === "--allow-downgrade") options.allowDowngrade = true; else if (arg === "--force") options.force = true; @@ -28,23 +52,65 @@ export function parseInstallArgs(argv) { } options.source = resolve(options.source); options.target = resolve(options.target); + options.codexHome = resolve(options.codexHome); if (options.backupRoot) options.backupRoot = resolve(options.backupRoot); if (options.restore) options.restore = resolve(options.restore); + if (options.lifecycleHooksSupported === undefined) { + options.lifecycleHooksSupported = options.host === "codex"; + } return options; } -export function main(argv = process.argv.slice(2)) { - const options = parseInstallArgs(argv); - let result; +export function installSkill(options) { if (options.restore) { - result = options.apply + return options.apply ? restoreBackup({ backup: options.restore, target: options.target }) : { action: "restore", apply: false, backup: options.restore, target: options.target }; - } else if (options.apply) { - result = applyInstallation(options); - } else { - result = { ...planInstallation(options), apply: false }; } + + const installation = options.apply + ? applyInstallation(options) + : { ...planInstallation(options), apply: false }; + + const selection = selectWatchdogMode({ + host: options.host, + streamLaunchControlled: options.streamLaunchControlled, + lifecycleHooksSupported: options.lifecycleHooksSupported, + }); + + let hookResult = null; + if (options.host === "codex" && options.lifecycleHooksSupported) { + hookResult = installCodexWatchdogHooks({ + hooksPath: join(options.codexHome, "hooks.json"), + skillDir: options.target, + apply: options.apply, + }); + } + + const receiptResult = writeActivationReceipt({ + codexHome: options.codexHome, + mode: selection.mode, + degradationReason: selection.degradationReason, + launcherPath: null, + apply: options.apply, + }); + + return { + ...installation, + watchdog: { + mode: selection.mode, + degradationReason: selection.degradationReason, + receiptPath: receiptResult.path, + receiptChanged: receiptResult.changed, + hookResult, + launcherPath: null, + }, + }; +} + +export function main(argv = process.argv.slice(2)) { + const options = parseInstallArgs(argv); + const result = installSkill(options); process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); return result; } From 8cb6744e909f63fd5ea9d187ca4f09e8b714f5ec Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:30:14 +0200 Subject: [PATCH 06/48] test: cover watchdog activation planner --- .../watchdog-activation-contract.test.mjs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/unit/watchdog-activation-contract.test.mjs diff --git a/tests/unit/watchdog-activation-contract.test.mjs b/tests/unit/watchdog-activation-contract.test.mjs new file mode 100644 index 0000000..f411337 --- /dev/null +++ b/tests/unit/watchdog-activation-contract.test.mjs @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import { existsSync, mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { + activationReceiptPath, + readActivationReceipt, + selectWatchdogMode, + writeActivationReceipt, +} from "../../scripts/lib/watchdog-activation.mjs"; + +test("watchdog selection prefers stream, then hooks, then none", () => { + assert.deepEqual( + selectWatchdogMode({ host: "codex", streamLaunchControlled: true, lifecycleHooksSupported: true }), + { mode: "stream", degradationReason: null }, + ); + assert.deepEqual( + selectWatchdogMode({ host: "codex", streamLaunchControlled: false, lifecycleHooksSupported: true }), + { mode: "hooks", degradationReason: "streaming_interruption_unavailable" }, + ); + assert.deepEqual( + selectWatchdogMode({ host: "unknown", streamLaunchControlled: false, lifecycleHooksSupported: false }), + { mode: "none", degradationReason: "progress_watchdog_unavailable" }, + ); +}); + +test("activation receipt is dry-run safe, non-sensitive, and idempotent", () => { + const codexHome = mkdtempSync(join(tmpdir(), "gd-activation-receipt-")); + const path = activationReceiptPath({ codexHome }); + const clock = () => new Date("2026-08-11T06:30:00.000Z"); + + const planned = writeActivationReceipt({ + codexHome, + mode: "hooks", + degradationReason: "streaming_interruption_unavailable", + apply: false, + now: clock, + }); + assert.equal(planned.changed, true); + assert.equal(planned.applied, false); + assert.equal(existsSync(path), false); + + const applied = writeActivationReceipt({ + codexHome, + mode: "hooks", + degradationReason: "streaming_interruption_unavailable", + apply: true, + now: clock, + }); + assert.equal(applied.applied, true); + const raw = readFileSync(path, "utf8"); + assert.doesNotMatch(raw, /prompt|conversation|toolInput|tool_input/i); + assert.deepEqual(readActivationReceipt({ codexHome }), applied.receipt); + + const repeated = writeActivationReceipt({ + codexHome, + mode: "hooks", + degradationReason: "streaming_interruption_unavailable", + apply: true, + now: () => new Date("2026-08-11T07:30:00.000Z"), + }); + assert.equal(repeated.changed, false); + assert.equal(repeated.applied, false); + assert.equal(repeated.receipt.updatedAt, "2026-08-11T06:30:00.000Z"); +}); From 7f5f81d13585f8bff980ec8be9d7a1011b09c315 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:30:52 +0200 Subject: [PATCH 07/48] feat: report installed watchdog capability --- scripts/lib/runtime-capabilities.mjs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/lib/runtime-capabilities.mjs b/scripts/lib/runtime-capabilities.mjs index dc88590..37d764c 100644 --- a/scripts/lib/runtime-capabilities.mjs +++ b/scripts/lib/runtime-capabilities.mjs @@ -16,6 +16,7 @@ export function buildRuntimeCapabilities({ os = process.platform, probes = {}, declarations = {}, + activation = null, repo = null, } = {}) { const tools = { @@ -34,9 +35,20 @@ export function buildRuntimeCapabilities({ subagents: boolean(declarations.subagents), reviewTool: boolean(declarations.reviewTool), }; + const declaredWatchdog = declarations.progressWatchdog; + const installedWatchdog = activation?.mode; const runtime = { - progressWatchdog: watchdogMode(declarations.progressWatchdog), + progressWatchdog: watchdogMode( + declaredWatchdog === undefined || declaredWatchdog === null || declaredWatchdog === "" + ? installedWatchdog + : declaredWatchdog, + ), + progressWatchdogDegradationReason: + activation?.degradationReason || null, + progressWatchdogLauncherPath: + typeof activation?.launcherPath === "string" ? activation.launcherPath : null, }; + runtime.progressWatchdogAvailable = runtime.progressWatchdog !== "none"; const ghReadable = tools.gh && tools.ghAuthenticated && boolean(probes.repoReadableViaGh); @@ -113,6 +125,7 @@ export function buildRuntimeCapabilities({ !github.rulesetsReadable && "rulesets_unreadable", !github.reviewThreadsReadable && "review_threads_unreadable", fallbacks.rateLimits === "unavailable" && "rate_limit_probe_unavailable", + runtime.progressWatchdog === "none" && "progress_watchdog_unavailable", ]); return { From ce6fbcb81860fccd488121f216c3a7fa31290694 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:31:15 +0200 Subject: [PATCH 08/48] feat: load persisted watchdog activation --- scripts/runtime-capabilities.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/runtime-capabilities.mjs b/scripts/runtime-capabilities.mjs index b876995..9df45f1 100644 --- a/scripts/runtime-capabilities.mjs +++ b/scripts/runtime-capabilities.mjs @@ -1,8 +1,11 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; import { buildRuntimeCapabilities } from "./lib/runtime-capabilities.mjs"; +import { readActivationReceipt } from "./lib/watchdog-activation.mjs"; const usage = "Usage: node scripts/runtime-capabilities.mjs [--repo OWNER/REPO] [--input FILE]"; @@ -13,8 +16,9 @@ function parseBoolean(value) { } function watchdogDeclaration(value) { - const normalized = String(value || "none").toLowerCase(); - return ["hooks", "stream"].includes(normalized) ? normalized : "none"; + if (value === undefined || value === null || value === "") return undefined; + const normalized = String(value).toLowerCase(); + return ["hooks", "stream", "none"].includes(normalized) ? normalized : "none"; } function commandAvailable(command, args = ["--version"]) { @@ -67,10 +71,12 @@ function liveInput(repo) { ]) : null; const permission = String(repoData?.viewerPermission || "").toUpperCase(); + const codexHome = resolve(process.env.CODEX_HOME || join(homedir(), ".codex")); return { host: process.env.SHIPPING_GITHUB_HOST || "unknown", os: process.platform, repo: resolvedRepo, + activation: readActivationReceipt({ codexHome }), probes: { node: true, git: commandAvailable("git"), From 4a8f8dd623902864ca9fb12ef4c787c16fdb2a55 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:31:37 +0200 Subject: [PATCH 09/48] test: cover persisted watchdog runtime state --- .../runtime-capabilities-activation.test.mjs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/unit/runtime-capabilities-activation.test.mjs diff --git a/tests/unit/runtime-capabilities-activation.test.mjs b/tests/unit/runtime-capabilities-activation.test.mjs new file mode 100644 index 0000000..da85405 --- /dev/null +++ b/tests/unit/runtime-capabilities-activation.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { buildRuntimeCapabilities } from "../../scripts/lib/runtime-capabilities.mjs"; + +test("persisted stream activation is reported without an environment declaration", () => { + const result = buildRuntimeCapabilities({ + probes: { node: true }, + activation: { + schemaVersion: 1, + mode: "stream", + degradationReason: null, + launcherPath: "/tmp/github-delivery-codex", + }, + declarations: {}, + }); + assert.equal(result.runtime.progressWatchdog, "stream"); + assert.equal(result.runtime.progressWatchdogAvailable, true); + assert.equal(result.runtime.progressWatchdogLauncherPath, "/tmp/github-delivery-codex"); + assert.equal(result.fallbacks.contextEconomy, "streaming-watchdog"); +}); + +test("persisted hook activation reports its streaming limitation", () => { + const result = buildRuntimeCapabilities({ + probes: { node: true }, + activation: { + schemaVersion: 1, + mode: "hooks", + degradationReason: "streaming_interruption_unavailable", + launcherPath: null, + }, + declarations: {}, + }); + assert.equal(result.runtime.progressWatchdog, "hooks"); + assert.equal( + result.runtime.progressWatchdogDegradationReason, + "streaming_interruption_unavailable", + ); + assert.equal(result.fallbacks.contextEconomy, "lifecycle-hooks"); +}); + +test("explicit runtime declaration can override persisted activation for controlled hosts", () => { + const result = buildRuntimeCapabilities({ + probes: { node: true }, + activation: { mode: "hooks", degradationReason: "streaming_interruption_unavailable" }, + declarations: { progressWatchdog: "stream" }, + }); + assert.equal(result.runtime.progressWatchdog, "stream"); +}); From c2b40b07e8b27f83744b4360b65dc8c40b8827ac Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:32:44 +0200 Subject: [PATCH 10/48] feat: add protected Codex remote bridge --- scripts/lib/codex-watchdog-remote-bridge.mjs | 218 +++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 scripts/lib/codex-watchdog-remote-bridge.mjs diff --git a/scripts/lib/codex-watchdog-remote-bridge.mjs b/scripts/lib/codex-watchdog-remote-bridge.mjs new file mode 100644 index 0000000..7539470 --- /dev/null +++ b/scripts/lib/codex-watchdog-remote-bridge.mjs @@ -0,0 +1,218 @@ +import { createHash, timingSafeEqual } from "node:crypto"; +import { createServer } from "node:http"; +import { createInterface } from "node:readline"; + +import { createAppServerWatchdogRouter } from "./codex-app-server-watchdog-proxy.mjs"; + +const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +const MAX_FRAME_BYTES = 16 * 1024 * 1024; + +function safeEqual(left, right) { + const a = Buffer.from(String(left || "")); + const b = Buffer.from(String(right || "")); + return a.length === b.length && timingSafeEqual(a, b); +} + +function encodeFrame(payload, opcode = 0x1) { + const data = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload), "utf8"); + if (data.length > MAX_FRAME_BYTES) throw new Error("WebSocket frame exceeds watchdog bridge limit"); + let header; + if (data.length < 126) { + header = Buffer.from([0x80 | opcode, data.length]); + } else if (data.length <= 0xffff) { + header = Buffer.alloc(4); + header[0] = 0x80 | opcode; + header[1] = 126; + header.writeUInt16BE(data.length, 2); + } else { + header = Buffer.alloc(10); + header[0] = 0x80 | opcode; + header[1] = 127; + header.writeBigUInt64BE(BigInt(data.length), 2); + } + return Buffer.concat([header, data]); +} + +function createFrameParser(onText, onClose, sendControl) { + let buffer = Buffer.alloc(0); + let fragmentedOpcode = null; + let fragments = []; + + function deliver(opcode, payload, fin) { + if (opcode === 0x8) { + onClose(); + return; + } + if (opcode === 0x9) { + sendControl(0xA, payload); + return; + } + if (opcode === 0xA) return; + + if (opcode === 0x0) { + if (fragmentedOpcode === null) throw new Error("unexpected continuation frame"); + fragments.push(payload); + if (fin) { + const complete = Buffer.concat(fragments); + const originalOpcode = fragmentedOpcode; + fragmentedOpcode = null; + fragments = []; + if (originalOpcode === 0x1) onText(complete.toString("utf8")); + } + return; + } + + if (opcode !== 0x1) throw new Error(`unsupported WebSocket opcode ${opcode}`); + if (fin) { + onText(payload.toString("utf8")); + } else { + fragmentedOpcode = opcode; + fragments = [payload]; + } + } + + return { + push(chunk) { + buffer = Buffer.concat([buffer, chunk]); + while (buffer.length >= 2) { + const first = buffer[0]; + const second = buffer[1]; + const fin = Boolean(first & 0x80); + const opcode = first & 0x0f; + const masked = Boolean(second & 0x80); + let length = second & 0x7f; + let offset = 2; + + if (length === 126) { + if (buffer.length < 4) return; + length = buffer.readUInt16BE(2); + offset = 4; + } else if (length === 127) { + if (buffer.length < 10) return; + const large = buffer.readBigUInt64BE(2); + if (large > BigInt(MAX_FRAME_BYTES)) throw new Error("WebSocket frame exceeds watchdog bridge limit"); + length = Number(large); + offset = 10; + } + if (length > MAX_FRAME_BYTES) throw new Error("WebSocket frame exceeds watchdog bridge limit"); + if (!masked) throw new Error("client WebSocket frames must be masked"); + if (buffer.length < offset + 4 + length) return; + + const mask = buffer.subarray(offset, offset + 4); + offset += 4; + const payload = Buffer.from(buffer.subarray(offset, offset + length)); + for (let index = 0; index < payload.length; index += 1) { + payload[index] ^= mask[index % 4]; + } + buffer = buffer.subarray(offset + length); + deliver(opcode, payload, fin); + } + }, + }; +} + +function rejectUpgrade(socket, status, message) { + socket.end( + `HTTP/1.1 ${status}\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: ${Buffer.byteLength(message)}\r\n\r\n${message}`, + ); +} + +export async function startCodexWatchdogRemoteBridge({ + appServerInput, + appServerOutput, + token = null, + router = createAppServerWatchdogRouter(), + host = "127.0.0.1", +} = {}) { + if (!appServerInput?.writable || !appServerOutput?.readable) { + throw new Error("appServerInput and appServerOutput streams are required"); + } + + const clients = new Set(); + const server = createServer((request, response) => { + response.writeHead(404).end(); + }); + + server.on("upgrade", (request, socket, head) => { + if (clients.size > 0) { + rejectUpgrade(socket, "409 Conflict", "watchdog bridge already has a client"); + return; + } + if (token !== null) { + const expected = `Bearer ${token}`; + if (!safeEqual(request.headers.authorization, expected)) { + rejectUpgrade(socket, "401 Unauthorized", "missing or invalid watchdog bridge token"); + return; + } + } + const key = request.headers["sec-websocket-key"]; + if (!key || String(request.headers.upgrade || "").toLowerCase() !== "websocket") { + rejectUpgrade(socket, "400 Bad Request", "invalid WebSocket upgrade"); + return; + } + const accept = createHash("sha1").update(`${key}${WEBSOCKET_GUID}`).digest("base64"); + socket.write( + "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + `Sec-WebSocket-Accept: ${accept}\r\n\r\n`, + ); + clients.add(socket); + + const parser = createFrameParser( + (text) => { + if (appServerInput.writable) appServerInput.write(`${text}\n`); + }, + () => socket.end(), + (opcode, payload) => socket.write(encodeFrame(payload, opcode)), + ); + socket.on("data", (chunk) => { + try { + parser.push(chunk); + } catch { + socket.destroy(); + } + }); + socket.on("close", () => clients.delete(socket)); + socket.on("error", () => clients.delete(socket)); + if (head?.length) parser.push(head); + }); + + const lines = createInterface({ input: appServerOutput, crlfDelay: Infinity }); + lines.on("line", (line) => { + let message; + try { + message = JSON.parse(line); + } catch { + for (const client of clients) client.write(encodeFrame(line)); + return; + } + const routed = router.onServerMessage(message); + if (routed.forward) { + const text = JSON.stringify(routed.forward); + for (const client of clients) client.write(encodeFrame(text)); + } + for (const request of routed.internalRequests) { + if (appServerInput.writable) appServerInput.write(`${JSON.stringify(request)}\n`); + } + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, host, () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("watchdog bridge did not bind TCP"); + + return { + url: `ws://${host}:${address.port}`, + close: async () => { + lines.close(); + for (const client of clients) client.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + }, + }; +} From fea6ee1576ff39f431c80e52dfd550213fecc7b8 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:33:05 +0200 Subject: [PATCH 11/48] feat: add protected Codex launcher --- scripts/codex-with-watchdog.mjs | 97 +++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 scripts/codex-with-watchdog.mjs diff --git a/scripts/codex-with-watchdog.mjs b/scripts/codex-with-watchdog.mjs new file mode 100644 index 0000000..e43d76b --- /dev/null +++ b/scripts/codex-with-watchdog.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +import { randomBytes } from "node:crypto"; +import { spawn } from "node:child_process"; +import { pathToFileURL } from "node:url"; + +import { startCodexWatchdogRemoteBridge } from "./lib/codex-watchdog-remote-bridge.mjs"; + +const TOKEN_ENV = "GITHUB_DELIVERY_CODEX_REMOTE_TOKEN"; + +function waitForExit(child) { + return new Promise((resolve, reject) => { + child.once("error", reject); + child.once("exit", (code, signal) => resolve({ code, signal })); + }); +} + +export async function runProtectedCodex({ + codexBin = process.env.CODEX_BIN || "codex", + args = process.argv.slice(2), + env = process.env, + spawnImpl = spawn, + stderr = process.stderr, +} = {}) { + const token = randomBytes(32).toString("base64url"); + const appServer = spawnImpl(codexBin, ["app-server"], { + stdio: ["pipe", "pipe", "inherit"], + windowsHide: true, + env, + }); + appServer.once("error", (error) => { + stderr.write(`github-delivery watchdog app-server error: ${error?.message || error}\n`); + }); + + let bridge; + try { + bridge = await startCodexWatchdogRemoteBridge({ + appServerInput: appServer.stdin, + appServerOutput: appServer.stdout, + token, + }); + } catch (error) { + if (!appServer.killed) appServer.kill(); + throw error; + } + + const clientEnv = { ...env, [TOKEN_ENV]: token }; + const client = spawnImpl( + codexBin, + ["--remote", bridge.url, "--remote-auth-token-env", TOKEN_ENV, ...args], + { + stdio: "inherit", + windowsHide: true, + env: clientEnv, + }, + ); + + const cleanup = async () => { + if (!client.killed) client.kill(); + if (!appServer.killed) appServer.kill(); + await bridge.close().catch(() => {}); + }; + const signals = ["SIGINT", "SIGTERM"]; + const handlers = new Map(); + for (const signal of signals) { + const handler = () => void cleanup(); + handlers.set(signal, handler); + process.once(signal, handler); + } + + try { + const outcome = await waitForExit(client); + if (!appServer.killed) appServer.kill(); + await bridge.close(); + return outcome; + } finally { + for (const [signal, handler] of handlers) process.off(signal, handler); + } +} + +export async function main() { + try { + const result = await runProtectedCodex(); + if (result.signal) { + process.stderr.write(`github-delivery protected Codex exited on ${result.signal}\n`); + process.exitCode = 1; + } else { + process.exitCode = Number.isInteger(result.code) ? result.code : 1; + } + } catch (error) { + process.stderr.write(`github-delivery protected Codex failed: ${error?.message || error}\n`); + process.exitCode = 1; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main(); +} From 41184d8162b0d1e868d644863df7b91dc0ea3514 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:33:43 +0200 Subject: [PATCH 12/48] test: verify installed protected launcher boundary --- tests/unit/watchdog-activation.test.mjs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/unit/watchdog-activation.test.mjs b/tests/unit/watchdog-activation.test.mjs index 6cbfee8..812ccc3 100644 --- a/tests/unit/watchdog-activation.test.mjs +++ b/tests/unit/watchdog-activation.test.mjs @@ -13,12 +13,14 @@ function fixture() { const source = join(root, "source"); const target = join(root, "skills", "github-delivery"); const codexHome = join(root, ".codex"); - mkdirSync(source, { recursive: true }); + mkdirSync(join(source, "scripts"), { recursive: true }); writeFileSync( join(source, "package.json"), `${JSON.stringify({ name: "github-delivery", version: "0.2.0" }, null, 2)}\n`, ); writeFileSync(join(source, "marker.txt"), "installed\n"); + writeFileSync(join(source, "scripts", "codex-watchdog-hook.mjs"), "// hook fixture\n"); + writeFileSync(join(source, "scripts", "codex-with-watchdog.mjs"), "// launcher fixture\n"); return { root, source, target, codexHome }; } @@ -61,9 +63,13 @@ test("normal Codex install activates lifecycle watchdog without a second install } assert.equal(existsSync(join(f.codexHome, "github-delivery", "watchdog-activation.json")), true); + assert.equal( + receipt.watchdog.streamLauncherPath, + join(f.target, "scripts", "codex-with-watchdog.mjs"), + ); }); -test("stream mode is selected only when the launch boundary is explicitly controllable", () => { +test("stream mode is selected only when the installed launch boundary is explicitly controllable", () => { const f = fixture(); const withoutBoundary = runInstall(f, ["--lifecycle-hooks-supported"]); assert.equal(withoutBoundary.status, 0, withoutBoundary.stderr); @@ -74,7 +80,9 @@ test("stream mode is selected only when the launch boundary is explicitly contro "--stream-launch-controlled", ]); assert.equal(withBoundary.status, 0, withBoundary.stderr); - assert.equal(JSON.parse(withBoundary.stdout).watchdog.mode, "stream"); + const result = JSON.parse(withBoundary.stdout); + assert.equal(result.watchdog.mode, "stream"); + assert.equal(result.watchdog.launcherPath, join(f.target, "scripts", "codex-with-watchdog.mjs")); }); test("unsupported hosts report watchdog unavailability without blocking skill installation", () => { From 5c04654f1ca0bd7747f6899d9e34685401295707 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:34:07 +0200 Subject: [PATCH 13/48] feat: bind stream mode to installed protected launcher --- scripts/install-skill.mjs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/install-skill.mjs b/scripts/install-skill.mjs index 84347ba..cf6b3b1 100755 --- a/scripts/install-skill.mjs +++ b/scripts/install-skill.mjs @@ -68,15 +68,27 @@ export function installSkill(options) { : { action: "restore", apply: false, backup: options.restore, target: options.target }; } + const sourceLauncherPath = join(options.source, "scripts", "codex-with-watchdog.mjs"); + const installedLauncherPath = join(options.target, "scripts", "codex-with-watchdog.mjs"); + const launcherBundled = existsSync(sourceLauncherPath); + const streamLaunchVerified = options.streamLaunchControlled === true && launcherBundled; + const installation = options.apply ? applyInstallation(options) : { ...planInstallation(options), apply: false }; + if (options.apply && launcherBundled && !existsSync(installedLauncherPath)) { + throw new Error("protected Codex launcher was not installed with the skill payload"); + } + const selection = selectWatchdogMode({ host: options.host, - streamLaunchControlled: options.streamLaunchControlled, + streamLaunchControlled: streamLaunchVerified, lifecycleHooksSupported: options.lifecycleHooksSupported, }); + if (options.streamLaunchControlled && !launcherBundled && selection.mode !== "stream") { + selection.degradationReason = "stream_launcher_unavailable"; + } let hookResult = null; if (options.host === "codex" && options.lifecycleHooksSupported) { @@ -87,11 +99,12 @@ export function installSkill(options) { }); } + const launcherPath = selection.mode === "stream" ? installedLauncherPath : null; const receiptResult = writeActivationReceipt({ codexHome: options.codexHome, mode: selection.mode, degradationReason: selection.degradationReason, - launcherPath: null, + launcherPath, apply: options.apply, }); @@ -103,7 +116,8 @@ export function installSkill(options) { receiptPath: receiptResult.path, receiptChanged: receiptResult.changed, hookResult, - launcherPath: null, + launcherPath, + streamLauncherPath: launcherBundled ? installedLauncherPath : null, }, }; } From 4fa4c21d6d8b5b8d10733a1756b04fb2a29c22ff Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:34:47 +0200 Subject: [PATCH 14/48] test: prove protected bridge interrupts live narration stalls --- .../codex-watchdog-remote-bridge.test.mjs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/unit/codex-watchdog-remote-bridge.test.mjs diff --git a/tests/unit/codex-watchdog-remote-bridge.test.mjs b/tests/unit/codex-watchdog-remote-bridge.test.mjs new file mode 100644 index 0000000..54f45e3 --- /dev/null +++ b/tests/unit/codex-watchdog-remote-bridge.test.mjs @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import { PassThrough } from "node:stream"; +import test from "node:test"; + +import { startCodexWatchdogRemoteBridge } from "../../scripts/lib/codex-watchdog-remote-bridge.mjs"; + +function nextTurn() { + return new Promise((resolve) => setImmediate(resolve)); +} + +function openWebSocket(url) { + return new Promise((resolve, reject) => { + const socket = new WebSocket(url); + socket.addEventListener("open", () => resolve(socket), { once: true }); + socket.addEventListener("error", () => reject(new Error("WebSocket connection failed")), { once: true }); + }); +} + +test("installed streaming boundary interrupts the observed Let me check type loop before 500 characters", async () => { + const appServerInput = new PassThrough(); + const appServerOutput = new PassThrough(); + let requests = ""; + appServerInput.on("data", (chunk) => { + requests += chunk.toString("utf8"); + }); + + const bridge = await startCodexWatchdogRemoteBridge({ + appServerInput, + appServerOutput, + token: null, + }); + const client = await openWebSocket(bridge.url); + + const phrases = [ + "Let me check the type.\n", + "Let me check the NOUS_DEF type.\n", + "Let me check the live test type.\n", + "Let me check the type.\n", + "Let me check the OAuthProviderDef type.\n", + "Let me check the type.\n", + "Let me check the current NOUS_DEF type.\n", + "Let me check the type.\n", + ]; + + let emitted = 0; + for (const delta of phrases) { + emitted += delta.length; + appServerOutput.write( + `${JSON.stringify({ + method: "item/agentMessage/delta", + params: { + threadId: "thr_live", + turnId: "turn_live", + itemId: "item_live", + delta, + }, + })}\n`, + ); + await nextTurn(); + if (requests.includes('"method":"turn/interrupt"')) break; + } + + assert.match(requests, /"method":"turn\/interrupt"/); + assert.ok(emitted < 500, `protected boundary allowed ${emitted} characters before interruption`); + assert.equal((requests.match(/"method":"turn\/interrupt"/g) || []).length, 1); + + appServerOutput.write( + `${JSON.stringify({ + method: "item/agentMessage/delta", + params: { + threadId: "thr_live", + turnId: "turn_live", + itemId: "item_live", + delta: "Let me check the type.\n", + }, + })}\n`, + ); + await nextTurn(); + assert.equal((requests.match(/"method":"turn\/interrupt"/g) || []).length, 1); + + client.close(); + await bridge.close(); +}); From a7f3496dbebd586077ca0adc242fbb9b99dfe931 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:35:47 +0200 Subject: [PATCH 15/48] fix: prevent protected Codex remote bypass --- scripts/codex-with-watchdog.mjs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/scripts/codex-with-watchdog.mjs b/scripts/codex-with-watchdog.mjs index e43d76b..df1e8ef 100644 --- a/scripts/codex-with-watchdog.mjs +++ b/scripts/codex-with-watchdog.mjs @@ -14,6 +14,16 @@ function waitForExit(child) { }); } +export function protectedClientArgs(args, url) { + if (args.some((arg) => arg === "--remote" || arg.startsWith("--remote="))) { + throw new Error("protected Codex launcher owns --remote; remove the caller-supplied remote endpoint"); + } + if (args.some((arg) => arg === "--remote-auth-token-env" || arg.startsWith("--remote-auth-token-env="))) { + throw new Error("protected Codex launcher owns --remote-auth-token-env"); + } + return ["--remote", url, "--remote-auth-token-env", TOKEN_ENV, ...args]; +} + export async function runProtectedCodex({ codexBin = process.env.CODEX_BIN || "codex", args = process.argv.slice(2), @@ -44,15 +54,11 @@ export async function runProtectedCodex({ } const clientEnv = { ...env, [TOKEN_ENV]: token }; - const client = spawnImpl( - codexBin, - ["--remote", bridge.url, "--remote-auth-token-env", TOKEN_ENV, ...args], - { - stdio: "inherit", - windowsHide: true, - env: clientEnv, - }, - ); + const client = spawnImpl(codexBin, protectedClientArgs(args, bridge.url), { + stdio: "inherit", + windowsHide: true, + env: clientEnv, + }); const cleanup = async () => { if (!client.killed) client.kill(); From 8b7cee8039db4306f231f22a23d09d2af78ff660 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:35:59 +0200 Subject: [PATCH 16/48] test: protect Codex launcher remote ownership --- tests/unit/codex-protected-launcher.test.mjs | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/unit/codex-protected-launcher.test.mjs diff --git a/tests/unit/codex-protected-launcher.test.mjs b/tests/unit/codex-protected-launcher.test.mjs new file mode 100644 index 0000000..7425828 --- /dev/null +++ b/tests/unit/codex-protected-launcher.test.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { protectedClientArgs } from "../../scripts/codex-with-watchdog.mjs"; + +test("protected launcher owns the remote endpoint and preserves normal Codex args", () => { + assert.deepEqual(protectedClientArgs(["resume", "abc"], "ws://127.0.0.1:4500"), [ + "--remote", + "ws://127.0.0.1:4500", + "--remote-auth-token-env", + "GITHUB_DELIVERY_CODEX_REMOTE_TOKEN", + "resume", + "abc", + ]); +}); + +test("protected launcher rejects caller attempts to bypass its remote bridge", () => { + assert.throws( + () => protectedClientArgs(["--remote", "ws://elsewhere:4500"], "ws://127.0.0.1:4500"), + /owns --remote/, + ); + assert.throws( + () => protectedClientArgs(["--remote-auth-token-env=OTHER"], "ws://127.0.0.1:4500"), + /owns --remote-auth-token-env/, + ); +}); From 2453de3d9cb05686755ba201c73912cfedcf16ed Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:37:55 +0200 Subject: [PATCH 17/48] docs: explain automatic watchdog activation --- INSTALL.md | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index c386e81..ca489c2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -47,23 +47,54 @@ node scripts/install-skill.mjs --apply Existing directory installations are backed up before replacement. Symlinks and non-skill directories fail closed unless the operator inspects the plan and explicitly supplies `--force`. Downgrades require `--allow-downgrade`. -## Optional Codex progress watchdog +## Codex progress watchdog activation -For Codex, GitHub Delivery can install lifecycle hooks that block duplicate unchanged reads, rate-limit manual status polling, bound oversized subagent briefs/tool output, and recover from completed no-progress narration stalls. +A standard Codex install/upgrade now plans the watchdog together with the skill. When Codex is detected and lifecycle hooks are supported, `--apply` also installs the GitHub Delivery hook entries in `~/.codex/hooks.json`. Existing hook configuration is preserved, backed up before a change, and updated idempotently. -The hook installer is separately opt-in and dry-runs by default. It preserves existing hooks and reports the planned change: +The installer records the effective mode in: + +```text +~/.codex/github-delivery/watchdog-activation.json +``` + +The receipt contains only activation metadata. It does not contain prompts, conversations, tool inputs, or secrets. + +The modes are intentionally strict: + +- `stream`: a host has explicitly bound future Codex launches to GitHub Delivery's protected streaming launcher; +- `hooks`: lifecycle enforcement is active, but in-progress assistant text cannot be interrupted before `Stop`; +- `none`: no runtime enforcement surface was verified and policy-only protection remains. + +The protected launcher is installed with the skill at: + +```text +~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs +``` + +Run Codex through it when you need in-flight repeated-narration interruption: ```bash -node scripts/install-codex-watchdog-hooks.mjs +node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs ``` -Apply it explicitly after installing/upgrading the skill: +Arguments after the script are passed to the normal Codex CLI, for example: ```bash -node scripts/install-codex-watchdog-hooks.mjs --apply +node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs resume ``` -The installer targets `~/.codex/hooks.json`, creates a backup before changing an existing file, fails closed on malformed or symlinked hook configuration, and adds only GitHub Delivery's missing `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd` entries. Reapplying is idempotent. +The launcher starts the real Codex App Server on its normal stdio transport, exposes an authenticated loopback bridge to the Codex `--remote` client, observes assistant-message deltas, and can issue a private `turn/interrupt` before a repeated no-progress message grows unbounded. It owns `--remote` and `--remote-auth-token-env`; caller-supplied replacements are rejected so the protected boundary cannot be bypassed accidentally. + +Installing the launcher does **not** make an ordinary `codex` or IDE process use it automatically. Codex currently exposes remote App Server selection as a launch option rather than a persistent default. A host must actually launch through the protected entry point before GitHub Delivery records `stream` as active. + +### Manual hook repair + +`scripts/install-codex-watchdog-hooks.mjs` remains available as a repair or non-standard-install tool. It is dry-run by default: + +```bash +node scripts/install-codex-watchdog-hooks.mjs +node scripts/install-codex-watchdog-hooks.mjs --apply +``` If the skill was installed somewhere other than `~/.agents/skills/github-delivery`, pass that path explicitly: @@ -71,7 +102,7 @@ If the skill was installed somewhere other than `~/.agents/skills/github-deliver node scripts/install-codex-watchdog-hooks.mjs --skill-dir ~/.codex/skills/github-delivery --apply ``` -Lifecycle hooks cannot stop tokens already emitted inside one assistant message. Custom Codex App Server clients can use the stronger streaming proxy described in [`references/agent-progress-watchdog.md`](references/agent-progress-watchdog.md). +See [`references/agent-progress-watchdog.md`](references/agent-progress-watchdog.md) for the enforcement boundaries and incident behaviour. ## Restore a backup From b65cd59277137fbe871ce890edd480dcbdaf91e5 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:39:18 +0200 Subject: [PATCH 18/48] docs: document activated watchdog boundaries --- references/agent-progress-watchdog.md | 72 ++++++++++++++------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/references/agent-progress-watchdog.md b/references/agent-progress-watchdog.md index d6d2c28..89154dd 100644 --- a/references/agent-progress-watchdog.md +++ b/references/agent-progress-watchdog.md @@ -4,7 +4,7 @@ GitHub Delivery uses a layered progress watchdog to reduce token waste without w ## What it protects against -- repeated in-turn intentions such as `Let me read ...` with no tool boundary; +- repeated in-turn intentions such as `Let me read ...` or `Let me check ...` with no tool boundary; - exact reads repeated on unchanged state; - ad-hoc high-frequency CI/status polling; - oversized model-facing tool output when only a focused diagnostic excerpt is required; @@ -12,6 +12,22 @@ GitHub Delivery uses a layered progress watchdog to reduce token waste without w The watchdog never grants GitHub mutation authority, executes a write on the agent's behalf, or treats omitted/unknown evidence as success. +## Activation truth + +A normal Codex install/upgrade through `scripts/install-skill.mjs --apply` activates lifecycle hooks when Codex is detected and records the effective watchdog mode in: + +```text +~/.codex/github-delivery/watchdog-activation.json +``` + +The receipt is non-sensitive activation metadata only. Runtime capability discovery reads it so `none`, `hooks`, and `stream` describe what is actually active instead of merely what code exists in the installed skill. + +Mode selection is strongest verified mode only: + +1. `stream` when the host has explicitly bound future launches to the protected streaming entry point; +2. `hooks` when lifecycle hooks are active but the launch boundary is not controlled; +3. `none` when neither runtime surface is verified. + ## Enforcement levels ### Policy only @@ -22,66 +38,52 @@ This reduces ordinary waste but cannot forcibly stop a pathological assistant me ### Codex lifecycle hooks -Use `scripts/codex-watchdog-hook.mjs` for `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd`. +`scripts/codex-watchdog-hook.mjs` handles `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd`. This layer can: - block an exact stable duplicate read on unchanged state; - rate-limit identical volatile polls; -- reject an oversized `Agent`/subagent tool input and require a focused brief that references source files instead of copying them; +- reject an oversized `Agent`/subagent tool input and require a focused source-referenced brief; - compact oversized model-facing tool output while retaining failure/error/blocker signals; - detect a completed no-progress assistant or subagent message and request one corrective continuation; - fail closed if that corrective continuation stalls again; - delete per-session state at `SessionEnd`. -The default subagent-input budget is 6,000 serialized characters. It is intentionally a context budget, not an authority or correctness gate. +The default subagent-input budget is 6,000 serialized characters. It is a context budget, not an authority or correctness gate. Hook state is stored outside repository content. Session ids and read inputs are represented only by SHA-256 fingerprints; raw tool arguments are not persisted. Lifecycle hooks cannot reclaim tokens already emitted inside the assistant message that reaches `Stop` or `SubagentStop`. -Example hook command for a skill installed under the standard agents directory: - -```json -{ - "description": "GitHub Delivery progress watchdog", - "hooks": { - "PreToolUse": [{"hooks": [{"type": "command", "command": "node ~/.agents/skills/github-delivery/scripts/codex-watchdog-hook.mjs", "commandWindows": "node \"%USERPROFILE%\\.agents\\skills\\github-delivery\\scripts\\codex-watchdog-hook.mjs\""}]}], - "PostToolUse": [{"hooks": [{"type": "command", "command": "node ~/.agents/skills/github-delivery/scripts/codex-watchdog-hook.mjs", "commandWindows": "node \"%USERPROFILE%\\.agents\\skills\\github-delivery\\scripts\\codex-watchdog-hook.mjs\""}]}], - "Stop": [{"hooks": [{"type": "command", "command": "node ~/.agents/skills/github-delivery/scripts/codex-watchdog-hook.mjs", "commandWindows": "node \"%USERPROFILE%\\.agents\\skills\\github-delivery\\scripts\\codex-watchdog-hook.mjs\""}]}], - "SubagentStop": [{"hooks": [{"type": "command", "command": "node ~/.agents/skills/github-delivery/scripts/codex-watchdog-hook.mjs", "commandWindows": "node \"%USERPROFILE%\\.agents\\skills\\github-delivery\\scripts\\codex-watchdog-hook.mjs\""}]}], - "SessionEnd": [{"hooks": [{"type": "command", "command": "node ~/.agents/skills/github-delivery/scripts/codex-watchdog-hook.mjs", "commandWindows": "node \"%USERPROFILE%\\.agents\\skills\\github-delivery\\scripts\\codex-watchdog-hook.mjs\""}]}] - } -} -``` +The normal Codex installer path now reuses the safe hook installer automatically on `--apply`. `scripts/install-codex-watchdog-hooks.mjs` remains available for repair and non-standard installs. Hook configuration is backup-first, preserves unrelated entries, rejects malformed or symlinked configuration, and is idempotent. -Configure this in the host's trusted hook configuration. GitHub Delivery does not silently modify global host configuration. +### Protected Codex streaming launcher -Declare this capability to runtime inspection with: +For the strongest boundary, launch Codex through the installed entry point: ```text -SHIPPING_GITHUB_PROGRESS_WATCHDOG=hooks +node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs ``` -### Codex App Server streaming proxy - -For the strongest boundary, launch Codex App Server through: +On Windows use the equivalent path under `%USERPROFILE%\.agents\skills\github-delivery`. -```text -node ~/.agents/skills/github-delivery/scripts/codex-app-server-watchdog-proxy.mjs -``` +The launcher: -On Windows use the equivalent path under `%USERPROFILE%\.agents\skills\github-delivery`. +1. starts the real `codex app-server` on its normal stdio transport; +2. creates a loopback-only authenticated bridge; +3. starts the ordinary Codex client with the documented `--remote` and `--remote-auth-token-env` flags pointed at that bridge; +4. forwards JSON-RPC traffic while observing `item/agentMessage/delta` notifications; +5. issues one private `turn/interrupt` when repeated low-novelty intent narration crosses the watchdog threshold; +6. consumes the private interrupt response rather than leaking it to the client. -The proxy transparently forwards App Server JSONL traffic, observes streamed assistant-message deltas, and issues one private `turn/interrupt` request when repeated low-novelty intent narration crosses the watchdog threshold. Responses to the proxy's private interrupt requests are consumed rather than forwarded to the client. +The bearer token is generated in memory for the launched client and is not persisted. The bridge binds only to loopback. The protected launcher owns the remote endpoint flags and rejects caller-supplied replacements. -This is the only GitHub Delivery layer that can stop the targeted failure while the assistant message is still streaming. A client must intentionally launch/use this proxy instead of plain `codex app-server`; the normal CLI or IDE is not automatically rerouted through it. +This is the only GitHub Delivery layer that can stop the targeted failure while an assistant message is still streaming. The incident regression includes the observed phrase family `Let me check the type`, `Let me check the NOUS_DEF type`, and `Let me check the OAuthProviderDef type`, and requires the interrupt before 500 emitted characters. -Declare this mode with: +Installing the launcher does not silently reroute an already-running or ordinarily-launched Codex CLI/IDE process. `stream` is recorded only when the host actually controls launches through this entry point. Otherwise lifecycle hooks remain active and the receipt reports `hooks` with `streaming_interruption_unavailable`. -```text -SHIPPING_GITHUB_PROGRESS_WATCHDOG=stream -``` +The older `scripts/codex-app-server-watchdog-proxy.mjs` remains useful to custom stdio App Server clients. It provides the same delta watchdog for clients that already own the App Server protocol connection. ## Read economy @@ -101,4 +103,4 @@ Unknown tools are not denied by economy classification. This avoids suppressing Oversized tool output is reduced deterministically to a bounded head/tail plus unique failure-signalling lines. The result records original and omitted character counts. -Compaction is never positive evidence. If omitted content is required to diagnose ambiguity or failure, retrieve the focused missing evidence or the full raw output as the final escalation step. \ No newline at end of file +Compaction is never positive evidence. If omitted content is required to diagnose ambiguity or failure, retrieve the focused missing evidence or the full raw output as the final escalation step. From c7178d94c06db30e920ef312060ccb8ff4181a35 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:39:46 +0200 Subject: [PATCH 19/48] docs: explain persisted watchdog capability --- references/runtime-capabilities.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/references/runtime-capabilities.md b/references/runtime-capabilities.md index 68db83d..b793880 100644 --- a/references/runtime-capabilities.md +++ b/references/runtime-capabilities.md @@ -10,7 +10,7 @@ Run internally at workflow start: node scripts/runtime-capabilities.mjs --repo OWNER/REPO ``` -`--repo` is optional when the probe runs inside the target checkout: it detects the repository from `gh repo view` (current-directory remote) or falls back to `git config --get remote.origin.url`. Pass `--repo` explicitly when the checkout is missing or the target differs from the local remote. +`--repo` is optional when the probe runs inside the target checkout: it detects the repository from `gh repo view` or falls back to `git config --get remote.origin.url`. Pass `--repo` explicitly when the checkout is missing or the target differs from the local remote. The script probes facts available to the local process: @@ -21,8 +21,9 @@ The script probes facts available to the local process: - GitHub CLI authentication - repository readability through `gh` - repository write permission through `gh` +- persisted GitHub Delivery watchdog activation under the active Codex home -Capabilities that exist only in the host must be declared by the agent environment: +Capabilities that exist only in the host may also be declared by the agent environment: ```text SHIPPING_GITHUB_HOST=codex|cursor|claude|unknown @@ -40,9 +41,9 @@ SHIPPING_GITHUB_PROGRESS_WATCHDOG=none|hooks|stream `CONNECTOR_WRITE` means the host connector has write permission. `BROKERED_CONNECTOR_WRITE` means the github-delivery mutation broker has an adapter that enforces the same request, expected-head, idempotency, exact-text, audit, and verification contract through that connector. Permission without an adapter is not a usable mutation path. -`PROGRESS_WATCHDOG=hooks` declares lifecycle-hook enforcement. `stream` declares a streaming host boundary capable of interrupting an in-flight no-progress turn. See `references/agent-progress-watchdog.md` for the Codex integrations and their different guarantees. +For the progress watchdog, an explicit environment declaration is useful for controlled host integrations and fixtures. When it is absent, runtime discovery reads `~/.codex/github-delivery/watchdog-activation.json` (or the equivalent under `CODEX_HOME`). Invalid or missing activation state never upgrades capability. -A Node process cannot discover a host connector or streaming interception boundary that was never exposed to it. These capabilities are declarations, not guesses. +`hooks` means lifecycle-hook enforcement is active. `stream` means the host has a verified launch boundary capable of interrupting an in-flight no-progress turn. `none` means policy-only protection. See `references/agent-progress-watchdog.md` for their different guarantees. ## Output contract @@ -67,7 +68,10 @@ A Node process cannot discover a host connector or streaming interception bounda "reviewThreadsReadable": true }, "runtime": { - "progressWatchdog": "stream" + "progressWatchdog": "stream", + "progressWatchdogAvailable": true, + "progressWatchdogDegradationReason": null, + "progressWatchdogLauncherPath": "/path/to/github-delivery/scripts/codex-with-watchdog.mjs" }, "fallbacks": { "githubReads": "connector", @@ -89,12 +93,14 @@ A Node process cannot discover a host connector or streaming interception bounda - Connected GitHub reads are preferred when declared; authenticated `gh` is the fallback. - A connected write path is usable only when a broker adapter is declared. Otherwise authenticated writable `gh` is used through `github-mutate.mjs`. - The write fallback is reported as `connector-broker`, `gh-broker`, or `unavailable`. -- When `gh` is authenticated but no repository could be detected, read/write fallbacks are reported as **`unprobed`** with `github_repo_not_detected` in `degraded` — that is a probe gap, not a permission denial. Do **not** treat `unprobed` as evidence that writes fail; re-run with `--repo OWNER/REPO` or rely on direct evidence (authenticated `gh`, successful broker execution) before claiming the write path is blocked. +- When `gh` is authenticated but no repository could be detected, read/write fallbacks are `unprobed` with `github_repo_not_detected` in `degraded`. Re-run with `--repo OWNER/REPO` or use direct evidence before claiming the write path is blocked. - Raw connector write permission without a broker adapter produces `github_write_not_brokered` and cannot make the workflow mutation-ready. - Composio rate-limit checks are preferred when declared; authenticated `gh` is the fallback. - Bugbot is used only on Cursor when both host and capability declarations permit it. Every other host uses complementary lenses. - Subagents are used only when declared. Otherwise run the work in-session without claiming fan-out occurred. - `runtime.progressWatchdog` is `stream`, `hooks`, or `none`; the corresponding `contextEconomy` fallback is `streaming-watchdog`, `lifecycle-hooks`, or `policy-only`. +- `progressWatchdogDegradationReason` makes hook-only or unavailable protection visible instead of letting the workflow assume streaming enforcement. +- `progress_watchdog_unavailable` is included in `degraded` when no runtime watchdog is active. - Missing ruleset or review-thread evidence is degraded capability and must flow into an unknown gate result rather than being guessed. ## Offline fixtures @@ -105,7 +111,7 @@ Tests and incident reproduction can supply probe data without invoking any exter node scripts/runtime-capabilities.mjs --input capability-fixture.json ``` -This mode is safe with an empty `PATH` and is used by CI. +This mode is safe with an empty `PATH` and is used by CI. Explicit fixture declarations can override persisted activation for controlled tests. ## Natural-language example @@ -116,4 +122,4 @@ For `merge PR #32`, the agent: 3. stops if no reliable GitHub read or brokered write path exists; 4. runs the authoritative gate; 5. executes broker mutations through `connector-broker` or `gh-broker`; -6. reports degraded fallbacks when the ideal tool was unavailable. +6. reports degraded fallbacks, including watchdog degradation, when the ideal runtime surface was unavailable. From dcf6d5a599c20c8164f871b27e4334befff04e27 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:41:37 +0200 Subject: [PATCH 20/48] fix: make protected launcher cleanup fail closed --- scripts/codex-with-watchdog.mjs | 46 +++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/scripts/codex-with-watchdog.mjs b/scripts/codex-with-watchdog.mjs index df1e8ef..918bcca 100644 --- a/scripts/codex-with-watchdog.mjs +++ b/scripts/codex-with-watchdog.mjs @@ -7,6 +7,13 @@ import { startCodexWatchdogRemoteBridge } from "./lib/codex-watchdog-remote-brid const TOKEN_ENV = "GITHUB_DELIVERY_CODEX_REMOTE_TOKEN"; +function waitForSpawn(child) { + return new Promise((resolve, reject) => { + child.once("spawn", resolve); + child.once("error", reject); + }); +} + function waitForExit(child) { return new Promise((resolve, reject) => { child.once("error", reject); @@ -14,13 +21,17 @@ function waitForExit(child) { }); } -export function protectedClientArgs(args, url) { +export function validateProtectedClientArgs(args) { if (args.some((arg) => arg === "--remote" || arg.startsWith("--remote="))) { throw new Error("protected Codex launcher owns --remote; remove the caller-supplied remote endpoint"); } if (args.some((arg) => arg === "--remote-auth-token-env" || arg.startsWith("--remote-auth-token-env="))) { throw new Error("protected Codex launcher owns --remote-auth-token-env"); } +} + +export function protectedClientArgs(args, url) { + validateProtectedClientArgs(args); return ["--remote", url, "--remote-auth-token-env", TOKEN_ENV, ...args]; } @@ -31,15 +42,14 @@ export async function runProtectedCodex({ spawnImpl = spawn, stderr = process.stderr, } = {}) { + validateProtectedClientArgs(args); const token = randomBytes(32).toString("base64url"); const appServer = spawnImpl(codexBin, ["app-server"], { stdio: ["pipe", "pipe", "inherit"], windowsHide: true, env, }); - appServer.once("error", (error) => { - stderr.write(`github-delivery watchdog app-server error: ${error?.message || error}\n`); - }); + await waitForSpawn(appServer); let bridge; try { @@ -54,14 +64,23 @@ export async function runProtectedCodex({ } const clientEnv = { ...env, [TOKEN_ENV]: token }; - const client = spawnImpl(codexBin, protectedClientArgs(args, bridge.url), { - stdio: "inherit", - windowsHide: true, - env: clientEnv, - }); + let client; + try { + client = spawnImpl(codexBin, protectedClientArgs(args, bridge.url), { + stdio: "inherit", + windowsHide: true, + env: clientEnv, + }); + await waitForSpawn(client); + } catch (error) { + if (client && !client.killed) client.kill(); + if (!appServer.killed) appServer.kill(); + await bridge.close().catch(() => {}); + throw error; + } const cleanup = async () => { - if (!client.killed) client.kill(); + if (client && !client.killed) client.kill(); if (!appServer.killed) appServer.kill(); await bridge.close().catch(() => {}); }; @@ -74,11 +93,10 @@ export async function runProtectedCodex({ } try { - const outcome = await waitForExit(client); - if (!appServer.killed) appServer.kill(); - await bridge.close(); - return outcome; + return await waitForExit(client); } finally { + if (!appServer.killed) appServer.kill(); + await bridge.close().catch(() => {}); for (const [signal, handler] of handlers) process.off(signal, handler); } } From ad5196f03e57f244deabf71f026cf66f75b0a54a Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:42:19 +0200 Subject: [PATCH 21/48] fix: preflight Codex hook activation before install --- scripts/install-skill.mjs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/install-skill.mjs b/scripts/install-skill.mjs index cf6b3b1..a2c0512 100755 --- a/scripts/install-skill.mjs +++ b/scripts/install-skill.mjs @@ -11,12 +11,17 @@ import { writeActivationReceipt, } from "./lib/watchdog-activation.mjs"; +function parseBoolean(value) { + return ["1", "true", "yes", "on"].includes(String(value || "").toLowerCase()); +} + function defaultCodexHome() { return resolve(process.env.CODEX_HOME || join(homedir(), ".codex")); } function inferHost(codexHome) { - return process.env.CODEX_HOME || existsSync(codexHome) ? "codex" : "unknown"; + return process.env.SHIPPING_GITHUB_HOST || + (process.env.CODEX_HOME || existsSync(codexHome) ? "codex" : "unknown"); } export function parseInstallArgs(argv) { @@ -32,7 +37,9 @@ export function parseInstallArgs(argv) { codexHome, host: inferHost(codexHome), lifecycleHooksSupported: undefined, - streamLaunchControlled: false, + streamLaunchControlled: parseBoolean( + process.env.SHIPPING_GITHUB_STREAM_LAUNCH_CONTROLLED, + ), }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; @@ -72,6 +79,16 @@ export function installSkill(options) { const installedLauncherPath = join(options.target, "scripts", "codex-with-watchdog.mjs"); const launcherBundled = existsSync(sourceLauncherPath); const streamLaunchVerified = options.streamLaunchControlled === true && launcherBundled; + const hooksPath = join(options.codexHome, "hooks.json"); + + let hookPlan = null; + if (options.host === "codex" && options.lifecycleHooksSupported) { + hookPlan = installCodexWatchdogHooks({ + hooksPath, + skillDir: options.target, + apply: false, + }); + } const installation = options.apply ? applyInstallation(options) @@ -90,12 +107,12 @@ export function installSkill(options) { selection.degradationReason = "stream_launcher_unavailable"; } - let hookResult = null; - if (options.host === "codex" && options.lifecycleHooksSupported) { + let hookResult = hookPlan; + if (options.apply && hookPlan) { hookResult = installCodexWatchdogHooks({ - hooksPath: join(options.codexHome, "hooks.json"), + hooksPath, skillDir: options.target, - apply: options.apply, + apply: true, }); } From ec09de80297320b99ffb9a014f42a03325e41be5 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:45:12 +0200 Subject: [PATCH 22/48] fix: require verified hook trust for active mode --- scripts/lib/watchdog-activation.mjs | 34 +++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/scripts/lib/watchdog-activation.mjs b/scripts/lib/watchdog-activation.mjs index 373e77d..566caa6 100644 --- a/scripts/lib/watchdog-activation.mjs +++ b/scripts/lib/watchdog-activation.mjs @@ -12,13 +12,17 @@ export function selectWatchdogMode({ host = "unknown", streamLaunchControlled = false, lifecycleHooksSupported = false, + hookTrustVerified = false, } = {}) { if (host === "codex" && streamLaunchControlled === true) { return { mode: "stream", degradationReason: null }; } - if (host === "codex" && lifecycleHooksSupported === true) { + if (host === "codex" && lifecycleHooksSupported === true && hookTrustVerified === true) { return { mode: "hooks", degradationReason: "streaming_interruption_unavailable" }; } + if (host === "codex" && lifecycleHooksSupported === true) { + return { mode: "none", degradationReason: "hook_trust_required" }; + } return { mode: "none", degradationReason: "progress_watchdog_unavailable" }; } @@ -32,11 +36,15 @@ function normalizeReceipt(value) { if (value.schemaVersion !== 1 || !MODES.has(value.mode)) return null; if (value.degradationReason !== null && typeof value.degradationReason !== "string") return null; if (value.launcherPath !== null && typeof value.launcherPath !== "string") return null; + if (value.hooksConfigured !== undefined && typeof value.hooksConfigured !== "boolean") return null; + if (value.hookTrustVerified !== undefined && typeof value.hookTrustVerified !== "boolean") return null; return { schemaVersion: 1, mode: value.mode, degradationReason: value.degradationReason, launcherPath: value.launcherPath, + hooksConfigured: value.hooksConfigured === true, + hookTrustVerified: value.hookTrustVerified === true, updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : null, }; } @@ -51,7 +59,13 @@ export function readActivationReceipt({ codexHome } = {}) { } } -function semanticReceipt({ mode, degradationReason = null, launcherPath = null }) { +function semanticReceipt({ + mode, + degradationReason = null, + launcherPath = null, + hooksConfigured = false, + hookTrustVerified = false, +}) { if (!MODES.has(mode)) throw new Error(`invalid watchdog mode: ${mode}`); if (degradationReason !== null && typeof degradationReason !== "string") { throw new Error("degradationReason must be a string or null"); @@ -64,6 +78,8 @@ function semanticReceipt({ mode, degradationReason = null, launcherPath = null } mode, degradationReason, launcherPath: launcherPath ? resolve(launcherPath) : null, + hooksConfigured: hooksConfigured === true, + hookTrustVerified: hookTrustVerified === true, }; } @@ -74,7 +90,9 @@ function sameSemantics(left, right) { left.schemaVersion === right.schemaVersion && left.mode === right.mode && left.degradationReason === right.degradationReason && - left.launcherPath === right.launcherPath, + left.launcherPath === right.launcherPath && + left.hooksConfigured === right.hooksConfigured && + left.hookTrustVerified === right.hookTrustVerified, ); } @@ -83,11 +101,19 @@ export function writeActivationReceipt({ mode, degradationReason = null, launcherPath = null, + hooksConfigured = false, + hookTrustVerified = false, apply = false, now = () => new Date(), } = {}) { const path = activationReceiptPath({ codexHome }); - const desired = semanticReceipt({ mode, degradationReason, launcherPath }); + const desired = semanticReceipt({ + mode, + degradationReason, + launcherPath, + hooksConfigured, + hookTrustVerified, + }); const existing = readActivationReceipt({ codexHome }); const changed = !sameSemantics(existing, desired); const receipt = changed From 2824afe5f86687c5ebbc766f1c7fb2d874b044d7 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:45:58 +0200 Subject: [PATCH 23/48] fix: report only trusted lifecycle hooks as active --- scripts/install-skill.mjs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/scripts/install-skill.mjs b/scripts/install-skill.mjs index a2c0512..94d695d 100755 --- a/scripts/install-skill.mjs +++ b/scripts/install-skill.mjs @@ -37,6 +37,7 @@ export function parseInstallArgs(argv) { codexHome, host: inferHost(codexHome), lifecycleHooksSupported: undefined, + hookTrustVerified: parseBoolean(process.env.SHIPPING_GITHUB_HOOK_TRUST_VERIFIED), streamLaunchControlled: parseBoolean( process.env.SHIPPING_GITHUB_STREAM_LAUNCH_CONTROLLED, ), @@ -51,6 +52,7 @@ export function parseInstallArgs(argv) { else if (arg === "--host") options.host = argv[++index]; else if (arg === "--lifecycle-hooks-supported") options.lifecycleHooksSupported = true; else if (arg === "--no-lifecycle-hooks") options.lifecycleHooksSupported = false; + else if (arg === "--hook-trust-verified") options.hookTrustVerified = true; else if (arg === "--stream-launch-controlled") options.streamLaunchControlled = true; else if (arg === "--apply") options.apply = true; else if (arg === "--allow-downgrade") options.allowDowngrade = true; @@ -98,15 +100,6 @@ export function installSkill(options) { throw new Error("protected Codex launcher was not installed with the skill payload"); } - const selection = selectWatchdogMode({ - host: options.host, - streamLaunchControlled: streamLaunchVerified, - lifecycleHooksSupported: options.lifecycleHooksSupported, - }); - if (options.streamLaunchControlled && !launcherBundled && selection.mode !== "stream") { - selection.degradationReason = "stream_launcher_unavailable"; - } - let hookResult = hookPlan; if (options.apply && hookPlan) { hookResult = installCodexWatchdogHooks({ @@ -116,12 +109,30 @@ export function installSkill(options) { }); } + const hooksConfigured = Boolean(hookPlan); + const hookDefinitionChanged = Boolean(hookPlan?.wouldChange || hookResult?.applied); + const hookTrustVerified = Boolean( + hooksConfigured && options.hookTrustVerified === true && !hookDefinitionChanged, + ); + + const selection = selectWatchdogMode({ + host: options.host, + streamLaunchControlled: streamLaunchVerified, + lifecycleHooksSupported: options.lifecycleHooksSupported, + hookTrustVerified, + }); + if (options.streamLaunchControlled && !launcherBundled && selection.mode !== "stream") { + selection.degradationReason = "stream_launcher_unavailable"; + } + const launcherPath = selection.mode === "stream" ? installedLauncherPath : null; const receiptResult = writeActivationReceipt({ codexHome: options.codexHome, mode: selection.mode, degradationReason: selection.degradationReason, launcherPath, + hooksConfigured, + hookTrustVerified, apply: options.apply, }); @@ -132,6 +143,9 @@ export function installSkill(options) { degradationReason: selection.degradationReason, receiptPath: receiptResult.path, receiptChanged: receiptResult.changed, + hooksConfigured, + hookTrustVerified, + hookTrustRequired: hooksConfigured && !hookTrustVerified, hookResult, launcherPath, streamLauncherPath: launcherBundled ? installedLauncherPath : null, From be0bc27e92807370cf59b849e5e2f8e4c21a4081 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:46:40 +0200 Subject: [PATCH 24/48] test: model Codex hook trust explicitly --- tests/unit/watchdog-activation.test.mjs | 47 ++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/unit/watchdog-activation.test.mjs b/tests/unit/watchdog-activation.test.mjs index 812ccc3..40a1d9a 100644 --- a/tests/unit/watchdog-activation.test.mjs +++ b/tests/unit/watchdog-activation.test.mjs @@ -43,14 +43,17 @@ function runInstall(f, extra = []) { ); } -test("normal Codex install activates lifecycle watchdog without a second installer", () => { +test("normal Codex install configures hooks but does not falsely claim untrusted hooks are active", () => { const f = fixture(); const result = runInstall(f, ["--lifecycle-hooks-supported", "--apply"]); assert.equal(result.status, 0, result.stderr); const receipt = JSON.parse(result.stdout); - assert.equal(receipt.watchdog.mode, "hooks"); - assert.equal(receipt.watchdog.degradationReason, "streaming_interruption_unavailable"); + assert.equal(receipt.watchdog.mode, "none"); + assert.equal(receipt.watchdog.degradationReason, "hook_trust_required"); + assert.equal(receipt.watchdog.hooksConfigured, true); + assert.equal(receipt.watchdog.hookTrustVerified, false); + assert.equal(receipt.watchdog.hookTrustRequired, true); assert.equal(readFileSync(join(f.target, "marker.txt"), "utf8"), "installed\n"); const hooksPath = join(f.codexHome, "hooks.json"); @@ -62,18 +65,52 @@ test("normal Codex install activates lifecycle watchdog without a second install assert.equal(commands.filter((command) => command.includes("codex-watchdog-hook.mjs")).length, 1); } - assert.equal(existsSync(join(f.codexHome, "github-delivery", "watchdog-activation.json")), true); + const persisted = JSON.parse( + readFileSync(join(f.codexHome, "github-delivery", "watchdog-activation.json"), "utf8"), + ); + assert.equal(persisted.mode, "none"); + assert.equal(persisted.hooksConfigured, true); + assert.equal(persisted.hookTrustVerified, false); assert.equal( receipt.watchdog.streamLauncherPath, join(f.target, "scripts", "codex-with-watchdog.mjs"), ); }); +test("verified unchanged hook definitions may be reported as active after user trust", () => { + const f = fixture(); + const first = runInstall(f, ["--lifecycle-hooks-supported", "--apply"]); + assert.equal(first.status, 0, first.stderr); + + const afterTrust = runInstall(f, [ + "--lifecycle-hooks-supported", + "--hook-trust-verified", + ]); + assert.equal(afterTrust.status, 0, afterTrust.stderr); + const result = JSON.parse(afterTrust.stdout); + assert.equal(result.watchdog.mode, "hooks"); + assert.equal(result.watchdog.degradationReason, "streaming_interruption_unavailable"); + assert.equal(result.watchdog.hookTrustVerified, true); +}); + +test("a hook definition change invalidates a claimed trust state", () => { + const f = fixture(); + const result = runInstall(f, [ + "--lifecycle-hooks-supported", + "--hook-trust-verified", + ]); + assert.equal(result.status, 0, result.stderr); + const planned = JSON.parse(result.stdout); + assert.equal(planned.watchdog.mode, "none"); + assert.equal(planned.watchdog.degradationReason, "hook_trust_required"); + assert.equal(planned.watchdog.hookTrustVerified, false); +}); + test("stream mode is selected only when the installed launch boundary is explicitly controllable", () => { const f = fixture(); const withoutBoundary = runInstall(f, ["--lifecycle-hooks-supported"]); assert.equal(withoutBoundary.status, 0, withoutBoundary.stderr); - assert.equal(JSON.parse(withoutBoundary.stdout).watchdog.mode, "hooks"); + assert.equal(JSON.parse(withoutBoundary.stdout).watchdog.mode, "none"); const withBoundary = runInstall(f, [ "--lifecycle-hooks-supported", From 3806437584844245c8952af8af9d9921eae1d3a4 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:47:07 +0200 Subject: [PATCH 25/48] test: require verified hook trust in activation planner --- .../watchdog-activation-contract.test.mjs | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/tests/unit/watchdog-activation-contract.test.mjs b/tests/unit/watchdog-activation-contract.test.mjs index f411337..165fba1 100644 --- a/tests/unit/watchdog-activation-contract.test.mjs +++ b/tests/unit/watchdog-activation-contract.test.mjs @@ -11,15 +11,34 @@ import { writeActivationReceipt, } from "../../scripts/lib/watchdog-activation.mjs"; -test("watchdog selection prefers stream, then hooks, then none", () => { +test("watchdog selection prefers stream, then trusted hooks, then truthful degradation", () => { assert.deepEqual( - selectWatchdogMode({ host: "codex", streamLaunchControlled: true, lifecycleHooksSupported: true }), + selectWatchdogMode({ + host: "codex", + streamLaunchControlled: true, + lifecycleHooksSupported: true, + hookTrustVerified: false, + }), { mode: "stream", degradationReason: null }, ); assert.deepEqual( - selectWatchdogMode({ host: "codex", streamLaunchControlled: false, lifecycleHooksSupported: true }), + selectWatchdogMode({ + host: "codex", + streamLaunchControlled: false, + lifecycleHooksSupported: true, + hookTrustVerified: true, + }), { mode: "hooks", degradationReason: "streaming_interruption_unavailable" }, ); + assert.deepEqual( + selectWatchdogMode({ + host: "codex", + streamLaunchControlled: false, + lifecycleHooksSupported: true, + hookTrustVerified: false, + }), + { mode: "none", degradationReason: "hook_trust_required" }, + ); assert.deepEqual( selectWatchdogMode({ host: "unknown", streamLaunchControlled: false, lifecycleHooksSupported: false }), { mode: "none", degradationReason: "progress_watchdog_unavailable" }, @@ -30,34 +49,29 @@ test("activation receipt is dry-run safe, non-sensitive, and idempotent", () => const codexHome = mkdtempSync(join(tmpdir(), "gd-activation-receipt-")); const path = activationReceiptPath({ codexHome }); const clock = () => new Date("2026-08-11T06:30:00.000Z"); - - const planned = writeActivationReceipt({ + const desired = { codexHome, mode: "hooks", degradationReason: "streaming_interruption_unavailable", - apply: false, - now: clock, - }); + hooksConfigured: true, + hookTrustVerified: true, + }; + + const planned = writeActivationReceipt({ ...desired, apply: false, now: clock }); assert.equal(planned.changed, true); assert.equal(planned.applied, false); assert.equal(existsSync(path), false); - const applied = writeActivationReceipt({ - codexHome, - mode: "hooks", - degradationReason: "streaming_interruption_unavailable", - apply: true, - now: clock, - }); + const applied = writeActivationReceipt({ ...desired, apply: true, now: clock }); assert.equal(applied.applied, true); const raw = readFileSync(path, "utf8"); assert.doesNotMatch(raw, /prompt|conversation|toolInput|tool_input/i); assert.deepEqual(readActivationReceipt({ codexHome }), applied.receipt); + assert.equal(applied.receipt.hooksConfigured, true); + assert.equal(applied.receipt.hookTrustVerified, true); const repeated = writeActivationReceipt({ - codexHome, - mode: "hooks", - degradationReason: "streaming_interruption_unavailable", + ...desired, apply: true, now: () => new Date("2026-08-11T07:30:00.000Z"), }); From 089d2015b86dbdef374df72adc36b7dc357f9a94 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:48:06 +0200 Subject: [PATCH 26/48] docs: account for Codex hook trust boundary --- .../2026-08-11-watchdog-activation-design.md | 118 +++++++++++------- 1 file changed, 71 insertions(+), 47 deletions(-) diff --git a/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md b/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md index 5a137bb..bd5dbab 100644 --- a/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md +++ b/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md @@ -4,13 +4,13 @@ `github-delivery` v0.2.0 contains a runtime progress watchdog, Codex lifecycle-hook integration, and a Codex App Server streaming proxy, but the normal skill installer does not activate either runtime boundary. Users can therefore install or update the skill successfully and still observe the exact failure the watchdog was built to stop: hundreds of repeated assistant-intent lines such as `Let me check the type` before any tool call occurs. -The strongest detector already exists. The missing work is activation, capability selection, and end-to-end proof that a supported Codex install actually uses the strongest boundary available. +The strongest detector already exists. The missing work is activation, capability selection, and end-to-end proof that a supported Codex launch actually uses the strongest boundary available. ## Goal -Make progress protection effective by default on supported Codex installations without weakening mutation, freshness, review, or evidence gates and without silently rewriting unsupported host configuration. +Make progress protection effective on supported Codex installations without weakening mutation, freshness, review, evidence, or Codex hook-trust gates and without silently rewriting unsupported host configuration. -Success means a normal supported install/upgrade chooses and activates the strongest watchdog mode it can safely use, records the resulting mode, and an end-to-end incident regression proves that repeated low-novelty narration is interrupted before the configured output budget is exceeded. +Success means a normal Codex install/upgrade configures the available watchdog surfaces, records only the protection that is actually active, and an end-to-end incident regression proves that repeated low-novelty narration is interrupted before the configured output budget is exceeded when the protected streaming launcher is used. ## Non-goals @@ -19,6 +19,7 @@ Success means a normal supported install/upgrade chooses and activates the stron - Do not require App Server streaming on hosts that cannot expose it. - Do not replace Codex itself or assume an undocumented host interception API. - Do not silently alter unrelated user hooks or editor configuration. +- Do not bypass Codex's persisted hook-trust review by default. ## Approaches considered @@ -26,19 +27,19 @@ Success means a normal supported install/upgrade chooses and activates the stron Keep hook/proxy installation separate and improve README/INSTALL instructions. -Rejected because it preserves the current failure mode: the protection can exist but remain inactive after a successful install. +Rejected because it preserves the current failure mode: the protection can exist but remain unconfigured after a successful install. ### B. Always install lifecycle hooks -Make `install-skill.mjs` install `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd` hooks automatically for Codex targets. +Make `install-skill.mjs` configure `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd` hooks automatically for Codex targets. -Better than today, but still cannot stop tokens already emitted inside a single assistant message. It solves tool-boundary waste but not the incident that motivated this PR. +Better than v0.2.0, but still cannot stop tokens already emitted inside a single assistant message. In addition, Codex requires non-managed command hooks to be reviewed and trusted by exact hook hash before they run. Configuration therefore must not be confused with active enforcement. ### C. Capability-driven activation with strongest-safe mode -Recommended. During install/upgrade, detect the target host and available watchdog integration surface, select `stream` when an App Server launch boundary is actually controllable, otherwise select `hooks` when Codex lifecycle hooks are supported, otherwise record `none` and surface the degradation clearly. Installation remains idempotent and preserves unrelated configuration. +Recommended. During install/upgrade, detect the target host and available watchdog integration surface, select `stream` only when a protected App Server launch boundary is actually controlled, otherwise select `hooks` only when lifecycle hooks are configured and their exact current definition is confirmed trusted, otherwise record `none` with a concrete degradation reason. -This is the only approach that both fixes the activation trap and keeps unsupported hosts safe. +A fresh Codex install may therefore configure hooks automatically while reporting `none / hook_trust_required` until the user reviews them in `/hooks`. This preserves Codex's trust model instead of silently weakening it. ## Architecture @@ -50,65 +51,85 @@ Add a deterministic activation planner with one input contract: - host hint/detection result; - available integration capabilities; - existing Codex hook configuration; +- whether the exact current hook definition is confirmed trusted; - whether the caller requested apply vs dry-run. It returns one of: -- `stream` — the caller/host can launch Codex through the watchdog App Server proxy; -- `hooks` — lifecycle hooks can be safely installed and used; -- `none` — no enforceable runtime boundary is available. +- `stream` — the caller/host can launch Codex through the protected streaming boundary; +- `hooks` — lifecycle hooks are configured and their exact current definition is confirmed trusted; +- `none` — no verified runtime boundary is active. The planner must never infer `stream` merely because `codex` exists. Streaming is selected only when the actual launch boundary is under installer/host control. +The planner must never infer `hooks` merely because `hooks.json` contains the command. Codex skips non-managed hooks until their exact definition is trusted. A hook configuration change invalidates any prior trust assertion for activation purposes. + ### 2. Installer integration `install-skill.mjs` remains dry-run by default and continues to own skill files/backups. For Codex targets it also produces a watchdog activation plan. On `--apply`: -- install/upgrade the skill first; -- activate the selected watchdog mode; -- preserve unrelated hooks/configuration; -- back up any modified host configuration before writing; -- emit one structured result containing the installed version, watchdog mode, changed files/configuration, and any degradation reason. +- preflight the hook configuration before replacing the skill; +- install/upgrade the skill; +- configure the GitHub Delivery lifecycle-hook entries while preserving unrelated hooks; +- back up modified hook configuration before writing; +- record only a verified active mode; +- emit one structured result containing the installed version, watchdog mode, configured/trusted hook state, launcher path, and any degradation reason. + +The existing standalone hook installer remains available for recovery/manual use but is no longer the normal required installation path. -The existing standalone hook installer remains available for recovery/manual use but is no longer the normal required path after a Codex install. +Because Codex records trust against the exact hook definition hash, a newly added or changed hook is reported as `hook_trust_required` until it is reviewed in `/hooks`. The installer does not use `--dangerously-bypass-hook-trust` as a default activation mechanism. ### 3. Streaming launcher integration -When `stream` is selected, installation must create or update a stable launcher/adapter owned by GitHub Delivery rather than asking the user to remember a different command. The launcher delegates to `scripts/codex-app-server-watchdog-proxy.mjs` and is referenced by the supported Codex/App Server integration point. +GitHub Delivery installs a protected launcher owned by the skill. It starts the real Codex App Server on the default stdio transport, exposes an authenticated loopback bridge, and starts the ordinary Codex client with its documented `--remote` flags pointed at that bridge. + +The bridge observes `item/agentMessage/delta` notifications and may issue one private `turn/interrupt` while the repeated narration is still being generated. -If the current Codex surface does not provide a safe way for the installer to replace or configure that launch boundary, the planner must choose `hooks`, not pretend that streaming is active. +Installing the launcher alone does not make plain `codex` or an IDE session use it. `stream` is recorded only when the caller/host explicitly controls launches through this protected entry point. This avoids claiming a mid-message boundary that is not actually in the traffic path. ### 4. Runtime capability truth -`runtime-capabilities.mjs` must report the mode actually activated by installation rather than relying only on an operator-set environment variable. Environment declarations may override or assist probing for controlled CI/fixtures, but a successful normal install should leave machine-readable activation state that runtime inspection can verify. +`runtime-capabilities.mjs` reports the mode recorded by installation rather than relying only on an operator-set environment variable. Environment declarations may override or assist probing for controlled host integrations and fixtures, but a normal install leaves machine-readable activation state under the active Codex home. -Activation state must contain no secrets, prompts, raw tool inputs, or conversation content. +Activation state contains no secrets, prompts, raw tool inputs, or conversation content. It may record: + +- mode; +- degradation reason; +- protected launcher path; +- whether hooks are configured; +- whether trust for the exact current hook definition was verified. ### 5. Visible degradation -If only `hooks` is available, the install result must explicitly say that tool-boundary protection is active but in-turn narration cannot be interrupted until `Stop`. +If hooks are configured but not confirmed trusted, the mode is `none` and the result reports `hook_trust_required` plus `hooksConfigured: true`. + +If trusted hooks are active but streaming is not, the mode is `hooks` with `streaming_interruption_unavailable`. -If mode is `none`, installation must not fail solely because the host lacks a watchdog surface, but it must clearly report `progress_watchdog_unavailable` so users are not told they are protected when they are not. +If no runtime surface is available, installation does not fail solely for that reason but reports `progress_watchdog_unavailable`. ## Data flow 1. User runs the normal skill installer/upgrade. -2. Installer plans skill replacement and watchdog activation together. -3. Host/capability probe selects `stream`, `hooks`, or `none`. -4. Dry-run reports exactly what would change. -5. `--apply` installs the skill and applies only the selected supported activation. -6. Runtime capability inspection reads the persisted activation receipt and confirms the effective mode. -7. A workflow uses the watchdog through that boundary without requiring the user to remember a second installer command. +2. Installer preflights skill replacement and watchdog configuration together. +3. The install configures available Codex lifecycle hooks without bypassing Codex trust review. +4. Host/capability evidence selects `stream`, trusted `hooks`, or `none`. +5. Dry-run reports exactly what would change. +6. `--apply` writes the skill, safe hook configuration, and a truthful activation receipt. +7. Runtime capability inspection reads the persisted receipt and confirms the effective mode. +8. The strongest in-flight protection is obtained by actually launching Codex through the protected streaming entry point. ## Safety and failure handling -- Host configuration writes remain atomic, backup-first, and idempotent. -- Malformed or symlinked host configuration fails closed before modification. -- A partial activation failure must not claim the requested watchdog mode. The result records the lower verified mode or `none`. -- Hook installation must preserve all unrelated hook entries. -- Streaming launch integration must never swallow ordinary App Server traffic, mutation prompts, or errors. +- Host configuration writes remain backup-first and idempotent. +- Malformed or symlinked host configuration fails closed before skill replacement when it can be preflighted. +- A partial activation failure must not claim the requested watchdog mode. +- Hook installation preserves all unrelated hook entries. +- Non-managed hooks are never reported active solely because they are configured. +- A changed hook definition invalidates a supplied trust assertion for that install pass. +- The default workflow never adds `--dangerously-bypass-hook-trust`. +- Streaming launch integration never swallows ordinary App Server traffic, mutation prompts, or errors. - The watchdog remains incapable of granting GitHub write authority. - Existing `GD-CORE-*`, `GD-AUTH-*`, CI, review, security, and final-evidence rules remain authoritative. @@ -116,28 +137,31 @@ If mode is `none`, installation must not fail solely because the host lacks a wa ### RED regression first -Add an end-to-end installation regression that installs to an isolated temporary Codex home, runs the normal installer, and asserts the expected activation mode without invoking the standalone hook installer. +Add an end-to-end installation regression that installs to an isolated temporary Codex home and asserts that the normal installer configures lifecycle hooks without invoking the standalone installer. A fresh configuration must **not** be reported as active until hook trust is confirmed. -Add an incident regression using the real repeated phrase family from the observed trace (`Let me check the type`, `Let me check the NOUS_DEF type`, etc.). Under the `stream` adapter, the generated assistant deltas must trigger one `turn/interrupt` before the configured character budget is exceeded. +Add an incident regression using the real repeated phrase family from the observed trace (`Let me check the type`, `Let me check the NOUS_DEF type`, etc.). Under the protected streaming adapter, generated assistant deltas must trigger one `turn/interrupt` before the configured character budget is exceeded. ### Additional contracts -- normal Codex install activates the strongest supported mode automatically; +- normal Codex install configures lifecycle hooks automatically; +- newly configured non-managed hooks report `hook_trust_required`, not `hooks`; +- an explicit trust assertion is accepted only when the hook definition is unchanged; +- changing the hook definition invalidates the trust assertion; - upgrade from v0.2.0 does not duplicate hook entries; -- existing unrelated hooks survive byte-for-semantic-content unchanged; +- existing unrelated hooks survive semantic-content unchanged; - malformed/symlinked config fails closed; -- `hooks` is selected when streaming cannot be controlled; +- `stream` is selected only when the protected launch boundary is controlled; - `none` is reported honestly on unsupported hosts; -- runtime capability output matches the persisted effective mode; +- runtime capability output matches persisted effective mode; - repeated install is idempotent; -- uninstall/restore does not leave a false `stream`/`hooks` receipt; - existing repository `npm run check`, distribution reproducibility, security, and cross-platform CI remain green. ## Acceptance criteria -1. A standard supported Codex install/upgrade no longer requires a second manual watchdog-install command to obtain available protection. -2. The installer chooses the strongest mode it can prove: `stream` > `hooks` > `none`. -3. The exact repeated `Let me check...` incident is interrupted in streaming mode before the configured generation budget is exceeded. -4. Hook-only installations clearly disclose that in-turn token burn cannot be stopped mid-message. -5. Runtime capability reporting reflects the mode actually installed. -6. Existing user configuration and GitHub Delivery safety/authority gates are preserved. +1. A standard Codex install/upgrade no longer requires a second watchdog-install command to configure available lifecycle protection. +2. Codex's non-managed hook trust review remains intact; untrusted hooks are never reported active. +3. The installer chooses the strongest mode it can prove: controlled `stream` > trusted `hooks` > `none`. +4. The exact repeated `Let me check...` incident is interrupted in streaming mode before the configured generation budget is exceeded. +5. Hook-only installations clearly disclose that in-turn token burn cannot be stopped mid-message. +6. Runtime capability reporting reflects the mode actually active, not merely configured code. +7. Existing user configuration and GitHub Delivery safety/authority gates are preserved. From 8b9a4335eb7c19b873b490b06237429558f675e5 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:50:19 +0200 Subject: [PATCH 27/48] fix: declare stream capability inside protected session --- scripts/codex-with-watchdog.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/codex-with-watchdog.mjs b/scripts/codex-with-watchdog.mjs index 918bcca..7e8540c 100644 --- a/scripts/codex-with-watchdog.mjs +++ b/scripts/codex-with-watchdog.mjs @@ -35,6 +35,15 @@ export function protectedClientArgs(args, url) { return ["--remote", url, "--remote-auth-token-env", TOKEN_ENV, ...args]; } +export function protectedRuntimeEnv(env = process.env) { + return { + ...env, + SHIPPING_GITHUB_HOST: "codex", + SHIPPING_GITHUB_PROGRESS_WATCHDOG: "stream", + SHIPPING_GITHUB_STREAM_LAUNCH_CONTROLLED: "true", + }; +} + export async function runProtectedCodex({ codexBin = process.env.CODEX_BIN || "codex", args = process.argv.slice(2), @@ -44,10 +53,11 @@ export async function runProtectedCodex({ } = {}) { validateProtectedClientArgs(args); const token = randomBytes(32).toString("base64url"); + const runtimeEnv = protectedRuntimeEnv(env); const appServer = spawnImpl(codexBin, ["app-server"], { stdio: ["pipe", "pipe", "inherit"], windowsHide: true, - env, + env: runtimeEnv, }); await waitForSpawn(appServer); @@ -63,7 +73,7 @@ export async function runProtectedCodex({ throw error; } - const clientEnv = { ...env, [TOKEN_ENV]: token }; + const clientEnv = { ...runtimeEnv, [TOKEN_ENV]: token }; let client; try { client = spawnImpl(codexBin, protectedClientArgs(args, bridge.url), { From 0dd4d2aab49dadd3085bb66e52ea712730f50a16 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:50:36 +0200 Subject: [PATCH 28/48] test: prove protected session declares streaming mode --- tests/unit/codex-protected-launcher.test.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/unit/codex-protected-launcher.test.mjs b/tests/unit/codex-protected-launcher.test.mjs index 7425828..1e381e5 100644 --- a/tests/unit/codex-protected-launcher.test.mjs +++ b/tests/unit/codex-protected-launcher.test.mjs @@ -1,7 +1,10 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { protectedClientArgs } from "../../scripts/codex-with-watchdog.mjs"; +import { + protectedClientArgs, + protectedRuntimeEnv, +} from "../../scripts/codex-with-watchdog.mjs"; test("protected launcher owns the remote endpoint and preserves normal Codex args", () => { assert.deepEqual(protectedClientArgs(["resume", "abc"], "ws://127.0.0.1:4500"), [ @@ -24,3 +27,11 @@ test("protected launcher rejects caller attempts to bypass its remote bridge", ( /owns --remote-auth-token-env/, ); }); + +test("protected launcher declares stream capability only inside its launched runtime", () => { + const env = protectedRuntimeEnv({ EXISTING: "keep" }); + assert.equal(env.EXISTING, "keep"); + assert.equal(env.SHIPPING_GITHUB_HOST, "codex"); + assert.equal(env.SHIPPING_GITHUB_PROGRESS_WATCHDOG, "stream"); + assert.equal(env.SHIPPING_GITHUB_STREAM_LAUNCH_CONTROLLED, "true"); +}); From 3ea79005f6d8f6c97126f98a6b32c529258f9a76 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:51:05 +0200 Subject: [PATCH 29/48] feat: allow same-version watchdog activation refresh --- scripts/install-skill.mjs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/install-skill.mjs b/scripts/install-skill.mjs index 94d695d..a898782 100755 --- a/scripts/install-skill.mjs +++ b/scripts/install-skill.mjs @@ -70,6 +70,19 @@ export function parseInstallArgs(argv) { return options; } +function sameVersionActivationReceipt(plan) { + return { + schemaVersion: 1, + kind: "github-delivery/install-receipt", + action: "same-version", + sourceVersion: plan.sourceVersion, + previousVersion: plan.targetVersion, + target: plan.target, + backupPath: null, + unchanged: true, + }; +} + export function installSkill(options) { if (options.restore) { return options.apply @@ -92,9 +105,17 @@ export function installSkill(options) { }); } - const installation = options.apply - ? applyInstallation(options) - : { ...planInstallation(options), apply: false }; + const installationPlan = planInstallation(options); + const activationRefreshRequested = + options.hookTrustVerified === true || options.streamLaunchControlled === true; + let installation; + if (!options.apply) { + installation = { ...installationPlan, apply: false }; + } else if (installationPlan.action === "same-version" && activationRefreshRequested) { + installation = sameVersionActivationReceipt(installationPlan); + } else { + installation = applyInstallation(options); + } if (options.apply && launcherBundled && !existsSync(installedLauncherPath)) { throw new Error("protected Codex launcher was not installed with the skill payload"); From e82641da9c24b5ea3fe13f02f495575bb4b029ee Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:51:30 +0200 Subject: [PATCH 30/48] test: persist same-version hook activation refresh --- tests/unit/watchdog-activation.test.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/unit/watchdog-activation.test.mjs b/tests/unit/watchdog-activation.test.mjs index 40a1d9a..4aa40cd 100644 --- a/tests/unit/watchdog-activation.test.mjs +++ b/tests/unit/watchdog-activation.test.mjs @@ -77,7 +77,7 @@ test("normal Codex install configures hooks but does not falsely claim untrusted ); }); -test("verified unchanged hook definitions may be reported as active after user trust", () => { +test("verified unchanged hook definitions can be persisted as active without reinstalling the same skill", () => { const f = fixture(); const first = runInstall(f, ["--lifecycle-hooks-supported", "--apply"]); assert.equal(first.status, 0, first.stderr); @@ -85,12 +85,21 @@ test("verified unchanged hook definitions may be reported as active after user t const afterTrust = runInstall(f, [ "--lifecycle-hooks-supported", "--hook-trust-verified", + "--apply", ]); assert.equal(afterTrust.status, 0, afterTrust.stderr); const result = JSON.parse(afterTrust.stdout); + assert.equal(result.action, "same-version"); + assert.equal(result.unchanged, true); assert.equal(result.watchdog.mode, "hooks"); assert.equal(result.watchdog.degradationReason, "streaming_interruption_unavailable"); assert.equal(result.watchdog.hookTrustVerified, true); + + const persisted = JSON.parse( + readFileSync(join(f.codexHome, "github-delivery", "watchdog-activation.json"), "utf8"), + ); + assert.equal(persisted.mode, "hooks"); + assert.equal(persisted.hookTrustVerified, true); }); test("a hook definition change invalidates a claimed trust state", () => { From 26da7fc6435e62cab1d2969cccde1a39eec08169 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:52:03 +0200 Subject: [PATCH 31/48] docs: document Codex hook trust activation --- INSTALL.md | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index ca489c2..39e7eba 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -49,21 +49,41 @@ Existing directory installations are backed up before replacement. Symlinks and ## Codex progress watchdog activation -A standard Codex install/upgrade now plans the watchdog together with the skill. When Codex is detected and lifecycle hooks are supported, `--apply` also installs the GitHub Delivery hook entries in `~/.codex/hooks.json`. Existing hook configuration is preserved, backed up before a change, and updated idempotently. +A standard Codex install/upgrade now plans the watchdog together with the skill. When Codex is detected and lifecycle hooks are supported, `--apply` also configures GitHub Delivery's `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd` entries in `~/.codex/hooks.json`. Existing hook configuration is preserved, backed up before a change, and updated idempotently. -The installer records the effective mode in: +Codex deliberately does **not** run a new or changed non-managed command hook until you review and trust its exact definition. A fresh install therefore reports: + +```text +mode: none +degradationReason: hook_trust_required +hooksConfigured: true +``` + +Open `/hooks` in Codex, review the GitHub Delivery hook definitions, and trust them. After that exact unchanged definition is trusted, refresh the activation receipt with the normal installer: + +```bash +node scripts/install-skill.mjs --hook-trust-verified --apply +``` + +A same-version run with that explicit activation refresh does not reinstall or back up the skill again. It verifies that the expected hook definition is unchanged and then records `hooks` as the active mode. If the hook definition has changed, the trust assertion is rejected for activation purposes and `hook_trust_required` remains. + +The installer never adds Codex's `--dangerously-bypass-hook-trust` flag by default. + +The effective installation state is recorded in: ```text ~/.codex/github-delivery/watchdog-activation.json ``` -The receipt contains only activation metadata. It does not contain prompts, conversations, tool inputs, or secrets. +The receipt contains only activation metadata. It does not contain prompts, conversations, tool inputs, bearer tokens, or other secrets. The modes are intentionally strict: -- `stream`: a host has explicitly bound future Codex launches to GitHub Delivery's protected streaming launcher; -- `hooks`: lifecycle enforcement is active, but in-progress assistant text cannot be interrupted before `Stop`; -- `none`: no runtime enforcement surface was verified and policy-only protection remains. +- `stream`: a host has explicitly bound launches to GitHub Delivery's protected streaming boundary, or the current process was started by the protected launcher; +- `hooks`: the expected lifecycle hooks are configured and their unchanged definition has been explicitly confirmed trusted; in-progress assistant text still cannot be interrupted before `Stop`; +- `none`: no runtime enforcement surface is verified. `hook_trust_required` distinguishes configured-but-untrusted hooks from a genuinely unavailable watchdog. + +### Protected streaming launcher The protected launcher is installed with the skill at: @@ -71,13 +91,13 @@ The protected launcher is installed with the skill at: ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs ``` -Run Codex through it when you need in-flight repeated-narration interruption: +Run Codex through it when you need the exact repeated-narration failure stopped while the assistant message is still being generated: ```bash node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs ``` -Arguments after the script are passed to the normal Codex CLI, for example: +Arguments after the script are passed to remote-compatible Codex CLI modes, for example: ```bash node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs resume @@ -85,7 +105,9 @@ node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs resume Date: Tue, 11 Aug 2026 08:52:40 +0200 Subject: [PATCH 32/48] docs: distinguish configured and trusted Codex hooks --- references/agent-progress-watchdog.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/references/agent-progress-watchdog.md b/references/agent-progress-watchdog.md index 89154dd..fb4a036 100644 --- a/references/agent-progress-watchdog.md +++ b/references/agent-progress-watchdog.md @@ -1,6 +1,6 @@ # Agent Progress Watchdog -GitHub Delivery uses a layered progress watchdog to reduce token waste without weakening evidence, freshness, review, or mutation-authority gates. +GitHub Delivery uses a layered progress watchdog to reduce token waste without weakening evidence, freshness, review, mutation-authority, or Codex hook-trust gates. ## What it protects against @@ -14,25 +14,27 @@ The watchdog never grants GitHub mutation authority, executes a write on the age ## Activation truth -A normal Codex install/upgrade through `scripts/install-skill.mjs --apply` activates lifecycle hooks when Codex is detected and records the effective watchdog mode in: +A normal Codex install/upgrade through `scripts/install-skill.mjs --apply` configures lifecycle hooks when Codex is detected and records watchdog installation state in: ```text ~/.codex/github-delivery/watchdog-activation.json ``` -The receipt is non-sensitive activation metadata only. Runtime capability discovery reads it so `none`, `hooks`, and `stream` describe what is actually active instead of merely what code exists in the installed skill. +The receipt is non-sensitive activation metadata only. It distinguishes hook configuration from verified active enforcement. Mode selection is strongest verified mode only: -1. `stream` when the host has explicitly bound future launches to the protected streaming entry point; -2. `hooks` when lifecycle hooks are active but the launch boundary is not controlled; -3. `none` when neither runtime surface is verified. +1. `stream` when the current protected launcher declares the streaming boundary or a host explicitly controls future launches through it; +2. `hooks` only when the expected lifecycle hooks are configured and the exact unchanged definition has been explicitly confirmed trusted; +3. `none` when no runtime surface is verified. `hook_trust_required` distinguishes configured-but-untrusted hooks from an unavailable watchdog. + +Codex requires non-managed command hooks to be reviewed and trusted before they run. Trust is tied to the current hook definition, so adding or changing the hook makes it review-pending again. GitHub Delivery therefore never treats `hooks.json` presence as proof that lifecycle enforcement is active and never enables `--dangerously-bypass-hook-trust` by default. ## Enforcement levels ### Policy only -`GD-CORE-008` through `GD-CORE-010` remain the universal fallback when the host exposes no runtime lifecycle or streaming interception. +`GD-CORE-008` through `GD-CORE-010` remain the universal fallback when the host exposes no verified runtime lifecycle or streaming interception. This reduces ordinary waste but cannot forcibly stop a pathological assistant message while that message is already being generated. @@ -56,7 +58,9 @@ Hook state is stored outside repository content. Session ids and read inputs are Lifecycle hooks cannot reclaim tokens already emitted inside the assistant message that reaches `Stop` or `SubagentStop`. -The normal Codex installer path now reuses the safe hook installer automatically on `--apply`. `scripts/install-codex-watchdog-hooks.mjs` remains available for repair and non-standard installs. Hook configuration is backup-first, preserves unrelated entries, rejects malformed or symlinked configuration, and is idempotent. +The normal Codex installer path configures GitHub Delivery's hook entries automatically on `--apply`. Hook configuration is backup-first, preserves unrelated entries, rejects malformed or symlinked configuration, and is idempotent. `scripts/install-codex-watchdog-hooks.mjs` remains available for repair and non-standard installs. + +After a fresh or changed hook definition, use Codex `/hooks` to review and trust it. A host/operator can then refresh the same installer with `--hook-trust-verified --apply`; same-version activation refreshes do not reinstall the skill. The installer accepts that trust assertion only when its expected hook definition is unchanged. ### Protected Codex streaming launcher @@ -75,13 +79,14 @@ The launcher: 3. starts the ordinary Codex client with the documented `--remote` and `--remote-auth-token-env` flags pointed at that bridge; 4. forwards JSON-RPC traffic while observing `item/agentMessage/delta` notifications; 5. issues one private `turn/interrupt` when repeated low-novelty intent narration crosses the watchdog threshold; -6. consumes the private interrupt response rather than leaking it to the client. +6. consumes the private interrupt response rather than leaking it to the client; +7. declares `SHIPPING_GITHUB_PROGRESS_WATCHDOG=stream` inside the launched process tree so runtime inspection sees the current protected session directly. The bearer token is generated in memory for the launched client and is not persisted. The bridge binds only to loopback. The protected launcher owns the remote endpoint flags and rejects caller-supplied replacements. This is the only GitHub Delivery layer that can stop the targeted failure while an assistant message is still streaming. The incident regression includes the observed phrase family `Let me check the type`, `Let me check the NOUS_DEF type`, and `Let me check the OAuthProviderDef type`, and requires the interrupt before 500 emitted characters. -Installing the launcher does not silently reroute an already-running or ordinarily-launched Codex CLI/IDE process. `stream` is recorded only when the host actually controls launches through this entry point. Otherwise lifecycle hooks remain active and the receipt reports `hooks` with `streaming_interruption_unavailable`. +Installing the launcher does not silently reroute an already-running or ordinarily-launched Codex CLI/IDE process. A one-off protected session gets its `stream` declaration from the launcher itself. A persisted `stream` activation receipt is reserved for a host integration that explicitly asserts it controls future launches through this entry point. The older `scripts/codex-app-server-watchdog-proxy.mjs` remains useful to custom stdio App Server clients. It provides the same delta watchdog for clients that already own the App Server protocol connection. From e297e6ae99ccae7d8a382f5ada79acb7c9278201 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:53:16 +0200 Subject: [PATCH 33/48] docs: make runtime watchdog claims trust-aware --- references/runtime-capabilities.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/references/runtime-capabilities.md b/references/runtime-capabilities.md index b793880..990e9bc 100644 --- a/references/runtime-capabilities.md +++ b/references/runtime-capabilities.md @@ -21,7 +21,7 @@ The script probes facts available to the local process: - GitHub CLI authentication - repository readability through `gh` - repository write permission through `gh` -- persisted GitHub Delivery watchdog activation under the active Codex home +- persisted GitHub Delivery watchdog installation/activation state under the active Codex home Capabilities that exist only in the host may also be declared by the agent environment: @@ -41,9 +41,11 @@ SHIPPING_GITHUB_PROGRESS_WATCHDOG=none|hooks|stream `CONNECTOR_WRITE` means the host connector has write permission. `BROKERED_CONNECTOR_WRITE` means the github-delivery mutation broker has an adapter that enforces the same request, expected-head, idempotency, exact-text, audit, and verification contract through that connector. Permission without an adapter is not a usable mutation path. -For the progress watchdog, an explicit environment declaration is useful for controlled host integrations and fixtures. When it is absent, runtime discovery reads `~/.codex/github-delivery/watchdog-activation.json` (or the equivalent under `CODEX_HOME`). Invalid or missing activation state never upgrades capability. +For the progress watchdog, an explicit environment declaration is authoritative for the current controlled runtime. The protected Codex launcher sets `SHIPPING_GITHUB_PROGRESS_WATCHDOG=stream` inside the App Server/client process tree so current-session capability discovery does not depend on a machine-wide guess. -`hooks` means lifecycle-hook enforcement is active. `stream` means the host has a verified launch boundary capable of interrupting an in-flight no-progress turn. `none` means policy-only protection. See `references/agent-progress-watchdog.md` for their different guarantees. +When no explicit runtime declaration exists, discovery reads `~/.codex/github-delivery/watchdog-activation.json` (or the equivalent under `CODEX_HOME`). Invalid or missing activation state never upgrades capability. A fresh Codex hook configuration is persisted as `none` with `hook_trust_required` until the expected unchanged non-managed hook definition has been explicitly confirmed trusted; file presence alone is not interpreted as active hooks. + +`hooks` means lifecycle-hook enforcement was explicitly verified for the expected definition. `stream` means the current process or host has a verified launch boundary capable of interrupting an in-flight no-progress turn. `none` means policy-only protection for that capability snapshot. See `references/agent-progress-watchdog.md` for their different guarantees. ## Output contract @@ -99,8 +101,8 @@ For the progress watchdog, an explicit environment declaration is useful for con - Bugbot is used only on Cursor when both host and capability declarations permit it. Every other host uses complementary lenses. - Subagents are used only when declared. Otherwise run the work in-session without claiming fan-out occurred. - `runtime.progressWatchdog` is `stream`, `hooks`, or `none`; the corresponding `contextEconomy` fallback is `streaming-watchdog`, `lifecycle-hooks`, or `policy-only`. -- `progressWatchdogDegradationReason` makes hook-only or unavailable protection visible instead of letting the workflow assume streaming enforcement. -- `progress_watchdog_unavailable` is included in `degraded` when no runtime watchdog is active. +- `progressWatchdogDegradationReason` can expose `hook_trust_required`, `streaming_interruption_unavailable`, or another concrete activation degradation instead of letting the workflow assume stronger enforcement. +- `progress_watchdog_unavailable` is included in `degraded` when no runtime watchdog is active. A more specific activation reason can still be present in `runtime.progressWatchdogDegradationReason`. - Missing ruleset or review-thread evidence is degraded capability and must flow into an unknown gate result rather than being guessed. ## Offline fixtures From 6597e38efef3c507cb668e5b6eba492526df8a4a Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:57:26 +0200 Subject: [PATCH 34/48] docs: explain protected Codex watchdog activation --- README.md | 65 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f3811cc..67899e0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Say the outcome, not the orchestration.** -`github-delivery` turns natural-language requests into evidence-backed GitHub workflows: PRDs, issue research, implementation, deep review, CI, fixes, stacks, and verified merges. On supported hosts, an optional runtime progress watchdog also cuts repeated narration, duplicate unchanged reads, manual polling, oversized tool output, and bloated subagent context. +`github-delivery` turns natural-language requests into evidence-backed GitHub workflows: PRDs, issue research, implementation, deep review, CI, fixes, stacks, and verified merges. Its layered progress watchdog also cuts repeated narration, duplicate unchanged reads, manual polling, oversized tool output, and bloated subagent context without weakening GitHub authority gates. [Quick start](#try-it-in-60-seconds) · [Progress watchdog](#agent-progress-watchdog) · [What it can own](#what-you-can-ask-it-to-own) · [Safety model](#safety-model) · [Installation](#installation) @@ -77,23 +77,28 @@ full review PR #42 For full install, upgrade, restore, downgrade, force, and manual-install behavior, see [`INSTALL.md`](INSTALL.md). -### Optional: add the Codex progress watchdog +### Codex progress watchdog -The skill works without host hooks. For Codex, you can additionally install lifecycle enforcement that blocks redundant reads/polls, bounds oversized context, and recovers from completed no-progress turns: +On a detected Codex install, the normal `--apply` path now configures GitHub Delivery's lifecycle-hook entries automatically. Codex requires new or changed non-managed hooks to be reviewed and trusted before they run, so open `/hooks`, review the exact GitHub Delivery definitions, and trust them. Then record that unchanged trusted definition without reinstalling the skill: ```bash -node scripts/install-codex-watchdog-hooks.mjs -node scripts/install-codex-watchdog-hooks.mjs --apply +node scripts/install-skill.mjs --hook-trust-verified --apply ``` -The first command is a dry run. The installer preserves existing hooks, backs up before writes, and adds only missing GitHub Delivery hook entries. For the stronger streaming boundary that can interrupt repeated narration **while the assistant message is still being generated**, see [Agent progress watchdog](#agent-progress-watchdog). +Lifecycle hooks stop duplicate reads/polls and recover from completed no-progress turns, but they cannot reclaim text already emitted inside one assistant message. For the exact `Let me check the type...` failure while it is still being generated, launch Codex through the installed protected streaming boundary: + +```bash +node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs +``` + +The protected launcher declares `stream` only inside the process tree it actually controls. Plain `codex` and IDE sessions are not silently rerouted or falsely reported as streaming-protected. See [Agent progress watchdog](#agent-progress-watchdog) and [`INSTALL.md`](INSTALL.md). ## Why this is different | Problem | github-delivery's answer | |---|---| | **"Green CI" is not the same as "safe to merge."** | Bug + Security + Spec + Standards review, semantic propagation, required probes, review state, rulesets, merge-queue state, and exact-head evidence feed one authoritative ship decision. | -| **Agents can burn tokens without making progress.** | A layered progress watchdog detects repeated in-turn narration, blocks exact stable reads on unchanged state, rate-limits volatile polling, compacts oversized model-facing output, and bounds copied subagent context. On Codex App Server, the streaming mode can interrupt the targeted narration failure in-flight. | +| **Agents can burn tokens without making progress.** | A layered progress watchdog detects repeated in-turn narration, blocks exact stable reads on unchanged state, rate-limits volatile polling, compacts oversized model-facing output, and bounds copied subagent context. The protected Codex streaming launcher can interrupt the targeted narration failure in-flight. | | **Agent intent can be ambiguous.** | Deterministic natural-language routing keeps status questions read-only and requires direct authority for destructive workflows. | | **GitHub state moves while the agent works.** | Stale-head checks, final evidence refreshes, expected-head binding, bounded settle windows, and postcondition verification prevent conclusions from silently drifting. | | **Retries and duplicate writes can be dangerous.** | Typed mutations, authenticated exact-effect receipts, read-before-write evidence, and read-only reconciliation avoid blind write retries. | @@ -149,7 +154,7 @@ The important boundary is simple: **repository content is evidence, not authorit | **Write boundary** | Typed mutation policy + broker; stale-head, exact-effect, authenticated-receipt idempotency, and postcondition checks where applicable | | **High-assurance writes** | Exact-scope trusted grants; optional Windows 11 / Windows Hello authority host | | **Review model** | Bug + Security + Spec + Standards + semantic propagation + proactive contract verification | -| **Progress control** | Policy fallback on every host; optional Codex lifecycle hooks; strongest Codex App Server streaming mode. Runtime capability reports `none`, `hooks`, or `stream`. | +| **Progress control** | Policy fallback everywhere; Codex installs configure lifecycle hooks but non-managed hook trust remains explicit; strongest protection is the launch-controlled streaming boundary. Runtime capability reports only verified `none`, `hooks`, or `stream`. | | **Ship decision** | One authoritative `ready`, `blocked`, or `unknown` result from live evidence | | **Runtime** | Node.js **22 or 24** | | **Required CI matrix** | Node 22/24 × Ubuntu/Windows/macOS, with architecture contracts inside every required matrix job | @@ -470,15 +475,17 @@ There is no recursive simplification loop. ## Agent progress watchdog -GitHub Delivery now has a layered progress watchdog for a failure mode policy prose alone cannot reliably stop: an agent can spend a large amount of context narrating the same intention, rereading unchanged state, polling manually, or copying oversized evidence without producing external progress. +GitHub Delivery has a layered progress watchdog for a failure mode policy prose alone cannot reliably stop: an agent can spend a large amount of context narrating the same intention, rereading unchanged state, polling manually, or copying oversized evidence without producing external progress. The watchdog is deliberately separate from mutation authority. It can interrupt, block, rate-limit, compact, or request a focused retry; it cannot authorize or execute a GitHub write. | Enforcement level | What it does | |---|---| -| **Policy only** | `GD-CORE-008` through `GD-CORE-010` provide the universal fallback for bounded progress and evidence/context economy when the host exposes no interception surface. | -| **Codex lifecycle hooks** | `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd` enforce duplicate-read blocking, volatile-poll limits, output compaction, focused subagent briefs, and one bounded corrective continuation. | -| **Codex App Server stream** | Observes streamed assistant deltas and issues one private `turn/interrupt` when repeated low-novelty intent narration crosses the watchdog threshold. This is the only layer that can stop the targeted failure while the message is still streaming. | +| **Policy only** | `GD-CORE-008` through `GD-CORE-010` provide the universal fallback for bounded progress and evidence/context economy when the host exposes no verified interception surface. | +| **Codex lifecycle hooks** | The normal install configures `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd`. After Codex's explicit non-managed hook trust review, they enforce duplicate-read blocking, volatile-poll limits, output compaction, focused subagent briefs, and one bounded corrective continuation. | +| **Protected Codex stream** | The installed `codex-with-watchdog.mjs` launch boundary observes streamed assistant deltas and issues one private `turn/interrupt` when repeated low-novelty intent narration crosses the watchdog threshold. This is the only layer that can stop the targeted failure while the message is still streaming. | + +A configured hook is not automatically an active hook: Codex ties trust to the exact hook definition and skips a new or changed non-managed hook until it is reviewed in `/hooks`. GitHub Delivery records `hook_trust_required` rather than falsely reporting `hooks` in that state. The protected launcher independently marks its own process tree `stream`, so a protected session does not depend on machine-wide activation guesswork. ### Read and context economy @@ -489,7 +496,7 @@ The watchdog is deliberately separate from mutation authority. It can interrupt, - Codex hook mode uses a **6,000 serialized-character** default subagent-input budget and requires focused briefs that reference source files rather than copying large parent context. - Raw tool arguments are not persisted in watchdog state; session ids and read inputs are represented by SHA-256 fingerprints. -For operator details, host integration, and the streaming proxy, see [`references/agent-progress-watchdog.md`](references/agent-progress-watchdog.md). +For operator details, hook trust, host integration, and the protected streaming boundary, see [`references/agent-progress-watchdog.md`](references/agent-progress-watchdog.md). --- @@ -542,10 +549,13 @@ See [`docs/live-integration.md`](docs/live-integration.md) and [`docs/live-githu | `authority-host/windows/` | Optional Windows 11 / Windows Hello local trusted-authority issuer | | `scripts/lib/github-retry.mjs` | Bounded retry policy for proven GitHub reads only | | `scripts/lib/agent-progress-watchdog.mjs` | Host-agnostic narration-stall detection, read fingerprints, state generations, and output economy | +| `scripts/lib/watchdog-activation.mjs` | Truthful `none` / trusted `hooks` / controlled `stream` activation selection and non-sensitive receipt state | | `scripts/codex-watchdog-hook.mjs` | Codex lifecycle-hook entrypoint for tool-boundary enforcement and bounded Stop recovery | -| `scripts/codex-app-server-watchdog-proxy.mjs` | Streaming Codex App Server proxy with private in-flight `turn/interrupt` handling | -| `scripts/install-codex-watchdog-hooks.mjs` | Dry-run-first, backup-safe, idempotent Codex hook installer | -| `scripts/runtime-capabilities.mjs` | Report active progress-watchdog capability as `none`, `hooks`, or `stream` | +| `scripts/lib/codex-watchdog-remote-bridge.mjs` | Authenticated loopback bridge that applies the streaming watchdog between Codex remote client and stdio App Server | +| `scripts/codex-with-watchdog.mjs` | Protected Codex launcher and current-session `stream` capability declaration | +| `scripts/codex-app-server-watchdog-proxy.mjs` | Stdio streaming proxy for custom App Server clients with private in-flight `turn/interrupt` handling | +| `scripts/install-codex-watchdog-hooks.mjs` | Dry-run-first, backup-safe, idempotent Codex hook installer/repair path | +| `scripts/runtime-capabilities.mjs` | Report verified progress-watchdog capability as `none`, `hooks`, or `stream` | | `scripts/review-scope.mjs` | Evidence-ranked review scope and required probes | | `scripts/lib/probe-registry.mjs` | Deterministic diff-shape → named review-probe routing | | `scripts/lib/probe-evidence.mjs` | Validate required probe evidence and reject required-trigger `n-a` downgrades | @@ -582,7 +592,7 @@ Or verify reproducibility while building release artifacts: npm run dist:check ``` -The installer is dry-run first. Full install, upgrade, backup, restore, downgrade, force, and manual-install behavior is documented in [`INSTALL.md`](INSTALL.md). +The installer is dry-run first. Full install, upgrade, backup, restore, downgrade, force, watchdog trust refresh, and manual-install behavior is documented in [`INSTALL.md`](INSTALL.md). Typical skill locations include: @@ -593,18 +603,23 @@ Typical skill locations include: ~/.claude/skills/github-delivery ``` -### Optional Codex progress watchdog +### Codex progress watchdog -Codex lifecycle hooks are separately opt-in and dry-run by default. They preserve existing hooks and back up the hook configuration before an applied write: +On Codex, the normal installer configures the lifecycle-hook definitions along with the skill. Codex still requires explicit review/trust of new or changed non-managed hooks in `/hooks`; GitHub Delivery does not bypass that trust gate. + +After trusting the unchanged definitions, persist the verified hook mode with: ```bash -node scripts/install-codex-watchdog-hooks.mjs -node scripts/install-codex-watchdog-hooks.mjs --apply +node scripts/install-skill.mjs --hook-trust-verified --apply ``` -The installer targets `~/.codex/hooks.json` by default and adds only GitHub Delivery's missing `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`, and `SessionEnd` entries. If the skill lives somewhere else, pass `--skill-dir` explicitly. +For mid-message repeated-narration interruption, use the protected launcher: + +```bash +node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs +``` -Lifecycle hooks cannot stop tokens already emitted inside the current message. Custom Codex App Server clients can opt into the stronger streaming proxy documented in [`references/agent-progress-watchdog.md`](references/agent-progress-watchdog.md). +The launcher starts the real App Server over stdio, interposes an authenticated loopback remote bridge, and marks only its own launched process tree as `stream`. Ordinary `codex` and IDE sessions are not silently rerouted. The standalone `scripts/install-codex-watchdog-hooks.mjs` remains available for repair/non-standard installs. ### Optional Windows authority host @@ -646,7 +661,7 @@ Repository controls also include: - offline routing, regression and review-scope evaluations; - documentation/policy contracts; - mutation-boundary and architecture regression tests; -- progress-watchdog regressions for narration stalls, duplicate reads, polling, output compaction, subagent budgets, hooks, streaming interruption, and safe installation; +- progress-watchdog regressions for narration stalls, duplicate reads, polling, output compaction, subagent budgets, hook trust/configuration, protected streaming interruption, and safe installation; - OpenSSF Scorecard; - release checksum/SBOM/provenance verification. @@ -664,6 +679,6 @@ Do not publish suspected vulnerability details in a public issue or pull request ## Current state -The complete issue/PR delivery lifecycle and its safety architecture are implemented: evidence-backed routing and ship gates, deferred-intent-safe merge routing, brokered lifecycle mutations, trusted exact-scope authority and durable verdict provenance, Windows Hello protection for high-assurance thread actions, deep review, semantic propagation, deterministic probes with non-bypassable required evidence, pre-open review, safe simplification, repository-qualified stacks, conflict recovery, merge-queue semantics, aggregated strict-ruleset enforcement, authenticated exact-effect idempotency receipts, ambiguous-merge readback reconciliation, safe read retries, layered progress/context economy with optional Codex hook and streaming enforcement, issue close-out, deterministic release packaging, repository controls, and dedicated live lifecycle fixtures. +The complete issue/PR delivery lifecycle and its safety architecture are implemented: evidence-backed routing and ship gates, deferred-intent-safe merge routing, brokered lifecycle mutations, trusted exact-scope authority and durable verdict provenance, Windows Hello protection for high-assurance thread actions, deep review, semantic propagation, deterministic probes with non-bypassable required evidence, pre-open review, safe simplification, repository-qualified stacks, conflict recovery, merge-queue semantics, aggregated strict-ruleset enforcement, authenticated exact-effect idempotency receipts, ambiguous-merge readback reconciliation, safe read retries, layered progress/context economy with trust-aware Codex hook configuration and a protected streaming launch boundary, issue close-out, deterministic release packaging, repository controls, and dedicated live lifecycle fixtures. Remaining work is primarily **operational** rather than a missing architecture layer: keep live repository rules/security settings aligned with the documented policy, provision and maintain the dedicated live fixture target/credential, run release acceptance for new versions, keep host integrations explicitly configured where runtime watchdog enforcement is desired, and extend the regression corpus as GitHub and agent hosts evolve. From 72559048a9fb94a0d203e8e32b0bfc885b0904e1 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:00:07 +0200 Subject: [PATCH 35/48] fix: harden protected Codex WebSocket bridge --- scripts/lib/codex-watchdog-remote-bridge.mjs | 43 ++++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/scripts/lib/codex-watchdog-remote-bridge.mjs b/scripts/lib/codex-watchdog-remote-bridge.mjs index 7539470..996854a 100644 --- a/scripts/lib/codex-watchdog-remote-bridge.mjs +++ b/scripts/lib/codex-watchdog-remote-bridge.mjs @@ -33,6 +33,21 @@ function encodeFrame(payload, opcode = 0x1) { return Buffer.concat([header, data]); } +function safeWriteFrame(socket, payload, opcode = 0x1) { + if (socket.destroyed || !socket.writable) return false; + try { + socket.write(encodeFrame(payload, opcode)); + return true; + } catch { + socket.destroy(); + return false; + } +} + +function broadcast(clients, payload) { + for (const client of clients) safeWriteFrame(client, payload); +} + function createFrameParser(onText, onClose, sendControl) { let buffer = Buffer.alloc(0); let fragmentedOpcode = null; @@ -117,6 +132,12 @@ function rejectUpgrade(socket, status, message) { ); } +function headerContainsToken(value, token) { + return String(value || "") + .split(",") + .some((part) => part.trim().toLowerCase() === token); +} + export async function startCodexWatchdogRemoteBridge({ appServerInput, appServerOutput, @@ -146,7 +167,10 @@ export async function startCodexWatchdogRemoteBridge({ } } const key = request.headers["sec-websocket-key"]; - if (!key || String(request.headers.upgrade || "").toLowerCase() !== "websocket") { + const version = String(request.headers["sec-websocket-version"] || ""); + const upgrade = String(request.headers.upgrade || "").toLowerCase(); + const connectionUpgrade = headerContainsToken(request.headers.connection, "upgrade"); + if (!key || version !== "13" || upgrade !== "websocket" || !connectionUpgrade) { rejectUpgrade(socket, "400 Bad Request", "invalid WebSocket upgrade"); return; } @@ -164,7 +188,7 @@ export async function startCodexWatchdogRemoteBridge({ if (appServerInput.writable) appServerInput.write(`${text}\n`); }, () => socket.end(), - (opcode, payload) => socket.write(encodeFrame(payload, opcode)), + (opcode, payload) => safeWriteFrame(socket, payload, opcode), ); socket.on("data", (chunk) => { try { @@ -175,7 +199,13 @@ export async function startCodexWatchdogRemoteBridge({ }); socket.on("close", () => clients.delete(socket)); socket.on("error", () => clients.delete(socket)); - if (head?.length) parser.push(head); + if (head?.length) { + try { + parser.push(head); + } catch { + socket.destroy(); + } + } }); const lines = createInterface({ input: appServerOutput, crlfDelay: Infinity }); @@ -184,14 +214,11 @@ export async function startCodexWatchdogRemoteBridge({ try { message = JSON.parse(line); } catch { - for (const client of clients) client.write(encodeFrame(line)); + broadcast(clients, line); return; } const routed = router.onServerMessage(message); - if (routed.forward) { - const text = JSON.stringify(routed.forward); - for (const client of clients) client.write(encodeFrame(text)); - } + if (routed.forward) broadcast(clients, JSON.stringify(routed.forward)); for (const request of routed.internalRequests) { if (appServerInput.writable) appServerInput.write(`${JSON.stringify(request)}\n`); } From 551d77ed17e5a495a4f68999bfd2a3ee6c1e7593 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:00:34 +0200 Subject: [PATCH 36/48] test: verify protected bridge rejects unauthenticated clients --- .../codex-watchdog-remote-bridge.test.mjs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/unit/codex-watchdog-remote-bridge.test.mjs b/tests/unit/codex-watchdog-remote-bridge.test.mjs index 54f45e3..64f8b7d 100644 --- a/tests/unit/codex-watchdog-remote-bridge.test.mjs +++ b/tests/unit/codex-watchdog-remote-bridge.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { connect } from "node:net"; import { PassThrough } from "node:stream"; import test from "node:test"; @@ -16,6 +17,45 @@ function openWebSocket(url) { }); } +function rawUpgrade(url) { + const parsed = new URL(url); + return new Promise((resolve, reject) => { + const socket = connect({ host: parsed.hostname, port: Number(parsed.port) }); + let response = ""; + socket.setEncoding("utf8"); + socket.once("error", reject); + socket.on("data", (chunk) => { + response += chunk; + }); + socket.once("end", () => resolve(response)); + socket.once("connect", () => { + socket.write( + "GET / HTTP/1.1\r\n" + + `Host: ${parsed.host}\r\n` + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + + "Sec-WebSocket-Version: 13\r\n\r\n", + ); + }); + }); +} + +test("protected bridge rejects a client that lacks its bearer token", async () => { + const appServerInput = new PassThrough(); + const appServerOutput = new PassThrough(); + const bridge = await startCodexWatchdogRemoteBridge({ + appServerInput, + appServerOutput, + token: "secret-token", + }); + + const response = await rawUpgrade(bridge.url); + assert.match(response, /^HTTP\/1\.1 401 Unauthorized/m); + + await bridge.close(); +}); + test("installed streaming boundary interrupts the observed Let me check type loop before 500 characters", async () => { const appServerInput = new PassThrough(); const appServerOutput = new PassThrough(); From d129386c2a801702cfcfb042ab68fe3eed512363 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:01:44 +0200 Subject: [PATCH 37/48] docs: mark protected Codex stream boundary experimental --- references/agent-progress-watchdog.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/references/agent-progress-watchdog.md b/references/agent-progress-watchdog.md index fb4a036..a0d56c9 100644 --- a/references/agent-progress-watchdog.md +++ b/references/agent-progress-watchdog.md @@ -64,7 +64,7 @@ After a fresh or changed hook definition, use Codex `/hooks` to review and trust ### Protected Codex streaming launcher -For the strongest boundary, launch Codex through the installed entry point: +For the strongest boundary currently exposed by Codex, launch through the installed entry point: ```text node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs @@ -82,12 +82,14 @@ The launcher: 6. consumes the private interrupt response rather than leaking it to the client; 7. declares `SHIPPING_GITHUB_PROGRESS_WATCHDOG=stream` inside the launched process tree so runtime inspection sees the current protected session directly. -The bearer token is generated in memory for the launched client and is not persisted. The bridge binds only to loopback. The protected launcher owns the remote endpoint flags and rejects caller-supplied replacements. +The bearer token is generated in memory for the launched client and is not persisted. The bridge binds only to loopback, validates the WebSocket v13 upgrade, requires the bearer token in normal launcher use, permits one client, and bounds individual frames. The protected launcher owns the remote endpoint flags and rejects caller-supplied replacements. This is the only GitHub Delivery layer that can stop the targeted failure while an assistant message is still streaming. The incident regression includes the observed phrase family `Let me check the type`, `Let me check the NOUS_DEF type`, and `Let me check the OAuthProviderDef type`, and requires the interrupt before 500 emitted characters. Installing the launcher does not silently reroute an already-running or ordinarily-launched Codex CLI/IDE process. A one-off protected session gets its `stream` declaration from the launcher itself. A persisted `stream` activation receipt is reserved for a host integration that explicitly asserts it controls future launches through this entry point. +**Maturity:** Codex currently documents `app-server` and its WebSocket transport as experimental and unsupported for production workloads. GitHub Delivery therefore treats this launcher as the strongest available Codex enforcement boundary, not as a stable production host API. Lifecycle hooks and policy fallback remain available when that experimental streaming surface is inappropriate. + The older `scripts/codex-app-server-watchdog-proxy.mjs` remains useful to custom stdio App Server clients. It provides the same delta watchdog for clients that already own the App Server protocol connection. ## Read economy From 3fbbcc38eb55aa08660c5607ef64d6686925e648 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:02:13 +0200 Subject: [PATCH 38/48] docs: disclose Codex streaming maturity --- INSTALL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 39e7eba..0648eea 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -109,6 +109,8 @@ Inside the launched App Server/client process tree, the launcher sets the runtim Installing the launcher does **not** make an ordinary `codex` or IDE process use it automatically. Codex exposes remote App Server selection as a launch option; GitHub Delivery does not replace your global `codex` executable or silently rewrite editor startup configuration. A persistent host integration may use `--stream-launch-controlled` only when it genuinely controls future launches through this boundary. +Codex currently documents `app-server` and its WebSocket transport as experimental and unsupported for production workloads. This launcher is therefore the strongest currently available Codex boundary for this failure mode, not a stable production host API. Use trusted lifecycle hooks plus the policy fallback when that experimental surface is inappropriate. + ### Manual hook repair `scripts/install-codex-watchdog-hooks.mjs` remains available as a repair or non-standard-install tool. It is dry-run by default: From df07861b18221457cb01fb94ee5c45badd900f60 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:03:08 +0200 Subject: [PATCH 39/48] docs: record trust-aware implementation deviations --- docs/superpowers/plans/2026-08-11-watchdog-activation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/superpowers/plans/2026-08-11-watchdog-activation.md b/docs/superpowers/plans/2026-08-11-watchdog-activation.md index f9db632..0ab1d17 100644 --- a/docs/superpowers/plans/2026-08-11-watchdog-activation.md +++ b/docs/superpowers/plans/2026-08-11-watchdog-activation.md @@ -1,5 +1,7 @@ # Automatic Watchdog Activation Implementation Plan +> **Implementation note:** During execution, the official Codex hook documentation exposed an additional trust boundary: a newly configured or changed non-managed command hook is skipped until its exact definition is reviewed/trusted. The approved design spec was updated accordingly. This plan therefore executes `stream > trusted hooks > none`, with fresh hook configuration reported as `none / hook_trust_required`, and uses the installed `scripts/codex-with-watchdog.mjs` protected remote launcher for in-flight interruption. The detailed task text below records the original implementation decomposition; the updated spec and final code/tests are authoritative where the trust discovery changed an interface or filename. + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make a standard supported Codex install/upgrade automatically activate the strongest watchdog mode it can prove, persist the effective mode, and prove the observed repeated `Let me check...` narration is interrupted in streaming mode before the configured output budget is exceeded. From cf70b84e615e21dcf57f615cf778f10dc4ba0b07 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:05:10 +0200 Subject: [PATCH 40/48] docs: disclose protected stream maturity --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 67899e0..b163853 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Lifecycle hooks stop duplicate reads/polls and recover from completed no-progres node ~/.agents/skills/github-delivery/scripts/codex-with-watchdog.mjs ``` -The protected launcher declares `stream` only inside the process tree it actually controls. Plain `codex` and IDE sessions are not silently rerouted or falsely reported as streaming-protected. See [Agent progress watchdog](#agent-progress-watchdog) and [`INSTALL.md`](INSTALL.md). +The protected launcher declares `stream` only inside the process tree it actually controls. Plain `codex` and IDE sessions are not silently rerouted or falsely reported as streaming-protected. Codex currently documents `app-server` and its WebSocket transport as experimental and unsupported for production workloads, so this is the strongest current boundary for the failure mode rather than a stable production host API. See [Agent progress watchdog](#agent-progress-watchdog) and [`INSTALL.md`](INSTALL.md). ## Why this is different @@ -487,6 +487,8 @@ The watchdog is deliberately separate from mutation authority. It can interrupt, A configured hook is not automatically an active hook: Codex ties trust to the exact hook definition and skips a new or changed non-managed hook until it is reviewed in `/hooks`. GitHub Delivery records `hook_trust_required` rather than falsely reporting `hooks` in that state. The protected launcher independently marks its own process tree `stream`, so a protected session does not depend on machine-wide activation guesswork. +Codex currently marks App Server/WebSocket transport experimental. The protected launcher is intentionally a strongest-current-boundary option rather than a claim that this host surface is production-stable. + ### Read and context economy - Stable reads use `SHA-256(state-generation + tool-name + canonical-tool-input)` and are reusable until relevant state changes. From 69200252ad77adf48c80d40004dc6cc7b23ec000 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:06:37 +0200 Subject: [PATCH 41/48] docs: record Codex streaming maturity in design --- .../2026-08-11-watchdog-activation-design.md | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md b/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md index bd5dbab..97ef4de 100644 --- a/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md +++ b/docs/superpowers/specs/2026-08-11-watchdog-activation-design.md @@ -20,6 +20,7 @@ Success means a normal Codex install/upgrade configures the available watchdog s - Do not replace Codex itself or assume an undocumented host interception API. - Do not silently alter unrelated user hooks or editor configuration. - Do not bypass Codex's persisted hook-trust review by default. +- Do not represent Codex App Server/WebSocket integration as production-stable while OpenAI documents that surface as experimental. ## Approaches considered @@ -41,6 +42,8 @@ Recommended. During install/upgrade, detect the target host and available watchd A fresh Codex install may therefore configure hooks automatically while reporting `none / hook_trust_required` until the user reviews them in `/hooks`. This preserves Codex's trust model instead of silently weakening it. +The streaming path uses Codex's documented remote TUI/App Server connection to place GitHub Delivery between assistant deltas and the client. Because OpenAI currently documents `codex app-server` and the WebSocket transport as experimental and unsupported for production workloads, this is explicitly a strongest-current enforcement option rather than a promise of a stable host API. + ## Architecture ### 1. Activation planner @@ -81,13 +84,17 @@ The existing standalone hook installer remains available for recovery/manual use Because Codex records trust against the exact hook definition hash, a newly added or changed hook is reported as `hook_trust_required` until it is reviewed in `/hooks`. The installer does not use `--dangerously-bypass-hook-trust` as a default activation mechanism. +A same-version `--hook-trust-verified --apply` activation refresh updates only the activation receipt when the hook definition is unchanged; it does not reinstall/back up the skill again. + ### 3. Streaming launcher integration -GitHub Delivery installs a protected launcher owned by the skill. It starts the real Codex App Server on the default stdio transport, exposes an authenticated loopback bridge, and starts the ordinary Codex client with its documented `--remote` flags pointed at that bridge. +GitHub Delivery installs a protected launcher owned by the skill. It starts the real Codex App Server on the default stdio transport, exposes an authenticated loopback WebSocket bridge, and starts the ordinary Codex client with its documented `--remote` flags pointed at that bridge. The bridge observes `item/agentMessage/delta` notifications and may issue one private `turn/interrupt` while the repeated narration is still being generated. -Installing the launcher alone does not make plain `codex` or an IDE session use it. `stream` is recorded only when the caller/host explicitly controls launches through this protected entry point. This avoids claiming a mid-message boundary that is not actually in the traffic path. +The bridge is loopback-only, bearer-authenticated in normal launcher operation, limited to one client, validates the WebSocket v13 upgrade, caps frame size, and destroys malformed/oversized connections instead of letting them crash the launcher. + +Installing the launcher alone does not make plain `codex` or an IDE session use it. `stream` is recorded only when the caller/host explicitly controls launches through this protected entry point. The launcher itself declares `stream` inside the process tree it creates, so current-session capability discovery is truthful without persisting a machine-wide stream claim. ### 4. Runtime capability truth @@ -129,9 +136,12 @@ If no runtime surface is available, installation does not fail solely for that r - Non-managed hooks are never reported active solely because they are configured. - A changed hook definition invalidates a supplied trust assertion for that install pass. - The default workflow never adds `--dangerously-bypass-hook-trust`. +- The protected launcher uses an in-memory bearer token and loopback-only bridge; the token is not persisted. +- The bridge fails closed on invalid upgrades, malformed frames, and oversized frames. - Streaming launch integration never swallows ordinary App Server traffic, mutation prompts, or errors. - The watchdog remains incapable of granting GitHub write authority. - Existing `GD-CORE-*`, `GD-AUTH-*`, CI, review, security, and final-evidence rules remain authoritative. +- Codex App Server/WebSocket maturity is disclosed rather than hidden. ## Testing strategy @@ -147,12 +157,15 @@ Add an incident regression using the real repeated phrase family from the observ - newly configured non-managed hooks report `hook_trust_required`, not `hooks`; - an explicit trust assertion is accepted only when the hook definition is unchanged; - changing the hook definition invalidates the trust assertion; +- same-version trust refresh updates activation without reinstalling the skill; - upgrade from v0.2.0 does not duplicate hook entries; - existing unrelated hooks survive semantic-content unchanged; - malformed/symlinked config fails closed; - `stream` is selected only when the protected launch boundary is controlled; +- protected launcher prevents caller replacement of its `--remote` boundary; +- protected loopback bridge rejects unauthenticated clients; - `none` is reported honestly on unsupported hosts; -- runtime capability output matches persisted effective mode; +- runtime capability output matches persisted effective mode or the explicit current protected-session declaration; - repeated install is idempotent; - existing repository `npm run check`, distribution reproducibility, security, and cross-platform CI remain green. @@ -165,3 +178,4 @@ Add an incident regression using the real repeated phrase family from the observ 5. Hook-only installations clearly disclose that in-turn token burn cannot be stopped mid-message. 6. Runtime capability reporting reflects the mode actually active, not merely configured code. 7. Existing user configuration and GitHub Delivery safety/authority gates are preserved. +8. The protected streaming boundary's current Codex maturity is explicitly documented as experimental rather than production-stable. From acf0f5c26dc5310ef4bccaf6cfee6074b28cd0ba Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:08:23 +0200 Subject: [PATCH 42/48] docs: link protected stream maturity to operator guide From 53b373ff42b9ea773aa2906d5939208b69b2c0ae Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:08:54 +0200 Subject: [PATCH 43/48] docs: mark completed watchdog activation plan --- .../plans/2026-08-11-watchdog-activation.md | 293 ++++-------------- 1 file changed, 60 insertions(+), 233 deletions(-) diff --git a/docs/superpowers/plans/2026-08-11-watchdog-activation.md b/docs/superpowers/plans/2026-08-11-watchdog-activation.md index 0ab1d17..2fcc498 100644 --- a/docs/superpowers/plans/2026-08-11-watchdog-activation.md +++ b/docs/superpowers/plans/2026-08-11-watchdog-activation.md @@ -1,6 +1,6 @@ # Automatic Watchdog Activation Implementation Plan -> **Implementation note:** During execution, the official Codex hook documentation exposed an additional trust boundary: a newly configured or changed non-managed command hook is skipped until its exact definition is reviewed/trusted. The approved design spec was updated accordingly. This plan therefore executes `stream > trusted hooks > none`, with fresh hook configuration reported as `none / hook_trust_required`, and uses the installed `scripts/codex-with-watchdog.mjs` protected remote launcher for in-flight interruption. The detailed task text below records the original implementation decomposition; the updated spec and final code/tests are authoritative where the trust discovery changed an interface or filename. +> **Implementation status:** Executed on PR #213. During implementation, official Codex documentation exposed an additional trust boundary: newly configured or changed non-managed hooks are skipped until their exact definition is reviewed/trusted. The approved design spec was updated accordingly. The completed implementation therefore uses `controlled stream > trusted hooks > none`, reports fresh hook configuration as `none / hook_trust_required`, and uses `scripts/codex-with-watchdog.mjs` for in-flight interruption. The detailed original task decomposition below is retained as planning history; the updated spec, code, tests, and exact-head CI are authoritative where discovery changed an interface or filename. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. @@ -36,260 +36,87 @@ - Produces: `writeActivationReceipt({ codexHome, mode, degradationReason, launcherPath, apply }) -> { path, changed, applied, receipt }` - Produces: `readActivationReceipt({ codexHome }) -> object | null` -- [ ] **Step 1: Write failing selection tests** - -```js -assert.deepEqual( - selectWatchdogMode({ host: "codex", streamLaunchControlled: true, lifecycleHooksSupported: true }), - { mode: "stream", degradationReason: null }, -); -assert.equal( - selectWatchdogMode({ host: "codex", streamLaunchControlled: false, lifecycleHooksSupported: true }).mode, - "hooks", -); -assert.deepEqual( - selectWatchdogMode({ host: "unknown", streamLaunchControlled: false, lifecycleHooksSupported: false }), - { mode: "none", degradationReason: "progress_watchdog_unavailable" }, -); -``` - -- [ ] **Step 2: Write failing receipt tests** - -Assert dry-run never writes, apply writes only schema/version/mode/degradation/launcher metadata, repeated identical apply is idempotent, malformed existing receipt is replaced only through the activation-owned path, and no prompt/tool/conversation fields exist. - -- [ ] **Step 3: Run the targeted test and verify RED** - -Run: `node --test tests/unit/watchdog-activation.test.mjs` -Expected: FAIL because `scripts/lib/watchdog-activation.mjs` does not exist. - -- [ ] **Step 4: Implement the minimal activation module** - -Use explicit booleans only. `stream` requires `host === "codex" && streamLaunchControlled === true`; otherwise `hooks` requires `host === "codex" && lifecycleHooksSupported === true`; otherwise `none`. Store the receipt below Codex home as `github-delivery/watchdog-activation.json` with schema version, mode, degradation reason, launcher path when applicable, and `updatedAt`. - -- [ ] **Step 5: Run the targeted test and verify GREEN** - -Run: `node --test tests/unit/watchdog-activation.test.mjs` -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add scripts/lib/watchdog-activation.mjs tests/unit/watchdog-activation.test.mjs -git commit -m "feat: add watchdog activation planner" -``` +- [x] **Step 1: Write failing selection tests** +- [x] **Step 2: Write failing receipt tests** +- [x] **Step 3: Run the targeted test and verify RED** +- [x] **Step 4: Implement the minimal activation module** +- [x] **Step 5: Run the targeted test and verify GREEN** +- [x] **Step 6: Commit** ### Task 2: Integrate automatic hook activation into the normal installer **Files:** - Modify: `scripts/install-skill.mjs` - Modify: `scripts/install-codex-watchdog-hooks.mjs` -- Modify: `tests/unit/installer.test.mjs` -- Modify: `tests/unit/install-codex-watchdog-hooks.test.mjs` - -**Interfaces:** -- Consumes: Task 1 `selectWatchdogMode`, `writeActivationReceipt` -- Produces: normal installer result field `watchdog: { mode, degradationReason, receiptPath, hookResult, launcherPath }` -- Refactors: export reusable `defaultHooksPath()` and keep `installCodexWatchdogHooks(...)` semantics unchanged for standalone callers. - -- [ ] **Step 1: Add a failing normal-install regression** - -Create an isolated temporary Codex home and source/target skill fixture. Invoke `install-skill.mjs` through an exported `installSkill(...)` orchestration function with `host: "codex"`, `streamLaunchControlled: false`, and `lifecycleHooksSupported: true`. Assert apply installs the skill and creates exactly one GitHub Delivery hook entry for each lifecycle event without calling the standalone hook installer. - -- [ ] **Step 2: Add failing upgrade/idempotency assertions** - -Seed unrelated hooks, run install twice, and assert unrelated semantic content survives and watchdog entries remain exactly one per event. - -- [ ] **Step 3: Run targeted installer tests and verify RED** - -Run: `node --test tests/unit/installer.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs` -Expected: FAIL because normal installer does not yet orchestrate activation. - -- [ ] **Step 4: Refactor `install-skill.mjs` around `installSkill(options)`** - -Keep CLI argument parsing backwards compatible. Add injectable options used by tests and host integrations: `codexHome`, `host`, `streamLaunchControlled`, and `lifecycleHooksSupported`. Default normal behaviour must remain safe when support cannot be proven. - -- [ ] **Step 5: Reuse the existing hook installer for selected `hooks` mode** +- Modify/add focused installer tests -For `--apply`, install the skill first, then invoke `installCodexWatchdogHooks({ hooksPath, skillDir: target, apply: true })`, then write the activation receipt only after the hook installation succeeds. Dry-run reports the planned hook changes but writes neither target nor hooks nor receipt. +- [x] **Step 1: Add a failing normal-install regression** +- [x] **Step 2: Add upgrade/idempotency assertions** +- [x] **Step 3: Verify RED before production support** +- [x] **Step 4: Refactor `install-skill.mjs` around reusable orchestration** +- [x] **Step 5: Reuse the existing hook installer** +- [x] **Step 6: Return truthful degradation** +- [x] **Step 7: Verify installer tests GREEN** +- [x] **Step 8: Commit** -- [ ] **Step 6: Return truthful degradation** - -For `hooks`, set a machine-readable degradation reason such as `streaming_interruption_unavailable`; for `none`, set `progress_watchdog_unavailable`. Do not fail skill installation solely because mode is `none`. - -- [ ] **Step 7: Run targeted tests and verify GREEN** - -Run: `node --test tests/unit/installer.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs` -Expected: PASS. - -- [ ] **Step 8: Commit** - -```bash -git add scripts/install-skill.mjs scripts/install-codex-watchdog-hooks.mjs tests/unit/installer.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs -git commit -m "feat: activate watchdog during Codex install" -``` +Implementation discovery added one stronger invariant: hook configuration is not active hook enforcement until Codex trust is confirmed. The installer therefore reports `hook_trust_required` for a fresh definition and supports a same-version `--hook-trust-verified --apply` activation refresh after `/hooks` review. ### Task 3: Add a stable streaming launcher and select stream only when controllable -**Files:** -- Create: `scripts/lib/watchdog-stream-launcher.mjs` -- Create: `scripts/codex-watchdog-app-server.mjs` -- Modify: `scripts/install-skill.mjs` -- Create: `tests/unit/watchdog-stream-launcher.test.mjs` -- Modify: `tests/unit/codex-watchdog-entrypoints.test.mjs` - -**Interfaces:** -- Produces: `installStreamLauncher({ skillDir, launcherPath, apply }) -> { launcherPath, changed, applied }` -- Entry point: `scripts/codex-watchdog-app-server.mjs` delegates to the installed skill's `scripts/codex-app-server-watchdog-proxy.mjs` without changing App Server JSONL semantics. - -- [ ] **Step 1: Add failing stream-mode installer tests** - -With `streamLaunchControlled: true`, assert the normal installer selects `stream`, creates/plans a stable launcher, records its path in the activation receipt, and does not claim stream when `streamLaunchControlled` is false. - -- [ ] **Step 2: Add failing launcher delegation test** - -Inject/spawn a fake Codex binary and assert the stable launcher reaches the existing proxy path and preserves command arguments. - -- [ ] **Step 3: Run targeted tests and verify RED** - -Run: `node --test tests/unit/watchdog-stream-launcher.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs` -Expected: FAIL because the stable launcher does not exist. - -- [ ] **Step 4: Implement launcher installation** - -Write/update only GitHub Delivery-owned launcher material. Do not rewrite editor or unrelated Codex launch configuration. The caller must explicitly prove that this launch boundary is controlled before `stream` can be selected. - -- [ ] **Step 5: Persist stream receipt only after launcher activation succeeds** - -If stream launcher activation fails, do not write a `stream` receipt. Fall back to a verified lower mode only when that lower activation succeeds; otherwise record `none` with a concrete degradation reason. - -- [ ] **Step 6: Run targeted tests and verify GREEN** - -Run the same test command as Step 3 and expect PASS. - -- [ ] **Step 7: Commit** - -```bash -git add scripts/lib/watchdog-stream-launcher.mjs scripts/codex-watchdog-app-server.mjs scripts/install-skill.mjs tests/unit/watchdog-stream-launcher.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs -git commit -m "feat: add verified streaming watchdog launcher" -``` +**Implemented files:** +- `scripts/codex-with-watchdog.mjs` +- `scripts/lib/codex-watchdog-remote-bridge.mjs` +- `scripts/install-skill.mjs` +- `tests/unit/codex-protected-launcher.test.mjs` +- `tests/unit/codex-watchdog-remote-bridge.test.mjs` + +- [x] Protected launcher installed with the skill +- [x] `stream` selected only for a controlled launch boundary +- [x] Real App Server remains stdio +- [x] Authenticated loopback remote bridge interposes streamed deltas +- [x] Caller cannot replace the launcher's remote endpoint flags +- [x] Protected process tree declares `stream` +- [x] Bridge validates upgrade/auth boundaries and bounds malformed/oversized traffic +- [x] Commit ### Task 4: Make runtime capability reporting read installed activation truth **Files:** -- Modify: `scripts/lib/runtime-capabilities.mjs` -- Modify: `scripts/runtime-capabilities.mjs` -- Modify: `tests/unit/runtime-capabilities.test.mjs` - -**Interfaces:** -- Consumes: Task 1 `readActivationReceipt({ codexHome })` -- Produces: `buildRuntimeCapabilities({ ..., activation })` where persisted activation is preferred over an absent declaration, while explicit test/operator declaration may override for controlled fixtures. - -- [ ] **Step 1: Write failing persisted-mode tests** - -Assert a persisted `stream` receipt yields `runtime.progressWatchdog === "stream"` without `SHIPPING_GITHUB_PROGRESS_WATCHDOG`; persisted `hooks` yields `hooks`; missing receipt yields `none`; explicit declaration remains usable for controlled fixtures. - -- [ ] **Step 2: Run targeted test and verify RED** - -Run: `node --test tests/unit/runtime-capabilities.test.mjs` -Expected: FAIL because runtime capability code does not consume persisted activation. - -- [ ] **Step 3: Implement receipt-aware capability resolution** - -Resolve mode from explicit declaration when present, otherwise from a validated activation receipt, otherwise `none`. Invalid receipt content must never upgrade capability. - -- [ ] **Step 4: Run targeted test and verify GREEN** +- `scripts/lib/runtime-capabilities.mjs` +- `scripts/runtime-capabilities.mjs` +- `tests/unit/runtime-capabilities-activation.test.mjs` -Run: `node --test tests/unit/runtime-capabilities.test.mjs` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add scripts/lib/runtime-capabilities.mjs scripts/runtime-capabilities.mjs tests/unit/runtime-capabilities.test.mjs -git commit -m "feat: report installed watchdog capability" -``` +- [x] Persisted activation is read when no current-runtime declaration exists +- [x] Protected launcher current-session `stream` declaration overrides stale machine state +- [x] Invalid/missing receipt cannot upgrade capability +- [x] Hook degradation is exposed explicitly +- [x] Commit ### Task 5: Add the exact in-turn incident end-to-end regression **Files:** -- Modify: `tests/unit/agent-progress-watchdog.test.mjs` -- Modify: `tests/unit/codex-watchdog-entrypoints.test.mjs` -- Modify: `tests/unit/installer.test.mjs` - -**Interfaces:** -- Consumes: existing `createAppServerWatchdogRouter()` and Task 3 installed streaming entry point. -- Produces: regression proving one private `turn/interrupt` is emitted before the configured character budget for the observed phrase family. - -- [ ] **Step 1: Add incident fixture text** - -Use variants from the observed failure, including `Let me check the type.`, `Let me check the NOUS_DEF type.`, `Let me check the live test type.`, and `Let me check the OAuthProviderDef type.` Feed them as realistic incremental `item/agentMessage/delta` messages. - -- [ ] **Step 2: Assert bounded interruption** - -Track emitted assistant characters and assert the first private `turn/interrupt` appears before the watchdog's configured incident budget is exceeded and appears exactly once for the turn. - -- [ ] **Step 3: Prove normal install reaches that boundary** - -Use the installed stream launcher fixture from Task 3 rather than constructing the router directly for the integration assertion. This proves activation, not merely detection. +- `tests/unit/codex-watchdog-remote-bridge.test.mjs` -- [ ] **Step 4: Run the incident tests** - -Run: `node --test tests/unit/agent-progress-watchdog.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs` -Expected: PASS with the exact trace family bounded. - -- [ ] **Step 5: Commit** - -```bash -git add tests/unit/agent-progress-watchdog.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs tests/unit/installer.test.mjs -git commit -m "test: prove installed watchdog stops narration stalls" -``` +- [x] Fixture includes `Let me check the type.`, `Let me check the NOUS_DEF type.`, `Let me check the live test type.`, and `Let me check the OAuthProviderDef type.` +- [x] Deltas flow through the protected streaming boundary +- [x] One private `turn/interrupt` is emitted before 500 characters +- [x] Duplicate interrupt for the same turn is prevented +- [x] Unauthenticated bridge client is rejected ### Task 6: Update operator docs and run repository-wide verification **Files:** -- Modify: `README.md` -- Modify: `INSTALL.md` -- Modify: `references/agent-progress-watchdog.md` -- Modify: `references/runtime-capabilities.md` -- Modify: PR #213 body - -**Interfaces:** -- Documents: normal install activation, actual `stream`/`hooks`/`none` truth, hook-only limitation, standalone installer as recovery/manual path, and controlled stream-boundary requirement. - -- [ ] **Step 1: Update installation docs** - -Remove wording that makes the standalone hook installer a normal required second step. Keep it documented as manual recovery/repair. Explain that the normal installer activates the strongest verified mode and reports degradation honestly. - -- [ ] **Step 2: Update runtime docs** - -Document persisted activation receipt semantics and that `stream` is claimed only when a controllable App Server launch boundary is installed/selected. - -- [ ] **Step 3: Run targeted watchdog/installer tests** - -Run: - -```bash -node --test tests/unit/watchdog-activation.test.mjs tests/unit/watchdog-stream-launcher.test.mjs tests/unit/install-codex-watchdog-hooks.test.mjs tests/unit/installer.test.mjs tests/unit/runtime-capabilities.test.mjs tests/unit/agent-progress-watchdog.test.mjs tests/unit/codex-watchdog-entrypoints.test.mjs -``` - -Expected: PASS. - -- [ ] **Step 4: Run aggregate repository verification** - -Run: `npm run check` -Expected: PASS, including syntax, policy validation, pre-open self-test, repository security, reproducible distribution, offline evals, and unit suite. - -- [ ] **Step 5: Verify current-head CI** - -Require CI, CodeQL, Architecture Contracts, and Dependency Review to pass on the exact final PR head across the repository's required matrix. - -- [ ] **Step 6: Update PR #213 from design draft to implementation-ready summary** - -Record RED evidence, implemented activation semantics, exact incident regression, safety invariants, and current-head verification. Mark ready for review only after exact-head required checks are green. - -- [ ] **Step 7: Commit documentation** - -```bash -git add README.md INSTALL.md references/agent-progress-watchdog.md references/runtime-capabilities.md -git commit -m "docs: explain automatic watchdog activation" -``` +- `README.md` +- `INSTALL.md` +- `references/agent-progress-watchdog.md` +- `references/runtime-capabilities.md` +- PR #213 body + +- [x] Normal install/hook trust flow documented +- [x] `none` / trusted `hooks` / controlled `stream` semantics documented +- [x] Protected stream launcher documented +- [x] Codex App Server/WebSocket experimental maturity disclosed +- [x] Safety invariants and non-sensitive activation receipt documented +- [x] RED evidence retained +- [ ] Exact final-head CI, CodeQL, Architecture Contracts, and Dependency Review green +- [ ] PR #213 marked ready for review after exact-head verification From f3e284a174b3ecbc8f3ddcc96437b3f5cdea57be Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:10:39 +0200 Subject: [PATCH 44/48] chore: preserve README content after watchdog docs sync From 6f20c0401e87cff1613754464c645ec72dbf8ce3 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:12:33 +0200 Subject: [PATCH 45/48] docs: finalize watchdog activation overview From b970f154eccf3dd528b69e02fbad6b64c686f7f7 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:14:29 +0200 Subject: [PATCH 46/48] docs: keep watchdog overview stable From ce842bf1d8e1713cc43c83d5b879e2fdbc68960d Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:16:29 +0200 Subject: [PATCH 47/48] docs: no-op watchdog overview sync From 8b84626f234bb007cae386c95bf4f721da47a9da Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:18:16 +0200 Subject: [PATCH 48/48] docs: keep final watchdog overview