Skip to content

fix: harden human-authorization boundaries + unit-of-work lifecycle receipts (2.5.49) - #692

Merged
apackeer merged 9 commits into
v2from
issue-681
Aug 7, 2026
Merged

fix: harden human-authorization boundaries + unit-of-work lifecycle receipts (2.5.49)#692
apackeer merged 9 commits into
v2from
issue-681

Conversation

@apackeer

Copy link
Copy Markdown
Contributor

Closes the verified authorization and lifecycle gaps from #681 (the brownfield Codex evaluation). Seven of the nine reported claims are addressed; the remaining two (decision-ID-bound reusable receipts, decision-ledger locking) describe a receipt subsystem v2 does not have and are deliberately left to a follow-up design discussion rather than bolted on here.

What each claim gets

Claim 8 - the public audit CLI could forge authority rows. aidlc-audit.ts append / append-batch now refuse the receipts the engine's guards read as authorization evidence (HUMAN_TURN, GATE_APPROVED, GATE_REJECTED, QUESTION_ANSWERED, REVIEW_REQUESTED, REVIEW_COMPLETED, SWARM_UNIT_CONVERGED, AUTONOMY_MODE_SET, and the four new UNIT_* receipts) with an error naming the owning tool. append-raw refuses a body carrying a taxonomy **Event**: line, and a caller-supplied Event field key is refused on every emit path (a second **Event**: line would spoof the multiline event queries; Timestamp stays legal - park/unpark rows carry it and the emitter's own line is written first). Owning tools and hooks emit through the library import, untouched. Test fixtures that simulate owning emitters set AIDLC_ALLOW_DIRECT_AUDIT_EVENTS=1; the suite runner sets it globally.

Claim 7 - autonomous execution could weaken mandatory gates. aidlc-bolt.ts set-autonomy --mode autonomous requires a fresh human turn (the ladder-prompt answer), and the grant consumes that turn exactly like a gate resolution - one human turn can no longer both grant autonomy and approve something else. De-escalating to gated never needs presence.

Claim 4 - same-second cross-shard ordering was unsafe. humanActedSinceGate now reads shards individually: buffer position remains the tiebreak only within a shard (where it reflects append order), and when the deciding human-turn/resolution pair shares one second-precision timestamp across different shards - where execution order is unknowable - the gate fails closed and requires a fresh turn instead of trusting shard filename order.

Claim 3 - ambiguous/auto-resolved question responses. Cancellation boilerplate ("Cancelled", "dismissed", "timed out", empty text - the exact status=completed / answer Cancelled repro from the issue) is refused as an interview answer (aidlc-log.ts answer), an approval choice (approve --user-input), and rejection feedback (reject --feedback). A substantive answer that merely contains a cancellation word ("cancel the standing order via cron") still passes. The presence mint itself stays content-blind by design.

Claims 1, 2, 9 - unit lifecycle. New UNIT_STARTED / UNIT_PAUSED / UNIT_RESUMED / UNIT_COMPLETED receipts (taxonomy 74 → 78), emitted by the new aidlc-state.ts unit start|pause|resume|complete verb on inline per-unit Construction stages:

  • Artifacts are evidence, receipts are the transition. unit complete verifies the unit's required artifacts on disk (kind-filtered, same rules as the engine's coverage walk) before committing. Once any receipt exists for a stage, the engine requires a receipt per unit - a scaffolded or partially-written unit can no longer read as done by file existence alone. Receipt-free workflows keep today's artifact-driven coverage, so nothing re-opens on upgrade.
  • Pause carries the checkpoint. unit pause requires --reason and --next-action; the state file mirrors them under ## Runtime State, resume returns them as JSON, and the session-start hook injects an Active Unit: context line so a fresh session lands on the exact stopping point.
  • A paused unit hard-stops the engine. next emits an ask naming the unit, its reason, and next action (unit_state: paused) until an explicit unit resume; report --result awaiting-approval is refused while a unit is paused; unit start refuses while another unit is open (single-active-unit invariant, so resume/restart races cannot create two active units).
  • No swarm interference. The verb refuses under Construction Autonomy Mode: autonomous (same rule as park); the referee's SWARM_UNIT_* ledger remains the sole per-unit authority on the autonomous path.

Tests

  • t261-audit-authority-floor - CLI forge refusals across all three append paths, the Event field-key floor, set-autonomy presence + consume-once, cross-shard fail-closed vs same-shard append order, and the non-answer floors including the issue's named repro.
  • t260-unit-lifecycle-receipts - receipts-not-artifacts completion, single-active-unit, pause checkpoint mirroring, the paused-unit engine ask, and the approval refusal.
  • Taxonomy pins moved in lock-step (t28, t81, t239, registry docs, user guide); coverage registry regenerated.

Gates run: package.ts --check, typecheck, lint, smoke + unit (183 files / 4481 assertions) all green on the rebased head. E2E ran sliced across the full 67-file tier: all deterministic journeys green including the construction-worktree scopes, swarm referee, and stop hook; the two reds (t113, t138) fail identically on the pristine base commit (t113's helper predates reviewer enforcement; t138 is the known live-SDK timeout) - both exonerated by baseline runs.

Notes for reviewers

@leandrodamascena leandrodamascena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authorization and unit-lifecycle additions have several fail-open paths that defeat the new authority floor or allow the workflow to reach a gate with an active/incomplete unit. The inline findings include direct reproductions for audit receipt forgery, rogue-unit deadlock, completed-unit restart, and same-second attempt reuse.

Comment thread core/tools/aidlc-audit.ts
Comment thread core/tools/aidlc-state.ts
Comment thread core/tools/aidlc-state.ts
Comment thread core/tools/aidlc-lib.ts Outdated
Comment thread core/tools/aidlc-bolt.ts Outdated
Comment thread core/tools/aidlc-state.ts

@leandrodamascena leandrodamascena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corrective commit resolves several earlier findings, but lifecycle integrity still has fail-open and upgrade-breaking paths.

  1. [P1] Bind unit start to the engine-routed stage/unit pair
    unit start checks DAG membership but not dependency readiness. Starting unit-b before unit-a succeeds, and next then routes unit-b, overriding topological order.

  2. [P1] Do not order same-second attempt boundaries by shard filename
    A new GATE_REJECTED in another shard can lose the tie to an older STAGE_STARTED, allowing the prior attempt’s UNIT_COMPLETED receipt to remain settled.

  3. [P1] Require artifacts to be regular files
    existsSync() accepts directories named *.md. I created directories for every required artifact and unit complete emitted UNIT_COMPLETED.

  4. [P2] Preserve in-flight swarms across upgrades
    finalize now requires the new stamped SWARM_STARTED. Worktrees prepared before upgrading cannot finalize or be re-stamped without being discarded and rebuilt.

  5. [P2] Preserve legacy safe unit names
    The new lowercase-only validator rejects previously accepted names such as api_v2 and WebUI, breaking existing receipt-free workflows despite the compatibility claim.

Focused validation passed: 111 tests, 476 assertions, package parity, and remote CI.

@apackeer

apackeer commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed @leandrodamascena's follow-up review in a7596c67.

  1. unit start now consumes steering continuations and requires the exact engine-routed directive.stage / directive.unit pair, so dependencies and earlier same-batch units cannot be bypassed. The compiled dispatcher path is covered too.
  2. Attempt-floor readers preserve shard identity. Same-second boundaries in different shards produce a deterministic fail-closed ambiguity floor instead of being ordered by shard filename.
  3. Unit completion and engine coverage now require regular files; directories named *.md no longer count as artifacts.
  4. Pre-upgrade in-flight swarms can finalize only when the frozen, hash-verified audit proves the ordered legacy SWARM_STARTED -> BOLT_STARTED -> STATE_FORKED -> AUDIT_FORKED preparation and its derived attempt still matches current state. Stale/incomplete preparations remain refused.
  5. Safe legacy unit names such as api_v2 and WebUI are accepted while traversal, separators, whitespace, and control characters remain rejected.

Verification:

  • focused unit routing/lifecycle/attempt slice: 46/46 green
  • compiled-dispatcher lifecycle slice: 21/21 green
  • swarm referee e2e: 16/16 green
  • coverage registry + version/changelog checks: 41/41 green
  • bun run typecheck, bun run lint, bun scripts/package.ts --check, bun tests/gen-coverage-registry.ts --check, and git diff --check all pass

@apackeer apackeer changed the title fix: harden human-authorization boundaries + unit-of-work lifecycle receipts (2.5.34) fix: harden human-authorization boundaries + unit-of-work lifecycle receipts (2.5.49) Aug 6, 2026

@leandrodamascena leandrodamascena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corrective commit resolves several earlier findings, but authority and lifecycle integrity still fail open under same-second cross-shard races.

[P1] Fail closed when a resolution competes with human presence across shards
When GATE_APPROVED exists in one shard and GATE_REJECTED plus HUMAN_TURN share its timestamp in another, humanActedSinceGate() returns true. The global order is unknowable, so the human turn may predate the latest resolution.

[P1] Do not order same-second unit lifecycle events by shard filename
With UNIT_STARTED and UNIT_PAUSED in different shards at the same timestamp, activeUnitCheckpoint() can report in-progress based on lexical shard order. This ignores a potentially effective pause and permits operations that should remain blocked.

Focused validation reproduced both issues. git diff --check and remote CI passed; full local guards were not rerun.

@apackeer

apackeer commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the latest cross-shard race findings in 5bac4587.

  1. humanActedSinceGate() now requires a candidate human turn to be provably newer than every latest resolution. If any same-second resolution is in another shard, authority fails closed regardless of shard filename order.
  2. Unit lifecycle reduction now preserves shard identity, keeps each shard's known append order, and resolves same-second cross-shard ties conservatively: paused, then another non-terminal state, then completed. This shared reduction protects both active checkpoints and completion settlement.

Regression coverage includes both filename orientations, the exact three-event authority repro, same-shard reverse ordering, pause/start and pause/resume ambiguity, completion/non-terminal ambiguity, and strictly later lifecycle events.

Verification:

  • focused authority/lifecycle slice: 47/47 assertions green
  • adjacent presence + shard-ordering slice: 25/25 assertions green
  • version/changelog sync: 7/7 assertions green
  • bun run typecheck
  • focused Biome check
  • bun scripts/package.ts --check
  • bun tests/gen-coverage-registry.ts --check
  • git diff --check

@leandrodamascena leandrodamascena left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest updates address the blocking authority and lifecycle issues from the previous reviews. Focused lifecycle, authority, swarm, parity, typecheck, lint, and diff checks pass.

Approving. Two non-blocking compatibility edge cases would still be worth fixing before merge:

  • Backward jumps to inline Construction stages while autonomy remains enabled cannot emit Unit lifecycle receipts because unit start/complete are refused.
  • Safe legacy Unit names beginning with a digit, such as 2fa, were accepted by v2 but are now reported as malformed.

Neither warrants another blocking review round, but addressing them would make the upgrade path more robust.

- The public audit CLI (append / append-batch) refuses authority-bearing
  receipts (HUMAN_TURN, GATE_*, QUESTION_ANSWERED, REVIEW_*,
  SWARM_UNIT_CONVERGED, AUTONOMY_MODE_SET); append-raw refuses bodies
  carrying a taxonomy **Event**: line; caller-supplied Event/Timestamp
  field keys are refused on every emit path.
- aidlc-bolt set-autonomy --mode autonomous requires a fresh human turn
  (the ladder answer), and the autonomous grant consumes the turn like a
  gate resolution does.
- humanActedSinceGate reads shards individually: same-second cross-shard
  ties on the deciding human-turn/resolution pair now fail closed instead
  of resolving by shard filename order.
- aidlc-log answer, aidlc-state approve/reject refuse cancellation
  boilerplate (Cancelled / dismissed / timed out) as answers, approval
  choices, or rejection feedback.

Suite runner sets AIDLC_ALLOW_DIRECT_AUDIT_EVENTS=1 so fixtures that
simulate owning emitters keep working.
…ms 1, 2, 9)

- New audit events UNIT_STARTED / UNIT_PAUSED / UNIT_RESUMED /
  UNIT_COMPLETED, emitted by the new 'aidlc-state.ts unit
  start|pause|resume|complete' verb (CLI-protected like other receipts).
- unit complete verifies the unit's required artifacts on disk before
  committing: the receipt is the transition, artifacts are evidence.
- Engine coverage (nextUncoveredUnit) now requires a UNIT_COMPLETED
  receipt per unit once any receipt exists for the stage; receipt-free
  workflows keep artifact-driven coverage (no upgrade break).
- A paused unit routes first and hard-stops the loop: the engine emits
  an ask naming the unit, its recorded reason, and next action
  (unit_state: paused) until an explicit unit resume; approval entry is
  refused while a unit is paused.
- unit start refuses a second active unit (single-active-unit
  invariant); resume/complete validate against the active checkpoint.
- State file mirrors the checkpoint (Active Unit / Unit State / Unit
  Pause Reason / Unit Next Action under Runtime State) for status reads.
- Taxonomy pins updated: t28 74 -> 78, registry tables + emitter rows in
  audit-format.md and 12-state-machine.md, stage-protocol receipt prose.
…fecycle (t259, t260)

- t259: CLI forge refusals (append/append-batch/append-raw + Event field
  key), set-autonomy presence + consume-once, cross-shard same-second
  fail-closed, cancellation-boilerplate floors (incl. the issue's
  status=completed answer=Cancelled repro).
- t260: receipts-not-artifacts completion, single-active-unit invariant,
  pause checkpoint mirroring, paused-unit engine ask + approval refusal.
- Timestamp removed from the reserved field keys (park/unpark rows carry
  it legitimately; the emitter's own line is written first so it cannot
  spoof).
- 74 -> 78 event-count pins updated (t28, t81, t239, docs, user guide);
  coverage registry regenerated; t259/t260 added to the none->cli
  reclassification ratchet.
- Version 2.5.31 + CHANGELOG entry + README badge.
- UNIT_STARTED/PAUSED/RESUMED/COMPLETED added to
  CLI_PROTECTED_EVENT_TYPES: routing trusts UNIT_COMPLETED as the
  completion signal, so a CLI-forged receipt would skip the owning verb's
  artifact verification.
- aidlc-state.ts unit refuses under Construction Autonomy Mode:
  autonomous (the swarm referee owns per-unit bookkeeping via
  SWARM_UNIT_*; same rule park applies) - keeps the two ledgers from
  interleaving.
- Resume surfaces the unit checkpoint: handleResume returns
  active_unit/unit_state/unit_pause_reason/unit_next_action, and the
  session-start hook injects an 'Active Unit: ...' context line so a
  fresh session lands on the exact stopping point.
- Doctor STATE_ALLOWLIST carries the four new runtime fields.
- Protected-set doc mentions updated (stage-protocol, audit-format,
  12-state-machine); t259 covers the four new refusals.
@apackeer
apackeer merged commit 0189309 into v2 Aug 7, 2026
5 checks passed
@apackeer
apackeer deleted the issue-681 branch August 7, 2026 05:02
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.

2 participants