@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.
Files changed (255) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -0
  3. package/agents/pp-code-fixer.md +1 -1
  4. package/agents/pp-debug-session-manager.md +1 -1
  5. package/agents/pp-debugger.md +1 -1
  6. package/agents/pp-executor.md +20 -10
  7. package/agents/pp-intel-updater.md +1 -1
  8. package/agents/pp-phase-researcher.md +1 -1
  9. package/agents/pp-plan-checker.md +5 -5
  10. package/agents/pp-planner.md +10 -24
  11. package/agents/pp-project-researcher.md +1 -1
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-researcher.md +1 -1
  14. package/agents/pp-verifier.md +8 -4
  15. package/bin/install.js +429 -82
  16. package/commands/pp-autonomous.md +8 -6
  17. package/commands/pp-discovery-to-build.md +1 -1
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-discuss-phase.md +2 -2
  20. package/commands/pp-execute-phase.md +3 -1
  21. package/commands/pp-graphify.md +5 -5
  22. package/commands/pp-import.md +1 -1
  23. package/commands/pp-map-codebase.md +1 -1
  24. package/commands/pp-ns-project.md +2 -3
  25. package/commands/pp-ns-workflow.md +1 -1
  26. package/commands/pp-plan-phase.md +2 -1
  27. package/commands/pp-verify-work.md +2 -1
  28. package/gemini-extension.json +1 -1
  29. package/hooks/dist/pp-cursor-session-start.js +2 -2
  30. package/hooks/dist/pp-session-state.sh +1 -1
  31. package/hooks/dist/pp-statusline.js +21 -2
  32. package/hooks/pp-cursor-session-start.js +2 -2
  33. package/hooks/pp-session-state.sh +1 -1
  34. package/hooks/pp-statusline.js +21 -2
  35. package/package.json +18 -5
  36. package/pareto-skills/pp-project-setup/SKILL.md +5 -63
  37. package/pp-core/bin/detect-build-commands.sh +182 -0
  38. package/pp-core/bin/lib/capability-registry.cjs +65 -7
  39. package/pp-core/bin/lib/check-command-router.cjs +79 -38
  40. package/pp-core/bin/lib/clusters.cjs +2 -3
  41. package/pp-core/bin/lib/command-aliases.cjs +36 -0
  42. package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
  43. package/pp-core/bin/lib/commands.cjs +21 -6
  44. package/pp-core/bin/lib/compose-bindings.cjs +70 -6
  45. package/pp-core/bin/lib/config-schema.cjs +16 -0
  46. package/pp-core/bin/lib/config.cjs +142 -27
  47. package/pp-core/bin/lib/core-utils.cjs +43 -7
  48. package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
  49. package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
  50. package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
  51. package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
  52. package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
  53. package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
  54. package/pp-core/bin/lib/discovery-table.cjs +98 -0
  55. package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
  56. package/pp-core/bin/lib/frontmatter.cjs +17 -2
  57. package/pp-core/bin/lib/init-command-router.cjs +8 -5
  58. package/pp-core/bin/lib/init-contract.cjs +93 -4
  59. package/pp-core/bin/lib/init.cjs +124 -134
  60. package/pp-core/bin/lib/install-profiles.cjs +5 -0
  61. package/pp-core/bin/lib/install-run.cjs +536 -0
  62. package/pp-core/bin/lib/io.cjs +15 -0
  63. package/pp-core/bin/lib/loop-resolver.cjs +24 -0
  64. package/pp-core/bin/lib/milestone.cjs +345 -48
  65. package/pp-core/bin/lib/phase-command-router.cjs +23 -1
  66. package/pp-core/bin/lib/phase-locator.cjs +15 -6
  67. package/pp-core/bin/lib/phase-scope.cjs +91 -0
  68. package/pp-core/bin/lib/phase-tier.cjs +467 -0
  69. package/pp-core/bin/lib/phase.cjs +247 -21
  70. package/pp-core/bin/lib/phases-command-router.cjs +2 -0
  71. package/pp-core/bin/lib/plan-graph.cjs +368 -0
  72. package/pp-core/bin/lib/planning-workspace.cjs +53 -0
  73. package/pp-core/bin/lib/platform-bindings.cjs +32 -2
  74. package/pp-core/bin/lib/profile-output.cjs +11 -3
  75. package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
  76. package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
  77. package/pp-core/bin/lib/roadmap.cjs +141 -39
  78. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  79. package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
  80. package/pp-core/bin/lib/state-command-router.cjs +82 -13
  81. package/pp-core/bin/lib/state-document.cjs +124 -7
  82. package/pp-core/bin/lib/state.cjs +784 -257
  83. package/pp-core/bin/lib/template.cjs +22 -6
  84. package/pp-core/bin/lib/vault-command-router.cjs +57 -6
  85. package/pp-core/bin/lib/vault-practices.cjs +6 -36
  86. package/pp-core/bin/lib/vault-write-back.cjs +20 -4
  87. package/pp-core/bin/lib/vault.cjs +555 -23
  88. package/pp-core/bin/lib/verify.cjs +53 -31
  89. package/pp-core/bin/lib/workstream.cjs +1 -1
  90. package/pp-core/bin/lib/worktree-safety.cjs +26 -15
  91. package/pp-core/bin/portable-timeout.sh +89 -0
  92. package/pp-core/bin/pp-tools.cjs +149 -9
  93. package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
  94. package/pp-core/bin/shared/config-schema.manifest.json +3 -0
  95. package/pp-core/references/autonomous-best-effort.md +461 -0
  96. package/pp-core/references/autonomous-lifecycle.md +146 -0
  97. package/pp-core/references/autonomous-smart-discuss.md +16 -12
  98. package/pp-core/references/checkpoints.md +1 -1
  99. package/pp-core/references/discovery-id-conventions.md +202 -0
  100. package/pp-core/references/discovery-rebaseline.md +254 -0
  101. package/pp-core/references/discovery-research-track.md +283 -0
  102. package/pp-core/references/discovery-traceability.md +400 -0
  103. package/pp-core/references/loop-hook-dispatch.md +7 -0
  104. package/pp-core/references/mandatory-initial-read.md +1 -1
  105. package/pp-core/references/planner-gap-closure.md +1 -0
  106. package/pp-core/references/planner-human-verify-mode.md +1 -1
  107. package/pp-core/references/planner-load-graph-context.md +1 -1
  108. package/pp-core/references/planner-plan-graph.md +66 -0
  109. package/pp-core/references/planner-tdd.md +1 -0
  110. package/pp-core/references/planning-config.md +8 -0
  111. package/pp-core/references/sketch-tooling.md +4 -0
  112. package/pp-core/references/tdd.md +1 -0
  113. package/pp-core/templates/claude-md.md +1 -1
  114. package/pp-core/templates/phase-prompt.md +6 -2
  115. package/pp-core/templates/prototype-review/review.js +303 -0
  116. package/pp-core/templates/state.md +6 -0
  117. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
  118. package/pp-core/workflows/add-backlog.md +1 -1
  119. package/pp-core/workflows/add-phase.md +1 -1
  120. package/pp-core/workflows/add-tests.md +1 -1
  121. package/pp-core/workflows/add-todo.md +1 -1
  122. package/pp-core/workflows/ai-integration-phase.md +2 -2
  123. package/pp-core/workflows/audit-fix.md +12 -16
  124. package/pp-core/workflows/audit-milestone.md +1 -1
  125. package/pp-core/workflows/audit-uat.md +1 -1
  126. package/pp-core/workflows/autonomous.md +68 -306
  127. package/pp-core/workflows/check-todos.md +1 -1
  128. package/pp-core/workflows/cleanup.md +1 -1
  129. package/pp-core/workflows/code-review-fix.md +1 -1
  130. package/pp-core/workflows/code-review.md +19 -7
  131. package/pp-core/workflows/complete-milestone.md +41 -60
  132. package/pp-core/workflows/debug.md +1 -1
  133. package/pp-core/workflows/diagnose-issues.md +1 -1
  134. package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
  135. package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
  136. package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
  137. package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
  138. package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
  139. package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
  140. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
  141. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
  142. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
  143. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
  144. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
  145. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
  146. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
  147. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
  148. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
  149. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
  150. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
  151. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
  152. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
  153. package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
  154. package/pp-core/workflows/discovery-sprint.md +79 -1714
  155. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
  156. package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
  157. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
  158. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
  159. package/pp-core/workflows/discovery-to-build.md +21 -238
  160. package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
  161. package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
  162. package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
  163. package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
  164. package/pp-core/workflows/discuss-phase.md +9 -2
  165. package/pp-core/workflows/do.md +4 -4
  166. package/pp-core/workflows/docs-update.md +1 -1
  167. package/pp-core/workflows/edit-phase.md +1 -1
  168. package/pp-core/workflows/eval-review.md +1 -1
  169. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
  170. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
  171. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  172. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
  173. package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
  174. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
  175. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
  176. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
  177. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
  178. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
  179. package/pp-core/workflows/execute-phase.md +102 -216
  180. package/pp-core/workflows/execute-plan.md +6 -2
  181. package/pp-core/workflows/explore.md +1 -1
  182. package/pp-core/workflows/extract-learnings.md +1 -1
  183. package/pp-core/workflows/forensics.md +4 -4
  184. package/pp-core/workflows/graduation.md +1 -1
  185. package/pp-core/workflows/health.md +1 -1
  186. package/pp-core/workflows/help/modes/brief.md +1 -0
  187. package/pp-core/workflows/help/modes/default.md +2 -1
  188. package/pp-core/workflows/help/modes/full.md +12 -5
  189. package/pp-core/workflows/import.md +1 -1
  190. package/pp-core/workflows/ingest-docs.md +1 -1
  191. package/pp-core/workflows/init.md +18 -2
  192. package/pp-core/workflows/insert-phase.md +1 -1
  193. package/pp-core/workflows/list-workspaces.md +1 -1
  194. package/pp-core/workflows/manager.md +3 -3
  195. package/pp-core/workflows/map-codebase.md +2 -2
  196. package/pp-core/workflows/milestone-summary.md +4 -4
  197. package/pp-core/workflows/mvp-phase.md +1 -1
  198. package/pp-core/workflows/new-milestone.md +7 -8
  199. package/pp-core/workflows/new-project.md +6 -1
  200. package/pp-core/workflows/new-workspace.md +1 -1
  201. package/pp-core/workflows/next.md +2 -2
  202. package/pp-core/workflows/note.md +2 -2
  203. package/pp-core/workflows/pause-work.md +1 -1
  204. package/pp-core/workflows/plan-milestone-gaps.md +1 -1
  205. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
  206. package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
  207. package/pp-core/workflows/plan-phase.md +43 -167
  208. package/pp-core/workflows/plan-review-convergence.md +2 -2
  209. package/pp-core/workflows/plant-seed.md +1 -1
  210. package/pp-core/workflows/pr-branch.md +1 -1
  211. package/pp-core/workflows/profile-user.md +1 -1
  212. package/pp-core/workflows/progress.md +3 -3
  213. package/pp-core/workflows/quick.md +3 -1
  214. package/pp-core/workflows/remove-phase.md +1 -1
  215. package/pp-core/workflows/remove-workspace.md +1 -1
  216. package/pp-core/workflows/resume-project.md +6 -12
  217. package/pp-core/workflows/review.md +1 -1
  218. package/pp-core/workflows/scan.md +1 -1
  219. package/pp-core/workflows/secure-phase.md +1 -1
  220. package/pp-core/workflows/settings-advanced.md +1 -1
  221. package/pp-core/workflows/settings-integrations.md +1 -1
  222. package/pp-core/workflows/settings.md +1 -1
  223. package/pp-core/workflows/ship.md +25 -8
  224. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  225. package/pp-core/workflows/sketch.md +2 -1
  226. package/pp-core/workflows/spec-phase.md +1 -1
  227. package/pp-core/workflows/spike-wrap-up.md +1 -1
  228. package/pp-core/workflows/spike.md +1 -1
  229. package/pp-core/workflows/stats.md +2 -2
  230. package/pp-core/workflows/thread.md +1 -1
  231. package/pp-core/workflows/transition.md +5 -9
  232. package/pp-core/workflows/ui-phase.md +23 -2
  233. package/pp-core/workflows/ui-review.md +1 -1
  234. package/pp-core/workflows/ultraplan-phase.md +2 -2
  235. package/pp-core/workflows/validate-phase.md +1 -1
  236. package/pp-core/workflows/vault-write-back.md +1 -1
  237. package/pp-core/workflows/verify-phase.md +29 -26
  238. package/pp-core/workflows/verify-work.md +14 -10
  239. package/scripts/base64-scan.sh +7 -41
  240. package/scripts/changeset/cli.cjs +25 -5
  241. package/scripts/dev-env.cjs +462 -0
  242. package/scripts/lib/allowlist-ratchet.cjs +103 -1
  243. package/scripts/lib/size-budget.cjs +311 -0
  244. package/scripts/lint-neutrality-names.cjs +694 -0
  245. package/scripts/lint-org-coordinates.allowlist.json +35 -0
  246. package/scripts/lint-org-coordinates.cjs +426 -0
  247. package/scripts/lint-skill-deps.cjs +48 -9
  248. package/scripts/lint-test-file-count.cjs +17 -2
  249. package/scripts/prompt-injection-scan.sh +41 -20
  250. package/scripts/sync-manifest-versions.cjs +41 -7
  251. package/scripts/sync-runtime-launcher.cjs +106 -1
  252. package/scripts/update-size-baseline.cjs +274 -30
  253. package/scripts/workflow-size.cjs +61 -9
  254. package/commands/pp-build-handoff.md +0 -46
  255. 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
+ }