@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,7 +1,7 @@
|
|
|
1
1
|
import { t as evaluate } from "./evaluate-CMiIqHeH.mjs";
|
|
2
2
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
3
|
import { t as commandToString } from "./command-string-CALMTnwN.mjs";
|
|
4
|
-
import { c as readClaudeInput } from "./claude-
|
|
4
|
+
import { c as readClaudeInput } from "./claude-D62hkUfS.mjs";
|
|
5
5
|
//#region src/adapters/hermes/index.ts
|
|
6
6
|
/**
|
|
7
7
|
* Hermes Agent adapter (hook-mode). Nous Research's hermes-agent (2026) pipes a
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as PromptKind, r as formatPrompt, t as Prompt } from "./types-Bh0jEF3_.mjs";
|
|
2
2
|
import { _ as queryHash, a as cacheLookupSubstringMeta, c as mcpCacheKey, d as extractText, f as IndexSummary, g as jaccardSimilar, h as compactMarkdown, i as cacheLookupSubstring, l as mcpCacheWrite, m as summarizeIndex, n as cacheLookup, o as cachePath, p as loadIndex, r as cacheLookupMeta, s as cacheStore, t as CacheHit, u as webfetchCacheWrite } from "./index-DhmV2MPh.mjs";
|
|
3
|
-
import { _ as DEFAULT_TTL_SEC, a as ProjectLayout, b as ttlLabel, c as projectLayout, d as MAX_LINES_ENV_KEY, f as hookBudget, g as storeBudget, h as splitTarget, i as parseEnvFile, l as DEFAULT_MAX_LINES, m as resolveStdinMaxBytes, n as envCandidates, o as STATE_GITIGNORE, p as resolveMaxLines, r as loadDotenv, s as STATE_ROOT, t as HOME_DIR, u as DEFAULT_STDIN_MAX_BYTES, v as TTL_ENV_KEY, x as parseEnvInt, y as resolveTtlSec } from "./index-oXXPs2xl.mjs";
|
|
4
3
|
import { i as HarnessVia, n as HarnessInfo, r as HarnessMode, t as HarnessId } from "./types-DvXTI71X.mjs";
|
|
4
|
+
import { _ as DEFAULT_TTL_SEC, a as ProjectLayout, b as ttlLabel, c as projectLayout, d as MAX_LINES_ENV_KEY, f as hookBudget, g as storeBudget, h as splitTarget, i as parseEnvFile, l as DEFAULT_MAX_LINES, m as resolveStdinMaxBytes, n as envCandidates, o as STATE_GITIGNORE, p as resolveMaxLines, r as loadDotenv, s as STATE_ROOT, t as HOME_DIR, u as DEFAULT_STDIN_MAX_BYTES, v as TTL_ENV_KEY, x as parseEnvInt, y as resolveTtlSec } from "./index-oXXPs2xl.mjs";
|
|
5
5
|
import { n as detectMode, r as modeFor, t as detectHarness } from "./index-CigBu4K9.mjs";
|
|
6
6
|
import { a as isDocConsulted, i as formatDocSatisfactionStatus, n as DocSatisfactionStatus, o as resolveSessions, r as formatDocDeny, t as AuthEntry } from "./doc-helpers-BGCzP9VF.mjs";
|
|
7
7
|
import { t as incrementTrivialEditCounter } from "./index-84heAAm_.mjs";
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { a as resolveMaxLines, c as storeBudget, i as hookBudget, l as parseEnvInt, n as DEFAULT_STDIN_MAX_BYTES, o as resolveStdinMaxBytes, r as MAX_LINES_ENV_KEY, s as splitTarget, t as DEFAULT_MAX_LINES } from "./limits-DG5pl-5s.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { i as ttlLabel, n as TTL_ENV_KEY, r as resolveTtlSec, t as DEFAULT_TTL_SEC } from "./ttl-Dgwg_QAv.mjs";
|
|
3
3
|
import { n as STATE_ROOT, r as projectLayout, t as STATE_GITIGNORE } from "./layout-KWoE_Mqn.mjs";
|
|
4
|
+
import { i as parseEnvFile, n as envCandidates, r as loadDotenv, t as HOME_DIR } from "./dotenv-C1LkcfW-.mjs";
|
|
4
5
|
import { t as compactJson } from "./compact-json-DK2nX-MK.mjs";
|
|
5
6
|
import { i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
6
7
|
import { n as detectMode, r as modeFor, t as detectHarness } from "./harness-BMuLJ9lm.mjs";
|
|
@@ -8,7 +9,7 @@ import { $ as isRalphMode, B as protectedPathGuard, C as isInterfacesPath, D as
|
|
|
8
9
|
import { d as countFrameworkCodeLines, f as countLines, l as PLUGINS_DIR, p as evaluateFileSize, u as SOLID_REF } from "./home-state-oUGFB4ds.mjs";
|
|
9
10
|
import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-CWZegVdR.mjs";
|
|
10
11
|
import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "./router-PKVNBHge.mjs";
|
|
11
|
-
import { A as
|
|
12
|
+
import { A as countStores, B as PRE_AUTH_GATES, C as usesTailwindUtilities, D as declaresQueryHook, E as declaresCustomHook, F as MAX_TOKENS, G as solidReadGate, H as docConsultedGate, I as capVerbosity, L as detectCreationIntent, O as importsTanstackQuery, P as MAX_EXA_RESULTS, R as APEX_GATES, S as skillTriggerGate, T as frameworkSolidGate, U as evaluateApex, V as brainstormGate, W as freshnessGate, _ as detectClaudeMdProjectType, a as firstHeading, b as parseField, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as buildClaudeMdContext, h as buildApexInstruction, i as firstComment, j as declaresStore, k as queryCapActive, l as PROJECT_INDICATORS, m as DEV_VERBS, n as missingSeoElements, o as parseEnrichment, r as descFromText, s as parseEntry, t as isHtmlLike, u as buildApexTaskContext, w as SKILL_TRIGGERS, x as detectRequiredSkills, y as parseBodyDesc, z as POST_AUTH_GATES } from "./validate-Dcjl0LUS.mjs";
|
|
12
13
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
13
14
|
import { a as nowStamp, c as stateFileFor, i as lessonsFileFor, l as throttleMs, n as readRoots, o as readState, r as registryFile, s as setStateField, t as addRoot, u as ensureMemoryGitignore } from "./registry-IhHk2KlT.mjs";
|
|
14
15
|
import { a as cacheLookupSubstring, c as cacheStore, d as loadIndex, f as summarizeIndex, h as queryHash, i as cacheLookupMeta, l as mcpCacheKey, m as jaccardSimilar, n as webfetchCacheWrite, o as cacheLookupSubstringMeta, p as compactMarkdown, r as cacheLookup, s as cachePath, t as mcpCacheWrite, u as extractText } from "./mcp-store-BkBDmuxN.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as evaluate } from "./evaluate-CMiIqHeH.mjs";
|
|
2
2
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
3
|
import { t as commandToString } from "./command-string-CALMTnwN.mjs";
|
|
4
|
-
import { c as readClaudeInput } from "./claude-
|
|
4
|
+
import { c as readClaudeInput } from "./claude-D62hkUfS.mjs";
|
|
5
5
|
//#region src/adapters/kimi/index.ts
|
|
6
6
|
/**
|
|
7
7
|
* Kimi Code CLI adapter (hook-mode). Moonshot AI's Kimi Code CLI (v0.27.0,
|
package/dist/policy/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { $ as isRalphMode, B as protectedPathGuard, C as isInterfacesPath, D as isVendorPath, E as isTypesPath, F as FILE_REDIRECT, H as ASK_PATTERNS, I as SAFE_PREFIXES, J as GIT_ASK, L as SESSION_STATE_FRAGMENT, M as ASK_WRITERS, N as CODE_MUTATORS, O as langOfPath, P as CODE_REDIRECT, Q as SYSTEM_INSTALL, R as PROTECTED_FRAGMENTS, S as isHooksPath, T as isStoresPath, U as CRITICAL_PATTERNS, W as securityGuard, X as PROJECT_INSTALL, Y as GIT_BLOCKED, Z as RALPH_SAFE, a as registerGuard, at as maskCommentsOnly, b as interfaceDeclLevel, c as GO_DECL_RE, ct as detectProjectType, d as PY_MODEL_RE, et as matchPatterns, f as RUST_DECL_RE, h as interfaceSeparationGuard, i as clearUserGuards, it as maskCommentsAndStrings, j as bashWriteGuard, k as lexProfileOf, l as JAVA_DECL_RE, lt as isApexCommand, m as TS_DECL_RE, n as FAIL_CLOSED, o as runGuards, ot as DEV_KEYWORDS, p as SWIFT_PROTO_RE, r as GUARDS, s as installGuard, st as detectModularArchitecture, t as evaluate, tt as detectFramework, u as PHP_DECL_RE, ut as requiredArchSkill, v as declaresExportedTypeAlias, w as isQueryPath, x as isComponentsPath, y as declaresInterface, z as PROTECTED_GIT_RE } from "../evaluate-CMiIqHeH.mjs";
|
|
2
2
|
import { d as countFrameworkCodeLines, f as countLines, l as PLUGINS_DIR, p as evaluateFileSize, u as SOLID_REF } from "../home-state-oUGFB4ds.mjs";
|
|
3
|
-
import { A as
|
|
3
|
+
import { A as countStores, B as PRE_AUTH_GATES, C as usesTailwindUtilities, D as declaresQueryHook, E as declaresCustomHook, F as MAX_TOKENS, G as solidReadGate, H as docConsultedGate, I as capVerbosity, L as detectCreationIntent, O as importsTanstackQuery, P as MAX_EXA_RESULTS, R as APEX_GATES, S as skillTriggerGate, T as frameworkSolidGate, U as evaluateApex, V as brainstormGate, W as freshnessGate, _ as detectClaudeMdProjectType, a as firstHeading, b as parseField, c as EXCLUDE_DIRS, d as buildApexTaskInjection, f as loadApexTaskState, g as buildClaudeMdContext, h as buildApexInstruction, i as firstComment, j as declaresStore, k as queryCapActive, l as PROJECT_INDICATORS, m as DEV_VERBS, n as missingSeoElements, o as parseEnrichment, r as descFromText, s as parseEntry, t as isHtmlLike, u as buildApexTaskContext, w as SKILL_TRIGGERS, x as detectRequiredSkills, y as parseBodyDesc, z as POST_AUTH_GATES } from "../validate-Dcjl0LUS.mjs";
|
|
4
4
|
import "../policy-la_KkjCS.mjs";
|
|
5
5
|
export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, CRITICAL_PATTERNS, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, FILE_REDIRECT, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GUARDS, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_TOKENS, PHP_DECL_RE, PLUGINS_DIR, POST_AUTH_GATES, PRE_AUTH_GATES, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PROTECTED_GIT_RE, PY_MODEL_RE, RALPH_SAFE, RUST_DECL_RE, SAFE_PREFIXES, SESSION_STATE_FRAGMENT, SKILL_TRIGGERS, SOLID_REF, SWIFT_PROTO_RE, SYSTEM_INSTALL, TS_DECL_RE, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, capVerbosity, clearUserGuards, countFrameworkCodeLines, countLines, countStores, declaresCustomHook, declaresExportedTypeAlias, declaresInterface, declaresQueryHook, declaresStore, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, firstComment, firstHeading, frameworkSolidGate, freshnessGate, importsTanstackQuery, installGuard, interfaceDeclLevel, interfaceSeparationGuard, isApexCommand, isComponentsPath, isHooksPath, isHtmlLike, isInterfacesPath, isQueryPath, isRalphMode, isStoresPath, isTypesPath, isVendorPath, langOfPath, lexProfileOf, loadApexTaskState, maskCommentsAndStrings, maskCommentsOnly, matchPatterns, missingSeoElements, parseBodyDesc, parseEnrichment, parseEntry, parseField, protectedPathGuard, queryCapActive, registerGuard, requiredArchSkill, runGuards, securityGuard, skillTriggerGate, solidReadGate, usesTailwindUtilities };
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { r as loadDotenv, t as HOME_DIR } from "./dotenv-C1LkcfW-.mjs";
|
|
2
|
+
import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
3
|
+
import { t as evaluate } from "./evaluate-CMiIqHeH.mjs";
|
|
4
|
+
import { r as harnessHomeSegment } from "./apex-target-Xc2M32Pl.mjs";
|
|
5
|
+
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
6
|
+
import { A as prdRouterPath, C as isCompacted, E as withSubTaskValidated, T as withRouterStatus, b as writeRouter, c as isPrdFlagSet, f as incompleteSubTasks, g as readRouter, h as readAllTaskFiles, k as prdDir, n as compactTaskFile, p as readAgentReport, r as acquireLock, u as crossCheckTask, v as readTaskFile, w as subTasksOf, x as writeTaskFile } from "./prd-compact-BE45t8UR.mjs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
//#region src/cli/run.ts
|
|
12
|
+
/** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
|
|
13
|
+
function stagedFiles() {
|
|
14
|
+
return execFileSync("git", [
|
|
15
|
+
"diff",
|
|
16
|
+
"--cached",
|
|
17
|
+
"--name-only",
|
|
18
|
+
"--diff-filter=ACMR"
|
|
19
|
+
], { encoding: "utf8" }).trim().split("\n").filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read a file's staged (index) content — not the working-tree version.
|
|
23
|
+
* Uses `execFileSync` with an argv array (never a shell), so a staged
|
|
24
|
+
* filename containing shell metacharacters (backticks, `$(...)`, quotes)
|
|
25
|
+
* reaches `git show` as a single literal argument — see issue #87.
|
|
26
|
+
*/
|
|
27
|
+
function stagedContent(path) {
|
|
28
|
+
return execFileSync("git", ["show", `:${path}`], { encoding: "utf8" });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Evaluate staged code files against the policy core; return violation blocks.
|
|
32
|
+
* `read` is injected (the real impl is {@link stagedContent}) so this is pure + testable.
|
|
33
|
+
*/
|
|
34
|
+
function checkStaged(files, read) {
|
|
35
|
+
const violations = [];
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
if (!isCodeFile(file)) continue;
|
|
38
|
+
const r = evaluate({
|
|
39
|
+
tool: "Write",
|
|
40
|
+
filePath: file,
|
|
41
|
+
content: read(file)
|
|
42
|
+
});
|
|
43
|
+
if (r.decision === "deny" && r.prompt) violations.push(`${file}\n${formatPrompt(r.prompt)}`);
|
|
44
|
+
}
|
|
45
|
+
return violations;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/cli/prd/resolve.ts
|
|
49
|
+
/**
|
|
50
|
+
* Argument parsing + harness resolution for `harness prd <cmd>`. Resolves
|
|
51
|
+
* `--root` (default `cwd`) and `--id`/`homeSeg` (default: the sole
|
|
52
|
+
* `HOME_DIR` (`config/dotenv.ts`) segment under which
|
|
53
|
+
* `<root>/<seg>/apex/prd.json` exists — ambiguous or absent is a caller
|
|
54
|
+
* error, never guessed).
|
|
55
|
+
*/
|
|
56
|
+
const VALUE_FLAGS = /* @__PURE__ */ new Set(["--id", "--root"]);
|
|
57
|
+
const BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--json"]);
|
|
58
|
+
/** Read `--<name> <value>` from argv, or `undefined` when absent. */
|
|
59
|
+
function readFlag(argv, name) {
|
|
60
|
+
const idx = argv.indexOf(name);
|
|
61
|
+
return idx !== -1 ? argv[idx + 1] : void 0;
|
|
62
|
+
}
|
|
63
|
+
/** True when `--json` is present in argv. */
|
|
64
|
+
function hasJsonFlag(argv) {
|
|
65
|
+
return argv.includes("--json");
|
|
66
|
+
}
|
|
67
|
+
/** Non-flag tokens in argv, skipping known flags and their values. */
|
|
68
|
+
function positionalArgs(argv) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = 0; i < argv.length; i++) {
|
|
71
|
+
const tok = argv[i];
|
|
72
|
+
if (tok === void 0) continue;
|
|
73
|
+
if (VALUE_FLAGS.has(tok)) {
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (BOOLEAN_FLAGS.has(tok)) continue;
|
|
78
|
+
out.push(tok);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve `{root, homeSeg, id}` for a `prd` sub-command. `--id` wins
|
|
84
|
+
* outright; otherwise auto-detects the sole `HOME_DIR` segment under which
|
|
85
|
+
* `<root>/<seg>/apex/prd.json` exists.
|
|
86
|
+
*/
|
|
87
|
+
function resolvePrdArgs(argv, cwd) {
|
|
88
|
+
const root = readFlag(argv, "--root") ?? cwd;
|
|
89
|
+
const idFlag = readFlag(argv, "--id");
|
|
90
|
+
if (idFlag) return {
|
|
91
|
+
ok: true,
|
|
92
|
+
value: {
|
|
93
|
+
root,
|
|
94
|
+
homeSeg: harnessHomeSegment(idFlag),
|
|
95
|
+
id: idFlag
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const matches = [];
|
|
99
|
+
for (const [id, seg] of Object.entries(HOME_DIR)) if (existsSync(join(root, seg, "apex", "prd.json"))) matches.push({
|
|
100
|
+
id,
|
|
101
|
+
seg
|
|
102
|
+
});
|
|
103
|
+
if (matches.length === 0) return {
|
|
104
|
+
ok: false,
|
|
105
|
+
message: `no PRD router found under ${root}/<home>/apex/prd.json — pass --id <harness>`
|
|
106
|
+
};
|
|
107
|
+
if (matches.length > 1) return {
|
|
108
|
+
ok: false,
|
|
109
|
+
message: `ambiguous harness: PRD routers found for ${matches.map((m) => m.id).join(", ")} — pass --id`
|
|
110
|
+
};
|
|
111
|
+
const only = matches[0];
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
value: {
|
|
115
|
+
root,
|
|
116
|
+
homeSeg: only.seg,
|
|
117
|
+
id: only.id
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Shared first step of every `prd` sub-command: {@link resolvePrdArgs}, then
|
|
123
|
+
* (on success) `loadDotenv` the resolved harness's home `.env` + `<root>/.env`
|
|
124
|
+
* as a side effect on `env` — same env-loading contract as the `hook` branch
|
|
125
|
+
* (`bin.ts`'s `loadDotenv(id)` call).
|
|
126
|
+
*/
|
|
127
|
+
function resolveAndLoadEnv(argv, cwd, env = process.env) {
|
|
128
|
+
const resolved = resolvePrdArgs(argv, cwd);
|
|
129
|
+
if (resolved.ok) loadDotenv(resolved.value.id, env, homedir(), resolved.value.root);
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/cli/prd/format.ts
|
|
134
|
+
/**
|
|
135
|
+
* Rendering helpers for `harness prd` output: an aligned ASCII table for the
|
|
136
|
+
* human-readable path, and pretty JSON for `--json`.
|
|
137
|
+
*/
|
|
138
|
+
/** Pad `s` with trailing spaces to `width`. */
|
|
139
|
+
function pad(s, width) {
|
|
140
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
141
|
+
}
|
|
142
|
+
/** Render an aligned ASCII table from a header row + data rows. */
|
|
143
|
+
function renderTable(header, rows) {
|
|
144
|
+
const all = [header, ...rows];
|
|
145
|
+
const widths = header.map((_, col) => Math.max(...all.map((r) => (r[col] ?? "").length)));
|
|
146
|
+
return all.map((r) => r.map((cell, col) => pad(cell ?? "", widths[col] ?? 0)).join(" ").trimEnd()).join("\n");
|
|
147
|
+
}
|
|
148
|
+
/** Render `data` as pretty-printed JSON with a trailing newline. */
|
|
149
|
+
function renderJson(data) {
|
|
150
|
+
return JSON.stringify(data, null, 2) + "\n";
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/cli/prd/shared.ts
|
|
154
|
+
/**
|
|
155
|
+
* Shared plumbing for `status`/`validate`/`compact`: root+env resolution,
|
|
156
|
+
* the `FUSE_PRD` write gate, task-PRD lookup, and the `.lock`-guarded write.
|
|
157
|
+
* All return a result instead of calling `process.exit` themselves —
|
|
158
|
+
* callers own the exit code (extracted to kill a jscpd-measured clone
|
|
159
|
+
* across the three `run*` functions).
|
|
160
|
+
*/
|
|
161
|
+
/** {@link resolveAndLoadEnv}, narrowed to `{root, homeSeg}` — the common first step of every sub-command. */
|
|
162
|
+
function resolveRoot(argv, cwd, env) {
|
|
163
|
+
const resolved = resolveAndLoadEnv(argv, cwd, env);
|
|
164
|
+
if (!resolved.ok) return {
|
|
165
|
+
ok: false,
|
|
166
|
+
code: 2,
|
|
167
|
+
message: resolved.message
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
root: resolved.value.root,
|
|
172
|
+
homeSeg: resolved.value.homeSeg
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** {@link resolveRoot} + the `FUSE_PRD=1` write gate shared by `validate`/`compact`. */
|
|
176
|
+
function requireFusePrd(argv, cwd, env, sub) {
|
|
177
|
+
const resolved = resolveRoot(argv, cwd, env);
|
|
178
|
+
if (!resolved.ok) return resolved;
|
|
179
|
+
if (!isPrdFlagSet(env)) return {
|
|
180
|
+
ok: false,
|
|
181
|
+
code: 1,
|
|
182
|
+
message: `prd ${sub} requires FUSE_PRD=1 in ${resolved.homeSeg}/.env or the project .env`
|
|
183
|
+
};
|
|
184
|
+
return resolved;
|
|
185
|
+
}
|
|
186
|
+
/** Reads the router + the named task's task-PRD file. Never calls `process.exit`. */
|
|
187
|
+
async function loadTaskFileFor(root, homeSeg, task) {
|
|
188
|
+
const router = await readRouter(root, homeSeg);
|
|
189
|
+
const routerEntry = router?.[task];
|
|
190
|
+
if (!router || !routerEntry) return {
|
|
191
|
+
ok: false,
|
|
192
|
+
code: 2,
|
|
193
|
+
message: `no such task "${task}" in PRD router`
|
|
194
|
+
};
|
|
195
|
+
const taskFile = await readTaskFile(root, homeSeg, routerEntry.prd);
|
|
196
|
+
if (!taskFile) return {
|
|
197
|
+
ok: false,
|
|
198
|
+
code: 2,
|
|
199
|
+
message: `task PRD not found or malformed: ${routerEntry.prd}`
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
router,
|
|
204
|
+
routerEntry,
|
|
205
|
+
taskFile
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Extracts `<task> [agent]` from argv, then {@link loadTaskFileFor} — the shared `validate`/`compact` entry lookup. */
|
|
209
|
+
async function resolveTaskFile(argv, root, homeSeg, usage) {
|
|
210
|
+
const [task, agentArg] = positionalArgs(argv);
|
|
211
|
+
if (!task) return {
|
|
212
|
+
ok: false,
|
|
213
|
+
code: 2,
|
|
214
|
+
message: usage
|
|
215
|
+
};
|
|
216
|
+
const lookup = await loadTaskFileFor(root, homeSeg, task);
|
|
217
|
+
if (!lookup.ok) return lookup;
|
|
218
|
+
return {
|
|
219
|
+
task,
|
|
220
|
+
agentArg,
|
|
221
|
+
...lookup
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Runs `fn` under the PRD `.lock` directory-lock. Never calls `process.exit`. */
|
|
225
|
+
async function withPrdLock(root, homeSeg, fn) {
|
|
226
|
+
const lockDir = join(prdDir(root, homeSeg), ".lock");
|
|
227
|
+
const release = await acquireLock(lockDir);
|
|
228
|
+
if (!release) return {
|
|
229
|
+
ok: false,
|
|
230
|
+
message: `lock held at ${lockDir}`
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
return {
|
|
234
|
+
ok: true,
|
|
235
|
+
value: await fn()
|
|
236
|
+
};
|
|
237
|
+
} finally {
|
|
238
|
+
await release();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/cli/prd/status.ts
|
|
243
|
+
/**
|
|
244
|
+
* `harness prd status [--json] [--id <harness>] [--root <dir>]` — read-only
|
|
245
|
+
* PRD snapshot. Never requires `FUSE_PRD`. Exit 0 on success, exit 1 when no
|
|
246
|
+
* router is found, exit 2 on an unresolved/ambiguous `--id`.
|
|
247
|
+
*/
|
|
248
|
+
const HEADER = [
|
|
249
|
+
"Task",
|
|
250
|
+
"Router status",
|
|
251
|
+
"Agents",
|
|
252
|
+
"Sub-tasks done/total",
|
|
253
|
+
"Violations"
|
|
254
|
+
];
|
|
255
|
+
/**
|
|
256
|
+
* Sub-task readiness counts for the "done/total" column: a sub-task counts as
|
|
257
|
+
* done once it is `validated` OR its owning agent's own report already shows
|
|
258
|
+
* `"done"` for it (readiness view — a report can land well before the
|
|
259
|
+
* coordinator runs `prd validate`). A compacted (fully-validated) entry
|
|
260
|
+
* counts as one done/one total unit — its per-sub-task detail is gone.
|
|
261
|
+
*/
|
|
262
|
+
function subTaskCounts(taskFile, reports, task) {
|
|
263
|
+
let done = 0;
|
|
264
|
+
let total = 0;
|
|
265
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
266
|
+
if (isCompacted(entry)) {
|
|
267
|
+
done++;
|
|
268
|
+
total++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const report = reports[agent];
|
|
272
|
+
for (const [sub, subTask] of Object.entries(subTasksOf(entry))) {
|
|
273
|
+
total++;
|
|
274
|
+
if (subTask.status === "validated" || report?.[task]?.[sub]?.status === "done") done++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
done,
|
|
279
|
+
total
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Run `harness prd status`. Prints an aligned table (or `--json` dump of
|
|
284
|
+
* `{router, taskFiles, reports}`) and returns the process exit code.
|
|
285
|
+
*/
|
|
286
|
+
async function runPrdStatus(argv, cwd, env = process.env) {
|
|
287
|
+
const resolved = resolveRoot(argv, cwd, env);
|
|
288
|
+
if (!resolved.ok) {
|
|
289
|
+
process.stderr.write(resolved.message + "\n");
|
|
290
|
+
return resolved.code;
|
|
291
|
+
}
|
|
292
|
+
const { root, homeSeg } = resolved;
|
|
293
|
+
const router = await readRouter(root, homeSeg);
|
|
294
|
+
if (!router) {
|
|
295
|
+
process.stderr.write(`no PRD router at ${prdRouterPath(root, homeSeg)}\n`);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
const taskFiles = await readAllTaskFiles(root, homeSeg, router);
|
|
299
|
+
const agentNames = /* @__PURE__ */ new Set();
|
|
300
|
+
for (const tf of Object.values(taskFiles)) if (tf) for (const agent of Object.keys(tf)) agentNames.add(agent);
|
|
301
|
+
const reports = {};
|
|
302
|
+
for (const agent of agentNames) reports[agent] = await readAgentReport(root, homeSeg, agent);
|
|
303
|
+
if (hasJsonFlag(argv)) {
|
|
304
|
+
process.stdout.write(renderJson({
|
|
305
|
+
router,
|
|
306
|
+
taskFiles,
|
|
307
|
+
reports
|
|
308
|
+
}));
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
const rows = [];
|
|
312
|
+
for (const [task, entry] of Object.entries(router)) {
|
|
313
|
+
const tf = taskFiles[task];
|
|
314
|
+
const agentCount = tf ? Object.keys(tf).length : 0;
|
|
315
|
+
const { done, total } = tf ? subTaskCounts(tf, reports, task) : {
|
|
316
|
+
done: 0,
|
|
317
|
+
total: 0
|
|
318
|
+
};
|
|
319
|
+
const violations = tf ? crossCheckTask(tf, reports, task) : [];
|
|
320
|
+
rows.push([
|
|
321
|
+
task,
|
|
322
|
+
entry.status,
|
|
323
|
+
String(agentCount),
|
|
324
|
+
`${done}/${total}`,
|
|
325
|
+
String(violations.length)
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
process.stdout.write(renderTable(HEADER, rows) + "\n");
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/cli/prd/validate.ts
|
|
333
|
+
/**
|
|
334
|
+
* `harness prd validate <task> [agent] [--id] [--root]` — cross-checks a
|
|
335
|
+
* task-PRD against the named agent(s)' own report(s); on success flips the
|
|
336
|
+
* matching sub-tasks to `validated` and promotes the router entry when every
|
|
337
|
+
* agent is fully validated. Requires `FUSE_PRD=1`.
|
|
338
|
+
*/
|
|
339
|
+
function allValidated(taskFile) {
|
|
340
|
+
return Object.values(taskFile).every((entry) => isCompacted(entry) || Object.values(subTasksOf(entry)).every((s) => s.status === "validated"));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Sub-tasks of `agents` with no matching `done` entry in their owning
|
|
344
|
+
* agent's own report — the readiness gate `validate` enforces before
|
|
345
|
+
* flipping anything. Reuses `incompleteSubTasks` (distinct from
|
|
346
|
+
* `crossCheckTask`, which only flags an ALREADY-`validated` sub-task lacking
|
|
347
|
+
* a report; here nothing has been validated yet).
|
|
348
|
+
*/
|
|
349
|
+
function findUnreadySubTasks(taskFile, reports, task, agents) {
|
|
350
|
+
const out = [];
|
|
351
|
+
for (const agent of agents) {
|
|
352
|
+
const missing = incompleteSubTasks(taskFile, agent, task, reports[agent] ?? null);
|
|
353
|
+
for (const sub of missing) out.push({
|
|
354
|
+
task,
|
|
355
|
+
agent,
|
|
356
|
+
sub,
|
|
357
|
+
reason: `no "done" report from "${agent}" for sub-task "${sub}"`
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Run `harness prd validate`. Exit 0 on success, exit 1 on cross-check
|
|
364
|
+
* violations or a missing `FUSE_PRD=1`/held lock, exit 2 on usage/lookup errors.
|
|
365
|
+
*/
|
|
366
|
+
async function runPrdValidate(argv, cwd, env = process.env) {
|
|
367
|
+
const resolved = requireFusePrd(argv, cwd, env, "validate");
|
|
368
|
+
if (!resolved.ok) {
|
|
369
|
+
process.stderr.write(resolved.message + "\n");
|
|
370
|
+
return resolved.code;
|
|
371
|
+
}
|
|
372
|
+
const { root, homeSeg } = resolved;
|
|
373
|
+
const tf = await resolveTaskFile(argv, root, homeSeg, "usage: harness prd validate <task> [agent]");
|
|
374
|
+
if (!tf.ok) {
|
|
375
|
+
process.stderr.write(tf.message + "\n");
|
|
376
|
+
return tf.code;
|
|
377
|
+
}
|
|
378
|
+
const { task, agentArg, router, routerEntry, taskFile } = tf;
|
|
379
|
+
if (agentArg && !(agentArg in taskFile)) {
|
|
380
|
+
process.stderr.write(`no such agent "${agentArg}" in task PRD for "${task}"\n`);
|
|
381
|
+
return 2;
|
|
382
|
+
}
|
|
383
|
+
const agents = agentArg ? [agentArg] : Object.keys(taskFile);
|
|
384
|
+
const reports = {};
|
|
385
|
+
for (const agent of Object.keys(taskFile)) reports[agent] = await readAgentReport(root, homeSeg, agent);
|
|
386
|
+
const violations = findUnreadySubTasks(taskFile, reports, task, agents);
|
|
387
|
+
if (violations.length > 0) {
|
|
388
|
+
process.stderr.write(`prd validate: ${violations.length} violation(s)\n`);
|
|
389
|
+
for (const v of violations) process.stderr.write(` - ${v.task}/${v.agent}/${v.sub}: ${v.reason}\n`);
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
const result = await withPrdLock(root, homeSeg, async () => {
|
|
393
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
394
|
+
let updated = taskFile;
|
|
395
|
+
for (const agent of agents) {
|
|
396
|
+
const entry = updated[agent];
|
|
397
|
+
if (!entry || isCompacted(entry)) continue;
|
|
398
|
+
for (const sub of Object.keys(subTasksOf(entry))) updated = withSubTaskValidated(updated, agent, sub, now);
|
|
399
|
+
}
|
|
400
|
+
await writeTaskFile(root, homeSeg, routerEntry.prd, updated);
|
|
401
|
+
if (allValidated(updated)) await writeRouter(root, homeSeg, withRouterStatus(router, task, "validated", now));
|
|
402
|
+
});
|
|
403
|
+
if (!result.ok) {
|
|
404
|
+
process.stderr.write(`prd validate: ${result.message}\n`);
|
|
405
|
+
return 1;
|
|
406
|
+
}
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/cli/prd/compact.ts
|
|
411
|
+
/**
|
|
412
|
+
* `harness prd compact <task> [--id] [--root]` — collapses every fully
|
|
413
|
+
* validated agent entry of a task-PRD to its compacted shape. Requires
|
|
414
|
+
* `FUSE_PRD=1`; refuses (exit 1) when any sub-task is not yet `validated`.
|
|
415
|
+
*/
|
|
416
|
+
/**
|
|
417
|
+
* Run `harness prd compact`. Exit 0 on success (including a no-op compact),
|
|
418
|
+
* exit 1 when `FUSE_PRD` is unset, a sub-task is unvalidated, or the lock is
|
|
419
|
+
* held, exit 2 on usage/lookup errors.
|
|
420
|
+
*/
|
|
421
|
+
async function runPrdCompact(argv, cwd, env = process.env) {
|
|
422
|
+
const resolved = requireFusePrd(argv, cwd, env, "compact");
|
|
423
|
+
if (!resolved.ok) {
|
|
424
|
+
process.stderr.write(resolved.message + "\n");
|
|
425
|
+
return resolved.code;
|
|
426
|
+
}
|
|
427
|
+
const { root, homeSeg } = resolved;
|
|
428
|
+
const tf = await resolveTaskFile(argv, root, homeSeg, "usage: harness prd compact <task>");
|
|
429
|
+
if (!tf.ok) {
|
|
430
|
+
process.stderr.write(tf.message + "\n");
|
|
431
|
+
return tf.code;
|
|
432
|
+
}
|
|
433
|
+
const { routerEntry, taskFile } = tf;
|
|
434
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
435
|
+
if (isCompacted(entry)) continue;
|
|
436
|
+
for (const [sub, status] of Object.entries(subTasksOf(entry))) if (status.status !== "validated") {
|
|
437
|
+
process.stderr.write(`prd compact: sub-task "${sub}" of agent "${agent}" is not validated\n`);
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const result = await withPrdLock(root, homeSeg, async () => {
|
|
442
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
443
|
+
const { file, compacted } = compactTaskFile(taskFile, now);
|
|
444
|
+
await writeTaskFile(root, homeSeg, routerEntry.prd, file);
|
|
445
|
+
return compacted;
|
|
446
|
+
});
|
|
447
|
+
if (!result.ok) {
|
|
448
|
+
process.stderr.write(`prd compact: ${result.message}\n`);
|
|
449
|
+
return 1;
|
|
450
|
+
}
|
|
451
|
+
process.stdout.write(result.value.length > 0 ? `compacted: ${result.value.join(", ")}\n` : "nothing to compact\n");
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/cli/prd/index.ts
|
|
456
|
+
/** Dispatch `harness prd <sub> ...rest` to the matching `run*` function. */
|
|
457
|
+
async function runPrd(argv, cwd, env = process.env) {
|
|
458
|
+
const [sub, ...rest] = argv;
|
|
459
|
+
if (sub === "status") return runPrdStatus(rest, cwd, env);
|
|
460
|
+
if (sub === "validate") return runPrdValidate(rest, cwd, env);
|
|
461
|
+
if (sub === "compact") return runPrdCompact(rest, cwd, env);
|
|
462
|
+
process.stderr.write(`harness prd: unknown sub-command "${sub ?? ""}" (expected status|validate|compact)\n`);
|
|
463
|
+
return 2;
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
export { renderJson as a, positionalArgs as c, resolvePrdArgs as d, checkStaged as f, runPrdStatus as i, readFlag as l, stagedFiles as m, runPrdCompact as n, renderTable as o, stagedContent as p, runPrdValidate as r, hasJsonFlag as s, runPrd as t, resolveAndLoadEnv as u };
|