@deftai/directive-core 0.88.0 → 0.90.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 (202) hide show
  1. package/dist/authz/actions.d.ts +55 -0
  2. package/dist/authz/actions.js +125 -0
  3. package/dist/authz/classify.d.ts +23 -0
  4. package/dist/authz/classify.js +217 -0
  5. package/dist/authz/closed-verb.d.ts +42 -0
  6. package/dist/authz/closed-verb.js +279 -0
  7. package/dist/authz/evaluate.d.ts +41 -0
  8. package/dist/authz/evaluate.js +298 -0
  9. package/dist/authz/index.d.ts +15 -0
  10. package/dist/authz/index.js +15 -0
  11. package/dist/authz/origin.d.ts +28 -0
  12. package/dist/authz/origin.js +64 -0
  13. package/dist/authz/paths.d.ts +6 -0
  14. package/dist/authz/paths.js +19 -0
  15. package/dist/authz/store.d.ts +40 -0
  16. package/dist/authz/store.js +317 -0
  17. package/dist/authz/templates.d.ts +139 -0
  18. package/dist/authz/templates.js +241 -0
  19. package/dist/authz/types.d.ts +111 -0
  20. package/dist/authz/types.js +41 -0
  21. package/dist/authz/verb-classification.d.ts +44 -0
  22. package/dist/authz/verb-classification.js +237 -0
  23. package/dist/branch/evaluate.js +6 -1
  24. package/dist/cache/fetch.js +10 -5
  25. package/dist/cache/io.d.ts +9 -2
  26. package/dist/cache/io.js +30 -10
  27. package/dist/cache/task-cache/store.js +11 -7
  28. package/dist/capacity/backfill.js +10 -4
  29. package/dist/category-b-namespace/index.js +10 -4
  30. package/dist/codebase/default-extractor.js +3 -0
  31. package/dist/codebase/map.js +11 -4
  32. package/dist/doctor/constants.d.ts +1 -1
  33. package/dist/doctor/constants.js +2 -0
  34. package/dist/doctor/doctor-state.js +28 -4
  35. package/dist/doctor/flags.js +21 -1
  36. package/dist/doctor/index.d.ts +1 -0
  37. package/dist/doctor/index.js +1 -0
  38. package/dist/doctor/main.d.ts +10 -0
  39. package/dist/doctor/main.js +182 -0
  40. package/dist/doctor/openclaw-skills.d.ts +109 -0
  41. package/dist/doctor/openclaw-skills.js +463 -0
  42. package/dist/doctor/types.d.ts +22 -0
  43. package/dist/escalation/actions.d.ts +63 -0
  44. package/dist/escalation/actions.js +137 -0
  45. package/dist/escalation/index.d.ts +8 -0
  46. package/dist/escalation/index.js +8 -0
  47. package/dist/escalation/paths.d.ts +3 -0
  48. package/dist/escalation/paths.js +10 -0
  49. package/dist/escalation/store.d.ts +17 -0
  50. package/dist/escalation/store.js +172 -0
  51. package/dist/escalation/types.d.ts +73 -0
  52. package/dist/escalation/types.js +43 -0
  53. package/dist/eval/crud-telemetry.js +30 -10
  54. package/dist/eval/health.js +22 -7
  55. package/dist/eval/readback.js +11 -10
  56. package/dist/eval/run.js +32 -16
  57. package/dist/events/attribution-enrichment.js +10 -4
  58. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  59. package/dist/finish-loop/directive-finish-loop.js +239 -0
  60. package/dist/finish-loop/grant-gate.d.ts +31 -0
  61. package/dist/finish-loop/grant-gate.js +169 -0
  62. package/dist/finish-loop/index.d.ts +11 -0
  63. package/dist/finish-loop/index.js +11 -0
  64. package/dist/finish-loop/main.d.ts +35 -0
  65. package/dist/finish-loop/main.js +365 -0
  66. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  67. package/dist/finish-loop/pr-finish-loop.js +217 -0
  68. package/dist/finish-loop/progress.d.ts +17 -0
  69. package/dist/finish-loop/progress.js +45 -0
  70. package/dist/finish-loop/queue.d.ts +16 -0
  71. package/dist/finish-loop/queue.js +65 -0
  72. package/dist/finish-loop/types.d.ts +52 -0
  73. package/dist/finish-loop/types.js +10 -0
  74. package/dist/fs/contained-write.d.ts +101 -0
  75. package/dist/fs/contained-write.js +281 -0
  76. package/dist/hooks/classify/classify.d.ts +10 -0
  77. package/dist/hooks/classify/classify.js +37 -0
  78. package/dist/hooks/classify/index.d.ts +14 -0
  79. package/dist/hooks/classify/index.js +13 -0
  80. package/dist/hooks/classify/paths.d.ts +22 -0
  81. package/dist/hooks/classify/paths.js +72 -0
  82. package/dist/hooks/classify/payload.d.ts +16 -0
  83. package/dist/hooks/classify/payload.js +45 -0
  84. package/dist/hooks/classify/stdin.d.ts +12 -0
  85. package/dist/hooks/classify/stdin.js +63 -0
  86. package/dist/hooks/classify/tool-name.d.ts +18 -0
  87. package/dist/hooks/classify/tool-name.js +85 -0
  88. package/dist/hooks/classify/types.d.ts +41 -0
  89. package/dist/hooks/classify/types.js +7 -0
  90. package/dist/hooks/dispatcher.d.ts +21 -18
  91. package/dist/hooks/dispatcher.js +232 -168
  92. package/dist/hooks/fixtures/cases.d.ts +44 -0
  93. package/dist/hooks/fixtures/cases.js +595 -0
  94. package/dist/hooks/fixtures/index.d.ts +2 -0
  95. package/dist/hooks/fixtures/index.js +2 -0
  96. package/dist/hooks/index.d.ts +2 -0
  97. package/dist/hooks/index.js +2 -0
  98. package/dist/index.d.ts +5 -0
  99. package/dist/index.js +6 -0
  100. package/dist/init-deposit/agent-hooks.js +27 -8
  101. package/dist/init-deposit/gitignore.js +11 -3
  102. package/dist/init-deposit/headless-manifest.js +12 -4
  103. package/dist/init-deposit/migrate.d.ts +1 -1
  104. package/dist/init-deposit/migrate.js +13 -3
  105. package/dist/init-deposit/prettierignore.js +10 -3
  106. package/dist/init-deposit/scaffold.js +35 -26
  107. package/dist/init-deposit/xbrief-projections.js +11 -4
  108. package/dist/intake/candidates-log.js +46 -35
  109. package/dist/intake/github-body-cli.d.ts +6 -0
  110. package/dist/intake/github-body-cli.js +17 -0
  111. package/dist/intake/github-body.d.ts +46 -3
  112. package/dist/intake/github-body.js +191 -20
  113. package/dist/intake/issue-emit.d.ts +1 -0
  114. package/dist/intake/issue-emit.js +41 -23
  115. package/dist/lifecycle/event-detect.js +12 -4
  116. package/dist/lifecycle/events.js +31 -20
  117. package/dist/lifecycle/index.d.ts +1 -0
  118. package/dist/lifecycle/index.js +1 -0
  119. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  120. package/dist/lifecycle/stats.d.ts +53 -0
  121. package/dist/lifecycle/stats.js +286 -0
  122. package/dist/packs/pack-render.js +19 -6
  123. package/dist/plan-sequence/store.js +11 -4
  124. package/dist/policy/hotfix-criteria.d.ts +79 -0
  125. package/dist/policy/hotfix-criteria.js +197 -0
  126. package/dist/policy/index.d.ts +4 -0
  127. package/dist/policy/index.js +31 -1
  128. package/dist/policy/intent-ceiling.d.ts +79 -0
  129. package/dist/policy/intent-ceiling.js +181 -0
  130. package/dist/policy/no-deft-directive.js +10 -3
  131. package/dist/policy/org-force-on-migration.js +9 -5
  132. package/dist/policy/require-human-merge.d.ts +75 -0
  133. package/dist/policy/require-human-merge.js +324 -0
  134. package/dist/policy/resolve.js +17 -6
  135. package/dist/policy/runtime-authority.d.ts +15 -2
  136. package/dist/policy/runtime-authority.js +23 -3
  137. package/dist/policy/write-fence.d.ts +78 -0
  138. package/dist/policy/write-fence.js +164 -0
  139. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  140. package/dist/pr-wait-mergeable/cascade.js +28 -0
  141. package/dist/preflight/evaluate.js +10 -0
  142. package/dist/product-signal/consent.js +21 -5
  143. package/dist/product-signal/submit.js +22 -12
  144. package/dist/release/build-dist-runner.js +5 -2
  145. package/dist/release/build-dist.d.ts +19 -1
  146. package/dist/release/build-dist.js +50 -2
  147. package/dist/release/native-steps.js +7 -2
  148. package/dist/release/spawn.js +13 -0
  149. package/dist/release-e2e/git-ops.d.ts +7 -0
  150. package/dist/release-e2e/git-ops.js +35 -2
  151. package/dist/release-publish/pipeline.d.ts +13 -0
  152. package/dist/release-publish/pipeline.js +46 -1
  153. package/dist/scope/audit-log.js +19 -24
  154. package/dist/scope/decompose.js +10 -5
  155. package/dist/scope/decomposed-refs.js +18 -3
  156. package/dist/scope/demote.js +9 -2
  157. package/dist/scope/undo.js +9 -2
  158. package/dist/session/index.d.ts +2 -0
  159. package/dist/session/index.js +2 -0
  160. package/dist/session/process-cost-constants.d.ts +9 -0
  161. package/dist/session/process-cost-constants.js +11 -0
  162. package/dist/session/process-cost.d.ts +53 -0
  163. package/dist/session/process-cost.js +82 -0
  164. package/dist/session/release-availability.js +26 -6
  165. package/dist/session/ritual-sentinel.d.ts +5 -0
  166. package/dist/session/ritual-sentinel.js +31 -13
  167. package/dist/session/session-ready.d.ts +67 -0
  168. package/dist/session/session-ready.js +264 -0
  169. package/dist/session/session-start.d.ts +56 -1
  170. package/dist/session/session-start.js +384 -23
  171. package/dist/session/verify-session-ritual.d.ts +8 -0
  172. package/dist/session/verify-session-ritual.js +95 -35
  173. package/dist/staleness-tickler/state.js +26 -6
  174. package/dist/swarm/launch.js +13 -3
  175. package/dist/swarm/routing.js +9 -3
  176. package/dist/task-surface/index.js +24 -9
  177. package/dist/tool-events/classify.d.ts +20 -0
  178. package/dist/tool-events/classify.js +634 -0
  179. package/dist/tool-events/index.d.ts +10 -0
  180. package/dist/tool-events/index.js +10 -0
  181. package/dist/tool-events/summarize.d.ts +34 -0
  182. package/dist/tool-events/summarize.js +93 -0
  183. package/dist/tool-events/types.d.ts +52 -0
  184. package/dist/tool-events/types.js +13 -0
  185. package/dist/triage/bootstrap/gitignore.js +53 -10
  186. package/dist/triage/cache-path.js +10 -3
  187. package/dist/triage/welcome/summary.js +11 -4
  188. package/dist/triage/welcome/writers.js +45 -16
  189. package/dist/validate-content/validate-links.js +5 -0
  190. package/dist/value/readback.js +11 -6
  191. package/dist/vbrief-activate/activate.js +12 -4
  192. package/dist/vbrief-build/project-definition-io.js +14 -6
  193. package/dist/verify-source/contained-writes.d.ts +59 -0
  194. package/dist/verify-source/contained-writes.js +272 -0
  195. package/dist/verify-source/index.d.ts +1 -0
  196. package/dist/verify-source/index.js +1 -0
  197. package/dist/verify-source/openclaw-tier1.js +14 -1
  198. package/dist/verify-source/verify-stubs.js +3 -0
  199. package/dist/xbrief-migrate/migrate-project.js +26 -12
  200. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  201. package/dist/xbrief-migrate/transforms.js +37 -14
  202. package/package.json +19 -3
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Local xBRIEF lifecycle folder stats for weekly process rollups (#2995).
3
+ *
4
+ * Filesystem-only inventory of `xbrief/{proposed,pending,active,completed,cancelled}/`.
5
+ * No network calls.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
8
+ import { join, resolve } from "node:path";
9
+ import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
10
+ import { parseDurationMs } from "../triage/scope/duration.js";
11
+ /** Lifecycle folders scanned for stats. */
12
+ export const STATS_LIFECYCLE_FOLDERS = [
13
+ "proposed",
14
+ "pending",
15
+ "active",
16
+ "completed",
17
+ "cancelled",
18
+ ];
19
+ /** Count semantics for WWYSYDH / process forms (stable strings for --json). */
20
+ export const LIFECYCLE_STATS_SEMANTICS = {
21
+ promoted: "xbriefs currently in pending/ whose event time falls inside the --since window " +
22
+ "(promoted and still pending; later-activated work is not double-counted here).",
23
+ activated: "xbriefs currently in active/ whose event time falls inside the --since window " +
24
+ "(activated and still active).",
25
+ completed: "xbriefs currently in completed/ with plan.status completed (or unset, treated as completed) " +
26
+ "whose event time falls inside the --since window.",
27
+ cancelled_or_failed: "xbriefs currently in cancelled/, or in completed/ with plan.status failed, " +
28
+ "whose event time falls inside the --since window.",
29
+ still_active: "snapshot count of all xbriefs currently in active/ (not filtered by --since).",
30
+ event_time: "event time = most recent of plan.metadata.completedAt, plan.updated, and xBRIEFInfo/vBRIEFInfo.updated; " +
31
+ "else file mtime. Window is [as_of - since, as_of] inclusive of as_of.",
32
+ note: "Counts are current-folder membership, not full transition history. A brief that was " +
33
+ "promoted then activated in the same week appears under activated (and still_active), not promoted.",
34
+ };
35
+ function parseIso(value) {
36
+ if (typeof value !== "string" || value.trim().length === 0) {
37
+ return null;
38
+ }
39
+ const text = value.trim().endsWith("Z") ? `${value.trim().slice(0, -1)}+00:00` : value.trim();
40
+ const parsed = new Date(text);
41
+ if (Number.isNaN(parsed.getTime())) {
42
+ return null;
43
+ }
44
+ return parsed;
45
+ }
46
+ function readJsonObject(path) {
47
+ try {
48
+ const raw = readFileSync(path, "utf8");
49
+ const data = JSON.parse(raw);
50
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
51
+ return null;
52
+ }
53
+ return data;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ function planOf(data) {
60
+ const plan = data.plan;
61
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
62
+ return null;
63
+ }
64
+ return plan;
65
+ }
66
+ function infoUpdated(data) {
67
+ for (const key of ["xBRIEFInfo", "vBRIEFInfo"]) {
68
+ const info = data[key];
69
+ if (typeof info === "object" && info !== null && !Array.isArray(info)) {
70
+ const stamp = parseIso(info.updated);
71
+ if (stamp !== null) {
72
+ return stamp;
73
+ }
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ function completedAt(plan) {
79
+ const metadata = plan.metadata;
80
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
81
+ return null;
82
+ }
83
+ return parseIso(metadata.completedAt);
84
+ }
85
+ /**
86
+ * Event time for window membership: most recent of completedAt / plan.updated /
87
+ * info.updated, else mtime. Preferring max (not completedAt-first) so a later
88
+ * cancel/restore transition is not stuck on a stale completion stamp.
89
+ */
90
+ function eventTimeFor(data, plan, path) {
91
+ const candidates = [];
92
+ const completed = completedAt(plan);
93
+ if (completed !== null) {
94
+ candidates.push(completed);
95
+ }
96
+ const planUpdated = parseIso(plan.updated);
97
+ if (planUpdated !== null) {
98
+ candidates.push(planUpdated);
99
+ }
100
+ const info = infoUpdated(data);
101
+ if (info !== null) {
102
+ candidates.push(info);
103
+ }
104
+ if (candidates.length > 0) {
105
+ return new Date(Math.max(...candidates.map((d) => d.getTime())));
106
+ }
107
+ try {
108
+ return new Date(statSync(path).mtimeMs);
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /** Stats root: xbrief/ if present, else legacy vbrief/ (read-only inventory), else canonical xbrief path. */
115
+ function resolveStatsLifecycleRoot(projectRoot) {
116
+ const migrated = join(projectRoot, MIGRATED_ARTIFACT_DIR);
117
+ if (existsSync(migrated)) {
118
+ try {
119
+ if (statSync(migrated).isDirectory()) {
120
+ return migrated;
121
+ }
122
+ }
123
+ catch {
124
+ // fall through
125
+ }
126
+ }
127
+ const legacy = join(projectRoot, LEGACY_ARTIFACT_DIR);
128
+ if (existsSync(legacy)) {
129
+ try {
130
+ if (statSync(legacy).isDirectory()) {
131
+ return legacy;
132
+ }
133
+ }
134
+ catch {
135
+ // fall through
136
+ }
137
+ }
138
+ return migrated;
139
+ }
140
+ function statusOf(plan, folder) {
141
+ const raw = plan.status;
142
+ if (typeof raw === "string" && raw.trim().length > 0) {
143
+ return raw.trim();
144
+ }
145
+ if (folder === "completed") {
146
+ return "completed";
147
+ }
148
+ if (folder === "cancelled") {
149
+ return "cancelled";
150
+ }
151
+ if (folder === "pending") {
152
+ return "pending";
153
+ }
154
+ if (folder === "active") {
155
+ return "running";
156
+ }
157
+ return "proposed";
158
+ }
159
+ function inWindow(eventTime, windowStart, asOf) {
160
+ if (eventTime === null) {
161
+ return false;
162
+ }
163
+ const t = eventTime.getTime();
164
+ return t >= windowStart.getTime() && t <= asOf.getTime();
165
+ }
166
+ function emptyFolderTotals() {
167
+ return { proposed: 0, pending: 0, active: 0, completed: 0, cancelled: 0 };
168
+ }
169
+ function scanArtifacts(lifecycleRoot) {
170
+ const out = [];
171
+ for (const folder of STATS_LIFECYCLE_FOLDERS) {
172
+ const dir = join(lifecycleRoot, folder);
173
+ if (!existsSync(dir)) {
174
+ continue;
175
+ }
176
+ let names;
177
+ try {
178
+ names = readdirSync(dir)
179
+ .filter((name) => hasArtifactSuffix(name))
180
+ .sort();
181
+ }
182
+ catch {
183
+ continue;
184
+ }
185
+ for (const name of names) {
186
+ const path = join(dir, name);
187
+ const data = readJsonObject(path);
188
+ if (data === null) {
189
+ continue;
190
+ }
191
+ const plan = planOf(data);
192
+ if (plan === null) {
193
+ continue;
194
+ }
195
+ out.push({
196
+ folder,
197
+ status: statusOf(plan, folder),
198
+ eventTime: eventTimeFor(data, plan, path),
199
+ });
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+ function utcIso(dt) {
205
+ return `${dt.toISOString().slice(0, 19)}Z`;
206
+ }
207
+ /**
208
+ * Collect lifecycle folder stats for a project root.
209
+ * Offline / filesystem only.
210
+ */
211
+ export function collectLifecycleStats(options) {
212
+ const sinceRaw = (options.since ?? "7d").trim() || "7d";
213
+ const sinceMs = parseDurationMs(sinceRaw);
214
+ const asOf = options.now ?? new Date();
215
+ const windowStart = new Date(asOf.getTime() - sinceMs);
216
+ const projectRoot = resolve(options.projectRoot);
217
+ const lifecycleRoot = resolveStatsLifecycleRoot(projectRoot);
218
+ const folder_totals = emptyFolderTotals();
219
+ let promoted = 0;
220
+ let activated = 0;
221
+ let completed = 0;
222
+ let cancelled_or_failed = 0;
223
+ let still_active = 0;
224
+ const records = existsSync(lifecycleRoot) ? scanArtifacts(lifecycleRoot) : [];
225
+ for (const rec of records) {
226
+ folder_totals[rec.folder] += 1;
227
+ if (rec.folder === "active") {
228
+ still_active += 1;
229
+ }
230
+ const win = inWindow(rec.eventTime, windowStart, asOf);
231
+ if (!win) {
232
+ continue;
233
+ }
234
+ if (rec.folder === "pending") {
235
+ promoted += 1;
236
+ }
237
+ else if (rec.folder === "active") {
238
+ activated += 1;
239
+ }
240
+ else if (rec.folder === "cancelled") {
241
+ cancelled_or_failed += 1;
242
+ }
243
+ else if (rec.folder === "completed") {
244
+ if (rec.status === "failed") {
245
+ cancelled_or_failed += 1;
246
+ }
247
+ else if (rec.status === "completed") {
248
+ completed += 1;
249
+ }
250
+ // Inconsistent statuses under completed/ (e.g. stale "running") are
251
+ // counted in folder_totals only — not in window metrics.
252
+ }
253
+ }
254
+ return {
255
+ since: sinceRaw,
256
+ since_ms: sinceMs,
257
+ as_of: utcIso(asOf),
258
+ window_start: utcIso(windowStart),
259
+ project_root: projectRoot,
260
+ lifecycle_root: lifecycleRoot,
261
+ promoted,
262
+ activated,
263
+ completed,
264
+ cancelled_or_failed,
265
+ still_active,
266
+ folder_totals: { ...folder_totals },
267
+ semantics: LIFECYCLE_STATS_SEMANTICS,
268
+ };
269
+ }
270
+ /** Format human-readable lifecycle stats text. */
271
+ export function formatLifecycleStatsText(stats) {
272
+ const lines = [
273
+ `lifecycle:stats (since ${stats.since}, as of ${stats.as_of})`,
274
+ ` window: ${stats.window_start} → ${stats.as_of}`,
275
+ ` promoted: ${stats.promoted}`,
276
+ ` activated: ${stats.activated}`,
277
+ ` completed: ${stats.completed}`,
278
+ ` cancelled_or_failed: ${stats.cancelled_or_failed}`,
279
+ ` still_active: ${stats.still_active}`,
280
+ ` folder_totals: proposed=${stats.folder_totals.proposed} pending=${stats.folder_totals.pending} active=${stats.folder_totals.active} completed=${stats.folder_totals.completed} cancelled=${stats.folder_totals.cancelled}`,
281
+ ];
282
+ return `${lines.join("\n")}\n`;
283
+ }
284
+ /** Re-export duration parse for CLI error messaging. */
285
+ export { parseDurationMs };
286
+ //# sourceMappingURL=stats.js.map
@@ -1,8 +1,21 @@
1
1
  /** Port of scripts/pack_render.py — content-pack projection renderer (#1294, #1295). */
2
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
3
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { contentRoot } from "../content-root.js";
6
+ import { containedWrite } from "../fs/contained-write.js";
7
+ /** Write under the parent directory as containment root (#2980 wave D). */
8
+ function writeContainedFile(output, text) {
9
+ const abs = resolve(output);
10
+ const dir = dirname(abs);
11
+ mkdirSync(dir, { recursive: true });
12
+ containedWrite({
13
+ root: dir,
14
+ target: basename(abs),
15
+ data: text,
16
+ mode: "replace",
17
+ });
18
+ }
6
19
  export function resolveRepoRoot() {
7
20
  return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
8
21
  }
@@ -382,8 +395,8 @@ export function renderFile(source) {
382
395
  }
383
396
  export function writeRender(source, output) {
384
397
  const text = renderFile(source);
385
- mkdirSync(dirname(output), { recursive: true });
386
- writeFileSync(output, text, "utf8");
398
+ // #2980 wave D: product write sink routes through containedWrite.
399
+ writeContainedFile(output, text);
387
400
  return text;
388
401
  }
389
402
  export function checkDrift(source, output) {
@@ -441,9 +454,9 @@ function runMultipack(packFilter, check) {
441
454
  process.stdout.write(`pack-drift: all ${targets.length} projection(s) in sync.\n`);
442
455
  return 0;
443
456
  }
457
+ // #2980 wave D: product write sinks route through containedWrite.
444
458
  for (const [_name, outPath, text] of targets) {
445
- mkdirSync(dirname(outPath), { recursive: true });
446
- writeFileSync(outPath, text, "utf8");
459
+ writeContainedFile(outPath, text);
447
460
  }
448
461
  process.stdout.write(`Rendered ${targets.length} projection(s) across ${Object.keys(RENDER_REGISTRY).length} pack(s).\n`);
449
462
  return 0;
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { containedWrite } from "../fs/contained-write.js";
3
4
  import { PLAN_SEQUENCE_FILENAME, parsePlanSequence } from "./types.js";
4
5
  export function planSequencePath(projectRoot) {
5
6
  return join(projectRoot, ".deft", PLAN_SEQUENCE_FILENAME);
@@ -14,8 +15,14 @@ export function readPlanSequence(projectRoot) {
14
15
  }
15
16
  export function writePlanSequence(projectRoot, sequence) {
16
17
  const path = planSequencePath(projectRoot);
17
- mkdirSync(dirname(path), { recursive: true });
18
- writeFileSync(path, `${JSON.stringify(sequence, null, 2)}\n`, "utf8");
18
+ // #2980 wave D: product write sink routes through containedWrite.
19
+ const root = resolve(projectRoot);
20
+ containedWrite({
21
+ root,
22
+ target: path,
23
+ data: `${JSON.stringify(sequence, null, 2)}\n`,
24
+ mode: "replace",
25
+ });
19
26
  return path;
20
27
  }
21
28
  export function clearPlanSequence(projectRoot) {
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Typed hotfix classifier (#1193 R2 / Wave 2 of #2948).
3
+ *
4
+ * Hard structural predicates — not agent judgement. Agent may propose the
5
+ * label `hotfix-candidate` only; a human promotes to `hotfix`. Pipeline keys
6
+ * off `hotfix` exclusively; `hotfix-candidate` has zero pipeline meaning.
7
+ */
8
+ import type { HotfixCriteria } from "@deftai/directive-types";
9
+ export declare const FIELD_HOTFIX_CRITERIA = "plan.policy.hotfixCriteria";
10
+ export declare const FIELD_HOTFIX_CRITERIA_CLI_ALIAS = "hotfixCriteria";
11
+ /** Agent-proposed beacon only — never pipeline-authorizing. */
12
+ export declare const HOTFIX_CANDIDATE_LABEL: "hotfix-candidate";
13
+ /** Human-promoted label; deploy pipeline keys off this exclusively. */
14
+ export declare const HOTFIX_LABEL: "hotfix";
15
+ export declare const DEFAULT_HOTFIX_MAX_LINES = 10;
16
+ export declare const DEFAULT_HOTFIX_MAX_FILES = 2;
17
+ /** Paths that never qualify as hotfix (deploy / CI / migrations / secrets). */
18
+ export declare const DEFAULT_FORBIDDEN_PATH_GLOBS: readonly string[];
19
+ export declare const DEFAULT_HOTFIX_CRITERIA: Required<HotfixCriteria>;
20
+ export interface HotfixEligibilityInput {
21
+ /** Total lines added+removed (or equivalent structural line count). */
22
+ readonly linesChanged: number;
23
+ /** Distinct files touched. */
24
+ readonly filesChanged: number;
25
+ /** Project-relative POSIX paths in the diff. */
26
+ readonly paths: readonly string[];
27
+ /** Pure revert of a prior change — always eligible. */
28
+ readonly isPureRevert?: boolean;
29
+ /** One-character / trivial edit signal (always eligible when true with restoresGreen). */
30
+ readonly isOneCharacterEdit?: boolean;
31
+ /** Refactors of any size are never hotfix. */
32
+ readonly isRefactor?: boolean;
33
+ /** New package / dependency entries. */
34
+ readonly addsNewDependency?: boolean;
35
+ /** Adds, removes, or renames an exported symbol/surface. */
36
+ readonly changesExportedSurface?: boolean;
37
+ /** Schema or migration changes. */
38
+ readonly touchesSchemaOrMigration?: boolean;
39
+ /** New handler/route/media-type surface. */
40
+ readonly addsNewHandlerOrRoute?: boolean;
41
+ /** Build was green on main; this change restores green. */
42
+ readonly restoresGreen?: boolean;
43
+ /** Criteria override (defaults + project typed policy). */
44
+ readonly criteria?: HotfixCriteria | null;
45
+ }
46
+ export type HotfixDenyReason = "refactor" | "new-export" | "new-dependency" | "schema-migration" | "new-handler-route" | "forbidden-path" | "too-many-lines" | "too-many-files" | "not-restoring-green";
47
+ export interface HotfixEligibilityResult {
48
+ readonly eligible: boolean;
49
+ /** When eligible, the agent may apply this label only. */
50
+ readonly proposedLabel: typeof HOTFIX_CANDIDATE_LABEL | null;
51
+ readonly reasons: readonly string[];
52
+ readonly denyCodes: readonly HotfixDenyReason[];
53
+ readonly criteria: Required<HotfixCriteria>;
54
+ }
55
+ /**
56
+ * Pure hotfix eligibility evaluator (#1193 R2).
57
+ *
58
+ * Eligible (agent may propose `hotfix-candidate`):
59
+ * - pure revert always
60
+ * - one-character edit that restores green
61
+ * - small fix: ≤ maxLines AND ≤ maxFiles, no new deps/exports/schema, no forbidden
62
+ * paths, not a refactor, restores green
63
+ *
64
+ * Never eligible: refactors, new handlers/routes, export surface changes, forbidden paths.
65
+ */
66
+ export declare function evaluateHotfixEligibility(input: HotfixEligibilityInput): HotfixEligibilityResult;
67
+ /** Resolve typed hotfixCriteria from a raw policy block value. */
68
+ export declare function resolveHotfixCriteria(raw: unknown): Required<HotfixCriteria>;
69
+ export interface HotfixCriteriaPolicyField {
70
+ readonly name: string;
71
+ readonly current: Required<HotfixCriteria>;
72
+ readonly default: Required<HotfixCriteria>;
73
+ readonly source: string;
74
+ }
75
+ /** Inspector row for `policy:show --field=hotfixCriteria`. */
76
+ export declare function inspectHotfixCriteria(data: Record<string, unknown> | null): HotfixCriteriaPolicyField;
77
+ /** Load hotfix criteria from project root. */
78
+ export declare function loadHotfixCriteriaFromProject(projectRoot: string): Required<HotfixCriteria>;
79
+ //# sourceMappingURL=hotfix-criteria.d.ts.map
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Typed hotfix classifier (#1193 R2 / Wave 2 of #2948).
3
+ *
4
+ * Hard structural predicates — not agent judgement. Agent may propose the
5
+ * label `hotfix-candidate` only; a human promotes to `hotfix`. Pipeline keys
6
+ * off `hotfix` exclusively; `hotfix-candidate` has zero pipeline meaning.
7
+ */
8
+ import { matchAny } from "../orchestration/pathspec.js";
9
+ import { readPlanPolicy } from "./plan-extensions.js";
10
+ import { loadProjectDefinition } from "./resolve.js";
11
+ export const FIELD_HOTFIX_CRITERIA = "plan.policy.hotfixCriteria";
12
+ export const FIELD_HOTFIX_CRITERIA_CLI_ALIAS = "hotfixCriteria";
13
+ /** Agent-proposed beacon only — never pipeline-authorizing. */
14
+ export const HOTFIX_CANDIDATE_LABEL = "hotfix-candidate";
15
+ /** Human-promoted label; deploy pipeline keys off this exclusively. */
16
+ export const HOTFIX_LABEL = "hotfix";
17
+ export const DEFAULT_HOTFIX_MAX_LINES = 10;
18
+ export const DEFAULT_HOTFIX_MAX_FILES = 2;
19
+ /** Paths that never qualify as hotfix (deploy / CI / migrations / secrets). */
20
+ export const DEFAULT_FORBIDDEN_PATH_GLOBS = [
21
+ "Dockerfile",
22
+ "**/Dockerfile",
23
+ "fly.toml",
24
+ "**/fly.toml",
25
+ ".github/workflows/**",
26
+ "migrations/**",
27
+ "**/migrations/**",
28
+ "**/*secret*",
29
+ "**/*billing*",
30
+ "**/.env",
31
+ "**/.env.*",
32
+ "**/auth/**",
33
+ "**/secrets/**",
34
+ ];
35
+ export const DEFAULT_HOTFIX_CRITERIA = {
36
+ maxLines: DEFAULT_HOTFIX_MAX_LINES,
37
+ maxFiles: DEFAULT_HOTFIX_MAX_FILES,
38
+ forbiddenPathGlobs: DEFAULT_FORBIDDEN_PATH_GLOBS,
39
+ };
40
+ function normalizeCriteria(raw) {
41
+ const maxLines = typeof raw?.maxLines === "number" && Number.isInteger(raw.maxLines) && raw.maxLines >= 0
42
+ ? raw.maxLines
43
+ : DEFAULT_HOTFIX_MAX_LINES;
44
+ const maxFiles = typeof raw?.maxFiles === "number" && Number.isInteger(raw.maxFiles) && raw.maxFiles >= 0
45
+ ? raw.maxFiles
46
+ : DEFAULT_HOTFIX_MAX_FILES;
47
+ const forbidden = Array.isArray(raw?.forbiddenPathGlobs) && raw.forbiddenPathGlobs.length > 0
48
+ ? raw.forbiddenPathGlobs.filter((g) => typeof g === "string" && g.length > 0)
49
+ : DEFAULT_FORBIDDEN_PATH_GLOBS;
50
+ return { maxLines, maxFiles, forbiddenPathGlobs: forbidden };
51
+ }
52
+ function pathIsForbidden(path, globs) {
53
+ const posix = path.replace(/\\/g, "/");
54
+ // Basename-only defaults (Dockerfile, fly.toml) match any depth.
55
+ const base = posix.includes("/") ? posix.slice(posix.lastIndexOf("/") + 1) : posix;
56
+ for (const g of globs) {
57
+ if (!g.includes("/") && !g.includes("*") && base === g)
58
+ return true;
59
+ }
60
+ return matchAny(globs, posix);
61
+ }
62
+ /**
63
+ * Pure hotfix eligibility evaluator (#1193 R2).
64
+ *
65
+ * Eligible (agent may propose `hotfix-candidate`):
66
+ * - pure revert always
67
+ * - one-character edit that restores green
68
+ * - small fix: ≤ maxLines AND ≤ maxFiles, no new deps/exports/schema, no forbidden
69
+ * paths, not a refactor, restores green
70
+ *
71
+ * Never eligible: refactors, new handlers/routes, export surface changes, forbidden paths.
72
+ */
73
+ export function evaluateHotfixEligibility(input) {
74
+ const criteria = normalizeCriteria(input.criteria);
75
+ const denyCodes = [];
76
+ const reasons = [];
77
+ if (input.isPureRevert === true) {
78
+ return {
79
+ eligible: true,
80
+ proposedLabel: HOTFIX_CANDIDATE_LABEL,
81
+ reasons: ["pure-revert always qualifies as hotfix-candidate"],
82
+ denyCodes: [],
83
+ criteria,
84
+ };
85
+ }
86
+ if (input.isOneCharacterEdit === true && input.restoresGreen !== false) {
87
+ return {
88
+ eligible: true,
89
+ proposedLabel: HOTFIX_CANDIDATE_LABEL,
90
+ reasons: ["one-character edit that restores green qualifies as hotfix-candidate"],
91
+ denyCodes: [],
92
+ criteria,
93
+ };
94
+ }
95
+ if (input.isRefactor === true) {
96
+ denyCodes.push("refactor");
97
+ reasons.push("refactors of any size are never hotfix");
98
+ }
99
+ if (input.changesExportedSurface === true) {
100
+ denyCodes.push("new-export");
101
+ reasons.push("exported surface add/remove/rename is never hotfix");
102
+ }
103
+ if (input.addsNewDependency === true) {
104
+ denyCodes.push("new-dependency");
105
+ reasons.push("new dependencies are never hotfix");
106
+ }
107
+ if (input.touchesSchemaOrMigration === true) {
108
+ denyCodes.push("schema-migration");
109
+ reasons.push("schema/migration changes are never hotfix");
110
+ }
111
+ if (input.addsNewHandlerOrRoute === true) {
112
+ denyCodes.push("new-handler-route");
113
+ reasons.push("new handlers/routes/media types are never hotfix");
114
+ }
115
+ for (const p of input.paths) {
116
+ if (pathIsForbidden(p, criteria.forbiddenPathGlobs)) {
117
+ denyCodes.push("forbidden-path");
118
+ reasons.push(`forbidden path: ${p}`);
119
+ break;
120
+ }
121
+ }
122
+ if (input.linesChanged > criteria.maxLines) {
123
+ denyCodes.push("too-many-lines");
124
+ reasons.push(`linesChanged ${input.linesChanged} > maxLines ${criteria.maxLines}`);
125
+ }
126
+ if (input.filesChanged > criteria.maxFiles) {
127
+ denyCodes.push("too-many-files");
128
+ reasons.push(`filesChanged ${input.filesChanged} > maxFiles ${criteria.maxFiles}`);
129
+ }
130
+ // Small-fix path requires restoring green unless pure-revert/one-char already returned.
131
+ if (input.restoresGreen === false) {
132
+ denyCodes.push("not-restoring-green");
133
+ reasons.push("change does not restore green on main");
134
+ }
135
+ if (denyCodes.length > 0) {
136
+ return {
137
+ eligible: false,
138
+ proposedLabel: null,
139
+ reasons,
140
+ denyCodes,
141
+ criteria,
142
+ };
143
+ }
144
+ return {
145
+ eligible: true,
146
+ proposedLabel: HOTFIX_CANDIDATE_LABEL,
147
+ reasons: [
148
+ `small fix within limits (≤${criteria.maxLines} lines, ≤${criteria.maxFiles} files); ` +
149
+ "agent may apply hotfix-candidate only — human promotes hotfix",
150
+ ],
151
+ denyCodes: [],
152
+ criteria,
153
+ };
154
+ }
155
+ /** Resolve typed hotfixCriteria from a raw policy block value. */
156
+ export function resolveHotfixCriteria(raw) {
157
+ if (raw === null || raw === undefined)
158
+ return { ...DEFAULT_HOTFIX_CRITERIA };
159
+ if (typeof raw !== "object" || Array.isArray(raw))
160
+ return { ...DEFAULT_HOTFIX_CRITERIA };
161
+ return normalizeCriteria(raw);
162
+ }
163
+ /** Inspector row for `policy:show --field=hotfixCriteria`. */
164
+ export function inspectHotfixCriteria(data) {
165
+ const defaults = { ...DEFAULT_HOTFIX_CRITERIA };
166
+ if (data === null) {
167
+ return { name: FIELD_HOTFIX_CRITERIA, current: defaults, default: defaults, source: "default" };
168
+ }
169
+ const policyBlock = readPlanPolicy(data.plan);
170
+ if (typeof policyBlock !== "object" ||
171
+ policyBlock === null ||
172
+ Array.isArray(policyBlock) ||
173
+ !("hotfixCriteria" in policyBlock)) {
174
+ return { name: FIELD_HOTFIX_CRITERIA, current: defaults, default: defaults, source: "default" };
175
+ }
176
+ return {
177
+ name: FIELD_HOTFIX_CRITERIA,
178
+ current: resolveHotfixCriteria(policyBlock.hotfixCriteria),
179
+ default: defaults,
180
+ source: "typed",
181
+ };
182
+ }
183
+ /** Load hotfix criteria from project root. */
184
+ export function loadHotfixCriteriaFromProject(projectRoot) {
185
+ const [data] = loadProjectDefinition(projectRoot);
186
+ if (data === null)
187
+ return { ...DEFAULT_HOTFIX_CRITERIA };
188
+ const policyBlock = readPlanPolicy(data.plan);
189
+ if (typeof policyBlock !== "object" ||
190
+ policyBlock === null ||
191
+ Array.isArray(policyBlock) ||
192
+ !("hotfixCriteria" in policyBlock)) {
193
+ return { ...DEFAULT_HOTFIX_CRITERIA };
194
+ }
195
+ return resolveHotfixCriteria(policyBlock.hotfixCriteria);
196
+ }
197
+ //# sourceMappingURL=hotfix-criteria.js.map
@@ -4,17 +4,21 @@ export * from "./capacity.js";
4
4
  export * from "./decisions.js";
5
5
  export * from "./disclosure.js";
6
6
  export * from "./host-hooks.js";
7
+ export * from "./hotfix-criteria.js";
8
+ export * from "./intent-ceiling.js";
7
9
  export * from "./no-deft-directive.js";
8
10
  export * from "./org-force-on-migration.js";
9
11
  export * from "./plan-extensions.js";
10
12
  export * from "./policy-invocation.js";
11
13
  export * from "./product-signal.js";
14
+ export * from "./require-human-merge.js";
12
15
  export * from "./resolve.js";
13
16
  export * from "./runtime-authority.js";
14
17
  export * from "./staleness-tickler.js";
15
18
  export * from "./value-feedback.js";
16
19
  export * from "./value-feedback-autoenable.js";
17
20
  export * from "./wip.js";
21
+ export * from "./write-fence.js";
18
22
  export declare const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
19
23
  export declare const FIELD_WIP_CAP = "plan.policy.wipCap";
20
24
  export declare const FIELD_SESSION_RITUAL_STALENESS_HOURS = "plan.policy.sessionRitualStalenessHours";