@deftai/directive-core 0.85.0 → 0.87.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 (50) hide show
  1. package/dist/check/gate-lists.js +1 -0
  2. package/dist/doctor/constants.d.ts +2 -2
  3. package/dist/doctor/constants.js +2 -2
  4. package/dist/doctor/main.d.ts +8 -6
  5. package/dist/doctor/main.js +102 -36
  6. package/dist/doctor/taskfile.d.ts +8 -0
  7. package/dist/doctor/taskfile.js +19 -0
  8. package/dist/doctor/types.d.ts +3 -0
  9. package/dist/hooks/dispatcher.d.ts +21 -1
  10. package/dist/hooks/dispatcher.js +85 -15
  11. package/dist/init-deposit/agent-hooks.js +14 -6
  12. package/dist/init-deposit/gitignore.js +5 -0
  13. package/dist/intake/issue-emit.d.ts +45 -2
  14. package/dist/intake/issue-emit.js +420 -17
  15. package/dist/intake/issue-ingest.js +54 -4
  16. package/dist/platform/platform-capabilities.js +3 -0
  17. package/dist/policy/org-force-on-migration.js +2 -0
  18. package/dist/render/framework-commands.d.ts +1 -1
  19. package/dist/render/framework-commands.js +6 -6
  20. package/dist/render/roadmap-render.d.ts +5 -1
  21. package/dist/render/roadmap-render.js +20 -2
  22. package/dist/render/rule-map.js +5 -0
  23. package/dist/review-monitor/constants.js +3 -2
  24. package/dist/review-monitor/tier-detection.d.ts +6 -2
  25. package/dist/review-monitor/tier-detection.js +27 -2
  26. package/dist/scope/transition.js +43 -0
  27. package/dist/session/release-availability.d.ts +2 -0
  28. package/dist/session/release-availability.js +23 -8
  29. package/dist/swarm/routing-set-cli.js +5 -10
  30. package/dist/swarm/routing.d.ts +3 -2
  31. package/dist/swarm/routing.js +16 -4
  32. package/dist/triage/help/registry-data.d.ts +7 -7
  33. package/dist/triage/help/registry-data.js +15 -6
  34. package/dist/triage/queue/index.d.ts +1 -0
  35. package/dist/triage/queue/index.js +1 -0
  36. package/dist/triage/queue/show.d.ts +69 -0
  37. package/dist/triage/queue/show.js +293 -0
  38. package/dist/triage/scope/cli.js +3 -0
  39. package/dist/triage/scope/coverage.d.ts +2 -0
  40. package/dist/triage/scope/coverage.js +18 -3
  41. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  42. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  43. package/dist/verify-env/index.d.ts +1 -0
  44. package/dist/verify-env/index.js +1 -0
  45. package/dist/verify-source/index.d.ts +1 -0
  46. package/dist/verify-source/index.js +1 -0
  47. package/dist/verify-source/openclaw-tier1.d.ts +37 -0
  48. package/dist/verify-source/openclaw-tier1.js +100 -0
  49. package/dist/xbrief-migrate/migrate-project.js +35 -22
  50. package/package.json +3 -3
@@ -0,0 +1,293 @@
1
+ /**
2
+ * triage:show default + operator brief renderers (#1128 / #2890).
3
+ *
4
+ * Default format mirrors the pre-Python-removal `render_show` surface.
5
+ * `--format=operator` emits a pasteable Phase 3 candidate brief backbone;
6
+ * the agent still owns lean (not invented here).
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "./constants.js";
11
+ function parseLabels(raw) {
12
+ if (!Array.isArray(raw)) {
13
+ return [];
14
+ }
15
+ const labels = [];
16
+ for (const item of raw) {
17
+ if (typeof item === "object" && item !== null) {
18
+ const name = item.name;
19
+ if (typeof name === "string") {
20
+ labels.push(name);
21
+ }
22
+ }
23
+ else if (typeof item === "string") {
24
+ labels.push(item);
25
+ }
26
+ }
27
+ return labels;
28
+ }
29
+ /** Collapse CR/LF so cached attacker text cannot break markdown bullets (P2). */
30
+ export function oneLine(value) {
31
+ return value.replace(/\r?\n/gu, " ").trim();
32
+ }
33
+ /**
34
+ * Resolve a safe issue link. Always construct the canonical github.com path for
35
+ * `owner/name#N` rather than trusting payload URL substrings (CodeQL
36
+ * incomplete-url-substring-sanitization).
37
+ */
38
+ export function resolveIssueHtmlUrl(repo, number) {
39
+ return `https://github.com/${repo}/issues/${number}`;
40
+ }
41
+ /** Load a single cached issue (include closed) or null on miss. */
42
+ export function loadCachedIssueDetail(repo, number, options = {
43
+ projectRoot: process.cwd(),
44
+ }) {
45
+ if (!repo.includes("/")) {
46
+ throw new Error(`repo must be 'owner/name'; got '${repo}'`);
47
+ }
48
+ const parts = repo.split("/", 2);
49
+ const owner = parts[0];
50
+ const name = parts[1];
51
+ if (owner === undefined || name === undefined || owner.length === 0 || name.length === 0) {
52
+ throw new Error(`repo must be 'owner/name'; got '${repo}'`);
53
+ }
54
+ const source = options.source ?? CACHE_SOURCE_GITHUB_ISSUE;
55
+ const cacheBase = options.cacheRoot !== null && options.cacheRoot !== undefined && options.cacheRoot.length > 0
56
+ ? resolve(options.cacheRoot)
57
+ : join(resolve(options.projectRoot), CACHE_DIR_NAME);
58
+ const entryDir = join(cacheBase, source, owner, name, String(number));
59
+ const rawPath = join(entryDir, "raw.json");
60
+ if (!existsSync(rawPath)) {
61
+ return null;
62
+ }
63
+ let payload;
64
+ try {
65
+ const parsed = JSON.parse(readFileSync(rawPath, { encoding: "utf8" }));
66
+ if (typeof parsed !== "object" || parsed === null) {
67
+ return null;
68
+ }
69
+ payload = parsed;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ const n = typeof payload.number === "number" ? payload.number : number;
75
+ const stateRaw = payload.state ?? "open";
76
+ const state = typeof stateRaw === "string" ? stateRaw.toLowerCase() : "open";
77
+ const title = typeof payload.title === "string" ? payload.title : "";
78
+ const body = typeof payload.body === "string" ? payload.body : "";
79
+ const updatedAt = typeof payload.updated_at === "string"
80
+ ? payload.updated_at
81
+ : typeof payload.updatedAt === "string"
82
+ ? payload.updatedAt
83
+ : "";
84
+ return {
85
+ number: n,
86
+ title,
87
+ state,
88
+ labels: parseLabels(payload.labels),
89
+ updatedAt,
90
+ body,
91
+ htmlUrl: resolveIssueHtmlUrl(repo, n),
92
+ };
93
+ }
94
+ /** Default triage:show text (audit/cache oriented). */
95
+ export function renderShow(options) {
96
+ const lines = [];
97
+ lines.push(`triage:show -- ${options.repo}#${options.number}`);
98
+ if (options.issue === null) {
99
+ lines.push("");
100
+ lines.push(" (issue not present in local cache)");
101
+ lines.push(" Run `task triage:bootstrap` to populate, or check the repo slug.");
102
+ return lines.join("\n");
103
+ }
104
+ const labels = options.issue.labels.map(oneLine);
105
+ lines.push(` title: ${oneLine(options.issue.title)}`);
106
+ lines.push(` state: ${oneLine(options.issue.state)}`);
107
+ lines.push(` labels: ${labels.length > 0 ? labels.join(", ") : "<none>"}`);
108
+ lines.push(` updated_at: ${oneLine(options.issue.updatedAt)}`);
109
+ lines.push("");
110
+ lines.push(` active xBRIEF reference: ${options.inActiveXbrief ? "yes" : "no"}`);
111
+ if (options.latestDecision !== null) {
112
+ const d = options.latestDecision;
113
+ lines.push(` latest decision: ${oneLine(String(d.decision ?? "?"))} at ${oneLine(String(d.timestamp ?? "?"))} by ${oneLine(String(d.actor ?? "?"))}`);
114
+ if (typeof d.reason === "string" && d.reason.length > 0) {
115
+ lines.push(` reason: ${oneLine(d.reason)}`);
116
+ }
117
+ }
118
+ else {
119
+ lines.push(" latest decision: <none -- untriaged>");
120
+ }
121
+ if (options.history.length > 0) {
122
+ lines.push("");
123
+ lines.push(` history (${options.history.length} entries, oldest first):`);
124
+ for (const entry of options.history) {
125
+ const decision = oneLine(String(entry.decision ?? "?")).padEnd(14);
126
+ lines.push(` - ${oneLine(String(entry.timestamp ?? "?"))} ${decision} by ${oneLine(String(entry.actor ?? "?"))}`);
127
+ }
128
+ }
129
+ return lines.join("\n");
130
+ }
131
+ /** Collapse blank lines and trim for summary extraction. */
132
+ function normalizeBody(body) {
133
+ return body.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
134
+ }
135
+ /**
136
+ * Extract a short problem/context summary (2–5 lines) from issue body.
137
+ * Prefers text before the first `##` section; falls back to leading paragraphs.
138
+ */
139
+ export function extractBodySummary(body, maxLines = 5) {
140
+ const text = normalizeBody(body);
141
+ if (text.length === 0) {
142
+ return "(thin body / no summary)";
143
+ }
144
+ // Drop leading H1/title lines
145
+ let rest = text.replace(/^#[^\n]*\n+/u, "");
146
+ // Prefer content before first ## heading (often Summary/Description)
147
+ const firstSection = rest.search(/^##\s+/mu);
148
+ if (firstSection > 0) {
149
+ rest = rest.slice(0, firstSection).trim();
150
+ }
151
+ else if (firstSection === 0) {
152
+ // Body starts with ## — take the first section body
153
+ const next = rest.search(/\n##\s+/u);
154
+ const block = next === -1 ? rest : rest.slice(0, next);
155
+ rest = block.replace(/^##[^\n]*\n?/u, "").trim();
156
+ }
157
+ const paragraphs = rest
158
+ .split(/\n{2,}/u)
159
+ .map((p) => p.replace(/\s+/gu, " ").trim())
160
+ .filter((p) => p.length > 0 && !p.startsWith("#"));
161
+ if (paragraphs.length === 0) {
162
+ const lines = rest
163
+ .split("\n")
164
+ .map((l) => l.trim())
165
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
166
+ if (lines.length === 0) {
167
+ return "(thin body / no summary)";
168
+ }
169
+ return lines.slice(0, maxLines).join("\n");
170
+ }
171
+ const out = [];
172
+ for (const para of paragraphs) {
173
+ if (out.length >= maxLines)
174
+ break;
175
+ // Soft-wrap long paragraphs into ~100-char lines for pasteability
176
+ if (para.length <= 120) {
177
+ out.push(para);
178
+ }
179
+ else {
180
+ const words = para.split(/\s+/u);
181
+ let line = "";
182
+ for (const w of words) {
183
+ const next = line.length === 0 ? w : `${line} ${w}`;
184
+ if (next.length > 100 && line.length > 0) {
185
+ out.push(line);
186
+ line = w;
187
+ if (out.length >= maxLines)
188
+ break;
189
+ }
190
+ else {
191
+ line = next;
192
+ }
193
+ }
194
+ if (line.length > 0 && out.length < maxLines) {
195
+ out.push(line);
196
+ }
197
+ }
198
+ }
199
+ return out.slice(0, maxLines).join("\n");
200
+ }
201
+ /**
202
+ * Extract acceptance-criteria bullets from body, or a thin-body note.
203
+ * Looks for AC / Acceptance headings and checkbox / bullet lists under them.
204
+ */
205
+ export function extractAcceptanceCriteria(body) {
206
+ const text = normalizeBody(body);
207
+ if (text.length === 0) {
208
+ return [];
209
+ }
210
+ const headingRe = /^#{1,3}\s*(?:acceptance\s*criteria|acceptance|ac\b|success\s*criteria)[^\n]*$/imu;
211
+ const match = headingRe.exec(text);
212
+ if (match === null || match.index === undefined) {
213
+ // Loose: checkbox list near top without a heading
214
+ const loose = [];
215
+ for (const line of text.split("\n")) {
216
+ const m = /^\s*[-*]\s*\[[ xX]\]\s+(.+)$/u.exec(line);
217
+ if (m?.[1] !== undefined) {
218
+ loose.push(m[1].trim());
219
+ }
220
+ }
221
+ return loose.slice(0, 12);
222
+ }
223
+ const after = text.slice(match.index + match[0].length);
224
+ // Stop only at a next ## (H2) peer section — keep ### subsections (A/B/C…) inside AC.
225
+ const nextPeer = after.search(/\n##\s+\S/u);
226
+ const section = nextPeer === -1 ? after : after.slice(0, nextPeer);
227
+ const bullets = [];
228
+ for (const line of section.split("\n")) {
229
+ const checkbox = /^\s*[-*]\s*\[[ xX]\]\s+(.+)$/u.exec(line);
230
+ if (checkbox?.[1] !== undefined) {
231
+ bullets.push(checkbox[1].trim());
232
+ continue;
233
+ }
234
+ // Prefer checkboxes under AC; plain bullets are also accepted
235
+ const bullet = /^\s*[-*]\s+(.+)$/u.exec(line);
236
+ if (bullet?.[1] !== undefined) {
237
+ // Skip nested subsection-only markers that are just headings-as-bullets
238
+ bullets.push(bullet[1].trim());
239
+ continue;
240
+ }
241
+ const numbered = /^\s*\d+[.)]\s+(.+)$/u.exec(line);
242
+ if (numbered?.[1] !== undefined) {
243
+ bullets.push(numbered[1].trim());
244
+ }
245
+ }
246
+ return bullets.slice(0, 12);
247
+ }
248
+ /** Operator-facing pasteable brief backbone for Phase 3 decisions (#2890). */
249
+ export function renderOperatorBrief(options) {
250
+ const lines = [];
251
+ lines.push(`triage:show --format=operator -- ${options.repo}#${options.number}`);
252
+ if (options.issue === null) {
253
+ lines.push("");
254
+ lines.push(" (issue not present in local cache)");
255
+ lines.push(" Run `task triage:bootstrap` / re-sync per Phase 0, or check the repo slug.");
256
+ return lines.join("\n");
257
+ }
258
+ const issue = options.issue;
259
+ const link = issue.htmlUrl ?? resolveIssueHtmlUrl(options.repo, options.number);
260
+ const labels = issue.labels.length > 0 ? issue.labels.map(oneLine).join(", ") : "<none>";
261
+ lines.push(`#${options.number} ${oneLine(issue.title)}`);
262
+ lines.push(`link: ${oneLine(link)}`);
263
+ lines.push(`labels: ${labels}`);
264
+ lines.push("");
265
+ lines.push("summary:");
266
+ for (const line of extractBodySummary(issue.body).split("\n")) {
267
+ lines.push(` ${oneLine(line)}`);
268
+ }
269
+ lines.push("");
270
+ const ac = extractAcceptanceCriteria(issue.body);
271
+ if (ac.length === 0) {
272
+ lines.push("acceptance criteria: (thin body / no AC)");
273
+ }
274
+ else {
275
+ lines.push("acceptance criteria:");
276
+ for (const item of ac) {
277
+ lines.push(` - ${oneLine(item)}`);
278
+ }
279
+ }
280
+ lines.push("");
281
+ if (options.latestDecision !== null) {
282
+ const d = options.latestDecision;
283
+ lines.push(`latest decision: ${oneLine(String(d.decision ?? "?"))} at ${oneLine(String(d.timestamp ?? "?"))} by ${oneLine(String(d.actor ?? "?"))}`);
284
+ }
285
+ else {
286
+ lines.push("latest decision: <none -- untriaged>");
287
+ }
288
+ lines.push(`active xBRIEF: ${options.inActiveXbrief ? "yes" : "no"}`);
289
+ lines.push("");
290
+ lines.push("lean: (agent-owned — not filled by triage:show)");
291
+ return lines.join("\n");
292
+ }
293
+ //# sourceMappingURL=show.js.map
@@ -232,9 +232,12 @@ export function runCliCapture(argv) {
232
232
  cacheRoot: args.cacheRoot !== undefined ? resolve(args.cacheRoot) : undefined,
233
233
  });
234
234
  const subHash = subscriptionHash(rules);
235
+ // When --cache-root is outside the project tree, contain against that root
236
+ // instead of projectRoot (tests and operators may redirect the cache) (#2869).
235
237
  const record = writeCoverageDenominator(path, {
236
238
  count: args.count,
237
239
  subscriptionHashValue: subHash,
240
+ projectRoot: args.cacheRoot !== undefined ? resolve(args.cacheRoot) : projectRoot,
238
241
  });
239
242
  stdout.push(`triage:scope: wrote coverage denominator count=${record.count} ` +
240
243
  `subscription-hash=${record.subscriptionHash} path=${path}\n`);
@@ -14,6 +14,8 @@ export declare function writeCoverageDenominator(path: string, options: {
14
14
  count: number;
15
15
  subscriptionHashValue: string;
16
16
  fetchedAt?: Date;
17
+ /** Project (or cache) root for symlink containment (#2869). */
18
+ projectRoot?: string;
17
19
  }): CoverageRecord;
18
20
  export declare function readCoverageDenominator(path: string, options: {
19
21
  currentHash: string;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe } from "../../fs/projection-containment.js";
3
4
  import { CACHE_DIR_NAME, COVERAGE_FILENAME, DEFAULT_COVERAGE_TTL_HOURS, ENV_COVERAGE_TTL_HOURS, } from "./constants.js";
4
5
  import { parseIso, utcIso, utcNow } from "./time.js";
5
6
  export function coveragePath(source, repo, options = {}) {
@@ -29,14 +30,28 @@ export function writeCoverageDenominator(path, options) {
29
30
  if (!options.subscriptionHashValue) {
30
31
  throw new Error("subscription_hash_value must be a non-empty string");
31
32
  }
33
+ const absPath = resolve(path);
34
+ // Production CLI always passes projectRoot. When omitted (unit tests with a
35
+ // bare cacheRoot), walk up to the deepest existing ancestor so containment
36
+ // can realpath the root before mkdir creates intermediate dirs (#2869).
37
+ let containmentRoot = options.projectRoot !== undefined ? resolve(options.projectRoot) : dirname(absPath);
38
+ if (options.projectRoot === undefined) {
39
+ while (!existsSync(containmentRoot)) {
40
+ const parent = dirname(containmentRoot);
41
+ if (parent === containmentRoot)
42
+ break;
43
+ containmentRoot = parent;
44
+ }
45
+ }
46
+ assertWriteTargetSafe(containmentRoot, absPath);
32
47
  const stamp = utcIso(options.fetchedAt ?? null);
33
- mkdirSync(join(path, ".."), { recursive: true });
48
+ mkdirSync(dirname(absPath), { recursive: true });
34
49
  const payload = {
35
50
  count: options.count,
36
51
  fetched_at: stamp,
37
52
  subscription_hash: options.subscriptionHashValue,
38
53
  };
39
- writeFileSync(path, `${JSON.stringify(payload, Object.keys(payload).sort())}\n`, "utf8");
54
+ writeFileSync(absPath, `${JSON.stringify(payload, Object.keys(payload).sort())}\n`, "utf8");
40
55
  return {
41
56
  count: options.count,
42
57
  fetchedAt: stamp,
@@ -0,0 +1,32 @@
1
+ import type { HookEvent, HookHost } from "../hooks/dispatcher.js";
2
+ export type AgentHookLiveProbeIssue = "hook-command-missing" | "spawn-failed" | "empty-stdout" | "unparseable-json" | "missing-allow" | "missing-deny";
3
+ export interface AgentHookLiveProbeCase {
4
+ readonly host: HookHost;
5
+ readonly event: HookEvent;
6
+ readonly fixture: "allow" | "deny";
7
+ readonly issue: AgentHookLiveProbeIssue;
8
+ readonly detail: string;
9
+ }
10
+ export interface AgentHookLiveProbeResult {
11
+ readonly code: 0 | 1 | 2;
12
+ readonly message: string;
13
+ readonly cases: readonly AgentHookLiveProbeCase[];
14
+ }
15
+ export interface AgentHookLiveProbeSeams {
16
+ readonly resolveCommand?: (name: string) => string | null;
17
+ readonly spawnHook?: (input: {
18
+ readonly command: string;
19
+ readonly args: readonly string[];
20
+ readonly stdin: string;
21
+ readonly cwd: string;
22
+ readonly env?: NodeJS.ProcessEnv;
23
+ }) => {
24
+ readonly status: number;
25
+ readonly stdout: string;
26
+ readonly stderr: string;
27
+ };
28
+ }
29
+ export declare function quoteWindowsCmdArg(value: string): string;
30
+ /** Spawn the configured hook command and assert Cursor tool.before allow/deny behavior. */
31
+ export declare function probeAgentHooksLive(projectRoot: string, seams?: AgentHookLiveProbeSeams): AgentHookLiveProbeResult;
32
+ //# sourceMappingURL=agent-hooks-live-probe.d.ts.map
@@ -0,0 +1,216 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { READ_ONLY_HOOK_ENV } from "../hooks/tools.js";
4
+ import { DEFT_HOOK_COMMAND_MARKER } from "../init-deposit/agent-hooks.js";
5
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
6
+ import { quoteWin32CommandForShell, resolveCommandOnPath, shouldUseShellForCommand, } from "./command-spawn.js";
7
+ const LIVE_PROBE_HOST = "cursor";
8
+ const LIVE_PROBE_EVENT = "tool.before";
9
+ function resolveHookCommand(seams) {
10
+ const resolveCommand = seams.resolveCommand ?? resolveCommandOnPath;
11
+ const deftHook = resolveCommand(DEFT_HOOK_COMMAND_MARKER);
12
+ if (deftHook !== null) {
13
+ return { command: deftHook, argsPrefix: [] };
14
+ }
15
+ const deft = resolveCommand("deft");
16
+ if (deft !== null) {
17
+ return { command: deft, argsPrefix: ["hook:dispatch"] };
18
+ }
19
+ const directive = resolveCommand("directive");
20
+ if (directive !== null) {
21
+ return { command: directive, argsPrefix: ["hook:dispatch"] };
22
+ }
23
+ return null;
24
+ }
25
+ export function quoteWindowsCmdArg(value) {
26
+ const escaped = value.replace(/%/g, "%%");
27
+ if (!/[\s"&|<>^()]/.test(escaped)) {
28
+ return escaped;
29
+ }
30
+ return `"${escaped.replace(/"/g, '""')}"`;
31
+ }
32
+ function spawnHookWithStdin(command, args, stdin, cwd, env = process.env) {
33
+ const shell = shouldUseShellForCommand(command);
34
+ if (shell && process.platform === "win32") {
35
+ const cmdLine = [quoteWin32CommandForShell(command), ...args.map(quoteWindowsCmdArg)].join(" ");
36
+ const proc = spawnSync(cmdLine, [], {
37
+ input: stdin,
38
+ cwd,
39
+ env,
40
+ encoding: "utf8",
41
+ stdio: ["pipe", "pipe", "pipe"],
42
+ shell: true,
43
+ windowsHide: true,
44
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
45
+ });
46
+ const status = proc.status ?? (proc.error ? 2 : proc.signal ? 128 : 0);
47
+ return {
48
+ status,
49
+ stdout: typeof proc.stdout === "string" ? proc.stdout : "",
50
+ stderr: typeof proc.stderr === "string" ? proc.stderr : "",
51
+ };
52
+ }
53
+ const spawnCmd = shell && process.platform === "win32" ? quoteWin32CommandForShell(command) : command;
54
+ const proc = spawnSync(spawnCmd, [...args], {
55
+ input: stdin,
56
+ cwd,
57
+ env,
58
+ encoding: "utf8",
59
+ stdio: ["pipe", "pipe", "pipe"],
60
+ shell,
61
+ windowsHide: true,
62
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
63
+ });
64
+ const status = proc.status ?? (proc.error ? 2 : proc.signal ? 128 : 0);
65
+ return {
66
+ status,
67
+ stdout: typeof proc.stdout === "string" ? proc.stdout : "",
68
+ stderr: typeof proc.stderr === "string" ? proc.stderr : "",
69
+ };
70
+ }
71
+ function allowFixture(projectRoot) {
72
+ return JSON.stringify({
73
+ tool_name: "Read",
74
+ cwd: projectRoot,
75
+ workspace_roots: [projectRoot],
76
+ });
77
+ }
78
+ function denyFixture(projectRoot) {
79
+ return JSON.stringify({
80
+ tool_name: "Task",
81
+ cwd: projectRoot,
82
+ workspace_roots: [projectRoot],
83
+ tool_input: { subagent_type: "generalPurpose", prompt: "implement" },
84
+ });
85
+ }
86
+ function denyProbeEnv() {
87
+ return { ...process.env, [READ_ONLY_HOOK_ENV]: "1" };
88
+ }
89
+ function parseCursorDecision(stdout) {
90
+ const trimmed = stdout.trim();
91
+ if (trimmed.length === 0) {
92
+ return { ok: false, detail: "empty stdout" };
93
+ }
94
+ try {
95
+ const parsed = JSON.parse(trimmed);
96
+ if (parsed !== null &&
97
+ typeof parsed === "object" &&
98
+ !Array.isArray(parsed) &&
99
+ parsed.permission === "allow") {
100
+ return { ok: true, permission: "allow", detail: "permission allow" };
101
+ }
102
+ if (parsed !== null &&
103
+ typeof parsed === "object" &&
104
+ !Array.isArray(parsed) &&
105
+ parsed.permission === "deny") {
106
+ return { ok: true, permission: "deny", detail: "permission deny" };
107
+ }
108
+ return { ok: false, detail: "stdout JSON missing permission allow/deny" };
109
+ }
110
+ catch {
111
+ return { ok: false, detail: "stdout is not valid JSON" };
112
+ }
113
+ }
114
+ function runFixtureProbe(resolved, projectRoot, fixture, stdin, env, spawnHook) {
115
+ const args = [
116
+ ...resolved.argsPrefix,
117
+ "--host",
118
+ LIVE_PROBE_HOST,
119
+ "--event",
120
+ LIVE_PROBE_EVENT,
121
+ "--project-root",
122
+ projectRoot,
123
+ ];
124
+ const spawned = spawnHook({
125
+ command: resolved.command,
126
+ args,
127
+ stdin,
128
+ cwd: projectRoot,
129
+ env,
130
+ });
131
+ if (spawned.status !== 0) {
132
+ return {
133
+ host: LIVE_PROBE_HOST,
134
+ event: LIVE_PROBE_EVENT,
135
+ fixture,
136
+ issue: "spawn-failed",
137
+ detail: `hook command exited ${spawned.status}${spawned.stderr.trim() ? `: ${spawned.stderr.trim()}` : ""}`,
138
+ };
139
+ }
140
+ const decision = parseCursorDecision(spawned.stdout);
141
+ if (!decision.ok) {
142
+ return {
143
+ host: LIVE_PROBE_HOST,
144
+ event: LIVE_PROBE_EVENT,
145
+ fixture,
146
+ issue: decision.detail.includes("JSON") ? "unparseable-json" : "empty-stdout",
147
+ detail: decision.detail,
148
+ };
149
+ }
150
+ if (fixture === "allow" && decision.permission !== "allow") {
151
+ return {
152
+ host: LIVE_PROBE_HOST,
153
+ event: LIVE_PROBE_EVENT,
154
+ fixture,
155
+ issue: "missing-allow",
156
+ detail: `expected permission allow, got ${decision.permission ?? "none"}`,
157
+ };
158
+ }
159
+ if (fixture === "deny" && decision.permission !== "deny") {
160
+ return {
161
+ host: LIVE_PROBE_HOST,
162
+ event: LIVE_PROBE_EVENT,
163
+ fixture,
164
+ issue: "missing-deny",
165
+ detail: `expected permission deny, got ${decision.permission ?? "none"}`,
166
+ };
167
+ }
168
+ return null;
169
+ }
170
+ /** Spawn the configured hook command and assert Cursor tool.before allow/deny behavior. */
171
+ export function probeAgentHooksLive(projectRoot, seams = {}) {
172
+ const root = resolve(projectRoot);
173
+ const resolved = resolveHookCommand(seams);
174
+ if (resolved === null) {
175
+ return {
176
+ code: 2,
177
+ message: "deft agent hooks live probe unavailable: neither deft-hook nor deft/directive hook:dispatch is on PATH.",
178
+ cases: [
179
+ {
180
+ host: LIVE_PROBE_HOST,
181
+ event: LIVE_PROBE_EVENT,
182
+ fixture: "allow",
183
+ issue: "hook-command-missing",
184
+ detail: `${DEFT_HOOK_COMMAND_MARKER} not found on PATH`,
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ const spawnHook = seams.spawnHook ??
190
+ ((input) => spawnHookWithStdin(input.command, input.args, input.stdin, input.cwd, input.env ?? process.env));
191
+ const failures = [];
192
+ for (const [fixture, stdin, env] of [
193
+ ["allow", allowFixture(root), process.env],
194
+ ["deny", denyFixture(root), denyProbeEnv()],
195
+ ]) {
196
+ const failure = runFixtureProbe(resolved, root, fixture, stdin, env, spawnHook);
197
+ if (failure !== null)
198
+ failures.push(failure);
199
+ }
200
+ if (failures.length === 0) {
201
+ return {
202
+ code: 0,
203
+ message: "deft agent hooks live probe passed for Cursor tool.before allow and deny fixtures.",
204
+ cases: [],
205
+ };
206
+ }
207
+ const summary = failures
208
+ .map((entry) => `${entry.fixture}: ${entry.issue} (${entry.detail})`)
209
+ .join("; ");
210
+ return {
211
+ code: 1,
212
+ message: `deft agent hooks live probe FAILED: ${summary}. Recovery: reinstall @deftai/directive and run \`deft update\`.`,
213
+ cases: failures,
214
+ };
215
+ }
216
+ //# sourceMappingURL=agent-hooks-live-probe.js.map
@@ -1,4 +1,5 @@
1
1
  export * from "./agent-hooks.js";
2
+ export * from "./agent-hooks-live-probe.js";
2
3
  export * from "./command-spawn.js";
3
4
  export * from "./node-runtime.js";
4
5
  export * from "./toolchain-check.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./agent-hooks.js";
2
+ export * from "./agent-hooks-live-probe.js";
2
3
  export * from "./command-spawn.js";
3
4
  export * from "./node-runtime.js";
4
5
  export * from "./toolchain-check.js";
@@ -3,6 +3,7 @@ export * from "./code-structure-validate.js";
3
3
  export * from "./content-manifest.js";
4
4
  export { CANONICAL_SCHEMA_REL, type ContractDriftOptions, type ContractDriftResult, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
5
5
  export * from "./cursor-tier1.js";
6
+ export * from "./openclaw-tier1.js";
6
7
  export * from "./python-call-scan.js";
7
8
  export * from "./rule-ownership-lint.js";
8
9
  export * from "./scm-boundary.js";
@@ -3,6 +3,7 @@ export * from "./code-structure-validate.js";
3
3
  export * from "./content-manifest.js";
4
4
  export { CANONICAL_SCHEMA_REL, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
5
5
  export * from "./cursor-tier1.js";
6
+ export * from "./openclaw-tier1.js";
6
7
  export * from "./python-call-scan.js";
7
8
  export * from "./rule-ownership-lint.js";
8
9
  export * from "./scm-boundary.js";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Deterministic gate: assert that OpenClaw is enumerated as a Tier-1 descriptor
3
+ * in the swarm Phase 3 capability matrix and that routing accepts openclaw as a
4
+ * dispatch_provider (#2875). Without this gate the "OpenClaw -> Tier 1" mapping
5
+ * is prose-trusted; a doc edit that drops the sessions_spawn descriptor would
6
+ * silently re-open misclassification (OpenClaw falling through to grok-build or
7
+ * generic-terminal).
8
+ *
9
+ * Review-cycle skill markers for OpenClaw Approach 1 are owned by sibling #2876
10
+ * and are intentionally out of scope for this gate.
11
+ */
12
+ export interface OpenclawTier1Target {
13
+ /** Project-root-relative path to the surface file. */
14
+ readonly path: string;
15
+ /** Human label for the surface, used in failure output. */
16
+ readonly label: string;
17
+ /** Whitespace-normalized substrings that MUST all be present. */
18
+ readonly markers: readonly string[];
19
+ }
20
+ export declare const OPENCLAW_TIER1_TARGETS: readonly OpenclawTier1Target[];
21
+ export interface OpenclawTier1Finding {
22
+ readonly path: string;
23
+ readonly label: string;
24
+ readonly missingMarkers: readonly string[];
25
+ }
26
+ export interface OpenclawTier1Result {
27
+ readonly code: 0 | 1 | 2;
28
+ readonly findings: readonly OpenclawTier1Finding[];
29
+ readonly message: string;
30
+ readonly stream: "stdout" | "stderr";
31
+ }
32
+ export interface OpenclawTier1Options {
33
+ readonly targets?: readonly OpenclawTier1Target[];
34
+ readonly quiet?: boolean;
35
+ }
36
+ export declare function evaluateOpenclawTier1(projectRoot: string, options?: OpenclawTier1Options): OpenclawTier1Result;
37
+ //# sourceMappingURL=openclaw-tier1.d.ts.map