@deftai/directive-core 0.81.0 → 0.83.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 (75) hide show
  1. package/dist/content-contracts/skills/helpers.d.ts +6 -0
  2. package/dist/content-contracts/skills/helpers.js +47 -3
  3. package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
  4. package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
  5. package/dist/eval/crud-telemetry.d.ts +2 -0
  6. package/dist/eval/crud-telemetry.js +30 -0
  7. package/dist/hooks/cursor-hooks.d.ts +23 -0
  8. package/dist/hooks/cursor-hooks.js +95 -0
  9. package/dist/hooks/dispatcher.d.ts +8 -1
  10. package/dist/hooks/dispatcher.js +23 -5
  11. package/dist/hooks/index.d.ts +1 -0
  12. package/dist/hooks/index.js +1 -0
  13. package/dist/init-deposit/agent-hooks.d.ts +4 -2
  14. package/dist/init-deposit/agent-hooks.js +137 -14
  15. package/dist/init-deposit/hygiene.js +1 -0
  16. package/dist/intake/github-body-cli.js +2 -0
  17. package/dist/intake/github-body.d.ts +11 -0
  18. package/dist/intake/github-body.js +95 -24
  19. package/dist/integration-e2e/helpers.js +1 -1
  20. package/dist/issue-sync/sync-from-xbrief.js +3 -0
  21. package/dist/lifecycle/events.d.ts +1 -0
  22. package/dist/lifecycle/events.js +59 -9
  23. package/dist/orphan-active/evaluate.d.ts +25 -0
  24. package/dist/orphan-active/evaluate.js +275 -0
  25. package/dist/orphan-active/index.d.ts +3 -0
  26. package/dist/orphan-active/index.js +3 -0
  27. package/dist/orphan-active/refs.d.ts +16 -0
  28. package/dist/orphan-active/refs.js +105 -0
  29. package/dist/policy/host-hooks.d.ts +21 -0
  30. package/dist/policy/host-hooks.js +96 -0
  31. package/dist/policy/index.d.ts +1 -0
  32. package/dist/policy/index.js +15 -1
  33. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  34. package/dist/pr-wait-mergeable/cascade.js +18 -0
  35. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  36. package/dist/pr-wait-mergeable/main.js +44 -73
  37. package/dist/pr-wait-mergeable/result.d.ts +2 -1
  38. package/dist/pr-wait-mergeable/result.js +4 -0
  39. package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
  40. package/dist/pr-wait-mergeable/semantic-green.js +202 -0
  41. package/dist/pr-wait-mergeable/types.d.ts +1 -0
  42. package/dist/pr-watch/constants.d.ts +4 -4
  43. package/dist/pr-watch/constants.js +4 -4
  44. package/dist/pr-watch/watch.js +5 -3
  45. package/dist/product-signal/consent.d.ts +20 -3
  46. package/dist/product-signal/consent.js +63 -6
  47. package/dist/product-signal/gates.d.ts +1 -1
  48. package/dist/product-signal/submit.d.ts +5 -1
  49. package/dist/product-signal/submit.js +42 -18
  50. package/dist/release/native-steps.js +11 -4
  51. package/dist/render/framework-commands.js +6 -0
  52. package/dist/scope/brief-io.d.ts +26 -0
  53. package/dist/scope/brief-io.js +77 -0
  54. package/dist/scope/decompose.js +2 -2
  55. package/dist/scope/decomposed-refs.js +3 -3
  56. package/dist/scope/demote.js +2 -2
  57. package/dist/scope/project-definition-sync.d.ts +2 -2
  58. package/dist/scope/project-definition-sync.js +15 -3
  59. package/dist/scope/registry-artifact-sync.d.ts +6 -1
  60. package/dist/scope/registry-artifact-sync.js +31 -13
  61. package/dist/scope/transition.js +23 -15
  62. package/dist/scope/undo.js +2 -2
  63. package/dist/scope/vbrief-json.d.ts +1 -2
  64. package/dist/scope/vbrief-json.js +1 -4
  65. package/dist/staleness-tickler/state.js +19 -2
  66. package/dist/triage/queue/cache.js +5 -0
  67. package/dist/vbrief-validate/plan-hooks.d.ts +2 -0
  68. package/dist/vbrief-validate/plan-hooks.js +25 -0
  69. package/dist/verify-env/agent-hooks.d.ts +2 -1
  70. package/dist/verify-env/agent-hooks.js +3 -2
  71. package/dist/verify-source/index.d.ts +1 -0
  72. package/dist/verify-source/index.js +1 -0
  73. package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
  74. package/dist/verify-source/skill-external-fetch-gate.js +71 -0
  75. package/package.json +7 -3
@@ -0,0 +1,275 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
+ import { defaultRunGh } from "../pr-protected-issues/gh.js";
5
+ import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "../triage/queue/constants.js";
6
+ import { resolveRepo } from "../triage/queue/repo.js";
7
+ import { collectGithubRefs } from "./refs.js";
8
+ function readJson(path) {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
11
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
12
+ ? parsed
13
+ : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function planOf(data) {
20
+ const plan = data?.plan;
21
+ return typeof plan === "object" && plan !== null && !Array.isArray(plan)
22
+ ? plan
23
+ : null;
24
+ }
25
+ function relBriefPath(path, projectRoot) {
26
+ try {
27
+ return relative(resolve(projectRoot), resolve(path)).replace(/\\/g, "/");
28
+ }
29
+ catch {
30
+ return path.replace(/\\/g, "/");
31
+ }
32
+ }
33
+ function listActiveRunningBriefs(projectRoot) {
34
+ let lifecycleRoot;
35
+ try {
36
+ lifecycleRoot = resolveLifecycleRoot(projectRoot);
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ const activeDir = join(lifecycleRoot, "active");
42
+ if (!existsSync(activeDir)) {
43
+ return [];
44
+ }
45
+ const out = [];
46
+ for (const entry of readdirSync(activeDir, { withFileTypes: true })) {
47
+ if (!entry.isFile() || !hasArtifactSuffix(entry.name)) {
48
+ continue;
49
+ }
50
+ const path = join(activeDir, entry.name);
51
+ const data = readJson(path);
52
+ const plan = planOf(data);
53
+ if (plan === null) {
54
+ continue;
55
+ }
56
+ if (String(plan.status ?? "").toLowerCase() !== "running") {
57
+ continue;
58
+ }
59
+ out.push({ path, plan });
60
+ }
61
+ return out.sort((a, b) => a.path.localeCompare(b.path));
62
+ }
63
+ function readCachedIssueState(projectRoot, ref) {
64
+ const [owner, name] = ref.repo.split("/", 2);
65
+ if (!owner || !name) {
66
+ return null;
67
+ }
68
+ const rawPath = join(projectRoot, CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, owner, name, String(ref.number), "raw.json");
69
+ if (!existsSync(rawPath)) {
70
+ return null;
71
+ }
72
+ const raw = readJson(rawPath);
73
+ if (raw === null) {
74
+ return null;
75
+ }
76
+ const state = typeof raw.state === "string" ? raw.state.toLowerCase() : "";
77
+ if (state === "closed") {
78
+ return "closed";
79
+ }
80
+ if (state === "open") {
81
+ return "open";
82
+ }
83
+ return null;
84
+ }
85
+ function fetchIssueStateLive(ref, runGh) {
86
+ const path = `repos/${ref.repo}/issues/${ref.number}`;
87
+ const result = runGh(["gh", "api", path]);
88
+ if (result.returncode !== 0) {
89
+ return null;
90
+ }
91
+ try {
92
+ const payload = JSON.parse(result.stdout);
93
+ if (payload === null || typeof payload !== "object") {
94
+ return null;
95
+ }
96
+ const state = payload.state;
97
+ if (state === "closed") {
98
+ return "closed";
99
+ }
100
+ if (state === "open") {
101
+ return "open";
102
+ }
103
+ return null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ function resolveIssueState(ref, projectRoot, runGh, skipGh) {
110
+ const cached = readCachedIssueState(projectRoot, ref);
111
+ if (cached !== null) {
112
+ return cached;
113
+ }
114
+ if (skipGh) {
115
+ return null;
116
+ }
117
+ return fetchIssueStateLive(ref, runGh);
118
+ }
119
+ function fetchPrMerged(ref, runGh) {
120
+ const path = `repos/${ref.repo}/pulls/${ref.number}`;
121
+ const result = runGh(["gh", "api", path]);
122
+ if (result.returncode !== 0) {
123
+ return null;
124
+ }
125
+ try {
126
+ const payload = JSON.parse(result.stdout);
127
+ if (payload === null || typeof payload !== "object") {
128
+ return null;
129
+ }
130
+ const mergedAt = payload.merged_at;
131
+ if (mergedAt === null) {
132
+ return false;
133
+ }
134
+ return typeof mergedAt === "string" && mergedAt.length > 0;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh) {
141
+ for (const pr of prRefs) {
142
+ if (skipGh) {
143
+ continue;
144
+ }
145
+ const merged = fetchPrMerged(pr, runGh);
146
+ if (merged === true) {
147
+ return { orphaned: true, reason: `linked PR #${pr.number} is merged` };
148
+ }
149
+ }
150
+ if (issueRefs.length === 0) {
151
+ return { orphaned: false, reason: null };
152
+ }
153
+ let resolved = 0;
154
+ for (const ref of issueRefs) {
155
+ const state = resolveIssueState(ref, projectRoot, runGh, skipGh);
156
+ if (state === null) {
157
+ return { orphaned: false, reason: null };
158
+ }
159
+ resolved += 1;
160
+ if (state !== "closed") {
161
+ return { orphaned: false, reason: null };
162
+ }
163
+ }
164
+ if (resolved > 0) {
165
+ return { orphaned: true, reason: "all referenced issues are closed" };
166
+ }
167
+ return { orphaned: false, reason: null };
168
+ }
169
+ function formatRefusal(orphans, projectRoot) {
170
+ const lines = [
171
+ `verify:orphan-active: ${orphans.length} active/running xBRIEF${orphans.length === 1 ? "" : "s"} look shipped but still consume WIP (project_root=${projectRoot}).`,
172
+ " Remediation: move each brief out of active/ with lifecycle ownership:",
173
+ " task scope:complete -- xbrief/active/<file>.xbrief.json",
174
+ " task scope:cancel -- xbrief/active/<file>.xbrief.json # when abandoning",
175
+ " For stop-at:pr-open workers the orchestrator owns post-merge complete/cancel;",
176
+ " for drive-to:merge-ready workers scope:complete is part of the worker unit (#2321).",
177
+ " Or run task swarm:finalize-cohort / task swarm:complete-cohort after cohort merge.",
178
+ " Offending briefs:",
179
+ ];
180
+ for (const orphan of orphans) {
181
+ lines.push(` - ${orphan.path} (${orphan.reason})`);
182
+ }
183
+ return lines.join("\n");
184
+ }
185
+ /**
186
+ * Pure evaluator for orphaned active/running xBRIEF detection (#2321).
187
+ * Fails when active/ briefs with plan.status==running reference only closed
188
+ * issues and/or a merged PR — the stop-at:pr-open orphan signature.
189
+ */
190
+ export function evaluate(projectRoot, options = {}) {
191
+ const root = resolve(projectRoot);
192
+ const quiet = options.quiet ?? false;
193
+ const skipGh = options.skipGh ?? false;
194
+ const runGh = options.runGh ?? defaultRunGh;
195
+ const defaultRepo = resolveRepo(options.repo, root);
196
+ if (!existsSync(root)) {
197
+ return {
198
+ code: 2,
199
+ message: `verify:orphan-active: project root does not exist: ${root}`,
200
+ stream: "stderr",
201
+ orphans: [],
202
+ };
203
+ }
204
+ let lifecycleRoot;
205
+ try {
206
+ lifecycleRoot = resolveLifecycleRoot(root);
207
+ }
208
+ catch (err) {
209
+ const message = err instanceof Error ? err.message : String(err);
210
+ // Consumer/greenfield fixtures may still use legacy vbrief/ only (#2112).
211
+ // Orphan detection applies only to xbrief/active/ — skip cleanly, not config fail.
212
+ if (message.includes("No xbrief/ layout found")) {
213
+ if (quiet) {
214
+ return { code: 0, message: "", stream: "none", orphans: [] };
215
+ }
216
+ return {
217
+ code: 0,
218
+ message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
219
+ stream: "stdout",
220
+ orphans: [],
221
+ };
222
+ }
223
+ return {
224
+ code: 2,
225
+ message: `verify:orphan-active: ${message}`,
226
+ stream: "stderr",
227
+ orphans: [],
228
+ };
229
+ }
230
+ if (!existsSync(lifecycleRoot)) {
231
+ if (quiet) {
232
+ return { code: 0, message: "", stream: "none", orphans: [] };
233
+ }
234
+ return {
235
+ code: 0,
236
+ message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
237
+ stream: "stdout",
238
+ orphans: [],
239
+ };
240
+ }
241
+ const orphans = [];
242
+ for (const brief of listActiveRunningBriefs(root)) {
243
+ const { issues, prs } = collectGithubRefs(brief.plan, defaultRepo);
244
+ if (issues.length === 0 && prs.length === 0) {
245
+ continue;
246
+ }
247
+ const assessment = assessOrphanSignature(issues, prs, root, runGh, skipGh);
248
+ if (assessment.orphaned && assessment.reason !== null) {
249
+ orphans.push({
250
+ path: relBriefPath(brief.path, root),
251
+ reason: assessment.reason,
252
+ });
253
+ }
254
+ }
255
+ if (orphans.length > 0) {
256
+ return {
257
+ code: 1,
258
+ message: formatRefusal(orphans, root),
259
+ stream: "stderr",
260
+ orphans,
261
+ };
262
+ }
263
+ if (quiet) {
264
+ return { code: 0, message: "", stream: "none", orphans: [] };
265
+ }
266
+ const scanned = listActiveRunningBriefs(root).length;
267
+ return {
268
+ code: 0,
269
+ message: `verify:orphan-active: no orphaned active/running xBRIEFs ` +
270
+ `(scanned ${scanned} running brief${scanned === 1 ? "" : "s"} in active/).`,
271
+ stream: "stdout",
272
+ orphans: [],
273
+ };
274
+ }
275
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,3 @@
1
+ export * from "./evaluate.js";
2
+ export * from "./refs.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from "./evaluate.js";
2
+ export * from "./refs.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ export interface IssueRef {
2
+ readonly repo: string;
3
+ readonly number: number;
4
+ }
5
+ export interface PrRef {
6
+ readonly repo: string;
7
+ readonly number: number;
8
+ }
9
+ /** Parse (repo, pr_number) from a github-pr reference URI. */
10
+ export declare function parseGithubPrUri(uri: unknown): [string | null, number | null];
11
+ /** Collect GitHub issue and PR refs from a scope plan object. */
12
+ export declare function collectGithubRefs(plan: Record<string, unknown>, defaultRepo: string | null): {
13
+ issues: IssueRef[];
14
+ prs: PrRef[];
15
+ };
16
+ //# sourceMappingURL=refs.d.ts.map
@@ -0,0 +1,105 @@
1
+ import { referenceTypeMatches } from "@deftai/directive-types";
2
+ import { parseGithubIssueUri } from "../triage/reconcile/parse-uri.js";
3
+ /** Parse (repo, pr_number) from a github-pr reference URI. */
4
+ export function parseGithubPrUri(uri) {
5
+ if (typeof uri !== "string") {
6
+ return [null, null];
7
+ }
8
+ const cleaned = uri.trim().replace(/\/$/, "");
9
+ if (!cleaned) {
10
+ return [null, null];
11
+ }
12
+ const noScheme = cleaned.includes("://") ? cleaned.split("://").slice(1).join("://") : cleaned;
13
+ const parts = noScheme.split("/").filter(Boolean);
14
+ if (parts.length >= 4 && parts[parts.length - 2] === "pull") {
15
+ const tail = parts[parts.length - 1] ?? "";
16
+ if (/^\d+$/.test(tail)) {
17
+ const owner = parts[parts.length - 4];
18
+ const repo = parts[parts.length - 3];
19
+ if (owner && repo) {
20
+ return [`${owner}/${repo}`, Number(tail)];
21
+ }
22
+ }
23
+ }
24
+ const tail = parts[parts.length - 1] ?? "";
25
+ if (/^\d+$/.test(tail)) {
26
+ return [null, Number(tail)];
27
+ }
28
+ return [null, null];
29
+ }
30
+ function parseTrackingIssue(value) {
31
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
32
+ return value;
33
+ }
34
+ if (typeof value !== "string") {
35
+ return null;
36
+ }
37
+ const match = value.match(/#(\d+)/);
38
+ return match ? Number(match[1]) : null;
39
+ }
40
+ function addIssueRef(out, seen, repo, number, defaultRepo) {
41
+ if (number === null) {
42
+ return;
43
+ }
44
+ const resolvedRepo = repo ?? defaultRepo;
45
+ if (resolvedRepo === null || resolvedRepo.length === 0) {
46
+ return;
47
+ }
48
+ const key = `${resolvedRepo}:${number}`;
49
+ if (seen.has(key)) {
50
+ return;
51
+ }
52
+ seen.add(key);
53
+ out.push({ repo: resolvedRepo, number });
54
+ }
55
+ function addPrRef(out, seen, repo, number, defaultRepo) {
56
+ if (number === null) {
57
+ return;
58
+ }
59
+ const resolvedRepo = repo ?? defaultRepo;
60
+ if (resolvedRepo === null || resolvedRepo.length === 0) {
61
+ return;
62
+ }
63
+ const key = `${resolvedRepo}:${number}`;
64
+ if (seen.has(key)) {
65
+ return;
66
+ }
67
+ seen.add(key);
68
+ out.push({ repo: resolvedRepo, number });
69
+ }
70
+ /** Collect GitHub issue and PR refs from a scope plan object. */
71
+ export function collectGithubRefs(plan, defaultRepo) {
72
+ const issues = [];
73
+ const prs = [];
74
+ const seenIssues = new Set();
75
+ const seenPrs = new Set();
76
+ const refs = plan.references;
77
+ if (Array.isArray(refs)) {
78
+ for (const ref of refs) {
79
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
80
+ continue;
81
+ }
82
+ const typed = ref;
83
+ const type = String(typed.type ?? "");
84
+ if (referenceTypeMatches(type, "github-issue")) {
85
+ const [repo, number] = parseGithubIssueUri(typed.uri);
86
+ addIssueRef(issues, seenIssues, repo, number, defaultRepo);
87
+ }
88
+ else if (referenceTypeMatches(type, "github-pr")) {
89
+ const [repo, number] = parseGithubPrUri(typed.uri);
90
+ addPrRef(prs, seenPrs, repo, number, defaultRepo);
91
+ }
92
+ }
93
+ }
94
+ const metadata = plan.metadata;
95
+ if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)) {
96
+ const tracking = metadata["x-tracking"];
97
+ if (typeof tracking === "object" && tracking !== null && !Array.isArray(tracking)) {
98
+ const t = tracking;
99
+ addIssueRef(issues, seenIssues, defaultRepo, parseTrackingIssue(t.parent_issue), defaultRepo);
100
+ addIssueRef(issues, seenIssues, defaultRepo, parseTrackingIssue(t.decomposition_origin), defaultRepo);
101
+ }
102
+ }
103
+ return { issues, prs };
104
+ }
105
+ //# sourceMappingURL=refs.js.map
@@ -0,0 +1,21 @@
1
+ import type { HookHost } from "../hooks/dispatcher.js";
2
+ export declare const FIELD_HOST_HOOKS = "plan.policy.hostHooks";
3
+ export declare const FIELD_HOST_HOOKS_CLI_ALIAS = "hostHooks";
4
+ /** Per-host Directive hook deposit toggles (#2752). */
5
+ export type HostHooksPolicy = Record<HookHost, boolean>;
6
+ export declare const DEFAULT_HOST_HOOKS_POLICY: HostHooksPolicy;
7
+ export interface HostHooksPolicyField {
8
+ readonly name: string;
9
+ readonly current: HostHooksPolicy;
10
+ readonly default: HostHooksPolicy;
11
+ readonly source: string;
12
+ }
13
+ /** Resolve typed host hook deposit policy from raw PROJECT-DEFINITION value. */
14
+ export declare function resolveHostHooksPolicy(raw: unknown): HostHooksPolicy;
15
+ export declare function validateHostHooks(value: unknown): string[];
16
+ export declare function isHostHookDepositEnabled(host: HookHost, policy?: HostHooksPolicy): boolean;
17
+ /** Inspector row for `policy:show --field=hostHooks`. */
18
+ export declare function inspectHostHooks(data: Record<string, unknown> | null): HostHooksPolicyField;
19
+ /** Resolve host hook deposit policy from PROJECT-DEFINITION on disk. */
20
+ export declare function loadHostHooksPolicyFromProject(projectRoot: string): HostHooksPolicy;
21
+ //# sourceMappingURL=host-hooks.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { HOOK_HOSTS } from "../hooks/dispatcher.js";
2
+ import { readPlanPolicy } from "./plan-extensions.js";
3
+ import { loadProjectDefinition } from "./resolve.js";
4
+ export const FIELD_HOST_HOOKS = "plan.policy.hostHooks";
5
+ export const FIELD_HOST_HOOKS_CLI_ALIAS = "hostHooks";
6
+ export const DEFAULT_HOST_HOOKS_POLICY = {
7
+ claude: true,
8
+ cursor: true,
9
+ grok: true,
10
+ codex: true,
11
+ };
12
+ function readHostBoolean(rec, host, fallback) {
13
+ if (host in rec && typeof rec[host] === "boolean") {
14
+ return rec[host];
15
+ }
16
+ return fallback;
17
+ }
18
+ /** Resolve typed host hook deposit policy from raw PROJECT-DEFINITION value. */
19
+ export function resolveHostHooksPolicy(raw) {
20
+ if (raw === null || raw === undefined) {
21
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
22
+ }
23
+ if (typeof raw !== "object" || Array.isArray(raw)) {
24
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
25
+ }
26
+ const rec = raw;
27
+ return {
28
+ claude: readHostBoolean(rec, "claude", DEFAULT_HOST_HOOKS_POLICY.claude),
29
+ cursor: readHostBoolean(rec, "cursor", DEFAULT_HOST_HOOKS_POLICY.cursor),
30
+ grok: readHostBoolean(rec, "grok", DEFAULT_HOST_HOOKS_POLICY.grok),
31
+ codex: readHostBoolean(rec, "codex", DEFAULT_HOST_HOOKS_POLICY.codex),
32
+ };
33
+ }
34
+ export function validateHostHooks(value) {
35
+ if (value === null || value === undefined) {
36
+ return [];
37
+ }
38
+ if (typeof value !== "object" || Array.isArray(value)) {
39
+ return [`${FIELD_HOST_HOOKS} must be an object; got ${typeof value}`];
40
+ }
41
+ const rec = value;
42
+ const errors = [];
43
+ for (const host of HOOK_HOSTS) {
44
+ if (host in rec && typeof rec[host] !== "boolean") {
45
+ errors.push(`${FIELD_HOST_HOOKS}.${host} must be a boolean`);
46
+ }
47
+ }
48
+ for (const key of Object.keys(rec)) {
49
+ if (!HOOK_HOSTS.includes(key)) {
50
+ errors.push(`${FIELD_HOST_HOOKS}.${key} is not a deposited host (${HOOK_HOSTS.join(", ")})`);
51
+ }
52
+ }
53
+ return errors;
54
+ }
55
+ export function isHostHookDepositEnabled(host, policy = DEFAULT_HOST_HOOKS_POLICY) {
56
+ return policy[host];
57
+ }
58
+ function fieldFromResolved(resolved, source) {
59
+ return {
60
+ name: FIELD_HOST_HOOKS,
61
+ current: resolved,
62
+ default: DEFAULT_HOST_HOOKS_POLICY,
63
+ source,
64
+ };
65
+ }
66
+ /** Inspector row for `policy:show --field=hostHooks`. */
67
+ export function inspectHostHooks(data) {
68
+ if (data === null) {
69
+ return fieldFromResolved(DEFAULT_HOST_HOOKS_POLICY, "default");
70
+ }
71
+ const policyBlock = readPlanPolicy(data.plan);
72
+ if (typeof policyBlock !== "object" ||
73
+ policyBlock === null ||
74
+ Array.isArray(policyBlock) ||
75
+ !("hostHooks" in policyBlock)) {
76
+ return fieldFromResolved(DEFAULT_HOST_HOOKS_POLICY, "default");
77
+ }
78
+ const resolved = resolveHostHooksPolicy(policyBlock.hostHooks);
79
+ return fieldFromResolved(resolved, "typed");
80
+ }
81
+ /** Resolve host hook deposit policy from PROJECT-DEFINITION on disk. */
82
+ export function loadHostHooksPolicyFromProject(projectRoot) {
83
+ const [data] = loadProjectDefinition(projectRoot);
84
+ if (data === null) {
85
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
86
+ }
87
+ const policyBlock = readPlanPolicy(data.plan);
88
+ if (typeof policyBlock !== "object" ||
89
+ policyBlock === null ||
90
+ Array.isArray(policyBlock) ||
91
+ !("hostHooks" in policyBlock)) {
92
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
93
+ }
94
+ return resolveHostHooksPolicy(policyBlock.hostHooks);
95
+ }
96
+ //# sourceMappingURL=host-hooks.js.map
@@ -3,6 +3,7 @@ export * from "./autonomy.js";
3
3
  export * from "./capacity.js";
4
4
  export * from "./decisions.js";
5
5
  export * from "./disclosure.js";
6
+ export * from "./host-hooks.js";
6
7
  export * from "./plan-extensions.js";
7
8
  export * from "./policy-invocation.js";
8
9
  export * from "./product-signal.js";
@@ -1,3 +1,4 @@
1
+ import { FIELD_HOST_HOOKS, FIELD_HOST_HOOKS_CLI_ALIAS, inspectHostHooks } from "./host-hooks.js";
1
2
  import { readPlanPolicy } from "./plan-extensions.js";
2
3
  import { FIELD_PRODUCT_SIGNAL, FIELD_PRODUCT_SIGNAL_CLI_ALIAS, inspectProductSignal, } from "./product-signal.js";
3
4
  import { coerceLegacyNarrative, LEGACY_NARRATIVE_KEY, loadProjectDefinition } from "./resolve.js";
@@ -10,6 +11,7 @@ export * from "./autonomy.js";
10
11
  export * from "./capacity.js";
11
12
  export * from "./decisions.js";
12
13
  export * from "./disclosure.js";
14
+ export * from "./host-hooks.js";
13
15
  export * from "./plan-extensions.js";
14
16
  export * from "./policy-invocation.js";
15
17
  export * from "./product-signal.js";
@@ -258,6 +260,15 @@ function inspectRuntimeAuthorityField(data) {
258
260
  source: field.source,
259
261
  };
260
262
  }
263
+ function inspectHostHooksField(data) {
264
+ const field = inspectHostHooks(data);
265
+ return {
266
+ name: field.name,
267
+ current: field.current,
268
+ default: field.default,
269
+ source: field.source,
270
+ };
271
+ }
261
272
  const REGISTERED_POLICIES = [
262
273
  inspectAllowDirectCommits,
263
274
  inspectWipCap,
@@ -270,6 +281,7 @@ const REGISTERED_POLICIES = [
270
281
  emptyIsTyped: true,
271
282
  }),
272
283
  inspectSwarmSubagentBackend,
284
+ inspectHostHooksField,
273
285
  inspectStalenessTicklerField,
274
286
  inspectRuntimeAuthorityField,
275
287
  inspectProductSignalField,
@@ -290,7 +302,9 @@ export function inspectOnePolicy(name, projectRoot) {
290
302
  ? FIELD_STALENESS_TICKLER
291
303
  : name === FIELD_RUNTIME_AUTHORITY_CLI_ALIAS
292
304
  ? FIELD_RUNTIME_AUTHORITY
293
- : name;
305
+ : name === FIELD_HOST_HOOKS_CLI_ALIAS
306
+ ? FIELD_HOST_HOOKS
307
+ : name;
294
308
  for (const field of inspectAllPolicies(projectRoot)) {
295
309
  if (field.name === normalized)
296
310
  return field;
@@ -1,8 +1,16 @@
1
+ import { type SemanticGreenFn } from "./semantic-green.js";
1
2
  import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
2
3
  export interface WaitMergeableOptions {
3
4
  readonly protectedFn?: ProtectedCheckFn;
4
5
  readonly monitorFn?: MonitorFn;
5
6
  readonly mergeFn?: MergeFn;
7
+ readonly semanticGreenFn?: SemanticGreenFn;
8
+ /** Enable merge-cascade semantic-green gate (#2385). */
9
+ readonly cascadeMode?: boolean;
10
+ /** After a prior cascade merge, require target-branch CI green at HEAD. */
11
+ readonly requireMasterCiGreen?: boolean;
12
+ /** Target branch override for stale-base comparison (defaults to PR base.ref). */
13
+ readonly baseBranch?: string | null;
6
14
  }
7
15
  /** Run protected-check -> wait -> merge cascade (#1369). */
8
16
  export declare function waitMergeableAndMerge(prNumber: number, repo: string, options: {
@@ -1,6 +1,7 @@
1
1
  import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
2
2
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
3
3
  import { makeResult } from "./result.js";
4
+ import { evaluateSemanticGreen } from "./semantic-green.js";
4
5
  import { runGhMerge, runMonitor, runProtectedCheck } from "./wrappers.js";
5
6
  /** Node module-not-found / missing script — not a protected-issue overlap (#2667). */
6
7
  function isProtectedCheckConfigFailure(stderr) {
@@ -14,6 +15,7 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
14
15
  const protectedFn = options.protectedFn ?? runProtectedCheck;
15
16
  const monitorFn = options.monitorFn ?? runMonitor;
16
17
  const mergeFn = options.mergeFn ?? runGhMerge;
18
+ const semanticGreenFn = options.semanticGreenFn ?? evaluateSemanticGreen;
17
19
  const protectedIssues = options.protected;
18
20
  let protectedCheckPayload = {};
19
21
  if (protectedIssues.length > 0) {
@@ -47,6 +49,22 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
47
49
  });
48
50
  }
49
51
  }
52
+ const semanticGreen = semanticGreenFn(prNumber, repo, {
53
+ cascadeMode: options.cascadeMode,
54
+ requireMasterCiGreen: options.requireMasterCiGreen,
55
+ baseBranch: options.baseBranch,
56
+ });
57
+ if (!semanticGreen.ok) {
58
+ return makeResult({
59
+ prNumber,
60
+ repo,
61
+ outcome: semanticGreen.outcome ?? "semantic-green-blocked",
62
+ exitCode: semanticGreen.exitCode ?? EXIT_TIMEOUT_OR_ESCALATION,
63
+ protectedCheck: protectedCheckPayload,
64
+ semanticGreen: { ...semanticGreen.payload },
65
+ error: semanticGreen.error,
66
+ });
67
+ }
50
68
  const [monRc, monStdout, monStderr] = monitorFn(prNumber, repo, options.capMinutes);
51
69
  const monitorPayload = parseMonitorPayload(monStdout);
52
70
  const [outcome, monitorExit] = classifyMonitorOutcome(monRc, monitorPayload);
@@ -5,6 +5,9 @@ export interface ParsedWaitMergeableArgs {
5
5
  readonly capMinutes: number;
6
6
  readonly protectedValues: readonly string[];
7
7
  readonly emitJson: boolean;
8
+ readonly cascadeMode: boolean;
9
+ readonly requireMasterCiGreen: boolean;
10
+ readonly baseBranch: string | null;
8
11
  readonly error?: string;
9
12
  }
10
13
  export declare function parseWaitMergeableArgs(argv: readonly string[]): ParsedWaitMergeableArgs;
@@ -12,6 +15,7 @@ export interface RunWaitMergeableOptions {
12
15
  readonly protectedFn?: Parameters<typeof waitMergeableAndMerge>[2]["protectedFn"];
13
16
  readonly monitorFn?: Parameters<typeof waitMergeableAndMerge>[2]["monitorFn"];
14
17
  readonly mergeFn?: Parameters<typeof waitMergeableAndMerge>[2]["mergeFn"];
18
+ readonly semanticGreenFn?: Parameters<typeof waitMergeableAndMerge>[2]["semanticGreenFn"];
15
19
  }
16
20
  export declare function runWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;
17
21
  export declare function cmdPrWaitMergeable(argv: readonly string[], options?: RunWaitMergeableOptions): number;