@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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as ttlLabel } from "./ttl-Dgwg_QAv.mjs";
|
|
2
2
|
import { n as fuseHarnessHome } from "./home-state-oUGFB4ds.mjs";
|
|
3
3
|
import { a as writeJsonFile, i as readJsonFile, t as atomicWrite } from "./json-io-DisYd2fb.mjs";
|
|
4
4
|
import { i as resolveSessions } from "./doc-helpers-CWZegVdR.mjs";
|
|
5
5
|
import { t as routeReferences } from "./router-PKVNBHge.mjs";
|
|
6
|
-
import { t as resolveSkillPath } from "./skill-path-
|
|
6
|
+
import { t as resolveSkillPath } from "./skill-path-Cz8WFaGu.mjs";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { createHmac, randomBytes } from "node:crypto";
|
|
@@ -142,6 +142,8 @@ const apexAuthorizationGate = (ctx) => {
|
|
|
142
142
|
};
|
|
143
143
|
//#endregion
|
|
144
144
|
//#region src/tracking/session-state.ts
|
|
145
|
+
/** Cap on {@link SessionTrack.prdViolations} (mirrors the `receipts` style bound — oldest evicted first). */
|
|
146
|
+
const PRD_VIOLATIONS_CAP = 50;
|
|
145
147
|
/** A fresh, empty track. */
|
|
146
148
|
function emptyTrack() {
|
|
147
149
|
return {
|
|
@@ -244,6 +246,45 @@ function recordBrainstormRequired(track, required) {
|
|
|
244
246
|
brainstormRequired: required
|
|
245
247
|
};
|
|
246
248
|
}
|
|
249
|
+
/** Bind `agentId` to its resolved PRD agent-report `name` (merge; idempotent no-op if already bound to the same name). Immutable. */
|
|
250
|
+
function recordPrdOwner(track, agentId, name) {
|
|
251
|
+
if (track.prdOwners?.[agentId] === name) return track;
|
|
252
|
+
return {
|
|
253
|
+
...track,
|
|
254
|
+
prdOwners: {
|
|
255
|
+
...track.prdOwners,
|
|
256
|
+
[agentId]: name
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Append one PRD cross-check violation. Immutable, uncapped in the mutator
|
|
262
|
+
* itself — same append-only shape as {@link Receipt} — so the journal diff's
|
|
263
|
+
* prefix comparison (`tail`, track-diff.ts) never sees an eviction it would
|
|
264
|
+
* mistake for a bulk rewrite. The {@link PRD_VIOLATIONS_CAP} bound is applied
|
|
265
|
+
* only when the journal is folded/read (track-journal.ts's `foldEvents`),
|
|
266
|
+
* mirroring how `trivialEdits`'s sliding window is re-derived at fold time.
|
|
267
|
+
*/
|
|
268
|
+
function recordPrdViolation(track, v) {
|
|
269
|
+
return {
|
|
270
|
+
...track,
|
|
271
|
+
prdViolations: [...track.prdViolations ?? [], v]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Record a PRD SubagentStop/Stop one-shot block marker (merge). Immutable. */
|
|
275
|
+
function recordPrdStopBlocked(track, key, ts) {
|
|
276
|
+
return {
|
|
277
|
+
...track,
|
|
278
|
+
prdStopBlocked: {
|
|
279
|
+
...track.prdStopBlocked,
|
|
280
|
+
[key]: ts
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/** True when the PRD one-shot block for `key` has already fired. */
|
|
285
|
+
function prdAlreadyBlocked(track, key) {
|
|
286
|
+
return track.prdStopBlocked?.[key] !== void 0;
|
|
287
|
+
}
|
|
247
288
|
//#endregion
|
|
248
289
|
//#region src/tracking/integrity.ts
|
|
249
290
|
/**
|
|
@@ -572,6 +613,9 @@ function foldEvents(events, base = emptyTrack()) {
|
|
|
572
613
|
const at = { ...base.refsReadAt };
|
|
573
614
|
const receipts = [...base.receipts ?? []];
|
|
574
615
|
const trivial = new Set(base.trivialEdits ?? []);
|
|
616
|
+
const prdOwners = { ...base.prdOwners };
|
|
617
|
+
const prdViolations = [...base.prdViolations ?? []];
|
|
618
|
+
const prdStopBlocked = { ...base.prdStopBlocked };
|
|
575
619
|
let targetTs = base.target ? Date.parse(base.target.set_at) || 0 : 0, brainstormTs = 0;
|
|
576
620
|
for (const ev of [...events].sort((a, b) => a.ts - b.ts)) if (ev.field === "refsRead") {
|
|
577
621
|
const p = String(ev.value);
|
|
@@ -591,9 +635,19 @@ function foldEvents(events, base = emptyTrack()) {
|
|
|
591
635
|
} else if (ev.field === "brainstormRequired" && ev.ts >= brainstormTs) {
|
|
592
636
|
t.brainstormRequired = Boolean(ev.value);
|
|
593
637
|
brainstormTs = ev.ts;
|
|
638
|
+
} else if (ev.field === "prdOwners") {
|
|
639
|
+
const [aid, name] = ev.value;
|
|
640
|
+
prdOwners[aid] = name;
|
|
641
|
+
} else if (ev.field === "prdViolations") prdViolations.push(ev.value);
|
|
642
|
+
else if (ev.field === "prdStopBlocked") {
|
|
643
|
+
const [key, ts] = ev.value;
|
|
644
|
+
prdStopBlocked[key] = ts;
|
|
594
645
|
}
|
|
595
646
|
if (Object.keys(at).length) t.refsReadAt = at;
|
|
596
647
|
if (receipts.length) t.receipts = receipts;
|
|
648
|
+
if (Object.keys(prdOwners).length) t.prdOwners = prdOwners;
|
|
649
|
+
if (prdViolations.length) t.prdViolations = prdViolations.slice(-50);
|
|
650
|
+
if (Object.keys(prdStopBlocked).length) t.prdStopBlocked = prdStopBlocked;
|
|
597
651
|
const maxT = Math.max(0, ...trivial);
|
|
598
652
|
t.trivialEdits = [...trivial].filter((x) => x > maxT - TRIVIAL_WINDOW_MS).sort((a, b) => a - b);
|
|
599
653
|
return t;
|
|
@@ -673,6 +727,24 @@ function diffTrackEvents(prev, next, now) {
|
|
|
673
727
|
value: next.brainstormRequired,
|
|
674
728
|
ts: now
|
|
675
729
|
});
|
|
730
|
+
for (const [aid, name] of Object.entries(next.prdOwners ?? {})) if (prev.prdOwners?.[aid] !== name) out.push({
|
|
731
|
+
field: "prdOwners",
|
|
732
|
+
op: "merge",
|
|
733
|
+
value: [aid, name],
|
|
734
|
+
ts: now
|
|
735
|
+
});
|
|
736
|
+
for (const v of tail(prev.prdViolations ?? [], next.prdViolations ?? [])) out.push({
|
|
737
|
+
field: "prdViolations",
|
|
738
|
+
op: "append",
|
|
739
|
+
value: v,
|
|
740
|
+
ts: v.ts ?? now
|
|
741
|
+
});
|
|
742
|
+
for (const [key, ts] of Object.entries(next.prdStopBlocked ?? {})) if (prev.prdStopBlocked?.[key] !== ts) out.push({
|
|
743
|
+
field: "prdStopBlocked",
|
|
744
|
+
op: "merge",
|
|
745
|
+
value: [key, ts],
|
|
746
|
+
ts
|
|
747
|
+
});
|
|
676
748
|
return out;
|
|
677
749
|
}
|
|
678
750
|
/** The journal log path twin of a track snapshot path. */
|
|
@@ -842,4 +914,4 @@ async function withTrack(file, mutate) {
|
|
|
842
914
|
}) !== LOCK_FAILED;
|
|
843
915
|
}
|
|
844
916
|
//#endregion
|
|
845
|
-
export {
|
|
917
|
+
export { recordPrdViolation as C, trivialCount as D, recordTrivialEdit as E, apexAuthorizationGate as O, recordPrdStopBlocked as S, recordTarget as T, prdAlreadyBlocked as _, journalLogPath as a, recordDoc as b, appendEvent as c, signTrack as d, verifyTrack as f, emptyTrack as g, agentsFresh as h, withTrack as i, withTrackLockSync as l, PRD_VIOLATIONS_CAP as m, saveTrack as n, readTrackSync as o, writeLastNonce as p, trackJournalEnabled as r, diffTrackEvents as s, loadTrack as t, LOCK_FAILED as u, recordAgent as v, recordRefRead as w, recordPrdOwner as x, recordBrainstormRequired as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as trivialCount, a as agentsFresh, c as recordAgent, d as recordPrdOwner, f as recordPrdStopBlocked, g as recordTrivialEdit, h as recordTarget, i as SessionTrack, l as recordBrainstormRequired, m as recordRefRead, n as PRD_VIOLATIONS_CAP, o as emptyTrack, p as recordPrdViolation, r as PrdViolationRecord, s as prdAlreadyBlocked, t as AgentQuality, u as recordDoc } from "../session-state-COg7Ej_2.mjs";
|
|
2
2
|
//#region src/tracking/store.d.ts
|
|
3
3
|
/** Journal mode is the default; `FUSE_TRACK_JOURNAL=0` reverts to the legacy locked RMW (see the module NOTE: =0 ignores the uncompacted journal — recoverable, keep the `.log`). */
|
|
4
4
|
declare function trackJournalEnabled(): boolean;
|
|
@@ -26,4 +26,4 @@ declare function saveTrack(file: string, track: SessionTrack): Promise<void>;
|
|
|
26
26
|
*/
|
|
27
27
|
declare function withTrack(file: string, mutate: (track: SessionTrack) => SessionTrack | Promise<SessionTrack>): Promise<boolean>;
|
|
28
28
|
//#endregion
|
|
29
|
-
export { AgentQuality, SessionTrack, agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
|
29
|
+
export { AgentQuality, PRD_VIOLATIONS_CAP, PrdViolationRecord, SessionTrack, agentsFresh, emptyTrack, loadTrack, prdAlreadyBlocked, recordAgent, recordBrainstormRequired, recordDoc, recordPrdOwner, recordPrdStopBlocked, recordPrdViolation, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
package/dist/tracking/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
|
1
|
+
import { C as recordPrdViolation, D as trivialCount, E as recordTrivialEdit, S as recordPrdStopBlocked, T as recordTarget, _ as prdAlreadyBlocked, b as recordDoc, g as emptyTrack, h as agentsFresh, i as withTrack, m as PRD_VIOLATIONS_CAP, n as saveTrack, r as trackJournalEnabled, t as loadTrack, v as recordAgent, w as recordRefRead, x as recordPrdOwner, y as recordBrainstormRequired } from "../store-BVY6gIYM.mjs";
|
|
2
|
+
export { PRD_VIOLATIONS_CAP, agentsFresh, emptyTrack, loadTrack, prdAlreadyBlocked, recordAgent, recordBrainstormRequired, recordDoc, recordPrdOwner, recordPrdStopBlocked, recordPrdViolation, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { l as parseEnvInt } from "./limits-DG5pl-5s.mjs";
|
|
2
|
+
//#region src/config/ttl.ts
|
|
3
|
+
/**
|
|
4
|
+
* Default enforcement-freshness window, in seconds (2 minutes). Matches the
|
|
5
|
+
* plugin's original `FUSE_ENFORCE_TTL_SEC` default. This is the constant that
|
|
6
|
+
* actually reaches production (via `resolveTtlSec` -> `bin.ts`); `gate.ts`'s
|
|
7
|
+
* `DEFAULT_WINDOW_MS` is only a fallback for direct programmatic callers that
|
|
8
|
+
* omit `windowMs` (e.g. tests) and never applies on the real CLI path.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_TTL_SEC = 120;
|
|
11
|
+
/** Default env var name carrying the TTL override. */
|
|
12
|
+
const TTL_ENV_KEY = "FUSE_ENFORCE_TTL_SEC";
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the enforcement TTL (seconds) from an env map.
|
|
15
|
+
* @param env - environment map (defaults to `process.env`)
|
|
16
|
+
* @param key - env var name (defaults to `FUSE_ENFORCE_TTL_SEC`)
|
|
17
|
+
*/
|
|
18
|
+
function resolveTtlSec(env = process.env, key = TTL_ENV_KEY) {
|
|
19
|
+
return parseEnvInt(env[key], 120);
|
|
20
|
+
}
|
|
21
|
+
/** Human label for a TTL: 120 -> "2min", 240 -> "4min", 90 -> "90s". */
|
|
22
|
+
function ttlLabel(sec) {
|
|
23
|
+
return sec % 60 === 0 ? `${sec / 60}min` : `${sec}s`;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { ttlLabel as i, TTL_ENV_KEY as n, resolveTtlSec as r, DEFAULT_TTL_SEC as t };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { a as resolveMaxLines, c as storeBudget, i as hookBudget } from "./limits-DG5pl-5s.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { i as ttlLabel } from "./ttl-Dgwg_QAv.mjs";
|
|
3
3
|
import { A as moduleAwarePath, S as isHooksPath, T as isStoresPath, _ as rolloutVerdict, at as maskCommentsOnly, it as maskCommentsAndStrings, nt as nearestManifestDir, ot as DEV_KEYWORDS, rt as projectCaps, x as isComponentsPath, y as declaresInterface } from "./evaluate-CMiIqHeH.mjs";
|
|
4
4
|
import { d as countFrameworkCodeLines, l as PLUGINS_DIR, u as SOLID_REF } from "./home-state-oUGFB4ds.mjs";
|
|
5
5
|
import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-CWZegVdR.mjs";
|
|
6
6
|
import { t as routeReferences } from "./router-PKVNBHge.mjs";
|
|
7
|
-
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-
|
|
7
|
+
import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-Cz8WFaGu.mjs";
|
|
8
|
+
import { n as apexPlanTool, r as harnessHomeSegment, t as apexDocName } from "./apex-target-Xc2M32Pl.mjs";
|
|
8
9
|
import { dirname, extname, join, resolve } from "node:path";
|
|
9
10
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
@@ -1446,52 +1447,6 @@ function getExpertAgent(type, pluginsDirOverride) {
|
|
|
1446
1447
|
return "general-purpose";
|
|
1447
1448
|
}
|
|
1448
1449
|
//#endregion
|
|
1449
|
-
//#region src/policy/apex-target.ts
|
|
1450
|
-
/**
|
|
1451
|
-
* Harness-target helpers for the APEX context builders. Single source of
|
|
1452
|
-
* truth for the home-dir segment / root doc name / plan-tool idiom that
|
|
1453
|
-
* differ between Claude Code and Codex (and beyond), so the builders in
|
|
1454
|
-
* `claude-md-context.ts`, `apex-task-context.ts`, and `inject-apex.ts` never
|
|
1455
|
-
* hardcode `.claude`/`CLAUDE.md`/`TaskCreate` again. Runtime `id` values flow
|
|
1456
|
-
* through the pipeline as plain `string` (see `handleHook(id: string, ...)`),
|
|
1457
|
-
* so these helpers accept `string` and narrow internally — same idiom as the
|
|
1458
|
-
* existing `HOME_DIR[id] ?? ".claude"` precedent in `cartographer/detect.ts`.
|
|
1459
|
-
*/
|
|
1460
|
-
/**
|
|
1461
|
-
* Home-dir segment for a harness target (".claude", ".codex", ...). Falls
|
|
1462
|
-
* back to ".claude" for any harness without a {@link HOME_DIR} entry.
|
|
1463
|
-
* @param id - Harness id (e.g. "claude-code", "codex").
|
|
1464
|
-
* @returns The home-dir segment, e.g. ".claude".
|
|
1465
|
-
*/
|
|
1466
|
-
function harnessHomeSegment(id) {
|
|
1467
|
-
return HOME_DIR[id] ?? ".claude";
|
|
1468
|
-
}
|
|
1469
|
-
/**
|
|
1470
|
-
* Root instructions doc name for a harness target: "AGENTS.md" for Codex and
|
|
1471
|
-
* Kimi Code CLI (both documented conventions), "CLAUDE.md" for every other
|
|
1472
|
-
* target (including the default/unknown fallback).
|
|
1473
|
-
* @param id - Harness id.
|
|
1474
|
-
* @returns The doc file name.
|
|
1475
|
-
*/
|
|
1476
|
-
function apexDocName(id) {
|
|
1477
|
-
return id === "codex" || id === "kimi" ? "AGENTS.md" : "CLAUDE.md";
|
|
1478
|
-
}
|
|
1479
|
-
/**
|
|
1480
|
-
* Native plan/track tool name for a harness target: Codex's `update_plan`,
|
|
1481
|
-
* Kimi Code CLI's `TodoList`, or Claude's `TaskCreate` (default). Covers only
|
|
1482
|
-
* the PLAN-step line — other target-specific tool references (`TaskList`,
|
|
1483
|
-
* `TaskUpdate`) are resolved via `apexTaskSteps`/`apexAgentSteps` in
|
|
1484
|
-
* `apex-target-steps.ts` since their surrounding prose differs structurally,
|
|
1485
|
-
* not just by token.
|
|
1486
|
-
* @param id - Harness id.
|
|
1487
|
-
* @returns The plan-tool name.
|
|
1488
|
-
*/
|
|
1489
|
-
function apexPlanTool(id) {
|
|
1490
|
-
if (id === "codex") return "update_plan";
|
|
1491
|
-
if (id === "kimi") return "TodoList";
|
|
1492
|
-
return "TaskCreate";
|
|
1493
|
-
}
|
|
1494
|
-
//#endregion
|
|
1495
1450
|
//#region src/policy/detect-claude-md-project-type.ts
|
|
1496
1451
|
/**
|
|
1497
1452
|
* Detect the project type from the cwd, reproducing the legacy Python logic:
|
|
@@ -1907,4 +1862,4 @@ function missingSeoElements(html) {
|
|
|
1907
1862
|
return missing;
|
|
1908
1863
|
}
|
|
1909
1864
|
//#endregion
|
|
1910
|
-
export {
|
|
1865
|
+
export { countStores as A, PRE_AUTH_GATES as B, usesTailwindUtilities as C, declaresQueryHook as D, declaresCustomHook as E, MAX_TOKENS as F, solidReadGate as G, docConsultedGate as H, capVerbosity as I, detectCreationIntent as L, isExcludedJsPath as M, isExcludedSwiftPath as N, importsTanstackQuery as O, MAX_EXA_RESULTS as P, APEX_GATES as R, skillTriggerGate as S, frameworkSolidGate as T, evaluateApex as U, brainstormGate as V, freshnessGate as W, detectClaudeMdProjectType as _, firstHeading as a, parseField as b, EXCLUDE_DIRS as c, buildApexTaskInjection as d, loadApexTaskState as f, buildClaudeMdContext as g, buildApexInstruction as h, firstComment as i, declaresStore as j, queryCapActive as k, PROJECT_INDICATORS as l, DEV_VERBS as m, missingSeoElements as n, parseEnrichment as o, apexAgentSteps as p, descFromText as r, parseEntry as s, isHtmlLike as t, buildApexTaskContext as u, scanPlugin as v, SKILL_TRIGGERS as w, detectRequiredSkills as x, parseBodyDesc as y, POST_AUTH_GATES as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.94",
|
|
4
4
|
"description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|
package/src/cli/bin.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { runDoctor, runningVersion, versionBanner } from "./doctor";
|
|
|
23
23
|
import { parseScope } from "./scope";
|
|
24
24
|
import { isMalformedCursorStdin, isOversize, oversizeStdout, readStdin, traceHook } from "./hook-io";
|
|
25
25
|
import { maybePlaySound } from "./hook-sound";
|
|
26
|
+
import { runPrd } from "./prd";
|
|
26
27
|
|
|
27
28
|
const cmd = process.argv[2];
|
|
28
29
|
|
|
@@ -77,6 +78,8 @@ if (cmd === "--version" || cmd === "-v") {
|
|
|
77
78
|
const dir = process.argv[3] ?? process.cwd();
|
|
78
79
|
process.stdout.write(JSON.stringify(runSecurityScan(dir), null, 2) + "\n");
|
|
79
80
|
process.exit(0);
|
|
81
|
+
} else if (cmd === "prd") {
|
|
82
|
+
process.exit(await runPrd(process.argv.slice(3), process.cwd(), process.env));
|
|
80
83
|
} else if (cmd === "codex-rules") {
|
|
81
84
|
const outIdx = process.argv.indexOf("--out");
|
|
82
85
|
const outPath = outIdx !== -1 ? process.argv[outIdx + 1] : undefined;
|
package/src/cli/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness prd compact <task> [--id] [--root]` — collapses every fully
|
|
3
|
+
* validated agent entry of a task-PRD to its compacted shape. Requires
|
|
4
|
+
* `FUSE_PRD=1`; refuses (exit 1) when any sub-task is not yet `validated`.
|
|
5
|
+
*/
|
|
6
|
+
import { writeTaskFile } from "../../policy/prd/prd-io";
|
|
7
|
+
import { compactTaskFile } from "../../policy/prd/prd-compact";
|
|
8
|
+
import { isCompacted, subTasksOf } from "../../policy/prd/prd-schema";
|
|
9
|
+
import { requireFusePrd, resolveTaskFile, withPrdLock } from "./shared";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run `harness prd compact`. Exit 0 on success (including a no-op compact),
|
|
13
|
+
* exit 1 when `FUSE_PRD` is unset, a sub-task is unvalidated, or the lock is
|
|
14
|
+
* held, exit 2 on usage/lookup errors.
|
|
15
|
+
*/
|
|
16
|
+
export async function runPrdCompact(argv: string[], cwd: string, env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
17
|
+
const resolved = requireFusePrd(argv, cwd, env, "compact");
|
|
18
|
+
if (!resolved.ok) {
|
|
19
|
+
process.stderr.write(resolved.message + "\n");
|
|
20
|
+
return resolved.code;
|
|
21
|
+
}
|
|
22
|
+
const { root, homeSeg } = resolved;
|
|
23
|
+
|
|
24
|
+
const tf = await resolveTaskFile(argv, root, homeSeg, "usage: harness prd compact <task>");
|
|
25
|
+
if (!tf.ok) {
|
|
26
|
+
process.stderr.write(tf.message + "\n");
|
|
27
|
+
return tf.code;
|
|
28
|
+
}
|
|
29
|
+
const { routerEntry, taskFile } = tf;
|
|
30
|
+
|
|
31
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
32
|
+
if (isCompacted(entry)) continue;
|
|
33
|
+
for (const [sub, status] of Object.entries(subTasksOf(entry))) {
|
|
34
|
+
if (status.status !== "validated") {
|
|
35
|
+
process.stderr.write(`prd compact: sub-task "${sub}" of agent "${agent}" is not validated\n`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await withPrdLock(root, homeSeg, async () => {
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const { file, compacted } = compactTaskFile(taskFile, now);
|
|
44
|
+
await writeTaskFile(root, homeSeg, routerEntry.prd, file);
|
|
45
|
+
return compacted;
|
|
46
|
+
});
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
process.stderr.write(`prd compact: ${result.message}\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
process.stdout.write(result.value.length > 0 ? `compacted: ${result.value.join(", ")}\n` : "nothing to compact\n");
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering helpers for `harness prd` output: an aligned ASCII table for the
|
|
3
|
+
* human-readable path, and pretty JSON for `--json`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Pad `s` with trailing spaces to `width`. */
|
|
7
|
+
function pad(s: string, width: number): string {
|
|
8
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Render an aligned ASCII table from a header row + data rows. */
|
|
12
|
+
export function renderTable(header: string[], rows: string[][]): string {
|
|
13
|
+
const all: string[][] = [header, ...rows];
|
|
14
|
+
const widths = header.map((_, col) => Math.max(...all.map((r) => (r[col] ?? "").length)));
|
|
15
|
+
return all
|
|
16
|
+
.map((r) => r.map((cell, col) => pad(cell ?? "", widths[col] ?? 0)).join(" ").trimEnd())
|
|
17
|
+
.join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Render `data` as pretty-printed JSON with a trailing newline. */
|
|
21
|
+
export function renderJson(data: unknown): string {
|
|
22
|
+
return JSON.stringify(data, null, 2) + "\n";
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness prd <status|validate|compact>` — CLI entry point for the PRD
|
|
3
|
+
* (task/agent ownership coordination) module.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./resolve";
|
|
6
|
+
export * from "./format";
|
|
7
|
+
export { runPrdStatus } from "./status";
|
|
8
|
+
export { runPrdValidate } from "./validate";
|
|
9
|
+
export { runPrdCompact } from "./compact";
|
|
10
|
+
|
|
11
|
+
import { runPrdStatus } from "./status";
|
|
12
|
+
import { runPrdValidate } from "./validate";
|
|
13
|
+
import { runPrdCompact } from "./compact";
|
|
14
|
+
|
|
15
|
+
/** Dispatch `harness prd <sub> ...rest` to the matching `run*` function. */
|
|
16
|
+
export async function runPrd(argv: string[], cwd: string, env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
17
|
+
const [sub, ...rest] = argv;
|
|
18
|
+
if (sub === "status") return runPrdStatus(rest, cwd, env);
|
|
19
|
+
if (sub === "validate") return runPrdValidate(rest, cwd, env);
|
|
20
|
+
if (sub === "compact") return runPrdCompact(rest, cwd, env);
|
|
21
|
+
process.stderr.write(`harness prd: unknown sub-command "${sub ?? ""}" (expected status|validate|compact)\n`);
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing + harness resolution for `harness prd <cmd>`. Resolves
|
|
3
|
+
* `--root` (default `cwd`) and `--id`/`homeSeg` (default: the sole
|
|
4
|
+
* `HOME_DIR` (`config/dotenv.ts`) segment under which
|
|
5
|
+
* `<root>/<seg>/apex/prd.json` exists — ambiguous or absent is a caller
|
|
6
|
+
* error, never guessed).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { HOME_DIR, loadDotenv } from "../../config/dotenv";
|
|
12
|
+
import { harnessHomeSegment } from "../../policy/apex-target";
|
|
13
|
+
import type { HarnessId } from "../../detect/interfaces/types";
|
|
14
|
+
|
|
15
|
+
const VALUE_FLAGS: ReadonlySet<string> = new Set(["--id", "--root"]);
|
|
16
|
+
const BOOLEAN_FLAGS: ReadonlySet<string> = new Set(["--json"]);
|
|
17
|
+
|
|
18
|
+
/** Resolved `--root`/`--id` pair for a `prd` sub-command. */
|
|
19
|
+
export interface PrdResolved {
|
|
20
|
+
root: string;
|
|
21
|
+
homeSeg: string;
|
|
22
|
+
id: HarnessId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Result of {@link resolvePrdArgs}: resolved args, or a usage error (exit 2). */
|
|
26
|
+
export type PrdResolveResult = { ok: true; value: PrdResolved } | { ok: false; message: string };
|
|
27
|
+
|
|
28
|
+
/** Read `--<name> <value>` from argv, or `undefined` when absent. */
|
|
29
|
+
export function readFlag(argv: string[], name: string): string | undefined {
|
|
30
|
+
const idx = argv.indexOf(name);
|
|
31
|
+
return idx !== -1 ? argv[idx + 1] : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True when `--json` is present in argv. */
|
|
35
|
+
export function hasJsonFlag(argv: string[]): boolean {
|
|
36
|
+
return argv.includes("--json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Non-flag tokens in argv, skipping known flags and their values. */
|
|
40
|
+
export function positionalArgs(argv: string[]): string[] {
|
|
41
|
+
const out: string[] = [];
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const tok = argv[i];
|
|
44
|
+
if (tok === undefined) continue;
|
|
45
|
+
if (VALUE_FLAGS.has(tok)) { i++; continue; }
|
|
46
|
+
if (BOOLEAN_FLAGS.has(tok)) continue;
|
|
47
|
+
out.push(tok);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve `{root, homeSeg, id}` for a `prd` sub-command. `--id` wins
|
|
54
|
+
* outright; otherwise auto-detects the sole `HOME_DIR` segment under which
|
|
55
|
+
* `<root>/<seg>/apex/prd.json` exists.
|
|
56
|
+
*/
|
|
57
|
+
export function resolvePrdArgs(argv: string[], cwd: string): PrdResolveResult {
|
|
58
|
+
const root = readFlag(argv, "--root") ?? cwd;
|
|
59
|
+
const idFlag = readFlag(argv, "--id");
|
|
60
|
+
if (idFlag) {
|
|
61
|
+
return { ok: true, value: { root, homeSeg: harnessHomeSegment(idFlag), id: idFlag as HarnessId } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const matches: Array<{ id: HarnessId; seg: string }> = [];
|
|
65
|
+
for (const [id, seg] of Object.entries(HOME_DIR) as Array<[HarnessId, string]>) {
|
|
66
|
+
if (existsSync(join(root, seg, "apex", "prd.json"))) matches.push({ id, seg });
|
|
67
|
+
}
|
|
68
|
+
if (matches.length === 0) {
|
|
69
|
+
return { ok: false, message: `no PRD router found under ${root}/<home>/apex/prd.json — pass --id <harness>` };
|
|
70
|
+
}
|
|
71
|
+
if (matches.length > 1) {
|
|
72
|
+
return { ok: false, message: `ambiguous harness: PRD routers found for ${matches.map((m) => m.id).join(", ")} — pass --id` };
|
|
73
|
+
}
|
|
74
|
+
const only = matches[0] as { id: HarnessId; seg: string };
|
|
75
|
+
return { ok: true, value: { root, homeSeg: only.seg, id: only.id } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Shared first step of every `prd` sub-command: {@link resolvePrdArgs}, then
|
|
80
|
+
* (on success) `loadDotenv` the resolved harness's home `.env` + `<root>/.env`
|
|
81
|
+
* as a side effect on `env` — same env-loading contract as the `hook` branch
|
|
82
|
+
* (`bin.ts`'s `loadDotenv(id)` call).
|
|
83
|
+
*/
|
|
84
|
+
export function resolveAndLoadEnv(argv: string[], cwd: string, env: NodeJS.ProcessEnv = process.env): PrdResolveResult {
|
|
85
|
+
const resolved = resolvePrdArgs(argv, cwd);
|
|
86
|
+
if (resolved.ok) loadDotenv(resolved.value.id, env, homedir(), resolved.value.root);
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for `status`/`validate`/`compact`: root+env resolution,
|
|
3
|
+
* the `FUSE_PRD` write gate, task-PRD lookup, and the `.lock`-guarded write.
|
|
4
|
+
* All return a result instead of calling `process.exit` themselves —
|
|
5
|
+
* callers own the exit code (extracted to kill a jscpd-measured clone
|
|
6
|
+
* across the three `run*` functions).
|
|
7
|
+
*/
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { prdDir } from "../../policy/prd/prd-paths";
|
|
10
|
+
import { readRouter, readTaskFile } from "../../policy/prd/prd-io";
|
|
11
|
+
import { isPrdFlagSet } from "../../policy/prd/prd-enabled";
|
|
12
|
+
import type { PrdRouter, PrdRouterEntry, PrdTaskFile } from "../../policy/prd/interfaces/types";
|
|
13
|
+
import { acquireLock } from "../../runtime/lifecycle/aipilot/apex-task-store";
|
|
14
|
+
import { positionalArgs, resolveAndLoadEnv } from "./resolve";
|
|
15
|
+
|
|
16
|
+
/** `{root, homeSeg}` on success, or an exit code + message on failure. */
|
|
17
|
+
export type PrdStepResult = { ok: true; root: string; homeSeg: string } | { ok: false; code: number; message: string };
|
|
18
|
+
|
|
19
|
+
/** {@link resolveAndLoadEnv}, narrowed to `{root, homeSeg}` — the common first step of every sub-command. */
|
|
20
|
+
export function resolveRoot(argv: string[], cwd: string, env: NodeJS.ProcessEnv): PrdStepResult {
|
|
21
|
+
const resolved = resolveAndLoadEnv(argv, cwd, env);
|
|
22
|
+
if (!resolved.ok) return { ok: false, code: 2, message: resolved.message };
|
|
23
|
+
return { ok: true, root: resolved.value.root, homeSeg: resolved.value.homeSeg };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** {@link resolveRoot} + the `FUSE_PRD=1` write gate shared by `validate`/`compact`. */
|
|
27
|
+
export function requireFusePrd(argv: string[], cwd: string, env: NodeJS.ProcessEnv, sub: string): PrdStepResult {
|
|
28
|
+
const resolved = resolveRoot(argv, cwd, env);
|
|
29
|
+
if (!resolved.ok) return resolved;
|
|
30
|
+
if (!isPrdFlagSet(env)) {
|
|
31
|
+
return { ok: false, code: 1, message: `prd ${sub} requires FUSE_PRD=1 in ${resolved.homeSeg}/.env or the project .env` };
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Result of {@link loadTaskFileFor}: the resolved router/task-PRD, or a usage error. */
|
|
37
|
+
export type PrdTaskLookup =
|
|
38
|
+
| { ok: true; router: PrdRouter; routerEntry: PrdRouterEntry; taskFile: PrdTaskFile }
|
|
39
|
+
| { ok: false; code: 2; message: string };
|
|
40
|
+
|
|
41
|
+
/** Reads the router + the named task's task-PRD file. Never calls `process.exit`. */
|
|
42
|
+
export async function loadTaskFileFor(root: string, homeSeg: string, task: string): Promise<PrdTaskLookup> {
|
|
43
|
+
const router = await readRouter(root, homeSeg);
|
|
44
|
+
const routerEntry = router?.[task];
|
|
45
|
+
if (!router || !routerEntry) return { ok: false, code: 2, message: `no such task "${task}" in PRD router` };
|
|
46
|
+
|
|
47
|
+
const taskFile = await readTaskFile(root, homeSeg, routerEntry.prd);
|
|
48
|
+
if (!taskFile) return { ok: false, code: 2, message: `task PRD not found or malformed: ${routerEntry.prd}` };
|
|
49
|
+
|
|
50
|
+
return { ok: true, router, routerEntry, taskFile };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Result of {@link resolveTaskFile}: the `<task>` positional plus its looked-up files, or a usage/lookup error. */
|
|
54
|
+
export type PrdTaskArgLookup =
|
|
55
|
+
| { ok: true; task: string; agentArg: string | undefined; router: PrdRouter; routerEntry: PrdRouterEntry; taskFile: PrdTaskFile }
|
|
56
|
+
| { ok: false; code: 2; message: string };
|
|
57
|
+
|
|
58
|
+
/** Extracts `<task> [agent]` from argv, then {@link loadTaskFileFor} — the shared `validate`/`compact` entry lookup. */
|
|
59
|
+
export async function resolveTaskFile(argv: string[], root: string, homeSeg: string, usage: string): Promise<PrdTaskArgLookup> {
|
|
60
|
+
const [task, agentArg] = positionalArgs(argv);
|
|
61
|
+
if (!task) return { ok: false, code: 2, message: usage };
|
|
62
|
+
const lookup = await loadTaskFileFor(root, homeSeg, task);
|
|
63
|
+
if (!lookup.ok) return lookup;
|
|
64
|
+
return { task, agentArg, ...lookup };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Result of {@link withPrdLock}. */
|
|
68
|
+
export type PrdLockResult<T> = { ok: true; value: T } | { ok: false; message: string };
|
|
69
|
+
|
|
70
|
+
/** Runs `fn` under the PRD `.lock` directory-lock. Never calls `process.exit`. */
|
|
71
|
+
export async function withPrdLock<T>(root: string, homeSeg: string, fn: () => Promise<T>): Promise<PrdLockResult<T>> {
|
|
72
|
+
const lockDir = join(prdDir(root, homeSeg), ".lock");
|
|
73
|
+
const release = await acquireLock(lockDir);
|
|
74
|
+
if (!release) return { ok: false, message: `lock held at ${lockDir}` };
|
|
75
|
+
try {
|
|
76
|
+
return { ok: true, value: await fn() };
|
|
77
|
+
} finally {
|
|
78
|
+
await release();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness prd status [--json] [--id <harness>] [--root <dir>]` — read-only
|
|
3
|
+
* PRD snapshot. Never requires `FUSE_PRD`. Exit 0 on success, exit 1 when no
|
|
4
|
+
* router is found, exit 2 on an unresolved/ambiguous `--id`.
|
|
5
|
+
*/
|
|
6
|
+
import { prdRouterPath } from "../../policy/prd/prd-paths";
|
|
7
|
+
import { readAgentReport, readAllTaskFiles, readRouter } from "../../policy/prd/prd-io";
|
|
8
|
+
import { crossCheckTask } from "../../policy/prd/prd-crosscheck";
|
|
9
|
+
import { isCompacted, subTasksOf } from "../../policy/prd/prd-schema";
|
|
10
|
+
import type { PrdAgentReportFile, PrdTaskFile } from "../../policy/prd/interfaces/types";
|
|
11
|
+
import { hasJsonFlag } from "./resolve";
|
|
12
|
+
import { renderJson, renderTable } from "./format";
|
|
13
|
+
import { resolveRoot } from "./shared";
|
|
14
|
+
|
|
15
|
+
const HEADER: string[] = ["Task", "Router status", "Agents", "Sub-tasks done/total", "Violations"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sub-task readiness counts for the "done/total" column: a sub-task counts as
|
|
19
|
+
* done once it is `validated` OR its owning agent's own report already shows
|
|
20
|
+
* `"done"` for it (readiness view — a report can land well before the
|
|
21
|
+
* coordinator runs `prd validate`). A compacted (fully-validated) entry
|
|
22
|
+
* counts as one done/one total unit — its per-sub-task detail is gone.
|
|
23
|
+
*/
|
|
24
|
+
function subTaskCounts(
|
|
25
|
+
taskFile: PrdTaskFile,
|
|
26
|
+
reports: Record<string, PrdAgentReportFile | null>,
|
|
27
|
+
task: string,
|
|
28
|
+
): { done: number; total: number } {
|
|
29
|
+
let done = 0;
|
|
30
|
+
let total = 0;
|
|
31
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
32
|
+
if (isCompacted(entry)) {
|
|
33
|
+
done++;
|
|
34
|
+
total++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const report = reports[agent];
|
|
38
|
+
for (const [sub, subTask] of Object.entries(subTasksOf(entry))) {
|
|
39
|
+
total++;
|
|
40
|
+
if (subTask.status === "validated" || report?.[task]?.[sub]?.status === "done") done++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { done, total };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run `harness prd status`. Prints an aligned table (or `--json` dump of
|
|
48
|
+
* `{router, taskFiles, reports}`) and returns the process exit code.
|
|
49
|
+
*/
|
|
50
|
+
export async function runPrdStatus(argv: string[], cwd: string, env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
51
|
+
const resolved = resolveRoot(argv, cwd, env);
|
|
52
|
+
if (!resolved.ok) {
|
|
53
|
+
process.stderr.write(resolved.message + "\n");
|
|
54
|
+
return resolved.code;
|
|
55
|
+
}
|
|
56
|
+
const { root, homeSeg } = resolved;
|
|
57
|
+
|
|
58
|
+
const router = await readRouter(root, homeSeg);
|
|
59
|
+
if (!router) {
|
|
60
|
+
process.stderr.write(`no PRD router at ${prdRouterPath(root, homeSeg)}\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const taskFiles = await readAllTaskFiles(root, homeSeg, router);
|
|
65
|
+
const agentNames = new Set<string>();
|
|
66
|
+
for (const tf of Object.values(taskFiles)) {
|
|
67
|
+
if (tf) for (const agent of Object.keys(tf)) agentNames.add(agent);
|
|
68
|
+
}
|
|
69
|
+
const reports: Record<string, PrdAgentReportFile | null> = {};
|
|
70
|
+
for (const agent of agentNames) reports[agent] = await readAgentReport(root, homeSeg, agent);
|
|
71
|
+
|
|
72
|
+
if (hasJsonFlag(argv)) {
|
|
73
|
+
process.stdout.write(renderJson({ router, taskFiles, reports }));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rows: string[][] = [];
|
|
78
|
+
for (const [task, entry] of Object.entries(router)) {
|
|
79
|
+
const tf = taskFiles[task];
|
|
80
|
+
const agentCount = tf ? Object.keys(tf).length : 0;
|
|
81
|
+
const { done, total } = tf ? subTaskCounts(tf, reports, task) : { done: 0, total: 0 };
|
|
82
|
+
const violations = tf ? crossCheckTask(tf, reports as Record<string, PrdAgentReportFile>, task) : [];
|
|
83
|
+
rows.push([task, entry.status, String(agentCount), `${done}/${total}`, String(violations.length)]);
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write(renderTable(HEADER, rows) + "\n");
|
|
86
|
+
return 0;
|
|
87
|
+
}
|