Skip to content

v0.59.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 07:15
· 116 commits to main since this release

[0.59.0] - 2026-09-21

Intent vocabulary and owners

  • Add canonical hm intent commands, role-aware advisory approvals, and explicit
    legacy migration. Project purpose, metrics, rules and open questions share the
    canonical vocabulary; intent records remain a 1:N grouping of SPECs.
  • Preserve approvals, timestamps, history and record bodies when migrating to
    intent/<ID>.md and .claude/intent/metrics.yaml. Serialize migration with
    writers and require interrupted migration to finish before writes resume.
    hm world remains a deprecated compatibility alias for one release.

BREAKING

  • dev_mode is removed; spec.strictness: block | warn replaces it. The axis existed
    because it decided which stages a harness had. Since /hm:plan was absorbed every task enters
    through a SPEC, so it only decided how strictly the SPEC gates fire — a preset default, not a
    second axis. Production now derives block, Side derives warn, and
    /hm:configure / harness-maker make --strictness override it. This reverses the earlier
    decision that dev_mode was orthogonal to preset with all four crosses allowed
    — every
    cross is still expressible, as a preset plus an explicit strictness.

    What changes at warn: the gates this axis owns render and block on none. /hm:verify
    has six checks at every strictness (Check 6 reports a missing SPEC operation as a WARN),
    /hm:execute records its SPEC-need verdict, and /hm:review passes the machine SPEC to the
    grade. Only the spec_gate hook — whose one job is to block — renders at block alone, and
    it now takes its severity from strictness instead of the preset-written
    security.gates.spec_gate literal, which used to defang an explicit block on a Side project.
    Two gates are deliberately NOT on this axis: /hm:wrapup Step 3.5's find-unbound and the
    judgment-AC binding gate still follow preset, because that is preset depth and this change
    does not move it.

    What you need to do: nothing, and re-render when convenient. The loader translates an
    existing dev_mode once, with one advisory — spec-driven→block, task-driven→warn,
    anything else→block — and never touches your preset, so a Production harness that chose
    the relaxed gates keeps both. A config with no strictness at all now derives it from the preset
    (a Production harness that lost its key is strict; it used to relax). spec_need's verify
    oracle is the one exception and still enforces unless warn is written explicitly.
    --dev-mode is gone from harness-maker make, spec_machine check / waiver-check and the
    Codex bootstrap, with no alias; a harness rendered earlier keeps calling the plugin version it
    was rendered with, which still accepts it. Onboarding asks one question fewer.

  • /hm:plan is removed. The stage, its interview and its auto-advance gate are gone from
    every target; there is no stub. The PLAN document survives — /hm:execute Step 0 now
    authors it (phases, file lists, order, risks, exit criteria) with no human gate, and every
    later stage reads the same artifact with the same frontmatter contract. The decisions that
    were worth a second interview — the ones that cannot be cheaply undone — live in the SPEC,
    which already elicits them as irreversible_decisions.

    Why: measured across the 21 task slugs in this repo that carry both an interviewed SPEC
    and a PLAN, the two stages cost 9.70 interview rounds per task (SPEC 4.65 + PLAN 5.05).
    A merged single interview costs 6.55 — a 32 % reduction, stable across three different
    absorption models. Of 194 classified PLAN interview entries, 52 % are questions the SPEC
    interview already asks and 21 % are IC questions that should never have reached a human.

    What you need to do: nothing, but re-render. An existing harness.yaml whose
    autonomy.pipeline names plan keeps loading — the entry is dropped once, with one
    advisory, and /harness-maker:make --update persists the new pipeline. AtomicStage.PLAN
    is removed from the schema, so a script constructing that enum member by name must stop.

  • plan-validator is replaced by spec-validator. Same critic, relocated to /hm:spec
    and re-scoped to what the DRI must decide: missing or contradictory ACs, circular oracles,
    irreversible decisions the ACs imply but do not list, and scope boundary. It runs once,
    only when the SPEC declares or implies an irreversible decision, and it never blocks
    approval — the agent it replaces returned APPROVED 0 times in 54 runs, which is unmeasured
    discrimination rather than proven findings, and an unproven gate should not hold a release
    while it is being measured. Every dispatch now writes a stage-agents.jsonl row so that
    measurement starts at run 1.

Added

  • hm spec_need frontmatter-upsert — writes spec_need_verdict / spec_need_target into
    a PLAN's frontmatter without overwriting a value already there. /hm:execute Step 0.1
    calls it. /hm:verify Check 6 reads those keys and treats an absent one as PASS (N-A), so
    a writer that silently no-ops turns that gate permanently green; the verb exists to give that
    behaviour an execution surface a property test can quantify over.

  • Codex-native hm-make and hm-update skills now run a bundled bootstrap, select
    the matching stable engine, and regenerate Codex project assets while preserving
    custom blocks. Plugin, engine and project observations are reported separately;
    a failed command cannot report a complete update.

  • Claude is available as a second-opinion provider through the saved Claude CLI
    authentication, with safe-mode, no tools, isolated working directory, bounded
    input/output and process-group cleanup. Existing provider and ledger behavior
    remains compatible.

  • Internal Codex bootstrap helpers validate plugin/engine version identity and track
    plugin, engine and project update completion independently. A bounded Claude result
    parser validates successful structured responses and reports fixed error codes.
    The lifecycle and provider entrypoints above now connect these helpers to execution.

Fixed

  • Preserve Codex/Claude integration after PLAN-stage retirement. Legacy
    second_opinion.agents entries for plan-validator migrate to spec-validator
    through the shared configuration model, preserving custom agents and opt-outs.
    The validator declares the configured providers, including Claude, and the English
    README again documents native Codex install/update entrypoints. Execute accepts
    an absent PLAN for Step 0 to create; concurrent SPEC-decision writes now lock the
    entire read-modify-write operation so the first recorded decision survives.

  • A mutant that breaks the module at import time is no longer counted as surviving.
    mutmut.tests_pass is return returncode != 1, so every exit status except 1 reads as
    "the tests passed": pytest exits 2 on a collection error and pytest-xdist exits 2 when
    -x stops the session. Measured 2026-09-20 — mutating ACType = Literal["mechanical", …]
    fails 8 tests at collection and mutmut recorded it as survived. measure_baseline now
    wraps the SPEC's runner in harness_maker.mutation_runner, which maps every non-zero exit
    to 1 (mutmut runs the runner through shlex.split with no shell, so || exit 1 is not
    available). Every mutation score this project has reported was deflated by its own
    import-breaking mutants.

  • .mutmut-cache is untracked and ignored. mutmut run re-runs every untested mutant in
    the cache, not only the ones under --paths-to-mutate; the tracked copy carried 52
    cache.py mutants from 0.18.0, so a run aimed at two other modules mutated cache.py.
    This is the most plausible cause of the execute-stage gate reporting zero mutants for a
    SPEC that named real paths.

  • Land-hold tests now reference the accepted SPEC's current AC tables and preserve
    explicit subject-cap boundary cases, restoring full-suite collection.

  • The intent layer's withdrawal criterion can fire again. hm world gap --json's
    withdrawal block judged a cumulative count — objectives_observed == 0 — so a single
    objective ever closed with an observed: verdict pinned due to false for the life of the
    project. It now counts hm:wrapup events since the layer last did anything: a measurement
    recorded, an objective created, approved or closed. The block reports
    {filled_at, last_signal_at, quiet_wrapups, revisit_candidates_now, due, reason};
    objectives_observed and wrapups_since_fill are removed, which is a breaking change for any
    out-of-repo consumer that parsed them. A stored instant ahead of now is skipped rather than
    used, so a mistyped future date cannot suppress the criterion; the returned value is stripped,
    closing a crash where a whitespace-padded YAML timestamp passed the producer's parser and
    failed the consumer's. filled_at is now resolved only when nothing has ever signalled — it
    is the fallback cutoff, and the git walk behind it was being paid on every read to fill a
    field the verdict never consulted. hm world status --json is unchanged.

  • The approval stamp names which field moved, not just that the hash mismatched.
    spec_machine's approval stamp now carries a per-field digest map alongside the aggregate
    content hash, so a hold reports the one field that changed instead of an undifferentiated
    hash mismatch. The digest map lives inside the stamp, which the existing hash deny-list
    already excludes, so adding it does not invalidate any prior approval. A pre-digest stamp
    keeps its recorded verdict and says it cannot name fields, rather than failing closed.

  • spec_machine's three writers (approve, mark-tested, mark-judged) share one lock.
    io_utils.rmw_lock extracts the flock-based read-modify-write lock previously private to
    world.py into a reusable helper, closing a race between concurrent writers to the same
    machine SPEC. A malformed SPEC now reports the underlying pydantic validation error in its
    detail (bounded, moved into the state constructor so no return branch can bypass the cap)
    instead of a generic failure.

  • worktree.py's DELIVERABLE_PREFIXES gained "MUTATION". .gitignore had carried
    !work-docs/MUTATION-*.md since 865e3ef5 without the matching source-of-truth entry,
    leaving tests/structural/test_deliverable_single_source.py red on main.

  • A mutant that breaks the module at import time is no longer counted as surviving.
    mutmut.tests_pass is return returncode != 1, so every exit status except 1 reads as
    "the tests passed": pytest exits 2 on a collection error and pytest-xdist exits 2 when
    -x stops the session. Measured 2026-09-20 — mutating ACType = Literal["mechanical", …]
    fails 8 tests at collection and mutmut recorded it as survived. measure_baseline now
    wraps the SPEC's runner in harness_maker.mutation_runner, which maps every non-zero exit
    to 1 (mutmut runs the runner through shlex.split with no shell, so || exit 1 is not
    available). Every mutation score this project has reported was deflated by its own
    import-breaking mutants.

  • .mutmut-cache is untracked and ignored. mutmut run re-runs every untested mutant in
    the cache, not only the ones under --paths-to-mutate; the tracked copy carried 52
    cache.py mutants from 0.18.0, so a run aimed at two other modules mutated cache.py.
    This is the most plausible cause of the execute-stage gate reporting zero mutants for a
    SPEC that named real paths.

Boundary tests (advisory)

Layer 1 of PLAN-test-fidelity-gap. Pure parser checks; advisory only.

Status: ✅ PASS

============================= test session starts ==============================
platform linux -- Python 3.12.14, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/runner/work/harness-maker/harness-maker
configfile: pyproject.toml
plugins: respx-0.23.1, hypothesis-6.155.7, asyncio-1.3.0, anyio-4.13.0, xdist-3.8.0
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 50 items

tests/integration/test_boundary_codex_toml.py ............               [ 24%]
tests/integration/test_boundary_cursor_mdc.py .......                    [ 38%]
tests/integration/test_boundary_harness_yaml.py ........                 [ 54%]
tests/integration/test_boundary_hooks_json.py .........                  [ 72%]
tests/integration/test_boundary_meta.py .......                          [ 86%]
tests/integration/test_boundary_settings_json.py .......                 [100%]

============================== 50 passed in 7.26s ==============================