@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
@@ -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
 
@@ -1204,17 +1216,6 @@ function validateGate(gate, prefix) {
1204
1216
  if (count !== 1) {
1205
1217
  errors.push(prefix + '.check must have exactly one of: query, predicate, agentVerdict');
1206
1218
  }
1207
- // check.args: literal tokens `loop run-gates` appends after the phase — query gates only,
1208
- // at most 8, each a flag or a plain word (the runner refuses anything else at run time too).
1209
- if (Object.prototype.hasOwnProperty.call(gate.check, 'args')) {
1210
- const a = gate.check.args;
1211
- if (!hasQuery) {
1212
- errors.push(prefix + '.check.args is only valid beside check.query');
1213
- } else if (!Array.isArray(a) || a.length > 8
1214
- || !a.every((t) => typeof t === 'string' && /^(?:--[a-z][a-z0-9-]*|[a-z0-9][a-z0-9._-]*)$/i.test(t))) {
1215
- errors.push(prefix + '.check.args must be an array of at most 8 flags (--name) or plain words');
1216
- }
1217
- }
1218
1219
  // agentVerdict forces blocking: false (advisory only)
1219
1220
  if (hasAgentVerdict && gate.blocking === true) {
1220
1221
  errors.push(
@@ -1246,8 +1247,7 @@ function validateGate(gate, prefix) {
1246
1247
  * - contribution.into ∈ step's agentRoles, OR contribution.into names an agent
1247
1248
  * already registered by some capability's own `agents` array (a non-pipeline
1248
1249
  * consumer of the same loop point, e.g. code-review.md reading execute:post
1249
- * alongside execute-phase.md — see capabilities/vault-practices's
1250
- * execute:post -> pp-code-reviewer contribution)
1250
+ * alongside execute-phase.md)
1251
1251
  * - when references a config key in cap.config
1252
1252
  *
1253
1253
  * NOTE: step.consumes satisfiability is NOT checked here — it requires the full
@@ -1330,6 +1330,102 @@ function validateAgainstContract(cap, capId, knownAgentIds) {
1330
1330
  return errors;
1331
1331
  }
1332
1332
 
1333
+ /**
1334
+ * Quick 261015-ect (QD-13): the vault-family rule for `requiresPlatformBindings`,
1335
+ * A capability that is `vault` or
1336
+ * lists `vault` in `requires` MUST declare the field, so a vault-family
1337
+ * capability added later cannot render hooks on a project the vault does not
1338
+ * govern by forgetting it. A capability outside the family may declare it too.
1339
+ *
1340
+ * @param {Map<string, object>} capMap
1341
+ * @returns {string[]} error strings; empty = all pass
1342
+ */
1343
+ function validatePlatformBindingsField(capMap) {
1344
+ const errors = [];
1345
+ for (const [capId, cap] of capMap) {
1346
+ if (!cap || typeof cap !== 'object') continue;
1347
+ const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
1348
+ if (capId !== 'vault' && !requiresVault) continue;
1349
+ if (!Object.prototype.hasOwnProperty.call(cap, 'requiresPlatformBindings')) {
1350
+ errors.push(
1351
+ 'capability "' + capId + '" is vault or requires vault and must declare requiresPlatformBindings, ' +
1352
+ 'so a vault-family capability cannot render hooks on a project the vault does not govern',
1353
+ );
1354
+ }
1355
+ }
1356
+ return errors;
1357
+ }
1358
+
1359
+ /**
1360
+ * Phase 16 D-19: the per-hook seal field's name. The resolver reads it as
1361
+ * `SEAL_FIELD` from capability-state; this generator cannot require that module
1362
+ * (its load graph reaches surface.cjs and config-loader.cjs, which read the
1363
+ * very registry this script regenerates), so the literal is restated here and
1364
+ * pinned equal to `SEAL_FIELD` by tests/capability-registry.test.cjs.
1365
+ */
1366
+ const SEAL_FIELD_NAME = 'onRecordedArtifacts';
1367
+
1368
+ /**
1369
+ * Phase 16 D-19 / D-25 / D-26: validate the per-hook seal field.
1370
+ *
1371
+ * A gate carrying the field renders for an enabled, config-inactive capability
1372
+ * over recorded vault artifacts, bypassing the capability's active check and
1373
+ * its `when` (D-18). That is a widening of render behaviour, so the field is
1374
+ * closed HERE, at generation, to exactly the shape the shipped seal gate has.
1375
+ * Five rules, each reported on its own:
1376
+ * (a) gates only (a step or contribution carrying it is refused);
1377
+ * (b) the value is literal `true` (`"true"`, `1`, `null`, `false` refused);
1378
+ * (c) the declaring capability is `vault` or lists `vault` in `requires`.
1379
+ * A seal gate never dispatches twice: the resolver drops it whenever its
1380
+ * capability is active, so it renders only with the vault switched off;
1381
+ * (d) the gate is `blocking: true`;
1382
+ * (e) the point is `ship:pre` (D-21: the requirement's block point; a seal at
1383
+ * verify:post would halt execute-phase on a switch set on purpose).
1384
+ *
1385
+ * Presence is own-property presence.
1386
+ *
1387
+ * @param {Map<string, object>} capMap
1388
+ * @returns {string[]} error strings; empty = all pass
1389
+ */
1390
+ function validateSealField(capMap) {
1391
+ const errors = [];
1392
+ const KINDS = [['steps', 'step'], ['contributions', 'contribution'], ['gates', 'gate']];
1393
+ for (const [capId, cap] of capMap) {
1394
+ if (!cap || typeof cap !== 'object') continue;
1395
+ const prefix = 'capability "' + capId + '"';
1396
+ const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
1397
+ for (const [arrKey, kind] of KINDS) {
1398
+ const arr = Array.isArray(cap[arrKey]) ? cap[arrKey] : [];
1399
+ for (const hook of arr) {
1400
+ if (!hook || typeof hook !== 'object') continue;
1401
+ if (!Object.prototype.hasOwnProperty.call(hook, SEAL_FIELD_NAME)) continue;
1402
+ const where = prefix + ' ' + kind + ' at point "' + String(hook.point) + '"';
1403
+ const value = hook[SEAL_FIELD_NAME];
1404
+ if (kind !== 'gate') {
1405
+ errors.push(where + ': ' + SEAL_FIELD_NAME + ' is allowed on gates only');
1406
+ continue;
1407
+ }
1408
+ if (value !== true) {
1409
+ errors.push(where + ': ' + SEAL_FIELD_NAME + ' must be literal true (got ' + JSON.stringify(value) + ')');
1410
+ }
1411
+ if (capId !== 'vault' && !requiresVault) {
1412
+ errors.push(
1413
+ where + ': ' + SEAL_FIELD_NAME + ' is allowed only on the "vault" capability or a capability that ' +
1414
+ 'lists "vault" in requires',
1415
+ );
1416
+ }
1417
+ if (hook.blocking !== true) {
1418
+ errors.push(where + ': a gate carrying ' + SEAL_FIELD_NAME + ' must be blocking: true');
1419
+ }
1420
+ if (hook.point !== 'ship:pre') {
1421
+ errors.push(where + ': ' + SEAL_FIELD_NAME + ' is allowed only at point "ship:pre"');
1422
+ }
1423
+ }
1424
+ }
1425
+ }
1426
+ return errors;
1427
+ }
1428
+
1333
1429
  /**
1334
1430
  * C1+C2: Global consumes-satisfiability validation.
1335
1431
  *
@@ -1490,6 +1586,14 @@ const TIER_RANK = { core: 0, standard: 1, full: 2 };
1490
1586
  function validateCrossCapability(capMap, centralKeys) {
1491
1587
  const errors = [];
1492
1588
 
1589
+ // Phase 16 D-19: the per-hook seal field (needs capMap for every capability's
1590
+ // `requires`).
1591
+ errors.push(...validateSealField(capMap));
1592
+
1593
+ // Quick 261015-ect (QD-13): every vault-family capability declares
1594
+ // requiresPlatformBindings (needs capMap for every capability's `requires`).
1595
+ errors.push(...validatePlatformBindingsField(capMap));
1596
+
1493
1597
  // Ownership: one owner per skill stem + agent name
1494
1598
  const skillOwner = new Map(); // skill → capId
1495
1599
  const agentOwner = new Map(); // agent → capId
@@ -2657,6 +2761,9 @@ module.exports = {
2657
2761
  validateAgainstContract,
2658
2762
  validateConsumesGlobal,
2659
2763
  validateCrossCapability,
2764
+ validateSealField,
2765
+ validatePlatformBindingsField,
2766
+ SEAL_FIELD_NAME,
2660
2767
  classifyCrossErrors,
2661
2768
  loadCentralConfigKeys,
2662
2769
  loadAndValidate,
@@ -233,106 +233,4 @@ function assertFileBaseline({ label, current, baseline, fail, updateHint }) {
233
233
  return { grown, shrunk, added, removed };
234
234
  }
235
235
 
236
- /**
237
- * Assert that each artifact's measured size stays within a per-change DELTA
238
- * BUDGET of its committed baseline (workflow size budget policy, 2026-10-11).
239
- *
240
- * Unlike `assertFileBaseline` (exact-byte pins), small growth is absorbed by a
241
- * slack so routine edits need no baseline commit, while large or cumulative
242
- * growth still fails by name:
243
- *
244
- * - A file may exceed its baseline by at most `perFileSlack` bytes.
245
- * - The SUM of all positive overages may not exceed `totalSlack` bytes, so many
246
- * small growths cannot add up to a large one unseen.
247
- * - Shrinking always passes and never needs a baseline commit.
248
- * - A file with no baseline entry (ADDED) or a baseline entry with no file
249
- * (REMOVED) fails: the baseline must name exactly the files on disk.
250
- * Recording either needs no reason.
251
- * - An EMPTY baseline fails: with no entries every file is "added", but an
252
- * explicit message makes the vacuous-pass case impossible to misread.
253
- *
254
- * Overage is measured against the committed baseline, not the previous commit,
255
- * so the slack is a budget that a re-baseline resets — not a per-commit
256
- * allowance that can be spent again on every commit.
257
- *
258
- * @param {object} opts
259
- * @param {string} opts.label - Guard name (used in messages).
260
- * @param {Object<string, number>} opts.current - Measured sizes by name.
261
- * @param {Object<string, number>} opts.baseline - Committed sizes by name
262
- * (numbers only; strip metadata keys first).
263
- * @param {number} opts.perFileSlack - Max bytes one file may exceed its baseline.
264
- * @param {number} opts.totalSlack - Max sum of all positive overages.
265
- * @param {function(string): void} opts.fail - Invoked once per violation category.
266
- * @param {string} [opts.updateHint] - Remediation hint appended to growth failures.
267
- * @returns {{ overages: Array<{name:string,from:number,to:number,delta:number}>,
268
- * totalOverage: number,
269
- * shrunk: Array<{name:string,from:number,to:number,delta:number}>,
270
- * added: string[], removed: string[] }}
271
- */
272
- function assertDeltaBudget({ label, current, baseline, perFileSlack, totalSlack, fail, updateHint }) {
273
- const currentNames = new Set(Object.keys(current));
274
- const baselineNames = new Set(Object.keys(baseline));
275
- const hint = updateHint ? `\n${updateHint}` : '';
276
-
277
- if (baselineNames.size === 0) {
278
- fail(
279
- `[${label}] the committed baseline has no size entries — an empty baseline would make ` +
280
- `every growth check pass vacuously. Regenerate it with \`npm run size:baseline\`.`
281
- );
282
- }
283
-
284
- const added = [...currentNames].filter((n) => !baselineNames.has(n)).sort();
285
- const removed = [...baselineNames].filter((n) => !currentNames.has(n)).sort();
286
-
287
- const overages = [];
288
- const shrunk = [];
289
- for (const name of [...currentNames].filter((n) => baselineNames.has(n)).sort()) {
290
- const from = baseline[name];
291
- const to = current[name];
292
- if (!Number.isInteger(from) || from < 0) {
293
- fail(`[${label}] baseline entry ${name} is not a non-negative integer byte size: ${JSON.stringify(from)}`);
294
- continue;
295
- }
296
- if (to > from) overages.push({ name, from, to, delta: to - from });
297
- else if (to < from) shrunk.push({ name, from, to, delta: from - to });
298
- }
299
- const totalOverage = overages.reduce((sum, o) => sum + o.delta, 0);
300
- const fmt = (o) => ` - ${o.name}: ${o.from} → ${o.to} (+${o.delta})`;
301
-
302
- const overFile = overages.filter((o) => o.delta > perFileSlack);
303
- if (overFile.length > 0) {
304
- fail(
305
- `[${label}] ${overFile.length} file(s) grew more than the per-file slack of ${perFileSlack} bytes ` +
306
- `past the committed baseline. Shrink them (extract content lazily), or re-baseline with a ` +
307
- `reason: npm run size:baseline -- --reason "<why this growth is needed>"\n` +
308
- `${overFile.map(fmt).join('\n')}${hint}`
309
- );
310
- }
311
-
312
- if (totalOverage > totalSlack) {
313
- fail(
314
- `[${label}] total growth past the committed baseline is ${totalOverage} bytes, over the total ` +
315
- `slack of ${totalSlack} bytes (each file may be under its own slack — the sum is what failed). ` +
316
- `Shrink, or re-baseline with a reason: npm run size:baseline -- --reason "<why>"\n` +
317
- `${overages.map(fmt).join('\n')}${hint}`
318
- );
319
- }
320
-
321
- if (added.length > 0) {
322
- fail(
323
- `[${label}] ${added.length} file(s) are not in the baseline — run \`npm run size:baseline\` ` +
324
- `to record them (no --reason needed for new files).\n${added.map((n) => ` - ${n}`).join('\n')}`
325
- );
326
- }
327
-
328
- if (removed.length > 0) {
329
- fail(
330
- `[${label}] ${removed.length} baseline entry(ies) no longer exist — run \`npm run size:baseline\` ` +
331
- `to drop them (no --reason needed).\n${removed.map((n) => ` - ${n}`).join('\n')}`
332
- );
333
- }
334
-
335
- return { overages, totalOverage, shrunk, added, removed };
336
- }
337
-
338
- module.exports = { assertWithinAllowlist, assertTightCeiling, assertFileBaseline, assertDeltaBudget };
236
+ module.exports = { assertWithinAllowlist, assertTightCeiling, assertFileBaseline };
@@ -288,7 +288,6 @@
288
288
  "tests/read-guard.test.cjs :: source-text-is-the-product",
289
289
  "tests/reapply-patches.test.cjs :: source-text-is-the-product",
290
290
  "tests/reapply-verify-hunks.test.cjs :: source-text-is-the-product",
291
- "tests/release-coverage-scope.test.cjs :: source-text-is-the-product",
292
291
  "tests/release-tarball-smoke-workflow.test.cjs :: source-text-is-the-product",
293
292
  "tests/release-tarball-smoke.install.test.cjs :: integration-test-input",
294
293
  "tests/research-agent-profiles.test.cjs :: <runtime-contract-is-the-product> research agent .md content is the governed surface",
@@ -321,7 +320,6 @@
321
320
  "tests/windows-test-parity-guard.test.cjs :: structural-regression-guard",
322
321
  "tests/workflow-compat.test.cjs :: source-text-is-the-product",
323
322
  "tests/workflow-guard-registration.test.cjs :: structural-regression-guard",
324
- "tests/workflow-maintainer-skip.test.cjs :: source-text-is-the-product",
325
323
  "tests/workflow-shell-pinning.test.cjs :: file-scope prefilter, not a test assertion — we need to",
326
324
  "tests/workflow-size-budget.test.cjs :: source-text-is-the-product",
327
325
  "tests/workspace.test.cjs :: source-text-is-the-product",
File without changes
@@ -9,9 +9,7 @@ const { runMain } = require('./lib/cli-exit.cjs');
9
9
  const ROOT = path.join(__dirname, '..');
10
10
 
11
11
  const DEFAULT_RELATIVE_FILES = [
12
- '.github/workflows/test.yml',
13
- '.github/workflows/pr-template-format.yml',
14
- '.github/workflows/changeset-required.yml',
12
+ '.github/workflows/ci.yml',
15
13
  'scripts/lint-command-contract.cjs',
16
14
  'scripts/lint-skill-deps.cjs',
17
15
  'scripts/lint-descriptions.cjs',
@@ -179,30 +179,14 @@
179
179
  },
180
180
  "vault": {
181
181
  "files": [
182
- "vault-practices.property.test.cjs",
182
+ "feat-71924-vault-required-gate.test.cjs",
183
+ "vault-doors.integration.test.cjs",
183
184
  "vault-read-path.test.cjs",
184
185
  "vault-security.test.cjs",
185
186
  "vault-status-check.test.cjs",
186
187
  "vault.test.cjs"
187
188
  ],
188
- "issue": "v1.7.0 Client Vault Load — four distinct concerns: vault.test (fetch/status/summary unit), vault-security (VSEC-02/03 write-guard + secret/fence), vault-read-path (VGATE-03 SUMMARY pointer), vault-status-check (VGATE-02 gate-query handler). Phase 24-02 added vault-practices.property.test.cjs (fast-check fuzz of the practices glob-fallback compiler + a practices-frontmatter round-trip, PRAC-02) — it is grouped under this bucket rather than \"vault-practices\" because the test-file-count lint's hyphen-prefix matcher treats \"vault-practices.property\" (period before \"property\") as starting with \"vault-\" before it can match the more specific \"vault-practices-\" prefix; the module it actually tests is pp-core/bin/lib/vault-practices.cjs, not vault.cjs."
189
- },
190
- "vault-review": {
191
- "files": [
192
- "vault-review.integration.test.cjs",
193
- "vault-review.security.test.cjs",
194
- "vault-review.test.cjs"
195
- ],
196
- "issue": "Agent practice review (vault assurance R5) — three distinct concerns for pp-core/bin/lib/vault-review.cjs: vault-review.test (the pure derivations — vocabularies, both derived-N-A arms, the counts, the range, the clean-render refusal, the scaffold renderer and the line stamper — plus the require-graph barrier), vault-review.integration.test (the seams: the artifact validator and the check vault-review / vault review scaffold|finalize arms driven through the real pp-tools.cjs, where the exit-0 / no-block-key contract is proved), and vault-review.security.test (adversarial practice bodies: a spoofed fence marker, directive-shaped text, a client table inside the fence, an option-shaped base branch). The .security marker is what identifies the adversarial lane to scripts/run-tests.cjs; consolidating would blur three different questions into one file."
197
- },
198
- "vault-practices": {
199
- "files": [
200
- "vault-practices-examples.test.cjs",
201
- "vault-practices-matcher.test.cjs",
202
- "vault-practices.slow.test.cjs",
203
- "vault-practices.test.cjs"
204
- ],
205
- "issue": "Phase 24 (24-01/24-02/24-04) — three distinct concerns for pp-core/bin/lib/vault-practices.cjs: vault-practices.test (24-01 parser/validation/discovery unit tests), vault-practices-matcher.test (24-02 glob matcher + two-bucket result + report unit tests), vault-practices-examples.test (24-04 shipped-example drift guard + minimal-comments global-bucket smoke case, PRAC-03 Success Criterion 3). All three test the same production module and are each independently load-bearing; consolidating would blur distinct plan-level verification gates. The practice-file contract extensions added vault-practices.slow.test.cjs — the >5 s worst-case matcher corpus behind the matching cost bound, split out per the .slow suffix convention so the default suite stays fast."
189
+ "issue": "v1.7.0 Client Vault Load — four distinct concerns: vault.test (fetch/status/summary unit), vault-security (VSEC-02/03 write-guard + secret/fence), vault-read-path (VGATE-03 SUMMARY pointer), vault-status-check (VGATE-02 gate-query handler). Vault Assurance phase 02 (#71924) added feat-71924-vault-required-gate.test.cjs as the phase's ONE consolidated source-grep feature file — it owns the source-text assertions for GATE-02, GATE-03, GATE-06, GATE-08, GATE-09 and GATE-10 together, deliberately in a single file so the phase's six requirements do not each spawn a sibling; later plans in the phase extend it rather than adding files. Vault Assurance phase 16 plan 10 added vault-doors.integration.test.cjs as the phase's ONE door fence-proof owner (DOOR-01..05, D-09): every door's shipped fence (execute-plan, quick, fast, debug, the secondary doors) composed by tests/helpers/gate-fence-composer.cjs and run in bash and zsh, one describe per door plan, so 16-12..16-15 extend it rather than each adding a sibling. The hyphen-prefix matcher attributes it here because no vault-doors module exists; it tests workflow fences, not vault.cjs."
206
190
  }
207
191
  }
208
192
  }
@@ -80,12 +80,11 @@ function prodPrefix(filename) {
80
80
  // stamps. Five forms are stripped: `.integration`, `.slow`, `.security`,
81
81
  // `.install` and the bare `.test`.
82
82
  //
83
- // `.property` is DELIBERATELY NOT stripped. The `vault` entry in
84
- // scripts/lint-test-file-count.allowlist.json documents
85
- // vault-practices.property.test.cjs as parked in that bucket precisely because
86
- // this function leaves the suffix on — stripping it would re-cluster the file
87
- // into `vault-practices` and turn that entry's own explanation into a false
88
- // statement. Changing it is a deliberate allowlist migration, not a tidy-up.
83
+ // `.property` is DELIBERATELY NOT stripped: a `<name>.property.test.cjs` keeps
84
+ // the suffix in its prefix, so the hyphen-prefix matcher clusters it under the
85
+ // shortest matching module rather than under `<name>`. Stripping it would
86
+ // re-cluster every property test the allowlist has already counted. Changing
87
+ // it is a deliberate allowlist migration, not a tidy-up.
89
88
  function testEffectivePrefix(testName) {
90
89
  const bare = testName
91
90
  .replace(/\.(integration|slow|security|install)\.test\.(ts|cjs)$/, '')
@@ -82,23 +82,25 @@ PATTERNS=(
82
82
  # THIS file and reads the array bash actually expands — adding, removing or
83
83
  # un-reasoning an entry here without editing the test turns the suite red.
84
84
  ALLOWLIST=(
85
- 'scripts/prompt-injection-scan.sh::the PATTERNS array above is the literal source of every phrase this scanner looks for, so the file matches itself by construction; measured 4 pattern hits at HEAD on 2026-10-11'
86
- 'scripts/base64-scan.sh::decodes base64 blobs and re-scans the recovered plaintext against the same injection vocabulary, so it names those phrases inline; measured 2 pattern hits at HEAD on 2026-10-11'
87
- 'scripts/secret-scan.sh::third of the three diff-mode scan scripts CI runs together in the security-scan job of .gitlab-ci.yml; measured 0 pattern hits at HEAD on 2026-10-11, retained because its own exemption list at line 223 already names security-prompt-injection.security.test.cjs and grows with that vocabulary'
88
- 'tests/security-scan.security.test.cjs::the behavioural suite for this very scanner, so every positive case embeds a real injection phrase as fixture input; measured 28 pattern hits at HEAD on 2026-10-11'
89
- 'tests/security.test.cjs::the adversarial suite for the library sanitisers, whose fixtures are real override and role-manipulation strings asserted to be neutralised; measured 54 pattern hits at HEAD on 2026-10-11'
90
- 'tests/prompt-injection-scan.security.test.cjs::drives scanForInjection in the compiled security library with crafted payloads, and carries a second allowlist of its own for that separate scanner; measured 11 pattern hits at HEAD on 2026-10-11'
91
- 'tests/vault-security.test.cjs::the vault fencing suite plants an instruction-override heading in a fixture vault file and asserts SUMMARY.md fences and defangs it rather than passing it to the agent as instructions; measured 1 pattern hit at HEAD on 2026-10-11'
92
- 'tests/vault-practices-matcher.test.cjs::the vault practices matcher suite plants an instruction-override line inside a fixture practice body and a spoofed fence end, and asserts the matcher keeps both inside the untrusted fenced region, so the fixtures must carry a real injection phrase'
93
- 'tests/vault-review.security.test.cjs::the adversarial suite of the agent practice review seeds a practice body with directive-shaped text aimed at the practice judge (the DIRECTIVE_TEXT constant) and asserts the rendered review entry carries it only between the fence markers, after the reference-data preamble, so the judge reads it as data; the text must be a literal for that assertion to mean anything; measured 1 pattern hit at HEAD on 2026-10-12'
94
- 'tests/verify.test.cjs::the verification suite plants an instruction-override line in a phase artifact and asserts it is rejected, so the line is present as a negative fixture; measured 1 pattern hit at HEAD on 2026-10-11'
95
- 'pp-core/bin/lib/security.cjs::gitignored build output compiled from src/security.cts per .gitignore line 117, so it is unreachable in diff mode and matters only to a local dir-mode run; it carries the library injection patterns inline; measured 8 pattern hits at HEAD on 2026-10-11'
96
- 'tests/read-injection-scanner.security.test.cjs::exercises the read-time injection scanner hook with the payloads a hostile file would carry, each asserted to be blocked rather than obeyed; measured 14 pattern hits at HEAD on 2026-10-11'
97
- 'tests/security-prompt-injection.security.test.cjs::carries two deliberate NUL bytes at lines 631 and 881 so grep classifies it as binary and reports whole-file matches with no line numbers; 10 binary pattern matches measured at HEAD on 2026-10-11'
98
- 'tests/fixtures/adversarial/security/::a directory of adversarial corpus files whose entire purpose is to BE injection payloads for the suites above; measured 20 pattern hits across its 7 tracked files at HEAD on 2026-10-11'
99
- 'SECURITY.md::the substring rule makes this exempt every path ending in SECURITY.md, including pp-core/templates/SECURITY.md and every per-phase threat register generated from that template; the breadth is accepted because a security policy is exactly where injection phrasing is legitimately quoted; measured 1 pattern hit across all 13 such files on disk at HEAD on 2026-10-11, a quoted attack shape in a phase threat register'
100
- 'TEST-EXAMPLES.md::documentation that demonstrates injection examples in order to explain what these scanners catch, so the example text is the point of the file rather than an attack; measured 1 pattern hit at HEAD on 2026-10-11'
101
- 'explanation/security-model.md::matches docs/explanation/security-model.md through the substring rule; it is the prose that explains the injection threat model and quotes the attack shapes pp-core defends against; measured 2 pattern hits at HEAD on 2026-10-11'
85
+ 'scripts/prompt-injection-scan.sh::the PATTERNS array above is the literal source of every phrase this scanner looks for, so the file matches itself by construction; measured 4 pattern hits at HEAD on 2026-09-19'
86
+ 'scripts/base64-scan.sh::decodes base64 blobs and re-scans the recovered plaintext against the same injection vocabulary, so it names those phrases inline; measured 2 pattern hits at HEAD on 2026-09-19'
87
+ 'scripts/secret-scan.sh::third of the three diff-mode scan scripts CI runs together in the security_scan job of .github/workflows/ci.yml; measured 0 pattern hits at HEAD on 2026-09-19, retained because its own exemption list at line 223 already names security-prompt-injection.security.test.cjs and grows with that vocabulary'
88
+ 'tests/security-scan.security.test.cjs::the behavioural suite for this very scanner, so every positive case embeds a real injection phrase as fixture input; measured 28 pattern hits at HEAD on 2026-09-19'
89
+ 'tests/security.test.cjs::the adversarial suite for the library sanitisers, whose fixtures are real override and role-manipulation strings asserted to be neutralised; measured 54 pattern hits at HEAD on 2026-09-19'
90
+ 'tests/prompt-injection-scan.security.test.cjs::drives scanForInjection in the compiled security library with crafted payloads, and carries a second allowlist of its own for that separate scanner; measured 11 pattern hits at HEAD on 2026-09-19'
91
+ 'tests/verify.test.cjs::the verification suite plants an instruction-override line in a phase artifact and asserts it is rejected, so the line is present as a negative fixture; measured 1 pattern hit at HEAD on 2026-09-19'
92
+ 'pp-core/bin/lib/security.cjs::gitignored build output compiled from src/security.cts per .gitignore line 117, so it is unreachable in diff mode and matters only to a local dir-mode run; it carries the library injection patterns inline; measured 8 pattern hits at HEAD on 2026-09-19'
93
+ 'tests/read-injection-scanner.security.test.cjs::exercises the read-time injection scanner hook with the payloads a hostile file would carry, each asserted to be blocked rather than obeyed; measured 14 pattern hits at HEAD on 2026-09-19'
94
+ 'tests/security-prompt-injection.security.test.cjs::carries two deliberate NUL bytes at lines 631 and 881 so grep classifies it as binary and reports whole-file matches with no line numbers; 10 binary pattern matches measured at HEAD on 2026-09-19'
95
+ 'tests/fixtures/adversarial/security/::a directory of adversarial corpus files whose entire purpose is to BE injection payloads for the suites above; measured 20 pattern hits across its 7 tracked files at HEAD on 2026-09-19'
96
+ 'tests/discovery-vault-context.test.cjs::the DISCO-03/SC4 suite plants a real role-manipulation line (the PLANTED constant) into a fixture vault document and asserts the shipped scanner FINDS it, which is the non-vacuity partner for the assertion that the subflow itself scans clean; the payload has to be a literal for that half to mean anything, so the file matches the vocabulary it exists to prove is caught'
97
+ 'tests/vault-status-check.test.cjs::sanitisation seeds for the vault status message carry a real instruction and a real control character on purpose, each paired with a non-vacuity assertion that the RENDERED message does not carry it intact; measured 5 pattern hits at HEAD on 2026-09-19'
98
+ 'tests/vault-index.integration.test.cjs::the fence-spoof suite seeds a vault document heading and an INDEX description that carry a forged end-fence marker followed by an instruction-override phrase, and asserts the rendered SUMMARY keeps exactly one live end marker with the planted line defanged inside the fence, so the phrase must be a literal for the defang proof to mean anything; measured 4 pattern hits at HEAD 5ddb0b3 on 2026-10-12'
99
+ 'tests/vault-seal.integration.test.cjs::the uncovered-commit suite builds a commit whose subject is an instruction-override phrase padded past the cap, modelling a hostile commit message, and asserts the subject leaving the seal leaf is controls-collapsed and bounded to SEAL_SUBJECT_CAP before it can reach a record; measured 1 pattern hit at HEAD 5ddb0b3 on 2026-10-12'
100
+ 'tests/vault-standards.integration.test.cjs::the D-14 suites plant instruction-override lines in a standards document and in its INDEX description, each tagged with a random token, and assert that neither the token nor the phrase appears in any output of vault standards block or consulted, so the phrase must be a literal for the absence proof to mean anything; measured 6 pattern hits at HEAD 5ddb0b3 on 2026-10-12'
101
+ 'SECURITY.md::the substring rule makes this exempt every path ending in SECURITY.md, including pp-core/templates/SECURITY.md and every per-phase threat register generated from that template; the breadth is accepted because a security policy is exactly where injection phrasing is legitimately quoted; measured 0 pattern hits across all 16 such files on disk at HEAD on 2026-09-19'
102
+ 'TEST-EXAMPLES.md::documentation that demonstrates injection examples in order to explain what these scanners catch, so the example text is the point of the file rather than an attack; measured 1 pattern hit at HEAD on 2026-09-19'
103
+ 'explanation/security-model.md::matches docs/explanation/security-model.md through the substring rule; it is the prose that explains the injection threat model and quotes the attack shapes pp-core defends against; measured 2 pattern hits at HEAD on 2026-09-19'
102
104
  )
103
105
 
104
106
  is_allowlisted() {
File without changes