@gobing-ai/spur 0.4.0 → 0.4.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.
Files changed (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/plugin-scripts.json +15 -0
  7. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  8. package/config/rules/recommended-pre-check.yaml +8 -3
  9. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  10. package/config/script-placement-baseline.json +2 -2
  11. package/config/templates/AGENTS.md +1 -1
  12. package/config/workflows/feature-lifecycle.yaml +9 -2
  13. package/config/workflows/history-anatomy.yaml +22 -0
  14. package/config/workflows/idea-pipeline.yaml +75 -0
  15. package/config/workflows/task-pipeline.yaml +216 -57
  16. package/config/workflows/wrapup-pipeline.yaml +107 -14
  17. package/package.json +10 -10
  18. package/plugins/sp/README.md +12 -3
  19. package/plugins/sp/agents/super-planner.md +16 -6
  20. package/plugins/sp/agents/super-reviewer.md +14 -0
  21. package/plugins/sp/bunfig.toml +8 -0
  22. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  23. package/plugins/sp/commands/dev-find-way.md +80 -0
  24. package/plugins/sp/commands/dev-idea.md +6 -0
  25. package/plugins/sp/commands/dev-plan.md +6 -0
  26. package/plugins/sp/commands/dev-review-session.md +4 -1
  27. package/plugins/sp/commands/dev-run.md +12 -2
  28. package/plugins/sp/commands/dev-runall.md +32 -15
  29. package/plugins/sp/hooks/context-owner.ts +56 -0
  30. package/plugins/sp/hooks/context-post-tool.ts +23 -0
  31. package/plugins/sp/hooks/context-session-start.ts +49 -27
  32. package/plugins/sp/hooks/context-session-stop.ts +131 -30
  33. package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
  34. package/plugins/sp/lib/idea-handoff.generated.mjs +43 -5
  35. package/plugins/sp/lib/inline-run-app.ts +28 -0
  36. package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
  37. package/plugins/sp/lib/inline-run.generated.mjs +351 -46
  38. package/plugins/sp/lib/plan-projection.generated.d.mts +47 -0
  39. package/plugins/sp/lib/plan-projection.generated.mjs +368 -0
  40. package/plugins/sp/lib/quality-gate.generated.d.mts +35 -1
  41. package/plugins/sp/lib/quality-gate.generated.mjs +30296 -71
  42. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  43. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  44. package/plugins/sp/lib/run-summary-core.ts +561 -0
  45. package/plugins/sp/lib/transcript.ts +150 -7
  46. package/plugins/sp/plugin.json +1 -1
  47. package/plugins/sp/references/roles.md +1 -1
  48. package/plugins/sp/scripts/batch-plan.mjs +488 -0
  49. package/plugins/sp/scripts/batch-plan.ts +145 -0
  50. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  51. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  52. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  53. package/plugins/sp/scripts/commit-guard.ts +249 -0
  54. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  55. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  56. package/plugins/sp/scripts/inline-run-setup.mjs +562 -80
  57. package/plugins/sp/scripts/inline-run-setup.ts +52 -78
  58. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  59. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  60. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  61. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  62. package/plugins/sp/scripts/quality-gate.mjs +30213 -61
  63. package/plugins/sp/scripts/quality-gate.ts +21 -5
  64. package/plugins/sp/scripts/residual-scan.mjs +86 -31
  65. package/plugins/sp/scripts/residual-scan.ts +28 -30
  66. package/plugins/sp/scripts/run-summary.mjs +432 -14
  67. package/plugins/sp/scripts/run-summary.ts +41 -177
  68. package/plugins/sp/scripts/session-timeline.mjs +109 -11
  69. package/plugins/sp/scripts/session-timeline.ts +57 -19
  70. package/plugins/sp/scripts/task-diffstat.mjs +32 -29
  71. package/plugins/sp/scripts/task-diffstat.ts +34 -34
  72. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  73. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  74. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  75. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  76. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  77. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  78. package/plugins/sp/skills/code-verification/SKILL.md +3 -4
  79. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  80. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  81. package/plugins/sp/skills/fleet-join/SKILL.md +1 -1
  82. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  83. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  84. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  85. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  86. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  87. package/plugins/sp/skills/session-review/SKILL.md +40 -7
  88. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  89. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  90. package/plugins/sp/skills/spur-cli/SKILL.md +5 -2
  91. package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
  92. package/plugins/sp/skills/spur-cli/references/decision.md +16 -8
  93. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  94. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  95. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -3
  96. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  97. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  98. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  99. package/plugins/sp/skills/spur-composer/SKILL.md +1 -1
  100. package/plugins/sp/skills/spur-dev/SKILL.md +20 -14
  101. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +33 -3
  102. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  103. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -17
  104. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  105. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +199 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-batch.md +156 -1024
  107. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +133 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -0
  109. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  110. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  111. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +44 -12
  112. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  113. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +340 -165
  114. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  115. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  116. package/schemas/state-machine-workflow.schema.json +23 -0
  117. package/spur.js +42402 -26889
  118. package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.B3GQZvj8.js} +1 -1
  119. package/web/_astro/BoardApp.BT7L2kSb.js +324 -0
  120. package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
  121. package/web/_astro/{arc.f4e0OYLg.js → arc.vaT7tPOB.js} +1 -1
  122. package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
  123. package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
  124. package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
  125. package/web/_astro/channel.D4nG9YLf.js +1 -0
  126. package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
  127. package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
  128. package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
  129. package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
  130. package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
  131. package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
  132. package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
  133. package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
  134. package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
  135. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
  136. package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
  137. package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
  138. package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
  139. package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
  140. package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
  141. package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
  142. package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
  143. package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
  144. package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
  145. package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
  146. package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
  147. package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
  148. package/web/_astro/index.BhSwN8yY.css +1 -0
  149. package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
  150. package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
  151. package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
  152. package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
  153. package/web/_astro/{linear.DguUUe6V.js → linear.BzGnBwNL.js} +1 -1
  154. package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.CSbTpIVz.js} +4 -4
  155. package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
  156. package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
  157. package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
  158. package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
  159. package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
  160. package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
  161. package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
  162. package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
  163. package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
  164. package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
  165. package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
  166. package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
  167. package/web/index.html +2 -2
  168. package/plugins/sp/commands/dev-parallel.md +0 -31
  169. package/web/_astro/BoardApp.Bzr4OXrW.js +0 -324
  170. package/web/_astro/TaskDetail.LAyR_Pvn.js +0 -1
  171. package/web/_astro/channel.CUFdtWhm.js +0 -1
  172. package/web/_astro/index.Dg_3bOaJ.css +0 -1
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // plugins/sp/scripts/batch-plan.ts
5
+ import { readFileSync } from "fs";
6
+
7
+ // plugins/sp/lib/plan-projection.generated.mjs
8
+ function planLetter(index) {
9
+ if (!Number.isInteger(index) || index < 0 || index > 25) {
10
+ throw new Error(`plan letter out of range: ${index} (expected 0..25)`);
11
+ }
12
+ return String.fromCharCode(65 + index);
13
+ }
14
+ function planChild(parent, index) {
15
+ if (!Number.isInteger(index) || index < 0 || index > 8) {
16
+ throw new Error(`plan child index out of range: ${index} (expected 0..8 under ${parent})`);
17
+ }
18
+ if (parent.length !== 1 || !/[A-Z]/.test(parent)) {
19
+ throw new Error(`plan child parent must be a single letter: ${parent}`);
20
+ }
21
+ return `${parent}${index + 1}`;
22
+ }
23
+ var PREPARE_ID = "prepare";
24
+ var REPORT_ID = "report";
25
+ var PHASE_ID_PREFIX = "phase.";
26
+ var TASK_ID_PREFIX = "task.";
27
+ var WAVE_SIZE = 24;
28
+ var TITLE_CAP = 60;
29
+ function isStateMachine(def) {
30
+ return def.kind !== "transition-flow" && def.kind !== "dag";
31
+ }
32
+ function hasDisplay(s) {
33
+ return s.display !== undefined;
34
+ }
35
+ function resolvePhaseTitle(states, phase) {
36
+ for (const s of states) {
37
+ if (s.display?.phase === phase && s.display.phaseTitle !== undefined)
38
+ return s.display.phaseTitle;
39
+ }
40
+ return phase;
41
+ }
42
+ function childText(label, title, id) {
43
+ return id.startsWith("prepare.") || id.startsWith("task.") || id === PREPARE_ID || id === REPORT_ID ? `${label} ${title}` : `${label} ${title} · ${id}`;
44
+ }
45
+ function buildPhasedPlan(def) {
46
+ if (!isStateMachine(def))
47
+ return null;
48
+ const annotated = def.states.filter(hasDisplay);
49
+ if (annotated.length === 0)
50
+ return null;
51
+ const items = [
52
+ { label: "A", id: PREPARE_ID, outcome: "pending", title: "Prepare", text: "A Prepare" },
53
+ {
54
+ label: "A1",
55
+ id: "prepare.1",
56
+ outcome: "pending",
57
+ parent: "A",
58
+ title: "Quick readiness",
59
+ text: "A1 Quick readiness"
60
+ },
61
+ { label: "A2", id: "prepare.2", outcome: "pending", parent: "A", title: "Prepare Git", text: "A2 Prepare Git" },
62
+ {
63
+ label: "A3",
64
+ id: "prepare.3",
65
+ outcome: "pending",
66
+ parent: "A",
67
+ title: "Publish plan",
68
+ text: "A3 Publish plan"
69
+ }
70
+ ];
71
+ const phases = [];
72
+ for (const s of def.states) {
73
+ if (s.display !== undefined && !phases.includes(s.display.phase))
74
+ phases.push(s.display.phase);
75
+ }
76
+ for (const [phaseIndex, phase] of phases.entries()) {
77
+ const letter = planLetter(phaseIndex + 1);
78
+ const phaseTitle = resolvePhaseTitle(def.states, phase);
79
+ items.push({
80
+ label: letter,
81
+ id: `${PHASE_ID_PREFIX}${phase}`,
82
+ outcome: "pending",
83
+ title: phaseTitle,
84
+ text: `${letter} ${phaseTitle}`
85
+ });
86
+ let childIndex = 0;
87
+ for (const s of def.states) {
88
+ if (s.display?.phase !== phase || s.display.show === "on-entry")
89
+ continue;
90
+ const label = planChild(letter, childIndex++);
91
+ const title = s.display.title ?? s.id;
92
+ items.push({
93
+ label,
94
+ id: s.id,
95
+ outcome: "pending",
96
+ parent: letter,
97
+ title,
98
+ text: childText(label, title, s.id)
99
+ });
100
+ }
101
+ }
102
+ return items;
103
+ }
104
+ function validatePhaseTable(def) {
105
+ if (!isStateMachine(def))
106
+ return [];
107
+ const annotated = def.states.filter(hasDisplay);
108
+ if (annotated.length === 0)
109
+ return [];
110
+ const errors = [];
111
+ const terminals = new Set(def.terminalStates ?? []);
112
+ for (const s of def.states) {
113
+ if (s.display === undefined && !terminals.has(s.id)) {
114
+ errors.push(`state "${s.id}" has no display phase — every non-terminal state must declare display when any state does`);
115
+ }
116
+ if (s.display !== undefined && terminals.has(s.id)) {
117
+ errors.push(`terminal state "${s.id}" must not declare display`);
118
+ }
119
+ }
120
+ const phases = [];
121
+ for (const s of annotated) {
122
+ if (!phases.includes(s.display.phase))
123
+ phases.push(s.display.phase);
124
+ }
125
+ if (phases.length > 25) {
126
+ errors.push(`${phases.length} phases exceed the 25 plan letters (B-Z); split the workflow or drop phase rows`);
127
+ }
128
+ for (const phase of phases) {
129
+ const members = annotated.filter((s) => s.display.phase === phase);
130
+ if (members.length > 9) {
131
+ errors.push(`phase "${phase}" has ${members.length} states — at most 9 digit children fit under one letter`);
132
+ }
133
+ const titles = [...new Set(members.map((s) => s.display.phaseTitle).filter((t) => t !== undefined))];
134
+ if (titles.length > 1) {
135
+ errors.push(`phase "${phase}" has conflicting phaseTitle values: ${titles.map((t) => `"${t}"`).join(", ")}`);
136
+ }
137
+ }
138
+ return errors;
139
+ }
140
+ function insertOnEntry(items, stateId, def) {
141
+ if (!isStateMachine(def))
142
+ return items;
143
+ if (items.some((i) => i.id === stateId))
144
+ return items;
145
+ const display = def.states.find((s) => s.id === stateId)?.display;
146
+ if (display === undefined || (def.terminalStates ?? []).includes(stateId))
147
+ return items;
148
+ const phaseRow = items.find((i) => i.id === `${PHASE_ID_PREFIX}${display.phase}`);
149
+ if (phaseRow === undefined)
150
+ return items;
151
+ const children = items.filter((i) => i.parent === phaseRow.label);
152
+ const label = planChild(phaseRow.label, children.length);
153
+ const title = display.title ?? stateId;
154
+ const item = {
155
+ label,
156
+ id: stateId,
157
+ outcome: "pending",
158
+ parent: phaseRow.label,
159
+ title,
160
+ text: childText(label, title, stateId)
161
+ };
162
+ const last = children.at(-1) ?? phaseRow;
163
+ const at = items.indexOf(last) + 1;
164
+ return [...items.slice(0, at), item, ...items.slice(at)];
165
+ }
166
+ function truncateTitle(name) {
167
+ if (name.length <= TITLE_CAP)
168
+ return name;
169
+ return `${name.slice(0, TITLE_CAP - 1).replace(/[\uD800-\uDBFF]$/, "")}…`;
170
+ }
171
+ function buildBatchPlan(tasks) {
172
+ const waves = [];
173
+ for (let offset = 0;offset < tasks.length; offset += WAVE_SIZE) {
174
+ const items = [
175
+ { label: "A", id: PREPARE_ID, outcome: "pending", title: "Prepare batch", text: "A Prepare batch" },
176
+ {
177
+ label: "A1",
178
+ id: "prepare.1",
179
+ outcome: "pending",
180
+ parent: "A",
181
+ title: "Resolve and freeze task set",
182
+ text: "A1 Resolve and freeze task set"
183
+ },
184
+ {
185
+ label: "A2",
186
+ id: "prepare.2",
187
+ outcome: "pending",
188
+ parent: "A",
189
+ title: "Order by dependencies",
190
+ text: "A2 Order by dependencies"
191
+ },
192
+ {
193
+ label: "A3",
194
+ id: "prepare.3",
195
+ outcome: "pending",
196
+ parent: "A",
197
+ title: "Prepare Git",
198
+ text: "A3 Prepare Git"
199
+ },
200
+ {
201
+ label: "A4",
202
+ id: "prepare.4",
203
+ outcome: "pending",
204
+ parent: "A",
205
+ title: "Publish plan",
206
+ text: "A4 Publish plan"
207
+ }
208
+ ];
209
+ tasks.slice(offset, offset + WAVE_SIZE).forEach((task, i) => {
210
+ const label = planLetter(i + 1);
211
+ const title = `${task.wbs} ${truncateTitle(task.name)}`;
212
+ items.push({
213
+ label,
214
+ id: `${TASK_ID_PREFIX}${task.wbs}`,
215
+ outcome: "pending",
216
+ title,
217
+ text: `${label} ${title}`
218
+ });
219
+ });
220
+ items.push({ label: "Z", id: REPORT_ID, outcome: "pending", title: "Batch report", text: "Z Batch report" });
221
+ waves.push(items);
222
+ }
223
+ return waves;
224
+ }
225
+ var byWbs = (a, b) => a.localeCompare(b, "en", { numeric: true });
226
+ function kahnOrder(nodes, preds) {
227
+ const indegree = new Map(nodes.map((n) => [n, preds.get(n)?.size ?? 0]));
228
+ const succ = new Map(nodes.map((n) => [n, []]));
229
+ for (const [n, ps] of preds)
230
+ for (const p of ps)
231
+ succ.get(p)?.push(n);
232
+ const ready = nodes.filter((n) => indegree.get(n) === 0).sort(byWbs);
233
+ const order = [];
234
+ for (let n = ready.shift();n !== undefined; n = ready.shift()) {
235
+ order.push(n);
236
+ for (const s of succ.get(n) ?? []) {
237
+ const d = (indegree.get(s) ?? 0) - 1;
238
+ indegree.set(s, d);
239
+ if (d === 0) {
240
+ ready.push(s);
241
+ ready.sort(byWbs);
242
+ }
243
+ }
244
+ }
245
+ return order;
246
+ }
247
+ function findCycle(remaining, preds) {
248
+ const state = new Map;
249
+ const stack = [];
250
+ const visit = (n) => {
251
+ state.set(n, "visiting");
252
+ stack.push(n);
253
+ for (const p of [...preds.get(n) ?? []].sort(byWbs)) {
254
+ if (!remaining.has(p))
255
+ continue;
256
+ if (state.get(p) === "visiting")
257
+ return [...stack.slice(stack.indexOf(p)), p];
258
+ if (!state.has(p)) {
259
+ const cycle = visit(p);
260
+ if (cycle)
261
+ return cycle;
262
+ }
263
+ }
264
+ stack.pop();
265
+ state.set(n, "done");
266
+ return null;
267
+ };
268
+ for (const n of [...remaining].sort(byWbs)) {
269
+ if (state.has(n))
270
+ continue;
271
+ const cycle = visit(n);
272
+ if (cycle)
273
+ return cycle;
274
+ }
275
+ return [];
276
+ }
277
+ function scheduleBatch(tasks, opts = {}) {
278
+ const byId = new Map;
279
+ for (const t of tasks)
280
+ if (!byId.has(t.wbs))
281
+ byId.set(t.wbs, t);
282
+ const nodes = [...byId.keys()].sort(byWbs);
283
+ const preds = new Map(nodes.map((n) => [n, new Set((byId.get(n)?.dependencies ?? []).filter((d) => byId.has(d)))]));
284
+ const depOrder = kahnOrder(nodes, preds);
285
+ if (depOrder.length < nodes.length) {
286
+ const cycle = findCycle(new Set(nodes.filter((n) => !depOrder.includes(n))), preds);
287
+ return {
288
+ order: [],
289
+ waves: [],
290
+ cycle,
291
+ overlaps: [],
292
+ mode: "sequential",
293
+ reason: `dependency cycle ${cycle.join(" → ")}`
294
+ };
295
+ }
296
+ const files = new Map(nodes.map((n) => [n, new Set((byId.get(n)?.files ?? []).map(normalizeTaskPath))]));
297
+ const overlaps = [];
298
+ for (let i = 0;i < depOrder.length; i++) {
299
+ const before = depOrder[i];
300
+ const beforeFiles = files.get(before) ?? new Set;
301
+ for (const after of depOrder.slice(i + 1)) {
302
+ const shared = [...beforeFiles].filter((f) => files.get(after)?.has(f)).sort();
303
+ if (shared.length === 0)
304
+ continue;
305
+ overlaps.push({ before, after, files: shared });
306
+ preds.get(after)?.add(before);
307
+ }
308
+ }
309
+ const order = kahnOrder(nodes, preds);
310
+ const level = new Map;
311
+ for (const n of order) {
312
+ let l = 0;
313
+ for (const p of preds.get(n) ?? [])
314
+ l = Math.max(l, (level.get(p) ?? 0) + 1);
315
+ level.set(n, l);
316
+ }
317
+ const waves = [];
318
+ for (const n of order) {
319
+ const l = level.get(n) ?? 0;
320
+ waves[l] = [...waves[l] ?? [], n].sort(byWbs);
321
+ }
322
+ const widest = Math.max(0, ...waves.map((w) => w.length));
323
+ const concurrency = opts.concurrency ?? 2;
324
+ let reason = `widest wave holds ${widest} independent tasks`;
325
+ if (nodes.length < 2)
326
+ reason = "fewer than two tasks";
327
+ else if (concurrency < 2)
328
+ reason = "concurrency is 1";
329
+ else if (widest < 2)
330
+ reason = "every wave holds one task (dependency or file-overlap chain)";
331
+ const mode = nodes.length >= 2 && concurrency >= 2 && widest >= 2 ? "parallel" : "sequential";
332
+ return { order, waves, cycle: null, overlaps, mode, reason };
333
+ }
334
+ function normalizeTaskPath(path) {
335
+ return path.trim().replace(/^\.\//, "");
336
+ }
337
+ function taskPhaseChildren(taskLetter, plan) {
338
+ if (!Array.isArray(plan))
339
+ return [];
340
+ return plan.filter((i) => i.id.startsWith(PHASE_ID_PREFIX) && i.label.length === 1).map((row, i) => {
341
+ const label = planChild(taskLetter, i);
342
+ return {
343
+ label,
344
+ id: row.id,
345
+ outcome: row.outcome,
346
+ parent: taskLetter,
347
+ title: row.title,
348
+ text: `${label} ${row.title}`
349
+ };
350
+ });
351
+ }
352
+ function hostStatus(outcome) {
353
+ if (outcome === "completed")
354
+ return "completed";
355
+ if (outcome === "active")
356
+ return "in_progress";
357
+ return "pending";
358
+ }
359
+ function hostText(item) {
360
+ const hostDecided = item.outcome === "skipped" || item.outcome === "failed" || item.outcome === "unattempted" || item.outcome === "blocked";
361
+ return hostDecided ? `${item.text} [${item.outcome}]` : item.text;
362
+ }
363
+
364
+ // plugins/sp/scripts/batch-plan.ts
365
+ var BATCH_PLAN_USAGE = "usage: batch-plan.ts waves --tasks <tasks.json> | order --tasks <tasks.json> [--concurrency <n>] | task-children --letter <A-Z> --plan <plan.json>";
366
+ function readJsonFile(path) {
367
+ return JSON.parse(readFileSync(path, "utf8"));
368
+ }
369
+ function isBatchTask(value) {
370
+ return typeof value === "object" && value !== null && typeof value.wbs === "string" && typeof value.name === "string";
371
+ }
372
+ function isStringArray(value) {
373
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
374
+ }
375
+ function isScheduleTask(value) {
376
+ if (typeof value !== "object" || value === null)
377
+ return false;
378
+ const t = value;
379
+ return typeof t.wbs === "string" && (t.dependencies === undefined || isStringArray(t.dependencies)) && (t.files === undefined || isStringArray(t.files));
380
+ }
381
+ function main(argv) {
382
+ const mode = argv[0];
383
+ if (mode === "waves") {
384
+ if (argv[1] !== "--tasks" || argv[2] === undefined) {
385
+ process.stderr.write(`${BATCH_PLAN_USAGE}
386
+ `);
387
+ return 2;
388
+ }
389
+ let tasks;
390
+ try {
391
+ tasks = readJsonFile(argv[2]);
392
+ } catch (error) {
393
+ process.stderr.write(`batch-plan: cannot read ${argv[2]} \u2014 ${error instanceof Error ? error.message : String(error)}
394
+ `);
395
+ return 1;
396
+ }
397
+ if (!Array.isArray(tasks) || !tasks.every(isBatchTask)) {
398
+ process.stderr.write(`batch-plan: --tasks file must be a JSON array of {wbs, name}
399
+ `);
400
+ return 1;
401
+ }
402
+ process.stdout.write(`${JSON.stringify({ waves: buildBatchPlan(tasks) })}
403
+ `);
404
+ return 0;
405
+ }
406
+ if (mode === "order") {
407
+ let tasksPath = "";
408
+ let concurrency = 2;
409
+ for (let i = 1;i + 1 < argv.length; i += 2) {
410
+ if (argv[i] === "--tasks")
411
+ tasksPath = argv[i + 1] ?? "";
412
+ else if (argv[i] === "--concurrency")
413
+ concurrency = Number(argv[i + 1]);
414
+ }
415
+ if (tasksPath === "" || !Number.isInteger(concurrency) || concurrency < 1) {
416
+ process.stderr.write(`${BATCH_PLAN_USAGE}
417
+ `);
418
+ return 2;
419
+ }
420
+ let tasks;
421
+ try {
422
+ tasks = readJsonFile(tasksPath);
423
+ } catch (error) {
424
+ process.stderr.write(`batch-plan: cannot read ${tasksPath} \u2014 ${error instanceof Error ? error.message : String(error)}
425
+ `);
426
+ return 1;
427
+ }
428
+ if (!Array.isArray(tasks) || !tasks.every(isScheduleTask)) {
429
+ process.stderr.write(`batch-plan: --tasks file must be a JSON array of {wbs, dependencies?, files?}
430
+ `);
431
+ return 1;
432
+ }
433
+ process.stdout.write(`${JSON.stringify(scheduleBatch(tasks, { concurrency }))}
434
+ `);
435
+ return 0;
436
+ }
437
+ if (mode === "task-children") {
438
+ let letter = "";
439
+ let planPath = "";
440
+ for (let i = 1;i + 1 < argv.length; i += 2) {
441
+ if (argv[i] === "--letter")
442
+ letter = argv[i + 1] ?? "";
443
+ else if (argv[i] === "--plan")
444
+ planPath = argv[i + 1] ?? "";
445
+ }
446
+ if (!/^[A-Z]$/.test(letter) || planPath === "") {
447
+ process.stderr.write(`${BATCH_PLAN_USAGE}
448
+ `);
449
+ return 2;
450
+ }
451
+ let payload;
452
+ try {
453
+ payload = readJsonFile(planPath);
454
+ } catch (error) {
455
+ process.stderr.write(`batch-plan: cannot read ${planPath} \u2014 ${error instanceof Error ? error.message : String(error)}
456
+ `);
457
+ return 1;
458
+ }
459
+ const plan = Array.isArray(payload) ? payload : payload.plan;
460
+ if (!Array.isArray(plan)) {
461
+ process.stderr.write("batch-plan: --plan file must be a plan array or a todo --json payload with a `plan` field\n");
462
+ return 1;
463
+ }
464
+ process.stdout.write(`${JSON.stringify(taskPhaseChildren(letter, plan))}
465
+ `);
466
+ return 0;
467
+ }
468
+ process.stderr.write(`${BATCH_PLAN_USAGE}
469
+ `);
470
+ return 2;
471
+ }
472
+ {
473
+ process.exit(main(process.argv.slice(2)));
474
+ }
475
+ export {
476
+ validatePhaseTable,
477
+ taskPhaseChildren,
478
+ scheduleBatch,
479
+ planLetter,
480
+ planChild,
481
+ main,
482
+ insertOnEntry,
483
+ hostText,
484
+ hostStatus,
485
+ buildPhasedPlan,
486
+ buildBatchPlan,
487
+ BATCH_PLAN_USAGE
488
+ };
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * batch-plan — letters-first batch plan surface (task 1104, feature I13).
4
+ *
5
+ * The projection logic lives in packages/app/src/workflow/plan-projection.ts (unit-tested
6
+ * there); this script is ADR-130 glue — argv/IO only — over the bundled core in
7
+ * ../lib/plan-projection.generated.* (regenerate: `bun run build:plugin-lib && bun run build:scripts`).
8
+ *
9
+ * Modes:
10
+ * waves --tasks <file> file: JSON array of {wbs, name}; prints `{ waves: [...] }` — waves
11
+ * of 24 tasks as A Prepare batch + A1..A4 + B..Y task rows + Z Batch report.
12
+ * order --tasks <file> [--concurrency <n>]
13
+ * file: JSON array of {wbs, dependencies?, files?}; prints the scheduleBatch
14
+ * result `{ order, waves, cycle, overlaps, mode, reason }` (dev-runall §2.4
15
+ * order + the --mode auto recommendation). A cycle is data, not an error.
16
+ * task-children --letter <A-Z> --plan <file>
17
+ * file: a `workflow show --format todo --json` payload (or a bare plan
18
+ * array); prints the task's phase digits as a JSON array.
19
+ * Exit 0 on success, 1 on unreadable/invalid input, 2 on usage error. Runs under bare
20
+ * `node batch-plan.mjs` (committed twin) — keep the twin in lockstep.
21
+ */
22
+ import { readFileSync } from 'node:fs';
23
+ import type { BatchTask, PhasedPlanItem, ScheduleTask } from '../lib/plan-projection.generated.mjs';
24
+ import { buildBatchPlan, scheduleBatch, taskPhaseChildren } from '../lib/plan-projection.generated.mjs';
25
+
26
+ export * from '../lib/plan-projection.generated.mjs';
27
+
28
+ export const BATCH_PLAN_USAGE =
29
+ 'usage: batch-plan.ts waves --tasks <tasks.json> | order --tasks <tasks.json> [--concurrency <n>] | task-children --letter <A-Z> --plan <plan.json>';
30
+
31
+ function readJsonFile(path: string): unknown {
32
+ return JSON.parse(readFileSync(path, 'utf8'));
33
+ }
34
+
35
+ function isBatchTask(value: unknown): value is BatchTask {
36
+ return (
37
+ typeof value === 'object' &&
38
+ value !== null &&
39
+ typeof (value as BatchTask).wbs === 'string' &&
40
+ typeof (value as BatchTask).name === 'string'
41
+ );
42
+ }
43
+
44
+ function isStringArray(value: unknown): value is string[] {
45
+ return Array.isArray(value) && value.every((v) => typeof v === 'string');
46
+ }
47
+
48
+ function isScheduleTask(value: unknown): value is ScheduleTask {
49
+ if (typeof value !== 'object' || value === null) return false;
50
+ const t = value as ScheduleTask;
51
+ return (
52
+ typeof t.wbs === 'string' &&
53
+ (t.dependencies === undefined || isStringArray(t.dependencies)) &&
54
+ (t.files === undefined || isStringArray(t.files))
55
+ );
56
+ }
57
+
58
+ export function main(argv: string[]): number {
59
+ const mode = argv[0];
60
+ if (mode === 'waves') {
61
+ if (argv[1] !== '--tasks' || argv[2] === undefined) {
62
+ process.stderr.write(`${BATCH_PLAN_USAGE}\n`);
63
+ return 2;
64
+ }
65
+ let tasks: unknown;
66
+ try {
67
+ tasks = readJsonFile(argv[2]);
68
+ } catch (error) {
69
+ process.stderr.write(
70
+ `batch-plan: cannot read ${argv[2]} — ${error instanceof Error ? error.message : String(error)}\n`,
71
+ );
72
+ return 1;
73
+ }
74
+ if (!Array.isArray(tasks) || !tasks.every(isBatchTask)) {
75
+ process.stderr.write('batch-plan: --tasks file must be a JSON array of {wbs, name}\n');
76
+ return 1;
77
+ }
78
+ process.stdout.write(`${JSON.stringify({ waves: buildBatchPlan(tasks) })}\n`);
79
+ return 0;
80
+ }
81
+ if (mode === 'order') {
82
+ let tasksPath = '';
83
+ let concurrency = 2;
84
+ for (let i = 1; i + 1 < argv.length; i += 2) {
85
+ if (argv[i] === '--tasks') tasksPath = argv[i + 1] ?? '';
86
+ else if (argv[i] === '--concurrency') concurrency = Number(argv[i + 1]);
87
+ }
88
+ if (tasksPath === '' || !Number.isInteger(concurrency) || concurrency < 1) {
89
+ process.stderr.write(`${BATCH_PLAN_USAGE}\n`);
90
+ return 2;
91
+ }
92
+ let tasks: unknown;
93
+ try {
94
+ tasks = readJsonFile(tasksPath);
95
+ } catch (error) {
96
+ process.stderr.write(
97
+ `batch-plan: cannot read ${tasksPath} — ${error instanceof Error ? error.message : String(error)}\n`,
98
+ );
99
+ return 1;
100
+ }
101
+ if (!Array.isArray(tasks) || !tasks.every(isScheduleTask)) {
102
+ process.stderr.write('batch-plan: --tasks file must be a JSON array of {wbs, dependencies?, files?}\n');
103
+ return 1;
104
+ }
105
+ process.stdout.write(`${JSON.stringify(scheduleBatch(tasks, { concurrency }))}\n`);
106
+ return 0;
107
+ }
108
+ if (mode === 'task-children') {
109
+ let letter = '';
110
+ let planPath = '';
111
+ for (let i = 1; i + 1 < argv.length; i += 2) {
112
+ if (argv[i] === '--letter') letter = argv[i + 1] ?? '';
113
+ else if (argv[i] === '--plan') planPath = argv[i + 1] ?? '';
114
+ }
115
+ if (!/^[A-Z]$/.test(letter) || planPath === '') {
116
+ process.stderr.write(`${BATCH_PLAN_USAGE}\n`);
117
+ return 2;
118
+ }
119
+ let payload: unknown;
120
+ try {
121
+ payload = readJsonFile(planPath);
122
+ } catch (error) {
123
+ process.stderr.write(
124
+ `batch-plan: cannot read ${planPath} — ${error instanceof Error ? error.message : String(error)}\n`,
125
+ );
126
+ return 1;
127
+ }
128
+ // Accept a bare plan array or a full `workflow show --format todo --json` payload.
129
+ const plan = Array.isArray(payload) ? payload : (payload as { plan?: PhasedPlanItem[] }).plan;
130
+ if (!Array.isArray(plan)) {
131
+ process.stderr.write(
132
+ 'batch-plan: --plan file must be a plan array or a todo --json payload with a `plan` field\n',
133
+ );
134
+ return 1;
135
+ }
136
+ process.stdout.write(`${JSON.stringify(taskPhaseChildren(letter, plan as PhasedPlanItem[]))}\n`);
137
+ return 0;
138
+ }
139
+ process.stderr.write(`${BATCH_PLAN_USAGE}\n`);
140
+ return 2;
141
+ }
142
+
143
+ if (import.meta.main) {
144
+ process.exit(main(process.argv.slice(2)));
145
+ }