@deftai/directive-content 0.59.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-commit +10 -128
- package/.githooks/pre-push +8 -108
- package/Taskfile.yml +48 -58
- package/UPGRADING.md +19 -3
- package/docs/assets/directive-lifecycle-diagram.png +0 -0
- package/docs/directive-lifecycle.md +73 -0
- package/docs/getting-started.md +5 -1
- package/package.json +3 -3
- package/packs/skills/skills-pack-0.1.json +1 -1
- package/packs/strategies/strategies-pack-0.1.json +19 -19
- package/scm/github.md +37 -6
- package/skills/deft-directive-setup/SKILL.md +24 -15
- package/strategies/speckit.md +14 -14
- package/strategies/v0-20-contract.md +12 -1
- package/tasks/change.yml +16 -31
- package/tasks/ci.yml +8 -0
- package/tasks/commit.yml +12 -19
- package/tasks/core.yml +10 -0
- package/tasks/engine.yml +42 -0
- package/tasks/framework.yml +3 -0
- package/tasks/install.yml +20 -19
- package/tasks/migrate.yml +26 -15
- package/tasks/project.yml +26 -0
- package/tasks/toolchain.yml +15 -5
- package/tasks/vbrief.yml +4 -3
- package/tasks/verify.yml +12 -14
- package/templates/agents-entry.md +1 -1
- package/scripts/_agents_md.py +0 -494
- package/scripts/_cache_fetch.py +0 -635
- package/scripts/_cache_quota.py +0 -529
- package/scripts/_cache_refresh.py +0 -163
- package/scripts/_cache_validate.py +0 -209
- package/scripts/_content_root.py +0 -42
- package/scripts/_doctor_state.py +0 -277
- package/scripts/_event_detect.py +0 -305
- package/scripts/_events.py +0 -514
- package/scripts/_lifecycle_hygiene.py +0 -568
- package/scripts/_pathspec.py +0 -91
- package/scripts/_policy_show_cli.py +0 -266
- package/scripts/_precutover.py +0 -92
- package/scripts/_project_context.py +0 -224
- package/scripts/_project_definition_io.py +0 -164
- package/scripts/_relocate_snapshot.py +0 -209
- package/scripts/_relocate_states.py +0 -343
- package/scripts/_resolve_preflight_path.py +0 -152
- package/scripts/_safe_subprocess.py +0 -167
- package/scripts/_session_start_hook.py +0 -205
- package/scripts/_sor_gate_diff.py +0 -365
- package/scripts/_stdio_utf8.py +0 -59
- package/scripts/_triage_bootstrap_gitignore.py +0 -904
- package/scripts/_triage_classify_cli.py +0 -122
- package/scripts/_triage_queue_cli.py +0 -625
- package/scripts/_triage_scope_cli.py +0 -343
- package/scripts/_triage_scope_drift_cli.py +0 -121
- package/scripts/_triage_scope_ignores.py +0 -286
- package/scripts/_triage_scope_milestone.py +0 -432
- package/scripts/_triage_scope_mutations.py +0 -337
- package/scripts/_triage_scope_renderers.py +0 -207
- package/scripts/_triage_smoketest_stages.py +0 -674
- package/scripts/_triage_subscribe_cli.py +0 -140
- package/scripts/_triage_welcome_cli.py +0 -421
- package/scripts/_vbrief_build.py +0 -239
- package/scripts/_vbrief_fidelity.py +0 -479
- package/scripts/_vbrief_legacy.py +0 -589
- package/scripts/_vbrief_reconciliation.py +0 -883
- package/scripts/_vbrief_routing.py +0 -277
- package/scripts/_vbrief_safety.py +0 -778
- package/scripts/_vbrief_sources.py +0 -312
- package/scripts/_vbrief_speckit.py +0 -262
- package/scripts/_vbrief_story_quality.py +0 -353
- package/scripts/_vbrief_validation.py +0 -299
- package/scripts/build_dist.py +0 -412
- package/scripts/cache.py +0 -1078
- package/scripts/cache_scanner.py +0 -745
- package/scripts/candidates_log.py +0 -432
- package/scripts/capacity_backfill.py +0 -680
- package/scripts/capacity_show.py +0 -653
- package/scripts/ci_local.py +0 -689
- package/scripts/code_structure_validate.py +0 -765
- package/scripts/codebase_default_extractor.py +0 -495
- package/scripts/codebase_map.py +0 -304
- package/scripts/codebase_map_fresh.py +0 -104
- package/scripts/codebase_projection_registry.py +0 -94
- package/scripts/codebase_provider.py +0 -582
- package/scripts/doctor.py +0 -2552
- package/scripts/framework_commands.py +0 -505
- package/scripts/gh_rest.py +0 -882
- package/scripts/github_auth_modes.py +0 -437
- package/scripts/github_body.py +0 -292
- package/scripts/ip_risk.py +0 -531
- package/scripts/issue_emit.py +0 -670
- package/scripts/issue_ingest.py +0 -1064
- package/scripts/migrate_preflight.py +0 -418
- package/scripts/migrate_vbrief.py +0 -2677
- package/scripts/monitor_pr.py +0 -401
- package/scripts/pack_migrate_lessons.py +0 -336
- package/scripts/pack_migrate_patterns.py +0 -254
- package/scripts/pack_migrate_rules.py +0 -350
- package/scripts/pack_migrate_skills.py +0 -423
- package/scripts/pack_migrate_strategies.py +0 -311
- package/scripts/pack_migrate_swarm_spec.py +0 -250
- package/scripts/pack_render.py +0 -434
- package/scripts/packs_slice.py +0 -712
- package/scripts/platform_capabilities.py +0 -336
- package/scripts/policy.py +0 -2826
- package/scripts/policy_set.py +0 -324
- package/scripts/pr_check_closing_keywords.py +0 -524
- package/scripts/pr_check_protected_issues.py +0 -267
- package/scripts/pr_merge_readiness.py +0 -1004
- package/scripts/pr_wait_mergeable.py +0 -669
- package/scripts/prd_render.py +0 -159
- package/scripts/preflight_architecture_sor.py +0 -974
- package/scripts/preflight_branch.py +0 -289
- package/scripts/preflight_cache.py +0 -974
- package/scripts/preflight_gh.py +0 -721
- package/scripts/preflight_implementation.py +0 -272
- package/scripts/preflight_story_start.py +0 -838
- package/scripts/preflight_wip_cap.py +0 -149
- package/scripts/probe_session.py +0 -545
- package/scripts/project_render.py +0 -293
- package/scripts/quarantine_ext.py +0 -237
- package/scripts/reconcile_issues.py +0 -1442
- package/scripts/refresh-path.ps1 +0 -107
- package/scripts/release.py +0 -2030
- package/scripts/release_e2e.py +0 -1011
- package/scripts/release_publish.py +0 -486
- package/scripts/release_rollback.py +0 -980
- package/scripts/relocate.py +0 -1034
- package/scripts/resolve_changelog_unreleased.py +0 -667
- package/scripts/resolve_version.py +0 -490
- package/scripts/resume_conditions.py +0 -706
- package/scripts/ritual_sentinel.py +0 -609
- package/scripts/roadmap_render.py +0 -635
- package/scripts/rule_ownership_lint.py +0 -325
- package/scripts/scm.py +0 -591
- package/scripts/scope_audit_log.py +0 -387
- package/scripts/scope_decompose.py +0 -654
- package/scripts/scope_demote.py +0 -509
- package/scripts/scope_lifecycle.py +0 -1126
- package/scripts/scope_undo.py +0 -772
- package/scripts/session_start.py +0 -406
- package/scripts/setup_ghx.py +0 -339
- package/scripts/setup_windows.ps1 +0 -220
- package/scripts/slice_audit.py +0 -585
- package/scripts/slice_record.py +0 -530
- package/scripts/slice_record_existing.py +0 -692
- package/scripts/slug_normalize.py +0 -178
- package/scripts/spec_render.py +0 -477
- package/scripts/spec_validate.py +0 -238
- package/scripts/subagent_monitor.py +0 -658
- package/scripts/swarm_complete_cohort.py +0 -644
- package/scripts/swarm_launch.py +0 -1206
- package/scripts/swarm_readiness.py +0 -554
- package/scripts/swarm_verify_review_clean.py +0 -438
- package/scripts/swarm_worktrees.py +0 -497
- package/scripts/toolchain-check.py +0 -52
- package/scripts/triage_actions.py +0 -871
- package/scripts/triage_bootstrap.py +0 -1153
- package/scripts/triage_bulk.py +0 -630
- package/scripts/triage_classify.py +0 -932
- package/scripts/triage_help.py +0 -1685
- package/scripts/triage_queue.py +0 -1944
- package/scripts/triage_reconcile.py +0 -581
- package/scripts/triage_refresh.py +0 -643
- package/scripts/triage_scope.py +0 -999
- package/scripts/triage_scope_drift.py +0 -575
- package/scripts/triage_smoketest.py +0 -396
- package/scripts/triage_subscribe.py +0 -399
- package/scripts/triage_summary.py +0 -1011
- package/scripts/triage_welcome.py +0 -1178
- package/scripts/ts_check_lane.py +0 -86
- package/scripts/validate-links.py +0 -64
- package/scripts/validate_strategy_output.py +0 -212
- package/scripts/vbrief_activate.py +0 -228
- package/scripts/vbrief_migrate_conformance.py +0 -368
- package/scripts/vbrief_reconcile_graph.py +0 -306
- package/scripts/vbrief_reconcile_labels.py +0 -460
- package/scripts/vbrief_reconcile_umbrellas.py +0 -741
- package/scripts/vbrief_validate.py +0 -1144
- package/scripts/verify-stubs.py +0 -61
- package/scripts/verify_capacity.py +0 -160
- package/scripts/verify_encoding.py +0 -699
- package/scripts/verify_hooks_installed.py +0 -206
- package/scripts/verify_investigation.py +0 -360
- package/scripts/verify_judgment_gates.py +0 -827
- package/scripts/verify_no_task_runtime.py +0 -171
- package/scripts/verify_scm_boundary.py +0 -509
- package/scripts/verify_session_ritual.py +0 -389
- package/scripts/verify_tools.py +0 -426
- package/scripts/verify_vbrief_conformance.py +0 -478
package/scripts/scm.py
DELETED
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""scripts/scm.py -- minimal scm:* stub wrapper for #883 v1 cache layer (Story 1).
|
|
3
|
-
|
|
4
|
-
DO NOT EXTEND. The full scm:* namespace lives at #881; this script is replaced
|
|
5
|
-
wholesale when #881 lands. The stub exposes only the four ``issue:*`` commands
|
|
6
|
-
the v1 cache consumer (Story 2 ``cache:fetch-all``) needs:
|
|
7
|
-
|
|
8
|
-
scm.py issue list <pass-through args>
|
|
9
|
-
scm.py issue view <pass-through args>
|
|
10
|
-
scm.py issue close <pass-through args>
|
|
11
|
-
scm.py issue edit <pass-through args>
|
|
12
|
-
|
|
13
|
-
Each command is a thin pass-through to ``ghx <namespace> <verb> ...`` when
|
|
14
|
-
``ghx`` is on PATH, falling back to ``gh <namespace> <verb> ...`` otherwise.
|
|
15
|
-
This mirrors the #884 ``ghx-as-standard-gh-proxy`` recommendation while
|
|
16
|
-
keeping the stub functional on machines where only ``gh`` is installed.
|
|
17
|
-
|
|
18
|
-
The JSON-shape contract Story 2 consumes is pinned independently by the
|
|
19
|
-
``tests/test_scm_contract.py`` contract test against
|
|
20
|
-
``tests/fixtures/scm_issue_view.json`` -- this script does NOT validate or
|
|
21
|
-
transform the JSON; it forwards stdout/stderr/exit-code from the underlying
|
|
22
|
-
binary verbatim.
|
|
23
|
-
|
|
24
|
-
REST opt-in mode (#976)
|
|
25
|
-
-----------------------
|
|
26
|
-
A new ``--rest`` flag is recognised on ``issue view`` and ``issue list``
|
|
27
|
-
invocations. When present, the stub routes the read through the REST
|
|
28
|
-
helpers in :mod:`scripts.gh_rest` (``rest_issue_view`` /
|
|
29
|
-
``rest_issue_list``) instead of forwarding ``gh issue view|list`` to the
|
|
30
|
-
underlying binary. This sidesteps the GraphQL bucket entirely so a
|
|
31
|
-
depleted ``graphql.remaining`` (a recurring failure mode -- see #976,
|
|
32
|
-
#961, #884) no longer fails read-only smoke / cache flows.
|
|
33
|
-
|
|
34
|
-
The REST shape differs from the gh ``--json`` GraphQL shape (e.g. REST
|
|
35
|
-
emits ``user`` not ``author``, ``created_at`` not ``createdAt``, lower-
|
|
36
|
-
case ``state``). Story 2 ``cache:fetch-all`` continues to consume the
|
|
37
|
-
legacy GraphQL shape via the default code path; only callers that
|
|
38
|
-
opt in via ``--rest`` see the REST shape. The smoke test
|
|
39
|
-
(``tests/integration/test_scm_smoke.py``) and any other non-cache
|
|
40
|
-
reader can opt in safely. Mutations (``close``, ``edit``) still
|
|
41
|
-
forward to ``gh`` -- they have non-trivial flag surfaces (--body-file,
|
|
42
|
-
--add-label, --remove-label, ...) that this stub deliberately does not
|
|
43
|
-
re-implement; #881 owns the full surface.
|
|
44
|
-
|
|
45
|
-
GraphQL-only operations (cannot be REST-migrated)
|
|
46
|
-
--------------------------------------------------
|
|
47
|
-
GitHub exposes two PR-state mutations only via GraphQL; they have NO
|
|
48
|
-
REST equivalent and remain budgeted GraphQL spend wherever they appear:
|
|
49
|
-
|
|
50
|
-
- ``markPullRequestReadyForReview`` (``gh pr ready``). Used by the
|
|
51
|
-
release/PR flow; documented in
|
|
52
|
-
:mod:`scripts.gh_rest` module docstring known limitations.
|
|
53
|
-
- ``addPullRequestReview`` (``gh pr review --approve|--request-changes``).
|
|
54
|
-
Required for formal review verdicts; ``rest_post_comment`` is the
|
|
55
|
-
REST-budget alternative when no approval semantics are needed.
|
|
56
|
-
|
|
57
|
-
Future agents touching this stub: those two surfaces are accidental-drain-free
|
|
58
|
-
and legitimate GraphQL spend. Every OTHER GraphQL-backed gh invocation in
|
|
59
|
-
this stub's surface is candidate for REST migration -- the ``--rest`` flag
|
|
60
|
-
is the v1 wedge.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
from __future__ import annotations
|
|
64
|
-
|
|
65
|
-
import importlib
|
|
66
|
-
import json
|
|
67
|
-
import shutil
|
|
68
|
-
import subprocess
|
|
69
|
-
import sys
|
|
70
|
-
from collections.abc import Sequence
|
|
71
|
-
from typing import Any
|
|
72
|
-
|
|
73
|
-
# ---------------------------------------------------------------------------
|
|
74
|
-
# Constants
|
|
75
|
-
# ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
#: Allowed ``<namespace>`` argv[1] -- the v1 stub only exposes ``issue``.
|
|
78
|
-
#: PR commands (#881 future) and any other namespace are rejected loudly so
|
|
79
|
-
#: a typo doesn't silently dispatch unexpected gh subcommands.
|
|
80
|
-
_ALLOWED_NAMESPACES: tuple[str, ...] = ("issue",)
|
|
81
|
-
|
|
82
|
-
#: Source-aware shim (#1145 / N5) supported sources. v1 ships with only
|
|
83
|
-
#: ``github-issue``; ``gitlab`` / ``gitea`` / ``local`` are placeholders for
|
|
84
|
-
#: #445 / #935 Workstream 6 and raise :class:`NotImplementedError` with the
|
|
85
|
-
#: canonical message documented on issue #1145. Adding a source here without
|
|
86
|
-
#: a matching backend implementation in :func:`call` is a bug -- the verifier
|
|
87
|
-
#: :mod:`scripts.verify_scm_boundary` does not enforce backend coverage, but
|
|
88
|
-
#: the unit test ``tests/test_scm_call.py::test_unknown_source_raises`` pins
|
|
89
|
-
#: the exhaustive-source contract.
|
|
90
|
-
_SUPPORTED_CALL_SOURCES: tuple[str, ...] = ("github-issue",)
|
|
91
|
-
|
|
92
|
-
#: Allowed ``<verb>`` argv[2] for the ``issue`` namespace. Mirrors the four
|
|
93
|
-
#: AC-1 commands in vbrief/active/2026-05-05-883-story-1-scm-stub.vbrief.json.
|
|
94
|
-
_ALLOWED_ISSUE_VERBS: tuple[str, ...] = ("list", "view", "close", "edit")
|
|
95
|
-
|
|
96
|
-
#: Binary preference order. ``ghx`` is the #884 standard proxy; ``gh`` is the
|
|
97
|
-
#: canonical fallback. Tests parametrise this via subprocess + shutil.which
|
|
98
|
-
#: mocks so the fallback path is exercised independent of the host PATH.
|
|
99
|
-
_BINARY_PREFERENCE: tuple[str, ...] = ("ghx", "gh")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class ScmStubError(RuntimeError):
|
|
103
|
-
"""Raised on argv-validation or binary-resolution failures."""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# ---------------------------------------------------------------------------
|
|
107
|
-
# Resolution
|
|
108
|
-
# ---------------------------------------------------------------------------
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def resolve_binary() -> str:
|
|
112
|
-
"""Return ``"ghx"`` if on PATH, else ``"gh"``; raise if neither is present.
|
|
113
|
-
|
|
114
|
-
The fallback order is fixed by :data:`_BINARY_PREFERENCE` so a regression
|
|
115
|
-
that re-orders or shadows a binary fails the unit test in
|
|
116
|
-
``tests/test_scm_stub.py`` rather than silently dispatching to the wrong
|
|
117
|
-
proxy. Both binaries accept identical ``issue list/view/close/edit``
|
|
118
|
-
surfaces for the v1 stub's purposes.
|
|
119
|
-
"""
|
|
120
|
-
for candidate in _BINARY_PREFERENCE:
|
|
121
|
-
if shutil.which(candidate) is not None:
|
|
122
|
-
return candidate
|
|
123
|
-
raise ScmStubError(
|
|
124
|
-
"neither 'ghx' nor 'gh' found on PATH; install GitHub CLI "
|
|
125
|
-
"(https://cli.github.com/) or the ghx proxy (#884)"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# ---------------------------------------------------------------------------
|
|
130
|
-
# Argv shaping
|
|
131
|
-
# ---------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def build_command(
|
|
135
|
-
namespace: str, verb: str, extra: Sequence[str], *, binary: str | None = None
|
|
136
|
-
) -> list[str]:
|
|
137
|
-
"""Construct the underlying ``[binary, namespace, verb, *extra]`` argv.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
namespace: One of :data:`_ALLOWED_NAMESPACES`. Anything else raises
|
|
141
|
-
:class:`ScmStubError` -- the stub deliberately refuses unknown
|
|
142
|
-
namespaces so a typo (``isue``) doesn't get forwarded to gh and
|
|
143
|
-
produce a confusing native-error message.
|
|
144
|
-
verb: For ``issue``, one of :data:`_ALLOWED_ISSUE_VERBS`. Same loud-
|
|
145
|
-
failure rationale as namespace validation.
|
|
146
|
-
extra: Pass-through positional / option args. Forwarded verbatim;
|
|
147
|
-
this stub does NOT inspect or rewrite them.
|
|
148
|
-
binary: Optional override for the resolved binary. When ``None``,
|
|
149
|
-
:func:`resolve_binary` is consulted. Tests pass an explicit
|
|
150
|
-
value so they don't depend on the host PATH.
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
The argv list ready for :func:`subprocess.run`.
|
|
154
|
-
"""
|
|
155
|
-
if namespace not in _ALLOWED_NAMESPACES:
|
|
156
|
-
raise ScmStubError(
|
|
157
|
-
f"unknown scm namespace {namespace!r}; expected one of "
|
|
158
|
-
f"{_ALLOWED_NAMESPACES}. The full scm:* namespace lives at #881."
|
|
159
|
-
)
|
|
160
|
-
if namespace == "issue" and verb not in _ALLOWED_ISSUE_VERBS:
|
|
161
|
-
raise ScmStubError(
|
|
162
|
-
f"unknown scm:issue verb {verb!r}; expected one of "
|
|
163
|
-
f"{_ALLOWED_ISSUE_VERBS}. The v1 stub only exposes these four; "
|
|
164
|
-
"additional scm:issue:* commands belong on #881."
|
|
165
|
-
)
|
|
166
|
-
resolved = binary if binary is not None else resolve_binary()
|
|
167
|
-
return [resolved, namespace, verb, *extra]
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
# ---------------------------------------------------------------------------
|
|
171
|
-
# Source-aware call shim (#1145 / N5)
|
|
172
|
-
# ---------------------------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def call(
|
|
176
|
-
source: str,
|
|
177
|
-
verb: str,
|
|
178
|
-
args: Sequence[str] | None = None,
|
|
179
|
-
*,
|
|
180
|
-
check: bool = False,
|
|
181
|
-
capture_output: bool = True,
|
|
182
|
-
text: bool = True,
|
|
183
|
-
timeout: float | None = None,
|
|
184
|
-
cwd: str | None = None,
|
|
185
|
-
binary: str | None = None,
|
|
186
|
-
**kwargs: Any,
|
|
187
|
-
) -> subprocess.CompletedProcess[str]:
|
|
188
|
-
"""Source-aware SCM invocation -- partial down-payment on #445 / #935 Workstream 6.
|
|
189
|
-
|
|
190
|
-
This is the single seam through which the deft framework's verb layer
|
|
191
|
-
(``scripts/triage_*.py``, ``scripts/scope_*.py``, ``scripts/slice_*.py``,
|
|
192
|
-
``scripts/issue_ingest.py``, ...) invokes the underlying SCM CLI.
|
|
193
|
-
Pre-N5, every consumer called ``subprocess.run(["gh", ...])`` directly;
|
|
194
|
-
the first non-GitHub consumer would have hit an undocumented coupling
|
|
195
|
-
deep in the call stack. The shim relocates that coupling to one
|
|
196
|
-
indirection point so the full SCM abstraction (#445 / #935 Workstream 6)
|
|
197
|
-
has a single seam to extend.
|
|
198
|
-
|
|
199
|
-
Routing (v1):
|
|
200
|
-
|
|
201
|
-
- ``source="github-issue"`` -- forwards to ``[binary, verb, *args]``
|
|
202
|
-
where ``binary`` comes from :func:`resolve_binary` (the #884
|
|
203
|
-
``ghx`` -> ``gh`` preference ladder). This is the only source v1
|
|
204
|
-
implements.
|
|
205
|
-
- Any other source (``"gitlab"``, ``"gitea"``, ``"local"``, ...)
|
|
206
|
-
raises :class:`NotImplementedError` with the canonical message
|
|
207
|
-
``"source=<x> not yet supported; see #445 / #935 Workstream 6 for
|
|
208
|
-
the abstraction."`` so a consumer on a non-GitHub forge sees the
|
|
209
|
-
deferred abstraction immediately instead of an obscure
|
|
210
|
-
``"gh: command not found"`` deep in the call stack.
|
|
211
|
-
|
|
212
|
-
Args:
|
|
213
|
-
source: Forge identity for the invocation. Currently only
|
|
214
|
-
``"github-issue"`` is implemented; see
|
|
215
|
-
:data:`_SUPPORTED_CALL_SOURCES` for the contract.
|
|
216
|
-
verb: The CLI verb passed to the resolved binary (e.g. ``"issue"``,
|
|
217
|
-
``"api"``, ``"pr"``). Forwarded verbatim as the first argv
|
|
218
|
-
element after the binary; this shim deliberately does NOT
|
|
219
|
-
validate the verb so callers can use any surface the
|
|
220
|
-
underlying binary supports.
|
|
221
|
-
args: Pass-through positional / option args appended after
|
|
222
|
-
``verb``. Defaults to an empty sequence.
|
|
223
|
-
check: Forwarded to :func:`subprocess.run`. Defaults to ``False``
|
|
224
|
-
so callers can inspect non-zero exits without an exception;
|
|
225
|
-
mutation call sites that want loud failures opt in via
|
|
226
|
-
``check=True``.
|
|
227
|
-
capture_output / text: Forwarded to :func:`subprocess.run`.
|
|
228
|
-
Defaults to capture+text so the common "parse stdout" usage
|
|
229
|
-
works without extra plumbing.
|
|
230
|
-
timeout: Optional wall-clock cap forwarded to
|
|
231
|
-
:func:`subprocess.run`. Mirrors the existing per-call-site
|
|
232
|
-
timeouts (e.g. ``issue_ingest._fetch_single_issue`` uses
|
|
233
|
-
30s).
|
|
234
|
-
cwd: Optional working directory forwarded to
|
|
235
|
-
:func:`subprocess.run`.
|
|
236
|
-
binary: Optional override for the resolved binary. Tests pass
|
|
237
|
-
this so they don't depend on the host PATH.
|
|
238
|
-
**kwargs: Additional :func:`subprocess.run` keyword args
|
|
239
|
-
(``env``, ``input``, ``stdin``, ...).
|
|
240
|
-
|
|
241
|
-
Returns:
|
|
242
|
-
The :class:`subprocess.CompletedProcess` from the underlying
|
|
243
|
-
invocation -- the shim does not parse or transform stdout /
|
|
244
|
-
stderr / returncode.
|
|
245
|
-
|
|
246
|
-
Raises:
|
|
247
|
-
NotImplementedError: When ``source`` is not in
|
|
248
|
-
:data:`_SUPPORTED_CALL_SOURCES`. The error message points at
|
|
249
|
-
#445 / #935 Workstream 6 so consumers on GitLab / Gitea /
|
|
250
|
-
local backends see the deferred abstraction immediately.
|
|
251
|
-
ScmStubError: When neither ``gh`` nor ``ghx`` is on PATH and no
|
|
252
|
-
explicit ``binary`` override was provided.
|
|
253
|
-
|
|
254
|
-
Notes on the verifier (`scripts/verify_scm_boundary.py`):
|
|
255
|
-
The companion deterministic gate scans tracked Python files in
|
|
256
|
-
the verb-layer scope (``scripts/triage_*.py``,
|
|
257
|
-
``scripts/scope_*.py``, ``scripts/slice_*.py``,
|
|
258
|
-
``scripts/_triage_*.py``, ``scripts/_scope_*.py``,
|
|
259
|
-
``scripts/resume_conditions.py``, ``scripts/issue_ingest.py``)
|
|
260
|
-
for subprocess / Popen / os.system invocations whose first
|
|
261
|
-
argv element is the literal ``"gh"`` or ``"ghx"``. Any such
|
|
262
|
-
call in those files is a violation because it bypasses this
|
|
263
|
-
shim. The verifier deliberately scopes by file glob rather
|
|
264
|
-
than scanning every tracked Python file so release tooling,
|
|
265
|
-
REST helpers (:mod:`scripts.gh_rest`), and the ghx installer
|
|
266
|
-
(:mod:`scripts.setup_ghx`) -- which have legitimate direct-gh
|
|
267
|
-
responsibilities -- are not flagged.
|
|
268
|
-
"""
|
|
269
|
-
if source not in _SUPPORTED_CALL_SOURCES:
|
|
270
|
-
raise NotImplementedError(
|
|
271
|
-
f"source={source!r} not yet supported; "
|
|
272
|
-
"see #445 / #935 Workstream 6 for the abstraction."
|
|
273
|
-
)
|
|
274
|
-
resolved = binary if binary is not None else resolve_binary()
|
|
275
|
-
argv = [resolved, verb, *(args if args is not None else ())]
|
|
276
|
-
return subprocess.run(
|
|
277
|
-
argv,
|
|
278
|
-
check=check,
|
|
279
|
-
capture_output=capture_output,
|
|
280
|
-
text=text,
|
|
281
|
-
timeout=timeout,
|
|
282
|
-
cwd=cwd,
|
|
283
|
-
**kwargs,
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
# ---------------------------------------------------------------------------
|
|
288
|
-
# REST opt-in (#976)
|
|
289
|
-
# ---------------------------------------------------------------------------
|
|
290
|
-
|
|
291
|
-
#: Verbs that support the ``--rest`` opt-in. Only read paths -- mutations
|
|
292
|
-
#: (close, edit) keep forwarding to gh in the v1 stub.
|
|
293
|
-
_REST_OPT_IN_VERBS: tuple[str, ...] = ("view", "list")
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def _extract_flag(extra: list[str], flag: str) -> tuple[bool, list[str]]:
|
|
297
|
-
"""Return ``(present, remainder)`` after removing every occurrence of ``flag``.
|
|
298
|
-
|
|
299
|
-
Used to peel off the ``--rest`` opt-in flag before the remaining argv
|
|
300
|
-
is consumed by the REST dispatcher (or, in the legacy path, forwarded
|
|
301
|
-
to ``ghx|gh``).
|
|
302
|
-
"""
|
|
303
|
-
present = flag in extra
|
|
304
|
-
remainder = [a for a in extra if a != flag]
|
|
305
|
-
return present, remainder
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def _extract_value_flag(
|
|
309
|
-
extra: list[str], flag: str, default: str | None = None
|
|
310
|
-
) -> tuple[str | None, list[str]]:
|
|
311
|
-
"""Return ``(value, remainder)`` for ``--flag VALUE`` or ``--flag=VALUE``.
|
|
312
|
-
|
|
313
|
-
Removes the consumed tokens from ``extra``. Mirrors the small subset of
|
|
314
|
-
argv parsing the stub does so it can extract ``--repo`` / ``--json`` /
|
|
315
|
-
``--state`` etc. from the pass-through args without pulling in argparse.
|
|
316
|
-
The first occurrence wins; a missing flag returns ``default``.
|
|
317
|
-
"""
|
|
318
|
-
out: list[str] = []
|
|
319
|
-
value = default
|
|
320
|
-
seen = False
|
|
321
|
-
i = 0
|
|
322
|
-
while i < len(extra):
|
|
323
|
-
token = extra[i]
|
|
324
|
-
if not seen and token == flag and i + 1 < len(extra):
|
|
325
|
-
value = extra[i + 1]
|
|
326
|
-
seen = True
|
|
327
|
-
i += 2
|
|
328
|
-
continue
|
|
329
|
-
if not seen and token.startswith(flag + "="):
|
|
330
|
-
value = token.split("=", 1)[1]
|
|
331
|
-
seen = True
|
|
332
|
-
i += 1
|
|
333
|
-
continue
|
|
334
|
-
out.append(token)
|
|
335
|
-
i += 1
|
|
336
|
-
return value, out
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def _filter_json_fields(obj: Any, fields: Sequence[str]) -> Any:
|
|
340
|
-
"""Project ``obj`` (dict or list[dict]) onto ``fields``.
|
|
341
|
-
|
|
342
|
-
Mirrors gh's ``--json field1,field2`` semantics for the REST shape:
|
|
343
|
-
only the named keys survive. Unknown fields are silently dropped
|
|
344
|
-
rather than raised, matching gh's own behaviour. Empty ``fields``
|
|
345
|
-
returns ``obj`` unchanged so callers that omit ``--json`` get the
|
|
346
|
-
full REST response.
|
|
347
|
-
"""
|
|
348
|
-
if not fields:
|
|
349
|
-
return obj
|
|
350
|
-
field_set = list(fields)
|
|
351
|
-
if isinstance(obj, list):
|
|
352
|
-
return [_filter_json_fields(item, field_set) for item in obj]
|
|
353
|
-
if isinstance(obj, dict):
|
|
354
|
-
return {k: obj[k] for k in field_set if k in obj}
|
|
355
|
-
return obj
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def _run_rest_view(extra: list[str]) -> int:
|
|
359
|
-
"""Dispatch ``scm.py issue view --rest <N> --repo X [--json fields]``.
|
|
360
|
-
|
|
361
|
-
Routes through :func:`scripts.gh_rest.rest_issue_view` so the read
|
|
362
|
-
never touches GraphQL. Emits the REST response (filtered to
|
|
363
|
-
``--json`` fields if provided) to stdout as JSON, mirroring the
|
|
364
|
-
legacy gh stdout contract callers consume.
|
|
365
|
-
|
|
366
|
-
Unknown flags (anything beginning with ``-`` after stripping the
|
|
367
|
-
consumed ``--repo`` / ``--json``) are rejected loudly so an
|
|
368
|
-
operator-side typo (e.g. ``--state closed`` accidentally passed to
|
|
369
|
-
``issue view``) surfaces immediately rather than being silently
|
|
370
|
-
ignored. Greptile P2 (#976 review): the prior implementation kept
|
|
371
|
-
these tokens in ``extra`` after extraction and never inspected
|
|
372
|
-
them again; the user got an unrelated successful response.
|
|
373
|
-
"""
|
|
374
|
-
repo, extra = _extract_value_flag(extra, "--repo")
|
|
375
|
-
json_spec, extra = _extract_value_flag(extra, "--json")
|
|
376
|
-
if not repo:
|
|
377
|
-
print("error: --rest issue view requires --repo OWNER/NAME", file=sys.stderr)
|
|
378
|
-
return 2
|
|
379
|
-
# The remaining positional arg (after stripping --repo/--json) is the
|
|
380
|
-
# issue number. Reject extra unknown flags loudly so a typo is caught.
|
|
381
|
-
positionals = [t for t in extra if not t.startswith("-")]
|
|
382
|
-
leftover_flags = [t for t in extra if t.startswith("-")]
|
|
383
|
-
if leftover_flags:
|
|
384
|
-
print(
|
|
385
|
-
f"error: --rest issue view does not recognise these flags: "
|
|
386
|
-
f"{leftover_flags!r}. Supported flags are --repo, --json. "
|
|
387
|
-
"Mutations / additional read filters belong on #881.",
|
|
388
|
-
file=sys.stderr,
|
|
389
|
-
)
|
|
390
|
-
return 2
|
|
391
|
-
if len(positionals) != 1:
|
|
392
|
-
print(
|
|
393
|
-
"error: --rest issue view expects exactly one positional issue "
|
|
394
|
-
f"number; got {positionals!r}",
|
|
395
|
-
file=sys.stderr,
|
|
396
|
-
)
|
|
397
|
-
return 2
|
|
398
|
-
try:
|
|
399
|
-
issue_n = int(positionals[0])
|
|
400
|
-
except ValueError:
|
|
401
|
-
print(
|
|
402
|
-
f"error: issue number must be an integer; got {positionals[0]!r}",
|
|
403
|
-
file=sys.stderr,
|
|
404
|
-
)
|
|
405
|
-
return 2
|
|
406
|
-
gh_rest = importlib.import_module("gh_rest")
|
|
407
|
-
try:
|
|
408
|
-
response = gh_rest.rest_issue_view(repo, issue_n)
|
|
409
|
-
except gh_rest.InvalidRepoError as exc:
|
|
410
|
-
# InvalidRepoError is a ValueError subclass raised by
|
|
411
|
-
# gh_rest._split_repo when --repo lacks the OWNER/NAME shape
|
|
412
|
-
# (e.g. ``--repo directive`` instead of ``--repo deftai/directive``).
|
|
413
|
-
# Treat it as an arg-validation failure (exit 2) so the user
|
|
414
|
-
# sees a clean error rather than an uncaught traceback.
|
|
415
|
-
# Greptile P1 #998 review at 367748e.
|
|
416
|
-
print(f"error: invalid --repo value: {exc}", file=sys.stderr)
|
|
417
|
-
return 2
|
|
418
|
-
except gh_rest.GhRestError as exc:
|
|
419
|
-
print(f"error: {exc}", file=sys.stderr)
|
|
420
|
-
return 1
|
|
421
|
-
fields = [f.strip() for f in json_spec.split(",")] if json_spec else []
|
|
422
|
-
filtered = _filter_json_fields(response, fields)
|
|
423
|
-
print(json.dumps(filtered, ensure_ascii=False))
|
|
424
|
-
return 0
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def _run_rest_list(extra: list[str]) -> int:
|
|
428
|
-
"""Dispatch ``scm.py issue list --rest --repo X [...flags]``.
|
|
429
|
-
|
|
430
|
-
Supported flags: ``--state {open|closed|all}`` (default open),
|
|
431
|
-
``--label NAME[,NAME...]`` (comma-separated label filter; multi-flag
|
|
432
|
-
repetition `--label A --label B` is also accepted and merged into
|
|
433
|
-
the same filter set), ``--author LOGIN`` (#1055 -- filter by issue
|
|
434
|
-
creator; maps to the REST ``creator`` param and composes with
|
|
435
|
-
``--label`` via AND), ``--limit N`` (REST per_page, default 30),
|
|
436
|
-
``--json field1,field2`` (project the response onto the named keys,
|
|
437
|
-
list-aware).
|
|
438
|
-
|
|
439
|
-
Unknown flags after stripping the consumed flag set are rejected
|
|
440
|
-
loudly (Greptile P2 #976 review): the prior implementation silently
|
|
441
|
-
dropped any leftover ``--foo`` token, which produced subtly wrong
|
|
442
|
-
behaviour (e.g. a typo'd ``--label-name`` was ignored without error).
|
|
443
|
-
|
|
444
|
-
The list verb takes NO positional arguments; any leftover positional
|
|
445
|
-
token is rejected loudly so a caller who typo'd `issue list --rest 123
|
|
446
|
-
--repo o/r` (meaning `issue view`) sees the mistake immediately
|
|
447
|
-
instead of receiving the full open-issues collection silently
|
|
448
|
-
(Greptile P1 #976 second-pass review). Mirrors the parallel guard
|
|
449
|
-
in ``_run_rest_view`` for symmetry.
|
|
450
|
-
|
|
451
|
-
Routes through :func:`scripts.gh_rest.rest_issue_list`.
|
|
452
|
-
"""
|
|
453
|
-
repo, extra = _extract_value_flag(extra, "--repo")
|
|
454
|
-
state, extra = _extract_value_flag(extra, "--state", default="open")
|
|
455
|
-
json_spec, extra = _extract_value_flag(extra, "--json")
|
|
456
|
-
author, extra = _extract_value_flag(extra, "--author")
|
|
457
|
-
# --label may appear multiple times; collect all occurrences and
|
|
458
|
-
# merge with comma-separated values from any single occurrence.
|
|
459
|
-
label_values: list[str] = []
|
|
460
|
-
while True:
|
|
461
|
-
label_part, extra = _extract_value_flag(extra, "--label")
|
|
462
|
-
if label_part is None:
|
|
463
|
-
break
|
|
464
|
-
label_values.append(label_part)
|
|
465
|
-
limit_str, extra = _extract_value_flag(extra, "--limit", default="30")
|
|
466
|
-
leftover_flags = [t for t in extra if t.startswith("-")]
|
|
467
|
-
if leftover_flags:
|
|
468
|
-
print(
|
|
469
|
-
f"error: --rest issue list does not recognise these flags: "
|
|
470
|
-
f"{leftover_flags!r}. Supported flags are --repo, --state, "
|
|
471
|
-
"--label, --author, --limit, --json. Additional filters "
|
|
472
|
-
"belong on #881.",
|
|
473
|
-
file=sys.stderr,
|
|
474
|
-
)
|
|
475
|
-
return 2
|
|
476
|
-
leftover_positionals = [t for t in extra if not t.startswith("-")]
|
|
477
|
-
if leftover_positionals:
|
|
478
|
-
print(
|
|
479
|
-
f"error: --rest issue list takes no positional arguments; "
|
|
480
|
-
f"got {leftover_positionals!r}. Did you mean "
|
|
481
|
-
f"`scm.py issue view --rest {leftover_positionals[0]} --repo OWNER/NAME`?",
|
|
482
|
-
file=sys.stderr,
|
|
483
|
-
)
|
|
484
|
-
return 2
|
|
485
|
-
if not repo:
|
|
486
|
-
print("error: --rest issue list requires --repo OWNER/NAME", file=sys.stderr)
|
|
487
|
-
return 2
|
|
488
|
-
try:
|
|
489
|
-
per_page = int(limit_str) if limit_str is not None else 30
|
|
490
|
-
except ValueError:
|
|
491
|
-
print(
|
|
492
|
-
f"error: --limit must be an integer; got {limit_str!r}",
|
|
493
|
-
file=sys.stderr,
|
|
494
|
-
)
|
|
495
|
-
return 2
|
|
496
|
-
labels: tuple[str, ...] = tuple(
|
|
497
|
-
item.strip()
|
|
498
|
-
for value in label_values
|
|
499
|
-
for item in value.split(",")
|
|
500
|
-
if item.strip()
|
|
501
|
-
)
|
|
502
|
-
gh_rest = importlib.import_module("gh_rest")
|
|
503
|
-
assert state is not None # default ensures non-None
|
|
504
|
-
try:
|
|
505
|
-
response = gh_rest.rest_issue_list(
|
|
506
|
-
repo, state=state, labels=labels, author=author, per_page=per_page
|
|
507
|
-
)
|
|
508
|
-
except gh_rest.InvalidRepoError as exc:
|
|
509
|
-
# See _run_rest_view for rationale; same gap (Greptile P1 #998
|
|
510
|
-
# review at 367748e) -- _split_repo validation must surface as
|
|
511
|
-
# exit 2 with a clean message, not an uncaught traceback.
|
|
512
|
-
print(f"error: invalid --repo value: {exc}", file=sys.stderr)
|
|
513
|
-
return 2
|
|
514
|
-
except gh_rest.GhRestError as exc:
|
|
515
|
-
print(f"error: {exc}", file=sys.stderr)
|
|
516
|
-
return 1
|
|
517
|
-
fields = [f.strip() for f in json_spec.split(",")] if json_spec else []
|
|
518
|
-
filtered = _filter_json_fields(response, fields)
|
|
519
|
-
print(json.dumps(filtered, ensure_ascii=False))
|
|
520
|
-
return 0
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
# ---------------------------------------------------------------------------
|
|
524
|
-
# Entry point
|
|
525
|
-
# ---------------------------------------------------------------------------
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
def main(argv: list[str] | None = None) -> int:
|
|
529
|
-
"""CLI entry point. Returns the underlying binary's exit code (or 2 on arg error).
|
|
530
|
-
|
|
531
|
-
Argv layout:
|
|
532
|
-
argv[0] = namespace (only ``issue`` in the v1 stub)
|
|
533
|
-
argv[1] = verb (one of list/view/close/edit)
|
|
534
|
-
argv[2:] = pass-through args forwarded to ``ghx|gh`` (legacy path)
|
|
535
|
-
OR consumed by the REST dispatcher when ``--rest`` is
|
|
536
|
-
present (#976).
|
|
537
|
-
|
|
538
|
-
No argparse: the stub deliberately avoids capturing ``--help`` / ``--json``
|
|
539
|
-
/ etc. flags itself in the legacy path, so they reach the underlying
|
|
540
|
-
binary untouched. The only argv inspection the stub performs is:
|
|
541
|
-
|
|
542
|
-
1. The namespace + verb whitelist in :func:`build_command` (which fails
|
|
543
|
-
loud rather than dispatching unknown surfaces).
|
|
544
|
-
2. The ``--rest`` opt-in extraction (#976) -- when present on a
|
|
545
|
-
supported read verb (``view``, ``list``), routes the read through
|
|
546
|
-
:mod:`scripts.gh_rest` REST helpers instead of forwarding to
|
|
547
|
-
``ghx|gh``. The flag is stripped before any forwarding so the
|
|
548
|
-
legacy path is unaffected on calls that don't opt in.
|
|
549
|
-
"""
|
|
550
|
-
args = list(sys.argv[1:] if argv is None else argv)
|
|
551
|
-
if len(args) < 2:
|
|
552
|
-
print(
|
|
553
|
-
"usage: scm.py <namespace> <verb> [pass-through args...]\n"
|
|
554
|
-
" (v1 stub: namespace=issue, verb=list|view|close|edit)\n"
|
|
555
|
-
" --rest opt-in is supported on issue view/list (#976)",
|
|
556
|
-
file=sys.stderr,
|
|
557
|
-
)
|
|
558
|
-
return 2
|
|
559
|
-
namespace, verb, *extra = args
|
|
560
|
-
# #976: detect and consume the --rest opt-in BEFORE any gh forwarding.
|
|
561
|
-
# The flag is stripped from extra so the legacy path stays argv-pure.
|
|
562
|
-
rest_mode, extra = _extract_flag(extra, "--rest")
|
|
563
|
-
if rest_mode:
|
|
564
|
-
if namespace != "issue" or verb not in _REST_OPT_IN_VERBS:
|
|
565
|
-
print(
|
|
566
|
-
f"error: --rest is only supported on 'issue {{view|list}}'; "
|
|
567
|
-
f"got 'scm.py {namespace} {verb}'. Mutations (close, edit) "
|
|
568
|
-
"still forward to gh in the v1 stub; #881 owns the full "
|
|
569
|
-
"REST migration.",
|
|
570
|
-
file=sys.stderr,
|
|
571
|
-
)
|
|
572
|
-
return 2
|
|
573
|
-
if verb == "view":
|
|
574
|
-
return _run_rest_view(extra)
|
|
575
|
-
return _run_rest_list(extra)
|
|
576
|
-
|
|
577
|
-
try:
|
|
578
|
-
cmd = build_command(namespace, verb, extra)
|
|
579
|
-
except ScmStubError as exc:
|
|
580
|
-
print(f"error: {exc}", file=sys.stderr)
|
|
581
|
-
return 2
|
|
582
|
-
|
|
583
|
-
# subprocess.run with check=False so we forward the underlying exit code
|
|
584
|
-
# rather than raising; gh's non-zero exits (e.g. issue not found) carry
|
|
585
|
-
# actionable stderr that the caller already handles.
|
|
586
|
-
proc = subprocess.run(cmd, check=False)
|
|
587
|
-
return int(proc.returncode)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if __name__ == "__main__":
|
|
591
|
-
raise SystemExit(main())
|