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