@pareto4/pp-core 1.13.0 → 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.
- package/.claude-plugin/plugin.json +3 -3
- package/README.md +26 -9
- package/agents/pp-code-reviewer.md +2 -1
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +10 -13
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +23 -9
- package/agents/pp-vault-reviewer.md +244 -21
- package/agents/pp-verifier.md +7 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-review.md +5 -3
- package/commands/pp-verify-work.md +1 -2
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update.js +0 -0
- package/hooks/dist/pp-cursor-session-start.js +1 -1
- package/hooks/dist/pp-statusline.js +1 -1
- package/hooks/dist/pp-update-banner.js +0 -0
- package/hooks/pp-check-update.js +0 -0
- package/hooks/pp-cursor-session-start.js +1 -1
- package/hooks/pp-statusline.js +1 -1
- package/hooks/pp-update-banner.js +0 -0
- package/package.json +14 -19
- package/pareto-skills/_shared/create-project-board.sh +0 -0
- package/pareto-skills/pp-project-setup/SKILL.md +1 -1
- package/pp-core/bin/check-latest-version.cjs +0 -0
- package/pp-core/bin/detect-build-commands.sh +2 -2
- package/pp-core/bin/lib/capability-activation.cjs +83 -9
- package/pp-core/bin/lib/capability-registry.cjs +396 -82
- package/pp-core/bin/lib/capability-state.cjs +160 -4
- package/pp-core/bin/lib/capability-writer.cjs +19 -5
- package/pp-core/bin/lib/check-command-router.cjs +3334 -946
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/command-aliases.cjs +0 -35
- package/pp-core/bin/lib/commands.cjs +9 -4
- package/pp-core/bin/lib/compose-bindings.cjs +251 -88
- package/pp-core/bin/lib/config-loader.cjs +7 -103
- package/pp-core/bin/lib/config-schema.cjs +1 -1
- package/pp-core/bin/lib/config.cjs +154 -62
- package/pp-core/bin/lib/decisions.cjs +4 -4
- package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
- package/pp-core/bin/lib/frontmatter.cjs +1 -38
- package/pp-core/bin/lib/git-base-branch.cjs +13 -11
- package/pp-core/bin/lib/git-provider.cjs +4 -4
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +648 -738
- package/pp-core/bin/lib/init-interactive.cjs +76 -4
- package/pp-core/bin/lib/init.cjs +24 -51
- package/pp-core/bin/lib/install-profiles.cjs +1 -1
- package/pp-core/bin/lib/io.cjs +8 -0
- package/pp-core/bin/lib/loop-resolver.cjs +189 -129
- package/pp-core/bin/lib/org-profile.cjs +469 -152
- package/pp-core/bin/lib/package-identity.cjs +3 -3
- package/pp-core/bin/lib/phase-command-router.cjs +1 -29
- package/pp-core/bin/lib/phase-scope.cjs +1 -1
- package/pp-core/bin/lib/phase.cjs +11 -253
- package/pp-core/bin/lib/platform-bindings.cjs +40 -19
- package/pp-core/bin/lib/practices-review-check.cjs +15 -9
- package/pp-core/bin/lib/profile-output.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
- package/pp-core/bin/lib/state.cjs +11 -85
- package/pp-core/bin/lib/template.cjs +1 -1
- package/pp-core/bin/lib/vault-audit.cjs +650 -126
- package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
- package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
- package/pp-core/bin/lib/vault-index.cjs +699 -0
- package/pp-core/bin/lib/vault-mode.cjs +235 -0
- package/pp-core/bin/lib/vault-practices.cjs +139 -72
- package/pp-core/bin/lib/vault-review.cjs +194 -451
- package/pp-core/bin/lib/vault-seal.cjs +1936 -0
- package/pp-core/bin/lib/vault-standards.cjs +1505 -0
- package/pp-core/bin/lib/vault-trace.cjs +607 -254
- package/pp-core/bin/lib/vault-update.cjs +9 -33
- package/pp-core/bin/lib/vault-write-back.cjs +70 -42
- package/pp-core/bin/lib/vault.cjs +2734 -920
- package/pp-core/bin/lib/verify.cjs +0 -11
- package/pp-core/bin/portable-timeout.sh +1 -1
- package/pp-core/bin/pp-tools.cjs +184 -54
- package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
- package/pp-core/bin/shared/config-schema.manifest.json +0 -3
- package/pp-core/bin/shared/model-catalog.json +2 -2
- package/pp-core/bin/verify-reapply-patches.cjs +0 -0
- package/pp-core/org-profile.json +26 -0
- package/pp-core/references/autonomous-best-effort.md +8 -7
- package/pp-core/references/loop-hook-dispatch.md +0 -25
- package/pp-core/references/planner-gap-closure.md +0 -1
- package/pp-core/references/planner-tdd.md +0 -1
- package/pp-core/references/planner-vault-practices.md +17 -34
- package/pp-core/references/planner-vault-standards.md +320 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/practice-arithmetic-contract.md +121 -0
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/references/verifier-practice-verdicts.md +263 -0
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +202 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- package/pp-core/templates/practices/README.md +20 -23
- package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
- package/pp-core/workflows/add-tests.md +55 -4
- package/pp-core/workflows/audit-fix.md +63 -2
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +85 -11
- package/pp-core/workflows/check-todos.md +13 -2
- package/pp-core/workflows/code-review-fix.md +59 -3
- package/pp-core/workflows/code-review.md +10 -1
- package/pp-core/workflows/complete-milestone.md +1 -1
- package/pp-core/workflows/debug.md +54 -4
- package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
- package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
- package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
- package/pp-core/workflows/discovery-sprint.md +7 -3
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
- package/pp-core/workflows/discovery-to-build.md +10 -2
- package/pp-core/workflows/discuss-phase.md +3 -10
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
- package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -20
- package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
- package/pp-core/workflows/execute-phase.md +155 -128
- package/pp-core/workflows/execute-plan.md +56 -6
- package/pp-core/workflows/fast.md +53 -4
- package/pp-core/workflows/help/modes/full.md +3 -3
- package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
- package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
- package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
- package/pp-core/workflows/init.md +232 -272
- package/pp-core/workflows/new-project.md +31 -23
- package/pp-core/workflows/plan-phase.md +98 -45
- package/pp-core/workflows/quick.md +117 -3
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +57 -28
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/update.md +3 -3
- package/pp-core/workflows/validate-phase.md +58 -3
- package/pp-core/workflows/vault-review.md +282 -22
- package/pp-core/workflows/verify-work.md +12 -14
- package/scripts/changeset/cli.cjs +6 -5
- package/scripts/changeset/lint.cjs +0 -0
- package/scripts/changeset/new.cjs +33 -50
- package/scripts/dev-env.cjs +25 -85
- package/scripts/gen-capability-registry.cjs +199 -11
- package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
- package/scripts/lib/allowlist-ratchet.cjs +1 -103
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
- package/scripts/lint-docs-required.cjs +0 -0
- package/scripts/lint-pr-check-project-dir.cjs +1 -3
- package/scripts/lint-test-file-count.allowlist.json +12 -10
- package/scripts/practice-arithmetic-contract.allow.json +35 -0
- package/scripts/prompt-injection-scan.sh +22 -17
- package/scripts/record-vault-mode-golden.cjs +89 -0
- package/scripts/release-channel.cjs +0 -0
- package/scripts/sync-manifest-versions.cjs +5 -4
- package/scripts/sync-rulesets.sh +1 -1
- package/scripts/update-size-baseline.cjs +30 -274
- package/scripts/vault-calibrate.cjs +996 -0
- package/scripts/verify-opengsd-sweep.cjs +3 -5
- package/scripts/workflow-size.cjs +9 -61
- package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
- package/pp-core/bin/lib/install-run.cjs +0 -536
- package/pp-core/bin/lib/loop-gates.cjs +0 -213
- package/pp-core/bin/lib/phase-manifest.cjs +0 -527
- package/pp-core/bin/lib/phase-tier.cjs +0 -480
- package/pp-core/bin/lib/plan-graph.cjs +0 -368
- package/pp-core/references/executor-vault-trace.md +0 -47
- package/pp-core/references/planner-plan-graph.md +0 -66
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
- package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
- package/scripts/lib/size-budget.cjs +0 -311
- package/scripts/lint-neutrality-names.cjs +0 -694
- package/scripts/lint-org-coordinates.allowlist.json +0 -35
- package/scripts/lint-org-coordinates.cjs +0 -426
|
@@ -1,116 +1,78 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
* invariant check, as code (milestone v1.11.0, Phase 34-07, gap closure).
|
|
5
|
-
*
|
|
6
|
-
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
7
|
-
* compiles to the git-ignored `pp-core/bin/lib/init-contract.cjs`; do NOT
|
|
8
|
-
* hand-edit the compiled output. Behaviour is locked by
|
|
9
|
-
* `tests/init-contract.test.cjs`.
|
|
10
|
-
*
|
|
11
|
-
* Born from a documented failure, not a speculative hardening. Two human
|
|
12
|
-
* UAT walks of `/pp-init` (`34-VALIDATION.md` § Manual-Only Verifications,
|
|
13
|
-
* `34-05-SUMMARY.md`) found that prose-driven load-bearing behaviour — a
|
|
14
|
-
* question set that must not shrink, a step that must not be skippable, a
|
|
15
|
-
* config key that must be set — is not reliably enforced by an executing
|
|
16
|
-
* agent re-interpreting Markdown, even after two escalating rounds of
|
|
17
|
-
* explicit in-file hardening. `buildInitQuestions` moves the canonical
|
|
18
|
-
* question set (and the provider option list, derived from `PROVIDERS`
|
|
19
|
-
* rather than retyped) out of prose into tested data; `verifyInitInvariants`
|
|
20
|
-
* inspects the RESULTING project state after init runs, rather than
|
|
21
|
-
* trusting that any one step announced itself — enforcement placed inside
|
|
22
|
-
* the step it protects does not constrain an agent that skips the step
|
|
23
|
-
* entirely (see `34-VALIDATION.md`'s diagnosis).
|
|
24
|
-
*
|
|
25
|
-
* Both functions are read-only and never throw. `verifyInitInvariants`
|
|
26
|
-
* calls `composeBindings` in offline mode to answer "was a composable
|
|
27
|
-
* binding ever produced" — this module therefore sits ABOVE
|
|
28
|
-
* `src/compose-bindings.cts` in the CommonJS graph. Nothing in
|
|
29
|
-
* `compose-bindings.cts`, `git-provider.cts` or `org-profile.cts` imports
|
|
30
|
-
* this module, at load time or lazily, so this introduces no require cycle.
|
|
31
|
-
*
|
|
32
|
-
* This module's own prose deliberately never names a concrete org profile
|
|
33
|
-
* package or client, matching the convention `src/org-profile.cts` and
|
|
34
|
-
* `src/compose-bindings.cts` both state in their own headers.
|
|
35
|
-
*
|
|
36
|
-
* Residual risk, stated plainly (see the plan's threat register): nothing
|
|
37
|
-
* here prevents an agent from declining to call this arm at all.
|
|
38
|
-
* `buildInitQuestions` makes the rendered content correct when called;
|
|
39
|
-
* `verifyInitInvariants` makes the omission provable afterwards, by
|
|
40
|
-
* inspecting state `init.md` cannot avoid leaving behind. It does not make
|
|
41
|
-
* step-skipping impossible.
|
|
42
|
-
*/
|
|
2
|
+
/** The `/pp-init` question set, session state machine and end-state invariant check, as tested code.
|
|
3
|
+
* Compiled to the git-ignored `pp-core/bin/lib/init-contract.cjs` — never hand-edit the output. */
|
|
43
4
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
44
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
45
6
|
};
|
|
46
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
-
exports.INIT_SESSION_WRITE_FAILED = exports.INIT_SESSION_MALFORMED = exports.INIT_CONTRACT_FLAGS = exports.INIT_ORG_PROFILE_DECLINED_KEY = exports.INIT_COMPOSE_DECLINED_KEY = void 0;
|
|
8
|
+
exports.INIT_SESSION_WRITE_FAILED = exports.INIT_SESSION_MALFORMED = exports.INIT_CONTRACT_FLAGS = exports.INIT_PROBE_STATUS = exports.INIT_PROBE_NOTE_KEY = exports.INIT_PROBE_WHO_KEY = exports.INIT_PROBE_WHEN_KEY = exports.INIT_PROBE_STATUS_KEY = exports.INIT_ORG_PROFILE_DECLINED_KEY = exports.INIT_COMPOSE_DECLINED_KEY = void 0;
|
|
9
|
+
exports.resolveVaultEnabledState = resolveVaultEnabledState;
|
|
10
|
+
exports.resolveVaultPracticesEnabledState = resolveVaultPracticesEnabledState;
|
|
11
|
+
exports.resolveVaultRequiredState = resolveVaultRequiredState;
|
|
12
|
+
exports.readVaultEnabledState = readVaultEnabledState;
|
|
13
|
+
exports.readVaultPracticesEnabledState = readVaultPracticesEnabledState;
|
|
14
|
+
exports.readVaultStandardsModeActive = readVaultStandardsModeActive;
|
|
15
|
+
exports.readVaultRequiredState = readVaultRequiredState;
|
|
48
16
|
exports.buildInitQuestions = buildInitQuestions;
|
|
17
|
+
exports.deriveSlug = deriveSlug;
|
|
18
|
+
exports.readProfileInitAnswers = readProfileInitAnswers;
|
|
49
19
|
exports.verifyInitInvariants = verifyInitInvariants;
|
|
20
|
+
exports.armVaultKeysWhereAbsent = armVaultKeysWhereAbsent;
|
|
21
|
+
exports.recordProbeOutcome = recordProbeOutcome;
|
|
22
|
+
exports.recordProbeDeferral = recordProbeDeferral;
|
|
50
23
|
exports.initSessionNext = initSessionNext;
|
|
51
24
|
exports.initSessionAnswer = initSessionAnswer;
|
|
52
25
|
exports.initSessionReset = initSessionReset;
|
|
53
|
-
exports.
|
|
26
|
+
exports.armVaultFromProfile = armVaultFromProfile;
|
|
54
27
|
exports.cmdInitContract = cmdInitContract;
|
|
55
28
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
56
29
|
const node_path_1 = __importDefault(require("node:path"));
|
|
57
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
31
|
const io = require("./io.cjs");
|
|
59
32
|
const { output } = io;
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const capabilityActivation = require("./capability-activation.cjs");
|
|
60
35
|
const git_provider_cjs_1 = require("./git-provider.cjs");
|
|
61
36
|
const org_profile_cjs_1 = require("./org-profile.cjs");
|
|
37
|
+
const platform_bindings_cjs_1 = require("./platform-bindings.cjs");
|
|
62
38
|
const compose_bindings_cjs_1 = require("./compose-bindings.cjs");
|
|
63
39
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
64
40
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
65
|
-
/**
|
|
66
|
-
* The dotted `.planning/config.json` key `/pp-init` Step 2.5 sets when the
|
|
67
|
-
* operator explicitly declines the compose-and-write confirmation. This is
|
|
68
|
-
* the ONLY signal that distinguishes "the operator was asked and said no"
|
|
69
|
-
* from "the step never ran at all" — both leave `.platform-bindings.json`
|
|
70
|
-
* absent, and `verifyInitInvariants` cannot tell them apart any other way
|
|
71
|
-
* (see `INVARIANT_CODE.COMPOSITION_NOT_RUN` below). Registered as a
|
|
72
|
-
* `vault` capability config key (`capabilities/vault/capability.json`) so
|
|
73
|
-
* `pp-tools config-set` accepts it without the "unknown config key" error
|
|
74
|
-
* a bare unregistered top-level namespace would trip.
|
|
75
|
-
*/
|
|
41
|
+
/** Set when the operator declines `compose_confirm` — the only way to tell "declined" from "never ran". */
|
|
76
42
|
exports.INIT_COMPOSE_DECLINED_KEY = 'init.compose_declined';
|
|
77
|
-
/**
|
|
78
|
-
* The dotted `.planning/config.json` key `pp-tools init` sets when the
|
|
79
|
-
* operator is offered a detected org profile and SKIPS it — a blank answer or
|
|
80
|
-
* `none` in any letter case. It is the only signal that tells "offered a
|
|
81
|
-
* detected profile and skipped it" from "the question never ran": both leave
|
|
82
|
-
* `org.profile` unset, and without it `verifyInitInvariants` reported every
|
|
83
|
-
* deliberate skip as `INIT_QUESTIONS_ORG_PROFILE_UNASKED`. Written by
|
|
84
|
-
* `initSessionAnswer` only when a profile was detectable (the predicate the
|
|
85
|
-
* violation reads), read back with strict boolean identity, and written back
|
|
86
|
-
* to `false` when a later run selects a profile. Registered under the
|
|
87
|
-
* `vault` capability so `config-set` accepts it. Kept OFF
|
|
88
|
-
* `ORG_PROFILE_ALLOWLIST`: a profile package must never answer the question
|
|
89
|
-
* about itself.
|
|
90
|
-
*/
|
|
43
|
+
/** Legacy upstream marker for a skipped org profile; nothing writes it any more, kept registered so old configs load. */
|
|
91
44
|
exports.INIT_ORG_PROFILE_DECLINED_KEY = 'init.org_profile_declined';
|
|
92
|
-
/** The
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
45
|
+
/** The /pp-init Step 2.6 reachability-proof record (status, when, who, note), read back by `--verify`.
|
|
46
|
+
* Registered vault config keys; never profile-settable and not for operator editing. */
|
|
47
|
+
exports.INIT_PROBE_STATUS_KEY = 'init.probe_status';
|
|
48
|
+
/** ISO-8601 UTC instant the record was written. See `INIT_PROBE_STATUS_KEY`. */
|
|
49
|
+
exports.INIT_PROBE_WHEN_KEY = 'init.probe_when';
|
|
50
|
+
/** Who recorded the proof; never empty (falls back to the literal `unknown`). */
|
|
51
|
+
exports.INIT_PROBE_WHO_KEY = 'init.probe_who';
|
|
52
|
+
/** The bounded human note: the probe's own summary on `passed`/`failed`, the
|
|
53
|
+
* operator's stated reason on `deferred`. See `INIT_PROBE_STATUS_KEY`. */
|
|
54
|
+
exports.INIT_PROBE_NOTE_KEY = 'init.probe_note';
|
|
55
|
+
/** Probe status vocabulary; `deferred` is only recordable through `recordProbeDeferral`, which requires a reason. */
|
|
56
|
+
exports.INIT_PROBE_STATUS = Object.freeze({
|
|
57
|
+
PASSED: 'passed',
|
|
58
|
+
FAILED: 'failed',
|
|
59
|
+
DEFERRED: 'deferred',
|
|
60
|
+
});
|
|
61
|
+
/** Provider choices that only record identity, appended after `PROVIDERS` (never a retyped copy of it). */
|
|
62
|
+
const IDENTITY_ONLY_PROVIDER_OPTIONS = Object.freeze(['other', 'none']);
|
|
100
63
|
const INVARIANT_CODE = Object.freeze({
|
|
101
|
-
ORG_PROFILE_UNASKED: 'INIT_QUESTIONS_ORG_PROFILE_UNASKED',
|
|
102
64
|
COMPOSITION_NOT_RUN: 'INIT_COMPOSITION_NOT_RUN',
|
|
103
65
|
VAULT_PROVIDER_UNSET: 'INIT_VAULT_PROVIDER_UNSET',
|
|
104
66
|
IDENTITY_MISSING: 'INIT_IDENTITY_MISSING',
|
|
67
|
+
/** Reachability proof never run, failed, or consciously deferred — three states that need three different responses. */
|
|
68
|
+
PROBE_NOT_RUN: 'INIT_PROBE_NOT_RUN',
|
|
69
|
+
PROBE_FAILED: 'INIT_PROBE_FAILED',
|
|
70
|
+
PROBE_DEFERRED: 'INIT_PROBE_DEFERRED',
|
|
71
|
+
/** `vault.enabled` is present but not a JSON boolean, so the activation resolver and the proof check disagree. */
|
|
72
|
+
VAULT_ENABLED_MALFORMED: 'INIT_VAULT_ENABLED_MALFORMED',
|
|
105
73
|
});
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
* `COMPOSE_BINDINGS_FLAGS`). Extended by Task 2 (34-08 gap closure) with the
|
|
109
|
-
* session relay flags — `--session-answer` additionally consumes the next
|
|
110
|
-
* argv token as its `<stepId>=<value>` payload (parsed in `pp-tools.cjs`,
|
|
111
|
-
* mirroring the `--cwd`/`--pick` convention), and `--offline` forwards to
|
|
112
|
-
* every `composeBindings` call the session makes so a CLI-driven loop can
|
|
113
|
-
* run with no network access — parity with `compose-bindings --offline`. */
|
|
74
|
+
/** Flags the `init-contract` CLI arm accepts; `--session-answer`, `--record-probe` and `--defer-probe` take the next token as payload.
|
|
75
|
+
* `--arm-vault` (quick 261003-kmd) is the non-interactive compose-and-arm `/pp-new-project` calls. */
|
|
114
76
|
exports.INIT_CONTRACT_FLAGS = Object.freeze([
|
|
115
77
|
'--questions',
|
|
116
78
|
'--verify',
|
|
@@ -118,39 +80,21 @@ exports.INIT_CONTRACT_FLAGS = Object.freeze([
|
|
|
118
80
|
'--session-answer',
|
|
119
81
|
'--session-reset',
|
|
120
82
|
'--offline',
|
|
83
|
+
'--record-probe',
|
|
84
|
+
'--defer-probe',
|
|
121
85
|
'--arm-vault',
|
|
122
86
|
]);
|
|
123
|
-
/**
|
|
124
|
-
* (Artifacts table, 34-08). Never hand-edited; malformed content is
|
|
125
|
-
* refused, not silently repaired (T-34-63). */
|
|
87
|
+
/** CLI-owned session state file; malformed content is refused, never repaired. */
|
|
126
88
|
const INIT_SESSION_FILE_NAME = '.init-session.json';
|
|
127
|
-
/**
|
|
128
|
-
* session file exists but does not parse as valid session state — the ONLY
|
|
129
|
-
* recovery is `--session-reset` (T-34-63); this module never resumes a
|
|
130
|
-
* malformed file into a guessed-at state. */
|
|
89
|
+
/** Reported when the session file is malformed; the only recovery is `--session-reset`. */
|
|
131
90
|
exports.INIT_SESSION_MALFORMED = 'INIT_SESSION_MALFORMED';
|
|
132
|
-
/**
|
|
133
|
-
* report when a filesystem write this module performs (`persistSession`,
|
|
134
|
-
* `writeConfigMerge`, the write-mode `composeBindings` call) fails — e.g. an
|
|
135
|
-
* unwritable `.planning` directory (permissions, a read-only mount, a full
|
|
136
|
-
* disk). Every sibling module in this seam (`composeBindings`,
|
|
137
|
-
* `resolveOrgProfileDocument`, `resolveOrgProfile`, `verifyInitInvariants`)
|
|
138
|
-
* never throws and degrades to a bounded data verdict instead (D-09); this
|
|
139
|
-
* module previously did not follow that convention on its write paths and
|
|
140
|
-
* let the exception escape uncaught. Recovery is to fix the underlying
|
|
141
|
-
* filesystem issue and retry the same step — nothing is left half-recorded,
|
|
142
|
-
* since the throw is caught before `session` is persisted. */
|
|
91
|
+
/** Reported when a session write fails (e.g. an unwritable `.planning`); fix the cause and retry the same step. */
|
|
143
92
|
exports.INIT_SESSION_WRITE_FAILED = 'INIT_SESSION_WRITE_FAILED';
|
|
144
|
-
/** compose_confirm
|
|
145
|
-
* relaying through `AskUserQuestion`/TEXT_MODE can answer naturally; every
|
|
146
|
-
* other string is rejected (never silently treated as a decline). */
|
|
93
|
+
/** `compose_confirm` answers; anything else is rejected, never treated as a decline. */
|
|
147
94
|
const YES_VALUES = Object.freeze(['yes', 'y', 'approve', 'approved', 'true']);
|
|
148
95
|
const NO_VALUES = Object.freeze(['no', 'n', 'decline', 'declined', 'false']);
|
|
149
|
-
/** Maps each of the 7 identity/provider/visibility question ids
|
|
150
|
-
*
|
|
151
|
-
* `initSessionAnswer` — so the very next question, `provider`, can
|
|
152
|
-
* re-resolve its `preselected` against it) to the short key it is written
|
|
153
|
-
* under inside `.planning/config.json`'s `project` block. */
|
|
96
|
+
/** Maps each of the 7 identity/provider/visibility question ids to the short
|
|
97
|
+
* key it is written under inside `.planning/config.json`'s `project` block. */
|
|
154
98
|
const PROJECT_BLOCK_QUESTION_MAP = Object.freeze({
|
|
155
99
|
project_name: 'name',
|
|
156
100
|
project_slug: 'slug',
|
|
@@ -160,9 +104,7 @@ const PROJECT_BLOCK_QUESTION_MAP = Object.freeze({
|
|
|
160
104
|
provider: 'provider',
|
|
161
105
|
visibility: 'visibility',
|
|
162
106
|
});
|
|
163
|
-
// ─── Small local helpers (
|
|
164
|
-
// established convention for small, cycle-risk-free helpers; see
|
|
165
|
-
// `src/git-provider.cts`'s `cfgGet` for the canonical example) ────────────────
|
|
107
|
+
// ─── Small local helpers (duplicated per module to avoid require cycles) ──────────
|
|
166
108
|
function isObject(v) {
|
|
167
109
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
168
110
|
}
|
|
@@ -172,35 +114,143 @@ function isUsableString(v) {
|
|
|
172
114
|
function getDotted(obj, dottedPath) {
|
|
173
115
|
return dottedPath.split('.').reduce((cur, key) => (isObject(cur) ? cur[key] : undefined), obj);
|
|
174
116
|
}
|
|
175
|
-
|
|
117
|
+
/** Arms the vault capability; written by the session machine, never by an org profile. */
|
|
118
|
+
const VAULT_ENABLED_CONFIG_KEY = 'vault.enabled';
|
|
119
|
+
/** Arms vault-practices; set beside `vault.enabled` under the same absent-only rule, inert without it. */
|
|
120
|
+
const VAULT_PRACTICES_ENABLED_CONFIG_KEY = 'vault.practices_enabled';
|
|
121
|
+
/** The single reading of `vault.enabled` in this module, in the form the gate honours
|
|
122
|
+
* (`resolveGateHonouredKeyState`, Phase 15 D-32). `value` is the observed value, never a
|
|
123
|
+
* coercion, so reports name what is really in the file. */
|
|
124
|
+
function resolveVaultEnabledState(parsed) {
|
|
125
|
+
return resolveGateHonouredKeyState(parsed, VAULT_ENABLED_CONFIG_KEY);
|
|
126
|
+
}
|
|
127
|
+
/** Four-state reading of `vault.practices_enabled`, like `resolveVaultEnabledState`. */
|
|
128
|
+
function resolveVaultPracticesEnabledState(parsed) {
|
|
129
|
+
return resolveGateHonouredKeyState(parsed, VAULT_PRACTICES_ENABLED_CONFIG_KEY);
|
|
130
|
+
}
|
|
131
|
+
/** Four-state reading of the project's OWN `vault.required` (not the profile-layered gate value),
|
|
132
|
+
* so the arming guard respects an explicit per-project `false`. */
|
|
133
|
+
function resolveVaultRequiredState(parsed) {
|
|
134
|
+
return resolveBooleanKeyState(parsed, org_profile_cjs_1.VAULT_REQUIRED_CONFIG_KEY);
|
|
135
|
+
}
|
|
136
|
+
/** Classify a PRESENT value into the three present states. */
|
|
137
|
+
function classifyPresentValue(value) {
|
|
138
|
+
if (value === true)
|
|
139
|
+
return { state: 'armed', value };
|
|
140
|
+
if (value === false)
|
|
141
|
+
return { state: 'off', value };
|
|
142
|
+
return { state: 'malformed', value };
|
|
143
|
+
}
|
|
144
|
+
/** Either-form four-state reader. Used for `vault.required`, whose gate-side ladder
|
|
145
|
+
* (`resolveVaultRequired`, `src/org-profile.cts`) reads both key forms. */
|
|
146
|
+
function resolveBooleanKeyState(parsed, key) {
|
|
147
|
+
const hit = capabilityActivation.getConfigValueEitherForm(parsed, key);
|
|
148
|
+
if (hit.found !== true)
|
|
149
|
+
return { state: 'absent', value: undefined };
|
|
150
|
+
return classifyPresentValue(hit.value);
|
|
151
|
+
}
|
|
176
152
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
153
|
+
* Four-state reading of a vault switch whose gates read ONLY the nested form (Phase 15
|
|
154
|
+
* D-32): `vault.enabled` and `vault.practices_enabled`. `loadConfig` materialises their
|
|
155
|
+
* nested defaults, so the gates' evaluator answers at its first level and never reaches a
|
|
156
|
+
* hand-written FLAT key (it warns that the key is ignored). Reading the flat form as
|
|
157
|
+
* `armed` therefore told /pp-init that a switch was on while no gate rendered. The
|
|
158
|
+
* absent-only arming then skipped it, and the done line promised governance nobody
|
|
159
|
+
* enforced (todo init-practices-reader-reads-flat-form-as-armed).
|
|
184
160
|
*
|
|
185
|
-
*
|
|
186
|
-
* `
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
161
|
+
* - nested present: its value decides, exactly as before;
|
|
162
|
+
* - flat-only `true`: `absent`, because the gate sees nothing. The arming writes the
|
|
163
|
+
* canonical nested key, which is what the operator asked for;
|
|
164
|
+
* - flat-only `false`: `off`. The gate reads off too, and an explicit false is never
|
|
165
|
+
* overwritten (07-09 WR-07);
|
|
166
|
+
* - flat-only anything else: `malformed`, kept and reported, never repaired.
|
|
167
|
+
*/
|
|
168
|
+
function resolveGateHonouredKeyState(parsed, key) {
|
|
169
|
+
const nested = capabilityActivation._getNestedConfigValue(parsed, key);
|
|
170
|
+
if (nested.found === true)
|
|
171
|
+
return classifyPresentValue(nested.value);
|
|
172
|
+
const flat = capabilityActivation._getFlatConfigValue(parsed, key);
|
|
173
|
+
if (flat.found !== true || flat.value === true)
|
|
174
|
+
return { state: 'absent', value: undefined };
|
|
175
|
+
return classifyPresentValue(flat.value);
|
|
176
|
+
}
|
|
177
|
+
/** The vault adapter's gate-side readers, required lazily: pp-tools loads this module on
|
|
178
|
+
* every command and the vault adapter is large. vault.cjs never requires this module, so
|
|
179
|
+
* there is no cycle. */
|
|
180
|
+
function vaultGateReaders() {
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
182
|
+
const vault = require('./vault.cjs');
|
|
183
|
+
return vault;
|
|
184
|
+
}
|
|
185
|
+
/** Ask one gate-side reader; any throw (including the require) reads as OFF. */
|
|
186
|
+
function gateReadsOn(read) {
|
|
187
|
+
try {
|
|
188
|
+
return read(vaultGateReaders()) === true;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The project-at-`cwd` reading the done line prints from (Phase 15 D-32): `armed` exactly
|
|
196
|
+
* when the gate-side reader is ON, so the line can never name a switch the gates do not
|
|
197
|
+
* honour. The root file's own four-state reading supplies everything else.
|
|
198
|
+
* - gate ON, root armed → the root reading;
|
|
199
|
+
* - gate ON, root not armed → `armed` (another config layer, a workstream config, arms
|
|
200
|
+
* it; `value` is the gate's reading, `true`);
|
|
201
|
+
* - gate OFF, root armed → `off` (a workstream config overrides it, or the active
|
|
202
|
+
* workstream has no config); `value` stays the root's;
|
|
203
|
+
* - gate OFF otherwise → the root reading (absent, off or malformed).
|
|
202
204
|
*/
|
|
203
|
-
function
|
|
205
|
+
function agreeWithGate(root, gateOn) {
|
|
206
|
+
if (gateOn)
|
|
207
|
+
return root.state === 'armed' ? root : { state: 'armed', value: true };
|
|
208
|
+
return root.state === 'armed' ? { state: 'off', value: root.value } : root;
|
|
209
|
+
}
|
|
210
|
+
/** The root config file's four-state reading through `resolve`; unreadable reads as `absent`. */
|
|
211
|
+
function readRootKeyState(cwd, resolve) {
|
|
212
|
+
try {
|
|
213
|
+
const { parsed } = readConfigFile(cwd);
|
|
214
|
+
return resolve(parsed);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return { state: 'absent', value: undefined };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** `vault.enabled` for the project at `cwd`: `armed` exactly when `isVaultGateActive` is
|
|
221
|
+
* (`agreeWithGate`); an unreadable config reads as `absent`. Never throws. */
|
|
222
|
+
function readVaultEnabledState(cwd) {
|
|
223
|
+
return agreeWithGate(readRootKeyState(cwd, resolveVaultEnabledState), gateReadsOn((v) => v.isVaultGateActive(cwd)));
|
|
224
|
+
}
|
|
225
|
+
/** `vault.practices_enabled` for the project at `cwd`: `armed` exactly when
|
|
226
|
+
* `isPracticesGateActive`, the one practices-switch reader (15-02), is ON. Never throws. */
|
|
227
|
+
function readVaultPracticesEnabledState(cwd) {
|
|
228
|
+
return agreeWithGate(readRootKeyState(cwd, resolveVaultPracticesEnabledState), gateReadsOn((v) => v.isPracticesGateActive(cwd)));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Whether the project at `cwd` is in standards mode, asked of the one mode owner
|
|
232
|
+
* (`isStandardsModeActive`, which honours the org-profile ladder). The /pp-init done line
|
|
233
|
+
* uses it so that, in standards mode, it names the standards record the `vault.trace`
|
|
234
|
+
* gate checks instead of practice recording, which standards mode does not do
|
|
235
|
+
* (15-REVIEW WR-09). Any throw reads as not standards. Never throws.
|
|
236
|
+
*/
|
|
237
|
+
function readVaultStandardsModeActive(cwd) {
|
|
238
|
+
return gateReadsOn((v) => v.isStandardsModeActive(cwd));
|
|
239
|
+
}
|
|
240
|
+
/** `vault.required` for the project at `cwd`: `armed` exactly when the load gate's own
|
|
241
|
+
* answer (`resolveVaultRequiredFlag`) is true. Never throws. */
|
|
242
|
+
function readVaultRequiredState(cwd) {
|
|
243
|
+
return agreeWithGate(readRootKeyState(cwd, resolveVaultRequiredState), gateReadsOn((v) => v.resolveVaultRequiredFlag(cwd)));
|
|
244
|
+
}
|
|
245
|
+
/** True only for the JSON boolean `true` — a truthy string does not count.
|
|
246
|
+
* A non-boolean value is reported by `verifyInitInvariants` as `INIT_VAULT_ENABLED_MALFORMED`. */
|
|
247
|
+
function isVaultEnabledInConfig(parsed) {
|
|
248
|
+
return resolveVaultEnabledState(parsed).state === 'armed';
|
|
249
|
+
}
|
|
250
|
+
// ─── buildInitQuestions ────────────────────────────────────────────────────
|
|
251
|
+
/** The canonical /pp-init question set, as data; the session may answer some itself (see "Automatic answers").
|
|
252
|
+
* No org-profile question in this fork (PBI 74316); provider options come from `PROVIDERS`, preselected via `resolveProvider`. */
|
|
253
|
+
function buildInitQuestions(cwd) {
|
|
204
254
|
let preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
|
|
205
255
|
try {
|
|
206
256
|
preselected = (0, git_provider_cjs_1.resolveProvider)(cwd).provider;
|
|
@@ -209,31 +259,6 @@ function buildInitQuestions(cwd, opts = {}) {
|
|
|
209
259
|
preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
|
|
210
260
|
}
|
|
211
261
|
const providerOptions = Object.freeze([...git_provider_cjs_1.PROVIDERS, ...IDENTITY_ONLY_PROVIDER_OPTIONS]);
|
|
212
|
-
const detectedOrgProfile = resolveDetectedOrgProfile(cwd, opts);
|
|
213
|
-
// The template example is written as an angle-bracket placeholder, not a
|
|
214
|
-
// literal package name. `tests/org-profile.test.cjs`'s client-confidentiality
|
|
215
|
-
// guard scans this file for any concrete scope-slash-org-prefix package
|
|
216
|
-
// identifier, so even a generic literal placeholder trips it — the template
|
|
217
|
-
// keeps the naming convention visible while staying clean, and the guard
|
|
218
|
-
// stays maximally strict, no allowlist entry was added for it. The detected
|
|
219
|
-
// value itself is never a literal in this file — it is a runtime string
|
|
220
|
-
// read from an installed package, interpolated into the prompt below.
|
|
221
|
-
// A detected profile is a HINT, never a default: no `preselected`, so a
|
|
222
|
-
// profile installed on the operator's machine (perhaps another client's)
|
|
223
|
-
// never becomes a project's binding by pressing Enter. Enter skips (and
|
|
224
|
-
// `initSessionAnswer` records the skip); typing the name selects it.
|
|
225
|
-
const orgProfileQuestion = isUsableString(detectedOrgProfile)
|
|
226
|
-
? {
|
|
227
|
-
id: 'org_profile',
|
|
228
|
-
prompt: `Org profile package (optional — "${detectedOrgProfile}" is installed: type its name to use it, ` +
|
|
229
|
-
'or press Enter to skip)',
|
|
230
|
-
optionalAnswer: true,
|
|
231
|
-
}
|
|
232
|
-
: {
|
|
233
|
-
id: 'org_profile',
|
|
234
|
-
prompt: 'Org profile package (optional — press Enter to skip, e.g. "@<your-scope>/org-<name>")',
|
|
235
|
-
optionalAnswer: true,
|
|
236
|
-
};
|
|
237
262
|
return [
|
|
238
263
|
{ id: 'project_name', prompt: 'Project name (human-readable, e.g. "Acme Dashboard")', optionalAnswer: false },
|
|
239
264
|
{ id: 'project_slug', prompt: 'Project slug (kebab-case, e.g. "acme-dashboard")', optionalAnswer: false },
|
|
@@ -248,7 +273,6 @@ function buildInitQuestions(cwd, opts = {}) {
|
|
|
248
273
|
optionalAnswer: false,
|
|
249
274
|
},
|
|
250
275
|
{ id: 'description', prompt: 'One-line description (what this project does)', optionalAnswer: false },
|
|
251
|
-
orgProfileQuestion,
|
|
252
276
|
{
|
|
253
277
|
id: 'provider',
|
|
254
278
|
prompt: 'Provider (gitlab / azure / github / other / none)',
|
|
@@ -260,179 +284,88 @@ function buildInitQuestions(cwd, opts = {}) {
|
|
|
260
284
|
{ id: 'visibility', prompt: 'Visibility (private / public / internal)', optionalAnswer: false },
|
|
261
285
|
];
|
|
262
286
|
}
|
|
263
|
-
// ───
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
* one level inside an `@scope/` directory), collecting the name of every
|
|
274
|
-
* package directory carrying `ORG_PROFILE_DATA_FILE` at its root instead of
|
|
275
|
-
* stopping at the first hit. `opts.limit`, when set, stops the walk once
|
|
276
|
-
* that many names have been collected — passing `1` reproduces the boolean
|
|
277
|
-
* check's early-exit behaviour exactly. Bounded at `NODE_MODULES_SCAN_CAP`
|
|
278
|
-
* total directory entries visited (T-34-54: this check must never hang on a
|
|
279
|
-
* pathological `node_modules` tree).
|
|
280
|
-
*
|
|
281
|
-
* This is a heuristic, not a resolution: it answers "a package shaped like
|
|
282
|
-
* an org profile is installed here", not "this exact profile would
|
|
283
|
-
* resolve" — callers never claim more than that.
|
|
284
|
-
*/
|
|
285
|
-
function collectOrgProfileShapedPackages(nodeModulesDir, opts = {}) {
|
|
286
|
-
const names = [];
|
|
287
|
-
let visited = 0;
|
|
288
|
-
const limitReached = () => opts.limit !== undefined && names.length >= opts.limit;
|
|
289
|
-
const addIfShaped = (name) => {
|
|
290
|
-
if (NPM_PACKAGE_NAME_SHAPE_RE.test(name))
|
|
291
|
-
names.push(name);
|
|
292
|
-
};
|
|
293
|
-
let entries;
|
|
294
|
-
try {
|
|
295
|
-
entries = node_fs_1.default.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
return { names, capped: false };
|
|
299
|
-
}
|
|
300
|
-
for (const entry of entries) {
|
|
301
|
-
if (limitReached())
|
|
302
|
-
return { names, capped: false };
|
|
303
|
-
if (visited >= NODE_MODULES_SCAN_CAP)
|
|
304
|
-
return { names, capped: true };
|
|
305
|
-
visited += 1;
|
|
306
|
-
if (!entry.isDirectory())
|
|
307
|
-
continue;
|
|
308
|
-
if (entry.name.startsWith('@')) {
|
|
309
|
-
const scopeDir = node_path_1.default.join(nodeModulesDir, entry.name);
|
|
310
|
-
let scoped;
|
|
311
|
-
try {
|
|
312
|
-
scoped = node_fs_1.default.readdirSync(scopeDir, { withFileTypes: true });
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
for (const scopedEntry of scoped) {
|
|
318
|
-
if (limitReached())
|
|
319
|
-
return { names, capped: false };
|
|
320
|
-
if (visited >= NODE_MODULES_SCAN_CAP)
|
|
321
|
-
return { names, capped: true };
|
|
322
|
-
visited += 1;
|
|
323
|
-
if (!scopedEntry.isDirectory())
|
|
324
|
-
continue;
|
|
325
|
-
try {
|
|
326
|
-
if (node_fs_1.default.existsSync(node_path_1.default.join(scopeDir, scopedEntry.name, org_profile_cjs_1.ORG_PROFILE_DATA_FILE))) {
|
|
327
|
-
addIfShaped(`${entry.name}/${scopedEntry.name}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
catch {
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
try {
|
|
337
|
-
if (node_fs_1.default.existsSync(node_path_1.default.join(nodeModulesDir, entry.name, org_profile_cjs_1.ORG_PROFILE_DATA_FILE))) {
|
|
338
|
-
addIfShaped(entry.name);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
287
|
+
// ─── Automatic answers (PBI 74299): an org profile init section fixes answers and can derive slugs ───
|
|
288
|
+
/** The visibility answers the question accepts. */
|
|
289
|
+
const VISIBILITY_OPTIONS = Object.freeze(['private', 'public', 'internal']);
|
|
290
|
+
/** Longest client name an org profile may fix — the composer's seed-value bound. */
|
|
291
|
+
const INIT_FIXED_VALUE_MAX_LENGTH = 200;
|
|
292
|
+
function hasControlChar(value) {
|
|
293
|
+
for (let i = 0; i < value.length; i++) {
|
|
294
|
+
const c = value.charCodeAt(i);
|
|
295
|
+
if (c <= 0x1f || c === 0x7f)
|
|
296
|
+
return true;
|
|
345
297
|
}
|
|
346
|
-
return
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* True when `nodeModulesDir` contains at least one package directory (one
|
|
350
|
-
* level, or one level inside an `@scope/` directory) carrying a file named
|
|
351
|
-
* `ORG_PROFILE_DATA_FILE` at its root — the same fixed filename
|
|
352
|
-
* `resolveOrgProfileDocument` reads, imported rather than retyped. Reduced to
|
|
353
|
-
* the collecting walk above with `limit: 1`, preserving its early-exit
|
|
354
|
-
* behaviour exactly.
|
|
355
|
-
*/
|
|
356
|
-
function containsOrgProfileShapedPackage(nodeModulesDir) {
|
|
357
|
-
return collectOrgProfileShapedPackages(nodeModulesDir, { limit: 1 }).names.length > 0;
|
|
298
|
+
return false;
|
|
358
299
|
}
|
|
359
|
-
/**
|
|
360
|
-
*
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return
|
|
300
|
+
/** Derive a slug: diacritics stripped, lowercased, other characters collapsed to "-".
|
|
301
|
+
* Returns `null` when the result fails `SLUG_RE`, so the slug is asked instead. */
|
|
302
|
+
function deriveSlug(name) {
|
|
303
|
+
const slug = name
|
|
304
|
+
.normalize('NFKD')
|
|
305
|
+
.replace(/[̀-ͯ]/g, '')
|
|
306
|
+
.toLowerCase()
|
|
307
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
308
|
+
.replace(/^-+|-+$/g, '');
|
|
309
|
+
return slug !== '' && platform_bindings_cjs_1.SLUG_RE.test(slug) ? slug : null;
|
|
369
310
|
}
|
|
370
|
-
|
|
311
|
+
/** The validated init section of the org profile; invalid values are dropped so their question is asked. Never throws. */
|
|
312
|
+
function readProfileInitAnswers(cwd, opts = {}) {
|
|
313
|
+
const none = { fixed: {}, deriveSlugs: false };
|
|
371
314
|
try {
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
return
|
|
315
|
+
const doc = (0, org_profile_cjs_1.resolveOrgProfileDocument)(cwd, { _globalRoot: opts._globalRoot });
|
|
316
|
+
if (!doc.resolved)
|
|
317
|
+
return none;
|
|
318
|
+
const section = doc.document[org_profile_cjs_1.ORG_PROFILE_INIT_SECTION];
|
|
319
|
+
if (!isObject(section))
|
|
320
|
+
return none;
|
|
321
|
+
const fixed = {};
|
|
322
|
+
const clientName = section.client_name;
|
|
323
|
+
if (typeof clientName === 'string' &&
|
|
324
|
+
clientName.trim() !== '' &&
|
|
325
|
+
clientName.length <= INIT_FIXED_VALUE_MAX_LENGTH &&
|
|
326
|
+
!hasControlChar(clientName)) {
|
|
327
|
+
fixed.client_name = clientName.trim();
|
|
375
328
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
329
|
+
const providerOptions = [...git_provider_cjs_1.PROVIDERS, ...IDENTITY_ONLY_PROVIDER_OPTIONS];
|
|
330
|
+
if (typeof section.provider === 'string' && providerOptions.includes(section.provider.trim())) {
|
|
331
|
+
fixed.provider = section.provider.trim();
|
|
380
332
|
}
|
|
381
|
-
|
|
333
|
+
if (typeof section.visibility === 'string' && VISIBILITY_OPTIONS.includes(section.visibility.trim())) {
|
|
334
|
+
fixed.visibility = section.visibility.trim();
|
|
335
|
+
}
|
|
336
|
+
return { fixed, deriveSlugs: section.derive_slugs === true };
|
|
382
337
|
}
|
|
383
338
|
catch {
|
|
384
|
-
return
|
|
339
|
+
return none;
|
|
385
340
|
}
|
|
386
341
|
}
|
|
387
|
-
/**
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
* both roots, and returns that single name only when exactly one distinct
|
|
393
|
-
* name was found and NEITHER scan reported `capped`. A capped scan is
|
|
394
|
-
* inconclusive and must never yield a default (T-QP-03). Two or more
|
|
395
|
-
* distinct names, or none, returns null. Never throws.
|
|
396
|
-
*/
|
|
397
|
-
function resolveDetectedOrgProfile(cwd, opts) {
|
|
398
|
-
try {
|
|
399
|
-
const names = new Set();
|
|
400
|
-
let anyCapped = false;
|
|
401
|
-
const projectNodeModules = node_path_1.default.join(cwd, 'node_modules');
|
|
402
|
-
if (node_fs_1.default.existsSync(projectNodeModules)) {
|
|
403
|
-
const projectResult = collectOrgProfileShapedPackages(projectNodeModules);
|
|
404
|
-
if (projectResult.capped)
|
|
405
|
-
anyCapped = true;
|
|
406
|
-
for (const name of projectResult.names)
|
|
407
|
-
names.add(name);
|
|
408
|
-
}
|
|
409
|
-
for (const globalRoot of detectionGlobalRoots(opts)) {
|
|
410
|
-
const globalResult = collectOrgProfileShapedPackages(globalRoot);
|
|
411
|
-
if (globalResult.capped)
|
|
412
|
-
anyCapped = true;
|
|
413
|
-
for (const name of globalResult.names)
|
|
414
|
-
names.add(name);
|
|
415
|
-
}
|
|
416
|
-
if (anyCapped)
|
|
417
|
-
return null;
|
|
418
|
-
return names.size === 1 ? [...names][0] : null;
|
|
342
|
+
/** The answer the session fills in itself for question `id`, or `null` to ask it. */
|
|
343
|
+
function autoAnswerFor(id, answers, init) {
|
|
344
|
+
if (id === 'client_name' || id === 'provider' || id === 'visibility') {
|
|
345
|
+
const value = init.fixed[id];
|
|
346
|
+
return value === undefined ? null : { value, source: 'org-profile' };
|
|
419
347
|
}
|
|
420
|
-
|
|
421
|
-
|
|
348
|
+
if (init.deriveSlugs && (id === 'project_slug' || id === 'client_slug')) {
|
|
349
|
+
const name = answers[id === 'project_slug' ? 'project_name' : 'client_name'];
|
|
350
|
+
const slug = typeof name === 'string' ? deriveSlug(name) : null;
|
|
351
|
+
return slug === null ? null : { value: slug, source: 'derived' };
|
|
422
352
|
}
|
|
353
|
+
return null;
|
|
423
354
|
}
|
|
424
|
-
/** The
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
355
|
+
/** The session's automatic answers, in question order. */
|
|
356
|
+
function listAutoAnswers(cwd, session) {
|
|
357
|
+
const sources = session.autoAnswers ?? {};
|
|
358
|
+
const out = [];
|
|
359
|
+
for (const q of buildInitQuestions(cwd)) {
|
|
360
|
+
const source = sources[q.id];
|
|
361
|
+
const value = session.answers[q.id];
|
|
362
|
+
if (source !== undefined && typeof value === 'string')
|
|
363
|
+
out.push({ id: q.id, value, source });
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
431
366
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
* on the latter (see `verifyInitInvariants`'s own doc comment for why an
|
|
435
|
-
* absent file must never trip it). Never throws. */
|
|
367
|
+
// ─── verifyInitInvariants ───────────────────────────────────────────────────
|
|
368
|
+
/** Read `.planning/config.json`, telling "absent" from "present but malformed". Never throws. */
|
|
436
369
|
function readConfigFile(cwd) {
|
|
437
370
|
const configPath = node_path_1.default.join(cwd, '.planning', 'config.json');
|
|
438
371
|
let exists = false;
|
|
@@ -451,26 +384,8 @@ function readConfigFile(cwd) {
|
|
|
451
384
|
return { exists, parsed: {} };
|
|
452
385
|
}
|
|
453
386
|
}
|
|
454
|
-
/**
|
|
455
|
-
*
|
|
456
|
-
* and report every named invariant it violates. Read-only: no filesystem
|
|
457
|
-
* write anywhere in this function or in the `composeBindings` call it
|
|
458
|
-
* makes (always `offline: true`, `write` omitted so it defaults to
|
|
459
|
-
* `false`). Never throws — each check below is independently guarded so
|
|
460
|
-
* one failing check cannot suppress the others; there is no outer
|
|
461
|
-
* catch-all because none is needed (see the design note below).
|
|
462
|
-
*
|
|
463
|
-
* An empty directory — nothing in `.planning/`, no `.platform-bindings.json`
|
|
464
|
-
* — returns `{ ok: true, violations: [] }`. Every check requires some
|
|
465
|
-
* positive signal that a partial init was attempted (a config file present
|
|
466
|
-
* for `INIT_IDENTITY_MISSING`; a "gitlab"/"azure" `project.provider` for
|
|
467
|
-
* `INIT_VAULT_PROVIDER_UNSET`; an installed org-profile-shaped package for
|
|
468
|
-
* `INIT_QUESTIONS_ORG_PROFILE_UNASKED`; a `composable: true` dry run for
|
|
469
|
-
* `INIT_COMPOSITION_NOT_RUN`) — an empty directory carries none of them, so
|
|
470
|
-
* nothing is reported as skipped. This is deliberate (see the module
|
|
471
|
-
* header): nothing was attempted, so nothing can be reported as attempted-
|
|
472
|
-
* and-abandoned.
|
|
473
|
-
*/
|
|
387
|
+
/** Report every invariant the project's state at `cwd` violates. Read-only and never throws;
|
|
388
|
+
* each check needs a positive signal that init was attempted, so an empty directory is ok. */
|
|
474
389
|
function verifyInitInvariants(cwd, opts = {}) {
|
|
475
390
|
const violations = [];
|
|
476
391
|
const { exists: configExists, parsed } = readConfigFile(cwd);
|
|
@@ -491,14 +406,11 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
491
406
|
catch {
|
|
492
407
|
// fail open — one check's fault must not suppress the others
|
|
493
408
|
}
|
|
494
|
-
//
|
|
495
|
-
// provider (an identity record, D-13) but vault.provider —
|
|
496
|
-
// resolveProvider's ONLY gating key — was never set, so the D-13
|
|
497
|
-
// provisioning gate this phase built never fires.
|
|
409
|
+
// `project.provider` names a vault provider but `vault.provider` was never set.
|
|
498
410
|
try {
|
|
499
411
|
const projectProvider = getDotted(parsed, 'project.provider');
|
|
500
412
|
const vaultProvider = getDotted(parsed, 'vault.provider');
|
|
501
|
-
if ((projectProvider
|
|
413
|
+
if (git_provider_cjs_1.PROVIDERS.includes(projectProvider) && !isUsableString(vaultProvider)) {
|
|
502
414
|
violations.push({
|
|
503
415
|
code: INVARIANT_CODE.VAULT_PROVIDER_UNSET,
|
|
504
416
|
message: `project.provider is "${String(projectProvider)}" but vault.provider is unset — the D-13 provisioning gate is disabled.`,
|
|
@@ -508,31 +420,7 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
508
420
|
catch {
|
|
509
421
|
// fail open
|
|
510
422
|
}
|
|
511
|
-
//
|
|
512
|
-
// installed (project node_modules or the machine's global root) but
|
|
513
|
-
// org.profile was never set, i.e. Question 6 appears to have been
|
|
514
|
-
// skipped rather than answered blank.
|
|
515
|
-
// A recorded skip (init.org_profile_declined, the JSON boolean true and
|
|
516
|
-
// nothing else) is an answer. A set pointer is checked first; the marker is
|
|
517
|
-
// checked before the node_modules scan.
|
|
518
|
-
try {
|
|
519
|
-
const orgProfilePointer = getDotted(parsed, org_profile_cjs_1.ORG_PROFILE_CONFIG_KEY);
|
|
520
|
-
const orgProfileDeclined = getDotted(parsed, exports.INIT_ORG_PROFILE_DECLINED_KEY) === true;
|
|
521
|
-
if (!isUsableString(orgProfilePointer) && !orgProfileDeclined && orgProfilePackageResolvable(cwd, opts)) {
|
|
522
|
-
violations.push({
|
|
523
|
-
code: INVARIANT_CODE.ORG_PROFILE_UNASKED,
|
|
524
|
-
message: 'An org-profile-shaped package is installed but org.profile is unset and no skip was recorded — the pointer question (Question 6) appears not to have been answered. ' +
|
|
525
|
-
'Re-run "pp-tools init --reset" and either type the package name to use it, or press Enter (or type "none") to record a skip.',
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
// fail open
|
|
531
|
-
}
|
|
532
|
-
// INIT_COMPOSITION_NOT_RUN — a binding was composable from the org
|
|
533
|
-
// profile and project answers, but .platform-bindings.json was never
|
|
534
|
-
// written and no explicit decline was recorded — Step 2.5 appears to
|
|
535
|
-
// have been skipped rather than run-and-declined.
|
|
423
|
+
// A binding was composable but never written, and no decline was recorded.
|
|
536
424
|
try {
|
|
537
425
|
const bindingsPath = node_path_1.default.join(cwd, '.platform-bindings.json');
|
|
538
426
|
const bindingsExist = node_fs_1.default.existsSync(bindingsPath);
|
|
@@ -542,7 +430,8 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
542
430
|
if (dryRun.composable === true) {
|
|
543
431
|
violations.push({
|
|
544
432
|
code: INVARIANT_CODE.COMPOSITION_NOT_RUN,
|
|
545
|
-
message: 'A binding is composable from the org profile and project answers, but .platform-bindings.json was never written and no decline was recorded — Step 2.5 appears to have been skipped.'
|
|
433
|
+
message: 'A binding is composable from the org profile and project answers, but .platform-bindings.json was never written and no decline was recorded — Step 2.5 appears to have been skipped. ' +
|
|
434
|
+
'Run "pp-tools init --reset" to redo the question set and reach the compose confirmation (init-friction F3: a completed session never re-offers composition on a bare re-run).',
|
|
546
435
|
});
|
|
547
436
|
}
|
|
548
437
|
}
|
|
@@ -550,32 +439,111 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
550
439
|
catch {
|
|
551
440
|
// fail open
|
|
552
441
|
}
|
|
442
|
+
// `vault.enabled` present but not a boolean: reported, never repaired (the session scope is exempt).
|
|
443
|
+
try {
|
|
444
|
+
if (opts.scope !== 'session' && configExists) {
|
|
445
|
+
const vaultEnabled = resolveVaultEnabledState(parsed);
|
|
446
|
+
if (vaultEnabled.state === 'malformed') {
|
|
447
|
+
// Bounded like every record field; JSON.stringify keeps the value's type visible.
|
|
448
|
+
const rendered = overrideHelpers().sanitizeOverrideReason(String(JSON.stringify(vaultEnabled.value)));
|
|
449
|
+
// Phase 15 D-32: what this value does to the gates is fixed BY CONSTRUCTION, so it is
|
|
450
|
+
// stated, not mirrored and not measured here (measuring would call loadConfig, which
|
|
451
|
+
// spawns git, and this check spawns nothing). The gates' config loader resolves vault
|
|
452
|
+
// keys through federated-config, which accepts only a JSON boolean and falls back to the
|
|
453
|
+
// default (off) for anything else. The old mirror, Boolean(value), told the operator
|
|
454
|
+
// that every truthy non-boolean armed the gates; the shipped gate armed on none of them.
|
|
455
|
+
const split = 'The config loader the vault gates read accepts only a JSON boolean and falls back to ' +
|
|
456
|
+
'off, so from this value the two blocking vault gates do NOT arm, and /pp-init Step 2.6 ' +
|
|
457
|
+
'and this reachability check require the boolean true, so they are inert too. ';
|
|
458
|
+
violations.push({
|
|
459
|
+
code: INVARIANT_CODE.VAULT_ENABLED_MALFORMED,
|
|
460
|
+
message: `vault.enabled in .planning/config.json is ${rendered}, which is not a JSON boolean. ` +
|
|
461
|
+
split +
|
|
462
|
+
'Store the boolean with "pp-tools config-set vault.enabled true" (which accepts only ' +
|
|
463
|
+
'true or false, and refuses any other spelling), or "pp-tools config-set vault.enabled ' +
|
|
464
|
+
'false" to turn the capability off.',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// fail open — one check's fault must not suppress the others
|
|
471
|
+
}
|
|
472
|
+
// A proof is owed when the vault is armed, a profile applies and a binding exists (config only, no machine state).
|
|
473
|
+
// A failed or deferred record is reported whenever the vault is armed, binding or not.
|
|
474
|
+
try {
|
|
475
|
+
if (opts.scope !== 'session') {
|
|
476
|
+
const orgProfilePointer = getDotted(parsed, org_profile_cjs_1.ORG_PROFILE_CONFIG_KEY);
|
|
477
|
+
// FORK-ONLY (PBI 74316): with no pointer the embedded profile applies; only "none" opts out.
|
|
478
|
+
const profileApplies = isUsableString(orgProfilePointer)
|
|
479
|
+
? orgProfilePointer.trim().toLowerCase() !== org_profile_cjs_1.ORG_PROFILE_NONE
|
|
480
|
+
: (0, org_profile_cjs_1.effectiveEmbeddedOrgProfilePath)() !== null;
|
|
481
|
+
const vaultArmed = configExists && isVaultEnabledInConfig(parsed);
|
|
482
|
+
const owesAProof = vaultArmed && profileApplies && node_fs_1.default.existsSync(node_path_1.default.join(cwd, '.platform-bindings.json'));
|
|
483
|
+
// Kept as an object: destructuring trips @typescript-eslint/unbound-method.
|
|
484
|
+
const helpers = overrideHelpers();
|
|
485
|
+
// Bound every record field again at print time; a non-string counts as absent.
|
|
486
|
+
const bounded = (v) => (typeof v === 'string' ? helpers.sanitizeOverrideReason(v) : '');
|
|
487
|
+
const profile = isUsableString(orgProfilePointer) ? bounded(orgProfilePointer) : org_profile_cjs_1.EMBEDDED_ORG_PROFILE_NAME;
|
|
488
|
+
const status = bounded(getDotted(parsed, exports.INIT_PROBE_STATUS_KEY));
|
|
489
|
+
const when = bounded(getDotted(parsed, exports.INIT_PROBE_WHEN_KEY));
|
|
490
|
+
const who = bounded(getDotted(parsed, exports.INIT_PROBE_WHO_KEY));
|
|
491
|
+
const note = bounded(getDotted(parsed, exports.INIT_PROBE_NOTE_KEY));
|
|
492
|
+
if (vaultArmed && status === exports.INIT_PROBE_STATUS.FAILED) {
|
|
493
|
+
violations.push({
|
|
494
|
+
code: INVARIANT_CODE.PROBE_FAILED,
|
|
495
|
+
message: `The reachability proof recorded FAILED on ${when} by ${who}: ${note}. ` +
|
|
496
|
+
'Re-run "/pp-init" after fixing the remote, or record an attributed deferral with ' +
|
|
497
|
+
'"pp-tools query init-contract --defer-probe <reason>".',
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
else if (vaultArmed && status === exports.INIT_PROBE_STATUS.DEFERRED) {
|
|
501
|
+
// A deferral stays a violation until a passing probe overwrites it.
|
|
502
|
+
violations.push({
|
|
503
|
+
code: INVARIANT_CODE.PROBE_DEFERRED,
|
|
504
|
+
message: `The reachability proof was deferred by ${who} on ${when} for reason: ${note}. ` +
|
|
505
|
+
'Init remains unproven until a re-run of "/pp-init" records a passing probe.',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
else if (owesAProof) {
|
|
509
|
+
// Name the repair (re-run, or record a deferral) — never deletion.
|
|
510
|
+
const notRunRemedy = ' Re-run "/pp-init" to probe, or record an attributed deferral with ' +
|
|
511
|
+
'"pp-tools query init-contract --defer-probe <reason>".';
|
|
512
|
+
const notRun = (clause) => ({
|
|
513
|
+
code: INVARIANT_CODE.PROBE_NOT_RUN,
|
|
514
|
+
message: `The org profile "${profile}" resolves, vault.enabled is true and .platform-bindings.json ` +
|
|
515
|
+
`is present, but ${clause} — /pp-init Step 2.6 appears not to have run.` +
|
|
516
|
+
notRunRemedy,
|
|
517
|
+
});
|
|
518
|
+
if (status === exports.INIT_PROBE_STATUS.PASSED) {
|
|
519
|
+
// Only an attributed, timestamped pass clears the check.
|
|
520
|
+
if (!isUsableString(who) || !isUsableString(when)) {
|
|
521
|
+
violations.push(notRun('the reachability-proof record is malformed (init.probe_status is "passed" but ' +
|
|
522
|
+
`${!isUsableString(who) ? 'init.probe_who' : 'init.probe_when'} is absent, so the ` +
|
|
523
|
+
'proof is unattributed)'));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
violations.push(notRun(status === ''
|
|
528
|
+
? 'no reachability-proof record exists (init.probe_status is unset)'
|
|
529
|
+
: `no valid reachability-proof record exists (init.probe_status is "${status}", which is not a recognised status)`));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
// fail open — one check's fault must not suppress the others
|
|
536
|
+
}
|
|
553
537
|
return { ok: violations.length === 0, violations };
|
|
554
538
|
}
|
|
555
|
-
// ─── Session state machine
|
|
556
|
-
//
|
|
557
|
-
// Why this exists at all: `buildInitQuestions`/`verifyInitInvariants` make
|
|
558
|
-
// the CONTENT correct when called and the OMISSION provable afterwards —
|
|
559
|
-
// they do not make skipping impossible, because prose can still hand an
|
|
560
|
-
// agent the full 8-question list and the agent can still compress it (this
|
|
561
|
-
// module's own header; `34-VERIFICATION.md`'s third walk). This session
|
|
562
|
-
// machine removes the list itself: `initSessionNext` returns exactly ONE
|
|
563
|
-
// step, `initSessionAnswer` records exactly ONE answer and rejects
|
|
564
|
-
// anything out of order, and the CLI performs every side effect (identity
|
|
565
|
-
// write, `vault.provider`, composition) at the state that needs it — never
|
|
566
|
-
// leaving it to a workflow's own prose to remember. Neither
|
|
567
|
-
// `buildInitQuestions` nor `verifyInitInvariants` nor `composeBindings` is
|
|
568
|
-
// reimplemented here: this machine only sequences calls into them.
|
|
539
|
+
// ─── Session state machine: one step at a time, every side effect performed by the CLI ───
|
|
569
540
|
function sessionFilePath(cwd) {
|
|
570
541
|
return node_path_1.default.join(cwd, '.planning', INIT_SESSION_FILE_NAME);
|
|
571
542
|
}
|
|
572
543
|
function newSession() {
|
|
573
544
|
return { version: 1, answers: {}, identityWritten: false, composeDecision: null, doneAt: null };
|
|
574
545
|
}
|
|
575
|
-
/** Validate the
|
|
576
|
-
* that merely parses as JSON but carries the wrong shape (hand-edited,
|
|
577
|
-
* truncated mid-write, a leftover from an incompatible future version) must
|
|
578
|
-
* fail closed exactly like a parse error (T-34-63). */
|
|
546
|
+
/** Validate the whole persisted shape; a wrong shape fails closed like a parse error. */
|
|
579
547
|
function isValidSessionShape(v) {
|
|
580
548
|
if (!isObject(v))
|
|
581
549
|
return false;
|
|
@@ -603,13 +571,18 @@ function isValidSessionShape(v) {
|
|
|
603
571
|
}
|
|
604
572
|
if (doneAt !== null && typeof doneAt !== 'string')
|
|
605
573
|
return false;
|
|
574
|
+
const autoAnswers = v.autoAnswers;
|
|
575
|
+
if (autoAnswers !== undefined) {
|
|
576
|
+
if (!isObject(autoAnswers))
|
|
577
|
+
return false;
|
|
578
|
+
for (const src of Object.values(autoAnswers)) {
|
|
579
|
+
if (src !== 'org-profile' && src !== 'derived')
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
606
583
|
return true;
|
|
607
584
|
}
|
|
608
|
-
/** Absent
|
|
609
|
-
* valid → that session. Present but invalid in ANY way → `{ malformed:
|
|
610
|
-
* true }`, and every caller below refuses to proceed rather than silently
|
|
611
|
-
* re-initializing (T-34-63) — `initSessionReset` is the only documented
|
|
612
|
-
* recovery. Never throws. */
|
|
585
|
+
/** Absent → a new session; invalid → `{ malformed: true }` (recover with `--session-reset`). Never throws. */
|
|
613
586
|
function loadSession(cwd) {
|
|
614
587
|
const p = sessionFilePath(cwd);
|
|
615
588
|
try {
|
|
@@ -627,8 +600,7 @@ function loadSession(cwd) {
|
|
|
627
600
|
return { malformed: true };
|
|
628
601
|
}
|
|
629
602
|
}
|
|
630
|
-
/**
|
|
631
|
-
* human-readable message — never a raw stack trace reaching the operator. */
|
|
603
|
+
/** Format an unexpected write failure as a bounded message — never a raw stack trace. */
|
|
632
604
|
function writeFailedMessage(err) {
|
|
633
605
|
const detail = err instanceof Error ? err.message : String(err);
|
|
634
606
|
return (`A filesystem write inside the init session failed (${detail}). ` +
|
|
@@ -641,13 +613,7 @@ function persistSession(cwd, session) {
|
|
|
641
613
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dir);
|
|
642
614
|
(0, shell_command_projection_cjs_1.platformWriteSync)(sessionFilePath(cwd), JSON.stringify(session, null, 2) + '\n');
|
|
643
615
|
}
|
|
644
|
-
/**
|
|
645
|
-
* `readConfigFile` above (which reports `exists`/malformed for
|
|
646
|
-
* `verifyInitInvariants`'s read-only purposes): this helper only ever
|
|
647
|
-
* needs "the object to merge into", degrading to `{}` on any absence or
|
|
648
|
-
* fault so a merge write never crashes on a pre-existing malformed file —
|
|
649
|
-
* it simply starts the merge from empty and the write below then produces
|
|
650
|
-
* a well-formed file going forward. */
|
|
616
|
+
/** The config to merge into; `{}` on absence or fault, so a merge write never crashes. */
|
|
651
617
|
function readConfigForMerge(cwd) {
|
|
652
618
|
try {
|
|
653
619
|
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, '.planning', 'config.json'), 'utf-8');
|
|
@@ -658,13 +624,7 @@ function readConfigForMerge(cwd) {
|
|
|
658
624
|
return {};
|
|
659
625
|
}
|
|
660
626
|
}
|
|
661
|
-
/**
|
|
662
|
-
* value (string, number, boolean, array) replaces the target outright.
|
|
663
|
-
* Bounded by the patches this module ever constructs itself (`project.*`,
|
|
664
|
-
* `vault.provider`, `org.profile`, `init.compose_declined` — never a
|
|
665
|
-
* caller-supplied arbitrary depth), so no depth cap is needed here the way
|
|
666
|
-
* `walkLeaves` in `compose-bindings.cts` needs one for untrusted file
|
|
667
|
-
* content. */
|
|
627
|
+
/** Recursive merge of plain objects; any other value replaces the target. */
|
|
668
628
|
function deepMergeConfigPatch(target, patch) {
|
|
669
629
|
for (const k of Object.keys(patch)) {
|
|
670
630
|
const pv = patch[k];
|
|
@@ -678,28 +638,14 @@ function deepMergeConfigPatch(target, patch) {
|
|
|
678
638
|
}
|
|
679
639
|
return target;
|
|
680
640
|
}
|
|
681
|
-
/** The
|
|
682
|
-
* effect the session performs (identity write, `vault.provider`,
|
|
683
|
-
* `org.profile`, `init.compose_declined`) routes through here, merged
|
|
684
|
-
* against whatever is already on disk so a session step never clobbers an
|
|
685
|
-
* unrelated key a human or another tool wrote. Direct `fs`/`JSON` I/O,
|
|
686
|
-
* bypassing `config-schema.cts`'s registered-key validation — the same
|
|
687
|
-
* convention `compose-bindings.cts`'s `cfgGet` and this module's own
|
|
688
|
-
* `readConfigFile` already use for `project.*`/`org.profile` reads; see
|
|
689
|
-
* `.planning/todos/pending/project-config-key-unknown.md` for why `project`
|
|
690
|
-
* is not (yet) a registered top-level key, out of scope for this plan. */
|
|
641
|
+
/** The only writer of `.planning/config.json` in this module; merges so unrelated keys survive. */
|
|
691
642
|
function writeConfigMerge(cwd, patch) {
|
|
692
643
|
const dir = node_path_1.default.join(cwd, '.planning');
|
|
693
644
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dir);
|
|
694
645
|
const merged = deepMergeConfigPatch(readConfigForMerge(cwd), patch);
|
|
695
646
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dir, 'config.json'), JSON.stringify(merged, null, 2) + '\n');
|
|
696
647
|
}
|
|
697
|
-
/**
|
|
698
|
-
* write the `project` identity block and — only when the `provider` answer
|
|
699
|
-
* is exactly `gitlab` or `azure` — `vault.provider`. `org_profile` is
|
|
700
|
-
* excluded from this map: it is written immediately when THAT answer is
|
|
701
|
-
* recorded (see `initSessionAnswer`), not deferred here, precisely so the
|
|
702
|
-
* `provider` question re-resolves its `preselected` against it. */
|
|
648
|
+
/** Write the project identity block, and `vault.provider` when the provider is a vault provider (gitlab, azure or github). */
|
|
703
649
|
function writeIdentityAndProvider(cwd, answers) {
|
|
704
650
|
const projectPatch = {};
|
|
705
651
|
for (const [questionId, configKey] of Object.entries(PROJECT_BLOCK_QUESTION_MAP)) {
|
|
@@ -710,14 +656,124 @@ function writeIdentityAndProvider(cwd, answers) {
|
|
|
710
656
|
}
|
|
711
657
|
const patch = { project: projectPatch };
|
|
712
658
|
const providerAnswer = answers.provider;
|
|
713
|
-
if (providerAnswer
|
|
659
|
+
if (git_provider_cjs_1.PROVIDERS.includes(providerAnswer)) {
|
|
714
660
|
patch.vault = { provider: providerAnswer };
|
|
715
661
|
}
|
|
716
662
|
writeConfigMerge(cwd, patch);
|
|
717
663
|
}
|
|
718
|
-
/**
|
|
719
|
-
* `
|
|
720
|
-
*
|
|
664
|
+
/** The ONE owner of the arm-where-absent rule (quick 261003-kmd): arms `vault.enabled`,
|
|
665
|
+
* `vault.practices_enabled` and `vault.required` only where ABSENT, from one read in one write.
|
|
666
|
+
*
|
|
667
|
+
* A successful composition means a client vault exists, so the three keys are armed (OD-1,
|
|
668
|
+
* 260915-lmc, 260916-s5n). An explicit `false` or a malformed value is kept — that is the
|
|
669
|
+
* operator's decision, never overwritten (07-09 WR-07). The write happens only when there is
|
|
670
|
+
* something to arm: the nested- and flat-form byte-identity pins depend on a no-op writing
|
|
671
|
+
* nothing. Callers: the session's `'unchanged'` decision (how existing projects get armed by a
|
|
672
|
+
* plain re-run), the approved `compose_confirm`, and `armVaultFromProfile` (`/pp-new-project`).
|
|
673
|
+
* It exists because the two session copies had already drifted once — the practices key was
|
|
674
|
+
* added to both by hand in 260915-lmc — and a third route would have been a third copy.
|
|
675
|
+
*
|
|
676
|
+
* @returns the armed key names, in the fixed order enabled, practices_enabled, required. */
|
|
677
|
+
function armVaultKeysWhereAbsent(cwd) {
|
|
678
|
+
const { parsed } = readConfigFile(cwd);
|
|
679
|
+
const vaultPatch = {};
|
|
680
|
+
if (resolveVaultEnabledState(parsed).state === 'absent') {
|
|
681
|
+
vaultPatch.enabled = true;
|
|
682
|
+
}
|
|
683
|
+
if (resolveVaultPracticesEnabledState(parsed).state === 'absent') {
|
|
684
|
+
vaultPatch.practices_enabled = true;
|
|
685
|
+
}
|
|
686
|
+
if (resolveVaultRequiredState(parsed).state === 'absent') {
|
|
687
|
+
vaultPatch.required = true;
|
|
688
|
+
}
|
|
689
|
+
const armed = Object.keys(vaultPatch);
|
|
690
|
+
if (armed.length > 0) {
|
|
691
|
+
writeConfigMerge(cwd, { vault: vaultPatch });
|
|
692
|
+
}
|
|
693
|
+
return armed;
|
|
694
|
+
}
|
|
695
|
+
/** The reason triad and identity chain, reused from the vault-trace leaf (never re-derived here).
|
|
696
|
+
* Required lazily: pp-tools loads this module on every command, and the trace leaf is large. */
|
|
697
|
+
function overrideHelpers() {
|
|
698
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
699
|
+
const trace = require('./vault-trace.cjs');
|
|
700
|
+
return trace;
|
|
701
|
+
}
|
|
702
|
+
/** Build the record with `who` and `when` bounded; `status` is checked against `INIT_PROBE_STATUS`. */
|
|
703
|
+
function buildProbeRecord(cwd, status, note, opts, helpers) {
|
|
704
|
+
const injectedWho = typeof opts._who === 'string' && opts._who.trim() !== '' ? opts._who : null;
|
|
705
|
+
const rawWho = injectedWho ?? helpers.resolveOverrideIdentity(cwd);
|
|
706
|
+
// Never an empty string and never an omitted key — an absent `who` and an
|
|
707
|
+
// unattributable one must not look identical (the trace leaf's own rule).
|
|
708
|
+
const who = helpers.sanitizeOverrideReason(rawWho) || 'unknown';
|
|
709
|
+
const when = helpers.sanitizeOverrideReason(opts.now ?? new Date().toISOString());
|
|
710
|
+
return { status, when, who, note };
|
|
711
|
+
}
|
|
712
|
+
/** The shared record write: four keys merged under `init`. Never throws (`write_failed`). */
|
|
713
|
+
function writeProbeRecord(cwd, record) {
|
|
714
|
+
try {
|
|
715
|
+
writeConfigMerge(cwd, {
|
|
716
|
+
init: {
|
|
717
|
+
probe_status: record.status,
|
|
718
|
+
probe_when: record.when,
|
|
719
|
+
probe_who: record.who,
|
|
720
|
+
probe_note: record.note,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
return { ok: false, reason: 'write_failed', message: writeFailedMessage(err) };
|
|
726
|
+
}
|
|
727
|
+
return { ok: true, record };
|
|
728
|
+
}
|
|
729
|
+
/** Record that the Step 2.6 probe passed or failed; an unknown status or an empty note is refused, writing nothing.
|
|
730
|
+
* Deferrals go through `recordProbeDeferral`. */
|
|
731
|
+
function recordProbeOutcome(cwd, input, opts = {}) {
|
|
732
|
+
const helpers = overrideHelpers();
|
|
733
|
+
const status = input?.status;
|
|
734
|
+
if (status !== exports.INIT_PROBE_STATUS.PASSED && status !== exports.INIT_PROBE_STATUS.FAILED) {
|
|
735
|
+
return {
|
|
736
|
+
ok: false,
|
|
737
|
+
reason: 'invalid_probe_status',
|
|
738
|
+
message: `Cannot record probe status ${JSON.stringify(status)} — expected ` +
|
|
739
|
+
`"${exports.INIT_PROBE_STATUS.PASSED}" or "${exports.INIT_PROBE_STATUS.FAILED}". ` +
|
|
740
|
+
`A deferral is recorded with --defer-probe "<reason>", which requires a stated reason.`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
// A non-string note counts as no note (never String()-coerced).
|
|
744
|
+
const note = helpers.sanitizeOverrideReason(typeof input?.note === 'string' ? input.note : '');
|
|
745
|
+
if (note === '') {
|
|
746
|
+
return {
|
|
747
|
+
ok: false,
|
|
748
|
+
reason: 'probe_note_required',
|
|
749
|
+
message: 'Cannot record a probe outcome with no note. Say what the probe measured — which targets ' +
|
|
750
|
+
'were reachable, and for a failure which one was not — so the record is evidence and not ' +
|
|
751
|
+
'just a verdict.',
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return writeProbeRecord(cwd, buildProbeRecord(cwd, status, note, opts, helpers));
|
|
755
|
+
}
|
|
756
|
+
/** Record a deferral of the Step 2.6 proof; the reason is required and validated, and a refusal writes nothing.
|
|
757
|
+
* `--verify` keeps reporting it until a passing probe overwrites it. */
|
|
758
|
+
function recordProbeDeferral(cwd, reason, opts = {}) {
|
|
759
|
+
const helpers = overrideHelpers();
|
|
760
|
+
const refusal = (rejection) => ({
|
|
761
|
+
ok: false,
|
|
762
|
+
reason: 'deferral_reason_rejected',
|
|
763
|
+
message: `Deferral refused: ${rejection}. State why the reachability proof is being deferred — ` +
|
|
764
|
+
'at least 20 characters, containing letters, not a placeholder.',
|
|
765
|
+
});
|
|
766
|
+
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
767
|
+
return refusal('no reason was given');
|
|
768
|
+
}
|
|
769
|
+
const verdict = helpers.validateOverrideReason(reason.trim());
|
|
770
|
+
if (verdict.state !== 'accepted') {
|
|
771
|
+
return refusal(verdict.state === 'rejected' ? verdict.rejection : 'the reason was not accepted');
|
|
772
|
+
}
|
|
773
|
+
const note = helpers.sanitizeOverrideReason(verdict.reason);
|
|
774
|
+
return writeProbeRecord(cwd, buildProbeRecord(cwd, exports.INIT_PROBE_STATUS.DEFERRED, note, opts, helpers));
|
|
775
|
+
}
|
|
776
|
+
/** Parse a `compose_confirm` answer; `null` for anything unrecognised (never a silent "no"). */
|
|
721
777
|
function normalizeYesNo(value) {
|
|
722
778
|
if (typeof value !== 'string')
|
|
723
779
|
return null;
|
|
@@ -728,43 +784,27 @@ function normalizeYesNo(value) {
|
|
|
728
784
|
return false;
|
|
729
785
|
return null;
|
|
730
786
|
}
|
|
731
|
-
/**
|
|
732
|
-
*
|
|
733
|
-
* Recomputes `buildInitQuestions(cwd)` FRESH on every call — never cached
|
|
734
|
-
* across calls — which is what makes the `provider` question's
|
|
735
|
-
* `preselected` reflect an `org_profile` answer given moments earlier in
|
|
736
|
-
* the SAME session (the walk-3 regression this plan's design hazards
|
|
737
|
-
* name): by the time this function is asked for the `provider` question,
|
|
738
|
-
* `org.profile` has already been written to disk by `initSessionAnswer`,
|
|
739
|
-
* so `buildInitQuestions`'s own `resolveProvider` call sees it.
|
|
740
|
-
*
|
|
741
|
-
* Performs three idempotent side effects, each guarded by its own session
|
|
742
|
-
* flag so re-calling this (as every public entry point does, to validate
|
|
743
|
-
* the current step) never repeats a write: the identity+provider batch
|
|
744
|
-
* write once all 8 questions are answered; marking `composeDecision:
|
|
745
|
-
* 'skipped'` when nothing is composable at all (the real inert path,
|
|
746
|
-
* INIT-02) or `composeDecision: 'unchanged'` when a binding IS composable
|
|
747
|
-
* but the existing file already matches it (a healthy, up-to-date state —
|
|
748
|
-
* NOT the inert path, and reported with its own distinct message; no
|
|
749
|
-
* `compose_confirm` prompt either way, nothing written); and recording
|
|
750
|
-
* `doneAt` once `verifyInitInvariants` first reports `ok: true`.
|
|
751
|
-
*/
|
|
787
|
+
/** Decide the current step, performing its idempotent side effects once: automatic answers, the identity write,
|
|
788
|
+
* the compose decision and `doneAt`. Recomputes the question set on every call. */
|
|
752
789
|
function computeCurrentStep(cwd, session, opts) {
|
|
753
|
-
//
|
|
754
|
-
// resolveOrgProfileDocument, resolveOrgProfile, verifyInitInvariants)
|
|
755
|
-
// wraps its entire body in a top-level try/catch so a filesystem fault
|
|
756
|
-
// degrades to a bounded result rather than crashing (D-09). This
|
|
757
|
-
// function's writes (writeIdentityAndProvider/persistSession, below) did
|
|
758
|
-
// not — an unwritable .planning directory threw an uncaught EACCES
|
|
759
|
-
// straight out of initSessionNext/initSessionAnswer. Fixed by matching
|
|
760
|
-
// the convention: any failure anywhere in this body degrades to a
|
|
761
|
-
// `write_failed` verdict instead of propagating.
|
|
790
|
+
// Any failure degrades to a `write_failed` step instead of throwing.
|
|
762
791
|
try {
|
|
763
|
-
const questions = buildInitQuestions(cwd
|
|
792
|
+
const questions = buildInitQuestions(cwd);
|
|
793
|
+
// PBI 74299: read once per call; only consulted for an unanswered question.
|
|
794
|
+
let profileInit = null;
|
|
764
795
|
for (const q of questions) {
|
|
765
|
-
if (
|
|
796
|
+
if (Object.prototype.hasOwnProperty.call(session.answers, q.id))
|
|
797
|
+
continue;
|
|
798
|
+
profileInit ??= readProfileInitAnswers(cwd, { _globalRoot: opts._globalRoot });
|
|
799
|
+
const auto = autoAnswerFor(q.id, session.answers, profileInit);
|
|
800
|
+
if (auto === null) {
|
|
766
801
|
return { kind: 'question', question: q };
|
|
767
802
|
}
|
|
803
|
+
// Filled in once and recorded, so a --session-next poll writes the
|
|
804
|
+
// session file only the first time an answer is decided.
|
|
805
|
+
session.answers[q.id] = auto.value;
|
|
806
|
+
session.autoAnswers = { ...(session.autoAnswers ?? {}), [q.id]: auto.source };
|
|
807
|
+
persistSession(cwd, session);
|
|
768
808
|
}
|
|
769
809
|
if (!session.identityWritten) {
|
|
770
810
|
writeIdentityAndProvider(cwd, session.answers);
|
|
@@ -773,20 +813,10 @@ function computeCurrentStep(cwd, session, opts) {
|
|
|
773
813
|
}
|
|
774
814
|
if (session.composeDecision === null) {
|
|
775
815
|
const dryRun = (0, compose_bindings_cjs_1.composeBindings)(cwd, { offline: opts.offline === true, _globalRoot: opts._globalRoot });
|
|
776
|
-
//
|
|
777
|
-
// change something. `action: 'unchanged'` means the existing file
|
|
778
|
-
// already carries every key the composer would produce (D-08's
|
|
779
|
-
// absent-only merge) — asking the operator to approve a write that
|
|
780
|
-
// writes nothing is exactly the shape of prompt that produced the
|
|
781
|
-
// false-success report this finding names. Fall through to the
|
|
782
|
-
// messaging below instead of asking — but see the coordinator
|
|
783
|
-
// follow-up: 'unchanged' and 'not composable at all' are DIFFERENT
|
|
784
|
-
// end states and must record (and later report) distinct decisions,
|
|
785
|
-
// never sharing the inert-path (INIT-02) message below.
|
|
816
|
+
// Offer compose_confirm only when approving would change something; 'unchanged' and 'skipped' are distinct end states.
|
|
786
817
|
if (dryRun.composable === true && dryRun.action !== 'unchanged') {
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
// file keeps the RAW coordinates.
|
|
818
|
+
// VAULT-EGRESS-1: `info` is display-only (the CLI and the TTY renderer print it); approval
|
|
819
|
+
// re-composes with `write: true` below, so the written file keeps the RAW coordinates.
|
|
790
820
|
const shown = (0, compose_bindings_cjs_1.redactComposeResultForEmission)(dryRun);
|
|
791
821
|
return {
|
|
792
822
|
kind: 'compose_confirm',
|
|
@@ -795,25 +825,24 @@ function computeCurrentStep(cwd, session, opts) {
|
|
|
795
825
|
drift: shown.drift,
|
|
796
826
|
action: dryRun.action,
|
|
797
827
|
composed: shown.composed,
|
|
798
|
-
warnings:
|
|
828
|
+
warnings: dryRun.warnings,
|
|
799
829
|
},
|
|
800
830
|
};
|
|
801
831
|
}
|
|
802
832
|
session.composeDecision = dryRun.composable === true && dryRun.action === 'unchanged' ? 'unchanged' : 'skipped';
|
|
833
|
+
if (session.composeDecision === 'unchanged') {
|
|
834
|
+
armVaultKeysWhereAbsent(cwd);
|
|
835
|
+
}
|
|
803
836
|
persistSession(cwd, session);
|
|
804
837
|
}
|
|
805
|
-
// Three
|
|
806
|
-
// nothing written, and each needs its own message — conflating them
|
|
807
|
-
// (as an earlier pass of this fix did) tells an operator with a
|
|
808
|
-
// healthy, up-to-date binding that their org profile produced nothing
|
|
809
|
-
// composable, which is exactly the "tool reports a state that isn't
|
|
810
|
-
// the real one" failure shape this phase exists to eliminate.
|
|
838
|
+
// Three different nothing-written end states, each with its own message.
|
|
811
839
|
const skippedComposeInfo = session.composeDecision === 'skipped'
|
|
812
840
|
? 'Nothing was composable from the org profile and project answers — no compose_confirm step was shown and nothing was written (the inert path, INIT-02).'
|
|
813
841
|
: session.composeDecision === 'unchanged'
|
|
814
842
|
? 'The existing .platform-bindings.json is already up to date with what the org profile and project answers would produce — no compose_confirm step was needed and nothing was written.'
|
|
815
843
|
: undefined;
|
|
816
|
-
|
|
844
|
+
// The session verify skips only the INIT_PROBE_* check: Step 2.6 records the proof after init returns.
|
|
845
|
+
const verifyResult = verifyInitInvariants(cwd, { _globalRoot: opts._globalRoot, scope: 'session' });
|
|
817
846
|
if (verifyResult.ok) {
|
|
818
847
|
if (!session.doneAt) {
|
|
819
848
|
session.doneAt = new Date().toISOString();
|
|
@@ -873,13 +902,7 @@ function currentStepToState(step) {
|
|
|
873
902
|
};
|
|
874
903
|
}
|
|
875
904
|
}
|
|
876
|
-
/**
|
|
877
|
-
* Return the ONE next step of the `/pp-init` session at `cwd` — a question,
|
|
878
|
-
* `compose_confirm`, `verify`, or `done`. Never a list. Fails closed (T-34-
|
|
879
|
-
* 63) with `error: INIT_SESSION_MALFORMED` when the session file exists but
|
|
880
|
-
* does not parse as valid session state; `--session-reset` is the
|
|
881
|
-
* documented recovery, never a silent re-initialize.
|
|
882
|
-
*/
|
|
905
|
+
/** Return the ONE next step of the session; malformed session state fails closed (recover with `--session-reset`). */
|
|
883
906
|
function initSessionNext(cwd, opts = {}) {
|
|
884
907
|
const loaded = loadSession(cwd);
|
|
885
908
|
if (loaded.malformed) {
|
|
@@ -892,25 +915,12 @@ function initSessionNext(cwd, opts = {}) {
|
|
|
892
915
|
'Run "pp_run query init-contract --session-reset" to start over — it is never resumed silently into an unknown state.',
|
|
893
916
|
};
|
|
894
917
|
}
|
|
895
|
-
|
|
918
|
+
const session = loaded.session;
|
|
919
|
+
const state = currentStepToState(computeCurrentStep(cwd, session, opts));
|
|
920
|
+
const autoAnswered = listAutoAnswers(cwd, session);
|
|
921
|
+
return autoAnswered.length > 0 ? { ...state, autoAnswered } : state;
|
|
896
922
|
}
|
|
897
|
-
/**
|
|
898
|
-
* Record ONE answer for the CURRENT step only. Rejects — with `ok: false`
|
|
899
|
-
* and a named `reason`, never a throw — an answer for any step other than
|
|
900
|
-
* the one `initSessionNext` would currently serve (T-34-60: an agent trying
|
|
901
|
-
* to batch or skip ahead fails loudly), a blank answer to a
|
|
902
|
-
* non-`optionalAnswer` question, an unparseable `compose_confirm` value, or
|
|
903
|
-
* any answer once the session is already `done`.
|
|
904
|
-
*
|
|
905
|
-
* The `org_profile` answer is written to `.planning/config.json` (`org.profile`)
|
|
906
|
-
* IMMEDIATELY when it selects a package — the one exception to the "batch
|
|
907
|
-
* write after the last answer" rule in `computeCurrentStep`'s own doc
|
|
908
|
-
* comment — because the very next question (`provider`) must see it to
|
|
909
|
-
* re-resolve its `preselected` (the walk-3 regression Task 1's acceptance
|
|
910
|
-
* names). A decline (`isOrgProfileDecline`: blank, or `none` in any case) is
|
|
911
|
-
* recorded instead as `init.org_profile_declined: true` when a profile was
|
|
912
|
-
* detectable.
|
|
913
|
-
*/
|
|
923
|
+
/** Record one answer for the current step only; out-of-order, blank required, unparseable or post-done answers are rejected. */
|
|
914
924
|
function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
915
925
|
const loaded = loadSession(cwd);
|
|
916
926
|
if (loaded.malformed) {
|
|
@@ -922,14 +932,7 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
922
932
|
};
|
|
923
933
|
}
|
|
924
934
|
const session = loaded.session;
|
|
925
|
-
//
|
|
926
|
-
// performs directly (the `org_profile` immediate write, the batch
|
|
927
|
-
// `persistSession` calls, the `compose_confirm` decline/approve writes)
|
|
928
|
-
// — in a top-level try/catch, matching the D-09 convention every sibling
|
|
929
|
-
// module in this seam already follows. `computeCurrentStep` degrades to
|
|
930
|
-
// a `write_failed` step on its own internal failures (handled explicitly
|
|
931
|
-
// below); this catch additionally covers the write sites that live in
|
|
932
|
-
// THIS function, after `computeCurrentStep` returns.
|
|
935
|
+
// Any write failure in this function degrades to `write_failed` instead of throwing.
|
|
933
936
|
try {
|
|
934
937
|
const step = computeCurrentStep(cwd, session, opts);
|
|
935
938
|
if (step.kind === 'write_failed') {
|
|
@@ -954,32 +957,6 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
954
957
|
return { ok: false, reason: 'answer_required', message: `Question "${stepId}" requires a non-blank answer.` };
|
|
955
958
|
}
|
|
956
959
|
const answerValue = typeof value === 'string' ? value : '';
|
|
957
|
-
if (stepId === 'org_profile') {
|
|
958
|
-
if (isOrgProfileDecline(answerValue)) {
|
|
959
|
-
// Enter and `none` leave the same session record. The marker is
|
|
960
|
-
// written exactly when the UNASKED violation would otherwise fire
|
|
961
|
-
// (the same predicate); with no profile detectable nothing was
|
|
962
|
-
// offered, so nothing is recorded.
|
|
963
|
-
session.answers[stepId] = '';
|
|
964
|
-
if (orgProfilePackageResolvable(cwd, { _globalRoot: opts._globalRoot })) {
|
|
965
|
-
writeConfigMerge(cwd, { init: { org_profile_declined: true } });
|
|
966
|
-
}
|
|
967
|
-
persistSession(cwd, session);
|
|
968
|
-
return { ok: true };
|
|
969
|
-
}
|
|
970
|
-
const trimmed = answerValue.trim();
|
|
971
|
-
session.answers[stepId] = answerValue;
|
|
972
|
-
const orgPatch = { org: { profile: trimmed } };
|
|
973
|
-
// A selection after an earlier recorded skip writes the marker back to
|
|
974
|
-
// false in the same merge, so a stale decline never sits beside a set
|
|
975
|
-
// pointer. No write when the marker is absent.
|
|
976
|
-
if (getDotted(readConfigFile(cwd).parsed, exports.INIT_ORG_PROFILE_DECLINED_KEY) === true) {
|
|
977
|
-
orgPatch.init = { org_profile_declined: false };
|
|
978
|
-
}
|
|
979
|
-
writeConfigMerge(cwd, orgPatch);
|
|
980
|
-
persistSession(cwd, session);
|
|
981
|
-
return { ok: true };
|
|
982
|
-
}
|
|
983
960
|
session.answers[stepId] = answerValue;
|
|
984
961
|
persistSession(cwd, session);
|
|
985
962
|
return { ok: true };
|
|
@@ -1001,24 +978,10 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
1001
978
|
};
|
|
1002
979
|
}
|
|
1003
980
|
if (normalized === true) {
|
|
1004
|
-
//
|
|
1005
|
-
// decision. The dry run inside computeCurrentStep already reported
|
|
1006
|
-
// composable:true/action!=='unchanged' moments earlier, but the
|
|
1007
|
-
// world can change between that dry run and this write (the file
|
|
1008
|
-
// became malformed on disk in between, a transient fault now caught
|
|
1009
|
-
// internally by composeBindings's own outer catch, etc.) — in that
|
|
1010
|
-
// case `wrote` comes back false even though the operator approved.
|
|
1011
|
-
// Recording 'approved' unconditionally here would make the session
|
|
1012
|
-
// proceed toward done/verify as though the write had succeeded,
|
|
1013
|
-
// and the eventual INIT_COMPOSITION_NOT_RUN violation would then
|
|
1014
|
-
// misdescribe what happened ("Step 2.5 appears to have been
|
|
1015
|
-
// skipped") when in fact the user approved and the write failed.
|
|
981
|
+
// Check the write result before recording "approved": the file can change between the dry run and this write.
|
|
1016
982
|
const writeResult = (0, compose_bindings_cjs_1.composeBindings)(cwd, { write: true, offline: opts.offline === true, _globalRoot: opts._globalRoot });
|
|
1017
983
|
if (writeResult.composable !== true || writeResult.wrote !== true) {
|
|
1018
|
-
//
|
|
1019
|
-
// --session-next call re-evaluates from a fresh dry run (and, if
|
|
1020
|
-
// still composable, re-offers compose_confirm) rather than
|
|
1021
|
-
// silently treating this as skipped or declined.
|
|
984
|
+
// Leave the decision unset so the next call re-evaluates and can re-offer compose_confirm.
|
|
1022
985
|
return {
|
|
1023
986
|
ok: false,
|
|
1024
987
|
reason: 'compose_write_failed',
|
|
@@ -1029,6 +992,7 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
1029
992
|
};
|
|
1030
993
|
}
|
|
1031
994
|
session.composeDecision = 'approved';
|
|
995
|
+
armVaultKeysWhereAbsent(cwd);
|
|
1032
996
|
}
|
|
1033
997
|
else {
|
|
1034
998
|
writeConfigMerge(cwd, { init: { compose_declined: true } });
|
|
@@ -1048,8 +1012,7 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
1048
1012
|
return { ok: false, reason: 'write_failed', message: writeFailedMessage(err) };
|
|
1049
1013
|
}
|
|
1050
1014
|
}
|
|
1051
|
-
/** Delete the session file
|
|
1052
|
-
* session (T-34-63). Never throws; a missing file is not an error. */
|
|
1015
|
+
/** Delete the session file (the recovery from a malformed session). Never throws. */
|
|
1053
1016
|
function initSessionReset(cwd) {
|
|
1054
1017
|
try {
|
|
1055
1018
|
const p = sessionFilePath(cwd);
|
|
@@ -1061,193 +1024,114 @@ function initSessionReset(cwd) {
|
|
|
1061
1024
|
}
|
|
1062
1025
|
return { ok: true };
|
|
1063
1026
|
}
|
|
1064
|
-
/**
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
* `
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
|
|
1084
|
-
function armVault(cwd) {
|
|
1085
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1086
|
-
const vaultMod = require('./vault.cjs');
|
|
1087
|
-
let binding = { bound: false, repo: null, ref: null };
|
|
1088
|
-
try {
|
|
1089
|
-
binding = vaultMod.readClientBinding(cwd);
|
|
1090
|
-
}
|
|
1091
|
-
catch {
|
|
1092
|
-
/* unbound */
|
|
1093
|
-
}
|
|
1094
|
-
const client = { repo: vaultMod.redactCloneTarget(binding.repo), ref: binding.ref };
|
|
1095
|
-
const configPath = node_path_1.default.join(cwd, '.planning', 'config.json');
|
|
1096
|
-
let cfg = {};
|
|
1097
|
-
let cfgExists = false;
|
|
1027
|
+
/** Remedy every failure verdict names: repair through the interactive relay, never deletion. */
|
|
1028
|
+
const ARM_VAULT_REMEDY = 'Run "/pp-init" to relay the binding compose interactively and arm the vault.';
|
|
1029
|
+
/** Compose the binding from the resolved org profile and arm the vault keys where absent, with no prompt
|
|
1030
|
+
* (quick 261003-kmd, FIX-1). `/pp-new-project` calls this so a project created through it is governed the
|
|
1031
|
+
* same way `/pp-init` leaves one; before it existed, only `/pp-init` armed the vault and a new-project
|
|
1032
|
+
* route left it silently off.
|
|
1033
|
+
*
|
|
1034
|
+
* The binding is written through `composeBindings` — the SAME composer `/pp-init` approves through — so
|
|
1035
|
+
* its shape and provenance are identical, and the keys through `armVaultKeysWhereAbsent`, the one owner.
|
|
1036
|
+
* An inert verdict (a recorded decline, a `vault.enabled` that is set but not the boolean `true`, or no
|
|
1037
|
+
* org profile resolves) writes nothing of any kind and never creates `.planning/`. The `vault_disabled`
|
|
1038
|
+
* arm exists because this route has no human confirm: an explicit `false` (or a malformed value) is the
|
|
1039
|
+
* operator's opt-out, so writing a binding and two keys into that project would turn `vault.status` from
|
|
1040
|
+
* `inert` into a verdict and let a report say "armed" where the gate says the vault is not governing
|
|
1041
|
+
* (261003-kmd verification gap; the more inert reading). `/pp-init`'s approved compose is unchanged — a
|
|
1042
|
+
* human confirmed it. `vault.provider` is deliberately NOT written: `resolveProvider` in
|
|
1043
|
+
* `src/git-provider.cts` already layers the org profile's provider when the config key is absent, so the
|
|
1044
|
+
* fetch resolves the right provider without a second writer. Never reads or writes the session file.
|
|
1045
|
+
* Never throws. */
|
|
1046
|
+
function armVaultFromProfile(cwd, opts = {}) {
|
|
1098
1047
|
try {
|
|
1099
|
-
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1048
|
+
const { parsed } = readConfigFile(cwd);
|
|
1049
|
+
if (capabilityActivation.getConfigValueEitherForm(parsed, exports.INIT_ORG_PROFILE_DECLINED_KEY).value === true) {
|
|
1050
|
+
return {
|
|
1051
|
+
ok: true,
|
|
1052
|
+
armed: false,
|
|
1053
|
+
reason: 'org_profile_declined',
|
|
1054
|
+
message: 'The org profile was declined for this project (init.org_profile_declined), so nothing was composed or armed.',
|
|
1055
|
+
};
|
|
1105
1056
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
practicesEnabled: undefined,
|
|
1114
|
-
client,
|
|
1115
|
-
message: '.planning/config.json is not valid JSON — fix it (or re-run /pp-init), then arm the vault again. Nothing was written.',
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
const vaultSection = isObject(cfg.vault) ? cfg.vault : null;
|
|
1119
|
-
const nested = vaultSection && Object.prototype.hasOwnProperty.call(vaultSection, 'enabled');
|
|
1120
|
-
const flat = Object.prototype.hasOwnProperty.call(cfg, 'vault.enabled');
|
|
1121
|
-
const current = nested && vaultSection ? vaultSection.enabled : flat ? cfg['vault.enabled'] : undefined;
|
|
1122
|
-
const requiredNested = vaultSection && Object.prototype.hasOwnProperty.call(vaultSection, 'required');
|
|
1123
|
-
const requiredFlat = Object.prototype.hasOwnProperty.call(cfg, 'vault.required');
|
|
1124
|
-
const currentRequired = requiredNested && vaultSection ? vaultSection.required : requiredFlat ? cfg['vault.required'] : undefined;
|
|
1125
|
-
const practicesNested = vaultSection && Object.prototype.hasOwnProperty.call(vaultSection, 'practices_enabled');
|
|
1126
|
-
const practicesFlat = Object.prototype.hasOwnProperty.call(cfg, 'vault.practices_enabled');
|
|
1127
|
-
const currentPractices = practicesNested && vaultSection
|
|
1128
|
-
? vaultSection.practices_enabled
|
|
1129
|
-
: practicesFlat
|
|
1130
|
-
? cfg['vault.practices_enabled']
|
|
1131
|
-
: undefined;
|
|
1132
|
-
const practicesAbsent = !(practicesNested || practicesFlat);
|
|
1133
|
-
if (!binding.bound) {
|
|
1134
|
-
return {
|
|
1135
|
-
ok: false,
|
|
1136
|
-
armed: false,
|
|
1137
|
-
enabled: current,
|
|
1138
|
-
required: currentRequired,
|
|
1139
|
-
practicesEnabled: currentPractices,
|
|
1140
|
-
client,
|
|
1141
|
-
message: 'No client vault is bound — .platform-bindings.json needs client.vaultRepo and client.vaultRef. Run /pp-init to bind it; vault.enabled was not changed.',
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
const enabledAbsent = !(nested || flat);
|
|
1145
|
-
const enabledAfter = enabledAbsent ? true : current;
|
|
1146
|
-
const requiredAbsent = !(requiredNested || requiredFlat);
|
|
1147
|
-
// An org profile that sets vault.required already decides it for this project (the gate
|
|
1148
|
-
// reads project > profile > default); arming must not override it with a project value.
|
|
1149
|
-
let profileRequired = null;
|
|
1150
|
-
if (requiredAbsent) {
|
|
1151
|
-
try {
|
|
1152
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1153
|
-
const orgProfile = require('./org-profile.cjs');
|
|
1154
|
-
const r = orgProfile.resolveVaultRequired(cwd);
|
|
1155
|
-
if (r.source === 'profile')
|
|
1156
|
-
profileRequired = r.required;
|
|
1057
|
+
if (capabilityActivation.getConfigValueEitherForm(parsed, exports.INIT_COMPOSE_DECLINED_KEY).value === true) {
|
|
1058
|
+
return {
|
|
1059
|
+
ok: true,
|
|
1060
|
+
armed: false,
|
|
1061
|
+
reason: 'compose_declined',
|
|
1062
|
+
message: 'The binding compose was declined for this project (init.compose_declined), so nothing was composed or armed.',
|
|
1063
|
+
};
|
|
1157
1064
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1065
|
+
const enabled = resolveVaultEnabledState(parsed);
|
|
1066
|
+
if (enabled.state !== 'absent' && enabled.state !== 'armed') {
|
|
1067
|
+
return {
|
|
1068
|
+
ok: true,
|
|
1069
|
+
armed: false,
|
|
1070
|
+
reason: 'vault_disabled',
|
|
1071
|
+
message: enabled.state === 'off'
|
|
1072
|
+
? 'vault.enabled is false for this project, so the vault was left off and nothing was composed or armed.'
|
|
1073
|
+
: 'vault.enabled is set but is not the boolean true, so the vault was left off and nothing was composed or armed.',
|
|
1074
|
+
};
|
|
1160
1075
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
:
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
: profileRequired !== null
|
|
1191
|
-
? profileRequired
|
|
1192
|
-
: currentRequired;
|
|
1193
|
-
const practicesAfter = Object.prototype.hasOwnProperty.call(patch, 'practices_enabled')
|
|
1194
|
-
? true
|
|
1195
|
-
: currentPractices;
|
|
1196
|
-
if (Object.keys(patch).length === 0) {
|
|
1076
|
+
if (!(0, org_profile_cjs_1.resolveOrgProfileDocument)(cwd, { _globalRoot: opts._globalRoot }).resolved) {
|
|
1077
|
+
return {
|
|
1078
|
+
ok: true,
|
|
1079
|
+
armed: false,
|
|
1080
|
+
reason: 'no_org_profile',
|
|
1081
|
+
message: 'No org profile resolves for this project, so there is no client vault to bind and nothing was written.',
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
const compose = (0, compose_bindings_cjs_1.composeBindings)(cwd, { write: true, offline: opts.offline === true, _globalRoot: opts._globalRoot });
|
|
1085
|
+
if (compose.composable !== true) {
|
|
1086
|
+
return {
|
|
1087
|
+
ok: false,
|
|
1088
|
+
armed: false,
|
|
1089
|
+
reason: 'not_composable',
|
|
1090
|
+
message: 'An org profile resolves, but no valid client-vault binding could be composed from it, so the vault was NOT armed. ' +
|
|
1091
|
+
ARM_VAULT_REMEDY,
|
|
1092
|
+
warnings: compose.warnings,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
if ((compose.action === 'create' || compose.action === 'update') && compose.wrote !== true) {
|
|
1096
|
+
return {
|
|
1097
|
+
ok: false,
|
|
1098
|
+
armed: false,
|
|
1099
|
+
reason: 'compose_write_failed',
|
|
1100
|
+
message: 'The binding was composed but .platform-bindings.json was not written, so the vault was NOT armed. ' + ARM_VAULT_REMEDY,
|
|
1101
|
+
warnings: compose.warnings,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const vaultKeysArmed = armVaultKeysWhereAbsent(cwd);
|
|
1197
1105
|
return {
|
|
1198
|
-
ok: true,
|
|
1199
|
-
|
|
1106
|
+
ok: true,
|
|
1107
|
+
armed: true,
|
|
1108
|
+
profile: {
|
|
1109
|
+
packageName: compose.profile.packageName,
|
|
1110
|
+
version: compose.profile.version,
|
|
1111
|
+
source: compose.profile.source,
|
|
1112
|
+
},
|
|
1113
|
+
compose: { action: compose.action, wrote: compose.wrote },
|
|
1114
|
+
vaultKeysArmed,
|
|
1115
|
+
// VAULT-EGRESS-1: the written file holds the raw coordinate; the report is display-safe.
|
|
1116
|
+
provenance: (0, compose_bindings_cjs_1.redactComposeResultForEmission)(compose).provenance,
|
|
1117
|
+
warnings: compose.warnings,
|
|
1118
|
+
note: 'Project identity (project.*) was not captured by this step; /pp-init completes it.',
|
|
1200
1119
|
};
|
|
1201
1120
|
}
|
|
1202
|
-
try {
|
|
1203
|
-
writeConfigMerge(cwd, { vault: patch });
|
|
1204
|
-
}
|
|
1205
1121
|
catch (err) {
|
|
1206
1122
|
return {
|
|
1207
|
-
ok: false,
|
|
1208
|
-
|
|
1123
|
+
ok: false,
|
|
1124
|
+
armed: false,
|
|
1125
|
+
reason: 'write_failed',
|
|
1126
|
+
message: writeFailedMessage(err) + ' ' + ARM_VAULT_REMEDY,
|
|
1127
|
+
warnings: [],
|
|
1209
1128
|
};
|
|
1210
1129
|
}
|
|
1211
|
-
return {
|
|
1212
|
-
ok: true, armed: true, enabled: enabledAfter, required: requiredAfter, practicesEnabled: practicesAfter, client,
|
|
1213
|
-
message: `${clauses.join('; ')}.`,
|
|
1214
|
-
};
|
|
1215
1130
|
}
|
|
1216
1131
|
// ─── cmdInitContract ────────────────────────────────────────────────────────
|
|
1217
|
-
/**
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
* `--verify` runs `verifyInitInvariants` and always prints the full result
|
|
1221
|
-
* as data (so `init.md` can read and show every violation verbatim), then
|
|
1222
|
-
* sets `process.exitCode = 1` when the result is not `ok` — a data
|
|
1223
|
-
* verdict, never an `error()` usage failure. This is the load-bearing
|
|
1224
|
-
* property Task 2's acceptance requires: a caller cannot mistake a failed
|
|
1225
|
-
* check for a pass, but the JSON body still reaches stdout either way.
|
|
1226
|
-
*
|
|
1227
|
-
* `--questions` (also the no-flag default, matching this seam's
|
|
1228
|
-
* safe-default convention — mirrors `compose-bindings`'s dry-run default)
|
|
1229
|
-
* is read-only and always exits 0.
|
|
1230
|
-
*
|
|
1231
|
-
* `--session-reset` / `--session-answer` / `--session-next` (34-08 gap
|
|
1232
|
-
* closure) drive the session state machine above. Precedence — reset,
|
|
1233
|
-
* then answer, then next, then verify, then the questions default — mirrors
|
|
1234
|
-
* the order a relay loop actually calls them in (an agent never sends more
|
|
1235
|
-
* than one of these per invocation; the order only matters for a caller
|
|
1236
|
-
* that somehow set two, which should never happen from `pp-tools.cjs`'s own
|
|
1237
|
-
* parsing). `--session-answer` and `--session-next` both set
|
|
1238
|
-
* `process.exitCode = 1` on a data verdict that means "not ready to
|
|
1239
|
-
* proceed" (`ok: false` / `error` present) — same never-a-usage-failure
|
|
1240
|
-
* convention as `--verify`.
|
|
1241
|
-
*/
|
|
1132
|
+
/** CLI adapter for `pp-tools query init-contract`: the session, record, arm-vault, verify and questions flags.
|
|
1133
|
+
* Data verdicts set `exitCode = 1` when not ok, but the JSON result is always printed. */
|
|
1242
1134
|
function cmdInitContract(cwd, options, raw) {
|
|
1243
|
-
if (options.armVault === true) {
|
|
1244
|
-
const result = armVault(cwd);
|
|
1245
|
-
output(result, raw);
|
|
1246
|
-
if (!result.ok) {
|
|
1247
|
-
process.exitCode = 1;
|
|
1248
|
-
}
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
1135
|
if (options.sessionReset === true) {
|
|
1252
1136
|
const result = initSessionReset(cwd);
|
|
1253
1137
|
output(result, raw);
|
|
@@ -1271,6 +1155,32 @@ function cmdInitContract(cwd, options, raw) {
|
|
|
1271
1155
|
}
|
|
1272
1156
|
return;
|
|
1273
1157
|
}
|
|
1158
|
+
// Record writes run before `--verify`, so a combined call verifies the written state.
|
|
1159
|
+
if (options.recordProbe !== undefined) {
|
|
1160
|
+
const result = recordProbeOutcome(cwd, options.recordProbe);
|
|
1161
|
+
output(result, raw);
|
|
1162
|
+
if (!result.ok) {
|
|
1163
|
+
process.exitCode = 1;
|
|
1164
|
+
}
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (options.deferProbe !== undefined) {
|
|
1168
|
+
const result = recordProbeDeferral(cwd, options.deferProbe);
|
|
1169
|
+
output(result, raw);
|
|
1170
|
+
if (!result.ok) {
|
|
1171
|
+
process.exitCode = 1;
|
|
1172
|
+
}
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
// An inert `armed: false` is a data verdict on exit 0, like the inert init path; only `ok: false` exits 1.
|
|
1176
|
+
if (options.armVault === true) {
|
|
1177
|
+
const result = armVaultFromProfile(cwd, { offline: options.offline === true });
|
|
1178
|
+
output(result, raw);
|
|
1179
|
+
if (result.ok === false) {
|
|
1180
|
+
process.exitCode = 1;
|
|
1181
|
+
}
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1274
1184
|
if (options.verify === true) {
|
|
1275
1185
|
const result = verifyInitInvariants(cwd);
|
|
1276
1186
|
output(result, raw);
|