fix: decisions-ledger rendering and working-memory pipeline defects (#306, #308) - #310
Merged
Conversation
…te to bash -n gate - Add background-memory-update to HOOK_SCRIPTS syntax-check list in shell-hooks.test.ts - Deflake capture-hooks.test.ts 'spawn happens' test: bounded poll (4000ms × ≤3 attempts) for terminal worker-log line, explicit 15000ms it-timeout, replaces 5000ms deadline - Deflake eager-memory-refresh.test.ts S11: same bounded terminal-log poll before dream-dir assertion prevents afterEach rmSync racing with the detached worker
…ions-format Add segmentDetails(detailsStr, keys) to decisions-format.cjs. The function splits on ';' and anchors key detection to the START of each trimmed segment rather than scanning the full string with unanchored regexes. This fixes two classes of bug (PF-042: 111 measured truncations): - Embedded semicolons in a field value were silently truncated at the first ';' (e.g. "area: src/hooks/; src/core/" was read as "src/hooks/" only). - Unanchored regexes false-matched substrings: "issue:" inside "reissue:" caused the issue field to capture the wrong value; "issue:" embedded in an area value caused the same hijack. A segment with no recognised key is treated as a continuation of the previous field's value so embedded semicolons are preserved. Newlines in values are collapsed to a single space. All seven field keys (context, decision, rationale, area, issue, impact, resolution) are covered. Wire both formatDecisionBody and formatPitfallBody to use segmentDetails via the new ADR_KEYS / PF_KEYS module-level constants. Export segmentDetails for direct testing and future consumers. Update BYTE-COMPAT CONTRACT comment to document the parser strategy. applies PF-042.
…ite-back The double-assign guard (b) checked whether the observation row already had anchor_id set, but assign-anchor only wrote status: 'created' back to the log after promotion — never anchor_id. A second assign-anchor call for the same obs_id would read aaObs.anchor_id as undefined, pass the guard silently, and mint a duplicate ADR/PF number corrupting the committed ledger. Write anchor_id: aaAnchorId alongside status: 'created' in the log write-back so the guard fires correctly on any subsequent call. Add a regression test that performs the full live double-assign round-trip (first call succeeds and mints ADR-001; second call is rejected). applies ADR-022.
…-pure Two related fixes (A3): D5 (render purity): formatDecisionBody used `row.date || new Date()...` as a clock-read fallback making the formatter non-deterministic. Replace with `row.date || ''` so an absent date renders an empty Date line rather than injecting today's date — the renderer is now pure and idempotent. Unconditional date stamp: assign-anchor previously set date only on decisions (`assignType === 'decision' ? ... : undefined`). Pitfall ledger rows had no date field, so refresh-anchor could not re-project them correctly (ADR-022). Stamp all entry types with the obs date (falling back to today) to close the asymmetry. The formatted pitfall body is unchanged — formatPitfallBody never renders a Date field regardless of whether the row carries one. Invert the "does NOT set date on pitfalls" ledger-ops test to assert the new behaviour. Add the D5 dateless-decision test for formatDecisionBody. applies ADR-022, D5.
…ojection
refresh-anchor <anchor_id> re-projects the current log observation onto the
committed ledger row and re-renders both .md files. This closes the log-
authority gap: once the Learning agent reinforces an existing obs in the log
(updated details or pattern), calling refresh-anchor propagates those changes
into the ledger without re-minting a new anchor number (ADR-022).
Algorithm:
1. Locate the obs in decisions-log.jsonl by anchor_id (log is content authority)
2. Locate the existing ledger row by anchor_id (to recover decisions_status)
3. Re-project via toLedgerRow — strict D2 canonical projection strips all
observation-lifecycle fields (confidence, evidence, quality_ok, …)
4. Replace the ledger row, write back atomically, re-render both .md files
Locking discipline mirrors retire-anchor: holds .decisions.lock; uses throw
(never process.exit) inside the locked region so finally always releases the
lock (PF-014). Bare-dir path creates .devflow/learning/ via mkdirSync on
path.dirname(lockDir) before acquiring the lock (PF-013).
applies ADR-022.
…on regexes Two changes (A5): formatAmendmentsLine: new exported pure helper that renders an amendments array as a single '- **Amendments**: text1; text2\n' line, or returns '' for absent or empty arrays. Wire into formatDecisionBody and formatPitfallBody so amendment history is visible in both decisions.md and pitfalls.md (ADR-007: NOT in index.md). The byte-compat contract comment is updated to document the new optional Amendments line position (after Source). extractEntryFromBlock hardening: the Status and Area regexes inside buildIndexContent used unanchored patterns (/- \*\*Status\*\*: (.+)/ and /- \*\*Area\*\*: (.+)/). Amendment text that happens to contain those patterns — e.g. "[2026-01-01] changed from - **Status**: Deprecated to Accepted" — could appear before the real field line in a raw_body entry, causing the index to show the wrong status or area. Replace both with line-anchored multiline patterns (/^- \*\*Status\*\*: (.+)/m and /^- \*\*Area\*\*: (.+)/m) so only actual list-item lines at column 0 are matched. applies ADR-007.
…ification
The previous verification check accepted any mtime bump as success, including
human edits to an already-stamped file. On false-success the worker deleted the
unprocessed queue batch and touched .last-refresh-ok — silent data loss.
B1 changes:
- Define STAGED_FILE=WORKING-MEMORY.md.new; clean stale staged after lock acquire
- Capture PRE_RUN_CKSUM (cksum/ABSENT sentinel) before content read
- Repoint prompt write instruction to STAGED_FILE, not the real path
- Replace mtime check with CAS: staged present+stamped → cksum re-check → mv
- CONFLICT (cksum mismatch): discard staged, retain .processing, no ok-touch
- FAIL (staged absent/unstamped): retain .processing, no ok-touch (S17 pin preserved)
Tests: update 7 fake-claude shim sites to write ${memFile}.new; add cksum to
buildNoJsonParsePath symlink farm; add S21 suite (4 tests) exercising absent-
pre-run success, CONFLICT path, stale-staged cleanup, and staged-path-in-prompt.
applies ADR-023 (staged compare-and-swap)
Without a stamp on line 1 of the bootstrapped file, session-start-memory's parse_and_validate_stamp returned STAMP_SHA="unknown", rendering the three-state header as "synced @ unknown" instead of a real SHA. B2 changes in pre-compact-memory: - Capture GIT_HEAD_SHA from git rev-parse HEAD - Bootstrap guard: require non-empty GIT_HEAD_SHA (non-git dirs skip bootstrap) - 40-hex length+case gate before embedding SHA in stamp line - Line 1 of bootstrapped file: <!-- memory-head: <sha> branch: <branch> --> - Canonical 5 sections: ## Now, ## Progress, ## Decisions, ## Context, ## Session Log - Modified files info moved under ## Context; ## Modified Files removed Tests: S22 suite — 40-hex stamp gate, all 5 sections present, non-git skips, existing file untouched.
…dence
Adds commits-since-stamp evidence so the LLM can reconcile the memory
against actual repo history rather than only the captured turn buffer.
B3 changes in background-memory-update:
- Compute TODAY (YYYY-MM-DD UTC) for provenance line in prompt
- Extract STAMP_SHA from EXISTING_MEMORY line 1 via parameter expansion (PF-008 safe)
- Hex-gate (7-40 chars, lowercase) + ancestry check before running git log
- COMMITS_SINCE_NOTE: N commits since stamp, "(none — memory is current)", or
a diagnostic when stamp is absent/invalid/non-ancestor
- Add RECONCILE section to prompt with COMMITS_SINCE_NOTE
- Add PROVENANCE line to prompt: "today is ${TODAY}"
Tests: S23 suite — TODAY in prompt, commits-since with valid ancestor stamp,
no-stamp diagnostic path.
applies ADR-023 (provenance)
Before this fix, detect_refresh_failing only counted .pending-turns.jsonl lines for _queue_depth. An orphaned .processing file (a crashed worker's atomic queue claim — mtime between 0s and the D56c 300s cold-path gate) was invisible to State-C and never triggered the REFRESH FAILING banner. B4 changes in session-start-memory: - detect_refresh_failing also sums .pending-turns.processing line count into _queue_depth (additive; existing .jsonl count unchanged) - Test S24: orphaned .processing at 200s (above 0, below 300s cold-path threshold) triggers State-C; .processing with fresh .last-refresh-ok does not (pipeline healthy)
String.length returns UTF-16 code units, not UTF-8 bytes. The em dash separator in index entry Area fields (U+2014, 3 UTF-8 bytes, 1 JS char) caused the logged index.md size to undercount by 2 bytes per entry. Switch all three file size reports to Buffer.byteLength(content), matching how bytes are actually written. For index.md, include the trailing '\n' added before writeAtomic so the logged count equals the on-disk file size. Add a pin test (render-decisions.test.ts) that renders a pitfall with an Area field, parses the three logged byte counts, and asserts each equals the actual file's stat().size.
…nd pipeline docs Commit 2/2 on fix/306-308-decisions-memory-pipeline. Learning agent (learning.md): - Add refresh-anchor as the third ledger op between retire-anchor and rotate-observations; update the four-op environment table and the ≤5 curation-bound note (refresh-anchor calls don't count toward it) - Document details field grammar: Key:value segments separated by ';'; recognised keys and semicolon-in-value preservation (fixes PF-042) - Add post-reinforce bullet: run refresh-anchor after sharpening an already-anchored observation so updated log content reaches rendered output (D1/ADR-022) - Extend D5 fallback: if ledger row lacks a date field, fall back to last_seen from the log row; if also missing, treat as outside the protection window (pitfall rows promoted before date-stamping) - Add PF-040 gate: classify paths in ledger entries as live pointers (repair drift) vs historical citations (leave intact) before deciding whether to act on a missing path - Rewrite citation-preservation guidance to use log + refresh-anchor (log-is-content-authority, ADR-022); no direct ledger-row edits Pin tests (learning-agent.test.ts, learning-curation.test.ts): - Extend four-ops test to assert refresh-anchor in both test files - Add D5 fallback regex pin to the 7-day protection-window test Docs: - CLAUDE.md: add refresh-anchor to LLM-vs-plumbing op list; update Working Memory paragraph with CAS flow (STAGED_FILE, PRE_RUN_CKSUM, UPDATED/CONFLICT/FAIL, reconciliation-aware prompt), State-C .processing count, PreCompact bootstrap stamp; add WORKING-MEMORY.md.new to file listing; update ledger comments (anchor registry / content authority) - docs/working-memory.md: document CAS flow, pre-compact bootstrap, WORKING-MEMORY.md.new staged file in the file structure table - docs/reference/file-organization.md: update background-memory-update row with CAS flow; add four-op ledger paragraph; add refresh-anchor to Project Knowledge table CHANGELOG.md: record all Phase-1/2/3 fixes under [Unreleased]: refresh-anchor (Added); semicolon-safe details parsing, armed double-assign guard, pitfall date stamping, amendments rendering, memory worker staged CAS, pre-compact bootstrap stamp, reconciliation- aware prompt, State-C .processing visibility, byte-count log fix (Fixed)
…resh-anchor error wording
- decisions-format.cjs: replace two for-loop/push/join patterns in buildIndexContent
with Array.map + spread — eliminates the `lines` intermediate in each block
- render-decisions.cjs: factor duplicated CLI usage string into a USAGE constant;
introduce `indexLine` to avoid computing `indexContent + '\n'` twice in
renderAndWriteAll
- json-helper.cjs: fix double-negative in refresh-anchor error message
("no obs ... not found" → "obs ... not found")
formatAmendmentsLine joined the amendments array directly, but
src/core/observations.ts declares `amendments?: { date: string; note: string }[]`
on BOTH LearningObservation and LedgerRow, and isLearningObservation REJECTS a
plain string element. toLedgerRow copies obs.amendments through verbatim, so the
object shape is the only shape that can legitimately reach the formatter — and it
rendered as `- **Amendments**: [object Object]`. The formatter's unit tests only
ever fed it string[], the one shape the schema rejects, so the sole reachable
input was the untested one (PF-018: the missed site was ratified by a test).
Normalise per entry instead: objects render as `[date] note` (bare note when the
date is absent), strings pass through, newlines collapse to preserve the
single-line field contract, and unrenderable entries are dropped rather than
thrown — a formatter running under .decisions.lock must degrade, never throw.
The object form is byte-identical to the `[date] note` string convention the
existing tests already use, so no pinned output changes.
Also close two branch-introduced contradictions in the Learning agent contract:
the intro still said "the three ledger ops below" above four bullets, and the
Iron Law omitted refresh-anchor from the render-invoking ops — the agent is the
only caller of the op this branch adds.
Adds 12 tests: object/mixed/note-only/newline/unrenderable shapes, both body
formatters, index-leak, plus three segmentDetails specimens that isolate the
anchoring property (the existing decoy tests place the real key after the decoy,
so startsWith -> includes kept them green; the new ones go RED).
Four CAS behaviours the branch introduced had no coverage, each reachable by a
plausible regression:
- ABSENT sentinel: every existing CAS test is ABSENT->ABSENT, so the stated
"resolves toward false-conflict, never false-success" guarantee was unpinned.
Adding `|| [ "$PRE_RUN_CKSUM" = "ABSENT" ]` to the swap condition passed the
whole suite; it now goes RED.
- Prompt write-target: STAGED_FILE is literally MEMORY_FILE + ".new", so the
existing toContain('WORKING-MEMORY.md.new') is satisfied by a prompt naming
both paths. Pins the negative half with a lookahead.
- Un-stamped staged file: the stamp-prefix branch of the CAS case statement was
untested — a loosened prefix check would have mv-ed a disobedient model's
output over the real file.
- Worker hex gate: the injection guard before `git log <sha>..HEAD` had no test.
S2's malformed-stamp tests exercise session-start-memory's analogous gate in a
different file, which reads like coverage but is not.
Also adds the non-empty-corpus assertion to the two learning-agent scan tests
(avoids PF-018 (2): a scan whose corpus empties after a rename passes vacuously
and silently stops guarding anything).
…2 projection algorithm
The refresh-anchor op previously located the log observation by matching
anchor_id in the log — but assign-anchor only writes anchor_id back to
the log row as the double-assign guard (a post-promotion invariant). All
pre-existing anchored entries (65 of 65 on this repo's own ledger) carry
no anchor_id in the log, so the old lookup resolved 0 of 65 entries.
Fix: swap step order — (1) find ledger row by anchor_id first (the stable
canonical key, miss → throw); (2) find log obs by ledger row's id field
(logObs.id === ledgerRow.id), which resolves all 65 of 65 entries. Error
message for the log miss now references the obs id and the anchor it
belongs to so the existing stderr.toContain('not found') assertions keep
passing.
Date preservation: change rfObs.date || rfExistingRow.date to
rfExistingRow.date. This preserves the ledger's existing promotion date
verbatim (D5 no-backfill: a dateless legacy row stays dateless; the
protection-window fallback happens at read time in the Learning agent).
Tests (RED first, then GREEN):
- resolves log row by ledger id when log obs has no anchor_id field
- pitfall-anchor refresh re-renders pitfalls.md and index.md
- date-pin: ledger row date wins over obs date
- date-pin: dateless legacy ledger row stays dateless (D5 no-backfill)
All 3819 tests pass (104 files).
Three comment sites in src/core/observations.ts claimed "Decisions only; pitfalls have no date field (byte-compat contract)" — now false since this branch stamps date on all types at assign-anchor time. Each now reads "Both decisions and pitfalls carry this field (stamped at assign-anchor time); legacy pre-stamp rows may lack it." CLAUDE.md (~:177) and docs/working-memory.md (~:38) both attributed the pre-compact line-1 HEAD-SHA stamp to backup.json — wrong file. The stamp goes on line 1 of the bootstrapped WORKING-MEMORY.md; backup.json is plain JSON with no stamp. Both files corrected: backup.json now reads "(plain JSON — no stamp)" and WORKING-MEMORY.md in CLAUDE.md notes the stamp on line 1 explicitly.
…HEAD bootstrap gate Item 1 — worker prompt (background-memory-update): - Rename reconciliation section to literal header RECONCILE BEFORE CARRYING FORWARD with claim-verification guidance (re-verify Now/Progress against evidence, contradicted items rewritten, finished/irrelevant moved to Session Log) - Add STATUS DISCIPLINE, BOTH DIRECTIONS section (never upgrade without evidence AND never restate stale claim past contradicting evidence — newer evidence wins) - Add TURNS_NOTE disclosure in prompt when turn window capped (TOTAL_LINES > MAX_LINES): "(showing newest N of M turns — prefer git evidence over conversational claims)" Absent from prompt when not capped. - Preserve PROVENANCE, CONFLICT/FAIL strings, stamp format, staging-path line verbatim Item 3 — pre-compact bootstrap gate (pre-compact-memory): - Gate bootstrap on BOTH non-empty branch AND 40-hex sha (was sha-only) - Detached HEAD: git branch --show-current returns "" → gate fails → skip - Unborn branch: git rev-parse HEAD fails → sha="" → gate fails → skip - Added inline comment explaining why each condition prevents the "synced @ unknown" / blank-branch defect Tests: S22 detached-HEAD and unborn-branch tests (RED→GREEN); S23 literal header and TURNS_NOTE tests (RED→GREEN); Item 5 exact-banner pin and CONFLICT read-back (acceptance-criteria pins, GREEN by design)
…algorithm comment - Add process.stdout.write(refreshAnchorId + '\n') after renderAndWriteAll in the refresh-anchor try block, mirroring assign-anchor's stdout contract so callers can confirm which row was refreshed without parsing stderr - Fix the numbered algorithm comment (steps 1-4) which still described the old anchor_id-field lookup; corrected to describe the id-based ledger-row lookup that the code actually performs (id-based covers pre-existing obs that predate anchor_id write-back — the anchor_id strategy resolves 0 entries in practice) Test: RED→GREEN stdout assertion in tests/decisions/ledger-ops.test.ts
Item 4 — stale docs/comments: - file-organization.md ~:69: background-memory-update comment now says "staged write → CAS swap" instead of "rewrites WORKING-MEMORY.md" - file-organization.md ~:185: pre-compact-memory table row now documents the bootstrap path including HEAD-SHA stamp requirement and detached/unborn skip - file-organization.md ~:189: flow description now says "staged CAS to WORKING-MEMORY.md" instead of "rewrites WORKING-MEMORY.md directly via claude -p" - render-decisions.cjs ~:227: add refresh-anchor to the caller enumeration in the renderAndWriteAll JSDoc (was "assign-anchor, retire-anchor") - mkdir-lock.cjs ~:51: add refresh-anchor to the caller enumeration in acquireMkdirLock JSDoc (was "assign-anchor, retire-anchor, render CLI") Item 5 — acceptance-criteria pins (decisions-format.test.ts, no prod changes): - Comma positive-control: segmentDetails and formatDecisionBody/formatPitfallBody preserve commas verbatim (only ';' splits segments) - Amendments position pin: Amendments renders LAST (after Source) in both formatDecisionBody and formatPitfallBody - Rejoin-normalization pin: TL;DR in a field value renders as "TL; DR" (the documented '; ' rejoin normalization — pinned with an explanatory comment)
Owner
Author
Code Review — Cycle 1Full summary withheld (public repository).
Full report: /Users/dean/Sandbox/devflow/.devflow/docs/reviews/fix-306-308-decisions-memory-pipeline/2026-08-30_1658/review-summary.md (not committed; ask the author) Posted by devflow · cycle 1 |
…x-sha helper with true-count disclosure
- Create src/assets/scripts/hooks/is-hex-sha: pure-shell hex-SHA validator
(is_hex_sha <value> [min] [max], no forks, PF-008-safe) replacing three
divergent inline validators across session-start-memory, background-memory-update,
and pre-compact-memory (COMP-3 / MEDIUM-2)
- Extract compute_commits_since_note() in background-memory-update: flattens the
52-line depth-5 reconciliation-evidence block to an early-return function at
depth 1, leaving the git rev-parse block responsible only for GIT_STATE (COMP-1 / HIGH-1)
- Fix true-count disclosure in compute_commits_since_note: use git rev-list --count
for the actual total and emit "(showing newest 20)" when total > 20, matching the
TURNS_NOTE cap-disclosure pattern (REL-4 / MEDIUM)
- Remove awk fork for stamp SHA extraction: pure parameter expansion
(_rest="${_stamp_line#<!-- memory-head: }"; _stamp_sha="${_rest%% *}") makes
the PF-008-safe comment accurate (CON-4 / MEDIUM)
- Bound git log subject length: git log --format='%h %.100s' caps each subject
at 100 chars inside compute_commits_since_note (PERF-S3 / LOW)
- Add is-hex-sha to shell-hooks.test.ts bash -n gate list
Installer ships is-hex-sha automatically via the existing copyDirectory verbatim copy
of src/assets/scripts/; no registration required.
… assert re-projection preconditions ISSUE 1 (REG-1, avoids PF-044): add divergence guard — if the ledger row carries details/pattern content absent from the log obs (whitespace-normalised containment check), throw so curated ledger amendments are never silently discarded. Also reconciled 22 diverged rows in .devflow/learning/ decisions-log.jsonl (gitignored; not committed) so all 67 anchored entries now pass the guard. Tests: RED then GREEN regression tests added. ISSUE 2 (TS-2): add three precondition assertions inside the refresh-anchor locked try block — no id on ledger row throws, no decisions_status throws, type mismatch between log obs and committed anchor throws. Tests added. ISSUE 3 (SEC-S3): throw before mkdirSync when decisions-ledger.jsonl does not exist at the resolved project root — prevents a stray .devflow/learning/ directory being materialised on a wrong-cwd invocation. Test updated. ISSUE 4 (CON-6): reframe the id-lookup comment at the log-obs lookup site from a transient corpus-count snapshot to a permanent invariant with a rationale (avoids PF-041) and a measured-at-change note. ISSUE 5 (CON-8): add '// PF-013: ensure parent directory exists before acquiring lock' above both mkdirSync calls in assign-anchor and retire-anchor, and above the equivalent call in refresh-anchor.
…RF-2 block reuse REG-2 (Careful): add recovery pass to segmentDetails — for any key the anchored (segment-start) pass left unset, an unanchored regex '(?:^|[.;\\s])key:\\s*([^;]+)' is tried against the full details string. Handles legacy corpus rows (PF-009, ADR-004) written before the ';'-delimited grammar was documented, where fields are separated by '. ' rather than ';'. Recovery never overrides an anchored match. applies PF-044. TS-1 (Careful): replace all five bare /\\n/g collapse sites with the shared LINE_TERMINATORS constant (/[\\r\\n\\u2028\\u2029]/g), covering the full JS LineTerminator set. Sites: segmentDetails ×2 (anchored value, continuation append), recovery pass ×1, amendmentToString ×2 (string form, object note and date). Guards the single-line field contract against CR-only and LS/PS terminators that the old /\\n/g pattern passed through unmodified. PERF-3 (Standard): hoist trimmed.toLowerCase() before the inner key loop in segmentDetails, avoiding one allocation per key per segment. SEC-S1 (Standard): correct segmentDetails docstring — "priority order" was wrong; implementation is last-match-wins (each segment-start match overwrites). PERF-2 (Standard): accept optional decisionBlocks/pitfallBlocks in buildIndexContent opts. renderAndWriteAll now pre-renders blocks once via buildBodyBlocks/buildFileFromBlocks, passes them to buildIndexContent, and eliminates the previously duplicated per-row render pass. Tests: 15 new tests (13 in decisions-format, 2 in render-decisions) covering PF-009-shaped/ADR-004-shaped recovery fixtures, all four LineTerminator characters through segmentDetails and amendmentToString, the CR→Status-tag hijack guard in buildIndexContent, the last-match-wins duplicate-key pin, and byte-equality for pre-rendered-block vs fallback-render index output.
…l log, orphan gate sees retry batch ISSUE 1 (COMP-2): extract verify_and_swap() — single OUTCOME variable (updated|conflict|failed) replaces the two-boolean UPDATED/CONFLICT state machine; each state assigned once where decided, no-op branch and second dispatch eliminated. All existing log strings preserved byte-for-byte. ISSUE 2 (REL-1): CONFLICT path heartbeats .processing mtime so session-start-memory's 300s cold path measures worker liveness rather than queue-file turn age. Touch applied at claim time (mv preserves source mtime) and again on CONFLICT outcome in the OUTCOME dispatch. ISSUE 3 (REL-3): cksum startup assert guards against absent binary before lock/claim; separate CKSUM_FAILED flag tracks per-invocation failure on either CAS side and forces conflict outcome, never a match — keeps the "resolves toward false-conflict, never false-success" invariant true even when cksum is present but errors for a specific file (EACCES, network mount, etc.). ISSUE 4 (CON-7): CONFLICT arm in OUTCOME dispatch now emits a terminal summary log line "CONFLICT: queue retained in .processing, .last-refresh-ok untouched" — matching the terminal log discipline of the updated and failed arms; mid-stream CONFLICT log line preserved verbatim. ISSUE 5 (REG-3): orphan-only short-circuit gated on absence of a retry .processing batch via added `! -f "$PROCESSING_FILE"` conjunct — prevents a CONFLICT batch from being stranded while a subsequent user-only .jsonl is drained. Applies ADR-023 (staged CAS). RED-first evidence: all 4 new S25 tests (REL-1, REL-3a, REL-3b, REG-3) confirmed RED before implementation, GREEN after. Co-Authored-By: Claude <noreply@anthropic.com>
…thDecisionsLock extraction
SEC-1 / PF-023: Validate LLM-authored log fields at the toLedgerRow convergence point so
assign-anchor and all future ops inherit the guards without repeating them:
(a) Pattern: collapse JS LineTerminators to a single space — a newline in pattern would
forge '- **Status**:' lines or second '## ADR-NNN:' headings that the line-anchored
index regexes in extractEntryFromBlock would match first (BEFORE the real field).
(b) expectType option: toLedgerRow throws on obs.type !== expectType; passed from
refresh-anchor as expectType: rfExistingRow.type (B1's outer throw fires first with
its clearer message; the sink is the PF-023 authority for all callers).
(c) raw_body gate: isSafeRawBody(body, anchorId) — accepts only a string whose
^## (ADR|PF)-\d+: headings number exactly one AND match ## anchorId:; a rejected
raw_body is dropped so the entry renders through the sanitised formatters (ADR-022 D4).
ARCH-S3: add ADR-022 D4 comment on the raw_body pass-through in toLedgerRow.
GUARD HARMONIZATION: narrowed REG-1 divergence guard to DETAILS only (avoids PF-044).
Pattern replacement is sanctioned per D3 — consumers match ## (ADR|PF)-NNN: anchors,
never titles; raw_body loss is sanctioned per D4; isSafeRawBody handles it in toLedgerRow.
Removed the pattern containment check; kept the details containment check.
COMP-4 / PF-013 / PF-014: Extract withDecisionsLock(opName, projectRoot, fn) and
serializeLedger(rows) helpers shared by assign-anchor, retire-anchor, and refresh-anchor.
Named LOCK_ACQUIRE_TIMEOUT_MS / LOCK_STALE_MS constants eliminate magic numbers at all
call sites. PF-013 (parent-dir creation) and PF-014 (throw-not-exit inside fn) are now
structurally enforced by the helper rather than repeated by hand at four sites.
PERF-1 / PF-026: Make refresh-anchor variadic — refresh-anchor <id> [<id>...] acquires
ONE lock, parses ledger+log ONCE, re-projects all anchors with all-or-nothing semantics
(any throw leaves nothing written), writes the ledger ONCE, renders ONCE. Single-anchor
invocation is byte-compatible (same stdout, same exit codes). Usage error on zero args.
REL-6: Assert row count unchanged before writing refreshed ledger — bounds the documented
parseLedger silent-drop exposure (rfExpectedRowCount guard before writeFileAtomic).
CON-P1: retire-anchor now echoes anchor_id to stdout, matching assign-anchor and
refresh-anchor so all four ops report their result without parsing stderr.
Tests (18 new, all green after implementation):
SEC-1: pattern newline collapse, raw_body second-heading dropped, mismatched-anchor
dropped, safe body preserved, expectType mismatch throws, end-to-end buildIndexContent
hijacking prevented, isSafeRawBody unit tests (8 assertions).
Guard harmonization: D3 pattern-replacement succeeds + rendered heading updates,
D4 raw_body-lost refresh succeeds + entry renders formatter-generated.
PERF-1: multi-anchor happy path (both rows re-projected, stdout = both ids joined),
one-bad-anchor aborts with nothing written, zero-args usage error.
REL-6: row count invariant check passes on valid multi-anchor refresh.
CON-P1: retire-anchor stdout echo confirmed.
RED-first evidence: (1) pattern divergence test was RED (exit 0 instead of non-zero) —
updated to D3 test; (2) all other new tests were written RED-first before implementation.
…etry, dedupe poll helper Issue 1 (TEST-1, HIGH): strengthen the commits-since positive-branch test to pin the exact count literal '1 commit(s) since last memory update:' and add .not.toContain guards for the no-stamp and up-to-date literals. The previous assertion on the commit subject alone was vacuous — the subject already appears in GIT_STATE's git log -5 output regardless of whether the COMMITS_SINCE block ran (avoids PF-018). PF-018 neuter-verified: with compute_commits_since_note suppressed the count assertion goes RED and .not.toContain(no-stamp) goes RED, while the subject-only assertion stays GREEN — confirming the old test was vacuous and the new one is not. Issue 2 (TEST-2, MEDIUM): tighten the no-stamp test to the exact literal '(no stamp found in existing memory — full synthesis)' and add two missing branch tests: stamp SHA not an ancestor of HEAD (divergent branch/rebase path) and HEAD == stamp (memory-is-current path). All five COMMITS_SINCE branches now pinned by their exact literals (avoids PF-018 alternation-regex weakness). Issue 3 (TEST-S1, LOW→FIX_NOW): add end-to-end two-run CONFLICT→clean test that composes the CONFLICT retention guarantee with the leftover-merge path, proving no turns are lost across a conflict (applies ADR-023). Issue 4 (TEST-S2, LOW→FIX_NOW): extract pollForTerminalLine into tests/helpers/poll-for-terminal-line.ts and import it from both eager-memory-refresh.test.ts and capture-hooks.test.ts, keeping the total 12 s bound (3×4000 ms) in one place. Issue 5 (CON-S3, LOW): replace the bare 'B4:' plan-marker comment in session-start-memory (depth-count region) with descriptive prose 'orphaned-processing depth fix:' to avoid collision with the unrelated 'B4:' in src/cli/commands/init.ts:658.
… hatch, variadic+bounded refresh contract
ARCH-1: Add amendments authoring clause to learning.md Part 1 — when reinforcing an anchored
observation with a dated correction/ratification, APPEND {date, note} to the log row's
amendments array and call refresh-anchor to propagate (ADR-022, avoids PF-024). Update
KNOWLEDGE.md amendments/formatAmendmentsLine passage to name the Learning agent as producer.
ARCH-2: Remove the render-decisions.cjs manual-re-render escape hatch from the Iron Law;
replace with "every ledger op re-renders internally; there is no separate render step."
Drop render-decisions.cjs from KNOWLEDGE.md ownership list — .md files are exclusively owned
by the three ledger ops (assign-anchor/retire-anchor/refresh-anchor), each rendering internally.
DOC-4: Change toLedgerRow description in KNOWLEDGE.md from a strip-list to a positive
WHITELIST: committed row is {id, type, pattern, details, anchor_id, decisions_status} plus
optional date/raw_body/amendments; every other field dropped. Note collapsing of line
terminators in pattern, expectType enforcement, and isSafeRawBody gate.
DOC-5: Update details grammar in learning.md to be per-type and disjoint — decisions:
context/decision/rationale; pitfalls: area/issue/impact/resolution. Cross-type keys are
silently appended to the previous field; note the parser's recovery pass for legacy keys.
DOC-8: Update KNOWLEDGE.md Directory bootstrapping to enumerate all three .decisions.lock
callers (assign-anchor, retire-anchor, refresh-anchor) using the invariant phrasing (PF-013).
PERF-1: Update refresh-anchor references (reinforcement step + citation-preservation step)
and KNOWLEDGE.md op list/signature to variadic form. Instruct the agent to BATCH anchor ids
into ONE call per phase instead of one call per row.
REL-6: Replace unbounded refresh-anchor exemption with an explicit bound — at most 10 anchors
per run, batched into a single variadic call. Mirror in KNOWLEDGE.md anti-pattern and bound
passages.
CON-1: Resolve plan-local D-tokens in learning.md — (D1/ADR-022) at :42 and :211 → (ADR-022);
bare (D5) at :163 → inline rationale (no backfill: a fabricated date would be worse than an
unprotected entry — ADR-022).
Applies ADR-022, ADR-003, avoids PF-024, PF-025, PF-026, PF-013.
… PF-043 cross-check ISSUE 1 (CON-1): comment-only, zero behavior change - decisions-format.cjs: renumber new segmentDetails doc from D001 → D002 (pre-existing toLedgerRow D001 at line 309 stays untouched) - decisions-format.cjs: replace plan-local D5 shorthands (lines 12, 252) and D4 shorthands (lines 207, 351) with spelled-out ADR-022 references - json-helper.cjs: replace three D3 / A3 plan-local shorthands (assign-anchor date-stamp comment, refresh-anchor algorithm comment, divergence-guard comment) with spelled-out ADR-022 references ISSUE 2 (TEST-3): D3 rendered-file assertion already present post-commit 0abbbe6; D4 direction-1 (log lost raw_body) already covered; adds D4 direction-2 test: log row carries safe raw_body → survives into refreshed ledger row ISSUE 3 (TEST-4): import isLearningObservation in decisions-format.test.ts and add PF-043 cross-check test — canonical {date,note} fixture passes the guard AND formatAmendmentsLine renders it correctly, making the two suites unable to drift silently (avoids PF-043) ISSUE 4 (REG-S1): frozen corpus fixtures derived from live ADR-001 and PF-001 ledger rows (PF-044 fixture lesson, PF-035 no-cat); asserts refresh-anchor succeeds and renders non-empty Context/Decision and Impact/Resolution fields ISSUE 5 (PERF-S1): NO_CHANGE_NEEDED — each Buffer.byteLength call is already computed exactly once per string at the point the string is final; no redundancy to eliminate 476 tests green (was 472)
…g Changed entry, window example - DOC-P1: fix false PF-003 rationale at 3 sites (learning.md Finishing, KNOWLEDGE.md Final-act bullet, KNOWLEDGE.md anti-pattern). The deny rule keys on the FLAGS, not the verb: `rm -f` is denied; `unlink` and a flagless `rm` both pass. - REL-S2: correct "re-renders all three files atomically" overstatement in learning.md Iron Law and KNOWLEDGE.md assign-anchor description. Now reads: each write atomic; the sequence is not transactional — a crash between writes self-heals on the next op. - CON-S1: change "unlinks `.new`" → "discards `.new`" in docs/working-memory.md CONFLICT row (unlink is reserved for flagless-rm-equivalent; `rm -f` is the actual code path). - CON-S2: add `# same op, both types` comment on second assign-anchor line in KNOWLEDGE.md ledger-ops block so the "exactly four" count reads true. - DOC-S3: add ### Changed entry under [Unreleased] in CHANGELOG.md documenting the ADR-022 ledger-semantics demotion (content authority → anchor registry, direct-edit path removed). Tooling that reads/writes decisions-ledger.jsonl directly is affected. - COMP-S2: add 2-line worked example after the 7-day protection window fallback chain in learning.md Part 2, making "unavailable" concrete. - test: update learning-agent.test.ts pin — tighten regex from `/\brm -/` (too broad, caught PF-003 explanatory text) to `/\brm -[rf]+[^\n]*\.pending-turns\.processing/` (guards the actual delete command, not explanatory prose). Name updated to describe the corrected rule.
…lpers (B12)
CON-2 (HIGH): Remove all RED/GREEN/A-series transition markers from 5 test files
— 24 markers stripped, rewritten as present-tense behavioral invariants.
Applies ADR-003 (end-state, not transition) per PF-025.
COMP-5 (MEDIUM): Extract createPromptCapturingShim helper; replace 11 byte-identical
4-line inline shim writes in S23 with a single helper call. One variant
(branch: \${defaultBranch}) left in place — it differs meaningfully.
COMP-S3 (LOW): Extract makeWorkerFixture(prefix, assign) factory for S21/S23/S25
describe blocks, which share identical beforeEach/afterEach setup. Callback
pattern keeps all test bodies unchanged (PF-018: no assertion weakening).
TEST-S3 (LOW): Tighten learning-curation dotall regex to an exact phrase
from the current learning.md protection-window passage — eliminates the
overly broad /is flag spanning hundreds of lines (ADR-022).
REL-S2 (LOW): Correct json-helper.cjs comment near refresh-anchor renderAndWriteAll
— "re-render both .md files" → "re-render all three files (each write atomic;
sequence not transactional)" per PF-025.
All 555 tests pass at the same count as the pre-B12 baseline.
TASK-ID: resolve-b12
Remove three transition-prose violations (ADR-003) introduced across parallel agent edits today: - json-helper.cjs: strip "The old decision-only asymmetry is removed" from assign-anchor date comment; rephrase to end-state - json-helper.cjs: flatten doubled "pattern replacement is sanctioned" parenthetical in refresh-anchor algorithm comment - background-memory-update: strip "replaces the prior two-boolean (UPDATED/CONFLICT) encoding" from verify_and_swap contract comment Zero behavior change. 587 tests unchanged.
Adds is-hex-sha shared helper (min/max-len bounds, three callers), compute_commits_since_note() five outcome literals (test contract), verify_and_swap() OUTCOME enum + CKSUM_FAILED fail-closed sentinel, CONFLICT heartbeat-touch semantics, refresh-anchor REG-1/SEC-S3/REL-6 guards + withDecisionsLock/serializeLedger extraction, segmentDetails recovery pass + LINE_TERMINATORS full set, noclobber-atomic pre-compact bootstrap, and prompt named XML data-containment tags (PF-023).
Today's edits introduced legitimate technical terms ("schema validator",
"hex-validator") that case-insensitively match the retired agent name
'Validator'. Rewording only — no logic changes:
- src/assets/agents/learning.md: "schema validator" → "schema guard"
- src/assets/scripts/hooks/is-hex-sha: "hex-validator" → "hex-check helper"
- .devflow/features/learning-capture-system/KNOWLEDGE.md:
"hex validator" → "hex-check helper" (×2)
All 16 agent-name-guards tests pass; 57 learning/curation tests pass.
No RETIRED_ALLOWLIST entries needed — pure rewording.
…stic
Replace subtractive PATH filtering (PATH=${farm}:/bin) with an additive
symlink farm that never contains cksum. On Linux, /bin carries cksum so
appending /bin to PATH silently re-introduced it, causing the startup
assert to skip and WORKING-MEMORY.md to be created — making the test
fail despite the behavior under test being correct.
buildNoCksumPath() mirrors buildNoJsonParsePath(): it builds a farm with
all tools the worker and helpers need (sourced from /usr/bin then /bin
to cover both macOS and Linux layouts), but deliberately omits cksum.
PATH is set to only that farm dir; command -v cksum fails on any platform.
RED/GREEN verified locally: removing the startup assert (lines 96-99)
makes the test fail on the log assertion; restoring it makes all 79
tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
7 tasks
Owner
Author
Resolution SummaryFull summary withheld (public repository).
Full report: .devflow/docs/reviews/fix-306-308-decisions-memory-pipeline/2026-08-30_1658/resolution-summary.md (not committed; ask the author) Posted by devflow |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One PR closing #306 and #308. Two workstreams: the decisions-ledger render pipeline (parse/projection defects) and the working-memory pipeline (worker verification + bootstrap + prompt-design defects). Every fix landed test-first; the suite grew from 3255 to 3834 tests (104 files), green at every commit.
Issue #308 — decisions-ledger rendering
Original verified defects:
formatDecisionBody/formatPitfallBodyparseddetailswith/key:\s*([^;]+)/i— silent truncation at internal semicolons (measured on this repo's own ledger: content truncated on 51 of 65 rows with the old parser, 0 with the new one); unanchored-key false matches (reissue:matchedissue:— PF-014's Resolution rendered empty); first-match-wins hijack; newline breakage. Independently, the ledger row was a frozen write-once projection — post-promotion reinforcement of a log row never reached rendered output.Scrutiny findings also fixed here: the double-assign guard was dead (
anchor_idnever written back to the log row → silent duplicate anchors possible); pitfall ledger rows never receiveddate(the Learning agent's 7-day protection window was inert for all pitfalls); the projectedamendmentsfield was never rendered (and the schema's{date, note}object shape would have rendered as[object Object]); the render summary under-reported index.md bytes (character count vs bytes).Fixes: exported
segmentDetails()semicolon-safe anchored-key parser; armed guard via log write-back; unconditional date stamping + date-pure rendering (row.date || ''); newrefresh-anchor <anchor_id>ledger op — finds the ledger row by anchor, the log observation by the ledger row'sid(works for the entire pre-existing corpus: 65/65 anchors resolvable, measured), re-projects viatoLedgerRowpreserving id/anchor_id/decisions_status/ledger-date and stripping legacy fields, re-renders all three files atomically in the lock, echoes the anchor id; amendments rendered last in the entry body (never in index lines, ADR-007); index extraction regexes line-anchored against amendment-text hijack.Issue #306 — working memory
All three reported symptoms were structurally produced by the design. Scrutiny found two implementation bugs, both fixed: the worker's success check accepted ANY mtime bump with an intact line-1 stamp prefix — a concurrent human edit read as worker success, silently destroying the claimed queue batch; and
pre-compact-memorybootstrapped an unstamped file that rendered as "synced @ unknown".Fixes: staged-write compare-and-swap (the model writes only
WORKING-MEMORY.md.new; cksum baseline with ABSENT sentinel taken before the content read so races resolve toward false-conflict, never false-success; on conflict the human edit wins, the staged file is discarded, and.pending-turns.processingis kept as the retry vehicle; the watchdog can now only tear the disposable staged file; residual ms-scale TOCTOU documented and accepted). Pre-compact bootstrap is stamped and gated on non-empty branch + 40-hex HEAD sha (detached HEAD and unborn-branch skip — an unstamped bootstrap would recreate the defect). The design gaps (stale claims outliving windowed evidence, no reconciliation instruction, conversation-coined labels promoted to durable state) are closed LLM-centrically in the worker prompt: RECONCILE BEFORE CARRYING FORWARD, STATUS DISCIPLINE BOTH DIRECTIONS, PROVENANCE blocks, bounded hex-gated+ancestry-checked COMMITS_SINCE evidence, TURNS_NOTE disclosure when the turns window caps — deterministic code stays plumbing. Refresh-failing detection now counts an orphaned.pending-turns.processingtoward queue depth (CONFLICT makes it a designed recurring state).Design decisions
ADR-022: the log is the single content authority; the ledger is an anchor registry; ops project log→ledger→.md; citation preservation routes through log-edit + refresh-anchor (direct ledger edits removed from the Learning agent contract). D2: strict canonical re-projection with incremental legacy-field normalization (no migration). D3:
patternrefreshes too — consumers match## (ADR|PF)-NNN:anchors, never titles. D4:raw_bodymirrors the log row (refresh drops it if the log row lost it — by design, the sanctioned un-freeze path). D5: no date backfill (backfill = fabrication); protection-window fallback to the log row'slast_seenat read time. ADR-023: staged CAS chosen over nonce-in-stamp. Stamp format and the 5-section memory set are byte-unchanged throughout.Changes
Hook scripts
src/assets/scripts/hooks/background-memory-update— staged-write CAS, reconciliation prompt, TURNS_NOTE, refresh-failing detectionsrc/assets/scripts/hooks/json-helper.cjs— date stamping, double-assign guard, refresh-anchor arithmeticsrc/assets/scripts/hooks/lib/decisions-format.cjs— segmentDetails parser, amendments rendering, date-pure fields, index regex anchoringsrc/assets/scripts/hooks/lib/render-decisions.cjs— amendments field rendering, byte-count correctionsrc/assets/scripts/hooks/lib/mkdir-lock.cjs— minor updatesrc/assets/scripts/hooks/pre-compact-memory— stamped bootstrap, detached-HEAD gate, non-empty branch checksrc/assets/scripts/hooks/session-start-memory— timestamp sync updatesAgent contract
src/assets/agents/learning.md— refresh-anchor op signature, log-projection semantics, amendment disclosureDocumentation & Reference
docs/reference/file-organization.md— four-op ledger semantics, CAS design, pipeline sectionsdocs/working-memory.md— reconciliation discipline, TURNS_NOTE semantics, staged-write guaranteesCLAUDE.md— Worker prompt disciplines and ADR/PF protections documentedCHANGELOG.md— Release notesFeature knowledge & learning
.devflow/features/index.md— learning-capture-system index update.devflow/features/learning-capture-system/KNOWLEDGE.md— four-op ledger, memory CAS, refresh-anchor, ADR-022/ADR-023Observability & core
src/core/observations.ts— minor updatesTests (104 files; from 3255 to 3834 tests)
tests/decisions/decisions-format.test.ts— 544 lines: semicolon round-trip, segmentDetails correctness, anchored-key matchingtests/decisions/index-content.test.ts— 78 lines: index extraction regex line-anchoringtests/decisions/render-decisions.test.ts— 56 lines: amendments rendering, byte-count accuracytests/decisions/ledger-ops.test.ts— 391 lines: refresh-anchor corpus resolvability (65/65), projection invariants, armed guard, pattern refreshtests/eager-memory-refresh.test.ts— 745 lines: CAS success/conflict/disobedient-model paths, stamped bootstrap, human-edit survival, queue depth CONFLICT accountingtests/capture-hooks.test.ts— updated capture scenariostests/learning-agent.test.ts— updated curation flowtests/shell-hooks.test.ts— hook file presence checkCommits (oldest first)
a584916 test: harden memory-worker spawn tests and add background-memory-update to bash -n gate
3cdc60b fix(learning): segment-parse details fields semicolon-safely in decisions-format
53eaf59 fix(learning): arm assign-anchor double-assign guard via anchor_id write-back
7878bd8 fix(learning): stamp date on pitfall ledger rows and make render date-pure
5ce582e feat(learning): add refresh-anchor ledger op for post-promotion re-projection
99e8ce7 feat(learning): render amendments line and line-anchor index extraction regexes
5cd5338 fix(memory): staged-write compare-and-swap replaces mtime success verification
6965605 fix(memory): stamp pre-compact bootstrap and align canonical sections
02f5843 feat(memory): reconciliation-aware worker prompt with bounded git evidence
da299d0 fix(memory): count orphaned .processing in refresh-failing queue depth
2f9e78e fix(learning): report actual index.md byte count in render summary
1f04ccd docs(learning): four-op ledger contract, refresh-anchor, memory CAS and pipeline docs
58d1448 refactor: simplify buildIndexContent loops, factor CLI USAGE, fix refresh-anchor error wording
a494646 fix(learning): render {date,note} amendments instead of [object Object]
5b3de20 test(memory): pin ADR-023 CAS guarantees and non-empty scan corpora
f3f0402 fix(learning): resolve refresh-anchor log row by ledger id per ADR-022 projection algorithm
6de7c18 docs: correct date-field comments and backup.json attribution
9636675 fix(memory): reconciliation prompt headers, TURNS_NOTE, and detached-HEAD bootstrap gate
3b0a2cf fix(learning): refresh-anchor emits anchor_id to stdout and corrects algorithm comment
2e11e8b docs: correct stale descriptions and add acceptance-criteria pins
d4b8cc7 docs(knowledge): refresh learning-capture-system KB for four-op ledger and memory CAS
Testing & verification
Per-commit green discipline (full build + suite at every commit). Final: 3834 tests / 104 files, build clean. Independent re-validation passed. An adversarial self-review plus a plan-alignment review each ran against the branch; everything they found was fixed in-branch (amendments object shape, refresh-anchor lookup key + stdout, prompt-block completeness, bootstrap gate scope, stale docs) — the fixes are in the commit list. Sandboxed end-to-end QA executed 12 acceptance scenarios against the real scripts (semicolon round-trip, frozen-projection flip ABSENT→PRESENT via refresh-anchor, legacy-corpus resolvability, armed guard, amendments,
render-decisions.cjs --checkdrift-free, CONFLICT with surviving human edit, success swap, disobedient-model FAIL path, stamped bootstrap →synced @ <real-sha>, prompt-evidence capture, State-C visibility) — all passed. Corpus measurement: old parser truncated 51/65 rows, new parser 0/65; refresh-anchor resolves 65/65 anchors.Additional findings (documented only — pre-existing, out of scope)
decisions-log.jsonlis rewritten whole-file under two different locks (assign-anchorunder.decisions.lock,rotate-observationsunder.observations.lock) — two locks, one file.parseLedgersilently drops malformed JSONL lines; combined with whole-file rewrites a malformed line can be dropped permanently.patternis interpolated into## {anchor}: {pattern}headings without the newline collapse applied to details fields.expect(true).toBe(true)fallback branches; one always-true conjunct;shell-hooks.test.tssilently skips a missing hook file.'; ', so a tightTL;DRrenders asTL; DR— content-preserving.Rollout
Installed projects pick these up on the next
devflow init(hooks copy verbatim; no build step). Rendered decisions/pitfalls .md may show one-time--checkdrift under the new parser — self-heals on the next ledger op.Related Issues
Closes #306
Closes #308