@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.
@@ -50,6 +50,7 @@
50
50
  *
51
51
  * Usage:
52
52
  * analysis-story-tree.mjs <analysis.md> [--json] [--brief] [--prefs FILE] [--page-url URL]
53
+ * analysis-story-tree.mjs --save-subtasks "<type>[,<type>...]"|none [--prefs FILE]
53
54
  *
54
55
  * Exit: 0 planned, 2 nothing to plan, 3 usage, 4 the document is not issue-ready.
55
56
  */
@@ -60,6 +61,7 @@ import { createHash } from "node:crypto";
60
61
  import { homedir } from "node:os";
61
62
  import { dirname, join } from "node:path";
62
63
  import { hostCommand } from "../lib/user-facing.mjs";
64
+ import { lockTimeoutFrom, updateJsonFileSync } from "../lib/json-file-lock.mjs";
63
65
  import { resolveConstitution } from "./constitution.mjs";
64
66
  import { gateContext, recordQuietly } from "./gate-ledger.mjs";
65
67
  import {
@@ -81,7 +83,11 @@ const PREFS_PATH =
81
83
  : join(homedir(), ".claude", "multi-agent-preferences.json");
82
84
  const pageUrlFlag = argv.indexOf("--page-url");
83
85
  const PAGE_URL = pageUrlFlag !== -1 ? argv[pageUrlFlag + 1] : null;
84
- const valueIdx = new Set([prefsFlag, pageUrlFlag].filter((i) => i !== -1).map((i) => i + 1));
86
+ const saveFlag = argv.indexOf("--save-subtasks");
87
+ const SAVE_SUBTASKS = saveFlag !== -1 ? (argv[saveFlag + 1] ?? "") : null;
88
+ const valueIdx = new Set(
89
+ [prefsFlag, pageUrlFlag, saveFlag].filter((i) => i !== -1).map((i) => i + 1),
90
+ );
85
91
  const file = argv.find((a, i) => !a.startsWith("--") && !valueIdx.has(i));
86
92
 
87
93
  function die(msg, code) {
@@ -153,7 +159,7 @@ export function identityLabel(prefix, docId, sourceIds) {
153
159
  * issueTree.projectKey where the tree is created
154
160
  * issueTree.epicIssueType the type name for an epic on this site
155
161
  * issueTree.storyIssueType the type name for a story
156
- * issueTree.subtaskIssueType null = discover from createmeta
162
+ * issueTree.subtaskIssueType a name, a {role: name} map, or null = discover
157
163
  * issueTree.subtaskRoles how a story is split; ships empty on purpose
158
164
  * issueTree.channelComponents channel -> this site's component name
159
165
  * issueTree.channelTeams channel -> this site's team name
@@ -162,6 +168,8 @@ export function identityLabel(prefix, docId, sourceIds) {
162
168
  * issueTree.epicLinkFieldId the epic-link field, when not discovered
163
169
  * issueTree.cloneByChannel one linked story per channel in channelComponents
164
170
  * issueTree.cloneLinkType the link type between a story and its clones
171
+ * issueTree.storyPanels story section panel colours, or false for none
172
+ * issueTree.subtaskRolesDeclined the person chose no sub-tasks; do not ask again
165
173
  */
166
174
  const ISSUE_TREE_SETTINGS = [
167
175
  "issueTree.enabled",
@@ -177,6 +185,8 @@ const ISSUE_TREE_SETTINGS = [
177
185
  "issueTree.epicLinkFieldId",
178
186
  "issueTree.cloneByChannel",
179
187
  "issueTree.cloneLinkType",
188
+ "issueTree.storyPanels",
189
+ "issueTree.subtaskRolesDeclined",
180
190
  ];
181
191
 
182
192
  function readPrefs() {
@@ -195,6 +205,17 @@ function readPrefs() {
195
205
  }
196
206
  }
197
207
 
208
+ /**
209
+ * The sub-task type a role is created as, from issueTree.subtaskIssueType: a
210
+ * name for every role, or a {role: name} map with "*" for any other role. Null
211
+ * means the writer discovers it from the site.
212
+ */
213
+ export function subtaskTypeFor(setting, role) {
214
+ if (typeof setting === "string" && setting) return setting;
215
+ if (setting && typeof setting === "object") return setting[role] ?? setting["*"] ?? null;
216
+ return null;
217
+ }
218
+
198
219
  /** Every open placeholder that makes a document unfit to become work. */
199
220
  export function openItems(text) {
200
221
  const found = [];
@@ -377,6 +398,7 @@ export function plan(text, prefs, { pageUrl = null } = {}) {
377
398
  profile,
378
399
  pageUrl: pageUrl || fm.confluence_url || null,
379
400
  redesign: String(fm.redesign || "").toLowerCase() === "true",
401
+ panels: prefs.storyPanels,
380
402
  };
381
403
  for (const story of stories) {
382
404
  const b = buildStoryBody(story, bodyCtx);
@@ -423,6 +445,8 @@ export function plan(text, prefs, { pageUrl = null } = {}) {
423
445
  epicLinkFieldId: prefs.epicLinkFieldId ?? null,
424
446
  cloneByChannel: prefs.cloneByChannel === true,
425
447
  cloneLinkType: prefs.cloneLinkType ?? "Relates",
448
+ storyPanels: prefs.storyPanels ?? null,
449
+ subtaskRolesDeclined: prefs.subtaskRolesDeclined === true,
426
450
  },
427
451
  nodes: stories,
428
452
  writeCount: stories.reduce(
@@ -453,7 +477,50 @@ function consistencyOf(text, nodes) {
453
477
  });
454
478
  }
455
479
 
480
+ /**
481
+ * Save the sub-tasks a person chose for every story: each chosen site type
482
+ * becomes a role named after it and mapped to it, or "none" records that the
483
+ * question was answered with no sub-tasks.
484
+ */
485
+ function saveSubtasks(choice) {
486
+ const names = String(choice)
487
+ .split(",")
488
+ .map((n) => n.trim())
489
+ .filter(Boolean);
490
+ const none = names.length === 1 && names[0].toLowerCase() === "none";
491
+ if (!names.length || names.some((n) => n.length > 100 || /[\n{}]/.test(n)))
492
+ return die("analysis-story-tree: --save-subtasks takes type names or none", 3);
493
+ try {
494
+ updateJsonFileSync(
495
+ PREFS_PATH,
496
+ (j) => {
497
+ if (!j || typeof j !== "object" || Array.isArray(j)) return undefined;
498
+ j.global = j.global || {};
499
+ const t = { ...(j.global.issueTree || {}) };
500
+ if (none) {
501
+ t.subtaskRoles = [];
502
+ t.subtaskRolesDeclined = true;
503
+ } else {
504
+ t.subtaskRoles = names;
505
+ t.subtaskIssueType = Object.fromEntries(names.map((n) => [n, n]));
506
+ delete t.subtaskRolesDeclined;
507
+ }
508
+ j.global.issueTree = t;
509
+ return j;
510
+ },
511
+ { mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(process.env) },
512
+ );
513
+ } catch (e) {
514
+ return die(`analysis-story-tree: could not write ${PREFS_PATH}: ${e.message}`, 3);
515
+ }
516
+ process.stdout.write(
517
+ none ? "issueTree: no sub-tasks\n" : `issueTree: sub-tasks ${names.join(", ")}\n`,
518
+ );
519
+ return null;
520
+ }
521
+
456
522
  async function main() {
523
+ if (SAVE_SUBTASKS !== null) return saveSubtasks(SAVE_SUBTASKS);
457
524
  if (!file)
458
525
  return die(
459
526
  "usage: analysis-story-tree.mjs <analysis.md> [--json] [--brief] [--prefs FILE] [--page-url URL]",
@@ -557,7 +624,10 @@ async function main() {
557
624
  for (const line of s.description.split("\n")) process.stdout.write(` ${line}\n`);
558
625
  }
559
626
  for (const t of s.subtasks) {
560
- process.stdout.write(` subtask ${t.role} [${t.label}]\n`);
627
+ const type = subtaskTypeFor(result.prefsUsed.subtaskIssueType, t.role);
628
+ process.stdout.write(
629
+ ` subtask ${t.role} type ${type ?? "discovered from createmeta"} [${t.label}]\n`,
630
+ );
561
631
  }
562
632
  for (const c of s.clones || []) {
563
633
  process.stdout.write(
@@ -47,7 +47,7 @@
47
47
  "importPatterns": [
48
48
  {
49
49
  "id": "import",
50
- "regex": "^\\s*import\\s+(?:class|struct|enum|protocol|func|var|let|typealias)\\s+([A-Za-z_][A-Za-z0-9_]*)|^\\s*import\\s+([A-Za-z_][A-Za-z0-9_]*)"
50
+ "regex": "^\\s*(?:@[A-Za-z_]\\w*(?:\\([^)\\n]*\\))?\\s+)*import\\s+(?:class|struct|enum|protocol|func|var|let|typealias)\\s+([A-Za-z_][A-Za-z0-9_]*)|^\\s*(?:@[A-Za-z_]\\w*(?:\\([^)\\n]*\\))?\\s+)*import\\s+([A-Za-z_][A-Za-z0-9_]*)"
51
51
  }
52
52
  ],
53
53
  "referenceKinds": ["class", "struct", "enum", "protocol", "actor", "typealias"],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-13T08:10:09Z",
3
+ "generatedAt": "2026-10-13T19:19:55Z",
4
4
  "skillCount": 228,
5
5
  "entries": [
6
6
  {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "shared/core/multi-agent-analysis-jira/SKILL.md",
16
- "sha256": "130543d1017115de351a1175b6b179778642ba0b3207034b54114b39d0546407"
16
+ "sha256": "798061199762b141d0312b56f9e8e10af1324fb867393929565ebe4e61fcbc81"
17
17
  },
18
18
  {
19
19
  "path": "shared/core/multi-agent-analysis-resolve/SKILL.md",
@@ -69,6 +69,13 @@ Then ask for the epic, once. `AskUserQuestion`:
69
69
  - **Link to an existing epic** - the user types its key (`--epic <KEY>`)
70
70
  - **No epic** - the stories are created unlinked
71
71
 
72
+ When the plan's `prefsUsed.subtaskRoles` is empty and `prefsUsed.subtaskRolesDeclined`
73
+ is false, attended runs ask once which sub-tasks every story gets. List the site's types
74
+ with `bash "$HOME/.claude/lib/analysis-jira-write.sh" --subtask-types --project "<KEY>"`,
75
+ offer them as a multi-select `AskUserQuestion` plus **No sub-tasks**, save the answer with
76
+ `node "$HOME/.claude/scripts/analysis-story-tree.mjs" --save-subtasks "<type>,<type>"` (or
77
+ `none`), and re-run Phase 1. Unattended runs never ask.
78
+
72
79
  Then the dry run, which is what proves the writer agrees with the plan:
73
80
 
74
81
  ```bash