@deftai/directive-content 0.59.0 → 0.61.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 (190) hide show
  1. package/.githooks/pre-commit +10 -128
  2. package/.githooks/pre-push +8 -108
  3. package/Taskfile.yml +48 -58
  4. package/UPGRADING.md +19 -3
  5. package/docs/assets/directive-lifecycle-diagram.png +0 -0
  6. package/docs/directive-lifecycle.md +73 -0
  7. package/docs/getting-started.md +5 -1
  8. package/package.json +3 -3
  9. package/packs/skills/skills-pack-0.1.json +1 -1
  10. package/packs/strategies/strategies-pack-0.1.json +19 -19
  11. package/scm/github.md +37 -6
  12. package/skills/deft-directive-setup/SKILL.md +24 -15
  13. package/strategies/speckit.md +14 -14
  14. package/strategies/v0-20-contract.md +12 -1
  15. package/tasks/change.yml +16 -31
  16. package/tasks/ci.yml +8 -0
  17. package/tasks/commit.yml +12 -19
  18. package/tasks/core.yml +10 -0
  19. package/tasks/engine.yml +42 -0
  20. package/tasks/framework.yml +3 -0
  21. package/tasks/install.yml +20 -19
  22. package/tasks/migrate.yml +26 -15
  23. package/tasks/project.yml +26 -0
  24. package/tasks/toolchain.yml +15 -5
  25. package/tasks/vbrief.yml +4 -3
  26. package/tasks/verify.yml +12 -14
  27. package/templates/agents-entry.md +1 -1
  28. package/scripts/_agents_md.py +0 -494
  29. package/scripts/_cache_fetch.py +0 -635
  30. package/scripts/_cache_quota.py +0 -529
  31. package/scripts/_cache_refresh.py +0 -163
  32. package/scripts/_cache_validate.py +0 -209
  33. package/scripts/_content_root.py +0 -42
  34. package/scripts/_doctor_state.py +0 -277
  35. package/scripts/_event_detect.py +0 -305
  36. package/scripts/_events.py +0 -514
  37. package/scripts/_lifecycle_hygiene.py +0 -568
  38. package/scripts/_pathspec.py +0 -91
  39. package/scripts/_policy_show_cli.py +0 -266
  40. package/scripts/_precutover.py +0 -92
  41. package/scripts/_project_context.py +0 -224
  42. package/scripts/_project_definition_io.py +0 -164
  43. package/scripts/_relocate_snapshot.py +0 -209
  44. package/scripts/_relocate_states.py +0 -343
  45. package/scripts/_resolve_preflight_path.py +0 -152
  46. package/scripts/_safe_subprocess.py +0 -167
  47. package/scripts/_session_start_hook.py +0 -205
  48. package/scripts/_sor_gate_diff.py +0 -365
  49. package/scripts/_stdio_utf8.py +0 -59
  50. package/scripts/_triage_bootstrap_gitignore.py +0 -904
  51. package/scripts/_triage_classify_cli.py +0 -122
  52. package/scripts/_triage_queue_cli.py +0 -625
  53. package/scripts/_triage_scope_cli.py +0 -343
  54. package/scripts/_triage_scope_drift_cli.py +0 -121
  55. package/scripts/_triage_scope_ignores.py +0 -286
  56. package/scripts/_triage_scope_milestone.py +0 -432
  57. package/scripts/_triage_scope_mutations.py +0 -337
  58. package/scripts/_triage_scope_renderers.py +0 -207
  59. package/scripts/_triage_smoketest_stages.py +0 -674
  60. package/scripts/_triage_subscribe_cli.py +0 -140
  61. package/scripts/_triage_welcome_cli.py +0 -421
  62. package/scripts/_vbrief_build.py +0 -239
  63. package/scripts/_vbrief_fidelity.py +0 -479
  64. package/scripts/_vbrief_legacy.py +0 -589
  65. package/scripts/_vbrief_reconciliation.py +0 -883
  66. package/scripts/_vbrief_routing.py +0 -277
  67. package/scripts/_vbrief_safety.py +0 -778
  68. package/scripts/_vbrief_sources.py +0 -312
  69. package/scripts/_vbrief_speckit.py +0 -262
  70. package/scripts/_vbrief_story_quality.py +0 -353
  71. package/scripts/_vbrief_validation.py +0 -299
  72. package/scripts/build_dist.py +0 -412
  73. package/scripts/cache.py +0 -1078
  74. package/scripts/cache_scanner.py +0 -745
  75. package/scripts/candidates_log.py +0 -432
  76. package/scripts/capacity_backfill.py +0 -680
  77. package/scripts/capacity_show.py +0 -653
  78. package/scripts/ci_local.py +0 -689
  79. package/scripts/code_structure_validate.py +0 -765
  80. package/scripts/codebase_default_extractor.py +0 -495
  81. package/scripts/codebase_map.py +0 -304
  82. package/scripts/codebase_map_fresh.py +0 -104
  83. package/scripts/codebase_projection_registry.py +0 -94
  84. package/scripts/codebase_provider.py +0 -582
  85. package/scripts/doctor.py +0 -2552
  86. package/scripts/framework_commands.py +0 -505
  87. package/scripts/gh_rest.py +0 -882
  88. package/scripts/github_auth_modes.py +0 -437
  89. package/scripts/github_body.py +0 -292
  90. package/scripts/ip_risk.py +0 -531
  91. package/scripts/issue_emit.py +0 -670
  92. package/scripts/issue_ingest.py +0 -1064
  93. package/scripts/migrate_preflight.py +0 -418
  94. package/scripts/migrate_vbrief.py +0 -2677
  95. package/scripts/monitor_pr.py +0 -401
  96. package/scripts/pack_migrate_lessons.py +0 -336
  97. package/scripts/pack_migrate_patterns.py +0 -254
  98. package/scripts/pack_migrate_rules.py +0 -350
  99. package/scripts/pack_migrate_skills.py +0 -423
  100. package/scripts/pack_migrate_strategies.py +0 -311
  101. package/scripts/pack_migrate_swarm_spec.py +0 -250
  102. package/scripts/pack_render.py +0 -434
  103. package/scripts/packs_slice.py +0 -712
  104. package/scripts/platform_capabilities.py +0 -336
  105. package/scripts/policy.py +0 -2826
  106. package/scripts/policy_set.py +0 -324
  107. package/scripts/pr_check_closing_keywords.py +0 -524
  108. package/scripts/pr_check_protected_issues.py +0 -267
  109. package/scripts/pr_merge_readiness.py +0 -1004
  110. package/scripts/pr_wait_mergeable.py +0 -669
  111. package/scripts/prd_render.py +0 -159
  112. package/scripts/preflight_architecture_sor.py +0 -974
  113. package/scripts/preflight_branch.py +0 -289
  114. package/scripts/preflight_cache.py +0 -974
  115. package/scripts/preflight_gh.py +0 -721
  116. package/scripts/preflight_implementation.py +0 -272
  117. package/scripts/preflight_story_start.py +0 -838
  118. package/scripts/preflight_wip_cap.py +0 -149
  119. package/scripts/probe_session.py +0 -545
  120. package/scripts/project_render.py +0 -293
  121. package/scripts/quarantine_ext.py +0 -237
  122. package/scripts/reconcile_issues.py +0 -1442
  123. package/scripts/refresh-path.ps1 +0 -107
  124. package/scripts/release.py +0 -2030
  125. package/scripts/release_e2e.py +0 -1011
  126. package/scripts/release_publish.py +0 -486
  127. package/scripts/release_rollback.py +0 -980
  128. package/scripts/relocate.py +0 -1034
  129. package/scripts/resolve_changelog_unreleased.py +0 -667
  130. package/scripts/resolve_version.py +0 -490
  131. package/scripts/resume_conditions.py +0 -706
  132. package/scripts/ritual_sentinel.py +0 -609
  133. package/scripts/roadmap_render.py +0 -635
  134. package/scripts/rule_ownership_lint.py +0 -325
  135. package/scripts/scm.py +0 -591
  136. package/scripts/scope_audit_log.py +0 -387
  137. package/scripts/scope_decompose.py +0 -654
  138. package/scripts/scope_demote.py +0 -509
  139. package/scripts/scope_lifecycle.py +0 -1126
  140. package/scripts/scope_undo.py +0 -772
  141. package/scripts/session_start.py +0 -406
  142. package/scripts/setup_ghx.py +0 -339
  143. package/scripts/setup_windows.ps1 +0 -220
  144. package/scripts/slice_audit.py +0 -585
  145. package/scripts/slice_record.py +0 -530
  146. package/scripts/slice_record_existing.py +0 -692
  147. package/scripts/slug_normalize.py +0 -178
  148. package/scripts/spec_render.py +0 -477
  149. package/scripts/spec_validate.py +0 -238
  150. package/scripts/subagent_monitor.py +0 -658
  151. package/scripts/swarm_complete_cohort.py +0 -644
  152. package/scripts/swarm_launch.py +0 -1206
  153. package/scripts/swarm_readiness.py +0 -554
  154. package/scripts/swarm_verify_review_clean.py +0 -438
  155. package/scripts/swarm_worktrees.py +0 -497
  156. package/scripts/toolchain-check.py +0 -52
  157. package/scripts/triage_actions.py +0 -871
  158. package/scripts/triage_bootstrap.py +0 -1153
  159. package/scripts/triage_bulk.py +0 -630
  160. package/scripts/triage_classify.py +0 -932
  161. package/scripts/triage_help.py +0 -1685
  162. package/scripts/triage_queue.py +0 -1944
  163. package/scripts/triage_reconcile.py +0 -581
  164. package/scripts/triage_refresh.py +0 -643
  165. package/scripts/triage_scope.py +0 -999
  166. package/scripts/triage_scope_drift.py +0 -575
  167. package/scripts/triage_smoketest.py +0 -396
  168. package/scripts/triage_subscribe.py +0 -399
  169. package/scripts/triage_summary.py +0 -1011
  170. package/scripts/triage_welcome.py +0 -1178
  171. package/scripts/ts_check_lane.py +0 -86
  172. package/scripts/validate-links.py +0 -64
  173. package/scripts/validate_strategy_output.py +0 -212
  174. package/scripts/vbrief_activate.py +0 -228
  175. package/scripts/vbrief_migrate_conformance.py +0 -368
  176. package/scripts/vbrief_reconcile_graph.py +0 -306
  177. package/scripts/vbrief_reconcile_labels.py +0 -460
  178. package/scripts/vbrief_reconcile_umbrellas.py +0 -741
  179. package/scripts/vbrief_validate.py +0 -1144
  180. package/scripts/verify-stubs.py +0 -61
  181. package/scripts/verify_capacity.py +0 -160
  182. package/scripts/verify_encoding.py +0 -699
  183. package/scripts/verify_hooks_installed.py +0 -206
  184. package/scripts/verify_investigation.py +0 -360
  185. package/scripts/verify_judgment_gates.py +0 -827
  186. package/scripts/verify_no_task_runtime.py +0 -171
  187. package/scripts/verify_scm_boundary.py +0 -509
  188. package/scripts/verify_session_ritual.py +0 -389
  189. package/scripts/verify_tools.py +0 -426
  190. package/scripts/verify_vbrief_conformance.py +0 -478
@@ -1,140 +0,0 @@
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
@@ -1,421 +0,0 @@
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