@deftai/directive-core 0.88.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 (168) 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/doctor-state.js +28 -4
  33. package/dist/doctor/main.d.ts +10 -0
  34. package/dist/doctor/main.js +160 -0
  35. package/dist/doctor/types.d.ts +11 -0
  36. package/dist/escalation/actions.d.ts +63 -0
  37. package/dist/escalation/actions.js +137 -0
  38. package/dist/escalation/index.d.ts +8 -0
  39. package/dist/escalation/index.js +8 -0
  40. package/dist/escalation/paths.d.ts +3 -0
  41. package/dist/escalation/paths.js +10 -0
  42. package/dist/escalation/store.d.ts +17 -0
  43. package/dist/escalation/store.js +172 -0
  44. package/dist/escalation/types.d.ts +73 -0
  45. package/dist/escalation/types.js +43 -0
  46. package/dist/eval/crud-telemetry.js +30 -10
  47. package/dist/eval/health.js +22 -7
  48. package/dist/eval/readback.js +11 -10
  49. package/dist/eval/run.js +32 -16
  50. package/dist/events/attribution-enrichment.js +10 -4
  51. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  52. package/dist/finish-loop/directive-finish-loop.js +239 -0
  53. package/dist/finish-loop/grant-gate.d.ts +31 -0
  54. package/dist/finish-loop/grant-gate.js +169 -0
  55. package/dist/finish-loop/index.d.ts +11 -0
  56. package/dist/finish-loop/index.js +11 -0
  57. package/dist/finish-loop/main.d.ts +35 -0
  58. package/dist/finish-loop/main.js +365 -0
  59. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  60. package/dist/finish-loop/pr-finish-loop.js +217 -0
  61. package/dist/finish-loop/progress.d.ts +17 -0
  62. package/dist/finish-loop/progress.js +45 -0
  63. package/dist/finish-loop/queue.d.ts +16 -0
  64. package/dist/finish-loop/queue.js +65 -0
  65. package/dist/finish-loop/types.d.ts +52 -0
  66. package/dist/finish-loop/types.js +10 -0
  67. package/dist/fs/contained-write.d.ts +101 -0
  68. package/dist/fs/contained-write.js +281 -0
  69. package/dist/hooks/classify/classify.d.ts +10 -0
  70. package/dist/hooks/classify/classify.js +37 -0
  71. package/dist/hooks/classify/index.d.ts +14 -0
  72. package/dist/hooks/classify/index.js +13 -0
  73. package/dist/hooks/classify/paths.d.ts +22 -0
  74. package/dist/hooks/classify/paths.js +72 -0
  75. package/dist/hooks/classify/payload.d.ts +16 -0
  76. package/dist/hooks/classify/payload.js +45 -0
  77. package/dist/hooks/classify/stdin.d.ts +12 -0
  78. package/dist/hooks/classify/stdin.js +63 -0
  79. package/dist/hooks/classify/tool-name.d.ts +18 -0
  80. package/dist/hooks/classify/tool-name.js +85 -0
  81. package/dist/hooks/classify/types.d.ts +41 -0
  82. package/dist/hooks/classify/types.js +7 -0
  83. package/dist/hooks/dispatcher.d.ts +21 -18
  84. package/dist/hooks/dispatcher.js +212 -163
  85. package/dist/hooks/fixtures/cases.d.ts +44 -0
  86. package/dist/hooks/fixtures/cases.js +595 -0
  87. package/dist/hooks/fixtures/index.d.ts +2 -0
  88. package/dist/hooks/fixtures/index.js +2 -0
  89. package/dist/hooks/index.d.ts +2 -0
  90. package/dist/hooks/index.js +2 -0
  91. package/dist/index.d.ts +4 -0
  92. package/dist/index.js +5 -0
  93. package/dist/init-deposit/agent-hooks.js +27 -8
  94. package/dist/init-deposit/gitignore.js +11 -3
  95. package/dist/init-deposit/headless-manifest.js +12 -4
  96. package/dist/init-deposit/migrate.d.ts +1 -1
  97. package/dist/init-deposit/migrate.js +13 -3
  98. package/dist/init-deposit/prettierignore.js +10 -3
  99. package/dist/init-deposit/scaffold.js +35 -26
  100. package/dist/init-deposit/xbrief-projections.js +11 -4
  101. package/dist/intake/candidates-log.js +46 -35
  102. package/dist/intake/github-body-cli.d.ts +6 -0
  103. package/dist/intake/github-body-cli.js +17 -0
  104. package/dist/intake/github-body.d.ts +46 -3
  105. package/dist/intake/github-body.js +191 -20
  106. package/dist/intake/issue-emit.d.ts +1 -0
  107. package/dist/intake/issue-emit.js +41 -23
  108. package/dist/lifecycle/event-detect.js +12 -4
  109. package/dist/lifecycle/events.js +29 -20
  110. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  111. package/dist/packs/pack-render.js +19 -6
  112. package/dist/plan-sequence/store.js +11 -4
  113. package/dist/policy/hotfix-criteria.d.ts +79 -0
  114. package/dist/policy/hotfix-criteria.js +197 -0
  115. package/dist/policy/index.d.ts +4 -0
  116. package/dist/policy/index.js +31 -1
  117. package/dist/policy/intent-ceiling.d.ts +79 -0
  118. package/dist/policy/intent-ceiling.js +181 -0
  119. package/dist/policy/no-deft-directive.js +10 -3
  120. package/dist/policy/org-force-on-migration.js +9 -5
  121. package/dist/policy/require-human-merge.d.ts +75 -0
  122. package/dist/policy/require-human-merge.js +324 -0
  123. package/dist/policy/resolve.js +17 -6
  124. package/dist/policy/runtime-authority.d.ts +15 -2
  125. package/dist/policy/runtime-authority.js +23 -3
  126. package/dist/policy/write-fence.d.ts +78 -0
  127. package/dist/policy/write-fence.js +164 -0
  128. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  129. package/dist/pr-wait-mergeable/cascade.js +28 -0
  130. package/dist/preflight/evaluate.js +10 -0
  131. package/dist/product-signal/consent.js +21 -5
  132. package/dist/product-signal/submit.js +22 -12
  133. package/dist/release/build-dist-runner.js +5 -2
  134. package/dist/release/build-dist.d.ts +19 -1
  135. package/dist/release/build-dist.js +50 -2
  136. package/dist/release/native-steps.js +7 -2
  137. package/dist/release-publish/pipeline.d.ts +13 -0
  138. package/dist/release-publish/pipeline.js +46 -1
  139. package/dist/scope/audit-log.js +19 -24
  140. package/dist/scope/decompose.js +10 -5
  141. package/dist/scope/decomposed-refs.js +18 -3
  142. package/dist/scope/demote.js +9 -2
  143. package/dist/scope/undo.js +9 -2
  144. package/dist/session/release-availability.js +26 -6
  145. package/dist/session/ritual-sentinel.js +19 -12
  146. package/dist/session/session-start.js +7 -0
  147. package/dist/staleness-tickler/state.js +26 -6
  148. package/dist/swarm/launch.js +13 -3
  149. package/dist/swarm/routing.js +9 -3
  150. package/dist/task-surface/index.js +24 -9
  151. package/dist/triage/bootstrap/gitignore.js +53 -10
  152. package/dist/triage/cache-path.js +10 -3
  153. package/dist/triage/welcome/summary.js +11 -4
  154. package/dist/triage/welcome/writers.js +45 -16
  155. package/dist/validate-content/validate-links.js +5 -0
  156. package/dist/value/readback.js +11 -6
  157. package/dist/vbrief-activate/activate.js +12 -4
  158. package/dist/vbrief-build/project-definition-io.js +14 -6
  159. package/dist/verify-source/contained-writes.d.ts +59 -0
  160. package/dist/verify-source/contained-writes.js +272 -0
  161. package/dist/verify-source/index.d.ts +1 -0
  162. package/dist/verify-source/index.js +1 -0
  163. package/dist/verify-source/openclaw-tier1.js +14 -1
  164. package/dist/verify-source/verify-stubs.js +3 -0
  165. package/dist/xbrief-migrate/migrate-project.js +26 -12
  166. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  167. package/dist/xbrief-migrate/transforms.js +37 -14
  168. package/package.json +15 -3
@@ -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";
@@ -1,6 +1,8 @@
1
1
  import { FIELD_HOST_HOOKS, FIELD_HOST_HOOKS_CLI_ALIAS, inspectHostHooks } from "./host-hooks.js";
2
+ import { FIELD_HOTFIX_CRITERIA, FIELD_HOTFIX_CRITERIA_CLI_ALIAS, inspectHotfixCriteria, } from "./hotfix-criteria.js";
2
3
  import { readPlanPolicy } from "./plan-extensions.js";
3
4
  import { FIELD_PRODUCT_SIGNAL, FIELD_PRODUCT_SIGNAL_CLI_ALIAS, inspectProductSignal, } from "./product-signal.js";
5
+ import { FIELD_REQUIRE_HUMAN_MERGE, FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS, inspectRequireHumanMerge, } from "./require-human-merge.js";
4
6
  import { coerceLegacyNarrative, LEGACY_NARRATIVE_KEY, loadProjectDefinition } from "./resolve.js";
5
7
  import { FIELD_RUNTIME_AUTHORITY, FIELD_RUNTIME_AUTHORITY_CLI_ALIAS, inspectRuntimeAuthority, } from "./runtime-authority.js";
6
8
  import { FIELD_STALENESS_TICKLER, FIELD_STALENESS_TICKLER_CLI_ALIAS, inspectStalenessTickler, } from "./staleness-tickler.js";
@@ -12,17 +14,21 @@ export * from "./capacity.js";
12
14
  export * from "./decisions.js";
13
15
  export * from "./disclosure.js";
14
16
  export * from "./host-hooks.js";
17
+ export * from "./hotfix-criteria.js";
18
+ export * from "./intent-ceiling.js";
15
19
  export * from "./no-deft-directive.js";
16
20
  export * from "./org-force-on-migration.js";
17
21
  export * from "./plan-extensions.js";
18
22
  export * from "./policy-invocation.js";
19
23
  export * from "./product-signal.js";
24
+ export * from "./require-human-merge.js";
20
25
  export * from "./resolve.js";
21
26
  export * from "./runtime-authority.js";
22
27
  export * from "./staleness-tickler.js";
23
28
  export * from "./value-feedback.js";
24
29
  export * from "./value-feedback-autoenable.js";
25
30
  export * from "./wip.js";
31
+ export * from "./write-fence.js";
26
32
  export const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
27
33
  export const FIELD_WIP_CAP = "plan.policy.wipCap";
28
34
  export const FIELD_SESSION_RITUAL_STALENESS_HOURS = "plan.policy.sessionRitualStalenessHours";
@@ -272,6 +278,24 @@ function inspectHostHooksField(data) {
272
278
  source: field.source,
273
279
  };
274
280
  }
281
+ function inspectRequireHumanMergeField(data, projectRoot) {
282
+ const field = inspectRequireHumanMerge(data, projectRoot);
283
+ return {
284
+ name: field.name,
285
+ current: field.current,
286
+ default: field.default,
287
+ source: field.source,
288
+ };
289
+ }
290
+ function inspectHotfixCriteriaField(data) {
291
+ const field = inspectHotfixCriteria(data);
292
+ return {
293
+ name: field.name,
294
+ current: field.current,
295
+ default: field.default,
296
+ source: field.source,
297
+ };
298
+ }
275
299
  const REGISTERED_POLICIES = [
276
300
  inspectAllowDirectCommits,
277
301
  inspectWipCap,
@@ -289,6 +313,8 @@ const REGISTERED_POLICIES = [
289
313
  inspectRuntimeAuthorityField,
290
314
  inspectProductSignalField,
291
315
  inspectValueFeedbackField,
316
+ inspectRequireHumanMergeField,
317
+ inspectHotfixCriteriaField,
292
318
  ];
293
319
  /** Walk registered inspectors and return one row per field (#1148). */
294
320
  export function inspectAllPolicies(projectRoot) {
@@ -307,7 +333,11 @@ export function inspectOnePolicy(name, projectRoot) {
307
333
  ? FIELD_RUNTIME_AUTHORITY
308
334
  : name === FIELD_HOST_HOOKS_CLI_ALIAS
309
335
  ? FIELD_HOST_HOOKS
310
- : name;
336
+ : name === FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS
337
+ ? FIELD_REQUIRE_HUMAN_MERGE
338
+ : name === FIELD_HOTFIX_CRITERIA_CLI_ALIAS
339
+ ? FIELD_HOTFIX_CRITERIA
340
+ : name;
311
341
  for (const field of inspectAllPolicies(projectRoot)) {
312
342
  if (field.name === normalized)
313
343
  return field;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Slash-command intent containment (#1193 / extends #810).
3
+ *
4
+ * When a session inherits a slash command as its verb, that verb is the *only*
5
+ * authorized intent for the session. Non-implement verbs must not authorize
6
+ * implement / push / PR / merge / deploy tool paths.
7
+ *
8
+ * Provenance: set `DEFT_SESSION_SLASH_VERB` (e.g. `/github-issue`, `github-issue`,
9
+ * `/build`) for the session, or pass `sessionVerb` explicitly to pure evaluators.
10
+ */
11
+ /** Env var carrying the session's slash-command verb (#1193). */
12
+ export declare const ENV_SESSION_SLASH_VERB = "DEFT_SESSION_SLASH_VERB";
13
+ /**
14
+ * Free-text action verbs that authorize implementation under #810.
15
+ * Parallel list for slash forms lives in IMPLEMENT_SLASH_VERBS.
16
+ */
17
+ export declare const FREE_TEXT_IMPLEMENT_VERBS: readonly ["build", "implement", "ship", "swarm", "run agents", "start agent"];
18
+ /**
19
+ * Slash-command stems that authorize implementation (#1193 R1).
20
+ * Matched after normalizing `/prefix` and `:namespace` forms.
21
+ */
22
+ export declare const IMPLEMENT_SLASH_VERBS: Set<string>;
23
+ /**
24
+ * Explicit non-implement slash stems — always contained.
25
+ * Unknown stems default to non-implement (fail closed for lifecycle escalation).
26
+ */
27
+ export declare const NON_IMPLEMENT_SLASH_VERBS: Set<string>;
28
+ /** Lifecycle ops gated by the intent ceiling. */
29
+ export declare const INTENT_CEILING_OPS: readonly ["implement", "push", "pr", "merge", "deploy"];
30
+ export type IntentCeilingOp = (typeof INTENT_CEILING_OPS)[number];
31
+ export type IntentCeilingCode = "intent-allow-no-slash" | "intent-allow-implement-verb" | "intent-deny-non-implement" | "intent-deny-unknown-slash";
32
+ export interface IntentCeilingDecision {
33
+ readonly allowed: boolean;
34
+ readonly code: IntentCeilingCode;
35
+ readonly reason: string;
36
+ /** Normalized slash stem (without leading `/`), or null when no slash provenance. */
37
+ readonly sessionVerb: string | null;
38
+ readonly requestedOp: IntentCeilingOp;
39
+ }
40
+ /**
41
+ * Normalize a slash command or free-text verb to a comparable stem.
42
+ * `/deft:directive:github-issue` → `github-issue`; `/build` → `build`.
43
+ */
44
+ export declare function normalizeSessionVerb(raw: string | null | undefined): string | null;
45
+ /** True when the normalized stem authorizes implementation. */
46
+ export declare function isImplementSlashVerb(stem: string | null): boolean;
47
+ /** True when the stem is an explicitly known non-implement command. */
48
+ export declare function isNonImplementSlashVerb(stem: string | null): boolean;
49
+ export interface EvaluateIntentCeilingInput {
50
+ readonly sessionVerb?: string | null;
51
+ readonly requestedOp: IntentCeilingOp;
52
+ }
53
+ /**
54
+ * Pure intent-ceiling evaluator (#1193 R1).
55
+ *
56
+ * - No slash verb → allow (free-text #810 action-verb remains agent/AGENTS gate).
57
+ * - Implement slash verb → allow all lifecycle ops.
58
+ * - Non-implement / unknown slash verb → deny implement/push/pr/merge/deploy.
59
+ */
60
+ export declare function evaluateIntentCeiling(input: EvaluateIntentCeilingInput): IntentCeilingDecision;
61
+ /** Read session slash verb from the environment. */
62
+ export declare function readSessionSlashVerbFromEnv(env?: NodeJS.ProcessEnv): string | null;
63
+ /**
64
+ * Evaluate intent ceiling for a lifecycle op using env-backed session verb.
65
+ * Convenience for preflight / hooks / merge gates.
66
+ */
67
+ export declare function evaluateIntentCeilingFromEnv(requestedOp: IntentCeilingOp, env?: NodeJS.ProcessEnv): IntentCeilingDecision;
68
+ /**
69
+ * Map classifiable shell/MCP lifecycle signals onto intent-ceiling ops.
70
+ * Returns null when the tool path is not a gated lifecycle op.
71
+ */
72
+ export declare function intentOpFromShellSignals(signals: {
73
+ readonly isPush?: boolean;
74
+ readonly isMerge?: boolean;
75
+ readonly isPr?: boolean;
76
+ readonly isDeploy?: boolean;
77
+ readonly isImplement?: boolean;
78
+ }): IntentCeilingOp | null;
79
+ //# sourceMappingURL=intent-ceiling.d.ts.map
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Slash-command intent containment (#1193 / extends #810).
3
+ *
4
+ * When a session inherits a slash command as its verb, that verb is the *only*
5
+ * authorized intent for the session. Non-implement verbs must not authorize
6
+ * implement / push / PR / merge / deploy tool paths.
7
+ *
8
+ * Provenance: set `DEFT_SESSION_SLASH_VERB` (e.g. `/github-issue`, `github-issue`,
9
+ * `/build`) for the session, or pass `sessionVerb` explicitly to pure evaluators.
10
+ */
11
+ /** Env var carrying the session's slash-command verb (#1193). */
12
+ export const ENV_SESSION_SLASH_VERB = "DEFT_SESSION_SLASH_VERB";
13
+ /**
14
+ * Free-text action verbs that authorize implementation under #810.
15
+ * Parallel list for slash forms lives in IMPLEMENT_SLASH_VERBS.
16
+ */
17
+ export const FREE_TEXT_IMPLEMENT_VERBS = [
18
+ "build",
19
+ "implement",
20
+ "ship",
21
+ "swarm",
22
+ "run agents",
23
+ "start agent",
24
+ ];
25
+ /**
26
+ * Slash-command stems that authorize implementation (#1193 R1).
27
+ * Matched after normalizing `/prefix` and `:namespace` forms.
28
+ */
29
+ export const IMPLEMENT_SLASH_VERBS = new Set([
30
+ "build",
31
+ "implement",
32
+ "ship",
33
+ "ship-hotfix",
34
+ "swarm",
35
+ "start-agent",
36
+ "start_agent",
37
+ "run-agents",
38
+ "run_agents",
39
+ ]);
40
+ /**
41
+ * Explicit non-implement slash stems — always contained.
42
+ * Unknown stems default to non-implement (fail closed for lifecycle escalation).
43
+ */
44
+ export const NON_IMPLEMENT_SLASH_VERBS = new Set([
45
+ "github-issue",
46
+ "github_issue",
47
+ "issue",
48
+ "triage",
49
+ "refine",
50
+ "refinement",
51
+ "discuss",
52
+ "research",
53
+ "probe",
54
+ "glossary",
55
+ "sync",
56
+ "continue",
57
+ "checkpoint",
58
+ "feedback",
59
+ "article-review",
60
+ "cost",
61
+ "setup",
62
+ "interview",
63
+ "map",
64
+ "yolo",
65
+ "speckit",
66
+ "change",
67
+ ]);
68
+ /** Lifecycle ops gated by the intent ceiling. */
69
+ export const INTENT_CEILING_OPS = ["implement", "push", "pr", "merge", "deploy"];
70
+ /**
71
+ * Normalize a slash command or free-text verb to a comparable stem.
72
+ * `/deft:directive:github-issue` → `github-issue`; `/build` → `build`.
73
+ */
74
+ export function normalizeSessionVerb(raw) {
75
+ if (raw === null || raw === undefined)
76
+ return null;
77
+ let s = raw.trim().toLowerCase();
78
+ if (s.length === 0)
79
+ return null;
80
+ // Strip leading slashes.
81
+ while (s.startsWith("/"))
82
+ s = s.slice(1);
83
+ // Drop product namespaces: deft:directive:github-issue → github-issue
84
+ if (s.includes(":")) {
85
+ const parts = s.split(":").filter((p) => p.length > 0);
86
+ s = parts[parts.length - 1] ?? s;
87
+ }
88
+ // Normalize underscores to hyphens for allowlist matching.
89
+ s = s.replace(/_/g, "-");
90
+ return s.length > 0 ? s : null;
91
+ }
92
+ /** True when the normalized stem authorizes implementation. */
93
+ export function isImplementSlashVerb(stem) {
94
+ if (stem === null)
95
+ return false;
96
+ if (IMPLEMENT_SLASH_VERBS.has(stem))
97
+ return true;
98
+ // Free-text multi-word forms already hyphenated by normalize (run agents → not here).
99
+ return false;
100
+ }
101
+ /** True when the stem is an explicitly known non-implement command. */
102
+ export function isNonImplementSlashVerb(stem) {
103
+ if (stem === null)
104
+ return false;
105
+ return NON_IMPLEMENT_SLASH_VERBS.has(stem);
106
+ }
107
+ /**
108
+ * Pure intent-ceiling evaluator (#1193 R1).
109
+ *
110
+ * - No slash verb → allow (free-text #810 action-verb remains agent/AGENTS gate).
111
+ * - Implement slash verb → allow all lifecycle ops.
112
+ * - Non-implement / unknown slash verb → deny implement/push/pr/merge/deploy.
113
+ */
114
+ export function evaluateIntentCeiling(input) {
115
+ const stem = normalizeSessionVerb(input.sessionVerb ?? null);
116
+ const op = input.requestedOp;
117
+ if (stem === null) {
118
+ return {
119
+ allowed: true,
120
+ code: "intent-allow-no-slash",
121
+ reason: "No slash-command session verb; free-text #810 action-verb rules apply.",
122
+ sessionVerb: null,
123
+ requestedOp: op,
124
+ };
125
+ }
126
+ if (isImplementSlashVerb(stem)) {
127
+ return {
128
+ allowed: true,
129
+ code: "intent-allow-implement-verb",
130
+ reason: `Slash verb '/${stem}' authorizes implementation lifecycle ops.`,
131
+ sessionVerb: stem,
132
+ requestedOp: op,
133
+ };
134
+ }
135
+ const knownNon = isNonImplementSlashVerb(stem);
136
+ const code = knownNon
137
+ ? "intent-deny-non-implement"
138
+ : "intent-deny-unknown-slash";
139
+ return {
140
+ allowed: false,
141
+ code,
142
+ reason: `Slash-command intent ceiling (#1193): session verb '/${stem}' does not authorize ` +
143
+ `${op}. Non-implement verbs terminate at their named outcome; file a follow-up issue ` +
144
+ `instead of escalating to implement/push/PR/merge/deploy. ` +
145
+ `Implement verbs: /build, /ship, /ship-hotfix, /swarm, /implement.`,
146
+ sessionVerb: stem,
147
+ requestedOp: op,
148
+ };
149
+ }
150
+ /** Read session slash verb from the environment. */
151
+ export function readSessionSlashVerbFromEnv(env = process.env) {
152
+ return normalizeSessionVerb(env[ENV_SESSION_SLASH_VERB] ?? null);
153
+ }
154
+ /**
155
+ * Evaluate intent ceiling for a lifecycle op using env-backed session verb.
156
+ * Convenience for preflight / hooks / merge gates.
157
+ */
158
+ export function evaluateIntentCeilingFromEnv(requestedOp, env = process.env) {
159
+ return evaluateIntentCeiling({
160
+ sessionVerb: readSessionSlashVerbFromEnv(env),
161
+ requestedOp,
162
+ });
163
+ }
164
+ /**
165
+ * Map classifiable shell/MCP lifecycle signals onto intent-ceiling ops.
166
+ * Returns null when the tool path is not a gated lifecycle op.
167
+ */
168
+ export function intentOpFromShellSignals(signals) {
169
+ if (signals.isDeploy)
170
+ return "deploy";
171
+ if (signals.isMerge)
172
+ return "merge";
173
+ if (signals.isPr)
174
+ return "pr";
175
+ if (signals.isPush)
176
+ return "push";
177
+ if (signals.isImplement)
178
+ return "implement";
179
+ return null;
180
+ }
181
+ //# sourceMappingURL=intent-ceiling.js.map
@@ -13,8 +13,9 @@
13
13
  * mutating install/update paths **fail closed**.
14
14
  * - Creating the flag does **not** delete an existing deposit.
15
15
  */
16
- import { existsSync, statSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { existsSync, statSync, unlinkSync } from "node:fs";
17
17
  import { join, resolve } from "node:path";
18
+ import { containedWrite } from "../fs/contained-write.js";
18
19
  import { assertWriteTargetSafe } from "../fs/projection-containment.js";
19
20
  import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
20
21
  /** Canonical root-only filename (lowercase). Presence = flag. */
@@ -75,13 +76,19 @@ export function isNoDeftDirectivePresent(projectRoot, seams = {}) {
75
76
  */
76
77
  export function createNoDeftDirectiveFlag(projectRoot, options = {}) {
77
78
  const path = noDeftDirectiveFlagPath(projectRoot);
78
- assertWriteTargetSafe(projectRoot, path);
79
79
  if (defaultIsDir(path)) {
80
80
  throw new Error(`${NO_DEFT_DIRECTIVE_FLAG_NAME} exists as a directory at ${path}; remove it before creating the opt-out flag file.`);
81
81
  }
82
82
  const rationale = options.rationale?.trim() ?? "";
83
83
  const body = rationale.length > 0 ? `# ${rationale}\n` : "";
84
- writeFileSync(path, body, "utf8");
84
+ // #2951: product write sinks route through containedWrite (create|replace).
85
+ // Use replace so re-running with a new rationale updates the flag file.
86
+ containedWrite({
87
+ root: resolve(projectRoot),
88
+ target: path,
89
+ data: body,
90
+ mode: "replace",
91
+ });
85
92
  return path;
86
93
  }
87
94
  /**