@deftai/directive-core 0.86.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 +2 -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.d.ts +6 -5
- package/dist/doctor/main.js +80 -18
- package/dist/doctor/taskfile.d.ts +8 -0
- package/dist/doctor/taskfile.js +19 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/dispatcher.d.ts +34 -2
- package/dist/hooks/dispatcher.js +234 -21
- 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-emit.d.ts +45 -2
- package/dist/intake/issue-emit.js +420 -17
- package/dist/intake/issue-ingest.js +65 -6
- 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/platform/platform-capabilities.js +3 -0
- 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/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +6 -2
- package/dist/review-monitor/tier-detection.js +27 -2
- package/dist/scope/transition.js +43 -0
- package/dist/session/release-availability.d.ts +2 -0
- package/dist/session/release-availability.js +23 -8
- 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/swarm/routing-set-cli.js +5 -10
- package/dist/swarm/routing.d.ts +3 -2
- package/dist/swarm/routing.js +16 -4
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +15 -6
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/show.d.ts +69 -0
- package/dist/triage/queue/show.js +293 -0
- package/dist/triage/scope/cli.js +3 -0
- package/dist/triage/scope/coverage.d.ts +2 -0
- package/dist/triage/scope/coverage.js +18 -3
- 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.d.ts +37 -0
- package/dist/verify-source/openclaw-tier1.js +105 -0
- package/dist/xbrief-migrate/migrate-project.js +9 -5
- package/package.json +4 -3
|
@@ -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) {
|
|
@@ -9,7 +9,7 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync,
|
|
|
9
9
|
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { platform } from "node:os";
|
|
11
11
|
import { dirname, join, relative } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { assertDestinationNotSymlink, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
13
13
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
14
14
|
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
15
15
|
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
@@ -17,10 +17,14 @@ import { installerManagedGuardEre } from "./hygiene.js";
|
|
|
17
17
|
import { syncConsumerXbriefSchemas } from "./xbrief-projections.js";
|
|
18
18
|
export { CANONICAL_INSTALL_ROOT };
|
|
19
19
|
export const CORE_GLOB = ".deft/core/**";
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Refuse init/update projection writes that escape via repo-controlled symlinks
|
|
22
|
+
* (#2446) OR that would follow an in-tree destination symlink on the write path
|
|
23
|
+
* (#2912). Every consumer projection sink in this module routes through here.
|
|
24
|
+
*/
|
|
21
25
|
function projectionTarget(projectDir, ...relSegments) {
|
|
22
26
|
const target = join(projectDir, ...relSegments);
|
|
23
|
-
|
|
27
|
+
assertDestinationNotSymlink(projectDir, target);
|
|
24
28
|
return target;
|
|
25
29
|
}
|
|
26
30
|
const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
9
9
|
import { join, relative } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { assertDestinationNotSymlink } from "../fs/projection-containment.js";
|
|
11
11
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
12
12
|
import { DEV_FALLBACK } from "../platform/constants.js";
|
|
13
13
|
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
@@ -54,7 +54,7 @@ export function rewriteProjectedSchemaContent(content) {
|
|
|
54
54
|
* Upstream vbrief/schemas/ may keep legacy paths; consumer copies must not (#2670).
|
|
55
55
|
*/
|
|
56
56
|
export function assertProjectedSchemaDescriptionsRooted(projectDir, destinationDir) {
|
|
57
|
-
|
|
57
|
+
assertDestinationNotSymlink(projectDir, destinationDir);
|
|
58
58
|
if (!isDirectory(destinationDir))
|
|
59
59
|
return;
|
|
60
60
|
for (const rel of collectSchemaFiles(destinationDir)) {
|
|
@@ -66,7 +66,7 @@ export function assertProjectedSchemaDescriptionsRooted(projectDir, destinationD
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
function writeFileIfChanged(projectDir, target, content) {
|
|
69
|
-
|
|
69
|
+
assertDestinationNotSymlink(projectDir, target);
|
|
70
70
|
const desired = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
71
71
|
try {
|
|
72
72
|
if (readFileSync(target).equals(desired))
|
|
@@ -91,7 +91,7 @@ export function syncConsumerXbriefSchemas(projectDir, deftDir) {
|
|
|
91
91
|
throw new Error(`cannot project xbrief schemas: framework payload is missing ${CURRENT_CORE_SCHEMA}`);
|
|
92
92
|
}
|
|
93
93
|
const destinationDir = join(projectDir, MIGRATED_ARTIFACT_DIR, "schemas");
|
|
94
|
-
|
|
94
|
+
assertDestinationNotSymlink(projectDir, destinationDir);
|
|
95
95
|
mkdirSync(destinationDir, { recursive: true });
|
|
96
96
|
let changed = false;
|
|
97
97
|
for (const rel of collectSchemaFiles(sourceDir)) {
|
|
@@ -103,7 +103,7 @@ export function syncConsumerXbriefSchemas(projectDir, deftDir) {
|
|
|
103
103
|
changed = writeFileIfChanged(projectDir, destination, projected) || changed;
|
|
104
104
|
}
|
|
105
105
|
const obsoleteDestination = join(destinationDir, OBSOLETE_CORE_SCHEMA);
|
|
106
|
-
|
|
106
|
+
assertDestinationNotSymlink(projectDir, obsoleteDestination);
|
|
107
107
|
if (existsSync(obsoleteDestination)) {
|
|
108
108
|
rmSync(obsoleteDestination, { force: true });
|
|
109
109
|
changed = true;
|
|
@@ -117,7 +117,7 @@ function syncBareVersionMarkerWithPolicy(projectDir, version, allowRootFallback)
|
|
|
117
117
|
return false;
|
|
118
118
|
const canonicalRoot = join(projectDir, MIGRATED_ARTIFACT_DIR);
|
|
119
119
|
if (existsSync(canonicalRoot)) {
|
|
120
|
-
|
|
120
|
+
assertDestinationNotSymlink(projectDir, join(canonicalRoot, ".deft-version"));
|
|
121
121
|
}
|
|
122
122
|
let targetDir = projectDir;
|
|
123
123
|
try {
|
|
@@ -1,11 +1,51 @@
|
|
|
1
1
|
import type { ScmCallFn } from "./reconcile-issues.js";
|
|
2
2
|
export declare const GITHUB_ISSUE_REF_TYPE = "x-xbrief/github-issue";
|
|
3
3
|
export declare const EXTERNAL_TRUST_LEVEL = "external";
|
|
4
|
+
/**
|
|
5
|
+
* Structured post-create failure: remote issue exists; local stamp/ledger may not.
|
|
6
|
+
* `createdUrl` is the durable handle for retry (also mirrored in process + OS-temp recovery).
|
|
7
|
+
*/
|
|
4
8
|
export declare class IssueEmitError extends Error {
|
|
5
|
-
|
|
9
|
+
readonly createdUrl?: string;
|
|
10
|
+
constructor(message: string, options?: {
|
|
11
|
+
createdUrl?: string;
|
|
12
|
+
});
|
|
6
13
|
}
|
|
14
|
+
export declare function recoverySidecarPath(vbriefAbsPath: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Always-on recovery after remote create: process memory + private OS-temp sidecar.
|
|
17
|
+
* Independent of project-contained ledger so dual local failure still reconciles on retry (#2880).
|
|
18
|
+
*/
|
|
19
|
+
export declare function rememberCreatedUrl(vbriefAbsPath: string, url: string): void;
|
|
20
|
+
export declare function loadRecoveredUrl(vbriefAbsPath: string): string | undefined;
|
|
21
|
+
export declare function clearRecoveredUrl(vbriefAbsPath: string): void;
|
|
22
|
+
/** Project ledger first, then process/OS-temp recovery (#2880). */
|
|
23
|
+
export declare function resolvePriorCreatedUrl(projectRoot: string, vbriefAbsPath: string): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Record URL immediately after remote create. Project ledger is best-effort;
|
|
26
|
+
* recovery layers always run first so dual local failure cannot force re-create (#2880).
|
|
27
|
+
*/
|
|
28
|
+
export declare function recordCreatedUrlDurable(projectRoot: string, vbriefAbsPath: string, url: string): void;
|
|
29
|
+
/** Contained durable map: abs vbrief path -> issue URL for in-flight emits (#2871). */
|
|
30
|
+
export declare function pendingEmitLedgerPath(projectRoot: string): string;
|
|
31
|
+
export declare function loadPendingEmitUrls(projectRoot: string): Record<string, string>;
|
|
32
|
+
export declare function savePendingEmitUrl(projectRoot: string, vbriefAbsPath: string, url: string): void;
|
|
33
|
+
export declare function clearPendingEmitUrl(projectRoot: string, vbriefAbsPath: string): void;
|
|
7
34
|
export declare function loadVbrief(path: string): Record<string, unknown>;
|
|
8
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Resolve project root and refuse unsafe write targets before any emit side-effect.
|
|
37
|
+
* Callers that create remote issues MUST invoke this before `fileIssue` so a
|
|
38
|
+
* containment refusal cannot leave an orphan GitHub issue (#2869 / #2871).
|
|
39
|
+
*
|
|
40
|
+
* ⊗ Do not fall back to `dirname(path)` as the containment root — that trusts a
|
|
41
|
+
* possibly-symlinked parent and would re-open the escape (Greptile P1 on #2871).
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertVbriefWriteTargetSafe(path: string, projectRoot?: string | null): string;
|
|
44
|
+
/**
|
|
45
|
+
* Persist an xBRIEF/vBRIEF JSON document. Gates the write target so a leaf or
|
|
46
|
+
* parent-directory symlink cannot divert the stamped file outside the project (#2869).
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeVbrief(path: string, data: Record<string, unknown>, projectRoot?: string | null): void;
|
|
9
49
|
export declare function vbriefTitle(data: Record<string, unknown>): string;
|
|
10
50
|
export declare function existingGithubIssueRef(data: Record<string, unknown>): string | null | undefined;
|
|
11
51
|
export declare function addGithubIssueReference(data: Record<string, unknown>, url: string): Record<string, unknown>;
|
|
@@ -23,12 +63,14 @@ export declare function emitSingle(path: string, options: {
|
|
|
23
63
|
scmCall?: ScmCallFn;
|
|
24
64
|
noNetwork?: boolean;
|
|
25
65
|
displayPath?: string | null;
|
|
66
|
+
projectRoot?: string | null;
|
|
26
67
|
}): EmitAction;
|
|
27
68
|
export declare function emitPerVbrief(paths: string[], options: {
|
|
28
69
|
repo: string;
|
|
29
70
|
scmCall?: ScmCallFn;
|
|
30
71
|
noNetwork?: boolean;
|
|
31
72
|
displayPaths?: string[] | null;
|
|
73
|
+
projectRoot?: string | null;
|
|
32
74
|
}): EmitAction[];
|
|
33
75
|
export interface UmbrellaAction {
|
|
34
76
|
result: "created" | "dryrun" | "skipped";
|
|
@@ -45,6 +87,7 @@ export declare function emitUmbrella(paths: string[], options: {
|
|
|
45
87
|
noNetwork?: boolean;
|
|
46
88
|
title?: string | null;
|
|
47
89
|
displayPaths?: string[] | null;
|
|
90
|
+
projectRoot?: string | null;
|
|
48
91
|
}): UmbrellaAction;
|
|
49
92
|
export declare function expandPatterns(patterns: string[], root?: string | null): string[];
|
|
50
93
|
export declare function isNoNetwork(dryRun: boolean): boolean;
|