Skip to content

feat(history): audited history-repair re-anchor command; clear legacy drift so pm health is ok - #44

Merged
unbraind merged 2 commits into
mainfrom
feat/history-repair-reanchor
May 22, 2026
Merged

unbraind merged 2 commits into
mainfrom
feat/history-repair-reanchor

Conversation

@unbraind

@unbraind unbraind commented May 22, 2026 •

Copy link
Copy Markdown
Owner

Why

For months pm health and pm validate --check-history-drift returned ok: false because 31 legacy history streams carried stale hash chains (31 chain + 15 current-item mismatches) left over from old hand-edits, hierarchy repairs, and the 2026-05 redaction one-off scrub. history-redact only re-anchors a chain when a redaction match occurs, so cleanly-drifted streams could never be repaired. Every agent running pm health saw a non-ok result and couldn't distinguish benign legacy drift from real corruption.

What

Add pm history-repair <id> — the missing peer to history-redact:

  • Replays a stream from empty and recomputes every before/after hash
  • Recovers legacy patch ops that no longer strictly apply (replace→add on first-write paths; skips unresolvable array-shape ops)
  • Reconciles the latest after_hash with the on-disk item document (so --verify and validate both pass)
  • Appends an auditable op=history_repair marker
  • Never modifies item content; safe no-op on a clean stream
  • --dry-run / --author / --message / --force with the same ownership + lock governance as history-redact

Wired into the CLI, MCP (history-repair action), SDK contracts, contracts, shell completion (bash/zsh/fish), and docs (COMMANDS.md, AGENT_GUIDE.md).

Replay dedup (pm-pjs5)

Consolidated the duplicated replay/patch mechanics shared by history, restore, history-redact, and history-repair into src/core/history/replay.ts. Each command keeps a thin local error-formatting wrapper to preserve exact CLI error contracts. No behavior change.

replayHash now falls back to a deterministic structural hash for legacy/partial replay states the canonicalizer can't normalize (e.g. a stream whose first entry is a partial update rather than a create); fully formed documents are unaffected.

Result

Ran history-repair across all 31 drifted streams:

  • pm health → ok: true (all 9 checks; was ok: false for months)
  • pm validate --check-history-drift → 0 drifted items
  • git diff confirms only history JSONL changed — no item content touched

Verification

  • Full coverage gate: 1457 tests pass, replay.ts at 100% lines/branches/functions/statements
  • tsc --noEmit, static-quality gate, docs-skills gate, check-secrets, and package-first dogfood all pass
  • New: tests/unit/history-replay.spec.ts, tests/unit/history-repair-command.spec.ts
  • Temp-dir manual dogfood: inject drift → repair → verify ok / drifted=0; idempotent no-op on clean streams; proper not_found error path

Refs pm-85hm, pm-pjs5, pm-xk39.

🤖 Generated with Claude Code

Summary by Sourcery

Introduce an audited history-repair workflow and centralize history replay logic to clear legacy history drift without touching item content.

New Features:

  • Add a pm history-repair command and MCP/SDK action to re-anchor drifted history chains, reconcile them with the current item document, and append an auditable history_repair marker while supporting dry-run/author/message/force options.

Enhancements:

  • Centralize shared history replay, patch application, verification, and re-anchoring logic into src/core/history/replay.ts and reuse it across history, restore, history-redact, history-repair, and health/validate drift checks without changing existing behavior.

Documentation:

  • Document the new history-repair command and its usage in COMMANDS.md and AGENT_GUIDE.md, including guidance for agents on when and how to clear history drift.

Tests:

  • Add unit tests for the shared history replay/reanchor helpers and the history-repair CLI command, and include the new replay module and command in coverage/release readiness suites.

…gacy drift so pm health is ok

For months `pm health` and `pm validate --check-history-drift` returned ok:false
because 31 legacy history streams carried stale hash chains (31 chain + 15
current-item mismatches) left over from old hand-edits, hierarchy repairs, and
the 2026-05 redaction one-off scrub. `history-redact` only re-anchors a chain
when a redaction match occurs, so cleanly-drifted streams could never be repaired.

Add `pm history-repair <id>`, the missing peer to `history-redact`:
- replays a stream from empty and recomputes every before/after hash
- recovers legacy patch ops that no longer strictly apply (replace->add on
  first-write paths; skips unresolvable array-shape ops)
- reconciles the latest after_hash with the on-disk item document
- appends an auditable op=history_repair marker
- never modifies item content; safe no-op on a clean stream
- --dry-run/--author/--message/--force with the same ownership+lock governance
  as history-redact

Wired into the CLI (register-mutation), MCP (history-repair action + server
instructions), SDK contracts (PM_CORE_COMMAND_NAMES, PM_TOOL_ACTIONS, flag
contracts, parameter keys), contracts.ts, shell completion (bash/zsh/fish), and
docs (COMMANDS.md, AGENT_GUIDE.md).

Consolidate the duplicated replay/patch mechanics shared by history, restore,
history-redact, and history-repair into src/core/history/replay.ts
(ReplayDocument, replayHash, normalizeReplayPatch*, toReplayDocument,
tryApplyReplayPatch, verifyHistoryChain, reanchorHistoryEntries). Each command
keeps a thin local error-formatting wrapper to preserve exact CLI error
contracts. Addresses pm-pjs5. No behavior change.

replayHash now falls back to a deterministic structural hash for legacy/partial
replay states that the canonicalizer cannot normalize (e.g. a stream whose first
entry is a partial update rather than a create); fully formed documents are
unaffected.

Ran history-repair across all 31 drifted streams: pm health is now ok:true and
validate reports 0 drifted items. git diff confirms only history JSONL changed.

Tests: tests/unit/history-replay.spec.ts (replay.ts at 100% coverage) and
tests/unit/history-repair-command.spec.ts; release-readiness allowlist updated.
Full coverage gate (1457 tests), static, docs-skills, check-secrets, and
package-first dogfood all pass.

Refs pm-85hm, pm-pjs5, pm-xk39.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented May 22, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Introduces a new audited pm history-repair command and a shared history replay/reanchor engine, then wires the command through CLI, MCP, SDK contracts, completion, and documentation while cleaning up duplicated replay logic and fixing legacy history drift issues.

Sequence diagram for the new history-repair command flow

sequenceDiagram
  actor User
  participant PmCli as pm_cli
  participant HistoryRepair as runHistoryRepair
  participant Replay as core_history_replay
  participant Store as readLocatedItem
  participant FS as writeFileAtomic

  User ->> PmCli: history-repair <id> [CLI]
  PmCli ->> HistoryRepair: runHistoryRepair(id, options, global)

  HistoryRepair ->> HistoryRepair: resolvePmRoot / readSettings
  HistoryRepair ->> HistoryRepair: resolveItemTypeRegistry
  HistoryRepair ->> HistoryRepair: resolveHistorySubject
  HistoryRepair ->> HistoryRepair: readHistoryEntries

  HistoryRepair ->> Replay: verifyHistoryChain(entries)
  Replay -->> HistoryRepair: { ok, errors }

  HistoryRepair ->> Replay: reanchorHistoryEntries(entries)
  Replay -->> HistoryRepair: { entries, finalDocument, details }

  alt item exists
    HistoryRepair ->> Store: readLocatedItem(located)
    Store -->> HistoryRepair: { document }
    HistoryRepair ->> Replay: replayHash(finalDocument)
    HistoryRepair ->> Replay: replayHash(currentItemReplay)
  end

  HistoryRepair ->> Replay: verifyHistoryChain(rewrittenEntries)
  Replay -->> HistoryRepair: { ok: true }

  alt changed && !dryRun
    HistoryRepair ->> HistoryRepair: acquireLock(...)
    HistoryRepair ->> FS: writeFileAtomic(historyPath, historyEntriesToRaw(rewrittenEntries))
    HistoryRepair ->> HistoryRepair: runActiveOnWriteHooks(...)
  end

  HistoryRepair -->> PmCli: HistoryRepairResult
  PmCli -->> User: printResult(result)
Loading

File-Level Changes

Change Details Files
Add shared history replay/reanchor engine used by history, restore, history-redact, history-repair, and health/validate drift checks.
  • Create core replay module with ReplayDocument model, canonical/toReplay conversions, and replayHash with deterministic fallback hashing for malformed legacy states.
  • Provide normalized patch application utilities (normalizeReplayPatchPath/normalizeReplayPatchOps, tryApplyReplayPatch, lenientApplyReplayPatch) to handle legacy paths and recoverable patch failures.
  • Implement verifyHistoryChain and reanchorHistoryEntries helpers plus JSONL serialization (historyEntriesToRaw) for deterministic chain verification and re-anchoring.
src/core/history/replay.ts
src/core/history/history.ts
src/core/history/index.ts
src/core/history/history-stream-policy.ts
vitest.config.ts
tests/unit/history-replay.spec.ts
Refactor existing CLI history commands to consume the shared replay utilities and expose verification helpers.
  • Export verifyHistoryChain from the new core module and re-use it in the history and history-redact commands instead of local implementations.
  • Replace local replay/replayHash/JSONL helpers in history-redact with imports from the shared replay module, and export a reusable HistorySubject/resolveHistorySubject pair.
  • Remove duplicated replay logic and local types from history and restore, delegating replay/patch behavior to the shared core mechanism while preserving existing error wording via thin wrappers.
src/cli/commands/history.ts
src/cli/commands/history-redact.ts
src/cli/commands/restore.ts
src/cli/commands/index.ts
Introduce the audited pm history-repair command to re-anchor drifted history streams and reconcile them with on-disk items without changing content.
  • Add history-repair CLI implementation that verifies the existing chain, reanchors entries via reanchorHistoryEntries, optionally reconciles the final replay with the current item document, and appends a history_repair audit entry.
  • Respect governance/ownership and locking semantics (author resolution, assignee checks, acquireLock) and integrate with on-write hooks while supporting --dry-run/--author/--message/--force.
  • Ensure the rewritten chain passes verifyHistoryChain and surface structured result fields (chain drift status, counts of rehashed/repaired entries, converted replace→add ops, skipped ops, reconciliation and audit flags).
src/cli/commands/history-repair.ts
src/cli/register-mutation.ts
src/core/extensions/index.ts
src/core/lock/lock.ts
src/core/store/*
tests/unit/history-repair-command.spec.ts
Wire history-repair through MCP, SDK contracts, shell completion, and documentation so agents and users can invoke it consistently.
  • Expose history-repair in CLI contracts and tool action schemas with its flag contracts, and route MCP pm_run actions for history-repair to runHistoryRepair.
  • Add bash/zsh/fish completion entries for history-repair and extend command lists so it appears in help/selection menus.
  • Document history-repair in COMMANDS.md, AGENT_GUIDE.md, and CHANGELOG.md, describing its behavior, guarantees (no item-content changes, idempotent on clean streams), and recommended usage when health/validate report drift.
src/mcp/server.ts
src/sdk/cli-contracts.ts
src/cli/commands/contracts.ts
src/cli/commands/completion.ts
docs/COMMANDS.md
docs/AGENT_GUIDE.md
CHANGELOG.md
Update project metadata, coverage configuration, and internal PM artifacts to reflect the new feature and the resolved drift.
  • Include the new replay module and history-repair command in the coverage/"release readiness" expectations and test configuration.
  • Add/adjust internal agent history and plan files to capture the work items (pm-85hm, pm-pjs5, pm-xk39) and their repaired histories.
  • Regenerate affected .agents history JSONL files to reflect the newly re-anchored chains (hashes only) without modifying item content.
tests/integration/release-readiness-runtime.spec.ts
vitest.config.ts
.agents/pm/history/*.jsonl
.agents/pm/issues/pm-xk39.toon
.agents/pm/issues/pm-85hm.toon
.agents/pm/plans/pm-c3dx.toon
.agents/pm/chores/pm-pjs5.toon

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented May 22, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@unbraind has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 50 minutes and 3 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 15eaa9cc-d1f3-4924-bb98-a300a7b88b18

📥 Commits

Reviewing files that changed from the base of the PR and between 5a23d8a and 6d54705.

📒 Files selected for processing (3)
  • .agents/pm/history/pm-85hm.jsonl
  • .agents/pm/issues/pm-85hm.toon
  • src/cli/commands/history-repair.ts
📝 Walkthrough

Walkthrough

This PR implements a new pm history-repair CLI command that deterministically re-anchors drifted history streams by replaying entries, recomputing hashes, and reconciling with on-disk items. It consolidates duplicated replay and patch logic from history-related commands into a shared src/core/history/replay.ts module.

Changes

History-Repair Feature

Layer / File(s) Summary
Shared replay and verification helpers
src/core/history/replay.ts, src/cli/commands/history.ts, src/cli/commands/history-redact.ts, src/cli/commands/restore.ts, vitest.config.ts
New core module defining ReplayDocument type, deterministic hashing, patch normalization, strict/lenient patch application, chain verification, and re-anchoring logic. Existing history commands refactored to import and use shared utilities instead of local replay implementations.
History-repair command implementation
src/cli/commands/history-repair.ts, src/cli/commands/index.ts
New exported runHistoryRepair command that validates tracker initialization, verifies history chains, re-anchors entries, reconciles with on-disk item state, appends audit markers, enforces governance ownership rules, acquires locks, and returns structured repair metrics with warnings.
CLI registration, completions, and contracts
src/cli/register-mutation.ts, src/cli/commands/completion.ts, src/cli/commands/contracts.ts, src/mcp/server.ts, src/sdk/cli-contracts.ts
Registers history-repair subcommand in mutation group with --dry-run, --author, --message, and --force options. Adds shell completions for Bash/Zsh/Fish. Extends CLI flag contracts and integrates command into MCP server and SDK action schema.
Documentation and changelog
CHANGELOG.md, docs/COMMANDS.md, docs/AGENT_GUIDE.md
Documents new pm history-repair <id> command entry with behavior, flags, and usage examples. Updates command family table. Adds consolidated replay mechanics note and agent guide bullets.
Unit and integration tests
tests/unit/history-replay.spec.ts, tests/unit/history-repair-command.spec.ts, tests/integration/release-readiness-runtime.spec.ts
New suite for shared replay helpers covering document handling, canonicalization, hashing, patch application, chain verification, lenient replay, and re-anchoring. End-to-end command test with drift injection and repair validation. Coverage inclusion update.
Agent metadata and audit tracking
.agents/pm/chores/pm-pjs5.toon, .agents/pm/history/*.jsonl, .agents/pm/issues/pm-85hm.toon, .agents/pm/plans/pm-c3dx.toon, .agents/pm/issues/pm-xk39.toon
Agent task/issue/plan records documenting history-repair implementation. New pm-85hm issue tracks the feature, new pm-c3dx plan enumerates seven implementation steps. Multiple task histories updated with metadata normalization, closure, and history_repair re-anchor entries.

🎯 4 (Complex) | ⏱️ ~60 minutes

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/cli/commands/history-repair.ts" line_range="94-96" />
<code_context>
+  let currentItemReplay: ReplayDocument | null = null;
+  let currentItemPath: string | null = subject.located?.itemPath ?? null;
+  let matchedChainBefore: boolean | null = null;
+  if (subject.located) {
+    const loaded = await readLocatedItem(subject.located, { schema: settings.schema });
+    currentItemReplay = {
+      metadata: loaded.document.metadata as unknown as Record<string, unknown>,
+      body: loaded.document.body,
</code_context>
<issue_to_address>
**suggestion:** Use shared `toReplayDocument` for item reconciliation to ensure hashing matches validate/health semantics.

Here you’re manually constructing `currentItemReplay` from `loaded.document` and then using `replayHash`. Please use the shared `toReplayDocument` helper from `core/history/replay.ts` so metadata canonicalization, ordering, and body defaults match the semantics used by validate/health, preventing subtle hash divergence and ensuring `after_hash` is consistent across the system.

Suggested implementation:

```typescript
import { replayHash, toReplayDocument } from "../../core/history/replay";

```

```typescript
  if (subject.located) {
    const loaded = await readLocatedItem(subject.located, { schema: settings.schema });
    // Use shared toReplayDocument so hashing semantics are consistent with validate/health.
    currentItemReplay = toReplayDocument(loaded.document, { schema: settings.schema });
    const lastOriginalAfterHash = historyEntries[historyEntries.length - 1]?.after_hash;
    matchedChainBefore = replayHash(currentItemReplay) === lastOriginalAfterHash;
  }

```

If `toReplayDocument` has a different signature in your codebase (for example it doesn’t take the `{ schema }` option, or it expects a different options shape), adjust the second argument accordingly, or drop it entirely if not needed:

- Either `currentItemReplay = toReplayDocument(loaded.document);`
- Or `currentItemReplay = toReplayDocument(loaded.document, settings.schema);`

Also verify that the existing import path for `replayHash` is correct in your tree; the SEARCH/REPLACE import edit should match exactly whatever import line currently brings in `replayHash`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/cli/commands/history-repair.ts Resolved

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the pm history-repair command to fix drifted history chains and consolidates shared history replay logic into a new core module. A review comment identifies a redundant item document read in the history-repair command, suggesting an optimization to avoid unnecessary I/O.

Comment thread src/cli/commands/history-repair.ts Outdated

if (changed && !dryRun) {
if (subject.located) {
const loaded = await readLocatedItem(subject.located, { schema: settings.schema });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Redundant read of the item document. The item was already read at line 95. Consider storing the result of the first read in a variable accessible to both blocks to avoid unnecessary I/O.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/commands/history-repair.ts`:
- Around line 81-107: The code reads history and the located item
(readHistoryEntries, verifyHistoryChain, reanchorHistoryEntries,
readLocatedItem, replayHash) and computes replay reconciliation
(currentItemReplay, matchedChainBefore, reconcileNeeded) before acquiring the
item lock, which risks reconciling/writing against stale state; move the calls
that load/compute the historyEntries, chainBefore, reanchor, and any
readLocatedItem + replayHash comparisons so they run while holding the subject's
item lock (i.e., acquire the same lock used for writes before calling
readHistoryEntries and readLocatedItem), then compute matchedChainBefore and
reconcileNeeded while locked and only release the lock after any
decision/read-modify-write sequence completes; keep checks that don't depend on
mutation outside the lock unchanged and ensure lock is released in all code
paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 6df4cc5f-8386-4c3e-b636-0154102d19f9

📥 Commits

Reviewing files that changed from the base of the PR and between 2b043f0 and 5a23d8a.

📒 Files selected for processing (56)
  • .agents/pm/chores/pm-pjs5.toon
  • .agents/pm/history/pm-0c9q.jsonl
  • .agents/pm/history/pm-0kjv.jsonl
  • .agents/pm/history/pm-2eb3.jsonl
  • .agents/pm/history/pm-3akm.jsonl
  • .agents/pm/history/pm-54d.jsonl
  • .agents/pm/history/pm-5rh2.jsonl
  • .agents/pm/history/pm-85hm.jsonl
  • .agents/pm/history/pm-8pzn.jsonl
  • .agents/pm/history/pm-axl0.jsonl
  • .agents/pm/history/pm-c3dx.jsonl
  • .agents/pm/history/pm-cakn.jsonl
  • .agents/pm/history/pm-e0b5.jsonl
  • .agents/pm/history/pm-f3q4.jsonl
  • .agents/pm/history/pm-fdla.jsonl
  • .agents/pm/history/pm-g8gj.jsonl
  • .agents/pm/history/pm-imob.jsonl
  • .agents/pm/history/pm-jkip.jsonl
  • .agents/pm/history/pm-lnq3.jsonl
  • .agents/pm/history/pm-lty.jsonl
  • .agents/pm/history/pm-m0fh.jsonl
  • .agents/pm/history/pm-mb4n.jsonl
  • .agents/pm/history/pm-mcli.jsonl
  • .agents/pm/history/pm-nhka.jsonl
  • .agents/pm/history/pm-pjs5.jsonl
  • .agents/pm/history/pm-q4jp.jsonl
  • .agents/pm/history/pm-rnpb.jsonl
  • .agents/pm/history/pm-sh6o.jsonl
  • .agents/pm/history/pm-tq1.jsonl
  • .agents/pm/history/pm-u83w.jsonl
  • .agents/pm/history/pm-w1r6.jsonl
  • .agents/pm/history/pm-wo8.jsonl
  • .agents/pm/history/pm-xk39.jsonl
  • .agents/pm/history/pm-xk8b.jsonl
  • .agents/pm/history/pm-zau.jsonl
  • .agents/pm/issues/pm-85hm.toon
  • .agents/pm/issues/pm-xk39.toon
  • .agents/pm/plans/pm-c3dx.toon
  • CHANGELOG.md
  • docs/AGENT_GUIDE.md
  • docs/COMMANDS.md
  • src/cli/commands/completion.ts
  • src/cli/commands/contracts.ts
  • src/cli/commands/history-redact.ts
  • src/cli/commands/history-repair.ts
  • src/cli/commands/history.ts
  • src/cli/commands/index.ts
  • src/cli/commands/restore.ts
  • src/cli/register-mutation.ts
  • src/core/history/replay.ts
  • src/mcp/server.ts
  • src/sdk/cli-contracts.ts
  • tests/integration/release-readiness-runtime.spec.ts
  • tests/unit/history-repair-command.spec.ts
  • tests/unit/history-replay.spec.ts
  • vitest.config.ts

Comment on lines +81 to +107
const historyEntries = await readHistoryEntries(subject.historyPath, subject.id);
if (historyEntries.length === 0) {
throw new PmCliError(`No history entries exist for ${subject.id}; nothing to repair.`, EXIT_CODE.USAGE);
}

const chainBefore = verifyHistoryChain(historyEntries);
const reanchor = reanchorHistoryEntries(historyEntries);

// Reconcile the replayed chain with the current on-disk item document so the
// latest after_hash matches what pm validate/health compute for the item.
let currentItemReplay: ReplayDocument | null = null;
let currentItemPath: string | null = subject.located?.itemPath ?? null;
let matchedChainBefore: boolean | null = null;
if (subject.located) {
const loaded = await readLocatedItem(subject.located, { schema: settings.schema });
currentItemReplay = {
metadata: loaded.document.metadata as unknown as Record<string, unknown>,
body: loaded.document.body,
};
const lastOriginalAfterHash = historyEntries[historyEntries.length - 1]?.after_hash;
matchedChainBefore = replayHash(currentItemReplay) === lastOriginalAfterHash;
}

const finalReplay = reanchor.finalDocument;
const reconcileNeeded =
currentItemReplay !== null && replayHash(finalReplay) !== replayHash(currentItemReplay);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Acquire the item lock before reading history/item state to prevent stale rewrites.

historyEntries, replay reconciliation input, and assignee checks are computed before locking. A concurrent mutation can land between those reads and the eventual write, which can overwrite newer history and reconcile against stale item state.

Suggested restructuring (move read/compute under lock)
-  const historyEntries = await readHistoryEntries(subject.historyPath, subject.id);
-  ...
-  const releaseLock = await acquireLock(...);
+  const releaseLock = await acquireLock(...);
   try {
+    const historyEntries = await readHistoryEntries(subject.historyPath, subject.id);
+    // recompute reanchor + reconcile using latest on-disk history/item snapshot
+    // run ownership check on the locked snapshot
+    // then write repaired history
   } finally {
     await releaseLock();
   }

Also applies to: 164-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/history-repair.ts` around lines 81 - 107, The code reads
history and the located item (readHistoryEntries, verifyHistoryChain,
reanchorHistoryEntries, readLocatedItem, replayHash) and computes replay
reconciliation (currentItemReplay, matchedChainBefore, reconcileNeeded) before
acquiring the item lock, which risks reconciling/writing against stale state;
move the calls that load/compute the historyEntries, chainBefore, reanchor, and
any readLocatedItem + replayHash comparisons so they run while holding the
subject's item lock (i.e., acquire the same lock used for writes before calling
readHistoryEntries and readLocatedItem), then compute matchedChainBefore and
reconcileNeeded while locked and only release the lock after any
decision/read-modify-write sequence completes; keep checks that don't depend on
mutation outside the lock unchanged and ensure lock is released in all code
paths.

…ed canonical replay form

- Hoist the located-item read into a single `readLocatedItem` call reused by both
  reconciliation and the ownership/governance check (was read twice). Addresses
  gemini-code-assist review.
- Build `currentItemReplay` via the shared `toReplayDocument` canonical replay
  form instead of manually constructing `{metadata, body}`, so reconciliation
  hashing matches the canonicalization semantics used by pm validate/health and
  avoids subtle hash divergence. Addresses sourcery-ai review.

Verified: tsc clean, history replay/repair/activity suites pass (40 tests), and a
fresh temp-dir dogfood (inject chain+item drift -> repair -> verify ok, validate
drifted=0, item content preserved).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Thanks for the reviews — addressed in 6d54705:

  • @gemini-code-assist: the redundant readLocatedItem is now a single hoisted read (loadedItem) reused by both reconciliation and the ownership check.
  • @sourcery-ai: currentItemReplay is now built via the shared toReplayDocument canonical replay form, so reconciliation hashing matches the canonicalization semantics used by pm validate/health. Re-verified with a temp-dir dogfood (inject chain+item drift → repair → verify ok, validate drifted=0, item content preserved).
  • @coderabbitai: good catch on read-then-lock ordering. history-repair intentionally mirrors the established pattern in history-redact and restore (read history + item → compute → acquire the per-item lock → atomic write with rollback). The actual mutation is guarded by the lock and an atomic, rollback-protected write, and a clean stream is a no-op. Tightening the read-compute-write window to hold the lock across the whole sequence is a reasonable hardening, but doing it for one command would diverge from its two siblings — I'd rather track that as a uniform cross-command follow-up than make the locking model inconsistent here.

All CI gates (coverage 1457 tests + replay.ts 100%, typecheck, static, docs-skills, smokes, macOS/Ubuntu) and check-secrets passed on the first commit and will re-run on the follow-up.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@unbraind Thanks for the detailed response — the consistency rationale makes sense. Keeping history-repair aligned with history-redact and restore's read-compute-lock-write pattern avoids an asymmetric locking model that would be harder to reason about across the three commands; a single follow-up to tighten the window uniformly is the right call.

Would you like me to open a follow-up GitHub issue to track the cross-command lock hardening (read inside the lock across history-redact, restore, and history-repair)?


✏️ Learnings added
Learnt from: unbraind
URL: https://github.com/unbraind/pm-cli/pull/44

Timestamp: 2026-05-22T21:06:50.068Z
Learning: In the pm-cli project, `history-redact`, `restore`, and `history-repair` all follow the same intentional read-compute-lock-write pattern: read history + item document outside the lock → compute the rewritten content → acquire the per-item lock → atomic write with rollback. Tightening the read inside the lock is acknowledged as a future hardening item but is intentionally deferred so the locking model stays consistent across all three sibling commands.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@gemini-code-assist

Copy link
Copy Markdown

Thanks for the detailed follow-up and for addressing the feedback. Your reasoning regarding the locking model makes sense—maintaining consistency with history-redact and restore is a reasonable approach here, and I agree that tracking the locking pattern as a cross-command improvement is the right path forward. The consolidation of the replay logic and the use of toReplayDocument for canonicalization are significant improvements for maintainability. Everything looks solid—good luck with the merge.

@unbraind
unbraind merged commit 1304846 into main May 22, 2026
11 checks passed
unbraind added a commit that referenced this pull request May 22, 2026
…0 lock-hardening follow-up

- pm-85hm closed: history-repair shipped + 31 legacy streams cleared (pm health ok)
- pm-pjs5 closed: replay/patch mechanics single-sourced in core/history/replay.ts
- pm-c3dx plan: 7/7 steps complete + design decision and pm-lty discovery recorded
- pm-uer0 (new): cross-command read-then-lock hardening follow-up from CodeRabbit review

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@unbraind
unbraind deleted the feat/history-repair-reanchor branch May 23, 2026 11:26
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