@deftai/directive-core 0.88.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/task-cache/store.js +11 -7
- package/dist/capacity/backfill.js +10 -4
- package/dist/category-b-namespace/index.js +10 -4
- package/dist/codebase/default-extractor.js +3 -0
- package/dist/codebase/map.js +11 -4
- package/dist/doctor/doctor-state.js +28 -4
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +160 -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/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 +21 -18
- package/dist/hooks/dispatcher.js +212 -163
- 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/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/init-deposit/agent-hooks.js +27 -8
- package/dist/init-deposit/gitignore.js +11 -3
- package/dist/init-deposit/headless-manifest.js +12 -4
- package/dist/init-deposit/migrate.d.ts +1 -1
- package/dist/init-deposit/migrate.js +13 -3
- package/dist/init-deposit/prettierignore.js +10 -3
- package/dist/init-deposit/scaffold.js +35 -26
- package/dist/init-deposit/xbrief-projections.js +11 -4
- 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/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.js +19 -6
- 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 +4 -0
- package/dist/policy/index.js +31 -1
- package/dist/policy/intent-ceiling.d.ts +79 -0
- package/dist/policy/intent-ceiling.js +181 -0
- package/dist/policy/no-deft-directive.js +10 -3
- package/dist/policy/org-force-on-migration.js +9 -5
- 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 +15 -2
- package/dist/policy/runtime-authority.js +23 -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.js +7 -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/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.js +14 -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 +15 -3
|
@@ -1,6 +1,7 @@
|
|
|
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 { containedWrite } from "../fs/contained-write.js";
|
|
4
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
7
|
export { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
|
|
@@ -184,14 +185,32 @@ function mergeCursorConfig(config, path) {
|
|
|
184
185
|
hooks.preCompact = [...preCompact, { command: command("cursor", "session.compact"), timeout: 5 }];
|
|
185
186
|
return { ...config, version: 1, hooks };
|
|
186
187
|
}
|
|
187
|
-
function writeJsonIfChanged(path, payload) {
|
|
188
|
+
function writeJsonIfChanged(projectRoot, path, payload) {
|
|
188
189
|
const next = `${JSON.stringify(payload, null, 2)}\n`;
|
|
189
190
|
if (existsSync(path) && readFileSync(path, "utf8") === next)
|
|
190
191
|
return false;
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
214
|
return true;
|
|
196
215
|
}
|
|
197
216
|
/** Merge Directive-owned project hook entries without replacing user configuration. */
|
|
@@ -247,7 +266,7 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
|
|
|
247
266
|
for (const item of prepared) {
|
|
248
267
|
if (item.mode === "skip")
|
|
249
268
|
continue;
|
|
250
|
-
if (writeJsonIfChanged(item.absolute, item.payload)) {
|
|
269
|
+
if (writeJsonIfChanged(projectRoot, item.absolute, item.payload)) {
|
|
251
270
|
if (item.mode === "strip")
|
|
252
271
|
strippedPaths.push(item.path);
|
|
253
272
|
else
|
|
@@ -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`);
|
|
@@ -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,8 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Refs #2534, #670.
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, readFileSync
|
|
11
|
-
import { join } from "node:path";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
12
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. */
|
|
@@ -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)}`);
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Refs #1942, #1430, #1463, #1179.
|
|
6
6
|
*/
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync
|
|
9
|
-
import { mkdir, readdir, rm, stat
|
|
8
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
9
|
+
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
10
10
|
import { platform } from "node:os";
|
|
11
|
-
import {
|
|
11
|
+
import { join, relative, resolve } from "node:path";
|
|
12
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
12
13
|
import { assertDestinationNotSymlink, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
13
14
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
14
15
|
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
@@ -27,6 +28,19 @@ function projectionTarget(projectDir, ...relSegments) {
|
|
|
27
28
|
assertDestinationNotSymlink(projectDir, target);
|
|
28
29
|
return target;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Product write sink: containment root is the project, then containedWrite
|
|
33
|
+
* (#2951 / #2980 wave A). Call after projectionTarget / assertDestinationNotSymlink
|
|
34
|
+
* when the early ProjectionContainmentError type is required by tests.
|
|
35
|
+
*/
|
|
36
|
+
function containedProjectWrite(projectDir, target, data) {
|
|
37
|
+
containedWrite({
|
|
38
|
+
root: resolve(projectDir),
|
|
39
|
+
target,
|
|
40
|
+
data,
|
|
41
|
+
mode: "replace",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
30
44
|
const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
|
|
31
45
|
const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
|
|
32
46
|
const FRAMEWORK_SELF_TEST_REL = ".deft/core/tests";
|
|
@@ -219,9 +233,9 @@ export function writeInstallManifest(projectDir, deftDir, fields) {
|
|
|
219
233
|
relative(projectDir, deftDir).split("\\").join("/") ||
|
|
220
234
|
CANONICAL_INSTALL_ROOT;
|
|
221
235
|
const body = buildInstallManifestText({ ...fields, installRoot });
|
|
222
|
-
mkdirSync(deftDir, { recursive: true });
|
|
223
236
|
const path = join(deftDir, "VERSION");
|
|
224
|
-
|
|
237
|
+
// #2980 wave A: product write sink routes through containedWrite.
|
|
238
|
+
containedProjectWrite(projectDir, path, body);
|
|
225
239
|
return path;
|
|
226
240
|
}
|
|
227
241
|
/** Canonical committed pin dependency name (mirrors resolution/pin.ts). */
|
|
@@ -275,7 +289,7 @@ export function ensurePackageJsonPin(projectDir, version, io) {
|
|
|
275
289
|
}
|
|
276
290
|
devDeps[PIN_DEPENDENCY_NAME] = pinVersion;
|
|
277
291
|
pkg.devDependencies = devDeps;
|
|
278
|
-
|
|
292
|
+
containedProjectWrite(projectDir, path, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
279
293
|
io.printf(existed
|
|
280
294
|
? `package.json updated: pinned ${PIN_DEPENDENCY_NAME}@${pinVersion} (exact).\n`
|
|
281
295
|
: `package.json created: pinned ${PIN_DEPENDENCY_NAME}@${pinVersion} (exact, private).\n`);
|
|
@@ -296,7 +310,7 @@ export function writeAgentsMd(projectDir, deftDir, io) {
|
|
|
296
310
|
throw new Error("AGENTS.md render produced no content");
|
|
297
311
|
}
|
|
298
312
|
const path = projectionTarget(projectDir, "AGENTS.md");
|
|
299
|
-
|
|
313
|
+
containedProjectWrite(projectDir, path, newContent);
|
|
300
314
|
if (state === "absent") {
|
|
301
315
|
io.printf("AGENTS.md created.\n");
|
|
302
316
|
}
|
|
@@ -320,7 +334,7 @@ async function ensureVbriefLifecycleDirs(projectDir) {
|
|
|
320
334
|
const entries = await readdir(dir);
|
|
321
335
|
if (entries.length > 0)
|
|
322
336
|
continue;
|
|
323
|
-
|
|
337
|
+
containedProjectWrite(projectDir, gitkeep, VBRIEF_LIFECYCLE_GITKEEP);
|
|
324
338
|
}
|
|
325
339
|
}
|
|
326
340
|
function vbriefLifecycleDirsPresent(consumerVbrief) {
|
|
@@ -354,7 +368,7 @@ export async function writeConsumerVbrief(projectDir, deftDir, io) {
|
|
|
354
368
|
copyFileSync(fwVbriefMd, vbriefMdDst);
|
|
355
369
|
}
|
|
356
370
|
else {
|
|
357
|
-
|
|
371
|
+
containedProjectWrite(projectDir, vbriefMdDst, VBRIEF_README_BODY);
|
|
358
372
|
}
|
|
359
373
|
}
|
|
360
374
|
await ensureVbriefLifecycleDirs(projectDir);
|
|
@@ -369,12 +383,10 @@ export function writeAgentsSkills(projectDir, io) {
|
|
|
369
383
|
return false;
|
|
370
384
|
}
|
|
371
385
|
for (const skill of AGENTS_SKILLS) {
|
|
372
|
-
const dir = projectionTarget(projectDir, ".agents", "skills", skill.dir);
|
|
373
|
-
mkdirSync(dir, { recursive: true });
|
|
374
386
|
const path = projectionTarget(projectDir, ".agents", "skills", skill.dir, "SKILL.md");
|
|
375
387
|
if (existsSync(path))
|
|
376
388
|
continue;
|
|
377
|
-
|
|
389
|
+
containedProjectWrite(projectDir, path, skill.content);
|
|
378
390
|
}
|
|
379
391
|
io.printf(".agents/skills/ created — deft skills will be auto-discovered.\n");
|
|
380
392
|
return true;
|
|
@@ -459,21 +471,20 @@ export function ensureTaskfile(projectDir, io) {
|
|
|
459
471
|
" optional: true\n";
|
|
460
472
|
io.printf("Appended new `includes:` block with deft entry to Taskfile.yml (Epic-4).\n");
|
|
461
473
|
}
|
|
462
|
-
|
|
474
|
+
containedProjectWrite(projectDir, path, resultText);
|
|
463
475
|
return true;
|
|
464
476
|
}
|
|
465
477
|
const HOOK_FILENAMES = ["pre-commit", "pre-push"];
|
|
466
478
|
const HOOK_SUPPORT_FILENAMES = ["_deft-run.sh"];
|
|
467
479
|
const HOOK_FILE_MODE = 0o755;
|
|
468
|
-
const HOOK_SUPPORT_FILE_MODE = 0o644;
|
|
469
480
|
export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
470
481
|
const srcDir = join(deftDir, ".githooks");
|
|
471
482
|
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
|
|
472
483
|
io.printf(`git hooks source ${srcDir} absent — skipping hook wiring.\n`);
|
|
473
484
|
return false;
|
|
474
485
|
}
|
|
475
|
-
|
|
476
|
-
|
|
486
|
+
// Ensure parent path is containment-checked before any file write.
|
|
487
|
+
projectionTarget(projectDir, ".githooks");
|
|
477
488
|
let filesDeposited = false;
|
|
478
489
|
for (const name of [...HOOK_FILENAMES, ...HOOK_SUPPORT_FILENAMES]) {
|
|
479
490
|
const src = join(srcDir, name);
|
|
@@ -484,8 +495,8 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
|
484
495
|
const existing = existsSync(dst) ? readFileSync(dst) : null;
|
|
485
496
|
const isHookScript = HOOK_FILENAMES.includes(name);
|
|
486
497
|
if (!existing?.equals(data)) {
|
|
487
|
-
|
|
488
|
-
|
|
498
|
+
// #2980 wave A: containedWrite (mode is applied via chmod below for hooks).
|
|
499
|
+
containedProjectWrite(projectDir, dst, data);
|
|
489
500
|
filesDeposited = true;
|
|
490
501
|
}
|
|
491
502
|
if (platform() !== "win32" && isHookScript) {
|
|
@@ -682,7 +693,7 @@ export function ensureGitattributes(projectDir, io) {
|
|
|
682
693
|
for (const add of additions) {
|
|
683
694
|
body += `${add}\n`;
|
|
684
695
|
}
|
|
685
|
-
|
|
696
|
+
containedProjectWrite(projectDir, path, body);
|
|
686
697
|
io.printf(`.gitattributes updated with Deft core markers: ${additions.join(", ")}\n`);
|
|
687
698
|
return true;
|
|
688
699
|
}
|
|
@@ -725,7 +736,7 @@ export function ensureGreptileIgnore(projectDir, io) {
|
|
|
725
736
|
return false;
|
|
726
737
|
}
|
|
727
738
|
obj.ignorePatterns = appendGreptilePattern(patterns, CORE_GLOB);
|
|
728
|
-
|
|
739
|
+
containedProjectWrite(projectDir, path, `${JSON.stringify(obj, null, 2)}\n`);
|
|
729
740
|
io.printf(fileExisted
|
|
730
741
|
? `greptile.json updated: bot review now ignores ${CORE_GLOB}.\n`
|
|
731
742
|
: `greptile.json created: bot review ignores ${CORE_GLOB}.\n`);
|
|
@@ -774,8 +785,7 @@ function insertCodeqlPathsIgnore(content, glob) {
|
|
|
774
785
|
export function ensureCodeqlPathsIgnore(projectDir, io) {
|
|
775
786
|
const path = projectionTarget(projectDir, CODEQL_CONFIG_REL);
|
|
776
787
|
if (!existsSync(path)) {
|
|
777
|
-
|
|
778
|
-
writeFileSync(path, codeqlConfigDefault(), "utf8");
|
|
788
|
+
containedProjectWrite(projectDir, path, codeqlConfigDefault());
|
|
779
789
|
io.printf(`${CODEQL_CONFIG_REL} created: CodeQL ignores ${CORE_GLOB}.\n`);
|
|
780
790
|
return true;
|
|
781
791
|
}
|
|
@@ -788,7 +798,7 @@ export function ensureCodeqlPathsIgnore(projectDir, io) {
|
|
|
788
798
|
const updated = inserted.ok
|
|
789
799
|
? inserted.content
|
|
790
800
|
: `${existing}${existing.endsWith("\n") ? "" : "\n"}paths-ignore:\n - '${CORE_GLOB}'\n`;
|
|
791
|
-
|
|
801
|
+
containedProjectWrite(projectDir, path, updated);
|
|
792
802
|
io.printf(`${CODEQL_CONFIG_REL} updated: CodeQL now ignores ${CORE_GLOB}.\n`);
|
|
793
803
|
return true;
|
|
794
804
|
}
|
|
@@ -806,12 +816,11 @@ export function ensureCoreGuardWorkflow(projectDir, io) {
|
|
|
806
816
|
io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
|
|
807
817
|
return false;
|
|
808
818
|
}
|
|
809
|
-
|
|
819
|
+
containedProjectWrite(projectDir, path, refreshed);
|
|
810
820
|
io.printf(`${CORE_GUARD_WORKFLOW_REL} refreshed: deft-core-guard allowlist updated (#1478).\n`);
|
|
811
821
|
return true;
|
|
812
822
|
}
|
|
813
|
-
|
|
814
|
-
writeFileSync(path, desired, "utf8");
|
|
823
|
+
containedProjectWrite(projectDir, path, desired);
|
|
815
824
|
io.printf(`${CORE_GUARD_WORKFLOW_REL} created: CI refuses PRs mixing ${CORE_GLOB} with app files.\n`);
|
|
816
825
|
return true;
|
|
817
826
|
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* freshness. A current `.deft/core/VERSION` therefore cannot short-circuit
|
|
6
6
|
* these repairs.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync
|
|
9
|
-
import { join, relative } from "node:path";
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
9
|
+
import { join, relative, resolve } from "node:path";
|
|
10
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
10
11
|
import { assertDestinationNotSymlink } from "../fs/projection-containment.js";
|
|
11
12
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
12
13
|
import { DEV_FALLBACK } from "../platform/constants.js";
|
|
@@ -66,6 +67,7 @@ export function assertProjectedSchemaDescriptionsRooted(projectDir, destinationD
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
function writeFileIfChanged(projectDir, target, content) {
|
|
70
|
+
// Keep early containment so ProjectionContainmentError type is preserved for callers/tests.
|
|
69
71
|
assertDestinationNotSymlink(projectDir, target);
|
|
70
72
|
const desired = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
71
73
|
try {
|
|
@@ -75,8 +77,13 @@ function writeFileIfChanged(projectDir, target, content) {
|
|
|
75
77
|
catch {
|
|
76
78
|
// Missing or unreadable target is replaced below.
|
|
77
79
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
// #2980 wave A: product write sink routes through containedWrite.
|
|
81
|
+
containedWrite({
|
|
82
|
+
root: resolve(projectDir),
|
|
83
|
+
target,
|
|
84
|
+
data: desired,
|
|
85
|
+
mode: "replace",
|
|
86
|
+
});
|
|
80
87
|
return true;
|
|
81
88
|
}
|
|
82
89
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { ContainedWriteError, ContainedWriteErrorCode, containedWrite, } from "../fs/contained-write.js";
|
|
4
5
|
import { pyRepr } from "../scm/py-format.js";
|
|
5
6
|
import { resolveCandidatesLogPath } from "../triage/cache-path.js";
|
|
6
7
|
function defaultLogPath() {
|
|
@@ -110,40 +111,45 @@ function validateEntry(entry) {
|
|
|
110
111
|
function resolvePath(path) {
|
|
111
112
|
return path !== undefined && path !== null ? resolve(path) : defaultLogPath();
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Cross-process exclusive lock via containedWrite mode=create (O_EXCL).
|
|
116
|
+
* #2980 wave B: lock + product appends route through containedWrite.
|
|
117
|
+
*/
|
|
118
|
+
function acquireAppendLock(logPath, containmentRoot) {
|
|
114
119
|
while (threadLocked) {
|
|
115
120
|
// spin-wait for in-process serialization
|
|
116
121
|
}
|
|
117
122
|
threadLocked = true;
|
|
118
|
-
const lockPath =
|
|
123
|
+
const lockPath = resolve(dirname(logPath), `${basename(logPath)}.lock`);
|
|
119
124
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
120
125
|
const deadline = Date.now() + 30_000;
|
|
121
|
-
|
|
122
|
-
while (fd === null) {
|
|
126
|
+
while (true) {
|
|
123
127
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
containedWrite({
|
|
129
|
+
root: containmentRoot,
|
|
130
|
+
target: lockPath,
|
|
131
|
+
data: "\0",
|
|
132
|
+
mode: "create",
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (err instanceof ContainedWriteError && err.code === ContainedWriteErrorCode.EXISTS) {
|
|
138
|
+
if (Date.now() > deadline) {
|
|
139
|
+
threadLocked = false;
|
|
140
|
+
throw new CandidatesLogError("timed out acquiring candidates log lock");
|
|
141
|
+
}
|
|
142
|
+
const spinEnd = Date.now() + 20;
|
|
143
|
+
while (Date.now() < spinEnd) {
|
|
144
|
+
// brief spin
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
134
147
|
}
|
|
148
|
+
threadLocked = false;
|
|
149
|
+
throw err;
|
|
135
150
|
}
|
|
136
151
|
}
|
|
137
|
-
writeSync(fd, Buffer.from("\0"));
|
|
138
152
|
return () => {
|
|
139
|
-
try {
|
|
140
|
-
if (fd !== null) {
|
|
141
|
-
closeSync(fd);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
// ignore
|
|
146
|
-
}
|
|
147
153
|
try {
|
|
148
154
|
unlinkSync(lockPath);
|
|
149
155
|
}
|
|
@@ -153,24 +159,29 @@ function acquireAppendLock(logPath) {
|
|
|
153
159
|
threadLocked = false;
|
|
154
160
|
};
|
|
155
161
|
}
|
|
162
|
+
/** Ensure parent exists and return it as the containment root for log writes. */
|
|
163
|
+
function containmentRootForLog(logPath) {
|
|
164
|
+
const parent = dirname(logPath);
|
|
165
|
+
mkdirSync(parent, { recursive: true });
|
|
166
|
+
return resolve(parent);
|
|
167
|
+
}
|
|
156
168
|
export function validateCandidatesEntry(entry) {
|
|
157
169
|
validateEntry(entry);
|
|
158
170
|
}
|
|
159
171
|
export function append(entry, options = {}) {
|
|
160
172
|
validateEntry(entry);
|
|
161
173
|
const logPath = resolvePath(options.path);
|
|
162
|
-
|
|
174
|
+
const root = containmentRootForLog(logPath);
|
|
163
175
|
const line = JSON.stringify(entry, Object.keys(entry).sort());
|
|
164
|
-
const release = acquireAppendLock(logPath);
|
|
176
|
+
const release = acquireAppendLock(logPath, root);
|
|
165
177
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
178
|
+
// #2980 wave B: product write sink routes through containedWrite.
|
|
179
|
+
containedWrite({
|
|
180
|
+
root,
|
|
181
|
+
target: logPath,
|
|
182
|
+
data: `${line}\n`,
|
|
183
|
+
mode: "append",
|
|
184
|
+
});
|
|
174
185
|
}
|
|
175
186
|
finally {
|
|
176
187
|
release();
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Production entry for `task scm:body:*` / `directive github-body <subcommand>`.
|
|
4
|
+
* Dispatch peels the `github-body` verb; argv[0] is the subcommand
|
|
5
|
+
* (`issue-lint`, `pr-lint`, `issue-edit`, …). Wired through githubBodyMain
|
|
6
|
+
* so create/edit fail closed and lint commands share one encoding gate (#2960).
|
|
7
|
+
*/
|
|
2
8
|
export declare function mainEntry(argv?: string[]): number;
|
|
3
9
|
//# sourceMappingURL=github-body-cli.d.ts.map
|
|
@@ -19,9 +19,26 @@ function parseArgs(argv) {
|
|
|
19
19
|
out.bodyFile = argv[++i];
|
|
20
20
|
else if (arg === "--out-file")
|
|
21
21
|
out.outFile = argv[++i];
|
|
22
|
+
else if (/^\d+$/.test(arg)) {
|
|
23
|
+
// Positional number: issue-lint / issue-* use --issue; pr-lint / pr-* use --pr (#2960).
|
|
24
|
+
const n = Number.parseInt(arg, 10);
|
|
25
|
+
if (out.command.startsWith("pr-")) {
|
|
26
|
+
if (out.pr === undefined)
|
|
27
|
+
out.pr = n;
|
|
28
|
+
}
|
|
29
|
+
else if (out.issue === undefined) {
|
|
30
|
+
out.issue = n;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
34
|
return out;
|
|
24
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Production entry for `task scm:body:*` / `directive github-body <subcommand>`.
|
|
38
|
+
* Dispatch peels the `github-body` verb; argv[0] is the subcommand
|
|
39
|
+
* (`issue-lint`, `pr-lint`, `issue-edit`, …). Wired through githubBodyMain
|
|
40
|
+
* so create/edit fail closed and lint commands share one encoding gate (#2960).
|
|
41
|
+
*/
|
|
25
42
|
export function mainEntry(argv = process.argv.slice(2)) {
|
|
26
43
|
const parsed = parseArgs(argv);
|
|
27
44
|
return githubBodyMain(parsed);
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { type Finding } from "../encoding/scan.js";
|
|
1
2
|
export declare const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
3
|
+
/** Stable error code for CP1252/CP437-as-UTF-8 / U+FFFD body corruption (#2960). */
|
|
4
|
+
export declare const SCM_BODY_ENCODING_CODE = "scm-body-encoding";
|
|
2
5
|
export declare class GitHubBodyError extends Error {
|
|
3
|
-
|
|
6
|
+
readonly code?: string;
|
|
7
|
+
constructor(message: string, options?: {
|
|
8
|
+
code?: string;
|
|
9
|
+
});
|
|
4
10
|
}
|
|
5
11
|
export type RunGhApiFn = (args: readonly string[], options?: {
|
|
6
12
|
inputText?: string | null;
|
|
@@ -9,7 +15,22 @@ export type RunGhApiFn = (args: readonly string[], options?: {
|
|
|
9
15
|
/** Resolve the live GitHub CLI used for writes and mutation read-back. */
|
|
10
16
|
export declare function resolveLiveGh(): string;
|
|
11
17
|
export declare function readBody(bodyFile: string, stdinText?: string | null): string;
|
|
12
|
-
/**
|
|
18
|
+
/** Strip a leading UTF-8 BOM so body-file writers stay UTF-8 no BOM (#2960 / #798). */
|
|
19
|
+
export declare function stripUtf8Bom(text: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Scan body text with the same mojibake pattern set as verify:encoding
|
|
22
|
+
* (`packages/core/src/encoding/patterns.ts` / scanLine). Issue/PR bodies are
|
|
23
|
+
* Markdown: strip fenced blocks and inline code spans so documented examples
|
|
24
|
+
* of the corruption class (e.g. `#2960` fixtures) are not false positives —
|
|
25
|
+
* matches `scanFile` .md behavior.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scanBodyText(body: string, source?: string): Finding[];
|
|
28
|
+
/**
|
|
29
|
+
* Fail closed when body text already contains CP1252/CP437-as-UTF-8 mojibake
|
|
30
|
+
* or U+FFFD (before PATCH and as a live-body diagnostic).
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertBodyEncoding(body: string, phase: string): void;
|
|
33
|
+
/** Fail closed when live read-back body is flattened or mojibaked vs intended payload (#2607 / #2960). */
|
|
13
34
|
export declare function verifyBodyPostcondition(intended: string, live: string): void;
|
|
14
35
|
export declare function createIssue(repo: string, options: {
|
|
15
36
|
title: string;
|
|
@@ -41,10 +62,29 @@ export declare function fetchIssueBody(repo: string, issue: number, options?: {
|
|
|
41
62
|
binary?: string | null;
|
|
42
63
|
runFn?: RunGhApiFn;
|
|
43
64
|
}): string;
|
|
65
|
+
export declare function fetchPrBody(repo: string, pr: number, options?: {
|
|
66
|
+
binary?: string | null;
|
|
67
|
+
runFn?: RunGhApiFn;
|
|
68
|
+
}): string;
|
|
44
69
|
export declare function writeIssueBodyToFile(repo: string, issue: number, outFile: string, options?: {
|
|
45
70
|
binary?: string | null;
|
|
46
71
|
runFn?: RunGhApiFn;
|
|
47
72
|
}): string;
|
|
73
|
+
/**
|
|
74
|
+
* Lint a live issue body for mojibake. Returns findings (empty = clean).
|
|
75
|
+
* Does not throw on hits — callers decide exit code.
|
|
76
|
+
*/
|
|
77
|
+
export declare function lintIssueBody(repo: string, issue: number, options?: {
|
|
78
|
+
binary?: string | null;
|
|
79
|
+
runFn?: RunGhApiFn;
|
|
80
|
+
}): Finding[];
|
|
81
|
+
/**
|
|
82
|
+
* Lint a live PR body for mojibake. Returns findings (empty = clean).
|
|
83
|
+
*/
|
|
84
|
+
export declare function lintPrBody(repo: string, pr: number, options?: {
|
|
85
|
+
binary?: string | null;
|
|
86
|
+
runFn?: RunGhApiFn;
|
|
87
|
+
}): Finding[];
|
|
48
88
|
export interface GitHubBodyCliArgs {
|
|
49
89
|
command: string;
|
|
50
90
|
repo?: string;
|
|
@@ -55,5 +95,8 @@ export interface GitHubBodyCliArgs {
|
|
|
55
95
|
bodyFile?: string;
|
|
56
96
|
outFile?: string;
|
|
57
97
|
}
|
|
58
|
-
export declare function githubBodyMain(args: GitHubBodyCliArgs
|
|
98
|
+
export declare function githubBodyMain(args: GitHubBodyCliArgs, options?: {
|
|
99
|
+
runFn?: RunGhApiFn;
|
|
100
|
+
binary?: string | null;
|
|
101
|
+
}): number;
|
|
59
102
|
//# sourceMappingURL=github-body.d.ts.map
|