Skip to content

fix: preserve decision completion after history pruning - #3159

Open
Charliekirk-creator wants to merge 35 commits into
kunchenguid:mainfrom
Charliekirk-creator:fm/fm-decision-hold-pruned-history
Open

Charliekirk-creator wants to merge 35 commits into
kunchenguid:mainfrom
Charliekirk-creator:fm/fm-decision-hold-pruned-history

Conversation

@Charliekirk-creator

Copy link
Copy Markdown

Intent

Fix the retained-history defect in the shared decision-completion owner so a live investigation or visual review can open and complete later decisions after older resolved decision records have left the backlog's normal retained Done window. Reproduce and regress the behavior through the public bin/fm-decision-hold.sh interface in isolated fixture homes, including the initiating trigger, retention masking condition, visible completion failure, a proven retained-history path, relevant history, the smallest counterfactual, and disconfirming evidence. Preserve durable ownership of every unresolved captain decision: missing, malformed, or mismatched active decision records must stop completion. A historical decision durably resolved and later pruned by normal configured retention may remain pruned without preventing a later review pass. Do not weaken origin identity, home isolation, dependency routing, decision-file, containment, ordinary-file, symlink, hardlink, or answer-routing protections, and do not infer decisions from prose, artifacts, status text, chat, or titles. Keep completion and verification deterministic, idempotent, compatible with existing metadata, and ensure bounded Done retention does not oscillate by restoring pruned historical rows. Keep semantic policy in .agents/skills/decision-hold-lifecycle/SKILL.md and exact mechanics in bin/fm-decision-hold.sh, using pointers elsewhere. Cover live origins with pruned earlier resolutions, successful later decisions, unresolved decisions with missing records, malformed or mismatched retained records, repeated complete and verify calls, and existing metadata compatibility. Do not modify private Workstack Compass artifacts or operational records, change retention limits, restore pruned history by hand, weaken current decision ownership, or modify live captain decisions.

What Changed

  • Allow later decision reviews to complete and verify against normally pruned resolved history without restoring archived rows.
  • Add provenance-backed retention handling and explicit legacy migration while preserving strict active-record, identity, ownership, and filesystem validation.
  • Route retention-affecting tasks-axi operations through the decision lifecycle owner and expand regression coverage for pruned history, retries, compatibility, and failure cases.

Risk Assessment

⚠️ Medium: No concrete defect was found, but the change substantially expands retention, provenance, migration, and filesystem-safety logic, leaving moderate integration risk for the dedicated test phase.

Testing

The focused lifecycle suite passed, and isolated public-interface verification reproduced the base defect then demonstrated successful, idempotent later completion on the target without restoring pruned history; missing current ownership still failed safely and the worktree remained clean.

Evidence: Retained-history defect reproduction and fixed public-interface transcript

Source: Retained-history defect reproduction and fixed public-interface transcript

$ fm-decision-hold.sh hold review-live old-choice ...
review-live-decision-old-choice
$ fm-decision-hold.sh complete review-live old-choice
complete: review-live decision inventory reviewed (old-choice)
$ fm-decision-hold.sh answer review-live old-choice --decision-file old-answer.txt
answered: review-live-decision-old-choice

After normal configured retention (done_keep remains 10):
  live old row count: 0
  archived old row count: 1
  retained Done row count: 10

$ fm-decision-hold.sh complete review-live missing-current  # smallest counterfactual
fm-decision-hold: captain hold review-live-decision-missing-current is absent from /var/folders/l9/13blqg851n3bj8p1b_3lxxk00000gn/T/no-mistakes-evidence/01M097074X95P818TR2BSD79ZQ/.retained-history-fixture/data/backlog.md
exit=1

$ fm-decision-hold.sh hold review-live later-choice ...
review-live-decision-later-choice
$ fm-decision-hold.sh complete review-live later-choice  # twice
complete: review-live decision inventory reviewed (later-choice,old-choice)
complete: review-live decision inventory reviewed (later-choice,old-choice)
$ fm-decision-hold.sh verify review-live  # twice
verified: review-live unresolved-decision inventory
verified: review-live unresolved-decision inventory
  repeated complete/verify: success
  backlog+archive hashes unchanged: yes
  retained Done row count: 10
  later hold state:
  state: queued
  held: yes
  completion metadata:
decision_keys=old-choice
decision_inventory_schema=fm-decision-completion.v1
decision_current_keys=old-choice
decision_historical_keys=
decision_keys=later-choice,old-choice
decision_inventory_schema=fm-decision-completion.v1
decision_current_keys=later-choice,old-choice
decision_historical_keys=

Base-commit reproduction of the retained-history defect:
$ [base 64d61ae] fm-decision-hold.sh complete review-live later-choice
  old live row count: 0
  old archive row count: 1
fm-decision-hold: captain decision review-live-decision-old-choice is absent from /var/folders/l9/13blqg851n3bj8p1b_3lxxk00000gn/T/no-mistakes-evidence/01M097074X95P818TR2BSD79ZQ/.base-repro/home/data/backlog.md
exit=1

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (31) ✅
  • 🚨 bin/fm-decision-hold.sh:370 - Intent requires “do not infer decisions from ... titles,” but these wildcard checks accept (kind: captain) and (hold-kind: captain) anywhere in the raw header. A normally pruned ordinary task can place both strings in its title before its actual trailing (kind: ship) metadata and, with a resolution-shaped body, satisfy completion for a missing hold. Parse canonical structured fields rather than matching title text.
  • 🚨 bin/fm-decision-hold.sh:470 - Intent requires “Do not weaken origin identity” and that missing active records stop completion, but archived proof is bound only to the concatenated hold ID. Because <origin>-decision-<key> is ambiguous, a genuine pruned resolution for origin sample, key route-decision-later also proves origin sample-decision-route, key later; complete then succeeds without that second decision ever having a hold. Bind and validate origin/key in the shared resolution record, including the retained-Done sibling path.
  • 🚨 bin/fm-decision-hold.sh:278 - Intent requires coverage and rejection of “malformed or mismatched retained records,” but the validator checks only that the digest is 64 lowercase hex characters and that decision text is nonempty. A retained or archived record containing 64 zeroes with unrelated captain text is accepted as resolved. Recompute and compare the decision digest, and validate routed identities against the routed-work section.
  • 🚨 bin/fm-decision-hold.sh:323 - Intent says “Do not weaken ... home isolation ... containment ... symlink ... protections,” but backlog reads run under FM_HOME while archived proof is read independently from FM_DATA_OVERRIDE, with only the final leaf checked. With FM_HOME=A and FM_DATA_OVERRIDE=B/data, a missing hold in A can be satisfied by B's archived row; a symlinked parent has the same issue. Resolve and verify the configured archive against the authoritative home before accepting it.

🔧 Fix: Harden archived decision resolution proof
7 errors still open:

  • 🚨 bin/fm-decision-hold.sh:619 - Intent requires “missing ... active decision records must stop completion.” After key k was resolved and pruned, a current structured needs-decision [key=k] with no active hold still passes because this fallback accepts the old archive; complete then records the captain-held transfer. Require active verification for currently open keys before allowing historical fallback.
  • 🚨 bin/fm-decision-hold.sh:370 - Intent requires compatibility with existing metadata, but every pre-change resolution body lacks Origin: and Decision key:. The parser recognizes that legacy shape, then this comparison rejects its empty identity fields, so retained or archived decisions created by released versions now fail complete, verify, and retries. Add a versioned migration or compatibility path that preserves origin safety.
  • 🚨 bin/fm-decision-hold.sh:428 - Intent forbids inferring decisions from titles. An ordinary kind-captain archived row titled Ordinary (hold: forged) (hold-kind: captain) has its actual (kind: captain) parsed first, then this loop continues into the title and treats both title fragments as hold provenance; a resolution-shaped body then satisfies completion. The added ship-kind test misses this sibling path. Stop parsing at the canonical metadata boundary or use a semantic provenance field that titles cannot supply.
  • 🚨 bin/fm-decision-hold.sh:412 - Intent requires history pruned by normal configured retention to remain usable, but this hardcodes data/done-archive.md instead of reading [markdown].archive from the same .tasks.toml used by tasks-axi. A home configured with an in-home archive such as data/history.md prunes normally there and then fails verification. Resolve and containment-check the configured archive path.
  • 🚨 bin/fm-decision-hold.sh:337 - Intent requires malformed or mismatched retained records to stop completion and preserves answer-routing semantics, but the parser accepts Resolution mode: declined|answered|repaired with nonempty routed identities, or routed with (none), whenever the two routing lists match. Enforce the mode-to-routing invariant, retaining only an explicitly supported legacy no-mode format.
  • 🚨 bin/fm-decision-hold.sh:697 - Intent says not to weaken origin identity and docs claim hold rejects identity collisions. For two origin/key pairs that concatenate to the same ID, an existing active hold with the same title reaches this mutation; tasks-axi changes its reason before line 699 discovers the body belongs to the other origin and fails. Validate existing body provenance before mutating the hold.
  • 🚨 bin/fm-decision-hold.sh:618 - Intent requires malformed active records to stop completion, but every tasks-axi show failure—not only NOT_FOUND—is treated as pruning. If the backlog or tasks configuration is unreadable/malformed while a matching archive remains readable, verification succeeds without inspecting active state. Distinguish an exact not-found result from backend, parse, permission, and configuration failures before consulting history.

🔧 Fix: Harden retained decision history verification
2 errors still open:

  • 🚨 bin/fm-decision-hold.sh:392 - Intent requires compatibility and deterministic, idempotent later reviews, but legacy resolution proof requires state/<origin>.meta. Normal teardown deletes that file, so a base-version resolution later pruned from Done is no longer recognized; a post-teardown hold retry can recreate the same resolved ID and eventually produce duplicate archive rows. Persist or migrate an unambiguous origin/key attestation before teardown rather than authenticating durable history with ephemeral live metadata.
  • 🚨 bin/fm-decision-hold.sh:1269 - Intent says “Do not weaken origin identity,” but repair validates only state, kind, and hold kind before replacing the body. If origin A/key X was directly closed and origin B/key Y composes to the same hold ID, repair B Y skips the resolution branch and overwrites A's structured awaiting provenance with a resolution bound to B. Validate the existing body's origin/key provenance before this mutation, as the active close paths do.

🔧 Fix: Preserve legacy decision identity through teardown
5 errors still open:

  • 🚨 bin/fm-decision-hold.sh:425 - Intent requires “Do not weaken origin identity.” This fallback treats surviving claimant metadata as proof of a legacy record’s origin, but metadata for historical owners is normally deleted. A released resolution for origin sample, key route-decision-later can therefore be accepted and attested for an existing colliding inventory at origin sample-decision-route, key later once the original metadata is gone. Only attest while the original structured owner is durably verifiable; unattested legacy records cannot be safely rebound from claimant metadata alone.
  • 🚨 bin/fm-decision-hold.sh:895 - Intent requires compatibility with existing metadata and deterministic, idempotent answer routing. If a released resolve wrote its legacy body and then failed while unblocking work, the hold remains queued. Teardown verification validates that body through live metadata here but returns without persisting an attestation; teardown then deletes the metadata, and the documented resolution retry fails provenance validation. Persist exact queued legacy resolution identity before teardown, while distinguishing it from an ordinary awaiting body.
  • 🚨 bin/fm-decision-hold.sh:395 - Intent says “Do not weaken ... home isolation ... symlink ... protections.” Archive and data paths are bound to FM_HOME, but legacy identity is authorized from an unvalidated STATE. FM_STATE_OVERRIDE pointing at another home, or a symlinked $FM_HOME/state, can supply reviewed metadata that causes an attestation to be written into the current home. Resolve and containment-check the authoritative state directory before metadata can authorize migration.
  • 🚨 bin/fm-decision-hold.sh:582 - Intent requires normal configured retention to work. tasks-axi lazily creates configured archive parents during pruning, but this check rejects a valid path such as data/history/done.md while data/history does not yet exist. Every new hold then fails during its absent-history check before creating the decision. Validate the normalized path through an existing contained ancestor without requiring the future archive directory to preexist.
  • 🚨 bin/fm-decision-hold.sh:323 - Intent requires malformed retained records to be rejected and decision-file protections preserved. New decisions are limited to 8192 bytes, but the retained-record parser only requires nonempty text and a matching digest, so a correctly hashed resolution containing an arbitrarily large captain decision is accepted despite being impossible for this owner to create. Enforce the same byte bound on parsed retained decisions.

🔧 Fix: Harden legacy resolution migration and archive validation
4 issues (3 errors, 1 warning) still open:

  • 🚨 bin/fm-decision-hold.sh:1098 - Intent requires “missing ... active decision records must stop completion.” For needs-decision [key=k] followed by a normal terminal done, origin_open_decisions suppresses k, so active verification is skipped. If an older k is archived, complete origin k succeeds and later appends a captain-held transfer for the nonexistent active hold. Require active ownership for every raw key being transferred before allowing historical fallback.
  • 🚨 bin/fm-decision-hold.sh:833 - Intent permits only a decision “later pruned by normal configured retention” and forbids inference from prose or artifacts, but this scan accepts a matching top-level bullet anywhere in the file without requiring an ## Archived YYYY-MM-DD block. A resolution-shaped row under ## Notes or before any archive heading can therefore prove a missing hold. Track archive section boundaries and accept only canonical archived Done records.
  • 🚨 bin/fm-decision-hold.sh:434 - Intent requires compatibility with existing metadata, but released versions allowed valid keys or origins containing -decision-. Their legacy resolution records are now rejected unconditionally before live metadata or an existing exact attestation is checked, even when no competing owner exists. This deliberate containment needs explicit approval or a migration path that preserves both compatibility and origin identity.
  • ⚠️ bin/fm-decision-hold.sh:745 - Attestation publication links the staging file to its final name and then removes the staging link without checking removal. Interruption or removal failure leaves link count 2; subsequent validation hard-fails as a hardlink and cannot retry even while valid metadata survives. Use a crash-recoverable no-clobber publication mechanism or safely recognize and clean the owned staging link.

🔧 Fix: Harden active and archived decision ownership
5 errors still open:

  • 🚨 bin/fm-decision-hold.sh:1024 - Intent requires “Do not weaken ... home isolation ... symlink ... protections,” but authoritative state validation runs only for legacy records. With FM_HOME=A, FM_STATE_OVERRIDE=B/state (or a symlinked A/state), B’s origin metadata, and A’s archived current-format record for the same origin/key, this return accepts history and complete can update B’s metadata. Validate the state root before any complete/verify state read, lock, or write.
  • 🚨 bin/fm-decision-hold.sh:1147 - Intent requires “missing ... active decision records must stop completion” and preserved symlink protections. status_open_decisions returns empty for a present symlinked or unreadable status file, so this code treats an unsafe current-key source as no current key and permits matching archived history to satisfy completion without an active hold. Reject unsafe present status files before historical fallback.
  • 🚨 bin/fm-decision-hold.sh:412 - Intent requires compatibility with existing metadata, but any alternate-decomposition origin blocks migration even when it does not claim the colliding key. A valid legacy record for sample/route-decision-later with exact reviewed source metadata is rejected merely because unrelated origin sample-decision-route exists without key later. Detect competing reviewed ownership, not mere origin existence.
  • 🚨 bin/fm-decision-hold.sh:812 - Intent forbids inferring decisions from titles, but this regex accepts an empty (hold:). A semantically ordinary Done row titled with trailing (repo: sample) (kind: captain) (hold:) (hold-kind: captain) can be normally pruned; tasks-axi does not parse the malformed hold token, while this archive parser strips it as provenance and accepts a resolution-shaped body. Require a nonempty canonical hold field and stop at malformed metadata.
  • 🚨 bin/fm-decision-hold.sh:381 - Intent requires malformed retained records to stop completion, but routed identities only receive a character whitelist. A retained body using Routed identities: -ghost with matching routed-work text, digest, and mode is accepted even though tasks-axi task IDs must begin with an alphanumeric character and the owner could never create that route. Enforce the tasks-axi ID grammar here.

🔧 Fix: Harden retained decision ownership and compatibility
3 errors still open:

  • 🚨 bin/fm-decision-hold.sh:1177 - Intent forbids inferring decisions from “status text,” but this branch uses the status fold to decide whether a listed key requires an active hold or may fall back to archived history. Consequently, the same explicit complete inventory receives weaker ownership checks when a visual review has no matching status event. Carry current-versus-historical provenance through the public completion inventory or another structured owner instead of deriving it from status text.
  • 🚨 bin/fm-decision-hold.sh:425 - Intent requires compatibility with existing metadata, but every pre-upgrade resolution lacking embedded origin/key is rejected after normal teardown unless this new version previously created an attestation. For a common unambiguous ID such as sample-review-decision-route, teardown removes sample-review.meta, so retained Done or normally archived history that worked before the upgrade can no longer satisfy complete or verify. Accept the unique ID decomposition directly while reserving attestations for genuinely ambiguous IDs.
  • 🚨 bin/fm-decision-hold.sh:443 - Intent says “Do not weaken origin identity” and missing active records must stop completion, but alternate ownership is checked only while the historical origin still has live metadata, a report, or an active backlog row. If released history for A/x is archived after A’s ship origin and metadata are pruned, a previously reviewed colliding B/y whose active hold is missing makes origin_exists_here(A) false; B’s metadata then attests A’s legacy row and completion succeeds. Ambiguous legacy rows need an exact pre-existing attestation or proof bound while their original owner remains verifiable, not claimant metadata plus absence of live artifacts.

🔧 Fix: Harden current and legacy decision provenance
3 errors still open:

  • 🚨 bin/fm-decision-hold.sh:421 - Intent requires compatibility with existing metadata, but released records such as origin sample, key route-decision-later have no embedded identity and cannot have this change's newly introduced attestation. This unconditional rejection means they fail verification even while original reviewed metadata still exists. Migrate only while the original structured owner is durably verifiable, or explicitly authorize this permanent fail-closed containment.
  • 🚨 bin/fm-decision-hold.sh:1150 - The shared slug grammar and hold still accept keys such as --route, and the base complete parser accepted them, but this new wildcard treats every leading-hyphen key as an option. Such a hold can now be created but never inventoried, and equivalent pre-upgrade active holds break compatibility. Add end-of-options handling or consistently reserve the grammar with a migration path.
  • 🚨 bin/fm-decision-hold.sh:236 - Every hold, complete, and verify operation now requires jq, but the documented universal toolchain and bootstrap do not require it for default tmux or orca homes. A currently supported installation without optional jq therefore cannot use the required completion gate and scout teardown remains blocked. Avoid the dependency or make it an explicit universal prerequisite through the toolchain owner.

🔧 Fix: Preserve legacy decisions and option-shaped keys without jq
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:515 - Intent requires “Do not weaken origin identity,” but this migration treats claimant metadata plus missing or excluding alternate artifacts as provenance. Once legacy history for sample/route-decision-later loses its origin artifacts, metadata for sample-decision-route/later can bind the same concatenated ID, persist a false attestation at line 527, and satisfy completion without that owner’s resolution. Only attest while the original owner is exactly verifiable; otherwise require an existing attestation or fail closed.

🔧 Fix: Prevent ambiguous legacy decision ownership rebinding
3 errors still open:

  • 🚨 bin/fm-decision-hold.sh:478 - Intent requires both “compatible with existing metadata” and “Do not weaken origin identity.” This rejects a valid legacy sample/route-decision-later record whenever alternate sample-decision-route.meta never existed, yet mutable metadata still permits rebinding if sample is later reused with a reviewed inventory excluding route-decision-later and claimant metadata claims sample-decision-route/later; the old row is then falsely attested. Ambiguous legacy rows need pre-existing durable identity proof or explicit approval for fail-closed/manual migration.
  • 🚨 bin/fm-decision-hold.sh:820 - Intent requires compatibility with existing metadata, but released task IDs have no length limit while attestations use <hold-id>.attestation as one filesystem component. On common 255-byte filesystems, a valid legacy hold ID over 243 bytes passes tasks-axi and identity validation but ln fails with ENAMETOOLONG, making verification permanently fail. Use a bounded digest-based filename while retaining the full identity in the attestation content.
  • 🚨 bin/fm-decision-hold.sh:798 - Intent forbids weakening hardlink protections, but any second hardlink named .attestation.* is treated as an owned interrupted stage and deleted. The added regression even uses .attestation.interrupted, which the six-character mktemp template cannot produce. Without durable stage ownership evidence, an unrelated hardlink is silently modified and accepted instead of rejected.

🔧 Fix: Harden legacy decision attestations and provenance
2 errors still open:

  • 🚨 bin/fm-decision-hold.sh:451 - Intent requires compatibility with existing metadata, but released records have no attestations. A valid pre-upgrade record for origin sample, key route-decision-later is rejected solely because its ID has another decomposition, even when exact reviewed metadata survives. The policy/test rewrite to fail closed does not satisfy the authoritative requirement; obtain explicit containment approval or provide a safe migration path.
  • 🚨 bin/fm-decision-hold.sh:472 - Intent requires malformed active records to stop completion, but this accepts any valid resolution mode as active provenance. A queued captain hold containing a correctly hashed Resolution mode: repaired body passes complete and verify, although repair can only create that mode after confirming the hold is Done. Enforce the mode/state invariant at the shared queued-record validation boundary.

🔧 Fix: Harden legacy migration and queued resolution validation
4 issues (3 errors, 1 warning) still open:

  • 🚨 bin/fm-decision-hold.sh:1276 - Intent requires “Do not weaken origin identity,” but migrate-legacy treats the absence of current competing metadata as ownership proof. After a genuine legacy row for sample/route-decision-later is archived and its metadata is removed by teardown, metadata for colliding sample-decision-route/later can supply the recorded answer, persist a false attestation, and make complete --none --resolved later succeed without that origin ever owning a hold. Ambiguous rows require pre-existing origin-bound proof or an independently authorized mapping; claimant metadata and a readable answer digest cannot establish ownership.
  • 🚨 bin/fm-decision-hold.sh:1353 - Intent requires preserving “home isolation ... symlink, hardlink ... protections,” but complete and verify validate only the state directory, then follow a symlinked or hardlinked <origin>.meta. A foreign metadata leaf can supply archived keys to the new fallback, and complete may append through that link. Validate the metadata leaf as readable, ordinary, non-symlinked, and single-linked after locking and before every read or write.
  • ⚠️ bin/fm-decision-hold.sh:713 - The new path checks require FM_DATA_OVERRIDE and FM_STATE_OVERRIDE to equal exactly $FM_HOME/data and $FM_HOME/state, breaking the documented use of alternate operational directories for specialized harnesses even when they are ordinary and contained within FM_HOME. A home-local fixture data directory with matching tasks-axi backlog/archive configuration now fails before creating a hold. Enforce physical containment and alignment with the effective override rather than equality with the default path.
  • 🚨 docs/decision-hold-lifecycle.md:20 - Intent requires “exact mechanics in bin/fm-decision-hold.sh, using pointers elsewhere,” but this section duplicates archive normalization, canonical header parsing, digest/routing validation, attestation publication/recovery, and migration mechanics. Replace these details with pointers to the script or its public help while retaining regression evidence.

🔧 Fix: Harden decision history ownership and path safety
2 errors still open:

  • 🚨 bin/fm-decision-hold.sh:454 - Intent requires “compatible with existing metadata,” but legacy records are accepted only when the composed ID is uniquely decomposable or this new feature's attestation already exists. Released versions allowed identities such as sample/route-decision-later and could not create that attestation; migrate-legacy also requires an existing attestation at line 1262. This is permanent fail-closed containment without explicit authorization. Provide independently origin-bound migration or obtain approval for the compatibility break.
  • 🚨 bin/fm-decision-hold.sh:274 - Intent requires preserving “home isolation ... containment ... symlink, hardlink ... protections,” but origin ownership follows metadata and report leaves using bare -f. A symlinked $FM_HOME/state/o.meta to any foreign regular file lets hold o k create a local captain hold because hold never invokes the new safe-state boundary; post-teardown completion similarly accepts a foreign or linked report for a current-format active hold. Validate resolved roots and ordinary, single-linked ownership leaves inside the shared origin_exists_here boundary before mutation or completion.

🔧 Fix: Harden legacy migration and origin ownership boundaries
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:1312 - Intent requires compatibility with existing metadata and says a durably resolved, pruned decision must not prevent a later review. However, migrate-legacy requires <origin>.meta, which normal teardown deletes. An ambiguous pre-upgrade resolution with a surviving report, archive row, decision, and authorized identity mapping therefore cannot be migrated: complete --resolved rejects the missing attestation, while migration fails before checking the mapping. Support migration from durable post-teardown ownership evidence or obtain explicit approval for permanent fail-closed containment; do not require recreating operational metadata.

🔧 Fix: Enable post-teardown legacy decision migration
3 errors still open:

  • 🚨 bin/fm-decision-hold.sh:837 - Intent permits only a decision “later pruned by normal configured retention” and forbids artifact inference, but this rejects only an alias with the active backlog. tasks-axi update --archive-body writes Done snapshots to note-archive.md using the same archive heading and task-row format. With markdown.archive = "data/note-archive.md", a superseded resolution snapshot can satisfy completion after the live task is removed even though Done retention never pruned it. Reject the note-archive alias or require retention-specific provenance.
  • 🚨 bin/fm-decision-hold.sh:1256 - Intent requires “missing ... active decision records must stop completion.” Released code could recreate an active hold after an older resolution with the same ID was pruned. The current positional completion accepts that active hold without detecting the archived duplicate; if it is later removed, repeated complete --none or verify reaches this fallback and accepts the older resolution. Reject active/archive generation collisions at the shared active-verification boundary or persist generation provenance.
  • 🚨 bin/fm-decision-hold.sh:1087 - Intent allows only normal retention history and forbids inference from artifacts, but archive mode remains enabled across ### headings or other column-zero prose while no record is being captured. A resolution-shaped row under ## Archived ... then ### Notes is therefore accepted as pruned history. Reject unexpected top-level content within archive sections instead of continuing to scan it as canonical retention output.

🔧 Fix: Harden retained decision archive provenance
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:1278 - Intent requires “missing ... active decision records must stop completion” and completion to remain “idempotent, compatible with existing metadata.” A base-version home can resolve and archive key k, recreate and successfully inventory an active k, then lose that active row before upgrade; because metadata stores only k, this fallback accepts the older archive generation and verify/complete succeeds despite the missing active record. Conversely, line 1511 makes complete origin k fail after k is answered, so an exact successful completion retry is no longer idempotent. Persist generation/current-vs-historical provenance at the shared completion metadata boundary and explicitly migrate ambiguous legacy inventories rather than inferring provenance from presence or absence.

🔧 Fix: Persist decision completion generation provenance
4 issues (3 errors, 1 warning) still open:

  • 🚨 bin/fm-decision-hold.sh:1730 - Intent requires compatibility with existing metadata, but verify rejects every nonempty pre-change inventory before examining its records. A normal upgraded origin with an exact active captain hold and no archived duplicate now fails teardown verification. Automatically classify source-verifiable active or retained-Done cases at the shared metadata boundary; require explicit reclassification only for genuinely ambiguous archive-only generations.
  • 🚨 bin/fm-decision-hold.sh:1674 - Intent requires completion to remain idempotent. After normal teardown removes metadata, positional complete origin key succeeds for an active post-teardown review but persists no provenance here. Once the hold resolves, repeating that exact successful command reaches the active-only check at line 1630 and fails. Persist completion provenance in a durable shared owner for metadata-free reviews, or explicitly authorize this non-idempotent behavior.
  • 🚨 bin/fm-decision-hold.sh:478 - The policy says a legacy identity remains compatible when it has exactly one valid origin/key decomposition, but this raw substring check counts invalid splits. A released key such as route-decision- is valid and produces origin-decision-route-decision-; the second marker leaves an empty key, so only the original decomposition is valid, yet verification fails as ambiguous. Enumerate only splits whose origin and key both satisfy the supported grammar.
  • ⚠️ .agents/skills/decision-hold-lifecycle/SKILL.md:47 - The normative policy says --resolved may be used only after live metadata is gone, while bin/fm-decision-hold.sh requires it to reclassify historical keys in a surviving released-version metadata file. Following the policy leaves those inventories permanently unverifiable. Permit --resolved for explicit live legacy reclassification as well.

🔧 Fix: Preserve legacy and metadata-free decision completion provenance
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/fm-decision-hold.sh:1779 - Intent requires completion and verification to remain “idempotent, compatible with existing metadata.” Released metadata stores only a union: after successful complete o a followed by complete o b, it contains a,b. Once both records are retained Done, this migration invents a,b as the last invocation (also persisted at line 1948), so retrying the actual last command complete o b is not considered exact and line 1804 incorrectly requires b to be active. Preserve source-verifiable per-key generation provenance without synthesizing call grouping, and let a successful post-upgrade completion establish the actual last lists.
  • ⚠️ docs/decision-hold-lifecycle.md:100 - The section claims to show the test suite's exact summarized output, but it omits the two newly emitted results for source-verifiable legacy migration and metadata-free retry provenance. Add those lines so the recorded regression evidence matches the executable suite.

🔧 Fix: Preserve exact legacy completion retry provenance
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:1891 - Intent requires “Preserve durable ownership of every unresolved captain decision: missing ... active decision records must stop completion” and idempotent completion, but durable inventory is persisted only when metadata is already absent. Sequence: live complete origin a records a only in metadata; normal teardown deletes that metadata; the active a hold disappears; a later report-backed review creates b and complete origin b succeeds because no durable inventory remembers a. Likewise, resolving a after teardown makes the exact original completion retry fail. Persist and reconcile completion provenance at the shared successful-completion boundary even while live metadata exists, so teardown cannot erase it.

🔧 Fix: Persist live decision completion provenance durably
2 errors still open:

  • 🚨 bin/fm-decision-hold.sh:2002 - Intent requires “Preserve durable ownership of every unresolved captain decision” and compatibility with existing metadata. A pre-upgrade inventory containing active key a can be source-classified by teardown’s verify, but this writes provenance only back to metadata; teardown then deletes that metadata. If a later disappears, a report-backed review can complete new key b because no durable inventory remembers a. Merge and persist the classified per-key provenance in the durable completion inventory before verification permits teardown.
  • 🚨 bin/fm-decision-hold.sh:1908 - Intent requires durable ownership and deterministic completion, but live metadata is marked reviewed before the durable inventory write at line 1913. If that later write fails (for example, staging or replacement runs out of space), complete returns failure while teardown’s verify still trusts the newly appended metadata and can delete it, losing the required durable provenance. Persist the durable owner first, then publish the ephemeral metadata attestation, or make both updates transactionally recoverable.

🔧 Fix: Persist decision provenance before metadata teardown
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/fm-decision-hold.sh:960 - Intent permits only history “later pruned by normal configured retention” and forbids inference from “artifacts,” but this alias check compares path strings only. On a common case-insensitive macOS filesystem, configuring data/NOTE-ARCHIVE.md aliases tasks-axi's data/note-archive.md while passing this check; a superseded --archive-body snapshot can then prove a removed decision without Done retention ever pruning it. Reject physical aliases of the backlog and note archive (for example with inode/-ef checks once either leaf exists), not just identical spellings.
  • ⚠️ bin/fm-decision-hold.sh:1439 - Each active or retained key invokes reject_archived_generation_collision, which reparses the entire configured archive, while complete/verify iterate the monotonically growing durable key union. This makes the target long-lived-review path O(keys × archive size). Parse and validate the archive once per invocation and reuse an indexed result for collision and historical lookups.

🔧 Fix: Reject archive aliases and cache retained-history scans
2 errors still open:

  • 🚨 bin/fm-decision-hold.sh:973 - Intent requires history to be “later pruned by normal configured retention” and forbids inference from artifacts, but this rejects only path/inode aliases to note-archive.md. A valid resolved snapshot produced by tasks-axi update --archive-body can be copied to data/history.md, configured as markdown.archive, and accepted after the live row is removed. Bind historical proof to retention-specific provenance rather than any canonical-looking file currently configured as the archive.
  • 🚨 bin/fm-decision-hold.sh:950 - The required compatibility with normal configured retention is broken for valid tasks-axi homes whose contained backlog is not exactly <FM_DATA_OVERRIDE>/backlog.md. For example, the supported/default path = "backlog.md" with archive = "done-archive.md" is rejected before any hold or completion can run. Use the effective contained backlog path and derive its archive owners instead of forcing the Firstmate default layout.

🔧 Fix: Bind retained decisions to effective backlog archives
3 issues (1 error, 2 warnings) still open:

  • 🚨 bin/fm-decision-hold.sh:1749 - Required: historical proof must come from “normal configured retention,” and copied artifacts must not qualify. This code creates legacy bindings after reading an unbound archive row, while note-copy detection at line 1629 is only byte-for-byte comparison. A resolved note snapshot can be appended under a canonical archive heading with only its title changed; the preexisting owner marker/binding then makes complete accept it without any retention prune. Conversely, an honestly pruned row identical to a note snapshot is rejected. Bind each record digest to provenance emitted at the actual tasks-axi prune boundary rather than self-attesting on read or comparing content.
  • ⚠️ bin/fm-decision-hold.sh:1158 - Bindings are named only by hold ID but embed the absolute backlog/archive paths. After an active hold is verified, changing to another valid contained tasks-axi backlog or archive creates the new owner marker, then this comparison hard-fails against the old binding; the source-verifiable active hold can no longer complete or resolve. Namespace bindings by retention owner and allow a new owner binding only while the exact active or retained-Done source remains verifiable.
  • ⚠️ bin/fm-decision-hold.sh:1133 - The marker is appended under a private owner lock rather than tasks-axi's effective-backlog lock. If this overlaps a tasks-axi prune whose later backlog persistence fails, tasks-axi rolls the archive back to its captured length and can remove the marker after the owner file was committed, permanently failing subsequent owner validation. Publish provenance through the same transaction or supported shared lock as archive retention.

🔧 Fix: Bind decision history to actual retention transitions
3 errors still open:

  • 🚨 bin/fm-decision-hold.sh:1734 - Required: completion must be “compatible with existing metadata,” and an older decision “later pruned by normal configured retention may remain pruned.” Every archive produced before this change lacks the newly invented HMAC marker, so this unconditional check rejects the exact already-pruned history the fix targets; even migrate-legacy calls this boundary first and cannot migrate it. Provide a safely authorized compatibility/migration path or obtain explicit approval for permanent fail-closed containment.
  • 🚨 bin/fm-decision-hold.sh:1121 - Required: history pruned by “normal configured retention” must remain usable. Provenance is emitted only while tasks-axi runs through this opt-in NODE_OPTIONS wrapper; the still-supported direct tasks-axi done, prune, or terminal public-followup paths can normally prune a resolved decision without a marker, after which line 1734 permanently rejects it. Put provenance at the shared tasks-axi prune transaction boundary rather than relying on callers to select a parallel wrapper.
  • 🚨 bin/fm-decision-hold.sh:1005 - On a case-insensitive filesystem, configuring NOTE-ARCHIVE.md while neither archive exists passes this check because paths_physically_alias requires both leaves to exist. The first retention transition then creates the shared note/Done archive successfully; subsequent completion detects the now-existing alias and permanently rejects the genuinely pruned records. Detect destination aliases before leaf creation, preserving the required artifact separation and usable normal retention.

🔧 Fix: Harden retained-history provenance and legacy migration
2 errors still open:

  • 🚨 bin/tasks-axi:44 - Required normally pruned resolutions must remain usable. In supported environments where the project bin directory is first on PATH, fm-decision-hold.sh already has a retention hook active when this wrapper re-enters it for done/prune. The nested command inherits the first NODE_OPTIONS hook and installs a second; during a decision archive append, one hook parses the other hook's added marker as malformed task bytes, causing retention to fail or roll back. When provenance is already active, invoke the resolved real tasks-axi binary instead of nesting the owner again.
  • 🚨 bin/fm-decision-hold.sh:957 - Required: “Do not weaken ... symlink ... protections.” destination() treats ENOENT from a dangling symlink as though the link name itself were simply absent. With note-archive.md -> history.md dangling and history.md configured as Done retention, preflight accepts the paths; the first prune creates history.md, after which every completion or verification rejects the now-visible alias. Resolve dangling links with lstat/readlink before accepting future destinations.

🔧 Fix: Harden nested retention and dangling archive alias handling
2 errors still open:

  • 🚨 bin/tasks-axi:48 - Required history “later pruned by normal configured retention” must remain usable, but provenance is added only when callers explicitly select this new wrapper. The repository still directs routine work to bare tasks-axi, no session prepends bin/ to PATH, and direct tasks-axi prune/done remains executable after this change; such a prune creates an unmarked archive row that later completion rejects as pre-boundary history. Route every supported retention command through this boundary or emit provenance in tasks-axi's shared prune transaction.
  • 🚨 bin/fm-decision-hold.sh:2844 - After answer, decline, or resolve succeeds and normal retention later archives the hold, an exact retry fails because verify_hold_resolved checks only tasks-axi show in the active backlog. With valid done_keep=0, this line even reports the first close as failed after it already committed and archived the decision. Validate exact proven archive history at the shared resolved-record boundary so close and channel-delivery retries remain idempotent.

🔧 Fix: Preserve pruned decision close retries
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:2143 - This contradicts “missing ... active decision records must stop completion” and “do not weaken current decision ownership.” A valid sequence remains: an old generation is pruned into configured archive A; configuration moves to contained archive B; the same key is recreated and completed as current; that active row disappears; configuration returns to A. The durable inventory stores only the key/classification, so this fallback accepts A’s older resolution and verify succeeds despite the current generation being missing. Line 2108 similarly lets close retries consume that older generation. Bind each current generation to its verified retention owner or exact record provenance at the shared completion boundary, and only allow historical fallback when that generation’s matching retention transition is proven.

🔧 Fix: Bind current decisions to retention generations
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:2659 - The required invariant says “missing ... active decision records must stop completion,” but a previously current key supplied via --resolved is checked only by verify_hold_historical. Sequence: inventory a replacement generation under archive owner B, remove its active row, switch back to owner A containing an older archived generation, then run complete origin --none --resolved key; this branch accepts A’s history, skips the current-key durable check below, and succeeds despite the missing B generation. Reject --resolved for already-current provenance or validate it through the generation-bound durable boundary.

🔧 Fix: Enforce generation ownership for resolved decision keys
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:2660 - Policy says --resolved carries an older key with exact durable resolution, but this branch calls verify_hold_durable, which also accepts a queued active hold. Sequence: successfully run complete origin key while the hold is active, then run complete origin --none --resolved key before answering it; completion succeeds and records the key in the resolved invocation even though it remains unresolved. Require generation-bound resolved-state verification here rather than the active-or-resolved durable check.

🔧 Fix: Require resolved state for current decision provenance
1 error still open:

  • 🚨 bin/fm-decision-hold.sh:2192 - Required: “missing ... active decision records must stop completion” and “do not weaken current decision ownership.” An older retained-Done generation can still replace a missing current generation: retain resolved key k in backlog owner A, switch to owner B, recreate/inventory k, remove B’s active row, then switch back to A and run complete --none --resolved k. verify_hold_resolved finds A’s old Done row and overwrites the B generation binding here before line 2207 checks it, so completion succeeds. Validate an existing generation binding before mutation; permit owner migration only when continuity with the exact bound active generation is verifiable.

🔧 Fix: Prevent retained Done generation rebinding
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/fm-decision-hold.sh:2286 - Intent requires that “missing ... active decision records must stop completion” and forbids weakening current decision ownership. A queued record can still overwrite an existing generation binding without validation: keep old key k active in backlog A, switch to backlog B, recreate and complete k (binding it to B), remove B’s row, switch back to A, then retry complete origin k or run verify. verify_hold_durable accepts A’s older queued row and rewrites the binding here, so the missing B generation is silently replaced. Check existing generation ownership before every queued-record persistence at the shared verification boundary, allowing migration only when exact continuity is verifiable.
  • ⚠️ docs/decision-hold-lifecycle.md:103 - This block claims to contain the test suite’s exact summarized output, but it omits the newly emitted results “current generations cannot fall back to older archive owners” and “current generations cannot fall back to older retained Done owners.” Add both lines to keep the recorded regression evidence synchronized.

🔧 Fix: Prevent queued decision generation rebinding
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-decision-hold-lifecycle.test.sh
  • Public bin/fm-decision-hold.sh end-to-end fixture: create/resolve an old decision, prune it through configured retention, create a later decision, repeat complete and verify, and confirm archive/backlog hashes and the 10-row Done bound remain unchanged.
  • Missing-record counterfactual: fm-decision-hold.sh complete review-live missing-current was rejected while the valid later hold remained queued and held.
  • Base commit 64d61ae reproduction in an isolated fixture: later completion failed after the older resolved decision was normally archived, demonstrating the original defect.
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed (2) ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: Fix shell lint diagnostics
1 warning still open:

  • ⚠️ linter found issues (exit code 127)

🔧 Fix: Verify shell and workflow lint passes
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

@Charliekirk-creator
Charliekirk-creator force-pushed the fm/fm-decision-hold-pruned-history branch from d8302e6 to 05b1298 Compare August 27, 2026 06:15
@greptile-apps

greptile-apps Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported teardown path now invokes the shared archive-aware decision verifier.

Reviews (3): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile

Comment thread bin/fm-decision-hold.sh
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.

1 participant