@pareto4/pp-core 1.9.0 → 1.11.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 (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-code-fixer.md +11 -0
  3. package/agents/pp-code-reviewer.md +77 -1
  4. package/agents/pp-executor.md +34 -1
  5. package/agents/pp-planner.md +24 -103
  6. package/commands/pp-ns-manage.md +2 -1
  7. package/commands/pp-vault-promote.md +31 -0
  8. package/gemini-extension.json +1 -1
  9. package/package.json +5 -3
  10. package/pareto-skills/pp-brand/SKILL.md +1 -1
  11. package/pp-core/bin/lib/capability-registry.cjs +160 -3
  12. package/pp-core/bin/lib/capability-state.cjs +41 -3
  13. package/pp-core/bin/lib/check-command-router.cjs +473 -3
  14. package/pp-core/bin/lib/clusters.cjs +3 -0
  15. package/pp-core/bin/lib/compose-bindings.cjs +736 -0
  16. package/pp-core/bin/lib/git-provider.cjs +279 -0
  17. package/pp-core/bin/lib/init-contract.cjs +989 -0
  18. package/pp-core/bin/lib/init-interactive.cjs +183 -0
  19. package/pp-core/bin/lib/io.cjs +9 -0
  20. package/pp-core/bin/lib/org-profile.cjs +696 -0
  21. package/pp-core/bin/lib/platform-bindings.cjs +268 -64
  22. package/pp-core/bin/lib/practices-review-check.cjs +938 -0
  23. package/pp-core/bin/lib/vault-command-router.cjs +239 -2
  24. package/pp-core/bin/lib/vault-coordinates.cjs +485 -0
  25. package/pp-core/bin/lib/vault-practices.cjs +982 -0
  26. package/pp-core/bin/lib/vault-write-back.cjs +842 -0
  27. package/pp-core/bin/lib/vault.cjs +170 -51
  28. package/pp-core/bin/pp-tools.cjs +225 -2
  29. package/pp-core/references/planner-checkpoints.md +72 -0
  30. package/pp-core/references/planner-tdd.md +55 -0
  31. package/pp-core/references/planner-vault-practices.md +67 -0
  32. package/pp-core/templates/practices/README.md +122 -0
  33. package/pp-core/templates/practices/error-handling.md +18 -0
  34. package/pp-core/templates/practices/minimal-comments.md +14 -0
  35. package/pp-core/templates/practices/naming-convention.md +18 -0
  36. package/pp-core/templates/practices/practice-template.md +23 -0
  37. package/pp-core/workflows/code-review.md +74 -18
  38. package/pp-core/workflows/discovery-sprint.md +1 -1
  39. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -1
  40. package/pp-core/workflows/init.md +137 -76
  41. package/pp-core/workflows/ship.md +1 -0
  42. package/pp-core/workflows/vault-write-back.md +213 -0
  43. package/scripts/gen-capability-registry.cjs +68 -8
  44. package/scripts/gen-platform-bindings-schema.cjs +148 -0
  45. package/scripts/lint-allow-test-rule-refs.allowlist.json +8 -1
  46. package/scripts/lint-descriptions.cjs +3 -1
  47. package/scripts/lint-legacy-dir-name.cjs +5 -0
  48. package/scripts/lint-test-file-count.allowlist.json +10 -1
@@ -29,11 +29,62 @@
29
29
  */
30
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
31
  const io = require("./io.cjs");
32
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
32
33
  const { ERROR_REASON } = io;
33
34
  // Default CoreModule implementation. _core seam overrides this for test injection.
34
35
  const _defaultCore = { output: io.output };
36
+ /**
37
+ * Render a short plain-text `vault org-profile` report from its structured
38
+ * shape — one line naming the pointer, one naming the resolution, a
39
+ * contributes list (or a "contributes nothing" line), an overridden list
40
+ * (D-05 drift made observable) prefixed by the precedence sentence, the
41
+ * ignored/rejected key names when non-empty, and the warnings verbatim.
42
+ * Kept to plain sentences a person can read without knowing pp-core
43
+ * internals (Phase 33-04).
44
+ */
45
+ function renderOrgProfileReport(report) {
46
+ const lines = [];
47
+ if (report.pointer.value === null) {
48
+ lines.push('No org profile is configured (org.profile is unset) — pp-core\'s built-in defaults apply.');
49
+ }
50
+ else {
51
+ lines.push(`org.profile is set to "${report.pointer.value}".`);
52
+ }
53
+ if (report.profile.resolved) {
54
+ lines.push(`Resolved org profile "${report.profile.name}" version ${report.profile.version ?? 'unknown'} from ${report.profile.source}.`);
55
+ }
56
+ else if (report.pointer.value !== null) {
57
+ lines.push(`The org profile "${report.pointer.value}" could not be resolved from the project or the global npm root.`);
58
+ }
59
+ const contributesKeys = Object.keys(report.contributes);
60
+ if (contributesKeys.length === 0) {
61
+ lines.push('The org profile contributes nothing.');
62
+ }
63
+ else {
64
+ lines.push('The org profile contributes:');
65
+ for (const key of contributesKeys) {
66
+ lines.push(` ${key} = ${report.contributes[key]}`);
67
+ }
68
+ }
69
+ if (report.overridden.length > 0) {
70
+ lines.push('A value the project sets itself always wins over anything an org profile contributes:');
71
+ for (const entry of report.overridden) {
72
+ lines.push(` ${entry.key}: profile offered "${entry.profile}", but "${entry.effective}" won.`);
73
+ }
74
+ }
75
+ if (report.ignored.length > 0) {
76
+ lines.push(`Ignored (not on the allowlist): ${report.ignored.join(', ')}`);
77
+ }
78
+ if (report.rejected.length > 0) {
79
+ lines.push(`Rejected: ${report.rejected.join(', ')}`);
80
+ }
81
+ for (const warning of report.warnings) {
82
+ lines.push(warning);
83
+ }
84
+ return lines.join('\n');
85
+ }
35
86
  // ─── Implementation ───────────────────────────────────────────────────────────
36
- function routeVaultCommand({ args, cwd, raw, error, _vault, _core }) {
87
+ function routeVaultCommand({ args, cwd, raw, error, _vault, _core, _practices, _exec, _writeBack, _orgProfile, _coordinates, _provider, }) {
37
88
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
38
89
  const vault = _vault ?? require('./vault.cjs');
39
90
  const c = _core ?? _defaultCore;
@@ -47,8 +98,194 @@ function routeVaultCommand({ args, cwd, raw, error, _vault, _core }) {
47
98
  else if (subcommand === 'summary') {
48
99
  c.output(vault.vaultSummary(cwd), raw);
49
100
  }
101
+ else if (subcommand === 'practices') {
102
+ const verb = args[2];
103
+ if (verb === 'match') {
104
+ // T-24-08b: reject any unrecognized `--`-prefixed argument BEFORE the
105
+ // `--changed` git call (so a typo never spawns a subprocess) instead
106
+ // of silently filtering it out of the positional file list. Matched on
107
+ // the WHOLE token so `--files=…` is caught by the same check as
108
+ // `--files value`. DECISION (recorded, not deferred): `--files=` is
109
+ // NOT implemented as a comma-splitting alias here, unlike
110
+ // `/pp-code-review`'s `--files=a,b` (pp-core/workflows/code-review.md).
111
+ // The auditor marked the alias "optional and secondary"; the required
112
+ // behavior is loud failure, and the error message below names the
113
+ // supported forms instead. The host (pp-core/bin/pp-tools.cjs) already
114
+ // splices out its own global flags (--json-errors/--cwd/--ws/--raw/
115
+ // --pick/--default) before dispatch, which is why the recognized set
116
+ // here is only two flags wide.
117
+ const RECOGNIZED_MATCH_FLAGS = new Set(['--changed', '--raw', '--block']);
118
+ const unrecognizedFlag = args.slice(3).find((a) => a.startsWith('--') && !RECOGNIZED_MATCH_FLAGS.has(a));
119
+ if (unrecognizedFlag !== undefined) {
120
+ error(`Unrecognized flag "${unrecognizedFlag}" for vault practices match. Supported: positional file paths, --changed, --raw, or --block.`, ERROR_REASON.SDK_FAIL_FAST);
121
+ return;
122
+ }
123
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
124
+ const practices = _practices ?? require('./vault-practices.cjs');
125
+ const runGit = _exec ?? shell_command_projection_cjs_1.execGit;
126
+ let files;
127
+ if (args.includes('--changed')) {
128
+ const gitResult = runGit(['diff', '--name-only', 'HEAD'], { cwd });
129
+ if (gitResult.exitCode !== 0) {
130
+ error('Could not determine changed files: git diff failed (WR-02)', ERROR_REASON.SDK_FAIL_FAST);
131
+ return;
132
+ }
133
+ files = gitResult.stdout.split('\n').map((f) => f.trim()).filter((f) => f.length > 0);
134
+ }
135
+ else {
136
+ files = args.slice(3).filter((a) => !a.startsWith('--'));
137
+ }
138
+ const result = practices.matchPractices({ cwd, files });
139
+ if (args.includes('--block')) {
140
+ // T-25-08: emit the finished <vault_practices> block so the planner
141
+ // embeds it VERBATIM. The planner never renames a field or applies a
142
+ // fence itself — that transformation used to be prose-only, which is
143
+ // what left the phase's prompt-injection mitigation unenforced.
144
+ c.output(result, true, practices.renderPracticesBlock(result));
145
+ }
146
+ else if (raw) {
147
+ c.output(result, true, JSON.stringify(result));
148
+ }
149
+ else {
150
+ c.output(result, true, practices.renderPracticesReport(result));
151
+ }
152
+ }
153
+ else if (verb === 'promote') {
154
+ // T-29-16: reject any unrecognized `--`-prefixed argument BEFORE the
155
+ // lazy require of the write-back module and before any subprocess can
156
+ // spawn — mirrors the `match` branch's T-24-08b guard exactly. Matched
157
+ // on the WHOLE token so `--name=value` is caught by the same check as
158
+ // `--name value`. The recognized set holds exactly three flags — no
159
+ // flag that disables, bypasses, or relaxes any guard exists.
160
+ const RECOGNIZED_PROMOTE_FLAGS = new Set(['--confirmed', '--expect', '--raw']);
161
+ const promoteArgs = args.slice(3);
162
+ const unrecognizedPromoteFlag = promoteArgs.find((a) => a.startsWith('--') && !RECOGNIZED_PROMOTE_FLAGS.has(a));
163
+ if (unrecognizedPromoteFlag !== undefined) {
164
+ error(`Unrecognized flag "${unrecognizedPromoteFlag}" for vault practices promote. Supported: --confirmed, --expect <token>, --raw.`, ERROR_REASON.SDK_FAIL_FAST);
165
+ return;
166
+ }
167
+ // Positional file path: the first token that is not `--`-prefixed and
168
+ // is not the value immediately following `--expect`.
169
+ const expectFlagIdx = promoteArgs.indexOf('--expect');
170
+ const expectValueIdx = expectFlagIdx !== -1 ? expectFlagIdx + 1 : -1;
171
+ const filePath = promoteArgs.find((a, i) => !a.startsWith('--') && i !== expectValueIdx);
172
+ if (filePath === undefined) {
173
+ error('No practice file was given. Usage: vault practices promote <file> [--confirmed --expect <token>] [--raw].', ERROR_REASON.SDK_MISSING_ARG);
174
+ return;
175
+ }
176
+ const confirmed = promoteArgs.includes('--confirmed');
177
+ const expect = expectValueIdx !== -1 ? promoteArgs[expectValueIdx] : undefined;
178
+ if (confirmed && (expect === undefined || expect.startsWith('--'))) {
179
+ error('This practice was not pushed: the confirmation is missing the preview token it needs. ' +
180
+ 'Run the promotion without confirming first, then pass the token it prints.', ERROR_REASON.VAULT_PROMOTE_CONCURRENT_EDIT);
181
+ return;
182
+ }
183
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
184
+ const writeBack = _writeBack ?? require('./vault-write-back.cjs');
185
+ const verdict = writeBack.promotePractice({ cwd, filePath, confirmed, expect });
186
+ // Emit the structured verdict BEFORE any error call — a refusal still
187
+ // carries its verdict to stdout. This ordering is the whole point of
188
+ // the uniform D-16 refusal contract and is what lets a wrapper
189
+ // workflow render results without scraping prose.
190
+ if (raw) {
191
+ c.output(verdict, true, JSON.stringify(verdict));
192
+ }
193
+ else {
194
+ c.output(verdict, true, writeBack.renderPromoteReport(verdict));
195
+ }
196
+ if (verdict.status === 'refused') {
197
+ // An action command's refusal must be observable in the exit code —
198
+ // do not adopt the ship gate's always-exit-zero convention here.
199
+ error(verdict.message ?? 'This practice was not promoted.', verdict.reason ?? ERROR_REASON.UNKNOWN);
200
+ }
201
+ }
202
+ else {
203
+ error('Unknown practices subcommand. Available: match, promote', ERROR_REASON.SDK_UNKNOWN_COMMAND);
204
+ }
205
+ }
206
+ else if (subcommand === 'org-profile') {
207
+ // T-33-13: reject any unrecognized `--`-prefixed argument BEFORE any
208
+ // lazy require or resolution work — mirrors the `practices match`
209
+ // T-24-08b guard exactly, so a typo can never trigger filesystem work.
210
+ const RECOGNIZED_ORG_PROFILE_FLAGS = new Set(['--raw']);
211
+ const unrecognizedOrgProfileFlag = args
212
+ .slice(2)
213
+ .find((a) => a.startsWith('--') && !RECOGNIZED_ORG_PROFILE_FLAGS.has(a));
214
+ if (unrecognizedOrgProfileFlag !== undefined) {
215
+ error(`Unrecognized flag "${unrecognizedOrgProfileFlag}" for vault org-profile. Supported: --raw.`, ERROR_REASON.SDK_FAIL_FAST);
216
+ return;
217
+ }
218
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
219
+ const orgProfile = _orgProfile ?? require('./org-profile.cjs');
220
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
221
+ const coordinates = _coordinates ?? require('./vault-coordinates.cjs');
222
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
223
+ const providerModule = _provider ?? require('./git-provider.cjs');
224
+ const resolution = orgProfile.resolveOrgProfile(cwd);
225
+ // Resolve the EFFECTIVE values by calling the real resolvers, so the
226
+ // report shows what actually won rather than what the profile offered.
227
+ // `vault.fallback_host` has no standalone resolver of its own — the
228
+ // shared resolution's own `host` is the honest answer to "what host
229
+ // does the shared tier actually use", which is the question the
230
+ // operator is asking.
231
+ const { provider } = providerModule.resolveProvider(cwd);
232
+ const shared = coordinates.resolveSharedCoordinates(cwd, provider);
233
+ const platformHost = coordinates.resolvePlatformHost(cwd);
234
+ const effectiveByKey = {
235
+ 'vault.provider': provider,
236
+ [coordinates.SHARED_REPO_CONFIG_KEY]: shared.repo,
237
+ [coordinates.SHARED_REF_CONFIG_KEY]: shared.ref,
238
+ [coordinates.SHARED_HOST_CONFIG_KEY]: shared.host,
239
+ [coordinates.PLATFORM_HOST_CONFIG_KEY]: platformHost.host,
240
+ };
241
+ // Build `effective`'s key list from ORG_PROFILE_ALLOWLIST rather than a
242
+ // fresh literal array, so a future allowlist change surfaces here
243
+ // automatically instead of silently dropping a key from the diagnostic.
244
+ const effective = {};
245
+ for (const key of orgProfile.ORG_PROFILE_ALLOWLIST) {
246
+ effective[key] = Object.prototype.hasOwnProperty.call(effectiveByKey, key) ? effectiveByKey[key] : null;
247
+ }
248
+ // `overridden` holds every key present in `contributes` whose
249
+ // contributed value is not strictly equal to its effective value — D-05's
250
+ // drift made observable, and the whole reason this command resolves
251
+ // effective values at all rather than just echoing the document.
252
+ const overridden = [];
253
+ for (const key of Object.keys(resolution.contributes)) {
254
+ const profileValue = resolution.contributes[key];
255
+ const effectiveValue = Object.prototype.hasOwnProperty.call(effective, key) ? effective[key] : null;
256
+ if (effectiveValue !== profileValue) {
257
+ overridden.push({ key, profile: profileValue, effective: effectiveValue });
258
+ }
259
+ }
260
+ const report = {
261
+ pointer: { key: orgProfile.ORG_PROFILE_CONFIG_KEY, value: resolution.configured },
262
+ profile: {
263
+ resolved: resolution.resolved,
264
+ name: resolution.packageName,
265
+ version: resolution.version,
266
+ source: resolution.source,
267
+ dataPath: resolution.dataPath,
268
+ },
269
+ contributes: resolution.contributes,
270
+ effective,
271
+ overridden,
272
+ ignored: resolution.ignored,
273
+ rejected: resolution.rejected,
274
+ warnings: resolution.warnings,
275
+ };
276
+ // Never call error() for a project with no profile, and never exit
277
+ // non-zero for a resolution failure — this is a diagnostic, not an
278
+ // action command (D-09's posture: a broken profile is a warning, not a
279
+ // failure).
280
+ if (raw) {
281
+ c.output(report, true, JSON.stringify(report));
282
+ }
283
+ else {
284
+ c.output(report, true, renderOrgProfileReport(report));
285
+ }
286
+ }
50
287
  else {
51
- error('Unknown vault subcommand. Available: fetch, status, summary', ERROR_REASON.SDK_UNKNOWN_COMMAND);
288
+ error('Unknown vault subcommand. Available: fetch, status, summary, practices, org-profile', ERROR_REASON.SDK_UNKNOWN_COMMAND);
52
289
  }
53
290
  }
54
291
  module.exports = {