@pareto4/pp-core 1.10.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.
- package/.claude-plugin/plugin.json +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +5 -3
- package/pareto-skills/pp-brand/SKILL.md +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +49 -1
- package/pp-core/bin/lib/compose-bindings.cjs +736 -0
- package/pp-core/bin/lib/git-provider.cjs +279 -0
- package/pp-core/bin/lib/init-contract.cjs +989 -0
- package/pp-core/bin/lib/init-interactive.cjs +183 -0
- package/pp-core/bin/lib/io.cjs +2 -0
- package/pp-core/bin/lib/org-profile.cjs +696 -0
- package/pp-core/bin/lib/platform-bindings.cjs +268 -64
- package/pp-core/bin/lib/vault-command-router.cjs +133 -2
- package/pp-core/bin/lib/vault-coordinates.cjs +485 -0
- package/pp-core/bin/lib/vault-write-back.cjs +178 -92
- package/pp-core/bin/lib/vault.cjs +158 -81
- package/pp-core/bin/pp-tools.cjs +199 -2
- package/pp-core/workflows/discovery-sprint.md +1 -1
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +1 -1
- package/pp-core/workflows/init.md +137 -76
- package/scripts/gen-platform-bindings-schema.cjs +148 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/init-interactive.cts — drives the 34-08 `/pp-init` session state
|
|
4
|
+
* machine against an injectable prompt seam (milestone v1.11.0, Phase
|
|
5
|
+
* 34-09, gap closure).
|
|
6
|
+
*
|
|
7
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
8
|
+
* compiles to the git-ignored `pp-core/bin/lib/init-interactive.cjs`; do NOT
|
|
9
|
+
* hand-edit the compiled output. Behaviour is locked by
|
|
10
|
+
* `tests/init-interactive.test.cjs`.
|
|
11
|
+
*
|
|
12
|
+
* Born from a documented failure, not a speculative hardening. Four human
|
|
13
|
+
* UAT walks of `/pp-init` on 2026-07-31 (`34-VALIDATION.md` § Manual-Only
|
|
14
|
+
* Verifications) all failed at the identical point, across four escalating
|
|
15
|
+
* enforcement strategies — prose, hardened prose, a data-supplying CLI arm
|
|
16
|
+
* (34-07), and 34-08's one-step-at-a-time session state machine. The agent
|
|
17
|
+
* executed every pure-bash step reliably in all four walks and completed
|
|
18
|
+
* Step 3 remote wiring every time; the ONLY step it never once performed as
|
|
19
|
+
* written is the one whose job is to ask the user a structured question
|
|
20
|
+
* set — at that boundary it substitutes its own `AskUserQuestion` wizard
|
|
21
|
+
* and discards the document. 34-08 gave the CLI the sequence but left the
|
|
22
|
+
* agent the interaction; this module gives the CLI the interaction itself.
|
|
23
|
+
*
|
|
24
|
+
* `runInitInteractive` reimplements NOTHING from `src/init-contract.cts` —
|
|
25
|
+
* it only sequences calls into `initSessionNext`/`initSessionAnswer`, which
|
|
26
|
+
* remain the sole authority for the question set, the identity/provider
|
|
27
|
+
* writes, and the compose-and-write decision. The `prompter` parameter is
|
|
28
|
+
* the seam: an object with `ask()`/`print()` that the CLI (pp-tools.cjs)
|
|
29
|
+
* implements with `node:readline` against a real terminal, and that tests
|
|
30
|
+
* implement with a scripted fake. This module deliberately never imports
|
|
31
|
+
* `node:readline` itself — that is what keeps the whole interactive flow
|
|
32
|
+
* testable by `node:test` with no pty (see the plan's own design hazards).
|
|
33
|
+
*
|
|
34
|
+
* Residual risk, stated plainly (see the plan's threat register): nothing
|
|
35
|
+
* here compels a user to run `pp-tools init`. What it removes is the
|
|
36
|
+
* failure mode that actually bit four times — `/pp-init` appearing to
|
|
37
|
+
* complete while silently producing no binding.
|
|
38
|
+
*/
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.runInitInteractive = runInitInteractive;
|
|
44
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
45
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
46
|
+
const init_contract_cjs_1 = require("./init-contract.cjs");
|
|
47
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
48
|
+
/** Defensive circuit breaker against a runaway loop (a misbehaving
|
|
49
|
+
* prompter or an unexpected session state that never reaches `done` or
|
|
50
|
+
* `verify`). The real session never exceeds ~10 steps (8 questions +
|
|
51
|
+
* `compose_confirm` + `done`); this is headroom, not a real budget. */
|
|
52
|
+
const MAX_SESSION_STEPS = 100;
|
|
53
|
+
// ─── Small local helpers ───────────────────────────────────────────────────
|
|
54
|
+
function isRecord(v) {
|
|
55
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
56
|
+
}
|
|
57
|
+
function bindingsPathFor(cwd) {
|
|
58
|
+
return node_path_1.default.join(cwd, '.platform-bindings.json');
|
|
59
|
+
}
|
|
60
|
+
/** Snapshot `.platform-bindings.json`'s content (`null` when absent) — used
|
|
61
|
+
* to compute `wroteBinding` from an actual pre/post diff rather than a bare
|
|
62
|
+
* post-hoc `existsSync`, which reads a pre-existing file (predating this
|
|
63
|
+
* run) as "we wrote it" (WR-01). Read failures degrade to `null` — this is
|
|
64
|
+
* a diagnostic snapshot, not a source of truth `initSessionAnswer` itself
|
|
65
|
+
* relies on, so it must never throw. */
|
|
66
|
+
function readBindingsContent(cwd) {
|
|
67
|
+
try {
|
|
68
|
+
return node_fs_1.default.readFileSync(bindingsPathFor(cwd), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Render `compose_confirm`'s provenance table, drift list and action to
|
|
75
|
+
* the prompter, BEFORE the caller asks for approval — plan Task 1's own
|
|
76
|
+
* ordering requirement. */
|
|
77
|
+
function renderComposeConfirm(prompter, info) {
|
|
78
|
+
if (!isRecord(info))
|
|
79
|
+
return;
|
|
80
|
+
const typed = info;
|
|
81
|
+
prompter.print('A binding can be composed from the org profile and project answers:');
|
|
82
|
+
for (const entry of typed.provenance ?? []) {
|
|
83
|
+
prompter.print(` ${entry.field} = ${JSON.stringify(entry.value)} (from ${entry.source})`);
|
|
84
|
+
}
|
|
85
|
+
const drift = typed.drift ?? [];
|
|
86
|
+
if (drift.length > 0) {
|
|
87
|
+
prompter.print('Drift (the already-committed file wins — nothing here is a proposed change):');
|
|
88
|
+
for (const d of drift) {
|
|
89
|
+
prompter.print(` the profile would have supplied ${JSON.stringify(d.profileValue)} for "${d.key}"; ` +
|
|
90
|
+
`the file already says ${JSON.stringify(d.effectiveValue)}, and ${JSON.stringify(d.effectiveValue)} wins.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const warning of typed.warnings ?? []) {
|
|
94
|
+
prompter.print(` warning: ${warning}`);
|
|
95
|
+
}
|
|
96
|
+
prompter.print(`Action: ${typed.action ?? 'unknown'}`);
|
|
97
|
+
}
|
|
98
|
+
// ─── runInitInteractive ─────────────────────────────────────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* Drive the `/pp-init` session at `cwd` to completion, asking `prompter`
|
|
101
|
+
* for exactly one answer per step and submitting it back to the session.
|
|
102
|
+
* Reimplements no session logic — `initSessionNext`/`initSessionAnswer`
|
|
103
|
+
* remain the sole authority for sequencing, validation and every write.
|
|
104
|
+
*
|
|
105
|
+
* Loop, per session step:
|
|
106
|
+
* - `error` (malformed session file) → print and stop, `ok: false`.
|
|
107
|
+
* - `done` → print a short summary (what was written, or that nothing was
|
|
108
|
+
* composed and why) and stop, `ok: true`.
|
|
109
|
+
* - `verify` (unresolved invariant violations) → print every violation
|
|
110
|
+
* and stop, `ok: false` — this is the intentional "visibly unfinished"
|
|
111
|
+
* outcome the session design is built around, never a crash.
|
|
112
|
+
* - `question` / `compose_confirm` → render (provenance/drift/action for
|
|
113
|
+
* `compose_confirm`, BEFORE asking), then `ask` the prompter and submit
|
|
114
|
+
* the answer. A REJECTED answer (`initSessionAnswer` returns `ok:
|
|
115
|
+
* false`) is re-asked with the session's own `message`, in a bounded
|
|
116
|
+
* inner loop — never swallowed, never silently treated as accepted.
|
|
117
|
+
*/
|
|
118
|
+
async function runInitInteractive(cwd, prompter, opts = {}) {
|
|
119
|
+
const sessionOpts = { offline: opts.offline === true, _globalRoot: opts._globalRoot };
|
|
120
|
+
let wroteBinding = false;
|
|
121
|
+
for (let steps = 1; steps <= MAX_SESSION_STEPS; steps++) {
|
|
122
|
+
const state = (0, init_contract_cjs_1.initSessionNext)(cwd, sessionOpts);
|
|
123
|
+
if (state.error !== undefined) {
|
|
124
|
+
prompter.print(`Error: ${state.message ?? state.error}`);
|
|
125
|
+
return { ok: false, wroteBinding, steps, violations: [] };
|
|
126
|
+
}
|
|
127
|
+
if (state.done) {
|
|
128
|
+
if (wroteBinding) {
|
|
129
|
+
prompter.print('Done — .platform-bindings.json was composed and written.');
|
|
130
|
+
}
|
|
131
|
+
else if (typeof state.info === 'string') {
|
|
132
|
+
prompter.print(state.info);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
prompter.print('Done — nothing was composed (declined, or nothing was composable).');
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, wroteBinding, steps, violations: [] };
|
|
138
|
+
}
|
|
139
|
+
if (state.step === 'verify') {
|
|
140
|
+
const violations = state.violations ?? [];
|
|
141
|
+
prompter.print('The session could not reach a clean end state:');
|
|
142
|
+
for (const v of violations) {
|
|
143
|
+
prompter.print(` - [${v.code}] ${v.message}`);
|
|
144
|
+
}
|
|
145
|
+
return { ok: false, wroteBinding, steps, violations };
|
|
146
|
+
}
|
|
147
|
+
if (state.step === null) {
|
|
148
|
+
// Defensive: the session contract never returns a null step outside
|
|
149
|
+
// `done`/`verify`/`error` above. Fail closed rather than crash.
|
|
150
|
+
prompter.print('Unexpected session state: no step id was returned.');
|
|
151
|
+
return { ok: false, wroteBinding, steps, violations: [] };
|
|
152
|
+
}
|
|
153
|
+
const stepId = state.step;
|
|
154
|
+
if (stepId === 'compose_confirm') {
|
|
155
|
+
renderComposeConfirm(prompter, state.info);
|
|
156
|
+
}
|
|
157
|
+
// Ask, re-asking on rejection — never swallowed (T-34-73).
|
|
158
|
+
for (;;) {
|
|
159
|
+
// WR-01: capture the pre-answer state of the binding file BEFORE
|
|
160
|
+
// submitting the answer, so a true write can be told apart from a
|
|
161
|
+
// pre-existing file that predates this run (a bare post-hoc
|
|
162
|
+
// `existsSync` cannot make that distinction).
|
|
163
|
+
const preBindings = stepId === 'compose_confirm' ? readBindingsContent(cwd) : null;
|
|
164
|
+
const answer = await prompter.ask({
|
|
165
|
+
prompt: state.prompt ?? '',
|
|
166
|
+
options: state.options,
|
|
167
|
+
preselected: state.preselected,
|
|
168
|
+
preselectedLabel: state.preselectedLabel,
|
|
169
|
+
optionalAnswer: state.optionalAnswer,
|
|
170
|
+
});
|
|
171
|
+
const result = (0, init_contract_cjs_1.initSessionAnswer)(cwd, stepId, answer, sessionOpts);
|
|
172
|
+
if (result.ok) {
|
|
173
|
+
if (stepId === 'compose_confirm') {
|
|
174
|
+
wroteBinding = readBindingsContent(cwd) !== preBindings;
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
prompter.print(result.message ?? `Rejected (${result.reason ?? 'unknown reason'}) — please answer again.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
prompter.print('init session exceeded the maximum step count — aborting to avoid an infinite loop.');
|
|
182
|
+
return { ok: false, wroteBinding, steps: MAX_SESSION_STEPS, violations: [] };
|
|
183
|
+
}
|
package/pp-core/bin/lib/io.cjs
CHANGED
|
@@ -177,6 +177,8 @@ const ERROR_REASON = Object.freeze({
|
|
|
177
177
|
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
178
178
|
// platform-bindings
|
|
179
179
|
PLATFORM_BINDINGS_PARSE_FAILED: 'platform_bindings_parse_failed',
|
|
180
|
+
// init (Phase 34-09 gap closure: `pp-tools init` interactive command)
|
|
181
|
+
INIT_NON_INTERACTIVE: 'init_non_interactive',
|
|
180
182
|
// vault write-back promote arm (Phase 29 WRB-01/03)
|
|
181
183
|
VAULT_PROMOTE_DISABLED: 'vault_promote_disabled',
|
|
182
184
|
VAULT_PROMOTE_INVALID_PRACTICE: 'vault_promote_invalid_practice',
|