@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,11 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { renderFinding, scanLine } from "../encoding/scan.js";
|
|
4
|
+
import { pythonSplitlines, stripMarkdownQuotes } from "../encoding/text.js";
|
|
5
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
2
6
|
import { defaultWhich } from "../scm/binary.js";
|
|
3
7
|
import { call } from "../scm/call.js";
|
|
4
8
|
export const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
9
|
+
/** Stable error code for CP1252/CP437-as-UTF-8 / U+FFFD body corruption (#2960). */
|
|
10
|
+
export const SCM_BODY_ENCODING_CODE = "scm-body-encoding";
|
|
5
11
|
export class GitHubBodyError extends Error {
|
|
6
|
-
|
|
12
|
+
code;
|
|
13
|
+
constructor(message, options) {
|
|
7
14
|
super(message);
|
|
8
15
|
this.name = "GitHubBodyError";
|
|
16
|
+
this.code = options?.code;
|
|
9
17
|
}
|
|
10
18
|
}
|
|
11
19
|
function splitRepo(repo) {
|
|
@@ -24,9 +32,54 @@ export function resolveLiveGh() {
|
|
|
24
32
|
}
|
|
25
33
|
export function readBody(bodyFile, stdinText) {
|
|
26
34
|
if (bodyFile === "-") {
|
|
27
|
-
return stdinText ?? "";
|
|
35
|
+
return stripUtf8Bom(stdinText ?? "");
|
|
28
36
|
}
|
|
29
|
-
return readFileSync(bodyFile, "utf8");
|
|
37
|
+
return stripUtf8Bom(readFileSync(bodyFile, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
/** Strip a leading UTF-8 BOM so body-file writers stay UTF-8 no BOM (#2960 / #798). */
|
|
40
|
+
export function stripUtf8Bom(text) {
|
|
41
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Scan body text with the same mojibake pattern set as verify:encoding
|
|
45
|
+
* (`packages/core/src/encoding/patterns.ts` / scanLine). Issue/PR bodies are
|
|
46
|
+
* Markdown: strip fenced blocks and inline code spans so documented examples
|
|
47
|
+
* of the corruption class (e.g. `#2960` fixtures) are not false positives —
|
|
48
|
+
* matches `scanFile` .md behavior.
|
|
49
|
+
*/
|
|
50
|
+
export function scanBodyText(body, source = "body") {
|
|
51
|
+
const findings = [];
|
|
52
|
+
const scanText = stripMarkdownQuotes(body);
|
|
53
|
+
const originalLines = pythonSplitlines(body);
|
|
54
|
+
const strippedLines = pythonSplitlines(scanText);
|
|
55
|
+
if (originalLines.length === 0 && body.length > 0) {
|
|
56
|
+
findings.push(...scanLine(source, 1, scanText, body));
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
// Preserve line alignment when strip blanks fenced blocks (newline-for-newline).
|
|
60
|
+
while (strippedLines.length < originalLines.length) {
|
|
61
|
+
strippedLines.push("");
|
|
62
|
+
}
|
|
63
|
+
originalLines.forEach((orig, idx) => {
|
|
64
|
+
const stripped = strippedLines[idx] ?? "";
|
|
65
|
+
findings.push(...scanLine(source, idx + 1, stripped, orig));
|
|
66
|
+
});
|
|
67
|
+
return findings;
|
|
68
|
+
}
|
|
69
|
+
function formatEncodingFindings(findings) {
|
|
70
|
+
return findings.map((f) => renderFinding(f).trim()).join("; ");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fail closed when body text already contains CP1252/CP437-as-UTF-8 mojibake
|
|
74
|
+
* or U+FFFD (before PATCH and as a live-body diagnostic).
|
|
75
|
+
*/
|
|
76
|
+
export function assertBodyEncoding(body, phase) {
|
|
77
|
+
const findings = scanBodyText(body, phase);
|
|
78
|
+
if (findings.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const detail = formatEncodingFindings(findings);
|
|
82
|
+
throw new GitHubBodyError(`body encoding failed (${phase}): ${detail}. Repair: task scm:body:issue:fetch -- --repo OWNER/REPO --issue N --out-file <path>, fix as UTF-8 (no BOM), task scm:body:issue:edit -- --body-file <path> (#2960 / #2948 / #2944).`, { code: SCM_BODY_ENCODING_CODE });
|
|
30
83
|
}
|
|
31
84
|
function jsonInput(payload) {
|
|
32
85
|
return JSON.stringify(payload);
|
|
@@ -73,10 +126,14 @@ function requireIntField(obj, field) {
|
|
|
73
126
|
}
|
|
74
127
|
return value;
|
|
75
128
|
}
|
|
76
|
-
|
|
77
|
-
|
|
129
|
+
/** Body may be null on empty GitHub issues/PRs — treat as empty string for lint/fetch. */
|
|
130
|
+
function bodyFieldOrEmpty(obj) {
|
|
131
|
+
const value = obj.body;
|
|
132
|
+
if (value === null || value === undefined) {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
78
135
|
if (typeof value !== "string") {
|
|
79
|
-
throw new GitHubBodyError(`response did not include string field
|
|
136
|
+
throw new GitHubBodyError(`response did not include string field "body"`);
|
|
80
137
|
}
|
|
81
138
|
return value;
|
|
82
139
|
}
|
|
@@ -91,7 +148,7 @@ function countNewlines(text) {
|
|
|
91
148
|
function normalizeNewlines(text) {
|
|
92
149
|
return text.replace(/\r\n/g, "\n");
|
|
93
150
|
}
|
|
94
|
-
/** Fail closed when live read-back body is flattened or mojibaked vs intended payload (#2607). */
|
|
151
|
+
/** Fail closed when live read-back body is flattened or mojibaked vs intended payload (#2607 / #2960). */
|
|
95
152
|
export function verifyBodyPostcondition(intended, live) {
|
|
96
153
|
if (intended === live) {
|
|
97
154
|
return;
|
|
@@ -103,6 +160,11 @@ export function verifyBodyPostcondition(intended, live) {
|
|
|
103
160
|
if (live.includes("\uFFFD") && !intended.includes("\uFFFD")) {
|
|
104
161
|
diagnoses.push("live body contains U+FFFD replacement character not present in intended payload");
|
|
105
162
|
}
|
|
163
|
+
const liveFindings = scanBodyText(live, "live");
|
|
164
|
+
const intendedFindings = scanBodyText(intended, "intended");
|
|
165
|
+
if (liveFindings.length > 0 && intendedFindings.length === 0) {
|
|
166
|
+
diagnoses.push(`live body contains mojibake not present in intended payload (${formatEncodingFindings(liveFindings)})`);
|
|
167
|
+
}
|
|
106
168
|
const intendedNl = countNewlines(intended);
|
|
107
169
|
const liveNl = countNewlines(live);
|
|
108
170
|
if (intendedNl > 0 && liveNl < intendedNl) {
|
|
@@ -112,12 +174,19 @@ export function verifyBodyPostcondition(intended, live) {
|
|
|
112
174
|
diagnoses.push("live body looks like intended newlines were collapsed to spaces (PowerShell string[] join)");
|
|
113
175
|
}
|
|
114
176
|
if (diagnoses.length > 0) {
|
|
115
|
-
|
|
177
|
+
const isEncoding = diagnoses.some((d) => d.includes("U+FFFD") || d.includes("mojibake") || d.includes("encoding"));
|
|
178
|
+
throw new GitHubBodyError(`body postcondition failed: ${diagnoses.join("; ")}`, {
|
|
179
|
+
code: isEncoding ? SCM_BODY_ENCODING_CODE : undefined,
|
|
180
|
+
});
|
|
116
181
|
}
|
|
117
182
|
throw new GitHubBodyError(`body postcondition failed: re-fetched body does not match intended payload (length intended=${intended.length}, live=${live.length})`);
|
|
118
183
|
}
|
|
119
184
|
function mutateWithReadback(mutationEndpoint, method, payload, readbackEndpoint, options = {}) {
|
|
120
185
|
const intendedBody = typeof payload.body === "string" ? payload.body : undefined;
|
|
186
|
+
if (intendedBody !== undefined) {
|
|
187
|
+
// Fail closed before PATCH when the intended payload is already corrupted (#2960).
|
|
188
|
+
assertBodyEncoding(intendedBody, "pre-write");
|
|
189
|
+
}
|
|
121
190
|
const mutation = runGhApiJson([mutationEndpoint, "--method", method, "--input", "-"], {
|
|
122
191
|
inputText: jsonInput(payload),
|
|
123
192
|
binary: options.binary,
|
|
@@ -126,7 +195,10 @@ function mutateWithReadback(mutationEndpoint, method, payload, readbackEndpoint,
|
|
|
126
195
|
const endpoint = typeof readbackEndpoint === "function" ? readbackEndpoint(mutation) : readbackEndpoint;
|
|
127
196
|
const readback = runGhApiJson([endpoint], { binary: options.binary, runFn: options.runFn });
|
|
128
197
|
if (intendedBody !== undefined) {
|
|
129
|
-
|
|
198
|
+
const liveBody = bodyFieldOrEmpty(readback);
|
|
199
|
+
verifyBodyPostcondition(intendedBody, liveBody);
|
|
200
|
+
// Defense in depth: live body must also be free of encoding corruption.
|
|
201
|
+
assertBodyEncoding(liveBody, "post-write-readback");
|
|
130
202
|
}
|
|
131
203
|
return readback;
|
|
132
204
|
}
|
|
@@ -168,14 +240,52 @@ export function fetchIssueBody(repo, issue, options = {}) {
|
|
|
168
240
|
const [owner, name] = splitRepo(repo);
|
|
169
241
|
const endpoint = `repos/${owner}/${name}/issues/${issue}`;
|
|
170
242
|
const response = runGhApiJson([endpoint], { binary: options.binary, runFn: options.runFn });
|
|
171
|
-
return
|
|
243
|
+
return bodyFieldOrEmpty(response);
|
|
244
|
+
}
|
|
245
|
+
export function fetchPrBody(repo, pr, options = {}) {
|
|
246
|
+
const [owner, name] = splitRepo(repo);
|
|
247
|
+
const endpoint = `repos/${owner}/${name}/pulls/${pr}`;
|
|
248
|
+
const response = runGhApiJson([endpoint], { binary: options.binary, runFn: options.runFn });
|
|
249
|
+
return bodyFieldOrEmpty(response);
|
|
172
250
|
}
|
|
173
251
|
export function writeIssueBodyToFile(repo, issue, outFile, options = {}) {
|
|
174
252
|
const body = fetchIssueBody(repo, issue, options);
|
|
175
|
-
|
|
253
|
+
// UTF-8 no BOM (Node default for encoding: "utf8").
|
|
254
|
+
// #2980 wave B: product write sink routes through containedWrite under out-file parent.
|
|
255
|
+
const abs = resolve(outFile);
|
|
256
|
+
const parent = dirname(abs);
|
|
257
|
+
mkdirSync(parent, { recursive: true });
|
|
258
|
+
containedWrite({
|
|
259
|
+
root: parent,
|
|
260
|
+
target: abs,
|
|
261
|
+
data: body,
|
|
262
|
+
mode: "replace",
|
|
263
|
+
});
|
|
176
264
|
return body;
|
|
177
265
|
}
|
|
178
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Lint a live issue body for mojibake. Returns findings (empty = clean).
|
|
268
|
+
* Does not throw on hits — callers decide exit code.
|
|
269
|
+
*/
|
|
270
|
+
export function lintIssueBody(repo, issue, options = {}) {
|
|
271
|
+
const body = fetchIssueBody(repo, issue, options);
|
|
272
|
+
return scanBodyText(body, `issue/${issue}`);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Lint a live PR body for mojibake. Returns findings (empty = clean).
|
|
276
|
+
*/
|
|
277
|
+
export function lintPrBody(repo, pr, options = {}) {
|
|
278
|
+
const body = fetchPrBody(repo, pr, options);
|
|
279
|
+
return scanBodyText(body, `pr/${pr}`);
|
|
280
|
+
}
|
|
281
|
+
function writeLintFailure(kind, id, findings) {
|
|
282
|
+
process.stderr.write(`error [${SCM_BODY_ENCODING_CODE}]: ${kind} #${id} body contains encoding mojibake (${findings.length} hit(s)):\n`);
|
|
283
|
+
for (const f of findings) {
|
|
284
|
+
process.stderr.write(`${renderFinding(f)}\n`);
|
|
285
|
+
}
|
|
286
|
+
process.stderr.write(`Repair: task scm:body:issue:fetch -- --repo OWNER/REPO --issue ${id} --out-file <path>, fix as UTF-8 (no BOM), task scm:body:issue:edit -- --body-file <path>. After non-scm:body mutations, re-run lint (#2960; recurrence #2948/#2944).\n`);
|
|
287
|
+
}
|
|
288
|
+
export function githubBodyMain(args, options = {}) {
|
|
179
289
|
try {
|
|
180
290
|
switch (args.command) {
|
|
181
291
|
case "issue-fetch": {
|
|
@@ -183,7 +293,42 @@ export function githubBodyMain(args) {
|
|
|
183
293
|
process.stderr.write("error: issue-fetch requires --repo, --issue, and --out-file\n");
|
|
184
294
|
return 1;
|
|
185
295
|
}
|
|
186
|
-
writeIssueBodyToFile(args.repo, args.issue, args.outFile
|
|
296
|
+
writeIssueBodyToFile(args.repo, args.issue, args.outFile, {
|
|
297
|
+
runFn: options.runFn,
|
|
298
|
+
binary: options.binary,
|
|
299
|
+
});
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
case "issue-lint": {
|
|
303
|
+
if (args.repo === undefined || args.issue === undefined) {
|
|
304
|
+
process.stderr.write("error: issue-lint requires --repo and --issue\n");
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
const findings = lintIssueBody(args.repo, args.issue, {
|
|
308
|
+
runFn: options.runFn,
|
|
309
|
+
binary: options.binary,
|
|
310
|
+
});
|
|
311
|
+
if (findings.length > 0) {
|
|
312
|
+
writeLintFailure("issue", args.issue, findings);
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
process.stdout.write(`ok: issue #${args.issue} body has no CP1252/CP437-as-UTF-8 mojibake\n`);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
case "pr-lint": {
|
|
319
|
+
if (args.repo === undefined || args.pr === undefined) {
|
|
320
|
+
process.stderr.write("error: pr-lint requires --repo and --pr\n");
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
const findings = lintPrBody(args.repo, args.pr, {
|
|
324
|
+
runFn: options.runFn,
|
|
325
|
+
binary: options.binary,
|
|
326
|
+
});
|
|
327
|
+
if (findings.length > 0) {
|
|
328
|
+
writeLintFailure("pr", args.pr, findings);
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
331
|
+
process.stdout.write(`ok: pr #${args.pr} body has no CP1252/CP437-as-UTF-8 mojibake\n`);
|
|
187
332
|
return 0;
|
|
188
333
|
}
|
|
189
334
|
default: {
|
|
@@ -191,19 +336,40 @@ export function githubBodyMain(args) {
|
|
|
191
336
|
let result;
|
|
192
337
|
switch (args.command) {
|
|
193
338
|
case "issue-create":
|
|
194
|
-
result = createIssue(args.repo, {
|
|
339
|
+
result = createIssue(args.repo, {
|
|
340
|
+
title: args.title,
|
|
341
|
+
body,
|
|
342
|
+
runFn: options.runFn,
|
|
343
|
+
binary: options.binary,
|
|
344
|
+
});
|
|
195
345
|
break;
|
|
196
346
|
case "issue-edit":
|
|
197
|
-
result = editIssueBody(args.repo, args.issue, {
|
|
347
|
+
result = editIssueBody(args.repo, args.issue, {
|
|
348
|
+
body,
|
|
349
|
+
runFn: options.runFn,
|
|
350
|
+
binary: options.binary,
|
|
351
|
+
});
|
|
198
352
|
break;
|
|
199
353
|
case "comment-create":
|
|
200
|
-
result = createIssueComment(args.repo, args.issue, {
|
|
354
|
+
result = createIssueComment(args.repo, args.issue, {
|
|
355
|
+
body,
|
|
356
|
+
runFn: options.runFn,
|
|
357
|
+
binary: options.binary,
|
|
358
|
+
});
|
|
201
359
|
break;
|
|
202
360
|
case "comment-edit":
|
|
203
|
-
result = editIssueCommentBody(args.repo, args.comment, {
|
|
361
|
+
result = editIssueCommentBody(args.repo, args.comment, {
|
|
362
|
+
body,
|
|
363
|
+
runFn: options.runFn,
|
|
364
|
+
binary: options.binary,
|
|
365
|
+
});
|
|
204
366
|
break;
|
|
205
367
|
case "pr-edit":
|
|
206
|
-
result = editPrBody(args.repo, args.pr, {
|
|
368
|
+
result = editPrBody(args.repo, args.pr, {
|
|
369
|
+
body,
|
|
370
|
+
runFn: options.runFn,
|
|
371
|
+
binary: options.binary,
|
|
372
|
+
});
|
|
207
373
|
break;
|
|
208
374
|
default:
|
|
209
375
|
process.stderr.write(`error: unknown command ${JSON.stringify(args.command)}\n`);
|
|
@@ -215,7 +381,12 @@ export function githubBodyMain(args) {
|
|
|
215
381
|
}
|
|
216
382
|
}
|
|
217
383
|
catch (exc) {
|
|
218
|
-
|
|
384
|
+
if (exc instanceof GitHubBodyError && exc.code !== undefined) {
|
|
385
|
+
process.stderr.write(`error [${exc.code}]: ${exc.message}\n`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
process.stderr.write(`error: ${String(exc)}\n`);
|
|
389
|
+
}
|
|
219
390
|
return 1;
|
|
220
391
|
}
|
|
221
392
|
}
|
|
@@ -44,6 +44,7 @@ export declare function assertVbriefWriteTargetSafe(path: string, projectRoot?:
|
|
|
44
44
|
/**
|
|
45
45
|
* Persist an xBRIEF/vBRIEF JSON document. Gates the write target so a leaf or
|
|
46
46
|
* parent-directory symlink cannot divert the stamped file outside the project (#2869).
|
|
47
|
+
* #2980 wave B: product write sink routes through containedWrite.
|
|
47
48
|
*/
|
|
48
49
|
export declare function writeVbrief(path: string, data: Record<string, unknown>, projectRoot?: string | null): void;
|
|
49
50
|
export declare function vbriefTitle(data: Record<string, unknown>): string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmodSync,
|
|
2
|
+
import { chmodSync, globSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
6
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
6
7
|
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
7
8
|
import { call } from "../scm/call.js";
|
|
8
9
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
@@ -83,7 +84,8 @@ export function recoverySidecarPath(vbriefAbsPath) {
|
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
86
|
* Write recovery sidecar without following symlinks (#2880 Greptile P1).
|
|
86
|
-
* Trusted per-uid dir (ownership + mode) + O_EXCL|O_NOFOLLOW
|
|
87
|
+
* Trusted per-uid dir (ownership + mode) + containedWrite create (O_EXCL|O_NOFOLLOW).
|
|
88
|
+
* #2980 wave B: product write sink routes through containedWrite.
|
|
87
89
|
*/
|
|
88
90
|
function writeRecoverySidecarSafe(sidePath, payload) {
|
|
89
91
|
const dir = ensureTrustedRecoveryDir();
|
|
@@ -107,17 +109,12 @@ function writeRecoverySidecarSafe(sidePath, payload) {
|
|
|
107
109
|
throw err;
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
writeSync(fd, payload, undefined, "utf8");
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
closeSync(fd);
|
|
120
|
-
}
|
|
112
|
+
containedWrite({
|
|
113
|
+
root: dir,
|
|
114
|
+
target: sidePath,
|
|
115
|
+
data: payload,
|
|
116
|
+
mode: "create",
|
|
117
|
+
});
|
|
121
118
|
}
|
|
122
119
|
/**
|
|
123
120
|
* Always-on recovery after remote create: process memory + private OS-temp sidecar.
|
|
@@ -250,15 +247,20 @@ export function loadPendingEmitUrls(projectRoot) {
|
|
|
250
247
|
return {};
|
|
251
248
|
}
|
|
252
249
|
export function savePendingEmitUrl(projectRoot, vbriefAbsPath, url) {
|
|
250
|
+
const root = resolve(projectRoot);
|
|
253
251
|
const ledger = pendingEmitLedgerPath(projectRoot);
|
|
254
|
-
assertWriteTargetSafe(projectRoot, ledger);
|
|
255
|
-
mkdirSync(dirname(ledger), { recursive: true });
|
|
256
252
|
const map = loadPendingEmitUrls(projectRoot);
|
|
257
253
|
map[resolve(vbriefAbsPath)] = url;
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
// #2980 wave B: product write sink routes through containedWrite.
|
|
255
|
+
containedWrite({
|
|
256
|
+
root,
|
|
257
|
+
target: ledger,
|
|
258
|
+
data: `${JSON.stringify(map, null, 2)}\n`,
|
|
259
|
+
mode: "replace",
|
|
260
|
+
});
|
|
260
261
|
}
|
|
261
262
|
export function clearPendingEmitUrl(projectRoot, vbriefAbsPath) {
|
|
263
|
+
const root = resolve(projectRoot);
|
|
262
264
|
const ledger = pendingEmitLedgerPath(projectRoot);
|
|
263
265
|
const key = resolve(vbriefAbsPath);
|
|
264
266
|
const map = loadPendingEmitUrls(projectRoot);
|
|
@@ -267,9 +269,13 @@ export function clearPendingEmitUrl(projectRoot, vbriefAbsPath) {
|
|
|
267
269
|
}
|
|
268
270
|
delete map[key];
|
|
269
271
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
// #2980 wave B: product write sink routes through containedWrite.
|
|
273
|
+
containedWrite({
|
|
274
|
+
root,
|
|
275
|
+
target: ledger,
|
|
276
|
+
data: `${JSON.stringify(map, null, 2)}\n`,
|
|
277
|
+
mode: "replace",
|
|
278
|
+
});
|
|
273
279
|
}
|
|
274
280
|
catch {
|
|
275
281
|
// Best-effort clear; next successful stamp still works via existingGithubIssueRef.
|
|
@@ -310,10 +316,16 @@ export function assertVbriefWriteTargetSafe(path, projectRoot) {
|
|
|
310
316
|
/**
|
|
311
317
|
* Persist an xBRIEF/vBRIEF JSON document. Gates the write target so a leaf or
|
|
312
318
|
* parent-directory symlink cannot divert the stamped file outside the project (#2869).
|
|
319
|
+
* #2980 wave B: product write sink routes through containedWrite.
|
|
313
320
|
*/
|
|
314
321
|
export function writeVbrief(path, data, projectRoot) {
|
|
315
|
-
assertVbriefWriteTargetSafe(path, projectRoot);
|
|
316
|
-
|
|
322
|
+
const root = assertVbriefWriteTargetSafe(path, projectRoot);
|
|
323
|
+
containedWrite({
|
|
324
|
+
root,
|
|
325
|
+
target: resolve(path),
|
|
326
|
+
data: `${JSON.stringify(data, null, 2)}\n`,
|
|
327
|
+
mode: "replace",
|
|
328
|
+
});
|
|
317
329
|
}
|
|
318
330
|
export function vbriefTitle(data) {
|
|
319
331
|
const plan = (data.plan ?? {});
|
|
@@ -417,7 +429,13 @@ export function fileIssue(repo, title, body, scmCall = call) {
|
|
|
417
429
|
const tmpDir = mkdtempSync(join(tmpdir(), "deft-issue-emit-"));
|
|
418
430
|
const bodyPath = join(tmpDir, "body.md");
|
|
419
431
|
try {
|
|
420
|
-
|
|
432
|
+
// #2980 wave B: temp body file via containedWrite under the OS-temp dir.
|
|
433
|
+
containedWrite({
|
|
434
|
+
root: tmpDir,
|
|
435
|
+
target: "body.md",
|
|
436
|
+
data: body,
|
|
437
|
+
mode: "create",
|
|
438
|
+
});
|
|
421
439
|
const result = scmCall("github-issue", "issue", ["create", "--repo", repo, "--title", title, "--body-file", bodyPath], { timeout: 60 });
|
|
422
440
|
if (result.returncode !== 0) {
|
|
423
441
|
throw new IssueEmitError(`gh issue create failed (exit ${result.returncode}): ${(result.stderr ?? "").trim()}`);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { contentRoot } from "../content-root.js";
|
|
5
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
5
6
|
import { findSkillPathsInText } from "../text/redos-safe.js";
|
|
6
7
|
/** Sentinel used by SKILL.md redirect stubs. */
|
|
7
8
|
export const DEPRECATED_SKILL_REDIRECT_SENTINEL = "<!-- deft:deprecated-skill-redirect -->";
|
|
@@ -87,9 +88,16 @@ export function emit(name, payload = null, options = {}) {
|
|
|
87
88
|
const logTarget = process.env[logPathEnv];
|
|
88
89
|
if (logTarget !== undefined && logTarget.length > 0) {
|
|
89
90
|
try {
|
|
91
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
90
92
|
const logPath = resolve(logTarget);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const dir = dirname(logPath);
|
|
94
|
+
mkdirSync(dir, { recursive: true });
|
|
95
|
+
containedWrite({
|
|
96
|
+
root: dir,
|
|
97
|
+
target: basename(logPath),
|
|
98
|
+
data: `${JSON.stringify(record)}\n`,
|
|
99
|
+
mode: "append",
|
|
100
|
+
});
|
|
93
101
|
}
|
|
94
102
|
catch {
|
|
95
103
|
// Swallow log-write failures.
|
package/dist/lifecycle/events.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { contentRoot } from "../content-root.js";
|
|
6
6
|
import { ATTRIBUTION_REQUIRED_PAYLOAD } from "../events/attribution-constants.js";
|
|
7
|
-
import {
|
|
7
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
8
|
+
import { ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
8
9
|
/** Default event log location (project-local). */
|
|
9
10
|
export const DEFAULT_EVENT_LOG = join(".deft-cache", "events.jsonl");
|
|
10
11
|
const BEHAVIORAL_CATEGORY = "behavioral";
|
|
@@ -166,22 +167,32 @@ function assertPathComponentsNotSymlinks(projectDir, targetPath) {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Append one event log line via containedWrite (#2951 Phase 2).
|
|
172
|
+
* Project-nested logs use projectRoot as containment root; explicit external
|
|
173
|
+
* logs still refuse symlink path components, then contain under the log parent.
|
|
174
|
+
*/
|
|
175
|
+
function appendEventLogLine(projectRoot, targetPath, line) {
|
|
176
|
+
const targetAbs = resolve(targetPath);
|
|
177
|
+
if (isNestedUnder(projectRoot, targetAbs)) {
|
|
178
|
+
containedWrite({
|
|
179
|
+
root: resolve(projectRoot),
|
|
180
|
+
target: targetAbs,
|
|
181
|
+
data: line,
|
|
182
|
+
mode: "append",
|
|
183
|
+
});
|
|
173
184
|
return;
|
|
174
185
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
186
|
+
// Explicit --log / DEFT_EVENT_LOG outside the project tree.
|
|
187
|
+
assertPathComponentsNotSymlinks(projectRoot, targetAbs);
|
|
188
|
+
const parent = dirname(targetAbs);
|
|
189
|
+
mkdirSync(parent, { recursive: true });
|
|
190
|
+
containedWrite({
|
|
191
|
+
root: resolve(parent),
|
|
192
|
+
target: basename(targetAbs),
|
|
193
|
+
data: line,
|
|
194
|
+
mode: "append",
|
|
195
|
+
});
|
|
185
196
|
}
|
|
186
197
|
function newEventId() {
|
|
187
198
|
const wallNs = BigInt(Date.now()) * 1000000n;
|
|
@@ -210,9 +221,7 @@ export function emit(name, payload, options = {}) {
|
|
|
210
221
|
};
|
|
211
222
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
212
223
|
const target = resolveLogPath(options.logPath, projectRoot);
|
|
213
|
-
|
|
214
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
215
|
-
appendLineNoFollow(target, `${jsonStringifySorted(record)}\n`);
|
|
224
|
+
appendEventLogLine(projectRoot, target, `${jsonStringifySorted(record)}\n`);
|
|
216
225
|
return record;
|
|
217
226
|
}
|
|
218
227
|
/** Return all events from the log in emission order. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
4
4
|
import { computeReport } from "../capacity/show.js";
|
|
5
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
5
6
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
6
7
|
import { resolveCapacityAllocation } from "../policy/capacity.js";
|
|
7
8
|
import { readPlanPolicy } from "../policy/plan-extensions.js";
|
|
@@ -202,14 +203,19 @@ export function recordTechDebtAcceptance(projectRoot, epic, options) {
|
|
|
202
203
|
throw new Error("follow_up_ref must be a non-empty reference string");
|
|
203
204
|
}
|
|
204
205
|
const path = techDebtLedgerPath(projectRoot);
|
|
205
|
-
mkdirSync(resolve(path, ".."), { recursive: true });
|
|
206
206
|
const record = {
|
|
207
207
|
accepted_at: utcIso(options.now ?? null),
|
|
208
208
|
actor: options.actor ?? DEFAULT_ACTOR,
|
|
209
209
|
epic: epicKey,
|
|
210
210
|
follow_up_ref: followUpRef.trim(),
|
|
211
211
|
};
|
|
212
|
-
|
|
212
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
213
|
+
containedWrite({
|
|
214
|
+
root: resolve(projectRoot),
|
|
215
|
+
target: path,
|
|
216
|
+
data: `${JSON.stringify(sortKeys(record))}\n`,
|
|
217
|
+
mode: "append",
|
|
218
|
+
});
|
|
213
219
|
return path;
|
|
214
220
|
}
|
|
215
221
|
/** Return the set of epic basenames already accepted as tech-debt. */
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/** Port of scripts/pack_render.py — content-pack projection renderer (#1294, #1295). */
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { contentRoot } from "../content-root.js";
|
|
6
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
7
|
+
/** Write under the parent directory as containment root (#2980 wave D). */
|
|
8
|
+
function writeContainedFile(output, text) {
|
|
9
|
+
const abs = resolve(output);
|
|
10
|
+
const dir = dirname(abs);
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
containedWrite({
|
|
13
|
+
root: dir,
|
|
14
|
+
target: basename(abs),
|
|
15
|
+
data: text,
|
|
16
|
+
mode: "replace",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
6
19
|
export function resolveRepoRoot() {
|
|
7
20
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
8
21
|
}
|
|
@@ -382,8 +395,8 @@ export function renderFile(source) {
|
|
|
382
395
|
}
|
|
383
396
|
export function writeRender(source, output) {
|
|
384
397
|
const text = renderFile(source);
|
|
385
|
-
|
|
386
|
-
|
|
398
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
399
|
+
writeContainedFile(output, text);
|
|
387
400
|
return text;
|
|
388
401
|
}
|
|
389
402
|
export function checkDrift(source, output) {
|
|
@@ -441,9 +454,9 @@ function runMultipack(packFilter, check) {
|
|
|
441
454
|
process.stdout.write(`pack-drift: all ${targets.length} projection(s) in sync.\n`);
|
|
442
455
|
return 0;
|
|
443
456
|
}
|
|
457
|
+
// #2980 wave D: product write sinks route through containedWrite.
|
|
444
458
|
for (const [_name, outPath, text] of targets) {
|
|
445
|
-
|
|
446
|
-
writeFileSync(outPath, text, "utf8");
|
|
459
|
+
writeContainedFile(outPath, text);
|
|
447
460
|
}
|
|
448
461
|
process.stdout.write(`Rendered ${targets.length} projection(s) across ${Object.keys(RENDER_REGISTRY).length} pack(s).\n`);
|
|
449
462
|
return 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
3
4
|
import { PLAN_SEQUENCE_FILENAME, parsePlanSequence } from "./types.js";
|
|
4
5
|
export function planSequencePath(projectRoot) {
|
|
5
6
|
return join(projectRoot, ".deft", PLAN_SEQUENCE_FILENAME);
|
|
@@ -14,8 +15,14 @@ export function readPlanSequence(projectRoot) {
|
|
|
14
15
|
}
|
|
15
16
|
export function writePlanSequence(projectRoot, sequence) {
|
|
16
17
|
const path = planSequencePath(projectRoot);
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
19
|
+
const root = resolve(projectRoot);
|
|
20
|
+
containedWrite({
|
|
21
|
+
root,
|
|
22
|
+
target: path,
|
|
23
|
+
data: `${JSON.stringify(sequence, null, 2)}\n`,
|
|
24
|
+
mode: "replace",
|
|
25
|
+
});
|
|
19
26
|
return path;
|
|
20
27
|
}
|
|
21
28
|
export function clearPlanSequence(projectRoot) {
|