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