@pareto4/pp-core 1.11.0 → 1.12.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 (256) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -0
  3. package/agents/pp-code-fixer.md +1 -1
  4. package/agents/pp-debug-session-manager.md +1 -1
  5. package/agents/pp-debugger.md +1 -1
  6. package/agents/pp-executor.md +20 -10
  7. package/agents/pp-intel-updater.md +1 -1
  8. package/agents/pp-phase-researcher.md +1 -1
  9. package/agents/pp-plan-checker.md +5 -5
  10. package/agents/pp-planner.md +10 -24
  11. package/agents/pp-project-researcher.md +1 -1
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-researcher.md +1 -1
  14. package/agents/pp-verifier.md +8 -4
  15. package/bin/install.js +429 -82
  16. package/commands/pp-autonomous.md +8 -6
  17. package/commands/pp-discovery-to-build.md +1 -1
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-discuss-phase.md +2 -2
  20. package/commands/pp-execute-phase.md +3 -1
  21. package/commands/pp-graphify.md +5 -5
  22. package/commands/pp-import.md +1 -1
  23. package/commands/pp-map-codebase.md +1 -1
  24. package/commands/pp-ns-project.md +2 -3
  25. package/commands/pp-ns-workflow.md +1 -1
  26. package/commands/pp-plan-phase.md +2 -1
  27. package/commands/pp-verify-work.md +2 -1
  28. package/gemini-extension.json +1 -1
  29. package/hooks/dist/pp-cursor-session-start.js +2 -2
  30. package/hooks/dist/pp-session-state.sh +1 -1
  31. package/hooks/dist/pp-statusline.js +21 -2
  32. package/hooks/pp-cursor-session-start.js +2 -2
  33. package/hooks/pp-session-state.sh +1 -1
  34. package/hooks/pp-statusline.js +21 -2
  35. package/package.json +18 -5
  36. package/pareto-skills/pp-project-setup/SKILL.md +5 -63
  37. package/pp-core/bin/detect-build-commands.sh +182 -0
  38. package/pp-core/bin/lib/capability-registry.cjs +65 -7
  39. package/pp-core/bin/lib/check-command-router.cjs +79 -38
  40. package/pp-core/bin/lib/clusters.cjs +2 -3
  41. package/pp-core/bin/lib/command-aliases.cjs +36 -0
  42. package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
  43. package/pp-core/bin/lib/commands.cjs +21 -6
  44. package/pp-core/bin/lib/compose-bindings.cjs +128 -15
  45. package/pp-core/bin/lib/config-schema.cjs +16 -0
  46. package/pp-core/bin/lib/config.cjs +142 -27
  47. package/pp-core/bin/lib/core-utils.cjs +43 -7
  48. package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
  49. package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
  50. package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
  51. package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
  52. package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
  53. package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
  54. package/pp-core/bin/lib/discovery-table.cjs +98 -0
  55. package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
  56. package/pp-core/bin/lib/frontmatter.cjs +17 -2
  57. package/pp-core/bin/lib/init-command-router.cjs +8 -5
  58. package/pp-core/bin/lib/init-contract.cjs +213 -33
  59. package/pp-core/bin/lib/init-interactive.cjs +20 -1
  60. package/pp-core/bin/lib/init.cjs +124 -134
  61. package/pp-core/bin/lib/install-profiles.cjs +5 -0
  62. package/pp-core/bin/lib/install-run.cjs +536 -0
  63. package/pp-core/bin/lib/io.cjs +15 -0
  64. package/pp-core/bin/lib/loop-resolver.cjs +24 -0
  65. package/pp-core/bin/lib/milestone.cjs +345 -48
  66. package/pp-core/bin/lib/phase-command-router.cjs +23 -1
  67. package/pp-core/bin/lib/phase-locator.cjs +15 -6
  68. package/pp-core/bin/lib/phase-scope.cjs +91 -0
  69. package/pp-core/bin/lib/phase-tier.cjs +467 -0
  70. package/pp-core/bin/lib/phase.cjs +247 -21
  71. package/pp-core/bin/lib/phases-command-router.cjs +2 -0
  72. package/pp-core/bin/lib/plan-graph.cjs +368 -0
  73. package/pp-core/bin/lib/planning-workspace.cjs +53 -0
  74. package/pp-core/bin/lib/platform-bindings.cjs +32 -2
  75. package/pp-core/bin/lib/profile-output.cjs +11 -3
  76. package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
  77. package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
  78. package/pp-core/bin/lib/roadmap.cjs +141 -39
  79. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  80. package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
  81. package/pp-core/bin/lib/state-command-router.cjs +82 -13
  82. package/pp-core/bin/lib/state-document.cjs +124 -7
  83. package/pp-core/bin/lib/state.cjs +784 -257
  84. package/pp-core/bin/lib/template.cjs +22 -6
  85. package/pp-core/bin/lib/vault-command-router.cjs +57 -6
  86. package/pp-core/bin/lib/vault-practices.cjs +6 -36
  87. package/pp-core/bin/lib/vault-write-back.cjs +20 -4
  88. package/pp-core/bin/lib/vault.cjs +555 -23
  89. package/pp-core/bin/lib/verify.cjs +53 -31
  90. package/pp-core/bin/lib/workstream.cjs +1 -1
  91. package/pp-core/bin/lib/worktree-safety.cjs +26 -15
  92. package/pp-core/bin/portable-timeout.sh +89 -0
  93. package/pp-core/bin/pp-tools.cjs +158 -8
  94. package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
  95. package/pp-core/bin/shared/config-schema.manifest.json +3 -0
  96. package/pp-core/references/autonomous-best-effort.md +461 -0
  97. package/pp-core/references/autonomous-lifecycle.md +146 -0
  98. package/pp-core/references/autonomous-smart-discuss.md +16 -12
  99. package/pp-core/references/checkpoints.md +1 -1
  100. package/pp-core/references/discovery-id-conventions.md +202 -0
  101. package/pp-core/references/discovery-rebaseline.md +254 -0
  102. package/pp-core/references/discovery-research-track.md +283 -0
  103. package/pp-core/references/discovery-traceability.md +400 -0
  104. package/pp-core/references/loop-hook-dispatch.md +7 -0
  105. package/pp-core/references/mandatory-initial-read.md +1 -1
  106. package/pp-core/references/planner-gap-closure.md +1 -0
  107. package/pp-core/references/planner-human-verify-mode.md +1 -1
  108. package/pp-core/references/planner-load-graph-context.md +1 -1
  109. package/pp-core/references/planner-plan-graph.md +66 -0
  110. package/pp-core/references/planner-tdd.md +1 -0
  111. package/pp-core/references/planning-config.md +8 -0
  112. package/pp-core/references/sketch-tooling.md +4 -0
  113. package/pp-core/references/tdd.md +1 -0
  114. package/pp-core/templates/claude-md.md +1 -1
  115. package/pp-core/templates/phase-prompt.md +6 -2
  116. package/pp-core/templates/prototype-review/review.js +303 -0
  117. package/pp-core/templates/state.md +6 -0
  118. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
  119. package/pp-core/workflows/add-backlog.md +1 -1
  120. package/pp-core/workflows/add-phase.md +1 -1
  121. package/pp-core/workflows/add-tests.md +1 -1
  122. package/pp-core/workflows/add-todo.md +1 -1
  123. package/pp-core/workflows/ai-integration-phase.md +2 -2
  124. package/pp-core/workflows/audit-fix.md +12 -16
  125. package/pp-core/workflows/audit-milestone.md +1 -1
  126. package/pp-core/workflows/audit-uat.md +1 -1
  127. package/pp-core/workflows/autonomous.md +68 -306
  128. package/pp-core/workflows/check-todos.md +1 -1
  129. package/pp-core/workflows/cleanup.md +1 -1
  130. package/pp-core/workflows/code-review-fix.md +1 -1
  131. package/pp-core/workflows/code-review.md +19 -7
  132. package/pp-core/workflows/complete-milestone.md +41 -60
  133. package/pp-core/workflows/debug.md +1 -1
  134. package/pp-core/workflows/diagnose-issues.md +1 -1
  135. package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
  136. package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
  137. package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
  138. package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
  139. package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
  140. package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
  141. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
  142. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
  143. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
  144. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
  145. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
  146. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
  147. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
  148. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
  149. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
  150. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
  151. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
  152. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
  153. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
  154. package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
  155. package/pp-core/workflows/discovery-sprint.md +79 -1714
  156. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
  157. package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
  158. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
  159. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
  160. package/pp-core/workflows/discovery-to-build.md +21 -238
  161. package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
  162. package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
  163. package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
  164. package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
  165. package/pp-core/workflows/discuss-phase.md +9 -2
  166. package/pp-core/workflows/do.md +4 -4
  167. package/pp-core/workflows/docs-update.md +1 -1
  168. package/pp-core/workflows/edit-phase.md +1 -1
  169. package/pp-core/workflows/eval-review.md +1 -1
  170. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
  171. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
  172. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  173. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
  174. package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
  175. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
  176. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
  177. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
  178. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
  179. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
  180. package/pp-core/workflows/execute-phase.md +102 -216
  181. package/pp-core/workflows/execute-plan.md +6 -2
  182. package/pp-core/workflows/explore.md +1 -1
  183. package/pp-core/workflows/extract-learnings.md +1 -1
  184. package/pp-core/workflows/forensics.md +4 -4
  185. package/pp-core/workflows/graduation.md +1 -1
  186. package/pp-core/workflows/health.md +1 -1
  187. package/pp-core/workflows/help/modes/brief.md +1 -0
  188. package/pp-core/workflows/help/modes/default.md +2 -1
  189. package/pp-core/workflows/help/modes/full.md +12 -5
  190. package/pp-core/workflows/import.md +1 -1
  191. package/pp-core/workflows/ingest-docs.md +1 -1
  192. package/pp-core/workflows/init.md +18 -2
  193. package/pp-core/workflows/insert-phase.md +1 -1
  194. package/pp-core/workflows/list-workspaces.md +1 -1
  195. package/pp-core/workflows/manager.md +3 -3
  196. package/pp-core/workflows/map-codebase.md +2 -2
  197. package/pp-core/workflows/milestone-summary.md +4 -4
  198. package/pp-core/workflows/mvp-phase.md +1 -1
  199. package/pp-core/workflows/new-milestone.md +7 -8
  200. package/pp-core/workflows/new-project.md +6 -1
  201. package/pp-core/workflows/new-workspace.md +1 -1
  202. package/pp-core/workflows/next.md +2 -2
  203. package/pp-core/workflows/note.md +2 -2
  204. package/pp-core/workflows/pause-work.md +1 -1
  205. package/pp-core/workflows/plan-milestone-gaps.md +1 -1
  206. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
  207. package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
  208. package/pp-core/workflows/plan-phase.md +43 -167
  209. package/pp-core/workflows/plan-review-convergence.md +2 -2
  210. package/pp-core/workflows/plant-seed.md +1 -1
  211. package/pp-core/workflows/pr-branch.md +1 -1
  212. package/pp-core/workflows/profile-user.md +1 -1
  213. package/pp-core/workflows/progress.md +3 -3
  214. package/pp-core/workflows/quick.md +3 -1
  215. package/pp-core/workflows/remove-phase.md +1 -1
  216. package/pp-core/workflows/remove-workspace.md +1 -1
  217. package/pp-core/workflows/resume-project.md +6 -12
  218. package/pp-core/workflows/review.md +1 -1
  219. package/pp-core/workflows/scan.md +1 -1
  220. package/pp-core/workflows/secure-phase.md +1 -1
  221. package/pp-core/workflows/settings-advanced.md +1 -1
  222. package/pp-core/workflows/settings-integrations.md +1 -1
  223. package/pp-core/workflows/settings.md +1 -1
  224. package/pp-core/workflows/ship.md +25 -8
  225. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  226. package/pp-core/workflows/sketch.md +2 -1
  227. package/pp-core/workflows/spec-phase.md +1 -1
  228. package/pp-core/workflows/spike-wrap-up.md +1 -1
  229. package/pp-core/workflows/spike.md +1 -1
  230. package/pp-core/workflows/stats.md +2 -2
  231. package/pp-core/workflows/thread.md +1 -1
  232. package/pp-core/workflows/transition.md +5 -9
  233. package/pp-core/workflows/ui-phase.md +23 -2
  234. package/pp-core/workflows/ui-review.md +1 -1
  235. package/pp-core/workflows/ultraplan-phase.md +2 -2
  236. package/pp-core/workflows/validate-phase.md +1 -1
  237. package/pp-core/workflows/vault-write-back.md +1 -1
  238. package/pp-core/workflows/verify-phase.md +29 -26
  239. package/pp-core/workflows/verify-work.md +14 -10
  240. package/scripts/base64-scan.sh +7 -41
  241. package/scripts/changeset/cli.cjs +25 -5
  242. package/scripts/dev-env.cjs +462 -0
  243. package/scripts/lib/allowlist-ratchet.cjs +103 -1
  244. package/scripts/lib/size-budget.cjs +311 -0
  245. package/scripts/lint-neutrality-names.cjs +694 -0
  246. package/scripts/lint-org-coordinates.allowlist.json +35 -0
  247. package/scripts/lint-org-coordinates.cjs +426 -0
  248. package/scripts/lint-skill-deps.cjs +48 -9
  249. package/scripts/lint-test-file-count.cjs +17 -2
  250. package/scripts/prompt-injection-scan.sh +41 -20
  251. package/scripts/sync-manifest-versions.cjs +41 -7
  252. package/scripts/sync-runtime-launcher.cjs +106 -1
  253. package/scripts/update-size-baseline.cjs +274 -30
  254. package/scripts/workflow-size.cjs +61 -9
  255. package/commands/pp-build-handoff.md +0 -46
  256. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
@@ -0,0 +1,311 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file size-budget.cjs
5
+ *
6
+ * The per-commit size budget for workflow and agent markdown (pack 9, item 4).
7
+ *
8
+ * ## What is measured
9
+ *
10
+ * Every CHANGE since the baseline file was last committed, one at a time:
11
+ * - each commit in `<last baseline commit>..HEAD` (at most MAX_COMMITS), against
12
+ * its parent — for a merge commit, against the larger of each file's sizes in
13
+ * its parents, so a merge adds nothing of its own and each plan commit is
14
+ * judged on its own branch;
15
+ * - the uncommitted edits under the guarded directory, against HEAD.
16
+ * A file's allowance base is the LARGER of its size before the change and its
17
+ * size in the committed baseline file. A change may grow one file by at most
18
+ * `per_file_bytes` and all files together by at most `per_commit_bytes`. So:
19
+ * - a plan that grows a few files a little passes without touching the
20
+ * baseline, however much the phase has grown in earlier plans;
21
+ * - growth already recorded in the baseline (a justified re-baseline) passes;
22
+ * - one change growing past the budget fails, by file and by total, and stays
23
+ * red until it is shrunk or recorded — a later commit does not hide it.
24
+ * A file that is in neither the baseline nor the parent (a new file) is not
25
+ * budgeted; the tier, nested and new-file hard caps in the guard tests bound it.
26
+ *
27
+ * The cumulative growth of a phase is recorded once, at phase close:
28
+ * `npm run size:baseline -- --reason "phase NN close"` in the phase's closing
29
+ * commit, which also starts the next window. The tier hard caps stay absolute
30
+ * red lines.
31
+ *
32
+ * ## Why per commit, not against the baseline alone
33
+ *
34
+ * Measured against the committed baseline alone, the slack is spent cumulatively:
35
+ * a long phase whose plans each grow a different file by a few hundred bytes goes
36
+ * red part-way through, and from then on every plan re-baselines — the baseline
37
+ * becomes a file every plan touches, which serialises plans that are otherwise
38
+ * independent.
39
+ *
40
+ * ## Fallback
41
+ *
42
+ * Not a git checkout, or no HEAD: the check is the previous one — growth
43
+ * against the committed baseline with the same two numbers (`assertDeltaBudget`),
44
+ * new and removed files included. In a shallow clone, commits whose parent is
45
+ * missing are skipped (CI's `test` job clones with GitLab's default depth).
46
+ *
47
+ * ## The numbers
48
+ *
49
+ * `size_budget.per_file_bytes` (default 1024) and `size_budget.per_commit_bytes`
50
+ * (default 4096) — the schema defaults in
51
+ * `pp-core/bin/shared/config-defaults.manifest.json`, overridable in the
52
+ * repository's `.planning/config.json`. Nothing else holds them.
53
+ */
54
+
55
+ const fs = require('fs');
56
+ const path = require('path');
57
+ const { execFileSync } = require('child_process');
58
+ const { assertDeltaBudget } = require('./allowlist-ratchet.cjs');
59
+
60
+ const REPO_ROOT = path.join(__dirname, '..', '..');
61
+ const DEFAULTS_MANIFEST = path.join(REPO_ROOT, 'pp-core', 'bin', 'shared', 'config-defaults.manifest.json');
62
+
63
+ function isPositiveInt(v) {
64
+ return Number.isInteger(v) && v > 0;
65
+ }
66
+
67
+ /**
68
+ * The budget numbers: project `.planning/config.json` `size_budget.*` when set to
69
+ * positive integers, else the schema defaults from the manifest.
70
+ *
71
+ * @param {{ repoRoot?: string, manifestPath?: string }} [opts]
72
+ * @returns {{ perFileBytes: number, perCommitBytes: number, source: { perFileBytes: string, perCommitBytes: string } }}
73
+ */
74
+ function readSizeBudget({ repoRoot = REPO_ROOT, manifestPath = DEFAULTS_MANIFEST } = {}) {
75
+ const defaults = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')).size_budget;
76
+ if (!defaults || !isPositiveInt(defaults.per_file_bytes) || !isPositiveInt(defaults.per_commit_bytes)) {
77
+ throw new Error(`${manifestPath} must define size_budget.per_file_bytes and size_budget.per_commit_bytes as positive integers`);
78
+ }
79
+ let project = {};
80
+ try {
81
+ const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.planning', 'config.json'), 'utf-8'));
82
+ if (cfg && typeof cfg.size_budget === 'object' && cfg.size_budget !== null) project = cfg.size_budget;
83
+ } catch {
84
+ // no project config: schema defaults apply
85
+ }
86
+ const pick = (key) =>
87
+ isPositiveInt(project[key])
88
+ ? { value: project[key], source: '.planning/config.json' }
89
+ : { value: defaults[key], source: 'schema default' };
90
+ const perFile = pick('per_file_bytes');
91
+ const perCommit = pick('per_commit_bytes');
92
+ return {
93
+ perFileBytes: perFile.value,
94
+ perCommitBytes: perCommit.value,
95
+ source: { perFileBytes: perFile.source, perCommitBytes: perCommit.source },
96
+ };
97
+ }
98
+
99
+ // A GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE inherited from a hook would point git at
100
+ // another repository; the guarded directory decides which repository is meant.
101
+ function git(cwd, args) {
102
+ const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('GIT_')));
103
+ return execFileSync('git', args, { cwd, env, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
104
+ }
105
+
106
+ /** Cap on how many commits back the guard looks (a re-baseline resets the window). */
107
+ const MAX_COMMITS = 300;
108
+
109
+ /**
110
+ * The changes to judge, oldest first, or null when git cannot provide them (see
111
+ * the Fallback section above). A change is one commit since the baseline file
112
+ * was last committed — `before` is, per file, the larger of its sizes in the
113
+ * commit's parents (so a merge commit adds nothing of its own) — plus the
114
+ * uncommitted edits under `dir` against HEAD.
115
+ *
116
+ * @param {{ dir: string, include: (rel: string) => boolean, baselinePath?: string }} opts
117
+ * `dir` the guarded directory; `include` filters POSIX paths relative to it
118
+ * (the same keys the measurer produces); `baselinePath` the committed baseline
119
+ * (its last commit starts the window; untracked → the last MAX_COMMITS commits).
120
+ * @returns {{ commits: Array<{ label: string, before: Object<string, number>, after: Object<string, number> }>,
121
+ * head: Object<string, number>, dirty: boolean, since: string | null } | null}
122
+ */
123
+ function commitChanges({ dir, include, baselinePath }) {
124
+ let top;
125
+ try {
126
+ top = git(dir, ['rev-parse', '--show-toplevel']).trim();
127
+ git(top, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}']);
128
+ } catch {
129
+ return null;
130
+ }
131
+ const realTop = fs.realpathSync(top);
132
+ const rel = path.relative(realTop, fs.realpathSync(dir)).split(path.sep).join('/');
133
+ const pathspec = rel === '' ? '.' : rel;
134
+ const prefix = rel === '' ? '' : `${rel}/`;
135
+
136
+ const cache = new Map();
137
+ const sizesAt = (rev) => {
138
+ if (cache.has(rev)) return cache.get(rev);
139
+ let listing = null;
140
+ try {
141
+ listing = git(top, ['ls-tree', '-r', '-l', '--full-tree', rev, '--', pathspec]);
142
+ } catch {
143
+ listing = null; // object missing (shallow boundary)
144
+ }
145
+ let sizes = null;
146
+ if (listing !== null) {
147
+ sizes = {};
148
+ for (const line of listing.split('\n')) {
149
+ const m = /^\d+ blob [0-9a-f]+\s+(\d+)\t(.+)$/.exec(line);
150
+ if (!m || !m[2].startsWith(prefix)) continue;
151
+ const key = m[2].slice(prefix.length);
152
+ if (include(key)) sizes[key] = Number(m[1]);
153
+ }
154
+ }
155
+ cache.set(rev, sizes);
156
+ return sizes;
157
+ };
158
+
159
+ let since = null;
160
+ if (baselinePath) {
161
+ try {
162
+ const relBaseline = path.relative(realTop, fs.realpathSync(baselinePath)).split(path.sep).join('/');
163
+ since = git(top, ['log', '-1', '--format=%H', 'HEAD', '--', relBaseline]).trim() || null;
164
+ } catch {
165
+ since = null; // baseline outside the repository or not there
166
+ }
167
+ }
168
+ const range = since ? [`${since}..HEAD`] : ['HEAD'];
169
+ // No pathspec here: path-limited rev-list rewrites parents and prunes merges.
170
+ const lines = git(top, ['rev-list', '--reverse', `--max-count=${MAX_COMMITS}`, '--parents', ...range])
171
+ .split('\n')
172
+ .filter(Boolean);
173
+ const commits = [];
174
+ for (const line of lines) {
175
+ const [sha, ...parents] = line.split(' ');
176
+ const after = sizesAt(sha);
177
+ const parentSizes = parents.map(sizesAt);
178
+ if (after === null || parents.length === 0 || parentSizes.some((p) => p === null)) continue;
179
+ const before = {};
180
+ for (const p of parentSizes) {
181
+ for (const [k, v] of Object.entries(p)) before[k] = Math.max(before[k] ?? -1, v);
182
+ }
183
+ const subject = git(top, ['log', '-1', '--format=%s', sha]).trim();
184
+ commits.push({ label: `commit ${sha.slice(0, 9)} "${subject}"`, before, after });
185
+ }
186
+ const dirty = git(top, ['status', '--porcelain', '--untracked-files=all', '--', pathspec]).trim() !== '';
187
+ return { commits, head: sizesAt('HEAD') ?? {}, dirty, since };
188
+ }
189
+
190
+ /**
191
+ * The per-commit budget check. Reports through `fail`; returns the measurement.
192
+ *
193
+ * @param {object} o
194
+ * @param {string} o.label - prefix for messages, e.g. 'workflow-size'
195
+ * @param {Object<string, number>} o.current - sizes now (working tree)
196
+ * @param {Object<string, number>} o.baseline - committed baseline sizes
197
+ * @param {ReturnType<typeof commitChanges>} o.changes - from `commitChanges`; null → baseline-only fallback
198
+ * @param {{ perFileBytes: number, perCommitBytes: number }} o.budget - from `readSizeBudget`
199
+ * @param {(msg: string) => void} o.fail
200
+ * @param {string} [o.updateHint]
201
+ */
202
+ function assertCommitDeltaBudget({ label, current, baseline, changes, budget, fail, updateHint }) {
203
+ const hint = updateHint ? `\n${updateHint}` : '';
204
+ if (changes === null) {
205
+ return {
206
+ mode: 'baseline',
207
+ ...assertDeltaBudget({
208
+ label,
209
+ current,
210
+ baseline,
211
+ perFileSlack: budget.perFileBytes,
212
+ totalSlack: budget.perCommitBytes,
213
+ fail,
214
+ updateHint: `(No git history available: measured against the committed baseline only.)${hint}`,
215
+ }),
216
+ };
217
+ }
218
+
219
+ if (Object.keys(baseline).length === 0) {
220
+ fail(
221
+ `[${label}] the committed baseline has no size entries — an empty baseline would make ` +
222
+ `every growth check pass vacuously. Regenerate it with \`npm run size:baseline\`.`
223
+ );
224
+ }
225
+ for (const [name, v] of Object.entries(baseline)) {
226
+ if (!Number.isInteger(v) || v < 0) fail(`[${label}] baseline entry ${name} is not a non-negative integer byte size: ${JSON.stringify(v)}`);
227
+ }
228
+ const base = (name) => (Number.isInteger(baseline[name]) && baseline[name] >= 0 ? baseline[name] : -1);
229
+ const todo =
230
+ `Shrink it — move content into a lazily-read steps/ or references/ file — or, if the growth is needed, ` +
231
+ `record it: npm run size:baseline -- --reason "<why this growth is needed>" and commit the baseline. ` +
232
+ `Growth within the budget needs no baseline commit: the phase's closing commit re-baselines once ` +
233
+ `with --reason "phase NN close".`;
234
+ const fmt = (o) => ` - ${o.name}: ${o.from} → ${o.to} (+${o.delta})`;
235
+
236
+ const judged = [...changes.commits];
237
+ if (changes.dirty) judged.push({ label: 'the uncommitted changes', before: changes.head, after: current });
238
+ const results = [];
239
+ for (const change of judged) {
240
+ const overages = [];
241
+ for (const name of Object.keys(change.after).sort()) {
242
+ const from = Math.max(base(name), change.before[name] ?? -1);
243
+ if (from < 0) continue; // new file: bounded by the hard caps, recorded at phase close
244
+ const to = change.after[name];
245
+ if (to > from) overages.push({ name, from, to, delta: to - from });
246
+ }
247
+ const totalOverage = overages.reduce((sum, o) => sum + o.delta, 0);
248
+ const where = `in ${change.label} (measured against its parent, or the committed baseline when larger)`;
249
+ const overFile = overages.filter((o) => o.delta > budget.perFileBytes);
250
+ if (overFile.length > 0) {
251
+ fail(
252
+ `[${label}] ${overFile.length} file(s) grew more than the per-file slack of ${budget.perFileBytes} bytes ` +
253
+ `(size_budget.per_file_bytes) ${where}. ${todo}\n${overFile.map(fmt).join('\n')}${hint}`
254
+ );
255
+ }
256
+ if (totalOverage > budget.perCommitBytes) {
257
+ fail(
258
+ `[${label}] total growth ${where} is ${totalOverage} bytes, over the total slack of ` +
259
+ `${budget.perCommitBytes} bytes (size_budget.per_commit_bytes). ${todo}\n${overages.map(fmt).join('\n')}${hint}`
260
+ );
261
+ }
262
+ results.push({ change: change.label, overages, totalOverage });
263
+ }
264
+ const unrecorded = Object.keys(current).filter((n) => base(n) < 0).sort();
265
+ const removed = Object.keys(baseline).filter((n) => !Object.prototype.hasOwnProperty.call(current, n)).sort();
266
+ return { mode: 'commit', since: changes.since, results, unrecorded, removed };
267
+ }
268
+
269
+ /**
270
+ * The whole guard for one tree, as the size-budget tests run it: read the
271
+ * committed baseline, measure, list the changes, check the budget and the
272
+ * `_rebaselines` log. Returns failure messages (empty = green).
273
+ *
274
+ * @param {object} o
275
+ * @param {string} o.label - 'workflow-size' / 'agent-size'
276
+ * @param {string} o.dir - guarded directory
277
+ * @param {string} o.baselinePath - committed baseline JSON
278
+ * @param {(dir: string) => Object<string, number>} o.measure - the measurer
279
+ * @param {(rel: string) => boolean} o.include - which repository paths (relative to dir) the measurer covers
280
+ * @param {{ perFileBytes: number, perCommitBytes: number }} [o.budget] - default: readSizeBudget()
281
+ * @param {object|null} [o.changes] - default: commitChanges({ dir, include, baselinePath }); null = baseline-only
282
+ * @param {string} [o.updateHint]
283
+ * @returns {string[]}
284
+ */
285
+ function checkSizeBudget({ label, dir, baselinePath, measure, include, budget, changes, updateHint }) {
286
+ // Lazy: update-size-baseline.cjs is the baseline file format's owner.
287
+ const { readBaseline, splitBaseline, validateRebaselineLog, REBASELINES_KEY } = require('../update-size-baseline.cjs');
288
+ const failures = [];
289
+ const raw = readBaseline(baselinePath);
290
+ if (raw === null) {
291
+ failures.push(`baseline ${baselinePath} does not exist — run \`npm run size:baseline\``);
292
+ } else if (!(REBASELINES_KEY in raw)) {
293
+ failures.push(`baseline ${baselinePath} has no ${REBASELINES_KEY} log — run \`npm run size:baseline\``);
294
+ }
295
+ const { sizes, rebaselines } = splitBaseline(raw);
296
+ assertCommitDeltaBudget({
297
+ label,
298
+ current: measure(dir),
299
+ baseline: sizes,
300
+ changes: changes === undefined ? commitChanges({ dir, include, baselinePath }) : changes,
301
+ budget: budget ?? readSizeBudget(),
302
+ fail: (m) => failures.push(m),
303
+ updateHint,
304
+ });
305
+ for (const problem of validateRebaselineLog(rebaselines)) {
306
+ failures.push(`[${label}] invalid ${REBASELINES_KEY} log: ${problem}`);
307
+ }
308
+ return failures;
309
+ }
310
+
311
+ module.exports = { readSizeBudget, commitChanges, assertCommitDeltaBudget, checkSizeBudget, DEFAULTS_MANIFEST, MAX_COMMITS };