@deftai/directive-content 0.55.2 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.githooks/pre-commit +143 -0
  2. package/.githooks/pre-push +121 -0
  3. package/QUICK-START.md +2 -2
  4. package/Taskfile.yml +934 -0
  5. package/UPGRADING.md +47 -1
  6. package/events/README.md +3 -3
  7. package/package.json +5 -4
  8. package/scripts/_agents_md.py +494 -0
  9. package/scripts/_cache_fetch.py +635 -0
  10. package/scripts/_cache_quota.py +529 -0
  11. package/scripts/_cache_refresh.py +163 -0
  12. package/scripts/_cache_validate.py +209 -0
  13. package/scripts/_content_root.py +42 -0
  14. package/scripts/_doctor_state.py +277 -0
  15. package/scripts/_event_detect.py +305 -0
  16. package/scripts/_events.py +514 -0
  17. package/scripts/_lifecycle_hygiene.py +568 -0
  18. package/scripts/_pathspec.py +91 -0
  19. package/scripts/_policy_show_cli.py +266 -0
  20. package/scripts/_precutover.py +92 -0
  21. package/scripts/_project_context.py +224 -0
  22. package/scripts/_project_definition_io.py +164 -0
  23. package/scripts/_relocate_snapshot.py +209 -0
  24. package/scripts/_relocate_states.py +343 -0
  25. package/scripts/_resolve_preflight_path.py +152 -0
  26. package/scripts/_safe_subprocess.py +167 -0
  27. package/scripts/_session_start_hook.py +205 -0
  28. package/scripts/_sor_gate_diff.py +365 -0
  29. package/scripts/_stdio_utf8.py +59 -0
  30. package/scripts/_triage_bootstrap_gitignore.py +904 -0
  31. package/scripts/_triage_classify_cli.py +122 -0
  32. package/scripts/_triage_queue_cli.py +625 -0
  33. package/scripts/_triage_scope_cli.py +343 -0
  34. package/scripts/_triage_scope_drift_cli.py +121 -0
  35. package/scripts/_triage_scope_ignores.py +286 -0
  36. package/scripts/_triage_scope_milestone.py +432 -0
  37. package/scripts/_triage_scope_mutations.py +337 -0
  38. package/scripts/_triage_scope_renderers.py +207 -0
  39. package/scripts/_triage_smoketest_stages.py +674 -0
  40. package/scripts/_triage_subscribe_cli.py +140 -0
  41. package/scripts/_triage_welcome_cli.py +421 -0
  42. package/scripts/_vbrief_build.py +239 -0
  43. package/scripts/_vbrief_fidelity.py +479 -0
  44. package/scripts/_vbrief_legacy.py +589 -0
  45. package/scripts/_vbrief_reconciliation.py +883 -0
  46. package/scripts/_vbrief_routing.py +277 -0
  47. package/scripts/_vbrief_safety.py +778 -0
  48. package/scripts/_vbrief_sources.py +312 -0
  49. package/scripts/_vbrief_speckit.py +262 -0
  50. package/scripts/_vbrief_story_quality.py +353 -0
  51. package/scripts/_vbrief_validation.py +299 -0
  52. package/scripts/build_dist.py +412 -0
  53. package/scripts/cache.py +1078 -0
  54. package/scripts/cache_scanner.py +745 -0
  55. package/scripts/candidates_log.py +432 -0
  56. package/scripts/capacity_backfill.py +680 -0
  57. package/scripts/capacity_show.py +653 -0
  58. package/scripts/ci_local.py +689 -0
  59. package/scripts/code_structure_validate.py +765 -0
  60. package/scripts/codebase_default_extractor.py +495 -0
  61. package/scripts/codebase_map.py +304 -0
  62. package/scripts/codebase_map_fresh.py +104 -0
  63. package/scripts/codebase_projection_registry.py +94 -0
  64. package/scripts/codebase_provider.py +582 -0
  65. package/scripts/doctor.py +2257 -0
  66. package/scripts/framework_commands.py +505 -0
  67. package/scripts/gh_rest.py +882 -0
  68. package/scripts/github_auth_modes.py +437 -0
  69. package/scripts/github_body.py +292 -0
  70. package/scripts/ip_risk.py +531 -0
  71. package/scripts/issue_emit.py +670 -0
  72. package/scripts/issue_ingest.py +1064 -0
  73. package/scripts/migrate_preflight.py +418 -0
  74. package/scripts/migrate_vbrief.py +2677 -0
  75. package/scripts/monitor_pr.py +401 -0
  76. package/scripts/pack_migrate_lessons.py +336 -0
  77. package/scripts/pack_migrate_patterns.py +254 -0
  78. package/scripts/pack_migrate_rules.py +350 -0
  79. package/scripts/pack_migrate_skills.py +423 -0
  80. package/scripts/pack_migrate_strategies.py +311 -0
  81. package/scripts/pack_migrate_swarm_spec.py +250 -0
  82. package/scripts/pack_render.py +434 -0
  83. package/scripts/packs_slice.py +712 -0
  84. package/scripts/platform_capabilities.py +336 -0
  85. package/scripts/policy.py +2826 -0
  86. package/scripts/policy_set.py +324 -0
  87. package/scripts/pr_check_closing_keywords.py +524 -0
  88. package/scripts/pr_check_protected_issues.py +267 -0
  89. package/scripts/pr_merge_readiness.py +1004 -0
  90. package/scripts/pr_wait_mergeable.py +669 -0
  91. package/scripts/prd_render.py +159 -0
  92. package/scripts/preflight_architecture_sor.py +974 -0
  93. package/scripts/preflight_branch.py +289 -0
  94. package/scripts/preflight_cache.py +974 -0
  95. package/scripts/preflight_gh.py +721 -0
  96. package/scripts/preflight_implementation.py +272 -0
  97. package/scripts/preflight_story_start.py +838 -0
  98. package/scripts/preflight_wip_cap.py +149 -0
  99. package/scripts/probe_session.py +545 -0
  100. package/scripts/project_render.py +293 -0
  101. package/scripts/quarantine_ext.py +237 -0
  102. package/scripts/reconcile_issues.py +1442 -0
  103. package/scripts/refresh-path.ps1 +107 -0
  104. package/scripts/release.py +2030 -0
  105. package/scripts/release_e2e.py +1011 -0
  106. package/scripts/release_publish.py +486 -0
  107. package/scripts/release_rollback.py +980 -0
  108. package/scripts/relocate.py +1034 -0
  109. package/scripts/resolve_changelog_unreleased.py +667 -0
  110. package/scripts/resolve_version.py +490 -0
  111. package/scripts/resume_conditions.py +706 -0
  112. package/scripts/ritual_sentinel.py +609 -0
  113. package/scripts/roadmap_render.py +635 -0
  114. package/scripts/rule_ownership_lint.py +325 -0
  115. package/scripts/scm.py +591 -0
  116. package/scripts/scope_audit_log.py +387 -0
  117. package/scripts/scope_decompose.py +654 -0
  118. package/scripts/scope_demote.py +509 -0
  119. package/scripts/scope_lifecycle.py +1126 -0
  120. package/scripts/scope_undo.py +772 -0
  121. package/scripts/session_start.py +406 -0
  122. package/scripts/setup_ghx.py +339 -0
  123. package/scripts/setup_windows.ps1 +220 -0
  124. package/scripts/slice_audit.py +585 -0
  125. package/scripts/slice_record.py +530 -0
  126. package/scripts/slice_record_existing.py +692 -0
  127. package/scripts/slug_normalize.py +178 -0
  128. package/scripts/spec_render.py +477 -0
  129. package/scripts/spec_validate.py +238 -0
  130. package/scripts/subagent_monitor.py +658 -0
  131. package/scripts/swarm_complete_cohort.py +644 -0
  132. package/scripts/swarm_launch.py +1206 -0
  133. package/scripts/swarm_readiness.py +554 -0
  134. package/scripts/swarm_verify_review_clean.py +438 -0
  135. package/scripts/swarm_worktrees.py +497 -0
  136. package/scripts/toolchain-check.py +52 -0
  137. package/scripts/triage_actions.py +871 -0
  138. package/scripts/triage_bootstrap.py +1153 -0
  139. package/scripts/triage_bulk.py +630 -0
  140. package/scripts/triage_classify.py +932 -0
  141. package/scripts/triage_help.py +1685 -0
  142. package/scripts/triage_queue.py +1944 -0
  143. package/scripts/triage_reconcile.py +581 -0
  144. package/scripts/triage_refresh.py +643 -0
  145. package/scripts/triage_scope.py +999 -0
  146. package/scripts/triage_scope_drift.py +575 -0
  147. package/scripts/triage_smoketest.py +396 -0
  148. package/scripts/triage_subscribe.py +399 -0
  149. package/scripts/triage_summary.py +1011 -0
  150. package/scripts/triage_welcome.py +1178 -0
  151. package/scripts/ts_check_lane.py +86 -0
  152. package/scripts/validate-links.py +64 -0
  153. package/scripts/validate_strategy_output.py +212 -0
  154. package/scripts/vbrief_activate.py +228 -0
  155. package/scripts/vbrief_migrate_conformance.py +368 -0
  156. package/scripts/vbrief_reconcile_graph.py +306 -0
  157. package/scripts/vbrief_reconcile_labels.py +460 -0
  158. package/scripts/vbrief_reconcile_umbrellas.py +741 -0
  159. package/scripts/vbrief_validate.py +1195 -0
  160. package/scripts/verify-stubs.py +61 -0
  161. package/scripts/verify_capacity.py +160 -0
  162. package/scripts/verify_encoding.py +699 -0
  163. package/scripts/verify_hooks_installed.py +206 -0
  164. package/scripts/verify_investigation.py +360 -0
  165. package/scripts/verify_judgment_gates.py +827 -0
  166. package/scripts/verify_no_task_runtime.py +171 -0
  167. package/scripts/verify_scm_boundary.py +509 -0
  168. package/scripts/verify_session_ritual.py +389 -0
  169. package/scripts/verify_tools.py +426 -0
  170. package/scripts/verify_vbrief_conformance.py +478 -0
  171. package/tasks/architecture.yml +13 -0
  172. package/tasks/cache.yml +69 -0
  173. package/tasks/capacity.yml +38 -0
  174. package/tasks/change.yml +46 -0
  175. package/tasks/changelog.yml +24 -0
  176. package/tasks/ci.yml +49 -0
  177. package/tasks/codebase.yml +47 -0
  178. package/tasks/commit.yml +30 -0
  179. package/tasks/core.yml +126 -0
  180. package/tasks/deployments.yml +54 -0
  181. package/tasks/framework.yml +74 -0
  182. package/tasks/install.yml +60 -0
  183. package/tasks/issue.yml +50 -0
  184. package/tasks/migrate.yml +73 -0
  185. package/tasks/packs.yml +92 -0
  186. package/tasks/policy.yml +75 -0
  187. package/tasks/pr.yml +89 -0
  188. package/tasks/prd.yml +39 -0
  189. package/tasks/project.yml +27 -0
  190. package/tasks/reconcile.yml +32 -0
  191. package/tasks/relocate.yml +56 -0
  192. package/tasks/roadmap.yml +28 -0
  193. package/tasks/scm.yml +126 -0
  194. package/tasks/scope-undo.yml +36 -0
  195. package/tasks/scope.yml +141 -0
  196. package/tasks/session.yml +19 -0
  197. package/tasks/setup.yml +37 -0
  198. package/tasks/slice.yml +69 -0
  199. package/tasks/spec.yml +41 -0
  200. package/tasks/swarm.yml +85 -0
  201. package/tasks/toolchain.yml +13 -0
  202. package/tasks/triage-actions.yml +94 -0
  203. package/tasks/triage-bootstrap.yml +43 -0
  204. package/tasks/triage-bulk.yml +75 -0
  205. package/tasks/triage-classify.yml +30 -0
  206. package/tasks/triage-queue.yml +50 -0
  207. package/tasks/triage-reconcile.yml +29 -0
  208. package/tasks/triage-scope-drift.yml +29 -0
  209. package/tasks/triage-scope.yml +31 -0
  210. package/tasks/triage-smoketest.yml +33 -0
  211. package/tasks/triage-subscribe.yml +36 -0
  212. package/tasks/triage-summary.yml +29 -0
  213. package/tasks/triage-welcome.yml +32 -0
  214. package/tasks/ts.yml +328 -0
  215. package/tasks/vbrief.yml +206 -0
  216. package/tasks/verify.yml +292 -0
  217. package/templates/agents-entry.md +1 -1
@@ -0,0 +1,140 @@
1
+ """CLI helpers for ``scripts/triage_subscribe.py`` (D14 / #1133)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ _RECONCILE_HINT = (
12
+ " Reconciliation: run `task triage:bootstrap -- --resume` to "
13
+ "backfill / mark out-of-scope cached entries."
14
+ )
15
+
16
+
17
+ def build_parser(op: str) -> argparse.ArgumentParser:
18
+ """Build the subscribe / unsubscribe arg parser. ``op`` is one of the two."""
19
+ if op not in {"subscribe", "unsubscribe"}:
20
+ raise ValueError(f"unknown op {op!r}")
21
+ parser = argparse.ArgumentParser(
22
+ prog=f"triage_subscribe.py {op}",
23
+ description=(
24
+ f"{op.capitalize()} a rule on plan.policy.triageScope[]. "
25
+ "Exactly one of --label / --milestone / --issue is required. "
26
+ "Atomic; idempotent; appends a subscription-change record to "
27
+ "vbrief/.eval/subscription-history.jsonl (D14 / #1133)."
28
+ ),
29
+ )
30
+ parser.add_argument(
31
+ "op",
32
+ choices=["subscribe", "unsubscribe"],
33
+ help="The operation to perform (positional discriminator).",
34
+ )
35
+ parser.add_argument(
36
+ "--project-root",
37
+ default=os.environ.get("DEFT_PROJECT_ROOT", "."),
38
+ help=(
39
+ "Consumer project root (default: $DEFT_PROJECT_ROOT or cwd)."
40
+ ),
41
+ )
42
+ parser.add_argument(
43
+ "--label",
44
+ default=None,
45
+ help="Label name to (un)subscribe (mutually exclusive with --milestone/--issue).",
46
+ )
47
+ parser.add_argument(
48
+ "--milestone",
49
+ default=None,
50
+ help="Milestone name to (un)subscribe (mutually exclusive).",
51
+ )
52
+ parser.add_argument(
53
+ "--issue",
54
+ type=int,
55
+ default=None,
56
+ help="Issue number to (un)subscribe via explicit-watch (mutually exclusive).",
57
+ )
58
+ parser.add_argument(
59
+ "--issue-note",
60
+ default="added via task triage:subscribe",
61
+ help=(
62
+ "Note attached to a new explicit-watch entry (subscribe only; "
63
+ "ignored on unsubscribe). Required for future-operator legibility "
64
+ "per #1131 Decision 4."
65
+ ),
66
+ )
67
+ parser.add_argument(
68
+ "--actor",
69
+ default=None,
70
+ help=(
71
+ "Override the audit-log actor field (default: $DEFT_TRIAGE_ACTOR "
72
+ "or 'user:<login>')."
73
+ ),
74
+ )
75
+ return parser
76
+
77
+
78
+ def run_cli(argv: list[str] | None, module: Any) -> int:
79
+ """Dispatch the subscribe / unsubscribe CLI."""
80
+ raw = list(argv) if argv is not None else sys.argv[1:]
81
+ if not raw or raw[0] not in {"subscribe", "unsubscribe"}:
82
+ print(
83
+ "triage:subscribe: first positional arg must be 'subscribe' or "
84
+ "'unsubscribe'; e.g. task triage:subscribe -- --label=bug",
85
+ file=sys.stderr,
86
+ )
87
+ return 2
88
+ op = raw[0]
89
+ parser = build_parser(op)
90
+ args = parser.parse_args(raw)
91
+
92
+ project_root = Path(args.project_root).resolve()
93
+ if not project_root.exists() or not project_root.is_dir():
94
+ print(
95
+ f"triage:{op}: --project-root {project_root} does not exist "
96
+ "or is not a directory.",
97
+ file=sys.stderr,
98
+ )
99
+ return 2
100
+
101
+ chosen = sum(
102
+ 1 for v in (args.label, args.milestone, args.issue) if v is not None
103
+ )
104
+ if chosen != 1:
105
+ print(
106
+ f"triage:{op}: exactly one of --label / --milestone / --issue "
107
+ "is required.",
108
+ file=sys.stderr,
109
+ )
110
+ return 2
111
+
112
+ try:
113
+ if op == "subscribe":
114
+ changed, message = module.subscribe(
115
+ project_root,
116
+ label=args.label,
117
+ milestone=args.milestone,
118
+ issue=args.issue,
119
+ issue_note=args.issue_note,
120
+ actor=args.actor,
121
+ )
122
+ else:
123
+ changed, message = module.unsubscribe(
124
+ project_root,
125
+ label=args.label,
126
+ milestone=args.milestone,
127
+ issue=args.issue,
128
+ actor=args.actor,
129
+ )
130
+ except Exception as exc: # pylint: disable=broad-except
131
+ print(f"triage:{op}: {exc}", file=sys.stderr)
132
+ return 1
133
+
134
+ if not changed:
135
+ print(f"triage:{op}: {message} (no-op).", file=sys.stderr)
136
+ return 0
137
+
138
+ print(f"triage:{op}: {message}.")
139
+ print(_RECONCILE_HINT, file=sys.stderr)
140
+ return 0
@@ -0,0 +1,421 @@
1
+ """CLI + prompt helpers for ``scripts/triage_welcome.py`` (#1143).
2
+
3
+ Extracted from ``scripts/triage_welcome.py`` so the parent module stays
4
+ under the 500-line SHOULD ceiling from ``coding/coding.md``. The public
5
+ ritual surface lives in :mod:`triage_welcome`; this module is the
6
+ argparse shim, the deterministic-questions-compliant numbered-menu
7
+ helpers, and the yes/no + integer prompt helpers only.
8
+
9
+ Mirrors the split convention established by ``scripts/_triage_scope_cli.py``
10
+ (#1131 / D12) and ``scripts/_triage_queue_cli.py`` (#1128 / D11).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import os
17
+ import sys
18
+ from collections.abc import Callable
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ if TYPE_CHECKING: # pragma: no cover -- import-time-only typing alias
24
+ from triage_welcome import PriorState, WelcomeOutcome
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Default-mode (non-onboard) nudge strings (#1309)
28
+ # ---------------------------------------------------------------------------
29
+
30
+ #: Environment variable used by Taskfile wrappers to tell the Python CLI which
31
+ #: namespace prefix exposes sibling tasks in the caller's project.
32
+ TASK_PREFIX_ENV_VAR: str = "DEFT_TASK_PREFIX"
33
+
34
+ #: Default-mode nudge string emitted by :func:`run_default_mode` when the
35
+ #: operator has never run ``deft triage:welcome --onboard``. Kept as a
36
+ #: module-level constant so tests can pin the exact byte-shape and so
37
+ #: future copy edits land in one place.
38
+ FIRST_TIME_NUDGE: str = (
39
+ "[welcome] First-time? Run `deft triage:welcome --onboard` "
40
+ "to set up triage."
41
+ )
42
+
43
+ #: Template for the partial-onboarding nudge. ``{missing}`` is filled with
44
+ #: a stable `" + "`-joined list of absent state pieces (see
45
+ #: :func:`_classify_onboarding`).
46
+ INCOMPLETE_NUDGE_TEMPLATE: str = (
47
+ "[welcome] Onboarding incomplete: {missing}. Run "
48
+ "`deft triage:welcome --onboard` to resume."
49
+ )
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Default IO -- tests inject overrides
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ def default_input(prompt: str) -> str:
58
+ return input(prompt)
59
+
60
+
61
+ def default_output(line: str = "") -> None:
62
+ print(line)
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Numbered-menu prompts (contracts/deterministic-questions.md compliant)
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ @dataclass
71
+ class PromptOutcome:
72
+ """Structured prompt result -- ``discuss`` / ``back`` / ``value``."""
73
+
74
+ discuss: bool = False
75
+ back: bool = False
76
+ value: Any = None
77
+
78
+
79
+ def prompt_menu(
80
+ *,
81
+ title: str,
82
+ options: list[tuple[str, str]],
83
+ default_index: int,
84
+ input_fn: Callable[[str], str],
85
+ output_fn: Callable[[str], None],
86
+ ) -> PromptOutcome:
87
+ """Render a numbered menu and return the operator's choice.
88
+
89
+ Options are ``(label, value-key)`` tuples; the renderer appends
90
+ ``Discuss`` and ``Back`` as the canonical final two options per
91
+ :doc:`contracts/deterministic-questions.md`. Empty input accepts
92
+ *default_index* (0-based). Invalid input re-renders the menu.
93
+ """
94
+ discuss_idx = len(options) + 1
95
+ back_idx = len(options) + 2
96
+ while True:
97
+ output_fn(title)
98
+ for i, (label, _key) in enumerate(options, start=1):
99
+ marker = " (default)" if i - 1 == default_index else ""
100
+ output_fn(f" {i}) {label}{marker}")
101
+ output_fn(f" {discuss_idx}) Discuss")
102
+ output_fn(f" {back_idx}) Back")
103
+ try:
104
+ raw = input_fn(f" > [{default_index + 1}] ")
105
+ except EOFError:
106
+ raw = ""
107
+ choice = raw.strip()
108
+ if not choice:
109
+ _label, key = options[default_index]
110
+ return PromptOutcome(value=key)
111
+ if not choice.isdecimal():
112
+ output_fn(f" ! Invalid selection: {choice!r}. Pick a number.")
113
+ continue
114
+ n = int(choice)
115
+ if 1 <= n <= len(options):
116
+ _label, key = options[n - 1]
117
+ return PromptOutcome(value=key)
118
+ if n == discuss_idx:
119
+ output_fn(
120
+ " [discuss] Pausing the ritual. Re-run "
121
+ "`deft triage:welcome` after the discussion to resume."
122
+ )
123
+ return PromptOutcome(discuss=True)
124
+ if n == back_idx:
125
+ return PromptOutcome(back=True)
126
+ output_fn(f" ! Out-of-range selection: {n}. Pick 1..{back_idx}.")
127
+
128
+
129
+ def prompt_yes_no(
130
+ *,
131
+ title: str,
132
+ default_yes: bool,
133
+ input_fn: Callable[[str], str],
134
+ output_fn: Callable[[str], None],
135
+ ) -> bool:
136
+ """Yes/no confirm; empty input accepts *default_yes*."""
137
+ suffix = "[Y/n]" if default_yes else "[y/N]"
138
+ try:
139
+ raw = input_fn(f" {title} {suffix} ")
140
+ except EOFError:
141
+ raw = ""
142
+ text = raw.strip().lower()
143
+ if not text:
144
+ return default_yes
145
+ if text in {"y", "yes"}:
146
+ return True
147
+ if text in {"n", "no"}:
148
+ return False
149
+ output_fn(f" ! Unrecognized: {raw!r}; treating as 'n'.")
150
+ return False
151
+
152
+
153
+ def prompt_int(
154
+ *,
155
+ title: str,
156
+ default: int,
157
+ input_fn: Callable[[str], str],
158
+ output_fn: Callable[[str], None],
159
+ minimum: int = 1,
160
+ ) -> int | None:
161
+ """Free-text positive int with default; returns None on Discuss/Back."""
162
+ while True:
163
+ try:
164
+ raw = input_fn(f" {title} (default {default}): ")
165
+ except EOFError:
166
+ raw = ""
167
+ text = raw.strip()
168
+ if not text:
169
+ return default
170
+ if text.lower() in {"discuss", "back"}:
171
+ return None
172
+ if not text.isdecimal():
173
+ output_fn(f" ! Not a positive integer: {raw!r}. Try again.")
174
+ continue
175
+ value = int(text)
176
+ if value < minimum:
177
+ output_fn(f" ! Value {value} below minimum {minimum}. Try again.")
178
+ continue
179
+ return value
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # argparse shim
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def build_parser() -> argparse.ArgumentParser:
188
+ parser = argparse.ArgumentParser(
189
+ prog="triage_welcome.py",
190
+ description=(
191
+ "Emit the `deft triage:welcome` session-start status surface "
192
+ "(#1309 default mode -- summary one-liner plus a state-conditional "
193
+ "first-time / incomplete-onboarding nudge), or run the full "
194
+ "6-phase interactive onboarding ritual (#1143) under --onboard. "
195
+ "Idempotent -- re-run after a partial completion to resume cleanly."
196
+ ),
197
+ )
198
+ parser.add_argument(
199
+ "--project-root",
200
+ default=os.environ.get("DEFT_PROJECT_ROOT", "."),
201
+ help="Consumer project root (default: $DEFT_PROJECT_ROOT or cwd).",
202
+ )
203
+ parser.add_argument(
204
+ "--onboard",
205
+ action="store_true",
206
+ help=(
207
+ "Run the interactive 6-phase onboarding ritual (#1143). "
208
+ "Without this flag (the default), `deft triage:welcome` emits "
209
+ "the non-interactive summary one-liner plus a state-conditional "
210
+ "nudge pointing at `--onboard` when state is missing or partial "
211
+ "(#1309)."
212
+ ),
213
+ )
214
+ parser.add_argument(
215
+ "--no-subprocess",
216
+ action="store_true",
217
+ help=(
218
+ "Skip the `deft triage:bootstrap` / `deft scope:demote` / "
219
+ "`deft triage:summary` follow-up hops. Test-mode flag for the "
220
+ "--onboard ritual; never set in production runs."
221
+ ),
222
+ )
223
+ parser.add_argument(
224
+ "--task-prefix",
225
+ default=os.environ.get(TASK_PREFIX_ENV_VAR, ""),
226
+ help=(
227
+ "Optional Taskfile namespace prefix for sibling task dispatches "
228
+ "(for example `deft:` in consumer includes). Defaults to "
229
+ f"${TASK_PREFIX_ENV_VAR}."
230
+ ),
231
+ )
232
+ parser.add_argument(
233
+ "--skip-bootstrap",
234
+ action="store_true",
235
+ help=(
236
+ "Explicitly decline the --onboard Phase 3 `deft triage:bootstrap` "
237
+ "invocation (#1244). The ritual still completes but emits a "
238
+ "visible audit message AND records the decline in "
239
+ "`meta/policy-changes.log`; downstream verbs that depend on "
240
+ "`vbrief/.eval/candidates.jsonl` will refuse to run until "
241
+ "bootstrap is invoked separately."
242
+ ),
243
+ )
244
+ parser.add_argument(
245
+ "--no-history",
246
+ action="store_true",
247
+ help=(
248
+ "Suppress the `vbrief/.eval/summary-history.jsonl` append "
249
+ "when emitting the default-mode summary (#1309). Test-mode "
250
+ "flag; production callers SHOULD let the history sidecar "
251
+ "track every invocation."
252
+ ),
253
+ )
254
+ return parser
255
+
256
+
257
+ def run_cli(argv: list[str] | None, tw_module: Any) -> int:
258
+ """Dispatch ``triage_welcome`` CLI args using ``tw_module`` backend.
259
+
260
+ ``tw_module`` is the parent :mod:`triage_welcome` module; passed
261
+ explicitly to avoid a circular import at module-load time.
262
+
263
+ Default invocation (no ``--onboard``) routes to the non-interactive
264
+ :func:`triage_welcome.run_default_mode` surface (#1309); ``--onboard``
265
+ routes to the original 6-phase interactive ritual
266
+ :func:`triage_welcome.run_welcome` (#1143).
267
+ """
268
+ parser = build_parser()
269
+ args = parser.parse_args(argv)
270
+ project_root = Path(args.project_root).resolve()
271
+ if not project_root.is_dir():
272
+ print(
273
+ f"triage:welcome: --project-root {project_root} is not a directory.",
274
+ file=sys.stderr,
275
+ )
276
+ return 2
277
+ if args.onboard:
278
+ outcome = tw_module.run_welcome(
279
+ project_root,
280
+ run_subprocess=not args.no_subprocess,
281
+ skip_bootstrap=args.skip_bootstrap,
282
+ task_prefix=args.task_prefix,
283
+ )
284
+ else:
285
+ outcome = tw_module.run_default_mode(
286
+ project_root,
287
+ write_history=not args.no_history,
288
+ task_prefix=args.task_prefix,
289
+ )
290
+ return outcome.exit_code
291
+
292
+
293
+ # ---------------------------------------------------------------------------
294
+ # Default-mode (non-onboard) helpers (#1309)
295
+ #
296
+ # Hosted here -- not in :mod:`triage_welcome` -- so the parent module stays
297
+ # under the 1000-line MUST cap from ``coding/coding.md``. Re-exported by
298
+ # :mod:`triage_welcome` for backward compatibility with callers / tests that
299
+ # reference ``triage_welcome.<name>``.
300
+ # ---------------------------------------------------------------------------
301
+
302
+
303
+ def _classify_onboarding(state: PriorState) -> tuple[str, list[str]]:
304
+ """Return ``(state_label, missing_pieces)`` for the default-mode nudge.
305
+
306
+ Three discrete states keyed off the canonical "has the operator run
307
+ onboarding?" signals (#1309 vBRIEF / paired with #1308):
308
+
309
+ - ``"first-time"`` -- NONE of the three signals present: no
310
+ ``vbrief/.eval/candidates.jsonl``, no ``plan.policy.triageScope``,
311
+ no ``plan.policy.wipCap``. The operator has never run
312
+ ``deft triage:welcome --onboard``.
313
+ - ``"incomplete"`` -- a strict subset (1 or 2) of the three signals
314
+ present; ``missing_pieces`` names the absent piece(s) so the
315
+ operator-facing nudge can be specific.
316
+ - ``"fully-set-up"`` -- all three signals present.
317
+
318
+ Pure helper -- no I/O, no audit log.
319
+ """
320
+ signals = {
321
+ "candidates.jsonl": state.audit_log_present,
322
+ "triageScope": state.triage_scope_set,
323
+ "wipCap": state.wip_cap_set,
324
+ }
325
+ present = [name for name, ok in signals.items() if ok]
326
+ missing = [name for name, ok in signals.items() if not ok]
327
+ if not present:
328
+ return "first-time", missing
329
+ if not missing:
330
+ return "fully-set-up", []
331
+ return "incomplete", missing
332
+
333
+
334
+ def emit_oneliner(
335
+ project_root: Path,
336
+ *,
337
+ output_fn: Callable[[str], None] | None = None,
338
+ write_history: bool = True,
339
+ ) -> str:
340
+ """Emit the ``deft triage:summary`` one-liner via internal Python call.
341
+
342
+ Mirrors the byte-shape produced by
343
+ ``scripts/triage_summary.py::main`` (the headline plus, when
344
+ applicable, the second ``[triage:scope]`` line per #1270) without
345
+ spawning a subprocess. ``write_history`` controls whether the
346
+ rolling ``vbrief/.eval/summary-history.jsonl`` sidecar is appended
347
+ to; default-mode welcome runs DO append so observability stays
348
+ aligned with direct ``deft triage:summary`` invocations.
349
+
350
+ Returns the rendered line(s) so callers can compose with downstream
351
+ state without re-rendering.
352
+ """
353
+ # Lazy-import to keep startup cost off the interactive ritual path
354
+ # and to mirror the existing :func:`run_welcome` Phase 6 idiom of
355
+ # treating ``triage_summary`` as a sibling module.
356
+ import triage_summary # noqa: I001
357
+
358
+ out_fn = output_fn or default_output
359
+ result = triage_summary.compute_summary(project_root)
360
+ line = triage_summary.format_summary(result)
361
+ out_fn(line)
362
+ if write_history:
363
+ history_path = project_root / triage_summary.SUMMARY_HISTORY_REL_PATH
364
+ triage_summary.append_history(history_path, result, line)
365
+ return line
366
+
367
+
368
+ def run_default_mode(
369
+ project_root: Path,
370
+ *,
371
+ output_fn: Callable[[str], None] | None = None,
372
+ write_history: bool = True,
373
+ task_prefix: str | None = None,
374
+ ) -> WelcomeOutcome:
375
+ """Non-interactive default mode for ``deft triage:welcome`` (#1309).
376
+
377
+ Subsumes the prior session-start step of running
378
+ ``deft triage:summary`` plus a state-conditional first-time /
379
+ incomplete-onboarding nudge so a fresh consumer sees one
380
+ actionable line. The interactive 6-phase ritual now lives behind
381
+ ``deft triage:welcome --onboard`` (see :func:`run_cli`).
382
+
383
+ No interactive prompts; the function never reads from stdin and is
384
+ safe to invoke from any non-tty surface (CI, cloud agents, etc.).
385
+ Always returns ``exit_code=0`` -- the default-mode surface is a
386
+ status report, not a gate.
387
+ """
388
+ # Lazy-import the parent module so we can reach ``detect_prior_state``
389
+ # / ``WelcomeOutcome`` without a module-load cycle (parent module
390
+ # imports names from this file at top level; reverse direction MUST
391
+ # be deferred).
392
+ import triage_welcome # noqa: I001
393
+
394
+ out_fn = output_fn or default_output
395
+ outcome = triage_welcome.WelcomeOutcome()
396
+ outcome.phases_run.append(0) # "phase 0" = default-mode summary
397
+ emit_oneliner(project_root, output_fn=out_fn, write_history=write_history)
398
+ state = triage_welcome.detect_prior_state(project_root)
399
+ label, missing = _classify_onboarding(state)
400
+ canonical_onboard_command = triage_welcome.format_welcome_command(
401
+ ["triage:welcome", "--onboard"]
402
+ )
403
+ onboard_command = triage_welcome.format_welcome_command(
404
+ ["triage:welcome", "--onboard"],
405
+ task_prefix=task_prefix,
406
+ )
407
+ if label == "first-time":
408
+ out_fn(FIRST_TIME_NUDGE.replace(canonical_onboard_command, onboard_command))
409
+ elif label == "incomplete":
410
+ # Stable, deterministic ordering for the missing-piece list so
411
+ # tests can pin the byte-shape across runs.
412
+ joined = " + ".join(missing)
413
+ out_fn(
414
+ INCOMPLETE_NUDGE_TEMPLATE.format(missing=joined).replace(
415
+ canonical_onboard_command,
416
+ onboard_command,
417
+ )
418
+ )
419
+ # ``fully-set-up`` is silent -- the summary line alone is enough.
420
+ outcome.exit_code = 0
421
+ return outcome