@mmerterden/multi-agent-pipeline 20.23.0 → 20.23.2

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,12 +1,12 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.23.0",
4
- "commit": "a64fcccc1e81e70293ec18d96f00b1bfc861dbe0",
5
- "generatedAt": "2026-10-13T11:40:10.540Z",
3
+ "version": "20.23.2",
4
+ "commit": "25bf3bd432f8e4da861b031ad8d61deaa2c56e05",
5
+ "generatedAt": "2026-10-13T21:30:35.928Z",
6
6
  "algorithm": "sha256",
7
7
  "fileCount": 1367,
8
8
  "files": {
9
- "CHANGELOG.md": "49fd9096d6707758216930437150dcf42cdd5d38ff33019a2bdfef97311782af",
9
+ "CHANGELOG.md": "7b1276a438d21099c9052c81943cf96b9f075282b7f96834eaac9a414456ab7d",
10
10
  "LICENSE": "8d8c51aaf1e1538b180c69965fa038db71be86798cc3e26a143bebfefe16a43e",
11
11
  "README.md": "4cb9fee0aed62bd1467bdc09954e011612b0de6e7895d663f297bc8778e00b2e",
12
12
  "README.tr.md": "f30edbc26e51e311077fbb5b64234e70285db92340e6dadc259cfd673f104326",
@@ -34,7 +34,7 @@
34
34
  "docs/best-practices.md": "f8b75b0d38b5896a6a852bb30a1edddeb24dab8b5b7b1d9313290e1241c72024",
35
35
  "docs/ecosystem.md": "328a56a9f48514bca380d311a8e62246d85ccb18668f873047a883596153395f",
36
36
  "docs/engineering.md": "550528cd47c7d2c921729c68d0782a00ac8e77a4bdfec479f6749f9c3235d188",
37
- "docs/facts.json": "64736478061d3d3af245acd7bc6695e3d96a31910da64622f597bc5831b1ba51",
37
+ "docs/facts.json": "df24755e012fbea4106bddd3a268b9ba98d27ceae910cc2e31ade9bc241e0f01",
38
38
  "docs/features.md": "79a4b5a9722375ea16d7b337148d47bbea2951c8eb7bc8a6e616c9b3ed2c8703",
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": "e708368bf9d2b704996ece7e79a2315d91e0244c437eb41c2d339b76399a7cb1",
57
+ "install/catalog-history.json": "51bba941b447217b999080c9ae31905286e625a35e400fc89a376b92ba6073a1",
58
58
  "install/claude.mjs": "106ce353ebc19c8319d057ce342e1f989519510465c798399076b5f3d8727904",
59
59
  "install/codex.mjs": "08925fe9f3ecdf509b37f01105652c847475d20fcde2db0d679b9a7d663ca145",
60
60
  "install/copilot.mjs": "41dc394b8a3ae77cfd194ce0ca9bbcafce9bdce349f1f72eeb86569f993e15fe",
@@ -65,7 +65,7 @@
65
65
  "install/templates/multi-agent-autopilot-awake.plist.template": "ddd25c76c56fcbb38a89de8e35637372d70ace993d6bc487a88ca9cfb702fe1f",
66
66
  "install/templates/multi-agent-autopilot.plist.template": "122c8a61cd927edd99375d6e3da40f1d42bcceed5b6641785c8b28bb930bc77e",
67
67
  "install/unattended-profile-legacy.json": "f52e86b998956f09ad306b1735a6c10b58580d7082d8a55665a4c6bb15c3aa1f",
68
- "package.json": "bc6e1a7d54eb993ef1db3ded8880bc52748f5a4622145c26d5a0311ffd4ff7e1",
68
+ "package.json": "364c6375f94f3da95979a320685e97790e4e7f13a68388d270527f291a41d26e",
69
69
  "pipeline/agents/android-architect.md": "11fb74543cb0ebcf9525ca6654fe95d53da7229e0bf4a1149b31fd9005abc453",
70
70
  "pipeline/agents/backend-architect.md": "b02a606c301e4714131aed70ae6d16eeb77fbc819a001e2752cff2488d4246de",
71
71
  "pipeline/agents/bulk-reader.md": "6597052cd7d205bac2b1c7633f5232fe1a40730664a913b8eb77efd5a786dc25",
@@ -81,7 +81,7 @@
81
81
  "pipeline/commands/deploy.md": "d20f95f3b1d2357c8c92f3047e8fe11eadf539ff13cab9b54f072f0ad697242a",
82
82
  "pipeline/commands/figma-to-swiftui.md": "cb198d279d36c6c34648ab9834433d00f81c9d427934cc1641c0e09f1878b0a6",
83
83
  "pipeline/commands/multi-agent/SKILL.md": "20e3d9af469af2e347700d1ff4a600e44a69a18f15f374ba107c011e52a1c31a",
84
- "pipeline/commands/multi-agent/analysis-jira/SKILL.md": "c08647b18a56f86690b7db7223e73194b6511f403796252ae8c17cb4265184cd",
84
+ "pipeline/commands/multi-agent/analysis-jira/SKILL.md": "90e41e01318fc3c0feb984fd119cc61b64b0dfa7a65d918b50bb592a0aafe87a",
85
85
  "pipeline/commands/multi-agent/analysis-resolve/SKILL.md": "48b72f59775ebfaab916afaeb2609e5694c0f7a3ce2bf2689cf25623d9755682",
86
86
  "pipeline/commands/multi-agent/analysis/SKILL.md": "8b92d8832f72e195408d6e8e137956df3a4920074ccc0a4a130f36995aa0f6d9",
87
87
  "pipeline/commands/multi-agent/autopilot-off/SKILL.md": "1574f9801993b8ac301de8d60f49df0036175ca7bf95e1b4682b76b56ae6479f",
@@ -169,7 +169,7 @@
169
169
  "pipeline/lib/_jira-auth.sh": "fc468736057f8841d824eb1a4db527442562f8979fea333ebaef5527f4369e9f",
170
170
  "pipeline/lib/account-resolver.sh": "f7bef9beba3f839268509361651be0b1e1f319f2f23821f90ba466fdf185a8f6",
171
171
  "pipeline/lib/agent-model-sync.mjs": "2e0264ca8d8fef761563db219d588fed9067847043f960f68f055c186c20b668",
172
- "pipeline/lib/analysis-jira-write.sh": "d9ad4065d8675db42f3f6248df992842e6f73fa6e5d571dfc5326ba9d37283c6",
172
+ "pipeline/lib/analysis-jira-write.sh": "e1b7399b23bf9a9a7cb935d2f89f85e613a9f6f54231839f732243b0ede63d0a",
173
173
  "pipeline/lib/analysis-quality.mjs": "58dec3447178ff05dc0687c256fa5134c909cd485dd22a0932eafc05ba5c9ad8",
174
174
  "pipeline/lib/analysis-sections.mjs": "e31ddf128ef7a6d1b9061896fc2d7cb4b5165202d8e03a46a1e0f9ec906ed01d",
175
175
  "pipeline/lib/ask-choice.sh": "b77282645cf64d016cdebf43d1ee299874a1411f5c51b6ae3b6c580d1bbfe78a",
@@ -282,7 +282,7 @@
282
282
  "pipeline/multi-agent-refs/design-check/figma-mapping.md": "731b7f19ddee2e67ea5d318e95e3a0d6840fa62c4ad682a25315509ac9ddcbc3",
283
283
  "pipeline/multi-agent-refs/design-check/init-inventory.md": "62defc5ea804f6c8f059a8d512b023791daac59274c0def28cefd7709a7fe862",
284
284
  "pipeline/multi-agent-refs/design-check/mcp-currency-gate.md": "70a96d3135af905989cd1819e6fb37fc44d25df9817b1efcd20121ec6db76d74",
285
- "pipeline/multi-agent-refs/features/analysis-jira.md": "5f6932d2d675c4f7812c6d4c44a8de1bd4c6b07d2ff92a05d8747e811d0f22ad",
285
+ "pipeline/multi-agent-refs/features/analysis-jira.md": "79d6f0784d2ee68181f65aa63c34e8c4152f63373079fdbd2aa4f329e791c7c5",
286
286
  "pipeline/multi-agent-refs/features/analysis-outline.md": "36a917343a7c07ae115aa1e89f825304e5878db931915aefef614d251b167e92",
287
287
  "pipeline/multi-agent-refs/features/analysis-sources.md": "12c8035e6aabf8e8a973b7264edc9a943e6758e79dbc53ae3e9b43c7f998000c",
288
288
  "pipeline/multi-agent-refs/features/assertion-judge.md": "ee9d1f32a5edfc35bf7f97126fa285eaddad6c1ba99724149bcf414c6e318e00",
@@ -331,7 +331,7 @@
331
331
  "pipeline/multi-agent-refs/features/verify.md": "1a6db5e7239eac01e51100353013abd16bf785a7fce88b40139fca27f4166be4",
332
332
  "pipeline/multi-agent-refs/features/visual-evidence.md": "2b6aa2e4ff05c7a1fbe9e0fedfa4b28b9ad01c93a64f1ca6f6cd3e02293866aa",
333
333
  "pipeline/multi-agent-refs/features/worktree-finalize.md": "628f627b94927a9c4a5b0b261fed1f0ffa42d4155390d57b5e56566a26ed8193",
334
- "pipeline/multi-agent-refs/generate-issue.md": "36a3d508bc9ec42b274a3069768a5a3530dea6e607f3cadb228e0ef8bbd0396b",
334
+ "pipeline/multi-agent-refs/generate-issue.md": "7a57f920ad72078fe9d75f561b5dac9aa3b504eb0c539a4598cb7080dc6bdbd7",
335
335
  "pipeline/multi-agent-refs/help/en.md": "e47435607e503f9042a06a815bc005d9e0d0965e21d3f50f6fd221b3f14453d8",
336
336
  "pipeline/multi-agent-refs/help/tr.md": "227fc73ff4e6cbc164072b17eee8c1212dc5e17dac841bbbca689c583d739df8",
337
337
  "pipeline/multi-agent-refs/issue-jira-triad.md": "a12f4e3b75badf8a466e0c3531a5f0e12e44a1dd7799522fb03ff11068ce391d",
@@ -458,7 +458,7 @@
458
458
  "pipeline/schemas/plan-todos.schema.json": "f1dcf8df338b29bbc1d874addd3408c0841a5f869c0b6075efd9242e3e8552d6",
459
459
  "pipeline/schemas/planning-output.schema.json": "bc6eda32ecd435cf84d846078f9d38347f8f741f8a427cff15913dd6c380d4b8",
460
460
  "pipeline/schemas/pr-request.schema.json": "ae75c4856349d62834802b1189af516bb6fb3d36646831b7ff2fa61d938afcd0",
461
- "pipeline/schemas/prefs.schema.json": "009d3f1c9b5dfbbcdc5fd38b2edbea12ec8dead9ab116557288db12a8e44cd46",
461
+ "pipeline/schemas/prefs.schema.json": "836f7ad7ef5932758dc38b8c2ce970b58f1ce568cec49885260e56e515f069d2",
462
462
  "pipeline/schemas/repo-profile.schema.json": "1a74ef8330da9a26a5a70955f6f9cbb20eda54a8615efaf86c7993d420a7f04e",
463
463
  "pipeline/schemas/repos.schema.json": "a9bf56f25c66201c170912e3389bd73e4a3f61922410f83e31a6c6ba47aee861",
464
464
  "pipeline/schemas/research-output.schema.json": "54e3ba1b04fdc20fbaa84333b943a8ab95d1395c7b0e0aedc67929184cb49523",
@@ -496,7 +496,7 @@
496
496
  "pipeline/scripts/_code-graph.mjs": "6b1848d78cef2000c11fcaaa6f1faaae89365a86a37642aa458db9ad422a4cc1",
497
497
  "pipeline/scripts/_command-contract.mjs": "e97a5c624e0768769da54d4175e9f7655f1de1dcd0832468bc66bf140b0f5a6f",
498
498
  "pipeline/scripts/_cost.mjs": "bdf03835af4719b72e4574f2d5ca827e94f9d80e49527a8e9e200a0aa4ecd7a9",
499
- "pipeline/scripts/_diff-impact.mjs": "fc61b34d6495ae8576ecccf1cf1e4effac2d025644259f30bfc46fafe5de264a",
499
+ "pipeline/scripts/_diff-impact.mjs": "cda016ca6071c741a680a3dbc35bb21926465b296defa4fa3677ce550704198a",
500
500
  "pipeline/scripts/_fingerprint.mjs": "d8c63b062a3be0e138b6675aa031824fcd96f5517cf0f7b477378913c1db7d73",
501
501
  "pipeline/scripts/_notices.mjs": "1f78037b0849ab3cf8a85ca353ed1610c2895250ac21a1d50ac1fb9732be4300",
502
502
  "pipeline/scripts/_path-match.mjs": "880b27d90351441b4f3c05e1a87ace62cd1caab3ec7a286663efb2176cb69a69",
@@ -508,14 +508,14 @@
508
508
  "pipeline/scripts/_smoke-root.sh": "dad129959a4e37a0cd0f4f6c04f6d66c5a43752b2bf33989db9c002eb106b832",
509
509
  "pipeline/scripts/_stack-adapter.mjs": "a754c042d237cd03abf11c6909099a7a3ea798cdd13d49b03787797fd3496cd7",
510
510
  "pipeline/scripts/_stack-routing.mjs": "d1f804d457295e5cc8ad7039e8c29431f0e9f52f4396cc54c1305eb82b9b61e9",
511
- "pipeline/scripts/_supertypes.mjs": "d31af077ee670dc68c25dca49b332ebefe44f1ccba3d6d5bd1f534c72609084f",
511
+ "pipeline/scripts/_supertypes.mjs": "7bf4bf8fca087ad93a19316fd24bfaae19d05f8a0907edda4c17946fcaf375d8",
512
512
  "pipeline/scripts/agent-guard.py": "ba8f0c08a80d953d339ea69492efb1fbe8db5d364c2bcb2094a9839cf6dcc081",
513
513
  "pipeline/scripts/agent-guard.sh": "5b57cea687754533c673b66a9dd2281ca06d12ef86bdc0d03fa285fc9e7debf8",
514
514
  "pipeline/scripts/aggregate-metrics.mjs": "c324e349993fb154a885263cf6665af0ff656e9c987b455a263dad8b96cca667",
515
515
  "pipeline/scripts/analysis-conform-coverage.mjs": "2b7de99855d1901b1b7803f47d4bb5f75ed5df1b5f4840d55bfc3537e3f65afe",
516
516
  "pipeline/scripts/analysis-sources.mjs": "53d843b0504a6cadd7d4a047331ebc34a2868304fcf791617ad0a215396e35a6",
517
- "pipeline/scripts/analysis-story-body.mjs": "309cf9f1a9859228d073aff6e7b4a9322bc960d4fe1a21a10ce58521a0c1b641",
518
- "pipeline/scripts/analysis-story-tree.mjs": "6c14d66b9718e0b68a7020058cbaeaf2f38f0ea3b1ee72ffa11a0878d8e38f44",
517
+ "pipeline/scripts/analysis-story-body.mjs": "34ffda565696d99827f1e63692417364f5bebd127e80a377d19bbbcff67b4926",
518
+ "pipeline/scripts/analysis-story-tree.mjs": "5e36db18bb78c987734142edc3a0c9acf38ec2c3b6fddf22f5b994f379f2ec77",
519
519
  "pipeline/scripts/analysis-tickets-writeback.mjs": "8c1f117ac5c6516385e3458462de6bde28c81768853c2d9524118d0155befe8b",
520
520
  "pipeline/scripts/anonymize-findings.mjs": "5d7e042793790c1b924d4d952b4f79b7e5f1dd0a3fe936f1ee16a4fa2ba820dd",
521
521
  "pipeline/scripts/answer-question.mjs": "b948df9d063f623a7ccd9cc65903c4388018b6b968228f77eb2e229223a61b1c",
@@ -545,7 +545,7 @@
545
545
  "pipeline/scripts/classify-plan-safety.mjs": "d25f21da21dd4ee1ea507fb0df89301715071121b1bd96aca9ef140a4eee896b",
546
546
  "pipeline/scripts/code-graph-rules/android.json": "6ba2cc0d496f5c6cc40cca9931e0903da3a64596d939fe2e86367615ec6241c2",
547
547
  "pipeline/scripts/code-graph-rules/go.json": "7d794157675c0d4c71d7ba3d6e538de0e20de6711c652d55844eac11b10e0bdf",
548
- "pipeline/scripts/code-graph-rules/ios.json": "097cb95842ddbe4ed66c2fee3f87ce3be319eaee79533ee46ba917978ddcd8cd",
548
+ "pipeline/scripts/code-graph-rules/ios.json": "b2c8acc464158b29c669050e7f13cf1f3445fcc2e8b8fb5c5da80feea78f9d2d",
549
549
  "pipeline/scripts/code-graph-rules/node.json": "062d7337519f5a743da6953d2748e95560729715502a665d19025108ceba4845",
550
550
  "pipeline/scripts/code-graph-rules/python.json": "91394dc31a9392e8d02cf46797bf39551fbe483aa2e6b34145cd33aa2460eca9",
551
551
  "pipeline/scripts/commands.mjs": "5b20a7d01aa0f205f2605fcfcbdb197bdfeafbdfc1e5164b5ee083ac6ff39cc2",
@@ -701,14 +701,14 @@
701
701
  "pipeline/scripts/worktrees.mjs": "90dfd09b70fffe9b56c331a4ae2692cc248f4898f4ee6aee4239ef0b20888a90",
702
702
  "pipeline/scripts/write-state.mjs": "e1198bf970eace07d3eff22b394808a526e473df65d3d58aa2d5a12cfdac8b49",
703
703
  "pipeline/scripts/write-store-config.mjs": "b994a061f6febb2ca3d396074d8b51d2c1c1f991d3072d321703380da3639a56",
704
- "pipeline/skills/.skill-manifest.json": "bae66a65ec530c9ca10931f0fdbda0b562b84eddf55d40d7663695c98b1d9b7d",
704
+ "pipeline/skills/.skill-manifest.json": "784b52afc412806ef44d0f5691d7f55d6b4c7f3a3b983410f2d9ad726ffc8fa4",
705
705
  "pipeline/skills/.skills-index.json": "b2917fa44e273b939549f1c3ab3dd81c26ba8168772bc7644c9fa076116d8a4c",
706
706
  "pipeline/skills/shared/README.md": "928fe4292be729c3b7d7d0898cade8be0dfa868f208e0c6d1fe7b2c6bd6223c5",
707
707
  "pipeline/skills/shared/core/apple-archive-compliance/SKILL.md": "7239fef717ea45b79276bfcc2c3903acd57950b6e00dd6a2a66399c4b09cbd51",
708
708
  "pipeline/skills/shared/core/apple-archive-compliance/references/rules.yml": "25129ce6d416242ac04c4032f7561e0c2761ab8bbadf08d9c9f49b573a663389",
709
709
  "pipeline/skills/shared/core/google-play-compliance/SKILL.md": "3089e8c5f99c1ec7c1358394c398335ccb5ebc20ca05d60d870127da4592de34",
710
710
  "pipeline/skills/shared/core/google-play-compliance/references/rules.yml": "21fc058762e7341a8917442f1ab4dc50def59aa3145eea120c474b0d0de6a5c1",
711
- "pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md": "130543d1017115de351a1175b6b179778642ba0b3207034b54114b39d0546407",
711
+ "pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md": "798061199762b141d0312b56f9e8e10af1324fb867393929565ebe4e61fcbc81",
712
712
  "pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md": "9e405b0f7403c72b00a7ddf1e4b8518cfe37feb31d91d8e5ba70f0341efa5d0c",
713
713
  "pipeline/skills/shared/core/multi-agent-analysis/SKILL.md": "a9c1b44208271ae2d0845cde7565542e2502324abc2c1aac0397d92f908d37db",
714
714
  "pipeline/skills/shared/core/multi-agent-autopilot-off/SKILL.md": "a7efde03189cebf6ff81a1233eddd59dbcfd1b2ebdb30175503e37468905afcb",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "20.23.0",
3
+ "version": "20.23.2",
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",
@@ -73,6 +73,13 @@ Then ask for the epic, once. `AskUserQuestion`:
73
73
  - **Link to an existing epic** - the user types its key (`--epic <KEY>`)
74
74
  - **No epic** - the stories are created unlinked
75
75
 
76
+ When the plan's `prefsUsed.subtaskRoles` is empty and `prefsUsed.subtaskRolesDeclined`
77
+ is false, attended runs ask once which sub-tasks every story gets. List the site's types
78
+ with `bash "$HOME/.claude/lib/analysis-jira-write.sh" --subtask-types --project "<KEY>"`,
79
+ offer them as a multi-select `AskUserQuestion` plus **No sub-tasks**, save the answer with
80
+ `node "$HOME/.claude/scripts/analysis-story-tree.mjs" --save-subtasks "<type>,<type>"` (or
81
+ `none`), and re-run Phase 1. Unattended runs never ask.
82
+
76
83
  Then the dry run, which is what proves the writer agrees with the plan:
77
84
 
78
85
  ```bash
@@ -36,6 +36,7 @@
36
36
  #
37
37
  # Usage:
38
38
  # analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE] [--epic KEY]
39
+ # analysis-jira-write.sh --subtask-types --project KEY the site's sub-task type names, one per line
39
40
  #
40
41
  # Exit: 0 written (or previewed), 3 the plan does not parse, 4 auth, 5 a Jira
41
42
  # call failed, 64 usage.
@@ -46,7 +47,7 @@ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
47
  # shellcheck source=/dev/null
47
48
  . "$SELF_DIR/_jira-auth.sh"
48
49
 
49
- PLAN=""; PROJECT=""; DRY=0; LEDGER=""; EPIC=""
50
+ PLAN=""; PROJECT=""; DRY=0; LEDGER=""; EPIC=""; LIST_TYPES=0
50
51
  while [ $# -gt 0 ]; do
51
52
  case "$1" in
52
53
  --plan) PLAN="${2:-}"; shift 2 ;;
@@ -54,13 +55,44 @@ while [ $# -gt 0 ]; do
54
55
  --ledger) LEDGER="${2:-}"; shift 2 ;;
55
56
  --dry-run) DRY=1; shift ;;
56
57
  --epic) EPIC="${2:-}"; shift 2 ;;
57
- *) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE] [--epic KEY]" >&2; exit 64 ;;
58
+ --subtask-types) LIST_TYPES=1; shift ;;
59
+ *) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE] [--epic KEY] | --subtask-types --project KEY" >&2; exit 64 ;;
58
60
  esac
59
61
  done
60
- [ -n "$PLAN" ] && [ -f "$PLAN" ] || { echo "ERR: --plan FILE is required and must exist" >&2; exit 64; }
61
62
  [ -n "$PROJECT" ] || { echo "ERR: --project KEY is required" >&2; exit 64; }
63
+ if [ "$LIST_TYPES" -eq 0 ] && { [ -z "$PLAN" ] || [ ! -f "$PLAN" ]; }; then
64
+ echo "ERR: --plan FILE is required and must exist" >&2; exit 64
65
+ fi
62
66
 
63
67
  command -v jq >/dev/null 2>&1 || { echo "ERR: jq is required" >&2; exit 3; }
68
+
69
+ # Discover rather than assume: the types this site marks subtask:true, under
70
+ # whatever names it gave them. Jira Data Center 9 answers only the per-project
71
+ # createmeta; older sites answer only the classic one. Read once per run.
72
+ SITE_SUBTASK_TYPES=""
73
+ SITE_SUBTASK_READ=0
74
+ site_subtask_types() {
75
+ SITE_SUBTASK_READ=1
76
+ SITE_SUBTASK_TYPES="$(jira_api GET "/rest/api/2/issue/createmeta/${PROJECT}/issuetypes?maxResults=100" 2>/dev/null \
77
+ | jq -r '.values[]? | select(.subtask == true) | .name' 2>/dev/null || echo "")"
78
+ [ -n "$SITE_SUBTASK_TYPES" ] || SITE_SUBTASK_TYPES="$(jira_api GET \
79
+ "/rest/api/2/issue/createmeta?projectKeys=${PROJECT}&expand=projects.issuetypes" 2>/dev/null \
80
+ | jq -r '.projects[]?.issuetypes[]? | select(.subtask == true) | .name' 2>/dev/null || echo "")"
81
+ local n
82
+ n="$(printf '%s' "$SITE_SUBTASK_TYPES" | grep -c . || true)"
83
+ if [ "${n:-0}" -gt 1 ]; then
84
+ echo " note ${PROJECT} has ${n} sub-task types ($(printf '%s' "$SITE_SUBTASK_TYPES" | paste -sd, - | sed 's/,/, /g')); using the first. Set issueTree.subtaskIssueType to a name, or to a {role: type} map, to choose." >&2
85
+ fi
86
+ }
87
+
88
+ if [ "$LIST_TYPES" -eq 1 ]; then
89
+ jira_auth_resolve || exit 4
90
+ site_subtask_types 2>/dev/null
91
+ [ -n "$SITE_SUBTASK_TYPES" ] || { echo "ERR: ${PROJECT} answered no sub-task types" >&2; exit 5; }
92
+ printf '%s\n' "$SITE_SUBTASK_TYPES"
93
+ exit 0
94
+ fi
95
+
64
96
  jq -e . "$PLAN" >/dev/null 2>&1 || { echo "ERR: the plan does not parse as JSON: $PLAN" >&2; exit 3; }
65
97
 
66
98
  DOC_ID="$(jq -r '.document.id // "unidentified"' "$PLAN")"
@@ -272,12 +304,13 @@ while IFS= read -r node; do
272
304
  [ -n "$sub" ] || continue
273
305
  t_label="$(printf '%s' "$sub" | jq -r '.label')"
274
306
  t_role="$(printf '%s' "$sub" | jq -r '.role')"
275
- t_type="$(jq -r '.prefsUsed.subtaskIssueType // empty' "$PLAN")"
307
+ # A name, or a map from role to name ("*" for any other role).
308
+ t_type="$(jq -r --arg r "$t_role" '.prefsUsed.subtaskIssueType
309
+ | if type == "object" then (.[$r] // .["*"] // empty)
310
+ elif type == "string" then . else empty end' "$PLAN")"
276
311
  if [ -z "$t_type" ] && [ "$DRY" -eq 0 ]; then
277
- # Discover rather than assume: whichever type this site marks subtask:true,
278
- # under whatever name the site gave it.
279
- t_type="$(jira_api GET "/rest/api/2/issue/createmeta?projectKeys=${PROJECT}&expand=projects.issuetypes" \
280
- | jq -r '[.projects[]?.issuetypes[]? | select(.subtask==true) | .name] | first // empty' 2>/dev/null || echo "")"
312
+ [ "$SITE_SUBTASK_READ" -eq 1 ] || site_subtask_types
313
+ t_type="$(printf '%s\n' "$SITE_SUBTASK_TYPES" | head -n 1)"
281
314
  fi
282
315
  [ -n "$t_type" ] || t_type="Sub-task"
283
316
  t_desc="$(printf '%s' "$sub" | jq -r '.description // empty')"
@@ -122,6 +122,9 @@ Each part sits in a coloured wiki panel with its `h3.` heading first: the user
122
122
  story in `{panel:borderColor=#d4d39e}`, the acceptance criteria in
123
123
  `{panel:borderColor=#d4a19e}`, current vs target and links in
124
124
  `{panel:borderColor=#9eb6d4}`. The `Sources:` line stays outside the panels.
125
+ `issueTree.storyPanels` replaces any of the three colours (`story`, `criteria`,
126
+ `other`), or turns the panels off with `false`; `/multi-agent:create-jira`
127
+ reads the same setting.
125
128
 
126
129
  At most 8 criteria. What is missing stays missing and becomes a body warning
127
130
  (no user story, fewer than 2 criteria, no negative criterion); nothing is filled
@@ -169,13 +172,22 @@ are values the site fills in:
169
172
  channel, the value is the site's own name for it
170
173
  - `subtaskRoles` ships **empty**. An empty list is an instruction to look at how
171
174
  this board actually splits work; a ready-made list would be the guess most
172
- worth avoiding
175
+ worth avoiding. An attended run asks once, offering the site's own sub-task
176
+ types (`analysis-jira-write.sh --subtask-types`), and saves the answer
177
+ (`analysis-story-tree.mjs --save-subtasks`): each chosen type becomes a role
178
+ mapped to itself, and "no sub-tasks" sets `subtaskRolesDeclined`
173
179
  - `epicLinkFieldId`, when set, is the site's own field id; otherwise it is
174
180
  discovered
175
181
  - `subtaskIssueType: null` means discover it - `createmeta` returns whichever
176
- type carries `subtask: true`, under whatever name the site gave it. Same rule
177
- as `features/jira-context.md`: the type travels as it comes from Jira and is
182
+ type carries `subtask: true`, under whatever name the site gave it (the
183
+ per-project `createmeta/{project}/issuetypes` first, which is all Jira Data
184
+ Center 9 answers, then the classic form). Same rule as
185
+ `features/jira-context.md`: the type travels as it comes from Jira and is
178
186
  never a matching criterion in code
187
+ - a site with several sub-task types (research, build and QA sub-tasks)
188
+ names them per role: `subtaskIssueType` as a map from a `subtaskRoles`
189
+ entry to the type, `"*"` for any other role. Without one the first discovered
190
+ type is used and the run prints the types it found
179
191
 
180
192
  The preview prints every field beside the pref key it came from, so a wrong
181
193
  setting is visible before the writes rather than in Jira afterwards.
@@ -66,7 +66,7 @@ Each type has a standard section skeleton. `A` = always present. `C` = condition
66
66
  | Acceptance Criteria | `{panel:borderColor=#d4a19e}` |
67
67
  | Every other section | `{panel:borderColor=#9eb6d4}` |
68
68
 
69
- Each panel closes with `{panel}` on its own line. Task and Bug keep plain `h3.` sections.
69
+ Each panel closes with `{panel}` on its own line. Task and Bug keep plain `h3.` sections. The colours are defaults: `prefs.global.issueTree.storyPanels` (`{story, criteria, other}`, `#rrggbb` each) replaces any of them, and `false` writes the Story with plain `h3.` sections. A value that is not a `#rrggbb` colour keeps the default. The `/multi-agent:analysis-jira` stories read the same setting.
70
70
 
71
71
  Section headings render in `outputLanguage` (e.g. tr: "Detaylı Açıklama", "Kapsam", "Kabul Kriterleri", "Test Senaryoları", "Adımlar", "Beklenen Sonuç", "Gerçekleşen Sonuç", "Ortam", "Ekran Görüntüleri", "API Contract", "Design Reference", "Notlar").
72
72
 
@@ -2202,9 +2202,30 @@
2202
2202
  "default": "Story"
2203
2203
  },
2204
2204
  "subtaskIssueType": {
2205
- "type": ["string", "null"],
2205
+ "type": ["string", "null", "object"],
2206
+ "additionalProperties": { "type": "string" },
2206
2207
  "default": null,
2207
- "description": "Null means discover from createmeta rather than assume a name."
2208
+ "description": "The sub-task type name, or a map from a subtaskRoles entry to this site's type name (\"*\" for any other role), e.g. {\"develop\": \"Build Sub-task\", \"test\": \"QA Sub-task\"}. Null means discover from createmeta (the per-project endpoint, else the classic one) rather than assume a name; when the site has several sub-task types the first is used and the run says so."
2209
+ },
2210
+ "subtaskRolesDeclined": {
2211
+ "type": "boolean",
2212
+ "default": false,
2213
+ "description": "true once the person answered the sub-task question with no sub-tasks, so /multi-agent:analysis-jira does not ask again. Written by analysis-story-tree.mjs --save-subtasks none."
2214
+ },
2215
+ "storyPanels": {
2216
+ "oneOf": [
2217
+ { "const": false },
2218
+ {
2219
+ "type": "object",
2220
+ "additionalProperties": false,
2221
+ "properties": {
2222
+ "story": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
2223
+ "criteria": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
2224
+ "other": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }
2225
+ }
2226
+ }
2227
+ ],
2228
+ "description": "Border colours of a Story's wiki panels, as written by /multi-agent:create-jira and /multi-agent:analysis-jira: story (the user story), criteria (the acceptance criteria), other (every other section). Unset means #d4d39e, #d4a19e and #9eb6d4; a colour left out keeps its default; false writes plain h3. sections."
2208
2229
  },
2209
2230
  "subtaskRoles": {
2210
2231
  "type": "array",
@@ -38,7 +38,7 @@ function addLine(set, line) {
38
38
  set.add(Math.max(1, line));
39
39
  }
40
40
 
41
- function toRanges(lines) {
41
+ function toRanges(lines, gaps = new Set()) {
42
42
  const sorted = [...lines].sort((a, b) => a - b);
43
43
  const out = [];
44
44
  for (const l of sorted) {
@@ -46,21 +46,28 @@ function toRanges(lines) {
46
46
  if (last && l === last.end + 1) last.end = l;
47
47
  else if (!last || l > last.end) out.push({ start: l, end: l });
48
48
  }
49
- return out;
49
+ // A gap is a point between two lines where lines went in (or, on the new
50
+ // side, came out): `{start: before, end: before + 1, gap: true}`.
51
+ for (const before of gaps)
52
+ if (!sorted.includes(before) && !sorted.includes(before + 1))
53
+ out.push({ start: before, end: before + 1, gap: true });
54
+ return out.sort((a, b) => a.start - b.start);
50
55
  }
51
56
 
52
57
  /**
53
58
  * Every file of a unified diff with the lines it touched on each side.
54
59
  *
55
- * New side: an added line counts as itself, and a deletion as the line now
56
- * standing where the removed lines were. Old side: a removed line counts as
57
- * itself, and an insertion as the old line standing where it went in. Either way a hunk that
58
- * only adds or only removes still lands inside the declaration it changed.
60
+ * New side: an added line counts as itself, and a pure deletion as the gap
61
+ * between the two new lines it fell between. Old side: a removed line counts as
62
+ * itself, and a pure insertion as the gap between the two old lines it went in
63
+ * between. A gap belongs to the innermost declaration spanning both of its
64
+ * lines, so a hunk that only adds or only removes lands inside the declaration
65
+ * it changed, whatever the context size.
59
66
  * Hunks are counted from their headers, so concatenated `diff -u` output with
60
67
  * no `diff --git` lines parses too, CRLF included, at any context size.
61
68
  *
62
69
  * @param {string} diffText - unified diff
63
- * @returns {Array<{oldPath: string|null, newPath: string|null, oldRanges: Array<{start: number, end: number}>, newRanges: Array<{start: number, end: number}>}>}
70
+ * @returns {Array<{oldPath: string|null, newPath: string|null, oldRanges: Array<{start: number, end: number, gap?: boolean}>, newRanges: Array<{start: number, end: number, gap?: boolean}>}>}
64
71
  */
65
72
  export function parseDiffFiles(diffText) {
66
73
  const files = [];
@@ -70,23 +77,36 @@ export function parseDiffFiles(diffText) {
70
77
  let newLine = 0;
71
78
  let oldLeft = 0;
72
79
  let newLeft = 0;
80
+ // One run of -/+ lines. Its removed lines stand for it on the old side and
81
+ // its added lines on the new side; a run with nothing on one side is a gap
82
+ // there, between the lines either side of it.
83
+ let run = null;
84
+ const endRun = () => {
85
+ if (run && !run.removed) cur.oldGaps.add(run.oldBefore);
86
+ if (run && !run.added) cur.newGaps.add(run.newBefore);
87
+ run = null;
88
+ };
73
89
  for (const line of String(diffText).split("\n")) {
74
90
  const raw = line.endsWith("\r") ? line.slice(0, -1) : line;
75
91
  if (oldLeft > 0 || newLeft > 0) {
76
- if (raw.startsWith("+")) {
77
- addLine(cur.newLines, newLine);
78
- addLine(cur.oldLines, oldLine);
79
- newLine++;
80
- newLeft--;
81
- continue;
82
- }
83
- if (raw.startsWith("-")) {
84
- addLine(cur.oldLines, oldLine);
85
- addLine(cur.newLines, newLine);
86
- oldLine++;
87
- oldLeft--;
92
+ if (raw.startsWith("+") || raw.startsWith("-")) {
93
+ run = run || { oldBefore: oldLine - 1, newBefore: newLine - 1, removed: 0, added: 0 };
94
+ if (raw.startsWith("+")) {
95
+ addLine(cur.newLines, newLine);
96
+ run.added++;
97
+ newLine++;
98
+ newLeft--;
99
+ } else {
100
+ addLine(cur.oldLines, oldLine);
101
+ run.removed++;
102
+ oldLine++;
103
+ oldLeft--;
104
+ }
105
+ if (oldLeft <= 0 && newLeft <= 0) endRun();
88
106
  continue;
89
107
  }
108
+ if (raw.startsWith("\\")) continue;
109
+ endRun();
90
110
  if (raw.startsWith(" ") || raw === "") {
91
111
  oldLine++;
92
112
  newLine++;
@@ -94,7 +114,6 @@ export function parseDiffFiles(diffText) {
94
114
  newLeft--;
95
115
  continue;
96
116
  }
97
- if (raw.startsWith("\\")) continue;
98
117
  oldLeft = 0;
99
118
  newLeft = 0;
100
119
  }
@@ -112,6 +131,8 @@ export function parseDiffFiles(diffText) {
112
131
  newPath: sidePath(raw.slice(4), "b/"),
113
132
  oldLines: new Set(),
114
133
  newLines: new Set(),
134
+ oldGaps: new Set(),
135
+ newGaps: new Set(),
115
136
  };
116
137
  files.push(cur);
117
138
  pendingOld = undefined;
@@ -121,12 +142,14 @@ export function parseDiffFiles(diffText) {
121
142
  if (hunk && cur) {
122
143
  const oldCount = hunk[2] === undefined ? 1 : Number(hunk[2]);
123
144
  const newCount = hunk[4] === undefined ? 1 : Number(hunk[4]);
124
- oldLine = Number(hunk[1]);
125
- newLine = Number(hunk[3]);
145
+ // An empty side names the line BEFORE the change; any other side names
146
+ // the change's first line.
147
+ oldLine = Number(hunk[1]) + (oldCount === 0 ? 1 : 0);
148
+ newLine = Number(hunk[3]) + (newCount === 0 ? 1 : 0);
126
149
  oldLeft = oldCount;
127
150
  newLeft = newCount;
128
- if (newCount === 0) addLine(cur.newLines, newLine);
129
- if (oldCount === 0) addLine(cur.oldLines, oldLine);
151
+ if (oldCount === 0) cur.oldGaps.add(oldLine - 1);
152
+ if (newCount === 0) cur.newGaps.add(newLine - 1);
130
153
  }
131
154
  }
132
155
  return files
@@ -134,8 +157,8 @@ export function parseDiffFiles(diffText) {
134
157
  .map((f) => ({
135
158
  oldPath: f.oldPath,
136
159
  newPath: f.newPath,
137
- oldRanges: toRanges(f.oldLines),
138
- newRanges: toRanges(f.newLines),
160
+ oldRanges: toRanges(f.oldLines, f.oldGaps),
161
+ newRanges: toRanges(f.newLines, f.newGaps),
139
162
  }));
140
163
  }
141
164
 
@@ -202,8 +225,13 @@ export function changedSymbols(graph, ranges) {
202
225
  continue;
203
226
  }
204
227
  for (const r of list) {
228
+ // A gap belongs to a declaration only when the lines on both sides do.
205
229
  const containing = spanned
206
- .filter((s) => (s.startLine ?? s.line) <= r.end && s.endLine >= r.start)
230
+ .filter((s) =>
231
+ r.gap
232
+ ? (s.startLine ?? s.line) <= r.start && s.endLine >= r.end
233
+ : (s.startLine ?? s.line) <= r.end && s.endLine >= r.start,
234
+ )
207
235
  .sort((a, b) => b.line - a.line);
208
236
  if (!containing.length) {
209
237
  seeds.set(fileNode.id, fileNode);
@@ -6,7 +6,7 @@
6
6
  * protocols or interfaces. Three syntaxes cover the stacks the graph indexes:
7
7
  *
8
8
  * colon Swift and Kotlin: `Name<T>(ctor): A, B<X>() where ... {`
9
- * parens Python: `Name(A, B, metaclass=M):`
9
+ * parens Python: `Name(A, B, metaclass=M):`, `Name[T](A):`
10
10
  * extends TypeScript / JavaScript: `Name<T> extends A implements B, C {`
11
11
  *
12
12
  * Only names that start with a letter or underscore are kept, qualified names
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  const HEADER_MAX = 4000;
23
+ const CALL_RE = /^\s*[A-Za-z_$][\w$.]*\s*(?:<[^()]*>)?\s*\(/;
23
24
  const OPEN = { "(": ")", "<": ">", "[": "]" };
24
25
  const CLOSE = new Set([")", ">", "]"]);
25
26
 
@@ -99,10 +100,14 @@ function names(list) {
99
100
  export function supertypesAt(body, start, syntax) {
100
101
  const header = headerText(body, start, syntax);
101
102
  if (syntax === "parens") {
102
- const m = /^\s*\(/.exec(header);
103
+ let k = 0;
104
+ while (k < header.length && /\s/.test(header[k])) k++;
105
+ // PEP 695 type parameters come before the bases: `class Box[T](Base):`.
106
+ const rest = header.slice(skipGroup(header, k, "["));
107
+ const m = /^\s*\(/.exec(rest);
103
108
  if (!m) return [];
104
- const end = skipGroup(header, m[0].length - 1, "(");
105
- return names(header.slice(m[0].length, end - 1));
109
+ const end = skipGroup(rest, m[0].length - 1, "(");
110
+ return names(rest.slice(m[0].length, end - 1));
106
111
  }
107
112
  if (syntax === "extends") {
108
113
  let k = 0;
@@ -111,8 +116,16 @@ export function supertypesAt(body, start, syntax) {
111
116
  const out = [];
112
117
  const ext = /\bextends\s+([\s\S]*?)(?=\bimplements\b|$)/.exec(rest);
113
118
  const imp = /\bimplements\s+([\s\S]*)$/.exec(rest);
119
+ // A base built by a call (`extends mixin(Base)`) is the call's result,
120
+ // which names no type the graph can point at.
114
121
  for (const part of [ext?.[1], imp?.[1]])
115
- if (part) for (const n of names(part)) if (!out.includes(n)) out.push(n);
122
+ if (part)
123
+ for (const n of names(
124
+ splitTop(part)
125
+ .filter((p) => !CALL_RE.test(p))
126
+ .join(","),
127
+ ))
128
+ if (!out.includes(n)) out.push(n);
116
129
  return out;
117
130
  }
118
131
  let i = 0;
@@ -17,10 +17,22 @@ import { redesignSections } from "../lib/analysis-sections.mjs";
17
17
 
18
18
  const MAX_CRITERIA = 8;
19
19
 
20
- // Panel border colours of a story section, the ones testers read stories in:
21
- // the user story, the acceptance criteria, and every other section.
22
- const PANEL = { story: "#d4d39e", criteria: "#d4a19e", info: "#9eb6d4" };
23
- const panel = (colour, lines) => [`{panel:borderColor=${colour}}`, ...lines, "{panel}", ""];
20
+ // Default panel border colours of a story section: the user story, the
21
+ // acceptance criteria, and every other section. issueTree.storyPanels
22
+ // overrides any of them, or turns panels off with false.
23
+ const PANEL_DEFAULTS = { story: "#d4d39e", criteria: "#d4a19e", other: "#9eb6d4" };
24
+ const HEX_COLOUR = /^#[0-9a-fA-F]{6}$/;
25
+
26
+ /** The colours to draw with, or null for plain sections. */
27
+ export function panelColours(setting) {
28
+ if (setting === false) return null;
29
+ const own = setting && typeof setting === "object" ? setting : {};
30
+ const pick = (k) => (HEX_COLOUR.test(own[k] ?? "") ? own[k] : PANEL_DEFAULTS[k]);
31
+ return { story: pick("story"), criteria: pick("criteria"), other: pick("other") };
32
+ }
33
+
34
+ const section = (colours, kind, lines) =>
35
+ colours ? [`{panel:borderColor=${colours[kind]}}`, ...lines, "{panel}", ""] : [...lines, ""];
24
36
  const MAX_DESIGN_LINKS = 5;
25
37
 
26
38
  // A criterion that describes a refusal, an error or an alternative path. One is
@@ -155,12 +167,13 @@ function linksOf(story, lines, ctx) {
155
167
  return links;
156
168
  }
157
169
 
158
- function render({ userStory, acceptance, currentVsTarget, links }, sourceIds) {
170
+ function render({ userStory, acceptance, currentVsTarget, links }, sourceIds, colours) {
159
171
  const out = [];
160
- if (userStory) out.push(...panel(PANEL.story, ["h3. User story", escapeJiraWiki(userStory)]));
172
+ if (userStory)
173
+ out.push(...section(colours, "story", ["h3. User story", escapeJiraWiki(userStory)]));
161
174
  if (acceptance.length) {
162
175
  out.push(
163
- ...panel(PANEL.criteria, [
176
+ ...section(colours, "criteria", [
164
177
  "h3. Acceptance criteria",
165
178
  ...acceptance.map((a) => `# ${escapeJiraWiki(a)}`),
166
179
  ]),
@@ -168,7 +181,7 @@ function render({ userStory, acceptance, currentVsTarget, links }, sourceIds) {
168
181
  }
169
182
  if (currentVsTarget.length) {
170
183
  out.push(
171
- ...panel(PANEL.info, [
184
+ ...section(colours, "other", [
172
185
  "h3. Current vs target",
173
186
  "||Id||Status||Where||",
174
187
  ...currentVsTarget.map(
@@ -179,7 +192,10 @@ function render({ userStory, acceptance, currentVsTarget, links }, sourceIds) {
179
192
  }
180
193
  if (links.length) {
181
194
  out.push(
182
- ...panel(PANEL.info, ["h3. Links", ...links.map((l) => `* ${wikiLink(l.label, l.url)}`)]),
195
+ ...section(colours, "other", [
196
+ "h3. Links",
197
+ ...links.map((l) => `* ${wikiLink(l.label, l.url)}`),
198
+ ]),
183
199
  );
184
200
  }
185
201
  out.push(
@@ -208,7 +224,7 @@ export function buildStoryBody(story, ctx) {
208
224
  if (!acceptance.some((a) => NEGATIVE.test(a))) warnings.push("no negative acceptance criterion");
209
225
 
210
226
  const body = { userStory, acceptance, currentVsTarget, links };
211
- return { ...body, warnings, wiki: render(body, story.sourceIds || []) };
227
+ return { ...body, warnings, wiki: render(body, story.sourceIds || [], panelColours(ctx.panels)) };
212
228
  }
213
229
 
214
230
  /**