From 6c92be73ac31e37f7ad6cc5be82ec4dcc479ada6 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 26 Jul 2026 06:19:55 +0100 Subject: [PATCH 1/6] Reconcile resolved failure ledger projections Signed-off-by: Chris0Jeky --- docs/agentic/FAILURE_LEDGER.md | 9 +++--- docs/agentic/failure_ledger.jsonl | 4 +++ scripts/agent_hooks/render_failure_ledger.py | 33 +++++++++++--------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/agentic/FAILURE_LEDGER.md b/docs/agentic/FAILURE_LEDGER.md index 83a123445..8b9346f78 100644 --- a/docs/agentic/FAILURE_LEDGER.md +++ b/docs/agentic/FAILURE_LEDGER.md @@ -8,7 +8,6 @@ Rows sharing a surface and first tracking issue in `future_fix` show only their | Date | Class | Surface | Failure | Workaround | Future fix | Status | | --- | --- | --- | --- | --- | --- | --- | -| 2026-05-11 | seed | agentic-pack | Ledger created | n/a | Start recording recurring failures and promote confirmed lessons | open | | 2026-05-29 | blocker | dependabot/nuget | dotnet-minor-patch repeatedly proposed EF Core 9.x while Sqlite/Design remained 8.x, causing ambiguous ExecuteDeleteAsync errors on #1102/#1106 | Pin EF Core back to 8.0.27 per PR | Resolved by #1112 ignore rule and the EF runtime 8.x pins in #760/#767 | resolved | | 2026-05-29 | blocker | dependabot/nuget | After FluentAssertions moved to free 7.x in #1088, Dependabot immediately proposed paid v8 in #1117 | Close the v8 PR | Resolved by the FluentAssertions major ignore rule in #1118/ADR-0034 | resolved | | 2026-05-29 | invalid_signal | ci/e2e-smoke | E2E Smoke intermittently times out at multi-board.spec.ts:197 restoredBoard visibility with a transient DB connection error unrelated to the PR diff | Investigate against the diff; rerun only the failed job when unrelated | Stabilize the archive-to-restore seed or adjust the visibility wait after root-cause proof | open | @@ -17,13 +16,13 @@ Rows sharing a surface and first tracking issue in `future_fix` show only their | 2026-07-13 | pre_existing_noise | test/redis-lifecycle | #1298's second full backend run failed RedisCacheServiceTests.Dispose_IsNotSerialized_BehindAnInFlightConnect; the exact test passed 5/5 | Park #1298 without a PR instead of treating a narrow rerun as a green full gate | Resolved by #1392 (closes #1332) on 2026-07-17: a dedicated named background Thread replaces Task.Run so the dispose-vs-connect seam is reached deterministicall... | resolved | | 2026-07-13 | pre_existing_noise | test/background-workers | #1334's full suite let a hosted LLM worker pre-claim a test row and a delayed presence join arrive after events were cleared | Keep the full run non-green and link exact evidence from the PR | Resolved 2026-07-17: workers half by #1394 (closes #1335) + #1391 (closes #1383); presence half by #1366 (snapshot ordering) + #1371 (phase drains). A shared Ho... | resolved | | 2026-07-17 | non_blocking_risk | github/project-sync | Resolution record for the 2026-07-13 project-sync scope row: the gh token lacked read:project scope so Priority fields were unaudited | No workaround needed any longer | Resolved 2026-07-17 (#1327 scope item): maintainer ran gh auth refresh -s project; Sync-TaskdeckProjectPriority.ps1 audit scanned 1000 items with 0 needing sync | resolved | -| 2026-07-13 | pre_existing_noise | frontend/workspace-mode-ordering | #1334 required E2E observed a late workspace summary restore guided after the user selected workbench during a failed preference save; exact local Playwright th... | Treat the CI failure as real, track the asynchronous ordering seam separately from the auth PR, and rerun only after focused investigation | #1343: version summary mode application against newer explicit preference actions and prove it with deterministic store tests plus repeated E2E | open | -| 2026-07-13 | blocker | frontend/paper-review-contract | Real Paper Review proposals deserialize numeric ConflictTone values into a frontend string-only contract, causing tone.toLowerCase to throw and the ErrorBoundar... | Park #1274 after preserving a clean local branch; do not treat passing API-level apply assertions as valid Paper UI proof | #1347: align deep-review enum wire contracts and add serialized API plus Paper browser regressions | open | -| 2026-07-13 | blocker | backend/similar-past | GET /api/automation/proposals/{id}/similar-past returned HTTP 500 for at least four distinct real SQLite-backed capture proposals during #1274 Paper runs | Keep the failure visible despite Promise.allSettled fallback and frontend retries; park the coverage PR rather than certifying a noisy review path | #1348: capture the server exception in a SQLite API test and repair the bounded board-scoped query path | open | | 2026-07-14 | blocker | ci/extended-workflow | Resolution record for the earlier #1330 open row: CI Extended reusable Gitleaks permission startup failures are repaired and the tracking issue is closed | No workaround remains; use exact-head Extended runs as the verification signal | Resolved by 66382e6c (Fix CI Extended Gitleaks permissions); #1330 closed 2026-07-13 | resolved | | 2026-07-14 | blocker | ci/nightly-k6 | CI Nightly run 29229402012 and prior runs lost both k6 summary JSON files to bind-mount permission denial while the tagged SQLite board-write p95 gate failed at... | Do not treat the always-red lane as trustworthy regression evidence until summary ownership and the measured capacity contract are repaired | #1358: map both k6 containers to the host UID/GID, warn at the measured 2000ms capacity, gate at 2200ms, and prove exact-head CI. Resolved: First post-#1359 nig... | resolved | | 2026-07-25 | non_blocking_risk | dependency/sqlite-native | Resolution record: the inherited HIGH SQLite native advisory is removed from Taskdeck's resolved dependency graph | No workaround remains; keep the runtime security-floor regression and dependency audit green | #1345 resolved by centrally pinning the matched SQLitePCLRaw bundle/core/provider/native family at 2.1.12; SQLite 3.53.3 loads, the >= 3.50.2 floor test passes,... | resolved | -| 2026-07-26 | non_blocking_risk | agent/tool-command-composition | Repeated PowerShell/native composition mistakes caused parser failures, GitHub body argument splitting, jq quote loss, and optional rg exits hiding independent ... | Collect statement-form foreach output before piping; handle optional rg exit 1 explicitly; use typed GitHub connectors for multiline bodies; prefer gh --json pl... | #1490: add copyable PowerShell/native composition forms to the MCP tooling guide, verify the failure-ledger renderer and docs gates, then append a resolution re... | open | +| 2026-07-26 | pre_existing_noise | frontend/workspace-mode-ordering | Resolution record for the #1343 preference-vs-summary ordering defect; the original observed failure remains in append-only history | No workaround remains for the core stale-summary overwrite; retain separate residual debt instead of overstating closure | #1343 core resolved by PR #1386 merge dba47320e348b80ecb678c41a51b686c954527ee; separate server-order/cross-device and client-state residuals remain #1388/#1410... | resolved | +| 2026-07-26 | blocker | frontend/paper-review-contract | Resolution record for the #1347 numeric deep-review enum wire-contract crash; the original observed failure remains in append-only history | No workaround remains; valid numeric payloads render directly and unexpected values fail closed to visible warning states | #1347 resolved by PR #1360 merge ed2538ffb0388523882238653b34fcc6f431a512: API numeric-enum serialization, frontend mapping, conservative malformed-value handli... | resolved | +| 2026-07-26 | blocker | backend/similar-past | Resolution record for the #1348 SQLite-backed similar-past HTTP 500; the original observed failure remains in append-only history | No workaround remains; keep the bounded board/user scope and real SQLite/browser coverage as regression evidence | #1348 resolved by PR #1361 merge ccd7bf93721c07171099383c77bfb508c23fbf48; Paper-browser completion by PR #1362 merge 40b7bb2f3819dcc7d0bd6349a8f516fd3169190f. ... | resolved | +| 2026-07-26 | non_blocking_risk | agent/tool-command-composition | Resolution record for the repeated #1490 PowerShell/native command-composition failures; the original recurrence remains in append-only history | No workaround remains; use the copyable safe forms and classify future failures through the ledger process | #1490 resolved by PR #1491 merge 61f718af941c56c31b8b7595333b9debd6e47da8: the MCP tooling guide now covers collected foreach output, optional rg exits, safe mu... | resolved | ## Classification diff --git a/docs/agentic/failure_ledger.jsonl b/docs/agentic/failure_ledger.jsonl index 5c770017a..b6090f06f 100644 --- a/docs/agentic/failure_ledger.jsonl +++ b/docs/agentic/failure_ledger.jsonl @@ -17,3 +17,7 @@ {"ts":"2026-07-14T00:00:00Z","class":"blocker","surface":"ci/nightly-k6","failure":"CI Nightly run 29229402012 and prior runs lost both k6 summary JSON files to bind-mount permission denial while the tagged SQLite board-write p95 gate failed at its stale 1500ms target","workaround":"Do not treat the always-red lane as trustworthy regression evidence until summary ownership and the measured capacity contract are repaired","future_fix":"#1358: map both k6 containers to the host UID/GID, warn at the measured 2000ms capacity, gate at 2200ms, and prove exact-head CI. Resolved: First post-#1359 nightly green on 326b6b5b @ 2026-07-18T05:39:02Z (run 29632619187).","status":"resolved"} {"ts":"2026-07-25T23:34:12Z","class":"non_blocking_risk","surface":"dependency/sqlite-native","failure":"Resolution record: the inherited HIGH SQLite native advisory is removed from Taskdeck's resolved dependency graph","workaround":"No workaround remains; keep the runtime security-floor regression and dependency audit green","future_fix":"#1345 resolved by centrally pinning the matched SQLitePCLRaw bundle/core/provider/native family at 2.1.12; SQLite 3.53.3 loads, the >= 3.50.2 floor test passes, the solution vulnerability audit is clean, EF has no pending model changes, and the self-contained win-x64 API health smoke returns 200","status":"resolved"} {"ts":"2026-07-26T03:59:00Z","class":"non_blocking_risk","surface":"agent/tool-command-composition","failure":"Repeated PowerShell/native composition mistakes caused parser failures, GitHub body argument splitting, jq quote loss, and optional rg exits hiding independent evidence during Cycle 4","workaround":"Collect statement-form foreach output before piping; handle optional rg exit 1 explicitly; use typed GitHub connectors for multiline bodies; prefer gh --json plus ConvertFrom-Json; avoid Markdown-bearing double-quoted wrappers","future_fix":"#1490: add copyable PowerShell/native composition forms to the MCP tooling guide, verify the failure-ledger renderer and docs gates, then append a resolution record only after merge and issue closure","status":"open"} +{"ts":"2026-07-26T05:11:40Z","class":"pre_existing_noise","surface":"frontend/workspace-mode-ordering","failure":"Resolution record for the #1343 preference-vs-summary ordering defect; the original observed failure remains in append-only history","workaround":"No workaround remains for the core stale-summary overwrite; retain separate residual debt instead of overstating closure","future_fix":"#1343 core resolved by PR #1386 merge dba47320e348b80ecb678c41a51b686c954527ee; separate server-order/cross-device and client-state residuals remain #1388/#1410. The merged Home/Today guard has deterministic store and repeated E2E proof.","status":"resolved"} +{"ts":"2026-07-26T05:11:40Z","class":"blocker","surface":"frontend/paper-review-contract","failure":"Resolution record for the #1347 numeric deep-review enum wire-contract crash; the original observed failure remains in append-only history","workaround":"No workaround remains; valid numeric payloads render directly and unexpected values fail closed to visible warning states","future_fix":"#1347 resolved by PR #1360 merge ed2538ffb0388523882238653b34fcc6f431a512: API numeric-enum serialization, frontend mapping, conservative malformed-value handling, and Paper Chromium coverage prove the repaired wire contract.","status":"resolved"} +{"ts":"2026-07-26T05:11:40Z","class":"blocker","surface":"backend/similar-past","failure":"Resolution record for the #1348 SQLite-backed similar-past HTTP 500; the original observed failure remains in append-only history","workaround":"No workaround remains; keep the bounded board/user scope and real SQLite/browser coverage as regression evidence","future_fix":"#1348 resolved by PR #1361 merge ccd7bf93721c07171099383c77bfb508c23fbf48; Paper-browser completion by PR #1362 merge 40b7bb2f3819dcc7d0bd6349a8f516fd3169190f. The SQLite root cause and unmocked UI path are covered.","status":"resolved"} +{"ts":"2026-07-26T05:11:40Z","class":"non_blocking_risk","surface":"agent/tool-command-composition","failure":"Resolution record for the repeated #1490 PowerShell/native command-composition failures; the original recurrence remains in append-only history","workaround":"No workaround remains; use the copyable safe forms and classify future failures through the ledger process","future_fix":"#1490 resolved by PR #1491 merge 61f718af941c56c31b8b7595333b9debd6e47da8: the MCP tooling guide now covers collected foreach output, optional rg exits, safe multiline GitHub bodies, Markdown/backtick boundaries, and gh --json plus ConvertFrom-Json; #1490 closed 2026-07-26.","status":"resolved"} diff --git a/scripts/agent_hooks/render_failure_ledger.py b/scripts/agent_hooks/render_failure_ledger.py index 87cbe6385..94634b676 100644 --- a/scripts/agent_hooks/render_failure_ledger.py +++ b/scripts/agent_hooks/render_failure_ledger.py @@ -54,24 +54,37 @@ def projection_key(entry: dict[str, object], index: int) -> tuple[str, ...]: def project_latest_entries(entries: list[dict[str, object]]) -> list[dict[str, object]]: - """Keep the latest file-order state for each tracked surface/issue pair.""" + """Hide seed metadata and keep the latest tracked surface/issue state.""" + visible_entries = [entry for entry in entries if entry.get("class") != "seed"] latest_indexes: dict[tuple[str, ...], int] = {} keys: list[tuple[str, ...]] = [] - for index, entry in enumerate(entries): + for index, entry in enumerate(visible_entries): key = projection_key(entry, index) keys.append(key) latest_indexes[key] = index return [ entry - for index, (entry, key) in enumerate(zip(entries, keys, strict=True)) + for index, (entry, key) in enumerate(zip(visible_entries, keys, strict=True)) if latest_indexes[key] == index ] -def main() -> int: +def render_markdown(entries: list[dict[str, object]]) -> str: + """Render projected ledger entries without inventing fallback history.""" rows: list[str] = [] + for entry in project_latest_entries(entries): + date = str(entry.get("ts", ""))[:10] or "unknown" + rows.append( + f"| {cell(date, 20)} | {cell(entry.get('class'), 40)} | {cell(entry.get('surface'), 80)} | " + f"{cell(entry.get('failure'))} | {cell(entry.get('workaround'))} | {cell(entry.get('future_fix'))} | {cell(entry.get('status'), 40)} |" + ) + + return HEADER + "\n".join(rows) + FOOTER + + +def main() -> int: entries: list[dict[str, object]] = [] if JSONL.exists(): for line in JSONL.read_text(encoding="utf-8").splitlines(): @@ -85,18 +98,8 @@ def main() -> int: continue entries.append(entry) - for entry in project_latest_entries(entries): - date = str(entry.get("ts", ""))[:10] or "unknown" - rows.append( - f"| {cell(date, 20)} | {cell(entry.get('class'), 40)} | {cell(entry.get('surface'), 80)} | " - f"{cell(entry.get('failure'))} | {cell(entry.get('workaround'))} | {cell(entry.get('future_fix'))} | {cell(entry.get('status'), 40)} |" - ) - - if not rows: - rows.append("| 2026-05-11 | seed | agentic-pack | Ledger created | n/a | Start recording recurring failures and promote confirmed lessons | open |") - MD.parent.mkdir(parents=True, exist_ok=True) - MD.write_text(HEADER + "\n".join(rows) + FOOTER, encoding="utf-8") + MD.write_text(render_markdown(entries), encoding="utf-8") return 0 From b3838d0632d9465f4ec16216d13fca312d568d4d Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 26 Jul 2026 06:20:03 +0100 Subject: [PATCH 2/6] Test failure ledger reconciliation Signed-off-by: Chris0Jeky --- .../agent_hooks/test_render_failure_ledger.py | 127 +++++++++++++++++- 1 file changed, 124 insertions(+), 3 deletions(-) diff --git a/scripts/agent_hooks/test_render_failure_ledger.py b/scripts/agent_hooks/test_render_failure_ledger.py index a9c8286cd..4f69c5e30 100644 --- a/scripts/agent_hooks/test_render_failure_ledger.py +++ b/scripts/agent_hooks/test_render_failure_ledger.py @@ -1,6 +1,7 @@ import unittest +from collections import Counter -from render_failure_ledger import project_latest_entries +from render_failure_ledger import project_latest_entries, render_markdown class FailureLedgerProjectionTests(unittest.TestCase): @@ -42,14 +43,134 @@ def test_same_surface_with_different_tracking_issues_remains_visible(self) -> No def test_rows_without_tracking_issues_remain_unique(self) -> None: entries = [ - {"surface": "tooling", "future_fix": "Investigate", "status": "open"}, - {"surface": "tooling", "future_fix": "Investigate", "status": "resolved"}, + { + "class": "invalid_signal", + "surface": "ci/e2e-smoke", + "future_fix": "Stabilize the archive-to-restore seed", + "status": "open", + }, + { + "class": "invalid_signal", + "surface": "ci/e2e-smoke", + "future_fix": "Stabilize the archive-to-restore seed", + "status": "resolved", + }, ] projected = project_latest_entries(entries) self.assertEqual(projected, entries) + def test_seed_only_input_renders_no_failure_rows(self) -> None: + entries = [ + { + "ts": "2026-05-11T00:00:00Z", + "class": "seed", + "surface": "agentic-pack", + "failure": "Ledger created", + "status": "open", + } + ] + + rendered = render_markdown(entries) + data_rows = rendered.split("| --- | --- | --- | --- | --- | --- | --- |", 1)[1] + + self.assertNotIn("| seed |", rendered) + self.assertFalse(any(line.startswith("| ") for line in data_rows.splitlines())) + + def test_reconciled_targets_project_exact_latest_state(self) -> None: + entries = [ + { + "class": "seed", + "surface": "agentic-pack", + "future_fix": "Start recording recurring failures", + "status": "open", + }, + { + "class": "invalid_signal", + "surface": "ci/e2e-smoke", + "future_fix": "Stabilize the archive-to-restore seed", + "status": "open", + }, + { + "class": "pre_existing_noise", + "surface": "frontend/workspace-mode-ordering", + "future_fix": "#1343: guard summary mode application", + "status": "open", + }, + { + "class": "blocker", + "surface": "frontend/paper-review-contract", + "future_fix": "#1347: align enum wire contracts", + "status": "open", + }, + { + "class": "blocker", + "surface": "backend/similar-past", + "future_fix": "#1348: repair the SQLite query", + "status": "open", + }, + { + "class": "non_blocking_risk", + "surface": "agent/tool-command-composition", + "future_fix": "#1490: document safe composition", + "status": "open", + }, + { + "class": "pre_existing_noise", + "surface": "frontend/workspace-mode-ordering", + "future_fix": "#1343 resolved by PR #1386", + "status": "resolved", + }, + { + "class": "blocker", + "surface": "frontend/paper-review-contract", + "future_fix": "#1347 resolved by PR #1360", + "status": "resolved", + }, + { + "class": "blocker", + "surface": "backend/similar-past", + "future_fix": "#1348 resolved by PR #1361 and PR #1362", + "status": "resolved", + }, + { + "class": "non_blocking_risk", + "surface": "agent/tool-command-composition", + "future_fix": "#1490 resolved by PR #1491", + "status": "resolved", + }, + ] + + projected = project_latest_entries(entries) + target_surfaces = { + "#1343": "frontend/workspace-mode-ordering", + "#1347": "frontend/paper-review-contract", + "#1348": "backend/similar-past", + "#1490": "agent/tool-command-composition", + } + + self.assertEqual(len(entries), 10) + self.assertEqual(len(projected), 5) + self.assertEqual(Counter(entry["status"] for entry in projected), {"resolved": 4, "open": 1}) + self.assertFalse(any(entry.get("class") == "seed" for entry in projected)) + + for issue, surface in target_surfaces.items(): + matches = [ + entry + for entry in projected + if entry.get("surface") == surface + and str(entry.get("future_fix", "")).startswith(issue) + ] + self.assertEqual(len(matches), 1, issue) + self.assertEqual(matches[0]["status"], "resolved", issue) + + open_entries = [entry for entry in projected if entry["status"] == "open"] + self.assertEqual( + [(entry["class"], entry["surface"]) for entry in open_entries], + [("invalid_signal", "ci/e2e-smoke")], + ) + if __name__ == "__main__": unittest.main() From 75eb8023597802f7d775dc775dd642e7fe00167b Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 26 Jul 2026 06:51:00 +0100 Subject: [PATCH 3/6] Fail closed on invalid failure ledger input Signed-off-by: Chris0Jeky --- scripts/agent_hooks/render_failure_ledger.py | 49 ++++++-- .../agent_hooks/test_render_failure_ledger.py | 112 +++++++++++++++++- 2 files changed, 148 insertions(+), 13 deletions(-) diff --git a/scripts/agent_hooks/render_failure_ledger.py b/scripts/agent_hooks/render_failure_ledger.py index 94634b676..d5b3ec53b 100644 --- a/scripts/agent_hooks/render_failure_ledger.py +++ b/scripts/agent_hooks/render_failure_ledger.py @@ -4,6 +4,7 @@ import json import re +import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] @@ -40,6 +41,10 @@ TRACKING_ISSUE = re.compile(r"#\d+") +class LedgerFormatError(ValueError): + """Raised when a nonblank JSONL line is not a JSON object.""" + + def cell(value: object, limit: int = 160) -> str: text = str(value or "").replace("\n", " ").replace("|", "\\|") return text[:limit] + ("..." if len(text) > limit else "") @@ -84,19 +89,39 @@ def render_markdown(entries: list[dict[str, object]]) -> str: return HEADER + "\n".join(rows) + FOOTER -def main() -> int: +def load_entries(path: Path) -> list[dict[str, object]]: + """Load and validate every nonblank JSONL line before rendering.""" + if not path.exists(): + return [] + entries: list[dict[str, object]] = [] - if JSONL.exists(): - for line in JSONL.read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - try: - entry = json.loads(line) - except json.JSONDecodeError: - continue - if not isinstance(entry, dict): - continue - entries.append(entry) + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if not line.strip(): + continue + + try: + entry = json.loads(line) + except json.JSONDecodeError as exc: + raise LedgerFormatError( + f"{path}: line {line_number}: invalid JSON: {exc.msg} at column {exc.colno}" + ) from exc + + if not isinstance(entry, dict): + raise LedgerFormatError( + f"{path}: line {line_number}: expected a JSON object, got {type(entry).__name__}" + ) + + entries.append(entry) + + return entries + + +def main() -> int: + try: + entries = load_entries(JSONL) + except (LedgerFormatError, OSError, UnicodeError) as exc: + print(f"Failure ledger render failed: {exc}", file=sys.stderr) + return 1 MD.parent.mkdir(parents=True, exist_ok=True) MD.write_text(render_markdown(entries), encoding="utf-8") diff --git a/scripts/agent_hooks/test_render_failure_ledger.py b/scripts/agent_hooks/test_render_failure_ledger.py index 4f69c5e30..0f099daf0 100644 --- a/scripts/agent_hooks/test_render_failure_ledger.py +++ b/scripts/agent_hooks/test_render_failure_ledger.py @@ -1,7 +1,14 @@ +import io +import json +import tempfile import unittest from collections import Counter +from contextlib import redirect_stderr +from pathlib import Path +from unittest.mock import patch -from render_failure_ledger import project_latest_entries, render_markdown +import render_failure_ledger as ledger +from render_failure_ledger import load_entries, project_latest_entries, render_markdown class FailureLedgerProjectionTests(unittest.TestCase): @@ -78,6 +85,36 @@ def test_seed_only_input_renders_no_failure_rows(self) -> None: self.assertNotIn("| seed |", rendered) self.assertFalse(any(line.startswith("| ") for line in data_rows.splitlines())) + def test_mixed_input_renders_issue_less_row_but_not_seed(self) -> None: + entries = [ + { + "ts": "2026-05-11T00:00:00Z", + "class": "seed", + "surface": "agentic-pack", + "failure": "Ledger created", + "status": "open", + }, + { + "ts": "2026-07-25T00:00:00Z", + "class": "invalid_signal", + "surface": "ci/e2e-smoke", + "failure": "Archive-to-restore seed was transiently unavailable", + "workaround": "Rerun after seed creation", + "future_fix": "Stabilize the archive-to-restore seed", + "status": "open", + }, + ] + + rendered = render_markdown(entries) + + self.assertNotIn("| seed |", rendered) + self.assertIn("| invalid_signal | ci/e2e-smoke |", rendered) + + def test_checked_in_markdown_matches_jsonl_projection(self) -> None: + entries = load_entries(ledger.JSONL) + + self.assertEqual(ledger.MD.read_text(encoding="utf-8"), render_markdown(entries)) + def test_reconciled_targets_project_exact_latest_state(self) -> None: entries = [ { @@ -172,5 +209,78 @@ def test_reconciled_targets_project_exact_latest_state(self) -> None: ) +class FailureLedgerEntryPointTests(unittest.TestCase): + def assert_invalid_input_preserves_target( + self, + payload: str, + expected_line: int, + expected_error: str, + ) -> None: + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "failure_ledger.jsonl" + target = Path(directory) / "FAILURE_LEDGER.md" + source.write_text(payload, encoding="utf-8") + target.write_text("sentinel", encoding="utf-8") + stderr = io.StringIO() + + with ( + patch.object(ledger, "JSONL", source), + patch.object(ledger, "MD", target), + redirect_stderr(stderr), + ): + return_code = ledger.main() + + self.assertNotEqual(return_code, 0) + self.assertEqual(target.read_text(encoding="utf-8"), "sentinel") + self.assertIn(str(source), stderr.getvalue()) + self.assertIn(f"line {expected_line}", stderr.getvalue()) + self.assertIn(expected_error, stderr.getvalue()) + + def test_malformed_json_returns_nonzero_without_overwriting_markdown(self) -> None: + self.assert_invalid_input_preserves_target( + '{"status": "open"}\n{not-json}\n', + 2, + "invalid JSON", + ) + + def test_non_object_json_returns_nonzero_without_overwriting_markdown(self) -> None: + self.assert_invalid_input_preserves_target( + "[]\n", + 1, + "expected a JSON object, got list", + ) + + def test_missing_empty_and_seed_only_sources_remain_valid(self) -> None: + seed = { + "ts": "2026-05-11T00:00:00Z", + "class": "seed", + "surface": "agentic-pack", + "failure": "Ledger created", + "status": "open", + } + cases = { + "missing": None, + "empty": "\n", + "seed-only": json.dumps(seed) + "\n", + } + + for name, payload in cases.items(): + with self.subTest(name=name), tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "failure_ledger.jsonl" + target = Path(directory) / "FAILURE_LEDGER.md" + if payload is not None: + source.write_text(payload, encoding="utf-8") + + with ( + patch.object(ledger, "JSONL", source), + patch.object(ledger, "MD", target), + ): + return_code = ledger.main() + + self.assertEqual(return_code, 0) + self.assertTrue(target.exists()) + self.assertNotIn("| seed |", target.read_text(encoding="utf-8")) + + if __name__ == "__main__": unittest.main() From c0d90457b9eb09b42a31eb38c9260ef5e386453e Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 26 Jul 2026 07:10:14 +0100 Subject: [PATCH 4/6] Enforce failure ledger synchronization in docs CI Signed-off-by: Chris0Jeky --- .github/workflows/reusable-docs-governance.yml | 8 ++++++++ docs/TESTING_GUIDE.md | 1 + 2 files changed, 9 insertions(+) diff --git a/.github/workflows/reusable-docs-governance.yml b/.github/workflows/reusable-docs-governance.yml index d4bb366b5..69278f613 100644 --- a/.github/workflows/reusable-docs-governance.yml +++ b/.github/workflows/reusable-docs-governance.yml @@ -25,6 +25,14 @@ jobs: with: node-version: ${{ inputs.node-version }} + - name: Setup Python + uses: actions/setup-python@v7 + with: + python-version: "3.12" + + - name: Validate failure ledger projection synchronization + run: python -m unittest discover -s scripts/agent_hooks -p "test_render_failure_ledger.py" + - name: Validate docs governance invariants run: node scripts/check-docs-governance.mjs diff --git a/docs/TESTING_GUIDE.md b/docs/TESTING_GUIDE.md index 4a6c837cd..4693b90ce 100644 --- a/docs/TESTING_GUIDE.md +++ b/docs/TESTING_GUIDE.md @@ -71,6 +71,7 @@ python $env:USERPROFILE\.codex\skills\.system\skill-creator\scripts\quick_valida python $env:USERPROFILE\.codex\skills\.system\skill-creator\scripts\quick_validate.py .claude\skills\taskdeck-interface-map Get-ChildItem scripts\agent_hooks -Filter *.py | ForEach-Object { python -m py_compile $_.FullName; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } python scripts\agent_hooks\smoke_test.py +py -3 -B -m unittest discover -s scripts\agent_hooks -p "test_render_failure_ledger.py"; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } python scripts\agent_hooks\render_failure_ledger.py node scripts\check-docs-governance.mjs node scripts\check-golden-principles.mjs From 9842e945af62416262a52db846f4cf71122346ae Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 26 Jul 2026 07:23:51 +0100 Subject: [PATCH 5/6] Record failure ledger synchronization gate Signed-off-by: Chris0Jeky --- docs/IMPLEMENTATION_MASTERPLAN.md | 4 ++++ docs/STATUS.md | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/IMPLEMENTATION_MASTERPLAN.md b/docs/IMPLEMENTATION_MASTERPLAN.md index 8e2156ec6..7ff5469c3 100644 --- a/docs/IMPLEMENTATION_MASTERPLAN.md +++ b/docs/IMPLEMENTATION_MASTERPLAN.md @@ -10,6 +10,10 @@ Companion Active Docs: - `docs/MANUAL_TEST_CHECKLIST.md` - `docs/GOLDEN_PRINCIPLES.md` +## Delivery update (2026-07-26, agentic governance) + +- **Failure-ledger projection gate (`#1492`):** Required Docs Governance now pins Python 3.12 and runs the existing JSONL↔Markdown synchronization unittest before the governance checks, so a JSONL-only change with stale generated Markdown fails Required CI before regeneration can mask it. The agentic operating-layer smoke sequence records the same fail-before-render order. + ## Delivery update (2026-07-26, security runway) - **SQLite native security floor (`#1345`):** centrally pin `SQLitePCLRaw.bundle_e_sqlite3` 2.1.12 and make Infrastructure's dependency direct, which moves the matched bundle/core/provider/native family from 2.1.6 to 2.1.12 without enabling global transitive pinning. A runtime regression enforces SQLite >= 3.50.2 (2.1.12 loads 3.53.3); the NuGet vulnerability audit is clean, EF reports no pending model changes after a fresh update, and a self-contained `win-x64` binary reaches `/health/ready` with SQLite loaded. diff --git a/docs/STATUS.md b/docs/STATUS.md index 815e721bb..149f5b648 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,6 +2,9 @@ Last Updated: 2026-07-26 +Required Docs Governance hardening (2026-07-26, `#1492`): +- **Required CI now enforces failure-ledger projection synchronization.** The reusable Docs Governance job pins Python 3.12 and runs the existing `failure_ledger.jsonl` ↔ `FAILURE_LEDGER.md` synchronization unittest before its governance checks, so a stale checked-in projection fails before any renderer can rewrite it. The documented agentic operating-layer sequence mirrors the same fail-before-render order. + Paper-default regression alignment (2026-07-14, `#1274`): authenticated E2E sessions now follow the production Paper default unless a frozen Legacy-selector suite opts out explicitly. The refreshed core lane proves the real capture → Accept → Review → approve/apply → board path and runs the full axe WCAG 2.1 A/AA ruleset against settled Paper Home, Today, Inbox, Review, and a seeded Board, plus Login and functional skip-link keyboard coverage. The global Vitest Legacy pin remains intentional because those component tests assert the frozen Legacy DOM directly. Focused Chromium verification is 37/37 across `accessibility`, `onboarding`, `capture-loop`, `review-proposals`, `first-run`, and `smoke`. Direction change (2026-07-10, maintainer-decided, **ADR-0044**): **the 2026-06-13 archive pivot is superseded — Taskdeck is being revived** and shipped as a **free open beta** (adoption/feedback/exposure first; the commercial side is developed in parallel by the maintainer). Positioning: the local-first, review-first **action-item engine** — transcripts/notes in, evidence-linked proposals out, applied to a board only on human approval — with the write-gated MCP server as the developer-facing second act. Everything shipped under MIT stays MIT (no retroactive relicensing). The active planning spine is **`docs/REVIVAL_PLAN.md`** (Phase 1 "truth + safety before strangers" repurposes the #1278 archive exit criteria as the v0.1 ship gate; Phase 2 is the LLM transcript-triage engine — the largest authorized new-backend-surface slice; Phase 3 is the beta launch; other scoped exceptions include registration gating, feedback/telemetry, and MCP key/identity hardening as enumerated in §7; then a ~8-week traction checkpoint with the archive plan as the documented fallback). New backend surface is authorized only by REVIVAL_PLAN §7 — the Phase-2 transcript-engine grants (ADR-0044) plus the Phase-4 generalist-expansion grants (**ADR-0046, Accepted 2026-07-13**: artefact intake, dossier read model, mode-scoped navigation; tracker `#1327`; the twin generalist app is explicitly deferred behind the GEN-12 `#1326` evidence gate). Evidence base: `docs/analysis/2026-07-10_revival_assessment.md` (7 code-review dimensions, ~25 adversarially verified claims, live-verified market data). Archive-closeout issues #1269–#1278 are re-scoped in place (same work, new purpose); dogfooding (#1271) remains the acceptance test for either path. **Dogfooding is now instrumented (2026-07-25):** `docs/dogfooding/README.md` holds the protocol — a dedicated `TASKDECK_DOGFOOD_DB` so dev/demo/E2E traffic cannot contaminate the signal, `scripts/dogfooding/dogfood-snapshot.py` (read-only; counts and dates only) for objective metrics, `docs/dogfooding/LOG.md` for friction, and a checkpoint rubric fixed before any real usage data existed. Read it before collecting checkpoint evidence, or the evidence will not be comparable. The measured pre-dogfooding baseline is recorded in `LOG.md`: **8 active days, none in the last 28, last activity 2026-04-23, 10 of 13 boards demo/test residue** — sustained use has not started, though the loop itself works (**17 of 20 proposals reached Apply**, counted by `AppliedAt`; a status-based count reads 1/20 because `Dismiss()` overwrites an `Applied` status). From 161638cf80a4b7d6116cf08a7b5cd8c61e1ec2e4 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 26 Jul 2026 07:40:23 +0100 Subject: [PATCH 6/6] Harden failure ledger operational checks Signed-off-by: Chris0Jeky --- docs/STATUS.md | 2 +- docs/TESTING_GUIDE.md | 2 +- scripts/agent_hooks/render_failure_ledger.py | 6 +++-- .../agent_hooks/test_render_failure_ledger.py | 22 +++++++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/STATUS.md b/docs/STATUS.md index 149f5b648..dc5ef480d 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -22,7 +22,7 @@ PR-queue clearing wave (2026-07-25, **8 PRs merged**, and the open-PR queue is d - **`#1295` CLOSED as superseded, not merged.** The deny-floor overlay failed **13 of 13 of its own bypass tests** and targeted global floor v1.3.0 against an installed 1.6.5. A repo-local overlay is still needed — **11 rule-classes are `allow` at every tier including T4** — so the full salvage plan, with acceptance criteria and the branch's 16 review threads retained as the successor's regression corpus, is recorded on **`#1293`**. - **Repository hardening:** `main` previously permitted **force-pushes and deletions**; both are now blocked (deliberately relaxed otherwise — no required checks, no required approvals). The GitHub project priority sync was reporting a false all-clear because its default `-Limit 1000` silently truncated a 1444-item project; **288 items** were actually unsynced and are now clean (`#1474`). - **Backlog triage (`#1270`):** 12 already-decided issues closed with dated pivot notes, after an adversarial pass **bounced 13 of 25 proposed closures** — including `#1123`, the v0.1 ship-gate item. `#1270` itself stays open: two of its own acceptance criteria are obsolete against ADR-0044 and REVIVAL_PLAN §6 and must not be executed as written. -- **Seeded this wave:** `#1470`, `#1473`, `#1474`, `#1475` (the repo hook denies 6 of 8 *benign* commands), `#1476` (`scripts/agent_hooks/` has **zero** CI coverage), `#1480`, `#1482`, plus upstream `agent-harness#56` (`docker exec` is not unwrapped by the global floor at any tier). +- **Seeded this wave:** `#1470`, `#1473`, `#1474`, `#1475` (the repo hook denies 6 of 8 *benign* commands), `#1476` (the configured-handler `smoke_test.py` / deny-floor matrix remains ungated; failure-ledger renderer synchronization now runs in Required Docs Governance), `#1480`, `#1482`, plus upstream `agent-harness#56` (`docker exec` is not unwrapped by the global floor at any tier). - **Still open — human-gated:** `#1457` only. Its pinned dispatcher hash was stale (v1.5.2 vs installed 1.6.5) and has been refreshed; what remains is a fresh interactive Codex `/hooks` trust session plus recorded live allow/deny canaries, which no agent can perform. Overnight wave (2026-07-23/24, **6 PRs merged** + 3 dependabot PRs — the four feature/substrate PRs below plus `#1447`, the docs sync that recorded the maintainer-merged `#1414` (its content is the 2026-07-18 post-merge comment further down), and `#1448` below; per-PR gate: two independent adversarial review lenses, all-severity findings fixed with posted evidence, full backend suite on the exact head where code changed, required CI green, bot window honored with a content sweep). The wave's own delivery sweep was `#1454`; `#1448`, `#1427`, and dependabot `#1441` merged *after* that sweep ran, so their entries below were added on 2026-07-25: diff --git a/docs/TESTING_GUIDE.md b/docs/TESTING_GUIDE.md index 4693b90ce..e3191d03e 100644 --- a/docs/TESTING_GUIDE.md +++ b/docs/TESTING_GUIDE.md @@ -2,7 +2,7 @@ This is the active testing guide for Taskdeck. -Last Updated: 2026-07-14 +Last Updated: 2026-07-26 Companion Active Docs: - `docs/STATUS.md` - `docs/IMPLEMENTATION_MASTERPLAN.md` diff --git a/scripts/agent_hooks/render_failure_ledger.py b/scripts/agent_hooks/render_failure_ledger.py index d5b3ec53b..7c54c096d 100644 --- a/scripts/agent_hooks/render_failure_ledger.py +++ b/scripts/agent_hooks/render_failure_ledger.py @@ -91,11 +91,13 @@ def render_markdown(entries: list[dict[str, object]]) -> str: def load_entries(path: Path) -> list[dict[str, object]]: """Load and validate every nonblank JSONL line before rendering.""" - if not path.exists(): + try: + content = path.read_text(encoding="utf-8") + except FileNotFoundError: return [] entries: list[dict[str, object]] = [] - for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + for line_number, line in enumerate(content.splitlines(), start=1): if not line.strip(): continue diff --git a/scripts/agent_hooks/test_render_failure_ledger.py b/scripts/agent_hooks/test_render_failure_ledger.py index 0f099daf0..cc5a12725 100644 --- a/scripts/agent_hooks/test_render_failure_ledger.py +++ b/scripts/agent_hooks/test_render_failure_ledger.py @@ -250,6 +250,28 @@ def test_non_object_json_returns_nonzero_without_overwriting_markdown(self) -> N "expected a JSON object, got list", ) + def test_permission_denied_source_returns_nonzero_when_exists_reports_false(self) -> None: + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "failure_ledger.jsonl" + target = Path(directory) / "FAILURE_LEDGER.md" + target.write_text("sentinel", encoding="utf-8") + stderr = io.StringIO() + permission_error = PermissionError(13, "Permission denied", str(source)) + + with ( + patch.object(ledger, "JSONL", source), + patch.object(ledger, "MD", target), + patch.object(Path, "exists", return_value=False), + patch.object(Path, "read_text", side_effect=permission_error), + redirect_stderr(stderr), + ): + return_code = ledger.main() + + self.assertNotEqual(return_code, 0) + self.assertEqual(target.read_text(encoding="utf-8"), "sentinel") + self.assertIn(source.name, stderr.getvalue()) + self.assertIn("Permission denied", stderr.getvalue()) + def test_missing_empty_and_seed_only_sources_remain_valid(self) -> None: seed = { "ts": "2026-05-11T00:00:00Z",