@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,266 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""``task policy:show`` argparse shim (#1148 / N8 of #1119 Wave-2d-1).
|
|
3
|
+
|
|
4
|
+
Extracted from :mod:`scripts.policy` so the parent module stays well
|
|
5
|
+
under the 1000-line MUST cap from ``coding/coding.md``. The CLI delegates
|
|
6
|
+
to :func:`policy.inspect_all_policies` / :func:`policy.inspect_one_policy`
|
|
7
|
+
for every read; the render layer here is purely cosmetic.
|
|
8
|
+
|
|
9
|
+
Flags (mirror the #1148 issue body):
|
|
10
|
+
|
|
11
|
+
* ``--format=text|json`` -- ``text`` is the default human form (one block
|
|
12
|
+
per field); ``json`` emits the stable schema
|
|
13
|
+
``{generated_at, fields: [{name, current, default, source}, ...]}``.
|
|
14
|
+
* ``--changed-only`` -- drop rows whose source is ``default`` so the
|
|
15
|
+
output focuses on what the operator actually configured. Combines
|
|
16
|
+
cleanly with ``--format=json`` for ``jq`` consumption.
|
|
17
|
+
* ``--field=<canonical-dotted-path>`` -- show exactly one registered
|
|
18
|
+
field; exit 2 with the recognised-names list when ``<name>`` is not
|
|
19
|
+
a registered field. Mutually compatible with ``--format=json``.
|
|
20
|
+
* ``--project-root <path>`` -- override the project root (defaults to
|
|
21
|
+
``Path.cwd()``); useful for tests + tools dispatching from outside
|
|
22
|
+
the consumer working directory.
|
|
23
|
+
|
|
24
|
+
Exit codes:
|
|
25
|
+
|
|
26
|
+
* ``0`` -- success (including the "all defaults" + "missing
|
|
27
|
+
PROJECT-DEFINITION" cases; the verb is informational).
|
|
28
|
+
* ``2`` -- argparse usage error OR unknown ``--field=<name>``.
|
|
29
|
+
|
|
30
|
+
Pure-stdlib; runs anywhere :mod:`scripts.policy` does.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import contextlib
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
from datetime import UTC, datetime
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
# Make sibling scripts importable when invoked as
|
|
44
|
+
# ``python scripts/_policy_show_cli.py`` (the dispatch shape go-task uses).
|
|
45
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
46
|
+
|
|
47
|
+
# UTF-8 self-reconfigure -- the rendered values for ``triageScope`` etc.
|
|
48
|
+
# can include non-ASCII characters that crash cp1252 on Windows.
|
|
49
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
50
|
+
if hasattr(_stream, "reconfigure"):
|
|
51
|
+
with contextlib.suppress(AttributeError, ValueError):
|
|
52
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
53
|
+
|
|
54
|
+
import policy # noqa: E402 (sibling import after sys.path tweak)
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Public helpers (test-injectable)
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _utc_iso(dt: datetime | None = None) -> str:
|
|
62
|
+
"""ISO-8601 UTC timestamp with seconds precision and ``Z`` suffix."""
|
|
63
|
+
return (dt or datetime.now(UTC)).astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def field_to_dict(field: policy.PolicyField) -> dict[str, Any]:
|
|
67
|
+
"""Render one :class:`policy.PolicyField` as a JSON-stable dict.
|
|
68
|
+
|
|
69
|
+
Stable key order: ``name``, ``current``, ``default``, ``source``.
|
|
70
|
+
Stable across releases -- the JSON schema is the scripting contract.
|
|
71
|
+
"""
|
|
72
|
+
return {
|
|
73
|
+
"name": field.name,
|
|
74
|
+
"current": field.current,
|
|
75
|
+
"default": field.default,
|
|
76
|
+
"source": field.source,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def render_json(
|
|
81
|
+
fields: list[policy.PolicyField],
|
|
82
|
+
*,
|
|
83
|
+
now: datetime | None = None,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Render the JSON envelope ``{generated_at, fields: [...]}``.
|
|
86
|
+
|
|
87
|
+
``ensure_ascii=False`` so non-ASCII operator values (rare but
|
|
88
|
+
possible -- e.g. milestone names with em dashes) survive the
|
|
89
|
+
serialisation round-trip without ``\\uXXXX`` escaping.
|
|
90
|
+
"""
|
|
91
|
+
envelope = {
|
|
92
|
+
"generated_at": _utc_iso(now),
|
|
93
|
+
"fields": [field_to_dict(f) for f in fields],
|
|
94
|
+
}
|
|
95
|
+
return json.dumps(envelope, ensure_ascii=False, indent=2, sort_keys=False)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _format_value(value: Any) -> str:
|
|
99
|
+
"""Render a value for the text format.
|
|
100
|
+
|
|
101
|
+
Booleans render as ``true`` / ``false`` (the issue-body example output
|
|
102
|
+
used lowercase JSON-style booleans). Lists and dicts round-trip
|
|
103
|
+
through ``json.dumps`` for a stable, copy-pasteable shape. Strings
|
|
104
|
+
render verbatim; numbers via ``repr`` so floats keep precision.
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(value, bool):
|
|
107
|
+
return "true" if value else "false"
|
|
108
|
+
if isinstance(value, (list, dict)):
|
|
109
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=False)
|
|
110
|
+
if isinstance(value, str):
|
|
111
|
+
return value
|
|
112
|
+
return repr(value)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def render_text(fields: list[policy.PolicyField]) -> str:
|
|
116
|
+
"""Render the human-readable text format from the issue body.
|
|
117
|
+
|
|
118
|
+
Each field renders as a four-line block:
|
|
119
|
+
|
|
120
|
+
.. code-block:: text
|
|
121
|
+
|
|
122
|
+
[policy] <name>
|
|
123
|
+
current: <value>
|
|
124
|
+
default: <value>
|
|
125
|
+
source: <typed|default|legacy>
|
|
126
|
+
|
|
127
|
+
Blocks are separated by a blank line. An empty ``fields`` list
|
|
128
|
+
(``--changed-only`` against an all-defaults config) renders a single
|
|
129
|
+
informational line so the operator does not see a blank screen.
|
|
130
|
+
"""
|
|
131
|
+
if not fields:
|
|
132
|
+
return (
|
|
133
|
+
"[policy] (no fields changed)\n"
|
|
134
|
+
" All registered policies are at their framework defaults. "
|
|
135
|
+
"Re-run without `--changed-only` to inspect them."
|
|
136
|
+
)
|
|
137
|
+
blocks: list[str] = []
|
|
138
|
+
for field in fields:
|
|
139
|
+
block = (
|
|
140
|
+
f"[policy] {field.name}\n"
|
|
141
|
+
f" current: {_format_value(field.current)}\n"
|
|
142
|
+
f" default: {_format_value(field.default)}\n"
|
|
143
|
+
f" source: {field.source}"
|
|
144
|
+
)
|
|
145
|
+
blocks.append(block)
|
|
146
|
+
return "\n\n".join(blocks)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# argparse setup
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
155
|
+
parser = argparse.ArgumentParser(
|
|
156
|
+
prog="task policy:show",
|
|
157
|
+
description=(
|
|
158
|
+
"Inspect every registered typed-policy field on "
|
|
159
|
+
"vbrief/PROJECT-DEFINITION.vbrief.json (#1148 / N8 of #1119)."
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--format",
|
|
164
|
+
choices=("text", "json"),
|
|
165
|
+
default="text",
|
|
166
|
+
help="Output format (default: text). Use json for stable scripting schema.",
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--changed-only",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help=(
|
|
172
|
+
"Filter out fields whose source is 'default'. "
|
|
173
|
+
"Keeps 'typed' and 'legacy' rows only."
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--field",
|
|
178
|
+
dest="field",
|
|
179
|
+
metavar="<name>",
|
|
180
|
+
default=None,
|
|
181
|
+
help=(
|
|
182
|
+
"Show exactly one registered field by canonical dotted path "
|
|
183
|
+
"(e.g. plan.policy.wipCap). Exits 2 on unknown name."
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--project-root",
|
|
188
|
+
dest="project_root",
|
|
189
|
+
metavar="<path>",
|
|
190
|
+
default=None,
|
|
191
|
+
help="Project root (default: current working directory).",
|
|
192
|
+
)
|
|
193
|
+
return parser
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def run_cli(argv: list[str] | None = None) -> int:
|
|
197
|
+
"""Argparse + dispatch for ``task policy:show``.
|
|
198
|
+
|
|
199
|
+
Exit 0 in every success path (including all-defaults and missing
|
|
200
|
+
PROJECT-DEFINITION). Exit 2 for argparse usage errors and for
|
|
201
|
+
``--field=<name>`` where ``<name>`` is not a registered field.
|
|
202
|
+
"""
|
|
203
|
+
parser = _build_parser()
|
|
204
|
+
try:
|
|
205
|
+
args = parser.parse_args(argv)
|
|
206
|
+
except SystemExit as exc:
|
|
207
|
+
# argparse already emitted its error to stderr; preserve its code.
|
|
208
|
+
return int(exc.code) if isinstance(exc.code, int) else 2
|
|
209
|
+
|
|
210
|
+
project_root = (
|
|
211
|
+
Path(args.project_root).resolve() if args.project_root else Path.cwd()
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Surface a friendly informational line on missing PROJECT-DEFINITION so
|
|
215
|
+
# the operator understands why every row will be at default. Exit 0;
|
|
216
|
+
# show is informational by contract.
|
|
217
|
+
pd_path = project_root / policy.PROJECT_DEFINITION_REL_PATH
|
|
218
|
+
if not pd_path.is_file():
|
|
219
|
+
sys.stderr.write(
|
|
220
|
+
f"[policy:show] PROJECT-DEFINITION not found at {pd_path}; "
|
|
221
|
+
"rendering framework defaults.\n"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if args.field is not None:
|
|
225
|
+
return _dispatch_single_field(args, project_root)
|
|
226
|
+
return _dispatch_all_fields(args, project_root)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _dispatch_single_field(args: argparse.Namespace, project_root: Path) -> int:
|
|
230
|
+
field = policy.inspect_one_policy(args.field, project_root)
|
|
231
|
+
if field is None:
|
|
232
|
+
known = policy.registered_policy_names()
|
|
233
|
+
sys.stderr.write(
|
|
234
|
+
f"[policy:show] unknown --field={args.field!r}; "
|
|
235
|
+
f"registered fields: {known}\n"
|
|
236
|
+
)
|
|
237
|
+
return 2
|
|
238
|
+
# ``--changed-only`` against a single-field default is a no-op render --
|
|
239
|
+
# operators asking for a single field by name almost always want to see
|
|
240
|
+
# it regardless of source. The default branch keeps the row; the
|
|
241
|
+
# ``--changed-only`` filter only fires across the all-fields surface.
|
|
242
|
+
if args.format == "json":
|
|
243
|
+
sys.stdout.write(render_json([field]) + "\n")
|
|
244
|
+
else:
|
|
245
|
+
sys.stdout.write(render_text([field]) + "\n")
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _dispatch_all_fields(args: argparse.Namespace, project_root: Path) -> int:
|
|
250
|
+
fields = policy.inspect_all_policies(project_root)
|
|
251
|
+
if args.changed_only:
|
|
252
|
+
fields = [f for f in fields if f.source != "default"]
|
|
253
|
+
if args.format == "json":
|
|
254
|
+
sys.stdout.write(render_json(fields) + "\n")
|
|
255
|
+
else:
|
|
256
|
+
sys.stdout.write(render_text(fields) + "\n")
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def main(argv: list[str] | None = None) -> int:
|
|
261
|
+
"""CLI entry point. Alias for :func:`run_cli` so tests can patch it."""
|
|
262
|
+
return run_cli(argv)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
sys.exit(main())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shared pre-v0.20 document-model detection helpers.
|
|
2
|
+
|
|
3
|
+
The session-start guard, CLI gate, validator, and migration preflight all need
|
|
4
|
+
the same distinction:
|
|
5
|
+
|
|
6
|
+
* deprecation redirect stubs are migrated/current enough;
|
|
7
|
+
* generated ``SPECIFICATION.md`` exports from ``task spec:render`` are not
|
|
8
|
+
hand-authored legacy docs when their source JSON exists, and are fully
|
|
9
|
+
current vBRIEF artifacts when their lifecycle folders also exist;
|
|
10
|
+
* hand-authored root docs are legacy pre-cutover inputs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
LIFECYCLE_FOLDERS: tuple[str, ...] = (
|
|
18
|
+
"proposed",
|
|
19
|
+
"pending",
|
|
20
|
+
"active",
|
|
21
|
+
"completed",
|
|
22
|
+
"cancelled",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
DEPRECATED_REDIRECT_SENTINEL = "<!-- deft:deprecated-redirect -->"
|
|
26
|
+
DEPRECATION_REDIRECT_PURPOSE = "<!-- Purpose: deprecation redirect -->"
|
|
27
|
+
|
|
28
|
+
GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->"
|
|
29
|
+
GENERATED_SPEC_SOURCE = "<!-- Source of truth: vbrief/specification.vbrief.json -->"
|
|
30
|
+
SPEC_SOURCE_RELPATH = Path("vbrief") / "specification.vbrief.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def missing_lifecycle_folders(project_root: Path) -> list[str]:
|
|
34
|
+
"""Return missing vBRIEF lifecycle folder names for ``project_root``."""
|
|
35
|
+
vbrief_root = project_root / "vbrief"
|
|
36
|
+
return [folder for folder in LIFECYCLE_FOLDERS if not (vbrief_root / folder).is_dir()]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_complete_lifecycle(project_root: Path) -> bool:
|
|
40
|
+
"""Return True when every canonical lifecycle folder exists."""
|
|
41
|
+
return not missing_lifecycle_folders(project_root)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_deprecation_redirect(content: str) -> bool:
|
|
45
|
+
"""Return True when markdown content is a migration redirect stub."""
|
|
46
|
+
return DEPRECATED_REDIRECT_SENTINEL in content or DEPRECATION_REDIRECT_PURPOSE in content
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_generated_specification_export(project_root: Path, content: str) -> bool:
|
|
50
|
+
"""Return True for a generated ``task spec:render`` root export.
|
|
51
|
+
|
|
52
|
+
The banner alone is not enough: the declared vBRIEF source must also
|
|
53
|
+
exist. Lifecycle completeness is checked separately by
|
|
54
|
+
``is_current_generated_specification``.
|
|
55
|
+
"""
|
|
56
|
+
return (
|
|
57
|
+
GENERATED_SPEC_PURPOSE in content
|
|
58
|
+
and GENERATED_SPEC_SOURCE in content
|
|
59
|
+
and (project_root / SPEC_SOURCE_RELPATH).is_file()
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_current_generated_specification(project_root: Path, content: str) -> bool:
|
|
64
|
+
"""Return True for a fully current ``task spec:render`` root export."""
|
|
65
|
+
return is_generated_specification_export(project_root, content) and has_complete_lifecycle(
|
|
66
|
+
project_root
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def root_markdown_is_legacy(project_root: Path, filename: str, content: str) -> bool:
|
|
71
|
+
"""Return True if a root markdown artifact should trigger migration."""
|
|
72
|
+
if is_deprecation_redirect(content):
|
|
73
|
+
return False
|
|
74
|
+
if filename == "SPECIFICATION.md" and is_generated_specification_export(project_root, content):
|
|
75
|
+
return False
|
|
76
|
+
return filename in {"SPECIFICATION.md", "PROJECT.md"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def detect_pre_cutover_legacy(project_root: Path) -> list[str]:
|
|
80
|
+
"""Return root artifact filenames that are legacy pre-v0.20 inputs."""
|
|
81
|
+
legacy: list[str] = []
|
|
82
|
+
for filename in ("SPECIFICATION.md", "PROJECT.md"):
|
|
83
|
+
candidate = project_root / filename
|
|
84
|
+
if not candidate.is_file():
|
|
85
|
+
continue
|
|
86
|
+
try:
|
|
87
|
+
content = candidate.read_text(encoding="utf-8", errors="replace")
|
|
88
|
+
except OSError:
|
|
89
|
+
continue
|
|
90
|
+
if root_markdown_is_legacy(project_root, filename, content):
|
|
91
|
+
legacy.append(filename)
|
|
92
|
+
return legacy
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""_project_context.py -- resolve consumer project root + GitHub repo slug.
|
|
2
|
+
|
|
3
|
+
Shared helpers used by ``scope_lifecycle``, ``issue_ingest``,
|
|
4
|
+
``reconcile_issues`` and ``prd_render`` so every script follows the same
|
|
5
|
+
precedence rules and fails loudly when no project context can be inferred.
|
|
6
|
+
|
|
7
|
+
Precedence for ``resolve_project_root``:
|
|
8
|
+
|
|
9
|
+
1. ``--project-root`` flag (explicit, highest precedence).
|
|
10
|
+
2. ``$DEFT_PROJECT_ROOT`` environment variable.
|
|
11
|
+
3. Walk upward from CWD looking for a ``vbrief/`` directory or a ``.git``
|
|
12
|
+
directory -- the first match is the project root.
|
|
13
|
+
4. Fall back to the current working directory ONLY if it visibly looks
|
|
14
|
+
like a project root (contains either ``vbrief/`` or ``.git``).
|
|
15
|
+
|
|
16
|
+
If none of those match, the caller gets ``None`` and is expected to emit
|
|
17
|
+
a loud, actionable error -- silently falling back to ``deft/`` is exactly
|
|
18
|
+
the bug that shipped #535 / #538.
|
|
19
|
+
|
|
20
|
+
Precedence for ``resolve_project_repo``:
|
|
21
|
+
|
|
22
|
+
1. ``--repo OWNER/NAME`` flag (explicit, highest precedence).
|
|
23
|
+
2. ``$DEFT_PROJECT_REPO`` environment variable.
|
|
24
|
+
3. ``git remote get-url origin`` run from the resolved project root --
|
|
25
|
+
this is the key anti-regression for #538: deft's own ``.git`` remote
|
|
26
|
+
(``deftai/directive``) is used only if the project root happens to be
|
|
27
|
+
deft itself.
|
|
28
|
+
4. ``None`` -- caller must emit a loud error.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import subprocess
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from framework_commands import CommandResult, run_framework_command
|
|
41
|
+
|
|
42
|
+
# Sentinel directories that mark a deft project root.
|
|
43
|
+
_PROJECT_ROOT_SENTINELS = ("vbrief", ".git")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_project_root(candidate: Path) -> bool:
|
|
47
|
+
"""Return True if *candidate* contains any deft project-root sentinel."""
|
|
48
|
+
return any((candidate / sentinel).exists() for sentinel in _PROJECT_ROOT_SENTINELS)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_project_root(
|
|
52
|
+
cli_project_root: str | None = None,
|
|
53
|
+
*,
|
|
54
|
+
start: Path | None = None,
|
|
55
|
+
) -> Path | None:
|
|
56
|
+
"""Resolve the consumer project root using the documented precedence.
|
|
57
|
+
|
|
58
|
+
Returns ``None`` when no candidate matches; callers MUST fail loudly
|
|
59
|
+
in that case (never silently fall back to deft's own directory).
|
|
60
|
+
"""
|
|
61
|
+
if cli_project_root:
|
|
62
|
+
candidate = Path(cli_project_root).resolve()
|
|
63
|
+
if candidate.is_dir():
|
|
64
|
+
return candidate
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
env_root = os.environ.get("DEFT_PROJECT_ROOT")
|
|
68
|
+
if env_root:
|
|
69
|
+
candidate = Path(env_root).resolve()
|
|
70
|
+
if candidate.is_dir():
|
|
71
|
+
return candidate
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
cwd = (start or Path.cwd()).resolve()
|
|
75
|
+
# Walk upward from CWD looking for a sentinel.
|
|
76
|
+
for candidate in (cwd, *cwd.parents):
|
|
77
|
+
if _is_project_root(candidate):
|
|
78
|
+
return candidate
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resolve_project_repo(
|
|
83
|
+
cli_repo: str | None = None,
|
|
84
|
+
*,
|
|
85
|
+
project_root: Path | None = None,
|
|
86
|
+
) -> str | None:
|
|
87
|
+
"""Resolve the consumer GitHub repo (``OWNER/NAME``).
|
|
88
|
+
|
|
89
|
+
Returns ``None`` when detection fails so the caller can emit an
|
|
90
|
+
actionable error. ``project_root`` narrows ``git remote`` detection to
|
|
91
|
+
the consumer repo; without it we fall back to CWD, which may be wrong
|
|
92
|
+
under a ``task deft:*`` include (#538).
|
|
93
|
+
"""
|
|
94
|
+
if cli_repo:
|
|
95
|
+
slug = _normalise_repo_slug(cli_repo)
|
|
96
|
+
if slug:
|
|
97
|
+
return slug
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
env_repo = os.environ.get("DEFT_PROJECT_REPO")
|
|
101
|
+
if env_repo:
|
|
102
|
+
slug = _normalise_repo_slug(env_repo)
|
|
103
|
+
if slug:
|
|
104
|
+
return slug
|
|
105
|
+
# Greptile P2 on #562: fail loudly when the env var is set but
|
|
106
|
+
# unparseable, to match the explicit-flag path (which returns
|
|
107
|
+
# None on a malformed value rather than falling through to git
|
|
108
|
+
# auto-detection). Silent fallback to git is exactly the
|
|
109
|
+
# anti-pattern this helper was introduced to prevent.
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
return _detect_repo_from_git(project_root)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _normalise_repo_slug(value: str) -> str | None:
|
|
116
|
+
r"""Accept ``OWNER/NAME`` or a full GitHub URL, return ``OWNER/NAME``.
|
|
117
|
+
|
|
118
|
+
Allows dots in the name component (``acme/dotnet.runtime``,
|
|
119
|
+
``acme/my.project.git``) -- the previous ``[^/\.\s]+`` pattern stopped
|
|
120
|
+
at the first dot and silently truncated the repo name, routing ``gh``
|
|
121
|
+
calls to the wrong (or non-existent) repository (Greptile P1 on #562).
|
|
122
|
+
Strips a trailing ``.git`` suffix explicitly so SSH clone URLs still
|
|
123
|
+
normalise to the bare ``OWNER/NAME`` form.
|
|
124
|
+
"""
|
|
125
|
+
value = value.strip()
|
|
126
|
+
if not value:
|
|
127
|
+
return None
|
|
128
|
+
match = re.search(
|
|
129
|
+
r"github\.com[:/]([^/\s]+)/([^/\s]+?)(?:\.git)?(?:\s|$)",
|
|
130
|
+
value,
|
|
131
|
+
)
|
|
132
|
+
if match:
|
|
133
|
+
return f"{match.group(1)}/{match.group(2)}"
|
|
134
|
+
if re.match(r"^[^/\s]+/[^/\s]+$", value):
|
|
135
|
+
return value
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _detect_repo_from_git(project_root: Path | None) -> str | None:
|
|
140
|
+
"""Run ``git remote get-url origin`` in *project_root* (or CWD)."""
|
|
141
|
+
cwd = str(project_root) if project_root else None
|
|
142
|
+
try:
|
|
143
|
+
result = subprocess.run(
|
|
144
|
+
["git", "remote", "get-url", "origin"],
|
|
145
|
+
capture_output=True,
|
|
146
|
+
text=True,
|
|
147
|
+
timeout=10,
|
|
148
|
+
cwd=cwd,
|
|
149
|
+
)
|
|
150
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
151
|
+
return None
|
|
152
|
+
if result.returncode != 0:
|
|
153
|
+
return None
|
|
154
|
+
return _normalise_repo_slug(result.stdout.strip())
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def is_framework_source_context(framework_root: Path, project_root: Path) -> bool:
|
|
158
|
+
"""Return True when a task is running from the framework checkout itself.
|
|
159
|
+
|
|
160
|
+
Vendored consumer installs execute framework tasks from ``.deft/core`` while
|
|
161
|
+
the user working directory remains the consumer repo. Equality of the two
|
|
162
|
+
lexical absolute roots is the stable distinction: only the source checkout
|
|
163
|
+
should run source-repo self-tests by default. Do not resolve symlinks here:
|
|
164
|
+
a consumer project may symlink ``.deft/core`` to a local framework checkout
|
|
165
|
+
and should still run the consumer-safe gate.
|
|
166
|
+
"""
|
|
167
|
+
return Path(os.path.abspath(framework_root)) == Path(os.path.abspath(project_root))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def dispatch_task_check(
|
|
171
|
+
framework_root: Path,
|
|
172
|
+
project_root: Path,
|
|
173
|
+
*,
|
|
174
|
+
runner: Callable[
|
|
175
|
+
[str, Path, Path],
|
|
176
|
+
CommandResult | subprocess.CompletedProcess[str],
|
|
177
|
+
]
|
|
178
|
+
| None = None,
|
|
179
|
+
) -> int:
|
|
180
|
+
"""Dispatch ``check`` to the context-appropriate aggregate target."""
|
|
181
|
+
target = (
|
|
182
|
+
"check:framework-source"
|
|
183
|
+
if is_framework_source_context(framework_root, project_root)
|
|
184
|
+
else "check:consumer"
|
|
185
|
+
)
|
|
186
|
+
command_runner = runner or (
|
|
187
|
+
lambda command, root, framework: run_framework_command(
|
|
188
|
+
command,
|
|
189
|
+
project_root=root,
|
|
190
|
+
framework_root=framework,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
result = command_runner(target, project_root, framework_root)
|
|
194
|
+
code = getattr(result, "code", None)
|
|
195
|
+
if code is None:
|
|
196
|
+
code = result.returncode
|
|
197
|
+
return int(code)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
201
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
202
|
+
parser.add_argument(
|
|
203
|
+
"--dispatch-task-check",
|
|
204
|
+
action="store_true",
|
|
205
|
+
help="Dispatch the check aggregate for the current install context.",
|
|
206
|
+
)
|
|
207
|
+
parser.add_argument("--framework-root", type=Path)
|
|
208
|
+
parser.add_argument("--project-root", type=Path)
|
|
209
|
+
return parser
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main(argv: list[str] | None = None) -> int:
|
|
213
|
+
parser = _build_parser()
|
|
214
|
+
args = parser.parse_args(argv)
|
|
215
|
+
if args.dispatch_task_check:
|
|
216
|
+
if args.framework_root is None or args.project_root is None:
|
|
217
|
+
raise SystemExit("--framework-root and --project-root are required")
|
|
218
|
+
return dispatch_task_check(args.framework_root, args.project_root)
|
|
219
|
+
parser.print_help()
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
raise SystemExit(main())
|