@fusengine/harness 0.1.92 → 0.1.94
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/README.md +17 -0
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.mjs +1 -1
- package/dist/apex-target-Xc2M32Pl.mjs +48 -0
- package/dist/{claude-Ckv2_TgP.mjs → claude-D62hkUfS.mjs} +2 -84
- package/dist/cli/bin.mjs +6 -4
- package/dist/cli/index.d.mts +69 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/config/index.mjs +2 -1
- package/dist/{dotenv-BLBkBTww.mjs → dotenv-C1LkcfW-.mjs} +1 -26
- package/dist/{handle-BF1dZFjY.mjs → handle-CDgBbPRz.mjs} +4648 -3699
- package/dist/{hermes-B9-p_3IF.mjs → hermes-ByopGx6C.mjs} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -2
- package/dist/{kimi-G2wcSh5-.mjs → kimi-C-Oia9q-.mjs} +1 -1
- package/dist/policy/index.mjs +1 -1
- package/dist/prd-CixxWJIR.mjs +466 -0
- package/dist/prd-compact-BE45t8UR.mjs +499 -0
- package/dist/runtime/index.d.mts +3 -1
- package/dist/runtime/index.mjs +1 -1
- package/dist/runtime-io-DuumUeE6.mjs +84 -0
- package/dist/{session-state-D5gLr66m.d.mts → session-state-COg7Ej_2.d.mts} +32 -1
- package/dist/{skill-path-DVML3zfp.mjs → skill-path-Cz8WFaGu.mjs} +1 -1
- package/dist/{store-5-ZPKb0u.mjs → store-BVY6gIYM.mjs} +75 -3
- package/dist/tracking/index.d.mts +2 -2
- package/dist/tracking/index.mjs +2 -2
- package/dist/ttl-Dgwg_QAv.mjs +26 -0
- package/dist/{validate-KjZ1X9tH.mjs → validate-Dcjl0LUS.mjs} +4 -49
- package/package.json +1 -1
- package/src/cli/bin.ts +3 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/prd/compact.ts +53 -0
- package/src/cli/prd/format.ts +23 -0
- package/src/cli/prd/index.ts +23 -0
- package/src/cli/prd/resolve.ts +88 -0
- package/src/cli/prd/shared.ts +80 -0
- package/src/cli/prd/status.ts +87 -0
- package/src/cli/prd/validate.ts +90 -0
- package/src/policy/prd/index.ts +36 -0
- package/src/policy/prd/interfaces/types.ts +103 -0
- package/src/policy/prd/prd-compact.ts +28 -0
- package/src/policy/prd/prd-context.ts +131 -0
- package/src/policy/prd/prd-crosscheck.ts +76 -0
- package/src/policy/prd/prd-enabled.ts +39 -0
- package/src/policy/prd/prd-io.ts +89 -0
- package/src/policy/prd/prd-ownership.ts +99 -0
- package/src/policy/prd/prd-paths.ts +88 -0
- package/src/policy/prd/prd-schema.ts +151 -0
- package/src/runtime/handle-post.ts +2 -0
- package/src/runtime/handle-pre.ts +11 -0
- package/src/runtime/lifecycle/dispatch.ts +18 -5
- package/src/runtime/normalize.ts +4 -0
- package/src/runtime/prd/index.ts +8 -0
- package/src/runtime/prd/prd-bash-targets.ts +199 -0
- package/src/runtime/prd/prd-candidate-files.ts +23 -0
- package/src/runtime/prd/prd-canon.ts +50 -0
- package/src/runtime/prd/prd-identity.ts +32 -0
- package/src/runtime/prd/prd-post-check.ts +73 -0
- package/src/runtime/prd/prd-pre-gate.ts +172 -0
- package/src/runtime/prd/prd-stop-gate.ts +167 -0
- package/src/runtime/prd/prd-subagent-context.ts +69 -0
- package/src/runtime/prd/prd-subagent-stop.ts +152 -0
- package/src/tracking/session-state.ts +46 -0
- package/src/tracking/track-diff.ts +3 -0
- package/src/tracking/track-journal.ts +10 -1
- package/dist/run-DkrzC0gb.mjs +0 -42
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { a as writeJsonFile, i as readJsonFile } from "./json-io-DisYd2fb.mjs";
|
|
2
|
+
import { r as harnessHomeSegment } from "./apex-target-Xc2M32Pl.mjs";
|
|
3
|
+
import { a as sleep } from "./runtime-io-DuumUeE6.mjs";
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { mkdir, rmdir } from "node:fs/promises";
|
|
7
|
+
//#region src/policy/prd/prd-paths.ts
|
|
8
|
+
/**
|
|
9
|
+
* Pure path builders and classifiers for the PRD file tree
|
|
10
|
+
* (`<root>/<homeSeg>/apex/prd*`). No fs access — string/path compare only.
|
|
11
|
+
*/
|
|
12
|
+
const AGENT_REPORT_SUFFIX = "-prd.json";
|
|
13
|
+
const DOCS_SUFFIX = ".md";
|
|
14
|
+
function apexDir(root, homeSeg) {
|
|
15
|
+
return join(root, homeSeg, "apex");
|
|
16
|
+
}
|
|
17
|
+
/** Absolute path to the router (`<root>/<homeSeg>/apex/prd.json`). */
|
|
18
|
+
function prdRouterPath(root, homeSeg) {
|
|
19
|
+
return join(apexDir(root, homeSeg), "prd.json");
|
|
20
|
+
}
|
|
21
|
+
/** Absolute path to the PRD directory (`<root>/<homeSeg>/apex/prd`). */
|
|
22
|
+
function prdDir(root, homeSeg) {
|
|
23
|
+
return join(apexDir(root, homeSeg), "prd");
|
|
24
|
+
}
|
|
25
|
+
/** Absolute path to a task-PRD file, given the router entry's `prd` field. */
|
|
26
|
+
function prdTaskPath(root, homeSeg, relPrd) {
|
|
27
|
+
return join(apexDir(root, homeSeg), relPrd);
|
|
28
|
+
}
|
|
29
|
+
/** Absolute path to an agent's own report file (`prd/agents/<agent>-prd.json`). */
|
|
30
|
+
function prdAgentReportPath(root, homeSeg, agent) {
|
|
31
|
+
return join(prdDir(root, homeSeg), "agents", `${agent}${AGENT_REPORT_SUFFIX}`);
|
|
32
|
+
}
|
|
33
|
+
function resolveAgainstRoot(filePath, root) {
|
|
34
|
+
return isAbsolute(filePath) ? resolve(filePath) : resolve(root, filePath);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True when `filePath` (absolute or root-relative) resolves under
|
|
38
|
+
* `<root>/<homeSeg>/apex/prd/`, or is exactly the router itself. Normalizes
|
|
39
|
+
* via `path.resolve`/`relative`; a `..` that escapes the PRD dir is rejected.
|
|
40
|
+
*/
|
|
41
|
+
function isPrdScopedPath(filePath, root, homeSeg) {
|
|
42
|
+
const abs = resolveAgainstRoot(filePath, root);
|
|
43
|
+
if (abs === resolve(prdRouterPath(root, homeSeg))) return true;
|
|
44
|
+
const rel = relative(resolve(prdDir(root, homeSeg)), abs);
|
|
45
|
+
return rel !== "" && rel !== "." && !rel.startsWith("..") && !isAbsolute(rel);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classifies an in-scope PRD path into one of the 4 file kinds (router,
|
|
49
|
+
* task, agentReport, docs), or `"other"` when it is textually under
|
|
50
|
+
* `apex/prd/` but matches none of the router-declared task files. `null`
|
|
51
|
+
* when the path is not in scope at all (see {@link isPrdScopedPath}).
|
|
52
|
+
*/
|
|
53
|
+
function classifyPrdPath(filePath, root, homeSeg, router) {
|
|
54
|
+
if (!isPrdScopedPath(filePath, root, homeSeg)) return null;
|
|
55
|
+
const abs = resolveAgainstRoot(filePath, root);
|
|
56
|
+
if (abs === resolve(prdRouterPath(root, homeSeg))) return { kind: "router" };
|
|
57
|
+
const parts = relative(resolve(prdDir(root, homeSeg)), abs).split(sep);
|
|
58
|
+
const [first, second] = parts;
|
|
59
|
+
if (parts.length === 2 && first === "agents" && second?.endsWith(AGENT_REPORT_SUFFIX)) return {
|
|
60
|
+
kind: "agentReport",
|
|
61
|
+
agent: second.slice(0, -9)
|
|
62
|
+
};
|
|
63
|
+
if (parts.length === 2 && first === "docs" && second?.endsWith(DOCS_SUFFIX)) return {
|
|
64
|
+
kind: "docs",
|
|
65
|
+
task: second.slice(0, -3)
|
|
66
|
+
};
|
|
67
|
+
if (router) {
|
|
68
|
+
for (const [task, entry] of Object.entries(router)) if (resolve(prdTaskPath(root, homeSeg, entry.prd)) === abs) return {
|
|
69
|
+
kind: "task",
|
|
70
|
+
task
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { kind: "other" };
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/policy/prd/prd-schema.ts
|
|
77
|
+
const ROUTER_STATUSES = [
|
|
78
|
+
"assigned",
|
|
79
|
+
"in-progress",
|
|
80
|
+
"validated"
|
|
81
|
+
];
|
|
82
|
+
function isRecord(v) {
|
|
83
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
84
|
+
}
|
|
85
|
+
function isStringArray(v) {
|
|
86
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
87
|
+
}
|
|
88
|
+
function parseRouterEntry(v) {
|
|
89
|
+
if (!isRecord(v)) return null;
|
|
90
|
+
if (typeof v.prd !== "string") return null;
|
|
91
|
+
if (typeof v.status !== "string" || !ROUTER_STATUSES.includes(v.status)) return null;
|
|
92
|
+
const validatedAt = v["validated-at"];
|
|
93
|
+
if (validatedAt !== void 0 && typeof validatedAt !== "string") return null;
|
|
94
|
+
return validatedAt === void 0 ? {
|
|
95
|
+
prd: v.prd,
|
|
96
|
+
status: v.status
|
|
97
|
+
} : {
|
|
98
|
+
prd: v.prd,
|
|
99
|
+
status: v.status,
|
|
100
|
+
"validated-at": validatedAt
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Parses the router file (`prd.json`); `null` on any malformed shape. */
|
|
104
|
+
function parseRouter(raw) {
|
|
105
|
+
if (!isRecord(raw)) return null;
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [task, v] of Object.entries(raw)) {
|
|
108
|
+
const entry = parseRouterEntry(v);
|
|
109
|
+
if (!entry) return null;
|
|
110
|
+
out[task] = entry;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function parseSubTask(v) {
|
|
115
|
+
if (!isRecord(v) || typeof v.status !== "string") return null;
|
|
116
|
+
if (v.status !== "assigned" && v.status !== "validated") return null;
|
|
117
|
+
const validatedAt = v["validated-at"];
|
|
118
|
+
if (validatedAt !== void 0 && typeof validatedAt !== "string") return null;
|
|
119
|
+
return validatedAt === void 0 ? { status: v.status } : {
|
|
120
|
+
status: v.status,
|
|
121
|
+
"validated-at": validatedAt
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function parseAgentEntry(v) {
|
|
125
|
+
if (!isRecord(v) || !isStringArray(v.files)) return null;
|
|
126
|
+
if (v.status === "validated") {
|
|
127
|
+
if (typeof v["validated-at"] !== "string") return null;
|
|
128
|
+
return {
|
|
129
|
+
status: "validated",
|
|
130
|
+
files: v.files,
|
|
131
|
+
"validated-at": v["validated-at"]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!isRecord(v["sub-tasks"])) return null;
|
|
135
|
+
const subTasks = {};
|
|
136
|
+
for (const [sub, sv] of Object.entries(v["sub-tasks"])) {
|
|
137
|
+
const parsed = parseSubTask(sv);
|
|
138
|
+
if (!parsed) return null;
|
|
139
|
+
subTasks[sub] = parsed;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
files: v.files,
|
|
143
|
+
"sub-tasks": subTasks
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Parses a task-PRD file; `null` on any malformed shape. */
|
|
147
|
+
function parseTaskFile(raw) {
|
|
148
|
+
if (!isRecord(raw)) return null;
|
|
149
|
+
const out = {};
|
|
150
|
+
for (const [agent, v] of Object.entries(raw)) {
|
|
151
|
+
const entry = parseAgentEntry(v);
|
|
152
|
+
if (!entry) return null;
|
|
153
|
+
out[agent] = entry;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** Parses an agent's own report file; `null` on any malformed shape. */
|
|
158
|
+
function parseAgentReportFile(raw) {
|
|
159
|
+
if (!isRecord(raw)) return null;
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const [task, subs] of Object.entries(raw)) {
|
|
162
|
+
if (!isRecord(subs)) return null;
|
|
163
|
+
const parsedSubs = {};
|
|
164
|
+
for (const [sub, v] of Object.entries(subs)) {
|
|
165
|
+
if (!isRecord(v) || v.status !== "done" || !isStringArray(v.modified) || !isStringArray(v.unchanged)) return null;
|
|
166
|
+
const doneAt = v["done-at"];
|
|
167
|
+
if (doneAt !== void 0 && typeof doneAt !== "string") return null;
|
|
168
|
+
parsedSubs[sub] = doneAt === void 0 ? {
|
|
169
|
+
status: "done",
|
|
170
|
+
modified: v.modified,
|
|
171
|
+
unchanged: v.unchanged
|
|
172
|
+
} : {
|
|
173
|
+
status: "done",
|
|
174
|
+
modified: v.modified,
|
|
175
|
+
unchanged: v.unchanged,
|
|
176
|
+
"done-at": doneAt
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
out[task] = parsedSubs;
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/** True when `e` is the post-compaction shape. */
|
|
184
|
+
function isCompacted(e) {
|
|
185
|
+
return "status" in e && e.status === "validated" && !("sub-tasks" in e);
|
|
186
|
+
}
|
|
187
|
+
/** Sub-tasks of an agent entry; `{}` when already compacted. */
|
|
188
|
+
function subTasksOf(e) {
|
|
189
|
+
return isCompacted(e) ? {} : e["sub-tasks"];
|
|
190
|
+
}
|
|
191
|
+
/** Files owned by an agent entry, expanded or compacted. */
|
|
192
|
+
function filesOf(e) {
|
|
193
|
+
return e.files;
|
|
194
|
+
}
|
|
195
|
+
/** Returns a new router with `task`'s status (and optional `validated-at`) updated. */
|
|
196
|
+
function withRouterStatus(router, task, status, at) {
|
|
197
|
+
const existing = router[task];
|
|
198
|
+
if (!existing) return router;
|
|
199
|
+
const entry = at === void 0 ? {
|
|
200
|
+
prd: existing.prd,
|
|
201
|
+
status
|
|
202
|
+
} : {
|
|
203
|
+
prd: existing.prd,
|
|
204
|
+
status,
|
|
205
|
+
"validated-at": at
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
...router,
|
|
209
|
+
[task]: entry
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/** Returns a new task-PRD file with one agent's sub-task flipped to `validated`. */
|
|
213
|
+
function withSubTaskValidated(taskFile, agent, sub, at) {
|
|
214
|
+
const entry = taskFile[agent];
|
|
215
|
+
if (!entry || isCompacted(entry)) return taskFile;
|
|
216
|
+
if (!entry["sub-tasks"][sub]) return taskFile;
|
|
217
|
+
const nextSubTasks = {
|
|
218
|
+
...entry["sub-tasks"],
|
|
219
|
+
[sub]: {
|
|
220
|
+
status: "validated",
|
|
221
|
+
"validated-at": at
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
...taskFile,
|
|
226
|
+
[agent]: {
|
|
227
|
+
files: entry.files,
|
|
228
|
+
"sub-tasks": nextSubTasks
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/policy/prd/prd-io.ts
|
|
234
|
+
/**
|
|
235
|
+
* Thin fail-closed wrappers over `util/json-io.ts` + the `prd-schema.ts`
|
|
236
|
+
* parsers. Both async (CLI, PostToolUse) and sync (SubagentStart/Stop, which
|
|
237
|
+
* must not float async work) variants. The only two files in this module
|
|
238
|
+
* that touch disk (besides `prd-enabled.ts`).
|
|
239
|
+
*/
|
|
240
|
+
function readJsonSync(path) {
|
|
241
|
+
try {
|
|
242
|
+
if (!existsSync(path)) return null;
|
|
243
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Reads the router file; `null` when missing or malformed. */
|
|
249
|
+
async function readRouter(root, homeSeg) {
|
|
250
|
+
const raw = await readJsonFile(prdRouterPath(root, homeSeg));
|
|
251
|
+
return raw === null ? null : parseRouter(raw);
|
|
252
|
+
}
|
|
253
|
+
/** Sync twin of {@link readRouter}, for SubagentStart/Stop call sites. */
|
|
254
|
+
function readRouterSync(root, homeSeg) {
|
|
255
|
+
const raw = readJsonSync(prdRouterPath(root, homeSeg));
|
|
256
|
+
return raw === null ? null : parseRouter(raw);
|
|
257
|
+
}
|
|
258
|
+
/** Reads a task-PRD file; `null` when missing or malformed. */
|
|
259
|
+
async function readTaskFile(root, homeSeg, relPrd) {
|
|
260
|
+
const raw = await readJsonFile(prdTaskPath(root, homeSeg, relPrd));
|
|
261
|
+
return raw === null ? null : parseTaskFile(raw);
|
|
262
|
+
}
|
|
263
|
+
/** Sync twin of {@link readTaskFile}. */
|
|
264
|
+
function readTaskFileSync(root, homeSeg, relPrd) {
|
|
265
|
+
const raw = readJsonSync(prdTaskPath(root, homeSeg, relPrd));
|
|
266
|
+
return raw === null ? null : parseTaskFile(raw);
|
|
267
|
+
}
|
|
268
|
+
/** Reads every task-PRD file named in `router`, keyed by task name (`null` per unreadable/malformed entry). */
|
|
269
|
+
async function readAllTaskFiles(root, homeSeg, router) {
|
|
270
|
+
const out = {};
|
|
271
|
+
for (const [task, entry] of Object.entries(router)) out[task] = await readTaskFile(root, homeSeg, entry.prd);
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
/** Reads an agent's own report file; `null` when missing or malformed. */
|
|
275
|
+
async function readAgentReport(root, homeSeg, agent) {
|
|
276
|
+
const raw = await readJsonFile(prdAgentReportPath(root, homeSeg, agent));
|
|
277
|
+
return raw === null ? null : parseAgentReportFile(raw);
|
|
278
|
+
}
|
|
279
|
+
/** Sync twin of {@link readAgentReport}. */
|
|
280
|
+
function readAgentReportSync(root, homeSeg, agent) {
|
|
281
|
+
const raw = readJsonSync(prdAgentReportPath(root, homeSeg, agent));
|
|
282
|
+
return raw === null ? null : parseAgentReportFile(raw);
|
|
283
|
+
}
|
|
284
|
+
/** Atomically writes the router file. */
|
|
285
|
+
async function writeRouter(root, homeSeg, router) {
|
|
286
|
+
await writeJsonFile(prdRouterPath(root, homeSeg), router);
|
|
287
|
+
}
|
|
288
|
+
/** Atomically writes a task-PRD file. */
|
|
289
|
+
async function writeTaskFile(root, homeSeg, relPrd, file) {
|
|
290
|
+
await writeJsonFile(prdTaskPath(root, homeSeg, relPrd), file);
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/policy/prd/prd-crosscheck.ts
|
|
294
|
+
/**
|
|
295
|
+
* Cross-checks a task-PRD's declared status against the agents' own reports
|
|
296
|
+
* (and, for the router, against the task-PRD's own internal consistency).
|
|
297
|
+
* Pure — no fs.
|
|
298
|
+
*/
|
|
299
|
+
/** Mismatches between a task-PRD's `validated` sub-tasks and the agents' own reports. */
|
|
300
|
+
function crossCheckTask(taskFile, reports, task) {
|
|
301
|
+
const violations = [];
|
|
302
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
303
|
+
if (isCompacted(entry)) continue;
|
|
304
|
+
const report = reports[agent];
|
|
305
|
+
for (const [sub, subTask] of Object.entries(subTasksOf(entry))) {
|
|
306
|
+
if (subTask.status !== "validated") continue;
|
|
307
|
+
if (report?.[task]?.[sub]?.status !== "done") violations.push({
|
|
308
|
+
task,
|
|
309
|
+
agent,
|
|
310
|
+
sub,
|
|
311
|
+
reason: "validated without a matching done report"
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return violations;
|
|
316
|
+
}
|
|
317
|
+
function taskFullyValidated(taskFile) {
|
|
318
|
+
return Object.values(taskFile).every((entry) => {
|
|
319
|
+
if (isCompacted(entry)) return true;
|
|
320
|
+
return Object.values(subTasksOf(entry)).every((sub) => sub.status === "validated");
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Router-level consistency check: a router entry marked `validated` whose
|
|
325
|
+
* task-PRD is missing/unparseable, or not actually fully validated, is a
|
|
326
|
+
* violation. Non-`validated` router entries are never checked.
|
|
327
|
+
*/
|
|
328
|
+
function crossCheckRouter(router, taskFiles) {
|
|
329
|
+
const violations = [];
|
|
330
|
+
for (const [task, entry] of Object.entries(router)) {
|
|
331
|
+
if (entry.status !== "validated") continue;
|
|
332
|
+
const taskFile = taskFiles[task];
|
|
333
|
+
if (!taskFile) violations.push({
|
|
334
|
+
task,
|
|
335
|
+
agent: "*",
|
|
336
|
+
sub: "*",
|
|
337
|
+
reason: "router marked validated but task-PRD is missing or unparseable"
|
|
338
|
+
});
|
|
339
|
+
else if (!taskFullyValidated(taskFile)) violations.push({
|
|
340
|
+
task,
|
|
341
|
+
agent: "*",
|
|
342
|
+
sub: "*",
|
|
343
|
+
reason: "router marked validated but task-PRD is not fully validated"
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return violations;
|
|
347
|
+
}
|
|
348
|
+
/** Sub-tasks assigned to `agent` in `taskFile` for `task` not `done` in its own report. */
|
|
349
|
+
function incompleteSubTasks(taskFile, agent, task, report) {
|
|
350
|
+
const entry = taskFile[agent];
|
|
351
|
+
if (!entry || isCompacted(entry)) return [];
|
|
352
|
+
const doneSubs = report?.[task] ?? {};
|
|
353
|
+
return Object.keys(subTasksOf(entry)).filter((sub) => doneSubs[sub]?.status !== "done");
|
|
354
|
+
}
|
|
355
|
+
/** True when the router has any `validated`-vs-task-PRD mismatch. */
|
|
356
|
+
function hasAnyViolations(router, taskFiles) {
|
|
357
|
+
return crossCheckRouter(router, taskFiles).length > 0;
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/policy/prd/prd-enabled.ts
|
|
361
|
+
/**
|
|
362
|
+
* The single activation check for the whole PRD module. Everything
|
|
363
|
+
* downstream short-circuits on `false` here — this is the only place the
|
|
364
|
+
* module's "totally inert when off" contract lives. The other file in this
|
|
365
|
+
* module that touches disk (besides `prd-io.ts`).
|
|
366
|
+
*/
|
|
367
|
+
/** True only when `FUSE_PRD` is the exact string `"1"` — no truthy-string leniency. */
|
|
368
|
+
function isPrdFlagSet(env = process.env) {
|
|
369
|
+
return env.FUSE_PRD === "1";
|
|
370
|
+
}
|
|
371
|
+
/** `CLAUDE_PROJECT_DIR` then `CURSOR_PROJECT_DIR` (first absolute value wins), else `cwd`. */
|
|
372
|
+
function prdProjectRoot(cwd, env = process.env) {
|
|
373
|
+
for (const candidate of [env.CLAUDE_PROJECT_DIR, env.CURSOR_PROJECT_DIR]) if (candidate !== void 0 && isAbsolute(candidate)) return candidate;
|
|
374
|
+
return cwd;
|
|
375
|
+
}
|
|
376
|
+
/** True when the router file exists on disk. */
|
|
377
|
+
function routerExistsSync(root, homeSeg) {
|
|
378
|
+
return existsSync(prdRouterPath(root, homeSeg));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Activation = `FUSE_PRD=1` AND the router is present. Short-circuits on the
|
|
382
|
+
* env check first, so a disabled module never touches disk.
|
|
383
|
+
*/
|
|
384
|
+
function isPrdEnabled(cwd, id, env = process.env) {
|
|
385
|
+
if (!isPrdFlagSet(env)) return false;
|
|
386
|
+
return routerExistsSync(prdProjectRoot(cwd, env), harnessHomeSegment(id));
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/runtime/lifecycle/aipilot/apex-task-store.ts
|
|
390
|
+
/**
|
|
391
|
+
* APEX `task.json` mutation helpers + a directory-based lock for the ai-pilot
|
|
392
|
+
* scope. Ported from the ai-pilot plugin's `apex/state.ts` + `apex/task-helpers.ts`
|
|
393
|
+
* (now removed).
|
|
394
|
+
*/
|
|
395
|
+
/**
|
|
396
|
+
* Acquire a directory-based lock with a timeout.
|
|
397
|
+
* @param lockDir - Path to the lock directory.
|
|
398
|
+
* @param timeoutMs - Max wait in ms (default 5000).
|
|
399
|
+
* @returns A release function, or null if acquisition timed out.
|
|
400
|
+
*/
|
|
401
|
+
async function acquireLock(lockDir, timeoutMs = 5e3) {
|
|
402
|
+
const start = Date.now();
|
|
403
|
+
while (Date.now() - start < timeoutMs) try {
|
|
404
|
+
await mkdir(lockDir, { recursive: false });
|
|
405
|
+
return async () => {
|
|
406
|
+
try {
|
|
407
|
+
await rmdir(lockDir);
|
|
408
|
+
} catch {}
|
|
409
|
+
};
|
|
410
|
+
} catch {
|
|
411
|
+
await sleep(100);
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
/** Add a new task to `task.json`. */
|
|
416
|
+
async function taskCreate(file, id, subject, desc) {
|
|
417
|
+
const data = await readJsonFile(file);
|
|
418
|
+
if (!data) return;
|
|
419
|
+
data.tasks[id] = {
|
|
420
|
+
subject,
|
|
421
|
+
description: desc,
|
|
422
|
+
status: "pending",
|
|
423
|
+
phase: "pending",
|
|
424
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
425
|
+
doc_consulted: {},
|
|
426
|
+
files_modified: [],
|
|
427
|
+
blockedBy: []
|
|
428
|
+
};
|
|
429
|
+
await writeJsonFile(file, data);
|
|
430
|
+
}
|
|
431
|
+
/** Mark a task as in_progress in `task.json`. */
|
|
432
|
+
async function taskStart(file, id, subject, desc, blocked) {
|
|
433
|
+
const data = await readJsonFile(file);
|
|
434
|
+
if (!data) return;
|
|
435
|
+
if (!data.tasks[id]) data.tasks[id] = {
|
|
436
|
+
subject: "",
|
|
437
|
+
description: "",
|
|
438
|
+
status: "in_progress",
|
|
439
|
+
phase: "analyze",
|
|
440
|
+
doc_consulted: {},
|
|
441
|
+
files_modified: []
|
|
442
|
+
};
|
|
443
|
+
data.current_task = id;
|
|
444
|
+
const task = data.tasks[id];
|
|
445
|
+
Object.assign(task, {
|
|
446
|
+
status: "in_progress",
|
|
447
|
+
phase: "analyze",
|
|
448
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
449
|
+
});
|
|
450
|
+
if (subject) task.subject = subject;
|
|
451
|
+
if (desc) task.description = desc;
|
|
452
|
+
if (blocked) task.blockedBy = blocked.split(",");
|
|
453
|
+
await writeJsonFile(file, data);
|
|
454
|
+
}
|
|
455
|
+
/** Mark a task as completed in `task.json`. */
|
|
456
|
+
async function taskComplete(file, id) {
|
|
457
|
+
const data = await readJsonFile(file);
|
|
458
|
+
if (!data?.tasks[id]) return;
|
|
459
|
+
Object.assign(data.tasks[id], {
|
|
460
|
+
status: "completed",
|
|
461
|
+
phase: "completed",
|
|
462
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
463
|
+
});
|
|
464
|
+
await writeJsonFile(file, data);
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/policy/prd/prd-compact.ts
|
|
468
|
+
/** Collapses fully-validated task-PRD agent entries to their compact shape. Pure — no fs. */
|
|
469
|
+
/** Collapses one agent entry to the compacted shape ONLY when every sub-task is `validated`; else returns it unchanged. */
|
|
470
|
+
function compactAgentEntry(e, at) {
|
|
471
|
+
if (isCompacted(e)) return e;
|
|
472
|
+
const subTasks = Object.values(subTasksOf(e));
|
|
473
|
+
if (subTasks.length === 0 || !subTasks.every((s) => s.status === "validated")) return e;
|
|
474
|
+
return {
|
|
475
|
+
status: "validated",
|
|
476
|
+
files: e.files,
|
|
477
|
+
"validated-at": at
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/** Compacts every eligible agent entry in a task-PRD; reports which agent names got collapsed. */
|
|
481
|
+
function compactTaskFile(taskFile, at) {
|
|
482
|
+
const compacted = [];
|
|
483
|
+
const file = {};
|
|
484
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
485
|
+
const next = compactAgentEntry(entry, at);
|
|
486
|
+
if (next !== entry) compacted.push(agent);
|
|
487
|
+
file[agent] = next;
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
file,
|
|
491
|
+
compacted
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** True when every agent in the task-PRD is already compacted+validated. */
|
|
495
|
+
function canPromoteRouterEntry(taskFile) {
|
|
496
|
+
return Object.values(taskFile).every((entry) => isCompacted(entry));
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
export { prdRouterPath as A, isCompacted as C, classifyPrdPath as D, withSubTaskValidated as E, isPrdScopedPath as O, filesOf as S, withRouterStatus as T, readRouterSync as _, taskCreate as a, writeRouter as b, isPrdFlagSet as c, hasAnyViolations as d, incompleteSubTasks as f, readRouter as g, readAllTaskFiles as h, taskComplete as i, prdDir as k, prdProjectRoot as l, readAgentReportSync as m, compactTaskFile as n, taskStart as o, readAgentReport as p, acquireLock as r, isPrdEnabled as s, canPromoteRouterEntry as t, crossCheckTask as u, readTaskFile as v, subTasksOf as w, writeTaskFile as x, readTaskFileSync as y };
|
package/dist/runtime/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as Prompt } from "../types-Bh0jEF3_.mjs";
|
|
2
2
|
import { t as RefMeta } from "../types-CY5qT2X1.mjs";
|
|
3
|
-
import { t as AgentQuality } from "../session-state-
|
|
3
|
+
import { t as AgentQuality } from "../session-state-COg7Ej_2.mjs";
|
|
4
4
|
//#region src/runtime/paths.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Compute a stable 8-char hex hash for a project directory path.
|
|
@@ -187,6 +187,8 @@ interface NormalizedEvent {
|
|
|
187
187
|
permissionMode?: string;
|
|
188
188
|
/** Codex logical tool-use identity, shared by sibling hook callbacks. */
|
|
189
189
|
toolUseId?: string;
|
|
190
|
+
/** Sub-agent identifier, if the tool-use came from one (Claude/Codex only — Cursor/Kimi never send this field, confirmed live). */
|
|
191
|
+
agentId?: string;
|
|
190
192
|
/** Harness-reported working directory used to scope Codex authorization. */
|
|
191
193
|
cwd?: string;
|
|
192
194
|
/** Validated Cursor multi-root workspace paths in wire order. */
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-KWoE_Mqn.mjs";
|
|
2
2
|
import { a as sanitizeSessionId, c as sessionsDir, i as loadSessionState, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, s as sessionStatePath, t as claudeHome } from "../home-state-oUGFB4ds.mjs";
|
|
3
|
-
import { A as
|
|
3
|
+
import { A as gate, At as taskContext, B as writePluginMap, Bt as todayUtc, C as trackEnrichment, Ct as removeOldFiles, D as DEFAULT_WINDOW_MS, Dt as projectContext, E as dispatchAipilot, Et as gitContext, F as extractSymbols, Ft as isoUtc, G as mergeLines, H as isProject, I as respond, It as loadSecurityState, J as listChildren, K as countFiles, L as dispatchLessons, Lt as saveSecurityState, M as preCommitGate, Mt as projectHash, N as detectDuplication, Nt as trackFile, O as REQUIRED_AGENTS, Ot as claudeMdKey, P as dryGate, Pt as normalizeEvent, R as cartoSessionStart, Rt as securityStateDir, S as trackSkillRead, St as purgeTtlTree, T as aipilotPostToolUse, Tt as devContext, U as writeTree, V as generateProjectMap, W as loadEnriched, X as lessonsFileFor, Y as lessonsArchiveFileFor, Z as lessonsStateFileFor, _ as securityAdvisory, _t as injectRules, a as mcpPostStore, at as cleanupSession, b as trackWatchResearch, bt as sessionStartCore, c as WEBFETCH_TTL_MS, ct as validateTeammateOutput, d as queryOf, dt as validateTailwind, f as lifecycleStdout, ft as validateSolidGate, g as dispatchMemory, gt as solidDetectStart, h as seoPostToolUseResponse, ht as detectSolidProfile, i as recordActivity, it as validateRulesLoaded, j as gateCommandCandidates, jt as defaultStateDir, k as TRIVIAL_BUDGET, kt as promptSubmitContext, l as cacheQueryOf, lt as trackAgentMemory, m as seoPostToolUse, mt as countLoc, n as activityFor, nt as postEditTypescript, o as mcpPreIntercept, ot as saveApexState, p as postEditContext, pt as checkFileSize, q as getFileDesc, r as handlePre, rt as trackSessionChanges, s as MCP_TTL_MS, st as logToolFailure, t as handleHook, u as isMcpTool, ut as subagentCacheContext, v as securityAdvisoryForPatch, vt as readRules, w as dispatchLifecycle, wt as trimLogFile, x as trackMcpResearch, xt as pruneEmptyDirs, y as postTrackingSideEffects, yt as runSessionStartCleanups, z as generateEcosystemMap, zt as securityStatePath } from "../handle-CDgBbPRz.mjs";
|
|
4
4
|
//#region src/runtime/storage.ts
|
|
5
5
|
/**
|
|
6
6
|
* The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
//#region src/util/runtime-io.ts
|
|
5
|
+
/**
|
|
6
|
+
* Cross-runtime I/O helpers (Node + Bun). The published CLI ships a `node`
|
|
7
|
+
* shebang and is invoked via `npx`/`npm`/`bunx`/`bun`; using only `node:*` APIs
|
|
8
|
+
* here keeps the bundle runnable under EVERY runtime — replacing the Bun-only
|
|
9
|
+
* `Bun.file/write/spawn/sleep/stdin` calls and the `bun` `Glob` import that made
|
|
10
|
+
* the package crash with `Cannot find package 'bun'` under plain Node.
|
|
11
|
+
*/
|
|
12
|
+
/** Read a file as UTF-8 text (throws on missing/unreadable — callers catch). */
|
|
13
|
+
function readText(path) {
|
|
14
|
+
return readFileSync(path, "utf8");
|
|
15
|
+
}
|
|
16
|
+
/** True when `path` exists. */
|
|
17
|
+
function pathExists(path) {
|
|
18
|
+
return existsSync(path);
|
|
19
|
+
}
|
|
20
|
+
/** Write `data` to `path`, creating parent dirs (mirrors `Bun.write`). */
|
|
21
|
+
function writeText(path, data) {
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
writeFileSync(path, data, { encoding: "utf8" });
|
|
24
|
+
}
|
|
25
|
+
/** Resolve after `ms` milliseconds (`Bun.sleep` replacement). */
|
|
26
|
+
function sleep(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
/** Run `cmd args` in `cwd` and capture stdout text ("" on failure/non-zero). */
|
|
30
|
+
function spawnCapture(cmd, args, cwd) {
|
|
31
|
+
try {
|
|
32
|
+
const r = spawnSync(cmd, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
encoding: "utf8"
|
|
35
|
+
});
|
|
36
|
+
return r.status === 0 ? r.stdout ?? "" : "";
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the full process stdin as UTF-8 text (works under Node and Bun).
|
|
43
|
+
* Synchronous fd-0 read — never `for await (const c of process.stdin)` —
|
|
44
|
+
* because Bun has a confirmed Linux-only bug where merely adding a new
|
|
45
|
+
* top-level module import shifts stdin's internal init order and makes
|
|
46
|
+
* that async iterator silently yield zero bytes (oven-sh/bun#25320,
|
|
47
|
+
* #27849): exactly what broke every `harness hook` invocation on CI the
|
|
48
|
+
* moment `figures` became this package's first runtime dependency.
|
|
49
|
+
* `readFileSync(0)` bypasses that stream/ownership machinery entirely and
|
|
50
|
+
* is well-supported for piped (non-TTY) stdin under both runtimes.
|
|
51
|
+
*/
|
|
52
|
+
async function readStdin() {
|
|
53
|
+
try {
|
|
54
|
+
return readFileSync(0, "utf8");
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Recursively collect files under `dir` whose extension is in `exts`, skipping
|
|
61
|
+
* `node_modules` and dot-dirs, capped at `cap`. `Bun.Glob().scan()` replacement.
|
|
62
|
+
* @param dir - Directory to walk.
|
|
63
|
+
* @param exts - Allowed extensions including the dot (e.g. `.ts`).
|
|
64
|
+
* @param out - Accumulator (mutated in place).
|
|
65
|
+
* @param cap - Max files to collect.
|
|
66
|
+
*/
|
|
67
|
+
function collectFiles(dir, exts, out, cap) {
|
|
68
|
+
if (out.length >= cap) return;
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const e of entries) {
|
|
76
|
+
if (out.length >= cap) return;
|
|
77
|
+
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
78
|
+
const full = join(dir, e.name);
|
|
79
|
+
if (e.isDirectory()) collectFiles(full, exts, out, cap);
|
|
80
|
+
else if (exts.has(e.name.slice(e.name.lastIndexOf(".")))) out.push(full);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { sleep as a, readText as i, pathExists as n, spawnCapture as o, readStdin as r, writeText as s, collectFiles as t };
|
|
@@ -50,7 +50,23 @@ interface SessionTrack {
|
|
|
50
50
|
brainstormRequired?: boolean;
|
|
51
51
|
/** Verification receipts (tsc/test) at PostToolUse; absent/empty reads as unverified in the TaskCompleted gate (backward compat, fail-closed). See {@link Receipt}. */
|
|
52
52
|
receipts?: Receipt[];
|
|
53
|
+
/** PRD (task/agent ownership coordination) — `agent_id` bound to its resolved agent-report name. Optional: absent on any track predating the PRD module (backward compat). */
|
|
54
|
+
prdOwners?: Record<string, string>;
|
|
55
|
+
/** PRD cross-check violations detected at PostToolUse (capped, append-only). */
|
|
56
|
+
prdViolations?: PrdViolationRecord[];
|
|
57
|
+
/** PRD SubagentStop/Stop one-shot block markers, keyed `"sessionId:event[:agent]"` -> the epoch-ms the block fired. */
|
|
58
|
+
prdStopBlocked?: Record<string, number>;
|
|
53
59
|
}
|
|
60
|
+
/** One PRD cross-check violation, timestamped for the journal/dedup. */
|
|
61
|
+
interface PrdViolationRecord {
|
|
62
|
+
ts: number;
|
|
63
|
+
task: string;
|
|
64
|
+
agent: string;
|
|
65
|
+
sub: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
/** Cap on {@link SessionTrack.prdViolations} (mirrors the `receipts` style bound — oldest evicted first). */
|
|
69
|
+
declare const PRD_VIOLATIONS_CAP = 50;
|
|
54
70
|
/** A fresh, empty track. */
|
|
55
71
|
declare function emptyTrack(): SessionTrack;
|
|
56
72
|
/**
|
|
@@ -85,5 +101,20 @@ declare function recordTrivialEdit(track: SessionTrack, ts: number, windowMs: nu
|
|
|
85
101
|
declare function trivialCount(track: SessionTrack, windowMs: number, now: number): number;
|
|
86
102
|
/** Set the brainstorm-required flag (from creation-intent detection). Immutable. */
|
|
87
103
|
declare function recordBrainstormRequired(track: SessionTrack, required: boolean): SessionTrack;
|
|
104
|
+
/** Bind `agentId` to its resolved PRD agent-report `name` (merge; idempotent no-op if already bound to the same name). Immutable. */
|
|
105
|
+
declare function recordPrdOwner(track: SessionTrack, agentId: string, name: string): SessionTrack;
|
|
106
|
+
/**
|
|
107
|
+
* Append one PRD cross-check violation. Immutable, uncapped in the mutator
|
|
108
|
+
* itself — same append-only shape as {@link Receipt} — so the journal diff's
|
|
109
|
+
* prefix comparison (`tail`, track-diff.ts) never sees an eviction it would
|
|
110
|
+
* mistake for a bulk rewrite. The {@link PRD_VIOLATIONS_CAP} bound is applied
|
|
111
|
+
* only when the journal is folded/read (track-journal.ts's `foldEvents`),
|
|
112
|
+
* mirroring how `trivialEdits`'s sliding window is re-derived at fold time.
|
|
113
|
+
*/
|
|
114
|
+
declare function recordPrdViolation(track: SessionTrack, v: PrdViolationRecord): SessionTrack;
|
|
115
|
+
/** Record a PRD SubagentStop/Stop one-shot block marker (merge). Immutable. */
|
|
116
|
+
declare function recordPrdStopBlocked(track: SessionTrack, key: string, ts: number): SessionTrack;
|
|
117
|
+
/** True when the PRD one-shot block for `key` has already fired. */
|
|
118
|
+
declare function prdAlreadyBlocked(track: SessionTrack, key: string): boolean;
|
|
88
119
|
//#endregion
|
|
89
|
-
export {
|
|
120
|
+
export { trivialCount as _, agentsFresh as a, recordAgent as c, recordPrdOwner as d, recordPrdStopBlocked as f, recordTrivialEdit as g, recordTarget as h, SessionTrack as i, recordBrainstormRequired as l, recordRefRead as m, PRD_VIOLATIONS_CAP as n, emptyTrack as o, recordPrdViolation as p, PrdViolationRecord as r, prdAlreadyBlocked as s, AgentQuality as t, recordDoc as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as HOME_DIR } from "./dotenv-
|
|
1
|
+
import { t as HOME_DIR } from "./dotenv-C1LkcfW-.mjs";
|
|
2
2
|
import { t as detectHarness } from "./harness-BMuLJ9lm.mjs";
|
|
3
3
|
import { l as PLUGINS_DIR } from "./home-state-oUGFB4ds.mjs";
|
|
4
4
|
import { join } from "node:path";
|