@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.
Files changed (83) hide show
  1. package/README.md +17 -0
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/codex/index.mjs +1 -1
  4. package/dist/adapters/cursor/index.mjs +1 -1
  5. package/dist/adapters/hermes/index.mjs +1 -1
  6. package/dist/adapters/kimi/index.mjs +1 -1
  7. package/dist/apex-target-Xc2M32Pl.mjs +48 -0
  8. package/dist/apex-task-store-Cb65rE5i.mjs +467 -0
  9. package/dist/{claude-Ckv2_TgP.mjs → claude-D62hkUfS.mjs} +2 -84
  10. package/dist/cli/bin.mjs +7 -5
  11. package/dist/cli/index.d.mts +69 -1
  12. package/dist/cli/index.mjs +2 -2
  13. package/dist/config/index.mjs +2 -1
  14. package/dist/{dotenv-BLBkBTww.mjs → dotenv-C1LkcfW-.mjs} +1 -26
  15. package/dist/{handle-C43gA-Pr.mjs → handle-B-g9KL_9.mjs} +4837 -3466
  16. package/dist/{hermes-B9-p_3IF.mjs → hermes-ByopGx6C.mjs} +1 -1
  17. package/dist/index.d.mts +1 -1
  18. package/dist/index.mjs +3 -2
  19. package/dist/{kimi-G2wcSh5-.mjs → kimi-C-Oia9q-.mjs} +1 -1
  20. package/dist/{normalize-BjG6unTj.mjs → normalize-Dy8g9Ybl.mjs} +83 -3
  21. package/dist/policy/index.mjs +1 -1
  22. package/dist/prd-PvK7PKWS.mjs +494 -0
  23. package/dist/runtime/index.d.mts +13 -1
  24. package/dist/runtime/index.mjs +1 -1
  25. package/dist/runtime-io-DuumUeE6.mjs +84 -0
  26. package/dist/{session-state-D5gLr66m.d.mts → session-state-COg7Ej_2.d.mts} +32 -1
  27. package/dist/{skill-path-DVML3zfp.mjs → skill-path-Cz8WFaGu.mjs} +1 -1
  28. package/dist/{store-5-ZPKb0u.mjs → store-BVY6gIYM.mjs} +75 -3
  29. package/dist/tracking/index.d.mts +2 -2
  30. package/dist/tracking/index.mjs +2 -2
  31. package/dist/ttl-Dgwg_QAv.mjs +26 -0
  32. package/dist/{validate-KjZ1X9tH.mjs → validate-Dcjl0LUS.mjs} +4 -49
  33. package/package.json +1 -1
  34. package/src/adapters/cursor/context-budget.ts +144 -0
  35. package/src/adapters/cursor/context-limit.ts +115 -0
  36. package/src/adapters/cursor/context.ts +21 -2
  37. package/src/adapters/cursor/interfaces/context-budget.ts +25 -0
  38. package/src/adapters/cursor/native-response.ts +10 -129
  39. package/src/adapters/cursor/native-schemas.ts +161 -0
  40. package/src/adapters/cursor/normalize.ts +65 -0
  41. package/src/adapters/cursor/plugin-root.ts +103 -0
  42. package/src/adapters/cursor/respond.ts +94 -47
  43. package/src/cli/bin.ts +3 -0
  44. package/src/cli/index.ts +1 -0
  45. package/src/cli/prd/compact.ts +53 -0
  46. package/src/cli/prd/format.ts +23 -0
  47. package/src/cli/prd/index.ts +23 -0
  48. package/src/cli/prd/resolve.ts +88 -0
  49. package/src/cli/prd/shared.ts +80 -0
  50. package/src/cli/prd/status.ts +87 -0
  51. package/src/cli/prd/validate.ts +90 -0
  52. package/src/policy/prd/index.ts +36 -0
  53. package/src/policy/prd/interfaces/types.ts +103 -0
  54. package/src/policy/prd/prd-compact.ts +28 -0
  55. package/src/policy/prd/prd-context.ts +131 -0
  56. package/src/policy/prd/prd-crosscheck.ts +76 -0
  57. package/src/policy/prd/prd-enabled.ts +39 -0
  58. package/src/policy/prd/prd-io.ts +89 -0
  59. package/src/policy/prd/prd-ownership.ts +99 -0
  60. package/src/policy/prd/prd-paths.ts +88 -0
  61. package/src/policy/prd/prd-schema.ts +151 -0
  62. package/src/runtime/handle-post.ts +2 -0
  63. package/src/runtime/handle-pre.ts +11 -0
  64. package/src/runtime/handle.ts +87 -6
  65. package/src/runtime/lifecycle/aipilot/dispatch-aipilot.ts +7 -1
  66. package/src/runtime/lifecycle/dispatch.ts +18 -5
  67. package/src/runtime/lifecycle/failure-lesson.ts +6 -2
  68. package/src/runtime/lifecycle/rules-root.ts +18 -2
  69. package/src/runtime/normalize.ts +4 -0
  70. package/src/runtime/prd/index.ts +8 -0
  71. package/src/runtime/prd/prd-bash-targets.ts +199 -0
  72. package/src/runtime/prd/prd-candidate-files.ts +23 -0
  73. package/src/runtime/prd/prd-canon.ts +50 -0
  74. package/src/runtime/prd/prd-identity.ts +32 -0
  75. package/src/runtime/prd/prd-post-check.ts +73 -0
  76. package/src/runtime/prd/prd-pre-gate.ts +172 -0
  77. package/src/runtime/prd/prd-stop-gate.ts +101 -0
  78. package/src/runtime/prd/prd-subagent-context.ts +69 -0
  79. package/src/runtime/prd/prd-subagent-stop.ts +152 -0
  80. package/src/tracking/session-state.ts +46 -0
  81. package/src/tracking/track-diff.ts +3 -0
  82. package/src/tracking/track-journal.ts +10 -1
  83. 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;
@@ -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 };