Repository navigation
v0.59.0
[0.59.0] - 2026-09-21
Intent vocabulary and owners
- Add canonical
hm intentcommands, 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>.mdand.claude/intent/metrics.yaml. Serialize migration with
writers and require interrupted migration to finish before writes resume.
hm worldremains a deprecated compatibility alias for one release.
BREAKING
-
dev_modeis removed;spec.strictness: block | warnreplaces it. The axis existed
because it decided which stages a harness had. Since/hm:planwas absorbed every task enters
through a SPEC, so it only decided how strictly the SPEC gates fire — a preset default, not a
second axis.Productionnow derivesblock,Sidederiveswarn, and
/hm:configure/harness-maker make --strictnessoverride it. This reverses the earlier
decision thatdev_modewas orthogonal topresetwith 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 aWARN),
/hm:executerecords its SPEC-need verdict, and/hm:reviewpasses the machine SPEC to the
grade. Only thespec_gatehook — whose one job is to block — renders atblockalone, and
it now takes its severity from strictness instead of the preset-written
security.gates.spec_gateliteral, which used to defang an explicitblockon a Side project.
Two gates are deliberately NOT on this axis:/hm:wrapupStep 3.5'sfind-unboundand the
judgment-AC binding gate still followpreset, 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
existingdev_modeonce, 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 unlesswarnis written explicitly.
--dev-modeis gone fromharness-maker make,spec_machine check/waiver-checkand 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:planis removed. The stage, its interview and its auto-advance gate are gone from
every target; there is no stub. The PLAN document survives —/hm:executeStep 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 asirreversible_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.yamlwhose
autonomy.pipelinenamesplankeeps loading — the entry is dropped once, with one
advisory, and/harness-maker:make --updatepersists the new pipeline.AtomicStage.PLAN
is removed from the schema, so a script constructing that enum member by name must stop. -
plan-validatoris replaced byspec-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 returnedAPPROVED0 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 astage-agents.jsonlrow so that
measurement starts at run 1.
Added
-
hm spec_need frontmatter-upsert— writesspec_need_verdict/spec_need_targetinto
a PLAN's frontmatter without overwriting a value already there./hm:executeStep 0.1
calls it./hm:verifyCheck 6 reads those keys and treats an absent one asPASS (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-makeandhm-updateskills 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.agentsentries forplan-validatormigrate tospec-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_passisreturn 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
-xstops the session. Measured 2026-09-20 — mutatingACType = Literal["mechanical", …]
fails 8 tests at collection and mutmut recorded it as survived.measure_baselinenow
wraps the SPEC's runner inharness_maker.mutation_runner, which maps every non-zero exit
to 1 (mutmut runs the runner throughshlex.splitwith no shell, so|| exit 1is not
available). Every mutation score this project has reported was deflated by its own
import-breaking mutants. -
.mutmut-cacheis untracked and ignored.mutmut runre-runs every untested mutant in
the cache, not only the ones under--paths-to-mutate; the tracked copy carried 52
cache.pymutants from 0.18.0, so a run aimed at two other modules mutatedcache.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
withdrawalblock judged a cumulative count —objectives_observed == 0— so a single
objective ever closed with anobserved:verdict pinneddueto false for the life of the
project. It now countshm:wrapupevents 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_observedandwrapups_since_fillare removed, which is a breaking change for any
out-of-repo consumer that parsed them. A stored instant ahead ofnowis 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_atis now resolved only when nothing has ever signalled — it
is the fallback cutoff, and thegitwalk behind it was being paid on every read to fill a
field the verdict never consulted.hm world status --jsonis 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_lockextracts the flock-based read-modify-write lock previously private to
world.pyinto 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'sDELIVERABLE_PREFIXESgained"MUTATION"..gitignorehad carried
!work-docs/MUTATION-*.mdsince865e3ef5without the matching source-of-truth entry,
leavingtests/structural/test_deliverable_single_source.pyred onmain. -
A mutant that breaks the module at import time is no longer counted as surviving.
mutmut.tests_passisreturn 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
-xstops the session. Measured 2026-09-20 — mutatingACType = Literal["mechanical", …]
fails 8 tests at collection and mutmut recorded it as survived.measure_baselinenow
wraps the SPEC's runner inharness_maker.mutation_runner, which maps every non-zero exit
to 1 (mutmut runs the runner throughshlex.splitwith no shell, so|| exit 1is not
available). Every mutation score this project has reported was deflated by its own
import-breaking mutants. -
.mutmut-cacheis untracked and ignored.mutmut runre-runs every untested mutant in
the cache, not only the ones under--paths-to-mutate; the tracked copy carried 52
cache.pymutants from 0.18.0, so a run aimed at two other modules mutatedcache.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 ==============================