@mmerterden/multi-agent-pipeline 20.25.1 → 20.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/manifest.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.25.1",
4
- "commit": "0eb30e00ade8fca0b4c5c778182d8e7dedac3901",
5
- "generatedAt": "2026-10-14T16:23:55.250Z",
3
+ "version": "20.26.0",
4
+ "commit": "35ac932f54214466d6a63effea9d3a5fdd3f90bb",
5
+ "generatedAt": "2026-10-14T20:14:41.247Z",
6
6
  "algorithm": "sha256",
7
- "fileCount": 1391,
7
+ "fileCount": 1394,
8
8
  "files": {
9
- "CHANGELOG.md": "c2c25cc5f3ca4fb30fe20d0ac8d1182142e4baf0f1d31e6a1f32961d95fc8fee",
9
+ "CHANGELOG.md": "bc477ff23f828f3302ddd5f79cfa52bbbe792b1decbbbce270f46b19740257d1",
10
10
  "LICENSE": "8d8c51aaf1e1538b180c69965fa038db71be86798cc3e26a143bebfefe16a43e",
11
- "README.md": "834be811f7e40d5a3cd5a04f61da666cc5fd746b8499781edfd2d083d88cc775",
12
- "README.tr.md": "ae0a9acf8e367716bbba716f31b800bc4b912d3e98874b3e2123ea8684abbec3",
11
+ "README.md": "c90b8d15713750c621fa8083531cdeb8158e9b67e995192874b6ac9da1dedc45",
12
+ "README.tr.md": "ce7978c53c733cf359c21423fbfed640f20644584ff153aaab473958fdccfdc3",
13
13
  "SECURITY.md": "0eb89553ddcfc56bfe9712315b55a6630ee2f579631aaeccd6ad24f8ac769a0b",
14
14
  "docs/FIGMA_PIPELINE.md": "ea01f3ea5fef1a9cb316040a2d416fcf87dd4f2a14b5497aa00994a0b5dc7e6a",
15
15
  "docs/adr/0001-three-model-triage.md": "79a6f1025469f607a5859add21854052d7433bc213f4aceee2e159b613eeb6a1",
@@ -34,7 +34,7 @@
34
34
  "docs/best-practices.md": "f8b75b0d38b5896a6a852bb30a1edddeb24dab8b5b7b1d9313290e1241c72024",
35
35
  "docs/ecosystem.md": "9b23467fca1bcdc0275e7eccd3428c36181ba9f8cd866778159edd7d65a4d563",
36
36
  "docs/engineering.md": "550528cd47c7d2c921729c68d0782a00ac8e77a4bdfec479f6749f9c3235d188",
37
- "docs/facts.json": "d3ea7392093c784ca410ce811fc57cb55ffb701fa1795c17f4afdabb2fc8006b",
37
+ "docs/facts.json": "7020352c8c1f73a6f9b616d1e3d8060b76d138beb8c68e85e4edc3c4a0cfb8a7",
38
38
  "docs/features.md": "d5fcbe3acc05e054efdf0c43ddfc1a82ef3d463f349ea2ec5b4842fa26d9d49d",
39
39
  "docs/performance.md": "bbf2f3922c25dcba0c1a9e69ef420e3871a729af5b35009a49b48a5d3c5cd151",
40
40
  "docs/recovery-guide.md": "59a8f1b66b34cb9ff5507b3d54dac3b9915e184aa044d9c758d18cac818d510e",
@@ -54,7 +54,7 @@
54
54
  "install/_unattended-profile.mjs": "e1ed82bde35d19105ee12c2a551eb778912b0e5c93ab227fd15c993098d98de0",
55
55
  "install/_usage-lifecycle.mjs": "4b010c4e75d6f8e13b58d80a93cc9407f42ccde781c9ed12dcb8e7faf05e526c",
56
56
  "install/_xcode-skills.mjs": "1a7e20f1e6743324b941c5d3ecac9f94c65fff1534ffd9c63b39cbea4f8d101a",
57
- "install/catalog-history.json": "6e9be7c160b7288084bd859a62e09cd0f4c43be1d1414fee9807aa343f5f7245",
57
+ "install/catalog-history.json": "ae53e8c4bbf1d1f17a91f247f53a5ba79d881ec0251bf25273b68c1482df30ee",
58
58
  "install/claude.mjs": "106ce353ebc19c8319d057ce342e1f989519510465c798399076b5f3d8727904",
59
59
  "install/codex.mjs": "08925fe9f3ecdf509b37f01105652c847475d20fcde2db0d679b9a7d663ca145",
60
60
  "install/copilot.mjs": "41dc394b8a3ae77cfd194ce0ca9bbcafce9bdce349f1f72eeb86569f993e15fe",
@@ -66,7 +66,7 @@
66
66
  "install/templates/multi-agent-autopilot-awake.plist.template": "ddd25c76c56fcbb38a89de8e35637372d70ace993d6bc487a88ca9cfb702fe1f",
67
67
  "install/templates/multi-agent-autopilot.plist.template": "122c8a61cd927edd99375d6e3da40f1d42bcceed5b6641785c8b28bb930bc77e",
68
68
  "install/unattended-profile-legacy.json": "f52e86b998956f09ad306b1735a6c10b58580d7082d8a55665a4c6bb15c3aa1f",
69
- "package.json": "dd4297297078a881bab0fdaeb64d804d3cbe8a73b0ccaad9cf09dd1c77ff5349",
69
+ "package.json": "e2aa2bd877d2f106314385ca681a67071158c9089a5a05e74b4f8a3039c2c39e",
70
70
  "pipeline/agents/android-architect.md": "11fb74543cb0ebcf9525ca6654fe95d53da7229e0bf4a1149b31fd9005abc453",
71
71
  "pipeline/agents/backend-architect.md": "b02a606c301e4714131aed70ae6d16eeb77fbc819a001e2752cff2488d4246de",
72
72
  "pipeline/agents/bulk-reader.md": "6597052cd7d205bac2b1c7633f5232fe1a40730664a913b8eb77efd5a786dc25",
@@ -91,7 +91,7 @@
91
91
  "pipeline/commands/multi-agent/autopilot/SKILL.md": "ae95364c28e21a5d12b8a579180d59963bad3738fcfd978180c5dca6e55a0d02",
92
92
  "pipeline/commands/multi-agent/bug-bash/SKILL.md": "0ae5883b72239c74f4f64fc2d60526091e471afa6a2886f90f70a469a85cc832",
93
93
  "pipeline/commands/multi-agent/build-optimize/SKILL.md": "e74d6f5be703acef1704092d5fe53485d95fe6c220bb768accaf68ebd212b7c1",
94
- "pipeline/commands/multi-agent/channels/SKILL.md": "5c6d54c7153ceba6b19ecd55f2211d5ef0f97c472aac0623729fbe9f2fb9a3c0",
94
+ "pipeline/commands/multi-agent/channels/SKILL.md": "4ccd28e5eaa8bd0b618fc23dbf49f007cf65150a4796aabafdb51d1dcffd6752",
95
95
  "pipeline/commands/multi-agent/complaint-analysis/SKILL.md": "b9ab7b9444cf99f25868d90f050ed96f04ac8cd8ea18d0999376184b067fc2bc",
96
96
  "pipeline/commands/multi-agent/create-jira/SKILL.md": "ac3d3e0ab2fb079b7c4107928573912699f68600f922b2f453be49c65317665a",
97
97
  "pipeline/commands/multi-agent/design-check/SKILL.md": "d3cb1b50f2d14cf22a38f4c0db98c852e8ed0586e969f7e6da7c1d65f36554eb",
@@ -168,7 +168,7 @@
168
168
  "pipeline/contract/frozen/toolbox.json": "d52f94ca81f62e4c0d67f0718ce6361f332f289b09fe7cc25c8f5f40feb0ffff",
169
169
  "pipeline/contract/manifest.json": "e9163ba923619447f872416d7f57ea48d67dc13c356c9544f24957f4131946bf",
170
170
  "pipeline/contract/types/index.d.ts": "1434e70701023a7274fb13c9dbdf901b2a0d7259e46f3fcf40fe2454300c32a3",
171
- "pipeline/lib/_jira-auth.sh": "fc468736057f8841d824eb1a4db527442562f8979fea333ebaef5527f4369e9f",
171
+ "pipeline/lib/_jira-auth.sh": "b5fd733ac26727eda299976c688456e95611ca40acafd3947c66c445d77f2237",
172
172
  "pipeline/lib/account-resolver.sh": "f7bef9beba3f839268509361651be0b1e1f319f2f23821f90ba466fdf185a8f6",
173
173
  "pipeline/lib/agent-model-sync.mjs": "2e0264ca8d8fef761563db219d588fed9067847043f960f68f055c186c20b668",
174
174
  "pipeline/lib/analysis-jira-write.sh": "26e17b8a972819a1649ead8cc4e6f0ff8548dd686c2572107e1777fb384f639d",
@@ -205,7 +205,8 @@
205
205
  "pipeline/lib/invoked-directly.mjs": "a7014f252174414c267cc303a992aa9f3986541c5aad188c87cc6809ba3b69bd",
206
206
  "pipeline/lib/issue-fetcher.sh": "36f6eb41935fa44072cf3c9a92a5698d4ffd762103eb57f733e65687ab47479e",
207
207
  "pipeline/lib/jira-epic-link.sh": "b5f9ce1067cd8e9f0e547da4e681b71e4ff8c145e9a759fc98f83703dfca4d35",
208
- "pipeline/lib/jira-publish.sh": "a32e6a682ae730ddd9c6df64b9ece1fb73c797111c63f5c247d7e733668da6b7",
208
+ "pipeline/lib/jira-family-write.sh": "b29674fd2dc6993636d0c8b6b674a069f37d76775420372a755a1ce3b0f94b95",
209
+ "pipeline/lib/jira-publish.sh": "d80c7c128fb16e67962d087f0e12ecbb396d8c7367bbb4f6d7eecb7cedf60fb3",
209
210
  "pipeline/lib/json-file-lock.mjs": "d8a9075e6fa4fd10d4fb84efd438f92906fb3181455d661ca5fe604c57441524",
210
211
  "pipeline/lib/md2confluence-v3.py": "3677ca47931e4b3d0fd2ce3e11299fc5f6d24799eb8332e37dd60b6c3c4653a2",
211
212
  "pipeline/lib/model-dispatch.sh": "6500a5a7cb6a4c099cf1d01678bc9465a3d4eb1001e1f516dcc5eacd69577158",
@@ -270,7 +271,7 @@
270
271
  "pipeline/multi-agent-refs/channels/board.md": "895edfd4a26f7f5fbe13f0eb5865d30975ed86ee3dbd4bd3bd12340f48001fcb",
271
272
  "pipeline/multi-agent-refs/channels/confluence.md": "1e1d731e9f57efdfe77efab3281d74b7f16e28d3d49699da516225492c551ffc",
272
273
  "pipeline/multi-agent-refs/channels/issue-comment.md": "19df6d8d6c23b5ea88687250dc10dd5fa35ba2b2e279efcb000e83622a0664f6",
273
- "pipeline/multi-agent-refs/channels/jira.md": "7969579dbc69c40743dc547163c5161af06205dece7327e33c9c4775bac96c14",
274
+ "pipeline/multi-agent-refs/channels/jira.md": "193152cb85396dcea312785b2a630c13a9403b9b9b1ae91c1fca719e8b37747e",
274
275
  "pipeline/multi-agent-refs/channels/multi-repo.md": "a309478d8ba253c797904b1ad174be958e4d365b5ff008634ad1d5fc87969474",
275
276
  "pipeline/multi-agent-refs/channels/pr-review-actions.md": "21bf53796ae4ecd59769f9944ad8c7d1cd3a5915b0956be99ec875b847b33c00",
276
277
  "pipeline/multi-agent-refs/channels/pr.md": "abe0101d4f2a7cb1ecd88c4260e9ae9e7d6dd65dd52afbf6c44c573c48d11f20",
@@ -306,6 +307,7 @@
306
307
  "pipeline/multi-agent-refs/features/external-context-injection.md": "be9a0e904efd52535fa18a254567c9d0c3fc2ed12fcbc7a116f7879048303df6",
307
308
  "pipeline/multi-agent-refs/features/free-models.md": "64afaba7e7a77575f4ed6a10905a01330a83f573907fb1a5d5356ae6c80999a0",
308
309
  "pipeline/multi-agent-refs/features/jira-context.md": "7d14802ed9ad1747e1a06dec26f22ff9e8959e03c47d7a018af45b8274aea47c",
310
+ "pipeline/multi-agent-refs/features/jira-family.md": "4be05f296e6a2050355c218ed1cd33b489cd8c89ab798e1e44c7ba45fb9517b8",
309
311
  "pipeline/multi-agent-refs/features/maturity-followup.md": "e663e5bcf0b3b4d4d2a67f4516dbb9fb12520dfbdedb47a9c5f57c13a9a0633e",
310
312
  "pipeline/multi-agent-refs/features/model-fallback.md": "5647b4dbeed6fea21d264f03a85796727a83a001710a8bcbb3932c5a3914d6c5",
311
313
  "pipeline/multi-agent-refs/features/mutation-probe.md": "e6d622ab061f6cdcc16da40311e8e7aeae3e6f3c1d4b76d3849859a48a8ae31a",
@@ -358,7 +360,7 @@
358
360
  "pipeline/multi-agent-refs/phases/log-format.md": "62d51e57cca8445ab40f3c0dc29615e4b5c8e4743286bf9ef0173610e81397aa",
359
361
  "pipeline/multi-agent-refs/phases/modes.md": "0733e20dce458c09f30343db9149fc93bc5a4c9531670b93952f223811dcb1aa",
360
362
  "pipeline/multi-agent-refs/phases/operations.md": "9506a26684abfff1e332d92c2321e840544fd4f049aff9f118731e198dabca94",
361
- "pipeline/multi-agent-refs/phases/phase-0-init.md": "fe67c915e16adc477c45861b4e0b51f0a4c80189f41f3388883296952cd6acf0",
363
+ "pipeline/multi-agent-refs/phases/phase-0-init.md": "c83e485b2be0461fdefe796d37788fcb11360616d07ab8e13c266d039ecd4e28",
362
364
  "pipeline/multi-agent-refs/phases/phase-1-plan.md": "de3f3dc16ae8853740540caa5497d2f18f016dcb7ea0bfc280ca5c136259a078",
363
365
  "pipeline/multi-agent-refs/phases/phase-2-dev.md": "7a87dcf07fc36d877eea92a34c1137dba8cd1f7865c8de83959fb2929bd4c248",
364
366
  "pipeline/multi-agent-refs/phases/phase-3-review.md": "60d3bff0370e3a31cbc7824560bd8598c19da0c4025be71e1df60661202d2a38",
@@ -415,7 +417,7 @@
415
417
  "pipeline/rules/swiftui-qa.md": "8c4afe9e72413e7bdab655f3d17588469e80ac34cb7ad2a690901fc5b6534818",
416
418
  "pipeline/rules/tdd.md": "4c1b3114506aa9794c9bb1e1aee1b1bbb4a993d192342e5697cc00bc2f6ea4a2",
417
419
  "pipeline/rules/testing.md": "695be5520b65451f0aa300a9643df2c05e607ff6ad372201ea34798dc5998bd5",
418
- "pipeline/schemas/agent-state.schema.json": "a53f8ae0e827034cc44df05fb5a42bd04f6d3ef27a3bda782e9511f0efddc096",
420
+ "pipeline/schemas/agent-state.schema.json": "85d9ebfb75f89adf9359320702b07742dff7ecc38b1b7801dbf3de9d4447ede4",
419
421
  "pipeline/schemas/analysis-output.schema.json": "41826f4bf7c96778f8cdbcc68531e984087897d438b7b49ac5a93fc29db47916",
420
422
  "pipeline/schemas/analysis-spec.schema.json": "43770223266be1c94e2cdea585efb6783043353bc5a374613b554596f46e1bcd",
421
423
  "pipeline/schemas/answer-request.schema.json": "29d4211a2a05a230e0707b171b73341d2e56f6b83edb178b731905211aab4e23",
@@ -469,7 +471,7 @@
469
471
  "pipeline/schemas/plan-todos.schema.json": "be4374b7eeb513fb8a7663374defa5b35ae084f72e112faa41b07c46532c50de",
470
472
  "pipeline/schemas/planning-output.schema.json": "6aea9b5b8bbf7f9a227215c29cc30a5b642981a02fe2a81eaf9e8a113f7c3947",
471
473
  "pipeline/schemas/pr-request.schema.json": "ae75c4856349d62834802b1189af516bb6fb3d36646831b7ff2fa61d938afcd0",
472
- "pipeline/schemas/prefs.schema.json": "82b1442c6ebc89f1ae518dce1e61a44c90e76e62a1118af25a1e7cc97ca90741",
474
+ "pipeline/schemas/prefs.schema.json": "e3b983744852a8572d92344cdb88367a892afb3e3ee63ce84e798b92b764b8d7",
473
475
  "pipeline/schemas/repo-profile.schema.json": "1a74ef8330da9a26a5a70955f6f9cbb20eda54a8615efaf86c7993d420a7f04e",
474
476
  "pipeline/schemas/repos.schema.json": "a9bf56f25c66201c170912e3389bd73e4a3f61922410f83e31a6c6ba47aee861",
475
477
  "pipeline/schemas/research-output.schema.json": "54e3ba1b04fdc20fbaa84333b943a8ab95d1395c7b0e0aedc67929184cb49523",
@@ -606,8 +608,9 @@
606
608
  "pipeline/scripts/host-hooks.mjs": "19fa3afc4c5caead2905144fe7727eacaa7c8e1fee5db20cfdfb8879e5aa7704",
607
609
  "pipeline/scripts/issues.mjs": "66c787f1d6be924a61cb8b1504354dd7fb0de89a2afee744aa31435b834ef383",
608
610
  "pipeline/scripts/jira-attach.sh": "300b6eb76ae2179324f70a2131388c0f56ca8bf49397cc858937b4d7f47372db",
611
+ "pipeline/scripts/jira-family.mjs": "b5860c5702ec8c03d0a9ac222fe9728c147445eab32c72173fa077ef82dcf632",
609
612
  "pipeline/scripts/jira-search.sh": "afa2e552eec83bf7ab8bc1aa43b99c7480a82bfbfc808ce22c89df1b57e90563",
610
- "pipeline/scripts/jira-wiki-escape.mjs": "79f5444e4684aaf09afc272e0c99df22c4976d3a9028dc901c9905dbf8861ecf",
613
+ "pipeline/scripts/jira-wiki-escape.mjs": "655a170c1ea0d366a7eac9eb25fbf80ddcd19d1d3315bbf75b210908d14237f9",
611
614
  "pipeline/scripts/keychain-save.sh": "7cc4d81acd88d84d710fa733d5c9604a0acb3d924738216ac7d34040bad7ae29",
612
615
  "pipeline/scripts/keychain.py": "727f55c6e2b87c6078d4a54cc9470b16fc4610abb3a24c60fdd226ae0bf10391",
613
616
  "pipeline/scripts/launch-request.mjs": "826c5ec100cd7def4d55c94894582959c1ce325ffc9527e4b3fe43c89fdd8b10",
@@ -724,7 +727,7 @@
724
727
  "pipeline/scripts/worktrees.mjs": "90dfd09b70fffe9b56c331a4ae2692cc248f4898f4ee6aee4239ef0b20888a90",
725
728
  "pipeline/scripts/write-state.mjs": "e1198bf970eace07d3eff22b394808a526e473df65d3d58aa2d5a12cfdac8b49",
726
729
  "pipeline/scripts/write-store-config.mjs": "b994a061f6febb2ca3d396074d8b51d2c1c1f991d3072d321703380da3639a56",
727
- "pipeline/skills/.skill-manifest.json": "62b0ff370385c7299a9cb13bb36234bbd618a10fcd3d33e28490816741e526cd",
730
+ "pipeline/skills/.skill-manifest.json": "df49c99c3f308b8d5dab153e2ed22b301de7417e52065146d11eb754adf75326",
728
731
  "pipeline/skills/.skills-index.json": "bfb04a72333aec2c25e21c8edce4a76e2128e5e7f1e88b0cd780e7f1cc6bf370",
729
732
  "pipeline/skills/shared/README.md": "47f5c77556bf1676857fe6a803fc5c85da5cd32e1b0332a6f3728b6da610cc58",
730
733
  "pipeline/skills/shared/core/apple-archive-compliance/SKILL.md": "7239fef717ea45b79276bfcc2c3903acd57950b6e00dd6a2a66399c4b09cbd51",
@@ -740,7 +743,7 @@
740
743
  "pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md": "a9731771fbc1f45260f214f069b976eff16e7b25e2f1375b0d9a213575271bc6",
741
744
  "pipeline/skills/shared/core/multi-agent-bug-bash/SKILL.md": "a44ad2520ef24444b68d9040519f82498abda3ebb6467890bb6b5e1460299400",
742
745
  "pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md": "8534891d1198597a0893d131e6b944d15443f62b24cb5584c486a235845fd281",
743
- "pipeline/skills/shared/core/multi-agent-channels/SKILL.md": "03f7c648e1938f3d6d7c48d4c1c01f2bd734cdce873ec5203120b5be3ae761db",
746
+ "pipeline/skills/shared/core/multi-agent-channels/SKILL.md": "cb5e1f9e82f3fd3e7d9ad20bff7a066cafff75323044a08726e3af368738b123",
744
747
  "pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md": "dd30708491303902aa8629130809e9601c2096dc12aa9fc671f798142cea3485",
745
748
  "pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md": "5aad0d841e9e5b7140bb441e07819209ca64c5b6fc209d6ad586b5fbb353e377",
746
749
  "pipeline/skills/shared/core/multi-agent-design-check/SKILL.md": "8888744fa52e935f53db9a1068e1b18c636b6c9d55b084dabfc19b9ae68bf6f8",
@@ -796,7 +799,7 @@
796
799
  "pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md": "b6c360c330ef0094cd7e7d9624e89624446b048951e43a180766bbe67d1b725c",
797
800
  "pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md": "9dccf78ffc2fb014d83f67d683d395f56f4fad8b5a36b9aa84e6cbd075b58818",
798
801
  "pipeline/skills/shared/core/multi-agent-update/SKILL.md": "2f9e224e240e1c9053a59066678f869ae36f18c088482488f3bda8fd207148ea",
799
- "pipeline/skills/shared/core/multi-agent/SKILL.md": "b2435de3df518efb8ddf9ac89b3089449e46bf55f29220e07a3aa95dfc8cd083",
802
+ "pipeline/skills/shared/core/multi-agent/SKILL.md": "079b49a1981c63d90798089a343b7f324571c4f7cc6ad3fb757e82719c35eb0a",
800
803
  "pipeline/skills/shared/external/NOTICE-avdlee-swiftui-agent-skill.md": "400842e4653f038fad22509fe3f3d152151120758f5f9b170e6dba59afaf4a48",
801
804
  "pipeline/skills/shared/external/NOTICE-dimillian-skills.md": "f9c3d783fd3563418c18291b90899251e890fd3fa8ee353c8c1fbbe61078526e",
802
805
  "pipeline/skills/shared/external/NOTICE-paul-hudson-skills.md": "bca5fe8779df70fb0f234483d65ff3562c67521e719b69b22aac260803b75244",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.25.1",
3
+ "version": "20.26.0",
4
4
  "description": "6-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -352,7 +352,7 @@ Aggregated Markdown from Step 5 → PR description. GitHub uses `gh pr edit --bo
352
352
  Full contract: [`$HOME/.claude/multi-agent-refs/channels/pr.md`]($HOME/.claude/multi-agent-refs/channels/pr.md) - Bitbucket payload assembly snippet, version-mismatch retry, `--ready` promotion, multi-repo cross-link block.
353
353
 
354
354
  #### Adapter: Jira comment
355
- Sections: `summary` + `test_scenarios` + `impact` + `context_refs` only - the technical ones go to the PR and Confluence (`channels/jira.md` fixes the order). The PR URL goes on line 1, above the first heading. The wiki-markup conversion table lives in that same ref and only there: the four-row summary that used to sit here mapped `### ...` to `*...*`, which is italic, not a heading - a shrunken copy of a table is a second answer, and it was the wrong one.
355
+ Sections: `summary` + `test_scenarios` + `impact` + `context_refs` only - the technical ones go to the PR and Confluence (`channels/jira.md` fixes the order). The PR URL goes on line 1, above the first heading. The wiki-markup conversion table lives in that same ref and only there.
356
356
 
357
357
  Then post it - do not assemble the request:
358
358
 
@@ -360,10 +360,12 @@ Then post it - do not assemble the request:
360
360
  bash "$HOME/.claude/lib/jira-publish.sh" --issue "$JIRA_ID" --body-file "$F" --target comment
361
361
  ```
362
362
 
363
- The publisher escapes every body (Jira renders `:)` `(x)` `(!)` `(/)` as emoticon images, and a Swift selector like `login(source:input:)` ends in `:)`) and resolves the token from `keychainMapping.jira` without putting it on argv. The hand-rolled `jq`+`curl` this replaced kept the escape as a separate step, and a shipped comment turned `hash(into:)` into a smiley.
363
+ The publisher escapes every body (Jira renders `:)` `(x)` `(!)` `(/)` as emoticon images, and a Swift selector like `login(source:input:)` ends in `:)`) and resolves the token from `keychainMapping.jira` without putting it on argv.
364
364
 
365
365
  Full contract: [`$HOME/.claude/multi-agent-refs/channels/jira.md`]($HOME/.claude/multi-agent-refs/channels/jira.md) - full conversion table, multi-repo PR-list prepend, Wiki→Jira triad interaction.
366
366
 
367
+ **Issue family** (`state.jiraFamily` set): instead, `bash $HOME/.claude/lib/jira-family-write.sh plan --state "$STATE_FILE" --dev-body "$F"`, the Step 6 preview as one multi-select under `plan.confirm`, then `apply --state "$STATE_FILE"`: `$HOME/.claude/multi-agent-refs/features/jira-family.md`.
368
+
367
369
  #### Adapter: Confluence page
368
370
  Token from `keychainMapping.confluence`; missing/expired triggers the inline Save Flow from `setup.md`. Parent page picked from `prefs.projects[<project>].confluenceUrls` recents (LRU, max 5) or paste new URL. Auto-suggest title `"{jiraId} - {taskTitle}"`. Body converted to Confluence storage format (`<h3>`, `<table>`, `<ac:structured-macro>` code blocks). POST `/rest/api/content`; URL saved back to recents.
369
371
 
@@ -38,7 +38,9 @@
38
38
 
39
39
  # shellcheck shell=bash
40
40
 
41
- JIRA_AUTH_PREFS="${JIRA_AUTH_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
41
+ # MULTI_AGENT_PREFS is the same override jira-publish.sh reads, so a read and
42
+ # the write after it resolve the same host and token.
43
+ JIRA_AUTH_PREFS="${JIRA_AUTH_PREFS:-${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}}"
42
44
 
43
45
  _jira_auth_pref() { # _jira_auth_pref <jq path> -> value or empty
44
46
  [ -f "$JIRA_AUTH_PREFS" ] || { printf ''; return 0; }
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # jira-family-write.sh - fetch the issue family of a Jira-started run, and
4
+ # apply the writes scripts/jira-family.mjs planned for it.
5
+ #
6
+ # The planning half is offline and testable; this half is the one that talks
7
+ # to Jira, so it stays small. Every write goes through jira-publish.sh, which
8
+ # escapes the body, runs the outbound gate and backs a description up before it
9
+ # replaces one. A write the plan marks `raw` (the story reformat: the author's
10
+ # lines plus escaped macros) is sent with --raw, so the author's lines reach
11
+ # Jira unchanged. Reads go through jira_api in _jira-auth.sh.
12
+ #
13
+ # A write is checked against the site right before it is sent:
14
+ # comment skipped when a comment by the authenticated user on the issue
15
+ # already carries its marker
16
+ # description skipped when the story already conforms, or when it changed
17
+ # since the plan read it (jira-publish.sh --expect-file)
18
+ # so running the same plan twice writes nothing the second time.
19
+ #
20
+ # Usage, from a run (the state file is agent-state.json; the family's files go
21
+ # to jira-family/ beside it):
22
+ # jira-family-write.sh resolve --state FILE
23
+ # fetch and resolve the family, record state.jiraFamily; no Jira write
24
+ # jira-family-write.sh plan --state FILE --dev-body FILE [--skip id,id]
25
+ # [--dev-task KEY] [--test-task KEY] [--build TEXT]
26
+ # re-resolve and plan; run id, PR URL and mode come from the state
27
+ # jira-family-write.sh apply --state FILE [--dry-run]
28
+ # apply jira-family/plan.json, write jira-family/writes.json, merge it
29
+ # into state.jiraFamily.writes by write id
30
+ #
31
+ # Lower level:
32
+ # jira-family-write.sh fetch --issue KEY --out DIR
33
+ # DIR/input.json, DIR/story.json (the parent when KEY is a sub-task, else
34
+ # the same issue), DIR/site-types.txt (empty when unreadable)
35
+ # jira-family-write.sh apply --plan FILE [--state FILE] [--result FILE] [--dry-run]
36
+ # --state the plan's autopilot decisions are recorded there
37
+ # (decisions-log.mjs) once their write has gone out
38
+ # --result the per-write outcome as JSON, written on every exit
39
+ #
40
+ # Exit: 0 done, 3 the input does not parse or the plan still has questions,
41
+ # 4 auth (also when a write failed on auth), 5 a write failed (the others
42
+ # went out; --result lists which, and a re-run sends only what is missing),
43
+ # 6 unattended (nothing read or written; the runner owns Jira),
44
+ # 64 usage or a malformed issue key.
45
+
46
+ set -uo pipefail
47
+
48
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
49
+ # shellcheck source=/dev/null
50
+ . "$SELF_DIR/_jira-auth.sh"
51
+ # shellcheck source=/dev/null
52
+ . "$SELF_DIR/unattended.sh"
53
+
54
+ USAGE="usage:
55
+ jira-family-write.sh resolve --state FILE
56
+ jira-family-write.sh plan --state FILE --dev-body FILE [--skip id,id] [--dev-task KEY] [--test-task KEY] [--build TEXT]
57
+ jira-family-write.sh apply --state FILE [--plan FILE] [--result FILE] [--dry-run]
58
+ jira-family-write.sh fetch --issue KEY --out DIR
59
+ jira-family-write.sh apply --plan FILE [--state FILE] [--result FILE] [--dry-run]"
60
+ usage() { echo "$USAGE" >&2; exit 64; }
61
+
62
+ CMD="${1:-}"; [ -n "$CMD" ] || usage; shift
63
+ case "$CMD" in -h|--help) sed -n '2,45p' "$0"; exit 0 ;; esac
64
+ ISSUE=""; OUT=""; PLAN=""; STATE=""; RESULT=""; DRY=0
65
+ DEV_BODY=""; SKIP=""; DEV_TASK=""; TEST_TASK=""; BUILD=""
66
+ while [ $# -gt 0 ]; do
67
+ case "$1" in
68
+ -h|--help) sed -n '2,45p' "$0"; exit 0 ;;
69
+ --dev-body) DEV_BODY="${2:-}"; shift 2 ;;
70
+ --skip) SKIP="${2:-}"; shift 2 ;;
71
+ --dev-task) DEV_TASK="${2:-}"; shift 2 ;;
72
+ --test-task) TEST_TASK="${2:-}"; shift 2 ;;
73
+ --build) BUILD="${2:-}"; shift 2 ;;
74
+ --issue) ISSUE="${2:-}"; shift 2 ;;
75
+ --out) OUT="${2:-}"; shift 2 ;;
76
+ --plan) PLAN="${2:-}"; shift 2 ;;
77
+ --state) STATE="${2:-}"; shift 2 ;;
78
+ --result) RESULT="${2:-}"; shift 2 ;;
79
+ --dry-run) DRY=1; shift ;;
80
+ *) usage ;;
81
+ esac
82
+ done
83
+
84
+ command -v jq >/dev/null 2>&1 || { echo "ERR: jq is required" >&2; exit 3; }
85
+
86
+ FAMILY_MJS="$SELF_DIR/../scripts/jira-family.mjs"
87
+ DECISIONS_MJS="$SELF_DIR/../scripts/decisions-log.mjs"
88
+ PUBLISH="$SELF_DIR/jira-publish.sh"
89
+ FIELDS="summary,issuetype,parent,subtasks,description,project,status"
90
+
91
+ # A key goes into a REST path, so anything but a key is refused before use.
92
+ valid_key() { printf '%s' "$1" | grep -Eq '^[A-Z][A-Z0-9_]+-[0-9]+$'; }
93
+
94
+ # What a jira-publish.sh exit code means, for the result line.
95
+ publish_reason() {
96
+ case "$1" in
97
+ 4) echo "Jira host or token could not be resolved (exit 4)" ;;
98
+ 5) echo "the Jira call failed (exit 5)" ;;
99
+ 64) echo "refused before sending, e.g. by the outbound gate (exit 64): $2" ;;
100
+ *) echo "jira-publish.sh exit $1: $2" ;;
101
+ esac
102
+ }
103
+
104
+ fetch_issue() { # fetch_issue <key> <file>
105
+ jira_api GET "/rest/api/2/issue/$1?fields=$FIELDS" > "$2" 2>/dev/null || return 5
106
+ jq -e '.key' "$2" >/dev/null 2>&1 || return 5
107
+ }
108
+
109
+ do_fetch() { # do_fetch <key> <dir>
110
+ local issue="$1" out="$2" parent project
111
+ valid_key "$issue" || { echo "ERR: not an issue key: $issue" >&2; return 64; }
112
+ if ma_unattended; then
113
+ echo "unattended run: the family is not read here; the runner owns Jira" >&2
114
+ return 6
115
+ fi
116
+ jira_auth_resolve || return 4
117
+ mkdir -p "$out"
118
+ fetch_issue "$issue" "$out/input.json" || { echo "ERR: could not read $issue" >&2; return 5; }
119
+ parent="$(jq -r 'if .fields.issuetype.subtask == true then .fields.parent.key // empty else empty end' "$out/input.json")"
120
+ if [ -n "$parent" ] && ! valid_key "$parent"; then
121
+ echo "WARN: the parent key does not look like a key: $parent" >&2; parent=""
122
+ fi
123
+ if [ -n "$parent" ]; then
124
+ fetch_issue "$parent" "$out/story.json" || { echo "WARN: could not read the parent $parent" >&2; rm -f "$out/story.json"; }
125
+ else
126
+ cp "$out/input.json" "$out/story.json"
127
+ fi
128
+ project="$(jq -r '.fields.project.key // empty' "$out/input.json")"
129
+ [ -n "$project" ] || project="${issue%%-*}"
130
+ bash "$SELF_DIR/analysis-jira-write.sh" --subtask-types --project "$project" \
131
+ > "$out/site-types.txt" 2>/dev/null || : > "$out/site-types.txt"
132
+ echo "fetched $issue${parent:+ (story $parent)} into $out"
133
+ }
134
+
135
+ # The run's own files: the issue key and the family directory beside agent-state.json.
136
+ state_key() { jq -r '.jiraId // .taskId // empty' "$STATE"; }
137
+ family_dir() { printf '%s/jira-family' "$(cd "$(dirname "$STATE")" && pwd)"; }
138
+
139
+ do_resolve() { # fetch, resolve, and record state.jiraFamily; asks and writes nothing in Jira
140
+ local key dir story=""
141
+ key="$(state_key)"; dir="$(family_dir)"
142
+ do_fetch "$key" "$dir" >/dev/null || return $?
143
+ [ -f "$dir/story.json" ] && story="--story $dir/story.json"
144
+ # shellcheck disable=SC2086
145
+ node "$FAMILY_MJS" resolve --input "$dir/input.json" $story \
146
+ --site-types "$dir/site-types.txt" > "$dir/family.json" || return 3
147
+ }
148
+
149
+ if [ "$CMD" = "fetch" ]; then
150
+ [ -n "$ISSUE" ] && [ -n "$OUT" ] || usage
151
+ do_fetch "$ISSUE" "$OUT"
152
+ exit $?
153
+ fi
154
+
155
+ WRITE_STATE="$SELF_DIR/../scripts/write-state.mjs"
156
+
157
+ if [ "$CMD" = "resolve" ]; then
158
+ [ -n "$STATE" ] && [ -f "$STATE" ] || usage
159
+ do_resolve || exit $?
160
+ DIR="$(family_dir)"
161
+ jq -n --slurpfile f "$DIR/family.json" --slurpfile s "$STATE" \
162
+ '{jiraFamily: ($f[0].stateEntry + {writes: ($s[0].jiraFamily.writes // [])})}' \
163
+ | node "$WRITE_STATE" "$STATE" || exit 3
164
+ jq -r '"family: story \(.stateEntry.storyKey), dev \(.stateEntry.devTaskKey // "-"), test \(.stateEntry.testTaskKey // "-"), story format \(.stateEntry.storyFormat.status)"' "$DIR/family.json"
165
+ exit 0
166
+ fi
167
+
168
+ if [ "$CMD" = "plan" ]; then
169
+ [ -n "$STATE" ] && [ -f "$STATE" ] && [ -n "$DEV_BODY" ] && [ -f "$DEV_BODY" ] || usage
170
+ do_resolve || exit $?
171
+ DIR="$(family_dir)"
172
+ RUN_ID="$(jq -r '"\(.taskId)-\(.shortId // 0)"' "$STATE")"
173
+ PR_URL="$(jq -r '.pr.url // .projects[0].pr.url // empty' "$STATE")"
174
+ MODE="$(jq -r 'if .autopilot == true then "autopilot" else "interactive" end' "$STATE")"
175
+ set -- --family "$DIR/family.json" --dev-body "$DEV_BODY" --run-id "$RUN_ID" --mode "$MODE"
176
+ [ -z "$PR_URL" ] || set -- "$@" --pr-url "$PR_URL"
177
+ [ -z "$BUILD" ] || set -- "$@" --build "$BUILD"
178
+ [ -z "$SKIP" ] || set -- "$@" --skip "$SKIP"
179
+ [ -z "$DEV_TASK" ] || set -- "$@" --dev-task "$DEV_TASK"
180
+ [ -z "$TEST_TASK" ] || set -- "$@" --test-task "$TEST_TASK"
181
+ node "$FAMILY_MJS" plan "$@" > "$DIR/plan.json" || exit 3
182
+ jq -r '.confirm, (.skipped[] | " not written: \(.id) (\(.reason))"), (.questions[] | " question \(.id): \(.question) [\(.options | join(", "))]")' "$DIR/plan.json"
183
+ echo "plan: $DIR/plan.json"
184
+ exit 0
185
+ fi
186
+
187
+ [ "$CMD" = "apply" ] || usage
188
+ MERGE=0
189
+ if [ -z "$PLAN" ] && [ -n "$STATE" ] && [ -f "$STATE" ]; then
190
+ PLAN="$(family_dir)/plan.json"
191
+ [ -n "$RESULT" ] || RESULT="$(family_dir)/writes.json"
192
+ MERGE=1
193
+ fi
194
+ [ -n "$PLAN" ] && [ -f "$PLAN" ] || usage
195
+
196
+ # Every exit leaves a result file, so the caller's follow-up never reads nothing.
197
+ early_exit() { # early_exit <code> <status> <reason>
198
+ if [ -n "$RESULT" ]; then
199
+ jq -c --arg s "$2" --arg r "$3" '[.writes[]? | {id, issue, status: $s, reason: $r}]' "$PLAN" \
200
+ > "$RESULT" 2>/dev/null || echo '[]' > "$RESULT"
201
+ fi
202
+ exit "$1"
203
+ }
204
+ if ma_unattended; then
205
+ echo "unattended run: the family writes are left to the runner" >&2
206
+ early_exit 6 skipped "unattended run: left to the runner"
207
+ fi
208
+ jq -e . "$PLAN" >/dev/null 2>&1 || { echo "ERR: the plan does not parse: $PLAN" >&2; early_exit 3 failed "the plan does not parse"; }
209
+ if [ "$(jq -r '.ready' "$PLAN")" != "true" ]; then
210
+ echo "ERR: the plan still has open questions ($(jq -r '[.questions[].id] | join(", ")' "$PLAN")); answer them and re-plan" >&2
211
+ early_exit 3 skipped "the plan still has open questions"
212
+ fi
213
+ [ "$DRY" -eq 1 ] || jira_auth_resolve || exit 4
214
+
215
+ WORK="$(mktemp -d)"
216
+ trap 'rm -rf "$WORK"' EXIT
217
+ RESULTS="$WORK/results.jsonl"; : > "$RESULTS"
218
+ ME=""
219
+ RC=0
220
+
221
+ record() { # record <id> <issue> <status> <reason>
222
+ jq -cn --arg i "$1" --arg k "$2" --arg s "$3" --arg r "$4" \
223
+ '{id: $i, issue: $k, status: $s, reason: $r}' >> "$RESULTS"
224
+ printf ' %-8s %-12s %s%s\n' "$3" "$1" "$2" "${4:+ ($4)}"
225
+ }
226
+
227
+ fail_rc() { # an auth failure (4) outranks any other failure (5) in the exit code
228
+ if [ "$1" -eq 4 ]; then RC=4; elif [ "$RC" -ne 4 ]; then RC=5; fi
229
+ }
230
+
231
+ record_decisions() { # record_decisions <writeId>, or "" for the decisions that made no write
232
+ [ -n "$STATE" ] || return 0
233
+ jq -c --arg w "$1" '.decisions[]? | select(if $w == "" then .writeId == null else .writeId == $w end)' "$PLAN" | while IFS= read -r d; do
234
+ node "$DECISIONS_MJS" add --state "$STATE" \
235
+ --what "$(printf '%s' "$d" | jq -r .what)" \
236
+ --why "$(printf '%s' "$d" | jq -r .why)" \
237
+ --cost-if-wrong "$(printf '%s' "$d" | jq -r .costIfWrong)" \
238
+ --phase "$(printf '%s' "$d" | jq -r .phase)" >/dev/null 2>&1 \
239
+ || echo " WARN: could not record the decision for $1" >&2
240
+ done
241
+ }
242
+
243
+ echo "jira-family: $(jq -r '.confirm' "$PLAN")$([ "$DRY" -eq 1 ] && echo " (dry run)")"
244
+ N="$(jq '.writes | length' "$PLAN")"
245
+ i=0
246
+ while [ "$i" -lt "$N" ]; do
247
+ W="$(jq -c ".writes[$i]" "$PLAN")"
248
+ i=$((i + 1))
249
+ id="$(printf '%s' "$W" | jq -r .id)"
250
+ kind="$(printf '%s' "$W" | jq -r .kind)"
251
+ issue="$(printf '%s' "$W" | jq -r .issue)"
252
+ printf '%s' "$W" | jq -j .body > "$WORK/body.txt"
253
+ if ! valid_key "$issue"; then
254
+ record "$id" "$issue" "failed" "not an issue key"; RC=5; continue
255
+ fi
256
+ if [ "$DRY" -eq 1 ]; then
257
+ record "$id" "$issue" "dry-run" "$kind"
258
+ continue
259
+ fi
260
+ if [ "$kind" = "comment" ]; then
261
+ mark="$(printf '%s' "$W" | jq -r .marker)"
262
+ if ! jira_api GET "/rest/api/2/issue/$issue/comment?maxResults=1000" > "$WORK/comments.json" 2>/dev/null \
263
+ || ! jq -e '.comments' "$WORK/comments.json" >/dev/null 2>&1; then
264
+ record "$id" "$issue" "failed" "could not read the comments"; RC=5; continue
265
+ fi
266
+ if [ -z "$ME" ]; then
267
+ jira_api GET "/rest/api/2/myself" > "$WORK/me.json" 2>/dev/null || :
268
+ ME="$(jq -c '{name, key, accountId} | with_entries(select(.value != null))' "$WORK/me.json" 2>/dev/null || echo "")"
269
+ fi
270
+ if [ -z "$ME" ] || [ "$ME" = "{}" ]; then
271
+ ME=""; record "$id" "$issue" "failed" "could not read the authenticated user"; RC=5; continue
272
+ fi
273
+ # Only our own comments count: another user can paste a marker, and it
274
+ # must not be able to stop this one from being posted.
275
+ seen="$(jq --arg m "$mark" --argjson me "$ME" '[.comments[]?
276
+ | select((.author // {}) as $a | any($me | to_entries[]; $a[.key] == .value))
277
+ | .body // "" | select(contains($m))] | length' "$WORK/comments.json")"
278
+ if [ "${seen:-0}" -gt 0 ]; then
279
+ record "$id" "$issue" "exists" "already posted by this run"; continue
280
+ fi
281
+ bash "$PUBLISH" --issue "$issue" --body-file "$WORK/body.txt" --target comment >/dev/null 2>"$WORK/err.txt"
282
+ prc=$?
283
+ if [ "$prc" -eq 0 ]; then
284
+ record "$id" "$issue" "written" ""
285
+ record_decisions "$id"
286
+ else
287
+ record "$id" "$issue" "failed" "$(publish_reason "$prc" "$(head -1 "$WORK/err.txt")")"
288
+ fail_rc "$prc"
289
+ fi
290
+ continue
291
+ fi
292
+ # description
293
+ if ! jira_api GET "/rest/api/2/issue/$issue?fields=description" > "$WORK/current.json" 2>/dev/null \
294
+ || ! jq -e '.fields' "$WORK/current.json" >/dev/null 2>&1; then
295
+ record "$id" "$issue" "failed" "could not read the description"; RC=5; continue
296
+ fi
297
+ jq -j '.fields.description // ""' "$WORK/current.json" > "$WORK/current.txt"
298
+ printf '%s' "$W" | jq -j '.original // ""' > "$WORK/original.txt"
299
+ jq '{global: {issueTree: (if .storyPanels == null then {} else {storyPanels: .storyPanels} end)}}' \
300
+ "$PLAN" > "$WORK/plan-prefs.json"
301
+ if node "$FAMILY_MJS" check --description "$WORK/current.txt" --prefs "$WORK/plan-prefs.json" >/dev/null 2>&1; then
302
+ record "$id" "$issue" "exists" "already in the story format"; continue
303
+ fi
304
+ if ! cmp -s "$WORK/current.txt" "$WORK/original.txt"; then
305
+ record "$id" "$issue" "skipped" "the description changed since the preview"; continue
306
+ fi
307
+ raw=""
308
+ [ "$(printf '%s' "$W" | jq -r '.raw == true')" = "true" ] && raw="--raw"
309
+ bash "$PUBLISH" --issue "$issue" --body-file "$WORK/body.txt" --target description \
310
+ --mode replace --confirm-overwrite --expect-file "$WORK/original.txt" $raw >/dev/null 2>"$WORK/err.txt"
311
+ prc=$?
312
+ if [ "$prc" -eq 0 ]; then
313
+ record "$id" "$issue" "written" "previous text backed up"
314
+ record_decisions "$id"
315
+ elif [ "$prc" -eq 6 ]; then
316
+ record "$id" "$issue" "skipped" "the description changed since the preview"
317
+ else
318
+ record "$id" "$issue" "failed" "$(publish_reason "$prc" "$(head -1 "$WORK/err.txt")")"
319
+ fail_rc "$prc"
320
+ fi
321
+ done
322
+
323
+ [ "$DRY" -eq 1 ] || record_decisions ""
324
+ [ -z "$RESULT" ] || jq -s '.' "$RESULTS" > "$RESULT"
325
+ # Merged by write id: a re-run that finds a comment already there ("exists")
326
+ # keeps the earlier "written" record instead of replacing it.
327
+ if [ "$MERGE" -eq 1 ] && [ "$DRY" -eq 0 ]; then
328
+ jq -n --slurpfile s "$STATE" --slurpfile n "$RESULT" '
329
+ ($s[0].jiraFamily // {}) as $f
330
+ | ($f.writes // []) as $old
331
+ | ($n[0] | map({(.id): .}) | add // {}) as $new
332
+ | ($old | map(.id)) as $oldIds
333
+ | {jiraFamily: ($f + {writes: (
334
+ [$old[] | . as $o | ($new[$o.id] // null) as $x
335
+ | if $x == null or ($o.status == "written" and $x.status == "exists") then $o else $x end]
336
+ + [$n[0][] | select(.id as $i | $oldIds | index($i) | not)])})}' \
337
+ | node "$WRITE_STATE" "$STATE" >/dev/null || echo " WARN: could not record the writes in $STATE" >&2
338
+ fi
339
+ exit "$RC"
@@ -19,6 +19,8 @@
19
19
  # [--mode append|replace] (description only, default: append)
20
20
  # [--confirm-overwrite] (required for mode=replace on a non-empty field)
21
21
  # [--backup-dir DIR]
22
+ # [--raw] (body is already Jira wiki markup; no emoticon escape)
23
+ # [--expect-file FILE] (description only: write only if it still equals FILE)
22
24
  # [--dry-run]
23
25
  #
24
26
  # Resolution:
@@ -29,6 +31,9 @@
29
31
  #
30
32
  # Every body is run through scripts/jira-wiki-escape.mjs before it is sent, so
31
33
  # Jira cannot manufacture an emoticon out of a Swift selector or a table cell.
34
+ # --raw skips that one step for a body built only from text the author already
35
+ # wrote in Jira markup plus macros the caller escaped itself (the story reformat
36
+ # of lib/jira-family-write.sh); the outbound gate and the backup still apply.
32
37
  #
33
38
  # Exit codes:
34
39
  # 0 published (or, with --dry-run, previewed)
@@ -36,6 +41,8 @@
36
41
  # --confirm-overwrite was given. The backup is already on disk.
37
42
  # 4 host or token could not be resolved
38
43
  # 5 a Jira API call failed
44
+ # 6 refused: --expect-file no longer matches the description (someone
45
+ # edited it after the caller read it). Nothing was written.
39
46
  # 64 usage error
40
47
 
41
48
  set -euo pipefail
@@ -91,6 +98,8 @@ TARGET="comment"
91
98
  MODE="append"
92
99
  CONFIRM=0
93
100
  DRY_RUN=0
101
+ RAW=0
102
+ EXPECT=""
94
103
  BACKUP_DIR="${JIRA_BACKUP_DIR:-$HOME/.claude/logs/multi-agent/jira-backups}"
95
104
 
96
105
  die() { echo "$1" >&2; exit "${2:-64}"; }
@@ -106,6 +115,8 @@ while [ "$#" -gt 0 ]; do
106
115
  --backup-dir) need_value "$@"; BACKUP_DIR="$2"; shift 2 ;;
107
116
  --confirm-overwrite) CONFIRM=1; shift ;;
108
117
  --dry-run) DRY_RUN=1; shift ;;
118
+ --raw) RAW=1; shift ;;
119
+ --expect-file) need_value "$@"; EXPECT="$2"; shift 2 ;;
109
120
  -h|--help) sed -n '2,40p' "$0"; exit 0 ;;
110
121
  *) die "unknown argument: $1" ;;
111
122
  esac
@@ -148,7 +159,16 @@ api() { "$CURL" -sS -m 30 -K <(auth_cfg "$TOKEN") -H "Content-Type: application/
148
159
  WORK="$(mktemp -d)"
149
160
  trap 'rm -rf "$WORK"' EXIT
150
161
 
151
- node "$ESCAPER" "$BODY_FILE" > "$WORK/new.txt"
162
+ if [ "$RAW" = "1" ]; then
163
+ cp "$BODY_FILE" "$WORK/new.txt"
164
+ else
165
+ node "$ESCAPER" "$BODY_FILE" > "$WORK/new.txt"
166
+ fi
167
+ # An escaper that printed nothing for a body that has text would post an empty
168
+ # comment or blank a description; that is refused, never sent.
169
+ if [ -s "$BODY_FILE" ] && [ ! -s "$WORK/new.txt" ]; then
170
+ die "the escaped body came out empty; nothing was posted" 5
171
+ fi
152
172
 
153
173
  if [ "$TARGET" = "comment" ]; then
154
174
  if [ "$DRY_RUN" = "1" ]; then
@@ -175,6 +195,13 @@ case "$http" in
175
195
  *) echo "could not read the current description of $ISSUE (HTTP $http)" >&2; exit 5 ;;
176
196
  esac
177
197
  jq -r '.fields.description // ""' "$WORK/issue.json" > "$WORK/existing.txt"
198
+ if [ -n "$EXPECT" ]; then
199
+ jq -j '.fields.description // ""' "$WORK/issue.json" > "$WORK/existing-exact.txt"
200
+ if ! cmp -s "$WORK/existing-exact.txt" "$EXPECT"; then
201
+ echo "REFUSED: the description of $ISSUE changed since it was read; nothing was written." >&2
202
+ exit 6
203
+ fi
204
+ fi
178
205
 
179
206
  existing_bytes=$(wc -c < "$WORK/existing.txt" | tr -d ' ')
180
207
  existing_lines=$(grep -c . "$WORK/existing.txt" || true)
@@ -232,6 +232,8 @@ Jira's wiki renderer turned it into an emoticon, which is what section
232
232
  *Emoticon escaping* below is about. A defence that has to be invoked by hand is
233
233
  a defence that is eventually not invoked.
234
234
 
235
+ A run with `state.jiraFamily` posts this body on the development sub-task, plus a test comment on the test sub-task: `features/jira-family.md`.
236
+
235
237
  The dispatch summary line includes the comment URL with `?focusedCommentId=...` so the user can paste it into Slack/Teams.
236
238
 
237
239
  ### Writing the issue description (not a comment)