Skip to content

fix: reconcile declared vs derived in the sizing faculty (#274) - #275

Merged
Jammy2211 merged 3 commits into
mainfrom
claude/intake-declared-difficulty-icstba
Aug 24, 2026
Merged

Jammy2211 merged 3 commits into
mainfrom
claude/intake-declared-difficulty-icstba

Conversation

@Jammy2211

Copy link
Copy Markdown
Contributor

Summary

Closes #274. Third instance of one family: a conductor derives a value from prose and ignores the header key the author declared. (1) the Feature Agent's ranker — fixed, #217; (2) the Bug Agent, over a declared Type: bug; (3) the Intake Agent, over a declared Difficulty:.

Intake never read a declared difficulty at all. analyse() hand-built the prompt-shaped dict it passes the sizing faculty, omitting the declared_header() keys the file-loading path folds in — so estimate_difficulty ran with no declared key present, nothing reconciled derived against declared, and the derived level was persisted into the header, silently replacing what the input asked for. Reported case: raw text declaring Difficulty: medium filed as too-large (score 11), unchanged when the full header block was prepended, hand-corrected after --apply.

That matters because the heuristic's biggest input is prompt length, which is a bad size proxy — a prompt is long when it carries a design and the sibling tasks' traps, not when the work is large. Declaring a level is the documented escape hatch for exactly that inversion (complete/2026/08/feature-ranker-ignores-header-keys.md), and on this path it did nothing.

Per the prompt, the precedence rule moves into the sizing faculty — the one place all three conductors already call — rather than being re-implemented per conductor. Three reconciliations were three chances to forget one, and two forgot.

API Changes

  • _sizing.effective_difficulty(p) — the single precedence rule (moved from _feature.py): declared wins, derived level + score returned alongside so a disagreement is reported rather than silently resolved.
  • _sizing.declared_header now also reads Type: and Autonomy:; new _sizing.declared_inline reads the same keys out of unheadered conception prose, and _sizing.strip_declarations keeps a declaration out of a derived title.
  • _feature.effective_difficulty is gone — imported from the faculty instead. Behaviour identical.
  • BugDecision gains difficulty_declared / difficulty_derived / difficulty_disagreement; IntakeDecision gains those plus difficulty_source / work_type_source / autonomy_source / priority_source.

See full details below.

Test Plan

  • pytest tests/ -q → 491 passed (15 new in tests/test_declared_header_precedence.py)
  • Tenant-firewall leg: repos_sync.py --check --only "tenant firewall (organ code)" → OK
  • The reported case, end to end: intake classify --file on the bug prompt now reports Work-type: bug (declared) and Difficulty: medium (declared) — it previously classified it feature and re-sized it
  • intake "… Difficulty large, supervised." (the ideas.md idiom) → large (declared; heuristic derived small), and the clause no longer leaks into the title or filename
  • pyauto-brain feature / bug on the same prompt report the declared level with the derived one alongside
Full API Changes (for automation & release notes)

Added

  • _sizing.effective_difficulty(p) -> (level, score, factors, derived_level) — declared-wins precedence over estimate_difficulty.
  • _sizing.declared_inline(text) -> (fields, spans) — declarations in unheadered prose (Difficulty: medium mid-sentence; the ideas.md idiom Difficulty large, supervised.). Code fences and inline spans are masked first, matching declared_header — a prompt quoting a header is documenting it, not declaring it.
  • _sizing.strip_declarations(text, spans) — declaration-free text for title derivation only; prompt bodies stay verbatim.
  • _sizing.AUTONOMY_LEVELS.

Changed

  • _sizing.declared_header returns declared_type and declared_autonomy alongside the existing keys; _HEADER_KEY_RE widened to type|autonomy.
  • _bug.analyse_bug / _bug.select_bug size through effective_difficulty; _bug.re_home_check returns None when the prompt declares Type: bug (a defect report legitimately says "refactor" or "documentation" while describing a crash).
  • _intake.analyse reads both declaration sources (header lines win a tie), sizes through effective_difficulty, honours a declared Type: over the prose classifier, and derives its title from the declaration-stripped text. _intake._derive_fields (the formalise path) takes the same route.

Removed

  • _feature.effective_difficulty — moved to the faculty and imported (_feature.effective_difficulty still resolves through the star-free import, so callers are unaffected).

Migration

  • None. No caller-visible signature changed; the only behaviour change is that a declared key is now honoured where it was previously discarded.

Gate

Heart is not reachable from this session (web-github; PyAutoHeart is not among the session's repos), so ship_library's documented fallback applies: the repo's own suite is the gate — 491 passed, plus the tenant-firewall leg CI also runs.

Generated by the PyAutoLabs agent workflow.


Generated by Claude Code

Jammy2211 and others added 3 commits August 24, 2026 22:23
The Intake Agent sized every idea with the shared sizing heuristic and wrote
that estimate into the prompt's `Difficulty:` header, silently discarding a
difficulty the author had already stated — the ideas.md house style ends a
bullet with "Difficulty large, supervised." Worse, the declaration was prose,
so it was swallowed by the derived title and leaked into the slug/filename.

A human who has thought about the work is better evidence than a keyword
heuristic that only sees words, so a declaration now wins:

- `_declared_fields` reads Difficulty/Autonomy/Priority from the raw text,
  including the trailing-autonomy idiom ("Difficulty large, supervised.") and
  spelling variants ("Too Large", "human required").
- Code fences and inline spans are masked first, so a prompt quoting a
  `Difficulty:` line (a repro, a transcript) does not re-size itself.
- The IntakeDecision reports provenance (difficulty_source / autonomy_source /
  priority_source, plus the estimate it overrode) and flags the override in the
  risks block, so the dry-run review sees both numbers.
- The declaration clause is stripped from the derived title/slug only; the
  prompt body stays verbatim.
- `formalise` applies the same precedence.

The sizing faculty is unchanged: it stays the pure estimator, and the override
lives in the agent that owns the header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RXdgVSW8bUhVdEXEz6AAJ
Third instance of one family: a conductor derives a value from prose and
ignores the header key the author declared. (1) the Feature Agent's ranker,
fixed #217; (2) the Bug Agent; (3) the Intake Agent's Difficulty:.

Intake never read a declared Difficulty at all. It hand-built the dict it
passes the sizing faculty, omitting the declared_header() keys the file-loading
path folds in, so estimate_difficulty ran with no declared key present, nothing
reconciled derived against declared, and the derived level was persisted into
the header — silently replacing what the input asked for. Reported case: raw
text declaring `Difficulty: medium` filed as `too-large` (score 11), unchanged
when the full header block was prepended, hand-corrected after --apply. The
heuristic's biggest input is length, and length is a bad size proxy — a prompt
is long when it carries a design, which is exactly what declaring a level
exists to correct.

Per the prompt, the precedence rule moves INTO the faculty both conductors
already call rather than being re-implemented per conductor — three
reconciliations were three chances to forget one, and two forgot:

- `_sizing.effective_difficulty` is now the single rule: declared wins, derived
  level + score returned alongside so a disagreement is reported, not silently
  resolved. `_feature` drops its local copy and imports it; `_bug` and
  `_intake` now go through it.
- `declared_header` reads `Type:` and `Autonomy:` too (it already read
  Difficulty/Priority/Status/Blocked-by), and `declared_inline` is its
  counterpart for unheadered conception prose — the ideas.md idiom
  "Difficulty large, supervised." included. Neither reads a fenced block: a
  prompt quoting a header documents it, rather than declaring it.
- Intake reads both, keeps the declaration out of the derived title/slug (it
  was leaking into the filename), and honours a declared `Type:` over its prose
  classifier — the filing-note leg, where the prompt naming itself a bug
  throughout was classified `feature`.
- The Bug Agent no longer re-homes a prompt that declares `Type: bug` on prose
  keywords: a defect report legitimately says "refactor" or "documentation"
  while describing a crash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RXdgVSW8bUhVdEXEz6AAJ
@Jammy2211
Jammy2211 merged commit 979fac5 into main Aug 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: conductors derive over a declared header key (difficulty, type)

2 participants