diff --git a/.agents/pm/history/pm-3esl28.jsonl b/.agents/pm/history/pm-3esl28.jsonl index 50034d304..f406334d4 100644 --- a/.agents/pm/history/pm-3esl28.jsonl +++ b/.agents/pm/history/pm-3esl28.jsonl @@ -1,2 +1,17 @@ {"ts":"2026-07-22T19:01:44.269Z","author":"codex-sdk-boundary-20260722","op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-3esl28"},{"op":"add","path":"/metadata/title","value":"GH-645: standard-depth get/show JSON omits notes without notes_count"},{"op":"add","path":"/metadata/description","value":"At the default standard depth, pm get/show --json suppresses notes but exposes no notes_count or omission metadata, so agents can falsely conclude the item has no working context."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":["agent-ux","area:cli","gh-issue","json","token-efficiency"]},{"op":"add","path":"/metadata/created_at","value":"2026-07-22T19:01:44.269Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-22T19:01:44.269Z"},{"op":"add","path":"/metadata/author","value":"codex-sdk-boundary-20260722"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Default standard-depth get/show JSON reports notes_count whenever notes are omitted; full/deep projections remain backward compatible and include notes; CLI aliases, SDK result contracts, MCP projections, TOON/JSON docs, and contract fixtures remain coherent; regression tests cover zero, one, and multiple notes plus explicit depth modes"},{"op":"add","path":"/metadata/parent","value":"pm-mpbb"},{"op":"add","path":"/metadata/reporter","value":"GitHub GH-645"},{"op":"add","path":"/metadata/severity","value":"medium"},{"op":"add","path":"/metadata/expected_result","value":"Standard-depth structured output makes omitted note context explicit through notes_count and depth/projection metadata, or includes the notes consistently."},{"op":"add","path":"/metadata/actual_result","value":"Default standard-depth JSON omits notes and exposes neither notes_count nor a signal that note context was projected out."},{"op":"add","path":"/metadata/affected_version","value":"2026.7.22"},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-srns","kind":"related","created_at":"2026-07-22T19:01:44.269Z"}]},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-07-22T19:01:44.269Z","author":"codex-sdk-boundary-20260722","text":"Duplicate check 2026-07-22: all-status searches for GH-645,show/get JSON notes_count,standard depth omitted notes,and projection metadata found adjacent output-contract lineage pm-srns and completed JSON work pm-cfed/pm-wrss,but no item owns this exact missing-context signal. Created open and unclaimed; not part of the active SDK boundary PR."}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"b34aa2923b6e6b31504602302b1e3e558518d64848af4707306bd6adedacc835","message":""} {"ts":"2026-07-22T19:01:57.176Z","author":"codex-sdk-boundary-20260722","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-07-22T19:01:57.175Z","author":"codex-sdk-boundary-20260722","text":"GitHub linkage evidence: posted the canonical pm item link on GH-645 at https://github.com/unbraind/pm-cli/issues/645. Item remains open/unclaimed and outside the active SDK boundary implementation."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-22T19:01:57.176Z"}],"before_hash":"b34aa2923b6e6b31504602302b1e3e558518d64848af4707306bd6adedacc835","after_hash":"7f84a82adab836fa1f1f2b1f29a50582bfacc06c13e6c6213049ad493a8e8eae"} +{"ts":"2026-07-23T04:53:19.079Z","author":"claude-code-agent","op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:19.079Z"},{"op":"add","path":"/metadata/assignee","value":"claude-code-agent"}],"before_hash":"7f84a82adab836fa1f1f2b1f29a50582bfacc06c13e6c6213049ad493a8e8eae","after_hash":"8d3a0f9999805e18abb8aea364167f0a15ad84cd0fe6314fd8406fdfe2c060a0"} +{"ts":"2026-07-23T04:53:19.780Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:19.780Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"8d3a0f9999805e18abb8aea364167f0a15ad84cd0fe6314fd8406fdfe2c060a0","after_hash":"52d8b9349ccea4cf2a54571fdaaa947202309dd1dd83b7c1c1bd19a96e888a34","message":"Start broad SDK/CLI truthful-context lifecycle implementation"} +{"ts":"2026-07-23T04:53:26.524Z","author":"claude-code-agent","op":"update","patch":[{"op":"add","path":"/metadata/dependencies/1","value":{"id":"pm-ds3b","kind":"verifies","created_at":"2026-07-23T04:53:26.301Z"}},{"op":"add","path":"/metadata/dependencies/2","value":{"id":"pm-k9t17l","kind":"related","created_at":"2026-07-23T04:53:26.301Z"}},{"op":"add","path":"/metadata/dependencies/3","value":{"id":"pm-mmm9o5","kind":"related","created_at":"2026-07-23T04:53:26.301Z"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:26.524Z"}],"before_hash":"52d8b9349ccea4cf2a54571fdaaa947202309dd1dd83b7c1c1bd19a96e888a34","after_hash":"094d0ed5872bcfbb18e11400d14092f7e27a9352a0fc7104bab36c0b6efa8032"} +{"ts":"2026-07-23T05:04:30.597Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:30.597Z"},{"op":"add","path":"/metadata/files","value":[{"path":"src/cli/commands/get.ts","scope":"project","note":"Truthful token-bounded note projection"}]}],"before_hash":"094d0ed5872bcfbb18e11400d14092f7e27a9352a0fc7104bab36c0b6efa8032","after_hash":"f741ff44b8258a0428d2e38026112f9da0d3805156eb458270281e9fb14f6f9c"} +{"ts":"2026-07-23T05:04:31.697Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"add","path":"/metadata/files/1","value":{"path":"tests/unit/commands/get-append-command.spec.ts","scope":"project","note":"Depth and narrow-field acceptance coverage"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:31.697Z"}],"before_hash":"f741ff44b8258a0428d2e38026112f9da0d3805156eb458270281e9fb14f6f9c","after_hash":"b06d78f20e15ee18dbbdb3262c5983ca55aab125b3831f5e3b855d3356d1c1c4"} +{"ts":"2026-07-23T05:04:32.716Z","author":"claude-code-agent","op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:32.716Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/SDK.md","scope":"project","note":"GetResult notes_count contract"}]}],"before_hash":"b06d78f20e15ee18dbbdb3262c5983ca55aab125b3831f5e3b855d3356d1c1c4","after_hash":"e79b8c960001e8da121cbc4b97ef19707b7388ddd20d6a38d0a30bc225a6c7b6"} +{"ts":"2026-07-23T05:04:33.663Z","author":"claude-code-agent","op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:33.663Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node scripts/run-tests.mjs test -- tests/unit/commands/get-append-command.spec.ts","scope":"project","timeout_seconds":240,"note":"Get projection acceptance"}]}],"before_hash":"e79b8c960001e8da121cbc4b97ef19707b7388ddd20d6a38d0a30bc225a6c7b6","after_hash":"43eff10b8be55b3140d5ecc97796c611009fcc70b8238db01f13dc99fc1786d0"} +{"ts":"2026-07-23T05:04:48.454Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-07-23T05:04:48.453Z","author":"claude-code-agent","text":"TDD evidence: added failing standard/brief/deep/full and narrow notes_count assertions, then implemented the compact projection contract. Focused acceptance is green."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:48.454Z"}],"before_hash":"43eff10b8be55b3140d5ecc97796c611009fcc70b8238db01f13dc99fc1786d0","after_hash":"c1d46d299f75ca39112bbafb3c181f6f98190366bbcd0ba4e0bfa93ceec2fa2b"} +{"ts":"2026-07-23T05:31:49.891Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-07-23T05:31:49.890Z","author":"claude-code-agent","text":"Packed installed-consumer acceptance passed from an isolated npm install: standard get returned notes_count=1 without note bodies; full get returned one note without redundant notes_count. Evidence workspace: /tmp/tmp.9DpM65yUme (local-only, not committed)."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:31:49.891Z"}],"before_hash":"c1d46d299f75ca39112bbafb3c181f6f98190366bbcd0ba4e0bfa93ceec2fa2b","after_hash":"98d8f61ad1e753166d51dd2fc00d9ec73fe396bf861903c6b142e2f2fa588ead"} +{"ts":"2026-07-23T05:32:05.020Z","author":"claude-code-agent","op":"test_run_track","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:32:05.020Z"},{"op":"add","path":"/metadata/test_runs","value":[{"run_id":"test-local-mrx2qrkl-ksdhg7","kind":"test","status":"passed","started_at":"2026-07-23T05:31:51.916Z","finished_at":"2026-07-23T05:32:05.013Z","recorded_at":"2026-07-23T05:32:05.013Z","passed":1,"failed":0,"skipped":0}]}],"before_hash":"98d8f61ad1e753166d51dd2fc00d9ec73fe396bf861903c6b142e2f2fa588ead","after_hash":"89ef3f580eae5b1f57c3ad8e3b0cd183dcccdeee5db94f667828ef6c6379c36b","message":"Track test run summary (test-local-mrx2qrkl-ksdhg7)"} +{"ts":"2026-07-23T05:33:56.176Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/actual_result","value":"CLI, PmClient, aliases, field projection, SDK docs, focused tests, full coverage, and packed installed-consumer acceptance all confirm the truthful count contract."},{"op":"replace","path":"/metadata/expected_result","value":"Agents can distinguish zero notes from omitted note context without requesting a full item payload."},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:33:56.176Z"},{"op":"add","path":"/metadata/resolution","value":"Standard and brief get projections now expose item.notes_count whenever note bodies are omitted; deep/full reads retain note bodies without a redundant count, and narrow fields accept notes_count."}],"before_hash":"89ef3f580eae5b1f57c3ad8e3b0cd183dcccdeee5db94f667828ef6c6379c36b","after_hash":"026b6768b86852d1cf3d0f8ba915cf9c937a5fe49b423553acba3a90a29f67fe","message":"Record verified issue resolution"} +{"ts":"2026-07-23T05:33:58.201Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-07-23T05:33:58.199Z","author":"claude-code-agent","text":"Final local evidence: 6,101 tests pass at exact 100% statements/branches/functions/lines; typecheck, ESLint, jscpd zero-clone, 100% docstrings, package boundary, token budget, docs, links, secrets, version, packed smoke, and 88-command dogfood gates pass."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:33:58.201Z"}],"before_hash":"026b6768b86852d1cf3d0f8ba915cf9c937a5fe49b423553acba3a90a29f67fe","after_hash":"411a0ad317b78901451befa16e379a8e4521e8d346f76055a8353c860ecf8bb9"} +{"ts":"2026-07-23T05:34:00.489Z","author":"claude-code-agent","op":"close","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:00.489Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-07-23T05:34:00.488Z"},{"op":"add","path":"/metadata/close_reason","value":"Implemented and verified truthful omitted-note counts across CLI/SDK projections, docs, full coverage, and packed consumer acceptance."}],"before_hash":"411a0ad317b78901451befa16e379a8e4521e8d346f76055a8353c860ecf8bb9","after_hash":"a71c9a5615c8117df0ba3df4d72d1f287fd3aae6b141847beea7320f70ca78ed"} +{"ts":"2026-07-23T05:34:01.240Z","author":"claude-code-agent","op":"release","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:01.240Z"}],"before_hash":"a71c9a5615c8117df0ba3df4d72d1f287fd3aae6b141847beea7320f70ca78ed","after_hash":"34860f6ee04bd331af8961d0a4a7af5c753c56938ebf79acef790c62c26c9968"} +{"ts":"2026-07-23T05:38:23.338Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-07-23T05:38:23.338Z","author":"claude-code-agent","text":"PR evidence: truthful SDK/CLI context bundle is published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:38:23.338Z"}],"before_hash":"34860f6ee04bd331af8961d0a4a7af5c753c56938ebf79acef790c62c26c9968","after_hash":"188b24b4e6ca5af846684cdd2a23f25524a8e64c2ba068c1e7a9bdba04a50fed"} diff --git a/.agents/pm/history/pm-7zs0.jsonl b/.agents/pm/history/pm-7zs0.jsonl index bd197818a..b084eaff7 100644 --- a/.agents/pm/history/pm-7zs0.jsonl +++ b/.agents/pm/history/pm-7zs0.jsonl @@ -167,3 +167,4 @@ {"ts":"2026-07-18T12:37:22.398Z","author":"codex-gpt5","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-18T12:37:22.398Z"},{"op":"replace","path":"/metadata/status","value":"open"}],"before_hash":"1debdc6ec704c9b6440de7cc4fb7efd06b17d757e4b08605fbf7f872cb13c38e","after_hash":"cd65b5afbb161577031e9b2e3a3e9b570209ea4f86ed74a1bae0121a74c29754","message":"Recurring verification recorded; return reminder to open"} {"ts":"2026-07-18T12:37:23.117Z","author":"codex-gpt5","op":"release","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-18T12:37:23.117Z"}],"before_hash":"cd65b5afbb161577031e9b2e3a3e9b570209ea4f86ed74a1bae0121a74c29754","after_hash":"bdf0dd4aa88417c9d3aeafaabd5a1d2b17815f1c538e8960b659436e9b125f11"} {"ts":"2026-07-21T16:07:17.087Z","author":"codex-tracker-governance","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/82","value":{"created_at":"2026-07-21T16:07:17.086Z","author":"codex-tracker-governance","text":"Release-health relationship update: exact main head 669055d9757850cdb54da198ab18df75312192d4 has one compatibility-gate regression after the published compact create response became the previous-version fixture input. Focused owner pm-pkdpyz tracks the harness fix and exact-head proof; the compact product response remains intentional and shipped."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T16:07:17.087Z"}],"before_hash":"bdf0dd4aa88417c9d3aeafaabd5a1d2b17815f1c538e8960b659436e9b125f11","after_hash":"e82698bdc92475a1d0cb8cc4e4b41e9ecb0712ac155f97f9c5062ee1e86ed10d"} +{"ts":"2026-07-23T05:48:21.916Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/83","value":{"created_at":"2026-07-23T05:48:21.914Z","author":"claude-code-agent","text":"Daily release verification 2026-07-23: scheduled run 29982743761 executed the production path and failed closed only on unresolved Sentry PM-CLI-2J, opening GH-653. PR #652 contains the verified fix; after merge, resolve PM-CLI-2J in the released version and close GH-653 to exercise the guarded same-day auto-release retry. No duplicate release item created."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:48:21.916Z"}],"before_hash":"e82698bdc92475a1d0cb8cc4e4b41e9ecb0712ac155f97f9c5062ee1e86ed10d","after_hash":"599d5e6f5b39d2bc6cd7bd2d08c2f998972c527082ad38fd8d7d2a1588f3ecfa"} diff --git a/.agents/pm/history/pm-ds3b.jsonl b/.agents/pm/history/pm-ds3b.jsonl index ce7e82c50..a8fb60444 100644 --- a/.agents/pm/history/pm-ds3b.jsonl +++ b/.agents/pm/history/pm-ds3b.jsonl @@ -91,3 +91,12 @@ {"ts":"2026-07-22T21:36:25.846Z","author":"codex-graph-platform-20260722","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-22T21:36:25.846Z"},{"op":"replace","path":"/metadata/status","value":"open"}],"before_hash":"4f1cd57a8809127fe59970b1e1127b0b19c3c28d591e96b89ba0c6ad379eeb50","after_hash":"f25e817e2a45c0bf0cef9c1f7615b70ee740e84d0d9320a4e379e10ba451eb98","message":"This graph adapter documentation tranche is complete and verified; return the rolling SDK reference to open for future promoted primitives."} {"ts":"2026-07-22T21:36:26.553Z","author":"codex-graph-platform-20260722","op":"release","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-22T21:36:26.553Z"}],"before_hash":"f25e817e2a45c0bf0cef9c1f7615b70ee740e84d0d9320a4e379e10ba451eb98","after_hash":"1de7803f9e0eb24c91e00a8b297bf655aa8dad0b6308adebccf042b1cac4b19d"} {"ts":"2026-07-22T21:38:24.990Z","author":"codex-graph-platform-20260722","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/22","value":{"created_at":"2026-07-22T21:38:24.989Z","author":"codex-graph-platform-20260722","text":"PR evidence: graph adapter and package-author reference updates are under hosted review in https://github.com/unbraind/pm-cli/pull/648 at commit c98ca0d8."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-22T21:38:24.990Z"}],"before_hash":"1de7803f9e0eb24c91e00a8b297bf655aa8dad0b6308adebccf042b1cac4b19d","after_hash":"3bdfb8915d6828c279a4269b1d036f8915224d7ed935ae6f4378f69f65b76610"} +{"ts":"2026-07-23T04:53:24.655Z","author":"claude-code-agent","op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:24.655Z"},{"op":"add","path":"/metadata/assignee","value":"claude-code-agent"}],"before_hash":"3bdfb8915d6828c279a4269b1d036f8915224d7ed935ae6f4378f69f65b76610","after_hash":"ef512ef7c4937a17405b77ac1e9cfcafeaeced6da79a15251e2aa45843014941"} +{"ts":"2026-07-23T04:53:25.476Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:25.476Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"ef512ef7c4937a17405b77ac1e9cfcafeaeced6da79a15251e2aa45843014941","after_hash":"74c2410ed6aa69af3d0336193faa709c07101c8daf70a4b55f28ff3269c44527","message":"Start broad SDK/CLI truthful-context lifecycle implementation"} +{"ts":"2026-07-23T05:04:47.700Z","author":"claude-code-agent","op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:47.700Z"}],"before_hash":"74c2410ed6aa69af3d0336193faa709c07101c8daf70a4b55f28ff3269c44527","after_hash":"907694d1cd9e148a5c9a54b19c70e00aedac7fdf7574667c91fd4e92a95bc52d"} +{"ts":"2026-07-23T05:04:50.917Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/23","value":{"created_at":"2026-07-23T05:04:50.917Z","author":"claude-code-agent","text":"SDK reference updated for GetResult.notes_count, assertRelationshipEdgeAllowed, and loss-aware HistoryRepairReconciliationReport semantics. Full gates and packed-consumer acceptance remain in progress."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:50.917Z"}],"before_hash":"907694d1cd9e148a5c9a54b19c70e00aedac7fdf7574667c91fd4e92a95bc52d","after_hash":"67d3774fcff86e923c3712913fd0944f14406581920e2e9355d7ef43eafab779"} +{"ts":"2026-07-23T05:30:04.054Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/24","value":{"created_at":"2026-07-23T05:30:04.053Z","author":"claude-code-agent","text":"Release-gate checkpoint: SDK docs, links, typecheck, static quality, token budgets, package boundary, packed smoke, dogfood, and exact 100/100/100/100 coverage pass. Live Sentry gate remains intentionally red on the pre-fix 2026.7.22 self-edge issue tracked by pm-k9t17l."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:30:04.054Z"}],"before_hash":"67d3774fcff86e923c3712913fd0944f14406581920e2e9355d7ef43eafab779","after_hash":"7b8b8753707e8614c46f01e6646caf3233cca432da7501fb393f3c96ad141e4b"} +{"ts":"2026-07-23T05:34:05.160Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:05.160Z"},{"op":"replace","path":"/metadata/status","value":"open"}],"before_hash":"7b8b8753707e8614c46f01e6646caf3233cca432da7501fb393f3c96ad141e4b","after_hash":"3ea731e0237f0f0cafa80cbe11d53880db09d4962ad6db74e2386220802cd54e","message":"Truthful-context SDK reference tranche complete; retain rolling item for future promoted primitives"} +{"ts":"2026-07-23T05:34:05.865Z","author":"claude-code-agent","op":"release","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:05.865Z"}],"before_hash":"3ea731e0237f0f0cafa80cbe11d53880db09d4962ad6db74e2386220802cd54e","after_hash":"8e41c918a8892a6ffa0df6421d23545ae669cf7989fd009fc9a7edd1d934afee"} +{"ts":"2026-07-23T05:38:25.746Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/25","value":{"created_at":"2026-07-23T05:38:25.746Z","author":"claude-code-agent","text":"PR evidence: this SDK reference tranche is published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:38:25.746Z"}],"before_hash":"8e41c918a8892a6ffa0df6421d23545ae669cf7989fd009fc9a7edd1d934afee","after_hash":"25f2fd63f947a1a46674334bd8a0731fa403ce533dab2aa678c19c265857856d"} +{"ts":"2026-07-23T05:48:22.533Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/26","value":{"created_at":"2026-07-23T05:48:22.532Z","author":"claude-code-agent","text":"Exact-head hosted review evidence: PR #652 head 8f6ba731 has 29 successful / 0 failing checks, full hosted coverage, no inline review threads, and only external reviewer quota-limit notices. Scheduled Auto Release reached the mandatory Sentry gate and correctly blocked on the pre-fix self-edge issue tracked by pm-k9t17l."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:48:22.533Z"}],"before_hash":"25f2fd63f947a1a46674334bd8a0731fa403ce533dab2aa678c19c265857856d","after_hash":"ca3d9ca44d4c5de91476b5bbe49902f3a83558ca96486ae938223953f100f6f5"} diff --git a/.agents/pm/history/pm-h90s.jsonl b/.agents/pm/history/pm-h90s.jsonl index 98dc3d11e..310123fa5 100644 --- a/.agents/pm/history/pm-h90s.jsonl +++ b/.agents/pm/history/pm-h90s.jsonl @@ -1,3 +1,4 @@ {"ts":"2026-07-17T04:42:36.823Z","author":"maintainer-agent","op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-h90s"},{"op":"add","path":"/metadata/title","value":"Author attribution enforcement: opt-in strict unknown-author rejection, SDK test-all author parity, and audited disposition for stranded actionable events"},{"op":"add","path":"/metadata/description","value":"Production evidence 2026-07-16: the PR#575 lane (codex-wide-sdk-20260716) authored 211 history events correctly but its 'pm test --run' invocations wrote 5 test_run_track events with author 'unknown' (pm-qfdd/pm-cbwg/pm-w89s/pm-9x6e x2, 21:05-21:13Z). Because pm-demq's legacy/actionable split anchors on a fixed baseline timestamp, these post-baseline events are permanently 'actionable' — pm health is now ok:false with history_unknown_author_events:5 and NO remediation path (history is immutable by design; never rewrite). Three gaps, one item: (1) PREVENTION — pm-cpja deliberately shipped attribution as advisory; add opt-in strict enforcement (settings key, e.g. require_attributed_author) that fail-fasts any mutation whose author resolves to 'unknown', so lanes cannot leak unattributed events into permanent history. (2) SDK PARITY — buildTestAllItemRunContext (src/sdk/test/batch.ts:631) hardcodes resolveAuthor(undefined, author_default): TestAllCommandOptions carries no author field, so programmatic SDK runTestAll callers cannot attribute tracking events except via process-global PM_AUTHOR (execution.ts single-run path DOES accept options.author — asymmetric). (3) DISPOSITION — an audited acknowledgment mechanism for stranded actionable unknown-author events (attribute-by-annotation recorded as a NEW history event with evidence, e.g. 'events X..Y attributed to codex-wide-sdk-20260716 by maintainer review', consumed by the health scanner), so health can return to ok truthfully without moving the baseline or editing history."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["agent-ux","area:governance","history-integrity"]},{"op":"add","path":"/metadata/created_at","value":"2026-07-17T04:42:36.823Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-17T04:42:36.823Z"},{"op":"add","path":"/metadata/author","value":"maintainer-agent"},{"op":"add","path":"/metadata/estimated_minutes","value":360},{"op":"add","path":"/metadata/acceptance_criteria","value":"strict mode (opt-in settings key) rejects mutations resolving to author 'unknown' with a typed usage error naming --author/PM_AUTHOR; TestAllCommandOptions accepts author and threads it to appendTrackedTestRunSummary (parity with single-run path); an audited acknowledgment flow dispositions the five 2026-07-16 events and pm health returns ok:true without baseline moves or history rewrites; validate/health docs cover all three; regression specs for each"},{"op":"add","path":"/metadata/parent","value":"pm-33cw"},{"op":"add","path":"/metadata/risk","value":"medium"},{"op":"add","path":"/metadata/confidence","value":"high"},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-3z0k","kind":"related","created_at":"2026-07-17T04:42:36.823Z"},{"id":"pm-cpja","kind":"related","created_at":"2026-07-17T04:42:36.823Z"},{"id":"pm-demq","kind":"related","created_at":"2026-07-17T04:42:36.823Z"},{"id":"pm-u14c","kind":"related","created_at":"2026-07-17T04:42:36.823Z"},{"id":"pm-wenq","kind":"related","created_at":"2026-07-17T04:42:36.823Z"}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"fe8756475e7ca65150759c40d1dfc92df53798dd093bf667bbee76fe5f0727db","message":""} {"ts":"2026-07-19T16:15:16.115Z","author":"codex-wide-sdk-20260719","op":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-19T16:15:16.115Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-07-19T16:15:16.114Z","author":"codex-wide-sdk-20260719","text":"2026-07-19 verification: pm health still reports exactly history_unknown_author_events:5 and validate reports no history drift, matching this canonical open issue's documented immutable-event scope. No duplicate item created and no history files were rewritten."}]}],"before_hash":"fe8756475e7ca65150759c40d1dfc92df53798dd093bf667bbee76fe5f0727db","after_hash":"951a349c768620dcf174c627b3f6cb10ef8b7f75a18d554e192f70d40b187d2c"} {"ts":"2026-07-21T07:57:07.125Z","author":"codex-root-20260721","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-07-21T07:57:07.125Z","author":"codex-root-20260721","text":"2026-07-21 shipping-lane verification: pm health still reports exactly 5 actionable unknown-author events, with 0 history drift, 0 active/stale locks, and storage integrity otherwise clean. This remains the canonical prevention/disposition item; no duplicate was created and immutable history was not rewritten."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T07:57:07.125Z"}],"before_hash":"951a349c768620dcf174c627b3f6cb10ef8b7f75a18d554e192f70d40b187d2c","after_hash":"0b47bac3ff2c0a51ed3fd8e1ab56e7bef92de9b742ba913f013c86c0ed8f3948"} +{"ts":"2026-07-23T05:33:55.569Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-07-23T05:33:55.569Z","author":"claude-code-agent","text":"Fresh health evidence 2026-07-23: pm health --check-only reports storage warn with 167 unknown-author events, partitioned into 162 legacy and 5 actionable across 1,925 streams / 32,373 checked events. This is the existing canonical item; no duplicate was created and it was not claimed by the truthful-context PR."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:33:55.569Z"}],"before_hash":"0b47bac3ff2c0a51ed3fd8e1ab56e7bef92de9b742ba913f013c86c0ed8f3948","after_hash":"1b326e93072094297a961019837f5dd03245bff0f8786964cc4b2883076eb00c"} diff --git a/.agents/pm/history/pm-k9t17l.jsonl b/.agents/pm/history/pm-k9t17l.jsonl new file mode 100644 index 000000000..72e24abba --- /dev/null +++ b/.agents/pm/history/pm-k9t17l.jsonl @@ -0,0 +1,23 @@ +{"ts":"2026-07-23T04:52:33.471Z","author":"claude-code-agent","op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-k9t17l"},{"op":"add","path":"/metadata/title","value":"GH-649: reject self-referential ordering relationships at the SDK mutation boundary"},{"op":"add","path":"/metadata/description","value":"A create/update dependency mutation can persist a self blocked_by or inverse ordering edge that graph governance forbids, permanently removing the item from readiness while pm deps fails to identify the one-node cycle."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["agent-ux","area:graph","area:sdk","data-integrity","gh-issue","multi-agent"]},{"op":"add","path":"/metadata/created_at","value":"2026-07-23T04:52:33.471Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-23T04:52:33.471Z"},{"op":"add","path":"/metadata/author","value":"claude-code-agent"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Every shared dependency mutation path rejects self edges when the resolved relationship kind disallows self links, inverse and alias spellings share the rule, custom allowSelf kinds remain writable, reads render legacy self cycles truthfully, no item or history write occurs on rejection, public SDK contracts and docs explain the invariant, focused and full coverage remain 100 percent"},{"op":"add","path":"/metadata/parent","value":"pm-6x7o"},{"op":"add","path":"/metadata/reporter","value":"GitHub GH-649"},{"op":"add","path":"/metadata/severity","value":"high"},{"op":"add","path":"/metadata/expected_result","value":"SDK, CLI, MCP, structured batch, and extension-hosted mutation paths reject non-allowSelf self relationships before persistence with a deterministic recovery error, while custom allowSelf kinds remain supported."},{"op":"add","path":"/metadata/actual_result","value":"pm update accepts and persists id=,kind=blocked_by, emits only a cycle warning, graph audit rejects the stored edge, pm deps reports cycle false, and pm next permanently excludes the item."},{"op":"add","path":"/metadata/affected_version","value":"2026.7.22"},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-4jqm","kind":"related","created_at":"2026-07-23T04:52:33.471Z"},{"id":"pm-6irg","kind":"related","created_at":"2026-07-23T04:52:33.471Z"},{"id":"pm-i6pi","kind":"related","created_at":"2026-07-23T04:52:33.471Z"},{"id":"pm-ju83","kind":"related","created_at":"2026-07-23T04:52:33.471Z"}]},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-07-23T04:52:33.471Z","author":"claude-code-agent","text":"Duplicate check 2026-07-23: all-status searches for GH-649,self-referential ordering edge,blocked_by self edge,and dependency cycle write guard found shipped cycle advisory pm-i6pi,governance lineage pm-6x7o/pm-ju83/pm-4jqm,and closed detection work pm-6irg,but no item owns the exact write-time self-edge inconsistency. Created this canonical leaf rather than reopening shipped work."}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"d0412fb047cf89c9439204509ee3d23d4eb166b8d8ecd69dff60e3c6ed1eac4a","message":""} +{"ts":"2026-07-23T04:53:20.610Z","author":"claude-code-agent","op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:20.610Z"},{"op":"add","path":"/metadata/assignee","value":"claude-code-agent"}],"before_hash":"d0412fb047cf89c9439204509ee3d23d4eb166b8d8ecd69dff60e3c6ed1eac4a","after_hash":"53fdc5ef0dcf952baa35971275f818caa6b0d248de12eed679045ec922672d97"} +{"ts":"2026-07-23T04:53:21.633Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:21.633Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"53fdc5ef0dcf952baa35971275f818caa6b0d248de12eed679045ec922672d97","after_hash":"d2c2357f663bb359926cd1aae03c55af5c21540532235684c342a85ed06dcafb","message":"Start broad SDK/CLI truthful-context lifecycle implementation"} +{"ts":"2026-07-23T04:53:27.550Z","author":"claude-code-agent","op":"update","patch":[{"op":"add","path":"/metadata/dependencies/4","value":{"id":"pm-3esl28","kind":"related","created_at":"2026-07-23T04:53:27.272Z"}},{"op":"add","path":"/metadata/dependencies/5","value":{"id":"pm-ds3b","kind":"verifies","created_at":"2026-07-23T04:53:27.272Z"}},{"op":"add","path":"/metadata/dependencies/6","value":{"id":"pm-mmm9o5","kind":"related","created_at":"2026-07-23T04:53:27.272Z"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:27.550Z"}],"before_hash":"d2c2357f663bb359926cd1aae03c55af5c21540532235684c342a85ed06dcafb","after_hash":"bc6679b802da53f51f58eb93df5b62f3c887390a56b0cc4c3dfafff21af44d06"} +{"ts":"2026-07-23T04:53:29.273Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-07-23T04:53:29.272Z","author":"claude-code-agent","text":"GitHub linkage evidence: canonical pm item posted on GH-649 at https://github.com/unbraind/pm-cli/issues/649#issuecomment-5054564177. Active bundle shares the truthful-context lifecycle acceptance surface with pm-3esl28, pm-mmm9o5, and rolling SDK reference pm-ds3b."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:29.273Z"}],"before_hash":"bc6679b802da53f51f58eb93df5b62f3c887390a56b0cc4c3dfafff21af44d06","after_hash":"26c3b2b045ba29972283d7b73f498194a78f120b78aa8d5fcf47275d5c58d24d"} +{"ts":"2026-07-23T05:04:34.843Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:34.843Z"},{"op":"add","path":"/metadata/files","value":[{"path":"src/sdk/relationships.ts","scope":"project","note":"Reusable relationship edge policy primitive"}]}],"before_hash":"26c3b2b045ba29972283d7b73f498194a78f120b78aa8d5fcf47275d5c58d24d","after_hash":"5e472ee98cf029e10200d3fa55146ad6cc5e15773e48ae5fb9ae81da2df1591b"} +{"ts":"2026-07-23T05:04:36.223Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"replace","path":"/metadata/files/0/note","value":"Shared dependency mutation validation"},{"op":"replace","path":"/metadata/files/0/path","value":"src/sdk/dependency-flag-validation.ts"},{"op":"add","path":"/metadata/files/1","value":{"path":"src/sdk/relationships.ts","scope":"project","note":"Reusable relationship edge policy primitive"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:36.223Z"}],"before_hash":"5e472ee98cf029e10200d3fa55146ad6cc5e15773e48ae5fb9ae81da2df1591b","after_hash":"caaeeb28f1a0d2374aad9f4f73784813c177109e8918d5924d8fa5e85712f08d"} +{"ts":"2026-07-23T05:04:37.427Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"replace","path":"/metadata/files/1/note","value":"Shared dependency mutation validation"},{"op":"replace","path":"/metadata/files/1/path","value":"src/sdk/dependency-flag-validation.ts"},{"op":"replace","path":"/metadata/files/0/note","value":"Pre-write create enforcement"},{"op":"replace","path":"/metadata/files/0/path","value":"src/cli/commands/create.ts"},{"op":"add","path":"/metadata/files/2","value":{"path":"src/sdk/relationships.ts","scope":"project","note":"Reusable relationship edge policy primitive"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:37.427Z"}],"before_hash":"caaeeb28f1a0d2374aad9f4f73784813c177109e8918d5924d8fa5e85712f08d","after_hash":"bca909ed4921a5f62a821a6670bad04f08022e592afa6f9c7d55e556e9d681b3"} +{"ts":"2026-07-23T05:04:38.567Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"replace","path":"/metadata/files/2/note","value":"Shared dependency mutation validation"},{"op":"replace","path":"/metadata/files/2/path","value":"src/sdk/dependency-flag-validation.ts"},{"op":"replace","path":"/metadata/files/1/note","value":"Pre-write update enforcement"},{"op":"replace","path":"/metadata/files/1/path","value":"src/cli/commands/update.ts"},{"op":"add","path":"/metadata/files/3","value":{"path":"src/sdk/relationships.ts","scope":"project","note":"Reusable relationship edge policy primitive"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:38.567Z"}],"before_hash":"bca909ed4921a5f62a821a6670bad04f08022e592afa6f9c7d55e556e9d681b3","after_hash":"cf41526ca7b1b29284d14791c9633c1de51f26c514e550b38dd16a7027d1a707"} +{"ts":"2026-07-23T05:04:39.643Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"add","path":"/metadata/files/4","value":{"path":"tests/unit/sdk/relationships.spec.ts","scope":"project","note":"Registry alias and allowSelf coverage"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:39.643Z"}],"before_hash":"cf41526ca7b1b29284d14791c9633c1de51f26c514e550b38dd16a7027d1a707","after_hash":"bbe30418a4a5241994cd3cc34db984343082e1a6da489e7538460559ee54bff3"} +{"ts":"2026-07-23T05:04:40.553Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"replace","path":"/metadata/files/4/note","value":"CLI zero-write acceptance"},{"op":"replace","path":"/metadata/files/4/path","value":"tests/unit/commands/agent-ux-feedback-wave.spec.ts"},{"op":"add","path":"/metadata/files/5","value":{"path":"tests/unit/sdk/relationships.spec.ts","scope":"project","note":"Registry alias and allowSelf coverage"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:40.553Z"}],"before_hash":"bbe30418a4a5241994cd3cc34db984343082e1a6da489e7538460559ee54bff3","after_hash":"e01c9957c3f80cad2374152d84699bfcd11cf48f2e1858b4d8230000bc97c31c"} +{"ts":"2026-07-23T05:04:41.431Z","author":"claude-code-agent","op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:41.431Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/SDK.md","scope":"project","note":"Package-author mutation contract"}]}],"before_hash":"e01c9957c3f80cad2374152d84699bfcd11cf48f2e1858b4d8230000bc97c31c","after_hash":"1a20e2b977ea5fc19cea540074b2341d687bc10d6da635b7344a440c8635c5e3"} +{"ts":"2026-07-23T05:04:42.396Z","author":"claude-code-agent","op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:42.396Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node scripts/run-tests.mjs test -- tests/unit/sdk/relationships.spec.ts tests/unit/commands/agent-ux-feedback-wave.spec.ts","scope":"project","timeout_seconds":240,"note":"SDK and CLI relationship integrity acceptance"}]}],"before_hash":"1a20e2b977ea5fc19cea540074b2341d687bc10d6da635b7344a440c8635c5e3","after_hash":"ca24b2f0e34c425de3d8200de57bbaeed0fdc055d3430bd9ce0dbfb1a8375cf2"} +{"ts":"2026-07-23T05:04:49.295Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-07-23T05:04:49.295Z","author":"claude-code-agent","text":"TDD evidence: added failing SDK alias/allowSelf policy tests plus CLI create/update zero-write acceptance, then implemented one shared pre-persistence edge validator used by both mutation paths. Focused acceptance is green."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:49.295Z"}],"before_hash":"ca24b2f0e34c425de3d8200de57bbaeed0fdc055d3430bd9ce0dbfb1a8375cf2","after_hash":"26bd58e411f749dc144dce98e1424a11c8f67bf6b341ed30dfb8fb107263d738"} +{"ts":"2026-07-23T05:30:02.577Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-07-23T05:30:02.577Z","author":"claude-code-agent","text":"Live observability evidence: release gate identified Sentry PM-CLI-2J as the sole blocker. Sanitized issue facts: unresolved/high, handled TypeError from normalizeRelationshipEdge, 3 events, first/last seen 2026-07-22, all on release pm-cli@2026.7.22, no events after 2026-07-22T21:40:25Z. This implementation converts create/update self-edge attempts into deterministic usage errors before persistence; keep the Sentry issue unresolved until the merged release is verified, then resolve and rerun the gate."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:30:02.577Z"}],"before_hash":"26bd58e411f749dc144dce98e1424a11c8f67bf6b341ed30dfb8fb107263d738","after_hash":"11a8a9ec6f163157405da514c0acce08fc4039b5919e6a2c5293661f77d19e4d"} +{"ts":"2026-07-23T05:31:50.579Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-07-23T05:31:50.579Z","author":"claude-code-agent","text":"Packed installed-consumer acceptance passed: public SDK custom allowSelf kind succeeded; CLI self dependency via depends-on exited usage=2 and created neither item nor history state. Real two-branch Git merge also retained both agents' notes. Evidence workspace: /tmp/tmp.9DpM65yUme."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:31:50.579Z"}],"before_hash":"11a8a9ec6f163157405da514c0acce08fc4039b5919e6a2c5293661f77d19e4d","after_hash":"a0a5a702d18b2386db09ca6d41f75e64f96f2da09a2fafdaeee20ec0fedf438d"} +{"ts":"2026-07-23T05:32:21.303Z","author":"claude-code-agent","op":"test_run_track","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:32:21.303Z"},{"op":"add","path":"/metadata/test_runs","value":[{"run_id":"test-local-mrx2r44v-r2wr3n","kind":"test","status":"passed","started_at":"2026-07-23T05:32:05.763Z","finished_at":"2026-07-23T05:32:21.296Z","recorded_at":"2026-07-23T05:32:21.296Z","passed":1,"failed":0,"skipped":0}]}],"before_hash":"a0a5a702d18b2386db09ca6d41f75e64f96f2da09a2fafdaeee20ec0fedf438d","after_hash":"29d8813b4da5f31faeaf57f3ccc11942940235655708657d2410205b71ae51d2","message":"Track test run summary (test-local-mrx2r44v-r2wr3n)"} +{"ts":"2026-07-23T05:33:56.842Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/actual_result","value":"CLI zero-write acceptance, public packed SDK import, real installed CLI usage=2 behavior, exact coverage, and full gates confirm the mutation boundary."},{"op":"replace","path":"/metadata/expected_result","value":"Disallowed self dependencies fail before item/history writes while extension-defined allowSelf relationships remain supported."},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:33:56.842Z"},{"op":"add","path":"/metadata/resolution","value":"Added public assertRelationshipEdgeAllowed and shared pre-persistence dependency enforcement for create/update, with alias-aware custom allowSelf semantics and deterministic usage errors."}],"before_hash":"29d8813b4da5f31faeaf57f3ccc11942940235655708657d2410205b71ae51d2","after_hash":"c23d0dd044b6653417835cdd382002fcf2aa4d454c730aa7bde150a44cef91fa","message":"Record verified issue resolution"} +{"ts":"2026-07-23T05:33:58.883Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-07-23T05:33:58.883Z","author":"claude-code-agent","text":"Final local evidence: 6,101 tests at exact 100/100/100/100; all static/package/security/dogfood gates pass. Consolidated release gate's only red check is the pre-fix live Sentry issue PM-CLI-2J, deliberately retained until this fix ships."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:33:58.883Z"}],"before_hash":"c23d0dd044b6653417835cdd382002fcf2aa4d454c730aa7bde150a44cef91fa","after_hash":"ccfa1da3344b1670f0c2729178de450ca8f581973c997f4efe8904bf4962510c"} +{"ts":"2026-07-23T05:34:02.127Z","author":"claude-code-agent","op":"close","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:02.127Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-07-23T05:34:02.127Z"},{"op":"add","path":"/metadata/close_reason","value":"Implemented and verified alias-aware pre-write self-edge policy with public SDK customization support and zero-write CLI acceptance."}],"before_hash":"ccfa1da3344b1670f0c2729178de450ca8f581973c997f4efe8904bf4962510c","after_hash":"9c8cd3ce60e457a908649a54a2cd66f4c101450784c1201d02ee5c26c428323e"} +{"ts":"2026-07-23T05:34:02.852Z","author":"claude-code-agent","op":"release","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:02.852Z"}],"before_hash":"9c8cd3ce60e457a908649a54a2cd66f4c101450784c1201d02ee5c26c428323e","after_hash":"7fcfd8ee238b683993f64a5a5503a4476819cefd905cfb94a166c14f13b5638a"} +{"ts":"2026-07-23T05:38:24.075Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-07-23T05:38:24.074Z","author":"claude-code-agent","text":"PR evidence: public SDK relationship policy and pre-write CLI enforcement are published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:38:24.075Z"}],"before_hash":"7fcfd8ee238b683993f64a5a5503a4476819cefd905cfb94a166c14f13b5638a","after_hash":"b7e7bec033b476c8523a5e5f1c18e862dd61a4cdc9136a733284b2360f25c25c"} +{"ts":"2026-07-23T05:48:21.300Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-07-23T05:48:21.300Z","author":"claude-code-agent","text":"Hosted review and release evidence: PR #652 head 8f6ba731 is mergeable with 29 successful / 0 failing checks and no inline review threads. CodeRabbit/Sourcery/Greptile were externally quota-limited and produced no code findings; all available bot reports were individually reacted to and acknowledged. Scheduled Auto Release run 29982743761 passed build, exact coverage, packed smoke, dogfood, version, and security, then correctly failed closed only on Sentry PM-CLI-2J and opened GH-653 without cutting a tag."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:48:21.300Z"}],"before_hash":"b7e7bec033b476c8523a5e5f1c18e862dd61a4cdc9136a733284b2360f25c25c","after_hash":"f32d007fd0fd33adee595359485007c57ce97b1820388c7f2060fdda89ec762c"} diff --git a/.agents/pm/history/pm-mmm9o5.jsonl b/.agents/pm/history/pm-mmm9o5.jsonl new file mode 100644 index 000000000..cea941d69 --- /dev/null +++ b/.agents/pm/history/pm-mmm9o5.jsonl @@ -0,0 +1,19 @@ +{"ts":"2026-07-23T04:52:48.714Z","author":"claude-code-agent","op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-mmm9o5"},{"op":"add","path":"/metadata/title","value":"GH-646: classify merge reconciliation discards by net outcome"},{"op":"add","path":"/metadata/description","value":"The merge-driver union path preserves append-only content and attribution, but history reconciliation reports discarded events, authors, reverted fields, and recovery guidance as though user-visible data were lost."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["agent-ux","area:history","area:merge","area:sdk","gh-issue","multi-agent","token-efficiency"]},{"op":"add","path":"/metadata/created_at","value":"2026-07-23T04:52:48.714Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-23T04:52:48.714Z"},{"op":"add","path":"/metadata/author","value":"claude-code-agent"},{"op":"add","path":"/metadata/acceptance_criteria","value":"Public reconciliation results classify every candidate event as net-lossy or net-lossless using field semantics and final on-disk state, discarded_authors and recovery guidance include only effective loss, append-only union fields retain attribution and avoid loss warnings, genuinely lossy GH-603 behavior remains loud, CLI SDK and documentation use truthful terminology, real git branch acceptance covers driver union and lossy resolution, focused and full coverage remain 100 percent"},{"op":"add","path":"/metadata/parent","value":"pm-o2kc"},{"op":"add","path":"/metadata/reporter","value":"GitHub GH-646"},{"op":"add","path":"/metadata/severity","value":"high"},{"op":"add","path":"/metadata/expected_result","value":"The public reconciliation SDK distinguishes replay-discarded operations from net-lossy outcomes, emits loud warnings and recovery only when final state lost effective content, and reports preserved union contributions without false data-loss alarms."},{"op":"add","path":"/metadata/actual_result","value":"Dry-run and apply report union-preserved note events and their authors as discarded, label notes reverted, and recommend recovery even though final item content, attribution, and history events are all intact."},{"op":"add","path":"/metadata/affected_version","value":"2026.7.22"},{"op":"add","path":"/metadata/dependencies","value":[{"id":"pm-9q2t","kind":"related","created_at":"2026-07-23T04:52:48.714Z"},{"id":"pm-g5sx","kind":"discovered_from","created_at":"2026-07-23T04:52:48.714Z"},{"id":"pm-gpo7","kind":"related","created_at":"2026-07-23T04:52:48.714Z"},{"id":"pm-mfkv92","kind":"related","created_at":"2026-07-23T04:52:48.714Z"},{"id":"pm-wc1r","kind":"related","created_at":"2026-07-23T04:52:48.714Z"}]},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-07-23T04:52:48.714Z","author":"claude-code-agent","text":"Duplicate check 2026-07-23: all-status searches for GH-646,merge reconcile discarded events/authors,history repair discard union field,and the GH-603/GH-623 lineage found closed canonical merge work pm-g5sx,pm-gpo7,pm-mfkv92,pm-wc1r,and pm-9q2t,but no item owns the complementary false-positive/net-lossless classification defect. Created this canonical leaf without reopening completed merge safety items."}]}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"37bb315140398472c65987fd4c8a944bac1b3fe4cf41e7e693fde328b7812b1c","message":""} +{"ts":"2026-07-23T04:53:22.569Z","author":"claude-code-agent","op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:22.569Z"},{"op":"add","path":"/metadata/assignee","value":"claude-code-agent"}],"before_hash":"37bb315140398472c65987fd4c8a944bac1b3fe4cf41e7e693fde328b7812b1c","after_hash":"e66345d76e31b0c97243f47a3224635fa8932b7eef6d8884655b024ac49a4372"} +{"ts":"2026-07-23T04:53:23.650Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:23.650Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"e66345d76e31b0c97243f47a3224635fa8932b7eef6d8884655b024ac49a4372","after_hash":"e427a361740463d6930d852d5954f765d614ebc7f595f8f70aa4893b51e3b184","message":"Start broad SDK/CLI truthful-context lifecycle implementation"} +{"ts":"2026-07-23T04:53:28.446Z","author":"claude-code-agent","op":"update","patch":[{"op":"add","path":"/metadata/dependencies/5","value":{"id":"pm-3esl28","kind":"related","created_at":"2026-07-23T04:53:28.251Z"}},{"op":"add","path":"/metadata/dependencies/6","value":{"id":"pm-ds3b","kind":"verifies","created_at":"2026-07-23T04:53:28.251Z"}},{"op":"add","path":"/metadata/dependencies/7","value":{"id":"pm-k9t17l","kind":"related","created_at":"2026-07-23T04:53:28.251Z"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:28.446Z"}],"before_hash":"e427a361740463d6930d852d5954f765d614ebc7f595f8f70aa4893b51e3b184","after_hash":"f7dccea98647f1943ba6eb24ee1b46542d2477a90821d38f6b0e14e55a67a57c"} +{"ts":"2026-07-23T04:53:30.152Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/1","value":{"created_at":"2026-07-23T04:53:30.151Z","author":"claude-code-agent","text":"GitHub linkage evidence: canonical pm item posted on GH-646 at https://github.com/unbraind/pm-cli/issues/646#issuecomment-5054564363. Active bundle shares the truthful-context lifecycle acceptance surface with pm-3esl28, pm-k9t17l, and rolling SDK reference pm-ds3b."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T04:53:30.152Z"}],"before_hash":"f7dccea98647f1943ba6eb24ee1b46542d2477a90821d38f6b0e14e55a67a57c","after_hash":"d17e2adc52ed32e7a0a5af9cf885fbc70df679e410ac112165a764be31c3e244"} +{"ts":"2026-07-23T05:04:43.554Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:43.554Z"},{"op":"add","path":"/metadata/files","value":[{"path":"src/sdk/history-repair.ts","scope":"project","note":"Loss-aware reconciliation classification"}]}],"before_hash":"d17e2adc52ed32e7a0a5af9cf885fbc70df679e410ac112165a764be31c3e244","after_hash":"30d6979eaba452b52020b6dacb9df3494b21da2d544ddfc67e5029539a68409a"} +{"ts":"2026-07-23T05:04:44.465Z","author":"claude-code-agent","op":"files_add","patch":[{"op":"add","path":"/metadata/files/1","value":{"path":"tests/unit/sdk/history-repair-reconciliation.spec.ts","scope":"project","note":"Lossy and union-preserved classification coverage"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:44.465Z"}],"before_hash":"30d6979eaba452b52020b6dacb9df3494b21da2d544ddfc67e5029539a68409a","after_hash":"04e3df561edab231b7a2769e17fb967ee6c1879fb5fcb9f009d822b27d9d47bb"} +{"ts":"2026-07-23T05:04:45.291Z","author":"claude-code-agent","op":"docs_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:45.291Z"},{"op":"add","path":"/metadata/docs","value":[{"path":"docs/MERGE_SAFETY.md","scope":"project","note":"Truthful post-merge warning semantics"}]}],"before_hash":"04e3df561edab231b7a2769e17fb967ee6c1879fb5fcb9f009d822b27d9d47bb","after_hash":"2c8484ce6202f0482192921380f82b087ee8e9fcae38d9a0c4c993f67c227e58"} +{"ts":"2026-07-23T05:04:46.092Z","author":"claude-code-agent","op":"docs_add","patch":[{"op":"add","path":"/metadata/docs/1","value":{"path":"docs/SDK.md","scope":"project","note":"Structured reconciliation result contract"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:46.092Z"}],"before_hash":"2c8484ce6202f0482192921380f82b087ee8e9fcae38d9a0c4c993f67c227e58","after_hash":"bcf38866d45a16e2f6ad656a3d70f16c12058d7592c6777f19990d6f7cbfccfb"} +{"ts":"2026-07-23T05:04:46.855Z","author":"claude-code-agent","op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:46.855Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node scripts/run-tests.mjs test -- tests/unit/sdk/history-repair-reconciliation.spec.ts tests/unit/commands/history-repair-command.spec.ts","scope":"project","timeout_seconds":240,"note":"Reconciliation unit and command acceptance"}]}],"before_hash":"bcf38866d45a16e2f6ad656a3d70f16c12058d7592c6777f19990d6f7cbfccfb","after_hash":"2e3450ad045521d3bc6e759625504e3b58c080c448260e6260e2bee54276e395"} +{"ts":"2026-07-23T05:04:50.147Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/2","value":{"created_at":"2026-07-23T05:04:50.147Z","author":"claude-code-agent","text":"TDD evidence: added failing lossy-versus-union reconciliation cases, then partitioned reconciled fields/events/authors into preserved and discarded results. Discard warnings now remain exclusive to genuine loss; focused command and SDK tests are green."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:04:50.147Z"}],"before_hash":"2e3450ad045521d3bc6e759625504e3b58c080c448260e6260e2bee54276e395","after_hash":"487839cf87251351b1589495daaaad8fa5af9f9df8983eb392c1d44d06729ac6"} +{"ts":"2026-07-23T05:30:03.248Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/3","value":{"created_at":"2026-07-23T05:30:03.247Z","author":"claude-code-agent","text":"Release-gate checkpoint: build/typecheck/docs/static/docstring/package-boundary/token-budget/exact 100% coverage/version/secrets/packed npx smoke/88-command dogfood passed. Consolidated gate remains red only for live Sentry PM-CLI-2J, which is canonical to pm-k9t17l and must remain unsuppressed until the fix is released."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:30:03.248Z"}],"before_hash":"487839cf87251351b1589495daaaad8fa5af9f9df8983eb392c1d44d06729ac6","after_hash":"2694879c8c55d78c5b084122fac79f02058c158c0272abba5d2c394e013b7645"} +{"ts":"2026-07-23T05:31:51.281Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/4","value":{"created_at":"2026-07-23T05:31:51.281Z","author":"claude-code-agent","text":"Packed real-Git acceptance passed using installed merge drivers: divergent agent-a/agent-b note appends merged to two notes; merge reconcile dry-run reported zero false discard warnings/events; strict history-drift validation passed. Evidence workspace: /tmp/tmp.9DpM65yUme."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:31:51.281Z"}],"before_hash":"2694879c8c55d78c5b084122fac79f02058c158c0272abba5d2c394e013b7645","after_hash":"2fd5fcddb6c97aa227ae6efccf50585b0fc2ff0f50328d09e05b8445c6b92e53"} +{"ts":"2026-07-23T05:32:37.252Z","author":"claude-code-agent","op":"test_run_track","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:32:37.252Z"},{"op":"add","path":"/metadata/test_runs","value":[{"run_id":"test-local-mrx2rgfv-d676hd","kind":"test","status":"passed","started_at":"2026-07-23T05:32:22.559Z","finished_at":"2026-07-23T05:32:37.243Z","recorded_at":"2026-07-23T05:32:37.243Z","passed":1,"failed":0,"skipped":0}]}],"before_hash":"2fd5fcddb6c97aa227ae6efccf50585b0fc2ff0f50328d09e05b8445c6b92e53","after_hash":"a3e6057dcc90edb36c5b9352f334af2a807f9e79ba287a08b190e07997b811f5","message":"Track test run summary (test-local-mrx2rgfv-d676hd)"} +{"ts":"2026-07-23T05:33:57.498Z","author":"claude-code-agent","op":"update","patch":[{"op":"replace","path":"/metadata/actual_result","value":"SDK/command tests, exact coverage, and an installed two-branch Git merge prove both notes survive with zero false discard warnings/events and strict history validation passes."},{"op":"replace","path":"/metadata/expected_result","value":"Append-only unions and deterministic reorderings remain visible without false data-loss alarms, while scalar replacement/removal remains loud."},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:33:57.498Z"},{"op":"add","path":"/metadata/resolution","value":"History reconciliation now partitions all changed fields/events/authors into structurally preserved versus genuinely discarded context; only actual loss emits discard warnings and recovery guidance."}],"before_hash":"a3e6057dcc90edb36c5b9352f334af2a807f9e79ba287a08b190e07997b811f5","after_hash":"4e558c961d2e4323331f24ffe5c99a0a845ca5f042520c1974d2036541342063","message":"Record verified issue resolution"} +{"ts":"2026-07-23T05:33:59.542Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-07-23T05:33:59.541Z","author":"claude-code-agent","text":"Final local evidence: exact full coverage and all repository-owned gates pass; installed two-branch merge validation is green. Live health storage warning belongs to existing open pm-h90s (unknown-author history disposition) and was refreshed there without expanding this PR."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:33:59.542Z"}],"before_hash":"4e558c961d2e4323331f24ffe5c99a0a845ca5f042520c1974d2036541342063","after_hash":"ffbd551d36de076652074e06f88f7f41be99c904a7cbe2d4a4cbb58fe0c7435d"} +{"ts":"2026-07-23T05:34:03.760Z","author":"claude-code-agent","op":"close","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:03.760Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-07-23T05:34:03.760Z"},{"op":"add","path":"/metadata/close_reason","value":"Implemented and verified loss-aware reconciliation classification through unit, command, packed, and real two-branch Git acceptance."}],"before_hash":"ffbd551d36de076652074e06f88f7f41be99c904a7cbe2d4a4cbb58fe0c7435d","after_hash":"86bd6a82d0da1478d4a22ba4f27a9d1dd99a26742ac9929573e6f6dcc1acd054"} +{"ts":"2026-07-23T05:34:04.479Z","author":"claude-code-agent","op":"release","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:34:04.479Z"}],"before_hash":"86bd6a82d0da1478d4a22ba4f27a9d1dd99a26742ac9929573e6f6dcc1acd054","after_hash":"d60aea7db3f028b03c8e39a10ea7428eb803c811cfd9e02d10cccb1c96cf4ded"} +{"ts":"2026-07-23T05:38:24.952Z","author":"claude-code-agent","op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-07-23T05:38:24.952Z","author":"claude-code-agent","text":"PR evidence: net-loss merge reconciliation classification is published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-23T05:38:24.952Z"}],"before_hash":"d60aea7db3f028b03c8e39a10ea7428eb803c811cfd9e02d10cccb1c96cf4ded","after_hash":"5004f1f2dffadc640a69290b85bc26a7226c7d3099a7da49eb068609f1a37530"} diff --git a/.agents/pm/issues/pm-3esl28.toon b/.agents/pm/issues/pm-3esl28.toon index f0401c902..226254a1d 100644 --- a/.agents/pm/issues/pm-3esl28.toon +++ b/.agents/pm/issues/pm-3esl28.toon @@ -2,22 +2,41 @@ id: pm-3esl28 title: "GH-645: standard-depth get/show JSON omits notes without notes_count" description: "At the default standard depth, pm get/show --json suppresses notes but exposes no notes_count or omission metadata, so agents can falsely conclude the item has no working context." type: Issue -status: open +status: closed priority: 2 tags[5]: agent-ux,"area:cli",gh-issue,json,token-efficiency created_at: "2026-07-22T19:01:44.269Z" -updated_at: "2026-07-22T19:01:57.176Z" +updated_at: "2026-07-23T05:38:23.338Z" +closed_at: "2026-07-23T05:34:00.488Z" author: codex-sdk-boundary-20260722 acceptance_criteria: "Default standard-depth get/show JSON reports notes_count whenever notes are omitted; full/deep projections remain backward compatible and include notes; CLI aliases, SDK result contracts, MCP projections, TOON/JSON docs, and contract fixtures remain coherent; regression tests cover zero, one, and multiple notes plus explicit depth modes" parent: pm-mpbb reporter: GitHub GH-645 severity: medium -expected_result: "Standard-depth structured output makes omitted note context explicit through notes_count and depth/projection metadata, or includes the notes consistently." -actual_result: Default standard-depth JSON omits notes and exposes neither notes_count nor a signal that note context was projected out. +resolution: "Standard and brief get projections now expose item.notes_count whenever note bodies are omitted; deep/full reads retain note bodies without a redundant count, and narrow fields accept notes_count." +expected_result: Agents can distinguish zero notes from omitted note context without requesting a full item payload. +actual_result: "CLI, PmClient, aliases, field projection, SDK docs, focused tests, full coverage, and packed installed-consumer acceptance all confirm the truthful count contract." affected_version: 2026.7.22 -dependencies[1]{id,kind,created_at,author,source_kind}: +dependencies[4]{id,kind,created_at,author,source_kind}: pm-srns,related,"2026-07-22T19:01:44.269Z",null,null -comments[2]{created_at,author,text}: + pm-ds3b,verifies,"2026-07-23T04:53:26.301Z",null,null + pm-k9t17l,related,"2026-07-23T04:53:26.301Z",null,null + pm-mmm9o5,related,"2026-07-23T04:53:26.301Z",null,null +comments[6]{created_at,author,text}: "2026-07-22T19:01:44.269Z",codex-sdk-boundary-20260722,"Duplicate check 2026-07-22: all-status searches for GH-645,show/get JSON notes_count,standard depth omitted notes,and projection metadata found adjacent output-contract lineage pm-srns and completed JSON work pm-cfed/pm-wrss,but no item owns this exact missing-context signal. Created open and unclaimed; not part of the active SDK boundary PR." "2026-07-22T19:01:57.175Z",codex-sdk-boundary-20260722,"GitHub linkage evidence: posted the canonical pm item link on GH-645 at https://github.com/unbraind/pm-cli/issues/645. Item remains open/unclaimed and outside the active SDK boundary implementation." + "2026-07-23T05:04:48.453Z",claude-code-agent,"TDD evidence: added failing standard/brief/deep/full and narrow notes_count assertions, then implemented the compact projection contract. Focused acceptance is green." + "2026-07-23T05:31:49.890Z",claude-code-agent,"Packed installed-consumer acceptance passed from an isolated npm install: standard get returned notes_count=1 without note bodies; full get returned one note without redundant notes_count. Evidence workspace: /tmp/tmp.9DpM65yUme (local-only, not committed)." + "2026-07-23T05:33:58.199Z",claude-code-agent,"Final local evidence: 6,101 tests pass at exact 100% statements/branches/functions/lines; typecheck, ESLint, jscpd zero-clone, 100% docstrings, package boundary, token budget, docs, links, secrets, version, packed smoke, and 88-command dogfood gates pass." + "2026-07-23T05:38:23.338Z",claude-code-agent,"PR evidence: truthful SDK/CLI context bundle is published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12." +files[2]{path,scope,note}: + src/cli/commands/get.ts,project,Truthful token-bounded note projection + tests/unit/commands/get-append-command.spec.ts,project,Depth and narrow-field acceptance coverage +tests[1]{command,scope,timeout_seconds,note}: + node scripts/run-tests.mjs test -- tests/unit/commands/get-append-command.spec.ts,project,240,Get projection acceptance +test_runs[1]{run_id,kind,status,started_at,finished_at,recorded_at,passed,failed,skipped}: + test-local-mrx2qrkl-ksdhg7,test,passed,"2026-07-23T05:31:51.916Z","2026-07-23T05:32:05.013Z","2026-07-23T05:32:05.013Z",1,0,0 +docs[1]{path,scope,note}: + docs/SDK.md,project,GetResult notes_count contract +close_reason: "Implemented and verified truthful omitted-note counts across CLI/SDK projections, docs, full coverage, and packed consumer acceptance." body: "" diff --git a/.agents/pm/issues/pm-h90s.toon b/.agents/pm/issues/pm-h90s.toon index e8abaf649..d4809195b 100644 --- a/.agents/pm/issues/pm-h90s.toon +++ b/.agents/pm/issues/pm-h90s.toon @@ -6,7 +6,7 @@ status: open priority: 1 tags[3]: agent-ux,"area:governance",history-integrity created_at: "2026-07-17T04:42:36.823Z" -updated_at: "2026-07-21T07:57:07.125Z" +updated_at: "2026-07-23T05:33:55.569Z" author: maintainer-agent estimated_minutes: 360 acceptance_criteria: "strict mode (opt-in settings key) rejects mutations resolving to author 'unknown' with a typed usage error naming --author/PM_AUTHOR; TestAllCommandOptions accepts author and threads it to appendTrackedTestRunSummary (parity with single-run path); an audited acknowledgment flow dispositions the five 2026-07-16 events and pm health returns ok:true without baseline moves or history rewrites; validate/health docs cover all three; regression specs for each" @@ -19,7 +19,8 @@ dependencies[5]{id,kind,created_at,author,source_kind}: pm-demq,related,"2026-07-17T04:42:36.823Z",null,null pm-u14c,related,"2026-07-17T04:42:36.823Z",null,null pm-wenq,related,"2026-07-17T04:42:36.823Z",null,null -comments[2]{created_at,author,text}: +comments[3]{created_at,author,text}: "2026-07-19T16:15:16.114Z",codex-wide-sdk-20260719,"2026-07-19 verification: pm health still reports exactly history_unknown_author_events:5 and validate reports no history drift, matching this canonical open issue's documented immutable-event scope. No duplicate item created and no history files were rewritten." "2026-07-21T07:57:07.125Z",codex-root-20260721,"2026-07-21 shipping-lane verification: pm health still reports exactly 5 actionable unknown-author events, with 0 history drift, 0 active/stale locks, and storage integrity otherwise clean. This remains the canonical prevention/disposition item; no duplicate was created and immutable history was not rewritten." + "2026-07-23T05:33:55.569Z",claude-code-agent,"Fresh health evidence 2026-07-23: pm health --check-only reports storage warn with 167 unknown-author events, partitioned into 162 legacy and 5 actionable across 1,925 streams / 32,373 checked events. This is the existing canonical item; no duplicate was created and it was not claimed by the truthful-context PR." body: "" diff --git a/.agents/pm/issues/pm-k9t17l.toon b/.agents/pm/issues/pm-k9t17l.toon new file mode 100644 index 000000000..1366cd710 --- /dev/null +++ b/.agents/pm/issues/pm-k9t17l.toon @@ -0,0 +1,51 @@ +id: pm-k9t17l +title: "GH-649: reject self-referential ordering relationships at the SDK mutation boundary" +description: "A create/update dependency mutation can persist a self blocked_by or inverse ordering edge that graph governance forbids, permanently removing the item from readiness while pm deps fails to identify the one-node cycle." +type: Issue +status: closed +priority: 1 +tags[6]: agent-ux,"area:graph","area:sdk",data-integrity,gh-issue,multi-agent +created_at: "2026-07-23T04:52:33.471Z" +updated_at: "2026-07-23T05:48:21.300Z" +closed_at: "2026-07-23T05:34:02.127Z" +author: claude-code-agent +acceptance_criteria: "Every shared dependency mutation path rejects self edges when the resolved relationship kind disallows self links, inverse and alias spellings share the rule, custom allowSelf kinds remain writable, reads render legacy self cycles truthfully, no item or history write occurs on rejection, public SDK contracts and docs explain the invariant, focused and full coverage remain 100 percent" +parent: pm-6x7o +reporter: GitHub GH-649 +severity: high +resolution: "Added public assertRelationshipEdgeAllowed and shared pre-persistence dependency enforcement for create/update, with alias-aware custom allowSelf semantics and deterministic usage errors." +expected_result: Disallowed self dependencies fail before item/history writes while extension-defined allowSelf relationships remain supported. +actual_result: "CLI zero-write acceptance, public packed SDK import, real installed CLI usage=2 behavior, exact coverage, and full gates confirm the mutation boundary." +affected_version: 2026.7.22 +dependencies[7]{id,kind,created_at,author,source_kind}: + pm-4jqm,related,"2026-07-23T04:52:33.471Z",null,null + pm-6irg,related,"2026-07-23T04:52:33.471Z",null,null + pm-i6pi,related,"2026-07-23T04:52:33.471Z",null,null + pm-ju83,related,"2026-07-23T04:52:33.471Z",null,null + pm-3esl28,related,"2026-07-23T04:53:27.272Z",null,null + pm-ds3b,verifies,"2026-07-23T04:53:27.272Z",null,null + pm-mmm9o5,related,"2026-07-23T04:53:27.272Z",null,null +comments[8]{created_at,author,text}: + "2026-07-23T04:52:33.471Z",claude-code-agent,"Duplicate check 2026-07-23: all-status searches for GH-649,self-referential ordering edge,blocked_by self edge,and dependency cycle write guard found shipped cycle advisory pm-i6pi,governance lineage pm-6x7o/pm-ju83/pm-4jqm,and closed detection work pm-6irg,but no item owns the exact write-time self-edge inconsistency. Created this canonical leaf rather than reopening shipped work." + "2026-07-23T04:53:29.272Z",claude-code-agent,"GitHub linkage evidence: canonical pm item posted on GH-649 at https://github.com/unbraind/pm-cli/issues/649#issuecomment-5054564177. Active bundle shares the truthful-context lifecycle acceptance surface with pm-3esl28, pm-mmm9o5, and rolling SDK reference pm-ds3b." + "2026-07-23T05:04:49.295Z",claude-code-agent,"TDD evidence: added failing SDK alias/allowSelf policy tests plus CLI create/update zero-write acceptance, then implemented one shared pre-persistence edge validator used by both mutation paths. Focused acceptance is green." + "2026-07-23T05:30:02.577Z",claude-code-agent,"Live observability evidence: release gate identified Sentry PM-CLI-2J as the sole blocker. Sanitized issue facts: unresolved/high, handled TypeError from normalizeRelationshipEdge, 3 events, first/last seen 2026-07-22, all on release pm-cli@2026.7.22, no events after 2026-07-22T21:40:25Z. This implementation converts create/update self-edge attempts into deterministic usage errors before persistence; keep the Sentry issue unresolved until the merged release is verified, then resolve and rerun the gate." + "2026-07-23T05:31:50.579Z",claude-code-agent,"Packed installed-consumer acceptance passed: public SDK custom allowSelf kind succeeded; CLI self dependency via depends-on exited usage=2 and created neither item nor history state. Real two-branch Git merge also retained both agents' notes. Evidence workspace: /tmp/tmp.9DpM65yUme." + "2026-07-23T05:33:58.883Z",claude-code-agent,"Final local evidence: 6,101 tests at exact 100/100/100/100; all static/package/security/dogfood gates pass. Consolidated release gate's only red check is the pre-fix live Sentry issue PM-CLI-2J, deliberately retained until this fix ships." + "2026-07-23T05:38:24.074Z",claude-code-agent,"PR evidence: public SDK relationship policy and pre-write CLI enforcement are published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12." + "2026-07-23T05:48:21.300Z",claude-code-agent,"Hosted review and release evidence: PR #652 head 8f6ba731 is mergeable with 29 successful / 0 failing checks and no inline review threads. CodeRabbit/Sourcery/Greptile were externally quota-limited and produced no code findings; all available bot reports were individually reacted to and acknowledged. Scheduled Auto Release run 29982743761 passed build, exact coverage, packed smoke, dogfood, version, and security, then correctly failed closed only on Sentry PM-CLI-2J and opened GH-653 without cutting a tag." +files[6]{path,scope,note}: + src/cli/commands/create.ts,project,Pre-write create enforcement + src/cli/commands/update.ts,project,Pre-write update enforcement + src/sdk/dependency-flag-validation.ts,project,Shared dependency mutation validation + src/sdk/relationships.ts,project,Reusable relationship edge policy primitive + tests/unit/commands/agent-ux-feedback-wave.spec.ts,project,CLI zero-write acceptance + tests/unit/sdk/relationships.spec.ts,project,Registry alias and allowSelf coverage +tests[1]{command,scope,timeout_seconds,note}: + node scripts/run-tests.mjs test -- tests/unit/sdk/relationships.spec.ts tests/unit/commands/agent-ux-feedback-wave.spec.ts,project,240,SDK and CLI relationship integrity acceptance +test_runs[1]{run_id,kind,status,started_at,finished_at,recorded_at,passed,failed,skipped}: + test-local-mrx2r44v-r2wr3n,test,passed,"2026-07-23T05:32:05.763Z","2026-07-23T05:32:21.296Z","2026-07-23T05:32:21.296Z",1,0,0 +docs[1]{path,scope,note}: + docs/SDK.md,project,Package-author mutation contract +close_reason: Implemented and verified alias-aware pre-write self-edge policy with public SDK customization support and zero-write CLI acceptance. +body: "" diff --git a/.agents/pm/issues/pm-mmm9o5.toon b/.agents/pm/issues/pm-mmm9o5.toon new file mode 100644 index 000000000..10bb70602 --- /dev/null +++ b/.agents/pm/issues/pm-mmm9o5.toon @@ -0,0 +1,48 @@ +id: pm-mmm9o5 +title: "GH-646: classify merge reconciliation discards by net outcome" +description: "The merge-driver union path preserves append-only content and attribution, but history reconciliation reports discarded events, authors, reverted fields, and recovery guidance as though user-visible data were lost." +type: Issue +status: closed +priority: 1 +tags[7]: agent-ux,"area:history","area:merge","area:sdk",gh-issue,multi-agent,token-efficiency +created_at: "2026-07-23T04:52:48.714Z" +updated_at: "2026-07-23T05:38:24.952Z" +closed_at: "2026-07-23T05:34:03.760Z" +author: claude-code-agent +acceptance_criteria: "Public reconciliation results classify every candidate event as net-lossy or net-lossless using field semantics and final on-disk state, discarded_authors and recovery guidance include only effective loss, append-only union fields retain attribution and avoid loss warnings, genuinely lossy GH-603 behavior remains loud, CLI SDK and documentation use truthful terminology, real git branch acceptance covers driver union and lossy resolution, focused and full coverage remain 100 percent" +parent: pm-o2kc +reporter: GitHub GH-646 +severity: high +resolution: History reconciliation now partitions all changed fields/events/authors into structurally preserved versus genuinely discarded context; only actual loss emits discard warnings and recovery guidance. +expected_result: "Append-only unions and deterministic reorderings remain visible without false data-loss alarms, while scalar replacement/removal remains loud." +actual_result: "SDK/command tests, exact coverage, and an installed two-branch Git merge prove both notes survive with zero false discard warnings/events and strict history validation passes." +affected_version: 2026.7.22 +dependencies[8]{id,kind,created_at,author,source_kind}: + pm-9q2t,related,"2026-07-23T04:52:48.714Z",null,null + pm-g5sx,discovered_from,"2026-07-23T04:52:48.714Z",null,null + pm-gpo7,related,"2026-07-23T04:52:48.714Z",null,null + pm-mfkv92,related,"2026-07-23T04:52:48.714Z",null,null + pm-wc1r,related,"2026-07-23T04:52:48.714Z",null,null + pm-3esl28,related,"2026-07-23T04:53:28.251Z",null,null + pm-ds3b,verifies,"2026-07-23T04:53:28.251Z",null,null + pm-k9t17l,related,"2026-07-23T04:53:28.251Z",null,null +comments[7]{created_at,author,text}: + "2026-07-23T04:52:48.714Z",claude-code-agent,"Duplicate check 2026-07-23: all-status searches for GH-646,merge reconcile discarded events/authors,history repair discard union field,and the GH-603/GH-623 lineage found closed canonical merge work pm-g5sx,pm-gpo7,pm-mfkv92,pm-wc1r,and pm-9q2t,but no item owns the complementary false-positive/net-lossless classification defect. Created this canonical leaf without reopening completed merge safety items." + "2026-07-23T04:53:30.151Z",claude-code-agent,"GitHub linkage evidence: canonical pm item posted on GH-646 at https://github.com/unbraind/pm-cli/issues/646#issuecomment-5054564363. Active bundle shares the truthful-context lifecycle acceptance surface with pm-3esl28, pm-k9t17l, and rolling SDK reference pm-ds3b." + "2026-07-23T05:04:50.147Z",claude-code-agent,"TDD evidence: added failing lossy-versus-union reconciliation cases, then partitioned reconciled fields/events/authors into preserved and discarded results. Discard warnings now remain exclusive to genuine loss; focused command and SDK tests are green." + "2026-07-23T05:30:03.247Z",claude-code-agent,"Release-gate checkpoint: build/typecheck/docs/static/docstring/package-boundary/token-budget/exact 100% coverage/version/secrets/packed npx smoke/88-command dogfood passed. Consolidated gate remains red only for live Sentry PM-CLI-2J, which is canonical to pm-k9t17l and must remain unsuppressed until the fix is released." + "2026-07-23T05:31:51.281Z",claude-code-agent,"Packed real-Git acceptance passed using installed merge drivers: divergent agent-a/agent-b note appends merged to two notes; merge reconcile dry-run reported zero false discard warnings/events; strict history-drift validation passed. Evidence workspace: /tmp/tmp.9DpM65yUme." + "2026-07-23T05:33:59.541Z",claude-code-agent,"Final local evidence: exact full coverage and all repository-owned gates pass; installed two-branch merge validation is green. Live health storage warning belongs to existing open pm-h90s (unknown-author history disposition) and was refreshed there without expanding this PR." + "2026-07-23T05:38:24.952Z",claude-code-agent,"PR evidence: net-loss merge reconciliation classification is published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12." +files[2]{path,scope,note}: + src/sdk/history-repair.ts,project,Loss-aware reconciliation classification + tests/unit/sdk/history-repair-reconciliation.spec.ts,project,Lossy and union-preserved classification coverage +tests[1]{command,scope,timeout_seconds,note}: + node scripts/run-tests.mjs test -- tests/unit/sdk/history-repair-reconciliation.spec.ts tests/unit/commands/history-repair-command.spec.ts,project,240,Reconciliation unit and command acceptance +test_runs[1]{run_id,kind,status,started_at,finished_at,recorded_at,passed,failed,skipped}: + test-local-mrx2rgfv-d676hd,test,passed,"2026-07-23T05:32:22.559Z","2026-07-23T05:32:37.243Z","2026-07-23T05:32:37.243Z",1,0,0 +docs[2]{path,scope,note}: + docs/MERGE_SAFETY.md,project,Truthful post-merge warning semantics + docs/SDK.md,project,Structured reconciliation result contract +close_reason: "Implemented and verified loss-aware reconciliation classification through unit, command, packed, and real two-branch Git acceptance." +body: "" diff --git a/.agents/pm/reminders/pm-7zs0.toon b/.agents/pm/reminders/pm-7zs0.toon index 72a37b9c0..3f8fc711d 100644 --- a/.agents/pm/reminders/pm-7zs0.toon +++ b/.agents/pm/reminders/pm-7zs0.toon @@ -6,7 +6,7 @@ status: open priority: 2 tags[3]: "area:release",living-map,reliability created_at: "2026-06-07T10:24:45.889Z" -updated_at: "2026-07-21T16:07:17.087Z" +updated_at: "2026-07-23T05:48:21.916Z" reminders[1]{at,text}: "2026-07-01T10:24:45.889Z",Confirm daily auto-release tagged a new version AND npm latest advanced; else inspect publish + Sentry-gate steps author: maintainer-agent @@ -18,7 +18,7 @@ confidence: high dependencies[2]{id,kind,created_at,author,source_kind}: pm-7n5a,related,"2026-07-15T18:17:35.524Z",null,null pm-d4ns,related,"2026-07-15T18:17:35.524Z",null,null -comments[83]{created_at,author,text}: +comments[84]{created_at,author,text}: "2026-06-10T14:44:30.277Z",maintainer-agent,"2026-06-10 daily auto-release (scheduled run 27258655654, 06:50Z) FAILED at sentry-telemetry-gate: PM-CLI-1J 'CommandError: {' — a CommandError whose message was the raw guided-error JSON envelope (code tracker_not_initialized) thrown by an ephemeral temp-dir script (readPmItemsForUpsert, source no longer on disk; 2 events 2026-06-09T21:34Z). Gate classifier fix already landed in PR #197 (fba75fb0, merged 11:34Z, adds 'tracker_not_initialized' to KNOWN_EXPECTED_HANDLED_CLI_ISSUE_PATTERNS) but AFTER the scheduled run. Recovery: resolved PM-CLI-1J in Sentry + re-dispatched auto-release.yml with push=true (run 27284359777). Will verify v2026.6.10 npm publish before closing." "2026-06-16T11:26:49.976Z",codex-sdk-cli-consolidation-20260616,"Auto-release gate reproduced locally with SENTRY_PERSONAL_ADMIN_TOKEN; failing high issue was PM-CLI-1Q (handled CommandError: all items failed to apply). Added count-agnostic handled pattern 'item(s) failed to apply' and blocking_short_ids/blocking_titles output for diagnostics. Gate now passes in strict mode with max-high=0." "2026-06-16T11:46:33.866Z",codex-sdk-cli-consolidation-20260616,"Release-health verification: sentry-telemetry gate now passes in strict mode (max-critical=0,max-high=0) after expected handled-CommandError classification update; output now includes blocking_short_ids/blocking_titles for actionable diagnostics." @@ -102,6 +102,7 @@ comments[83]{created_at,author,text}: "2026-07-18T07:14:44.882Z",codex-sdk-vcs-20260718,"Release-health snapshot 2026-07-18 before this branch ships: scheduled Auto Release run 29631792634 succeeded on main head 5c399c6d; GitHub release v2026.7.18 published at 05:24Z and is Latest. The automatic daily path is currently healthy. This branch changes package/SDK code but no release workflow; post-merge exact-head release verification remains required." "2026-07-18T12:37:21.792Z",codex-gpt5,"Release-health evidence 2026-07-18 for PR #593: scheduled Auto Release run 29631792634 succeeded on main head 5c399c6d, GitHub release v2026.7.18 is Latest, and npm @unbrained/pm-cli latest is 2026.7.18. The authenticated strict Sentry/telemetry gate passes with critical=0, high=0, 17 expected handled groups, seven-day finish_error_rate_pct=0.29, and zero failures missing error_code. The real-time one-day telemetry query at 2026-07-18T12:36:27Z returned 7,451 finish events, 1.93% aggregate finish error rate, and complete explicit error-code coverage. PR exact-head checks are green; post-merge exact-head release proof remains pending." "2026-07-21T16:07:17.086Z",codex-tracker-governance,"Release-health relationship update: exact main head 669055d9757850cdb54da198ab18df75312192d4 has one compatibility-gate regression after the published compact create response became the previous-version fixture input. Focused owner pm-pkdpyz tracks the harness fix and exact-head proof; the compact product response remains intentional and shipped." + "2026-07-23T05:48:21.914Z",claude-code-agent,"Daily release verification 2026-07-23: scheduled run 29982743761 executed the production path and failed closed only on unresolved Sentry PM-CLI-2J, opening GH-653. PR #652 contains the verified fix; after merge, resolve PM-CLI-2J in the released version and close GH-653 to exercise the guarded same-day auto-release retry. No duplicate release item created." notes[7]{created_at,author,text}: "2026-07-02T06:41:55.766Z",claude-code-agent,"2026-07-02: Auto Release run 28569991109 success; tag v2026.7.2 created and npm dist-tags.latest=2026.7.2 verified live via npm view. Release-commit CI/Release/CodeQL/Docs all green. Release health verified." "2026-07-04T06:16:41.031Z",maintainer-agent,"2026-07-04: Release v2026.7.4 verified — Release workflow 28697208639 succeeded, npm @unbrained/pm-cli@2026.7.4 live, GH release published 06:16Z (not draft). Auto-release healthy." diff --git a/.agents/pm/tasks/pm-ds3b.toon b/.agents/pm/tasks/pm-ds3b.toon index 00deed539..3ff02562a 100644 --- a/.agents/pm/tasks/pm-ds3b.toon +++ b/.agents/pm/tasks/pm-ds3b.toon @@ -6,7 +6,7 @@ status: open priority: 2 tags[2]: "area:docs","area:sdk" created_at: "2026-07-05T04:54:31.675Z" -updated_at: "2026-07-22T21:38:24.990Z" +updated_at: "2026-07-23T05:48:22.533Z" author: maintainer-agent estimated_minutes: 240 acceptance_criteria: SDK reference covers all promoted primitives with runnable examples; architecture doc explains core/sdk/cli-mcp layering and the boundary gate; docs gates pass; docstring coverage epic (pm-4ak1) conventions followed @@ -29,7 +29,7 @@ dependencies[15]{id,kind,created_at,author,source_kind}: pm-xm7c,related,"2026-07-20T08:49:51.219Z",null,null pm-3hps,related,"2026-07-21T16:54:34.602Z",codex-derived-context-20260721,null pm-801d,related,"2026-07-21T16:54:34.602Z",codex-derived-context-20260721,null -comments[23]{created_at,author,text}: +comments[27]{created_at,author,text}: "2026-07-13T22:03:50.226Z",codex-root,"Scope note 2026-07-13: when pm-ju83 lands, extend this task's SDK reference and migration work with relationship-kind registration, immutable relationship events, bounded adjacency/path/closure/impact queries, cursor and streaming contracts, compact CLI and MCP projections, explain/cost envelopes, legacy-kind migration, and shell pipeline examples. Link implementation docs to pm-96h7 and validate with the existing docs gates." "2026-07-17T13:10:37.141Z",codex-root,"Documentation progress: SDK and relationship-graph references now cover public actionability, workspace assembly, semantic traversal, governance findings, layering boundaries, and a non-PM domain reuse pattern. Docs gate passes." "2026-07-17T13:47:20.846Z",codex-root,"Verification evidence: linked docs-skills gate passed; SDK and relationship graph docs explain public actionability, assembly, semantic traversal, governance findings, layering, VCS/company reuse, bounds, and migration semantics. Strict 100% docstring coverage, docs links, package tarball, and runnable installed SDK examples pass. This documentation tranche is complete; the task remains open because its acceptance follows every future promoted primitive in pm-usfg/pm-ju83." @@ -53,6 +53,10 @@ comments[23]{created_at,author,text}: "2026-07-22T20:57:35.435Z",codex-graph-platform-20260722,"Implementation start 2026-07-22: actively consolidate the public SDK graph adapter, snapshot, migration, and package-author workflow into the reference while implementation and installed-consumer examples are verified." "2026-07-22T21:28:06.906Z",codex-graph-platform-20260722,"Documentation evidence: SDK and relationship-graph references now cover portable content-addressed snapshots, CAS adapters, federation collision rules, external conformance, million-node fixtures, authority boundaries, and package-author workflows. Both docs/skills and link gates pass." "2026-07-22T21:38:24.989Z",codex-graph-platform-20260722,"PR evidence: graph adapter and package-author reference updates are under hosted review in https://github.com/unbraind/pm-cli/pull/648 at commit c98ca0d8." + "2026-07-23T05:04:50.917Z",claude-code-agent,"SDK reference updated for GetResult.notes_count, assertRelationshipEdgeAllowed, and loss-aware HistoryRepairReconciliationReport semantics. Full gates and packed-consumer acceptance remain in progress." + "2026-07-23T05:30:04.053Z",claude-code-agent,"Release-gate checkpoint: SDK docs, links, typecheck, static quality, token budgets, package boundary, packed smoke, dogfood, and exact 100/100/100/100 coverage pass. Live Sentry gate remains intentionally red on the pre-fix 2026.7.22 self-edge issue tracked by pm-k9t17l." + "2026-07-23T05:38:25.746Z",claude-code-agent,"PR evidence: this SDK reference tranche is published for hosted review at https://github.com/unbraind/pm-cli/pull/652, initial implementation commit c0358d12." + "2026-07-23T05:48:22.532Z",claude-code-agent,"Exact-head hosted review evidence: PR #652 head 8f6ba731 has 29 successful / 0 failing checks, full hosted coverage, no inline review threads, and only external reviewer quota-limit notices. Scheduled Auto Release reached the mandatory Sentry gate and correctly blocked on the pre-fix self-edge issue tracked by pm-k9t17l." notes[1]{created_at,author,text}: "2026-07-18T10:06:40.159Z",maintainer-agent,"Documentation-target expansion 2026-07-18 (PR#587): the promoted-primitive set this reference must cover grew by the SDK relationship event platform — registerRelationshipKinds/RelationshipKindRegistry (extension-contributed kinds with direction/inverse/ordering/cardinality/lifecycle/aliases/version), RelationshipEventLog/RelationshipEventStore bounded immutable streams + reducer projections (in-memory and durable, checkpoint boundary semantics), workspace graph assembly merging active package ontologies, and host-bound SDK injection into extension command handlers (context.sdk with caller authorship). PR#587 seeded docs/SDK.md + docs/EXTENSIONS.md deltas and packages/pm-vcs/README.md is the runnable exemplar to cite; this item still owns the consolidated reference + migration notes. Prior-tranche targets unchanged (graph/analytics + governance subpath modules from PR#579/#580/#581)." learnings[1]{created_at,author,text}: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e71a4ff8..68fbaf516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ ### Fixed +- GH-646: classify merge reconciliation discards by net outcome ([pm-mmm9o5](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-mmm9o5.toon)) +- GH-649: reject self-referential ordering relationships at the SDK mutation boundary ([pm-k9t17l](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-k9t17l.toon)) +- GH-645: standard-depth get/show JSON omits notes without notes_count ([pm-3esl28](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-3esl28.toon)) - GH-638: delete JSON envelope reports the removed item as open ([pm-tz2ikr](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-tz2ikr.toon)) - GH-635: plan create cannot forward strict Plan-required metadata ([pm-qd3woa](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-qd3woa.toon)) - GH-642: linked-test runner enters interactive init wizard before repository commands ([pm-lcnk2n](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-lcnk2n.toon)) diff --git a/docs/MERGE_SAFETY.md b/docs/MERGE_SAFETY.md index 2bff2927a..5d89a43a2 100644 --- a/docs/MERGE_SAFETY.md +++ b/docs/MERGE_SAFETY.md @@ -33,12 +33,12 @@ pm merge install --dry-run --json ## Artifact semantics -| Artifact | Driver | Merge behavior | -| -------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Item `.toon` / `.md` | `pm-item-toon` / `pm-item-markdown` | Three-way field merge; format-specific drivers avoid passing the repository path through Git's shell command, append-like collections use set union, `updated_at` uses latest timestamp, and canonical serialization recomputes TOON counts. | -| `history/*.jsonl` | `pm-history` | Preserves the common prefix and both divergent suffixes, orders deterministically, then re-anchors the resulting hash chain. | -| tracker `**/*.jsonl` except the later `history/*.jsonl` override | `pm-relationship` | Covers default and package-owned custom relationship event paths, unions divergent suffixes by `eventId` (timestamp-ordered, ours-first on ties), and renumbers `sequence` consecutively so the strict-sequence store loader accepts the merged stream. | -| `settings.json`, `schema/*.json` | `pm-json` | Recursively merges objects per key; disjoint settings changes compose without a whole-file conflict. | +| Artifact | Driver | Merge behavior | +| ---------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Item `.toon` / `.md` | `pm-item-toon` / `pm-item-markdown` | Three-way field merge; format-specific drivers avoid passing the repository path through Git's shell command, append-like collections use set union, `updated_at` uses latest timestamp, and canonical serialization recomputes TOON counts. | +| `history/*.jsonl` | `pm-history` | Preserves the common prefix and both divergent suffixes, orders deterministically, then re-anchors the resulting hash chain. | +| tracker `**/*.jsonl` except the later `history/*.jsonl` override | `pm-relationship` | Covers default and package-owned custom relationship event paths, unions divergent suffixes by `eventId` (timestamp-ordered, ours-first on ties), and renumbers `sequence` consecutively so the strict-sequence store loader accepts the merged stream. | +| `settings.json`, `schema/*.json` | `pm-json` | Recursively merges objects per key; disjoint settings changes compose without a whole-file conflict. | When both sides change the same scalar or JSON leaf differently, the driver writes a parseable preferred-side result but exits nonzero. Git keeps the path conflicted so a human or coordinating agent must review the losing value and explicitly `git add` the resolution. Use `--prefer theirs` only when that is the intended resolution policy. @@ -78,7 +78,7 @@ pm history-repair pm merge reconcile --dry-run --json ``` -`history-repair` records the reconciliation patch and surfaces discarded event authors/operations when the on-disk document would otherwise revert effective merged history. Re-apply any intended losing mutation as a normal `pm update` so it remains explicit and auditable. +`history-repair` records the reconciliation patch and classifies its changed fields against the final item. Append-only collection unions and deterministic reordering are reported as preserved context without a data-loss warning. Fields whose replayed values are actually removed or replaced remain loud with discarded event authors/operations and recovery guidance. Re-apply any intended losing mutation as a normal `pm update` so it remains explicit and auditable. ## Delete versus modify policy diff --git a/docs/SDK.md b/docs/SDK.md index c2e1b3e17..017f31188 100644 --- a/docs/SDK.md +++ b/docs/SDK.md @@ -163,7 +163,7 @@ Command/action contract exports: - `PmClient` / `runAction` (high-level in-process action execution for custom tools, bots, CI, and embedded runtimes) - Typed read primitives on `PmClient`: `get` (including `GetOptions.at` point-in-time reads), `list`, `search`, `context`, `next`, `aggregate`, and `stats`; direct `getItemAt` reconstructs a canonical historical document without mutation -- Read primitive option/result contracts: `GetOptions` / `GetResult`, `ListOptions` / `ListResult`, `SearchOptions` / `SearchResult`, `ContextOptions` / `ContextResult`, `NextOptions` / `NextResult`, `AggregateOptions` / `AggregateResult`, `StatsCommandOptions` / `StatsResult` +- Read primitive option/result contracts: `GetOptions` / `GetResult`, `ListOptions` / `ListResult`, `SearchOptions` / `SearchResult`, `ContextOptions` / `ContextResult`, `NextOptions` / `NextResult`, `AggregateOptions` / `AggregateResult`, `StatsCommandOptions` / `StatsResult`. Standard and brief `get` projections omit note bodies but expose `item.notes_count`; deep/full reads return the notes themselves, and narrow consumers can request `notes_count` explicitly. - Context relevance primitives: `buildItemContextRelevanceCandidates`, `buildContextSignalSnapshot`, `ContextSignalStore`, `JsonFileContextSignalStoreAdapter`, `parseContextSignalSnapshot`, `defaultScoreContextCandidates`, `scoreContextCandidates`, `scoreContextCandidatesWithActiveExtensions`, `evaluateContextRanking`, `runContextEvaluationScenario`, `runContextEvaluationCorpus`, and `summarizeContextEvaluationReports` - Context relevance contracts: `ContextRelevanceCandidate`, `ContextRelevanceSignals`, `ContextSignalSnapshot`, `ContextSignalStoreAdapter`, `ContextSignalStoreReadResult`, `ContextRelevanceScorer`, `ContextRelevanceReport`, `ContextEvaluationReader`, `ContextEvaluationScenario`, `ContextEvaluationScenarioReport`, `ContextEvaluationThresholds`, and `ContextEvaluationCorpusReport` - Context packing and feedback primitives: `packContextCandidates`, `recordContextUsageServing`, `recordContextUsageTouch`, `recordContextUsageTouches`, and `readContextUsageAffinity` @@ -182,7 +182,7 @@ Command/action contract exports: - Linked-resource kernel primitives: `runFiles`, `runFilesDiscover`, `runDocs`, `runDeps`, `runLinkedArtifacts`, parsing/normalization/path-validation helpers, and their typed contracts. The CLI files/docs/deps modules are presentation-only re-exports of these SDK implementations. - Actionability primitives: `collectBlockedByIds`, `resolveItemBlockers`, `collectDependencyBlockedIds`, and `computeActionabilityReport` expose the same edge-aware blocked/ready definition used by `pm next`, `pm context`, and `pm list-blocked`. Embedded schedulers can therefore classify custom lifecycle schemas without importing CLI or core modules. - Dependency-governance primitives: `collectDanglingDependencyReferences`, `collectMissingDependencyTargetIds`, and `assembleWorkspaceRelationshipGraph` normalize hierarchy, scalar blockers, and structured dependencies into one graph while partitioning missing targets into actionable active holders, informational terminal-history holders, and the legacy `no-active-blocker` sentinel without mutating stored history. -- Relationship graph primitives: `RelationshipKindRegistry`, `createRelationshipKindRegistry`, `RelationshipGraph`, `RelationshipEventLog`, `RelationshipEventStore`, `planRelationshipEventBackfill`, `buildRelationshipContext`, `buildDepsRelationshipContext`, `hierarchyAncestors`, `hierarchyDescendants`, `orderingPredecessors`, `orderingSuccessors`, `enumerateRelationshipPaths`, `auditWorkspaceRelationshipGraph`, `isOrderingRelationshipKind`, and `dependencyToRelationship` provide application-defined edge semantics, durable replay, deterministic legacy migration, bounded semantic traversal, policy-aware governance, and explainable context queries. `RelationshipEventLog.stream/project` and their durable-store equivalents page immutable prefixes and fold them into deterministic application state with exact version, processed-count, and as-of metadata. `RelationshipEventStore.appendBatch` validates a complete import under one cross-process lock and atomically publishes it; `skip_identical` resume mode rejects same-id semantic collisions. `RelationshipGraphAdapter`, `createRelationshipGraphSnapshot`, `syncRelationshipGraphAdapter`, `loadRelationshipGraphAdapter`, and `federateRelationshipGraphSnapshots` form the backend-neutral content-addressed projection boundary for database or remote graph packages. `MemoryRelationshipGraphAdapter`, `assertRelationshipGraphAdapterConformance`, and `createRelationshipGraphScaleFixture` give package authors a reference implementation, reusable compatibility contract, and lazy deterministic fixtures through one million nodes. See [Relationship graph semantics](RELATIONSHIP_GRAPH.md). +- Relationship graph primitives: `RelationshipKindRegistry`, `createRelationshipKindRegistry`, `assertRelationshipEdgeAllowed`, `RelationshipGraph`, `RelationshipEventLog`, `RelationshipEventStore`, `planRelationshipEventBackfill`, `buildRelationshipContext`, `buildDepsRelationshipContext`, `hierarchyAncestors`, `hierarchyDescendants`, `orderingPredecessors`, `orderingSuccessors`, `enumerateRelationshipPaths`, `auditWorkspaceRelationshipGraph`, `isOrderingRelationshipKind`, and `dependencyToRelationship` provide application-defined edge semantics, durable replay, deterministic legacy migration, bounded semantic traversal, policy-aware governance, and explainable context queries. Mutation adapters should call `assertRelationshipEdgeAllowed` with the active registry before persistence; it resolves aliases and honors custom `allowSelf` definitions while built-in self edges fail before item or history writes. `RelationshipEventLog.stream/project` and their durable-store equivalents page immutable prefixes and fold them into deterministic application state with exact version, processed-count, and as-of metadata. `RelationshipEventStore.appendBatch` validates a complete import under one cross-process lock and atomically publishes it; `skip_identical` resume mode rejects same-id semantic collisions. `RelationshipGraphAdapter`, `createRelationshipGraphSnapshot`, `syncRelationshipGraphAdapter`, `loadRelationshipGraphAdapter`, and `federateRelationshipGraphSnapshots` form the backend-neutral content-addressed projection boundary for database or remote graph packages. `MemoryRelationshipGraphAdapter`, `assertRelationshipGraphAdapterConformance`, and `createRelationshipGraphScaleFixture` give package authors a reference implementation, reusable compatibility contract, and lazy deterministic fixtures through one million nodes. See [Relationship graph semantics](RELATIONSHIP_GRAPH.md). - Atomic application transactions: `commitWorkspaceTransaction` coordinates ordered, idempotent item and relationship mutations under one workspace writer lock and a durable replay journal. Interrupted work resumes from step inspection; ordinary failures append reverse-order compensations without rewriting immutable histories. - Multi-branch merge primitives: `mergeItemDocuments`, `mergeHistoryStreams`, `mergeRelationshipEventStreams`, `mergeJsonDocuments`, `runMergeDriver`, `runMergeInstall`, and `runMergeReconcile` provide the same field-aware item, hash-chain-preserving history, sequence-renumbering relationship-event, key-level configuration, and audited post-merge repair-and-verify semantics as `pm merge`; `buildMergeAttributePatterns`, `refreshMergeAttributeFenceIfInstalled`, and `auditMergeAttributeFence` expose the fence coverage contract, the post-schema-mutation refresh, and the validate-side drift audit. See [Multi-Branch Merge Safety](MERGE_SAFETY.md). - Dependency provenance primitives: `EXTERNAL_DEPENDENCY_SOURCE_KIND`, `isExternalDependencySourceKind`, and `normalizeDependencySeedId` let custom importers preserve cross-workspace dependency ids explicitly while retaining local prefix normalization for ordinary seeds. @@ -329,6 +329,10 @@ the latest available state. Restore also retains command-specific diagnostics. Generic reads also expose two composable SDK facets: +- `GetResult.item.notes_count` makes omitted note context explicit at brief and + standard depth without paying for note bodies. Deep/full reads expose + `item.notes` and omit the redundant count; `GetOptions.fields` accepts + `notes_count` for narrow agent reads. - `buildItemSchedule` normalizes deadlines, reminders, and events into a stable schedule projection with convenient earliest-event `start_at`, `end_at`, and `location` aliases. `pm get` includes it at standard/deep depth and supports @@ -344,6 +348,14 @@ The CLI automatically computes that workspace projection for container-oriented built-ins and custom types. Built-in leaf reads stay constant-cost unless the caller explicitly projects `children`; SDK consumers remain fully type-agnostic. +History reconciliation results distinguish changed fields whose replayed value +is still structurally present from fields that were genuinely overwritten. +`HistoryRepairReconciliationReport` exposes `reconciled_*`, `preserved_*`, and +legacy-compatible `reverted_*`/`discarded_*` partitions. Append-only collection +unions and pure reordering are preserved context, so they do not emit discard +warnings or recovery guidance; actual scalar replacement and removal remain +loud with attributed events and authors. + ### Linked resources and dependency governance Tracked: [pm-jcvg](../.agents/pm/tasks/pm-jcvg.toon), diff --git a/src/cli/commands/create.ts b/src/cli/commands/create.ts index 5bac93d21..17f7022d0 100644 --- a/src/cli/commands/create.ts +++ b/src/cli/commands/create.ts @@ -122,7 +122,10 @@ import type { SharedLinkedResourceOptions, } from "./mutation-command-options.js"; import { ensureEnumValue } from "./recurrence-parsers.js"; -import { assertValidBareDependencyFlagValue } from "../../sdk/dependency-flag-validation.js"; +import { + assertDependencyEdgesAllowed, + assertValidBareDependencyFlagValue, +} from "../../sdk/dependency-flag-validation.js"; import { normalizeDependencySeedId, normalizeDependencySourceKind, @@ -2799,6 +2802,7 @@ export async function runCreate( author, }); const dependencyValues = blockedByResolution.dependencyValues; + assertDependencyEdgesAllowed(id, dependencyValues); status = blockedByResolution.status; const blockedReason = resolveUnsettableOptionalString( unsetKeys, diff --git a/src/cli/commands/get.ts b/src/cli/commands/get.ts index 777b8661c..af80e30c1 100644 --- a/src/cli/commands/get.ts +++ b/src/cli/commands/get.ts @@ -64,13 +64,19 @@ interface ClaimStateContext { last_release: ClaimHistoryContext | null; } +type GetItemProjection = Partial & { + body?: string; + /** Number of notes omitted by a token-bounded projection. */ + notes_count?: number; +}; + /** Documents the get result payload exchanged by command, SDK, and package integrations. */ export interface GetResult { // `body` lives inside `item` (alongside `description`/`acceptance_criteria`) // for parity with `pm list --include-body`, so agents reliably find it at // `.item.body` in JSON output instead of a top-level sibling. /** Value that configures or reports item for this contract. */ - item: Partial & { body?: string }; + item: GetItemProjection; /** Value that configures or reports linked for this contract. */ linked?: { files: LinkedFile[]; @@ -187,7 +193,7 @@ function parseGetDepth(raw: string | undefined): GetDepth { function projectItemForDepth( item: ItemMetadata, depth: GetDepth, -): Partial { +): GetItemProjection { if (depth === "deep") { return item; } @@ -202,7 +208,10 @@ function projectItemForDepth( events: _events, ...projected } = item; - return projected; + return { + ...projected, + notes_count: item.notes?.length ?? 0, + }; } function parseGetFields(raw: string | undefined): string[] | null { @@ -236,6 +245,7 @@ function validateGetFields( const itemFields = new Set([ ...ITEM_METADATA_KEY_ORDER, ...runtimeMetadataKeys, + "notes_count", ]); const allowedRootFields = new Set([ "body", @@ -292,7 +302,7 @@ function validateGetFields( function projectItemForFields( item: ItemMetadata, fields: string[], -): Partial { +): GetItemProjection { const source = toItemRecord(item); const projected: Record = {}; for (const field of fields) { @@ -310,9 +320,12 @@ function projectItemForFields( ) { continue; } - projected[normalized] = source[normalized]; + projected[normalized] = + normalized === "notes_count" + ? (item.notes?.length ?? 0) + : source[normalized]; } - return projected as Partial; + return projected as GetItemProjection; } function fieldsInclude(fields: string[] | null, name: string): boolean { @@ -550,9 +563,7 @@ function attachGetSchedule( } result.schedule = Object.fromEntries( Object.entries(schedule).filter(([key]) => - fields.some( - (field) => normalizeGetField(field) === `schedule.${key}`, - ), + fields.some((field) => normalizeGetField(field) === `schedule.${key}`), ), ) as Partial; } diff --git a/src/cli/commands/update.ts b/src/cli/commands/update.ts index 7091c6597..743612f17 100644 --- a/src/cli/commands/update.ts +++ b/src/cli/commands/update.ts @@ -81,7 +81,10 @@ import { parseReminderEntries, parseTypeOptionEntries, } from "./repeatable-metadata-parsers.js"; -import { assertValidBareDependencyFlagValue } from "../../sdk/dependency-flag-validation.js"; +import { + assertDependencyEdgesAllowed, + assertValidBareDependencyFlagValue, +} from "../../sdk/dependency-flag-validation.js"; import { normalizeDependencySeedId, normalizeDependencySourceKind, @@ -337,10 +340,7 @@ const OWNERSHIP_BYPASS_RESTRICTED_FLAG_REPLACEMENTS: ReadonlyMap< string, (id: string) => string > = new Map([ - [ - "--comment", - (id: string) => `pm comments ${id} --add "" --force`, - ], + ["--comment", (id: string) => `pm comments ${id} --add "" --force`], [ "--file", (id: string) => @@ -2381,10 +2381,7 @@ function applyAcceptanceCriteriaAdditiveMutation( if (document.metadata.acceptance_criteria === nextValue) { return; } - if ( - nextValue === "" && - document.metadata.acceptance_criteria === undefined - ) { + if (nextValue === "" && document.metadata.acceptance_criteria === undefined) { return; } document.metadata.acceptance_criteria = nextValue; @@ -2721,6 +2718,18 @@ function mutateUpdateDocument( scalarMutationContext, warnings, ); + if ( + context.options.dep !== undefined || + context.options.depRemove !== undefined || + context.options.blockedBy !== undefined || + context.clearItemMetadataKeys.has("dependencies") || + context.clearItemMetadataKeys.has("blocked_by") + ) { + assertDependencyEdgesAllowed( + document.metadata.id, + document.metadata.dependencies, + ); + } if ( normalizeStatusInput(document.metadata.status, context.statusRegistry) === context.statusRegistry.canceled_status && @@ -2927,14 +2936,16 @@ export async function runUpdate( typeToFolder: typeRegistry.type_to_folder, id, op: - options.ownershipMetadataBypass === true || options.ownershipDependencyBypass === true + options.ownershipMetadataBypass === true || + options.ownershipDependencyBypass === true ? "update_ownership_bypass" : "update", author, message: options.message, force: options.force, bypassAssigneeConflict: - options.ownershipMetadataBypass === true || options.ownershipDependencyBypass === true, + options.ownershipMetadataBypass === true || + options.ownershipDependencyBypass === true, extensionFieldNames, skipNoop: true, mutate(document) { diff --git a/src/sdk/dependency-flag-validation.ts b/src/sdk/dependency-flag-validation.ts index 451262444..d1edc5b0b 100644 --- a/src/sdk/dependency-flag-validation.ts +++ b/src/sdk/dependency-flag-validation.ts @@ -1,6 +1,13 @@ /** @module cli/commands/dependency-flag-validation */ import { EXIT_CODE } from "../core/shared/constants.js"; import { PmCliError } from "../core/shared/errors.js"; +import { toErrorMessage } from "../core/shared/primitives.js"; +import type { Dependency } from "../types/index.js"; +import { resolveWorkspaceRelationshipKindRegistry } from "./graph/assembly.js"; +import { + assertRelationshipEdgeAllowed, + type RelationshipKindRegistry, +} from "./relationships.js"; /** Reject malformed shorthand before item-id prefix normalization can create a dangling graph node. */ export function assertValidBareDependencyFlagValue( @@ -14,3 +21,29 @@ export function assertValidBareDependencyFlagValue( ); } } + +/** + * Validate prospective stored dependencies against the active relationship + * ontology before a create or update mutation writes item or history state. + */ +export function assertDependencyEdgesAllowed( + holderId: string, + dependencies: readonly Dependency[] | undefined, + registry: RelationshipKindRegistry = resolveWorkspaceRelationshipKindRegistry(), +): void { + for (const dependency of dependencies ?? []) { + try { + assertRelationshipEdgeAllowed( + holderId, + dependency.id, + dependency.kind, + registry, + ); + } catch (error: unknown) { + throw new PmCliError(toErrorMessage(error), EXIT_CODE.USAGE, { + code: "dependency_edge_not_allowed", + why: `${holderId} -> ${dependency.id} (${dependency.kind}) violates the active relationship-kind policy`, + }); + } + } +} diff --git a/src/sdk/history-repair.ts b/src/sdk/history-repair.ts index bf7f69c29..e5be6b3b7 100644 --- a/src/sdk/history-repair.ts +++ b/src/sdk/history-repair.ts @@ -91,22 +91,26 @@ export interface HistoryRepairResult { generated_at: string; } -/** Documents one history event whose replayed effect a repair reconciliation overwrites. */ +/** Documents one history event attributed to fields changed by reconciliation. */ export interface HistoryRepairDiscardedEvent { /** 1-based index of the event in the (re-anchored) history stream. */ index: number; - /** Timestamp of the discarded event. */ + /** Timestamp of the attributed event. */ ts: string; - /** Author whose mutation the reconciliation overwrites. */ + /** Author whose mutation is being classified. */ author: string; /** Operation label of the discarded event. */ op: string; - /** Reverted fields this event last wrote. */ + /** Reconciled fields this event last wrote. */ fields: string[]; } /** Documents the repair reconciliation data-loss report exchanged by command, SDK, and package integrations. */ export interface HistoryRepairReconciliationReport { + /** Total number of fields changed by the reconciliation patch. */ + reconciled_field_count: number; + /** Every field changed by the reconciliation patch. */ + reconciled_fields: string[]; /** Number of fields the reconciliation patch reverts relative to the replayed chain. */ reverted_field_count: number; /** Fields whose replayed values the reconciliation overwrites with the on-disk item's values. */ @@ -115,30 +119,84 @@ export interface HistoryRepairReconciliationReport { discarded_events: HistoryRepairDiscardedEvent[]; /** Distinct authors whose mutations the reconciliation discards. */ discarded_authors: string[]; + /** Number of changed fields whose replayed value remains present in the on-disk item. */ + preserved_field_count: number; + /** Changed fields whose replayed value remains present after reconciliation. */ + preserved_fields: string[]; + /** Newest history events whose effects remain preserved by reconciliation. */ + preserved_events: HistoryRepairDiscardedEvent[]; + /** Distinct authors whose reconciled mutations remain preserved. */ + preserved_authors: string[]; /** How to recover a discarded mutation instead of accepting the revert. */ - recovery_hint: string; + recovery_hint: string | null; } -/** Compute which fields a chain-vs-item reconciliation reverts and which history events (and authors) last wrote them. The reconciliation patch rewrites the replayed final state into the on-disk item state, so any field it touches is a replayed mutation being overwritten — after a lossy merge resolution this is exactly the other branch's discarded work (GH-603). */ -export function analyzeReconciliationDiscard( - entries: HistoryEntry[], - finalReplay: ReplayDocument, - currentItemReplay: ReplayDocument, -): HistoryRepairReconciliationReport | undefined { - const reconcilePatch = jsonPatch.compare( - finalReplay, - currentItemReplay, - ) as HistoryPatchOp[]; - if (reconcilePatch.length === 0) { - return undefined; +function replayFieldValue(document: ReplayDocument, field: string): unknown { + return field === "body" + ? document.body + : (document.metadata as unknown as Record)[field]; +} + +/** + * Report whether the replayed value survives inside the reconciled value. + * + * Arrays use multiset containment so append-only unions and deterministic + * reordering remain lossless, while removals and scalar replacements stay loud. + */ +function isReplayValuePreserved( + replayed: unknown, + reconciled: unknown, +): boolean { + if (replayed === undefined || Object.is(replayed, reconciled)) { + return true; } - const revertedFields = new Set(); - for (const op of reconcilePatch) { - revertedFields.add(patchPathToChangedField(op.path)); + if (Array.isArray(replayed)) { + if (!Array.isArray(reconciled)) { + return false; + } + const matched = new Set(); + return replayed.every((expected) => { + const index = reconciled.findIndex( + (candidate, candidateIndex) => + !matched.has(candidateIndex) && + isReplayValuePreserved(expected, candidate), + ); + if (index < 0) { + return false; + } + matched.add(index); + return true; + }); + } + if ( + replayed === null || + reconciled === null || + typeof replayed !== "object" || + typeof reconciled !== "object" || + Array.isArray(reconciled) + ) { + return false; } - const remaining = new Set(revertedFields); - const discardedEvents: HistoryRepairDiscardedEvent[] = []; - for (let index = entries.length - 1; index >= 0 && remaining.size > 0; index -= 1) { + const reconciledRecord = reconciled as Record; + return Object.entries(replayed as Record).every( + ([key, value]) => + Object.hasOwn(reconciledRecord, key) && + isReplayValuePreserved(value, reconciledRecord[key]), + ); +} + +/** Attribute reconciled fields to the newest history event that wrote each one. */ +function collectReconciliationEvents( + entries: HistoryEntry[], + fields: ReadonlySet, +): HistoryRepairDiscardedEvent[] { + const remaining = new Set(fields); + const events: HistoryRepairDiscardedEvent[] = []; + for ( + let index = entries.length - 1; + index >= 0 && remaining.size > 0; + index -= 1 + ) { const entry = entries[index]; const entryFields = new Set(); for (const op of normalizeReplayPatchOps(entry.patch)) { @@ -154,7 +212,7 @@ export function analyzeReconciliationDiscard( for (const field of overlap) { remaining.delete(field); } - discardedEvents.push({ + events.push({ index: index + 1, ts: entry.ts, author: entry.author, @@ -162,18 +220,69 @@ export function analyzeReconciliationDiscard( fields: overlap.sort((left, right) => left.localeCompare(right)), }); } - discardedEvents.reverse(); + return events.reverse(); +} + +/** Compute which chain-vs-item differences preserve replayed context and which genuinely discard it. */ +export function analyzeReconciliationDiscard( + entries: HistoryEntry[], + finalReplay: ReplayDocument, + currentItemReplay: ReplayDocument, +): HistoryRepairReconciliationReport | undefined { + const reconcilePatch = jsonPatch.compare( + finalReplay, + currentItemReplay, + ) as HistoryPatchOp[]; + if (reconcilePatch.length === 0) { + return undefined; + } + const reconciledFields = new Set(); + for (const op of reconcilePatch) { + reconciledFields.add(patchPathToChangedField(op.path)); + } + const preservedFields = new Set( + [...reconciledFields].filter((field) => + isReplayValuePreserved( + replayFieldValue(finalReplay, field), + replayFieldValue(currentItemReplay, field), + ), + ), + ); + const revertedFields = new Set( + [...reconciledFields].filter((field) => !preservedFields.has(field)), + ); + const discardedEvents = collectReconciliationEvents(entries, revertedFields); + const preservedEvents = collectReconciliationEvents(entries, preservedFields); + const sortedReconciledFields = [...reconciledFields].sort((left, right) => + left.localeCompare(right), + ); + const sortedRevertedFields = [...revertedFields].sort((left, right) => + left.localeCompare(right), + ); + const sortedPreservedFields = [...preservedFields].sort((left, right) => + left.localeCompare(right), + ); + const discardedAuthors = [ + ...new Set(discardedEvents.map((event) => event.author)), + ].sort((left, right) => left.localeCompare(right)); + const preservedAuthors = [ + ...new Set(preservedEvents.map((event) => event.author)), + ].sort((left, right) => left.localeCompare(right)); return { + reconciled_field_count: reconciledFields.size, + reconciled_fields: sortedReconciledFields, reverted_field_count: revertedFields.size, - reverted_fields: [...revertedFields].sort((left, right) => - left.localeCompare(right), - ), + reverted_fields: sortedRevertedFields, discarded_events: discardedEvents, - discarded_authors: [ - ...new Set(discardedEvents.map((event) => event.author)), - ].sort((left, right) => left.localeCompare(right)), + discarded_authors: discardedAuthors, + preserved_field_count: preservedFields.size, + preserved_fields: sortedPreservedFields, + preserved_events: preservedEvents, + preserved_authors: preservedAuthors, recovery_hint: - "Reconciliation keeps the on-disk item and overwrites these replayed mutations. To recover one, inspect it with pm history --diff --field , then re-apply the lost value with pm update.", + discardedEvents.length > 0 + ? "Reconciliation keeps the on-disk item and overwrites these replayed mutations. To recover one, inspect it with pm history --diff --field , then re-apply the lost value with pm update." + : null, }; } @@ -432,7 +541,7 @@ export async function runHistoryRepair( } const warnings = collectHistoryRepairWarnings(changed, reanchor.skippedOps); - if (reconciliation) { + if (reconciliation && reconciliation.discarded_events.length > 0) { warnings.push( `history_repair_reconcile_discards_events:${reconciliation.discarded_events.length}`, ); @@ -620,8 +729,8 @@ export async function runHistoryRepairAll( drifted_streams: drift.driftedItems.length, streams, totals, - warnings: [...new Set([...itemReadWarnings, ...repairWarnings])].sort((left, right) => - left.localeCompare(right), + warnings: [...new Set([...itemReadWarnings, ...repairWarnings])].sort( + (left, right) => left.localeCompare(right), ), generated_at: nowIso(), }; diff --git a/src/sdk/relationships.ts b/src/sdk/relationships.ts index 62bedd76a..3b3515949 100644 --- a/src/sdk/relationships.ts +++ b/src/sdk/relationships.ts @@ -458,6 +458,34 @@ export function isOrderingRelationshipKind( return registry.resolve(kind)?.ordering === true; } +/** + * Validate one prospective edge against the resolved relationship ontology. + * + * Mutation adapters use this before persistence so aliases, extension-defined + * kinds, and self-edge policy share the same semantics as graph construction. + * The canonical definition is returned for callers that also need normalized + * kind metadata. + */ +export function assertRelationshipEdgeAllowed( + source: string, + target: string, + kind: string, + registry: RelationshipKindRegistry = defaultRegistry, +): RelationshipKindDefinition { + const normalizedSource = normalizeNodeId(source); + const normalizedTarget = normalizeNodeId(target); + if (!normalizedSource || !normalizedTarget) { + throw new TypeError("Relationship endpoint must not be empty"); + } + const definition = registry.require(kind); + if (normalizedSource === normalizedTarget && !definition.allowSelf) { + throw new TypeError( + `Self relationship is not allowed for ${definition.kind}`, + ); + } + return definition; +} + function compareEdges(left: RelationshipEdge, right: RelationshipEdge): number { return ( left.source.localeCompare(right.source) || @@ -475,15 +503,16 @@ function normalizeRelationshipEdge( typeof candidate.source === "string" ? candidate.source.trim() : ""; const target = typeof candidate.target === "string" ? candidate.target.trim() : ""; - const definition = registry.require(candidate.kind); if (!nodes.has(source) || !nodes.has(target)) throw new TypeError( `Relationship endpoint not found: ${source} -> ${target}`, ); - if (source === target && !definition.allowSelf) - throw new TypeError( - `Self relationship is not allowed for ${definition.kind}`, - ); + const definition = assertRelationshipEdgeAllowed( + source, + target, + candidate.kind, + registry, + ); const edge = Object.freeze({ ...candidate, source, diff --git a/tests/unit/commands/agent-ux-feedback-wave.spec.ts b/tests/unit/commands/agent-ux-feedback-wave.spec.ts index 4ffb4034d..d41087691 100644 --- a/tests/unit/commands/agent-ux-feedback-wave.spec.ts +++ b/tests/unit/commands/agent-ux-feedback-wave.spec.ts @@ -84,12 +84,13 @@ describe("agent UX feedback wave", () => { expect((listAfter.json as { count: number }).count).toBe(0); } - const next = context.runCli( - ["next", "--assignee", "bob", "--json"], - { expectJson: true }, - ); + const next = context.runCli(["next", "--assignee", "bob", "--json"], { + expectJson: true, + }); expect(next.code).toBe(0); - expect((next.json as { recommended: { id: string } }).recommended.id).toBe(itemId); + expect( + (next.json as { recommended: { id: string } }).recommended.id, + ).toBe(itemId); }); }); @@ -178,19 +179,15 @@ describe("agent UX feedback wave", () => { `id=${first},kind=blocked_by`, ]); const update = context.runCli( - [ - "update", - first, - "--dep", - `id=${second},kind=blocked_by`, - "--json", - ], + ["update", first, "--dep", `id=${second},kind=blocked_by`, "--json"], { expectJson: true }, ); expect(update.code).toBe(0); expect((update.json as { warnings: string[] }).warnings).toEqual( expect.arrayContaining([ - expect.stringMatching(/^ordering_cycle_created:.*run_pm_graph_audit$/), + expect.stringMatching( + /^ordering_cycle_created:.*run_pm_graph_audit$/, + ), ]), ); @@ -208,6 +205,63 @@ describe("agent UX feedback wave", () => { }); }); + it("rejects self dependencies before create or update writes any state", async () => { + await withTempPmPath(async (context) => { + const explicitId = "pm-self-create"; + const rejectedCreate = context.runCli([ + "create", + "--id", + explicitId, + "--title", + "Rejected self create", + "--description", + "Must never reach storage", + "--type", + "Task", + "--status", + "open", + "--priority", + "1", + "--dep", + `id=${explicitId},kind=depends-on`, + "--json", + ]); + expect(rejectedCreate.code).toBe(EXIT_CODE.USAGE); + expect(rejectedCreate.stderr).toContain( + "Self relationship is not allowed for blocked_by", + ); + expect(context.runCli(["get", explicitId, "--json"]).code).toBe( + EXIT_CODE.NOT_FOUND, + ); + + const itemId = createTask(context, "Rejected self update"); + const historyBefore = context.runCli( + ["history", itemId, "--full", "--json"], + { expectJson: true }, + ); + const rejectedUpdate = context.runCli([ + "update", + itemId, + "--dep", + `id=${itemId},kind=related`, + "--json", + ]); + expect(rejectedUpdate.code).toBe(EXIT_CODE.USAGE); + expect(rejectedUpdate.stderr).toContain( + "Self relationship is not allowed for related", + ); + const historyAfter = context.runCli( + ["history", itemId, "--full", "--json"], + { expectJson: true }, + ); + expect( + (historyAfter.json as { history: unknown[] }).history, + ).toHaveLength( + (historyBefore.json as { history: unknown[] }).history.length, + ); + }); + }); + it("uses agenda references for listed focus items", async () => { await withTempPmPath(async (context) => { const itemId = createTask(context, "Agenda reference", [ @@ -231,7 +285,9 @@ describe("agent UX feedback wave", () => { it("points unconfigured invocations at a nearby custom tracker", async () => { await withTempPmPath(async (context) => { - const workspace = await mkdtemp(path.join(os.tmpdir(), "pm-nearby-root-")); + const workspace = await mkdtemp( + path.join(os.tmpdir(), "pm-nearby-root-"), + ); try { const customRoot = path.join(workspace, ".pm"); const secondCustomRoot = path.join(workspace, ".pm-two"); @@ -243,8 +299,12 @@ describe("agent UX feedback wave", () => { await cp(context.pmPath, secondCustomRoot, { recursive: true }); await cp(context.pmPath, ignoredDependencyRoot, { recursive: true }); expect(discoverNearbyPmRoot(workspace)).toBe(customRoot); - expect(discoverNearbyPmRoot(workspace, customRoot)).toBe(secondCustomRoot); - expect(discoverNearbyPmRoot(path.join(workspace, "missing"))).toBeUndefined(); + expect(discoverNearbyPmRoot(workspace, customRoot)).toBe( + secondCustomRoot, + ); + expect( + discoverNearbyPmRoot(path.join(workspace, "missing")), + ).toBeUndefined(); const previousCwd = process.cwd(); process.chdir(workspace); try { diff --git a/tests/unit/commands/get-append-command.spec.ts b/tests/unit/commands/get-append-command.spec.ts index 1a0b1eb36..08488b76d 100644 --- a/tests/unit/commands/get-append-command.spec.ts +++ b/tests/unit/commands/get-append-command.spec.ts @@ -10,8 +10,14 @@ import { EXIT_CODE } from "../../../src/core/shared/constants.js"; import { PmCliError } from "../../../src/core/shared/errors.js"; import { locateItem } from "../../../src/core/store/item-store.js"; import * as itemStoreModule from "../../../src/core/store/item-store.js"; -import { readSettings, writeSettings } from "../../../src/core/store/settings.js"; -import { withTempPmPath, type TempPmContext } from "../../helpers/withTempPmPath.js"; +import { + readSettings, + writeSettings, +} from "../../../src/core/store/settings.js"; +import { + withTempPmPath, + type TempPmContext, +} from "../../helpers/withTempPmPath.js"; afterEach(() => { vi.restoreAllMocks(); @@ -79,9 +85,7 @@ function createTask( "none", ...linkArgs, ...(params.event === undefined ? [] : ["--event", params.event]), - ...(params.reminder === undefined - ? [] - : ["--reminder", params.reminder]), + ...(params.reminder === undefined ? [] : ["--reminder", params.reminder]), ]; const created = context.runCli(args, { expectJson: true }); @@ -103,7 +107,10 @@ describe("runGet and runAppend", () => { try { await runGet(id, { path: context.pmPath }); const settings = await readSettings(context.pmPath); - await writeSettings(context.pmPath, { ...settings, author_default: "" }); + await writeSettings(context.pmPath, { + ...settings, + author_default: "", + }); await runGet(id, { path: context.pmPath }); } finally { if (previousAuthor === undefined) { @@ -116,12 +123,18 @@ describe("runGet and runAppend", () => { }); it("fails when tracker is not initialized", async () => { - const tempDir = await mkdtemp(path.join(os.tmpdir(), "pm-get-append-not-init-")); + const tempDir = await mkdtemp( + path.join(os.tmpdir(), "pm-get-append-not-init-"), + ); try { - await expect(runGet("pm-missing", { path: tempDir })).rejects.toMatchObject({ + await expect( + runGet("pm-missing", { path: tempDir }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.NOT_FOUND, }); - await expect(runAppend("pm-missing", { body: "append text" }, { path: tempDir })).rejects.toMatchObject({ + await expect( + runAppend("pm-missing", { body: "append text" }, { path: tempDir }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.NOT_FOUND, }); } finally { @@ -142,7 +155,9 @@ describe("runGet and runAppend", () => { expect(linkedResult.linked.files).toEqual([ { path: "src/cli/commands/get.ts", scope: "project", note: "get-link" }, ]); - expect(linkedResult.linked.docs).toEqual([{ path: "README.md", scope: "project", note: "doc-link" }]); + expect(linkedResult.linked.docs).toEqual([ + { path: "README.md", scope: "project", note: "doc-link" }, + ]); expect(linkedResult.linked.tests).toEqual([ { command: "node --version", @@ -171,49 +186,86 @@ describe("runGet and runAppend", () => { includeLinks: true, }); - context.runCli(["comments", id, "depth comment", "--json", "--author", "owner-a"], { expectJson: true }); - context.runCli(["notes", id, "--add", "depth note", "--json", "--author", "owner-a"], { expectJson: true }); + context.runCli( + ["comments", id, "depth comment", "--json", "--author", "owner-a"], + { expectJson: true }, + ); + context.runCli( + ["notes", id, "--add", "depth note", "--json", "--author", "owner-a"], + { expectJson: true }, + ); const defaultRead = await runGet(id, { path: context.pmPath }); expect(defaultRead.item.comments).toBeUndefined(); expect(defaultRead.item.notes).toBeUndefined(); + expect(defaultRead.item.notes_count).toBe(1); expect(defaultRead.linked.files).toHaveLength(1); expect(defaultRead.item.body).toBe("depth body"); - const explicitFull = await runGet(id, { path: context.pmPath }, { full: true }); + const explicitFull = await runGet( + id, + { path: context.pmPath }, + { full: true }, + ); expect(explicitFull.item.comments).toBeDefined(); expect(explicitFull.item.notes).toBeDefined(); + expect(explicitFull.item.notes_count).toBeUndefined(); expect(explicitFull.linked.files).toHaveLength(1); expect(explicitFull.item.body).toBe("depth body"); - const depthFullAlias = await runGet(id, { path: context.pmPath }, { depth: "full" }); + const depthFullAlias = await runGet( + id, + { path: context.pmPath }, + { depth: "full" }, + ); expect(depthFullAlias.item.comments).toBeDefined(); expect(depthFullAlias.item.notes).toBeDefined(); + expect(depthFullAlias.item.notes_count).toBeUndefined(); expect(depthFullAlias.linked.files).toHaveLength(1); expect(depthFullAlias.item.body).toBe("depth body"); - const standard = await runGet(id, { path: context.pmPath }, { depth: "standard" }); + const standard = await runGet( + id, + { path: context.pmPath }, + { depth: "standard" }, + ); expect(standard.item.id).toBe(id); expect(standard.item.comments).toBeUndefined(); expect(standard.item.notes).toBeUndefined(); + expect(standard.item.notes_count).toBe(1); expect(standard.item.files).toBeUndefined(); expect(standard.linked.files).toHaveLength(1); expect(standard.item.body).toBe("depth body"); - const brief = await runGet(id, { path: context.pmPath }, { depth: "brief" }); + const brief = await runGet( + id, + { path: context.pmPath }, + { depth: "brief" }, + ); expect(brief.item.id).toBe(id); expect(brief.item.comments).toBeUndefined(); + expect(brief.item.notes_count).toBe(1); expect(brief.linked).toBeUndefined(); expect(brief.item.body).toBeUndefined(); expect(brief.claim_state).toBeUndefined(); - await expect(runGet(id, { path: context.pmPath }, { depth: "verbose" })).rejects.toMatchObject({ + await expect( + runGet(id, { path: context.pmPath }, { depth: "verbose" }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, }); - await expect(runGet(id, { path: context.pmPath }, { full: true, fields: "id,title" })).rejects.toMatchObject({ + await expect( + runGet( + id, + { path: context.pmPath }, + { full: true, fields: "id,title" }, + ), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, }); - await expect(runGet(id, { path: context.pmPath }, { full: true, depth: "brief" })).rejects.toMatchObject({ + await expect( + runGet(id, { path: context.pmPath }, { full: true, depth: "brief" }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, }); }); @@ -227,7 +279,11 @@ describe("runGet and runAppend", () => { includeLinks: true, }); - const focused = await runGet(id, { path: context.pmPath }, { fields: "id,title,status,parent,type" }); + const focused = await runGet( + id, + { path: context.pmPath }, + { fields: "id,title,status,parent,type" }, + ); expect(focused.item).toEqual({ id, title: "get-fields-projection", @@ -239,13 +295,21 @@ describe("runGet and runAppend", () => { expect(focused.linked).toBeUndefined(); expect(focused.claim_state).toBeUndefined(); - const withBodyAndFiles = await runGet(id, { path: context.pmPath }, { fields: "item.id,body,linked.files" }); + const withBodyAndFiles = await runGet( + id, + { path: context.pmPath }, + { fields: "item.id,body,linked.files" }, + ); expect(withBodyAndFiles.item).toEqual({ id, body: "fields body" }); expect(withBodyAndFiles.item.body).toBe("fields body"); expect(withBodyAndFiles.linked.files).toHaveLength(1); expect(withBodyAndFiles.linked.tests).toEqual([]); - const withOnlyTests = await runGet(id, { path: context.pmPath }, { fields: "id,linked.tests" }); + const withOnlyTests = await runGet( + id, + { path: context.pmPath }, + { fields: "id,linked.tests" }, + ); expect(withOnlyTests.item).toEqual({ id }); expect(withOnlyTests.linked).toBeDefined(); if (withOnlyTests.linked === undefined) { @@ -255,7 +319,18 @@ describe("runGet and runAppend", () => { expect(withOnlyTests.linked.tests).toHaveLength(1); expect(withOnlyTests.linked.docs).toEqual([]); - const withClaimState = await runGet(id, { path: context.pmPath }, { fields: "id,claim_state" }); + const withNotesCount = await runGet( + id, + { path: context.pmPath }, + { fields: "id,notes_count" }, + ); + expect(withNotesCount.item).toEqual({ id, notes_count: 0 }); + + const withClaimState = await runGet( + id, + { path: context.pmPath }, + { fields: "id,claim_state" }, + ); expect(withClaimState.item).toEqual({ id }); expect(withClaimState.claim_state).toEqual({ claimed: false, @@ -264,7 +339,11 @@ describe("runGet and runAppend", () => { last_release: null, }); - const withDottedClaimState = await runGet(id, { path: context.pmPath }, { fields: "id,claim_state.claimed" }); + const withDottedClaimState = await runGet( + id, + { path: context.pmPath }, + { fields: "id,claim_state.claimed" }, + ); expect(withDottedClaimState.item).toEqual({ id }); expect(withDottedClaimState.claim_state).toBeDefined(); if (withDottedClaimState.claim_state === undefined) { @@ -272,7 +351,11 @@ describe("runGet and runAppend", () => { } expect(withDottedClaimState.claim_state.claimed).toBe(false); - const withChildren = await runGet(id, { path: context.pmPath }, { fields: "id,children" }); + const withChildren = await runGet( + id, + { path: context.pmPath }, + { fields: "id,children" }, + ); expect(withChildren.item).toEqual({ id }); expect(withChildren.children).toMatchObject({ count: 0, @@ -292,12 +375,18 @@ describe("runGet and runAppend", () => { expect(withItemPrefixedClaimState.item).toEqual({ id }); expect(withItemPrefixedClaimState.claim_state?.claimed).toBe(false); - await expect(runGet(id, { path: context.pmPath }, { fields: " , " })).rejects.toMatchObject({ + await expect( + runGet(id, { path: context.pmPath }, { fields: " , " }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, }); - await expect(runGet(id, { path: context.pmPath }, { fields: "id,bogus" })).rejects.toMatchObject({ + await expect( + runGet(id, { path: context.pmPath }, { fields: "id,bogus" }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, - message: expect.stringContaining("Unknown get --fields value(s): bogus"), + message: expect.stringContaining( + "Unknown get --fields value(s): bogus", + ), }); }); }); @@ -318,7 +407,17 @@ describe("runGet and runAppend", () => { }); const childLink = context.runCli( - ["update", "--json", childId, "--parent", rootId, "--author", "test-author", "--message", "link child to root"], + [ + "update", + "--json", + childId, + "--parent", + rootId, + "--author", + "test-author", + "--message", + "link child to root", + ], { expectJson: true }, ); expect(childLink.code).toBe(0); @@ -338,11 +437,17 @@ describe("runGet and runAppend", () => { ); expect(grandchildLink.code).toBe(0); - await expect(runGet(rootId, { path: context.pmPath }, { treeDepth: "1" })).rejects.toMatchObject({ + await expect( + runGet(rootId, { path: context.pmPath }, { treeDepth: "1" }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, }); - const treeResult = await runGet(rootId, { path: context.pmPath }, { tree: true, treeDepth: "1" }); + const treeResult = await runGet( + rootId, + { path: context.pmPath }, + { tree: true, treeDepth: "1" }, + ); expect(treeResult.tree).toBeDefined(); if (treeResult.tree === undefined) { throw new TypeError("tree result was not returned"); @@ -354,16 +459,24 @@ describe("runGet and runAppend", () => { const ids = treeItems.map((entry) => String(entry.id)); expect(ids).toEqual([childId, grandchildId]); expect(treeItems).toHaveLength(2); - const [childTreeItem, grandchildTreeItem] = treeItems as Array<{ tree_depth?: number }>; + const [childTreeItem, grandchildTreeItem] = treeItems as Array<{ + tree_depth?: number; + }>; expect(childTreeItem).toBeDefined(); expect(grandchildTreeItem).toBeDefined(); if (childTreeItem === undefined || grandchildTreeItem === undefined) { - throw new TypeError("tree result did not include expected child entries"); + throw new TypeError( + "tree result did not include expected child entries", + ); } expect(childTreeItem.tree_depth).toBe(0); expect(grandchildTreeItem.tree_depth).toBe(1); - const unboundedTree = await runGet(rootId, { path: context.pmPath }, { tree: true }); + const unboundedTree = await runGet( + rootId, + { path: context.pmPath }, + { tree: true }, + ); expect(unboundedTree.tree).toBeDefined(); if (unboundedTree.tree === undefined) { throw new TypeError("unbounded tree result was not returned"); @@ -395,25 +508,59 @@ describe("runGet and runAppend", () => { for (const childId of [openChildId, secondOpenChildId, closedChildId]) { const link = context.runCli( - ["update", childId, "--parent", epicId, "--json", "--author", "test-author", "--message", "Link child to epic"], + [ + "update", + childId, + "--parent", + epicId, + "--json", + "--author", + "test-author", + "--message", + "Link child to epic", + ], { expectJson: true }, ); expect(link.code).toBe(0); } - const close = context.runCli(["close", closedChildId, "Child complete", "--json", "--author", "test-author"], { - expectJson: true, - }); + const close = context.runCli( + [ + "close", + closedChildId, + "Child complete", + "--json", + "--author", + "test-author", + ], + { + expectJson: true, + }, + ); expect(close.code).toBe(0); - const openChildPath = path.join(context.pmPath, "tasks", `${openChildId}.toon`); + const openChildPath = path.join( + context.pmPath, + "tasks", + `${openChildId}.toon`, + ); const openChildSource = await readFile(openChildPath, "utf8"); await writeFile( openChildPath, - openChildSource.replace(`parent: ${epicId}`, `parent: "${epicId.toUpperCase()}"`).replace("status: open", 'status: " Open "'), + openChildSource + .replace(`parent: ${epicId}`, `parent: "${epicId.toUpperCase()}"`) + .replace("status: open", 'status: " Open "'), "utf8", ); - const secondOpenChildPath = path.join(context.pmPath, "tasks", `${secondOpenChildId}.toon`); + const secondOpenChildPath = path.join( + context.pmPath, + "tasks", + `${secondOpenChildId}.toon`, + ); const secondOpenChildSource = await readFile(secondOpenChildPath, "utf8"); - await writeFile(secondOpenChildPath, secondOpenChildSource.replace("status: open", 'status: ""'), "utf8"); + await writeFile( + secondOpenChildPath, + secondOpenChildSource.replace("status: open", 'status: ""'), + "utf8", + ); const standard = await runGet(epicId, { path: context.pmPath }); expect(standard.children).toMatchObject({ @@ -432,7 +579,11 @@ describe("runGet and runAppend", () => { ), ); - const brief = await runGet(epicId, { path: context.pmPath }, { depth: "brief" }); + const brief = await runGet( + epicId, + { path: context.pmPath }, + { depth: "brief" }, + ); expect(brief.children).toBeUndefined(); const emptyPlanId = createTask(context, { @@ -448,7 +599,11 @@ describe("runGet and runAppend", () => { expect(leaf.children).toBeUndefined(); expect(listAllSpy).not.toHaveBeenCalled(); - const projectedLeaf = await runGet(openChildId, { path: context.pmPath }, { fields: "id,children" }); + const projectedLeaf = await runGet( + openChildId, + { path: context.pmPath }, + { fields: "id,children" }, + ); expect(projectedLeaf.item).toEqual({ id: openChildId }); expect(projectedLeaf.children).toMatchObject({ count: 0, @@ -468,9 +623,15 @@ describe("runGet and runAppend", () => { count: 2, active: 1, }); - expect(_testOnlyGetCommand.shouldAutoIncludeGetChildren("Plan")).toBe(true); - expect(_testOnlyGetCommand.shouldAutoIncludeGetChildren("Task")).toBe(false); - expect(_testOnlyGetCommand.shouldAutoIncludeGetChildren("CustomContainer")).toBe(true); + expect(_testOnlyGetCommand.shouldAutoIncludeGetChildren("Plan")).toBe( + true, + ); + expect(_testOnlyGetCommand.shouldAutoIncludeGetChildren("Task")).toBe( + false, + ); + expect( + _testOnlyGetCommand.shouldAutoIncludeGetChildren("CustomContainer"), + ).toBe(true); expect(_testOnlyGetCommand.shouldAutoIncludeGetChildren(" ")).toBe(false); }); }); @@ -516,7 +677,11 @@ describe("runGet and runAppend", () => { expect(itemPrefixedProjection.schedule).toEqual({ start_at: "2026-07-20T09:00:00.000Z", }); - const brief = await runGet(id, { path: context.pmPath }, { depth: "brief" }); + const brief = await runGet( + id, + { path: context.pmPath }, + { depth: "brief" }, + ); expect(brief.schedule).toBeUndefined(); }); }); @@ -546,7 +711,11 @@ describe("runGet and runAppend", () => { expect(update.code).toBe(0); const beforeReadHistory = await readFile(historyPath, "utf8"); expect(beforeReadHistory.length).toBeGreaterThan(initialHistory.length); - const usagePath = path.join(context.pmPath, "runtime", "context-usage.jsonl"); + const usagePath = path.join( + context.pmPath, + "runtime", + "context-usage.jsonl", + ); const beforeReadUsage = await readFile(usagePath, "utf8"); const historical = await runGet( @@ -580,22 +749,36 @@ describe("runGet and runAppend", () => { }, }); await expect( - runGet(id, { path: context.pmPath }, { at: "2100-01-01T00:00:00.000Z" }), + runGet( + id, + { path: context.pmPath }, + { at: "2100-01-01T00:00:00.000Z" }, + ), ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE }); await expect( runGet(id, { path: context.pmPath }, { at: "1", tree: true }), ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE }); await expect( - runGet(id, { path: context.pmPath }, { at: "1", fields: "id,children" }), + runGet( + id, + { path: context.pmPath }, + { at: "1", fields: "id,children" }, + ), ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, - message: expect.stringContaining( - "Get --at cannot project children", - ), + message: expect.stringContaining("Get --at cannot project children"), }); - await runHistoryCompact(id, { author: "test-author" }, { path: context.pmPath }); - const checkpoint = await runGet(id, { path: context.pmPath }, { at: "1" }); + await runHistoryCompact( + id, + { author: "test-author" }, + { path: context.pmPath }, + ); + const checkpoint = await runGet( + id, + { path: context.pmPath }, + { at: "1" }, + ); expect(checkpoint).toMatchObject({ reconstructed: true, as_of_version: 1, @@ -616,7 +799,17 @@ describe("runGet and runAppend", () => { body: "child body", }); const linked = context.runCli( - ["update", childId, "--parent", epicId, "--json", "--author", "test-author", "--message", "Link child to epic"], + [ + "update", + childId, + "--parent", + epicId, + "--json", + "--author", + "test-author", + "--message", + "Link child to epic", + ], { expectJson: true }, ); expect(linked.code).toBe(0); @@ -628,9 +821,15 @@ describe("runGet and runAppend", () => { } const epicPath = locatedEpic.itemPath; const epicSource = await readFile(epicPath, "utf8"); - await writeFile(epicPath, epicSource.replace("type: Epic", 'type: ""'), "utf8"); + await writeFile( + epicPath, + epicSource.replace("type: Epic", 'type: ""'), + "utf8", + ); - await expect(runGet(epicId, { path: context.pmPath }, { fields: "id,children" })).rejects.toMatchObject({ + await expect( + runGet(epicId, { path: context.pmPath }, { fields: "id,children" }), + ).rejects.toMatchObject({ message: expect.stringContaining("type must be a non-empty string"), }); }); @@ -649,25 +848,32 @@ describe("runGet and runAppend", () => { }, ]; await writeSettings(context.pmPath, settings, "settings:write"); - const created = context.runCli([ - "create", - "--json", - "--title", - "Runtime field get", - "--description", - "Runtime field get description", - "--type", - "Task", - "--status", - "open", - "--priority", - "1", - "--customer-segment", - "enterprise", - ], { expectJson: true }); + const created = context.runCli( + [ + "create", + "--json", + "--title", + "Runtime field get", + "--description", + "Runtime field get description", + "--type", + "Task", + "--status", + "open", + "--priority", + "1", + "--customer-segment", + "enterprise", + ], + { expectJson: true }, + ); const id = (created.json as { item: { id: string } }).item.id; - const projected = await runGet(id, { path: context.pmPath }, { fields: "id,customer_segment" }); + const projected = await runGet( + id, + { path: context.pmPath }, + { fields: "id,customer_segment" }, + ); expect(projected.item).toEqual({ id, customer_segment: "enterprise" }); }); }); @@ -687,9 +893,20 @@ describe("runGet and runAppend", () => { last_release: null, }); - const claim = context.runCli(["claim", id, "--json", "--author", "owner-a", "--message", "claim metadata context"], { - expectJson: true, - }); + const claim = context.runCli( + [ + "claim", + id, + "--json", + "--author", + "owner-a", + "--message", + "claim metadata context", + ], + { + expectJson: true, + }, + ); expect(claim.code).toBe(0); const afterClaim = await runGet(id, { path: context.pmPath }); @@ -699,7 +916,16 @@ describe("runGet and runAppend", () => { expect(afterClaim.claim_state.last_release).toBeNull(); const release = context.runCli( - ["release", id, "--json", "--author", "audit-reviewer", "--force", "--message", "release metadata context"], + [ + "release", + id, + "--json", + "--author", + "audit-reviewer", + "--force", + "--message", + "release metadata context", + ], { expectJson: true }, ); expect(release.code).toBe(0); @@ -708,7 +934,9 @@ describe("runGet and runAppend", () => { expect(afterRelease.claim_state.claimed).toBe(false); expect(afterRelease.claim_state.assignee).toBeNull(); expect(afterRelease.claim_state.last_claim?.author).toBe("owner-a"); - expect(afterRelease.claim_state.last_release?.author).toBe("audit-reviewer"); + expect(afterRelease.claim_state.last_release?.author).toBe( + "audit-reviewer", + ); }); }); @@ -722,12 +950,21 @@ describe("runGet and runAppend", () => { const historyPath = path.join(context.pmPath, "history", `${id}.jsonl`); await writeFile(historyPath, "{not valid jsonl}\n", "utf8"); - await expect(runGet(id, { path: context.pmPath })).rejects.toMatchObject({ + await expect( + runGet(id, { path: context.pmPath }), + ).rejects.toMatchObject({ message: expect.stringContaining("contains invalid JSON"), }); - const projected = await runGet(id, { path: context.pmPath }, { fields: "id,title" }); - expect(projected.item).toEqual({ id, title: "get-history-decode-fallback" }); + const projected = await runGet( + id, + { path: context.pmPath }, + { fields: "id,title" }, + ); + expect(projected.item).toEqual({ + id, + title: "get-history-decode-fallback", + }); expect(projected.claim_state).toBeUndefined(); }); }); @@ -739,14 +976,20 @@ describe("runGet and runAppend", () => { body: "claim metadata null message body", }); - const claim = context.runCli(["claim", id, "--json", "--author", "owner-a"], { - expectJson: true, - }); + const claim = context.runCli( + ["claim", id, "--json", "--author", "owner-a"], + { + expectJson: true, + }, + ); expect(claim.code).toBe(0); - const release = context.runCli(["release", id, "--json", "--author", "owner-a"], { - expectJson: true, - }); + const release = context.runCli( + ["release", id, "--json", "--author", "owner-a"], + { + expectJson: true, + }, + ); expect(release.code).toBe(0); const result = await runGet(id, { path: context.pmPath }); @@ -757,7 +1000,9 @@ describe("runGet and runAppend", () => { it("returns not found for unknown ids", async () => { await withTempPmPath(async (context) => { - await expect(runGet("pm-does-not-exist", { path: context.pmPath })).rejects.toMatchObject({ + await expect( + runGet("pm-does-not-exist", { path: context.pmPath }), + ).rejects.toMatchObject({ exitCode: EXIT_CODE.NOT_FOUND, }); }); @@ -770,7 +1015,16 @@ describe("runGet and runAppend", () => { body: "seed body", }); await expect( - runAppend(id, {} as unknown as { body: string; author?: string; message?: string; force?: boolean }, { path: context.pmPath }), + runAppend( + id, + {} as unknown as { + body: string; + author?: string; + message?: string; + force?: boolean; + }, + { path: context.pmPath }, + ), ).rejects.toMatchObject({ exitCode: EXIT_CODE.USAGE, }); @@ -779,31 +1033,81 @@ describe("runGet and runAppend", () => { it("accepts append text as positional shorthand or --text alias and rejects conflicting/missing sources", async () => { await withTempPmPath(async (context) => { - const id = createTask(context, { title: "append-text-forms", body: "seed body" }); - - const positional = context.runCli(["append", id, "appended via positional", "--json", "--author", "owner-a"], { - expectJson: true, + const id = createTask(context, { + title: "append-text-forms", + body: "seed body", }); + + const positional = context.runCli( + [ + "append", + id, + "appended via positional", + "--json", + "--author", + "owner-a", + ], + { + expectJson: true, + }, + ); expect(positional.code).toBe(0); - expect((positional.json as { appended?: string }).appended).toBe("appended via positional"); + expect((positional.json as { appended?: string }).appended).toBe( + "appended via positional", + ); - const aliased = context.runCli(["append", id, "--text", "appended via text alias", "--json", "--author", "owner-a"], { - expectJson: true, - }); + const aliased = context.runCli( + [ + "append", + id, + "--text", + "appended via text alias", + "--json", + "--author", + "owner-a", + ], + { + expectJson: true, + }, + ); expect(aliased.code).toBe(0); - expect((aliased.json as { appended?: string }).appended).toBe("appended via text alias"); + expect((aliased.json as { appended?: string }).appended).toBe( + "appended via text alias", + ); - const stdinText = context.runCli(["append", id, "--text", "-", "--json", "--author", "owner-a"], { - expectJson: true, - input: "appended from stdin", - }); + const stdinText = context.runCli( + ["append", id, "--text", "-", "--json", "--author", "owner-a"], + { + expectJson: true, + input: "appended from stdin", + }, + ); expect(stdinText.code).toBe(0); - expect((stdinText.json as { appended?: string }).appended).toBe("appended from stdin"); + expect((stdinText.json as { appended?: string }).appended).toBe( + "appended from stdin", + ); const conflictCases = [ ["append", id, "positional", "--text", "alias", "--author", "owner-a"], - ["append", id, "--body", "from-body", "--text", "from-text", "--author", "owner-a"], - ["append", id, "from-positional", "--body", "from-body", "--author", "owner-a"], + [ + "append", + id, + "--body", + "from-body", + "--text", + "from-text", + "--author", + "owner-a", + ], + [ + "append", + id, + "from-positional", + "--body", + "from-body", + "--author", + "owner-a", + ], ]; for (const args of conflictCases) { const conflicting = context.runCli(args); @@ -824,12 +1128,14 @@ describe("runGet and runAppend", () => { body: "seed body", }); const before = await runGet(id, { path: context.pmPath }); - const historyBefore = context.runCli(["history", id, "--json", "--full"], { - expectJson: true, - }); - const historyCountBefore = ( - historyBefore.json as { history: unknown[] } - ).history.length; + const historyBefore = context.runCli( + ["history", id, "--json", "--full"], + { + expectJson: true, + }, + ); + const historyCountBefore = (historyBefore.json as { history: unknown[] }) + .history.length; for (const blank of [" ", ""]) { await expect( @@ -848,7 +1154,13 @@ describe("runGet and runAppend", () => { }); } - const positionalBlank = context.runCli(["append", id, " ", "--author", "owner-a"]); + const positionalBlank = context.runCli([ + "append", + id, + " ", + "--author", + "owner-a", + ]); expect(positionalBlank.code).toBe(EXIT_CODE.USAGE); expect(positionalBlank.stderr).toContain("cannot be empty"); @@ -858,9 +1170,9 @@ describe("runGet and runAppend", () => { const historyAfter = context.runCli(["history", id, "--json", "--full"], { expectJson: true, }); - expect( - (historyAfter.json as { history: unknown[] }).history.length, - ).toBe(historyCountBefore); + expect((historyAfter.json as { history: unknown[] }).history.length).toBe( + historyCountBefore, + ); }); }); @@ -880,11 +1192,18 @@ describe("runGet and runAppend", () => { ); expect(firstAppend.appended).toBe("first entry"); expect(firstAppend.changed_fields).toContain("body"); - const afterFirstAppend = await runGet(emptyBodyId, { path: context.pmPath }); + const afterFirstAppend = await runGet(emptyBodyId, { + path: context.pmPath, + }); expect(afterFirstAppend.item.body).toBe("first entry"); - const firstHistory = context.runCli(["history", emptyBodyId, "--json", "--full"], { expectJson: true }); + const firstHistory = context.runCli( + ["history", emptyBodyId, "--json", "--full"], + { expectJson: true }, + ); expect(firstHistory.code).toBe(0); - const firstHistoryJson = firstHistory.json as { history: Array<{ op: string; author: string }> }; + const firstHistoryJson = firstHistory.json as { + history: Array<{ op: string; author: string }>; + }; const firstAppendAuthor = [...firstHistoryJson.history] .reverse() .find((entry) => entry.op === "append")?.author; @@ -913,12 +1232,21 @@ describe("runGet and runAppend", () => { expect(secondAppend.appended).toBe("second entry"); expect(secondAppend.changed_fields).toContain("body"); - const afterSecondAppend = await runGet(spacedBodyId, { path: context.pmPath }); - expect(afterSecondAppend.item.body).toBe("existing body\n\nsecond entry"); + const afterSecondAppend = await runGet(spacedBodyId, { + path: context.pmPath, + }); + expect(afterSecondAppend.item.body).toBe( + "existing body\n\nsecond entry", + ); - const history = context.runCli(["history", spacedBodyId, "--json", "--full"], { expectJson: true }); + const history = context.runCli( + ["history", spacedBodyId, "--json", "--full"], + { expectJson: true }, + ); expect(history.code).toBe(0); - const historyJson = history.json as { history: Array<{ op: string; author: string }> }; + const historyJson = history.json as { + history: Array<{ op: string; author: string }>; + }; const appendAuthor = [...historyJson.history] .reverse() .find((entry) => entry.op === "append")?.author; @@ -929,7 +1257,11 @@ describe("runGet and runAppend", () => { author_default?: string; }; settings.author_default = "settings-author"; - await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); + await writeFile( + settingsPath, + `${JSON.stringify(settings, null, 2)}\n`, + "utf8", + ); const settingsAppend = await runAppend( settingsAuthorId, @@ -940,7 +1272,10 @@ describe("runGet and runAppend", () => { { path: context.pmPath }, ); expect(settingsAppend.changed_fields).toContain("body"); - const settingsHistory = context.runCli(["history", settingsAuthorId, "--json", "--full"], { expectJson: true }); + const settingsHistory = context.runCli( + ["history", settingsAuthorId, "--json", "--full"], + { expectJson: true }, + ); expect(settingsHistory.code).toBe(0); const settingsHistoryJson = settingsHistory.json as { history: Array<{ op: string; author: string }>; @@ -967,14 +1302,22 @@ describe("runGet and runAppend", () => { }); const stdin = new PassThrough(); stdin.end("markdown from stdin"); - Object.defineProperty(stdin, "isTTY", { value: false, configurable: true }); - vi.spyOn(process, "stdin", "get").mockReturnValue(stdin as unknown as NodeJS.ReadStream); + Object.defineProperty(stdin, "isTTY", { + value: false, + configurable: true, + }); + vi.spyOn(process, "stdin", "get").mockReturnValue( + stdin as unknown as NodeJS.ReadStream, + ); - const appendResult = await runAppend(id, { body: "-", message: "append stdin payload" }, { path: context.pmPath }); + const appendResult = await runAppend( + id, + { body: "-", message: "append stdin payload" }, + { path: context.pmPath }, + ); expect(appendResult.changed_fields).toContain("body"); const getResult = await runGet(id, { path: context.pmPath }); expect(getResult.item.body).toBe("existing body\n\nmarkdown from stdin"); }); }); - }); diff --git a/tests/unit/sdk/history-repair-reconciliation.spec.ts b/tests/unit/sdk/history-repair-reconciliation.spec.ts index f81cefe64..4c3614b54 100644 --- a/tests/unit/sdk/history-repair-reconciliation.spec.ts +++ b/tests/unit/sdk/history-repair-reconciliation.spec.ts @@ -22,7 +22,9 @@ describe("history repair reconciliation analysis", () => { it("attributes every reverted field to the newest writing event and author", () => { const report = analyzeReconciliationDiscard( [ - entry("agent-a", [{ op: "replace", path: "/metadata/title", value: "new" }]), + entry("agent-a", [ + { op: "replace", path: "/metadata/title", value: "new" }, + ]), entry("agent-b", [ { op: "move", from: "/metadata/tags/0", path: "/metadata/tags/1" }, { op: "replace", path: "/body", value: "new body" }, @@ -33,13 +35,131 @@ describe("history repair reconciliation analysis", () => { ); expect(report).toMatchObject({ - reverted_field_count: 3, - reverted_fields: ["body", "tags", "title"], + reverted_field_count: 2, + reverted_fields: ["body", "title"], + preserved_field_count: 1, + preserved_fields: ["tags"], discarded_authors: ["agent-a", "agent-b"], }); expect(report?.discarded_events).toEqual([ expect.objectContaining({ author: "agent-a", fields: ["title"] }), - expect.objectContaining({ author: "agent-b", fields: ["body", "tags"] }), + expect.objectContaining({ author: "agent-b", fields: ["body"] }), + ]); + expect(report?.preserved_events).toEqual([ + expect.objectContaining({ author: "agent-b", fields: ["tags"] }), + ]); + expect(report?.recovery_hint).toEqual(expect.any(String)); + }); + + it("classifies append-only union additions as preserved instead of discarded", () => { + const report = analyzeReconciliationDiscard( + [ + entry("agent-a", [ + { + op: "add", + path: "/metadata/notes/0", + value: { text: "from agent a", author: "agent-a" }, + }, + { + op: "add", + path: "/metadata/comments/0", + value: { text: "from agent a", author: "agent-a" }, + }, + ]), + entry("agent-z", [ + { + op: "add", + path: "/metadata/learnings/0", + value: { text: "from agent z", author: "agent-z" }, + }, + ]), + ], + { + metadata: { + notes: [{ text: "from agent a", author: "agent-a" }], + comments: [{ text: "from agent a", author: "agent-a" }], + learnings: [{ text: "from agent z", author: "agent-z" }], + }, + body: "", + }, + { + metadata: { + notes: [ + { text: "from agent a", author: "agent-a" }, + { text: "from agent b", author: "agent-b" }, + ], + comments: [ + { text: "from agent b", author: "agent-b" }, + { text: "from agent a", author: "agent-a" }, + ], + learnings: [ + { text: "from agent z", author: "agent-z" }, + { text: "from agent b", author: "agent-b" }, + ], + }, + body: "", + }, + ); + + expect(report).toMatchObject({ + reverted_field_count: 0, + reverted_fields: [], + preserved_field_count: 3, + preserved_fields: ["comments", "learnings", "notes"], + discarded_events: [], + discarded_authors: [], + preserved_authors: ["agent-a", "agent-z"], + recovery_hint: null, + }); + expect(report?.preserved_events).toEqual([ + expect.objectContaining({ + author: "agent-a", + fields: ["comments", "notes"], + }), + expect.objectContaining({ + author: "agent-z", + fields: ["learnings"], + }), ]); }); + + it("keeps array removals and multiplicity loss in the discarded partition", () => { + const report = analyzeReconciliationDiscard( + [ + entry("agent-a", [ + { op: "add", path: "/metadata/tags/0", value: "context" }, + { + op: "add", + path: "/metadata/notes/0", + value: { text: "duplicate" }, + }, + { + op: "add", + path: "/metadata/notes/1", + value: { text: "duplicate" }, + }, + ]), + ], + { + metadata: { + tags: ["context"], + notes: [{ text: "duplicate" }, { text: "duplicate" }], + }, + body: "", + }, + { + metadata: { + tags: "context", + notes: [{ text: "duplicate" }], + }, + body: "", + }, + ); + + expect(report).toMatchObject({ + reverted_field_count: 2, + reverted_fields: ["notes", "tags"], + preserved_field_count: 0, + }); + }); }); diff --git a/tests/unit/sdk/relationships.spec.ts b/tests/unit/sdk/relationships.spec.ts index ea8e05db8..29495f986 100644 --- a/tests/unit/sdk/relationships.spec.ts +++ b/tests/unit/sdk/relationships.spec.ts @@ -1,7 +1,10 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { assertDependencyEdgesAllowed } from "../../../src/sdk/dependency-flag-validation.js"; +import { PmCliError } from "../../../src/core/shared/errors.js"; import { RelationshipGraph, RelationshipKindRegistry, + assertRelationshipEdgeAllowed, createRelationshipKindRegistry, dependencyToRelationship, isOrderingRelationshipKind, @@ -151,6 +154,57 @@ describe("relationship kind registry", () => { }), ).toThrow("Non-hierarchy relationship kind"); }); + + it("enforces self-edge policy after alias resolution while preserving custom opt-ins", () => { + const registry = createRelationshipKindRegistry().register({ + kind: "reflects", + direction: "directed", + ordering: false, + hierarchy: false, + outgoing: "many", + incoming: "many", + lifecycle: "persistent", + aliases: ["self_reflection"], + compatibilityVersion: 1, + allowSelf: true, + }); + + expect(() => + assertRelationshipEdgeAllowed("pm-a", "pm-a", "depends-on", registry), + ).toThrow("Self relationship is not allowed for blocked_by"); + expect( + assertRelationshipEdgeAllowed("pm-a", "pm-a", "self-reflection", registry) + .kind, + ).toBe("reflects"); + expect(() => + assertRelationshipEdgeAllowed(" ", "pm-a", "related", registry), + ).toThrow("Relationship endpoint must not be empty"); + }); + + it("normalizes defensive dependency-policy failures into structured usage errors", () => { + const registry = createRelationshipKindRegistry(); + vi.spyOn(registry, "require").mockImplementation(() => { + throw "non-error registry failure"; + }); + + expect(() => + assertDependencyEdgesAllowed( + "pm-a", + [{ id: "pm-b", kind: "related", created_at: "now" }], + registry, + ), + ).toThrowError( + expect.objectContaining({ + message: "non-error registry failure", + context: expect.objectContaining({ + code: "dependency_edge_not_allowed", + }), + }), + ); + expect(() => + assertDependencyEdgesAllowed("pm-a", undefined, registry), + ).not.toThrow(); + }); }); describe("relationship graph", () => { @@ -469,14 +523,19 @@ describe("neighborEdges", () => { "root", ]); expect( - graph.neighborEdges("root", { direction: "incoming" }).value.map(({ id }) => id), + graph + .neighborEdges("root", { direction: "incoming" }) + .value.map(({ id }) => id), ).toEqual(["alpha", "beta"]); expect( graph .neighborEdges("root", { direction: "both", kinds: ["parent"] }) .value.map(({ id }) => id), ).toEqual(["beta"]); - const bounded = graph.neighborEdges("root", { direction: "both", limit: 1 }); + const bounded = graph.neighborEdges("root", { + direction: "both", + limit: 1, + }); expect(bounded.value).toHaveLength(1); expect(bounded.meta.truncated).toBe(true); expect(() => graph.neighborEdges("missing-node", {})).toThrow(