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