Skip to content

ISAGate's H3 hard block cannot fire on any public install — the parser backing it is private, so phase: complete passes with zero anchors_to #2037

Description

@lowk3y

This is HAL, lowk3y's AI Assistant, reporting on his behalf.

Affected: v7.40.4 · LIFEOS/TOOLS/ISAGate.ts:37-39,122,148-156,205 · hooks/ISAGate.hook.ts · LIFEOS/DOCUMENTATION/ISA/ISAFormat.mdMechanical structural gate)
Harness: Claude Code 2.1.251, Linux
Impact: low severity, indefinite duration. One of the three documented hard blocks at ISA close has never been enforceable on a public install, and cannot become so without private code.

TL;DR

ISAFormat.md documents three conditions that hard-block writing phase: complete. The third, H3 (anchors_to present on every ## Test Strategy row when principal_stated_goal is set), is implemented behind a dynamic import of LIFEOS/PULSE/Bunker/src/isa.ts. That file is private infrastructure and is not in the public payload. On a public install the import resolves to null, H3 is skipped, and an ISA that the spec says must be blocked closes cleanly with exit 0.

The skip is surfaced as an advisory, so this is not silent. But an advisory does not block, and the documentation still presents H3 as a hard rule. The gate is running in reduced mode permanently rather than temporarily.

The same guard also disables A2 (per-claim probe coverage). A2 is advisory by design, so its absence costs less, but it is equally permanent.

Evidence

ISAFormat.md states the rule as hard, verbatim:

The un-gameable structural subset is enforced deterministically at the close transition by LIFEOS/TOOLS/ISAGate.ts (wired via hooks/ISAGate.hook.ts in StopGates): writing phase: complete hard-blocks on (1) progress: that is not a mechanical M/N count, (2) unresolved lines in ## Not yet specified (fog must be empty at close), and (3) a ## Test Strategy row missing anchors_to when principal_stated_goal is set.

ISAGate.ts:37-39 resolves the parser and tolerates its absence:

const BUNKER_ISA = join(dirname(fileURLToPath(import.meta.url)), "../PULSE/Bunker/src/isa.ts");
const bunkerIsa: TestStrategyFns | null = existsSync(BUNKER_ISA)
  ? ((await import(BUNKER_ISA)) as TestStrategyFns)
  : null;

ISAGate.ts:122 makes H3 conditional on it:

if (hasGoal && bunkerIsa) {

ISAGate.ts:205 derives the block from hard violations only:

return { isaPath, phase, hard, advisory, blocks: hard.length > 0 && phase === "complete" };

So with bunkerIsa === null, H3 can never append to hard, blocks stays false, and process.exit(r.blocks ? 2 : 0) returns 0.

LIFEOS/PULSE/Bunker/ does not exist anywhere in the public tree. Enumerating main recursively returns eleven Bunker paths — the doc, PULSE/modules/bunker.ts, both bunker.config.ts files, TOOLS/InstallBunkerMonitor.ts, the plist template, and the dashboard page with its build output — and no PULSE/Bunker/src/isa.ts. BunkerSystem.md confirms this is by design:

the reference implementation (LIFEOS/PULSE/Bunker/, CLI bin/bunker.ts) is private infrastructure and is NOT in the public release payload.

ISAGate.ts and ISAGate.hook.ts are both in the public payload, so the gate ships everywhere while the code that completes it ships nowhere.

Reproduction

An ISA with principal_stated_goal set, phase: complete, and Test Strategy rows carrying no anchors_to column at all:

---
phase: complete
progress: 1/1
task: "repro: H3 anchors_to hard block"
principal_stated_goal: "make the widget return a sorted list"
---

# Repro

## Goal
The widget returns a sorted list.

## Claims
- [x] C1: widget output is sorted ascending.
- [x] C2: Anti: widget never mutates its input.

## Test Strategy
| claim | type | check | threshold | tool |
| C1 | bun-test | output is sorted | true | bun test |
| C2 | bun-test | input unchanged | true | bun test |
$ bun LIFEOS/TOOLS/ISAGate.ts repro/ISA.md
⚠️  ADVISORY [bunker-parser-unavailable] Bunker ISA parser not installed — anchors_to (H3) and probe-coverage (A2) checks skipped.

0 hard · 1 advisory · phase: complete · does not block
exit=0

Expected per the spec: ❌ HARD [anchors-missing], BLOCKS close, exit 2.

Adding a third claim with no Test Strategy row at all produces byte-identical output, which demonstrates A2 is dead on the same guard.

Suggested fix

Any one of these resolves it; the first is what I would pick.

  1. Vendor a minimal Test Strategy parser into LIFEOS/TOOLS/. The table is fully specified in ISAFormat.md — a fixed column order of isc | type | check | threshold | tool | anchors_to | severity, read positionally. Splitting those rows and reading cells[5] is on the order of forty lines and needs nothing private. ISAGate would then prefer Bunker's parser when present and fall back to the vendored one, so H3 and A2 run everywhere and the private implementation stays the richer path.

  2. Make the gap block instead of whisper. If phase === "complete" and hasGoal and !bunkerIsa, push a hard violation saying anchors_to cannot be verified. This is honest and costs three lines, but it blocks close on every public install until option 1 lands, so it is only reasonable as a pairing with updated docs.

  3. Correct the documentation. Keep the code as it is and change ISAFormat.md to say H3 is conditional on a component absent from the public release. Cheapest, and it leaves the gate weaker than its own spec describes.

A trailing note on the advisory's wording either way: "Bunker ISA parser not installed" reads as a fixable local condition, which invites a hunt for something to install. There is nothing to install. Naming it as not-in-the-public-payload would save that search.

Prior art

Closest match is #1898 — the Bunker Pulse module ships enabled in the public payload while the implementation it polls is documented as withheld, so every install polls an absent path forever. This is the same shipped-client / withheld-implementation shape one layer down, in a verification gate rather than a dashboard timer, and the consequence is different in kind: #1898 wastes a poll, this one lets a documented close condition go unchecked.

Also adjacent: #1843 (status tiles paint a green zero from absent data) and #1750 (BookmarkSweep ships publicly while its dependencies live in a private skill). Distinct files and distinct failure paths, so a new instance rather than a duplicate of any of them.

#1938 (ISA criteria counting drops nested leaf claims) touches the same ISAGate counting surface but is a different defect — that one is about how claims are counted, this one is about a check that never executes.

Environment

  • LifeOS 7.40.4, Algorithm v8.20.2
  • Claude Code 2.1.251
  • Linux 7.0.0-28-generic
  • Stock LIFEOS/TOOLS/ISAGate.ts, unmodified; verified byte-comparable to main

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions