Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .agents/pm/history/pm-linear-nvt5.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{"ts":"2026-07-21T12:39:12.520Z","author":"pi-agent","op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-linear-nvt5"},{"op":"add","path":"/metadata/title","value":"Add opt-in --atomic to linear import/sync via SDK commitItemMutations"},{"op":"add","path":"/metadata/description","value":"Atomic, crash-resumable, multi-agent-safe bulk-write path for pm-linear. Mirrors the merged pm-github reference. Companion tracker: pm-cli-website-c9u0 at https://github.com/unbraind/pm-cli-companion (parent epic pm-cli-website-s4wz)."},{"op":"add","path":"/metadata/type","value":"Task"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-07-21T12:39:12.520Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-21T12:39:12.520Z"},{"op":"add","path":"/metadata/author","value":"pi-agent"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"6374b99c5d7d2d9567e4bf454cb8d439e3b617483e96687073b72adcd7753cbf","message":""}
{"ts":"2026-07-21T12:39:19.418Z","author":"pi-agent","op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T12:39:19.418Z"},{"op":"add","path":"/metadata/assignee","value":"pi-agent"}],"before_hash":"6374b99c5d7d2d9567e4bf454cb8d439e3b617483e96687073b72adcd7753cbf","after_hash":"44ba9ea0fa5280f68f9fa3cfe890cfcc920eedaff60988fc8d769937e2027935"}
{"ts":"2026-07-21T12:39:19.440Z","author":"pi-agent","op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T12:39:19.440Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"44ba9ea0fa5280f68f9fa3cfe890cfcc920eedaff60988fc8d769937e2027935","after_hash":"67d148c21bf112fe6d0360c84c8c03449adedf4188be415474220aa54f90442e"}
{"ts":"2026-07-21T12:39:58.574Z","author":"pi-agent","op":"note_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T12:39:58.574Z"},{"op":"add","path":"/metadata/notes","value":[{"created_at":"2026-07-21T12:39:58.574Z","author":"pi-agent","text":"DESIGN DECISIONS:\n- Stable item id keyed on Linear issue identifier (e.g. ENG-123), the most stable human-meaningful external key, never fetch order. deriveAtomicItemId hashes lowercased team + identifier through normalizeItemId.\n- Transaction id fingerprints lowercased team + canonical entries sorted by the stable identifier + the exact ordered BulkItemMutation plan. A changed id_prefix or provenance match yields a fresh journal; a reordered retry resumes the same one.\n- Mutation plan: create+update upsert for new/deterministic-id items (same plan on recovery when provenance scan finds the deterministic id); update(+close) for legacy matches at a different id. close is a separate mutation preserving a reason and is independently reversible.\n- Compensation: createCompensation='delete' for creates; SDK version-restores updates+closes. On AggregateError/compensation-failed -> warn tracker MAY be partial + same-transaction recovery path; normal SDK compensation -> no new partial state; WorkspaceTransactionInterruptedError -> resumable.\n- Byte-compatible default: non-atomic per-issue spawnSync path is UNCHANGED. --atomic --dry-run shares the atomic prep/matching path (fetches issues, reports create/update/skip counts) and NEVER commits; the non-atomic --dry-run stays fully offline (GraphQL request preview).\n- All-skipped non-dry atomic import throws the SAME error shape as legacy (CommandError 'Imported 0 issue(s); N failed.', exitCode GENERIC_FAILURE) BEFORE commit.\n- Exported CommandError/EXIT_CODE/LinearIssue/syncLinearIssues (+SyncRunDependencies test seams) mirroring pm-github; package.json peerDep >=2026.7.20, devDep ^2026.7.20 (latest published with commitItemMutations; ^2026.7.21 not yet on npm)."}]}],"before_hash":"67d148c21bf112fe6d0360c84c8c03449adedf4188be415474220aa54f90442e","after_hash":"aff6e23c078cfec3cd71033ecdfd405a92c0d45570b1e734757a29fe785f2f7b"}
{"ts":"2026-07-21T12:40:15.159Z","author":"pi-agent","op":"note_add","patch":[{"op":"add","path":"/metadata/notes/1","value":{"created_at":"2026-07-21T12:40:15.159Z","author":"pi-agent","text":"QUALITY GATE: npm run release:check PASSED.\n- typecheck (tsc --noEmit): clean\n- build (tsc): clean\n- npm test: 61 tests pass / 0 fail (51 smoke + 10 new atomic)\n- audit:prod: 0 vulnerabilities\n- pack:dry-run: ok (9 files, 50.7 kB)\n- changelog:check: up to date (no regeneration needed)\n\nAtomic test coverage (test/atomic.test.ts, 10 tests):\n1. mid-run failure -> reverse compensation restores created/updated/closed items (failed batch deletes every create; mixed batch restores updates+closes)\n2. resume idempotence -> re-running same fetched set recovers SAME transaction, 0 duplicates\n3. reordered-retry stability -> shuffled fetch order yields SAME transaction id & item ids\n4. --atomic --dry-run -> shared prep path, correct counts (1 import/1 update/1 skip), NO SDK commit call\n5. all-skipped non-dry atomic import -> throws legacy error shape (CommandError, 'Imported 0 issue(s); 1 failed'), before commit\n6. transaction id changes when a mutation target changes (id, id_prefix, status)\nPlus: incomplete compensation reported as partial+resumable; end-to-end update+close commit through the real SDK against a live tracker."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T12:40:15.159Z"}],"before_hash":"aff6e23c078cfec3cd71033ecdfd405a92c0d45570b1e734757a29fe785f2f7b","after_hash":"027f2144762d1631a804b7a756970071c2dbe6a14e76a1033f0f50feb79e97c4"}
{"ts":"2026-07-21T12:55:11.202Z","author":"claude-orchestrator","op":"note_add","patch":[{"op":"add","path":"/metadata/notes/2","value":{"created_at":"2026-07-21T12:55:11.201Z","author":"claude-orchestrator","text":"Orchestrator independent verification (2026-07-21): re-ran the full release gate — 61/61 tests pass, 0 prod vulnerabilities, changelog current, pack clean. Reviewed the atomic design against the merged pm-github reference: deriveAtomicTransactionId hashes team + identifier-sorted canonical entries + exact ordered mutation plan; deriveAtomicItemId keys on the stable Linear identifier (linear-<teamToken>-<identifier>), never fetch order; importLinearAtomic sorts entries by identifier BEFORE building the plan (order-independence). Ran an independent live harness on the built dist/index.js + REAL SDK against throwaway workspaces: (A) atomic import of 5 realistic entries (2 closed) -> 5 items, txn linear-import-333458e94167573e; re-run recovered the SAME transaction, 0 new, 5 items total (no duplicates); (B) SHUFFLED fetch order through importLinearAtomic -> IDENTICAL transaction id and identical managed item ids. Local greptile CLI produced empty output (known flakiness) -> relying on the PR-time Greptile app + CodeRabbit/Sourcery/cubic/semgrep. Ready to push + PR."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T12:55:11.202Z"}],"before_hash":"027f2144762d1631a804b7a756970071c2dbe6a14e76a1033f0f50feb79e97c4","after_hash":"6a54d17f27f250f160028f18dbce4dc3f710f489d572882a81a98da4f94fbaa7"}
{"ts":"2026-07-21T15:55:36.080Z","author":"claude-orchestrator","op":"note_add","patch":[{"op":"add","path":"/metadata/notes/3","value":{"created_at":"2026-07-21T15:55:36.079Z","author":"claude-orchestrator","text":"Bot-review hardening round (PR #36): (1) gemini-high + no reopen churn — removed shared status field; create seeds open, update omits status, close owns the closed transition; (2) gemini-high + greptile-P1 — all-filtered batch now returns a successful zero-sync result before touching the SDK (matches legacy path; unblocks cron/CI); (3) greptile-P1 — settings read fault now throws CommandError instead of forking id_prefix to 'pm-' (protects transaction/journal identity so resume can't duplicate items); (4) greptile-P1 — linear-sync importer now forwards --dry-run so --atomic --dry-run previews without writing. +2 tests (63 total). Functionally verified end-to-end against a real throwaway pm workspace via real SDK commitItemMutations: create/close, idempotent recovered resume, tx id stable under reorder."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T15:55:36.080Z"}],"before_hash":"6a54d17f27f250f160028f18dbce4dc3f710f489d572882a81a98da4f94fbaa7","after_hash":"fa8b34cb87ac1dab24b721530b1842770730d6ca5ddce5755b98b40715684d7d"}
{"ts":"2026-07-21T16:09:16.974Z","author":"claude-orchestrator","op":"note_add","patch":[{"op":"add","path":"/metadata/notes/4","value":{"created_at":"2026-07-21T16:09:16.974Z","author":"claude-orchestrator","text":"Static-analysis cleanup (DeepScan '1 new issue' on PR #36): removed genuine dead code surfaced by strict tsc (noUnusedLocals/noUnusedParameters) — the unused, unexported KNOWN_LINEAR_FIELDS const (parseFieldMap accepts any key, never referenced it) and the unused 'options' parameter on preflightLinear (2 call sites updated). Build+typecheck+63 tests green. All other analyzers clean (Greptile pass, CodeRabbit full review 0 findings, Hound pass, CI pass)."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T16:09:16.974Z"}],"before_hash":"fa8b34cb87ac1dab24b721530b1842770730d6ca5ddce5755b98b40715684d7d","after_hash":"4794b710046111e4f2af0cfe9d36c2cc292ed4f2dce089ca2809120e76df58ee"}
{"ts":"2026-07-21T16:13:05.646Z","author":"claude-orchestrator","op":"note_add","patch":[{"op":"add","path":"/metadata/notes/5","value":{"created_at":"2026-07-21T16:13:05.643Z","author":"claude-orchestrator","text":"DeepScan 'raw 1 new' root cause: the --atomic block was inserted BEFORE the legacy loop's 'let created=0; let updated=0;' declarations and returns early, so those two assignments are a dead store on the atomic path (a UNUSED_VAR_ASSIGNMENT tsc's noUnusedLocals can't see because the vars are read on the OTHER branch). Moved created/updated declarations down to the legacy loop; kept 'skipped' hoisted (both paths use it). No behavior change; 63 tests green."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T16:13:05.646Z"}],"before_hash":"4794b710046111e4f2af0cfe9d36c2cc292ed4f2dce089ca2809120e76df58ee","after_hash":"6f975d4dcafcf973f1d795403051eb04ea9baeaf553bdfce401533c759cd8df7"}
{"ts":"2026-07-21T16:43:05.393Z","author":"claude-orchestrator","op":"note_add","patch":[{"op":"add","path":"/metadata/notes/6","value":{"created_at":"2026-07-21T16:43:05.392Z","author":"claude-orchestrator","text":"DeepScan '1 new' root-caused via local typescript-eslint strictTypeChecked (TS6 API): a type-aware no-unnecessary-condition/redundant-conversion on defensive SDK-boundary code, NOT a runtime bug (all high-signal rules — floating-promises/await/constant-condition/dupe-keys — clean). The only findings NEW to this branch were 3 lines: assertSdkFunction<F> (type param used once), 'settings?.id_prefix / String(...)', and 'Boolean(result?.recovered)'. Applied behavior-preserving fixes (per pinned SDK types these are provable no-ops): dropped redundant optional-chains + Boolean()/String(); constrained assertSdkFunction<F extends (...args:never[])=>unknown>(fn: F|undefined) so F is used twice (also stronger typing). typecheck+build+63 tests green. User-approved cleanup."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-07-21T16:43:05.393Z"}],"before_hash":"6f975d4dcafcf973f1d795403051eb04ea9baeaf553bdfce401533c759cd8df7","after_hash":"48822fc0579c9ee51d4575ebc1701f935a77e47739b002a817622036bdbde106"}
20 changes: 20 additions & 0 deletions .agents/pm/tasks/pm-linear-nvt5.toon
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
id: pm-linear-nvt5
title: Add opt-in --atomic to linear import/sync via SDK commitItemMutations
description: "Atomic, crash-resumable, multi-agent-safe bulk-write path for pm-linear. Mirrors the merged pm-github reference. Companion tracker: pm-cli-website-c9u0 at https://github.com/unbraind/pm-cli-companion (parent epic pm-cli-website-s4wz)."
type: Task
status: in_progress
priority: 2
tags: []
created_at: "2026-07-21T12:39:12.520Z"
updated_at: "2026-07-21T16:43:05.393Z"
assignee: pi-agent
author: pi-agent
notes[7]{created_at,author,text}:
"2026-07-21T12:39:58.574Z",pi-agent,"DESIGN DECISIONS:\n- Stable item id keyed on Linear issue identifier (e.g. ENG-123), the most stable human-meaningful external key, never fetch order. deriveAtomicItemId hashes lowercased team + identifier through normalizeItemId.\n- Transaction id fingerprints lowercased team + canonical entries sorted by the stable identifier + the exact ordered BulkItemMutation plan. A changed id_prefix or provenance match yields a fresh journal; a reordered retry resumes the same one.\n- Mutation plan: create+update upsert for new/deterministic-id items (same plan on recovery when provenance scan finds the deterministic id); update(+close) for legacy matches at a different id. close is a separate mutation preserving a reason and is independently reversible.\n- Compensation: createCompensation='delete' for creates; SDK version-restores updates+closes. On AggregateError/compensation-failed -> warn tracker MAY be partial + same-transaction recovery path; normal SDK compensation -> no new partial state; WorkspaceTransactionInterruptedError -> resumable.\n- Byte-compatible default: non-atomic per-issue spawnSync path is UNCHANGED. --atomic --dry-run shares the atomic prep/matching path (fetches issues, reports create/update/skip counts) and NEVER commits; the non-atomic --dry-run stays fully offline (GraphQL request preview).\n- All-skipped non-dry atomic import throws the SAME error shape as legacy (CommandError 'Imported 0 issue(s); N failed.', exitCode GENERIC_FAILURE) BEFORE commit.\n- Exported CommandError/EXIT_CODE/LinearIssue/syncLinearIssues (+SyncRunDependencies test seams) mirroring pm-github; package.json peerDep >=2026.7.20, devDep ^2026.7.20 (latest published with commitItemMutations; ^2026.7.21 not yet on npm)."
"2026-07-21T12:40:15.159Z",pi-agent,"QUALITY GATE: npm run release:check PASSED.\n- typecheck (tsc --noEmit): clean\n- build (tsc): clean\n- npm test: 61 tests pass / 0 fail (51 smoke + 10 new atomic)\n- audit:prod: 0 vulnerabilities\n- pack:dry-run: ok (9 files, 50.7 kB)\n- changelog:check: up to date (no regeneration needed)\n\nAtomic test coverage (test/atomic.test.ts, 10 tests):\n1. mid-run failure -> reverse compensation restores created/updated/closed items (failed batch deletes every create; mixed batch restores updates+closes)\n2. resume idempotence -> re-running same fetched set recovers SAME transaction, 0 duplicates\n3. reordered-retry stability -> shuffled fetch order yields SAME transaction id & item ids\n4. --atomic --dry-run -> shared prep path, correct counts (1 import/1 update/1 skip), NO SDK commit call\n5. all-skipped non-dry atomic import -> throws legacy error shape (CommandError, 'Imported 0 issue(s); 1 failed'), before commit\n6. transaction id changes when a mutation target changes (id, id_prefix, status)\nPlus: incomplete compensation reported as partial+resumable; end-to-end update+close commit through the real SDK against a live tracker."
"2026-07-21T12:55:11.201Z",claude-orchestrator,"Orchestrator independent verification (2026-07-21): re-ran the full release gate — 61/61 tests pass, 0 prod vulnerabilities, changelog current, pack clean. Reviewed the atomic design against the merged pm-github reference: deriveAtomicTransactionId hashes team + identifier-sorted canonical entries + exact ordered mutation plan; deriveAtomicItemId keys on the stable Linear identifier (linear-<teamToken>-<identifier>), never fetch order; importLinearAtomic sorts entries by identifier BEFORE building the plan (order-independence). Ran an independent live harness on the built dist/index.js + REAL SDK against throwaway workspaces: (A) atomic import of 5 realistic entries (2 closed) -> 5 items, txn linear-import-333458e94167573e; re-run recovered the SAME transaction, 0 new, 5 items total (no duplicates); (B) SHUFFLED fetch order through importLinearAtomic -> IDENTICAL transaction id and identical managed item ids. Local greptile CLI produced empty output (known flakiness) -> relying on the PR-time Greptile app + CodeRabbit/Sourcery/cubic/semgrep. Ready to push + PR."
"2026-07-21T15:55:36.079Z",claude-orchestrator,"Bot-review hardening round (PR #36): (1) gemini-high + no reopen churn — removed shared status field; create seeds open, update omits status, close owns the closed transition; (2) gemini-high + greptile-P1 — all-filtered batch now returns a successful zero-sync result before touching the SDK (matches legacy path; unblocks cron/CI); (3) greptile-P1 — settings read fault now throws CommandError instead of forking id_prefix to 'pm-' (protects transaction/journal identity so resume can't duplicate items); (4) greptile-P1 — linear-sync importer now forwards --dry-run so --atomic --dry-run previews without writing. +2 tests (63 total). Functionally verified end-to-end against a real throwaway pm workspace via real SDK commitItemMutations: create/close, idempotent recovered resume, tx id stable under reorder."
"2026-07-21T16:09:16.974Z",claude-orchestrator,"Static-analysis cleanup (DeepScan '1 new issue' on PR #36): removed genuine dead code surfaced by strict tsc (noUnusedLocals/noUnusedParameters) — the unused, unexported KNOWN_LINEAR_FIELDS const (parseFieldMap accepts any key, never referenced it) and the unused 'options' parameter on preflightLinear (2 call sites updated). Build+typecheck+63 tests green. All other analyzers clean (Greptile pass, CodeRabbit full review 0 findings, Hound pass, CI pass)."
"2026-07-21T16:13:05.643Z",claude-orchestrator,"DeepScan 'raw 1 new' root cause: the --atomic block was inserted BEFORE the legacy loop's 'let created=0; let updated=0;' declarations and returns early, so those two assignments are a dead store on the atomic path (a UNUSED_VAR_ASSIGNMENT tsc's noUnusedLocals can't see because the vars are read on the OTHER branch). Moved created/updated declarations down to the legacy loop; kept 'skipped' hoisted (both paths use it). No behavior change; 63 tests green."
"2026-07-21T16:43:05.392Z",claude-orchestrator,"DeepScan '1 new' root-caused via local typescript-eslint strictTypeChecked (TS6 API): a type-aware no-unnecessary-condition/redundant-conversion on defensive SDK-boundary code, NOT a runtime bug (all high-signal rules — floating-promises/await/constant-condition/dupe-keys — clean). The only findings NEW to this branch were 3 lines: assertSdkFunction<F> (type param used once), 'settings?.id_prefix / String(...)', and 'Boolean(result?.recovered)'. Applied behavior-preserving fixes (per pinned SDK types these are provable no-ops): dropped redundant optional-chains + Boolean()/String(); constrained assertSdkFunction<F extends (...args:never[])=>unknown>(fn: F|undefined) so F is used twice (also stronger typing). typecheck+build+63 tests green. User-approved cleanup."
body: ""
Loading