@pareto4/pp-core 1.12.1 → 1.17.1

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 (196) hide show
  1. package/.claude-plugin/plugin.json +3 -3
  2. package/README.md +26 -9
  3. package/agents/pp-code-reviewer.md +3 -2
  4. package/agents/pp-debug-session-manager.md +9 -6
  5. package/agents/pp-executor.md +7 -21
  6. package/agents/pp-plan-checker.md +4 -4
  7. package/agents/pp-planner.md +23 -9
  8. package/agents/pp-vault-reviewer.md +274 -0
  9. package/agents/pp-verifier.md +7 -3
  10. package/bin/install.js +10 -3
  11. package/commands/pp-execute-phase.md +1 -3
  12. package/commands/pp-ns-review.md +2 -1
  13. package/commands/pp-plan-phase.md +1 -2
  14. package/commands/pp-vault-review.md +35 -0
  15. package/commands/pp-verify-work.md +1 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/dist/pp-check-update.js +0 -0
  18. package/hooks/dist/pp-cursor-session-start.js +1 -1
  19. package/hooks/dist/pp-statusline.js +1 -1
  20. package/hooks/dist/pp-update-banner.js +0 -0
  21. package/hooks/pp-check-update.js +0 -0
  22. package/hooks/pp-cursor-session-start.js +1 -1
  23. package/hooks/pp-statusline.js +1 -1
  24. package/hooks/pp-update-banner.js +0 -0
  25. package/package.json +14 -19
  26. package/pareto-skills/_shared/create-project-board.sh +0 -0
  27. package/pareto-skills/pp-project-setup/SKILL.md +1 -1
  28. package/pp-core/bin/check-latest-version.cjs +0 -0
  29. package/pp-core/bin/detect-build-commands.sh +2 -2
  30. package/pp-core/bin/lib/capability-activation.cjs +83 -9
  31. package/pp-core/bin/lib/capability-registry.cjs +526 -22
  32. package/pp-core/bin/lib/capability-state.cjs +160 -4
  33. package/pp-core/bin/lib/capability-writer.cjs +19 -5
  34. package/pp-core/bin/lib/check-command-router.cjs +3867 -274
  35. package/pp-core/bin/lib/clusters.cjs +4 -0
  36. package/pp-core/bin/lib/command-aliases.cjs +0 -28
  37. package/pp-core/bin/lib/commands.cjs +9 -4
  38. package/pp-core/bin/lib/compose-bindings.cjs +253 -76
  39. package/pp-core/bin/lib/config-loader.cjs +7 -103
  40. package/pp-core/bin/lib/config-schema.cjs +1 -1
  41. package/pp-core/bin/lib/config.cjs +154 -62
  42. package/pp-core/bin/lib/decisions.cjs +104 -27
  43. package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
  44. package/pp-core/bin/lib/frontmatter.cjs +1 -1
  45. package/pp-core/bin/lib/git-base-branch.cjs +138 -10
  46. package/pp-core/bin/lib/git-provider.cjs +4 -4
  47. package/pp-core/bin/lib/init-command-router.cjs +5 -8
  48. package/pp-core/bin/lib/init-contract.cjs +653 -629
  49. package/pp-core/bin/lib/init-interactive.cjs +77 -21
  50. package/pp-core/bin/lib/init.cjs +24 -51
  51. package/pp-core/bin/lib/install-profiles.cjs +1 -1
  52. package/pp-core/bin/lib/io.cjs +8 -0
  53. package/pp-core/bin/lib/loop-resolver.cjs +169 -47
  54. package/pp-core/bin/lib/org-profile.cjs +544 -53
  55. package/pp-core/bin/lib/package-identity.cjs +3 -3
  56. package/pp-core/bin/lib/phase-command-router.cjs +1 -23
  57. package/pp-core/bin/lib/phase-scope.cjs +1 -1
  58. package/pp-core/bin/lib/phase.cjs +11 -203
  59. package/pp-core/bin/lib/platform-bindings.cjs +40 -19
  60. package/pp-core/bin/lib/practices-review-check.cjs +15 -9
  61. package/pp-core/bin/lib/profile-output.cjs +1 -1
  62. package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
  63. package/pp-core/bin/lib/state.cjs +11 -85
  64. package/pp-core/bin/lib/template.cjs +1 -1
  65. package/pp-core/bin/lib/vault-audit.cjs +2594 -0
  66. package/pp-core/bin/lib/vault-command-router.cjs +2756 -60
  67. package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
  68. package/pp-core/bin/lib/vault-index.cjs +699 -0
  69. package/pp-core/bin/lib/vault-mode.cjs +235 -0
  70. package/pp-core/bin/lib/vault-practices.cjs +1194 -89
  71. package/pp-core/bin/lib/vault-review.cjs +2102 -0
  72. package/pp-core/bin/lib/vault-seal.cjs +1936 -0
  73. package/pp-core/bin/lib/vault-standards.cjs +1505 -0
  74. package/pp-core/bin/lib/vault-trace.cjs +3184 -0
  75. package/pp-core/bin/lib/vault-update.cjs +242 -0
  76. package/pp-core/bin/lib/vault-write-back.cjs +78 -31
  77. package/pp-core/bin/lib/vault.cjs +2713 -634
  78. package/pp-core/bin/lib/verify.cjs +0 -11
  79. package/pp-core/bin/portable-timeout.sh +1 -1
  80. package/pp-core/bin/pp-tools.cjs +184 -37
  81. package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
  82. package/pp-core/bin/shared/config-schema.manifest.json +0 -3
  83. package/pp-core/bin/shared/model-catalog.json +1 -0
  84. package/pp-core/bin/verify-reapply-patches.cjs +0 -0
  85. package/pp-core/org-profile.json +26 -0
  86. package/pp-core/references/autonomous-best-effort.md +8 -7
  87. package/pp-core/references/loop-hook-dispatch.md +0 -14
  88. package/pp-core/references/planner-gap-closure.md +0 -1
  89. package/pp-core/references/planner-tdd.md +0 -1
  90. package/pp-core/references/planner-vault-practices.md +31 -3
  91. package/pp-core/references/planner-vault-standards.md +320 -0
  92. package/pp-core/references/planning-config.md +0 -8
  93. package/pp-core/references/practice-arithmetic-contract.md +121 -0
  94. package/pp-core/references/tdd.md +0 -1
  95. package/pp-core/references/verifier-practice-verdicts.md +263 -0
  96. package/pp-core/templates/README.md +1 -0
  97. package/pp-core/templates/VAULT-REVIEW.md +202 -0
  98. package/pp-core/templates/phase-prompt.md +2 -6
  99. package/pp-core/templates/practices/README.md +137 -11
  100. package/pp-core/templates/project-charter.md +226 -0
  101. package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
  102. package/pp-core/workflows/add-tests.md +55 -0
  103. package/pp-core/workflows/audit-fix.md +63 -0
  104. package/pp-core/workflows/audit-milestone.md +1 -1
  105. package/pp-core/workflows/autonomous.md +85 -11
  106. package/pp-core/workflows/check-todos.md +13 -2
  107. package/pp-core/workflows/code-review-fix.md +59 -1
  108. package/pp-core/workflows/code-review.md +10 -1
  109. package/pp-core/workflows/complete-milestone.md +1 -1
  110. package/pp-core/workflows/debug.md +56 -2
  111. package/pp-core/workflows/discovery-sprint/prefill.md +21 -0
  112. package/pp-core/workflows/discovery-sprint/prototype.md +46 -48
  113. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  114. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
  115. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
  116. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
  117. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
  118. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
  119. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
  120. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
  121. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
  122. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
  123. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
  124. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
  125. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
  126. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
  127. package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
  128. package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
  129. package/pp-core/workflows/discovery-sprint.md +7 -3
  130. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
  131. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
  132. package/pp-core/workflows/discovery-to-build.md +9 -1
  133. package/pp-core/workflows/discuss-phase.md +3 -10
  134. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
  135. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
  136. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
  137. package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
  138. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
  139. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
  140. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
  141. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -18
  142. package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
  143. package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
  144. package/pp-core/workflows/execute-phase.md +154 -110
  145. package/pp-core/workflows/execute-plan.md +58 -4
  146. package/pp-core/workflows/fast.md +53 -0
  147. package/pp-core/workflows/help/modes/full.md +1 -0
  148. package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
  149. package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
  150. package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
  151. package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
  152. package/pp-core/workflows/init.md +232 -272
  153. package/pp-core/workflows/new-project.md +31 -23
  154. package/pp-core/workflows/plan-phase.md +99 -43
  155. package/pp-core/workflows/quick.md +120 -2
  156. package/pp-core/workflows/secure-phase.md +1 -1
  157. package/pp-core/workflows/ship.md +57 -24
  158. package/pp-core/workflows/update.md +3 -3
  159. package/pp-core/workflows/validate-phase.md +60 -1
  160. package/pp-core/workflows/vault-review.md +325 -0
  161. package/pp-core/workflows/verify-work.md +13 -12
  162. package/scripts/changeset/cli.cjs +6 -5
  163. package/scripts/changeset/lint.cjs +0 -0
  164. package/scripts/changeset/new.cjs +33 -50
  165. package/scripts/dev-env.cjs +25 -85
  166. package/scripts/gen-capability-registry.cjs +199 -0
  167. package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
  168. package/scripts/lib/allowlist-ratchet.cjs +1 -103
  169. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  170. package/scripts/lint-docs-required.cjs +0 -0
  171. package/scripts/lint-pr-check-project-dir.cjs +1 -3
  172. package/scripts/lint-test-file-count.allowlist.json +13 -2
  173. package/scripts/practice-arithmetic-contract.allow.json +35 -0
  174. package/scripts/prompt-injection-scan.sh +22 -15
  175. package/scripts/record-vault-mode-golden.cjs +89 -0
  176. package/scripts/release-channel.cjs +0 -0
  177. package/scripts/sync-manifest-versions.cjs +5 -4
  178. package/scripts/sync-rulesets.sh +1 -1
  179. package/scripts/update-size-baseline.cjs +30 -274
  180. package/scripts/vault-calibrate.cjs +996 -0
  181. package/scripts/verify-opengsd-sweep.cjs +3 -5
  182. package/scripts/workflow-size.cjs +9 -61
  183. package/pp-core/bin/lib/dispatch-plan.cjs +0 -472
  184. package/pp-core/bin/lib/install-run.cjs +0 -536
  185. package/pp-core/bin/lib/phase-tier.cjs +0 -467
  186. package/pp-core/bin/lib/plan-graph.cjs +0 -368
  187. package/pp-core/references/planner-plan-graph.md +0 -66
  188. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
  189. package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
  190. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
  191. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -72
  192. package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -34
  193. package/scripts/lib/size-budget.cjs +0 -311
  194. package/scripts/lint-neutrality-names.cjs +0 -694
  195. package/scripts/lint-org-coordinates.allowlist.json +0 -35
  196. package/scripts/lint-org-coordinates.cjs +0 -426
@@ -90,11 +90,12 @@ function parseArgs(argv) {
90
90
  return { ok: true, opts };
91
91
  }
92
92
 
93
- // Bare-checkout verify. A release tag job may run `verify` on a bare checkout:
94
- // no `npm ci`, no `build:lib`, and the compiled semver module under
95
- // pp-core/bin/lib is gitignored build output. Requiring that module at the top
96
- // of this file made `verify` (and `render`) die at require time on such a
97
- // checkout.
93
+ // Release tag gate (#71924 gap plan 01-06). CI's
94
+ // `tag_version_check` job runs `verify` on a bare checkout: no `npm ci`, no
95
+ // `build:lib`, and the compiled semver module under pp-core/bin/lib is
96
+ // gitignored build output. The first shipped version of that step required the
97
+ // compiled module at the top of this file, so it died at require time on every
98
+ // hosted checkout (never observed live: the step was skipped on every probe).
98
99
  // `verify` and `render` therefore require node builtins and tracked files only;
99
100
  // the compiled dependency is loaded lazily by `extract`, which the update
100
101
  // workflow runs on built trees.
File without changes
@@ -4,56 +4,42 @@
4
4
  /**
5
5
  * Scaffolds a new changeset fragment (#2975).
6
6
  *
7
- * npm run changeset -- --type Fixed --pr 1234 --body "fix the thing"
7
+ * npm run changeset -- --type Fixed --pr 1234 --body "fix the thing" [--name <slug>]
8
8
  *
9
- * Writes `.changeset/<adjective>-<noun>-<noun>.md` with frontmatter
10
- * + body. The random three-word filename minimizes filename collision
11
- * across concurrent PRs.
9
+ * Writes `.changeset/<pr>-<slug>.md` with frontmatter + body. The slug is
10
+ * --name, else derived from the bold lead sentence; a taken name gets -2, -3, ...
11
+ * FORK-ONLY (PBI 74336): upstream draws a random three-word name.
12
12
  */
13
13
 
14
14
  const fs = require('node:fs');
15
15
  const path = require('node:path');
16
16
  const { ExitError, runMain } = require('../lib/cli-exit.cjs');
17
17
 
18
- // Small word lists — keep the function simple and dependency-free.
19
- // Together this gives ~40 * 40 * 40 = 64,000 distinct names. The lint
20
- // rejects any duplicate filename, so collisions are caught even when
21
- // the random draw repeats.
22
- const ADJECTIVES = [
23
- 'silly', 'brave', 'calm', 'eager', 'gentle', 'happy', 'jolly', 'kind',
24
- 'lively', 'merry', 'nimble', 'plucky', 'quick', 'sturdy', 'witty', 'zesty',
25
- 'bold', 'clever', 'daring', 'fierce', 'graceful', 'humble', 'lucky', 'noble',
26
- 'proud', 'rapid', 'sharp', 'tidy', 'vivid', 'wise', 'agile', 'curious',
27
- 'eager', 'gallant', 'mellow', 'patient', 'serene', 'steady', 'sturdy', 'sunny',
28
- ];
29
- const NOUNS_A = [
30
- 'bears', 'birds', 'cats', 'dogs', 'elks', 'foxes', 'goats', 'hawks',
31
- 'ibex', 'jays', 'koalas', 'lynx', 'moles', 'newts', 'otters', 'pumas',
32
- 'quails', 'rams', 'seals', 'tigers', 'voles', 'wolves', 'yaks', 'zebras',
33
- 'badgers', 'cranes', 'deer', 'eagles', 'finches', 'geese', 'herons', 'jaguars',
34
- 'lemurs', 'mice', 'orcas', 'pandas', 'ravens', 'sloths', 'tunas', 'wasps',
35
- ];
36
- const NOUNS_B = [
37
- 'dance', 'sing', 'leap', 'run', 'jump', 'climb', 'fly', 'swim',
38
- 'rest', 'wake', 'roam', 'greet', 'wander', 'gather', 'forage', 'travel',
39
- 'glide', 'sprint', 'tumble', 'wave', 'cheer', 'rally', 'parade', 'march',
40
- 'hop', 'frolic', 'caper', 'romp', 'zip', 'dart', 'snooze', 'munch',
41
- 'chatter', 'squeak', 'howl', 'bark', 'purr', 'roar', 'hum', 'click',
42
- ];
18
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
19
+ const SLUG_STOPWORDS = new Set(['a', 'an', 'the', 'and', 'or', 'of', 'to', 'for', 'in', 'on', 'is', 'are', 'now', 'its']);
20
+ const SLUG_MAX_WORDS = 7;
43
21
 
44
- function pick(arr) {
45
- return arr[Math.floor(Math.random() * arr.length)];
22
+ // Kebab-case slug from the bold lead sentence (or the first sentence), minus filler words.
23
+ function deriveFragmentSlug(body) {
24
+ const bold = /\*\*([^*]+)\*\*/.exec(body || '');
25
+ const lead = bold ? bold[1] : String(body || '').trim().split(/[.!?\n]/)[0];
26
+ const words = lead
27
+ .toLowerCase()
28
+ .replace(/'s\b/g, '')
29
+ .split(/[^a-z0-9]+/)
30
+ .filter((w) => w && !SLUG_STOPWORDS.has(w));
31
+ return words.slice(0, SLUG_MAX_WORDS).join('-') || 'change';
46
32
  }
47
33
 
48
- function generateFragmentName() {
49
- return `${pick(ADJECTIVES)}-${pick(NOUNS_A)}-${pick(NOUNS_B)}`;
34
+ function fragmentBaseName({ pr, body, name }) {
35
+ return `${pr}-${name || deriveFragmentSlug(body)}`;
50
36
  }
51
37
 
52
38
  // Allowed Keep-a-Changelog section types. Used by both scaffoldFragment
53
39
  // (sanitization at write time) and parse.cjs (validation at consume time).
54
40
  const ALLOWED_TYPES = new Set(['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']);
55
41
 
56
- function scaffoldFragment({ repo, type, pr, body }) {
42
+ function scaffoldFragment({ repo, type, pr, body, name = null }) {
57
43
  // Sanitize: reject any type value not on the allowlist BEFORE embedding it
58
44
  // in frontmatter. A newline in `type` would corrupt the fragment; an
59
45
  // unrecognized value would be rejected later by parse.cjs but with a
@@ -63,28 +49,24 @@ function scaffoldFragment({ repo, type, pr, body }) {
63
49
  `scaffoldFragment: type=${JSON.stringify(type)} is not one of [${[...ALLOWED_TYPES].join(', ')}]`,
64
50
  );
65
51
  }
52
+ if (name !== null && !SLUG_RE.test(name)) {
53
+ throw new Error(`scaffoldFragment: name=${JSON.stringify(name)} is not kebab-case (a-z, 0-9, hyphens)`);
54
+ }
66
55
  const dir = path.join(repo, '.changeset');
67
56
  fs.mkdirSync(dir, { recursive: true });
68
57
  const content = `---\ntype: ${type}\npr: ${pr}\n---\n${body}\n`;
69
- // Atomic create: writeFileSync with `flag: 'wx'` fails (EEXIST) when the
70
- // file already exists, so concurrent invocations can't race past
71
- // `existsSync` and overwrite each other. Re-roll the random name on
72
- // collision; fail loudly after exhausting the retry budget.
73
- for (let i = 0; i < 16; i++) {
74
- const name = generateFragmentName();
75
- const target = path.join(dir, `${name}.md`);
58
+ // flag 'wx' fails with EEXIST instead of overwriting, so concurrent runs cannot clobber a fragment.
59
+ const base = fragmentBaseName({ pr, body, name });
60
+ for (let i = 1; i <= 50; i++) {
61
+ const target = path.join(dir, `${i === 1 ? base : `${base}-${i}`}.md`);
76
62
  try {
77
63
  fs.writeFileSync(target, content, { flag: 'wx' });
78
64
  return target;
79
65
  } catch (e) {
80
66
  if (e.code !== 'EEXIST') throw e;
81
- // collision — try another random draw
82
67
  }
83
68
  }
84
- throw new Error(
85
- 'scaffoldFragment: 16 random filename draws all collided; ' +
86
- 'expand the word lists or investigate corrupted .changeset/ state',
87
- );
69
+ throw new Error(`scaffoldFragment: ${base}.md and 49 numbered variants already exist; pass --name`);
88
70
  }
89
71
 
90
72
  function parseArgs(argv) {
@@ -102,7 +84,7 @@ function parseArgs(argv) {
102
84
 
103
85
  for (let i = 0; i < argv.length; i++) {
104
86
  const a = argv[i];
105
- if (a === '--type' || a === '--pr' || a === '--body' || a === '--repo') {
87
+ if (a === '--type' || a === '--pr' || a === '--body' || a === '--name' || a === '--repo') {
106
88
  const r = requireValue(a, i);
107
89
  if (!r.ok) return { ok: false, error: r.error };
108
90
  if (a === '--type') opts.type = r.value;
@@ -114,6 +96,7 @@ function parseArgs(argv) {
114
96
  const trimmed = r.value.trim();
115
97
  opts.pr = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
116
98
  } else if (a === '--body') opts.body = r.value;
99
+ else if (a === '--name') opts.name = r.value;
117
100
  else if (a === '--repo') opts.repo = r.value;
118
101
  i++;
119
102
  continue;
@@ -127,7 +110,7 @@ function main() {
127
110
  const parsed = parseArgs(process.argv.slice(2));
128
111
  if (!parsed.ok) {
129
112
  process.stderr.write(`${parsed.error}\n`);
130
- process.stderr.write('usage: changeset/new.cjs --type <Fixed|Added|...> --pr NNNN --body "..."\n');
113
+ process.stderr.write('usage: changeset/new.cjs --type <Fixed|Added|...> --pr NNNN --body "..." [--name <slug>]\n');
131
114
  throw new ExitError(2);
132
115
  }
133
116
  const { opts } = parsed;
@@ -140,7 +123,7 @@ function main() {
140
123
  const prMissing = opts.pr === null;
141
124
  const prNaN = typeof opts.pr === 'number' && Number.isNaN(opts.pr);
142
125
  if (!opts.type || prMissing || prNaN || !opts.body) {
143
- throw new ExitError(2, 'usage: changeset/new.cjs --type <Fixed|Added|...> --pr NNNN --body "..."');
126
+ throw new ExitError(2, 'usage: changeset/new.cjs --type <Fixed|Added|...> --pr NNNN --body "..." [--name <slug>]');
144
127
  }
145
128
  const file = scaffoldFragment(opts);
146
129
  process.stdout.write(`${path.relative(process.cwd(), file)}\n`);
@@ -148,4 +131,4 @@ function main() {
148
131
 
149
132
  if (require.main === module) runMain(main);
150
133
 
151
- module.exports = { generateFragmentName, scaffoldFragment, parseArgs, ALLOWED_TYPES };
134
+ module.exports = { deriveFragmentSlug, fragmentBaseName, scaffoldFragment, parseArgs, ALLOWED_TYPES };
@@ -2,16 +2,16 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * dev-env.cjs — keep a stable pp-core install and a development build apart.
5
+ * dev-env.cjs — keep the global pp-core install and a development build apart.
6
+ * FORK-ONLY (PBI 74336)
6
7
  *
7
- * stable [<tag>] [--dir <dir>] [--runtime <id>] worktree on the latest release tag, installed --global
8
+ * setup [--runtime <id>] build this checkout on its current branch, installed --global
8
9
  * dev-setup [--config-dir <dir>] [--no-shell] npm ci, install the dev profile, put claude-dev on PATH
9
10
  * install [--config-dir <dir>] [--no-build] build, install into a separate Claude profile
10
11
  * install --local [<dir>] [--no-build] build, install into a sandbox project
11
12
  * claude [--config-dir <dir>] [-- <args>] start the Claude CLI on the dev profile
12
13
  *
13
- * Defaults: the worktree is pp-core-stable next to the main checkout; the profile is
14
- * PP_DEV_CONFIG_DIR, else ~/.claude-dev; the sandbox is ../pp-sandbox.
14
+ * Defaults: the profile is PP_DEV_CONFIG_DIR, else ~/.claude-dev; the sandbox is ../pp-sandbox.
15
15
  */
16
16
 
17
17
  const fs = require('node:fs');
@@ -25,27 +25,21 @@ const INSTALLER = path.join(ROOT, 'bin', 'install.js');
25
25
 
26
26
  const USAGE = [
27
27
  'Usage:',
28
- ' npm run setup [-- [<tag>] [--dir <dir>] [--runtime <id>]]',
28
+ ' npm run setup [-- --runtime <id>]',
29
29
  ' npm run dev:setup [-- [--config-dir <dir>] [--no-shell]]',
30
30
  ' npm run dev:install [-- [--config-dir <dir>] [--no-build]]',
31
31
  ' npm run dev:install -- --local [<dir>] [--no-build]',
32
32
  ' npm run dev:claude [-- <claude args>]',
33
33
  ].join('\n');
34
34
 
35
- function parseStableArgs(rest) {
36
- const opts = { command: 'stable', tag: null, dir: null, runtime: 'claude' };
35
+ function parseSetupArgs(rest) {
36
+ const opts = { command: 'setup', runtime: 'claude' };
37
37
  for (let i = 0; i < rest.length; i++) {
38
38
  const arg = rest[i];
39
- if (arg === '--dir') {
40
- const value = rest[++i];
41
- if (!value) return { error: '--dir requires a path' };
42
- opts.dir = value;
43
- } else if (arg === '--runtime') {
39
+ if (arg === '--runtime') {
44
40
  const value = rest[++i];
45
41
  if (!value) return { error: '--runtime requires a runtime id' };
46
42
  opts.runtime = value;
47
- } else if (!arg.startsWith('-') && opts.tag === null) {
48
- opts.tag = arg;
49
43
  } else {
50
44
  return { error: `unknown option: ${arg}` };
51
45
  }
@@ -72,7 +66,7 @@ function parseDevSetupArgs(rest) {
72
66
 
73
67
  function parseArgs(argv) {
74
68
  const [command, ...rest] = argv;
75
- if (command === 'stable') return parseStableArgs(rest);
69
+ if (command === 'setup') return parseSetupArgs(rest);
76
70
  if (command === 'dev-setup') return parseDevSetupArgs(rest);
77
71
  if (command !== 'install' && command !== 'claude') {
78
72
  return { error: `unknown command: ${command === undefined ? '(none)' : command}` };
@@ -133,7 +127,7 @@ function resolveLocalDir(opts, root = ROOT) {
133
127
  return path.resolve(opts.localDir || path.join(root, '..', 'pp-sandbox'));
134
128
  }
135
129
 
136
- // The stable profile is ~/.claude; overwriting it with a dev build is what this script exists to avoid.
130
+ // ~/.claude holds the global install from npm run setup; the dev routes never write there.
137
131
  function assertNotStableProfile(dir, home = os.homedir()) {
138
132
  if (samePath(dir, path.join(home, '.claude'))) {
139
133
  throw new ExitError(1, `Refusing to install a dev build into the stable profile ${dir}.`);
@@ -167,23 +161,6 @@ function git(args, cwd) {
167
161
  return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
168
162
  }
169
163
 
170
- // Highest vX.Y.Z (or X.Y.Z) tag; pre-release tags are skipped.
171
- function pickLatestTag(tags) {
172
- const parsed = tags
173
- .map((tag) => ({ tag, m: /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag) }))
174
- .filter((t) => t.m)
175
- .map((t) => ({ tag: t.tag, v: t.m.slice(1).map(Number) }));
176
- parsed.sort((a, b) => b.v[0] - a.v[0] || b.v[1] - a.v[1] || b.v[2] - a.v[2]);
177
- return parsed.length ? parsed[0].tag : null;
178
- }
179
-
180
- // Next to the main checkout, so running this from any worktree finds the same directory.
181
- function resolveStableDir(opts, repo = ROOT) {
182
- if (opts.dir) return path.resolve(opts.dir);
183
- const commonDir = path.resolve(repo, git(['rev-parse', '--git-common-dir'], repo));
184
- return path.join(path.dirname(commonDir), '..', 'pp-core-stable');
185
- }
186
-
187
164
  // The runtimes install.js takes as --<id>, read from the capability manifests so no build is needed.
188
165
  function listRuntimes(root = ROOT) {
189
166
  const capDir = path.join(root, 'capabilities');
@@ -206,57 +183,22 @@ function resolveRuntime(id, root = ROOT) {
206
183
  return found;
207
184
  }
208
185
 
209
- function listWorktrees(repo) {
210
- return git(['worktree', 'list', '--porcelain'], repo)
211
- .split(/\r?\n/)
212
- .filter((line) => line.startsWith('worktree '))
213
- .map((line) => path.resolve(line.slice('worktree '.length)));
214
- }
215
-
216
- // Adds the worktree at the tag, or moves an existing clean one to it; never touches your own checkout.
217
- function prepareStableWorktree({ repo, dir, tag }) {
218
- try {
219
- git(['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`], repo);
220
- } catch {
221
- throw new ExitError(1, `Tag ${tag} does not exist in ${repo}.`);
222
- }
223
- let known = listWorktrees(repo).some((w) => samePath(w, dir));
224
- // A worktree folder deleted by hand stays registered; prune it so it is added again.
225
- if (known && !fs.existsSync(dir)) {
226
- git(['worktree', 'prune'], repo);
227
- known = false;
228
- }
229
- if (!known) {
230
- if (fs.existsSync(dir)) {
231
- throw new ExitError(1, `${dir} exists but is not a worktree of ${repo}; pass --dir <other dir>.`);
232
- }
233
- git(['worktree', 'add', '--detach', dir, tag], repo);
234
- return;
235
- }
236
- if (git(['status', '--porcelain'], dir) !== '') {
237
- throw new ExitError(1, `The stable worktree ${dir} has local changes; commit or discard them first.`);
238
- }
239
- git(['checkout', '--quiet', '--detach', tag], dir);
240
- }
241
-
242
- function installStable(opts) {
186
+ // Installs the checkout this runs from, so the global install follows its current branch.
187
+ function installSetup(opts) {
243
188
  const runtime = resolveRuntime(opts.runtime);
244
189
  npmCliPath();
245
- const dir = resolveStableDir(opts);
246
- try {
247
- git(['fetch', '--tags', '--quiet', 'origin'], ROOT);
248
- } catch {
249
- process.stderr.write('Could not fetch tags from origin; using the tags already in this clone.\n');
250
- }
251
- const tag = opts.tag || pickLatestTag(git(['tag', '--list'], ROOT).split(/\r?\n/));
252
- if (!tag) throw new ExitError(1, 'No release tag found; pass one: npm run setup -- <tag>.');
253
- prepareStableWorktree({ repo: ROOT, dir, tag });
254
- process.stdout.write(`Stable worktree ${dir} is on ${tag}.\n`);
255
- runNpm(['ci'], dir);
256
- runNpm(['run', 'build'], dir);
257
- execFileSync(process.execPath, [path.join(dir, 'bin', 'install.js'), `--${runtime.id}`, '--global'], { cwd: dir, stdio: 'inherit' });
190
+ runNpm(['ci'], ROOT);
191
+ runBuild();
192
+ execFileSync(process.execPath, [INSTALLER, `--${runtime.id}`, '--global'], { cwd: ROOT, stdio: 'inherit' });
258
193
  const again = runtime.id === 'claude' ? 'npm run setup' : `npm run setup -- --runtime ${runtime.id}`;
259
- process.stdout.write(`\npp-core ${tag} is installed as the global pp-core for ${runtime.title}. Update later with: ${again}\n`);
194
+ process.stdout.write(`
195
+ pp-core from ${describeCheckout(devBuildInfo())} is installed as the global pp-core for ${runtime.title}. Update later with: ${again}
196
+ `);
197
+ }
198
+
199
+ function describeCheckout(info) {
200
+ const ref = info.commit ? `${info.branch || 'detached HEAD'}@${info.commit}` : info.checkout;
201
+ return info.dirty ? `${ref} (with uncommitted changes)` : ref;
260
202
  }
261
203
 
262
204
  // The statusline shows a DEV badge with this build's branch and commit when this file is present.
@@ -429,7 +371,7 @@ function main(argv = process.argv.slice(2)) {
429
371
  if (opts.error) throw new ExitError(2, `${opts.error}\n${USAGE}`);
430
372
  if (opts.command === 'claude') return launchClaude(opts);
431
373
  if (opts.command === 'dev-setup') devSetup(opts);
432
- else if (opts.command === 'stable') installStable(opts);
374
+ else if (opts.command === 'setup') installSetup(opts);
433
375
  else if (opts.local) installSandbox(opts);
434
376
  else installProfile(opts);
435
377
  return 0;
@@ -441,9 +383,9 @@ module.exports = {
441
383
  parseArgs,
442
384
  resolveConfigDir,
443
385
  resolveLocalDir,
444
- resolveStableDir,
445
386
  resolveLaunchDir,
446
387
  devBuildInfo,
388
+ describeCheckout,
447
389
  renderRcLauncher,
448
390
  upsertLauncher,
449
391
  windowsLauncherDir,
@@ -452,10 +394,8 @@ module.exports = {
452
394
  writeWindowsLaunchers,
453
395
  pathHasEntry,
454
396
  appendPathEntry,
455
- pickLatestTag,
456
397
  listRuntimes,
457
398
  resolveRuntime,
458
- prepareStableWorktree,
459
399
  assertNotStableProfile,
460
400
  assertOutsideRepo,
461
401
  samePath,
@@ -568,6 +568,18 @@ function validateFeatureBody(cap) {
568
568
  }
569
569
  }
570
570
 
571
+ // Quick 261015-ect (QD-13): requiresPlatformBindings, optional. When present it
572
+ // must be a boolean; the loop resolver drops every hook of a capability that
573
+ // declares `true` on a project with no .platform-bindings.json. Which
574
+ // capabilities MUST declare it is a cross-capability rule
575
+ // (validatePlatformBindingsField).
576
+ if (cap.requiresPlatformBindings !== undefined && typeof cap.requiresPlatformBindings !== 'boolean') {
577
+ errors.push(
578
+ 'capability "' + (cap.id || '(unknown)') + '" requiresPlatformBindings must be a boolean (got: ' +
579
+ JSON.stringify(cap.requiresPlatformBindings) + ')',
580
+ );
581
+ }
582
+
571
583
  return errors;
572
584
  }
573
585
 
@@ -1319,6 +1331,177 @@ function validateAgainstContract(cap, capId, knownAgentIds) {
1319
1331
  return errors;
1320
1332
  }
1321
1333
 
1334
+ /**
1335
+ * Phase 15 D-02: validate the per-hook `vaultMode` filter field.
1336
+ *
1337
+ * The pure loop resolver evaluates `vaultMode` against an injected mode value
1338
+ * and never emits it, so an unknown or misplaced value must be refused HERE,
1339
+ * at generation, never interpreted at runtime. Three rules, each reported on
1340
+ * its own:
1341
+ * (a) the field is allowed on contributions and gates only (a step is
1342
+ * refused);
1343
+ * (b) the value must be a string in the vault capability's declared
1344
+ * `config["vault.mode"].values`. That enum declaration is the ONE owner of
1345
+ * the closed set; it is read here, never restated. With no vault
1346
+ * capability, or no such values array, there is no closed set and every
1347
+ * `vaultMode` is refused (fail closed);
1348
+ * (c) the declaring capability must be `vault` or list `vault` in `requires`,
1349
+ * so a filtered hook can only exist where the vault gate's closure holds.
1350
+ *
1351
+ * Presence is own-property presence, so a JSON `null` is refused under (b).
1352
+ *
1353
+ * @param {Map<string, object>} capMap
1354
+ * @returns {string[]} error strings; empty = all pass
1355
+ */
1356
+ function validateVaultModeField(capMap) {
1357
+ const errors = [];
1358
+ const vaultCap = capMap.get('vault');
1359
+ const modeDecl = vaultCap && vaultCap.config && typeof vaultCap.config === 'object'
1360
+ ? vaultCap.config['vault.mode']
1361
+ : undefined;
1362
+ const allowed = modeDecl && Array.isArray(modeDecl.values)
1363
+ ? modeDecl.values.filter((v) => typeof v === 'string')
1364
+ : null;
1365
+ const KINDS = [['steps', 'step'], ['contributions', 'contribution'], ['gates', 'gate']];
1366
+ for (const [capId, cap] of capMap) {
1367
+ if (!cap || typeof cap !== 'object') continue;
1368
+ const prefix = 'capability "' + capId + '"';
1369
+ const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
1370
+ for (const [arrKey, kind] of KINDS) {
1371
+ const arr = Array.isArray(cap[arrKey]) ? cap[arrKey] : [];
1372
+ for (const hook of arr) {
1373
+ if (!hook || typeof hook !== 'object') continue;
1374
+ if (!Object.prototype.hasOwnProperty.call(hook, 'vaultMode')) continue;
1375
+ const where = prefix + ' ' + kind + ' at point "' + String(hook.point) + '"';
1376
+ const value = hook.vaultMode;
1377
+ if (kind === 'step') {
1378
+ errors.push(where + ': vaultMode is allowed on contributions and gates only');
1379
+ }
1380
+ if (allowed === null) {
1381
+ errors.push(
1382
+ where + ': vaultMode ' + JSON.stringify(value) + ' cannot be checked: the vault capability ' +
1383
+ 'declares no "vault.mode" values, which own the closed set',
1384
+ );
1385
+ } else if (typeof value !== 'string' || !allowed.includes(value)) {
1386
+ errors.push(
1387
+ where + ': vaultMode ' + JSON.stringify(value) + ' is not one of [' + allowed.join(', ') +
1388
+ '] (the closed set owned by the vault capability\'s "vault.mode" values)',
1389
+ );
1390
+ }
1391
+ if (capId !== 'vault' && !requiresVault) {
1392
+ errors.push(
1393
+ where + ': vaultMode is allowed only on the "vault" capability or a capability that ' +
1394
+ 'lists "vault" in requires',
1395
+ );
1396
+ }
1397
+ }
1398
+ }
1399
+ }
1400
+ return errors;
1401
+ }
1402
+
1403
+ /**
1404
+ * Quick 261015-ect (QD-13): the vault-family rule for `requiresPlatformBindings`,
1405
+ * the mirror of the vaultMode rule (c) above. A capability that is `vault` or
1406
+ * lists `vault` in `requires` MUST declare the field, so a vault-family
1407
+ * capability added later cannot render hooks on a project the vault does not
1408
+ * govern by forgetting it. A capability outside the family may declare it too.
1409
+ *
1410
+ * @param {Map<string, object>} capMap
1411
+ * @returns {string[]} error strings; empty = all pass
1412
+ */
1413
+ function validatePlatformBindingsField(capMap) {
1414
+ const errors = [];
1415
+ for (const [capId, cap] of capMap) {
1416
+ if (!cap || typeof cap !== 'object') continue;
1417
+ const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
1418
+ if (capId !== 'vault' && !requiresVault) continue;
1419
+ if (!Object.prototype.hasOwnProperty.call(cap, 'requiresPlatformBindings')) {
1420
+ errors.push(
1421
+ 'capability "' + capId + '" is vault or requires vault and must declare requiresPlatformBindings, ' +
1422
+ 'so a vault-family capability cannot render hooks on a project the vault does not govern',
1423
+ );
1424
+ }
1425
+ }
1426
+ return errors;
1427
+ }
1428
+
1429
+ /**
1430
+ * Phase 16 D-19: the per-hook seal field's name. The resolver reads it as
1431
+ * `SEAL_FIELD` from capability-state; this generator cannot require that module
1432
+ * (its load graph reaches surface.cjs and config-loader.cjs, which read the
1433
+ * very registry this script regenerates), so the literal is restated here and
1434
+ * pinned equal to `SEAL_FIELD` by tests/capability-registry.test.cjs.
1435
+ */
1436
+ const SEAL_FIELD_NAME = 'onRecordedArtifacts';
1437
+
1438
+ /**
1439
+ * Phase 16 D-19 / D-25 / D-26: validate the per-hook seal field.
1440
+ *
1441
+ * A gate carrying the field renders for an enabled, config-inactive capability
1442
+ * over recorded vault artifacts, bypassing the capability's active check and
1443
+ * its `when` (D-18). That is a widening of render behaviour, so the field is
1444
+ * closed HERE, at generation, to exactly the shape the two shipped seal gates
1445
+ * have. Six rules, each reported on its own:
1446
+ * (a) gates only (a step or contribution carrying it is refused);
1447
+ * (b) the value is literal `true` (`"true"`, `1`, `null`, `false` refused);
1448
+ * (c) the declaring capability lists `vault` in `requires` and is not `vault`
1449
+ * itself (D-26: with the vault off the injected mode is null, so both
1450
+ * modes' filtered gates pass and sealing the vault capability too would
1451
+ * dispatch `check vault.trace` twice);
1452
+ * (d) the gate also carries `vaultMode` (the resolver filters by mode BEFORE
1453
+ * the predicate, research C-1);
1454
+ * (e) the gate is `blocking: true`;
1455
+ * (f) the point is `ship:pre` (D-21: the requirement's block point; a seal at
1456
+ * verify:post would halt execute-phase on a switch set on purpose).
1457
+ *
1458
+ * Presence is own-property presence.
1459
+ *
1460
+ * @param {Map<string, object>} capMap
1461
+ * @returns {string[]} error strings; empty = all pass
1462
+ */
1463
+ function validateSealField(capMap) {
1464
+ const errors = [];
1465
+ const KINDS = [['steps', 'step'], ['contributions', 'contribution'], ['gates', 'gate']];
1466
+ for (const [capId, cap] of capMap) {
1467
+ if (!cap || typeof cap !== 'object') continue;
1468
+ const prefix = 'capability "' + capId + '"';
1469
+ const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
1470
+ for (const [arrKey, kind] of KINDS) {
1471
+ const arr = Array.isArray(cap[arrKey]) ? cap[arrKey] : [];
1472
+ for (const hook of arr) {
1473
+ if (!hook || typeof hook !== 'object') continue;
1474
+ if (!Object.prototype.hasOwnProperty.call(hook, SEAL_FIELD_NAME)) continue;
1475
+ const where = prefix + ' ' + kind + ' at point "' + String(hook.point) + '"';
1476
+ const value = hook[SEAL_FIELD_NAME];
1477
+ if (kind !== 'gate') {
1478
+ errors.push(where + ': ' + SEAL_FIELD_NAME + ' is allowed on gates only');
1479
+ continue;
1480
+ }
1481
+ if (value !== true) {
1482
+ errors.push(where + ': ' + SEAL_FIELD_NAME + ' must be literal true (got ' + JSON.stringify(value) + ')');
1483
+ }
1484
+ if (capId === 'vault' || !requiresVault) {
1485
+ errors.push(
1486
+ where + ': ' + SEAL_FIELD_NAME + ' is allowed only on a capability that lists "vault" in requires ' +
1487
+ 'and is not "vault" itself',
1488
+ );
1489
+ }
1490
+ if (!Object.prototype.hasOwnProperty.call(hook, 'vaultMode')) {
1491
+ errors.push(where + ': a gate carrying ' + SEAL_FIELD_NAME + ' must also carry vaultMode');
1492
+ }
1493
+ if (hook.blocking !== true) {
1494
+ errors.push(where + ': a gate carrying ' + SEAL_FIELD_NAME + ' must be blocking: true');
1495
+ }
1496
+ if (hook.point !== 'ship:pre') {
1497
+ errors.push(where + ': ' + SEAL_FIELD_NAME + ' is allowed only at point "ship:pre"');
1498
+ }
1499
+ }
1500
+ }
1501
+ }
1502
+ return errors;
1503
+ }
1504
+
1322
1505
  /**
1323
1506
  * C1+C2: Global consumes-satisfiability validation.
1324
1507
  *
@@ -1479,6 +1662,18 @@ const TIER_RANK = { core: 0, standard: 1, full: 2 };
1479
1662
  function validateCrossCapability(capMap, centralKeys) {
1480
1663
  const errors = [];
1481
1664
 
1665
+ // Phase 15 D-02: the per-hook vaultMode filter field (needs capMap to read the
1666
+ // vault capability's closed value set and every capability's `requires`).
1667
+ errors.push(...validateVaultModeField(capMap));
1668
+
1669
+ // Phase 16 D-19: the per-hook seal field (needs capMap for every capability's
1670
+ // `requires`).
1671
+ errors.push(...validateSealField(capMap));
1672
+
1673
+ // Quick 261015-ect (QD-13): every vault-family capability declares
1674
+ // requiresPlatformBindings (needs capMap for every capability's `requires`).
1675
+ errors.push(...validatePlatformBindingsField(capMap));
1676
+
1482
1677
  // Ownership: one owner per skill stem + agent name
1483
1678
  const skillOwner = new Map(); // skill → capId
1484
1679
  const agentOwner = new Map(); // agent → capId
@@ -2646,6 +2841,10 @@ module.exports = {
2646
2841
  validateAgainstContract,
2647
2842
  validateConsumesGlobal,
2648
2843
  validateCrossCapability,
2844
+ validateVaultModeField,
2845
+ validateSealField,
2846
+ validatePlatformBindingsField,
2847
+ SEAL_FIELD_NAME,
2649
2848
  classifyCrossErrors,
2650
2849
  loadCentralConfigKeys,
2651
2850
  loadAndValidate,