@pareto4/pp-core 1.13.0 → 1.18.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.
Files changed (204) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +31 -12
  3. package/agents/pp-code-fixer.md +0 -11
  4. package/agents/pp-code-reviewer.md +1 -77
  5. package/agents/pp-debug-session-manager.md +9 -6
  6. package/agents/pp-executor.md +12 -29
  7. package/agents/pp-plan-checker.md +4 -4
  8. package/agents/pp-planner.md +34 -20
  9. package/agents/pp-vault-reviewer.md +100 -23
  10. package/agents/pp-verifier.md +1 -3
  11. package/bin/install.js +10 -3
  12. package/commands/pp-execute-phase.md +1 -3
  13. package/commands/pp-ns-manage.md +1 -1
  14. package/commands/pp-ns-review.md +1 -1
  15. package/commands/pp-plan-phase.md +1 -2
  16. package/commands/pp-vault-promote.md +7 -8
  17. package/commands/pp-vault-review.md +6 -5
  18. package/commands/pp-verify-work.md +1 -2
  19. package/gemini-extension.json +1 -1
  20. package/hooks/dist/pp-check-update.js +0 -0
  21. package/hooks/dist/pp-cursor-session-start.js +1 -1
  22. package/hooks/dist/pp-statusline.js +1 -1
  23. package/hooks/dist/pp-update-banner.js +0 -0
  24. package/hooks/pp-check-update.js +0 -0
  25. package/hooks/pp-cursor-session-start.js +1 -1
  26. package/hooks/pp-statusline.js +1 -1
  27. package/hooks/pp-update-banner.js +0 -0
  28. package/package.json +10 -18
  29. package/pareto-skills/_shared/create-project-board.sh +0 -0
  30. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  31. package/pp-core/bin/check-latest-version.cjs +0 -0
  32. package/pp-core/bin/detect-build-commands.sh +2 -2
  33. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  34. package/pp-core/bin/lib/capability-registry.cjs +201 -229
  35. package/pp-core/bin/lib/capability-state.cjs +93 -1
  36. package/pp-core/bin/lib/capability-writer.cjs +3 -3
  37. package/pp-core/bin/lib/check-command-router.cjs +1906 -1514
  38. package/pp-core/bin/lib/clusters.cjs +3 -0
  39. package/pp-core/bin/lib/command-aliases.cjs +0 -35
  40. package/pp-core/bin/lib/commands.cjs +9 -4
  41. package/pp-core/bin/lib/compose-bindings.cjs +251 -88
  42. package/pp-core/bin/lib/config-loader.cjs +7 -103
  43. package/pp-core/bin/lib/config-schema.cjs +1 -1
  44. package/pp-core/bin/lib/config.cjs +151 -62
  45. package/pp-core/bin/lib/decisions.cjs +4 -4
  46. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  47. package/pp-core/bin/lib/frontmatter.cjs +1 -38
  48. package/pp-core/bin/lib/git-base-branch.cjs +11 -12
  49. package/pp-core/bin/lib/git-provider.cjs +9 -10
  50. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  51. package/pp-core/bin/lib/init-contract.cjs +620 -738
  52. package/pp-core/bin/lib/init-interactive.cjs +62 -4
  53. package/pp-core/bin/lib/init.cjs +24 -51
  54. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  55. package/pp-core/bin/lib/io.cjs +4 -2
  56. package/pp-core/bin/lib/loop-resolver.cjs +165 -129
  57. package/pp-core/bin/lib/org-profile.cjs +402 -161
  58. package/pp-core/bin/lib/package-identity.cjs +3 -3
  59. package/pp-core/bin/lib/phase-command-router.cjs +1 -29
  60. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  61. package/pp-core/bin/lib/phase.cjs +11 -253
  62. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  63. package/pp-core/bin/lib/profile-output.cjs +1 -1
  64. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  65. package/pp-core/bin/lib/state.cjs +11 -85
  66. package/pp-core/bin/lib/template.cjs +1 -1
  67. package/pp-core/bin/lib/vault-command-router.cjs +1682 -477
  68. package/pp-core/bin/lib/vault-coordinates.cjs +16 -7
  69. package/pp-core/bin/lib/vault-index.cjs +656 -0
  70. package/pp-core/bin/lib/vault-seal.cjs +1851 -0
  71. package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
  72. package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1504 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +155 -2778
  75. package/pp-core/bin/lib/vault-update.cjs +10 -59
  76. package/pp-core/bin/lib/vault.cjs +2570 -950
  77. package/pp-core/bin/lib/verify.cjs +0 -11
  78. package/pp-core/bin/portable-timeout.sh +1 -1
  79. package/pp-core/bin/pp-tools.cjs +157 -54
  80. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  81. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  82. package/pp-core/bin/shared/model-catalog.json +2 -2
  83. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  84. package/pp-core/org-profile.json +26 -0
  85. package/pp-core/references/autonomous-best-effort.md +8 -7
  86. package/pp-core/references/loop-hook-dispatch.md +0 -25
  87. package/pp-core/references/planner-gap-closure.md +0 -1
  88. package/pp-core/references/planner-tdd.md +0 -1
  89. package/pp-core/references/planner-vault-standards.md +311 -0
  90. package/pp-core/references/planning-config.md +0 -8
  91. package/pp-core/references/tdd.md +0 -1
  92. package/pp-core/templates/README.md +1 -0
  93. package/pp-core/templates/VAULT-REVIEW.md +56 -0
  94. package/pp-core/templates/phase-prompt.md +2 -6
  95. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  96. package/pp-core/workflows/add-tests.md +55 -4
  97. package/pp-core/workflows/audit-fix.md +63 -2
  98. package/pp-core/workflows/audit-milestone.md +1 -1
  99. package/pp-core/workflows/autonomous.md +85 -11
  100. package/pp-core/workflows/check-todos.md +13 -2
  101. package/pp-core/workflows/code-review-fix.md +59 -3
  102. package/pp-core/workflows/code-review.md +10 -57
  103. package/pp-core/workflows/complete-milestone.md +1 -1
  104. package/pp-core/workflows/debug.md +54 -4
  105. package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
  106. package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
  107. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  108. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  109. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  110. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  111. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  112. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  113. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  114. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  118. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  122. package/pp-core/workflows/discovery-sprint/vault-context.md +207 -0
  123. package/pp-core/workflows/discovery-sprint.md +7 -3
  124. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  125. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  126. package/pp-core/workflows/discovery-to-build.md +10 -2
  127. package/pp-core/workflows/discuss-phase.md +3 -10
  128. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +327 -0
  129. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  130. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  131. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +78 -0
  132. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  133. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  134. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  135. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +219 -20
  136. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +147 -0
  137. package/pp-core/workflows/execute-phase.md +152 -128
  138. package/pp-core/workflows/execute-plan.md +54 -6
  139. package/pp-core/workflows/fast.md +53 -4
  140. package/pp-core/workflows/help/modes/full.md +3 -3
  141. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  142. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  143. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  144. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  145. package/pp-core/workflows/init.md +232 -272
  146. package/pp-core/workflows/new-project.md +31 -23
  147. package/pp-core/workflows/plan-phase.md +98 -45
  148. package/pp-core/workflows/quick.md +117 -3
  149. package/pp-core/workflows/secure-phase.md +1 -1
  150. package/pp-core/workflows/ship.md +57 -28
  151. package/pp-core/workflows/undo.md +1 -1
  152. package/pp-core/workflows/update.md +3 -3
  153. package/pp-core/workflows/validate-phase.md +58 -3
  154. package/pp-core/workflows/vault-review.md +161 -23
  155. package/pp-core/workflows/vault-write-back.md +73 -110
  156. package/pp-core/workflows/verify-work.md +12 -14
  157. package/scripts/changeset/cli.cjs +6 -5
  158. package/scripts/changeset/lint.cjs +0 -0
  159. package/scripts/changeset/new.cjs +33 -50
  160. package/scripts/dev-env.cjs +25 -85
  161. package/scripts/gen-capability-registry.cjs +120 -13
  162. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  163. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  164. package/scripts/lint-docs-required.cjs +0 -0
  165. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  166. package/scripts/lint-test-file-count.allowlist.json +3 -19
  167. package/scripts/lint-test-file-count.cjs +5 -6
  168. package/scripts/prompt-injection-scan.sh +19 -17
  169. package/scripts/release-channel.cjs +0 -0
  170. package/scripts/sync-manifest-versions.cjs +5 -4
  171. package/scripts/sync-rulesets.sh +1 -1
  172. package/scripts/update-size-baseline.cjs +30 -274
  173. package/scripts/verify-opengsd-sweep.cjs +3 -5
  174. package/scripts/workflow-size.cjs +9 -61
  175. package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
  176. package/pp-core/bin/lib/install-run.cjs +0 -536
  177. package/pp-core/bin/lib/loop-gates.cjs +0 -213
  178. package/pp-core/bin/lib/phase-manifest.cjs +0 -527
  179. package/pp-core/bin/lib/phase-tier.cjs +0 -480
  180. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  181. package/pp-core/bin/lib/practices-review-check.cjs +0 -938
  182. package/pp-core/bin/lib/vault-audit.cjs +0 -2070
  183. package/pp-core/bin/lib/vault-practices.cjs +0 -1990
  184. package/pp-core/bin/lib/vault-review.cjs +0 -2359
  185. package/pp-core/bin/lib/vault-write-back.cjs +0 -877
  186. package/pp-core/references/executor-vault-trace.md +0 -47
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/references/planner-vault-practices.md +0 -112
  189. package/pp-core/templates/practices/README.md +0 -251
  190. package/pp-core/templates/practices/error-handling.md +0 -18
  191. package/pp-core/templates/practices/minimal-comments.md +0 -14
  192. package/pp-core/templates/practices/naming-convention.md +0 -18
  193. package/pp-core/templates/practices/practice-template.md +0 -23
  194. package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
  195. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  196. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  197. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  198. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
  199. package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
  200. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
  201. package/scripts/lib/size-budget.cjs +0 -311
  202. package/scripts/lint-neutrality-names.cjs +0 -694
  203. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  204. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -1,368 +0,0 @@
1
- "use strict";
2
- /**
3
- * Plan graph — the single owner of how a phase's plans are ordered.
4
- *
5
- * Computes, from each PLAN.md's frontmatter (`depends_on`, `files_modified`,
6
- * `serial_group`, `chain_reason`, task shapes):
7
- *
8
- * - the wave of every plan;
9
- * - the serial groups (plans that share a wave but must run one after
10
- * another because they touch the same non-source file);
11
- * - the chain metrics (plan count, wave count, longest chain, and how many
12
- * links of that chain exist only because two plans touch the same
13
- * non-source file);
14
- * - the chain check the plan checker enforces (a longest chain longer than
15
- * half the plan count FAILS unless every link of it carries a recorded
16
- * `chain_reason`);
17
- * - same-shape merge candidates for the planner.
18
- *
19
- * Callers: `phase-plan-index` (src/phase.cts) adds this to the plan inventory.
20
- * The planner and plan-checker agents read that output; they never compute a
21
- * wave or a chain themselves.
22
- *
23
- * ## The wave rule
24
- *
25
- * A plan's wave is its longest-path level over two kinds of edges:
26
- * 1. declared `depends_on` edges;
27
- * 2. implicit edges between two plans that share a SOURCE file (see
28
- * `classifyPlanFile`) and are not already ordered by (1). The implicit
29
- * edge runs from the earlier plan (file order) to the later one.
30
- * Two plans that share only NON-source files (a test file, a size baseline, a
31
- * changeset, a doc, a planning file) get no edge: they stay in the same wave
32
- * and are put in one serial group instead.
33
- */
34
- Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.PLAN_TASK_CAP = exports.PLAN_TITLE_MAX = void 0;
36
- exports.classifyPlanFile = classifyPlanFile;
37
- exports.isInstructionFile = isInstructionFile;
38
- exports.isSourceFile = isSourceFile;
39
- exports.planTitleProblem = planTitleProblem;
40
- exports.readPlanTitle = readPlanTitle;
41
- exports.computePlanGraph = computePlanGraph;
42
- const SOURCE_DIRS = ['src/', 'bin/', 'pp-core/bin/', 'hooks/', 'scripts/'];
43
- const DOC_EXTENSIONS = /\.(md|mdx|markdown|rst|txt|adoc)$/i;
44
- const DOC_BASENAMES = /^(LICENSE|NOTICE|CHANGELOG|AUTHORS)(\.[a-z]+)?$/i;
45
- function normalizePath(file) {
46
- return String(file).trim().replace(/\\/g, '/').replace(/^\.\//, '');
47
- }
48
- /**
49
- * Classify one `files_modified` entry. This is THE source/non-source predicate
50
- * for plan ordering; nothing else decides it.
51
- *
52
- * Checked in this order (first match wins):
53
- * planning — under `.planning/`
54
- * changeset — under `.changeset/` or `.changesets/`
55
- * baseline — a `*baseline*.json`/`*.baseline.*` file, `__snapshots__/`, `*.snap`
56
- * test — under a `test/`, `tests/` or `__tests__/` directory or a
57
- * `*.Tests/` project; or named `*.test.*`, `*.spec.*`,
58
- * `*_test.go|py`, `test_*.py`
59
- * source — under `src/`, `bin/`, `pp-core/bin/`, `hooks/` or `scripts/`
60
- * (pp-core ships executable code from these)
61
- * doc — a `.md/.mdx/.markdown/.rst/.txt/.adoc` file, anything under
62
- * `docs/` or `doc/`, or LICENSE/NOTICE/CHANGELOG/AUTHORS. In
63
- * pp-core this includes workflow, agent, reference and template
64
- * markdown.
65
- * source — anything else. An unrecognised file is treated as source, so
66
- * it keeps the strict "shared file = later wave" rule.
67
- */
68
- function classifyPlanFile(file) {
69
- const p = normalizePath(file);
70
- const base = p.split('/').pop() ?? p;
71
- if (p.startsWith('.planning/'))
72
- return 'planning';
73
- if (/^\.changesets?\//.test(p))
74
- return 'changeset';
75
- if (/baseline[^/]*\.json$/i.test(base) || /\.baseline\./i.test(base) || /(^|\/)__snapshots__\//.test(p) || /\.snap$/i.test(base)) {
76
- return 'baseline';
77
- }
78
- if (/(^|\/)(tests?|__tests__)\//.test(p) ||
79
- /(^|\/)[^/]+\.Tests?\//.test(p) ||
80
- /\.(test|spec)\.[^/]+$/i.test(base) ||
81
- /_test\.(go|py)$/i.test(base) ||
82
- /^test_[^/]*\.py$/i.test(base)) {
83
- return 'test';
84
- }
85
- if (SOURCE_DIRS.some((dir) => p.startsWith(dir)))
86
- return 'source';
87
- if (DOC_EXTENSIONS.test(base) || /^docs?\//.test(p) || DOC_BASENAMES.test(base))
88
- return 'doc';
89
- return 'source';
90
- }
91
- /** A workflow or agent markdown file (the unit same-shape plans are made of). */
92
- function isInstructionFile(file) {
93
- const p = normalizePath(file);
94
- return /\.md$/i.test(p) && (/(^|\/)workflows\//.test(p) || /(^|\/)agents\//.test(p));
95
- }
96
- /** True when `file` is a source file for plan ordering (see `classifyPlanFile`). */
97
- function isSourceFile(file) {
98
- return classifyPlanFile(file) === 'source';
99
- }
100
- /** Longest a plan title may be before it is reported (still valid). */
101
- exports.PLAN_TITLE_MAX = 100;
102
- /**
103
- * Problem with a plan's `title:` frontmatter value, or null when it is a valid
104
- * one-line title. `undefined` means the key is absent.
105
- */
106
- function planTitleProblem(value) {
107
- if (value === undefined)
108
- return 'Missing required frontmatter field: title';
109
- if (typeof value !== 'string' || value.trim() === '') {
110
- return 'Frontmatter field title must be a non-empty one-line string';
111
- }
112
- if (/[\r\n]/.test(value))
113
- return 'Frontmatter field title must be one line';
114
- return null;
115
- }
116
- /** Normalised title, or null when absent/invalid. */
117
- function readPlanTitle(value) {
118
- return planTitleProblem(value) === null ? value.trim() : null;
119
- }
120
- /** Default task cap per plan (agents/pp-planner.md: "Each plan: 2-3 tasks maximum."). */
121
- exports.PLAN_TASK_CAP = 3;
122
- function slug(text) {
123
- return text.toLowerCase().replace(/\.[a-z0-9]+$/i, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'group';
124
- }
125
- function intersect(a, b) {
126
- return [...new Set(a.filter((f) => b.has(f)))].sort();
127
- }
128
- /**
129
- * Compute the plan graph. `nodes` must be in plan file order (that order
130
- * breaks ties and orients implicit edges).
131
- */
132
- function computePlanGraph(nodes, opts = {}) {
133
- const taskCap = opts.taskCap ?? exports.PLAN_TASK_CAP;
134
- const ids = nodes.map((n) => n.id);
135
- const index = new Map(ids.map((id, i) => [id, i]));
136
- const files = nodes.map((n) => n.filesModified.map(normalizePath).filter((f) => f !== ''));
137
- const fileSets = files.map((f) => new Set(f));
138
- const warnings = [];
139
- // Adjacency over indices: edges[i] = successors of i.
140
- const succ = nodes.map(() => new Set());
141
- nodes.forEach((n, j) => {
142
- for (const dep of n.dependsOn) {
143
- const i = index.get(dep);
144
- if (i === undefined || i === j)
145
- continue;
146
- succ[i].add(j);
147
- }
148
- });
149
- const levelsOf = () => {
150
- const inDeg = nodes.map(() => 0);
151
- succ.forEach((s) => s.forEach((j) => { inDeg[j]++; }));
152
- const level = nodes.map(() => 0);
153
- const queue = [];
154
- inDeg.forEach((d, i) => { if (d === 0)
155
- queue.push(i); });
156
- let head = 0;
157
- while (head < queue.length) {
158
- const cur = queue[head++];
159
- for (const nxt of [...succ[cur]].sort((a, b) => a - b)) {
160
- level[nxt] = Math.max(level[nxt], level[cur] + 1);
161
- if (--inDeg[nxt] === 0)
162
- queue.push(nxt);
163
- }
164
- }
165
- return { level, done: new Set(queue) };
166
- };
167
- // A declared cycle has no waves; nodes Kahn never dequeued are on or behind it.
168
- const declaredLevels = levelsOf();
169
- if (declaredLevels.done.size < nodes.length) {
170
- return emptyGraph(nodes, ids.filter((_, i) => !declaredLevels.done.has(i)));
171
- }
172
- const reaches = (from, to) => {
173
- const stack = [from];
174
- const seen = new Set([from]);
175
- while (stack.length > 0) {
176
- const cur = stack.pop();
177
- if (cur === to)
178
- return true;
179
- for (const nxt of succ[cur]) {
180
- if (!seen.has(nxt)) {
181
- seen.add(nxt);
182
- stack.push(nxt);
183
- }
184
- }
185
- }
186
- return false;
187
- };
188
- // Implicit edges: a shared SOURCE file orders two otherwise-unordered plans.
189
- const implicitEdges = [];
190
- for (let i = 0; i < nodes.length; i++) {
191
- for (let j = i + 1; j < nodes.length; j++) {
192
- const sharedSource = intersect(files[j], fileSets[i]).filter(isSourceFile);
193
- if (sharedSource.length === 0)
194
- continue;
195
- if (reaches(i, j) || reaches(j, i))
196
- continue;
197
- succ[i].add(j);
198
- implicitEdges.push({ from: ids[i], to: ids[j], shared_source_files: sharedSource });
199
- }
200
- }
201
- const { level } = levelsOf();
202
- // Same-shape merge candidates: no source file, exactly one workflow/agent file
203
- // (any other files are tests, baselines, changesets or planning), same task shape.
204
- const shapeGroups = new Map();
205
- nodes.forEach((n, i) => {
206
- if (n.taskTypes.length === 0)
207
- return;
208
- const kinds = files[i].map(classifyPlanFile);
209
- if (kinds.includes('source'))
210
- return;
211
- if (files[i].filter(isInstructionFile).length !== 1 || kinds.filter((k) => k === 'doc').length !== 1)
212
- return;
213
- const shape = n.taskTypes.join('+');
214
- if (!shapeGroups.has(shape))
215
- shapeGroups.set(shape, []);
216
- shapeGroups.get(shape).push(i);
217
- });
218
- const mergeCandidates = [];
219
- for (const [shape, members] of shapeGroups) {
220
- if (members.length < 2)
221
- continue;
222
- const totalTasks = members.reduce((sum, i) => sum + nodes[i].taskTypes.length, 0);
223
- mergeCandidates.push({
224
- shape,
225
- plans: members.map((i) => ids[i]),
226
- total_tasks: totalTasks,
227
- task_cap: taskCap,
228
- action: totalTasks <= taskCap ? 'merge' : 'serial_group',
229
- });
230
- }
231
- // Serial groups: union-find within each level.
232
- const parent = nodes.map((_, i) => i);
233
- const find = (x) => (parent[x] === x ? x : (parent[x] = find(parent[x])));
234
- const union = (a, b) => {
235
- const ra = find(a);
236
- const rb = find(b);
237
- if (ra !== rb)
238
- parent[Math.max(ra, rb)] = Math.min(ra, rb);
239
- };
240
- for (let i = 0; i < nodes.length; i++) {
241
- for (let j = i + 1; j < nodes.length; j++) {
242
- if (level[i] !== level[j])
243
- continue;
244
- const shared = intersect(files[j], fileSets[i]);
245
- const sameDeclared = nodes[i].serialGroup !== null && nodes[i].serialGroup === nodes[j].serialGroup;
246
- if (shared.length > 0 || sameDeclared)
247
- union(i, j);
248
- }
249
- }
250
- for (const cand of mergeCandidates) {
251
- if (cand.action !== 'serial_group')
252
- continue;
253
- const members = cand.plans.map((id) => index.get(id));
254
- for (const a of members)
255
- for (const b of members)
256
- if (a < b && level[a] === level[b])
257
- union(a, b);
258
- }
259
- const groupsByRoot = new Map();
260
- nodes.forEach((_, i) => {
261
- const r = find(i);
262
- if (!groupsByRoot.has(r))
263
- groupsByRoot.set(r, []);
264
- groupsByRoot.get(r).push(i);
265
- });
266
- const serialGroupOf = Object.fromEntries(ids.map((id) => [id, null]));
267
- const serialGroups = [];
268
- const usedNames = new Set();
269
- const roots = [...groupsByRoot.keys()].sort((a, b) => a - b);
270
- for (const root of roots) {
271
- const members = groupsByRoot.get(root).sort((a, b) => a - b);
272
- if (members.length < 2)
273
- continue;
274
- const sharedFiles = new Set();
275
- for (const a of members)
276
- for (const b of members)
277
- if (a < b)
278
- intersect(files[b], fileSets[a]).forEach((f) => sharedFiles.add(f));
279
- const shared = [...sharedFiles].sort();
280
- const declaredName = members.map((i) => nodes[i].serialGroup).find((g) => !!g);
281
- let name = declaredName ?? `w${level[root] + 1}-${shared.length > 0 ? slug(shared[0].split('/').pop()) : 'same-shape'}`;
282
- if (usedNames.has(name)) {
283
- let k = 2;
284
- while (usedNames.has(`${name}-${k}`))
285
- k++;
286
- name = `${name}-${k}`;
287
- }
288
- usedNames.add(name);
289
- for (const i of members)
290
- serialGroupOf[ids[i]] = name;
291
- serialGroups.push({ name, level: level[root], plans: members.map((i) => ids[i]), shared_files: shared });
292
- }
293
- nodes.forEach((n, i) => {
294
- if (n.serialGroup && serialGroupOf[ids[i]] !== n.serialGroup) {
295
- warnings.push(`Plan ${ids[i]}: declares serial_group: ${n.serialGroup} but the plan graph puts it in ` +
296
- `${serialGroupOf[ids[i]] ?? 'no serial group'} (a serial group only holds plans of one wave).`);
297
- }
298
- });
299
- // Longest chain (by plan count) over the full DAG.
300
- const order = ids.map((_, i) => i).sort((a, b) => level[a] - level[b] || a - b);
301
- const best = nodes.map(() => 1);
302
- const prev = nodes.map(() => null);
303
- const preds = nodes.map(() => []);
304
- succ.forEach((s, i) => s.forEach((j) => preds[j].push(i)));
305
- for (const j of order) {
306
- for (const i of preds[j].sort((a, b) => a - b)) {
307
- if (best[i] + 1 > best[j]) {
308
- best[j] = best[i] + 1;
309
- prev[j] = i;
310
- }
311
- }
312
- }
313
- let end = -1;
314
- nodes.forEach((_, i) => { if (end === -1 || best[i] > best[end])
315
- end = i; });
316
- const chain = [];
317
- for (let cur = end; cur !== null && cur !== -1; cur = prev[cur])
318
- chain.unshift(cur);
319
- const links = [];
320
- for (let k = 1; k < chain.length; k++) {
321
- const a = chain[k - 1];
322
- const b = chain[k];
323
- const shared = intersect(files[b], fileSets[a]);
324
- const kind = shared.some(isSourceFile) ? 'source-file' : shared.length > 0 ? 'non-source-file' : 'declared';
325
- links.push({ from: ids[a], to: ids[b], kind, shared_files: shared, reason: nodes[b].chainReason });
326
- }
327
- const nonSourceLinks = links.filter((l) => l.kind === 'non-source-file').length;
328
- const limit = nodes.length / 2;
329
- const unreasoned = links.filter((l) => !l.reason).map((l) => ({ from: l.from, to: l.to, kind: l.kind }));
330
- const passed = chain.length <= limit || unreasoned.length === 0;
331
- const waveCount = nodes.length === 0 ? 0 : Math.max(...level) + 1;
332
- const message = passed
333
- ? `longest chain ${chain.length} of ${nodes.length} plans (limit ${limit})` +
334
- (chain.length > limit ? ' — every link has a chain_reason' : '')
335
- : `longest chain is ${chain.length} plans, over half the plan count (${nodes.length} plans, limit ${limit}), ` +
336
- `and ${unreasoned.length} link(s) have no chain_reason: ${unreasoned.map((l) => `${l.from}->${l.to} (${l.kind})`).join(', ')}. ` +
337
- `Drop depends_on edges that exist only because two plans touch the same non-source file ` +
338
- `(they share a wave in a serial group instead), merge same-shape plans, or record a ` +
339
- `chain_reason on each later plan saying why it must wait.`;
340
- return {
341
- cycle: null,
342
- level: Object.fromEntries(ids.map((id, i) => [id, level[i]])),
343
- serialGroupOf,
344
- serialGroups,
345
- implicitEdges,
346
- plan_count: nodes.length,
347
- wave_count: waveCount,
348
- longest_chain: { length: chain.length, plans: chain.map((i) => ids[i]), links, non_source_links: nonSourceLinks },
349
- chain_check: { passed, limit, unreasoned_links: passed ? [] : unreasoned, message },
350
- merge_candidates: mergeCandidates,
351
- warnings,
352
- };
353
- }
354
- function emptyGraph(nodes, cycle) {
355
- return {
356
- cycle,
357
- level: {},
358
- serialGroupOf: Object.fromEntries(nodes.map((n) => [n.id, null])),
359
- serialGroups: [],
360
- implicitEdges: [],
361
- plan_count: nodes.length,
362
- wave_count: 0,
363
- longest_chain: { length: 0, plans: [], links: [], non_source_links: 0 },
364
- chain_check: { passed: false, limit: nodes.length / 2, unreasoned_links: [], message: `depends_on cycle: ${cycle.join(', ')}` },
365
- merge_candidates: [],
366
- warnings: [],
367
- };
368
- }