@deftai/directive-core 0.103.0 → 0.105.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 (255) hide show
  1. package/dist/authz/classify.js +682 -35
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/gate-lists.d.ts +3 -2
  5. package/dist/check/gate-lists.js +5 -2
  6. package/dist/check/index.d.ts +1 -0
  7. package/dist/check/index.js +1 -0
  8. package/dist/check/named-cause.d.ts +1 -1
  9. package/dist/check/named-cause.js +33 -3
  10. package/dist/check/session-completed-ac.d.ts +39 -0
  11. package/dist/check/session-completed-ac.js +198 -0
  12. package/dist/codebase/default-extractor.d.ts +5 -0
  13. package/dist/codebase/default-extractor.js +7 -15
  14. package/dist/deposit/copy-tree.js +60 -0
  15. package/dist/deposit/python-free.js +26 -18
  16. package/dist/doctor/checks.d.ts +8 -0
  17. package/dist/doctor/checks.js +37 -25
  18. package/dist/doctor/doctor-state.d.ts +4 -0
  19. package/dist/doctor/doctor-state.js +7 -1
  20. package/dist/doctor/main.js +9 -14
  21. package/dist/doctor/types.d.ts +9 -0
  22. package/dist/fs/contained-write.d.ts +115 -1
  23. package/dist/fs/contained-write.js +242 -5
  24. package/dist/fs/mutation-ledger.d.ts +54 -0
  25. package/dist/fs/mutation-ledger.js +133 -0
  26. package/dist/fs/non-product-dirs.d.ts +79 -0
  27. package/dist/fs/non-product-dirs.js +96 -0
  28. package/dist/hooks/dispatcher.d.ts +3 -1
  29. package/dist/hooks/dispatcher.js +14 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +3 -0
  32. package/dist/init-deposit/agent-hooks.js +17 -15
  33. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  34. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  35. package/dist/init-deposit/gitignore.js +9 -0
  36. package/dist/init-deposit/hygiene.d.ts +47 -11
  37. package/dist/init-deposit/hygiene.js +172 -37
  38. package/dist/init-deposit/legacy-detect.js +6 -2
  39. package/dist/init-deposit/refresh.d.ts +21 -1
  40. package/dist/init-deposit/refresh.js +106 -9
  41. package/dist/init-deposit/scaffold.js +28 -279
  42. package/dist/init-deposit/slash-deposit.js +13 -7
  43. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  44. package/dist/init-deposit/xbrief-projections.js +24 -11
  45. package/dist/intake/clause-derivation.d.ts +74 -0
  46. package/dist/intake/clause-derivation.js +409 -0
  47. package/dist/intake/index.d.ts +1 -0
  48. package/dist/intake/index.js +1 -0
  49. package/dist/intake/issue-ingest.d.ts +3 -1
  50. package/dist/intake/issue-ingest.js +36 -43
  51. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  52. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  53. package/dist/literal-acceptance/capture.d.ts +10 -0
  54. package/dist/literal-acceptance/capture.js +224 -75
  55. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  56. package/dist/literal-acceptance/evaluate.js +70 -2
  57. package/dist/literal-acceptance/index.d.ts +3 -3
  58. package/dist/literal-acceptance/index.js +3 -3
  59. package/dist/literal-acceptance/safety.d.ts +41 -0
  60. package/dist/literal-acceptance/safety.js +126 -2
  61. package/dist/literal-acceptance/types.d.ts +8 -0
  62. package/dist/orphan-active/evaluate.d.ts +7 -1
  63. package/dist/orphan-active/evaluate.js +102 -23
  64. package/dist/platform/platform-capabilities.js +2 -3
  65. package/dist/policy/base-branch.d.ts +37 -0
  66. package/dist/policy/base-branch.js +114 -0
  67. package/dist/policy/branch-sync.d.ts +80 -0
  68. package/dist/policy/branch-sync.js +211 -0
  69. package/dist/policy/ceremony-dial-escalation.js +1 -0
  70. package/dist/policy/ceremony-dial.js +4 -14
  71. package/dist/policy/delivery-branch.d.ts +5 -3
  72. package/dist/policy/delivery-branch.js +7 -3
  73. package/dist/policy/file-size-thresholds.d.ts +24 -0
  74. package/dist/policy/file-size-thresholds.js +23 -0
  75. package/dist/policy/forge-outage-retry.d.ts +47 -0
  76. package/dist/policy/forge-outage-retry.js +157 -0
  77. package/dist/policy/index.d.ts +7 -0
  78. package/dist/policy/index.js +66 -7
  79. package/dist/policy/project-invariants.d.ts +50 -0
  80. package/dist/policy/project-invariants.js +205 -0
  81. package/dist/policy/sync-default.d.ts +129 -0
  82. package/dist/policy/sync-default.js +452 -0
  83. package/dist/policy/sync-max-files.d.ts +93 -0
  84. package/dist/policy/sync-max-files.js +192 -0
  85. package/dist/pr-monitor/absent-required.d.ts +11 -0
  86. package/dist/pr-monitor/absent-required.js +61 -0
  87. package/dist/pr-monitor/constants.d.ts +7 -0
  88. package/dist/pr-monitor/constants.js +7 -0
  89. package/dist/pr-monitor/index.d.ts +2 -1
  90. package/dist/pr-monitor/index.js +2 -1
  91. package/dist/pr-monitor/main.js +4 -0
  92. package/dist/pr-monitor/monitor.js +24 -1
  93. package/dist/pr-wait-mergeable/classify.js +6 -0
  94. package/dist/preflight/evaluate.d.ts +9 -0
  95. package/dist/preflight/evaluate.js +49 -1
  96. package/dist/preflight/index.d.ts +5 -1
  97. package/dist/preflight/index.js +2 -0
  98. package/dist/preflight/intended-placement.d.ts +62 -0
  99. package/dist/preflight/intended-placement.js +275 -0
  100. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  101. package/dist/preflight/project-invariants-gate.js +82 -0
  102. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  103. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  104. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  105. package/dist/product-first-done-gate/acceptance.js +79 -8
  106. package/dist/product-first-done-gate/empty-resolution.d.ts +5 -2
  107. package/dist/product-first-done-gate/empty-resolution.js +13 -2
  108. package/dist/product-first-done-gate/evaluate.d.ts +29 -0
  109. package/dist/product-first-done-gate/evaluate.js +307 -33
  110. package/dist/product-first-done-gate/index.d.ts +3 -1
  111. package/dist/product-first-done-gate/index.js +2 -1
  112. package/dist/release/build-dist.d.ts +101 -0
  113. package/dist/release/build-dist.js +428 -46
  114. package/dist/run-summary/emit.d.ts +50 -4
  115. package/dist/run-summary/emit.js +262 -26
  116. package/dist/run-summary/index.d.ts +3 -2
  117. package/dist/run-summary/index.js +2 -1
  118. package/dist/run-summary/session-id.d.ts +21 -0
  119. package/dist/run-summary/session-id.js +92 -0
  120. package/dist/run-summary/share.d.ts +1 -1
  121. package/dist/run-summary/share.js +2 -2
  122. package/dist/run-summary/types.d.ts +38 -6
  123. package/dist/run-summary/types.js +1 -0
  124. package/dist/scope/acceptance-activate-gate.d.ts +1 -1
  125. package/dist/scope/acceptance-activate-gate.js +4 -4
  126. package/dist/scope/acceptance-evidence.d.ts +27 -0
  127. package/dist/scope/acceptance-evidence.js +66 -0
  128. package/dist/scope/capacity-stamp.d.ts +2 -0
  129. package/dist/scope/capacity-stamp.js +4 -0
  130. package/dist/scope/delivery-evidence.d.ts +9 -3
  131. package/dist/scope/delivery-evidence.js +39 -27
  132. package/dist/scope/index.d.ts +1 -0
  133. package/dist/scope/index.js +1 -0
  134. package/dist/scope/parent-lineage.js +7 -6
  135. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  136. package/dist/scope/project-invariant-coverage.js +322 -0
  137. package/dist/scope/transition.js +66 -4
  138. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  139. package/dist/scope-provenance/compare-intent.js +233 -0
  140. package/dist/scope-provenance/digest.d.ts +18 -1
  141. package/dist/scope-provenance/digest.js +16 -11
  142. package/dist/scope-provenance/evaluate.d.ts +5 -1
  143. package/dist/scope-provenance/evaluate.js +64 -4
  144. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  145. package/dist/scope-provenance/extract-intent.js +262 -0
  146. package/dist/scope-provenance/index.d.ts +7 -1
  147. package/dist/scope-provenance/index.js +7 -1
  148. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  149. package/dist/scope-provenance/intent-digest.js +33 -0
  150. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  151. package/dist/scope-provenance/intent-evaluate.js +149 -0
  152. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  153. package/dist/scope-provenance/json-tokenizer.js +225 -0
  154. package/dist/scope-provenance/known-machine.d.ts +38 -0
  155. package/dist/scope-provenance/known-machine.js +157 -0
  156. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  157. package/dist/scope-provenance/mint-artifacts.js +425 -0
  158. package/dist/session/ac-pass-banking.d.ts +4 -36
  159. package/dist/session/ac-pass-banking.js +30 -77
  160. package/dist/session/ac-pass-reuse.d.ts +53 -0
  161. package/dist/session/ac-pass-reuse.js +95 -0
  162. package/dist/session/ceremony-dial-evidence.d.ts +42 -0
  163. package/dist/session/ceremony-dial-evidence.js +194 -0
  164. package/dist/session/git.js +19 -4
  165. package/dist/session/index.d.ts +5 -0
  166. package/dist/session/index.js +5 -0
  167. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  168. package/dist/session/occupancy-stress-worker.js +36 -0
  169. package/dist/session/occupancy.d.ts +97 -0
  170. package/dist/session/occupancy.js +422 -0
  171. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  172. package/dist/session/product-state-hash.d.ts +26 -0
  173. package/dist/session/product-state-hash.js +683 -0
  174. package/dist/session/session-ready.d.ts +2 -0
  175. package/dist/session/session-ready.js +52 -30
  176. package/dist/session/session-start.d.ts +7 -0
  177. package/dist/session/session-start.js +88 -5
  178. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  179. package/dist/session/verify-ac-session-cache.js +126 -0
  180. package/dist/session/verify-session-ritual.d.ts +5 -0
  181. package/dist/session/verify-session-ritual.js +9 -0
  182. package/dist/slash/openclaw-deposit.js +13 -9
  183. package/dist/slice/constants.d.ts +2 -2
  184. package/dist/slice/constants.js +2 -2
  185. package/dist/slice/existing.js +21 -4
  186. package/dist/slice/lock.d.ts +16 -1
  187. package/dist/slice/lock.js +137 -11
  188. package/dist/slice/record.d.ts +8 -5
  189. package/dist/slice/record.js +21 -13
  190. package/dist/story-ready/evaluate.d.ts +2 -0
  191. package/dist/story-ready/evaluate.js +13 -0
  192. package/dist/swarm/complete-cohort.d.ts +3 -0
  193. package/dist/swarm/complete-cohort.js +59 -11
  194. package/dist/swarm/finalize-cohort.js +33 -2
  195. package/dist/swarm/launch.d.ts +22 -0
  196. package/dist/swarm/launch.js +155 -0
  197. package/dist/swarm/routing-set-cli.d.ts +11 -1
  198. package/dist/swarm/routing-set-cli.js +13 -3
  199. package/dist/swarm/routing-verify.d.ts +12 -0
  200. package/dist/swarm/routing-verify.js +59 -20
  201. package/dist/swarm/routing.d.ts +14 -0
  202. package/dist/swarm/routing.js +53 -3
  203. package/dist/telemetry-coverage/evaluate.d.ts +42 -0
  204. package/dist/telemetry-coverage/evaluate.js +148 -0
  205. package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
  206. package/dist/telemetry-coverage/fake-trial.js +204 -0
  207. package/dist/telemetry-coverage/index.d.ts +5 -0
  208. package/dist/telemetry-coverage/index.js +5 -0
  209. package/dist/telemetry-coverage/kinds.d.ts +19 -0
  210. package/dist/telemetry-coverage/kinds.js +54 -0
  211. package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
  212. package/dist/telemetry-coverage/scan-callers.js +168 -0
  213. package/dist/triage/actions/index.js +4 -1
  214. package/dist/ts-check-lane/index.d.ts +3 -0
  215. package/dist/ts-check-lane/index.js +2 -0
  216. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  217. package/dist/ts-check-lane/progress-reporter.js +38 -0
  218. package/dist/ts-check-lane/progress.d.ts +32 -0
  219. package/dist/ts-check-lane/progress.js +57 -0
  220. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  221. package/dist/ts-check-lane/run-lane.js +8 -6
  222. package/dist/validate-content/validate-links.d.ts +8 -0
  223. package/dist/validate-content/validate-links.js +9 -15
  224. package/dist/value/feedback-file.js +12 -2
  225. package/dist/vbrief-build/index.d.ts +1 -0
  226. package/dist/vbrief-build/index.js +1 -0
  227. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  228. package/dist/vbrief-build/project-invariants-io.js +41 -0
  229. package/dist/vbrief-reconcile/graph.js +18 -1
  230. package/dist/vbrief-reconcile/index.d.ts +3 -1
  231. package/dist/vbrief-reconcile/index.js +2 -1
  232. package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
  233. package/dist/vbrief-reconcile/origin-freshness.js +221 -0
  234. package/dist/vbrief-reconcile/types.d.ts +13 -0
  235. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  236. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  237. package/dist/vbrief-validate/index.d.ts +1 -1
  238. package/dist/vbrief-validate/index.js +1 -1
  239. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  240. package/dist/vbrief-validate/plan-hooks.js +68 -0
  241. package/dist/verify-ac/evaluate.d.ts +7 -1
  242. package/dist/verify-ac/evaluate.js +34 -26
  243. package/dist/verify-ac/flag.d.ts +4 -0
  244. package/dist/verify-ac/flag.js +16 -0
  245. package/dist/verify-ac/index.d.ts +2 -2
  246. package/dist/verify-ac/index.js +2 -2
  247. package/dist/verify-source/contained-writes.js +2 -0
  248. package/dist/verify-source/index.d.ts +1 -0
  249. package/dist/verify-source/index.js +1 -0
  250. package/dist/verify-source/verify-stubs.d.ts +15 -1
  251. package/dist/verify-source/verify-stubs.js +20 -16
  252. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  253. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  254. package/dist/xbrief-migrate/migrate-project.js +2 -5
  255. package/package.json +3 -3
@@ -0,0 +1,221 @@
1
+ import { extractReferencesFromVbrief, GITHUB_ISSUE_REF_TYPES, parseIssueNumber, } from "../intake/reconcile-issues.js";
2
+ import { call } from "../scm/call.js";
3
+ const ISSUE_URL_PATTERN = /https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/issues\/(\d+)/g;
4
+ const ISSUE_API_URL_PATTERN = /https:\/\/api\.github\.com\/repos\/([^/\s]+)\/([^/\s]+)\/issues\/(\d+)/g;
5
+ export const ORIGIN_FRESHNESS_REMEDIATION = "Re-read the issue body and comments (#2143), then either refresh the brief or record intentional divergence and bump xBRIEFInfo.updated. Do not auto-write origin text onto the brief (#309 D12).";
6
+ export function briefUpdatedOf(payload) {
7
+ const info = payload.xBRIEFInfo;
8
+ if (info !== null && typeof info === "object" && !Array.isArray(info)) {
9
+ const updated = info.updated;
10
+ if (typeof updated === "string" && updated.trim().length > 0) {
11
+ return updated.trim();
12
+ }
13
+ }
14
+ const plan = payload.plan;
15
+ if (plan !== null && typeof plan === "object" && !Array.isArray(plan)) {
16
+ const updated = plan.updated;
17
+ if (typeof updated === "string" && updated.trim().length > 0) {
18
+ return updated.trim();
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ export function extractGithubIssueOrigins(payload) {
24
+ const origins = [];
25
+ const seen = new Set();
26
+ const add = (origin) => {
27
+ const key = `${origin.owner}/${origin.repo}#${origin.number}`;
28
+ if (seen.has(key)) {
29
+ return;
30
+ }
31
+ seen.add(key);
32
+ origins.push(origin);
33
+ };
34
+ for (const rec of extractReferencesFromVbrief(payload)) {
35
+ const type = String(rec.type ?? "");
36
+ if (!GITHUB_ISSUE_REF_TYPES.has(type)) {
37
+ continue;
38
+ }
39
+ const fromUri = originFromUri(rec);
40
+ let origin = fromUri !== null ? { ...fromUri, type } : null;
41
+ if (origin === null) {
42
+ const number = parseIssueNumber(rec);
43
+ if (number === null) {
44
+ continue;
45
+ }
46
+ origin = {
47
+ owner: "",
48
+ repo: "",
49
+ number,
50
+ uri: typeof rec.uri === "string" ? rec.uri : "",
51
+ type,
52
+ };
53
+ }
54
+ add(origin);
55
+ }
56
+ for (const text of textualOriginFields(payload)) {
57
+ for (const fromText of originsFromText(text)) {
58
+ add({ ...fromText, type: "x-xbrief/github-issue" });
59
+ }
60
+ }
61
+ return origins;
62
+ }
63
+ export function extractGithubIssueOrigin(payload) {
64
+ return extractGithubIssueOrigins(payload)[0] ?? null;
65
+ }
66
+ function originFromUri(ref) {
67
+ for (const key of ["uri", "url"]) {
68
+ const value = ref[key];
69
+ if (typeof value !== "string" || value.length === 0) {
70
+ continue;
71
+ }
72
+ const parsed = originFromText(value);
73
+ if (parsed !== null) {
74
+ return parsed;
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+ function originsFromText(text) {
80
+ const origins = [];
81
+ const seen = new Set();
82
+ for (const pattern of [ISSUE_URL_PATTERN, ISSUE_API_URL_PATTERN]) {
83
+ for (const match of text.matchAll(pattern)) {
84
+ if (!match[1] || !match[2] || !match[3]) {
85
+ continue;
86
+ }
87
+ const owner = match[1];
88
+ const repo = match[2];
89
+ const number = Number.parseInt(match[3], 10);
90
+ const key = `${owner}/${repo}#${number}`;
91
+ if (seen.has(key)) {
92
+ continue;
93
+ }
94
+ seen.add(key);
95
+ origins.push({
96
+ owner,
97
+ repo,
98
+ number,
99
+ uri: `https://github.com/${owner}/${repo}/issues/${number}`,
100
+ });
101
+ }
102
+ }
103
+ return origins;
104
+ }
105
+ function originFromText(text) {
106
+ return originsFromText(text)[0] ?? null;
107
+ }
108
+ function textualOriginFields(payload) {
109
+ const texts = [];
110
+ const plan = payload.plan;
111
+ if (plan !== null && typeof plan === "object" && !Array.isArray(plan)) {
112
+ const narratives = plan.narratives;
113
+ if (narratives !== null && typeof narratives === "object" && !Array.isArray(narratives)) {
114
+ const origin = narratives.Origin;
115
+ if (typeof origin === "string" && origin.length > 0) {
116
+ texts.push(origin);
117
+ }
118
+ }
119
+ }
120
+ for (const key of ["xBRIEFInfo", "vBRIEFInfo"]) {
121
+ const info = payload[key];
122
+ if (info !== null && typeof info === "object" && !Array.isArray(info)) {
123
+ const description = info.description;
124
+ if (typeof description === "string" && description.length > 0) {
125
+ texts.push(description);
126
+ }
127
+ }
128
+ }
129
+ return texts;
130
+ }
131
+ export function compareOriginFreshness(briefUpdated, originUpdatedAt) {
132
+ if (originUpdatedAt === null || originUpdatedAt.trim().length === 0) {
133
+ return "uncomparable";
134
+ }
135
+ if (briefUpdated === null || briefUpdated.trim().length === 0) {
136
+ return "stale";
137
+ }
138
+ const briefMs = Date.parse(briefUpdated);
139
+ const originMs = Date.parse(originUpdatedAt);
140
+ if (Number.isNaN(briefMs) || Number.isNaN(originMs)) {
141
+ return "uncomparable";
142
+ }
143
+ return originMs > briefMs ? "stale" : "current";
144
+ }
145
+ export function formatOriginStaleMessage(origin, originUpdatedAt, briefUpdated) {
146
+ const slug = origin.owner.length > 0 ? `${origin.owner}/${origin.repo}` : "origin";
147
+ const briefStamp = briefUpdated ?? "(missing)";
148
+ return (`Origin GitHub issue #${origin.number} (${slug}) is newer than this xBRIEF ` +
149
+ `(origin updated_at=${originUpdatedAt}, brief updated=${briefStamp}). ` +
150
+ ORIGIN_FRESHNESS_REMEDIATION);
151
+ }
152
+ export function fetchGithubIssueUpdatedAt(origin, options = {}) {
153
+ if (origin.owner.length === 0 || origin.repo.length === 0) {
154
+ return { error: "origin reference is missing owner/repo in the GitHub issue URI" };
155
+ }
156
+ const scmCall = options.scmCall ?? call;
157
+ let result;
158
+ try {
159
+ result = scmCall("github-issue", "api", [`repos/${origin.owner}/${origin.repo}/issues/${origin.number}`], { timeout: 30, cwd: options.cwd });
160
+ }
161
+ catch (err) {
162
+ const message = err instanceof Error ? err.message : String(err);
163
+ return { error: `gh CLI not available (${message})` };
164
+ }
165
+ if (result.returncode !== 0) {
166
+ const stderr = result.stderr.trim();
167
+ return { error: stderr.length > 0 ? stderr : `gh api exited ${result.returncode}` };
168
+ }
169
+ try {
170
+ const payload = JSON.parse(result.stdout);
171
+ const updatedAt = payload.updated_at;
172
+ if (typeof updatedAt !== "string" || updatedAt.trim().length === 0) {
173
+ return { error: "origin issue payload lacks updated_at" };
174
+ }
175
+ return { updatedAt: updatedAt.trim() };
176
+ }
177
+ catch {
178
+ return { error: "origin issue payload is not valid JSON" };
179
+ }
180
+ }
181
+ export function evaluateOriginFreshness(payload, options = {}) {
182
+ if (options.skip === true) {
183
+ return { ok: true, kind: "no-origin" };
184
+ }
185
+ const origins = extractGithubIssueOrigins(payload);
186
+ if (origins.length === 0) {
187
+ return { ok: true, kind: "no-origin" };
188
+ }
189
+ const fetch = options.fetchOriginUpdatedAt ??
190
+ ((next) => fetchGithubIssueUpdatedAt(next, { cwd: options.cwd }));
191
+ const briefUpdated = briefUpdatedOf(payload);
192
+ for (const origin of origins) {
193
+ const fetched = fetch(origin);
194
+ if ("error" in fetched) {
195
+ return {
196
+ ok: false,
197
+ kind: "uncomparable",
198
+ message: `Could not fetch origin GitHub issue #${origin.number} for freshness check: ${fetched.error}. ` +
199
+ ORIGIN_FRESHNESS_REMEDIATION,
200
+ };
201
+ }
202
+ const kind = compareOriginFreshness(briefUpdated, fetched.updatedAt);
203
+ if (kind === "stale") {
204
+ return {
205
+ ok: false,
206
+ kind,
207
+ message: formatOriginStaleMessage(origin, fetched.updatedAt, briefUpdated),
208
+ };
209
+ }
210
+ if (kind === "uncomparable") {
211
+ return {
212
+ ok: false,
213
+ kind,
214
+ message: `Could not compare origin GitHub issue #${origin.number} updated_at=${fetched.updatedAt} ` +
215
+ `to brief updated=${briefUpdated ?? "(missing)"}. ${ORIGIN_FRESHNESS_REMEDIATION}`,
216
+ };
217
+ }
218
+ }
219
+ return { ok: true, kind: "current" };
220
+ }
221
+ //# sourceMappingURL=origin-freshness.js.map
@@ -54,6 +54,11 @@ export interface Candidate {
54
54
  }
55
55
  export interface ReconcileGraphOutcome {
56
56
  promoted: string[];
57
+ /** Successful promote messages, including refused-stamp remediation (#3398). */
58
+ promotedNotices?: Array<{
59
+ story_id: string;
60
+ message: string;
61
+ }>;
57
62
  deferredWip: string[];
58
63
  waiting: Array<{
59
64
  story_id: string;
@@ -110,6 +115,12 @@ export interface UmbrellaChange {
110
115
  * `unchanged` when already correct, `skipped` when body APIs unavailable.
111
116
  */
112
117
  readonly checklist_action?: "edited" | "unchanged" | "skipped";
118
+ /**
119
+ * slices.jsonl expected_close_signal close (#3428): `closed` when the
120
+ * umbrella was closed this run, `unchanged` when already closed under
121
+ * all-children-merged, `skipped` when the signal is not a close.
122
+ */
123
+ readonly close_action?: "closed" | "unchanged" | "skipped";
113
124
  }
114
125
  export interface ReconcileUmbrellasOutcome {
115
126
  changed: UmbrellaChange[];
@@ -138,5 +149,7 @@ export interface UmbrellaClient {
138
149
  fetchIssueBody?(repo: string, issueNumber: number): string;
139
150
  /** Optional: write reconciled checklist body (#1649). */
140
151
  editIssueBody?(repo: string, issueNumber: number, body: string): void;
152
+ /** Optional: close an umbrella via REST (#3428). */
153
+ closeIssue?(repo: string, issueNumber: number): void;
141
154
  }
142
155
  //# sourceMappingURL=types.d.ts.map
@@ -63,8 +63,22 @@ export declare class ScmUmbrellaClient implements UmbrellaClient {
63
63
  fetchIssueStates(repo: string, issueNumbers: readonly number[]): ReadonlyMap<number, ForgeIssueState>;
64
64
  fetchIssueBody(repo: string, issueNumber: number): string;
65
65
  editIssueBody(repo: string, issueNumber: number, body: string): void;
66
+ closeIssue(repo: string, issueNumber: number): void;
66
67
  }
67
68
  export declare function nowIso(date?: Date): string;
69
+ /** Children listed on a slices.jsonl row (#3428). Last write of a duplicate `n` wins. */
70
+ export declare function childrenFromSliceRecord(record: Record<string, unknown>): Child[];
71
+ /**
72
+ * Close only when the slice signal is all-children-merged, the umbrella is
73
+ * still open, and every child has forge state closed (#3428).
74
+ */
75
+ export declare function shouldCloseOnAllChildrenMerged(options: {
76
+ readonly signal: unknown;
77
+ readonly children: readonly Child[];
78
+ readonly forgeStates: ReadonlyMap<number, ForgeIssueState> | null | undefined;
79
+ readonly umbrellaState: ForgeIssueState | null | undefined;
80
+ }): boolean;
81
+ export declare function formatCloseComment(signal: string, children: readonly Child[]): string;
68
82
  export interface ReconcileUmbrellasOptions {
69
83
  readonly repo?: string | null;
70
84
  readonly dryRun?: boolean;
@@ -5,6 +5,8 @@ import { createIssueComment, editIssueBody, editIssueCommentBody, fetchIssueBody
5
5
  import { fetchIssueStates, IssueState } from "../intake/reconcile-issues.js";
6
6
  import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
7
7
  import { call } from "../scm/call.js";
8
+ import { restCloseIssue } from "../scm/gh-rest.js";
9
+ import { readAll } from "../slice/record.js";
8
10
  import { extractIssueRef, parseGithubIssueUri } from "../triage/reconcile/parse-uri.js";
9
11
  import { isRepoMutationAllowed } from "./repo-guard.js";
10
12
  export const OPEN_FOLDERS = ["proposed", "pending", "active"];
@@ -307,33 +309,43 @@ export function classifyPassType(prevTotal, total) {
307
309
  function hasCurrentShape(body) {
308
310
  return HEADER_RE.test(body);
309
311
  }
312
+ function parseCommentPage(data) {
313
+ if (!Array.isArray(data))
314
+ return [];
315
+ const comments = [];
316
+ for (const entry of data) {
317
+ if (typeof entry === "object" &&
318
+ entry !== null &&
319
+ !Array.isArray(entry) &&
320
+ typeof entry.id === "number" &&
321
+ typeof entry.body === "string") {
322
+ const rec = entry;
323
+ comments.push({ id: rec.id, body: rec.body });
324
+ }
325
+ }
326
+ return comments;
327
+ }
310
328
  export class ScmUmbrellaClient {
311
329
  fetchComments(repo, issueNumber) {
312
- const proc = call(SCM_SOURCE, "api", [
313
- `repos/${repo}/issues/${issueNumber}/comments?per_page=100`,
314
- ]);
315
- if (proc.returncode !== 0) {
316
- throw new UmbrellaScmError(`list comments #${issueNumber} (${repo}) failed: ${(proc.stderr || "").trim()}`);
317
- }
318
- let data;
319
- try {
320
- data = JSON.parse(proc.stdout || "[]");
321
- }
322
- catch (exc) {
323
- throw new UmbrellaScmError(`list comments #${issueNumber} (${repo}) returned non-JSON: ${String(exc)}`);
324
- }
325
- if (!Array.isArray(data))
326
- return [];
327
330
  const comments = [];
328
- for (const entry of data) {
329
- if (typeof entry === "object" &&
330
- entry !== null &&
331
- !Array.isArray(entry) &&
332
- typeof entry.id === "number" &&
333
- typeof entry.body === "string") {
334
- const rec = entry;
335
- comments.push({ id: rec.id, body: rec.body });
331
+ for (let page = 1; page <= 50; page += 1) {
332
+ const proc = call(SCM_SOURCE, "api", [
333
+ `repos/${repo}/issues/${issueNumber}/comments?per_page=100&page=${page}`,
334
+ ]);
335
+ if (proc.returncode !== 0) {
336
+ throw new UmbrellaScmError(`list comments #${issueNumber} (${repo}) failed: ${(proc.stderr || "").trim()}`);
337
+ }
338
+ let data;
339
+ try {
340
+ data = JSON.parse(proc.stdout || "[]");
341
+ }
342
+ catch (exc) {
343
+ throw new UmbrellaScmError(`list comments #${issueNumber} (${repo}) returned non-JSON: ${String(exc)}`);
336
344
  }
345
+ const batch = parseCommentPage(data);
346
+ comments.push(...batch);
347
+ if (batch.length < 100)
348
+ break;
337
349
  }
338
350
  return comments;
339
351
  }
@@ -394,6 +406,15 @@ export class ScmUmbrellaClient {
394
406
  throw new UmbrellaScmError(`edit issue body #${issueNumber} (${repo}) failed: ${message}`);
395
407
  }
396
408
  }
409
+ closeIssue(repo, issueNumber) {
410
+ try {
411
+ restCloseIssue(repo, issueNumber, "completed");
412
+ }
413
+ catch (exc) {
414
+ const message = exc instanceof Error ? exc.message : String(exc);
415
+ throw new UmbrellaScmError(`close issue #${issueNumber} (${repo}) failed: ${message}`);
416
+ }
417
+ }
397
418
  }
398
419
  function planShape(epicData, index, options = {}) {
399
420
  const children = computeChildren(epicData, index, {
@@ -545,6 +566,147 @@ function reconcileOneEpic(epicData, index, options) {
545
566
  export function nowIso(date = new Date()) {
546
567
  return date.toISOString().replace(/\.\d{3}Z$/, "Z");
547
568
  }
569
+ const ALL_CHILDREN_MERGED = "all-children-merged";
570
+ /** Children listed on a slices.jsonl row (#3428). Last write of a duplicate `n` wins. */
571
+ export function childrenFromSliceRecord(record) {
572
+ const raw = record.children;
573
+ if (!Array.isArray(raw))
574
+ return [];
575
+ const byNumber = new Map();
576
+ for (const entry of raw) {
577
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
578
+ continue;
579
+ const rec = entry;
580
+ const n = rec.n;
581
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1)
582
+ continue;
583
+ byNumber.set(n, {
584
+ story_id: `#${n}`,
585
+ title: `#${n}`,
586
+ kind: "story",
587
+ folder: UNKNOWN_FOLDER,
588
+ depends_on: [],
589
+ issue_number: n,
590
+ });
591
+ }
592
+ return [...byNumber.values()];
593
+ }
594
+ /**
595
+ * Close only when the slice signal is all-children-merged, the umbrella is
596
+ * still open, and every child has forge state closed (#3428).
597
+ */
598
+ export function shouldCloseOnAllChildrenMerged(options) {
599
+ if (options.signal !== ALL_CHILDREN_MERGED)
600
+ return false;
601
+ if (options.umbrellaState !== "open")
602
+ return false;
603
+ if (options.children.length === 0)
604
+ return false;
605
+ if (options.forgeStates == null)
606
+ return false;
607
+ return options.children.every((c) => typeof c.issue_number === "number" && options.forgeStates?.get(c.issue_number) === "closed");
608
+ }
609
+ export function formatCloseComment(signal, children) {
610
+ const nums = children
611
+ .map((c) => c.issue_number)
612
+ .filter((n) => typeof n === "number")
613
+ .sort((a, b) => a - b)
614
+ .map((n) => `#${n}`);
615
+ return `expected_close_signal=${signal}; children: ${nums.join(", ")}`;
616
+ }
617
+ function loadLatestSliceRecords(vbriefDir) {
618
+ const logPath = join(vbriefDir, ".triage-cache", "slices.jsonl");
619
+ if (!existsSync(logPath))
620
+ return [];
621
+ const latest = new Map();
622
+ for (const rec of readAll({ path: logPath })) {
623
+ const n = rec.umbrella;
624
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1)
625
+ continue;
626
+ const [urlRepo] = parseGithubIssueUri(rec.umbrella_url);
627
+ latest.set(`${urlRepo ?? ""}:${n}`, rec);
628
+ }
629
+ return [...latest.values()];
630
+ }
631
+ function epicDataFromSlice(repo, number, children) {
632
+ const refs = [
633
+ { type: "x-xbrief/github-issue", uri: `https://github.com/${repo}/issues/${number}` },
634
+ ...children.map((c) => ({
635
+ type: "x-xbrief/github-issue",
636
+ uri: `https://github.com/${repo}/issues/${c.issue_number}`,
637
+ title: c.title,
638
+ })),
639
+ ];
640
+ return {
641
+ plan: {
642
+ id: `slice-umbrella-${number}`,
643
+ metadata: { kind: "epic" },
644
+ references: refs,
645
+ },
646
+ };
647
+ }
648
+ function maybeCloseSliceUmbrella(options) {
649
+ if (typeof options.client.closeIssue !== "function")
650
+ return "skipped";
651
+ if (typeof options.client.fetchIssueStates !== "function")
652
+ return "skipped";
653
+ const mutateGate = isRepoMutationAllowed(options.repo, options.projectRoot, {
654
+ allowCrossRepo: options.allowCrossRepo,
655
+ allowlist: options.repoAllowlist,
656
+ explicitRepo: options.explicitRepo ?? null,
657
+ });
658
+ if (!mutateGate.allowed) {
659
+ throw new Error(mutateGate.reason ?? `refusing cross-repo mutation on ${options.repo}`);
660
+ }
661
+ const childNumbers = options.children
662
+ .map((c) => c.issue_number)
663
+ .filter((n) => typeof n === "number");
664
+ const states = options.client.fetchIssueStates(options.repo, [...childNumbers, options.number]);
665
+ const umbrellaState = states.get(options.number) ?? null;
666
+ if (!shouldCloseOnAllChildrenMerged({
667
+ signal: options.signal,
668
+ children: options.children,
669
+ forgeStates: states,
670
+ umbrellaState,
671
+ })) {
672
+ return options.signal === ALL_CHILDREN_MERGED && umbrellaState === "closed"
673
+ ? "unchanged"
674
+ : "skipped";
675
+ }
676
+ const closeBody = formatCloseComment(ALL_CHILDREN_MERGED, options.children);
677
+ if (!options.dryRun) {
678
+ const alreadyCommented = options.client
679
+ .fetchComments(options.repo, options.number)
680
+ .some((c) => c.body.trim() === closeBody);
681
+ if (!alreadyCommented) {
682
+ options.client.createComment(options.repo, options.number, closeBody);
683
+ }
684
+ options.client.closeIssue(options.repo, options.number);
685
+ }
686
+ return "closed";
687
+ }
688
+ function pushUmbrellaChange(outcome, change) {
689
+ if (change.action === "unchanged" && change.close_action !== "closed") {
690
+ outcome.unchanged.push(change);
691
+ }
692
+ else {
693
+ outcome.changed.push(change);
694
+ }
695
+ }
696
+ function replaceCloseAction(outcome, repo, issueNumber, closeAction) {
697
+ const match = (c) => c.repo === repo && c.issue_number === issueNumber;
698
+ for (const list of [outcome.changed, outcome.unchanged]) {
699
+ const idx = list.findIndex(match);
700
+ if (idx < 0)
701
+ continue;
702
+ const prev = list[idx];
703
+ if (prev === undefined)
704
+ return;
705
+ list.splice(idx, 1);
706
+ pushUmbrellaChange(outcome, { ...prev, close_action: closeAction });
707
+ return;
708
+ }
709
+ }
548
710
  export function reconcileUmbrellas(projectRoot, options = {}) {
549
711
  const root = resolve(projectRoot);
550
712
  let vbriefDir;
@@ -616,7 +778,6 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
616
778
  const key = `${effectiveRepo}:${number}`;
617
779
  if (seenIssues.has(key))
618
780
  continue;
619
- seenIssues.add(key);
620
781
  try {
621
782
  const change = reconcileOneEpic(data, index, {
622
783
  storyId,
@@ -630,6 +791,7 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
630
791
  repoAllowlist: options.repoAllowlist,
631
792
  explicitRepo: options.repo ?? null,
632
793
  });
794
+ seenIssues.add(key);
633
795
  if (change.action === "unchanged")
634
796
  outcome.unchanged.push(change);
635
797
  else
@@ -643,6 +805,71 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
643
805
  }
644
806
  }
645
807
  }
808
+ // slices.jsonl umbrellas have no epic xBRIEF (#3428). Last row per umbrella wins.
809
+ for (const record of loadLatestSliceRecords(vbriefDir)) {
810
+ const number = record.umbrella;
811
+ if (typeof number !== "number")
812
+ continue;
813
+ const [urlRepo] = parseGithubIssueUri(record.umbrella_url);
814
+ const effectiveRepo = urlRepo ?? options.repo ?? null;
815
+ if (effectiveRepo === null) {
816
+ outcome.skipped_no_ref.push(`slice-umbrella-${number}`);
817
+ continue;
818
+ }
819
+ const key = `${effectiveRepo}:${number}`;
820
+ const children = childrenFromSliceRecord(record);
821
+ const storyId = `slice-umbrella-${number}`;
822
+ try {
823
+ if (seenIssues.has(key)) {
824
+ const closeAction = maybeCloseSliceUmbrella({
825
+ signal: record.expected_close_signal,
826
+ children,
827
+ repo: effectiveRepo,
828
+ number,
829
+ client,
830
+ dryRun: options.dryRun ?? false,
831
+ projectRoot: root,
832
+ allowCrossRepo: options.allowCrossRepo ?? false,
833
+ repoAllowlist: options.repoAllowlist,
834
+ explicitRepo: options.repo ?? null,
835
+ });
836
+ replaceCloseAction(outcome, effectiveRepo, number, closeAction);
837
+ continue;
838
+ }
839
+ const change = reconcileOneEpic(epicDataFromSlice(effectiveRepo, number, children), index, {
840
+ storyId,
841
+ repo: effectiveRepo,
842
+ number,
843
+ client,
844
+ dryRun: options.dryRun ?? false,
845
+ now,
846
+ projectRoot: root,
847
+ allowCrossRepo: options.allowCrossRepo ?? false,
848
+ repoAllowlist: options.repoAllowlist,
849
+ explicitRepo: options.repo ?? null,
850
+ });
851
+ seenIssues.add(key);
852
+ const closeAction = maybeCloseSliceUmbrella({
853
+ signal: record.expected_close_signal,
854
+ children,
855
+ repo: effectiveRepo,
856
+ number,
857
+ client,
858
+ dryRun: options.dryRun ?? false,
859
+ projectRoot: root,
860
+ allowCrossRepo: options.allowCrossRepo ?? false,
861
+ repoAllowlist: options.repoAllowlist,
862
+ explicitRepo: options.repo ?? null,
863
+ });
864
+ pushUmbrellaChange(outcome, { ...change, close_action: closeAction });
865
+ }
866
+ catch (exc) {
867
+ outcome.errors.push({
868
+ story_id: storyId,
869
+ message: exc instanceof Error ? exc.message : String(exc),
870
+ });
871
+ }
872
+ }
646
873
  return [outcome.errors.length > 0 ? 1 : 0, outcome];
647
874
  }
648
875
  function formatChecklistSuffix(action) {
@@ -650,6 +877,11 @@ function formatChecklistSuffix(action) {
650
877
  return "";
651
878
  return `; checklist=${action}`;
652
879
  }
880
+ function formatCloseSuffix(action) {
881
+ if (action === undefined || action === "skipped")
882
+ return "";
883
+ return `; close=${action}`;
884
+ }
653
885
  export function renderUmbrellasReport(outcome) {
654
886
  const lines = ["vBRIEF reconcile umbrellas", ""];
655
887
  const suffix = outcome.dry_run ? " (dry-run)" : "";
@@ -657,7 +889,8 @@ export function renderUmbrellasReport(outcome) {
657
889
  if (outcome.changed.length > 0) {
658
890
  for (const c of outcome.changed) {
659
891
  lines.push(`- #${c.issue_number} (${c.repo}) [${c.story_id}]: ${c.action} -> pass-${c.pass_n}` +
660
- formatChecklistSuffix(c.checklist_action));
892
+ formatChecklistSuffix(c.checklist_action) +
893
+ formatCloseSuffix(c.close_action));
661
894
  }
662
895
  }
663
896
  else {
@@ -668,7 +901,8 @@ export function renderUmbrellasReport(outcome) {
668
901
  if (outcome.unchanged.length > 0) {
669
902
  for (const c of outcome.unchanged) {
670
903
  lines.push(`- #${c.issue_number} (${c.repo}) [${c.story_id}]: pass-${c.pass_n}` +
671
- formatChecklistSuffix(c.checklist_action));
904
+ formatChecklistSuffix(c.checklist_action) +
905
+ formatCloseSuffix(c.close_action));
672
906
  }
673
907
  }
674
908
  else {
@@ -696,6 +930,7 @@ export function umbrellasOutcomeToJson(outcome) {
696
930
  action: c.action,
697
931
  pass_n: c.pass_n,
698
932
  checklist_action: c.checklist_action ?? "skipped",
933
+ close_action: c.close_action ?? "skipped",
699
934
  });
700
935
  return {
701
936
  changed: outcome.changed.map(toChange),
@@ -6,7 +6,7 @@ export { validateFolderStatus } from "./folder-status.js";
6
6
  export type { ConformanceCliOptions, ValidateCliOptions } from "./main.js";
7
7
  export { cmdVbriefValidate, runConformance, runValidate } from "./main.js";
8
8
  export { validateOriginProvenance } from "./origin.js";
9
- export { validateSessionRitualStalenessHoursOnPlan, validateTriageRankingLabelsOnPlan, validateWipCapOnPlan, } from "./plan-hooks.js";
9
+ export { validateForgeOutageRetryMinutesOnPlan, validateProjectInvariantsOnPlan, validateSessionRitualStalenessHoursOnPlan, validateTriageRankingLabelsOnPlan, validateWipCapOnPlan, } from "./plan-hooks.js";
10
10
  export { validateProjectDefinition } from "./project-definition.js";
11
11
  export type { ExtensionEntry, ExtensionRoundtripEvaluateResult, ExtensionRoundtripFinding, } from "./roundtrip.js";
12
12
  export { collectExtensionEntries, EXTENSION_CONFORMANCE_FIXTURES_DIR, evaluateExtensionRoundtrip, findExtensionPreservationViolations, reEmitVbriefArtifact, renderExtensionRoundtripFinding, VbriefSchemaValidationError, } from "./roundtrip.js";
@@ -4,7 +4,7 @@ export { matchesFilenameConvention, validateFilename } from "./filename.js";
4
4
  export { validateFolderStatus } from "./folder-status.js";
5
5
  export { cmdVbriefValidate, runConformance, runValidate } from "./main.js";
6
6
  export { validateOriginProvenance } from "./origin.js";
7
- export { validateSessionRitualStalenessHoursOnPlan, validateTriageRankingLabelsOnPlan, validateWipCapOnPlan, } from "./plan-hooks.js";
7
+ export { validateForgeOutageRetryMinutesOnPlan, validateProjectInvariantsOnPlan, validateSessionRitualStalenessHoursOnPlan, validateTriageRankingLabelsOnPlan, validateWipCapOnPlan, } from "./plan-hooks.js";
8
8
  export { validateProjectDefinition } from "./project-definition.js";
9
9
  export { collectExtensionEntries, EXTENSION_CONFORMANCE_FIXTURES_DIR, evaluateExtensionRoundtrip, findExtensionPreservationViolations, reEmitVbriefArtifact, renderExtensionRoundtripFinding, VbriefSchemaValidationError, } from "./roundtrip.js";
10
10
  export { normalizeNarrativeKey, validateVbriefSchema } from "./schema.js";
@@ -2,6 +2,10 @@
2
2
  export declare function validateWipCapOnPlan(plan: unknown, filepath: string): string[];
3
3
  /** vbrief_validate hook for ``sessionRitualStalenessHours`` (#1348). */
4
4
  export declare function validateSessionRitualStalenessHoursOnPlan(plan: unknown, filepath: string): string[];
5
+ /** vbrief_validate hook for ``projectInvariants`` (#3425). */
6
+ export declare function validateProjectInvariantsOnPlan(plan: unknown, filepath: string): string[];
7
+ /** vbrief_validate hook for ``forgeOutageRetryMinutes`` (#3422). */
8
+ export declare function validateForgeOutageRetryMinutesOnPlan(plan: unknown, filepath: string): string[];
5
9
  /** vbrief_validate hook: validate ``plan.policy.triageRankingLabels`` (#1128). */
6
10
  export declare function validateTriageRankingLabelsOnPlan(plan: unknown, filepath: string): string[];
7
11
  /** vbrief_validate hook: validate ``plan.policy.runtimeAuthority`` (#1394). */