@fusengine/harness 0.1.93 → 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/dist/cli/bin.mjs +2 -2
- package/dist/cli/index.mjs +1 -1
- package/dist/{handle-B-g9KL_9.mjs → handle-CDgBbPRz.mjs} +64 -3
- package/dist/{prd-PvK7PKWS.mjs → prd-CixxWJIR.mjs} +1 -29
- package/dist/{apex-task-store-Cb65rE5i.mjs → prd-compact-BE45t8UR.mjs} +33 -1
- package/dist/runtime/index.mjs +1 -1
- package/package.json +1 -1
- package/src/runtime/prd/prd-stop-gate.ts +71 -5
package/dist/cli/bin.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import { r as resolveTtlSec } from "../ttl-Dgwg_QAv.mjs";
|
|
|
4
4
|
import { r as loadDotenv } from "../dotenv-C1LkcfW-.mjs";
|
|
5
5
|
import { t as detectHarness } from "../harness-BMuLJ9lm.mjs";
|
|
6
6
|
import { t as claudeHome } from "../home-state-oUGFB4ds.mjs";
|
|
7
|
-
import { f as checkStaged, m as stagedFiles, p as stagedContent, t as runPrd } from "../prd-
|
|
7
|
+
import { f as checkStaged, m as stagedFiles, p as stagedContent, t as runPrd } from "../prd-CixxWJIR.mjs";
|
|
8
8
|
import { n as writeInitFile, t as initFor } from "../run-GHAMqcOn.mjs";
|
|
9
|
-
import { $ as runDoctor, Bt as todayUtc, I as respond, Q as notify, et as runningVersion, t as handleHook, tt as versionBanner } from "../handle-
|
|
9
|
+
import { $ as runDoctor, Bt as todayUtc, I as respond, Q as notify, et as runningVersion, t as handleHook, tt as versionBanner } from "../handle-CDgBbPRz.mjs";
|
|
10
10
|
import { r as cursorEventContract } from "../normalize-Dy8g9Ybl.mjs";
|
|
11
11
|
import { delimiter, join } from "node:path";
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readSync, readdirSync, writeFileSync } from "node:fs";
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as renderJson, c as positionalArgs, d as resolvePrdArgs, f as checkStaged, i as runPrdStatus, l as readFlag, m as stagedFiles, n as runPrdCompact, o as renderTable, p as stagedContent, r as runPrdValidate, s as hasJsonFlag, t as runPrd, u as resolveAndLoadEnv } from "../prd-
|
|
1
|
+
import { a as renderJson, c as positionalArgs, d as resolvePrdArgs, f as checkStaged, i as runPrdStatus, l as readFlag, m as stagedFiles, n as runPrdCompact, o as renderTable, p as stagedContent, r as runPrdValidate, s as hasJsonFlag, t as runPrd, u as resolveAndLoadEnv } from "../prd-CixxWJIR.mjs";
|
|
2
2
|
export { checkStaged, hasJsonFlag, positionalArgs, readFlag, renderJson, renderTable, resolveAndLoadEnv, resolvePrdArgs, runPrd, runPrdCompact, runPrdStatus, runPrdValidate, stagedContent, stagedFiles };
|
|
@@ -15,7 +15,7 @@ import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
|
15
15
|
import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as setStateField, t as addRoot } from "./registry-IhHk2KlT.mjs";
|
|
16
16
|
import { d as loadIndex, i as cacheLookupMeta, n as webfetchCacheWrite, o as cacheLookupSubstringMeta, t as mcpCacheWrite, u as extractText } from "./mcp-store-BkBDmuxN.mjs";
|
|
17
17
|
import { t as loadRefs } from "./loader-AGz4nK7d.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { C as isCompacted, D as classifyPrdPath, O as isPrdScopedPath, S as filesOf, _ as readRouterSync, a as taskCreate, d as hasAnyViolations, f as incompleteSubTasks, g as readRouter, h as readAllTaskFiles, i as taskComplete, l as prdProjectRoot, m as readAgentReportSync, o as taskStart, p as readAgentReport, r as acquireLock, s as isPrdEnabled, t as canPromoteRouterEntry, u as crossCheckTask, v as readTaskFile, w as subTasksOf, y as readTaskFileSync } from "./prd-compact-BE45t8UR.mjs";
|
|
19
19
|
import { a as sleep, i as readText$1, n as pathExists, o as spawnCapture, s as writeText, t as collectFiles } from "./runtime-io-DuumUeE6.mjs";
|
|
20
20
|
import { C as recordPrdViolation, D as trivialCount, E as recordTrivialEdit, O as apexAuthorizationGate, S as recordPrdStopBlocked, T as recordTarget, _ as prdAlreadyBlocked, a as journalLogPath, b as recordDoc, c as appendEvent, d as signTrack, f as verifyTrack, g as emptyTrack, h as agentsFresh, i as withTrack, l as withTrackLockSync, o as readTrackSync, p as writeLastNonce, r as trackJournalEnabled, s as diffTrackEvents, t as loadTrack, u as LOCK_FAILED, v as recordAgent, w as recordRefRead, x as recordPrdOwner, y as recordBrainstormRequired } from "./store-BVY6gIYM.mjs";
|
|
21
21
|
import { i as parseApplyPatch, n as canonicalizeMcpToolName, r as canonicalizeCodexShellTool, t as isBypassPermissions } from "./permission-mode-B-SFyR0X.mjs";
|
|
@@ -7853,6 +7853,65 @@ function persistStopBlocked(trackFilePath, track, key, now) {
|
|
|
7853
7853
|
writeLastNonce(envelope.nonce);
|
|
7854
7854
|
});
|
|
7855
7855
|
}
|
|
7856
|
+
/** Task names whose task-PRD is fully validated (every agent, every sub-task) but not yet compacted (`prd-compact.ts`'s own per-agent test, applied task-wide). */
|
|
7857
|
+
function compactableTasks(router, taskFiles) {
|
|
7858
|
+
const ready = [];
|
|
7859
|
+
for (const task of Object.keys(router)) {
|
|
7860
|
+
const taskFile = taskFiles[task];
|
|
7861
|
+
if (!taskFile || canPromoteRouterEntry(taskFile)) continue;
|
|
7862
|
+
if (Object.values(taskFile).every((entry) => {
|
|
7863
|
+
if (isCompacted(entry)) return true;
|
|
7864
|
+
const subs = Object.values(subTasksOf(entry));
|
|
7865
|
+
return subs.length > 0 && subs.every((s) => s.status === "validated");
|
|
7866
|
+
})) ready.push(task);
|
|
7867
|
+
}
|
|
7868
|
+
return ready;
|
|
7869
|
+
}
|
|
7870
|
+
/** Names the compactable task(s) and the exact `harness prd compact` invocation (verified against `cli/prd/compact.ts`'s own usage string). */
|
|
7871
|
+
function compactHintText(tasks) {
|
|
7872
|
+
if (tasks.length === 1) return `PRD task "${tasks[0]}" is fully validated and ready to compact. Run \`harness prd compact ${tasks[0]}\`.`;
|
|
7873
|
+
return `PRD tasks fully validated and ready to compact: ${tasks.join(", ")}. Run \`harness prd compact <task>\` for each.`;
|
|
7874
|
+
}
|
|
7875
|
+
/**
|
|
7876
|
+
* Non-blocking "ready to compact" hint. Target-agnostic by design: an id
|
|
7877
|
+
* allowlist here would silently go stale the day a target's real Stop wiring
|
|
7878
|
+
* changes, so this mirrors the SAME per-target form choice as the block path
|
|
7879
|
+
* directly below instead of hardcoding one target — kimi/gemini-cli/cline
|
|
7880
|
+
* get their own native `respond()` inform envelope, everyone else gets the
|
|
7881
|
+
* `Stop` `additionalContext` channel. Whether this function is ever REACHED
|
|
7882
|
+
* for a given id is a wiring question, not a code question: measured live
|
|
7883
|
+
* against the real hooks.json this project ships (claude-plugins/
|
|
7884
|
+
* codex-plugins/kimi-code-plugins) plus each target's own hook docs — only
|
|
7885
|
+
* Codex's Stop wiring is a plain `hook codex core` with no short-circuit;
|
|
7886
|
+
* claude-code's and Kimi's own Stop hook always carries `--sound stop`,
|
|
7887
|
+
* which exits in `maybePlaySound()` (`src/cli/hook-sound.ts`) before stdin
|
|
7888
|
+
* is ever read, and hermes/gemini-cli/cline have no event literally named
|
|
7889
|
+
* `Stop` in their own documented hook taxonomy at all — see prd.md's Known
|
|
7890
|
+
* limitations for the full measurement. One-shot per session, on a marker
|
|
7891
|
+
* key DISTINCT from the block-once key so neither shadows the other.
|
|
7892
|
+
* @param id - Harness target id.
|
|
7893
|
+
* @param payload - The raw Stop hook payload.
|
|
7894
|
+
* @param router - The parsed router.
|
|
7895
|
+
* @param taskFiles - Every router task's parsed task-PRD (or `null`).
|
|
7896
|
+
* @param track - The current session track.
|
|
7897
|
+
* @param trackFilePath - The session track file path.
|
|
7898
|
+
* @param now - Event clock.
|
|
7899
|
+
* @returns The native non-blocking stdout (once per session), else `""`.
|
|
7900
|
+
*/
|
|
7901
|
+
function compactReminder(id, payload, router, taskFiles, track, trackFilePath, now) {
|
|
7902
|
+
const ready = compactableTasks(router, taskFiles);
|
|
7903
|
+
if (ready.length === 0) return "";
|
|
7904
|
+
const key = `${typeof payload.session_id === "string" ? payload.session_id : "unknown"}:StopCompactHint`;
|
|
7905
|
+
if (prdAlreadyBlocked(track, key)) return "";
|
|
7906
|
+
persistStopBlocked(trackFilePath, track, key, now);
|
|
7907
|
+
const text = compactHintText(ready);
|
|
7908
|
+
if (id === "kimi" || id === "gemini-cli" || id === "cline") return respond(id, {
|
|
7909
|
+
kind: "inform",
|
|
7910
|
+
title: "PRD compact",
|
|
7911
|
+
reason: text
|
|
7912
|
+
}, "Stop");
|
|
7913
|
+
return contextResponse("Stop", text);
|
|
7914
|
+
}
|
|
7856
7915
|
/**
|
|
7857
7916
|
* Run the PRD lead-Stop block-once gate.
|
|
7858
7917
|
* @param payload - The raw Stop hook payload.
|
|
@@ -7860,7 +7919,8 @@ function persistStopBlocked(trackFilePath, track, key, now) {
|
|
|
7860
7919
|
* @param id - Harness target id.
|
|
7861
7920
|
* @param trackFilePath - The session track file path.
|
|
7862
7921
|
* @param now - Event clock.
|
|
7863
|
-
* @returns The native block stdout (once per session)
|
|
7922
|
+
* @returns The native block stdout (once per session); else the compact
|
|
7923
|
+
* hint (once per session, whichever ids' real Stop wiring reaches it); else `""`.
|
|
7864
7924
|
*/
|
|
7865
7925
|
function prdStopGate(payload, cwd, id, trackFilePath, now) {
|
|
7866
7926
|
if (!isPrdEnabled(cwd, id)) return "";
|
|
@@ -7871,7 +7931,8 @@ function prdStopGate(payload, cwd, id, trackFilePath, now) {
|
|
|
7871
7931
|
if (!router) return "";
|
|
7872
7932
|
const track = readTrackSync(trackFilePath, trackJournalEnabled());
|
|
7873
7933
|
if (!((track.prdViolations?.length ?? 0) > 0)) {
|
|
7874
|
-
|
|
7934
|
+
const taskFiles = readAllTaskFilesSync(root, homeSeg, router);
|
|
7935
|
+
if (!hasAnyViolations(router, taskFiles)) return compactReminder(id, payload, router, taskFiles, track, trackFilePath, now);
|
|
7875
7936
|
}
|
|
7876
7937
|
const key = `${typeof payload.session_id === "string" ? payload.session_id : "unknown"}:Stop`;
|
|
7877
7938
|
if (prdAlreadyBlocked(track, key)) return "";
|
|
@@ -3,7 +3,7 @@ import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
|
3
3
|
import { t as evaluate } from "./evaluate-CMiIqHeH.mjs";
|
|
4
4
|
import { r as harnessHomeSegment } from "./apex-target-Xc2M32Pl.mjs";
|
|
5
5
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
6
|
-
import { C as
|
|
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
7
|
import { join } from "node:path";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
@@ -407,34 +407,6 @@ async function runPrdValidate(argv, cwd, env = process.env) {
|
|
|
407
407
|
return 0;
|
|
408
408
|
}
|
|
409
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
410
|
//#region src/cli/prd/compact.ts
|
|
439
411
|
/**
|
|
440
412
|
* `harness prd compact <task> [--id] [--root]` — collapses every fully
|
|
@@ -464,4 +464,36 @@ async function taskComplete(file, id) {
|
|
|
464
464
|
await writeJsonFile(file, data);
|
|
465
465
|
}
|
|
466
466
|
//#endregion
|
|
467
|
-
|
|
467
|
+
//#region src/policy/prd/prd-compact.ts
|
|
468
|
+
/** Collapses fully-validated task-PRD agent entries to their compact shape. Pure — no fs. */
|
|
469
|
+
/** Collapses one agent entry to the compacted shape ONLY when every sub-task is `validated`; else returns it unchanged. */
|
|
470
|
+
function compactAgentEntry(e, at) {
|
|
471
|
+
if (isCompacted(e)) return e;
|
|
472
|
+
const subTasks = Object.values(subTasksOf(e));
|
|
473
|
+
if (subTasks.length === 0 || !subTasks.every((s) => s.status === "validated")) return e;
|
|
474
|
+
return {
|
|
475
|
+
status: "validated",
|
|
476
|
+
files: e.files,
|
|
477
|
+
"validated-at": at
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/** Compacts every eligible agent entry in a task-PRD; reports which agent names got collapsed. */
|
|
481
|
+
function compactTaskFile(taskFile, at) {
|
|
482
|
+
const compacted = [];
|
|
483
|
+
const file = {};
|
|
484
|
+
for (const [agent, entry] of Object.entries(taskFile)) {
|
|
485
|
+
const next = compactAgentEntry(entry, at);
|
|
486
|
+
if (next !== entry) compacted.push(agent);
|
|
487
|
+
file[agent] = next;
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
file,
|
|
491
|
+
compacted
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** True when every agent in the task-PRD is already compacted+validated. */
|
|
495
|
+
function canPromoteRouterEntry(taskFile) {
|
|
496
|
+
return Object.values(taskFile).every((entry) => isCompacted(entry));
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
export { prdRouterPath as A, isCompacted as C, classifyPrdPath as D, withSubTaskValidated as E, isPrdScopedPath as O, filesOf as S, withRouterStatus as T, readRouterSync as _, taskCreate as a, writeRouter as b, isPrdFlagSet as c, hasAnyViolations as d, incompleteSubTasks as f, readRouter as g, readAllTaskFiles as h, taskComplete as i, prdDir as k, prdProjectRoot as l, readAgentReportSync as m, compactTaskFile as n, taskStart as o, readAgentReport as p, acquireLock as r, isPrdEnabled as s, canPromoteRouterEntry as t, crossCheckTask as u, readTaskFile as v, subTasksOf as w, writeTaskFile as x, readTaskFileSync as y };
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-KWoE_Mqn.mjs";
|
|
2
2
|
import { a as sanitizeSessionId, c as sessionsDir, i as loadSessionState, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, s as sessionStatePath, t as claudeHome } from "../home-state-oUGFB4ds.mjs";
|
|
3
|
-
import { A as gate, At as taskContext, B as writePluginMap, Bt as todayUtc, C as trackEnrichment, Ct as removeOldFiles, D as DEFAULT_WINDOW_MS, Dt as projectContext, E as dispatchAipilot, Et as gitContext, F as extractSymbols, Ft as isoUtc, G as mergeLines, H as isProject, I as respond, It as loadSecurityState, J as listChildren, K as countFiles, L as dispatchLessons, Lt as saveSecurityState, M as preCommitGate, Mt as projectHash, N as detectDuplication, Nt as trackFile, O as REQUIRED_AGENTS, Ot as claudeMdKey, P as dryGate, Pt as normalizeEvent, R as cartoSessionStart, Rt as securityStateDir, S as trackSkillRead, St as purgeTtlTree, T as aipilotPostToolUse, Tt as devContext, U as writeTree, V as generateProjectMap, W as loadEnriched, X as lessonsFileFor, Y as lessonsArchiveFileFor, Z as lessonsStateFileFor, _ as securityAdvisory, _t as injectRules, a as mcpPostStore, at as cleanupSession, b as trackWatchResearch, bt as sessionStartCore, c as WEBFETCH_TTL_MS, ct as validateTeammateOutput, d as queryOf, dt as validateTailwind, f as lifecycleStdout, ft as validateSolidGate, g as dispatchMemory, gt as solidDetectStart, h as seoPostToolUseResponse, ht as detectSolidProfile, i as recordActivity, it as validateRulesLoaded, j as gateCommandCandidates, jt as defaultStateDir, k as TRIVIAL_BUDGET, kt as promptSubmitContext, l as cacheQueryOf, lt as trackAgentMemory, m as seoPostToolUse, mt as countLoc, n as activityFor, nt as postEditTypescript, o as mcpPreIntercept, ot as saveApexState, p as postEditContext, pt as checkFileSize, q as getFileDesc, r as handlePre, rt as trackSessionChanges, s as MCP_TTL_MS, st as logToolFailure, t as handleHook, u as isMcpTool, ut as subagentCacheContext, v as securityAdvisoryForPatch, vt as readRules, w as dispatchLifecycle, wt as trimLogFile, x as trackMcpResearch, xt as pruneEmptyDirs, y as postTrackingSideEffects, yt as runSessionStartCleanups, z as generateEcosystemMap, zt as securityStatePath } from "../handle-
|
|
3
|
+
import { A as gate, At as taskContext, B as writePluginMap, Bt as todayUtc, C as trackEnrichment, Ct as removeOldFiles, D as DEFAULT_WINDOW_MS, Dt as projectContext, E as dispatchAipilot, Et as gitContext, F as extractSymbols, Ft as isoUtc, G as mergeLines, H as isProject, I as respond, It as loadSecurityState, J as listChildren, K as countFiles, L as dispatchLessons, Lt as saveSecurityState, M as preCommitGate, Mt as projectHash, N as detectDuplication, Nt as trackFile, O as REQUIRED_AGENTS, Ot as claudeMdKey, P as dryGate, Pt as normalizeEvent, R as cartoSessionStart, Rt as securityStateDir, S as trackSkillRead, St as purgeTtlTree, T as aipilotPostToolUse, Tt as devContext, U as writeTree, V as generateProjectMap, W as loadEnriched, X as lessonsFileFor, Y as lessonsArchiveFileFor, Z as lessonsStateFileFor, _ as securityAdvisory, _t as injectRules, a as mcpPostStore, at as cleanupSession, b as trackWatchResearch, bt as sessionStartCore, c as WEBFETCH_TTL_MS, ct as validateTeammateOutput, d as queryOf, dt as validateTailwind, f as lifecycleStdout, ft as validateSolidGate, g as dispatchMemory, gt as solidDetectStart, h as seoPostToolUseResponse, ht as detectSolidProfile, i as recordActivity, it as validateRulesLoaded, j as gateCommandCandidates, jt as defaultStateDir, k as TRIVIAL_BUDGET, kt as promptSubmitContext, l as cacheQueryOf, lt as trackAgentMemory, m as seoPostToolUse, mt as countLoc, n as activityFor, nt as postEditTypescript, o as mcpPreIntercept, ot as saveApexState, p as postEditContext, pt as checkFileSize, q as getFileDesc, r as handlePre, rt as trackSessionChanges, s as MCP_TTL_MS, st as logToolFailure, t as handleHook, u as isMcpTool, ut as subagentCacheContext, v as securityAdvisoryForPatch, vt as readRules, w as dispatchLifecycle, wt as trimLogFile, x as trackMcpResearch, xt as pruneEmptyDirs, y as postTrackingSideEffects, yt as runSessionStartCleanups, z as generateEcosystemMap, zt as securityStatePath } from "../handle-CDgBbPRz.mjs";
|
|
4
4
|
//#region src/runtime/storage.ts
|
|
5
5
|
/**
|
|
6
6
|
* The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
|
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",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import { dirname } from "node:path";
|
|
10
10
|
import { harnessHomeSegment } from "../../policy/apex-target";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
type PrdRouter, type PrdTaskFile,
|
|
12
|
+
canPromoteRouterEntry, hasAnyViolations, isCompacted, isPrdEnabled, prdProjectRoot,
|
|
13
|
+
readRouterSync, readTaskFileSync, subTasksOf, type PrdRouter, type PrdTaskFile,
|
|
14
14
|
} from "../../policy/prd";
|
|
15
15
|
import { journalLogPath, readTrackSync } from "../../tracking/track-compact";
|
|
16
16
|
import { trackJournalEnabled } from "../../tracking/store";
|
|
@@ -20,7 +20,7 @@ import { prdAlreadyBlocked, recordPrdStopBlocked, type SessionTrack } from "../.
|
|
|
20
20
|
import { signTrack, writeLastNonce } from "../../tracking/integrity";
|
|
21
21
|
import { withTrackLockSync } from "../../tracking/track-lock-sync";
|
|
22
22
|
import { atomicWrite } from "../../util/json-io";
|
|
23
|
-
import { blockResponse } from "../../adapters/claude";
|
|
23
|
+
import { blockResponse, contextResponse } from "../../adapters/claude";
|
|
24
24
|
import { respond } from "../respond";
|
|
25
25
|
|
|
26
26
|
const REASON = "PRD cross-check found unresolved violation(s): a task/sub-task is marked validated without a matching done report. Run `harness prd status` for details.";
|
|
@@ -57,6 +57,71 @@ function persistStopBlocked(trackFilePath: string, track: SessionTrack, key: str
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Task names whose task-PRD is fully validated (every agent, every sub-task) but not yet compacted (`prd-compact.ts`'s own per-agent test, applied task-wide). */
|
|
61
|
+
function compactableTasks(router: PrdRouter, taskFiles: Record<string, PrdTaskFile | null>): string[] {
|
|
62
|
+
const ready: string[] = [];
|
|
63
|
+
for (const task of Object.keys(router)) {
|
|
64
|
+
const taskFile = taskFiles[task];
|
|
65
|
+
if (!taskFile || canPromoteRouterEntry(taskFile)) continue; // missing/unparseable, or already fully compacted
|
|
66
|
+
const fullyValidated = Object.values(taskFile).every((entry) => {
|
|
67
|
+
if (isCompacted(entry)) return true;
|
|
68
|
+
const subs = Object.values(subTasksOf(entry));
|
|
69
|
+
return subs.length > 0 && subs.every((s) => s.status === "validated");
|
|
70
|
+
});
|
|
71
|
+
if (fullyValidated) ready.push(task);
|
|
72
|
+
}
|
|
73
|
+
return ready;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Names the compactable task(s) and the exact `harness prd compact` invocation (verified against `cli/prd/compact.ts`'s own usage string). */
|
|
77
|
+
function compactHintText(tasks: string[]): string {
|
|
78
|
+
if (tasks.length === 1) return `PRD task "${tasks[0]}" is fully validated and ready to compact. Run \`harness prd compact ${tasks[0]}\`.`;
|
|
79
|
+
return `PRD tasks fully validated and ready to compact: ${tasks.join(", ")}. Run \`harness prd compact <task>\` for each.`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Non-blocking "ready to compact" hint. Target-agnostic by design: an id
|
|
84
|
+
* allowlist here would silently go stale the day a target's real Stop wiring
|
|
85
|
+
* changes, so this mirrors the SAME per-target form choice as the block path
|
|
86
|
+
* directly below instead of hardcoding one target — kimi/gemini-cli/cline
|
|
87
|
+
* get their own native `respond()` inform envelope, everyone else gets the
|
|
88
|
+
* `Stop` `additionalContext` channel. Whether this function is ever REACHED
|
|
89
|
+
* for a given id is a wiring question, not a code question: measured live
|
|
90
|
+
* against the real hooks.json this project ships (claude-plugins/
|
|
91
|
+
* codex-plugins/kimi-code-plugins) plus each target's own hook docs — only
|
|
92
|
+
* Codex's Stop wiring is a plain `hook codex core` with no short-circuit;
|
|
93
|
+
* claude-code's and Kimi's own Stop hook always carries `--sound stop`,
|
|
94
|
+
* which exits in `maybePlaySound()` (`src/cli/hook-sound.ts`) before stdin
|
|
95
|
+
* is ever read, and hermes/gemini-cli/cline have no event literally named
|
|
96
|
+
* `Stop` in their own documented hook taxonomy at all — see prd.md's Known
|
|
97
|
+
* limitations for the full measurement. One-shot per session, on a marker
|
|
98
|
+
* key DISTINCT from the block-once key so neither shadows the other.
|
|
99
|
+
* @param id - Harness target id.
|
|
100
|
+
* @param payload - The raw Stop hook payload.
|
|
101
|
+
* @param router - The parsed router.
|
|
102
|
+
* @param taskFiles - Every router task's parsed task-PRD (or `null`).
|
|
103
|
+
* @param track - The current session track.
|
|
104
|
+
* @param trackFilePath - The session track file path.
|
|
105
|
+
* @param now - Event clock.
|
|
106
|
+
* @returns The native non-blocking stdout (once per session), else `""`.
|
|
107
|
+
*/
|
|
108
|
+
function compactReminder(
|
|
109
|
+
id: string, payload: Record<string, unknown>, router: PrdRouter,
|
|
110
|
+
taskFiles: Record<string, PrdTaskFile | null>, track: SessionTrack, trackFilePath: string, now: number,
|
|
111
|
+
): string {
|
|
112
|
+
const ready = compactableTasks(router, taskFiles);
|
|
113
|
+
if (ready.length === 0) return "";
|
|
114
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
115
|
+
const key = `${sessionId}:StopCompactHint`;
|
|
116
|
+
if (prdAlreadyBlocked(track, key)) return "";
|
|
117
|
+
persistStopBlocked(trackFilePath, track, key, now);
|
|
118
|
+
const text = compactHintText(ready);
|
|
119
|
+
if (id === "kimi" || id === "gemini-cli" || id === "cline") {
|
|
120
|
+
return respond(id, { kind: "inform", title: "PRD compact", reason: text }, "Stop");
|
|
121
|
+
}
|
|
122
|
+
return contextResponse("Stop", text);
|
|
123
|
+
}
|
|
124
|
+
|
|
60
125
|
/**
|
|
61
126
|
* Run the PRD lead-Stop block-once gate.
|
|
62
127
|
* @param payload - The raw Stop hook payload.
|
|
@@ -64,7 +129,8 @@ function persistStopBlocked(trackFilePath: string, track: SessionTrack, key: str
|
|
|
64
129
|
* @param id - Harness target id.
|
|
65
130
|
* @param trackFilePath - The session track file path.
|
|
66
131
|
* @param now - Event clock.
|
|
67
|
-
* @returns The native block stdout (once per session)
|
|
132
|
+
* @returns The native block stdout (once per session); else the compact
|
|
133
|
+
* hint (once per session, whichever ids' real Stop wiring reaches it); else `""`.
|
|
68
134
|
*/
|
|
69
135
|
export function prdStopGate(payload: Record<string, unknown>, cwd: string, id: string, trackFilePath: string, now: number): string {
|
|
70
136
|
if (!isPrdEnabled(cwd, id)) return "";
|
|
@@ -82,7 +148,7 @@ export function prdStopGate(payload: Record<string, unknown>, cwd: string, id: s
|
|
|
82
148
|
const trackHasViolations = (track.prdViolations?.length ?? 0) > 0;
|
|
83
149
|
if (!trackHasViolations) {
|
|
84
150
|
const taskFiles = readAllTaskFilesSync(root, homeSeg, router);
|
|
85
|
-
if (!hasAnyViolations(router, taskFiles)) return
|
|
151
|
+
if (!hasAnyViolations(router, taskFiles)) return compactReminder(id, payload, router, taskFiles, track, trackFilePath, now);
|
|
86
152
|
}
|
|
87
153
|
|
|
88
154
|
const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|