@deftai/directive-content 0.55.1 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-commit +143 -0
- package/.githooks/pre-push +121 -0
- package/QUICK-START.md +13 -3
- package/Taskfile.yml +934 -0
- package/UPGRADING.md +82 -11
- package/events/README.md +3 -3
- package/package.json +5 -4
- package/packs/skills/skills-pack-0.1.json +22 -22
- package/scripts/_agents_md.py +494 -0
- package/scripts/_cache_fetch.py +635 -0
- package/scripts/_cache_quota.py +529 -0
- package/scripts/_cache_refresh.py +163 -0
- package/scripts/_cache_validate.py +209 -0
- package/scripts/_content_root.py +42 -0
- package/scripts/_doctor_state.py +277 -0
- package/scripts/_event_detect.py +305 -0
- package/scripts/_events.py +514 -0
- package/scripts/_lifecycle_hygiene.py +568 -0
- package/scripts/_pathspec.py +91 -0
- package/scripts/_policy_show_cli.py +266 -0
- package/scripts/_precutover.py +92 -0
- package/scripts/_project_context.py +224 -0
- package/scripts/_project_definition_io.py +164 -0
- package/scripts/_relocate_snapshot.py +209 -0
- package/scripts/_relocate_states.py +343 -0
- package/scripts/_resolve_preflight_path.py +152 -0
- package/scripts/_safe_subprocess.py +167 -0
- package/scripts/_session_start_hook.py +205 -0
- package/scripts/_sor_gate_diff.py +365 -0
- package/scripts/_stdio_utf8.py +59 -0
- package/scripts/_triage_bootstrap_gitignore.py +904 -0
- package/scripts/_triage_classify_cli.py +122 -0
- package/scripts/_triage_queue_cli.py +625 -0
- package/scripts/_triage_scope_cli.py +343 -0
- package/scripts/_triage_scope_drift_cli.py +121 -0
- package/scripts/_triage_scope_ignores.py +286 -0
- package/scripts/_triage_scope_milestone.py +432 -0
- package/scripts/_triage_scope_mutations.py +337 -0
- package/scripts/_triage_scope_renderers.py +207 -0
- package/scripts/_triage_smoketest_stages.py +674 -0
- package/scripts/_triage_subscribe_cli.py +140 -0
- package/scripts/_triage_welcome_cli.py +421 -0
- package/scripts/_vbrief_build.py +239 -0
- package/scripts/_vbrief_fidelity.py +479 -0
- package/scripts/_vbrief_legacy.py +589 -0
- package/scripts/_vbrief_reconciliation.py +883 -0
- package/scripts/_vbrief_routing.py +277 -0
- package/scripts/_vbrief_safety.py +778 -0
- package/scripts/_vbrief_sources.py +312 -0
- package/scripts/_vbrief_speckit.py +262 -0
- package/scripts/_vbrief_story_quality.py +353 -0
- package/scripts/_vbrief_validation.py +299 -0
- package/scripts/build_dist.py +412 -0
- package/scripts/cache.py +1078 -0
- package/scripts/cache_scanner.py +745 -0
- package/scripts/candidates_log.py +432 -0
- package/scripts/capacity_backfill.py +680 -0
- package/scripts/capacity_show.py +653 -0
- package/scripts/ci_local.py +689 -0
- package/scripts/code_structure_validate.py +765 -0
- package/scripts/codebase_default_extractor.py +495 -0
- package/scripts/codebase_map.py +304 -0
- package/scripts/codebase_map_fresh.py +104 -0
- package/scripts/codebase_projection_registry.py +94 -0
- package/scripts/codebase_provider.py +582 -0
- package/scripts/doctor.py +2257 -0
- package/scripts/framework_commands.py +505 -0
- package/scripts/gh_rest.py +882 -0
- package/scripts/github_auth_modes.py +437 -0
- package/scripts/github_body.py +292 -0
- package/scripts/ip_risk.py +531 -0
- package/scripts/issue_emit.py +670 -0
- package/scripts/issue_ingest.py +1064 -0
- package/scripts/migrate_preflight.py +418 -0
- package/scripts/migrate_vbrief.py +2677 -0
- package/scripts/monitor_pr.py +401 -0
- package/scripts/pack_migrate_lessons.py +336 -0
- package/scripts/pack_migrate_patterns.py +254 -0
- package/scripts/pack_migrate_rules.py +350 -0
- package/scripts/pack_migrate_skills.py +423 -0
- package/scripts/pack_migrate_strategies.py +311 -0
- package/scripts/pack_migrate_swarm_spec.py +250 -0
- package/scripts/pack_render.py +434 -0
- package/scripts/packs_slice.py +712 -0
- package/scripts/platform_capabilities.py +336 -0
- package/scripts/policy.py +2826 -0
- package/scripts/policy_set.py +324 -0
- package/scripts/pr_check_closing_keywords.py +524 -0
- package/scripts/pr_check_protected_issues.py +267 -0
- package/scripts/pr_merge_readiness.py +1004 -0
- package/scripts/pr_wait_mergeable.py +669 -0
- package/scripts/prd_render.py +159 -0
- package/scripts/preflight_architecture_sor.py +974 -0
- package/scripts/preflight_branch.py +289 -0
- package/scripts/preflight_cache.py +974 -0
- package/scripts/preflight_gh.py +721 -0
- package/scripts/preflight_implementation.py +272 -0
- package/scripts/preflight_story_start.py +838 -0
- package/scripts/preflight_wip_cap.py +149 -0
- package/scripts/probe_session.py +545 -0
- package/scripts/project_render.py +293 -0
- package/scripts/quarantine_ext.py +237 -0
- package/scripts/reconcile_issues.py +1442 -0
- package/scripts/refresh-path.ps1 +107 -0
- package/scripts/release.py +2030 -0
- package/scripts/release_e2e.py +1011 -0
- package/scripts/release_publish.py +486 -0
- package/scripts/release_rollback.py +980 -0
- package/scripts/relocate.py +1034 -0
- package/scripts/resolve_changelog_unreleased.py +667 -0
- package/scripts/resolve_version.py +490 -0
- package/scripts/resume_conditions.py +706 -0
- package/scripts/ritual_sentinel.py +609 -0
- package/scripts/roadmap_render.py +635 -0
- package/scripts/rule_ownership_lint.py +325 -0
- package/scripts/scm.py +591 -0
- package/scripts/scope_audit_log.py +387 -0
- package/scripts/scope_decompose.py +654 -0
- package/scripts/scope_demote.py +509 -0
- package/scripts/scope_lifecycle.py +1126 -0
- package/scripts/scope_undo.py +772 -0
- package/scripts/session_start.py +406 -0
- package/scripts/setup_ghx.py +339 -0
- package/scripts/setup_windows.ps1 +220 -0
- package/scripts/slice_audit.py +585 -0
- package/scripts/slice_record.py +530 -0
- package/scripts/slice_record_existing.py +692 -0
- package/scripts/slug_normalize.py +178 -0
- package/scripts/spec_render.py +477 -0
- package/scripts/spec_validate.py +238 -0
- package/scripts/subagent_monitor.py +658 -0
- package/scripts/swarm_complete_cohort.py +644 -0
- package/scripts/swarm_launch.py +1206 -0
- package/scripts/swarm_readiness.py +554 -0
- package/scripts/swarm_verify_review_clean.py +438 -0
- package/scripts/swarm_worktrees.py +497 -0
- package/scripts/toolchain-check.py +52 -0
- package/scripts/triage_actions.py +871 -0
- package/scripts/triage_bootstrap.py +1153 -0
- package/scripts/triage_bulk.py +630 -0
- package/scripts/triage_classify.py +932 -0
- package/scripts/triage_help.py +1685 -0
- package/scripts/triage_queue.py +1944 -0
- package/scripts/triage_reconcile.py +581 -0
- package/scripts/triage_refresh.py +643 -0
- package/scripts/triage_scope.py +999 -0
- package/scripts/triage_scope_drift.py +575 -0
- package/scripts/triage_smoketest.py +396 -0
- package/scripts/triage_subscribe.py +399 -0
- package/scripts/triage_summary.py +1011 -0
- package/scripts/triage_welcome.py +1178 -0
- package/scripts/ts_check_lane.py +86 -0
- package/scripts/validate-links.py +64 -0
- package/scripts/validate_strategy_output.py +212 -0
- package/scripts/vbrief_activate.py +228 -0
- package/scripts/vbrief_migrate_conformance.py +368 -0
- package/scripts/vbrief_reconcile_graph.py +306 -0
- package/scripts/vbrief_reconcile_labels.py +460 -0
- package/scripts/vbrief_reconcile_umbrellas.py +741 -0
- package/scripts/vbrief_validate.py +1195 -0
- package/scripts/verify-stubs.py +61 -0
- package/scripts/verify_capacity.py +160 -0
- package/scripts/verify_encoding.py +699 -0
- package/scripts/verify_hooks_installed.py +206 -0
- package/scripts/verify_investigation.py +360 -0
- package/scripts/verify_judgment_gates.py +827 -0
- package/scripts/verify_no_task_runtime.py +171 -0
- package/scripts/verify_scm_boundary.py +509 -0
- package/scripts/verify_session_ritual.py +389 -0
- package/scripts/verify_tools.py +426 -0
- package/scripts/verify_vbrief_conformance.py +478 -0
- package/skills/deft-directive-swarm/SKILL.md +7 -26
- package/skills/deft-directive-sync/SKILL.md +1 -1
- package/tasks/architecture.yml +13 -0
- package/tasks/cache.yml +69 -0
- package/tasks/capacity.yml +38 -0
- package/tasks/change.yml +46 -0
- package/tasks/changelog.yml +24 -0
- package/tasks/ci.yml +49 -0
- package/tasks/codebase.yml +47 -0
- package/tasks/commit.yml +30 -0
- package/tasks/core.yml +126 -0
- package/tasks/deployments.yml +54 -0
- package/tasks/framework.yml +74 -0
- package/tasks/install.yml +60 -0
- package/tasks/issue.yml +50 -0
- package/tasks/migrate.yml +73 -0
- package/tasks/packs.yml +92 -0
- package/tasks/policy.yml +75 -0
- package/tasks/pr.yml +89 -0
- package/tasks/prd.yml +39 -0
- package/tasks/project.yml +27 -0
- package/tasks/reconcile.yml +32 -0
- package/tasks/relocate.yml +56 -0
- package/tasks/roadmap.yml +28 -0
- package/tasks/scm.yml +126 -0
- package/tasks/scope-undo.yml +36 -0
- package/tasks/scope.yml +141 -0
- package/tasks/session.yml +19 -0
- package/tasks/setup.yml +37 -0
- package/tasks/slice.yml +69 -0
- package/tasks/spec.yml +41 -0
- package/tasks/swarm.yml +85 -0
- package/tasks/toolchain.yml +13 -0
- package/tasks/triage-actions.yml +94 -0
- package/tasks/triage-bootstrap.yml +43 -0
- package/tasks/triage-bulk.yml +75 -0
- package/tasks/triage-classify.yml +30 -0
- package/tasks/triage-queue.yml +50 -0
- package/tasks/triage-reconcile.yml +29 -0
- package/tasks/triage-scope-drift.yml +29 -0
- package/tasks/triage-scope.yml +31 -0
- package/tasks/triage-smoketest.yml +33 -0
- package/tasks/triage-subscribe.yml +36 -0
- package/tasks/triage-summary.yml +29 -0
- package/tasks/triage-welcome.yml +32 -0
- package/tasks/ts.yml +328 -0
- package/tasks/vbrief.yml +206 -0
- package/tasks/verify.yml +292 -0
- package/templates/agents-entry.md +2 -2
|
@@ -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
|