@fusengine/harness 0.1.91 → 0.1.93
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/cursor/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/apex-task-store-Cb65rE5i.mjs +467 -0
- package/dist/{claude-Ckv2_TgP.mjs → claude-D62hkUfS.mjs} +2 -84
- package/dist/cli/bin.mjs +7 -5
- 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-C43gA-Pr.mjs → handle-B-g9KL_9.mjs} +4837 -3466
- 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/{normalize-BjG6unTj.mjs → normalize-Dy8g9Ybl.mjs} +83 -3
- package/dist/policy/index.mjs +1 -1
- package/dist/prd-PvK7PKWS.mjs +494 -0
- package/dist/runtime/index.d.mts +13 -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/adapters/cursor/context-budget.ts +144 -0
- package/src/adapters/cursor/context-limit.ts +115 -0
- package/src/adapters/cursor/context.ts +21 -2
- package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
- package/src/adapters/cursor/native-response.ts +10 -129
- package/src/adapters/cursor/native-schemas.ts +161 -0
- package/src/adapters/cursor/normalize.ts +65 -0
- package/src/adapters/cursor/plugin-root.ts +103 -0
- package/src/adapters/cursor/respond.ts +94 -47
- 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/handle.ts +87 -6
- package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +7 -1
- package/src/runtime/lifecycle/dispatch.ts +18 -5
- package/src/runtime/lifecycle/failure-lesson.ts +6 -2
- package/src/runtime/lifecycle/rules-root.ts +18 -2
- 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 +101 -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
package/README.md
CHANGED
|
@@ -64,6 +64,9 @@ don't assume either the old or the "everything in one place" story):
|
|
|
64
64
|
| `harness check` | cli-mode: check staged files in a pre-commit step, exit non-zero on a violation. For harnesses without hooks. |
|
|
65
65
|
| `harness doctor` | Print the version + resolved path of the harness *actually executing*, and compare it to npm's latest — the fast way to catch a stale global (see Pinning). |
|
|
66
66
|
| `harness --version` | Print the running version (bare, on stdout) and exit. |
|
|
67
|
+
| `harness prd status [--json]` | Read-only, works without `FUSE_PRD=1`. Print each task's router status and sub-task progress. |
|
|
68
|
+
| `harness prd validate <task> [agent]` | Requires `FUSE_PRD=1`. Cross-check a task PRD against agent reports and promote matching sub-tasks/router entries to `validated`. |
|
|
69
|
+
| `harness prd compact <task>` | Requires `FUSE_PRD=1`. Collapse a fully-validated task PRD to its compacted shape. |
|
|
67
70
|
|
|
68
71
|
Every invocation writes a `@fusengine/harness vX.Y.Z` banner to **stderr** (never
|
|
69
72
|
stdout — the hook JSON contract stays clean) so you can see which version ran.
|
|
@@ -313,6 +316,7 @@ non-zero exit is swallowed — a broken or absent player can never break a hook
|
|
|
313
316
|
| `FUSE_LESSONS_THROTTLE_MIN` | Lessons-injection throttle, minutes (default `5`). |
|
|
314
317
|
| `FUSE_ENFORCE_GEMINI_MCP` | **Opt-in (default off).** Blocks hand-written Tailwind UI (`.tsx/.jsx/.vue/.svelte`) until a `mcp__gemini-design__*` call is made this session. Read fresh per call (`src/policy/gemini-mcp-gate.ts`). |
|
|
315
318
|
| `FUSE_DESIGN_GEMINI` | **Opt-in (default off), a *different* gate from the one above.** Enables the design-pipeline's own Gemini gates (`create_frontend` validation + "generate before hand-writing HTML/CSS") — inert unless a design agent is active (`src/policy/design/gates.ts:58-60`, see [docs/design.md](docs/design.md)). |
|
|
319
|
+
| `FUSE_PRD` | **Opt-in (default off).** Set to exactly `1` to activate task/agent PRD ownership coordination — still inert without a `prd.json` router under `<homeSeg>/apex/`. See [PRD coordination](#prd-coordination-opt-in) below and [docs/prd.md](docs/prd.md). |
|
|
316
320
|
| `FUSE_MCP_TTL_SEC` | MCP (Context7/Exa) cache freshness, seconds (default 48h, `src/runtime/mcp-key.ts`). |
|
|
317
321
|
| `FUSE_WEBFETCH_TTL_SEC` | WebFetch cache freshness, seconds (default 24h — pages stale faster than docs). |
|
|
318
322
|
| `FUSE_CONFIRM_SUBAGENT_WINDOW_SEC` | G0 cool-down (seconds, default `300`) for the `CONFIRM <code>` mechanism above — no token can be placed within this window of the last SubagentStart/Stop seen for the session. |
|
|
@@ -358,6 +362,18 @@ registerGuard(({ tool, command }) =>
|
|
|
358
362
|
: null);
|
|
359
363
|
```
|
|
360
364
|
|
|
365
|
+
## PRD coordination (opt-in)
|
|
366
|
+
|
|
367
|
+
Set `FUSE_PRD=1` and write a router file so a lead can split one task
|
|
368
|
+
across several sub-agents, each restricted to writing only its own
|
|
369
|
+
report. Full walkthrough, guard behavior, and the CLI: [docs/prd.md](docs/prd.md).
|
|
370
|
+
|
|
371
|
+
Minimal example — `.claude/apex/prd.json`:
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{ "auth-refactor": { "prd": "prd/auth-refactor-prd.json", "status": "assigned" } }
|
|
375
|
+
```
|
|
376
|
+
|
|
361
377
|
## Subpath exports
|
|
362
378
|
|
|
363
379
|
| Subpath | What |
|
|
@@ -386,6 +402,7 @@ registerGuard(({ tool, command }) =>
|
|
|
386
402
|
| [docs/modules.md](https://github.com/fusengine/harness/blob/main/docs/modules.md) | cache · refs · state · memory · statusline · util |
|
|
387
403
|
| [docs/adapters.md](https://github.com/fusengine/harness/blob/main/docs/adapters.md) | adapters, compatibility, `harness init`/`hook` wiring |
|
|
388
404
|
| [docs/design.md](https://github.com/fusengine/harness/blob/main/docs/design.md) | design-agent pipeline — state machine, gates, opt-in Gemini |
|
|
405
|
+
| [docs/prd.md](https://github.com/fusengine/harness/blob/main/docs/prd.md) | PRD task/agent ownership coordination — opt-in, `FUSE_PRD=1` |
|
|
389
406
|
| [CHANGELOG.md](https://github.com/fusengine/harness/blob/main/CHANGELOG.md) | release history |
|
|
390
407
|
|
|
391
408
|
Run `bun run docs:api` for the generated typedoc API reference.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-
|
|
1
|
+
import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-D62hkUfS.mjs";
|
|
2
2
|
export { attachSystemMessage, blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, informResponse, readClaudeInput, systemMessage, toClaudeResponse };
|
|
@@ -3,7 +3,7 @@ import { f as countLines } from "../../home-state-oUGFB4ds.mjs";
|
|
|
3
3
|
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
4
4
|
import { i as parseApplyPatch, n as canonicalizeMcpToolName, r as canonicalizeCodexShellTool, t as isBypassPermissions } from "../../permission-mode-B-SFyR0X.mjs";
|
|
5
5
|
import { t as commandToString } from "../../command-string-CALMTnwN.mjs";
|
|
6
|
-
import { c as readClaudeInput, i as denyResponse, r as contextResponse, s as informResponse } from "../../claude-
|
|
6
|
+
import { c as readClaudeInput, i as denyResponse, r as contextResponse, s as informResponse } from "../../claude-D62hkUfS.mjs";
|
|
7
7
|
//#region src/adapters/codex/index.ts
|
|
8
8
|
/**
|
|
9
9
|
* OpenAI Codex CLI adapter (hook-mode). Codex's `PreToolUse` hook (since 2026)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as evaluate } from "../../evaluate-CMiIqHeH.mjs";
|
|
2
2
|
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
3
|
-
import { t as extractCursorEvent } from "../../normalize-
|
|
3
|
+
import { t as extractCursorEvent } from "../../normalize-Dy8g9Ybl.mjs";
|
|
4
4
|
//#region src/adapters/cursor/index.ts
|
|
5
5
|
/** Cursor hook adapter; post-edit handling remains observe-only. */
|
|
6
6
|
function guardCursor(payload) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-
|
|
1
|
+
import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-ByopGx6C.mjs";
|
|
2
2
|
export { guard, readHermesInput, toHermesResponse };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as toKimiResponse, n as kimiDenyResponse, r as readKimiInput, t as guard } from "../../kimi-
|
|
1
|
+
import { i as toKimiResponse, n as kimiDenyResponse, r as readKimiInput, t as guard } from "../../kimi-C-Oia9q-.mjs";
|
|
2
2
|
export { guard, kimiDenyResponse, readKimiInput, toKimiResponse };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { t as HOME_DIR } from "./dotenv-C1LkcfW-.mjs";
|
|
2
|
+
//#region src/policy/apex-target.ts
|
|
3
|
+
/**
|
|
4
|
+
* Harness-target helpers for the APEX context builders. Single source of
|
|
5
|
+
* truth for the home-dir segment / root doc name / plan-tool idiom that
|
|
6
|
+
* differ between Claude Code and Codex (and beyond), so the builders in
|
|
7
|
+
* `claude-md-context.ts`, `apex-task-context.ts`, and `inject-apex.ts` never
|
|
8
|
+
* hardcode `.claude`/`CLAUDE.md`/`TaskCreate` again. Runtime `id` values flow
|
|
9
|
+
* through the pipeline as plain `string` (see `handleHook(id: string, ...)`),
|
|
10
|
+
* so these helpers accept `string` and narrow internally — same idiom as the
|
|
11
|
+
* existing `HOME_DIR[id] ?? ".claude"` precedent in `cartographer/detect.ts`.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Home-dir segment for a harness target (".claude", ".codex", ...). Falls
|
|
15
|
+
* back to ".claude" for any harness without a {@link HOME_DIR} entry.
|
|
16
|
+
* @param id - Harness id (e.g. "claude-code", "codex").
|
|
17
|
+
* @returns The home-dir segment, e.g. ".claude".
|
|
18
|
+
*/
|
|
19
|
+
function harnessHomeSegment(id) {
|
|
20
|
+
return HOME_DIR[id] ?? ".claude";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Root instructions doc name for a harness target: "AGENTS.md" for Codex and
|
|
24
|
+
* Kimi Code CLI (both documented conventions), "CLAUDE.md" for every other
|
|
25
|
+
* target (including the default/unknown fallback).
|
|
26
|
+
* @param id - Harness id.
|
|
27
|
+
* @returns The doc file name.
|
|
28
|
+
*/
|
|
29
|
+
function apexDocName(id) {
|
|
30
|
+
return id === "codex" || id === "kimi" ? "AGENTS.md" : "CLAUDE.md";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Native plan/track tool name for a harness target: Codex's `update_plan`,
|
|
34
|
+
* Kimi Code CLI's `TodoList`, or Claude's `TaskCreate` (default). Covers only
|
|
35
|
+
* the PLAN-step line — other target-specific tool references (`TaskList`,
|
|
36
|
+
* `TaskUpdate`) are resolved via `apexTaskSteps`/`apexAgentSteps` in
|
|
37
|
+
* `apex-target-steps.ts` since their surrounding prose differs structurally,
|
|
38
|
+
* not just by token.
|
|
39
|
+
* @param id - Harness id.
|
|
40
|
+
* @returns The plan-tool name.
|
|
41
|
+
*/
|
|
42
|
+
function apexPlanTool(id) {
|
|
43
|
+
if (id === "codex") return "update_plan";
|
|
44
|
+
if (id === "kimi") return "TodoList";
|
|
45
|
+
return "TaskCreate";
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { apexPlanTool as n, harnessHomeSegment as r, apexDocName as t };
|
|
@@ -0,0 +1,467 @@
|
|
|
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
|
+
export { withRouterStatus as C, prdDir as D, isPrdScopedPath as E, prdRouterPath as O, subTasksOf as S, classifyPrdPath as T, readTaskFileSync as _, isPrdEnabled as a, filesOf as b, crossCheckTask as c, readAgentReport as d, readAgentReportSync as f, readTaskFile as g, readRouterSync as h, taskStart as i, hasAnyViolations as l, readRouter as m, taskComplete as n, isPrdFlagSet as o, readAllTaskFiles as p, taskCreate as r, prdProjectRoot as s, acquireLock as t, incompleteSubTasks as u, writeRouter as v, withSubTaskValidated as w, isCompacted as x, writeTaskFile as y };
|