@deftai/directive-core 0.87.0 → 0.88.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.
- package/dist/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/check/gate-lists.js +1 -0
- package/dist/content-contracts/skills/helpers.d.ts +10 -0
- package/dist/content-contracts/skills/helpers.js +35 -0
- package/dist/deposit/copy-tree.d.ts +19 -1
- package/dist/deposit/copy-tree.js +134 -5
- package/dist/doctor/main.js +48 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/dispatcher.d.ts +15 -3
- package/dist/hooks/dispatcher.js +149 -6
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +38 -2
- package/dist/init-deposit/hygiene.d.ts +16 -0
- package/dist/init-deposit/hygiene.js +26 -0
- package/dist/init-deposit/init-dispatch.js +28 -0
- package/dist/init-deposit/prettierignore.js +2 -2
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +7 -3
- package/dist/init-deposit/xbrief-projections.js +6 -6
- package/dist/intake/issue-ingest.js +11 -2
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +155 -9
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +103 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +260 -22
- package/dist/policy/runtime-authority.d.ts +41 -0
- package/dist/policy/runtime-authority.js +274 -0
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +15 -0
- package/dist/session/session-start.js +30 -0
- package/dist/verify-source/cursor-tier1.js +7 -2
- package/dist/verify-source/openclaw-tier1.js +7 -2
- package/package.json +4 -3
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
4
|
-
import {
|
|
4
|
+
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
|
|
5
|
+
import { classifyMcpTool, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
|
|
5
6
|
import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
|
|
6
7
|
import { runSessionStartHookWrite } from "../session/session-start-hook.js";
|
|
7
8
|
import { inspectSessionRitual } from "../session/verify-session-ritual.js";
|
|
8
9
|
import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
9
10
|
import { inspectActiveScope } from "./scope.js";
|
|
10
|
-
import { isDirectWriteTool, isSpawnTool } from "./tools.js";
|
|
11
|
+
import { isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool } from "./tools.js";
|
|
11
12
|
export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
12
|
-
export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isSpawnTool, READ_ONLY_HOOK_ENV, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
|
|
13
|
+
export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool, MCP_HOOK_MATCHER, MCP_PUSH_MERGE_BARE_NAMES, READ_ONLY_HOOK_ENV, SHELL_HOOK_MATCHER, SHELL_TOOL_NAMES, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
|
|
13
14
|
export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
|
|
14
15
|
export const HOOK_EVENTS = ["session.start", "session.compact", "tool.before"];
|
|
15
16
|
/** Hosts that receive compact/resume hook deposits via init/update (#2113). */
|
|
@@ -21,7 +22,8 @@ function record(value) {
|
|
|
21
22
|
? value
|
|
22
23
|
: null;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
/** First non-empty trimmed string from candidates (array form — clear arity for static tools). */
|
|
26
|
+
function firstString(values) {
|
|
25
27
|
for (const value of values) {
|
|
26
28
|
if (typeof value === "string" && value.trim().length > 0)
|
|
27
29
|
return value.trim();
|
|
@@ -138,7 +140,14 @@ export function hookWriteTargetPath(payload) {
|
|
|
138
140
|
if (input === null)
|
|
139
141
|
return null;
|
|
140
142
|
const toolInput = toolInputRecord(input);
|
|
141
|
-
return firstString(
|
|
143
|
+
return firstString([
|
|
144
|
+
toolInput?.file_path,
|
|
145
|
+
toolInput?.filePath,
|
|
146
|
+
toolInput?.path,
|
|
147
|
+
input.file_path,
|
|
148
|
+
input.filePath,
|
|
149
|
+
input.path,
|
|
150
|
+
]);
|
|
142
151
|
}
|
|
143
152
|
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
144
153
|
export function toProjectRelativePosix(projectRoot, targetPath) {
|
|
@@ -305,6 +314,113 @@ function runtimeAuthorityForDirectWrite(input, toolName, seams, scopePath) {
|
|
|
305
314
|
return null;
|
|
306
315
|
return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under runtime authority policy.", scopePath);
|
|
307
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Best-effort shell command string from host PreToolUse payloads (#2711).
|
|
319
|
+
* Hosts disagree on nesting (`tool_input.command` vs top-level `command`).
|
|
320
|
+
*/
|
|
321
|
+
export function hookShellCommand(payload) {
|
|
322
|
+
const input = record(payload);
|
|
323
|
+
if (input === null)
|
|
324
|
+
return null;
|
|
325
|
+
const toolInput = toolInputRecord(input);
|
|
326
|
+
return firstString([
|
|
327
|
+
toolInput !== null ? toolInput.command : null,
|
|
328
|
+
toolInput !== null ? toolInput.cmd : null,
|
|
329
|
+
toolInput !== null ? toolInput.shell_command : null,
|
|
330
|
+
input.command,
|
|
331
|
+
input.cmd,
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
/** Serialize tool args for MCP classification when nested objects are present (#2711). */
|
|
335
|
+
function hookMcpArgsText(payload) {
|
|
336
|
+
const input = record(payload);
|
|
337
|
+
if (input === null)
|
|
338
|
+
return null;
|
|
339
|
+
const toolInput = toolInputRecord(input);
|
|
340
|
+
if (toolInput === null)
|
|
341
|
+
return null;
|
|
342
|
+
try {
|
|
343
|
+
return JSON.stringify(toolInput);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function loadRuntimeAuthorityPolicySafe(input, seams) {
|
|
350
|
+
const projectRoot = resolve(input.projectRoot);
|
|
351
|
+
try {
|
|
352
|
+
return (seams.loadRuntimeAuthority ?? loadRuntimeAuthorityFromProject)(projectRoot);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Evaluate scopes.push / scopes.merge for Shell/Bash or classifiable MCP tools (#2711).
|
|
360
|
+
* Returns a full HookDecision (allow or deny). Unclassifiable ops fail open.
|
|
361
|
+
*/
|
|
362
|
+
function decideShellOrMcpRuntimeAuthority(input, toolName, seams) {
|
|
363
|
+
const projectRoot = resolve(input.projectRoot);
|
|
364
|
+
const policy = loadRuntimeAuthorityPolicySafe(input, seams);
|
|
365
|
+
if (policy === null) {
|
|
366
|
+
return {
|
|
367
|
+
verdict: "allow",
|
|
368
|
+
code: "shell-op-unclassifiable",
|
|
369
|
+
event: input.event,
|
|
370
|
+
host: input.host,
|
|
371
|
+
toolName,
|
|
372
|
+
projectRoot,
|
|
373
|
+
message: `Directive allowed ${toolName}: runtimeAuthority policy load failed (fail open).`,
|
|
374
|
+
scopePath: null,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// Shell: evaluate every classifiable op in compound/multi-line commands (#2711 Greptile).
|
|
378
|
+
// MCP / bare push-merge names: single tool name maps to at most one op
|
|
379
|
+
// (includes bare `merge_pull_request` etc. that isMcpTool does not flag).
|
|
380
|
+
const ops = [];
|
|
381
|
+
if (isShellTool(toolName)) {
|
|
382
|
+
const command = hookShellCommand(input.payload);
|
|
383
|
+
if (command !== null)
|
|
384
|
+
ops.push(...listShellOps(command));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
const mcpOp = classifyMcpTool(toolName, hookMcpArgsText(input.payload));
|
|
388
|
+
if (mcpOp !== null)
|
|
389
|
+
ops.push(mcpOp);
|
|
390
|
+
}
|
|
391
|
+
if (ops.length === 0) {
|
|
392
|
+
return {
|
|
393
|
+
verdict: "allow",
|
|
394
|
+
code: "shell-op-unclassifiable",
|
|
395
|
+
event: input.event,
|
|
396
|
+
host: input.host,
|
|
397
|
+
toolName,
|
|
398
|
+
projectRoot,
|
|
399
|
+
message: `${toolName} is classifiable as Shell/MCP but the command/tool was not recognized as ` +
|
|
400
|
+
"push or merge — fail open (host gap; see runtime-authority.md).",
|
|
401
|
+
scopePath: null,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// Deny if any classifiable op is out of scope (compound commands must not short-circuit).
|
|
405
|
+
for (const op of ops) {
|
|
406
|
+
const verdict = evaluateRuntimeAuthorityShellOp({ policy, op });
|
|
407
|
+
if (!verdict.allowed) {
|
|
408
|
+
return deny(input, verdict.code ?? "runtime-policy-deny-scope", toolName, verdict.reason ??
|
|
409
|
+
"Directive denied this shell/MCP operation under runtime authority policy.");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const opsLabel = ops.join("+");
|
|
413
|
+
return {
|
|
414
|
+
verdict: "allow",
|
|
415
|
+
code: "shell-op-ready",
|
|
416
|
+
event: input.event,
|
|
417
|
+
host: input.host,
|
|
418
|
+
toolName,
|
|
419
|
+
projectRoot,
|
|
420
|
+
message: `Directive runtimeAuthority allowed classifiable ${opsLabel} via ${toolName}.`,
|
|
421
|
+
scopePath: null,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
308
424
|
function inspectMutationGates(input, toolName, seams, options) {
|
|
309
425
|
const projectRoot = resolve(input.projectRoot);
|
|
310
426
|
let ritual;
|
|
@@ -431,6 +547,24 @@ export function decideHook(input, seams = {}) {
|
|
|
431
547
|
}
|
|
432
548
|
}
|
|
433
549
|
if (input.event === "session.start") {
|
|
550
|
+
// #2926: root `.no-deft-directive` wins — skip host SessionStart bookkeeping.
|
|
551
|
+
const detectOptOut = seams.detectNoDeftDirective ?? detectNoDeftDirective;
|
|
552
|
+
const optOut = detectOptOut(projectRoot);
|
|
553
|
+
if (optOut.present) {
|
|
554
|
+
const message = optOut.inconsistent
|
|
555
|
+
? `${NO_DEFT_DIRECTIVE_DISABLED_MESSAGE}. ${NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE}`
|
|
556
|
+
: NO_DEFT_DIRECTIVE_DISABLED_MESSAGE;
|
|
557
|
+
return {
|
|
558
|
+
verdict: "allow",
|
|
559
|
+
code: "session-start-disabled",
|
|
560
|
+
event: input.event,
|
|
561
|
+
host: input.host,
|
|
562
|
+
toolName: null,
|
|
563
|
+
projectRoot,
|
|
564
|
+
message,
|
|
565
|
+
scopePath: null,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
434
568
|
try {
|
|
435
569
|
const result = (seams.sessionStart ?? runSessionStartHookWrite)(projectRoot);
|
|
436
570
|
if (result.code !== 0) {
|
|
@@ -510,6 +644,15 @@ export function decideHook(input, seams = {}) {
|
|
|
510
644
|
}
|
|
511
645
|
return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: false });
|
|
512
646
|
}
|
|
647
|
+
// Shell/Bash and classifiable MCP: enforce scopes.push / scopes.merge (#2711).
|
|
648
|
+
// Route bare push/merge MCP names (merge_pull_request, git_push, …) even when
|
|
649
|
+
// isMcpTool is false — classifyMcpTool is the gate (dispatcher-side, no tools↔policy cycle).
|
|
650
|
+
// Does not require active-scope ritual — only runtimeAuthority when enabled.
|
|
651
|
+
if (isShellTool(toolName) ||
|
|
652
|
+
isMcpTool(toolName) ||
|
|
653
|
+
classifyMcpTool(toolName, hookMcpArgsText(input.payload)) !== null) {
|
|
654
|
+
return decideShellOrMcpRuntimeAuthority(input, toolName, seams);
|
|
655
|
+
}
|
|
513
656
|
if (!isDirectWriteTool(toolName)) {
|
|
514
657
|
return {
|
|
515
658
|
verdict: "allow",
|
|
@@ -518,7 +661,7 @@ export function decideHook(input, seams = {}) {
|
|
|
518
661
|
host: input.host,
|
|
519
662
|
toolName,
|
|
520
663
|
projectRoot,
|
|
521
|
-
message: `${toolName} is outside the P0 direct-write/spawn enforcement slice.`,
|
|
664
|
+
message: `${toolName} is outside the P0 direct-write/spawn/shell enforcement slice.`,
|
|
522
665
|
scopePath: null,
|
|
523
666
|
};
|
|
524
667
|
}
|
package/dist/hooks/tools.d.ts
CHANGED
|
@@ -2,10 +2,41 @@
|
|
|
2
2
|
export declare const DIRECT_WRITE_TOOL_NAMES: readonly ["Edit", "Write", "WriteFile", "CreateFile", "MultiEdit", "NotebookEdit", "StrReplace", "SearchReplace", "Delete", "DeleteFile", "ApplyPatch", "apply_patch"];
|
|
3
3
|
/** PreToolUse spawn / sub-agent dispatch tools (#1185 / #2437). */
|
|
4
4
|
export declare const SPAWN_TOOL_NAMES: readonly ["Task", "SubagentStart", "spawn_subagent", "start_agent", "CreateAgent"];
|
|
5
|
+
/**
|
|
6
|
+
* Host spellings for Shell/Bash execution tools (#2711).
|
|
7
|
+
* Used for runtimeAuthority scopes.push / scopes.merge classification.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SHELL_TOOL_NAMES: readonly ["Shell", "Bash", "BashTool", "shell", "bash"];
|
|
5
10
|
/** Env override forcing hook-level read-only write denial (#1185). */
|
|
6
11
|
export declare const READ_ONLY_HOOK_ENV = "DEFT_HOOK_READ_ONLY";
|
|
7
12
|
export declare function isDirectWriteTool(toolName: string): boolean;
|
|
8
13
|
export declare function isSpawnTool(toolName: string): boolean;
|
|
14
|
+
/** True for host Shell/Bash-class tools that carry a command string (#2711). */
|
|
15
|
+
export declare function isShellTool(toolName: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Best-effort MCP tool detection for runtimeAuthority push/merge scopes (#2711).
|
|
18
|
+
* Host spellings vary (`mcp__*`, `server__tool`, bare MCP-looking names).
|
|
19
|
+
* Unclassifiable MCP tools fail open at the operation classifier.
|
|
20
|
+
*
|
|
21
|
+
* Bare push/merge names (e.g. `merge_pull_request`) are NOT detected here —
|
|
22
|
+
* they are classified by `classifyMcpTool` and routed from `decideHook` so
|
|
23
|
+
* tools.ts stays free of policy imports (#2711 Greptile conf holdout).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isMcpTool(toolName: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Bare tool names that hosts may emit without mcp__/server__ prefixes and that
|
|
28
|
+
* `classifyMcpTool` treats as push or merge (#2711).
|
|
29
|
+
* Keep in sync with classifyMcpTool name patterns (narrow list for PreToolUse matchers).
|
|
30
|
+
*/
|
|
31
|
+
export declare const MCP_PUSH_MERGE_BARE_NAMES: readonly ["merge_pull_request", "merge-pull-request", "pull_request_merge", "pull-request-merge", "merge_pr", "pr_merge", "pr-merge", "git_push", "git-push", "push_branch", "push-branch"];
|
|
9
32
|
export declare const DIRECT_WRITE_HOOK_MATCHER: string;
|
|
10
33
|
export declare const SPAWN_HOOK_MATCHER: string;
|
|
34
|
+
export declare const SHELL_HOOK_MATCHER: string;
|
|
35
|
+
/**
|
|
36
|
+
* PreToolUse matcher for MCP-class push/merge tools (#2711).
|
|
37
|
+
* Regex-friendly for Claude/nested hosts. Prefer broad install match +
|
|
38
|
+
* fail-open classify in the dispatcher over missing a classifiable push/merge
|
|
39
|
+
* tool name (e.g. `server__push_to_remote` via push+remote).
|
|
40
|
+
*/
|
|
41
|
+
export declare const MCP_HOOK_MATCHER: string;
|
|
11
42
|
//# sourceMappingURL=tools.d.ts.map
|
package/dist/hooks/tools.js
CHANGED
|
@@ -21,6 +21,11 @@ export const SPAWN_TOOL_NAMES = [
|
|
|
21
21
|
"start_agent",
|
|
22
22
|
"CreateAgent",
|
|
23
23
|
];
|
|
24
|
+
/**
|
|
25
|
+
* Host spellings for Shell/Bash execution tools (#2711).
|
|
26
|
+
* Used for runtimeAuthority scopes.push / scopes.merge classification.
|
|
27
|
+
*/
|
|
28
|
+
export const SHELL_TOOL_NAMES = ["Shell", "Bash", "BashTool", "shell", "bash"];
|
|
24
29
|
/** Env override forcing hook-level read-only write denial (#1185). */
|
|
25
30
|
export const READ_ONLY_HOOK_ENV = "DEFT_HOOK_READ_ONLY";
|
|
26
31
|
function normalizedToolName(toolName) {
|
|
@@ -28,12 +33,81 @@ function normalizedToolName(toolName) {
|
|
|
28
33
|
}
|
|
29
34
|
const DIRECT_WRITE_TOOLS = new Set(DIRECT_WRITE_TOOL_NAMES.map(normalizedToolName));
|
|
30
35
|
const SPAWN_TOOLS = new Set(SPAWN_TOOL_NAMES.map(normalizedToolName));
|
|
36
|
+
const SHELL_TOOLS = new Set(SHELL_TOOL_NAMES.map(normalizedToolName));
|
|
31
37
|
export function isDirectWriteTool(toolName) {
|
|
32
38
|
return DIRECT_WRITE_TOOLS.has(normalizedToolName(toolName));
|
|
33
39
|
}
|
|
34
40
|
export function isSpawnTool(toolName) {
|
|
35
41
|
return SPAWN_TOOLS.has(normalizedToolName(toolName));
|
|
36
42
|
}
|
|
43
|
+
/** True for host Shell/Bash-class tools that carry a command string (#2711). */
|
|
44
|
+
export function isShellTool(toolName) {
|
|
45
|
+
return SHELL_TOOLS.has(normalizedToolName(toolName));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Best-effort MCP tool detection for runtimeAuthority push/merge scopes (#2711).
|
|
49
|
+
* Host spellings vary (`mcp__*`, `server__tool`, bare MCP-looking names).
|
|
50
|
+
* Unclassifiable MCP tools fail open at the operation classifier.
|
|
51
|
+
*
|
|
52
|
+
* Bare push/merge names (e.g. `merge_pull_request`) are NOT detected here —
|
|
53
|
+
* they are classified by `classifyMcpTool` and routed from `decideHook` so
|
|
54
|
+
* tools.ts stays free of policy imports (#2711 Greptile conf holdout).
|
|
55
|
+
*/
|
|
56
|
+
export function isMcpTool(toolName) {
|
|
57
|
+
const raw = toolName.trim();
|
|
58
|
+
if (raw.length === 0)
|
|
59
|
+
return false;
|
|
60
|
+
const lower = raw.toLowerCase();
|
|
61
|
+
if (lower.startsWith("mcp__") || lower.startsWith("mcp_"))
|
|
62
|
+
return true;
|
|
63
|
+
if (lower.includes("__") && !isDirectWriteTool(toolName) && !isShellTool(toolName)) {
|
|
64
|
+
// server__tool style used by some MCP bridges
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Bare tool names that hosts may emit without mcp__/server__ prefixes and that
|
|
71
|
+
* `classifyMcpTool` treats as push or merge (#2711).
|
|
72
|
+
* Keep in sync with classifyMcpTool name patterns (narrow list for PreToolUse matchers).
|
|
73
|
+
*/
|
|
74
|
+
export const MCP_PUSH_MERGE_BARE_NAMES = [
|
|
75
|
+
"merge_pull_request",
|
|
76
|
+
"merge-pull-request",
|
|
77
|
+
"pull_request_merge",
|
|
78
|
+
"pull-request-merge",
|
|
79
|
+
"merge_pr",
|
|
80
|
+
"pr_merge",
|
|
81
|
+
"pr-merge",
|
|
82
|
+
"git_push",
|
|
83
|
+
"git-push",
|
|
84
|
+
"push_branch",
|
|
85
|
+
"push-branch",
|
|
86
|
+
];
|
|
37
87
|
export const DIRECT_WRITE_HOOK_MATCHER = DIRECT_WRITE_TOOL_NAMES.join("|");
|
|
38
88
|
export const SPAWN_HOOK_MATCHER = SPAWN_TOOL_NAMES.join("|");
|
|
89
|
+
export const SHELL_HOOK_MATCHER = SHELL_TOOL_NAMES.join("|");
|
|
90
|
+
/**
|
|
91
|
+
* PreToolUse matcher for MCP-class push/merge tools (#2711).
|
|
92
|
+
* Regex-friendly for Claude/nested hosts. Prefer broad install match +
|
|
93
|
+
* fail-open classify in the dispatcher over missing a classifiable push/merge
|
|
94
|
+
* tool name (e.g. `server__push_to_remote` via push+remote).
|
|
95
|
+
*/
|
|
96
|
+
export const MCP_HOOK_MATCHER = [
|
|
97
|
+
// Prefixed / server-bridge styles (isMcpTool); dispatcher fail-opens non-ops.
|
|
98
|
+
"mcp__.*",
|
|
99
|
+
"mcp_.*",
|
|
100
|
+
".*__.*",
|
|
101
|
+
// Bare names classifyMcpTool recognizes without prefixes.
|
|
102
|
+
...MCP_PUSH_MERGE_BARE_NAMES,
|
|
103
|
+
// Name fragments for hosts that strip server prefixes differently.
|
|
104
|
+
".*merge[_-]?pull[_-]?request.*",
|
|
105
|
+
".*pull[_-]?request[_-]?merge.*",
|
|
106
|
+
".*(?:^|[_-])merge_pr(?:$|[_-]).*",
|
|
107
|
+
".*pr[_-]?merge.*",
|
|
108
|
+
".*git[_-]?push.*",
|
|
109
|
+
".*push[_-]?branch.*",
|
|
110
|
+
".*push.*(?:git|branch|remote|ref).*",
|
|
111
|
+
".*(?:git|branch|remote|ref).*push.*",
|
|
112
|
+
].join("|");
|
|
39
113
|
//# sourceMappingURL=tools.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { HookHost } from "../hooks/dispatcher.js";
|
|
2
2
|
import { type HostHooksPolicy } from "../policy/host-hooks.js";
|
|
3
3
|
import type { InitDepositIo } from "./constants.js";
|
|
4
|
-
export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
|
|
4
|
+
export { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
|
|
5
5
|
export declare const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
|
|
6
6
|
export declare const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
|
|
7
7
|
export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { assertDepositContained } from "../deposit/contain.js";
|
|
4
|
-
import { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
|
|
4
|
+
import { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
|
|
5
5
|
import { isHostHookDepositEnabled, loadHostHooksPolicyFromProject, } from "../policy/host-hooks.js";
|
|
6
|
-
export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
|
|
6
|
+
export { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
|
|
7
7
|
export const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
|
|
8
8
|
export const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
|
|
9
9
|
export const AGENT_HOOK_PATHS = [
|
|
@@ -103,6 +103,10 @@ function mergeNestedConfig(config, path, host, options = {}) {
|
|
|
103
103
|
...preTool,
|
|
104
104
|
nestedGroup(host, "tool.before", DIRECT_WRITE_HOOK_MATCHER),
|
|
105
105
|
nestedGroup(host, "tool.before", SPAWN_HOOK_MATCHER),
|
|
106
|
+
// Shell/Bash for runtimeAuthority scopes.push / scopes.merge (#2711)
|
|
107
|
+
nestedGroup(host, "tool.before", SHELL_HOOK_MATCHER),
|
|
108
|
+
// MCP push/merge (mcp__*, bare merge_pull_request / git_push, …) (#2711)
|
|
109
|
+
nestedGroup(host, "tool.before", MCP_HOOK_MATCHER),
|
|
106
110
|
];
|
|
107
111
|
if (options.compact) {
|
|
108
112
|
const preCompact = eventArray(hooks, "PreCompact", path).filter((entry) => !isManagedNestedGroup(entry));
|
|
@@ -164,6 +168,18 @@ function mergeCursorConfig(config, path) {
|
|
|
164
168
|
failClosed: true,
|
|
165
169
|
timeout: 5,
|
|
166
170
|
},
|
|
171
|
+
{
|
|
172
|
+
command: command("cursor", "tool.before"),
|
|
173
|
+
matcher: SHELL_HOOK_MATCHER,
|
|
174
|
+
failClosed: true,
|
|
175
|
+
timeout: 5,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
command: command("cursor", "tool.before"),
|
|
179
|
+
matcher: MCP_HOOK_MATCHER,
|
|
180
|
+
failClosed: true,
|
|
181
|
+
timeout: 5,
|
|
182
|
+
},
|
|
167
183
|
];
|
|
168
184
|
hooks.preCompact = [...preCompact, { command: command("cursor", "session.compact"), timeout: 5 }];
|
|
169
185
|
return { ...config, version: 1, hooks };
|
|
@@ -285,6 +301,14 @@ function hasNestedRegistration(config, host, options = {}) {
|
|
|
285
301
|
preTool.some((entry) => {
|
|
286
302
|
const group = object(entry);
|
|
287
303
|
return group?.matcher === SPAWN_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand);
|
|
304
|
+
}) &&
|
|
305
|
+
preTool.some((entry) => {
|
|
306
|
+
const group = object(entry);
|
|
307
|
+
return group?.matcher === SHELL_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand);
|
|
308
|
+
}) &&
|
|
309
|
+
preTool.some((entry) => {
|
|
310
|
+
const group = object(entry);
|
|
311
|
+
return group?.matcher === MCP_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand);
|
|
288
312
|
});
|
|
289
313
|
if (!base)
|
|
290
314
|
return false;
|
|
@@ -315,6 +339,18 @@ function hasCursorRegistration(config) {
|
|
|
315
339
|
hook.matcher === SPAWN_HOOK_MATCHER &&
|
|
316
340
|
hook.failClosed === true);
|
|
317
341
|
}) &&
|
|
342
|
+
preTool.some((entry) => {
|
|
343
|
+
const hook = object(entry);
|
|
344
|
+
return (hook?.command === command("cursor", "tool.before") &&
|
|
345
|
+
hook.matcher === SHELL_HOOK_MATCHER &&
|
|
346
|
+
hook.failClosed === true);
|
|
347
|
+
}) &&
|
|
348
|
+
preTool.some((entry) => {
|
|
349
|
+
const hook = object(entry);
|
|
350
|
+
return (hook?.command === command("cursor", "tool.before") &&
|
|
351
|
+
hook.matcher === MCP_HOOK_MATCHER &&
|
|
352
|
+
hook.failClosed === true);
|
|
353
|
+
}) &&
|
|
318
354
|
preCompact.some((entry) => object(entry)?.command === command("cursor", "session.compact")));
|
|
319
355
|
}
|
|
320
356
|
/** Read-only registration probe shared by verify and doctor. */
|
|
@@ -77,8 +77,24 @@ export interface PrunePackageAbsentDepositPathsResult {
|
|
|
77
77
|
* Remove deposit files not shipped by `@deftai/directive-content`, preserving
|
|
78
78
|
* generated deposit metadata such as `VERSION` (#2804). Subsumes the legacy
|
|
79
79
|
* hard-coded `packages/` prune (#2347).
|
|
80
|
+
*
|
|
81
|
+
* Individual removal failures are reported but do not throw — callers that must
|
|
82
|
+
* refuse a VERSION stamp until the deposit matches the content package should
|
|
83
|
+
* use {@link reconcileDepositToContentPackage} (#2913).
|
|
80
84
|
*/
|
|
81
85
|
export declare function prunePackageAbsentDepositPaths(deftDir: string, contentRoot: string, io: InitDepositIo): Promise<PrunePackageAbsentDepositPathsResult>;
|
|
86
|
+
/**
|
|
87
|
+
* Fail-closed deposit reconcile against the installed content package (#2913).
|
|
88
|
+
*
|
|
89
|
+
* Runs {@link prunePackageAbsentDepositPaths}, then re-scans. If any
|
|
90
|
+
* package-absent path remains, throws so callers refuse the VERSION stamp
|
|
91
|
+
* (dst-only stale/malicious agent content must not survive a refresh).
|
|
92
|
+
*
|
|
93
|
+
* After a successful {@link replaceTree} full-swap this is typically a no-op
|
|
94
|
+
* verification; it also covers additive copy seams and the already-current
|
|
95
|
+
* refresh path that skips payload copy.
|
|
96
|
+
*/
|
|
97
|
+
export declare function reconcileDepositToContentPackage(deftDir: string, contentRoot: string, io: InitDepositIo): Promise<PrunePackageAbsentDepositPathsResult>;
|
|
82
98
|
export interface PruneStrayDepositPathsResult {
|
|
83
99
|
readonly pruned: string[];
|
|
84
100
|
}
|
|
@@ -257,6 +257,10 @@ async function pruneEmptyParentsForFile(deftDir, contentDirs, relFile) {
|
|
|
257
257
|
* Remove deposit files not shipped by `@deftai/directive-content`, preserving
|
|
258
258
|
* generated deposit metadata such as `VERSION` (#2804). Subsumes the legacy
|
|
259
259
|
* hard-coded `packages/` prune (#2347).
|
|
260
|
+
*
|
|
261
|
+
* Individual removal failures are reported but do not throw — callers that must
|
|
262
|
+
* refuse a VERSION stamp until the deposit matches the content package should
|
|
263
|
+
* use {@link reconcileDepositToContentPackage} (#2913).
|
|
260
264
|
*/
|
|
261
265
|
export async function prunePackageAbsentDepositPaths(deftDir, contentRoot, io) {
|
|
262
266
|
const absent = await findPackageAbsentDepositPaths(deftDir, contentRoot);
|
|
@@ -278,6 +282,28 @@ export async function prunePackageAbsentDepositPaths(deftDir, contentRoot, io) {
|
|
|
278
282
|
}
|
|
279
283
|
return { pruned, prunedDirs };
|
|
280
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Fail-closed deposit reconcile against the installed content package (#2913).
|
|
287
|
+
*
|
|
288
|
+
* Runs {@link prunePackageAbsentDepositPaths}, then re-scans. If any
|
|
289
|
+
* package-absent path remains, throws so callers refuse the VERSION stamp
|
|
290
|
+
* (dst-only stale/malicious agent content must not survive a refresh).
|
|
291
|
+
*
|
|
292
|
+
* After a successful {@link replaceTree} full-swap this is typically a no-op
|
|
293
|
+
* verification; it also covers additive copy seams and the already-current
|
|
294
|
+
* refresh path that skips payload copy.
|
|
295
|
+
*/
|
|
296
|
+
export async function reconcileDepositToContentPackage(deftDir, contentRoot, io) {
|
|
297
|
+
const result = await prunePackageAbsentDepositPaths(deftDir, contentRoot, io);
|
|
298
|
+
const remaining = await findPackageAbsentDepositPaths(deftDir, contentRoot);
|
|
299
|
+
if (remaining.length > 0) {
|
|
300
|
+
const sample = remaining.slice(0, 5).join(", ");
|
|
301
|
+
const more = remaining.length > 5 ? ` (+${remaining.length - 5} more)` : "";
|
|
302
|
+
throw new Error(`deposit reconcile failed: ${remaining.length} package-absent path(s) remain under .deft/core ` +
|
|
303
|
+
`(e.g. ${sample}${more}). Refusing VERSION stamp until dst-only content is removed (#2913).`);
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
281
307
|
/**
|
|
282
308
|
* Remove framework-source subdirectories from `.deft/core/` that are not
|
|
283
309
|
* present in the deposited content package (#2347). Silently skips any path
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* dispatcher never re-implements deposit, refresh, or migrate behavior.
|
|
22
22
|
*/
|
|
23
23
|
import { resolve } from "node:path";
|
|
24
|
+
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
|
|
24
25
|
import { classify, plan } from "../resolution/index.js";
|
|
25
26
|
import { runInitDepositCli } from "./init-deposit.js";
|
|
26
27
|
import { runMigrateCli } from "./migrate.js";
|
|
@@ -119,6 +120,33 @@ export async function runInitDispatchCli(options) {
|
|
|
119
120
|
options.writeOut(text);
|
|
120
121
|
},
|
|
121
122
|
};
|
|
123
|
+
// #2926: root opt-out — do not scaffold/install/refresh when flag is present.
|
|
124
|
+
const optOut = detectNoDeftDirective(projectDir);
|
|
125
|
+
if (optOut.present) {
|
|
126
|
+
io.printf(`\n[directive init] ${NO_DEFT_DIRECTIVE_DISABLED_MESSAGE}\n`);
|
|
127
|
+
if (optOut.inconsistent) {
|
|
128
|
+
io.printf(`[directive init] ${NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE}\n`);
|
|
129
|
+
}
|
|
130
|
+
if (options.jsonOut) {
|
|
131
|
+
options.writeOut(`${JSON.stringify({
|
|
132
|
+
success: false,
|
|
133
|
+
action: "init",
|
|
134
|
+
disabled: true,
|
|
135
|
+
disabled_via: NO_DEFT_DIRECTIVE_FLAG_NAME,
|
|
136
|
+
inconsistent: optOut.inconsistent,
|
|
137
|
+
...(optOut.inconsistent
|
|
138
|
+
? { inconsistent_policy: NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY }
|
|
139
|
+
: {}),
|
|
140
|
+
deposit_present: optOut.depositPresent,
|
|
141
|
+
project_dir: projectDir,
|
|
142
|
+
message: optOut.inconsistent
|
|
143
|
+
? NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE
|
|
144
|
+
: NO_DEFT_DIRECTIVE_DISABLED_MESSAGE,
|
|
145
|
+
}, null, 2)}\n`);
|
|
146
|
+
}
|
|
147
|
+
// Clean opt-out is a successful short-circuit; inconsistent state fails closed.
|
|
148
|
+
return optOut.inconsistent ? 1 : 0;
|
|
149
|
+
}
|
|
122
150
|
const classification = classifyInitDispatch(projectDir, seams.classifySeams ?? {});
|
|
123
151
|
const { decision, plan: resolutionPlan } = classification;
|
|
124
152
|
printInitDispatchSummary(io, decision, resolutionPlan);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { assertDestinationNotSymlink } from "../fs/projection-containment.js";
|
|
13
13
|
import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
|
|
14
14
|
/** Directory ignore entry for the hybrid deposit. */
|
|
15
15
|
export const PRETTIERIGNORE_DEFT_CORE_LINE = ".deft/core/";
|
|
@@ -18,7 +18,7 @@ const DEFT_CORE_COVERING_LINES = new Set([".deft/core/", ".deft/core", ".deft/co
|
|
|
18
18
|
const DEFT_FRAMEWORK_PRETTIERIGNORE_HEADER = "# Deft framework: the vendored payload is outside the consumer Prettier gate (#2534).\n";
|
|
19
19
|
function projectionTarget(projectDir) {
|
|
20
20
|
const target = join(projectDir, ".prettierignore");
|
|
21
|
-
|
|
21
|
+
assertDestinationNotSymlink(projectDir, target);
|
|
22
22
|
return target;
|
|
23
23
|
}
|
|
24
24
|
function prettierIgnoreCoversLine(present, line) {
|
|
@@ -12,19 +12,20 @@ import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
|
12
12
|
import { platform as osPlatform } from "node:os";
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
14
|
import { assertDepositContained } from "../deposit/contain.js";
|
|
15
|
-
import {
|
|
15
|
+
import { replaceTree } from "../deposit/copy-tree.js";
|
|
16
16
|
import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
|
|
17
17
|
import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
|
|
18
18
|
import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
|
|
19
19
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
20
20
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
21
|
+
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
|
|
21
22
|
import { runOrgForceOnMigration } from "../policy/org-force-on-migration.js";
|
|
22
23
|
import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
|
|
23
24
|
import { gitPorcelain } from "../story-ready/git.js";
|
|
24
25
|
import { removeStaleMigratedFrameworkNarrative } from "../xbrief-migrate/migrate-project.js";
|
|
25
26
|
import { writeAgentHookDeposit } from "./agent-hooks.js";
|
|
26
27
|
import { ensureInitGitignoreLines, isDepositTrackedInGit } from "./gitignore.js";
|
|
27
|
-
import { depositStagePaths, isInstallerManagedPath, printCommitGuidance,
|
|
28
|
+
import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, reconcileDepositToContentPackage, } from "./hygiene.js";
|
|
28
29
|
import { parseInitArgv } from "./init-deposit.js";
|
|
29
30
|
import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
|
|
30
31
|
import { printMigrateNudgeIfNeeded } from "./migrate.js";
|
|
@@ -392,7 +393,9 @@ export async function runRefreshDeposit(args, io, seams = {}) {
|
|
|
392
393
|
// the resolved project tree. Writes nothing on refusal.
|
|
393
394
|
assertDepositContained(projectDir, deftDir);
|
|
394
395
|
const resolveContent = seams.resolveContentRoot ?? resolveInstalledContentRoot;
|
|
395
|
-
|
|
396
|
+
// #2913: default is full-tree replace (Go swapInCore parity), not additive copyTree.
|
|
397
|
+
// Injected seams.copyContent still wins (tests / specialized callers).
|
|
398
|
+
const copyContent = seams.copyContent ?? replaceTree;
|
|
396
399
|
const readEngine = seams.readEngineVersion ?? readCorePackageVersion;
|
|
397
400
|
const readPackageVersion = seams.readPackageVersion ?? readCorePackageVersion;
|
|
398
401
|
const contentRoot = await resolveContent();
|
|
@@ -406,15 +409,19 @@ export async function runRefreshDeposit(args, io, seams = {}) {
|
|
|
406
409
|
const strategy = alreadyCurrent ? "no-op" : "file-swap";
|
|
407
410
|
if (alreadyCurrent) {
|
|
408
411
|
io.printf("[deft update] Framework payload already current; skipping payload copy.\n");
|
|
412
|
+
// #2913: still fail-closed reconcile so dst-only leftovers cannot linger when
|
|
413
|
+
// VERSION already matches (e.g. pre-#2804 additive deposits). Does not re-stamp.
|
|
414
|
+
await reconcileDepositToContentPackage(deftDir, contentRoot, io);
|
|
409
415
|
migrateLegacyInstallManifest(projectDir, join(deftDir, "VERSION"));
|
|
410
416
|
}
|
|
411
417
|
else {
|
|
418
|
+
// Full-tree replace (or injected seam). Additive copy is no longer the default.
|
|
412
419
|
await copyContent(contentRoot, deftDir);
|
|
413
420
|
await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
|
|
414
|
-
// #2804 / #2347:
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
await
|
|
421
|
+
// #2913 / #2804 / #2347: fail-closed delete-not-in-source BEFORE VERSION stamp.
|
|
422
|
+
// replaceTree already drops dst-only paths; reconcile verifies and covers
|
|
423
|
+
// additive seams. Throws => no VERSION rewrite (refuse stamp until clean).
|
|
424
|
+
await reconcileDepositToContentPackage(deftDir, contentRoot, io);
|
|
418
425
|
const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
|
|
419
426
|
const manifestFields = {
|
|
420
427
|
ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
|
|
@@ -586,6 +593,30 @@ export async function runRefreshDepositCli(options) {
|
|
|
586
593
|
// Legacy layouts keep their existing refusal path (thrown by runRefreshDeposit
|
|
587
594
|
// and handled below), so classification is skipped for them.
|
|
588
595
|
const projectDir = resolve(options.projectDir);
|
|
596
|
+
// #2926: root opt-out wins over update/refresh and ambient force-on.
|
|
597
|
+
const optOut = detectNoDeftDirective(projectDir);
|
|
598
|
+
if (optOut.present) {
|
|
599
|
+
const message = optOut.inconsistent
|
|
600
|
+
? NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE
|
|
601
|
+
: NO_DEFT_DIRECTIVE_DISABLED_MESSAGE;
|
|
602
|
+
io.printf(`${message}\n`);
|
|
603
|
+
if (options.jsonOut) {
|
|
604
|
+
options.writeOut(`${JSON.stringify({
|
|
605
|
+
success: false,
|
|
606
|
+
action: "update",
|
|
607
|
+
disabled: true,
|
|
608
|
+
disabled_via: NO_DEFT_DIRECTIVE_FLAG_NAME,
|
|
609
|
+
inconsistent: optOut.inconsistent,
|
|
610
|
+
...(optOut.inconsistent
|
|
611
|
+
? { inconsistent_policy: NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY }
|
|
612
|
+
: {}),
|
|
613
|
+
deposit_present: optOut.depositPresent,
|
|
614
|
+
project_dir: projectDir,
|
|
615
|
+
message,
|
|
616
|
+
}, null, 2)}\n`);
|
|
617
|
+
}
|
|
618
|
+
return UPDATE_REFUSED_EXIT_CODE;
|
|
619
|
+
}
|
|
589
620
|
const detectLegacy = options.seams?.detectLegacy ?? detectLegacyLayout;
|
|
590
621
|
let classification = null;
|
|
591
622
|
if (!detectLegacy(projectDir).legacy) {
|