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