@pareto4/pp-core 1.13.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +3 -3
- package/README.md +31 -12
- package/agents/pp-code-fixer.md +0 -11
- package/agents/pp-code-reviewer.md +1 -77
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +12 -29
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +34 -20
- package/agents/pp-vault-reviewer.md +100 -23
- package/agents/pp-verifier.md +1 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-manage.md +1 -1
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-promote.md +7 -8
- package/commands/pp-vault-review.md +6 -5
- 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 +10 -18
- 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 +201 -229
- package/pp-core/bin/lib/capability-state.cjs +93 -1
- package/pp-core/bin/lib/capability-writer.cjs +3 -3
- package/pp-core/bin/lib/check-command-router.cjs +1906 -1514
- 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 +151 -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 +11 -12
- package/pp-core/bin/lib/git-provider.cjs +9 -10
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +620 -738
- package/pp-core/bin/lib/init-interactive.cjs +62 -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 +4 -2
- package/pp-core/bin/lib/loop-resolver.cjs +165 -129
- package/pp-core/bin/lib/org-profile.cjs +402 -161
- 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/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-command-router.cjs +1682 -477
- package/pp-core/bin/lib/vault-coordinates.cjs +16 -7
- package/pp-core/bin/lib/vault-index.cjs +656 -0
- package/pp-core/bin/lib/vault-seal.cjs +1851 -0
- package/pp-core/bin/lib/vault-standards-promote.cjs +577 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +293 -0
- package/pp-core/bin/lib/vault-standards.cjs +1504 -0
- package/pp-core/bin/lib/vault-trace.cjs +155 -2778
- package/pp-core/bin/lib/vault-update.cjs +10 -59
- package/pp-core/bin/lib/vault.cjs +2570 -950
- 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 +157 -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-standards.md +311 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +56 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- 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 -57
- 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 +207 -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 +327 -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 +78 -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 +219 -20
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +147 -0
- package/pp-core/workflows/execute-phase.md +152 -128
- package/pp-core/workflows/execute-plan.md +54 -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 +161 -23
- package/pp-core/workflows/vault-write-back.md +73 -110
- 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 +120 -13
- 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 +3 -19
- package/scripts/lint-test-file-count.cjs +5 -6
- package/scripts/prompt-injection-scan.sh +19 -17
- 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/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/bin/lib/practices-review-check.cjs +0 -938
- package/pp-core/bin/lib/vault-audit.cjs +0 -2070
- package/pp-core/bin/lib/vault-practices.cjs +0 -1990
- package/pp-core/bin/lib/vault-review.cjs +0 -2359
- package/pp-core/bin/lib/vault-write-back.cjs +0 -877
- package/pp-core/references/executor-vault-trace.md +0 -47
- package/pp-core/references/planner-plan-graph.md +0 -66
- package/pp-core/references/planner-vault-practices.md +0 -112
- package/pp-core/templates/practices/README.md +0 -251
- package/pp-core/templates/practices/error-handling.md +0 -18
- package/pp-core/templates/practices/minimal-comments.md +0 -14
- package/pp-core/templates/practices/naming-convention.md +0 -18
- package/pp-core/templates/practices/practice-template.md +0 -23
- 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,75 @@
|
|
|
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.resolveVaultRequiredState = resolveVaultRequiredState;
|
|
11
|
+
exports.readVaultEnabledState = readVaultEnabledState;
|
|
12
|
+
exports.readVaultRequiredState = readVaultRequiredState;
|
|
48
13
|
exports.buildInitQuestions = buildInitQuestions;
|
|
14
|
+
exports.deriveSlug = deriveSlug;
|
|
15
|
+
exports.readProfileInitAnswers = readProfileInitAnswers;
|
|
49
16
|
exports.verifyInitInvariants = verifyInitInvariants;
|
|
17
|
+
exports.armVaultKeysWhereAbsent = armVaultKeysWhereAbsent;
|
|
18
|
+
exports.recordProbeOutcome = recordProbeOutcome;
|
|
19
|
+
exports.recordProbeDeferral = recordProbeDeferral;
|
|
50
20
|
exports.initSessionNext = initSessionNext;
|
|
51
21
|
exports.initSessionAnswer = initSessionAnswer;
|
|
52
22
|
exports.initSessionReset = initSessionReset;
|
|
53
|
-
exports.
|
|
23
|
+
exports.armVaultFromProfile = armVaultFromProfile;
|
|
54
24
|
exports.cmdInitContract = cmdInitContract;
|
|
55
25
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
56
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
57
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
28
|
const io = require("./io.cjs");
|
|
59
29
|
const { output } = io;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const capabilityActivation = require("./capability-activation.cjs");
|
|
60
32
|
const git_provider_cjs_1 = require("./git-provider.cjs");
|
|
61
33
|
const org_profile_cjs_1 = require("./org-profile.cjs");
|
|
34
|
+
const platform_bindings_cjs_1 = require("./platform-bindings.cjs");
|
|
62
35
|
const compose_bindings_cjs_1 = require("./compose-bindings.cjs");
|
|
63
36
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
64
37
|
// ─── 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
|
-
*/
|
|
38
|
+
/** Set when the operator declines `compose_confirm` — the only way to tell "declined" from "never ran". */
|
|
76
39
|
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
|
-
*/
|
|
40
|
+
/** Legacy upstream marker for a skipped org profile; nothing writes it any more, kept registered so old configs load. */
|
|
91
41
|
exports.INIT_ORG_PROFILE_DECLINED_KEY = 'init.org_profile_declined';
|
|
92
|
-
/** The
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
42
|
+
/** The /pp-init Step 2.6 reachability-proof record (status, when, who, note), read back by `--verify`.
|
|
43
|
+
* Registered vault config keys; never profile-settable and not for operator editing. */
|
|
44
|
+
exports.INIT_PROBE_STATUS_KEY = 'init.probe_status';
|
|
45
|
+
/** ISO-8601 UTC instant the record was written. See `INIT_PROBE_STATUS_KEY`. */
|
|
46
|
+
exports.INIT_PROBE_WHEN_KEY = 'init.probe_when';
|
|
47
|
+
/** Who recorded the proof; never empty (falls back to the literal `unknown`). */
|
|
48
|
+
exports.INIT_PROBE_WHO_KEY = 'init.probe_who';
|
|
49
|
+
/** The bounded human note: the probe's own summary on `passed`/`failed`, the
|
|
50
|
+
* operator's stated reason on `deferred`. See `INIT_PROBE_STATUS_KEY`. */
|
|
51
|
+
exports.INIT_PROBE_NOTE_KEY = 'init.probe_note';
|
|
52
|
+
/** Probe status vocabulary; `deferred` is only recordable through `recordProbeDeferral`, which requires a reason. */
|
|
53
|
+
exports.INIT_PROBE_STATUS = Object.freeze({
|
|
54
|
+
PASSED: 'passed',
|
|
55
|
+
FAILED: 'failed',
|
|
56
|
+
DEFERRED: 'deferred',
|
|
57
|
+
});
|
|
58
|
+
/** Provider choices that only record identity, appended after `PROVIDERS` (never a retyped copy of it). */
|
|
59
|
+
const IDENTITY_ONLY_PROVIDER_OPTIONS = Object.freeze(['other', 'none']);
|
|
100
60
|
const INVARIANT_CODE = Object.freeze({
|
|
101
|
-
ORG_PROFILE_UNASKED: 'INIT_QUESTIONS_ORG_PROFILE_UNASKED',
|
|
102
61
|
COMPOSITION_NOT_RUN: 'INIT_COMPOSITION_NOT_RUN',
|
|
103
62
|
VAULT_PROVIDER_UNSET: 'INIT_VAULT_PROVIDER_UNSET',
|
|
104
63
|
IDENTITY_MISSING: 'INIT_IDENTITY_MISSING',
|
|
64
|
+
/** Reachability proof never run, failed, or consciously deferred — three states that need three different responses. */
|
|
65
|
+
PROBE_NOT_RUN: 'INIT_PROBE_NOT_RUN',
|
|
66
|
+
PROBE_FAILED: 'INIT_PROBE_FAILED',
|
|
67
|
+
PROBE_DEFERRED: 'INIT_PROBE_DEFERRED',
|
|
68
|
+
/** `vault.enabled` is present but not a JSON boolean, so the activation resolver and the proof check disagree. */
|
|
69
|
+
VAULT_ENABLED_MALFORMED: 'INIT_VAULT_ENABLED_MALFORMED',
|
|
105
70
|
});
|
|
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`. */
|
|
71
|
+
/** Flags the `init-contract` CLI arm accepts; `--session-answer`, `--record-probe` and `--defer-probe` take the next token as payload.
|
|
72
|
+
* `--arm-vault` (quick 261003-kmd) is the non-interactive compose-and-arm `/pp-new-project` calls. */
|
|
114
73
|
exports.INIT_CONTRACT_FLAGS = Object.freeze([
|
|
115
74
|
'--questions',
|
|
116
75
|
'--verify',
|
|
@@ -118,39 +77,21 @@ exports.INIT_CONTRACT_FLAGS = Object.freeze([
|
|
|
118
77
|
'--session-answer',
|
|
119
78
|
'--session-reset',
|
|
120
79
|
'--offline',
|
|
80
|
+
'--record-probe',
|
|
81
|
+
'--defer-probe',
|
|
121
82
|
'--arm-vault',
|
|
122
83
|
]);
|
|
123
|
-
/**
|
|
124
|
-
* (Artifacts table, 34-08). Never hand-edited; malformed content is
|
|
125
|
-
* refused, not silently repaired (T-34-63). */
|
|
84
|
+
/** CLI-owned session state file; malformed content is refused, never repaired. */
|
|
126
85
|
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. */
|
|
86
|
+
/** Reported when the session file is malformed; the only recovery is `--session-reset`. */
|
|
131
87
|
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. */
|
|
88
|
+
/** Reported when a session write fails (e.g. an unwritable `.planning`); fix the cause and retry the same step. */
|
|
143
89
|
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). */
|
|
90
|
+
/** `compose_confirm` answers; anything else is rejected, never treated as a decline. */
|
|
147
91
|
const YES_VALUES = Object.freeze(['yes', 'y', 'approve', 'approved', 'true']);
|
|
148
92
|
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. */
|
|
93
|
+
/** Maps each of the 7 identity/provider/visibility question ids to the short
|
|
94
|
+
* key it is written under inside `.planning/config.json`'s `project` block. */
|
|
154
95
|
const PROJECT_BLOCK_QUESTION_MAP = Object.freeze({
|
|
155
96
|
project_name: 'name',
|
|
156
97
|
project_slug: 'slug',
|
|
@@ -160,9 +101,7 @@ const PROJECT_BLOCK_QUESTION_MAP = Object.freeze({
|
|
|
160
101
|
provider: 'provider',
|
|
161
102
|
visibility: 'visibility',
|
|
162
103
|
});
|
|
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) ────────────────
|
|
104
|
+
// ─── Small local helpers (duplicated per module to avoid require cycles) ──────────
|
|
166
105
|
function isObject(v) {
|
|
167
106
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
168
107
|
}
|
|
@@ -172,35 +111,121 @@ function isUsableString(v) {
|
|
|
172
111
|
function getDotted(obj, dottedPath) {
|
|
173
112
|
return dottedPath.split('.').reduce((cur, key) => (isObject(cur) ? cur[key] : undefined), obj);
|
|
174
113
|
}
|
|
175
|
-
|
|
114
|
+
/** Arms the vault capability; written by the session machine, never by an org profile. */
|
|
115
|
+
const VAULT_ENABLED_CONFIG_KEY = 'vault.enabled';
|
|
116
|
+
/** The single reading of `vault.enabled` in this module, in the form the gate honours
|
|
117
|
+
* (`resolveGateHonouredKeyState`, Phase 15 D-32). `value` is the observed value, never a
|
|
118
|
+
* coercion, so reports name what is really in the file. */
|
|
119
|
+
function resolveVaultEnabledState(parsed) {
|
|
120
|
+
return resolveGateHonouredKeyState(parsed, VAULT_ENABLED_CONFIG_KEY);
|
|
121
|
+
}
|
|
122
|
+
/** Four-state reading of the project's OWN `vault.required` (not the profile-layered gate value),
|
|
123
|
+
* so the arming guard respects an explicit per-project `false`. */
|
|
124
|
+
function resolveVaultRequiredState(parsed) {
|
|
125
|
+
return resolveBooleanKeyState(parsed, org_profile_cjs_1.VAULT_REQUIRED_CONFIG_KEY);
|
|
126
|
+
}
|
|
127
|
+
/** Classify a PRESENT value into the three present states. */
|
|
128
|
+
function classifyPresentValue(value) {
|
|
129
|
+
if (value === true)
|
|
130
|
+
return { state: 'armed', value };
|
|
131
|
+
if (value === false)
|
|
132
|
+
return { state: 'off', value };
|
|
133
|
+
return { state: 'malformed', value };
|
|
134
|
+
}
|
|
135
|
+
/** Either-form four-state reader. Used for `vault.required`, whose gate-side ladder
|
|
136
|
+
* (`resolveVaultRequired`, `src/org-profile.cts`) reads both key forms. */
|
|
137
|
+
function resolveBooleanKeyState(parsed, key) {
|
|
138
|
+
const hit = capabilityActivation.getConfigValueEitherForm(parsed, key);
|
|
139
|
+
if (hit.found !== true)
|
|
140
|
+
return { state: 'absent', value: undefined };
|
|
141
|
+
return classifyPresentValue(hit.value);
|
|
142
|
+
}
|
|
176
143
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* is a complete, valid response), never whether the question is asked.
|
|
184
|
-
*
|
|
185
|
-
* Question 6 (`org_profile`) names a HINT, never a default: when
|
|
186
|
-
* `resolveDetectedOrgProfile` finds exactly one org-profile-shaped package
|
|
187
|
-
* installed (INIT-PATCH-C), the prompt names it, Enter skips, and typing its
|
|
188
|
-
* name (or any other) selects it — the question carries no `preselected`.
|
|
189
|
-
* The prompt text is built from that detected value rather than kept as two
|
|
190
|
-
* hard-coded strings that can drift.
|
|
191
|
-
* `opts._globalRoot` is forwarded to it unchanged, the same test-seam
|
|
192
|
-
* convention `verifyInitInvariants`'s own `_globalRoot` already follows —
|
|
193
|
-
* production callers omit it.
|
|
144
|
+
* Four-state reading of a vault switch whose gates read ONLY the nested form (Phase 15
|
|
145
|
+
* D-32): `vault.enabled`. `loadConfig` materialises its nested default, so the gates' evaluator answers at its first level and never reaches a
|
|
146
|
+
* hand-written FLAT key (it warns that the key is ignored). Reading the flat form as
|
|
147
|
+
* `armed` therefore told /pp-init that a switch was on while no gate rendered. The
|
|
148
|
+
* absent-only arming then skipped it, and the done line promised governance nobody
|
|
149
|
+
* enforced.
|
|
194
150
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* calling it directly here is equivalent, not a second implementation.
|
|
151
|
+
* - nested present: its value decides, exactly as before;
|
|
152
|
+
* - flat-only `true`: `absent`, because the gate sees nothing. The arming writes the
|
|
153
|
+
* canonical nested key, which is what the operator asked for;
|
|
154
|
+
* - flat-only `false`: `off`. The gate reads off too, and an explicit false is never
|
|
155
|
+
* overwritten (07-09 WR-07);
|
|
156
|
+
* - flat-only anything else: `malformed`, kept and reported, never repaired.
|
|
202
157
|
*/
|
|
203
|
-
function
|
|
158
|
+
function resolveGateHonouredKeyState(parsed, key) {
|
|
159
|
+
const nested = capabilityActivation._getNestedConfigValue(parsed, key);
|
|
160
|
+
if (nested.found === true)
|
|
161
|
+
return classifyPresentValue(nested.value);
|
|
162
|
+
const flat = capabilityActivation._getFlatConfigValue(parsed, key);
|
|
163
|
+
if (flat.found !== true || flat.value === true)
|
|
164
|
+
return { state: 'absent', value: undefined };
|
|
165
|
+
return classifyPresentValue(flat.value);
|
|
166
|
+
}
|
|
167
|
+
/** The vault adapter's gate-side readers, required lazily: pp-tools loads this module on
|
|
168
|
+
* every command and the vault adapter is large. vault.cjs never requires this module, so
|
|
169
|
+
* there is no cycle. */
|
|
170
|
+
function vaultGateReaders() {
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
172
|
+
const vault = require('./vault.cjs');
|
|
173
|
+
return vault;
|
|
174
|
+
}
|
|
175
|
+
/** Ask one gate-side reader; any throw (including the require) reads as OFF. */
|
|
176
|
+
function gateReadsOn(read) {
|
|
177
|
+
try {
|
|
178
|
+
return read(vaultGateReaders()) === true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The project-at-`cwd` reading the done line prints from (Phase 15 D-32): `armed` exactly
|
|
186
|
+
* when the gate-side reader is ON, so the line can never name a switch the gates do not
|
|
187
|
+
* honour. The root file's own four-state reading supplies everything else.
|
|
188
|
+
* - gate ON, root armed → the root reading;
|
|
189
|
+
* - gate ON, root not armed → `armed` (another config layer, a workstream config, arms
|
|
190
|
+
* it; `value` is the gate's reading, `true`);
|
|
191
|
+
* - gate OFF, root armed → `off` (a workstream config overrides it, or the active
|
|
192
|
+
* workstream has no config); `value` stays the root's;
|
|
193
|
+
* - gate OFF otherwise → the root reading (absent, off or malformed).
|
|
194
|
+
*/
|
|
195
|
+
function agreeWithGate(root, gateOn) {
|
|
196
|
+
if (gateOn)
|
|
197
|
+
return root.state === 'armed' ? root : { state: 'armed', value: true };
|
|
198
|
+
return root.state === 'armed' ? { state: 'off', value: root.value } : root;
|
|
199
|
+
}
|
|
200
|
+
/** The root config file's four-state reading through `resolve`; unreadable reads as `absent`. */
|
|
201
|
+
function readRootKeyState(cwd, resolve) {
|
|
202
|
+
try {
|
|
203
|
+
const { parsed } = readConfigFile(cwd);
|
|
204
|
+
return resolve(parsed);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { state: 'absent', value: undefined };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** `vault.enabled` for the project at `cwd`: `armed` exactly when `isVaultGateActive` is
|
|
211
|
+
* (`agreeWithGate`); an unreadable config reads as `absent`. Never throws. */
|
|
212
|
+
function readVaultEnabledState(cwd) {
|
|
213
|
+
return agreeWithGate(readRootKeyState(cwd, resolveVaultEnabledState), gateReadsOn((v) => v.isVaultGateActive(cwd)));
|
|
214
|
+
}
|
|
215
|
+
/** `vault.required` for the project at `cwd`: `armed` exactly when the load gate's own
|
|
216
|
+
* answer (`resolveVaultRequiredFlag`) is true. Never throws. */
|
|
217
|
+
function readVaultRequiredState(cwd) {
|
|
218
|
+
return agreeWithGate(readRootKeyState(cwd, resolveVaultRequiredState), gateReadsOn((v) => v.resolveVaultRequiredFlag(cwd)));
|
|
219
|
+
}
|
|
220
|
+
/** True only for the JSON boolean `true` — a truthy string does not count.
|
|
221
|
+
* A non-boolean value is reported by `verifyInitInvariants` as `INIT_VAULT_ENABLED_MALFORMED`. */
|
|
222
|
+
function isVaultEnabledInConfig(parsed) {
|
|
223
|
+
return resolveVaultEnabledState(parsed).state === 'armed';
|
|
224
|
+
}
|
|
225
|
+
// ─── buildInitQuestions ────────────────────────────────────────────────────
|
|
226
|
+
/** The canonical /pp-init question set, as data; the session may answer some itself (see "Automatic answers").
|
|
227
|
+
* No org-profile question in this fork (PBI 74316); provider options come from `PROVIDERS`, preselected via `resolveProvider`. */
|
|
228
|
+
function buildInitQuestions(cwd) {
|
|
204
229
|
let preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
|
|
205
230
|
try {
|
|
206
231
|
preselected = (0, git_provider_cjs_1.resolveProvider)(cwd).provider;
|
|
@@ -209,31 +234,6 @@ function buildInitQuestions(cwd, opts = {}) {
|
|
|
209
234
|
preselected = git_provider_cjs_1.DEFAULT_PROVIDER;
|
|
210
235
|
}
|
|
211
236
|
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
237
|
return [
|
|
238
238
|
{ id: 'project_name', prompt: 'Project name (human-readable, e.g. "Acme Dashboard")', optionalAnswer: false },
|
|
239
239
|
{ id: 'project_slug', prompt: 'Project slug (kebab-case, e.g. "acme-dashboard")', optionalAnswer: false },
|
|
@@ -248,7 +248,6 @@ function buildInitQuestions(cwd, opts = {}) {
|
|
|
248
248
|
optionalAnswer: false,
|
|
249
249
|
},
|
|
250
250
|
{ id: 'description', prompt: 'One-line description (what this project does)', optionalAnswer: false },
|
|
251
|
-
orgProfileQuestion,
|
|
252
251
|
{
|
|
253
252
|
id: 'provider',
|
|
254
253
|
prompt: 'Provider (gitlab / azure / github / other / none)',
|
|
@@ -260,179 +259,88 @@ function buildInitQuestions(cwd, opts = {}) {
|
|
|
260
259
|
{ id: 'visibility', prompt: 'Visibility (private / public / internal)', optionalAnswer: false },
|
|
261
260
|
];
|
|
262
261
|
}
|
|
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
|
-
}
|
|
262
|
+
// ─── Automatic answers (PBI 74299): an org profile init section fixes answers and can derive slugs ───
|
|
263
|
+
/** The visibility answers the question accepts. */
|
|
264
|
+
const VISIBILITY_OPTIONS = Object.freeze(['private', 'public', 'internal']);
|
|
265
|
+
/** Longest client name an org profile may fix — the composer's seed-value bound. */
|
|
266
|
+
const INIT_FIXED_VALUE_MAX_LENGTH = 200;
|
|
267
|
+
function hasControlChar(value) {
|
|
268
|
+
for (let i = 0; i < value.length; i++) {
|
|
269
|
+
const c = value.charCodeAt(i);
|
|
270
|
+
if (c <= 0x1f || c === 0x7f)
|
|
271
|
+
return true;
|
|
345
272
|
}
|
|
346
|
-
return
|
|
273
|
+
return false;
|
|
347
274
|
}
|
|
348
|
-
/**
|
|
349
|
-
*
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* The global roots DETECTION scans — the org-profile hint and the UNASKED
|
|
361
|
-
* check — from the one candidate builder in `src/org-profile.cts`, never a
|
|
362
|
-
* private copy (the single-root duplicate that lived here missed every
|
|
363
|
-
* Homebrew layout). `explicitPrefixOnly`: an explicit npm prefix is
|
|
364
|
-
* authoritative, so a hint or a violation never comes from a tree the
|
|
365
|
-
* operator did not point at. The `_globalRoot` seam still wins.
|
|
366
|
-
*/
|
|
367
|
-
function detectionGlobalRoots(opts) {
|
|
368
|
-
return (0, org_profile_cjs_1.globalRootCandidatesFor)({ _globalRoot: opts._globalRoot, explicitPrefixOnly: true });
|
|
275
|
+
/** Derive a slug: diacritics stripped, lowercased, other characters collapsed to "-".
|
|
276
|
+
* Returns `null` when the result fails `SLUG_RE`, so the slug is asked instead. */
|
|
277
|
+
function deriveSlug(name) {
|
|
278
|
+
const slug = name
|
|
279
|
+
.normalize('NFKD')
|
|
280
|
+
.replace(/[̀-ͯ]/g, '')
|
|
281
|
+
.toLowerCase()
|
|
282
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
283
|
+
.replace(/^-+|-+$/g, '');
|
|
284
|
+
return slug !== '' && platform_bindings_cjs_1.SLUG_RE.test(slug) ? slug : null;
|
|
369
285
|
}
|
|
370
|
-
|
|
286
|
+
/** The validated init section of the org profile; invalid values are dropped so their question is asked. Never throws. */
|
|
287
|
+
function readProfileInitAnswers(cwd, opts = {}) {
|
|
288
|
+
const none = { fixed: {}, deriveSlugs: false };
|
|
371
289
|
try {
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
return
|
|
290
|
+
const doc = (0, org_profile_cjs_1.resolveOrgProfileDocument)(cwd, { _globalRoot: opts._globalRoot });
|
|
291
|
+
if (!doc.resolved)
|
|
292
|
+
return none;
|
|
293
|
+
const section = doc.document[org_profile_cjs_1.ORG_PROFILE_INIT_SECTION];
|
|
294
|
+
if (!isObject(section))
|
|
295
|
+
return none;
|
|
296
|
+
const fixed = {};
|
|
297
|
+
const clientName = section.client_name;
|
|
298
|
+
if (typeof clientName === 'string' &&
|
|
299
|
+
clientName.trim() !== '' &&
|
|
300
|
+
clientName.length <= INIT_FIXED_VALUE_MAX_LENGTH &&
|
|
301
|
+
!hasControlChar(clientName)) {
|
|
302
|
+
fixed.client_name = clientName.trim();
|
|
375
303
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
304
|
+
const providerOptions = [...git_provider_cjs_1.PROVIDERS, ...IDENTITY_ONLY_PROVIDER_OPTIONS];
|
|
305
|
+
if (typeof section.provider === 'string' && providerOptions.includes(section.provider.trim())) {
|
|
306
|
+
fixed.provider = section.provider.trim();
|
|
380
307
|
}
|
|
381
|
-
|
|
308
|
+
if (typeof section.visibility === 'string' && VISIBILITY_OPTIONS.includes(section.visibility.trim())) {
|
|
309
|
+
fixed.visibility = section.visibility.trim();
|
|
310
|
+
}
|
|
311
|
+
return { fixed, deriveSlugs: section.derive_slugs === true };
|
|
382
312
|
}
|
|
383
313
|
catch {
|
|
384
|
-
return
|
|
314
|
+
return none;
|
|
385
315
|
}
|
|
386
316
|
}
|
|
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;
|
|
317
|
+
/** The answer the session fills in itself for question `id`, or `null` to ask it. */
|
|
318
|
+
function autoAnswerFor(id, answers, init) {
|
|
319
|
+
if (id === 'client_name' || id === 'provider' || id === 'visibility') {
|
|
320
|
+
const value = init.fixed[id];
|
|
321
|
+
return value === undefined ? null : { value, source: 'org-profile' };
|
|
419
322
|
}
|
|
420
|
-
|
|
421
|
-
|
|
323
|
+
if (init.deriveSlugs && (id === 'project_slug' || id === 'client_slug')) {
|
|
324
|
+
const name = answers[id === 'project_slug' ? 'project_name' : 'client_name'];
|
|
325
|
+
const slug = typeof name === 'string' ? deriveSlug(name) : null;
|
|
326
|
+
return slug === null ? null : { value: slug, source: 'derived' };
|
|
422
327
|
}
|
|
328
|
+
return null;
|
|
423
329
|
}
|
|
424
|
-
/** The
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
330
|
+
/** The session's automatic answers, in question order. */
|
|
331
|
+
function listAutoAnswers(cwd, session) {
|
|
332
|
+
const sources = session.autoAnswers ?? {};
|
|
333
|
+
const out = [];
|
|
334
|
+
for (const q of buildInitQuestions(cwd)) {
|
|
335
|
+
const source = sources[q.id];
|
|
336
|
+
const value = session.answers[q.id];
|
|
337
|
+
if (source !== undefined && typeof value === 'string')
|
|
338
|
+
out.push({ id: q.id, value, source });
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
431
341
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
* on the latter (see `verifyInitInvariants`'s own doc comment for why an
|
|
435
|
-
* absent file must never trip it). Never throws. */
|
|
342
|
+
// ─── verifyInitInvariants ───────────────────────────────────────────────────
|
|
343
|
+
/** Read `.planning/config.json`, telling "absent" from "present but malformed". Never throws. */
|
|
436
344
|
function readConfigFile(cwd) {
|
|
437
345
|
const configPath = node_path_1.default.join(cwd, '.planning', 'config.json');
|
|
438
346
|
let exists = false;
|
|
@@ -451,26 +359,8 @@ function readConfigFile(cwd) {
|
|
|
451
359
|
return { exists, parsed: {} };
|
|
452
360
|
}
|
|
453
361
|
}
|
|
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
|
-
*/
|
|
362
|
+
/** Report every invariant the project's state at `cwd` violates. Read-only and never throws;
|
|
363
|
+
* each check needs a positive signal that init was attempted, so an empty directory is ok. */
|
|
474
364
|
function verifyInitInvariants(cwd, opts = {}) {
|
|
475
365
|
const violations = [];
|
|
476
366
|
const { exists: configExists, parsed } = readConfigFile(cwd);
|
|
@@ -491,14 +381,11 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
491
381
|
catch {
|
|
492
382
|
// fail open — one check's fault must not suppress the others
|
|
493
383
|
}
|
|
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.
|
|
384
|
+
// `project.provider` names a vault provider but `vault.provider` was never set.
|
|
498
385
|
try {
|
|
499
386
|
const projectProvider = getDotted(parsed, 'project.provider');
|
|
500
387
|
const vaultProvider = getDotted(parsed, 'vault.provider');
|
|
501
|
-
if ((projectProvider
|
|
388
|
+
if (git_provider_cjs_1.PROVIDERS.includes(projectProvider) && !isUsableString(vaultProvider)) {
|
|
502
389
|
violations.push({
|
|
503
390
|
code: INVARIANT_CODE.VAULT_PROVIDER_UNSET,
|
|
504
391
|
message: `project.provider is "${String(projectProvider)}" but vault.provider is unset — the D-13 provisioning gate is disabled.`,
|
|
@@ -508,31 +395,7 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
508
395
|
catch {
|
|
509
396
|
// fail open
|
|
510
397
|
}
|
|
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.
|
|
398
|
+
// A binding was composable but never written, and no decline was recorded.
|
|
536
399
|
try {
|
|
537
400
|
const bindingsPath = node_path_1.default.join(cwd, '.platform-bindings.json');
|
|
538
401
|
const bindingsExist = node_fs_1.default.existsSync(bindingsPath);
|
|
@@ -542,7 +405,8 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
542
405
|
if (dryRun.composable === true) {
|
|
543
406
|
violations.push({
|
|
544
407
|
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.'
|
|
408
|
+
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. ' +
|
|
409
|
+
'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
410
|
});
|
|
547
411
|
}
|
|
548
412
|
}
|
|
@@ -550,32 +414,111 @@ function verifyInitInvariants(cwd, opts = {}) {
|
|
|
550
414
|
catch {
|
|
551
415
|
// fail open
|
|
552
416
|
}
|
|
417
|
+
// `vault.enabled` present but not a boolean: reported, never repaired (the session scope is exempt).
|
|
418
|
+
try {
|
|
419
|
+
if (opts.scope !== 'session' && configExists) {
|
|
420
|
+
const vaultEnabled = resolveVaultEnabledState(parsed);
|
|
421
|
+
if (vaultEnabled.state === 'malformed') {
|
|
422
|
+
// Bounded like every record field; JSON.stringify keeps the value's type visible.
|
|
423
|
+
const rendered = overrideHelpers().sanitizeOverrideReason(String(JSON.stringify(vaultEnabled.value)));
|
|
424
|
+
// Phase 15 D-32: what this value does to the gates is fixed BY CONSTRUCTION, so it is
|
|
425
|
+
// stated, not mirrored and not measured here (measuring would call loadConfig, which
|
|
426
|
+
// spawns git, and this check spawns nothing). The gates' config loader resolves vault
|
|
427
|
+
// keys through federated-config, which accepts only a JSON boolean and falls back to the
|
|
428
|
+
// default (off) for anything else. The old mirror, Boolean(value), told the operator
|
|
429
|
+
// that every truthy non-boolean armed the gates; the shipped gate armed on none of them.
|
|
430
|
+
const split = 'The config loader the vault gates read accepts only a JSON boolean and falls back to ' +
|
|
431
|
+
'off, so from this value the two blocking vault gates do NOT arm, and /pp-init Step 2.6 ' +
|
|
432
|
+
'and this reachability check require the boolean true, so they are inert too. ';
|
|
433
|
+
violations.push({
|
|
434
|
+
code: INVARIANT_CODE.VAULT_ENABLED_MALFORMED,
|
|
435
|
+
message: `vault.enabled in .planning/config.json is ${rendered}, which is not a JSON boolean. ` +
|
|
436
|
+
split +
|
|
437
|
+
'Store the boolean with "pp-tools config-set vault.enabled true" (which accepts only ' +
|
|
438
|
+
'true or false, and refuses any other spelling), or "pp-tools config-set vault.enabled ' +
|
|
439
|
+
'false" to turn the capability off.',
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// fail open — one check's fault must not suppress the others
|
|
446
|
+
}
|
|
447
|
+
// A proof is owed when the vault is armed, a profile applies and a binding exists (config only, no machine state).
|
|
448
|
+
// A failed or deferred record is reported whenever the vault is armed, binding or not.
|
|
449
|
+
try {
|
|
450
|
+
if (opts.scope !== 'session') {
|
|
451
|
+
const orgProfilePointer = getDotted(parsed, org_profile_cjs_1.ORG_PROFILE_CONFIG_KEY);
|
|
452
|
+
// FORK-ONLY (PBI 74316): with no pointer the embedded profile applies; only "none" opts out.
|
|
453
|
+
const profileApplies = isUsableString(orgProfilePointer)
|
|
454
|
+
? orgProfilePointer.trim().toLowerCase() !== org_profile_cjs_1.ORG_PROFILE_NONE
|
|
455
|
+
: (0, org_profile_cjs_1.effectiveEmbeddedOrgProfilePath)() !== null;
|
|
456
|
+
const vaultArmed = configExists && isVaultEnabledInConfig(parsed);
|
|
457
|
+
const owesAProof = vaultArmed && profileApplies && node_fs_1.default.existsSync(node_path_1.default.join(cwd, '.platform-bindings.json'));
|
|
458
|
+
// Kept as an object: destructuring trips @typescript-eslint/unbound-method.
|
|
459
|
+
const helpers = overrideHelpers();
|
|
460
|
+
// Bound every record field again at print time; a non-string counts as absent.
|
|
461
|
+
const bounded = (v) => (typeof v === 'string' ? helpers.sanitizeOverrideReason(v) : '');
|
|
462
|
+
const profile = isUsableString(orgProfilePointer) ? bounded(orgProfilePointer) : org_profile_cjs_1.EMBEDDED_ORG_PROFILE_NAME;
|
|
463
|
+
const status = bounded(getDotted(parsed, exports.INIT_PROBE_STATUS_KEY));
|
|
464
|
+
const when = bounded(getDotted(parsed, exports.INIT_PROBE_WHEN_KEY));
|
|
465
|
+
const who = bounded(getDotted(parsed, exports.INIT_PROBE_WHO_KEY));
|
|
466
|
+
const note = bounded(getDotted(parsed, exports.INIT_PROBE_NOTE_KEY));
|
|
467
|
+
if (vaultArmed && status === exports.INIT_PROBE_STATUS.FAILED) {
|
|
468
|
+
violations.push({
|
|
469
|
+
code: INVARIANT_CODE.PROBE_FAILED,
|
|
470
|
+
message: `The reachability proof recorded FAILED on ${when} by ${who}: ${note}. ` +
|
|
471
|
+
'Re-run "/pp-init" after fixing the remote, or record an attributed deferral with ' +
|
|
472
|
+
'"pp-tools query init-contract --defer-probe <reason>".',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
else if (vaultArmed && status === exports.INIT_PROBE_STATUS.DEFERRED) {
|
|
476
|
+
// A deferral stays a violation until a passing probe overwrites it.
|
|
477
|
+
violations.push({
|
|
478
|
+
code: INVARIANT_CODE.PROBE_DEFERRED,
|
|
479
|
+
message: `The reachability proof was deferred by ${who} on ${when} for reason: ${note}. ` +
|
|
480
|
+
'Init remains unproven until a re-run of "/pp-init" records a passing probe.',
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
else if (owesAProof) {
|
|
484
|
+
// Name the repair (re-run, or record a deferral) — never deletion.
|
|
485
|
+
const notRunRemedy = ' Re-run "/pp-init" to probe, or record an attributed deferral with ' +
|
|
486
|
+
'"pp-tools query init-contract --defer-probe <reason>".';
|
|
487
|
+
const notRun = (clause) => ({
|
|
488
|
+
code: INVARIANT_CODE.PROBE_NOT_RUN,
|
|
489
|
+
message: `The org profile "${profile}" resolves, vault.enabled is true and .platform-bindings.json ` +
|
|
490
|
+
`is present, but ${clause} — /pp-init Step 2.6 appears not to have run.` +
|
|
491
|
+
notRunRemedy,
|
|
492
|
+
});
|
|
493
|
+
if (status === exports.INIT_PROBE_STATUS.PASSED) {
|
|
494
|
+
// Only an attributed, timestamped pass clears the check.
|
|
495
|
+
if (!isUsableString(who) || !isUsableString(when)) {
|
|
496
|
+
violations.push(notRun('the reachability-proof record is malformed (init.probe_status is "passed" but ' +
|
|
497
|
+
`${!isUsableString(who) ? 'init.probe_who' : 'init.probe_when'} is absent, so the ` +
|
|
498
|
+
'proof is unattributed)'));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
violations.push(notRun(status === ''
|
|
503
|
+
? 'no reachability-proof record exists (init.probe_status is unset)'
|
|
504
|
+
: `no valid reachability-proof record exists (init.probe_status is "${status}", which is not a recognised status)`));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// fail open — one check's fault must not suppress the others
|
|
511
|
+
}
|
|
553
512
|
return { ok: violations.length === 0, violations };
|
|
554
513
|
}
|
|
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.
|
|
514
|
+
// ─── Session state machine: one step at a time, every side effect performed by the CLI ───
|
|
569
515
|
function sessionFilePath(cwd) {
|
|
570
516
|
return node_path_1.default.join(cwd, '.planning', INIT_SESSION_FILE_NAME);
|
|
571
517
|
}
|
|
572
518
|
function newSession() {
|
|
573
519
|
return { version: 1, answers: {}, identityWritten: false, composeDecision: null, doneAt: null };
|
|
574
520
|
}
|
|
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). */
|
|
521
|
+
/** Validate the whole persisted shape; a wrong shape fails closed like a parse error. */
|
|
579
522
|
function isValidSessionShape(v) {
|
|
580
523
|
if (!isObject(v))
|
|
581
524
|
return false;
|
|
@@ -603,13 +546,18 @@ function isValidSessionShape(v) {
|
|
|
603
546
|
}
|
|
604
547
|
if (doneAt !== null && typeof doneAt !== 'string')
|
|
605
548
|
return false;
|
|
549
|
+
const autoAnswers = v.autoAnswers;
|
|
550
|
+
if (autoAnswers !== undefined) {
|
|
551
|
+
if (!isObject(autoAnswers))
|
|
552
|
+
return false;
|
|
553
|
+
for (const src of Object.values(autoAnswers)) {
|
|
554
|
+
if (src !== 'org-profile' && src !== 'derived')
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
606
558
|
return true;
|
|
607
559
|
}
|
|
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. */
|
|
560
|
+
/** Absent → a new session; invalid → `{ malformed: true }` (recover with `--session-reset`). Never throws. */
|
|
613
561
|
function loadSession(cwd) {
|
|
614
562
|
const p = sessionFilePath(cwd);
|
|
615
563
|
try {
|
|
@@ -627,8 +575,7 @@ function loadSession(cwd) {
|
|
|
627
575
|
return { malformed: true };
|
|
628
576
|
}
|
|
629
577
|
}
|
|
630
|
-
/**
|
|
631
|
-
* human-readable message — never a raw stack trace reaching the operator. */
|
|
578
|
+
/** Format an unexpected write failure as a bounded message — never a raw stack trace. */
|
|
632
579
|
function writeFailedMessage(err) {
|
|
633
580
|
const detail = err instanceof Error ? err.message : String(err);
|
|
634
581
|
return (`A filesystem write inside the init session failed (${detail}). ` +
|
|
@@ -641,13 +588,7 @@ function persistSession(cwd, session) {
|
|
|
641
588
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dir);
|
|
642
589
|
(0, shell_command_projection_cjs_1.platformWriteSync)(sessionFilePath(cwd), JSON.stringify(session, null, 2) + '\n');
|
|
643
590
|
}
|
|
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. */
|
|
591
|
+
/** The config to merge into; `{}` on absence or fault, so a merge write never crashes. */
|
|
651
592
|
function readConfigForMerge(cwd) {
|
|
652
593
|
try {
|
|
653
594
|
const raw = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, '.planning', 'config.json'), 'utf-8');
|
|
@@ -658,13 +599,7 @@ function readConfigForMerge(cwd) {
|
|
|
658
599
|
return {};
|
|
659
600
|
}
|
|
660
601
|
}
|
|
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. */
|
|
602
|
+
/** Recursive merge of plain objects; any other value replaces the target. */
|
|
668
603
|
function deepMergeConfigPatch(target, patch) {
|
|
669
604
|
for (const k of Object.keys(patch)) {
|
|
670
605
|
const pv = patch[k];
|
|
@@ -678,28 +613,14 @@ function deepMergeConfigPatch(target, patch) {
|
|
|
678
613
|
}
|
|
679
614
|
return target;
|
|
680
615
|
}
|
|
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. */
|
|
616
|
+
/** The only writer of `.planning/config.json` in this module; merges so unrelated keys survive. */
|
|
691
617
|
function writeConfigMerge(cwd, patch) {
|
|
692
618
|
const dir = node_path_1.default.join(cwd, '.planning');
|
|
693
619
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(dir);
|
|
694
620
|
const merged = deepMergeConfigPatch(readConfigForMerge(cwd), patch);
|
|
695
621
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(dir, 'config.json'), JSON.stringify(merged, null, 2) + '\n');
|
|
696
622
|
}
|
|
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. */
|
|
623
|
+
/** Write the project identity block, and `vault.provider` when the provider is a vault provider (gitlab, azure or github). */
|
|
703
624
|
function writeIdentityAndProvider(cwd, answers) {
|
|
704
625
|
const projectPatch = {};
|
|
705
626
|
for (const [questionId, configKey] of Object.entries(PROJECT_BLOCK_QUESTION_MAP)) {
|
|
@@ -710,14 +631,121 @@ function writeIdentityAndProvider(cwd, answers) {
|
|
|
710
631
|
}
|
|
711
632
|
const patch = { project: projectPatch };
|
|
712
633
|
const providerAnswer = answers.provider;
|
|
713
|
-
if (providerAnswer
|
|
634
|
+
if (git_provider_cjs_1.PROVIDERS.includes(providerAnswer)) {
|
|
714
635
|
patch.vault = { provider: providerAnswer };
|
|
715
636
|
}
|
|
716
637
|
writeConfigMerge(cwd, patch);
|
|
717
638
|
}
|
|
718
|
-
/**
|
|
719
|
-
* `
|
|
720
|
-
*
|
|
639
|
+
/** The ONE owner of the arm-where-absent rule (quick 261003-kmd): arms `vault.enabled`,
|
|
640
|
+
* and `vault.required` only where ABSENT, from one read in one write.
|
|
641
|
+
*
|
|
642
|
+
* A successful composition means a client vault exists, so the two keys are armed (OD-1,
|
|
643
|
+
* 260915-lmc, 260916-s5n). An explicit `false` or a malformed value is kept — that is the
|
|
644
|
+
* operator's decision, never overwritten (07-09 WR-07). The write happens only when there is
|
|
645
|
+
* something to arm: the nested- and flat-form byte-identity pins depend on a no-op writing
|
|
646
|
+
* nothing. Callers: the session's `'unchanged'` decision (how existing projects get armed by a
|
|
647
|
+
* plain re-run), the approved `compose_confirm`, and `armVaultFromProfile` (`/pp-new-project`).
|
|
648
|
+
* It exists because the two session copies had already drifted once, and a third route would
|
|
649
|
+
* have been a third copy.
|
|
650
|
+
*
|
|
651
|
+
* @returns the armed key names, in the fixed order enabled, required. */
|
|
652
|
+
function armVaultKeysWhereAbsent(cwd) {
|
|
653
|
+
const { parsed } = readConfigFile(cwd);
|
|
654
|
+
const vaultPatch = {};
|
|
655
|
+
if (resolveVaultEnabledState(parsed).state === 'absent') {
|
|
656
|
+
vaultPatch.enabled = true;
|
|
657
|
+
}
|
|
658
|
+
if (resolveVaultRequiredState(parsed).state === 'absent') {
|
|
659
|
+
vaultPatch.required = true;
|
|
660
|
+
}
|
|
661
|
+
const armed = Object.keys(vaultPatch);
|
|
662
|
+
if (armed.length > 0) {
|
|
663
|
+
writeConfigMerge(cwd, { vault: vaultPatch });
|
|
664
|
+
}
|
|
665
|
+
return armed;
|
|
666
|
+
}
|
|
667
|
+
/** The reason triad and identity chain, reused from the vault-trace leaf (never re-derived here).
|
|
668
|
+
* Required lazily: pp-tools loads this module on every command, and the trace leaf is large. */
|
|
669
|
+
function overrideHelpers() {
|
|
670
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
671
|
+
const trace = require('./vault-trace.cjs');
|
|
672
|
+
return trace;
|
|
673
|
+
}
|
|
674
|
+
/** Build the record with `who` and `when` bounded; `status` is checked against `INIT_PROBE_STATUS`. */
|
|
675
|
+
function buildProbeRecord(cwd, status, note, opts, helpers) {
|
|
676
|
+
const injectedWho = typeof opts._who === 'string' && opts._who.trim() !== '' ? opts._who : null;
|
|
677
|
+
const rawWho = injectedWho ?? helpers.resolveOverrideIdentity(cwd);
|
|
678
|
+
// Never an empty string and never an omitted key — an absent `who` and an
|
|
679
|
+
// unattributable one must not look identical (the trace leaf's own rule).
|
|
680
|
+
const who = helpers.sanitizeOverrideReason(rawWho) || 'unknown';
|
|
681
|
+
const when = helpers.sanitizeOverrideReason(opts.now ?? new Date().toISOString());
|
|
682
|
+
return { status, when, who, note };
|
|
683
|
+
}
|
|
684
|
+
/** The shared record write: four keys merged under `init`. Never throws (`write_failed`). */
|
|
685
|
+
function writeProbeRecord(cwd, record) {
|
|
686
|
+
try {
|
|
687
|
+
writeConfigMerge(cwd, {
|
|
688
|
+
init: {
|
|
689
|
+
probe_status: record.status,
|
|
690
|
+
probe_when: record.when,
|
|
691
|
+
probe_who: record.who,
|
|
692
|
+
probe_note: record.note,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
return { ok: false, reason: 'write_failed', message: writeFailedMessage(err) };
|
|
698
|
+
}
|
|
699
|
+
return { ok: true, record };
|
|
700
|
+
}
|
|
701
|
+
/** Record that the Step 2.6 probe passed or failed; an unknown status or an empty note is refused, writing nothing.
|
|
702
|
+
* Deferrals go through `recordProbeDeferral`. */
|
|
703
|
+
function recordProbeOutcome(cwd, input, opts = {}) {
|
|
704
|
+
const helpers = overrideHelpers();
|
|
705
|
+
const status = input?.status;
|
|
706
|
+
if (status !== exports.INIT_PROBE_STATUS.PASSED && status !== exports.INIT_PROBE_STATUS.FAILED) {
|
|
707
|
+
return {
|
|
708
|
+
ok: false,
|
|
709
|
+
reason: 'invalid_probe_status',
|
|
710
|
+
message: `Cannot record probe status ${JSON.stringify(status)} — expected ` +
|
|
711
|
+
`"${exports.INIT_PROBE_STATUS.PASSED}" or "${exports.INIT_PROBE_STATUS.FAILED}". ` +
|
|
712
|
+
`A deferral is recorded with --defer-probe "<reason>", which requires a stated reason.`,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
// A non-string note counts as no note (never String()-coerced).
|
|
716
|
+
const note = helpers.sanitizeOverrideReason(typeof input?.note === 'string' ? input.note : '');
|
|
717
|
+
if (note === '') {
|
|
718
|
+
return {
|
|
719
|
+
ok: false,
|
|
720
|
+
reason: 'probe_note_required',
|
|
721
|
+
message: 'Cannot record a probe outcome with no note. Say what the probe measured — which targets ' +
|
|
722
|
+
'were reachable, and for a failure which one was not — so the record is evidence and not ' +
|
|
723
|
+
'just a verdict.',
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return writeProbeRecord(cwd, buildProbeRecord(cwd, status, note, opts, helpers));
|
|
727
|
+
}
|
|
728
|
+
/** Record a deferral of the Step 2.6 proof; the reason is required and validated, and a refusal writes nothing.
|
|
729
|
+
* `--verify` keeps reporting it until a passing probe overwrites it. */
|
|
730
|
+
function recordProbeDeferral(cwd, reason, opts = {}) {
|
|
731
|
+
const helpers = overrideHelpers();
|
|
732
|
+
const refusal = (rejection) => ({
|
|
733
|
+
ok: false,
|
|
734
|
+
reason: 'deferral_reason_rejected',
|
|
735
|
+
message: `Deferral refused: ${rejection}. State why the reachability proof is being deferred — ` +
|
|
736
|
+
'at least 20 characters, containing letters, not a placeholder.',
|
|
737
|
+
});
|
|
738
|
+
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
739
|
+
return refusal('no reason was given');
|
|
740
|
+
}
|
|
741
|
+
const verdict = helpers.validateOverrideReason(reason.trim());
|
|
742
|
+
if (verdict.state !== 'accepted') {
|
|
743
|
+
return refusal(verdict.state === 'rejected' ? verdict.rejection : 'the reason was not accepted');
|
|
744
|
+
}
|
|
745
|
+
const note = helpers.sanitizeOverrideReason(verdict.reason);
|
|
746
|
+
return writeProbeRecord(cwd, buildProbeRecord(cwd, exports.INIT_PROBE_STATUS.DEFERRED, note, opts, helpers));
|
|
747
|
+
}
|
|
748
|
+
/** Parse a `compose_confirm` answer; `null` for anything unrecognised (never a silent "no"). */
|
|
721
749
|
function normalizeYesNo(value) {
|
|
722
750
|
if (typeof value !== 'string')
|
|
723
751
|
return null;
|
|
@@ -728,43 +756,27 @@ function normalizeYesNo(value) {
|
|
|
728
756
|
return false;
|
|
729
757
|
return null;
|
|
730
758
|
}
|
|
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
|
-
*/
|
|
759
|
+
/** Decide the current step, performing its idempotent side effects once: automatic answers, the identity write,
|
|
760
|
+
* the compose decision and `doneAt`. Recomputes the question set on every call. */
|
|
752
761
|
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.
|
|
762
|
+
// Any failure degrades to a `write_failed` step instead of throwing.
|
|
762
763
|
try {
|
|
763
|
-
const questions = buildInitQuestions(cwd
|
|
764
|
+
const questions = buildInitQuestions(cwd);
|
|
765
|
+
// PBI 74299: read once per call; only consulted for an unanswered question.
|
|
766
|
+
let profileInit = null;
|
|
764
767
|
for (const q of questions) {
|
|
765
|
-
if (
|
|
768
|
+
if (Object.prototype.hasOwnProperty.call(session.answers, q.id))
|
|
769
|
+
continue;
|
|
770
|
+
profileInit ??= readProfileInitAnswers(cwd, { _globalRoot: opts._globalRoot });
|
|
771
|
+
const auto = autoAnswerFor(q.id, session.answers, profileInit);
|
|
772
|
+
if (auto === null) {
|
|
766
773
|
return { kind: 'question', question: q };
|
|
767
774
|
}
|
|
775
|
+
// Filled in once and recorded, so a --session-next poll writes the
|
|
776
|
+
// session file only the first time an answer is decided.
|
|
777
|
+
session.answers[q.id] = auto.value;
|
|
778
|
+
session.autoAnswers = { ...(session.autoAnswers ?? {}), [q.id]: auto.source };
|
|
779
|
+
persistSession(cwd, session);
|
|
768
780
|
}
|
|
769
781
|
if (!session.identityWritten) {
|
|
770
782
|
writeIdentityAndProvider(cwd, session.answers);
|
|
@@ -773,20 +785,10 @@ function computeCurrentStep(cwd, session, opts) {
|
|
|
773
785
|
}
|
|
774
786
|
if (session.composeDecision === null) {
|
|
775
787
|
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.
|
|
788
|
+
// Offer compose_confirm only when approving would change something; 'unchanged' and 'skipped' are distinct end states.
|
|
786
789
|
if (dryRun.composable === true && dryRun.action !== 'unchanged') {
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
// file keeps the RAW coordinates.
|
|
790
|
+
// VAULT-EGRESS-1: `info` is display-only (the CLI and the TTY renderer print it); approval
|
|
791
|
+
// re-composes with `write: true` below, so the written file keeps the RAW coordinates.
|
|
790
792
|
const shown = (0, compose_bindings_cjs_1.redactComposeResultForEmission)(dryRun);
|
|
791
793
|
return {
|
|
792
794
|
kind: 'compose_confirm',
|
|
@@ -795,25 +797,24 @@ function computeCurrentStep(cwd, session, opts) {
|
|
|
795
797
|
drift: shown.drift,
|
|
796
798
|
action: dryRun.action,
|
|
797
799
|
composed: shown.composed,
|
|
798
|
-
warnings:
|
|
800
|
+
warnings: dryRun.warnings,
|
|
799
801
|
},
|
|
800
802
|
};
|
|
801
803
|
}
|
|
802
804
|
session.composeDecision = dryRun.composable === true && dryRun.action === 'unchanged' ? 'unchanged' : 'skipped';
|
|
805
|
+
if (session.composeDecision === 'unchanged') {
|
|
806
|
+
armVaultKeysWhereAbsent(cwd);
|
|
807
|
+
}
|
|
803
808
|
persistSession(cwd, session);
|
|
804
809
|
}
|
|
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.
|
|
810
|
+
// Three different nothing-written end states, each with its own message.
|
|
811
811
|
const skippedComposeInfo = session.composeDecision === 'skipped'
|
|
812
812
|
? '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
813
|
: session.composeDecision === 'unchanged'
|
|
814
814
|
? '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
815
|
: undefined;
|
|
816
|
-
|
|
816
|
+
// The session verify skips only the INIT_PROBE_* check: Step 2.6 records the proof after init returns.
|
|
817
|
+
const verifyResult = verifyInitInvariants(cwd, { _globalRoot: opts._globalRoot, scope: 'session' });
|
|
817
818
|
if (verifyResult.ok) {
|
|
818
819
|
if (!session.doneAt) {
|
|
819
820
|
session.doneAt = new Date().toISOString();
|
|
@@ -873,13 +874,7 @@ function currentStepToState(step) {
|
|
|
873
874
|
};
|
|
874
875
|
}
|
|
875
876
|
}
|
|
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
|
-
*/
|
|
877
|
+
/** Return the ONE next step of the session; malformed session state fails closed (recover with `--session-reset`). */
|
|
883
878
|
function initSessionNext(cwd, opts = {}) {
|
|
884
879
|
const loaded = loadSession(cwd);
|
|
885
880
|
if (loaded.malformed) {
|
|
@@ -892,25 +887,12 @@ function initSessionNext(cwd, opts = {}) {
|
|
|
892
887
|
'Run "pp_run query init-contract --session-reset" to start over — it is never resumed silently into an unknown state.',
|
|
893
888
|
};
|
|
894
889
|
}
|
|
895
|
-
|
|
890
|
+
const session = loaded.session;
|
|
891
|
+
const state = currentStepToState(computeCurrentStep(cwd, session, opts));
|
|
892
|
+
const autoAnswered = listAutoAnswers(cwd, session);
|
|
893
|
+
return autoAnswered.length > 0 ? { ...state, autoAnswered } : state;
|
|
896
894
|
}
|
|
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
|
-
*/
|
|
895
|
+
/** Record one answer for the current step only; out-of-order, blank required, unparseable or post-done answers are rejected. */
|
|
914
896
|
function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
915
897
|
const loaded = loadSession(cwd);
|
|
916
898
|
if (loaded.malformed) {
|
|
@@ -922,14 +904,7 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
922
904
|
};
|
|
923
905
|
}
|
|
924
906
|
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.
|
|
907
|
+
// Any write failure in this function degrades to `write_failed` instead of throwing.
|
|
933
908
|
try {
|
|
934
909
|
const step = computeCurrentStep(cwd, session, opts);
|
|
935
910
|
if (step.kind === 'write_failed') {
|
|
@@ -954,32 +929,6 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
954
929
|
return { ok: false, reason: 'answer_required', message: `Question "${stepId}" requires a non-blank answer.` };
|
|
955
930
|
}
|
|
956
931
|
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
932
|
session.answers[stepId] = answerValue;
|
|
984
933
|
persistSession(cwd, session);
|
|
985
934
|
return { ok: true };
|
|
@@ -1001,24 +950,10 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
1001
950
|
};
|
|
1002
951
|
}
|
|
1003
952
|
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.
|
|
953
|
+
// Check the write result before recording "approved": the file can change between the dry run and this write.
|
|
1016
954
|
const writeResult = (0, compose_bindings_cjs_1.composeBindings)(cwd, { write: true, offline: opts.offline === true, _globalRoot: opts._globalRoot });
|
|
1017
955
|
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.
|
|
956
|
+
// Leave the decision unset so the next call re-evaluates and can re-offer compose_confirm.
|
|
1022
957
|
return {
|
|
1023
958
|
ok: false,
|
|
1024
959
|
reason: 'compose_write_failed',
|
|
@@ -1029,6 +964,7 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
1029
964
|
};
|
|
1030
965
|
}
|
|
1031
966
|
session.composeDecision = 'approved';
|
|
967
|
+
armVaultKeysWhereAbsent(cwd);
|
|
1032
968
|
}
|
|
1033
969
|
else {
|
|
1034
970
|
writeConfigMerge(cwd, { init: { compose_declined: true } });
|
|
@@ -1048,8 +984,7 @@ function initSessionAnswer(cwd, stepId, value, opts = {}) {
|
|
|
1048
984
|
return { ok: false, reason: 'write_failed', message: writeFailedMessage(err) };
|
|
1049
985
|
}
|
|
1050
986
|
}
|
|
1051
|
-
/** Delete the session file
|
|
1052
|
-
* session (T-34-63). Never throws; a missing file is not an error. */
|
|
987
|
+
/** Delete the session file (the recovery from a malformed session). Never throws. */
|
|
1053
988
|
function initSessionReset(cwd) {
|
|
1054
989
|
try {
|
|
1055
990
|
const p = sessionFilePath(cwd);
|
|
@@ -1061,193 +996,114 @@ function initSessionReset(cwd) {
|
|
|
1061
996
|
}
|
|
1062
997
|
return { ok: true };
|
|
1063
998
|
}
|
|
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;
|
|
999
|
+
/** Remedy every failure verdict names: repair through the interactive relay, never deletion. */
|
|
1000
|
+
const ARM_VAULT_REMEDY = 'Run "/pp-init" to relay the binding compose interactively and arm the vault.';
|
|
1001
|
+
/** Compose the binding from the resolved org profile and arm the vault keys where absent, with no prompt
|
|
1002
|
+
* (quick 261003-kmd, FIX-1). `/pp-new-project` calls this so a project created through it is governed the
|
|
1003
|
+
* same way `/pp-init` leaves one; before it existed, only `/pp-init` armed the vault and a new-project
|
|
1004
|
+
* route left it silently off.
|
|
1005
|
+
*
|
|
1006
|
+
* The binding is written through `composeBindings` — the SAME composer `/pp-init` approves through — so
|
|
1007
|
+
* its shape and provenance are identical, and the keys through `armVaultKeysWhereAbsent`, the one owner.
|
|
1008
|
+
* An inert verdict (a recorded decline, a `vault.enabled` that is set but not the boolean `true`, or no
|
|
1009
|
+
* org profile resolves) writes nothing of any kind and never creates `.planning/`. The `vault_disabled`
|
|
1010
|
+
* arm exists because this route has no human confirm: an explicit `false` (or a malformed value) is the
|
|
1011
|
+
* operator's opt-out, so writing a binding and two keys into that project would turn `vault.status` from
|
|
1012
|
+
* `inert` into a verdict and let a report say "armed" where the gate says the vault is not governing
|
|
1013
|
+
* (261003-kmd verification gap; the more inert reading). `/pp-init`'s approved compose is unchanged — a
|
|
1014
|
+
* human confirmed it. `vault.provider` is deliberately NOT written: `resolveProvider` in
|
|
1015
|
+
* `src/git-provider.cts` already layers the org profile's provider when the config key is absent, so the
|
|
1016
|
+
* fetch resolves the right provider without a second writer. Never reads or writes the session file.
|
|
1017
|
+
* Never throws. */
|
|
1018
|
+
function armVaultFromProfile(cwd, opts = {}) {
|
|
1098
1019
|
try {
|
|
1099
|
-
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1020
|
+
const { parsed } = readConfigFile(cwd);
|
|
1021
|
+
if (capabilityActivation.getConfigValueEitherForm(parsed, exports.INIT_ORG_PROFILE_DECLINED_KEY).value === true) {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: true,
|
|
1024
|
+
armed: false,
|
|
1025
|
+
reason: 'org_profile_declined',
|
|
1026
|
+
message: 'The org profile was declined for this project (init.org_profile_declined), so nothing was composed or armed.',
|
|
1027
|
+
};
|
|
1105
1028
|
}
|
|
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;
|
|
1029
|
+
if (capabilityActivation.getConfigValueEitherForm(parsed, exports.INIT_COMPOSE_DECLINED_KEY).value === true) {
|
|
1030
|
+
return {
|
|
1031
|
+
ok: true,
|
|
1032
|
+
armed: false,
|
|
1033
|
+
reason: 'compose_declined',
|
|
1034
|
+
message: 'The binding compose was declined for this project (init.compose_declined), so nothing was composed or armed.',
|
|
1035
|
+
};
|
|
1157
1036
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1037
|
+
const enabled = resolveVaultEnabledState(parsed);
|
|
1038
|
+
if (enabled.state !== 'absent' && enabled.state !== 'armed') {
|
|
1039
|
+
return {
|
|
1040
|
+
ok: true,
|
|
1041
|
+
armed: false,
|
|
1042
|
+
reason: 'vault_disabled',
|
|
1043
|
+
message: enabled.state === 'off'
|
|
1044
|
+
? 'vault.enabled is false for this project, so the vault was left off and nothing was composed or armed.'
|
|
1045
|
+
: 'vault.enabled is set but is not the boolean true, so the vault was left off and nothing was composed or armed.',
|
|
1046
|
+
};
|
|
1160
1047
|
}
|
|
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) {
|
|
1048
|
+
if (!(0, org_profile_cjs_1.resolveOrgProfileDocument)(cwd, { _globalRoot: opts._globalRoot }).resolved) {
|
|
1049
|
+
return {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
armed: false,
|
|
1052
|
+
reason: 'no_org_profile',
|
|
1053
|
+
message: 'No org profile resolves for this project, so there is no client vault to bind and nothing was written.',
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
const compose = (0, compose_bindings_cjs_1.composeBindings)(cwd, { write: true, offline: opts.offline === true, _globalRoot: opts._globalRoot });
|
|
1057
|
+
if (compose.composable !== true) {
|
|
1058
|
+
return {
|
|
1059
|
+
ok: false,
|
|
1060
|
+
armed: false,
|
|
1061
|
+
reason: 'not_composable',
|
|
1062
|
+
message: 'An org profile resolves, but no valid client-vault binding could be composed from it, so the vault was NOT armed. ' +
|
|
1063
|
+
ARM_VAULT_REMEDY,
|
|
1064
|
+
warnings: compose.warnings,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
if ((compose.action === 'create' || compose.action === 'update') && compose.wrote !== true) {
|
|
1068
|
+
return {
|
|
1069
|
+
ok: false,
|
|
1070
|
+
armed: false,
|
|
1071
|
+
reason: 'compose_write_failed',
|
|
1072
|
+
message: 'The binding was composed but .platform-bindings.json was not written, so the vault was NOT armed. ' + ARM_VAULT_REMEDY,
|
|
1073
|
+
warnings: compose.warnings,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const vaultKeysArmed = armVaultKeysWhereAbsent(cwd);
|
|
1197
1077
|
return {
|
|
1198
|
-
ok: true,
|
|
1199
|
-
|
|
1078
|
+
ok: true,
|
|
1079
|
+
armed: true,
|
|
1080
|
+
profile: {
|
|
1081
|
+
packageName: compose.profile.packageName,
|
|
1082
|
+
version: compose.profile.version,
|
|
1083
|
+
source: compose.profile.source,
|
|
1084
|
+
},
|
|
1085
|
+
compose: { action: compose.action, wrote: compose.wrote },
|
|
1086
|
+
vaultKeysArmed,
|
|
1087
|
+
// VAULT-EGRESS-1: the written file holds the raw coordinate; the report is display-safe.
|
|
1088
|
+
provenance: (0, compose_bindings_cjs_1.redactComposeResultForEmission)(compose).provenance,
|
|
1089
|
+
warnings: compose.warnings,
|
|
1090
|
+
note: 'Project identity (project.*) was not captured by this step; /pp-init completes it.',
|
|
1200
1091
|
};
|
|
1201
1092
|
}
|
|
1202
|
-
try {
|
|
1203
|
-
writeConfigMerge(cwd, { vault: patch });
|
|
1204
|
-
}
|
|
1205
1093
|
catch (err) {
|
|
1206
1094
|
return {
|
|
1207
|
-
ok: false,
|
|
1208
|
-
|
|
1095
|
+
ok: false,
|
|
1096
|
+
armed: false,
|
|
1097
|
+
reason: 'write_failed',
|
|
1098
|
+
message: writeFailedMessage(err) + ' ' + ARM_VAULT_REMEDY,
|
|
1099
|
+
warnings: [],
|
|
1209
1100
|
};
|
|
1210
1101
|
}
|
|
1211
|
-
return {
|
|
1212
|
-
ok: true, armed: true, enabled: enabledAfter, required: requiredAfter, practicesEnabled: practicesAfter, client,
|
|
1213
|
-
message: `${clauses.join('; ')}.`,
|
|
1214
|
-
};
|
|
1215
1102
|
}
|
|
1216
1103
|
// ─── 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
|
-
*/
|
|
1104
|
+
/** CLI adapter for `pp-tools query init-contract`: the session, record, arm-vault, verify and questions flags.
|
|
1105
|
+
* Data verdicts set `exitCode = 1` when not ok, but the JSON result is always printed. */
|
|
1242
1106
|
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
1107
|
if (options.sessionReset === true) {
|
|
1252
1108
|
const result = initSessionReset(cwd);
|
|
1253
1109
|
output(result, raw);
|
|
@@ -1271,6 +1127,32 @@ function cmdInitContract(cwd, options, raw) {
|
|
|
1271
1127
|
}
|
|
1272
1128
|
return;
|
|
1273
1129
|
}
|
|
1130
|
+
// Record writes run before `--verify`, so a combined call verifies the written state.
|
|
1131
|
+
if (options.recordProbe !== undefined) {
|
|
1132
|
+
const result = recordProbeOutcome(cwd, options.recordProbe);
|
|
1133
|
+
output(result, raw);
|
|
1134
|
+
if (!result.ok) {
|
|
1135
|
+
process.exitCode = 1;
|
|
1136
|
+
}
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (options.deferProbe !== undefined) {
|
|
1140
|
+
const result = recordProbeDeferral(cwd, options.deferProbe);
|
|
1141
|
+
output(result, raw);
|
|
1142
|
+
if (!result.ok) {
|
|
1143
|
+
process.exitCode = 1;
|
|
1144
|
+
}
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
// An inert `armed: false` is a data verdict on exit 0, like the inert init path; only `ok: false` exits 1.
|
|
1148
|
+
if (options.armVault === true) {
|
|
1149
|
+
const result = armVaultFromProfile(cwd, { offline: options.offline === true });
|
|
1150
|
+
output(result, raw);
|
|
1151
|
+
if (result.ok === false) {
|
|
1152
|
+
process.exitCode = 1;
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1274
1156
|
if (options.verify === true) {
|
|
1275
1157
|
const result = verifyInitInvariants(cwd);
|
|
1276
1158
|
output(result, raw);
|