@deftai/directive-core 0.87.0 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authz/actions.d.ts +55 -0
- package/dist/authz/actions.js +125 -0
- package/dist/authz/classify.d.ts +23 -0
- package/dist/authz/classify.js +217 -0
- package/dist/authz/closed-verb.d.ts +42 -0
- package/dist/authz/closed-verb.js +279 -0
- package/dist/authz/evaluate.d.ts +41 -0
- package/dist/authz/evaluate.js +298 -0
- package/dist/authz/index.d.ts +15 -0
- package/dist/authz/index.js +15 -0
- package/dist/authz/origin.d.ts +28 -0
- package/dist/authz/origin.js +64 -0
- package/dist/authz/paths.d.ts +6 -0
- package/dist/authz/paths.js +19 -0
- package/dist/authz/store.d.ts +40 -0
- package/dist/authz/store.js +317 -0
- package/dist/authz/templates.d.ts +139 -0
- package/dist/authz/templates.js +241 -0
- package/dist/authz/types.d.ts +111 -0
- package/dist/authz/types.js +41 -0
- package/dist/authz/verb-classification.d.ts +44 -0
- package/dist/authz/verb-classification.js +237 -0
- package/dist/branch/evaluate.js +6 -1
- package/dist/cache/fetch.js +10 -5
- package/dist/cache/io.d.ts +9 -2
- package/dist/cache/io.js +30 -10
- package/dist/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/cache/task-cache/store.js +11 -7
- package/dist/capacity/backfill.js +10 -4
- package/dist/category-b-namespace/index.js +10 -4
- package/dist/check/gate-lists.js +1 -0
- package/dist/codebase/default-extractor.js +3 -0
- package/dist/codebase/map.js +11 -4
- 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/doctor-state.js +28 -4
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +208 -0
- package/dist/doctor/types.d.ts +11 -0
- package/dist/escalation/actions.d.ts +63 -0
- package/dist/escalation/actions.js +137 -0
- package/dist/escalation/index.d.ts +8 -0
- package/dist/escalation/index.js +8 -0
- package/dist/escalation/paths.d.ts +3 -0
- package/dist/escalation/paths.js +10 -0
- package/dist/escalation/store.d.ts +17 -0
- package/dist/escalation/store.js +172 -0
- package/dist/escalation/types.d.ts +73 -0
- package/dist/escalation/types.js +43 -0
- package/dist/eval/crud-telemetry.js +30 -10
- package/dist/eval/health.js +22 -7
- package/dist/eval/readback.js +11 -10
- package/dist/eval/run.js +32 -16
- package/dist/events/attribution-enrichment.js +10 -4
- package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
- package/dist/finish-loop/directive-finish-loop.js +239 -0
- package/dist/finish-loop/grant-gate.d.ts +31 -0
- package/dist/finish-loop/grant-gate.js +169 -0
- package/dist/finish-loop/index.d.ts +11 -0
- package/dist/finish-loop/index.js +11 -0
- package/dist/finish-loop/main.d.ts +35 -0
- package/dist/finish-loop/main.js +365 -0
- package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
- package/dist/finish-loop/pr-finish-loop.js +217 -0
- package/dist/finish-loop/progress.d.ts +17 -0
- package/dist/finish-loop/progress.js +45 -0
- package/dist/finish-loop/queue.d.ts +16 -0
- package/dist/finish-loop/queue.js +65 -0
- package/dist/finish-loop/types.d.ts +52 -0
- package/dist/finish-loop/types.js +10 -0
- package/dist/fs/contained-write.d.ts +101 -0
- package/dist/fs/contained-write.js +281 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/classify/classify.d.ts +10 -0
- package/dist/hooks/classify/classify.js +37 -0
- package/dist/hooks/classify/index.d.ts +14 -0
- package/dist/hooks/classify/index.js +13 -0
- package/dist/hooks/classify/paths.d.ts +22 -0
- package/dist/hooks/classify/paths.js +72 -0
- package/dist/hooks/classify/payload.d.ts +16 -0
- package/dist/hooks/classify/payload.js +45 -0
- package/dist/hooks/classify/stdin.d.ts +12 -0
- package/dist/hooks/classify/stdin.js +63 -0
- package/dist/hooks/classify/tool-name.d.ts +18 -0
- package/dist/hooks/classify/tool-name.js +85 -0
- package/dist/hooks/classify/types.d.ts +41 -0
- package/dist/hooks/classify/types.js +7 -0
- package/dist/hooks/dispatcher.d.ts +30 -15
- package/dist/hooks/dispatcher.js +326 -134
- package/dist/hooks/fixtures/cases.d.ts +44 -0
- package/dist/hooks/fixtures/cases.js +595 -0
- package/dist/hooks/fixtures/index.d.ts +2 -0
- package/dist/hooks/fixtures/index.js +2 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +65 -10
- package/dist/init-deposit/gitignore.js +11 -3
- package/dist/init-deposit/headless-manifest.js +12 -4
- 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/migrate.d.ts +1 -1
- package/dist/init-deposit/migrate.js +13 -3
- package/dist/init-deposit/prettierignore.js +12 -5
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +42 -29
- package/dist/init-deposit/xbrief-projections.js +17 -10
- package/dist/intake/candidates-log.js +46 -35
- package/dist/intake/github-body-cli.d.ts +6 -0
- package/dist/intake/github-body-cli.js +17 -0
- package/dist/intake/github-body.d.ts +46 -3
- package/dist/intake/github-body.js +191 -20
- package/dist/intake/issue-emit.d.ts +1 -0
- package/dist/intake/issue-emit.js +41 -23
- package/dist/intake/issue-ingest.js +11 -2
- package/dist/lifecycle/event-detect.js +12 -4
- package/dist/lifecycle/events.js +29 -20
- package/dist/lifecycle/lifecycle-hygiene.js +9 -3
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +172 -13
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/plan-sequence/store.js +11 -4
- package/dist/policy/hotfix-criteria.d.ts +79 -0
- package/dist/policy/hotfix-criteria.js +197 -0
- package/dist/policy/index.d.ts +5 -0
- package/dist/policy/index.js +32 -1
- package/dist/policy/intent-ceiling.d.ts +79 -0
- package/dist/policy/intent-ceiling.js +181 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +110 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +269 -27
- package/dist/policy/require-human-merge.d.ts +75 -0
- package/dist/policy/require-human-merge.js +324 -0
- package/dist/policy/resolve.js +17 -6
- package/dist/policy/runtime-authority.d.ts +56 -2
- package/dist/policy/runtime-authority.js +297 -3
- package/dist/policy/write-fence.d.ts +78 -0
- package/dist/policy/write-fence.js +164 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -0
- package/dist/preflight/evaluate.js +10 -0
- package/dist/product-signal/consent.js +21 -5
- package/dist/product-signal/submit.js +22 -12
- package/dist/release/build-dist-runner.js +5 -2
- package/dist/release/build-dist.d.ts +19 -1
- package/dist/release/build-dist.js +50 -2
- package/dist/release/native-steps.js +7 -2
- package/dist/release-publish/pipeline.d.ts +13 -0
- package/dist/release-publish/pipeline.js +46 -1
- package/dist/scope/audit-log.js +19 -24
- package/dist/scope/decompose.js +10 -5
- package/dist/scope/decomposed-refs.js +18 -3
- package/dist/scope/demote.js +9 -2
- package/dist/scope/undo.js +9 -2
- package/dist/session/release-availability.js +26 -6
- package/dist/session/ritual-sentinel.js +19 -12
- 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 +37 -0
- package/dist/staleness-tickler/state.js +26 -6
- package/dist/swarm/launch.js +13 -3
- package/dist/swarm/routing.js +9 -3
- package/dist/task-surface/index.js +24 -9
- package/dist/triage/bootstrap/gitignore.js +53 -10
- package/dist/triage/cache-path.js +10 -3
- package/dist/triage/welcome/summary.js +11 -4
- package/dist/triage/welcome/writers.js +45 -16
- package/dist/validate-content/validate-links.js +5 -0
- package/dist/value/readback.js +11 -6
- package/dist/vbrief-activate/activate.js +12 -4
- package/dist/vbrief-build/project-definition-io.js +14 -6
- package/dist/verify-source/contained-writes.d.ts +59 -0
- package/dist/verify-source/contained-writes.js +272 -0
- package/dist/verify-source/cursor-tier1.js +7 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.js +19 -1
- package/dist/verify-source/verify-stubs.js +3 -0
- package/dist/xbrief-migrate/migrate-project.js +26 -12
- package/dist/xbrief-migrate/transforms.d.ts +4 -2
- package/dist/xbrief-migrate/transforms.js +37 -14
- package/package.json +16 -3
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
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { EngineInfo } from "@deftai/directive-types";
|
|
|
9
9
|
* namespaces here (and as `@deftai/directive-core/<gate>` subpaths in package.json) to
|
|
10
10
|
* avoid colliding on those shared symbol names.
|
|
11
11
|
*/
|
|
12
|
+
export * as authz from "./authz/index.js";
|
|
12
13
|
export * as branch from "./branch/index.js";
|
|
13
14
|
export * as cache from "./cache/index.js";
|
|
14
15
|
export * as capacity from "./capacity/index.js";
|
|
@@ -16,12 +17,15 @@ export * as codebase from "./codebase/index.js";
|
|
|
16
17
|
export * from "./coverage-hotspots/index.js";
|
|
17
18
|
export * as doctor from "./doctor/index.js";
|
|
18
19
|
export * from "./encoding/index.js";
|
|
20
|
+
export * as escalation from "./escalation/index.js";
|
|
19
21
|
export * as evalCrud from "./eval/crud-telemetry.js";
|
|
20
22
|
export * as evalHealth from "./eval/health.js";
|
|
21
23
|
export * as evalReport from "./eval/report.js";
|
|
22
24
|
export * as evalRun from "./eval/run.js";
|
|
23
25
|
export * as events from "./events/attribution-ledger.js";
|
|
24
26
|
export * from "./forward-coverage/evaluate.js";
|
|
27
|
+
export { ContainedWriteError, ContainedWriteErrorCode, type ContainedWriteInput, type ContainedWriteMode, type ContainedWriteResult, containedWrite, resolveContainedTarget, } from "./fs/contained-write.js";
|
|
28
|
+
export { assertDestinationNotSymlink, assertDirectoryNotSymlink, assertProjectionContained, assertWriteTargetSafe, PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE, ProjectionContainmentError, walkDirectoryRejectSymlinks, } from "./fs/projection-containment.js";
|
|
25
29
|
export * as intake from "./intake/index.js";
|
|
26
30
|
export * as layout from "./layout/index.js";
|
|
27
31
|
export * as legacyBridge from "./legacy-bridge/index.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readCorePackageVersion } from "./engine-version.js";
|
|
|
9
9
|
* namespaces here (and as `@deftai/directive-core/<gate>` subpaths in package.json) to
|
|
10
10
|
* avoid colliding on those shared symbol names.
|
|
11
11
|
*/
|
|
12
|
+
export * as authz from "./authz/index.js";
|
|
12
13
|
export * as branch from "./branch/index.js";
|
|
13
14
|
export * as cache from "./cache/index.js";
|
|
14
15
|
export * as capacity from "./capacity/index.js";
|
|
@@ -16,12 +17,16 @@ export * as codebase from "./codebase/index.js";
|
|
|
16
17
|
export * from "./coverage-hotspots/index.js";
|
|
17
18
|
export * as doctor from "./doctor/index.js";
|
|
18
19
|
export * from "./encoding/index.js";
|
|
20
|
+
export * as escalation from "./escalation/index.js";
|
|
19
21
|
export * as evalCrud from "./eval/crud-telemetry.js";
|
|
20
22
|
export * as evalHealth from "./eval/health.js";
|
|
21
23
|
export * as evalReport from "./eval/report.js";
|
|
22
24
|
export * as evalRun from "./eval/run.js";
|
|
23
25
|
export * as events from "./events/attribution-ledger.js";
|
|
24
26
|
export * from "./forward-coverage/evaluate.js";
|
|
27
|
+
// #2951 Phase 1: contained-write API for product sinks.
|
|
28
|
+
export { ContainedWriteError, ContainedWriteErrorCode, containedWrite, resolveContainedTarget, } from "./fs/contained-write.js";
|
|
29
|
+
export { assertDestinationNotSymlink, assertDirectoryNotSymlink, assertProjectionContained, assertWriteTargetSafe, PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE, ProjectionContainmentError, walkDirectoryRejectSymlinks, } from "./fs/projection-containment.js";
|
|
25
30
|
export * as intake from "./intake/index.js";
|
|
26
31
|
export * as layout from "./layout/index.js";
|
|
27
32
|
export * as legacyBridge from "./legacy-bridge/index.js";
|
|
@@ -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,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { assertDepositContained } from "../deposit/contain.js";
|
|
4
|
-
import {
|
|
4
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
5
|
+
import { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
|
|
5
6
|
import { isHostHookDepositEnabled, loadHostHooksPolicyFromProject, } from "../policy/host-hooks.js";
|
|
6
|
-
export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
|
|
7
|
+
export { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
|
|
7
8
|
export const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
|
|
8
9
|
export const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
|
|
9
10
|
export const AGENT_HOOK_PATHS = [
|
|
@@ -103,6 +104,10 @@ function mergeNestedConfig(config, path, host, options = {}) {
|
|
|
103
104
|
...preTool,
|
|
104
105
|
nestedGroup(host, "tool.before", DIRECT_WRITE_HOOK_MATCHER),
|
|
105
106
|
nestedGroup(host, "tool.before", SPAWN_HOOK_MATCHER),
|
|
107
|
+
// Shell/Bash for runtimeAuthority scopes.push / scopes.merge (#2711)
|
|
108
|
+
nestedGroup(host, "tool.before", SHELL_HOOK_MATCHER),
|
|
109
|
+
// MCP push/merge (mcp__*, bare merge_pull_request / git_push, …) (#2711)
|
|
110
|
+
nestedGroup(host, "tool.before", MCP_HOOK_MATCHER),
|
|
106
111
|
];
|
|
107
112
|
if (options.compact) {
|
|
108
113
|
const preCompact = eventArray(hooks, "PreCompact", path).filter((entry) => !isManagedNestedGroup(entry));
|
|
@@ -164,18 +169,48 @@ function mergeCursorConfig(config, path) {
|
|
|
164
169
|
failClosed: true,
|
|
165
170
|
timeout: 5,
|
|
166
171
|
},
|
|
172
|
+
{
|
|
173
|
+
command: command("cursor", "tool.before"),
|
|
174
|
+
matcher: SHELL_HOOK_MATCHER,
|
|
175
|
+
failClosed: true,
|
|
176
|
+
timeout: 5,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
command: command("cursor", "tool.before"),
|
|
180
|
+
matcher: MCP_HOOK_MATCHER,
|
|
181
|
+
failClosed: true,
|
|
182
|
+
timeout: 5,
|
|
183
|
+
},
|
|
167
184
|
];
|
|
168
185
|
hooks.preCompact = [...preCompact, { command: command("cursor", "session.compact"), timeout: 5 }];
|
|
169
186
|
return { ...config, version: 1, hooks };
|
|
170
187
|
}
|
|
171
|
-
function writeJsonIfChanged(path, payload) {
|
|
188
|
+
function writeJsonIfChanged(projectRoot, path, payload) {
|
|
172
189
|
const next = `${JSON.stringify(payload, null, 2)}\n`;
|
|
173
190
|
if (existsSync(path) && readFileSync(path, "utf8") === next)
|
|
174
191
|
return false;
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
// Atomic replace via temp under project root (#2951 / #2980 wave A).
|
|
193
|
+
const parent = dirname(path);
|
|
194
|
+
const tmpName = `${basename(path)}.deft-${process.pid}.tmp`;
|
|
195
|
+
const temporary = join(parent, tmpName);
|
|
196
|
+
try {
|
|
197
|
+
containedWrite({
|
|
198
|
+
root: resolve(projectRoot),
|
|
199
|
+
target: temporary,
|
|
200
|
+
data: next,
|
|
201
|
+
mode: "replace",
|
|
202
|
+
});
|
|
203
|
+
renameSync(temporary, path);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
try {
|
|
207
|
+
rmSync(temporary, { force: true });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* best-effort cleanup */
|
|
211
|
+
}
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
179
214
|
return true;
|
|
180
215
|
}
|
|
181
216
|
/** Merge Directive-owned project hook entries without replacing user configuration. */
|
|
@@ -231,7 +266,7 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
|
|
|
231
266
|
for (const item of prepared) {
|
|
232
267
|
if (item.mode === "skip")
|
|
233
268
|
continue;
|
|
234
|
-
if (writeJsonIfChanged(item.absolute, item.payload)) {
|
|
269
|
+
if (writeJsonIfChanged(projectRoot, item.absolute, item.payload)) {
|
|
235
270
|
if (item.mode === "strip")
|
|
236
271
|
strippedPaths.push(item.path);
|
|
237
272
|
else
|
|
@@ -285,6 +320,14 @@ function hasNestedRegistration(config, host, options = {}) {
|
|
|
285
320
|
preTool.some((entry) => {
|
|
286
321
|
const group = object(entry);
|
|
287
322
|
return group?.matcher === SPAWN_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand);
|
|
323
|
+
}) &&
|
|
324
|
+
preTool.some((entry) => {
|
|
325
|
+
const group = object(entry);
|
|
326
|
+
return group?.matcher === SHELL_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand);
|
|
327
|
+
}) &&
|
|
328
|
+
preTool.some((entry) => {
|
|
329
|
+
const group = object(entry);
|
|
330
|
+
return group?.matcher === MCP_HOOK_MATCHER && nestedCommands(entry).includes(toolCommand);
|
|
288
331
|
});
|
|
289
332
|
if (!base)
|
|
290
333
|
return false;
|
|
@@ -315,6 +358,18 @@ function hasCursorRegistration(config) {
|
|
|
315
358
|
hook.matcher === SPAWN_HOOK_MATCHER &&
|
|
316
359
|
hook.failClosed === true);
|
|
317
360
|
}) &&
|
|
361
|
+
preTool.some((entry) => {
|
|
362
|
+
const hook = object(entry);
|
|
363
|
+
return (hook?.command === command("cursor", "tool.before") &&
|
|
364
|
+
hook.matcher === SHELL_HOOK_MATCHER &&
|
|
365
|
+
hook.failClosed === true);
|
|
366
|
+
}) &&
|
|
367
|
+
preTool.some((entry) => {
|
|
368
|
+
const hook = object(entry);
|
|
369
|
+
return (hook?.command === command("cursor", "tool.before") &&
|
|
370
|
+
hook.matcher === MCP_HOOK_MATCHER &&
|
|
371
|
+
hook.failClosed === true);
|
|
372
|
+
}) &&
|
|
318
373
|
preCompact.some((entry) => object(entry)?.command === command("cursor", "session.compact")));
|
|
319
374
|
}
|
|
320
375
|
/** Read-only registration probe shared by verify and doctor. */
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* Refs #1942, #1941, #1015, #1464, #1672.
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
|
-
import { existsSync, readFileSync
|
|
13
|
-
import { join } from "node:path";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
14
15
|
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
15
16
|
import { FORBIDDEN_BLANKET_EVAL_LINES, stripGitignoreInlineComment, } from "../triage/bootstrap/gitignore.js";
|
|
16
17
|
/** Directory ignore entry for the hybrid deposit (greenfield only). */
|
|
@@ -196,8 +197,15 @@ function reconcileGitignoreFile(projectDir, targetLines, includeDeftCoreRational
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
try {
|
|
200
|
+
// Keep early containment so ProjectionContainmentError type is preserved for callers/tests.
|
|
199
201
|
assertWriteTargetSafe(projectDir, path);
|
|
200
|
-
|
|
202
|
+
// #2980 wave A: product write sink routes through containedWrite.
|
|
203
|
+
containedWrite({
|
|
204
|
+
root: resolve(projectDir),
|
|
205
|
+
target: path,
|
|
206
|
+
data: body,
|
|
207
|
+
mode: "replace",
|
|
208
|
+
});
|
|
201
209
|
}
|
|
202
210
|
catch (cause) {
|
|
203
211
|
if (cause instanceof ProjectionContainmentError) {
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
* emits a JSON error object so the calling backend can treat it as non-fatal
|
|
23
23
|
* (never a crash or a partial write).
|
|
24
24
|
*/
|
|
25
|
-
import { mkdirSync, readdirSync, readFileSync
|
|
26
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
25
|
+
import { mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
26
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
27
27
|
import { ContentPackageNotFoundError, resolveInstalledContentRoot, } from "../deposit/resolve-content.js";
|
|
28
28
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
29
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
29
30
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
30
31
|
import { plan } from "../resolution/plan.js";
|
|
31
32
|
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
@@ -233,8 +234,15 @@ export async function runInitHeadlessCli(options) {
|
|
|
233
234
|
const abs = resolve(options.outputPath);
|
|
234
235
|
const writeFile = options.writeFile ??
|
|
235
236
|
((path, data) => {
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
// Contain under the output parent (arbitrary --output path; #2980 wave A).
|
|
238
|
+
const parent = dirname(path);
|
|
239
|
+
mkdirSync(parent, { recursive: true });
|
|
240
|
+
containedWrite({
|
|
241
|
+
root: resolve(parent),
|
|
242
|
+
target: basename(path),
|
|
243
|
+
data,
|
|
244
|
+
mode: "replace",
|
|
245
|
+
});
|
|
238
246
|
});
|
|
239
247
|
writeFile(abs, serialized);
|
|
240
248
|
options.writeErr(`directive init --headless: wrote ${manifest.files.length}-file manifest (v${manifest.version}) to ${abs}\n`);
|
|
@@ -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);
|
|
@@ -49,7 +49,7 @@ export interface MigrateSeams {
|
|
|
49
49
|
isFile?: (path: string) => boolean;
|
|
50
50
|
/** Text read returning null on failure (default: node:fs readFileSync, utf8). */
|
|
51
51
|
readText?: (path: string) => string | null;
|
|
52
|
-
/** Text write (default:
|
|
52
|
+
/** Text write (default: containedWrite under projectRoot; #2980 wave A). */
|
|
53
53
|
writeText?: (path: string, text: string) => void;
|
|
54
54
|
/** ISO-8601 UTC timestamp source for the backup filename (default: Date.now). */
|
|
55
55
|
nowIso?: () => string;
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Refs #1941, #1912 (freeze prerequisite b), #1933 (never-first-start), #1670.
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync, readFileSync
|
|
28
|
-
import { dirname, join } from "node:path";
|
|
27
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
28
|
+
import { dirname, join, resolve } from "node:path";
|
|
29
29
|
import { fileURLToPath } from "node:url";
|
|
30
30
|
import { resolveContentPackageRoot } from "../content-root.js";
|
|
31
31
|
import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
|
|
32
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
32
33
|
import { CANONICAL_INSTALL_ROOT } from "./scaffold.js";
|
|
33
34
|
/**
|
|
34
35
|
* The npm-managed handshake sentinel. `directive migrate` adds this key/value to
|
|
@@ -99,7 +100,16 @@ function engineMissingMessage() {
|
|
|
99
100
|
export function runMigrate(projectRoot, seams = {}) {
|
|
100
101
|
const isFile = seams.isFile ?? existsSync;
|
|
101
102
|
const readText = seams.readText ?? defaultReadText;
|
|
102
|
-
const writeText = seams.writeText ??
|
|
103
|
+
const writeText = seams.writeText ??
|
|
104
|
+
((path, text) => {
|
|
105
|
+
// #2980 wave A: product write sink routes through containedWrite.
|
|
106
|
+
containedWrite({
|
|
107
|
+
root: resolve(projectRoot),
|
|
108
|
+
target: path,
|
|
109
|
+
data: text,
|
|
110
|
+
mode: "replace",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
103
113
|
const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
|
|
104
114
|
const resolveEngine = seams.resolveEngine ?? defaultResolveEngine;
|
|
105
115
|
const manifestPath = detectCanonicalVendoredManifest(projectRoot, isFile);
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Refs #2534, #670.
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, readFileSync
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import {
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
13
|
+
import { assertDestinationNotSymlink } from "../fs/projection-containment.js";
|
|
13
14
|
import { stripGitignoreInlineComment } from "../triage/bootstrap/gitignore.js";
|
|
14
15
|
/** Directory ignore entry for the hybrid deposit. */
|
|
15
16
|
export const PRETTIERIGNORE_DEFT_CORE_LINE = ".deft/core/";
|
|
@@ -18,7 +19,7 @@ const DEFT_CORE_COVERING_LINES = new Set([".deft/core/", ".deft/core", ".deft/co
|
|
|
18
19
|
const DEFT_FRAMEWORK_PRETTIERIGNORE_HEADER = "# Deft framework: the vendored payload is outside the consumer Prettier gate (#2534).\n";
|
|
19
20
|
function projectionTarget(projectDir) {
|
|
20
21
|
const target = join(projectDir, ".prettierignore");
|
|
21
|
-
|
|
22
|
+
assertDestinationNotSymlink(projectDir, target);
|
|
22
23
|
return target;
|
|
23
24
|
}
|
|
24
25
|
function prettierIgnoreCoversLine(present, line) {
|
|
@@ -72,7 +73,13 @@ export function ensurePrettierIgnoreLines(projectDir, io) {
|
|
|
72
73
|
body += `${add}\n`;
|
|
73
74
|
}
|
|
74
75
|
try {
|
|
75
|
-
|
|
76
|
+
// #2980 wave A: product write sink routes through containedWrite.
|
|
77
|
+
containedWrite({
|
|
78
|
+
root: resolve(projectDir),
|
|
79
|
+
target: path,
|
|
80
|
+
data: body,
|
|
81
|
+
mode: "replace",
|
|
82
|
+
});
|
|
76
83
|
}
|
|
77
84
|
catch (cause) {
|
|
78
85
|
throw new Error(`could not write .prettierignore: ${String(cause)}`);
|
|
@@ -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) {
|