@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
|
@@ -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,
|
|
@@ -189,14 +189,32 @@ function contains(root, filePath) {
|
|
|
189
189
|
const rel = relative(root, filePath);
|
|
190
190
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
191
191
|
}
|
|
192
|
-
/**
|
|
193
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Select Cursor's project scope without replacing a valid payload cwd. Order:
|
|
194
|
+
* payload `cwd` -> longest workspace root containing `filePath` ->
|
|
195
|
+
* `workspaceRoots[0]` -> `CURSOR_PROJECT_DIR` env -> `CLAUDE_PROJECT_DIR` env
|
|
196
|
+
* -> `fallback`. Both env vars are validated the same way as any other Cursor
|
|
197
|
+
* path (`cursorAbsolutePath`: absolute, NUL-free, realpath-resolved), so an
|
|
198
|
+
* unset or malformed value is silently skipped rather than trusted.
|
|
199
|
+
* @param cwd - Cursor payload `cwd`, when present.
|
|
200
|
+
* @param workspaceRoots - Validated, deduped Cursor `workspace_roots`.
|
|
201
|
+
* @param filePath - The file the current event targets, when present.
|
|
202
|
+
* @param fallback - Caller-supplied last resort (never `process.cwd()`).
|
|
203
|
+
* @param env - Environment (defaults to `process.env`).
|
|
204
|
+
* @returns The resolved project root.
|
|
205
|
+
*/
|
|
206
|
+
function cursorProjectCwd(cwd, workspaceRoots, filePath, fallback, env = process.env) {
|
|
194
207
|
if (cwd) return cwd;
|
|
195
208
|
if (filePath) {
|
|
196
209
|
const matches = workspaceRoots.filter((root) => contains(root, filePath));
|
|
197
210
|
if (matches.length > 0) return matches.sort((a, b) => b.length - a.length)[0];
|
|
198
211
|
}
|
|
199
|
-
|
|
212
|
+
if (workspaceRoots[0]) return workspaceRoots[0];
|
|
213
|
+
const fromCursorEnv = cursorAbsolutePath(env.CURSOR_PROJECT_DIR);
|
|
214
|
+
if (fromCursorEnv) return fromCursorEnv;
|
|
215
|
+
const fromClaudeEnv = cursorAbsolutePath(env.CLAUDE_PROJECT_DIR);
|
|
216
|
+
if (fromClaudeEnv) return fromClaudeEnv;
|
|
217
|
+
return fallback;
|
|
200
218
|
}
|
|
201
219
|
//#endregion
|
|
202
220
|
//#region src/adapters/cursor/normalize.ts
|
|
@@ -240,10 +258,72 @@ function sanitizedCursorInput(input) {
|
|
|
240
258
|
else delete safe.workspace_roots;
|
|
241
259
|
return safe;
|
|
242
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Closed table: Cursor's `MCP:<tool>` tool_name form on preToolUse/
|
|
263
|
+
* postToolUse/postToolUseFailure LOSES the MCP server name (ground truth:
|
|
264
|
+
* Cursor CLI 3.18.25 + official docs — only beforeMCPExecution/
|
|
265
|
+
* afterMCPExecution carry `mcp_server_name`). This reconstructs the real
|
|
266
|
+
* server for the closed set of tool names this repo's gates actually depend
|
|
267
|
+
* on (GATED_TOOLS in doc-cache-gate.ts, CONTEXT7_SOURCE, RESEARCH_TOOLS,
|
|
268
|
+
* SHOT_TOOLS, gemini-mcp-gate, shadcn-skill-gate) — same closed-table
|
|
269
|
+
* philosophy as `mcp-tool-name.ts`'s Codex aliasing, never a blanket
|
|
270
|
+
* reversal. Coordinator decision: a tool name OUTSIDE this table (server
|
|
271
|
+
* genuinely unrecoverable, and no safe placeholder) is left as Cursor's raw
|
|
272
|
+
* `MCP:<tool>` string — `test/cursor-followup-normalize.test.ts` pins this
|
|
273
|
+
* as the committed contract ("commandless MCP tools keep their name"), so a
|
|
274
|
+
* fabricated `mcp__cursor__<tool>` placeholder is never introduced for the
|
|
275
|
+
* unknown case.
|
|
276
|
+
*/
|
|
277
|
+
const CURSOR_MCP_TOOL_SERVERS = Object.assign(Object.create(null), {
|
|
278
|
+
"query-docs": "context7",
|
|
279
|
+
"resolve-library-id": "context7",
|
|
280
|
+
web_search_exa: "exa",
|
|
281
|
+
get_code_context_exa: "exa",
|
|
282
|
+
deep_researcher_start: "exa",
|
|
283
|
+
deep_researcher_check: "exa",
|
|
284
|
+
create_frontend: "gemini-design",
|
|
285
|
+
modify_frontend: "gemini-design",
|
|
286
|
+
snippet_frontend: "gemini-design",
|
|
287
|
+
search_items_in_registries: "shadcn",
|
|
288
|
+
view_items_in_registries: "shadcn",
|
|
289
|
+
get_item_examples_from_registries: "shadcn",
|
|
290
|
+
get_add_command_for_items: "shadcn",
|
|
291
|
+
get_audit_checklist: "shadcn"
|
|
292
|
+
});
|
|
293
|
+
/**
|
|
294
|
+
* The real MCP server for a bare Cursor tool name (the part after `MCP:`),
|
|
295
|
+
* or `undefined` when it isn't in the closed table. fuse-browser is inferred
|
|
296
|
+
* from the `browser_*` prefix — every fuse-browser tool is named that way
|
|
297
|
+
* and no other server in this ecosystem uses it — the remaining,
|
|
298
|
+
* non-distinctive tool names go through {@link CURSOR_MCP_TOOL_SERVERS}.
|
|
299
|
+
* NO placeholder fallback (coordinator decision, see {@link CURSOR_MCP_TOOL_SERVERS}):
|
|
300
|
+
* an unknown tool name means the server is genuinely unrecoverable, so the
|
|
301
|
+
* caller leaves the raw `MCP:<tool>` string untouched instead of fabricating one.
|
|
302
|
+
*/
|
|
303
|
+
function cursorMcpServer(bareTool) {
|
|
304
|
+
if (bareTool.startsWith("browser_")) return "fuse-browser";
|
|
305
|
+
return CURSOR_MCP_TOOL_SERVERS[bareTool];
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Canonicalize Cursor's `MCP:<tool>` tool_name (preToolUse/postToolUse/
|
|
309
|
+
* postToolUseFailure) into the shared `mcp__<server>__<tool>` shape every
|
|
310
|
+
* other harness/gate expects. Returns `undefined` — meaning "leave the raw
|
|
311
|
+
* `MCP:<tool>` string as-is" — both when `tool` isn't the `MCP:` form and
|
|
312
|
+
* when the bare tool name is outside the closed {@link CURSOR_MCP_TOOL_SERVERS}
|
|
313
|
+
* table (server unrecoverable, no placeholder fabricated).
|
|
314
|
+
*/
|
|
315
|
+
function cursorBareMcpToolName(tool) {
|
|
316
|
+
if (!tool || !tool.startsWith("MCP:")) return void 0;
|
|
317
|
+
const bare = tool.slice(4);
|
|
318
|
+
const server = cursorMcpServer(bare);
|
|
319
|
+
return server ? `mcp__${server}__${bare}` : void 0;
|
|
320
|
+
}
|
|
243
321
|
function cursorToolName(raw, event, tool, hasCommand) {
|
|
244
322
|
if (hasCommand) return "Bash";
|
|
245
323
|
const server = str(raw.mcp_server_name)?.trim().replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
246
324
|
if (/^(before|after)MCPExecution$/i.test(event) && server && tool && !tool.startsWith("mcp__")) return `mcp__${server}__${tool}`;
|
|
325
|
+
const bareMcp = cursorBareMcpToolName(tool);
|
|
326
|
+
if (bareMcp) return bareMcp;
|
|
247
327
|
if (tool === "Write") return "Edit";
|
|
248
328
|
return tool ?? "";
|
|
249
329
|
}
|
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,494 @@
|
|
|
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 { C as withRouterStatus, D as prdDir, O as prdRouterPath, S as subTasksOf, c as crossCheckTask, d as readAgentReport, g as readTaskFile, m as readRouter, o as isPrdFlagSet, p as readAllTaskFiles, t as acquireLock, u as incompleteSubTasks, v as writeRouter, w as withSubTaskValidated, x as isCompacted, y as writeTaskFile } from "./apex-task-store-Cb65rE5i.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/policy/prd/prd-compact.ts
|
|
411
|
+
/** Collapses fully-validated task-PRD agent entries to their compact shape. Pure — no fs. */
|
|
412
|
+
/** Collapses one agent entry to the compacted shape ONLY when every sub-task is `validated`; else returns it unchanged. */
|
|
413
|
+
function compactAgentEntry(e, at) {
|
|
414
|
+
if (isCompacted(e)) return e;
|
|
415
|
+
const subTasks = Object.values(subTasksOf(e));
|
|
416
|
+
if (subTasks.length === 0 || !subTasks.every((s) => s.status === "validated")) return e;
|
|
417
|
+
return {
|
|
418
|
+
status: "validated",
|
|
419
|
+
files: e.files,
|
|
420
|
+
"validated-at": at
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/** Compacts every eligible agent entry in a task-PRD; reports which agent names got collapsed. */
|
|
424
|
+
function compactTaskFile(taskFile, at) {
|
|
425
|
+
const compacted = [];
|
|
426
|
+
const file = {};
|
|
427
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
428
|
+
const next = compactAgentEntry(entry, at);
|
|
429
|
+
if (next !== entry) compacted.push(agent);
|
|
430
|
+
file[agent] = next;
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
file,
|
|
434
|
+
compacted
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/cli/prd/compact.ts
|
|
439
|
+
/**
|
|
440
|
+
* `harness prd compact <task> [--id] [--root]` — collapses every fully
|
|
441
|
+
* validated agent entry of a task-PRD to its compacted shape. Requires
|
|
442
|
+
* `FUSE_PRD=1`; refuses (exit 1) when any sub-task is not yet `validated`.
|
|
443
|
+
*/
|
|
444
|
+
/**
|
|
445
|
+
* Run `harness prd compact`. Exit 0 on success (including a no-op compact),
|
|
446
|
+
* exit 1 when `FUSE_PRD` is unset, a sub-task is unvalidated, or the lock is
|
|
447
|
+
* held, exit 2 on usage/lookup errors.
|
|
448
|
+
*/
|
|
449
|
+
async function runPrdCompact(argv, cwd, env = process.env) {
|
|
450
|
+
const resolved = requireFusePrd(argv, cwd, env, "compact");
|
|
451
|
+
if (!resolved.ok) {
|
|
452
|
+
process.stderr.write(resolved.message + "\n");
|
|
453
|
+
return resolved.code;
|
|
454
|
+
}
|
|
455
|
+
const { root, homeSeg } = resolved;
|
|
456
|
+
const tf = await resolveTaskFile(argv, root, homeSeg, "usage: harness prd compact <task>");
|
|
457
|
+
if (!tf.ok) {
|
|
458
|
+
process.stderr.write(tf.message + "\n");
|
|
459
|
+
return tf.code;
|
|
460
|
+
}
|
|
461
|
+
const { routerEntry, taskFile } = tf;
|
|
462
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
463
|
+
if (isCompacted(entry)) continue;
|
|
464
|
+
for (const [sub, status] of Object.entries(subTasksOf(entry))) if (status.status !== "validated") {
|
|
465
|
+
process.stderr.write(`prd compact: sub-task "${sub}" of agent "${agent}" is not validated\n`);
|
|
466
|
+
return 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const result = await withPrdLock(root, homeSeg, async () => {
|
|
470
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
471
|
+
const { file, compacted } = compactTaskFile(taskFile, now);
|
|
472
|
+
await writeTaskFile(root, homeSeg, routerEntry.prd, file);
|
|
473
|
+
return compacted;
|
|
474
|
+
});
|
|
475
|
+
if (!result.ok) {
|
|
476
|
+
process.stderr.write(`prd compact: ${result.message}\n`);
|
|
477
|
+
return 1;
|
|
478
|
+
}
|
|
479
|
+
process.stdout.write(result.value.length > 0 ? `compacted: ${result.value.join(", ")}\n` : "nothing to compact\n");
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/cli/prd/index.ts
|
|
484
|
+
/** Dispatch `harness prd <sub> ...rest` to the matching `run*` function. */
|
|
485
|
+
async function runPrd(argv, cwd, env = process.env) {
|
|
486
|
+
const [sub, ...rest] = argv;
|
|
487
|
+
if (sub === "status") return runPrdStatus(rest, cwd, env);
|
|
488
|
+
if (sub === "validate") return runPrdValidate(rest, cwd, env);
|
|
489
|
+
if (sub === "compact") return runPrdCompact(rest, cwd, env);
|
|
490
|
+
process.stderr.write(`harness prd: unknown sub-command "${sub ?? ""}" (expected status|validate|compact)\n`);
|
|
491
|
+
return 2;
|
|
492
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
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 };
|