@deftai/directive-core 0.88.0 → 0.90.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/constants.d.ts +1 -1
- package/dist/doctor/constants.js +2 -0
- package/dist/doctor/doctor-state.js +28 -4
- package/dist/doctor/flags.js +21 -1
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +182 -0
- package/dist/doctor/openclaw-skills.d.ts +109 -0
- package/dist/doctor/openclaw-skills.js +463 -0
- package/dist/doctor/types.d.ts +22 -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 +232 -168
- 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 +5 -0
- package/dist/index.js +6 -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 +31 -20
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle/lifecycle-hygiene.js +9 -3
- package/dist/lifecycle/stats.d.ts +53 -0
- package/dist/lifecycle/stats.js +286 -0
- 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/spawn.js +13 -0
- package/dist/release-e2e/git-ops.d.ts +7 -0
- package/dist/release-e2e/git-ops.js +35 -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/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/process-cost-constants.d.ts +9 -0
- package/dist/session/process-cost-constants.js +11 -0
- package/dist/session/process-cost.d.ts +53 -0
- package/dist/session/process-cost.js +82 -0
- package/dist/session/release-availability.js +26 -6
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +31 -13
- package/dist/session/session-ready.d.ts +67 -0
- package/dist/session/session-ready.js +264 -0
- package/dist/session/session-start.d.ts +56 -1
- package/dist/session/session-start.js +384 -23
- package/dist/session/verify-session-ritual.d.ts +8 -0
- package/dist/session/verify-session-ritual.js +95 -35
- 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/tool-events/classify.d.ts +20 -0
- package/dist/tool-events/classify.js +634 -0
- package/dist/tool-events/index.d.ts +10 -0
- package/dist/tool-events/index.js +10 -0
- package/dist/tool-events/summarize.d.ts +34 -0
- package/dist/tool-events/summarize.js +93 -0
- package/dist/tool-events/types.d.ts +52 -0
- package/dist/tool-events/types.js +13 -0
- 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 +19 -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,15 +1,18 @@
|
|
|
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";
|
|
9
|
+
import { PROCESS_COST_REQUIRED_PAYLOAD } from "../session/process-cost-constants.js";
|
|
8
10
|
/** Default event log location (project-local). */
|
|
9
11
|
export const DEFAULT_EVENT_LOG = join(".deft-cache", "events.jsonl");
|
|
10
12
|
const BEHAVIORAL_CATEGORY = "behavioral";
|
|
11
13
|
const REQUIRED_BEHAVIORAL_PAYLOAD = {
|
|
12
14
|
...ATTRIBUTION_REQUIRED_PAYLOAD,
|
|
15
|
+
...PROCESS_COST_REQUIRED_PAYLOAD,
|
|
13
16
|
"session:interrupted": ["session_id", "reason"],
|
|
14
17
|
"session:resumed": ["session_id", "interrupted_id"],
|
|
15
18
|
"plan:approved": ["plan_ref", "approver"],
|
|
@@ -166,22 +169,32 @@ function assertPathComponentsNotSymlinks(projectDir, targetPath) {
|
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Append one event log line via containedWrite (#2951 Phase 2).
|
|
174
|
+
* Project-nested logs use projectRoot as containment root; explicit external
|
|
175
|
+
* logs still refuse symlink path components, then contain under the log parent.
|
|
176
|
+
*/
|
|
177
|
+
function appendEventLogLine(projectRoot, targetPath, line) {
|
|
178
|
+
const targetAbs = resolve(targetPath);
|
|
179
|
+
if (isNestedUnder(projectRoot, targetAbs)) {
|
|
180
|
+
containedWrite({
|
|
181
|
+
root: resolve(projectRoot),
|
|
182
|
+
target: targetAbs,
|
|
183
|
+
data: line,
|
|
184
|
+
mode: "append",
|
|
185
|
+
});
|
|
173
186
|
return;
|
|
174
187
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
188
|
+
// Explicit --log / DEFT_EVENT_LOG outside the project tree.
|
|
189
|
+
assertPathComponentsNotSymlinks(projectRoot, targetAbs);
|
|
190
|
+
const parent = dirname(targetAbs);
|
|
191
|
+
mkdirSync(parent, { recursive: true });
|
|
192
|
+
containedWrite({
|
|
193
|
+
root: resolve(parent),
|
|
194
|
+
target: basename(targetAbs),
|
|
195
|
+
data: line,
|
|
196
|
+
mode: "append",
|
|
197
|
+
});
|
|
185
198
|
}
|
|
186
199
|
function newEventId() {
|
|
187
200
|
const wallNs = BigInt(Date.now()) * 1000000n;
|
|
@@ -210,9 +223,7 @@ export function emit(name, payload, options = {}) {
|
|
|
210
223
|
};
|
|
211
224
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
212
225
|
const target = resolveLogPath(options.logPath, projectRoot);
|
|
213
|
-
|
|
214
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
215
|
-
appendLineNoFollow(target, `${jsonStringifySorted(record)}\n`);
|
|
226
|
+
appendEventLogLine(projectRoot, target, `${jsonStringifySorted(record)}\n`);
|
|
216
227
|
return record;
|
|
217
228
|
}
|
|
218
229
|
/** Return all events from the log in emission order. */
|
package/dist/lifecycle/index.js
CHANGED
|
@@ -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. */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseDurationMs } from "../triage/scope/duration.js";
|
|
2
|
+
/** Lifecycle folders scanned for stats. */
|
|
3
|
+
export declare const STATS_LIFECYCLE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
|
|
4
|
+
export type StatsLifecycleFolder = (typeof STATS_LIFECYCLE_FOLDERS)[number];
|
|
5
|
+
/** Count semantics for WWYSYDH / process forms (stable strings for --json). */
|
|
6
|
+
export declare const LIFECYCLE_STATS_SEMANTICS: {
|
|
7
|
+
readonly promoted: string;
|
|
8
|
+
readonly activated: string;
|
|
9
|
+
readonly completed: string;
|
|
10
|
+
readonly cancelled_or_failed: string;
|
|
11
|
+
readonly still_active: "snapshot count of all xbriefs currently in active/ (not filtered by --since).";
|
|
12
|
+
readonly event_time: string;
|
|
13
|
+
readonly note: string;
|
|
14
|
+
};
|
|
15
|
+
export interface LifecycleFolderTotals {
|
|
16
|
+
readonly proposed: number;
|
|
17
|
+
readonly pending: number;
|
|
18
|
+
readonly active: number;
|
|
19
|
+
readonly completed: number;
|
|
20
|
+
readonly cancelled: number;
|
|
21
|
+
}
|
|
22
|
+
export interface LifecycleStats {
|
|
23
|
+
readonly since: string;
|
|
24
|
+
readonly since_ms: number;
|
|
25
|
+
readonly as_of: string;
|
|
26
|
+
readonly window_start: string;
|
|
27
|
+
readonly project_root: string;
|
|
28
|
+
readonly lifecycle_root: string;
|
|
29
|
+
readonly promoted: number;
|
|
30
|
+
readonly activated: number;
|
|
31
|
+
readonly completed: number;
|
|
32
|
+
readonly cancelled_or_failed: number;
|
|
33
|
+
readonly still_active: number;
|
|
34
|
+
readonly folder_totals: LifecycleFolderTotals;
|
|
35
|
+
readonly semantics: typeof LIFECYCLE_STATS_SEMANTICS;
|
|
36
|
+
}
|
|
37
|
+
export interface CollectLifecycleStatsOptions {
|
|
38
|
+
readonly projectRoot: string;
|
|
39
|
+
/** Window duration string, e.g. "7d", "24h", "1w". Default "7d". */
|
|
40
|
+
readonly since?: string;
|
|
41
|
+
/** Clock for window end (tests inject a fixed Date). */
|
|
42
|
+
readonly now?: Date;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Collect lifecycle folder stats for a project root.
|
|
46
|
+
* Offline / filesystem only.
|
|
47
|
+
*/
|
|
48
|
+
export declare function collectLifecycleStats(options: CollectLifecycleStatsOptions): LifecycleStats;
|
|
49
|
+
/** Format human-readable lifecycle stats text. */
|
|
50
|
+
export declare function formatLifecycleStatsText(stats: LifecycleStats): string;
|
|
51
|
+
/** Re-export duration parse for CLI error messaging. */
|
|
52
|
+
export { parseDurationMs };
|
|
53
|
+
//# sourceMappingURL=stats.d.ts.map
|