@deftai/directive-core 0.87.0 → 0.89.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 (193) 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/scanner.d.ts +11 -1
  28. package/dist/cache/scanner.js +29 -4
  29. package/dist/cache/task-cache/store.js +11 -7
  30. package/dist/capacity/backfill.js +10 -4
  31. package/dist/category-b-namespace/index.js +10 -4
  32. package/dist/check/gate-lists.js +1 -0
  33. package/dist/codebase/default-extractor.js +3 -0
  34. package/dist/codebase/map.js +11 -4
  35. package/dist/content-contracts/skills/helpers.d.ts +10 -0
  36. package/dist/content-contracts/skills/helpers.js +35 -0
  37. package/dist/deposit/copy-tree.d.ts +19 -1
  38. package/dist/deposit/copy-tree.js +134 -5
  39. package/dist/doctor/doctor-state.js +28 -4
  40. package/dist/doctor/main.d.ts +10 -0
  41. package/dist/doctor/main.js +208 -0
  42. package/dist/doctor/types.d.ts +11 -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/fs/projection-containment.d.ts +18 -0
  77. package/dist/fs/projection-containment.js +40 -0
  78. package/dist/hooks/classify/classify.d.ts +10 -0
  79. package/dist/hooks/classify/classify.js +37 -0
  80. package/dist/hooks/classify/index.d.ts +14 -0
  81. package/dist/hooks/classify/index.js +13 -0
  82. package/dist/hooks/classify/paths.d.ts +22 -0
  83. package/dist/hooks/classify/paths.js +72 -0
  84. package/dist/hooks/classify/payload.d.ts +16 -0
  85. package/dist/hooks/classify/payload.js +45 -0
  86. package/dist/hooks/classify/stdin.d.ts +12 -0
  87. package/dist/hooks/classify/stdin.js +63 -0
  88. package/dist/hooks/classify/tool-name.d.ts +18 -0
  89. package/dist/hooks/classify/tool-name.js +85 -0
  90. package/dist/hooks/classify/types.d.ts +41 -0
  91. package/dist/hooks/classify/types.js +7 -0
  92. package/dist/hooks/dispatcher.d.ts +30 -15
  93. package/dist/hooks/dispatcher.js +326 -134
  94. package/dist/hooks/fixtures/cases.d.ts +44 -0
  95. package/dist/hooks/fixtures/cases.js +595 -0
  96. package/dist/hooks/fixtures/index.d.ts +2 -0
  97. package/dist/hooks/fixtures/index.js +2 -0
  98. package/dist/hooks/index.d.ts +2 -0
  99. package/dist/hooks/index.js +2 -0
  100. package/dist/hooks/tools.d.ts +31 -0
  101. package/dist/hooks/tools.js +74 -0
  102. package/dist/index.d.ts +4 -0
  103. package/dist/index.js +5 -0
  104. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  105. package/dist/init-deposit/agent-hooks.js +65 -10
  106. package/dist/init-deposit/gitignore.js +11 -3
  107. package/dist/init-deposit/headless-manifest.js +12 -4
  108. package/dist/init-deposit/hygiene.d.ts +16 -0
  109. package/dist/init-deposit/hygiene.js +26 -0
  110. package/dist/init-deposit/init-dispatch.js +28 -0
  111. package/dist/init-deposit/migrate.d.ts +1 -1
  112. package/dist/init-deposit/migrate.js +13 -3
  113. package/dist/init-deposit/prettierignore.js +12 -5
  114. package/dist/init-deposit/refresh.js +38 -7
  115. package/dist/init-deposit/scaffold.js +42 -29
  116. package/dist/init-deposit/xbrief-projections.js +17 -10
  117. package/dist/intake/candidates-log.js +46 -35
  118. package/dist/intake/github-body-cli.d.ts +6 -0
  119. package/dist/intake/github-body-cli.js +17 -0
  120. package/dist/intake/github-body.d.ts +46 -3
  121. package/dist/intake/github-body.js +191 -20
  122. package/dist/intake/issue-emit.d.ts +1 -0
  123. package/dist/intake/issue-emit.js +41 -23
  124. package/dist/intake/issue-ingest.js +11 -2
  125. package/dist/lifecycle/event-detect.js +12 -4
  126. package/dist/lifecycle/events.js +29 -20
  127. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  128. package/dist/packs/pack-render.d.ts +33 -0
  129. package/dist/packs/pack-render.js +172 -13
  130. package/dist/packs/quarantine-ext.d.ts +10 -0
  131. package/dist/packs/quarantine-ext.js +26 -2
  132. package/dist/plan-sequence/store.js +11 -4
  133. package/dist/policy/hotfix-criteria.d.ts +79 -0
  134. package/dist/policy/hotfix-criteria.js +197 -0
  135. package/dist/policy/index.d.ts +5 -0
  136. package/dist/policy/index.js +32 -1
  137. package/dist/policy/intent-ceiling.d.ts +79 -0
  138. package/dist/policy/intent-ceiling.js +181 -0
  139. package/dist/policy/no-deft-directive.d.ts +59 -0
  140. package/dist/policy/no-deft-directive.js +110 -0
  141. package/dist/policy/org-force-on-migration.d.ts +52 -0
  142. package/dist/policy/org-force-on-migration.js +269 -27
  143. package/dist/policy/require-human-merge.d.ts +75 -0
  144. package/dist/policy/require-human-merge.js +324 -0
  145. package/dist/policy/resolve.js +17 -6
  146. package/dist/policy/runtime-authority.d.ts +56 -2
  147. package/dist/policy/runtime-authority.js +297 -3
  148. package/dist/policy/write-fence.d.ts +78 -0
  149. package/dist/policy/write-fence.js +164 -0
  150. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  151. package/dist/pr-wait-mergeable/cascade.js +28 -0
  152. package/dist/preflight/evaluate.js +10 -0
  153. package/dist/product-signal/consent.js +21 -5
  154. package/dist/product-signal/submit.js +22 -12
  155. package/dist/release/build-dist-runner.js +5 -2
  156. package/dist/release/build-dist.d.ts +19 -1
  157. package/dist/release/build-dist.js +50 -2
  158. package/dist/release/native-steps.js +7 -2
  159. package/dist/release-publish/pipeline.d.ts +13 -0
  160. package/dist/release-publish/pipeline.js +46 -1
  161. package/dist/scope/audit-log.js +19 -24
  162. package/dist/scope/decompose.js +10 -5
  163. package/dist/scope/decomposed-refs.js +18 -3
  164. package/dist/scope/demote.js +9 -2
  165. package/dist/scope/undo.js +9 -2
  166. package/dist/session/release-availability.js +26 -6
  167. package/dist/session/ritual-sentinel.js +19 -12
  168. package/dist/session/session-start-hook.d.ts +3 -0
  169. package/dist/session/session-start-hook.js +15 -0
  170. package/dist/session/session-start.js +37 -0
  171. package/dist/staleness-tickler/state.js +26 -6
  172. package/dist/swarm/launch.js +13 -3
  173. package/dist/swarm/routing.js +9 -3
  174. package/dist/task-surface/index.js +24 -9
  175. package/dist/triage/bootstrap/gitignore.js +53 -10
  176. package/dist/triage/cache-path.js +10 -3
  177. package/dist/triage/welcome/summary.js +11 -4
  178. package/dist/triage/welcome/writers.js +45 -16
  179. package/dist/validate-content/validate-links.js +5 -0
  180. package/dist/value/readback.js +11 -6
  181. package/dist/vbrief-activate/activate.js +12 -4
  182. package/dist/vbrief-build/project-definition-io.js +14 -6
  183. package/dist/verify-source/contained-writes.d.ts +59 -0
  184. package/dist/verify-source/contained-writes.js +272 -0
  185. package/dist/verify-source/cursor-tier1.js +7 -2
  186. package/dist/verify-source/index.d.ts +1 -0
  187. package/dist/verify-source/index.js +1 -0
  188. package/dist/verify-source/openclaw-tier1.js +19 -1
  189. package/dist/verify-source/verify-stubs.js +3 -0
  190. package/dist/xbrief-migrate/migrate-project.js +26 -12
  191. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  192. package/dist/xbrief-migrate/transforms.js +37 -14
  193. package/package.json +16 -3
@@ -127,7 +127,7 @@ export function loadRuntimeAuthorityFromProject(projectRoot) {
127
127
  const [data] = loadProjectDefinition(projectRoot);
128
128
  return loadRuntimeAuthorityPolicy(data);
129
129
  }
130
- /** Evaluate a project-relative POSIX path against allow/deny globs. */
130
+ /** Evaluate a project-relative POSIX path against allow/deny globs (+ optional story layer). */
131
131
  export function evaluateRuntimeAuthorityPath(policy, relPathPosix) {
132
132
  if (!policy.enabled)
133
133
  return "allow";
@@ -136,8 +136,20 @@ export function evaluateRuntimeAuthorityPath(policy, relPathPosix) {
136
136
  if (policy.allowPaths.length > 0 && !matchAny(policy.allowPaths, relPathPosix)) {
137
137
  return "deny-allowlist";
138
138
  }
139
+ // Story file_scope layer (#516 / #2443): path must match when non-empty.
140
+ const storyAllow = policy.storyAllowPaths ?? [];
141
+ if (storyAllow.length > 0 && !matchAny(storyAllow, relPathPosix)) {
142
+ return "deny-story-scope";
143
+ }
139
144
  return "allow";
140
145
  }
146
+ /** Human-readable fence source label for deny reasons. */
147
+ function fenceSourceLabel(policy) {
148
+ const sources = policy.fenceSources;
149
+ if (sources === undefined || sources.length === 0)
150
+ return "project";
151
+ return sources.join("+");
152
+ }
141
153
  /**
142
154
  * Evaluate direct-write runtime authority after ritual/scope gates (#1394).
143
155
  * Unclassifiable paths (null) pass — host gap documented for Shell/MCP.
@@ -164,7 +176,7 @@ export function evaluateRuntimeAuthorityDirectWrite(input) {
164
176
  allowed: false,
165
177
  code: "runtime-policy-deny-path",
166
178
  reason: `Directive denied this direct write: path ${relPathPosix} matches ` +
167
- "plan.policy.runtimeAuthority.denyPaths.",
179
+ `write-fence denyPaths (source: ${fenceSourceLabel(policy)}).`,
168
180
  };
169
181
  }
170
182
  if (pathVerdict === "deny-allowlist") {
@@ -172,9 +184,291 @@ export function evaluateRuntimeAuthorityDirectWrite(input) {
172
184
  allowed: false,
173
185
  code: "runtime-policy-deny-path",
174
186
  reason: `Directive denied this direct write: path ${relPathPosix} is outside ` +
175
- "plan.policy.runtimeAuthority.allowPaths.",
187
+ `write fence project allowPaths (source: ${fenceSourceLabel(policy)}).`,
188
+ };
189
+ }
190
+ if (pathVerdict === "deny-story-scope") {
191
+ return {
192
+ allowed: false,
193
+ code: "runtime-policy-deny-path",
194
+ reason: `Directive denied this direct write: path ${relPathPosix} is outside ` +
195
+ `write fence story file_scope (source: ${fenceSourceLabel(policy)}).`,
176
196
  };
177
197
  }
178
198
  return { allowed: true, reason: null, code: null };
179
199
  }
200
+ /** True when token is a shell env assignment (FOO=1 / FOO=). Linear; no nested quantifiers. */
201
+ function isShellEnvAssignToken(token) {
202
+ const eq = token.indexOf("=");
203
+ if (eq <= 0)
204
+ return false;
205
+ const name = token.slice(0, eq);
206
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
207
+ return false;
208
+ return true;
209
+ }
210
+ /**
211
+ * Normalize a shell token for classification (#2711).
212
+ * Shell strips quotes, empty quote pairs, and backslash escapes before exec
213
+ * (`g''it` / `g\it` / `'push'` → `git` / `push`). Dropping `'`/`"`/`\` after
214
+ * whitespace split closes those bypasses without nested-quantifier regex. O(n).
215
+ */
216
+ function normalizeShellToken(token) {
217
+ return token.replace(/['"\\]/g, "");
218
+ }
219
+ /**
220
+ * Split a shell command into list/pipeline/newline segments without splitting
221
+ * on separators that appear inside quotes or after a backslash escape (#2711).
222
+ * Prevents `printf '%s' ';' 'git push'` and `hello\; git push` false denials.
223
+ */
224
+ function splitShellSegments(command) {
225
+ const segments = [];
226
+ let cur = "";
227
+ let quote = null;
228
+ for (let i = 0; i < command.length; i++) {
229
+ const c = command[i];
230
+ if (c === undefined)
231
+ break;
232
+ if (quote !== null) {
233
+ if (c === quote)
234
+ quote = null;
235
+ cur += c;
236
+ continue;
237
+ }
238
+ // Outside quotes: backslash escapes the next character (including separators).
239
+ if (c === "\\" && i + 1 < command.length) {
240
+ cur += c;
241
+ cur += command[i + 1] ?? "";
242
+ i++;
243
+ continue;
244
+ }
245
+ if (c === "'" || c === '"') {
246
+ quote = c;
247
+ cur += c;
248
+ continue;
249
+ }
250
+ // Outside quotes: list/pipeline separators and newlines start a new segment.
251
+ if (c === "&" && command[i + 1] === "&") {
252
+ segments.push(cur);
253
+ cur = "";
254
+ i++;
255
+ continue;
256
+ }
257
+ if (c === "|" && command[i + 1] === "|") {
258
+ segments.push(cur);
259
+ cur = "";
260
+ i++;
261
+ continue;
262
+ }
263
+ if (c === ";" || c === "|" || c === "&" || c === "\n" || c === "\r") {
264
+ segments.push(cur);
265
+ cur = "";
266
+ continue;
267
+ }
268
+ cur += c;
269
+ }
270
+ segments.push(cur);
271
+ return segments;
272
+ }
273
+ /** Git global options that take a separate value token (not `--opt=value`). */
274
+ const GIT_GLOBAL_VALUE_OPTS = new Set([
275
+ "-C",
276
+ "-c",
277
+ "--git-dir",
278
+ "--work-tree",
279
+ "--namespace",
280
+ "--config-env",
281
+ "--super-prefix",
282
+ "--list-cmds",
283
+ ]);
284
+ /**
285
+ * Classify one shell list/pipeline segment for push/merge (#2711).
286
+ * Token walk is O(n) — avoids nested-quantifier ReDoS that CodeQL flags on
287
+ * `git (?:options)* push` style regexes (alerts #77 / #78 on this PR).
288
+ */
289
+ function classifyShellSegment(segment) {
290
+ const tokens = segment
291
+ .trim()
292
+ .split(/\s+/)
293
+ .filter((t) => t.length > 0);
294
+ let i = 0;
295
+ while (i < tokens.length) {
296
+ const tok = tokens[i];
297
+ if (tok === undefined || !isShellEnvAssignToken(tok))
298
+ break;
299
+ i++;
300
+ }
301
+ const wrapTok = tokens[i];
302
+ if (wrapTok !== undefined) {
303
+ const wrap = normalizeShellToken(wrapTok).toLowerCase();
304
+ if (wrap === "sudo" || wrap === "env" || wrap === "command") {
305
+ i++;
306
+ while (i < tokens.length) {
307
+ const tok = tokens[i];
308
+ if (tok === undefined || !isShellEnvAssignToken(tok))
309
+ break;
310
+ i++;
311
+ }
312
+ }
313
+ }
314
+ const binTok = tokens[i];
315
+ if (binTok === undefined)
316
+ return null;
317
+ const bin = normalizeShellToken(binTok).toLowerCase();
318
+ if (bin === "git" || bin === "git.exe") {
319
+ i++;
320
+ // Skip git global options before the subcommand (-C, --git-dir, -c, …).
321
+ while (i < tokens.length) {
322
+ const raw = tokens[i];
323
+ if (raw === undefined)
324
+ return null;
325
+ const t = normalizeShellToken(raw);
326
+ const lower = t.toLowerCase();
327
+ if (!t.startsWith("-")) {
328
+ return lower === "push" ? "push" : null;
329
+ }
330
+ // --opt=value forms never consume a following token.
331
+ if (t.startsWith("--") && t.includes("=")) {
332
+ i++;
333
+ continue;
334
+ }
335
+ // Value-taking globals: -C path, --git-dir /repo, -c name=value, …
336
+ if (GIT_GLOBAL_VALUE_OPTS.has(t)) {
337
+ i += 2;
338
+ continue;
339
+ }
340
+ // Glued short forms: -C/path, -cname=value
341
+ if (t.startsWith("-C") || t.startsWith("-c")) {
342
+ i++;
343
+ continue;
344
+ }
345
+ // Boolean / other short/long flags without a separate value.
346
+ i++;
347
+ }
348
+ return null;
349
+ }
350
+ if (bin === "gh" || bin === "gh.exe") {
351
+ i++;
352
+ while (i < tokens.length) {
353
+ const flagRaw = tokens[i];
354
+ if (flagRaw === undefined)
355
+ break;
356
+ const flag = normalizeShellToken(flagRaw);
357
+ if (!flag.startsWith("-"))
358
+ break;
359
+ i++;
360
+ }
361
+ const pr = tokens[i];
362
+ const merge = tokens[i + 1];
363
+ if (pr !== undefined &&
364
+ merge !== undefined &&
365
+ normalizeShellToken(pr).toLowerCase() === "pr" &&
366
+ normalizeShellToken(merge).toLowerCase() === "merge") {
367
+ return "merge";
368
+ }
369
+ return null;
370
+ }
371
+ return null;
372
+ }
373
+ /**
374
+ * List all classifiable push/merge ops in a shell command (#2711).
375
+ * Scans every list/pipeline/newline segment so compound commands like
376
+ * `gh pr merge 1 && git push` surface both ops (dispatcher evaluates each).
377
+ * Newlines are delimiters so multi-line scripts cannot hide a later push/merge.
378
+ */
379
+ export function listShellOps(command) {
380
+ const cmd = command.trim();
381
+ if (cmd.length === 0)
382
+ return [];
383
+ const found = new Set();
384
+ // Quote-aware split so separators inside quotes are not treated as list ops.
385
+ for (const raw of splitShellSegments(cmd)) {
386
+ const op = classifyShellSegment(raw);
387
+ if (op !== null)
388
+ found.add(op);
389
+ }
390
+ const out = [];
391
+ // Stable order for deterministic multi-op evaluation.
392
+ if (found.has("push"))
393
+ out.push("push");
394
+ if (found.has("merge"))
395
+ out.push("merge");
396
+ return out;
397
+ }
398
+ /**
399
+ * Classify a shell command string for push/merge scopes (#2711).
400
+ * Unclassifiable commands return null (fail open at the gate).
401
+ * When a compound command has multiple ops, returns the first of listShellOps
402
+ * (push before merge); prefer listShellOps + evaluate-each for enforcement.
403
+ *
404
+ * Patterns (intentionally narrow; prefer false-open over false-deny):
405
+ * - push: `git push`, `git.exe push`, with optional env / -C / -c prefixes
406
+ * - merge: `gh pr merge`, `gh.exe pr merge`
407
+ */
408
+ export function classifyShellCommand(command) {
409
+ const ops = listShellOps(command);
410
+ return ops[0] ?? null;
411
+ }
412
+ /**
413
+ * Classify an MCP (or MCP-like) tool name + optional argument blob for push/merge (#2711).
414
+ * Returns null when the tool is not a known push/merge mutation (fail open).
415
+ */
416
+ export function classifyMcpTool(toolName, argsText = null) {
417
+ const name = toolName.trim().toLowerCase();
418
+ if (name.length === 0)
419
+ return null;
420
+ // Common GitHub MCP / bridge spellings for merge
421
+ if (/merge[_-]?pull[_-]?request/.test(name) ||
422
+ /pull[_-]?request[_-]?merge/.test(name) ||
423
+ /(^|__)merge_pr($|__)/.test(name) ||
424
+ /pr[_-]?merge/.test(name)) {
425
+ return "merge";
426
+ }
427
+ // Push-like tool names (narrow — prefer fail-open)
428
+ if (/(^|__)git[_-]?push($|__)/.test(name) || /push[_-]?branch/.test(name)) {
429
+ return "push";
430
+ }
431
+ if (/push/.test(name) && /(git|branch|remote|ref)/.test(name))
432
+ return "push";
433
+ const blob = (argsText ?? "").toLowerCase();
434
+ if (blob.length > 0) {
435
+ if (/\bgit(?:\.exe)?\s+push\b/.test(blob))
436
+ return "push";
437
+ if (/\bgh(?:\.exe)?\s+pr\s+merge\b/.test(blob))
438
+ return "merge";
439
+ }
440
+ return null;
441
+ }
442
+ /**
443
+ * Evaluate scopes.push / scopes.merge for a classifiable shell/MCP operation (#2711).
444
+ * null op → allow (unclassifiable fail-open). disabled policy → allow.
445
+ */
446
+ export function evaluateRuntimeAuthorityShellOp(input) {
447
+ const { policy, op } = input;
448
+ if (!policy.enabled) {
449
+ return { allowed: true, reason: null, code: null, unclassifiable: op === null };
450
+ }
451
+ if (op === null) {
452
+ return { allowed: true, reason: null, code: null, unclassifiable: true };
453
+ }
454
+ if (op === "push" && !policy.scopes.push) {
455
+ return {
456
+ allowed: false,
457
+ code: "runtime-policy-deny-scope",
458
+ unclassifiable: false,
459
+ reason: "Directive denied this shell/MCP operation: plan.policy.runtimeAuthority.scopes.push is false. " +
460
+ "Grant the push scope in PROJECT-DEFINITION or disable runtimeAuthority.",
461
+ };
462
+ }
463
+ if (op === "merge" && !policy.scopes.merge) {
464
+ return {
465
+ allowed: false,
466
+ code: "runtime-policy-deny-scope",
467
+ unclassifiable: false,
468
+ reason: "Directive denied this shell/MCP operation: plan.policy.runtimeAuthority.scopes.merge is false. " +
469
+ "Grant the merge scope in PROJECT-DEFINITION or disable runtimeAuthority.",
470
+ };
471
+ }
472
+ return { allowed: true, reason: null, code: null, unclassifiable: false };
473
+ }
180
474
  //# sourceMappingURL=runtime-authority.js.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Unified path write fence (#516 / #2443 / #2948 Wave 3).
3
+ *
4
+ * Single evaluation SoT: path checks always go through
5
+ * `evaluateRuntimeAuthorityPath` / `evaluateRuntimeAuthorityDirectWrite`.
6
+ * This module only **resolves** a RuntimeAuthorityPolicy by intersecting
7
+ * project `runtimeAuthority.allowPaths` / `denyPaths` with optional story
8
+ * `plan.metadata.swarm.file_scope` (and a read-time `writeScope` alias).
9
+ *
10
+ * There is no second independent writeScope evaluation engine.
11
+ */
12
+ import { type RuntimeAuthorityPolicy, type WriteFenceSource } from "./runtime-authority.js";
13
+ /**
14
+ * Normalize a legacy #2443 `writeScope` alias into `file_scope` + deny globs.
15
+ * Accepts:
16
+ * - `string[]` → allow list
17
+ * - `{ allow?: string[], deny?: string[] }`
18
+ * - `{ file_scope?: string[], denyPaths?: string[] }` (already-normalized shape)
19
+ *
20
+ * Callers MUST feed the result into {@link resolveWriteFence}; do not evaluate
21
+ * writeScope with a separate matcher.
22
+ */
23
+ export declare function normalizeStoryWriteScope(raw: unknown): {
24
+ readonly fileScope: string[];
25
+ readonly denyPaths: string[];
26
+ };
27
+ /**
28
+ * Extract story write fence from an xBRIEF/vBRIEF document.
29
+ * Prefer `plan.metadata.swarm.file_scope`; fall back to normalized `writeScope`
30
+ * on `plan.metadata.swarm` or `plan.metadata` (read-time alias only).
31
+ */
32
+ export declare function extractStoryFileScope(storyData: unknown): {
33
+ readonly fileScope: string[];
34
+ readonly denyPaths: string[];
35
+ };
36
+ /** Load story fence from an active xBRIEF path (fail-open on IO/parse errors). */
37
+ export declare function loadStoryWriteFenceFromPath(scopePath: string | null | undefined): {
38
+ readonly fileScope: string[];
39
+ readonly denyPaths: string[];
40
+ };
41
+ export interface ResolveWriteFenceOptions {
42
+ /** Extra deny globs from normalized writeScope.deny (merged; deny always wins). */
43
+ readonly storyDenyPaths?: readonly string[] | null;
44
+ }
45
+ export interface ResolvedWriteFence {
46
+ /**
47
+ * Policy for `evaluateRuntimeAuthorityPath` / `evaluateRuntimeAuthorityDirectWrite`.
48
+ * This is the only evaluation shape — no parallel engine.
49
+ */
50
+ readonly policy: RuntimeAuthorityPolicy;
51
+ /** Layers that contribute to this fence (empty when inactive). */
52
+ readonly sources: readonly WriteFenceSource[];
53
+ /** True when project and/or story path fence is active. */
54
+ readonly fenceActive: boolean;
55
+ readonly storyAllowPaths: readonly string[];
56
+ readonly storyDenyPaths: readonly string[];
57
+ }
58
+ /**
59
+ * Intersect project `runtimeAuthority` with optional story `file_scope`.
60
+ *
61
+ * Rules:
62
+ * - Empty project `allowPaths` when enabled = all paths until story narrows.
63
+ * - Empty story scope = project policy only.
64
+ * - Story `file_scope` alone enables a fence even when project policy is disabled.
65
+ * - `denyPaths` always win (project + story-normalized denys).
66
+ * - When both project allowPaths and story file_scope are non-empty, a path must
67
+ * match **both** layers (AND / intersection semantics via dual allow layers).
68
+ */
69
+ export declare function resolveWriteFence(projectPolicy: RuntimeAuthorityPolicy, storyFileScope?: readonly string[] | null, options?: ResolveWriteFenceOptions): ResolvedWriteFence;
70
+ /**
71
+ * Resolve the write fence for PreToolUse / product sinks.
72
+ * Composes project policy with story fence extracted from an active xBRIEF path.
73
+ */
74
+ export declare function resolveWriteFenceForScope(projectPolicy: RuntimeAuthorityPolicy, scopePath: string | null | undefined, loadStory?: (path: string | null | undefined) => {
75
+ fileScope: readonly string[];
76
+ denyPaths: readonly string[];
77
+ }): ResolvedWriteFence;
78
+ //# sourceMappingURL=write-fence.d.ts.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Unified path write fence (#516 / #2443 / #2948 Wave 3).
3
+ *
4
+ * Single evaluation SoT: path checks always go through
5
+ * `evaluateRuntimeAuthorityPath` / `evaluateRuntimeAuthorityDirectWrite`.
6
+ * This module only **resolves** a RuntimeAuthorityPolicy by intersecting
7
+ * project `runtimeAuthority.allowPaths` / `denyPaths` with optional story
8
+ * `plan.metadata.swarm.file_scope` (and a read-time `writeScope` alias).
9
+ *
10
+ * There is no second independent writeScope evaluation engine.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { DEFAULT_RUNTIME_AUTHORITY_POLICY, DEFAULT_RUNTIME_AUTHORITY_SCOPES, } from "./runtime-authority.js";
14
+ /** Clean non-empty path glob strings. */
15
+ function cleanGlobs(raw) {
16
+ if (raw === null || raw === undefined)
17
+ return [];
18
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0);
19
+ }
20
+ function cleanGlobArray(raw) {
21
+ if (!Array.isArray(raw))
22
+ return [];
23
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0);
24
+ }
25
+ /**
26
+ * Normalize a legacy #2443 `writeScope` alias into `file_scope` + deny globs.
27
+ * Accepts:
28
+ * - `string[]` → allow list
29
+ * - `{ allow?: string[], deny?: string[] }`
30
+ * - `{ file_scope?: string[], denyPaths?: string[] }` (already-normalized shape)
31
+ *
32
+ * Callers MUST feed the result into {@link resolveWriteFence}; do not evaluate
33
+ * writeScope with a separate matcher.
34
+ */
35
+ export function normalizeStoryWriteScope(raw) {
36
+ if (raw === null || raw === undefined) {
37
+ return { fileScope: [], denyPaths: [] };
38
+ }
39
+ if (Array.isArray(raw)) {
40
+ return { fileScope: cleanGlobArray(raw), denyPaths: [] };
41
+ }
42
+ if (typeof raw !== "object") {
43
+ return { fileScope: [], denyPaths: [] };
44
+ }
45
+ const rec = raw;
46
+ const allow = cleanGlobArray(rec.allow ?? rec.file_scope ?? rec.fileScope);
47
+ const deny = cleanGlobArray(rec.deny ?? rec.denyPaths);
48
+ return { fileScope: allow, denyPaths: deny };
49
+ }
50
+ /**
51
+ * Extract story write fence from an xBRIEF/vBRIEF document.
52
+ * Prefer `plan.metadata.swarm.file_scope`; fall back to normalized `writeScope`
53
+ * on `plan.metadata.swarm` or `plan.metadata` (read-time alias only).
54
+ */
55
+ export function extractStoryFileScope(storyData) {
56
+ if (typeof storyData !== "object" || storyData === null || Array.isArray(storyData)) {
57
+ return { fileScope: [], denyPaths: [] };
58
+ }
59
+ const plan = storyData.plan;
60
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
61
+ return { fileScope: [], denyPaths: [] };
62
+ }
63
+ const metadata = plan.metadata;
64
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
65
+ return { fileScope: [], denyPaths: [] };
66
+ }
67
+ const meta = metadata;
68
+ const swarmRaw = meta.swarm;
69
+ const swarm = typeof swarmRaw === "object" && swarmRaw !== null && !Array.isArray(swarmRaw)
70
+ ? swarmRaw
71
+ : null;
72
+ const fromFileScope = cleanGlobArray(swarm?.file_scope ?? swarm?.fileScope);
73
+ const fromWriteScope = normalizeStoryWriteScope(swarm?.writeScope ?? swarm?.write_scope ?? meta.writeScope ?? meta.write_scope);
74
+ // file_scope is SoT when present; writeScope alias only when file_scope empty.
75
+ if (fromFileScope.length > 0) {
76
+ // Still merge writeScope.deny so legacy deny lists are not lost.
77
+ return {
78
+ fileScope: fromFileScope,
79
+ denyPaths: fromWriteScope.denyPaths,
80
+ };
81
+ }
82
+ return fromWriteScope;
83
+ }
84
+ /** Load story fence from an active xBRIEF path (fail-open on IO/parse errors). */
85
+ export function loadStoryWriteFenceFromPath(scopePath) {
86
+ if (scopePath === null || scopePath === undefined || scopePath.trim().length === 0) {
87
+ return { fileScope: [], denyPaths: [] };
88
+ }
89
+ try {
90
+ const text = readFileSync(scopePath, "utf8");
91
+ const data = JSON.parse(text);
92
+ return extractStoryFileScope(data);
93
+ }
94
+ catch {
95
+ // Residual: host/session may not surface a readable active story.
96
+ // Fail open at story layer (project policy still applies if enabled).
97
+ return { fileScope: [], denyPaths: [] };
98
+ }
99
+ }
100
+ /**
101
+ * Intersect project `runtimeAuthority` with optional story `file_scope`.
102
+ *
103
+ * Rules:
104
+ * - Empty project `allowPaths` when enabled = all paths until story narrows.
105
+ * - Empty story scope = project policy only.
106
+ * - Story `file_scope` alone enables a fence even when project policy is disabled.
107
+ * - `denyPaths` always win (project + story-normalized denys).
108
+ * - When both project allowPaths and story file_scope are non-empty, a path must
109
+ * match **both** layers (AND / intersection semantics via dual allow layers).
110
+ */
111
+ export function resolveWriteFence(projectPolicy, storyFileScope, options) {
112
+ const storyAllow = cleanGlobs(storyFileScope);
113
+ const storyDeny = cleanGlobs(options?.storyDenyPaths);
114
+ const projectActive = projectPolicy.enabled;
115
+ const storyActive = storyAllow.length > 0 || storyDeny.length > 0;
116
+ if (!projectActive && !storyActive) {
117
+ return {
118
+ policy: DEFAULT_RUNTIME_AUTHORITY_POLICY,
119
+ sources: [],
120
+ fenceActive: false,
121
+ storyAllowPaths: [],
122
+ storyDenyPaths: [],
123
+ };
124
+ }
125
+ const sources = [];
126
+ if (projectActive)
127
+ sources.push("project");
128
+ if (storyActive)
129
+ sources.push("story");
130
+ // Project denyPaths + story denyPaths; deny always wins at evaluation time.
131
+ const denyPaths = [...projectPolicy.denyPaths, ...storyDeny];
132
+ // Project allow layer only when project policy is enabled (empty = unrestricted until story).
133
+ const allowPaths = projectActive ? [...projectPolicy.allowPaths] : [];
134
+ // Story-only: enable fence with default scopes (edits true) so direct writes are gated.
135
+ const scopes = projectActive
136
+ ? projectPolicy.scopes
137
+ : { ...DEFAULT_RUNTIME_AUTHORITY_SCOPES, edits: true };
138
+ const policy = {
139
+ enabled: true,
140
+ allowPaths,
141
+ denyPaths,
142
+ scopes,
143
+ storyAllowPaths: storyAllow,
144
+ fenceSources: sources,
145
+ };
146
+ return {
147
+ policy,
148
+ sources,
149
+ fenceActive: true,
150
+ storyAllowPaths: storyAllow,
151
+ storyDenyPaths: storyDeny,
152
+ };
153
+ }
154
+ /**
155
+ * Resolve the write fence for PreToolUse / product sinks.
156
+ * Composes project policy with story fence extracted from an active xBRIEF path.
157
+ */
158
+ export function resolveWriteFenceForScope(projectPolicy, scopePath, loadStory = loadStoryWriteFenceFromPath) {
159
+ const story = loadStory(scopePath);
160
+ return resolveWriteFence(projectPolicy, story.fileScope, {
161
+ storyDenyPaths: story.denyPaths,
162
+ });
163
+ }
164
+ //# sourceMappingURL=write-fence.js.map
@@ -1,3 +1,4 @@
1
+ import { type AgentMergeEvaluateResult } from "../policy/require-human-merge.js";
1
2
  import { type SemanticGreenFn } from "./semantic-green.js";
2
3
  import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
3
4
  export interface WaitMergeableOptions {
@@ -11,6 +12,15 @@ export interface WaitMergeableOptions {
11
12
  readonly requireMasterCiGreen?: boolean;
12
13
  /** Target branch override for stale-base comparison (defaults to PR base.ref). */
13
14
  readonly baseBranch?: string | null;
15
+ /**
16
+ * Project root for human-merge + intent-ceiling preflight (#1193).
17
+ * Defaults to process.cwd(). Pass explicitly in tests.
18
+ */
19
+ readonly projectRoot?: string;
20
+ /** Inject agent-merge evaluator (unit tests). */
21
+ readonly agentMergeFn?: (projectRoot: string) => AgentMergeEvaluateResult;
22
+ /** When true, skip human-merge / intent preflight (tests only). */
23
+ readonly skipHumanMergeGate?: boolean;
14
24
  }
15
25
  /** Run protected-check -> wait -> merge cascade (#1369). */
16
26
  export declare function waitMergeableAndMerge(prNumber: number, repo: string, options: {
@@ -1,3 +1,5 @@
1
+ import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
2
+ import { evaluateAgentMerge, } from "../policy/require-human-merge.js";
1
3
  import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
2
4
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
3
5
  import { makeResult } from "./result.js";
@@ -17,6 +19,32 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
17
19
  const mergeFn = options.mergeFn ?? runGhMerge;
18
20
  const semanticGreenFn = options.semanticGreenFn ?? evaluateSemanticGreen;
19
21
  const protectedIssues = options.protected;
22
+ const projectRoot = options.projectRoot ?? process.cwd();
23
+ // #1193 surface (1): refuse agent merge when requireHumanMerge is true,
24
+ // and refuse when slash-command intent ceiling does not authorize merge.
25
+ if (options.skipHumanMergeGate !== true) {
26
+ const intent = evaluateIntentCeilingFromEnv("merge");
27
+ if (!intent.allowed) {
28
+ return makeResult({
29
+ prNumber,
30
+ repo,
31
+ outcome: "config-error",
32
+ exitCode: EXIT_CONFIG_ERROR,
33
+ error: intent.reason,
34
+ });
35
+ }
36
+ const agentMergeFn = options.agentMergeFn ?? evaluateAgentMerge;
37
+ const hm = agentMergeFn(projectRoot);
38
+ if (!hm.allowed) {
39
+ return makeResult({
40
+ prNumber,
41
+ repo,
42
+ outcome: "config-error",
43
+ exitCode: EXIT_CONFIG_ERROR,
44
+ error: hm.message,
45
+ });
46
+ }
47
+ }
20
48
  let protectedCheckPayload = {};
21
49
  if (protectedIssues.length > 0) {
22
50
  const [prcRc, prcStdout, prcStderr] = protectedFn(prNumber, repo, protectedIssues);
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
2
  import { basename, dirname } from "node:path";
3
+ import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
3
4
  /** Canonical eligibility folder — only vbrief/active/ may spawn implementation. */
4
5
  export const ACTIVE_FOLDER = "active";
5
6
  /** Canonical eligibility status — only `running` signals an active handoff. */
@@ -124,6 +125,15 @@ export function evaluate(vbriefPath) {
124
125
  message: buildReject(path, `plan.status is '${status}' -- only '${ELIGIBLE_STATUS}' is eligible for implementation.`),
125
126
  };
126
127
  }
128
+ // Slash-command intent containment (#1193 / extends #810): non-implement session
129
+ // verbs must not authorize implementation preflight even when the xBRIEF is active.
130
+ const intent = evaluateIntentCeilingFromEnv("implement");
131
+ if (!intent.allowed) {
132
+ return {
133
+ exitCode: 1,
134
+ message: buildReject(path, intent.reason),
135
+ };
136
+ }
127
137
  return {
128
138
  exitCode: 0,
129
139
  message: `OK ${path} -- ready for implementation.`,