@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,674 @@
|
|
|
1
|
+
"""_triage_smoketest_stages.py -- 9-stage assertion helpers for triage_smoketest.py.
|
|
2
|
+
|
|
3
|
+
Extracted from ``scripts/triage_smoketest.py`` so the driver stays under
|
|
4
|
+
the 1000-line MUST cap (coding/coding.md). Each ``stage_*`` function
|
|
5
|
+
takes the project root, the smoketest's :class:`AssertLog`, and any
|
|
6
|
+
prior-stage output it needs, and raises :class:`SmoketestFailure` on
|
|
7
|
+
the first assertion that fails.
|
|
8
|
+
|
|
9
|
+
The functions deliberately keep their own subprocess calls so the
|
|
10
|
+
driver can compose them in any order (current order matches the issue
|
|
11
|
+
body's demoability block 1..9).
|
|
12
|
+
|
|
13
|
+
Refs:
|
|
14
|
+
|
|
15
|
+
* Umbrella: #1119
|
|
16
|
+
* This deliverable: #1146 (N6)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime, timedelta
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
from uuid import uuid4
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from triage_smoketest import AssertLog
|
|
32
|
+
|
|
33
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
34
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
35
|
+
|
|
36
|
+
FIXTURE_REPO = "deftai/smoketest"
|
|
37
|
+
WARN_GLYPH = "\u26a0"
|
|
38
|
+
SUMMARY_MAX_CHARS = 120
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Subprocess helper
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_script(
|
|
47
|
+
script_name: str,
|
|
48
|
+
*cli_args: str,
|
|
49
|
+
project_root: Path,
|
|
50
|
+
extra_env: dict[str, str] | None = None,
|
|
51
|
+
include_repo_env: bool = True,
|
|
52
|
+
) -> subprocess.CompletedProcess[str]:
|
|
53
|
+
"""Run a sibling script with ``cli_args`` against ``project_root``.
|
|
54
|
+
|
|
55
|
+
``include_repo_env`` controls whether ``DEFT_TRIAGE_REPO`` is injected
|
|
56
|
+
into the subprocess env. Stages that explicitly pass ``--repo`` (audit
|
|
57
|
+
/ queue / defer) leave it on for redundancy; the bootstrap stage
|
|
58
|
+
turns it off so ``triage_bootstrap.py``'s populate_cache step skips
|
|
59
|
+
cleanly (no --repo, no env fallback, no .git -- so the watchdog has
|
|
60
|
+
nothing to attempt).
|
|
61
|
+
"""
|
|
62
|
+
env = dict(os.environ)
|
|
63
|
+
env["PYTHONUTF8"] = "1"
|
|
64
|
+
env["DEFT_PROJECT_ROOT"] = str(project_root)
|
|
65
|
+
if include_repo_env:
|
|
66
|
+
env["DEFT_TRIAGE_REPO"] = FIXTURE_REPO
|
|
67
|
+
else:
|
|
68
|
+
env.pop("DEFT_TRIAGE_REPO", None)
|
|
69
|
+
if extra_env:
|
|
70
|
+
env.update(extra_env)
|
|
71
|
+
cmd = [sys.executable, str(_SCRIPTS_DIR / script_name), *cli_args]
|
|
72
|
+
return subprocess.run( # noqa: S603 -- known scripts, env-controlled paths
|
|
73
|
+
cmd,
|
|
74
|
+
cwd=str(project_root),
|
|
75
|
+
env=env,
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
errors="replace",
|
|
80
|
+
check=False,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Stage 1: bootstrap + auto-classify
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def stage_bootstrap_and_classify(
|
|
90
|
+
project_root: Path,
|
|
91
|
+
issues_spec: dict[str, Any],
|
|
92
|
+
log: AssertLog,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Run triage:bootstrap, then emulate the D10 auto-classify apply-step.
|
|
95
|
+
|
|
96
|
+
D10 / #1129 landed the universal rules + ``classify_issue`` library but
|
|
97
|
+
did NOT yet wire the apply-step into bootstrap (deferred to a follow-up
|
|
98
|
+
child). The smoketest emulates the eventual apply-step in-process so
|
|
99
|
+
the assertion targets are reachable today.
|
|
100
|
+
"""
|
|
101
|
+
stage = 1
|
|
102
|
+
name = "bootstrap + auto-classify"
|
|
103
|
+
|
|
104
|
+
bootstrap = run_script(
|
|
105
|
+
"triage_bootstrap.py",
|
|
106
|
+
"--project-root", str(project_root),
|
|
107
|
+
"--quiet",
|
|
108
|
+
"--json",
|
|
109
|
+
project_root=project_root,
|
|
110
|
+
include_repo_env=False,
|
|
111
|
+
)
|
|
112
|
+
if bootstrap.returncode != 0:
|
|
113
|
+
raise log.fail(
|
|
114
|
+
stage, name,
|
|
115
|
+
expected="bootstrap exit 0",
|
|
116
|
+
actual=f"exit {bootstrap.returncode}",
|
|
117
|
+
cause="triage_bootstrap.py failed: " + bootstrap.stderr.strip()[:200],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
from candidates_log import append as audit_append # noqa: PLC0415
|
|
121
|
+
from triage_classify import ( # noqa: PLC0415
|
|
122
|
+
classify_issue,
|
|
123
|
+
extract_referenced_issues,
|
|
124
|
+
resolve_classify_rules,
|
|
125
|
+
resolve_hold_markers,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
rules = resolve_classify_rules(project_root)
|
|
129
|
+
markers = resolve_hold_markers(project_root)
|
|
130
|
+
referenced = extract_referenced_issues(project_root)
|
|
131
|
+
now_dt = datetime.fromisoformat(issues_spec["now_iso"].replace("Z", "+00:00"))
|
|
132
|
+
audit_path = project_root / "vbrief" / ".eval" / "candidates.jsonl"
|
|
133
|
+
|
|
134
|
+
counts: dict[str, int] = {"accept": 0, "defer": 0, "archive": 0, "untriaged": 0}
|
|
135
|
+
defer_reasons: dict[str, int] = {}
|
|
136
|
+
|
|
137
|
+
for issue in issues_spec["issues"]:
|
|
138
|
+
n = int(issue["number"])
|
|
139
|
+
gh_issue = {
|
|
140
|
+
"number": n,
|
|
141
|
+
"title": issue["title"],
|
|
142
|
+
"state": issue.get("state", "open"),
|
|
143
|
+
"labels": [{"name": label} for label in issue.get("labels", [])],
|
|
144
|
+
"body": issue.get("body", ""),
|
|
145
|
+
"updated_at": issue.get("updated_at"),
|
|
146
|
+
"created_at": issue.get("created_at"),
|
|
147
|
+
}
|
|
148
|
+
result = classify_issue(
|
|
149
|
+
gh_issue,
|
|
150
|
+
rules=rules,
|
|
151
|
+
hold_markers=markers,
|
|
152
|
+
vbrief_referenced=referenced,
|
|
153
|
+
has_triage_decision=False,
|
|
154
|
+
now=now_dt,
|
|
155
|
+
)
|
|
156
|
+
if result is None:
|
|
157
|
+
counts["untriaged"] += 1
|
|
158
|
+
continue
|
|
159
|
+
if result.action == "archive":
|
|
160
|
+
counts["archive"] += 1
|
|
161
|
+
continue
|
|
162
|
+
entry = {
|
|
163
|
+
"decision_id": str(uuid4()),
|
|
164
|
+
"timestamp": (now_dt + timedelta(seconds=n)).strftime(
|
|
165
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
166
|
+
),
|
|
167
|
+
"repo": FIXTURE_REPO,
|
|
168
|
+
"issue_number": n,
|
|
169
|
+
"decision": result.action,
|
|
170
|
+
"actor": "agent:smoketest-classify",
|
|
171
|
+
"reason": result.reason,
|
|
172
|
+
}
|
|
173
|
+
audit_append(entry, path=audit_path)
|
|
174
|
+
counts[result.action] = counts.get(result.action, 0) + 1
|
|
175
|
+
if result.action == "defer":
|
|
176
|
+
defer_reasons[result.reason] = defer_reasons.get(result.reason, 0) + 1
|
|
177
|
+
|
|
178
|
+
expected_counts = {"accept": 1, "defer": 7, "archive": 0, "untriaged": 12}
|
|
179
|
+
if counts != expected_counts:
|
|
180
|
+
raise log.fail(
|
|
181
|
+
stage, name,
|
|
182
|
+
expected=expected_counts,
|
|
183
|
+
actual=counts,
|
|
184
|
+
cause="auto-classify decision counts diverged from fixture spec",
|
|
185
|
+
)
|
|
186
|
+
expected_defer = {
|
|
187
|
+
"hold marker in body": 3,
|
|
188
|
+
"research": 2,
|
|
189
|
+
"dormant; needs AC refresh": 2,
|
|
190
|
+
}
|
|
191
|
+
if defer_reasons != expected_defer:
|
|
192
|
+
raise log.fail(
|
|
193
|
+
stage, name,
|
|
194
|
+
expected=expected_defer,
|
|
195
|
+
actual=defer_reasons,
|
|
196
|
+
cause="defer-reason bucket counts diverged",
|
|
197
|
+
)
|
|
198
|
+
log.passed(stage, name, detail=f"counts={counts} defer_reasons={defer_reasons}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Stage 2: audit decision counts
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def stage_audit_counts(project_root: Path, log: AssertLog) -> None:
|
|
207
|
+
stage = 2
|
|
208
|
+
name = "audit decision counts"
|
|
209
|
+
audit_log = project_root / "vbrief" / ".eval" / "candidates.jsonl"
|
|
210
|
+
proc = run_script(
|
|
211
|
+
"triage_queue.py",
|
|
212
|
+
"audit",
|
|
213
|
+
"--project-root", str(project_root),
|
|
214
|
+
"--repo", FIXTURE_REPO,
|
|
215
|
+
"--audit-log", str(audit_log),
|
|
216
|
+
"--format=json",
|
|
217
|
+
project_root=project_root,
|
|
218
|
+
)
|
|
219
|
+
if proc.returncode != 0:
|
|
220
|
+
raise log.fail(
|
|
221
|
+
stage, name,
|
|
222
|
+
expected="exit 0",
|
|
223
|
+
actual=f"exit {proc.returncode}",
|
|
224
|
+
cause="triage_queue.py audit failed: " + proc.stderr.strip()[:200],
|
|
225
|
+
)
|
|
226
|
+
try:
|
|
227
|
+
payload = json.loads(proc.stdout)
|
|
228
|
+
except json.JSONDecodeError as exc:
|
|
229
|
+
raise log.fail(
|
|
230
|
+
stage, name,
|
|
231
|
+
expected="JSON envelope",
|
|
232
|
+
actual=proc.stdout[:200],
|
|
233
|
+
cause=f"JSON decode error: {exc}",
|
|
234
|
+
) from exc
|
|
235
|
+
|
|
236
|
+
entries = payload.get("entries") if isinstance(payload, dict) else None
|
|
237
|
+
if not isinstance(entries, list):
|
|
238
|
+
raise log.fail(
|
|
239
|
+
stage, name,
|
|
240
|
+
expected="dict with entries[]",
|
|
241
|
+
actual=type(payload).__name__,
|
|
242
|
+
cause="audit JSON envelope shape unexpected",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
by_decision: dict[str, int] = {}
|
|
246
|
+
for entry in entries:
|
|
247
|
+
if isinstance(entry, dict):
|
|
248
|
+
decision = entry.get("decision")
|
|
249
|
+
if isinstance(decision, str):
|
|
250
|
+
by_decision[decision] = by_decision.get(decision, 0) + 1
|
|
251
|
+
|
|
252
|
+
expected = {"accept": 1, "defer": 7}
|
|
253
|
+
actual_subset = {k: by_decision.get(k, 0) for k in expected}
|
|
254
|
+
if actual_subset != expected:
|
|
255
|
+
raise log.fail(
|
|
256
|
+
stage, name,
|
|
257
|
+
expected=expected,
|
|
258
|
+
actual=actual_subset,
|
|
259
|
+
cause="audit-log decision counts diverged from stage-1 writes",
|
|
260
|
+
)
|
|
261
|
+
log.passed(
|
|
262
|
+
stage, name, detail=f"entries={len(entries)} by_decision={by_decision}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Stage 3: queue determinism + untriaged visibility
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def stage_queue_determinism(project_root: Path, log: AssertLog) -> None:
|
|
272
|
+
stage = 3
|
|
273
|
+
name = "queue ranking determinism"
|
|
274
|
+
|
|
275
|
+
audit_log = project_root / "vbrief" / ".eval" / "candidates.jsonl"
|
|
276
|
+
|
|
277
|
+
def _run_queue() -> str:
|
|
278
|
+
proc = run_script(
|
|
279
|
+
"triage_queue.py",
|
|
280
|
+
"queue",
|
|
281
|
+
"--project-root", str(project_root),
|
|
282
|
+
"--repo", FIXTURE_REPO,
|
|
283
|
+
"--audit-log", str(audit_log),
|
|
284
|
+
"--limit", "20",
|
|
285
|
+
project_root=project_root,
|
|
286
|
+
)
|
|
287
|
+
if proc.returncode != 0:
|
|
288
|
+
raise log.fail(
|
|
289
|
+
stage, name,
|
|
290
|
+
expected="exit 0",
|
|
291
|
+
actual=f"exit {proc.returncode}",
|
|
292
|
+
cause="triage_queue.py queue failed: " + proc.stderr.strip()[:200],
|
|
293
|
+
)
|
|
294
|
+
return proc.stdout
|
|
295
|
+
|
|
296
|
+
out1 = _run_queue()
|
|
297
|
+
out2 = _run_queue()
|
|
298
|
+
if out1 != out2:
|
|
299
|
+
raise log.fail(
|
|
300
|
+
stage, name,
|
|
301
|
+
expected="identical stdout across two runs",
|
|
302
|
+
actual="stdout diverged on second run",
|
|
303
|
+
cause="ranking non-deterministic",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
expected_untriaged = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
|
|
307
|
+
missing = sorted(n for n in expected_untriaged if f"#{n}" not in out1)
|
|
308
|
+
if missing:
|
|
309
|
+
raise log.fail(
|
|
310
|
+
stage, name,
|
|
311
|
+
expected="all 12 untriaged numbers visible",
|
|
312
|
+
actual=f"missing: {missing}",
|
|
313
|
+
cause="queue rendering dropped untriaged rows",
|
|
314
|
+
)
|
|
315
|
+
log.passed(stage, name, detail=f"chars={len(out1)} stable across runs")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
# Stage 4: defer with resume-on
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def stage_defer_resume_on(project_root: Path, log: AssertLog) -> str:
|
|
324
|
+
"""Defer issue #5 with a past-date resume-on; assert the audit entry.
|
|
325
|
+
|
|
326
|
+
Runs in-process (not via the CLI subprocess) because ``triage_actions.defer``
|
|
327
|
+
writes through ``candidates_log.append(entry)`` without a path override,
|
|
328
|
+
which would resolve to ``candidates_log.DEFAULT_LOG_PATH`` -- the deft
|
|
329
|
+
framework's own audit log, not the smoketest's tmpdir. The smoketest
|
|
330
|
+
builds the same audit entry that ``triage_actions.defer`` would build
|
|
331
|
+
(via ``triage_actions._build_entry``) and appends it to the tmpdir's
|
|
332
|
+
candidates.jsonl via the explicit ``path=`` override. Hermetic, exercises
|
|
333
|
+
the same entry shape, and avoids leaking writes into the framework tree.
|
|
334
|
+
"""
|
|
335
|
+
stage = 4
|
|
336
|
+
name = "defer with resume-on"
|
|
337
|
+
|
|
338
|
+
import triage_actions # noqa: PLC0415
|
|
339
|
+
from candidates_log import append as audit_append # noqa: PLC0415
|
|
340
|
+
|
|
341
|
+
audit_path = project_root / "vbrief" / ".eval" / "candidates.jsonl"
|
|
342
|
+
entry = triage_actions._build_entry(
|
|
343
|
+
"defer",
|
|
344
|
+
5,
|
|
345
|
+
FIXTURE_REPO,
|
|
346
|
+
actor="agent:smoketest",
|
|
347
|
+
reason="smoketest defer w/ resume-on",
|
|
348
|
+
resume_on="date:>=2020-01-01",
|
|
349
|
+
)
|
|
350
|
+
audit_append(entry, path=audit_path)
|
|
351
|
+
decision_id: str | None = None
|
|
352
|
+
resume_on: str | None = None
|
|
353
|
+
for line in audit_path.read_text(encoding="utf-8").splitlines():
|
|
354
|
+
try:
|
|
355
|
+
entry = json.loads(line)
|
|
356
|
+
except json.JSONDecodeError:
|
|
357
|
+
continue
|
|
358
|
+
if (
|
|
359
|
+
entry.get("issue_number") == 5
|
|
360
|
+
and entry.get("decision") == "defer"
|
|
361
|
+
and entry.get("actor") == "agent:smoketest"
|
|
362
|
+
):
|
|
363
|
+
decision_id = entry.get("decision_id")
|
|
364
|
+
resume_on = entry.get("resume_on")
|
|
365
|
+
|
|
366
|
+
if decision_id is None:
|
|
367
|
+
raise log.fail(
|
|
368
|
+
stage, name,
|
|
369
|
+
expected="defer audit entry for issue 5",
|
|
370
|
+
actual="no smoketest defer entry",
|
|
371
|
+
cause="triage_actions.py defer did not write the audit entry",
|
|
372
|
+
)
|
|
373
|
+
if resume_on != "date:>=2020-01-01":
|
|
374
|
+
raise log.fail(
|
|
375
|
+
stage, name,
|
|
376
|
+
expected="resume_on=date:>=2020-01-01",
|
|
377
|
+
actual=str(resume_on),
|
|
378
|
+
cause="resume_on field absent or mismatched",
|
|
379
|
+
)
|
|
380
|
+
log.passed(stage, name, detail=f"decision_id={decision_id}")
|
|
381
|
+
return decision_id
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
# Stage 5: evaluate-resume marker
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def stage_evaluate_resume(
|
|
390
|
+
project_root: Path, prior_defer_id: str, log: AssertLog
|
|
391
|
+
) -> None:
|
|
392
|
+
stage = 5
|
|
393
|
+
name = "evaluate-resume marker"
|
|
394
|
+
audit_log = project_root / "vbrief" / ".eval" / "candidates.jsonl"
|
|
395
|
+
proc = run_script(
|
|
396
|
+
"triage_queue.py",
|
|
397
|
+
"audit",
|
|
398
|
+
"--project-root", str(project_root),
|
|
399
|
+
"--repo", FIXTURE_REPO,
|
|
400
|
+
"--audit-log", str(audit_log),
|
|
401
|
+
"--evaluate-resume",
|
|
402
|
+
"--format=json",
|
|
403
|
+
project_root=project_root,
|
|
404
|
+
)
|
|
405
|
+
if proc.returncode != 0:
|
|
406
|
+
raise log.fail(
|
|
407
|
+
stage, name,
|
|
408
|
+
expected="exit 0",
|
|
409
|
+
actual=f"exit {proc.returncode}",
|
|
410
|
+
cause="audit --evaluate-resume failed: " + proc.stderr.strip()[:200],
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
audit_path = project_root / "vbrief" / ".eval" / "candidates.jsonl"
|
|
414
|
+
found: dict[str, Any] | None = None
|
|
415
|
+
for line in audit_path.read_text(encoding="utf-8").splitlines():
|
|
416
|
+
try:
|
|
417
|
+
entry = json.loads(line)
|
|
418
|
+
except json.JSONDecodeError:
|
|
419
|
+
continue
|
|
420
|
+
if (
|
|
421
|
+
entry.get("decision") == "resume-eligible"
|
|
422
|
+
and entry.get("prior_decision_id") == prior_defer_id
|
|
423
|
+
):
|
|
424
|
+
found = entry
|
|
425
|
+
break
|
|
426
|
+
if found is None:
|
|
427
|
+
raise log.fail(
|
|
428
|
+
stage, name,
|
|
429
|
+
expected=f"resume-eligible entry referencing {prior_defer_id}",
|
|
430
|
+
actual="no resume-eligible entry written",
|
|
431
|
+
cause="evaluate-resume did not fire on date:>=2020-01-01",
|
|
432
|
+
)
|
|
433
|
+
log.passed(stage, name, detail=f"decision_id={found.get('decision_id')}")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
# Stage 6: scope:promote (D18 fallback)
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def stage_scope_promote(project_root: Path, log: AssertLog) -> Path:
|
|
442
|
+
"""Promote ``vbrief/proposed/test-1.vbrief.json`` to pending/.
|
|
443
|
+
|
|
444
|
+
NOTE: D18 / #1136 (``scope:promote --from-issue=<N>``) is OPEN-but-
|
|
445
|
+
not-implemented at this commit. The smoketest uses the existing
|
|
446
|
+
``scope:promote <file>`` form per the orchestrator's fallback note;
|
|
447
|
+
see the PR body for the future-integration link.
|
|
448
|
+
"""
|
|
449
|
+
stage = 6
|
|
450
|
+
name = "scope:promote (D18 fallback)"
|
|
451
|
+
proposed = project_root / "vbrief" / "proposed" / "test-1.vbrief.json"
|
|
452
|
+
if not proposed.is_file():
|
|
453
|
+
raise log.fail(
|
|
454
|
+
stage, name,
|
|
455
|
+
expected="vbrief/proposed/test-1.vbrief.json",
|
|
456
|
+
actual="missing",
|
|
457
|
+
cause="fixture copy did not place test-1.vbrief.json",
|
|
458
|
+
)
|
|
459
|
+
proc = run_script(
|
|
460
|
+
"scope_lifecycle.py",
|
|
461
|
+
"promote",
|
|
462
|
+
str(proposed),
|
|
463
|
+
"--project-root", str(project_root),
|
|
464
|
+
"--force", # framework worktrees inherit a 60-vBRIEF WIP overage
|
|
465
|
+
project_root=project_root,
|
|
466
|
+
)
|
|
467
|
+
if proc.returncode != 0:
|
|
468
|
+
raise log.fail(
|
|
469
|
+
stage, name,
|
|
470
|
+
expected="exit 0",
|
|
471
|
+
actual=f"exit {proc.returncode}",
|
|
472
|
+
cause="scope_lifecycle.py promote failed: " + proc.stderr.strip()[:200],
|
|
473
|
+
)
|
|
474
|
+
pending = project_root / "vbrief" / "pending" / "test-1.vbrief.json"
|
|
475
|
+
if not pending.is_file():
|
|
476
|
+
raise log.fail(
|
|
477
|
+
stage, name,
|
|
478
|
+
expected="vbrief/pending/test-1.vbrief.json",
|
|
479
|
+
actual="file not in pending/",
|
|
480
|
+
cause="scope:promote did not move the file",
|
|
481
|
+
)
|
|
482
|
+
data = json.loads(pending.read_text(encoding="utf-8"))
|
|
483
|
+
if data.get("plan", {}).get("status") != "pending":
|
|
484
|
+
raise log.fail(
|
|
485
|
+
stage, name,
|
|
486
|
+
expected="plan.status=pending",
|
|
487
|
+
actual=str(data.get("plan", {}).get("status")),
|
|
488
|
+
cause="plan.status not flipped to pending",
|
|
489
|
+
)
|
|
490
|
+
log.passed(stage, name, detail="proposed -> pending OK")
|
|
491
|
+
return pending
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ---------------------------------------------------------------------------
|
|
495
|
+
# Stage 7: scope:demote single-file
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def stage_scope_demote(project_root: Path, pending: Path, log: AssertLog) -> None:
|
|
500
|
+
stage = 7
|
|
501
|
+
name = "scope:demote single-file"
|
|
502
|
+
reason = "smoketest single demote"
|
|
503
|
+
proc = run_script(
|
|
504
|
+
"scope_demote.py",
|
|
505
|
+
str(pending),
|
|
506
|
+
"--reason", reason,
|
|
507
|
+
"--actor", "agent:smoketest",
|
|
508
|
+
"--project-root", str(project_root),
|
|
509
|
+
project_root=project_root,
|
|
510
|
+
)
|
|
511
|
+
if proc.returncode != 0:
|
|
512
|
+
raise log.fail(
|
|
513
|
+
stage, name,
|
|
514
|
+
expected="exit 0",
|
|
515
|
+
actual=f"exit {proc.returncode}",
|
|
516
|
+
cause="scope_demote.py failed: " + proc.stderr.strip()[:200],
|
|
517
|
+
)
|
|
518
|
+
proposed = project_root / "vbrief" / "proposed" / "test-1.vbrief.json"
|
|
519
|
+
if not proposed.is_file():
|
|
520
|
+
raise log.fail(
|
|
521
|
+
stage, name,
|
|
522
|
+
expected="file back in proposed/",
|
|
523
|
+
actual="missing",
|
|
524
|
+
cause="scope:demote did not move file back",
|
|
525
|
+
)
|
|
526
|
+
log_path = project_root / "vbrief" / ".eval" / "scope-lifecycle.jsonl"
|
|
527
|
+
last_demote: dict[str, Any] | None = None
|
|
528
|
+
if log_path.is_file():
|
|
529
|
+
for line in log_path.read_text(encoding="utf-8").splitlines():
|
|
530
|
+
try:
|
|
531
|
+
entry = json.loads(line)
|
|
532
|
+
except json.JSONDecodeError:
|
|
533
|
+
continue
|
|
534
|
+
if (
|
|
535
|
+
entry.get("action") == "demote"
|
|
536
|
+
and entry.get("actor") == "agent:smoketest"
|
|
537
|
+
):
|
|
538
|
+
last_demote = entry
|
|
539
|
+
if last_demote is None:
|
|
540
|
+
raise log.fail(
|
|
541
|
+
stage, name,
|
|
542
|
+
expected="demote audit entry from smoketest",
|
|
543
|
+
actual="no matching entry",
|
|
544
|
+
cause="scope:demote audit entry missing",
|
|
545
|
+
)
|
|
546
|
+
meta = last_demote.get("demote_meta") or {}
|
|
547
|
+
if meta.get("demoted_from") != "pending":
|
|
548
|
+
raise log.fail(
|
|
549
|
+
stage, name,
|
|
550
|
+
expected="demote_meta.demoted_from=pending",
|
|
551
|
+
actual=meta.get("demoted_from"),
|
|
552
|
+
cause="demote_meta.demoted_from mismatched",
|
|
553
|
+
)
|
|
554
|
+
if meta.get("demote_reason") != reason:
|
|
555
|
+
raise log.fail(
|
|
556
|
+
stage, name,
|
|
557
|
+
expected=f"demote_meta.demote_reason={reason!r}",
|
|
558
|
+
actual=meta.get("demote_reason"),
|
|
559
|
+
cause="demote_meta.demote_reason mismatched",
|
|
560
|
+
)
|
|
561
|
+
log.passed(
|
|
562
|
+
stage, name,
|
|
563
|
+
detail=(
|
|
564
|
+
f"demoted_from={meta.get('demoted_from')} "
|
|
565
|
+
f"days_in_pending={meta.get('days_in_pending')}"
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ---------------------------------------------------------------------------
|
|
571
|
+
# Stage 8: scope:undo (graceful skip when D15 / #1134 absent)
|
|
572
|
+
# ---------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def stage_scope_undo(project_root: Path, log: AssertLog) -> None:
|
|
576
|
+
stage = 8
|
|
577
|
+
name = "scope:undo idempotency"
|
|
578
|
+
candidate = _SCRIPTS_DIR / "scope_undo.py"
|
|
579
|
+
if not candidate.is_file():
|
|
580
|
+
sys.stderr.write(
|
|
581
|
+
"[triage:smoketest] D15 / #1134 (scope:undo) has not landed yet; "
|
|
582
|
+
"skipping stage 8 with informational stderr per the orchestrator's "
|
|
583
|
+
"graceful-skip rule.\n"
|
|
584
|
+
)
|
|
585
|
+
log.skipped(stage, name, reason="D15 / #1134 not yet merged")
|
|
586
|
+
return
|
|
587
|
+
proc = run_script( # pragma: no cover -- exercised once D15 lands
|
|
588
|
+
"scope_undo.py",
|
|
589
|
+
"--latest",
|
|
590
|
+
"--project-root", str(project_root),
|
|
591
|
+
project_root=project_root,
|
|
592
|
+
)
|
|
593
|
+
if proc.returncode != 0:
|
|
594
|
+
raise log.fail(
|
|
595
|
+
stage, name,
|
|
596
|
+
expected="exit 0",
|
|
597
|
+
actual=f"exit {proc.returncode}",
|
|
598
|
+
cause="scope_undo.py failed: " + proc.stderr.strip()[:200],
|
|
599
|
+
)
|
|
600
|
+
log.passed(stage, name, detail="undo recorded")
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ---------------------------------------------------------------------------
|
|
604
|
+
# Stage 9: triage:summary bounded output
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def stage_triage_summary(project_root: Path, log: AssertLog) -> None:
|
|
609
|
+
stage = 9
|
|
610
|
+
name = "triage:summary bounded output"
|
|
611
|
+
proc = run_script(
|
|
612
|
+
"triage_summary.py",
|
|
613
|
+
"--project-root", str(project_root),
|
|
614
|
+
"--no-history",
|
|
615
|
+
project_root=project_root,
|
|
616
|
+
)
|
|
617
|
+
if proc.returncode != 0:
|
|
618
|
+
raise log.fail(
|
|
619
|
+
stage, name,
|
|
620
|
+
expected="exit 0",
|
|
621
|
+
actual=f"exit {proc.returncode}",
|
|
622
|
+
cause="triage_summary.py failed: " + proc.stderr.strip()[:200],
|
|
623
|
+
)
|
|
624
|
+
out = proc.stdout.strip()
|
|
625
|
+
lines = [line for line in out.splitlines() if line.strip()]
|
|
626
|
+
if not lines:
|
|
627
|
+
raise log.fail(
|
|
628
|
+
stage, name,
|
|
629
|
+
expected="at least the bounded headline",
|
|
630
|
+
actual="no output",
|
|
631
|
+
cause="triage:summary emitted nothing",
|
|
632
|
+
)
|
|
633
|
+
# #1122 bounds the HEADLINE (the first physical line). #1270
|
|
634
|
+
# ([triage:scope]) and #1468 ([triage:reconcile]) add intentional
|
|
635
|
+
# informational lines BELOW the headline; the fixture has a proposed
|
|
636
|
+
# vBRIEF (test-1, issue #1) with no audit decision, which is a
|
|
637
|
+
# legitimate reconcile divergence, so the summary correctly emits a
|
|
638
|
+
# second line. Validate the bounded headline and assert any extra
|
|
639
|
+
# lines are ONLY the recognized informational divergence/hint lines
|
|
640
|
+
# (genuine multi-line garbage still fails).
|
|
641
|
+
headline = lines[0]
|
|
642
|
+
extra_lines = lines[1:]
|
|
643
|
+
unexpected = [
|
|
644
|
+
ln
|
|
645
|
+
for ln in extra_lines
|
|
646
|
+
if not ln.startswith(("[triage:scope]", "[triage:reconcile]"))
|
|
647
|
+
]
|
|
648
|
+
if unexpected:
|
|
649
|
+
raise log.fail(
|
|
650
|
+
stage, name,
|
|
651
|
+
expected=(
|
|
652
|
+
"only [triage:scope] / [triage:reconcile] informational "
|
|
653
|
+
"lines below the headline"
|
|
654
|
+
),
|
|
655
|
+
actual=f"{len(unexpected)} unexpected extra line(s): {unexpected[0][:60]!r}",
|
|
656
|
+
cause="unexpected multi-line output",
|
|
657
|
+
)
|
|
658
|
+
if len(headline) > SUMMARY_MAX_CHARS:
|
|
659
|
+
raise log.fail(
|
|
660
|
+
stage, name,
|
|
661
|
+
expected=f"<= {SUMMARY_MAX_CHARS} chars",
|
|
662
|
+
actual=f"{len(headline)} chars",
|
|
663
|
+
cause="exceeded MAX_LINE_CHARS budget",
|
|
664
|
+
)
|
|
665
|
+
if WARN_GLYPH in headline:
|
|
666
|
+
raise log.fail(
|
|
667
|
+
stage, name,
|
|
668
|
+
expected="no warning glyph (WIP under cap)",
|
|
669
|
+
actual="warning glyph U+26A0 present",
|
|
670
|
+
cause="emitted U+26A0 against under-cap fixture",
|
|
671
|
+
)
|
|
672
|
+
log.passed(
|
|
673
|
+
stage, name, detail=f"chars={len(headline)} extra_lines={len(extra_lines)}"
|
|
674
|
+
)
|