@deftai/directive-content 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.githooks/pre-commit +143 -0
  2. package/.githooks/pre-push +121 -0
  3. package/QUICK-START.md +13 -3
  4. package/Taskfile.yml +934 -0
  5. package/UPGRADING.md +82 -11
  6. package/events/README.md +3 -3
  7. package/package.json +5 -4
  8. package/packs/skills/skills-pack-0.1.json +22 -22
  9. package/scripts/_agents_md.py +494 -0
  10. package/scripts/_cache_fetch.py +635 -0
  11. package/scripts/_cache_quota.py +529 -0
  12. package/scripts/_cache_refresh.py +163 -0
  13. package/scripts/_cache_validate.py +209 -0
  14. package/scripts/_content_root.py +42 -0
  15. package/scripts/_doctor_state.py +277 -0
  16. package/scripts/_event_detect.py +305 -0
  17. package/scripts/_events.py +514 -0
  18. package/scripts/_lifecycle_hygiene.py +568 -0
  19. package/scripts/_pathspec.py +91 -0
  20. package/scripts/_policy_show_cli.py +266 -0
  21. package/scripts/_precutover.py +92 -0
  22. package/scripts/_project_context.py +224 -0
  23. package/scripts/_project_definition_io.py +164 -0
  24. package/scripts/_relocate_snapshot.py +209 -0
  25. package/scripts/_relocate_states.py +343 -0
  26. package/scripts/_resolve_preflight_path.py +152 -0
  27. package/scripts/_safe_subprocess.py +167 -0
  28. package/scripts/_session_start_hook.py +205 -0
  29. package/scripts/_sor_gate_diff.py +365 -0
  30. package/scripts/_stdio_utf8.py +59 -0
  31. package/scripts/_triage_bootstrap_gitignore.py +904 -0
  32. package/scripts/_triage_classify_cli.py +122 -0
  33. package/scripts/_triage_queue_cli.py +625 -0
  34. package/scripts/_triage_scope_cli.py +343 -0
  35. package/scripts/_triage_scope_drift_cli.py +121 -0
  36. package/scripts/_triage_scope_ignores.py +286 -0
  37. package/scripts/_triage_scope_milestone.py +432 -0
  38. package/scripts/_triage_scope_mutations.py +337 -0
  39. package/scripts/_triage_scope_renderers.py +207 -0
  40. package/scripts/_triage_smoketest_stages.py +674 -0
  41. package/scripts/_triage_subscribe_cli.py +140 -0
  42. package/scripts/_triage_welcome_cli.py +421 -0
  43. package/scripts/_vbrief_build.py +239 -0
  44. package/scripts/_vbrief_fidelity.py +479 -0
  45. package/scripts/_vbrief_legacy.py +589 -0
  46. package/scripts/_vbrief_reconciliation.py +883 -0
  47. package/scripts/_vbrief_routing.py +277 -0
  48. package/scripts/_vbrief_safety.py +778 -0
  49. package/scripts/_vbrief_sources.py +312 -0
  50. package/scripts/_vbrief_speckit.py +262 -0
  51. package/scripts/_vbrief_story_quality.py +353 -0
  52. package/scripts/_vbrief_validation.py +299 -0
  53. package/scripts/build_dist.py +412 -0
  54. package/scripts/cache.py +1078 -0
  55. package/scripts/cache_scanner.py +745 -0
  56. package/scripts/candidates_log.py +432 -0
  57. package/scripts/capacity_backfill.py +680 -0
  58. package/scripts/capacity_show.py +653 -0
  59. package/scripts/ci_local.py +689 -0
  60. package/scripts/code_structure_validate.py +765 -0
  61. package/scripts/codebase_default_extractor.py +495 -0
  62. package/scripts/codebase_map.py +304 -0
  63. package/scripts/codebase_map_fresh.py +104 -0
  64. package/scripts/codebase_projection_registry.py +94 -0
  65. package/scripts/codebase_provider.py +582 -0
  66. package/scripts/doctor.py +2257 -0
  67. package/scripts/framework_commands.py +505 -0
  68. package/scripts/gh_rest.py +882 -0
  69. package/scripts/github_auth_modes.py +437 -0
  70. package/scripts/github_body.py +292 -0
  71. package/scripts/ip_risk.py +531 -0
  72. package/scripts/issue_emit.py +670 -0
  73. package/scripts/issue_ingest.py +1064 -0
  74. package/scripts/migrate_preflight.py +418 -0
  75. package/scripts/migrate_vbrief.py +2677 -0
  76. package/scripts/monitor_pr.py +401 -0
  77. package/scripts/pack_migrate_lessons.py +336 -0
  78. package/scripts/pack_migrate_patterns.py +254 -0
  79. package/scripts/pack_migrate_rules.py +350 -0
  80. package/scripts/pack_migrate_skills.py +423 -0
  81. package/scripts/pack_migrate_strategies.py +311 -0
  82. package/scripts/pack_migrate_swarm_spec.py +250 -0
  83. package/scripts/pack_render.py +434 -0
  84. package/scripts/packs_slice.py +712 -0
  85. package/scripts/platform_capabilities.py +336 -0
  86. package/scripts/policy.py +2826 -0
  87. package/scripts/policy_set.py +324 -0
  88. package/scripts/pr_check_closing_keywords.py +524 -0
  89. package/scripts/pr_check_protected_issues.py +267 -0
  90. package/scripts/pr_merge_readiness.py +1004 -0
  91. package/scripts/pr_wait_mergeable.py +669 -0
  92. package/scripts/prd_render.py +159 -0
  93. package/scripts/preflight_architecture_sor.py +974 -0
  94. package/scripts/preflight_branch.py +289 -0
  95. package/scripts/preflight_cache.py +974 -0
  96. package/scripts/preflight_gh.py +721 -0
  97. package/scripts/preflight_implementation.py +272 -0
  98. package/scripts/preflight_story_start.py +838 -0
  99. package/scripts/preflight_wip_cap.py +149 -0
  100. package/scripts/probe_session.py +545 -0
  101. package/scripts/project_render.py +293 -0
  102. package/scripts/quarantine_ext.py +237 -0
  103. package/scripts/reconcile_issues.py +1442 -0
  104. package/scripts/refresh-path.ps1 +107 -0
  105. package/scripts/release.py +2030 -0
  106. package/scripts/release_e2e.py +1011 -0
  107. package/scripts/release_publish.py +486 -0
  108. package/scripts/release_rollback.py +980 -0
  109. package/scripts/relocate.py +1034 -0
  110. package/scripts/resolve_changelog_unreleased.py +667 -0
  111. package/scripts/resolve_version.py +490 -0
  112. package/scripts/resume_conditions.py +706 -0
  113. package/scripts/ritual_sentinel.py +609 -0
  114. package/scripts/roadmap_render.py +635 -0
  115. package/scripts/rule_ownership_lint.py +325 -0
  116. package/scripts/scm.py +591 -0
  117. package/scripts/scope_audit_log.py +387 -0
  118. package/scripts/scope_decompose.py +654 -0
  119. package/scripts/scope_demote.py +509 -0
  120. package/scripts/scope_lifecycle.py +1126 -0
  121. package/scripts/scope_undo.py +772 -0
  122. package/scripts/session_start.py +406 -0
  123. package/scripts/setup_ghx.py +339 -0
  124. package/scripts/setup_windows.ps1 +220 -0
  125. package/scripts/slice_audit.py +585 -0
  126. package/scripts/slice_record.py +530 -0
  127. package/scripts/slice_record_existing.py +692 -0
  128. package/scripts/slug_normalize.py +178 -0
  129. package/scripts/spec_render.py +477 -0
  130. package/scripts/spec_validate.py +238 -0
  131. package/scripts/subagent_monitor.py +658 -0
  132. package/scripts/swarm_complete_cohort.py +644 -0
  133. package/scripts/swarm_launch.py +1206 -0
  134. package/scripts/swarm_readiness.py +554 -0
  135. package/scripts/swarm_verify_review_clean.py +438 -0
  136. package/scripts/swarm_worktrees.py +497 -0
  137. package/scripts/toolchain-check.py +52 -0
  138. package/scripts/triage_actions.py +871 -0
  139. package/scripts/triage_bootstrap.py +1153 -0
  140. package/scripts/triage_bulk.py +630 -0
  141. package/scripts/triage_classify.py +932 -0
  142. package/scripts/triage_help.py +1685 -0
  143. package/scripts/triage_queue.py +1944 -0
  144. package/scripts/triage_reconcile.py +581 -0
  145. package/scripts/triage_refresh.py +643 -0
  146. package/scripts/triage_scope.py +999 -0
  147. package/scripts/triage_scope_drift.py +575 -0
  148. package/scripts/triage_smoketest.py +396 -0
  149. package/scripts/triage_subscribe.py +399 -0
  150. package/scripts/triage_summary.py +1011 -0
  151. package/scripts/triage_welcome.py +1178 -0
  152. package/scripts/ts_check_lane.py +86 -0
  153. package/scripts/validate-links.py +64 -0
  154. package/scripts/validate_strategy_output.py +212 -0
  155. package/scripts/vbrief_activate.py +228 -0
  156. package/scripts/vbrief_migrate_conformance.py +368 -0
  157. package/scripts/vbrief_reconcile_graph.py +306 -0
  158. package/scripts/vbrief_reconcile_labels.py +460 -0
  159. package/scripts/vbrief_reconcile_umbrellas.py +741 -0
  160. package/scripts/vbrief_validate.py +1195 -0
  161. package/scripts/verify-stubs.py +61 -0
  162. package/scripts/verify_capacity.py +160 -0
  163. package/scripts/verify_encoding.py +699 -0
  164. package/scripts/verify_hooks_installed.py +206 -0
  165. package/scripts/verify_investigation.py +360 -0
  166. package/scripts/verify_judgment_gates.py +827 -0
  167. package/scripts/verify_no_task_runtime.py +171 -0
  168. package/scripts/verify_scm_boundary.py +509 -0
  169. package/scripts/verify_session_ritual.py +389 -0
  170. package/scripts/verify_tools.py +426 -0
  171. package/scripts/verify_vbrief_conformance.py +478 -0
  172. package/skills/deft-directive-swarm/SKILL.md +7 -26
  173. package/skills/deft-directive-sync/SKILL.md +1 -1
  174. package/tasks/architecture.yml +13 -0
  175. package/tasks/cache.yml +69 -0
  176. package/tasks/capacity.yml +38 -0
  177. package/tasks/change.yml +46 -0
  178. package/tasks/changelog.yml +24 -0
  179. package/tasks/ci.yml +49 -0
  180. package/tasks/codebase.yml +47 -0
  181. package/tasks/commit.yml +30 -0
  182. package/tasks/core.yml +126 -0
  183. package/tasks/deployments.yml +54 -0
  184. package/tasks/framework.yml +74 -0
  185. package/tasks/install.yml +60 -0
  186. package/tasks/issue.yml +50 -0
  187. package/tasks/migrate.yml +73 -0
  188. package/tasks/packs.yml +92 -0
  189. package/tasks/policy.yml +75 -0
  190. package/tasks/pr.yml +89 -0
  191. package/tasks/prd.yml +39 -0
  192. package/tasks/project.yml +27 -0
  193. package/tasks/reconcile.yml +32 -0
  194. package/tasks/relocate.yml +56 -0
  195. package/tasks/roadmap.yml +28 -0
  196. package/tasks/scm.yml +126 -0
  197. package/tasks/scope-undo.yml +36 -0
  198. package/tasks/scope.yml +141 -0
  199. package/tasks/session.yml +19 -0
  200. package/tasks/setup.yml +37 -0
  201. package/tasks/slice.yml +69 -0
  202. package/tasks/spec.yml +41 -0
  203. package/tasks/swarm.yml +85 -0
  204. package/tasks/toolchain.yml +13 -0
  205. package/tasks/triage-actions.yml +94 -0
  206. package/tasks/triage-bootstrap.yml +43 -0
  207. package/tasks/triage-bulk.yml +75 -0
  208. package/tasks/triage-classify.yml +30 -0
  209. package/tasks/triage-queue.yml +50 -0
  210. package/tasks/triage-reconcile.yml +29 -0
  211. package/tasks/triage-scope-drift.yml +29 -0
  212. package/tasks/triage-scope.yml +31 -0
  213. package/tasks/triage-smoketest.yml +33 -0
  214. package/tasks/triage-subscribe.yml +36 -0
  215. package/tasks/triage-summary.yml +29 -0
  216. package/tasks/triage-welcome.yml +32 -0
  217. package/tasks/ts.yml +328 -0
  218. package/tasks/vbrief.yml +206 -0
  219. package/tasks/verify.yml +292 -0
  220. package/templates/agents-entry.md +2 -2
@@ -0,0 +1,277 @@
1
+ """Lifecycle folder routing for reconciled scope items (Agent B, #499).
2
+
3
+ Single source of truth for the lifecycle <-> status mapping used by
4
+ ``migrate:vbrief``. The mapping mirrors the authoritative table in master
5
+ tracking issue #506 (Shared conventions) and the schema vocabulary in
6
+ ``vbrief/schemas/vbrief-core.schema.json``:
7
+
8
+ proposed/ <-> draft | proposed
9
+ pending/ <-> approved | pending
10
+ active/ <-> running | blocked
11
+ completed/ <-> completed
12
+ cancelled/ <-> cancelled
13
+
14
+ The migrator MUST NOT emit the legacy value ``in_progress`` -- this was the
15
+ critical correction to the original #499 issue body. Use ``running``.
16
+
17
+ Exposes:
18
+ * FOLDER_TO_STATUSES / STATUS_TO_FOLDER
19
+ * folder_for_status(status) -> folder
20
+ * default_status_for_folder(folder) -> status
21
+ * plan_status_matches_folder(status, folder) -> bool
22
+ * build_scope_vbrief_from_reconciled(reconciled, repo_url) -> dict
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import sys
28
+ from datetime import UTC, datetime
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ # Make the sibling ``_vbrief_build`` helper importable whether this module is
33
+ # imported as part of the ``scripts/`` package layout or as a top-level module.
34
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
35
+
36
+ from _vbrief_build import ( # noqa: E402
37
+ MIGRATOR_METADATA_KEY as _MIGRATOR_METADATA_KEY,
38
+ create_scope_vbrief as _create_scope_vbrief,
39
+ )
40
+
41
+
42
+ def _migration_timestamp() -> str:
43
+ """Return an ISO-8601 UTC timestamp for ``vBRIEFInfo.updated`` stamps.
44
+
45
+ Emitted at second precision. This helper is ONLY the fallback used when
46
+ ``build_scope_vbrief_from_reconciled(..., migration_timestamp=None)`` is
47
+ called directly. Under the normal ``migrate()`` entry point the caller
48
+ always passes ``migration_timestamp=migrate_vbrief._MIGRATION_TIMESTAMP``
49
+ (a module-level constant stamped once per migration run), so this helper
50
+ is effectively unreachable there.
51
+
52
+ Test-pinning knob: deterministic migrate() tests (for example the
53
+ byte-for-byte golden-fixture suite in ``test_migrate_vbrief.py``) MUST
54
+ monkeypatch ``migrate_vbrief._MIGRATION_TIMESTAMP`` -- NOT this helper --
55
+ because the full migrate() path always threads the module-level constant
56
+ through to ``build_scope_vbrief_from_reconciled(migration_timestamp=...)``.
57
+ Callers that invoke ``build_scope_vbrief_from_reconciled`` directly can
58
+ either pass ``migration_timestamp=`` explicitly or monkeypatch this helper.
59
+ """
60
+ return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Lifecycle <-> status mapping (#506 Shared conventions, schema-locked)
64
+ # ---------------------------------------------------------------------------
65
+
66
+ FOLDER_TO_STATUSES: dict[str, tuple[str, ...]] = {
67
+ "proposed": ("draft", "proposed"),
68
+ "pending": ("approved", "pending"),
69
+ "active": ("running", "blocked"),
70
+ "completed": ("completed",),
71
+ "cancelled": ("cancelled",),
72
+ }
73
+
74
+ STATUS_TO_FOLDER: dict[str, str] = {
75
+ status: folder
76
+ for folder, statuses in FOLDER_TO_STATUSES.items()
77
+ for status in statuses
78
+ }
79
+
80
+ LIFECYCLE_FOLDERS: tuple[str, ...] = tuple(FOLDER_TO_STATUSES.keys())
81
+
82
+ # Canonical default status the migrator emits when a folder is chosen but no
83
+ # sharper signal exists (e.g. orphans routed to proposed/ use ``proposed`` not
84
+ # ``draft``; reconciled-active with no explicit blocked signal uses ``running``).
85
+ DEFAULT_STATUS_FOR_FOLDER: dict[str, str] = {
86
+ "proposed": "proposed",
87
+ "pending": "pending",
88
+ "active": "running",
89
+ "completed": "completed",
90
+ "cancelled": "cancelled",
91
+ }
92
+
93
+
94
+ def folder_for_status(status: str) -> str:
95
+ """Return the canonical lifecycle folder for a schema status.
96
+
97
+ Raises ``ValueError`` for unknown statuses so callers can surface the
98
+ corruption early rather than silently routing to ``pending/``.
99
+ """
100
+ try:
101
+ return STATUS_TO_FOLDER[status]
102
+ except KeyError as exc: # pragma: no cover - defensive
103
+ raise ValueError(
104
+ f"No lifecycle folder defined for status {status!r}; "
105
+ f"expected one of {sorted(STATUS_TO_FOLDER)}."
106
+ ) from exc
107
+
108
+
109
+ def default_status_for_folder(folder: str) -> str:
110
+ """Return the canonical default status the migrator uses for a folder."""
111
+ try:
112
+ return DEFAULT_STATUS_FOR_FOLDER[folder]
113
+ except KeyError as exc: # pragma: no cover - defensive
114
+ raise ValueError(
115
+ f"Unknown lifecycle folder {folder!r}; expected one of "
116
+ f"{sorted(DEFAULT_STATUS_FOR_FOLDER)}."
117
+ ) from exc
118
+
119
+
120
+ def plan_status_matches_folder(status: str, folder: str) -> bool:
121
+ """Return True if ``status`` is permitted inside ``folder/`` per #506."""
122
+ return status in FOLDER_TO_STATUSES.get(folder, ())
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Scope vBRIEF construction from reconciled item
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ def _narrative_str(value: Any) -> str:
131
+ """Coerce a narrative field to a stripped string (schema requires strings)."""
132
+ if value is None:
133
+ return ""
134
+ if isinstance(value, str):
135
+ return value.strip()
136
+ return str(value).strip()
137
+
138
+
139
+ def build_scope_vbrief_from_reconciled(
140
+ reconciled: dict,
141
+ repo_url: str = "",
142
+ migration_timestamp: str | None = None,
143
+ ) -> dict:
144
+ """Build a scope vBRIEF dict from a reconciled item (#496 + #499 + #616).
145
+
146
+ ``reconciled`` is a dict with the following recognised keys (produced by
147
+ ``_vbrief_reconciliation.reconcile_scope_items``):
148
+
149
+ number, task_id, title, status, folder, description, description_source,
150
+ status_source, title_source, phase, phase_description, tier, spec_phase,
151
+ roadmap_summary, source_conflict, override_applied, references.
152
+
153
+ The output preserves the ``_create_scope_vbrief`` envelope shape that
154
+ tests already rely on. Per issue #616 (option A, scope-clamped) the
155
+ user-visible ``plan.narratives`` is left almost EMPTY on per-issue
156
+ scope vBRIEFs -- ROADMAP rows do not carry enough data to populate
157
+ any canonical narrative key meaningfully. Reconciliation provenance
158
+ (Description / Description_source / Status_source / Title_source /
159
+ SpecPhase / RoadmapSummary / SourceConflict) is relocated to
160
+ ``plan.metadata['x-migrator']`` so downstream tooling that cares
161
+ about SPEC/ROADMAP lineage can still read it without the invented
162
+ keys leaking into the user-facing summary surface.
163
+
164
+ ``SourceSection`` is the named exception to the #616 clamp: it
165
+ remains in ``plan.narratives`` because it is a deliberate,
166
+ user-visible audit-trail narrative (``ROADMAP Completed section``
167
+ vs ``ROADMAP active phase``) added by #593 so operators can audit
168
+ the routing decision post-migration without re-running the
169
+ migrator. Unlike the reconciler-internal provenance above (which
170
+ really is plumbing noise), SourceSection is intentional signal.
171
+ The Windows task-dispatch regression asserts
172
+ ``plan.narratives.SourceSection`` specifically.
173
+ """
174
+ status = reconciled.get("status") or default_status_for_folder(
175
+ reconciled.get("folder", "pending")
176
+ )
177
+
178
+ # Seed with the shared helper so origin-provenance (references) and the
179
+ # vBRIEFInfo envelope stay consistent with non-reconciled scope vBRIEFs.
180
+ seed_item = {
181
+ "number": reconciled.get("number", ""),
182
+ "title": reconciled.get("title", "Untitled"),
183
+ "phase": reconciled.get("phase", ""),
184
+ "tier": reconciled.get("tier", ""),
185
+ }
186
+ scope = _create_scope_vbrief(
187
+ seed_item,
188
+ repo_url=repo_url,
189
+ status=status,
190
+ phase_description=reconciled.get("phase_description", ""),
191
+ )
192
+
193
+ # #616: migrator provenance flows into plan.metadata['x-migrator'],
194
+ # NOT plan.narratives. The shared ``create_scope_vbrief`` helper
195
+ # already seeded Phase / Tier / PhaseDescription under the same key
196
+ # when populated; we extend that bucket with reconciler-specific
197
+ # fields so a single metadata blob captures the full lineage.
198
+ plan_meta = scope["plan"].setdefault("metadata", {})
199
+ migrator_meta = plan_meta.setdefault(_MIGRATOR_METADATA_KEY, {})
200
+
201
+ def _store(key: str, value: Any) -> None:
202
+ coerced = _narrative_str(value)
203
+ if coerced:
204
+ migrator_meta[key] = coerced
205
+
206
+ _store("Description", reconciled.get("description"))
207
+ _store("Description_source", reconciled.get("description_source"))
208
+ _store("Status_source", reconciled.get("status_source"))
209
+ _store("Title_source", reconciled.get("title_source"))
210
+ _store("SpecPhase", reconciled.get("spec_phase"))
211
+ _store("RoadmapSummary", reconciled.get("roadmap_summary"))
212
+ _store("SourceConflict", reconciled.get("source_conflict"))
213
+
214
+ # #593: SourceSection is the named exception to the #616 narrative
215
+ # clamp -- it is a deliberate, user-visible audit-trail narrative
216
+ # (``ROADMAP Completed section`` vs ``ROADMAP active phase``) that
217
+ # operators need surfaced at the narrative level so the routing
218
+ # decision is auditable without re-running the migrator. Unlike the
219
+ # reconciler-internal provenance above (Description_source /
220
+ # Status_source / ...), which is plumbing noise, SourceSection is
221
+ # intended signal. The Windows task-dispatch regression asserts
222
+ # ``plan.narratives.SourceSection`` specifically. Single source of
223
+ # truth: we record it in narratives only, not duplicated under
224
+ # x-migrator metadata.
225
+ source_section = _narrative_str(reconciled.get("source_section"))
226
+ if source_section:
227
+ narratives = scope["plan"].setdefault("narratives", {})
228
+ narratives["SourceSection"] = source_section
229
+
230
+ # Clean up an empty migrator bucket so the emitted JSON doesn't
231
+ # carry an empty ``metadata.x-migrator`` payload on fully bare
232
+ # reconciled items (happens in unit tests that bypass the
233
+ # reconciler). Mirrors the clean-up path in ``create_scope_vbrief``
234
+ # where plan.metadata is only materialised when there is something
235
+ # to store.
236
+ if not migrator_meta:
237
+ plan_meta.pop(_MIGRATOR_METADATA_KEY, None)
238
+ if not plan_meta:
239
+ scope["plan"].pop("metadata", None)
240
+
241
+ # #593: stamp ``vBRIEFInfo.updated`` with the migration timestamp when
242
+ # we route an item to ``completed/``. The vBRIEF carries completion
243
+ # provenance in its envelope so downstream tooling that sorts by
244
+ # completion time has a non-null date to work with. Active/pending/
245
+ # proposed items do not receive an ``updated`` stamp because the
246
+ # scope has not yet reached a terminal state. ``migration_timestamp``
247
+ # is pinnable by callers (tests monkeypatch it for byte-for-byte
248
+ # fixture determinism).
249
+ if reconciled.get("status") == "completed":
250
+ envelope = scope.setdefault("vBRIEFInfo", {})
251
+ if isinstance(envelope, dict):
252
+ envelope.setdefault(
253
+ "updated", migration_timestamp or _migration_timestamp()
254
+ )
255
+
256
+ # Preserve any explicitly supplied references (e.g. spec back-link) on top
257
+ # of the origin-provenance reference set by ``_create_scope_vbrief``.
258
+ extra_refs = reconciled.get("references") or []
259
+ if extra_refs:
260
+ existing = scope["plan"].setdefault("references", [])
261
+ for ref in extra_refs:
262
+ if isinstance(ref, dict) and ref not in existing:
263
+ existing.append(ref)
264
+
265
+ return scope
266
+
267
+
268
+ __all__ = [
269
+ "DEFAULT_STATUS_FOR_FOLDER",
270
+ "FOLDER_TO_STATUSES",
271
+ "LIFECYCLE_FOLDERS",
272
+ "STATUS_TO_FOLDER",
273
+ "build_scope_vbrief_from_reconciled",
274
+ "default_status_for_folder",
275
+ "folder_for_status",
276
+ "plan_status_matches_folder",
277
+ ]