@pareto4/pp-core 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +31 -0
- package/agents/pp-code-fixer.md +1 -1
- package/agents/pp-debug-session-manager.md +1 -1
- package/agents/pp-debugger.md +1 -1
- package/agents/pp-executor.md +20 -10
- package/agents/pp-intel-updater.md +1 -1
- package/agents/pp-phase-researcher.md +1 -1
- package/agents/pp-plan-checker.md +5 -5
- package/agents/pp-planner.md +10 -24
- package/agents/pp-project-researcher.md +1 -1
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-researcher.md +1 -1
- package/agents/pp-verifier.md +8 -4
- package/bin/install.js +429 -82
- package/commands/pp-autonomous.md +8 -6
- package/commands/pp-discovery-to-build.md +1 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-discuss-phase.md +2 -2
- package/commands/pp-execute-phase.md +3 -1
- package/commands/pp-graphify.md +5 -5
- package/commands/pp-import.md +1 -1
- package/commands/pp-map-codebase.md +1 -1
- package/commands/pp-ns-project.md +2 -3
- package/commands/pp-ns-workflow.md +1 -1
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-verify-work.md +2 -1
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-cursor-session-start.js +2 -2
- package/hooks/dist/pp-session-state.sh +1 -1
- package/hooks/dist/pp-statusline.js +21 -2
- package/hooks/pp-cursor-session-start.js +2 -2
- package/hooks/pp-session-state.sh +1 -1
- package/hooks/pp-statusline.js +21 -2
- package/package.json +18 -5
- package/pareto-skills/pp-project-setup/SKILL.md +5 -63
- package/pp-core/bin/detect-build-commands.sh +182 -0
- package/pp-core/bin/lib/capability-registry.cjs +65 -7
- package/pp-core/bin/lib/check-command-router.cjs +79 -38
- package/pp-core/bin/lib/clusters.cjs +2 -3
- package/pp-core/bin/lib/command-aliases.cjs +36 -0
- package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
- package/pp-core/bin/lib/commands.cjs +21 -6
- package/pp-core/bin/lib/compose-bindings.cjs +70 -6
- package/pp-core/bin/lib/config-schema.cjs +16 -0
- package/pp-core/bin/lib/config.cjs +142 -27
- package/pp-core/bin/lib/core-utils.cjs +43 -7
- package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
- package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
- package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
- package/pp-core/bin/lib/discovery-table.cjs +98 -0
- package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
- package/pp-core/bin/lib/frontmatter.cjs +17 -2
- package/pp-core/bin/lib/init-command-router.cjs +8 -5
- package/pp-core/bin/lib/init-contract.cjs +93 -4
- package/pp-core/bin/lib/init.cjs +124 -134
- package/pp-core/bin/lib/install-profiles.cjs +5 -0
- package/pp-core/bin/lib/install-run.cjs +536 -0
- package/pp-core/bin/lib/io.cjs +15 -0
- package/pp-core/bin/lib/loop-resolver.cjs +24 -0
- package/pp-core/bin/lib/milestone.cjs +345 -48
- package/pp-core/bin/lib/phase-command-router.cjs +23 -1
- package/pp-core/bin/lib/phase-locator.cjs +15 -6
- package/pp-core/bin/lib/phase-scope.cjs +91 -0
- package/pp-core/bin/lib/phase-tier.cjs +467 -0
- package/pp-core/bin/lib/phase.cjs +247 -21
- package/pp-core/bin/lib/phases-command-router.cjs +2 -0
- package/pp-core/bin/lib/plan-graph.cjs +368 -0
- package/pp-core/bin/lib/planning-workspace.cjs +53 -0
- package/pp-core/bin/lib/platform-bindings.cjs +32 -2
- package/pp-core/bin/lib/profile-output.cjs +11 -3
- package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
- package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
- package/pp-core/bin/lib/roadmap.cjs +141 -39
- package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
- package/pp-core/bin/lib/state-command-router.cjs +82 -13
- package/pp-core/bin/lib/state-document.cjs +124 -7
- package/pp-core/bin/lib/state.cjs +784 -257
- package/pp-core/bin/lib/template.cjs +22 -6
- package/pp-core/bin/lib/vault-command-router.cjs +57 -6
- package/pp-core/bin/lib/vault-practices.cjs +6 -36
- package/pp-core/bin/lib/vault-write-back.cjs +20 -4
- package/pp-core/bin/lib/vault.cjs +555 -23
- package/pp-core/bin/lib/verify.cjs +53 -31
- package/pp-core/bin/lib/workstream.cjs +1 -1
- package/pp-core/bin/lib/worktree-safety.cjs +26 -15
- package/pp-core/bin/portable-timeout.sh +89 -0
- package/pp-core/bin/pp-tools.cjs +149 -9
- package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
- package/pp-core/bin/shared/config-schema.manifest.json +3 -0
- package/pp-core/references/autonomous-best-effort.md +461 -0
- package/pp-core/references/autonomous-lifecycle.md +146 -0
- package/pp-core/references/autonomous-smart-discuss.md +16 -12
- package/pp-core/references/checkpoints.md +1 -1
- package/pp-core/references/discovery-id-conventions.md +202 -0
- package/pp-core/references/discovery-rebaseline.md +254 -0
- package/pp-core/references/discovery-research-track.md +283 -0
- package/pp-core/references/discovery-traceability.md +400 -0
- package/pp-core/references/loop-hook-dispatch.md +7 -0
- package/pp-core/references/mandatory-initial-read.md +1 -1
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-human-verify-mode.md +1 -1
- package/pp-core/references/planner-load-graph-context.md +1 -1
- package/pp-core/references/planner-plan-graph.md +66 -0
- package/pp-core/references/planner-tdd.md +1 -0
- package/pp-core/references/planning-config.md +8 -0
- package/pp-core/references/sketch-tooling.md +4 -0
- package/pp-core/references/tdd.md +1 -0
- package/pp-core/templates/claude-md.md +1 -1
- package/pp-core/templates/phase-prompt.md +6 -2
- package/pp-core/templates/prototype-review/review.js +303 -0
- package/pp-core/templates/state.md +6 -0
- package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-phase.md +1 -1
- package/pp-core/workflows/add-tests.md +1 -1
- package/pp-core/workflows/add-todo.md +1 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -2
- package/pp-core/workflows/audit-fix.md +12 -16
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/audit-uat.md +1 -1
- package/pp-core/workflows/autonomous.md +68 -306
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/cleanup.md +1 -1
- package/pp-core/workflows/code-review-fix.md +1 -1
- package/pp-core/workflows/code-review.md +19 -7
- package/pp-core/workflows/complete-milestone.md +41 -60
- package/pp-core/workflows/debug.md +1 -1
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
- package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
- package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
- package/pp-core/workflows/discovery-sprint.md +79 -1714
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
- package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
- package/pp-core/workflows/discovery-to-build.md +21 -238
- package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
- package/pp-core/workflows/discuss-phase.md +9 -2
- package/pp-core/workflows/do.md +4 -4
- package/pp-core/workflows/docs-update.md +1 -1
- package/pp-core/workflows/edit-phase.md +1 -1
- package/pp-core/workflows/eval-review.md +1 -1
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
- package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
- package/pp-core/workflows/execute-phase.md +102 -216
- package/pp-core/workflows/execute-plan.md +6 -2
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/extract-learnings.md +1 -1
- package/pp-core/workflows/forensics.md +4 -4
- package/pp-core/workflows/graduation.md +1 -1
- package/pp-core/workflows/health.md +1 -1
- package/pp-core/workflows/help/modes/brief.md +1 -0
- package/pp-core/workflows/help/modes/default.md +2 -1
- package/pp-core/workflows/help/modes/full.md +12 -5
- package/pp-core/workflows/import.md +1 -1
- package/pp-core/workflows/ingest-docs.md +1 -1
- package/pp-core/workflows/init.md +18 -2
- package/pp-core/workflows/insert-phase.md +1 -1
- package/pp-core/workflows/list-workspaces.md +1 -1
- package/pp-core/workflows/manager.md +3 -3
- package/pp-core/workflows/map-codebase.md +2 -2
- package/pp-core/workflows/milestone-summary.md +4 -4
- package/pp-core/workflows/mvp-phase.md +1 -1
- package/pp-core/workflows/new-milestone.md +7 -8
- package/pp-core/workflows/new-project.md +6 -1
- package/pp-core/workflows/new-workspace.md +1 -1
- package/pp-core/workflows/next.md +2 -2
- package/pp-core/workflows/note.md +2 -2
- package/pp-core/workflows/pause-work.md +1 -1
- package/pp-core/workflows/plan-milestone-gaps.md +1 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
- package/pp-core/workflows/plan-phase.md +43 -167
- package/pp-core/workflows/plan-review-convergence.md +2 -2
- package/pp-core/workflows/plant-seed.md +1 -1
- package/pp-core/workflows/pr-branch.md +1 -1
- package/pp-core/workflows/profile-user.md +1 -1
- package/pp-core/workflows/progress.md +3 -3
- package/pp-core/workflows/quick.md +3 -1
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/remove-workspace.md +1 -1
- package/pp-core/workflows/resume-project.md +6 -12
- package/pp-core/workflows/review.md +1 -1
- package/pp-core/workflows/scan.md +1 -1
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/settings-advanced.md +1 -1
- package/pp-core/workflows/settings-integrations.md +1 -1
- package/pp-core/workflows/settings.md +1 -1
- package/pp-core/workflows/ship.md +25 -8
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +2 -1
- package/pp-core/workflows/spec-phase.md +1 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +1 -1
- package/pp-core/workflows/stats.md +2 -2
- package/pp-core/workflows/thread.md +1 -1
- package/pp-core/workflows/transition.md +5 -9
- package/pp-core/workflows/ui-phase.md +23 -2
- package/pp-core/workflows/ui-review.md +1 -1
- package/pp-core/workflows/ultraplan-phase.md +2 -2
- package/pp-core/workflows/validate-phase.md +1 -1
- package/pp-core/workflows/vault-write-back.md +1 -1
- package/pp-core/workflows/verify-phase.md +29 -26
- package/pp-core/workflows/verify-work.md +14 -10
- package/scripts/base64-scan.sh +7 -41
- package/scripts/changeset/cli.cjs +25 -5
- package/scripts/dev-env.cjs +462 -0
- package/scripts/lib/allowlist-ratchet.cjs +103 -1
- package/scripts/lib/size-budget.cjs +311 -0
- package/scripts/lint-neutrality-names.cjs +694 -0
- package/scripts/lint-org-coordinates.allowlist.json +35 -0
- package/scripts/lint-org-coordinates.cjs +426 -0
- package/scripts/lint-skill-deps.cjs +48 -9
- package/scripts/lint-test-file-count.cjs +17 -2
- package/scripts/prompt-injection-scan.sh +41 -20
- package/scripts/sync-manifest-versions.cjs +41 -7
- package/scripts/sync-runtime-launcher.cjs +106 -1
- package/scripts/update-size-baseline.cjs +274 -30
- package/scripts/workflow-size.cjs +61 -9
- package/commands/pp-build-handoff.md +0 -46
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase Scope — is this phase-directory match the current milestone's, and may
|
|
4
|
+
* this caller use it?
|
|
5
|
+
*
|
|
6
|
+
* THE owner of that decision for every consumer of the raw phase locator
|
|
7
|
+
*. The raw locator in phase-locator.cts answers a
|
|
8
|
+
* phase token from the live `phases/` tree first and then falls through to the
|
|
9
|
+
* closed milestones' `milestones/<v>-phases/` archives, marking such a match
|
|
10
|
+
* with the archive's version. It applies no milestone compare, and it must not:
|
|
11
|
+
* phase-locator stays a leaf that does not import roadmap-parser (quick
|
|
12
|
+
* 261007-jkl D2), which is why the compare lives here.
|
|
13
|
+
*
|
|
14
|
+
* History. 261007-jkl D2 put the compare into init.cts for the four init phase
|
|
15
|
+
* verbs only; every other consumer kept the raw fall-through, so `template fill`
|
|
16
|
+
* and `phase scaffold` wrote new files into a closed milestone's archive and
|
|
17
|
+
* `phase complete`, `roadmap update-plan-progress`, `check ui.plan-gate`, the ship
|
|
18
|
+
* pointer and the vault resolver answered a v2.0 phase from a v1.0 archive whose
|
|
19
|
+
* number the current ROADMAP reuses (todo "template-fill-writes-into-an-archived-
|
|
20
|
+
* phase-directory"). This module lifts the decision out of init so all of them
|
|
21
|
+
* share one answer (D-01: a new leaf-importing module, so every consumer can
|
|
22
|
+
* require it with no cycle — measured in the quick's sweep/CYCLES.md).
|
|
23
|
+
*
|
|
24
|
+
* Scopes (`resolvePhaseScope`):
|
|
25
|
+
* - `live` the match is in the live `phases/` tree;
|
|
26
|
+
* - `archived-current` archived, the ROADMAP lists the number, and the archive
|
|
27
|
+
* version equals the current milestone version — the
|
|
28
|
+
* roadmap entry IS the archived phase;
|
|
29
|
+
* - `archived-unlisted` archived and the ROADMAP no longer lists the number;
|
|
30
|
+
* - `archived-collision` archived, the ROADMAP lists the number, versions differ —
|
|
31
|
+
* a PRIOR milestone's phase whose number the current
|
|
32
|
+
* ROADMAP reuses for a new phase;
|
|
33
|
+
* - `none` no match anywhere.
|
|
34
|
+
*
|
|
35
|
+
* Readers (D-03) use `forRead`: the match on live / archived-current /
|
|
36
|
+
* archived-unlisted, null on a collision — exactly what the init family has
|
|
37
|
+
* answered since 261007-jkl. Writers (D-02) use `forWrite`: a live match only;
|
|
38
|
+
* any archived match the caller would write into is refused naming the archive
|
|
39
|
+
* (archived-current, archived-unlisted), and a collision takes the writer's own
|
|
40
|
+
* not-found path. Nothing ever resolves a writer into a closed milestone's record.
|
|
41
|
+
*
|
|
42
|
+
* The current version comes from getMilestoneInfo(cwd) and is read ONLY for an
|
|
43
|
+
* archived match, so a live match does no extra I/O. Known limit (unchanged from
|
|
44
|
+
* 261007-jkl D2): with no version anywhere getMilestoneInfo defaults to v1.0, so
|
|
45
|
+
* a v1.0 archive under a version-less ROADMAP that lists the number counts as
|
|
46
|
+
* current. The archived marker has two readers left: phase-locator, which sets
|
|
47
|
+
* it, and this module, which classifies it.
|
|
48
|
+
*/
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const phaseLocatorModule = require("./phase-locator.cjs");
|
|
51
|
+
const { findPhaseInternal } = phaseLocatorModule;
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
+
const roadmapParserModule = require("./roadmap-parser.cjs");
|
|
54
|
+
const { getRoadmapPhaseInternal, getMilestoneInfo } = roadmapParserModule;
|
|
55
|
+
/** Milestone versions compare after dropping whitespace and one leading v/V; exact match otherwise. */
|
|
56
|
+
function normalizeMilestoneVersion(version) {
|
|
57
|
+
return (typeof version === 'string' ? version : '').trim().replace(/^[vV]/, '').trim();
|
|
58
|
+
}
|
|
59
|
+
function resolvePhaseScope(cwd, phase) {
|
|
60
|
+
const match = findPhaseInternal(cwd, phase);
|
|
61
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
62
|
+
if (!match) {
|
|
63
|
+
return { scope: 'none', match: null, roadmapPhase, currentVersion: null, forRead: null, forWrite: { ok: false, reason: 'not_found' } };
|
|
64
|
+
}
|
|
65
|
+
if (!match.archived) {
|
|
66
|
+
return { scope: 'live', match, roadmapPhase, currentVersion: null, forRead: match, forWrite: { ok: true, match } };
|
|
67
|
+
}
|
|
68
|
+
const currentVersion = getMilestoneInfo(cwd).version;
|
|
69
|
+
const archivedRefusal = { ok: false, reason: 'archived', version: match.archived, directory: match.directory };
|
|
70
|
+
if (!roadmapPhase?.found) {
|
|
71
|
+
return { scope: 'archived-unlisted', match, roadmapPhase, currentVersion, forRead: match, forWrite: archivedRefusal };
|
|
72
|
+
}
|
|
73
|
+
if (normalizeMilestoneVersion(match.archived) === normalizeMilestoneVersion(currentVersion)) {
|
|
74
|
+
return { scope: 'archived-current', match, roadmapPhase, currentVersion, forRead: match, forWrite: archivedRefusal };
|
|
75
|
+
}
|
|
76
|
+
return { scope: 'archived-collision', match, roadmapPhase, currentVersion, forRead: null, forWrite: { ok: false, reason: 'not_found' } };
|
|
77
|
+
}
|
|
78
|
+
/** The match a READER may use (D-03): null on a prior-milestone collision and on no match. */
|
|
79
|
+
function resolvePhaseForRead(cwd, phase) {
|
|
80
|
+
return resolvePhaseScope(cwd, phase).forRead;
|
|
81
|
+
}
|
|
82
|
+
/** The verdict a WRITER must obey (D-02): ok only for a live match. */
|
|
83
|
+
function resolvePhaseForWrite(cwd, phase) {
|
|
84
|
+
return resolvePhaseScope(cwd, phase).forWrite;
|
|
85
|
+
}
|
|
86
|
+
module.exports = {
|
|
87
|
+
resolvePhaseScope,
|
|
88
|
+
resolvePhaseForRead,
|
|
89
|
+
resolvePhaseForWrite,
|
|
90
|
+
normalizeMilestoneVersion,
|
|
91
|
+
};
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase tier — the single owner of how much ceremony a phase runs.
|
|
4
|
+
*
|
|
5
|
+
* A tier is a named preset over the existing gate booleans
|
|
6
|
+
* `workflow.{research, pattern_mapper, plan_check, verifier,
|
|
7
|
+
* nyquist_validation, code_review, post_planning_gaps, security_enforcement}`:
|
|
8
|
+
*
|
|
9
|
+
* light — planner and executor only; every gate off.
|
|
10
|
+
* standard — verifier and code review (with its fix loop) on, plan checker
|
|
11
|
+
* ONE pass and no bounce; everything else off. Schema default.
|
|
12
|
+
* heavy — every gate on, the plan-checker loop up to its maximum
|
|
13
|
+
* (`PLAN_CHECK_MAX_PASSES`, the plan-phase revision cap), plan
|
|
14
|
+
* bounce on, and the fresh-install verification slot
|
|
15
|
+
* (`install_verification`).
|
|
16
|
+
*
|
|
17
|
+
* ## Precedence (`resolvePhaseTier`)
|
|
18
|
+
*
|
|
19
|
+
* CLI `--tier` > the phase's ROADMAP entry `**Tier:**` field > project
|
|
20
|
+
* `phase.tier` in `.planning/config.json` > schema default `standard`.
|
|
21
|
+
*
|
|
22
|
+
* The ROADMAP entry is the ONE owner of the phase field: it exists for every
|
|
23
|
+
* phase before discuss runs, and a light phase may skip discuss (so a
|
|
24
|
+
* CONTEXT.md field would be missing exactly where it matters).
|
|
25
|
+
*
|
|
26
|
+
* A gate boolean the project EXPLICITLY sets to `false` in its own config
|
|
27
|
+
* wins over every tier (listed in `suppressed` when the tier wanted it on). A
|
|
28
|
+
* boolean the project explicitly sets to `true` does NOT force it on: the tier
|
|
29
|
+
* decides on-ness; the project can only switch a gate off. "Explicit" means
|
|
30
|
+
* present in the project's own config.json (workstream, then root) — a value
|
|
31
|
+
* inherited from the manifest default is not explicit.
|
|
32
|
+
*
|
|
33
|
+
* ## Proposal (`proposePhaseTier`)
|
|
34
|
+
*
|
|
35
|
+
* heavy — a plan touches a gate, init, redaction, config-precedence or
|
|
36
|
+
* hook-dispatch file (`HEAVY_PATH_RULES`), or — before any plan
|
|
37
|
+
* exists — the requirement tags or ROADMAP text name one
|
|
38
|
+
* (`HEAVY_TEXT_RULES`);
|
|
39
|
+
* light — plans exist and none touches a source file (`isSourceFile`,
|
|
40
|
+
* the plan-graph predicate);
|
|
41
|
+
* standard — otherwise.
|
|
42
|
+
*
|
|
43
|
+
* Readers: `init plan-phase` / `init execute-phase` / `init verify-work`,
|
|
44
|
+
* `loop render-hooks --phase`, `phase.dispatch-plan`, `phase.tier` and
|
|
45
|
+
* `phase.tier-propose`. Workflows never resolve a tier themselves.
|
|
46
|
+
*/
|
|
47
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.HEAVY_TEXT_RULES = exports.HEAVY_PATH_RULES = exports.TIER_PRESETS = exports.DEFAULT_PLAN_BOUNCE_PASSES = exports.PLAN_CHECK_MAX_PASSES = exports.TIER_GATE_KEYS = exports.SCHEMA_DEFAULT_TIER = exports.PHASE_TIERS = void 0;
|
|
52
|
+
exports.isPhaseTier = isPhaseTier;
|
|
53
|
+
exports.resolvePhaseTier = resolvePhaseTier;
|
|
54
|
+
exports.heavyCategoryForPath = heavyCategoryForPath;
|
|
55
|
+
exports.proposePhaseTier = proposePhaseTier;
|
|
56
|
+
exports.readProjectTierConfig = readProjectTierConfig;
|
|
57
|
+
exports.parsePhaseTierField = parsePhaseTierField;
|
|
58
|
+
exports.readPhaseRecord = readPhaseRecord;
|
|
59
|
+
exports.resolvePhaseTierForProject = resolvePhaseTierForProject;
|
|
60
|
+
exports.overlayTierGates = overlayTierGates;
|
|
61
|
+
exports.recordPhaseTier = recordPhaseTier;
|
|
62
|
+
exports.parseTierFlag = parseTierFlag;
|
|
63
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
64
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
65
|
+
const plan_graph_cjs_1 = require("./plan-graph.cjs");
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
67
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
+
const roadmapParser = require("./roadmap-parser.cjs");
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
71
|
+
const phaseIdModule = require("./phase-id.cjs");
|
|
72
|
+
const { planningDir, planningRoot, withPlanningLock } = planningWorkspace;
|
|
73
|
+
const { getRoadmapPhaseInternal } = roadmapParser;
|
|
74
|
+
const { phaseMarkdownRegexSource } = phaseIdModule;
|
|
75
|
+
exports.PHASE_TIERS = ['light', 'standard', 'heavy'];
|
|
76
|
+
exports.SCHEMA_DEFAULT_TIER = 'standard';
|
|
77
|
+
exports.TIER_GATE_KEYS = [
|
|
78
|
+
'research',
|
|
79
|
+
'pattern_mapper',
|
|
80
|
+
'plan_check',
|
|
81
|
+
'verifier',
|
|
82
|
+
'nyquist_validation',
|
|
83
|
+
'code_review',
|
|
84
|
+
'post_planning_gaps',
|
|
85
|
+
'security_enforcement',
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* The plan-checker loop's current maximum: plan-phase §12 "Revision Loop (Max
|
|
89
|
+
* 3 Iterations)" — the checker runs once after planning and at most twice more
|
|
90
|
+
* after a revision (verify-work's gap-plan loop has the same cap).
|
|
91
|
+
*/
|
|
92
|
+
exports.PLAN_CHECK_MAX_PASSES = 3;
|
|
93
|
+
/** `workflow.plan_bounce_passes` schema default (config-defaults manifest). */
|
|
94
|
+
exports.DEFAULT_PLAN_BOUNCE_PASSES = 2;
|
|
95
|
+
function gates(on) {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const k of exports.TIER_GATE_KEYS)
|
|
98
|
+
out[k] = on.includes(k);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
exports.TIER_PRESETS = Object.freeze({
|
|
102
|
+
light: { gates: gates([]), plan_check_passes: 0, plan_bounce: false, install_verification: false },
|
|
103
|
+
standard: {
|
|
104
|
+
gates: gates(['plan_check', 'verifier', 'code_review']),
|
|
105
|
+
plan_check_passes: 1,
|
|
106
|
+
plan_bounce: false,
|
|
107
|
+
install_verification: false,
|
|
108
|
+
},
|
|
109
|
+
heavy: {
|
|
110
|
+
gates: gates([...exports.TIER_GATE_KEYS]),
|
|
111
|
+
plan_check_passes: exports.PLAN_CHECK_MAX_PASSES,
|
|
112
|
+
plan_bounce: true,
|
|
113
|
+
install_verification: true,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
function isPhaseTier(value) {
|
|
117
|
+
return typeof value === 'string' && exports.PHASE_TIERS.includes(value);
|
|
118
|
+
}
|
|
119
|
+
function isExplicitFalse(value) {
|
|
120
|
+
return value === false || value === 'false';
|
|
121
|
+
}
|
|
122
|
+
function isExplicitTrue(value) {
|
|
123
|
+
return value === true || value === 'true';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the effective gate set for one phase.
|
|
127
|
+
*
|
|
128
|
+
* `cli` must already be a valid tier or null (the CLI rejects anything else).
|
|
129
|
+
*/
|
|
130
|
+
function resolvePhaseTier(input) {
|
|
131
|
+
const ignored = [];
|
|
132
|
+
const candidates = [
|
|
133
|
+
{ source: 'cli', value: input.cli },
|
|
134
|
+
{ source: 'phase', value: input.phaseRecord?.tier },
|
|
135
|
+
{ source: 'project', value: input.config?.tier },
|
|
136
|
+
];
|
|
137
|
+
let tier = exports.SCHEMA_DEFAULT_TIER;
|
|
138
|
+
let source = 'default';
|
|
139
|
+
for (const c of candidates) {
|
|
140
|
+
if (c.value === null || c.value === undefined || c.value === '')
|
|
141
|
+
continue;
|
|
142
|
+
const v = String(c.value).trim().toLowerCase();
|
|
143
|
+
if (isPhaseTier(v)) {
|
|
144
|
+
tier = v;
|
|
145
|
+
source = c.source;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
ignored.push({ source: c.source, value: String(c.value) });
|
|
149
|
+
}
|
|
150
|
+
const preset = exports.TIER_PRESETS[tier];
|
|
151
|
+
const explicit = input.config?.explicit ?? {};
|
|
152
|
+
const suppressed = [];
|
|
153
|
+
const notForced = [];
|
|
154
|
+
const resolve = (key, wanted) => {
|
|
155
|
+
const value = explicit[key];
|
|
156
|
+
if (wanted && isExplicitFalse(value)) {
|
|
157
|
+
suppressed.push(key);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (!wanted && isExplicitTrue(value))
|
|
161
|
+
notForced.push(key);
|
|
162
|
+
return wanted;
|
|
163
|
+
};
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const key of exports.TIER_GATE_KEYS)
|
|
166
|
+
out[key] = resolve(key, preset.gates[key]);
|
|
167
|
+
out.plan_check_passes = out.plan_check ? preset.plan_check_passes : 0;
|
|
168
|
+
out.plan_bounce = resolve('plan_bounce', preset.plan_bounce);
|
|
169
|
+
const passes = input.config?.plan_bounce_passes;
|
|
170
|
+
out.plan_bounce_passes = out.plan_bounce
|
|
171
|
+
? (typeof passes === 'number' && Number.isInteger(passes) && passes > 0 ? passes : exports.DEFAULT_PLAN_BOUNCE_PASSES)
|
|
172
|
+
: 0;
|
|
173
|
+
out.install_verification = preset.install_verification;
|
|
174
|
+
return { tier, source, gates: out, suppressed, not_forced: notForced, ignored };
|
|
175
|
+
}
|
|
176
|
+
/** `src/<name>.cts` or its compiled `pp-core/bin/lib/<name>.cjs`. */
|
|
177
|
+
const LIB = String.raw `^(?:src|pp-core/bin/lib)/`;
|
|
178
|
+
/**
|
|
179
|
+
* Paths that make a phase heavy. Matched against a plan's `files_modified`
|
|
180
|
+
* (normalised, repo-relative). A matching file must also be a source file by
|
|
181
|
+
* the plan-graph predicate, so a test or doc named after a gate never counts.
|
|
182
|
+
*/
|
|
183
|
+
exports.HEAVY_PATH_RULES = Object.freeze([
|
|
184
|
+
{ category: 'gate', example: 'src/ui-safety-gate.cts', pattern: new RegExp(`${LIB}(?:[^/]+/)*[^/]*gate[^/]*\\.c?[jt]s$`) },
|
|
185
|
+
{ category: 'gate', example: 'src/check-command-router.cts', pattern: new RegExp(`${LIB}(?:check-command-router|prohibition-enforcement|gap-checker|plan-graph|dispatch-plan)\\.c?[jt]s$`) },
|
|
186
|
+
{ category: 'gate', example: 'src/practices-review-check.cts', pattern: new RegExp(`${LIB}[^/]+-(?:check|guard)\\.c?[jt]s$`) },
|
|
187
|
+
{ category: 'init', example: 'src/init.cts', pattern: new RegExp(`${LIB}init[^/]*\\.c?[jt]s$`) },
|
|
188
|
+
{ category: 'redaction', example: 'src/observability/redaction.cts', pattern: /(?:^|\/)[^/]*(?:redact|secret)[^/]*$/i },
|
|
189
|
+
{ category: 'config-precedence', example: 'src/config-loader.cts', pattern: new RegExp(`${LIB}(?:config[^/]*|configuration|federated-config|capability-activation|phase-tier)\\.c?[jt]s$`) },
|
|
190
|
+
{ category: 'config-precedence', example: 'pp-core/bin/shared/config-defaults.manifest.json', pattern: /^pp-core\/bin\/shared\/config-[^/]+\.json$/ },
|
|
191
|
+
{ category: 'hook-dispatch', example: 'hooks/pp-workflow-guard.js', pattern: /^hooks\// },
|
|
192
|
+
{ category: 'hook-dispatch', example: 'src/loop-resolver.cts', pattern: new RegExp(`${LIB}(?:loop-resolver|capability-state|runtime-hooks-surface)\\.c?[jt]s$`) },
|
|
193
|
+
{ category: 'hook-dispatch', example: 'capabilities/code-review/capability.json', pattern: /^capabilities\/[^/]+\/capability\.json$/ },
|
|
194
|
+
{ category: 'hook-dispatch', example: 'scripts/build-hooks.js', pattern: /^scripts\/build-hooks\.js$/ },
|
|
195
|
+
]);
|
|
196
|
+
/** Words in requirement tags or ROADMAP text that make a not-yet-planned phase heavy. */
|
|
197
|
+
exports.HEAVY_TEXT_RULES = Object.freeze([
|
|
198
|
+
{ category: 'gate', pattern: /\b(?:gates?|gating)\b/i },
|
|
199
|
+
{ category: 'init', pattern: /\b(?:init|pp-init|initiali[sz]ation)\b/i },
|
|
200
|
+
{ category: 'redaction', pattern: /\b(?:redact(?:s|ed|ion)?|secrets?)\b/i },
|
|
201
|
+
{ category: 'config-precedence', pattern: /\bconfig(?:uration)?\s+(?:precedence|resolution|loader|defaults)\b|\bprecedence\b/i },
|
|
202
|
+
{ category: 'hook-dispatch', pattern: /\bhooks?\b[^.\n]{0,40}\bdispatch\w*\b|\bloop[- ]hooks?\b|\bcapability registry\b/i },
|
|
203
|
+
]);
|
|
204
|
+
/** Requirement-tag prefixes that name a heavy category (e.g. `INIT-02`). */
|
|
205
|
+
const HEAVY_TAG_PREFIX = Object.freeze([
|
|
206
|
+
{ category: 'gate', pattern: /^GATES?-/i },
|
|
207
|
+
{ category: 'init', pattern: /^INIT-/i },
|
|
208
|
+
{ category: 'redaction', pattern: /^(?:REDACT|REDACTION|SECRETS?)-/i },
|
|
209
|
+
{ category: 'config-precedence', pattern: /^(?:CONFIG|CFG|PREC)-/i },
|
|
210
|
+
{ category: 'hook-dispatch', pattern: /^HOOKS?-/i },
|
|
211
|
+
]);
|
|
212
|
+
function normalizePath(file) {
|
|
213
|
+
return String(file).trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
214
|
+
}
|
|
215
|
+
function heavyCategoryForPath(file) {
|
|
216
|
+
const f = normalizePath(file);
|
|
217
|
+
if (f === '' || !(0, plan_graph_cjs_1.isSourceFile)(f))
|
|
218
|
+
return null;
|
|
219
|
+
for (const rule of exports.HEAVY_PATH_RULES)
|
|
220
|
+
if (rule.pattern.test(f))
|
|
221
|
+
return rule;
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function proposePhaseTier(input) {
|
|
225
|
+
const plans = input.plans ?? [];
|
|
226
|
+
if (plans.length > 0) {
|
|
227
|
+
const heavy = [];
|
|
228
|
+
const source = [];
|
|
229
|
+
for (const p of plans) {
|
|
230
|
+
const files = (p.files_modified ?? []).map(normalizePath).filter((f) => f !== '');
|
|
231
|
+
if (files.length === 0)
|
|
232
|
+
source.push({ category: 'undeclared', plan: p.id });
|
|
233
|
+
for (const f of files) {
|
|
234
|
+
const rule = heavyCategoryForPath(f);
|
|
235
|
+
if (rule)
|
|
236
|
+
heavy.push({ category: rule.category, plan: p.id, file: f });
|
|
237
|
+
else if ((0, plan_graph_cjs_1.isSourceFile)(f))
|
|
238
|
+
source.push({ category: 'source', plan: p.id, file: f });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (heavy.length > 0) {
|
|
242
|
+
const cats = [...new Set(heavy.map((m) => m.category))].join(', ');
|
|
243
|
+
return {
|
|
244
|
+
tier: 'heavy',
|
|
245
|
+
basis: 'plans',
|
|
246
|
+
reason: `a plan touches ${cats} code (${heavy.slice(0, 3).map((m) => `${m.plan}: ${m.file}`).join('; ')}${heavy.length > 3 ? '; …' : ''})`,
|
|
247
|
+
matched: heavy,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (source.length === 0) {
|
|
251
|
+
return { tier: 'light', basis: 'plans', reason: 'no plan touches a source file; only docs, tests, workflow and planning files', matched: [] };
|
|
252
|
+
}
|
|
253
|
+
const undeclared = source.filter((m) => m.category === 'undeclared').map((m) => m.plan);
|
|
254
|
+
return {
|
|
255
|
+
tier: 'standard',
|
|
256
|
+
basis: 'plans',
|
|
257
|
+
reason: undeclared.length > 0 && undeclared.length === source.length
|
|
258
|
+
? `plan(s) ${undeclared.join(', ')} declare no files_modified, so source is assumed`
|
|
259
|
+
: 'plans touch source files, none of them gate, init, redaction, config-precedence or hook-dispatch code',
|
|
260
|
+
matched: source,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const matched = [];
|
|
264
|
+
for (const tag of input.requirements ?? []) {
|
|
265
|
+
const t = String(tag).trim();
|
|
266
|
+
for (const r of HEAVY_TAG_PREFIX)
|
|
267
|
+
if (r.pattern.test(t))
|
|
268
|
+
matched.push({ category: r.category, text: t });
|
|
269
|
+
}
|
|
270
|
+
// The phase's own **Tier:** line (and a recorded proposal's reason) is never evidence.
|
|
271
|
+
const text = (input.roadmapText ?? '').replace(/^\*\*Tier(?::\*\*|\*\*:)[^\n]*$/gim, '');
|
|
272
|
+
for (const r of exports.HEAVY_TEXT_RULES) {
|
|
273
|
+
const m = r.pattern.exec(text);
|
|
274
|
+
if (m)
|
|
275
|
+
matched.push({ category: r.category, text: m[0] });
|
|
276
|
+
}
|
|
277
|
+
if (matched.length > 0) {
|
|
278
|
+
const cats = [...new Set(matched.map((m) => m.category))].join(', ');
|
|
279
|
+
return { tier: 'heavy', basis: 'text', reason: `no plans yet; the requirements or ROADMAP entry name ${cats} work (${matched.slice(0, 3).map((m) => `"${m.text}"`).join(', ')})`, matched };
|
|
280
|
+
}
|
|
281
|
+
return { tier: 'standard', basis: 'text', reason: 'no plans yet and nothing names gate, init, redaction, config-precedence or hook-dispatch work; planning re-proposes from files_modified', matched: [] };
|
|
282
|
+
}
|
|
283
|
+
// ─── I/O: project config, phase record, recording ─────────────────────────────
|
|
284
|
+
const FLAT_ALIASES = {
|
|
285
|
+
research: 'research',
|
|
286
|
+
plan_check: 'plan_checker',
|
|
287
|
+
verifier: 'verifier',
|
|
288
|
+
nyquist_validation: 'nyquist_validation',
|
|
289
|
+
post_planning_gaps: 'post_planning_gaps',
|
|
290
|
+
};
|
|
291
|
+
function readJson(file) {
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(file, 'utf-8'));
|
|
294
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function lookup(cfg, section, key, flat) {
|
|
301
|
+
if (flat !== undefined && cfg[flat] !== undefined)
|
|
302
|
+
return cfg[flat];
|
|
303
|
+
const sec = cfg[section];
|
|
304
|
+
if (sec && typeof sec === 'object' && !Array.isArray(sec)) {
|
|
305
|
+
const v = sec[key];
|
|
306
|
+
if (v !== undefined)
|
|
307
|
+
return v;
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The project's own config (workstream config.json first, then the root
|
|
313
|
+
* config.json): `phase.tier`, the gate booleans actually written there, and
|
|
314
|
+
* `workflow.plan_bounce_passes`. Manifest defaults are never read here.
|
|
315
|
+
*/
|
|
316
|
+
function readProjectTierConfig(cwd) {
|
|
317
|
+
const files = [node_path_1.default.join(planningDir(cwd), 'config.json')];
|
|
318
|
+
const root = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
319
|
+
if (root !== files[0])
|
|
320
|
+
files.push(root);
|
|
321
|
+
const configs = files.map(readJson).filter((c) => c !== null);
|
|
322
|
+
const first = (section, key, flat) => {
|
|
323
|
+
for (const c of configs) {
|
|
324
|
+
const v = lookup(c, section, key, flat);
|
|
325
|
+
if (v !== undefined)
|
|
326
|
+
return v;
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
};
|
|
330
|
+
const explicit = {};
|
|
331
|
+
for (const key of [...exports.TIER_GATE_KEYS, 'plan_bounce']) {
|
|
332
|
+
const v = first('workflow', key, FLAT_ALIASES[key]);
|
|
333
|
+
if (v !== undefined)
|
|
334
|
+
explicit[key] = v;
|
|
335
|
+
}
|
|
336
|
+
const tier = first('phase', 'tier');
|
|
337
|
+
const passes = first('workflow', 'plan_bounce_passes');
|
|
338
|
+
const script = first('workflow', 'plan_bounce_script');
|
|
339
|
+
return {
|
|
340
|
+
tier: typeof tier === 'string' ? tier : null,
|
|
341
|
+
explicit,
|
|
342
|
+
plan_bounce_passes: typeof passes === 'number' ? passes : null,
|
|
343
|
+
plan_bounce_script: typeof script === 'string' && script.trim() !== '' ? script : null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const TIER_FIELD_RE = /^\*\*Tier(?::\*\*|\*\*:)[^\S\n]*([A-Za-z-]+)([^\n]*)$/im;
|
|
347
|
+
/** The `**Tier:**` field of a ROADMAP phase section, or null when absent. */
|
|
348
|
+
function parsePhaseTierField(section) {
|
|
349
|
+
if (!section)
|
|
350
|
+
return null;
|
|
351
|
+
const m = TIER_FIELD_RE.exec(section);
|
|
352
|
+
if (!m)
|
|
353
|
+
return null;
|
|
354
|
+
return { tier: m[1].toLowerCase(), proposed: /\(\s*proposed\b/i.test(m[2]), line: m[0] };
|
|
355
|
+
}
|
|
356
|
+
function readPhaseRecord(cwd, phase) {
|
|
357
|
+
const rp = getRoadmapPhaseInternal(cwd, phase);
|
|
358
|
+
const section = rp?.section ?? null;
|
|
359
|
+
const field = parsePhaseTierField(section);
|
|
360
|
+
return {
|
|
361
|
+
tier: field?.tier ?? null,
|
|
362
|
+
proposed: field?.proposed ?? false,
|
|
363
|
+
section,
|
|
364
|
+
phase_name: rp?.phase_name ?? null,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/** Resolve the tier of `phase` in the project at `cwd` (CLI value already validated). */
|
|
368
|
+
function resolvePhaseTierForProject(cwd, phase, cli) {
|
|
369
|
+
const config = readProjectTierConfig(cwd);
|
|
370
|
+
const record = phase ? readPhaseRecord(cwd, phase) : null;
|
|
371
|
+
const phaseRecord = record && record.tier !== null ? { tier: record.tier, proposed: record.proposed } : null;
|
|
372
|
+
const resolved = resolvePhaseTier({ cli, phaseRecord, config });
|
|
373
|
+
return { ...resolved, phase_record: phaseRecord, project_tier: config.tier ?? null, plan_bounce_script: config.plan_bounce_script };
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The gate booleans of a resolved tier as a config overlay
|
|
377
|
+
* (`{ workflow: { research: …, … } }`), merged over `base` without mutating
|
|
378
|
+
* it. Used by `loop render-hooks --phase` so capability `when` keys and
|
|
379
|
+
* activation keys read the tier-resolved values first.
|
|
380
|
+
*/
|
|
381
|
+
function overlayTierGates(base, resolved) {
|
|
382
|
+
const wf = base['workflow'] && typeof base['workflow'] === 'object' && !Array.isArray(base['workflow'])
|
|
383
|
+
? { ...base['workflow'] }
|
|
384
|
+
: {};
|
|
385
|
+
for (const key of exports.TIER_GATE_KEYS)
|
|
386
|
+
wf[key] = resolved.gates[key];
|
|
387
|
+
wf['plan_bounce'] = resolved.gates.plan_bounce;
|
|
388
|
+
const out = { ...base, workflow: wf };
|
|
389
|
+
for (const [key, flat] of Object.entries(FLAT_ALIASES)) {
|
|
390
|
+
if (flat !== undefined && flat in base)
|
|
391
|
+
out[flat] = resolved.gates[key];
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Write `**Tier:** <tier>` into the phase's ROADMAP entry (current milestone
|
|
397
|
+
* only). A proposal (`proposed: true`) is written as `**Tier:** <tier>
|
|
398
|
+
* (proposed: <reason>)` and never replaces an operator choice (a field without
|
|
399
|
+
* the `(proposed` marker). An operator choice always replaces the field.
|
|
400
|
+
*/
|
|
401
|
+
function recordPhaseTier(cwd, phase, tier, opts) {
|
|
402
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
403
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
404
|
+
return { recorded: false, tier: null, proposed: false, reason: 'ROADMAP.md not found' };
|
|
405
|
+
let result = { recorded: false, tier: null, proposed: false, reason: 'phase entry not found in ROADMAP.md' };
|
|
406
|
+
withPlanningLock(cwd, () => {
|
|
407
|
+
const content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
408
|
+
const lastClose = content.lastIndexOf('</details>');
|
|
409
|
+
const offset = lastClose === -1 ? 0 : lastClose + '</details>'.length;
|
|
410
|
+
const scope = content.slice(offset);
|
|
411
|
+
const header = new RegExp(`^#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phase)}\\s*:[^\\n]*$`, 'im');
|
|
412
|
+
const hm = header.exec(scope);
|
|
413
|
+
if (!hm)
|
|
414
|
+
return;
|
|
415
|
+
const start = hm.index;
|
|
416
|
+
const after = scope.slice(start + hm[0].length);
|
|
417
|
+
const next = /\n#{2,4}\s+(?:\[[^\]]+\]\s*)?Phase\s+[\w]|\n#{1,2}\s+(?!#)/.exec(after);
|
|
418
|
+
const end = start + hm[0].length + (next ? next.index : after.length);
|
|
419
|
+
const section = scope.slice(start, end);
|
|
420
|
+
const existing = parsePhaseTierField(section);
|
|
421
|
+
if (existing && opts.proposed && !existing.proposed) {
|
|
422
|
+
result = { recorded: false, tier: existing.tier, proposed: false, reason: `kept the operator's choice (${existing.tier})` };
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const reason = (opts.reason ?? '').replace(/\s+/g, ' ').trim();
|
|
426
|
+
const line = opts.proposed ? `**Tier:** ${tier} (proposed${reason ? `: ${reason}` : ''})` : `**Tier:** ${tier}`;
|
|
427
|
+
let newSection;
|
|
428
|
+
if (existing) {
|
|
429
|
+
newSection = section.replace(existing.line, line);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
const goal = /^\*\*Goal(?:\*\*:|\*?\*?:\*\*)[^\n]*$/im.exec(section);
|
|
433
|
+
const anchorEnd = goal ? goal.index + goal[0].length : hm[0].length;
|
|
434
|
+
newSection = `${section.slice(0, anchorEnd)}\n${line}${section.slice(anchorEnd)}`;
|
|
435
|
+
}
|
|
436
|
+
if (newSection === section) {
|
|
437
|
+
result = { recorded: false, tier, proposed: opts.proposed, reason: 'already recorded' };
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const updated = content.slice(0, offset) + scope.slice(0, start) + newSection + scope.slice(end);
|
|
441
|
+
node_fs_1.default.writeFileSync(roadmapPath, updated, 'utf-8');
|
|
442
|
+
result = { recorded: true, tier, proposed: opts.proposed, reason: opts.proposed ? 'proposal recorded' : 'operator choice recorded' };
|
|
443
|
+
});
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
/** Parse a `--tier` value; returns the tier, null when absent, or an error message. */
|
|
447
|
+
function parseTierFlag(args) {
|
|
448
|
+
const rest = [];
|
|
449
|
+
let tier = null;
|
|
450
|
+
for (let i = 0; i < args.length; i++) {
|
|
451
|
+
const a = args[i];
|
|
452
|
+
let value;
|
|
453
|
+
if (a === '--tier')
|
|
454
|
+
value = args[++i];
|
|
455
|
+
else if (a.startsWith('--tier='))
|
|
456
|
+
value = a.slice('--tier='.length);
|
|
457
|
+
else {
|
|
458
|
+
rest.push(a);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const v = (value ?? '').trim().toLowerCase();
|
|
462
|
+
if (!isPhaseTier(v))
|
|
463
|
+
return { error: `--tier needs one of ${exports.PHASE_TIERS.join(', ')}` };
|
|
464
|
+
tier = v;
|
|
465
|
+
}
|
|
466
|
+
return { tier, rest };
|
|
467
|
+
}
|