Skip to content

v5.15.0

Choose a tag to compare

@Kanevry Kanevry released this 28 Oct 21:55
· 6 commits to main since this release

Changed

  • The CI reading says when a pipeline is still running or was canceled (#856): checkCiStatus() gains an optional phase (running | canceled); status keeps its three values green / red / unknown, so a caller that never reads phase sees no change. phase: 'canceled' is set only when no failed pipeline sits behind the canceled one and none was set aside for the same commit; otherwise the reading stays the red alert it was. The dispatcher no longer cuts a candidate's readiness to a quarter for either phase, the autonomy gate G3 reads both as "no verdict" with their own warning, and the session-start banner prints CI running on HEAD … — no verdict yet or last CI run on HEAD was canceled … — no verdict instead of "could not be determined" or 🚨 CI RED. scripts/release.mjs still accepts only green. Replayed over the last 100 real pipelines: no status changed, the two canceled ones carry phase.

Fixed

  • so-hud no longer reports a failed question as the owner's refusal (#1856): the protection gate wrapped $.ui.ask in .catch(() => null) for both outward-action steps and the collision question. A question that could not be shown therefore looked like a closed dialog, the model was told "Der Owner hat … abgelehnt … nicht erneut versuchen", and aktionen.jsonl recorded abgelehnt although the owner never decided.
    • The gate still refuses in that case, but now says the question could not be shown and that the owner did not decide, and records ergebnis: "fehler" (already allowed by the receipt schema).
    • The engine rejects $.ui.ask with the same HooksError when the owner answers without a label: …: no answer (the dialog was dismissed) for a closed dialog, …: no answer (The user doesn't want to proceed …) for Esc (claude-code 2.1.312). Both stay a refusal; any other or unknown wording counts as a failed question, so a changed engine message can never invent an owner decision.
  • Codex hooks start on native Windows (#1849): every handler in hooks/hooks-codex.json started through SO_PLATFORM=codex … sh hooks/run-node.sh …. PowerShell reads SO_PLATFORM=codex as a command name and cmd.exe has no env prefix, so on native Windows no hook ran, the guards included (reproduced 2026-10-28: Windows 11, PowerShell 5.1.26100, exit 1).
    • Each handler now also carries commandWindows: set NoDefaultCurrentDirectoryInExePath=1&& node -e "require(require('path').join(require('path').isAbsolute(process.env.PLUGIN_ROOT||'')?process.env.PLUGIN_ROOT:process.exit(1),'hooks','codex-win.cjs'))" <handler>.mjs. Codex runs Windows hooks through %COMSPEC% /C (cmd.exe; codex-rs hooks/src/engine/command_runner.rs). The set stops cmd.exe from running a node planted in the session directory before the one on PATH; node reads PLUGIN_ROOT itself and uses it only when it is absolute, so a plugin root with spaces needs no quoting and a relative one cannot load a launcher from the session directory's node_modules. The new hooks/codex-win.cjs sets SO_PLATFORM=codex and CODEX_PLUGIN_ROOT and runs the handler with stdin/stdout/stderr and exit code passed through. The banner prints through node -e console.log because cmd.exe pipes on |.
    • Measured natively (PowerShell 5.1 and cmd.exe, Node 24.14.0, root with spaces): the destructive guard denies printf x > CLAUDE.md and allows ls -la; an empty or relative PLUGIN_ROOT exits 1 without loading anything. A real Codex run on Windows needs the owner's hook trust and is not measured yet.
    • The Codex plugin contract requires commandWindows on every handler, in exactly this form and for the same handler as command (windows-entry, trusted-windows-wrapper, windows-entry-mismatch). The changed hook file needs a new trust approval in Codex.
  • An abandoned stub now says whether it did any work (#1835): a backfilled abandoned record carried total_waves / total_agents / total_files_changed as placeholder zeros, so a count of status: "abandoned" mixed empty restarts with sessions that ran agents. On one fleet ledger, two of six stubs since 2026-10-25 carried 15 and 86 agent.stopped events.
    • The backfill now writes an optional work_evidence field: { platform, turn_stops, agent_stops, wave_completions, activity_events }, event counts from the session's own events. Turn stops are counted under the current name and under the pre-2026-09-06 name orchestrator.session.stopped, and the larger of the two counts is used. Every event beyond start/end bookkeeping counts as activity, and so does every Skill call in skill-invocations.jsonl.
    • The field is written only when the session's history was read whole, it started fresh (native_source startup/clear, not compact/resume), it has exactly one raw id, and its platform emits turn stops (claude, codex). Otherwise it is absent, never 0.
    • A null line in events.jsonl no longer turns a backfill into an error.
    • classifyStubWork() sorts a stub into empty / conversation / dispatched / unmeasured.
    • The new read-only scripts/session-ledger-census.mjs classifies old stubs by joining the events ledger. It prints close and abort rates with and without measured empty restarts, and names every population. --stubs lists each stub with its class. status stays abandoned.
  • A child-process timeout under load no longer blocks the push (#1673): the full gate re-runs failing test files serially when the parallel run failed only by timeouts, but it recognised only vitest's own Test timed out in Nms. A spawnSync(…, {timeout}) or execFileSync(…, {timeout}) that runs out reaches vitest as Error: spawnSync <cmd> ETIMEDOUT (measured, vitest 4.1.12), so the whole run counted as a real failure. On 2026-10-28 a push whose only red tests were one vitest timeout and one spawnSync /bin/zsh ETIMEDOUT was blocked for that reason. At base 888da931, 143 test files set a timeout: option next to such a call (upper bound, rg).
    • The classifier now accepts that error line too. Everything else stays fail-closed: an assertion that mentions ETIMEDOUT, other errnos, Command failed: …, a Serialized Error or Caused by: line, and any mixed run. The serial re-run still has to pass, so a real hang stays red.
    • A red test step that gets no re-run now says why: orchestrator.quality_gate.* carries rerun_skipped (e.g. non-timeout-failure, too-many-files, gate-timeout), from the CLI and from the remote pre-push route. None of the 16 quality_gate.failed events since 2026-10-18 carried a re-run or a reason.
    • tests/so-hud/hud.test.mjs accepts discovery-timeout beside discovery-fallback: under load the 700 ms git worktree list ceiling fires, and that assertion was the one red test the re-run may never rescue.
  • Leftover process groups of the gate itself and of its re-run reach the event (#1708): leftover_groups now also lists the gate sub-script's own reaped group (command: "gate", or the gate command in the library emitter) and the serial re-run's (command: "test-rerun"). The re-run's timed_out, kill_signals and survivors join the event's kill fields.
  • The security delta reads pnpm 12 lockfiles again (#1837): with a packageManager field, pnpm 12 writes the package manager's own dependencies as a first YAML document ahead of the project lockfile. yaml.parse refused two documents (MULTIPLE_DOCS), so the whole file counted unparsed and none of its new packages reached OSV. readLockfileDelta now reads every document and merges their packages; a document with errors still counts as unparsed.
  • A session without a navigator boundary now gets the Standard-Grenzsatz (#1842): fleet-auflagen.mjs --render printed nothing for an absent or rejected conditions file, or an accepted file without grenze, so a session could search across repos with no boundary rule in its prompt.
    • scripts/lib/fleet-protocol.mjs exports STANDARD_GRENZSATZ; renderAuflagenNotice() is never empty. An accepted navigator grenze is still printed verbatim. For absent, rejected, an accepted file without grenze, or a withheld grenze (control characters, not a string, empty or blank; the withheld line comes first), it prints the Standard-Grenzsatz under the header fleet-auflagen: grenze (Standard-Grenzsatz: no navigator boundary accepted, binding):.
    • The boundary binds immediately and fail-closed; unlike the other Standard-Auflagen it does not wait for fallback_due_at. Documented in skills/_shared/fleet-protocol.md § Standard-Auflagen, skills/session-start/SKILL.md Phase 7.6 and the operations contract.
  • A fleet check-in no longer fails when another repo holds the semantic session id (#1839): the semantic id collides across repos, and fleet-checkin.mjs then exited 2 without a trace. The navigator was also observed writing a session's conditions under the native session UUID while the session had checked in under the semantic id.
    • The check-in carries an optional session_uuid; the CLI stamps it from this process's native id when that is a native UUID (readProcessLocalSessionIds(): CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID by platform, none when an inherited id contradicts the other; a process-local witness that overrides input). When another repo's check-in holds checkin/<semantic id>.json, the file is written once more under checkin/<session_uuid>.json; session stays the semantic id. stdout gains key and key_fallback.
    • Every refusal emits the new orchestrator.fleet.checkin_rejected (reason occupied|changed, fallback uuid|none), also when the UUID retry then fails with a write error (exit 1). Exit 2 now means nothing was written.
    • fleet-auflagen.mjs <session_id> [<session_id>...] [--render] tries the same process-local native id first, then each positional id; among accepted files the newest zeit wins (a resumed session keeps its UUID, so an older UUID-keyed file must not shadow a fresh semantic one; a zeit outside the UTC-seconds form or more than 5 minutes ahead ranks last), else the first rejected, else absent. The result carries key; the repo check per key is unchanged.
  • The dialectic deriver no longer aborts when the input is slightly over budget (#1840): on 2026-10-28 this repo's input estimated 34,392 tokens against the default dialectic.budget-tokens of 32,000, so every run ended budget-exceeded before the agent.
    • runDialecticDeriver() now drops the lowest-ranked learnings as whole records until the prompt fits, tells the model the actual top_n_learnings, and reports learnings_used / learnings_dropped in the result. No text inside a record is cut.
    • budget-exceeded is returned only when even zero learnings do not fit (the fixed load of peer cards, steering and sessions); used is then the full-input estimate, as before.
  • Session start reads the canonical VCS when origin is only a mirror (#1846): in a clone with a GitHub snapshot mirror on origin and the canonical GitLab remote beside it, detectVcsFamily() reported ambiguous: true and both of its callers took the mirror. The mode selector's backlog scan read 7 GitHub issues instead of 148 GitLab issues, the one priority::critical among them included, and the CI banner would have read the mirror's checks. The Session Config vcs: value now breaks an AMBIGUOUS tie (detectVcsFamily({ preferVcs }), new exports normalizeVcsPreference and readConfiguredVcs; only the leading token counts, so vcs: github — prose works). Unambiguous remotes keep their answer and an explicitly passed vcs still wins. Measured over 145 repos on one host: 19 ambiguous, 4 change family, each to its configured value.
    • npm run update:local fetches and fast-forwards from main's configured upstream (branch.main.remote / branch.main.merge) instead of a fixed origin/main, and falls back to origin/main only without an upstream. updateRepo is exported with a ctx.root and tested against real git repositories.
  • A skill call is no longer counted twice when the skill ledger is tracked (#1841): backfill and census added up the skill-invocations.jsonl rows of the main checkout and of the worktree, so a row committed into both counted twice in work_evidence.activity_events. countSkillInvocationsAcrossFiles takes the highest count per row identity (timestamp, skill, session, event) across the files instead of the sum; distinct rows still add up.
  • The fleet check-in audit keys duplicates on session and repo (#1843): the same semantic session id in two repos no longer marks both records ambiguous; two records with the same id in one repo are still duplicate-session.
  • The worktree-removal guard measures worktrees whose path differs from git's only in letter case (#1829). On macOS git reports the spelling on disk (_wt/FeedFoundryV2/…) while the guard compared it as a string with the spelling git lists (_wt/feedfoundryv2/…), so every such removal was denied as "not measurable". The guard now compares the directory itself (device and inode), and a denial says which step failed.