@fusengine/harness 0.1.92 → 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/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 +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-B-g9KL_9.mjs} +4583 -3695
- 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-PvK7PKWS.mjs +494 -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 +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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module prd-subagent-context
|
|
3
|
+
* SubagentStart injection (design doc §2.3) — the sub-agent's own PRD slice,
|
|
4
|
+
* never the whole task. Kept SYNCHRONOUS on purpose: `dispatchLifecycle` is
|
|
5
|
+
* called synchronously (no `await`) from `lifecycle-bridge.ts`/`handle.ts`,
|
|
6
|
+
* and making that chain async for one new injector would touch 3 unrelated
|
|
7
|
+
* files purely for signature plumbing — `prd-io.ts`'s sync twins
|
|
8
|
+
* (`readRouterSync`/`readTaskFileSync`) exist precisely so this call site
|
|
9
|
+
* needs zero signature changes anywhere else.
|
|
10
|
+
*/
|
|
11
|
+
import { contextResponse } from "../../adapters/claude";
|
|
12
|
+
import { harnessHomeSegment } from "../../policy/apex-target";
|
|
13
|
+
import {
|
|
14
|
+
agentSlices, isPrdEnabled, prdProjectRoot, readRouterSync, readTaskFileSync, renderAgentSliceMarkdown,
|
|
15
|
+
type PrdSubagentSlice,
|
|
16
|
+
} from "../../policy/prd";
|
|
17
|
+
import { respond } from "../respond";
|
|
18
|
+
|
|
19
|
+
/** Compliance reminder appended after the rendered slice(s) — the 3 rules a sub-agent must keep in mind. */
|
|
20
|
+
const RULES = [
|
|
21
|
+
"1. Write ONLY the files listed in your slice above.",
|
|
22
|
+
"2. Report completion to YOUR OWN agent-report file above — never another agent's.",
|
|
23
|
+
"3. Do not mark a sub-task done until the work is actually finished; the coordinator validates from your report.",
|
|
24
|
+
].join("\n");
|
|
25
|
+
|
|
26
|
+
/** `agent_type`, same fallback `dispatch-aipilot.ts`'s `agentTypeOf` uses (duplicated here per SOLID file-size discipline, not cross-imported). */
|
|
27
|
+
function agentTypeOf(payload: Record<string, unknown>): string {
|
|
28
|
+
return String(payload.agent_type ?? payload.subagent_type ?? "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the SubagentStart PRD context injection for this agent, or `""` when
|
|
33
|
+
* the module is off, the caller is unnamed, or nothing in the router
|
|
34
|
+
* concerns it.
|
|
35
|
+
* @param payload - The raw SubagentStart hook payload.
|
|
36
|
+
* @param cwd - Project root.
|
|
37
|
+
* @param id - Harness target id.
|
|
38
|
+
* @returns The native `additionalContext` hook stdout, or `""`.
|
|
39
|
+
*/
|
|
40
|
+
export function prdSubagentContext(payload: Record<string, unknown>, cwd: string, id: string): string {
|
|
41
|
+
if (!isPrdEnabled(cwd, id)) return "";
|
|
42
|
+
const agentType = agentTypeOf(payload);
|
|
43
|
+
if (!agentType) return "";
|
|
44
|
+
const root = prdProjectRoot(cwd);
|
|
45
|
+
const homeSeg = harnessHomeSegment(id);
|
|
46
|
+
const router = readRouterSync(root, homeSeg);
|
|
47
|
+
if (!router) return "";
|
|
48
|
+
|
|
49
|
+
const slices: PrdSubagentSlice[] = [];
|
|
50
|
+
for (const [task, entry] of Object.entries(router)) {
|
|
51
|
+
const taskFile = readTaskFileSync(root, homeSeg, entry.prd);
|
|
52
|
+
if (!taskFile) continue;
|
|
53
|
+
slices.push(...agentSlices(taskFile, agentType, task));
|
|
54
|
+
}
|
|
55
|
+
if (slices.length === 0) return "";
|
|
56
|
+
|
|
57
|
+
const text = `${renderAgentSliceMarkdown(slices)}\n\n### Rules\n${RULES}`;
|
|
58
|
+
// gemini-cli/cline don't understand Claude's hookSpecificOutput.additionalContext
|
|
59
|
+
// envelope: gemini-cli's native shape has no hookEventName field and cline's
|
|
60
|
+
// is `{contextModification}` entirely — route them through respond()'s
|
|
61
|
+
// already-correct "inform" branches. claude-code/codex/kimi/cursor/hermes
|
|
62
|
+
// stay on contextResponse unchanged: respond()'s "inform" kind re-wraps the
|
|
63
|
+
// text through formatPrompt (adds a "[NOTE] title" line), which would NOT
|
|
64
|
+
// reproduce today's byte-identical raw-markdown output.
|
|
65
|
+
if (id === "gemini-cli" || id === "cline") {
|
|
66
|
+
return respond(id, { kind: "inform", title: "PRD assignment", reason: text }, "SubagentStart");
|
|
67
|
+
}
|
|
68
|
+
return contextResponse("SubagentStart", text);
|
|
69
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module prd-subagent-stop
|
|
3
|
+
* Block-once gate for a finishing sub-agent with incomplete PRD sub-tasks
|
|
4
|
+
* (design doc §2.4). SYNC on purpose — the SubagentStop dispatch runs
|
|
5
|
+
* synchronously in a short-lived hook process; an async write could be
|
|
6
|
+
* dropped before exit (same reasoning as `track-lock-sync.ts`).
|
|
7
|
+
*/
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import { harnessHomeSegment } from "../../policy/apex-target";
|
|
10
|
+
import {
|
|
11
|
+
candidateAgentNames, incompleteSubTasks, isPrdEnabled, prdProjectRoot,
|
|
12
|
+
readAgentReportSync, readRouterSync, readTaskFileSync, type PrdTaskFile,
|
|
13
|
+
} from "../../policy/prd";
|
|
14
|
+
import { journalLogPath, readTrackSync } from "../../tracking/track-compact";
|
|
15
|
+
import { trackJournalEnabled } from "../../tracking/store";
|
|
16
|
+
import { appendEvent } from "../../tracking/track-journal";
|
|
17
|
+
import { diffTrackEvents } from "../../tracking/track-diff";
|
|
18
|
+
import { prdAlreadyBlocked, recordPrdStopBlocked, type SessionTrack } from "../../tracking/session-state";
|
|
19
|
+
import { signTrack, writeLastNonce } from "../../tracking/integrity";
|
|
20
|
+
import { withTrackLockSync } from "../../tracking/track-lock-sync";
|
|
21
|
+
import { atomicWrite } from "../../util/json-io";
|
|
22
|
+
import { blockResponse } from "../../adapters/claude";
|
|
23
|
+
import { respond } from "../respond";
|
|
24
|
+
|
|
25
|
+
/** `agent_type`, same fallback duplicated across the PRD runtime modules (SOLID file-size discipline). */
|
|
26
|
+
function agentTypeOf(payload: Record<string, unknown>): string {
|
|
27
|
+
return String(payload.agent_type ?? payload.subagent_type ?? "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Renders the block reason for one finishing agent's incomplete sub-tasks. */
|
|
31
|
+
function reasonFor(agent: string, task: string, subs: string[]): string {
|
|
32
|
+
return `PRD sub-task(s) not done for ${agent} on task "${task}": ${subs.join(", ")}. Finish the work (or ask the coordinator to reassign) before stopping.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One-shot key for this finishing instance. Keyed by `agentId` when known —
|
|
37
|
+
* the STABLE identity of "this specific sub-agent call", regardless of which
|
|
38
|
+
* same-`agentType` candidate name it happens to match — so an unbound agent
|
|
39
|
+
* (no prior Write) whose incompleteness is first found under one sibling
|
|
40
|
+
* name doesn't get blocked a SECOND time on replay under the other sibling
|
|
41
|
+
* name (both unbound candidates are checked every call; only the key must
|
|
42
|
+
* stay identical). Falls back to per-name keying when `agentId` is absent
|
|
43
|
+
* (Kimi-shaped payloads structurally never carry it).
|
|
44
|
+
*/
|
|
45
|
+
function stopKey(sessionId: string, agentId: string | undefined, agent: string): string {
|
|
46
|
+
return agentId !== undefined ? `${sessionId}:SubagentStop:agent:${agentId}` : `${sessionId}:SubagentStop:${agent}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Narrows the agentType-matching candidates to the ONE this specific
|
|
51
|
+
* `agent_id` is already bound to (per `prdOwners`), when known — otherwise
|
|
52
|
+
* every same-`agentType` sibling would be checked (and could block THIS
|
|
53
|
+
* instance for a SIBLING's incomplete work, e.g. `backend-expert` vs
|
|
54
|
+
* `backend-expert-2` sharing the same reported `agent_type`). Unbound (the
|
|
55
|
+
* agent hasn't written anything yet) falls back to checking every candidate,
|
|
56
|
+
* the only defensible default with no binding to disambiguate on.
|
|
57
|
+
*/
|
|
58
|
+
function resolveCandidates(agentType: string, taskFile: PrdTaskFile, agentId: string | undefined, bindings: Record<string, string>): string[] {
|
|
59
|
+
const all = candidateAgentNames(agentType, taskFile);
|
|
60
|
+
const bound = agentId !== undefined ? bindings[agentId] : undefined;
|
|
61
|
+
return bound !== undefined && all.includes(bound) ? [bound] : all;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Persist the one-shot block marker on the SAME side `trackJournalEnabled()`
|
|
66
|
+
* reads from — mirrors `harvestSubagentTrack`'s branching (`evidence-harvest-
|
|
67
|
+
* io.ts`), the codebase's only other sync lifecycle writer with this exact
|
|
68
|
+
* constraint. Bug this fixes: the old code always appended to the journal
|
|
69
|
+
* regardless of mode, but `readTrackSync(file, trackJournalEnabled())` never
|
|
70
|
+
* folds the log when `FUSE_TRACK_JOURNAL=0` (track-compact.ts) — so a legacy
|
|
71
|
+
* replay never saw the marker and re-blocked on every call. Journal mode
|
|
72
|
+
* (default): unchanged, append the diff. Legacy mode: locked read-modify-
|
|
73
|
+
* write straight into the snapshot (`signTrack` + atomic write + nonce, same
|
|
74
|
+
* body as `store.ts#saveTrack`'s legacy branch), so the very next
|
|
75
|
+
* `readTrackSync(file, false)` sees it.
|
|
76
|
+
*/
|
|
77
|
+
function persistStopBlocked(trackFilePath: string, track: SessionTrack, key: string, now: number): void {
|
|
78
|
+
if (trackJournalEnabled()) {
|
|
79
|
+
const next = recordPrdStopBlocked(track, key, now);
|
|
80
|
+
for (const ev of diffTrackEvents(track, next, now)) appendEvent(journalLogPath(trackFilePath), ev.field, ev.op, ev.value, ev.ts);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
withTrackLockSync(dirname(trackFilePath), () => {
|
|
84
|
+
const fresh = readTrackSync(trackFilePath, false); // reload under lock: avoid clobbering a concurrent legacy RMW
|
|
85
|
+
const envelope = signTrack(recordPrdStopBlocked(fresh, key, now));
|
|
86
|
+
atomicWrite(trackFilePath, JSON.stringify(envelope, null, 2));
|
|
87
|
+
writeLastNonce(envelope.nonce);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run the PRD SubagentStop block-once gate.
|
|
93
|
+
* @param payload - The raw SubagentStop hook payload.
|
|
94
|
+
* @param cwd - Project root.
|
|
95
|
+
* @param id - Harness target id.
|
|
96
|
+
* @param trackFilePath - The session track file path.
|
|
97
|
+
* @param now - Event clock.
|
|
98
|
+
* @returns `null` when PRD had nothing to say (module off, unnamed agent, or
|
|
99
|
+
* every assigned sub-task is genuinely done) — the caller falls through to
|
|
100
|
+
* its normal SubagentStop handling (e.g. `trackAgentMemory`). A non-null
|
|
101
|
+
* string means PRD DID find an incomplete sub-task for this agent: the
|
|
102
|
+
* block stdout the first time, or `""` on every later replay of the SAME
|
|
103
|
+
* incompleteness (already-blocked-once) — either way the caller must return
|
|
104
|
+
* it AS-IS and skip its normal handling (a stale "agent completed" message
|
|
105
|
+
* would contradict the block that was just — or already — issued).
|
|
106
|
+
*/
|
|
107
|
+
export function prdSubagentStopGate(payload: Record<string, unknown>, cwd: string, id: string, trackFilePath: string, now: number): string | null {
|
|
108
|
+
if (!isPrdEnabled(cwd, id)) return null;
|
|
109
|
+
// Cursor's sub-agent identity is structurally unlinkable at this event
|
|
110
|
+
// (design doc Risks §1) and Cursor never honors a block on SubagentStop
|
|
111
|
+
// anyway (adapters.md) — never even build a block payload for it.
|
|
112
|
+
if (id === "cursor") return null;
|
|
113
|
+
const agentType = agentTypeOf(payload);
|
|
114
|
+
if (!agentType) return null;
|
|
115
|
+
const agentId = typeof payload.agent_id === "string" ? payload.agent_id : undefined;
|
|
116
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
117
|
+
const root = prdProjectRoot(cwd);
|
|
118
|
+
const homeSeg = harnessHomeSegment(id);
|
|
119
|
+
const router = readRouterSync(root, homeSeg);
|
|
120
|
+
if (!router) return null;
|
|
121
|
+
const track = readTrackSync(trackFilePath, trackJournalEnabled());
|
|
122
|
+
const bindings = track.prdOwners ?? {};
|
|
123
|
+
|
|
124
|
+
for (const [task, entry] of Object.entries(router)) {
|
|
125
|
+
const taskFile = readTaskFileSync(root, homeSeg, entry.prd);
|
|
126
|
+
if (!taskFile) continue;
|
|
127
|
+
for (const agent of resolveCandidates(agentType, taskFile, agentId, bindings)) {
|
|
128
|
+
const report = readAgentReportSync(root, homeSeg, agent);
|
|
129
|
+
const incomplete = incompleteSubTasks(taskFile, agent, task, report);
|
|
130
|
+
if (incomplete.length === 0) continue;
|
|
131
|
+
const key = stopKey(sessionId, agentId, agent);
|
|
132
|
+
if (prdAlreadyBlocked(track, key)) return ""; // already blocked once — silent, but still skip trackAgentMemory
|
|
133
|
+
persistStopBlocked(trackFilePath, track, key, now);
|
|
134
|
+
const reason = reasonFor(agent, task, incomplete);
|
|
135
|
+
// gemini-cli/cline have their OWN native block envelopes (respond()
|
|
136
|
+
// already knows them: `{decision:"deny"}` / `{cancel:true}`) — the old
|
|
137
|
+
// hardcoded `blockResponse` sent them the Claude `{decision:"block"}`
|
|
138
|
+
// shape, which neither honors. claude-code/codex/hermes stay on
|
|
139
|
+
// `blockResponse` unchanged: `respond()`'s claude-code/codex branch
|
|
140
|
+
// always calls `denyResponse` (a PreToolUse-only `permissionDecision`,
|
|
141
|
+
// ignored on Stop-family events — see `blockResponse`'s own doc
|
|
142
|
+
// comment), and hermes's `toHermesResponse` re-wraps the reason through
|
|
143
|
+
// `formatPrompt` (adds a "[BLOCKED] title" line) — neither reproduces
|
|
144
|
+
// today's byte-identical `{decision:"block",reason}`.
|
|
145
|
+
if (id === "kimi" || id === "gemini-cli" || id === "cline") {
|
|
146
|
+
return respond(id, { kind: "block", title: "PRD sub-task incomplete", reason }, "SubagentStop");
|
|
147
|
+
}
|
|
148
|
+
return blockResponse(reason);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
@@ -25,7 +25,25 @@ export interface SessionTrack {
|
|
|
25
25
|
brainstormRequired?: boolean;
|
|
26
26
|
/** Verification receipts (tsc/test) at PostToolUse; absent/empty reads as unverified in the TaskCompleted gate (backward compat, fail-closed). See {@link Receipt}. */
|
|
27
27
|
receipts?: Receipt[];
|
|
28
|
+
/** PRD (task/agent ownership coordination) — `agent_id` bound to its resolved agent-report name. Optional: absent on any track predating the PRD module (backward compat). */
|
|
29
|
+
prdOwners?: Record<string, string>;
|
|
30
|
+
/** PRD cross-check violations detected at PostToolUse (capped, append-only). */
|
|
31
|
+
prdViolations?: PrdViolationRecord[];
|
|
32
|
+
/** PRD SubagentStop/Stop one-shot block markers, keyed `"sessionId:event[:agent]"` -> the epoch-ms the block fired. */
|
|
33
|
+
prdStopBlocked?: Record<string, number>;
|
|
28
34
|
}
|
|
35
|
+
|
|
36
|
+
/** One PRD cross-check violation, timestamped for the journal/dedup. */
|
|
37
|
+
export interface PrdViolationRecord {
|
|
38
|
+
ts: number;
|
|
39
|
+
task: string;
|
|
40
|
+
agent: string;
|
|
41
|
+
sub: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Cap on {@link SessionTrack.prdViolations} (mirrors the `receipts` style bound — oldest evicted first). */
|
|
46
|
+
export const PRD_VIOLATIONS_CAP = 50;
|
|
29
47
|
/** A fresh, empty track. */
|
|
30
48
|
export function emptyTrack(): SessionTrack {
|
|
31
49
|
return { authorizations: {}, refsRead: [], agents: [], trivialEdits: [] };
|
|
@@ -92,3 +110,31 @@ export function trivialCount(track: SessionTrack, windowMs: number, now: number)
|
|
|
92
110
|
export function recordBrainstormRequired(track: SessionTrack, required: boolean): SessionTrack {
|
|
93
111
|
return { ...track, brainstormRequired: required };
|
|
94
112
|
}
|
|
113
|
+
|
|
114
|
+
/** Bind `agentId` to its resolved PRD agent-report `name` (merge; idempotent no-op if already bound to the same name). Immutable. */
|
|
115
|
+
export function recordPrdOwner(track: SessionTrack, agentId: string, name: string): SessionTrack {
|
|
116
|
+
if (track.prdOwners?.[agentId] === name) return track;
|
|
117
|
+
return { ...track, prdOwners: { ...track.prdOwners, [agentId]: name } };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Append one PRD cross-check violation. Immutable, uncapped in the mutator
|
|
122
|
+
* itself — same append-only shape as {@link Receipt} — so the journal diff's
|
|
123
|
+
* prefix comparison (`tail`, track-diff.ts) never sees an eviction it would
|
|
124
|
+
* mistake for a bulk rewrite. The {@link PRD_VIOLATIONS_CAP} bound is applied
|
|
125
|
+
* only when the journal is folded/read (track-journal.ts's `foldEvents`),
|
|
126
|
+
* mirroring how `trivialEdits`'s sliding window is re-derived at fold time.
|
|
127
|
+
*/
|
|
128
|
+
export function recordPrdViolation(track: SessionTrack, v: PrdViolationRecord): SessionTrack {
|
|
129
|
+
return { ...track, prdViolations: [...(track.prdViolations ?? []), v] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Record a PRD SubagentStop/Stop one-shot block marker (merge). Immutable. */
|
|
133
|
+
export function recordPrdStopBlocked(track: SessionTrack, key: string, ts: number): SessionTrack {
|
|
134
|
+
return { ...track, prdStopBlocked: { ...track.prdStopBlocked, [key]: ts } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True when the PRD one-shot block for `key` has already fired. */
|
|
138
|
+
export function prdAlreadyBlocked(track: SessionTrack, key: string): boolean {
|
|
139
|
+
return track.prdStopBlocked?.[key] !== undefined;
|
|
140
|
+
}
|
|
@@ -46,5 +46,8 @@ export function diffTrackEvents(prev: SessionTrack, next: SessionTrack, now: num
|
|
|
46
46
|
for (const ts of added(prev.trivialEdits ?? [], next.trivialEdits ?? [])) out.push({ field: "trivialEdits", op: "add", value: ts, ts });
|
|
47
47
|
if (next.target && JSON.stringify(next.target) !== JSON.stringify(prev.target)) out.push({ field: "target", op: "set", value: next.target, ts: Date.parse(next.target.set_at) || now });
|
|
48
48
|
if (next.brainstormRequired !== prev.brainstormRequired) out.push({ field: "brainstormRequired", op: "set", value: next.brainstormRequired, ts: now });
|
|
49
|
+
for (const [aid, name] of Object.entries(next.prdOwners ?? {})) if (prev.prdOwners?.[aid] !== name) out.push({ field: "prdOwners", op: "merge", value: [aid, name], ts: now });
|
|
50
|
+
for (const v of tail(prev.prdViolations ?? [], next.prdViolations ?? [])) out.push({ field: "prdViolations", op: "append", value: v, ts: v.ts ?? now });
|
|
51
|
+
for (const [key, ts] of Object.entries(next.prdStopBlocked ?? {})) if (prev.prdStopBlocked?.[key] !== ts) out.push({ field: "prdStopBlocked", op: "merge", value: [key, ts], ts });
|
|
49
52
|
return out;
|
|
50
53
|
}
|
|
@@ -14,7 +14,7 @@ import { dirname } from "node:path";
|
|
|
14
14
|
import { randomBytes } from "node:crypto";
|
|
15
15
|
import { computeMac, loadOrCreateKey } from "./integrity";
|
|
16
16
|
import { withTrackLockSyncBlocking } from "./track-lock-sync";
|
|
17
|
-
import { emptyTrack, type SessionTrack } from "./session-state";
|
|
17
|
+
import { emptyTrack, PRD_VIOLATIONS_CAP, type PrdViolationRecord, type SessionTrack } from "./session-state";
|
|
18
18
|
import type { AuthEntry } from "../freshness/doc-helpers";
|
|
19
19
|
import type { SessionTarget } from "../policy/apex-authorization";
|
|
20
20
|
import type { Receipt } from "./receipts";
|
|
@@ -78,6 +78,9 @@ export function foldEvents(events: TrackEvent[], base: SessionTrack = emptyTrack
|
|
|
78
78
|
const at: Record<string, number> = { ...base.refsReadAt };
|
|
79
79
|
const receipts: Receipt[] = [...(base.receipts ?? [])];
|
|
80
80
|
const trivial = new Set(base.trivialEdits ?? []);
|
|
81
|
+
const prdOwners: Record<string, string> = { ...base.prdOwners };
|
|
82
|
+
const prdViolations: PrdViolationRecord[] = [...(base.prdViolations ?? [])];
|
|
83
|
+
const prdStopBlocked: Record<string, number> = { ...base.prdStopBlocked };
|
|
81
84
|
let targetTs = base.target ? Date.parse(base.target.set_at) || 0 : 0, brainstormTs = 0; // a base flag is unstamped: any event overrides it
|
|
82
85
|
for (const ev of [...events].sort((a, b) => a.ts - b.ts)) { // stable: log order breaks ts ties
|
|
83
86
|
if (ev.field === "refsRead") { const p = String(ev.value); if (!t.refsRead.includes(p)) t.refsRead.push(p); }
|
|
@@ -88,9 +91,15 @@ export function foldEvents(events: TrackEvent[], base: SessionTrack = emptyTrack
|
|
|
88
91
|
else if (ev.field === "trivialEdits") trivial.add(Number(ev.value));
|
|
89
92
|
else if (ev.field === "target" && ev.ts >= targetTs) { t.target = ev.value as SessionTarget; targetTs = ev.ts; }
|
|
90
93
|
else if (ev.field === "brainstormRequired" && ev.ts >= brainstormTs) { t.brainstormRequired = Boolean(ev.value); brainstormTs = ev.ts; }
|
|
94
|
+
else if (ev.field === "prdOwners") { const [aid, name] = ev.value as [string, string]; prdOwners[aid] = name; }
|
|
95
|
+
else if (ev.field === "prdViolations") { prdViolations.push(ev.value as PrdViolationRecord); }
|
|
96
|
+
else if (ev.field === "prdStopBlocked") { const [key, ts] = ev.value as [string, number]; prdStopBlocked[key] = ts; }
|
|
91
97
|
}
|
|
92
98
|
if (Object.keys(at).length) t.refsReadAt = at;
|
|
93
99
|
if (receipts.length) t.receipts = receipts;
|
|
100
|
+
if (Object.keys(prdOwners).length) t.prdOwners = prdOwners;
|
|
101
|
+
if (prdViolations.length) t.prdViolations = prdViolations.slice(-PRD_VIOLATIONS_CAP);
|
|
102
|
+
if (Object.keys(prdStopBlocked).length) t.prdStopBlocked = prdStopBlocked;
|
|
94
103
|
const maxT = Math.max(0, ...trivial);
|
|
95
104
|
t.trivialEdits = [...trivial].filter((x) => x > maxT - TRIVIAL_WINDOW_MS).sort((a, b) => a - b);
|
|
96
105
|
return t;
|
package/dist/run-DkrzC0gb.mjs
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
2
|
-
import { t as evaluate } from "./evaluate-CMiIqHeH.mjs";
|
|
3
|
-
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
//#region src/cli/run.ts
|
|
6
|
-
/** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
|
|
7
|
-
function stagedFiles() {
|
|
8
|
-
return execFileSync("git", [
|
|
9
|
-
"diff",
|
|
10
|
-
"--cached",
|
|
11
|
-
"--name-only",
|
|
12
|
-
"--diff-filter=ACMR"
|
|
13
|
-
], { encoding: "utf8" }).trim().split("\n").filter(Boolean);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Read a file's staged (index) content — not the working-tree version.
|
|
17
|
-
* Uses `execFileSync` with an argv array (never a shell), so a staged
|
|
18
|
-
* filename containing shell metacharacters (backticks, `$(...)`, quotes)
|
|
19
|
-
* reaches `git show` as a single literal argument — see issue #87.
|
|
20
|
-
*/
|
|
21
|
-
function stagedContent(path) {
|
|
22
|
-
return execFileSync("git", ["show", `:${path}`], { encoding: "utf8" });
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Evaluate staged code files against the policy core; return violation blocks.
|
|
26
|
-
* `read` is injected (the real impl is {@link stagedContent}) so this is pure + testable.
|
|
27
|
-
*/
|
|
28
|
-
function checkStaged(files, read) {
|
|
29
|
-
const violations = [];
|
|
30
|
-
for (const file of files) {
|
|
31
|
-
if (!isCodeFile(file)) continue;
|
|
32
|
-
const r = evaluate({
|
|
33
|
-
tool: "Write",
|
|
34
|
-
filePath: file,
|
|
35
|
-
content: read(file)
|
|
36
|
-
});
|
|
37
|
-
if (r.decision === "deny" && r.prompt) violations.push(`${file}\n${formatPrompt(r.prompt)}`);
|
|
38
|
-
}
|
|
39
|
-
return violations;
|
|
40
|
-
}
|
|
41
|
-
//#endregion
|
|
42
|
-
export { stagedContent as n, stagedFiles as r, checkStaged as t };
|