SLOPSHOPPER

frontend-skills-mods

Opt-in Claude Code harness desk: verification receipts, Skill flight recorder, bounded edit replay, context readout and user-controlled prompt briefing…

newpanebandguardcommandprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · frontend-skills-mods
│ ┃ Harness ✕ › fix the failing auth test and add an audit log call │ ┃ [ Proof ][ Skills ][ Replay ][ Brief ] │ ┃ Proof Desk Observed commands, not a release ⏺ Read(src/auth.ts) │ ┃ certification. Types: missing Lint: ⎿ Read 6 lines │ ┃ missing Tests: missing Mod tests: ⏺ Update(src/auth.ts) │ ┃ missing Dogfood: missing Visual: missing Repo ⎿ Added 2 lines, removed 1 line │ ┃ comparison: last refresh only; refresh after ⏺ Bash(bun test) │ ┃ external edits. Scope: tracked/unignored repo ⎿ 3 pass, 1 fail │ ┃ content; no environment or dependency proof. │ ┃ [ Refresh repo state ] ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ [ Close ] │ ✻ Worked for 42s · done 4:20 PM │ │ › /harness │ ⎿ frontend-skills-mods: Proof Desk │ ⎿ frontend-skills-mods: Observed commands, not a release certifica │ ⎿ frontend-skills-mods: Types: missing │ ⎿ frontend-skills-mods: Lint: missing │ ⎿ frontend-skills-mods: Tests: missing │ ⎿ frontend-skills-mods: Mod tests: missing │ │ ⟨Claude Code's own drawing⟩ Context 49% ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ Context 49%
Pane · Harness
[ Proof ][ Skills ][ Replay ][ Brief ] Proof Desk Observed commands, not a release certification. Types: missing Lint: missing Tests: missing Mod tests: missing Dogfood: missing Visual: missing Repo comparison: last refresh only; refresh after external edits. Scope: tracked/unignored repo content; no environment or dependency proof. [ Refresh repo state ] [ Close ]
README

Harness desk

Opt-in Claude Code mods, tested with 2.1.287. Inspect check receipts, Skill calls, successful edit patches and a user-controlled workflow brief in one native pane. Harness lens keeps its context/last-skill readout above the prompt, with a Brief on indicator while prompt enrichment is enabled. Existing shell enforcement and Codex behavior stay unchanged.

Review before installing: mods have Claude Code's machine access. This plugin makes bounded read-only Git/process and file-metadata calls for comparisons; no file writes, model/network calls, permission changes or automatic undo. Live terminal/Desktop painting and before/after captures remain unverified.

Install and rollback

After adding malinskibeniamin/skills as a marketplace:

/plugin marketplace update skills
/plugin install frontend-skills-mods@skills
/reload-plugins

Disable with claude plugin disable frontend-skills-mods@skills, or remove with claude plugin uninstall frontend-skills-mods@skills, then reload plugins.

One pane, four views

/harness proof    Check receipts and repo comparison
/harness skills   Skill Flight Recorder
/harness replay   Step through returned edit patches
/harness brief    Preview or configure prompt enrichment

Commands run without a model turn. Tabs, refresh, previous/next, clear and close use native buttons. If a pane cannot be placed, the command still returns text.

Workflow brief

Default off. Configure every field, inspect the exact injected block in the Brief tab, then enable it with the command or native button:

/harness brief objective Add pagination
/harness brief guardrails Keep the public API
/harness brief verification Run the pagination tests
/harness brief stop Commit only
/harness brief on

The mod adds this user-configured guidance to prompt.submit's model-only context, without changing user text, attachments, queue flags or origin. Existing context from other mods is preserved; an identical block is not added twice. Only composer, Remote Control bridge and SDK submissions qualify, not slash commands, plugin messages (even asUser), peers, schedules or notifications. These origins may also configure the brief; other origins can only view it.

Editing a field pauses enrichment until explicitly re-enabled. Pause brief or /harness brief off stops future injection. Clear brief or /harness brief clear also erases the mod's copy. Session end, including clear, resume or switching conversations, resets it; hot reload retains session state. Overlapping field commands preserve both updates. A state-read failure passes the prompt unchanged and logs an unavailable notice. Downstream prompt blocks stay blocked.

Each field is 1 nonempty line, at most 1024 UTF-8 bytes, without control or hidden format characters. Invalid inputs leave the brief unchanged. Nothing is inferred from repository files or complete prompts. The brief is guidance, not skill selection, adherence proof, new permission or a tool rewrite; current prompts and higher-priority instructions take precedence.

Privacy: these fields are retained in session memory and, when enabled, sent to the model with each eligible prompt. Command output and injected context may remain in Claude's conversation/logs. Clearing cannot retract prior context; do not put secrets in the brief. Extra context consumes tokens. The mod itself makes no model/network calls; normal prompt submission still does.

Proof Desk

Only exact foreground main-loop Bash commands are recognized: bun run type:check, bun run lint, bun run lint:fix, bun run test, bun run test:mods. Compound commands, aliases, subagents and background checks are not evidence. A host error is failed; denial is denied; interrupted, backgrounded or special-return-code outcomes are incomplete. Concurrent checks keep the latest started receipt, not whichever finishes last.

A passed result is compared with bounded Git fingerprints before/after the check and on /harness proof or Refresh repo state. Changes produce stale; unavailable comparisons produce unverified. Every tool call invalidates the last comparison. External edits require refresh; fingerprints are non-atomic, not continuous monitoring or release certification. Dogfood and visual evidence remain missing: the mod cannot infer them from a successful command.

Comparison scope: session cwd must equal the Git root; HEAD and tracked/unignored working content only. No ignored dependencies, environment or full staging-state proof. Limit: 200 surviving changed/untracked files, 4 MiB each, 16 MiB combined, 1.5 seconds per Git query. Symlinks, submodules, failed/truncated output and unsupported hosts become unverified. Git runs with optional locks, fsmonitor, external diffs, textconv and hash-object filters disabled. The tested SDK supports process calls only on CLI; Desktop comparisons are unavailable, receipts still display.

Skill Flight Recorder

Latest 24 completed Skill calls: resolved name when available, outcome and main/subagent provenance. Read-only loading differs from execution; a background fork is a launch with unknown outcome, not completed work. No arguments, prompts, error text or fork result bodies retained. Clear discards history, including already-in-flight calls; it does not erase the separate Last skill readout.

Not an instruction-adherence audit. Slash expansion, installed versions and which references Claude actually read are not attributed by this recorder.

Edit replay

Independent implementation inspired by Replay Theater, not copied upstream code. Retains only successful, non-staged Edit/Write returned patches, including owner-modified output. Not attempted inputs or full file contents. Other tools and external edits are not recorded.

Completion order; tool id, agent and observed main-turn id attached at call start. A late subagent completion is not attributed to a guessed agent turn. Empty patches say diff unavailable. Session memory only: latest 20 entries, 8 KiB per serialized entry, 64 KiB combined. Old entries evict; clipped patches are labeled. Clear cancels in-flight retention; no snapshots, persistence or undo.

Sensitive paths and recognizable secret markers are excluded without retaining their names or contents; terminal controls are neutralized. Filters are best effort, not guaranteed redaction. Unrecognized secrets may appear in ordinary-file patches. Disable the plugin when that risk is unacceptable.

Harness lens

  • Context refreshes at session start and after main-loop turns, not continuously. Missing/failed usage shows unavailable rather than stale figures.
  • Last skill means the latest successful main-loop Skill result, not an active skill, completed background work or proven adherence.
  • Host-owned state survives module reloads within the session. Independent keys protect skill observations from pending context updates.
  • CLI/Desktop AbovePrompt composes downstream output; surveys, fewer than 32 columns and subagent views pass through. Other surfaces lack this band.

Verify or extend

CLAUDE_BIN=/absolute/path/to/claude bun run test:mods

From repo root with dependencies installed: isolated HOME/config/cache install, strict validation, real host-kit tests, generated SDK compilation and rollback. No model calls or changes to your Claude config. Generated .claude-plugin/types/ is untracked; the dedicated gate owns host-specific types, not root typecheck.

Tests validate native element trees and interactions, not terminal/Desktop paint. Separate live visual checks and before/after captures remain a draft gap. Read extend-harness/MODS.md in the source repository (outside the standalone cache). Official authoring and the target build's generated declarations own this early-access contract.

Source 4 files
hooks/harness-lens.ts 675 lines
1import {
2  type EngineInterface,
3  type On,
4  type Register,
5  type ToolCallResult,
6  update,
7} from "claude-code";
8import type {
9  CheckKind,
10  ContextReading,
11  DeskView,
12  ProofReceipt,
13  ReplayEntry,
14  ReplayState,
15  SkillsState,
16  WorkflowBrief,
17} from "../types";
18
19import {
20  boundedText,
21  byteLength,
22  displayLabel,
23  emptyReplay,
24  returnedPatch,
25  skillObservation,
26} from "./observations";
27import {
28  briefContext,
29  canEnableBrief,
30  changeBrief,
31  emptyBrief,
32  isBriefOrigin,
33} from "./workflow-brief";
34
35const contextState = {
36  plugin: "frontend-skills-mods",
37  key: "context",
38} as const;
39const skillState = {
40  plugin: "frontend-skills-mods",
41  key: "lastSkill",
42} as const;
43
44export const register: Register = (on) => {
45  registerDesk(on);
46  on("session.start", async ($, e, next) => {
47    const result = await next(e);
48    await readContext($);
49    await startDesk($);
50    return result;
51  });
52
53  on("turn.complete", async ($, e, next) => {
54    const result = await next(e);
55    if (!e.agentId) await readContext($);
56    return result;
57  });
58
59  on("tool.call", { tool: "Skill" }, async ($, e, next) => {
60    const { value: initial } = await $.state.get(skillsState);
61    const generation = initial?.generation ?? 0;
62    const result = await next(e);
63    const observation = skillObservation(e.skill, e.agentId, result);
64    await update($, skillsState, (current): SkillsState => {
65      const state = current ?? { entries: [], generation: 0 };
66      return state.generation !== generation
67        ? state
68        : { ...state, entries: [...state.entries, observation].slice(-24) };
69    });
70    if (
71      !e.agentId &&
72      result.deny === undefined &&
73      !result.isError &&
74      result.result?.success
75    ) {
76      await $.state.set(skillState, e.skill);
77    }
78    return result;
79  });
80
81  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
82    if (e.props.hasSurvey || e.props.bodyColumns < 32 || e.props.view.agentId) {
83      return next(e);
84    }
85    const { value: usage } = await $.state.get(contextState);
86    const { value: lastSkill } = await $.state.get(skillState);
87    const { value: brief } = await $.state.get(briefState);
88    const context = usage ? `Context ${usage.percent}%` : "Context unavailable";
89    const briefing = brief?.enabled ? " · Brief on" : "";
90    const skill = lastSkill ? ` · Last skill ${displayLabel(lastSkill)}` : "";
91    const { Box, Text } = $.ui.resolve(e);
92    return Box({
93      flexDirection: "column",
94      children: [
95        await next(e),
96        Text({
97          dimColor: true,
98          children: `${context}${briefing}${skill}`.slice(
99            0,
100            e.props.bodyColumns,
101          ),
102        }),
103      ],
104    });
105  });
106};
107
108async function readContext($: EngineInterface): Promise<void> {
109  let context: ContextReading = null;
110  try {
111    const { context: usage } = await $.session.usage();
112    if (usage.window > 0 && usage.tokens !== undefined) {
113      const percent = usage.percent ?? (usage.tokens / usage.window) * 100;
114      if (Number.isFinite(percent)) {
115        context = {
116          tokens: usage.tokens,
117          window: usage.window,
118          percent: Math.round(percent),
119        };
120      }
121    }
122  } catch {
123    // Visible unavailable state replaces stale numbers; no tool/turn is denied.
124    context = null;
125  }
126  await $.state.set(contextState, context);
127}
128
129const viewState = { plugin: "frontend-skills-mods", key: "deskView" } as const;
130
131function registerDesk(on: On): void {
132  registerReplay(on);
133  registerBrief(on);
134  on("tool.call", async ($, e, next) => {
135    // Any tool may mutate repo state, including MCP tools. Require a fresh comparison.
136    await $.state.set(repositoryState, null);
137    return next(e);
138  });
139  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
140    const checks = new Map<string, CheckKind>([
141      ["bun run type:check", "types"],
142      ["bun run lint", "lint"],
143      ["bun run lint:fix", "lint"],
144      ["bun run test", "tests"],
145      ["bun run test:mods", "mods"],
146    ]);
147    const kind = checks.get(e.command.trim());
148    if (!kind || e.agentId || e.run_in_background) return next(e);
149    const ref = {
150      plugin: "frontend-skills-mods",
151      key: "proof",
152      id: kind,
153    } as const;
154    await $.state.set(ref, {
155      toolId: e.tool_use_id,
156      outcome: "pending",
157      fingerprint: null,
158      changedDuringCheck: false,
159    });
160    const before = await repositoryFingerprint($);
161    let result: ToolCallResult<"Bash">;
162    try {
163      result = await next(e);
164    } catch (error) {
165      await update(
166        $,
167        ref,
168        (current): ProofReceipt =>
169          current?.toolId !== e.tool_use_id
170            ? (current ?? {
171                toolId: e.tool_use_id,
172                outcome: "incomplete",
173                fingerprint: null,
174                changedDuringCheck: false,
175              })
176            : { ...current, outcome: "incomplete" },
177      );
178      throw error;
179    }
180    const after = await repositoryFingerprint($);
181    const outcome = proofOutcome(result);
182    await update(
183      $,
184      ref,
185      (current): ProofReceipt =>
186        current?.toolId !== e.tool_use_id
187          ? (current ?? {
188              toolId: e.tool_use_id,
189              outcome: "incomplete",
190              fingerprint: null,
191              changedDuringCheck: false,
192            })
193          : {
194              toolId: e.tool_use_id,
195              outcome,
196              fingerprint: before === after ? after : null,
197              changedDuringCheck:
198                before !== null && after !== null && before !== after,
199            },
200    );
201    return result;
202  });
203  on("command.run", { command: "harness" }, async ($, e) => {
204    const args = e.args.trim();
205    const briefCommand = /^brief(?:\s+([\s\S]*))?$/.exec(args);
206    const view = briefCommand ? "brief" : args || "proof";
207    if (
208      view !== "proof" &&
209      view !== "skills" &&
210      view !== "replay" &&
211      view !== "brief"
212    ) {
213      return { text: "Usage: /harness [proof|skills|replay|brief]" };
214    }
215    if (briefCommand?.[1]) {
216      if (!isBriefOrigin(e.origin)) {
217        return {
218          text: "Workflow brief not changed: use a composer, bridge or SDK command.",
219        };
220      }
221      const briefArgs = briefCommand[1];
222      let error: string | undefined;
223      await update($, briefState, (current): WorkflowBrief => {
224        const state = current ?? emptyBrief();
225        const changed = changeBrief(state, briefArgs);
226        error = "error" in changed ? changed.error : undefined;
227        return "state" in changed ? changed.state : state;
228      });
229      if (error) return { text: `Workflow brief not changed: ${error}` };
230    }
231    if (view === "proof") await refreshRepository($);
232    await $.state.set(viewState, view);
233    const { isPlaced } = await $.ui.open({
234      id: "harness",
235      title: "Harness",
236      focus: true,
237      closeOnEscape: true,
238      rows: 16,
239    });
240    const text = await deskText($, view);
241    return {
242      text: isPlaced ? text : `${text}\nPane unavailable on this surface.`,
243    };
244  });
245  on("ui.render", { component: "Pane" }, async ($, e, next) => {
246    if (e.requestId !== "harness") return next(e);
247    const { value: view = "proof" } = await $.state.get(viewState);
248    const { value: brief = emptyBrief() } = await $.state.get(briefState);
249    const { Box, Text, Button } = $.ui.resolve(e);
250    return Box({
251      flexDirection: "column",
252      children: [
253        Box({
254          children: (["proof", "skills", "replay", "brief"] as const).map(
255            (tab) =>
256              Button({
257                key: tab,
258                label: tab.charAt(0).toUpperCase() + tab.slice(1),
259                onPress: () => $.state.set(viewState, tab),
260              }),
261          ),
262        }),
263        Text({ children: await deskText($, view) }),
264        ...(view === "brief"
265          ? [
266              ...(brief.enabled || canEnableBrief(brief)
267                ? [
268                    Button({
269                      key: "toggle-brief",
270                      label: brief.enabled ? "Pause brief" : "Enable brief",
271                      onPress: () =>
272                        update($, briefState, (current): WorkflowBrief => {
273                          const state = current ?? emptyBrief();
274                          const changed = changeBrief(
275                            state,
276                            state.enabled ? "off" : "on",
277                          );
278                          return "state" in changed ? changed.state : state;
279                        }),
280                    }),
281                  ]
282                : []),
283              Button({
284                key: "clear-brief",
285                label: "Clear brief",
286                onPress: () => $.state.set(briefState, emptyBrief()),
287              }),
288            ]
289          : []),
290        ...(view === "replay"
291          ? [
292              Button({
293                key: "previous",
294                label: "Previous edit",
295                onPress: () =>
296                  update(
297                    $,
298                    replayState,
299                    (state): ReplayState => ({
300                      ...(state ?? emptyReplay()),
301                      index: Math.max(0, (state?.index ?? 0) - 1),
302                    }),
303                  ),
304              }),
305              Button({
306                key: "next",
307                label: "Next edit",
308                onPress: () =>
309                  update(
310                    $,
311                    replayState,
312                    (state): ReplayState => ({
313                      ...(state ?? emptyReplay()),
314                      index: Math.min(
315                        Math.max(0, (state?.entries.length ?? 0) - 1),
316                        (state?.index ?? 0) + 1,
317                      ),
318                    }),
319                  ),
320              }),
321              Button({
322                key: "clear-replay",
323                label: "Clear replay",
324                onPress: () =>
325                  update(
326                    $,
327                    replayState,
328                    (state): ReplayState => ({
329                      ...emptyReplay(),
330                      generation: (state?.generation ?? 0) + 1,
331                    }),
332                  ),
333              }),
334            ]
335          : []),
336        ...(view === "skills"
337          ? [
338              Button({
339                key: "clear-skills",
340                label: "Clear skill history",
341                onPress: () =>
342                  update(
343                    $,
344                    skillsState,
345                    (state): SkillsState => ({
346                      entries: [],
347                      generation: (state?.generation ?? 0) + 1,
348                    }),
349                  ),
350              }),
351            ]
352          : []),
353        ...(view === "proof"
354          ? [
355              Button({
356                key: "refresh",
357                label: "Refresh repo state",
358                onPress: () => refreshRepository($),
359              }),
360            ]
361          : []),
362        Button({
363          key: "close",
364          label: "Close",
365          role: "dismiss",
366          onPress: () => $.ui.close({ id: "harness" }),
367        }),
368      ],
369    });
370  });
371}
372
373function proofOutcome(result: ToolCallResult<"Bash">): ProofReceipt["outcome"] {
374  if (result.deny !== undefined) return "denied";
375  if (result.isError) return "failed";
376  const output = result.result;
377  return output.interrupted ||
378    output.backgroundTaskId !== undefined ||
379    output.backgroundedByUser ||
380    output.backgroundedByTurnAbort ||
381    output.backgroundedToDeliverMessage ||
382    output.timedOutAfterMs !== undefined ||
383    output.returnCodeInterpretation !== undefined
384    ? "incomplete"
385    : "passed";
386}
387
388async function deskText($: EngineInterface, view: DeskView): Promise<string> {
389  if (view === "proof") return proofText($);
390  if (view === "brief") {
391    const { value: brief = emptyBrief() } = await $.state.get(briefState);
392    return [
393      `Workflow brief: ${brief.enabled ? "enabled" : "disabled"}`,
394      "Future composer/bridge/SDK non-command prompts only. All 4 fields required.",
395      "Edit with /harness brief <field> <text>; enable with /harness brief on.",
396      "Disable with off; clear erases this mod's copy, not prior conversation context.",
397      "Injected block preview:",
398      briefContext(brief),
399    ].join("\n");
400  }
401  if (view === "skills") {
402    const { value: history } = await $.state.get(skillsState);
403    return [
404      "Skill Flight Recorder",
405      "Latest 24 completions; tool observations, not adherence.",
406      ...(history?.entries.length
407        ? history.entries.map(
408            (entry) => `${entry.name} · ${entry.outcome} · ${entry.provenance}`,
409          )
410        : ["No Skill calls observed."]),
411    ].join("\n");
412  }
413  const { value: replay } = await $.state.get(replayState);
414  const entry = replay?.entries[replay.index];
415  const body = entry
416    ? `Edit ${replay.index + 1}/${replay.entries.length} · ${entry.path}\n${entry.provenance} · observed main turn ${entry.turn} · tool ${entry.toolId}\n${entry.diff}${entry.truncated ? "\n[Patch truncated at retention limit.]" : ""}`
417    : "Edit replay\nNo successful edits observed (after exclusions or clearing).";
418  return `${body}\nExcluded sensitive edits: ${replay?.excluded ?? 0}\nSession memory only; latest 20 entries / 64 KiB. Secret filtering is best effort.`;
419}
420
421async function startDesk($: EngineInterface): Promise<void> {
422  await $.command.register({
423    name: "harness",
424    description: "Inspect evidence or configure an opt-in workflow brief.",
425    argumentHint: "[proof|skills|replay|brief]",
426  });
427}
428
429const briefState = { plugin: "frontend-skills-mods", key: "brief" } as const;
430
431function registerBrief(on: On): void {
432  on("prompt.submit", async ($, e, next) => {
433    if (!isBriefOrigin(e.origin) || e.text.trimStart().startsWith("/"))
434      return next(e);
435    let brief: WorkflowBrief | undefined;
436    try {
437      brief = (await $.state.get(briefState)).value;
438    } catch {
439      $.ui.log("Workflow brief unavailable; prompt passed unchanged.");
440      return next(e);
441    }
442    if (!brief?.enabled) return next(e);
443    const context = briefContext(brief);
444    if (e.context?.includes(context)) return next(e);
445    return next({ ...e, context: [...(e.context ?? []), context] });
446  });
447  on("session.end", async ($, e, next) => {
448    await $.state.set(briefState, emptyBrief());
449    return next(e);
450  });
451}
452
453const repositoryState = {
454  plugin: "frontend-skills-mods",
455  key: "repository",
456} as const;
457
458async function refreshRepository($: EngineInterface): Promise<void> {
459  await $.state.set(repositoryState, await repositoryFingerprint($));
460}
461
462async function proofText($: EngineInterface): Promise<string> {
463  const { value: repository } = await $.state.get(repositoryState);
464  const lines = [
465    "Proof Desk",
466    "Observed commands, not a release certification.",
467  ];
468  for (const [id, label] of [
469    ["types", "Types"],
470    ["lint", "Lint"],
471    ["tests", "Tests"],
472    ["mods", "Mod tests"],
473  ] as const) {
474    const { value: receipt } = await $.state.get({
475      plugin: "frontend-skills-mods",
476      key: "proof",
477      id,
478    });
479    let status: string = receipt?.outcome ?? "missing";
480    if (receipt?.outcome === "passed") {
481      status = receipt.changedDuringCheck
482        ? "stale (passed; files changed during check)"
483        : !receipt.fingerprint || !repository
484          ? "unverified (passed)"
485          : receipt.fingerprint !== repository
486            ? "stale (passed)"
487            : "passed";
488    }
489    lines.push(`${label}: ${status}`);
490  }
491  lines.push(
492    "Dogfood: missing",
493    "Visual: missing",
494    "Repo comparison: last refresh only; refresh after external edits.",
495    "Scope: tracked/unignored repo content; no environment or dependency proof.",
496  );
497  return lines.join("\n");
498}
499
500async function gitOutput(
501  $: EngineInterface,
502  cwd: string,
503  args: string[],
504): Promise<string> {
505  const result = await $.process.run(
506    ["git", "-c", "core.fsmonitor=false", ...args],
507    {
508      cwd,
509      timeoutMs: 1500,
510      env: { GIT_OPTIONAL_LOCKS: "0" },
511    },
512  );
513  if (
514    result.exitCode !== 0 ||
515    result.isStdoutTruncated ||
516    result.isStderrTruncated
517  ) {
518    throw new Error("Repository comparison unavailable");
519  }
520  return result.stdout;
521}
522
523async function repositoryFingerprint(
524  $: EngineInterface,
525): Promise<string | null> {
526  try {
527    const cwd = await $.session.cwd();
528    const root = (
529      await gitOutput($, cwd, ["rev-parse", "--show-toplevel"])
530    ).trim();
531    if (cwd !== root) return null;
532    const head = await gitOutput($, cwd, ["rev-parse", "--verify", "HEAD"]);
533    const raw = await gitOutput($, cwd, [
534      "diff",
535      "--no-ext-diff",
536      "--no-textconv",
537      "--raw",
538      "HEAD",
539      "--",
540    ]);
541    const changed = await gitOutput($, cwd, [
542      "diff",
543      "--no-ext-diff",
544      "--no-textconv",
545      "--name-only",
546      "-z",
547      "HEAD",
548      "--",
549    ]);
550    const deleted = await gitOutput($, cwd, [
551      "diff",
552      "--diff-filter=D",
553      "--name-only",
554      "-z",
555      "HEAD",
556      "--",
557    ]);
558    const untracked = await gitOutput($, cwd, [
559      "ls-files",
560      "--others",
561      "--exclude-standard",
562      "-z",
563    ]);
564    const removed = new Set(deleted.split("\0"));
565    const files = [...new Set(`${changed}${untracked}`.split("\0"))]
566      .filter((path) => path && !removed.has(path))
567      .sort();
568    if (
569      files.length > 200 ||
570      files.some(
571        (path) => path.startsWith("/") || path.split("/").includes(".."),
572      )
573    )
574      return null;
575    const stats = await Promise.all(
576      files.map((path) => $.fs.stat(`${cwd}/${path}`)),
577    );
578    if (
579      stats.some(
580        (stat) =>
581          stat.isLink || stat.kind !== "file" || stat.size > 4 * 1024 * 1024,
582      ) ||
583      stats.reduce((size, stat) => size + stat.size, 0) > 16 * 1024 * 1024
584    )
585      return null;
586    const hashes = files.length
587      ? await gitOutput($, cwd, ["hash-object", "--no-filters", "--", ...files])
588      : "";
589    if (files.length && hashes.trim().split("\n").length !== files.length)
590      return null;
591    const digest = await crypto.subtle.digest(
592      "SHA-256",
593      new TextEncoder().encode(
594        JSON.stringify([
595          cwd,
596          head,
597          raw,
598          changed,
599          deleted,
600          untracked,
601          files,
602          hashes,
603        ]),
604      ),
605    );
606    return [...new Uint8Array(digest)]
607      .map((byte) => byte.toString(16).padStart(2, "0"))
608      .join("");
609  } catch {
610    // Unsupported host/process/repo state stays visibly unverified, never green.
611    return null;
612  }
613}
614
615const skillsState = { plugin: "frontend-skills-mods", key: "skills" } as const;
616
617const turnState = { plugin: "frontend-skills-mods", key: "turn" } as const;
618const replayState = { plugin: "frontend-skills-mods", key: "replay" } as const;
619
620function registerReplay(on: On): void {
621  on("turn.start", async ($, e, next) => {
622    await $.state.set(turnState, displayLabel(e.turnId));
623    return next(e);
624  });
625  on("tool.call", { tool: ["Edit", "Write"] }, async ($, e, next) => {
626    const { value: turn } = await $.state.get(turnState);
627    const { value: initial } = await $.state.get(replayState);
628    const generation = initial?.generation ?? 0;
629    const result = await next(e);
630    if (result.deny !== undefined || result.isError || result.result.staged)
631      return result;
632    const output = result.result;
633    const sensitivePath =
634      /(?:^|[\\/])(?:\.env(?:[.\\/]|$)|\.(?:ssh|aws|kube|git)(?:[\\/]|$)|\.(?:npmrc|netrc|pypirc)$|(?:credentials?|secrets?|passwords?)(?:[.\\/]|$)|id_(?:rsa|ed25519|dsa|ecdsa)(?:[.]|$))|\.(?:pem|key|p12|pfx|keystore)$/i;
635    const patch = sensitivePath.test(output.filePath)
636      ? null
637      : returnedPatch(output.structuredPatch);
638    const excluded =
639      !patch ||
640      /-----BEGIN [^\n]*PRIVATE KEY-----|\b(?:sk-(?:ant|proj)-|gh[pousr]_|github_pat_|(?:AKIA|ASIA)[A-Z0-9]{16})|(?:api[_-]?key|access[_-]?token|password|secret)\s*["']?\s*[:=]/i.test(
641        patch.diff,
642      );
643    let entry: ReplayEntry | null = null;
644    if (patch && !excluded) {
645      entry = {
646        path: displayLabel(output.filePath),
647        toolId: displayLabel(e.tool_use_id),
648        provenance: e.agentId ? `agent ${displayLabel(e.agentId)}` : "main",
649        turn: turn ?? "unknown",
650        ...patch,
651      };
652      while (byteLength(JSON.stringify(entry)) > 8192) {
653        entry.diff = boundedText(
654          entry.diff,
655          Math.max(
656            0,
657            byteLength(entry.diff) - (byteLength(JSON.stringify(entry)) - 8192),
658          ),
659        );
660        entry.truncated = true;
661      }
662    }
663    const retained = entry;
664    await update($, replayState, (current): ReplayState => {
665      const state = current ?? emptyReplay();
666      if (state.generation !== generation) return state;
667      if (!retained) return { ...state, excluded: state.excluded + 1 };
668      const entries = [...state.entries, retained].slice(-20);
669      while (byteLength(JSON.stringify(entries)) > 64 * 1024) entries.shift();
670      return { ...state, entries, index: entries.length - 1 };
671    });
672    return result;
673  });
674}
675
hooks/observations.ts 85 lines
1import type { ToolCallResult, ToolResultOf } from "claude-code";
2import type { ReplayState, SkillObservation } from "../types";
3
4export function displayLabel(value: string): string {
5  return value.slice(0, 120).replace(/[\p{Cc}\p{Cf}]/gu, "?");
6}
7
8export function skillObservation(
9  requested: string,
10  agentId: string | undefined,
11  result: ToolCallResult<"Skill">,
12): SkillObservation {
13  const provenance = agentId ? `agent ${displayLabel(agentId)}` : "main";
14  if (result.deny !== undefined)
15    return { name: displayLabel(requested), provenance, outcome: "denied" };
16  if (result.isError)
17    return { name: displayLabel(requested), provenance, outcome: "failed" };
18  const output = result.result;
19  const name = displayLabel(output.commandName);
20  const outcome: SkillObservation["outcome"] = !output.success
21    ? "failed"
22    : output.status === "forked"
23      ? output.background
24        ? "forked (launched; outcome unknown)"
25        : "forked (completed)"
26      : output.readOnly
27        ? "loaded read-only"
28        : "succeeded";
29  return { name, provenance, outcome };
30}
31
32export function emptyReplay(): ReplayState {
33  return { entries: [], index: 0, generation: 0, excluded: 0 };
34}
35
36export function byteLength(value: string): number {
37  return new TextEncoder().encode(value).length;
38}
39
40export function boundedText(value: string, max: number): string {
41  let result = "";
42  let bytes = 0;
43  for (const character of value.slice(0, max)) {
44    bytes += byteLength(character);
45    if (bytes > max) break;
46    result += character;
47  }
48  return result;
49}
50
51export function returnedPatch(patch: ToolResultOf<"Edit">["structuredPatch"]): {
52  diff: string;
53  truncated: boolean;
54} {
55  if (!patch.length)
56    return {
57      diff: "Diff unavailable (host returned no patch).",
58      truncated: false,
59    };
60  let diff = "";
61  let truncated = false;
62  const append = (text: string) => {
63    const remaining = 8192 - byteLength(diff);
64    const part = boundedText(text, remaining);
65    diff += part;
66    truncated = part.length !== text.length;
67  };
68  for (const hunk of patch) {
69    append(
70      `${diff ? "\n" : ""}@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`,
71    );
72    if (truncated) break;
73    for (const line of hunk.lines) {
74      append(`\n${line}`);
75      if (truncated) break;
76    }
77    if (truncated) break;
78  }
79  // Keep newlines/tabs, neutralize terminal control sequences before retaining them.
80  diff = diff.replace(/[\p{Cc}\p{Cf}]/gu, (character) =>
81    character === "\n" || character === "\t" ? character : "?",
82  );
83  return { diff, truncated };
84}
85
hooks/workflow-brief.ts 70 lines
1import type { PromptOrigin } from "claude-code";
2import type { WorkflowBrief } from "../types";
3import { byteLength } from "./observations";
4
5const fields = ["objective", "guardrails", "verification", "stop"] as const;
6
7export function emptyBrief(): WorkflowBrief {
8  return {
9    objective: "",
10    guardrails: "",
11    verification: "",
12    stop: "",
13    enabled: false,
14  };
15}
16
17export function isBriefOrigin(origin: PromptOrigin): boolean {
18  return (
19    origin.kind === "composer" ||
20    origin.kind === "bridge" ||
21    origin.kind === "sdk"
22  );
23}
24
25export function canEnableBrief(brief: WorkflowBrief): boolean {
26  return fields.every((field) => Boolean(brief[field]));
27}
28
29export function briefContext(brief: WorkflowBrief): string {
30  return [
31    "Harness workflow brief (user-configured guidance, not enforcement).",
32    "Current prompt and higher-priority instructions take precedence; this grants no tool permissions.",
33    `Objective: ${brief.objective}`,
34    `Guardrails: ${brief.guardrails}`,
35    `Verification: ${brief.verification}`,
36    `Stop: ${brief.stop}`,
37  ].join("\n");
38}
39
40export function changeBrief(
41  brief: WorkflowBrief,
42  args: string,
43): { state: WorkflowBrief } | { error: string } {
44  if (args === "clear") return { state: emptyBrief() };
45  if (args === "off") return { state: { ...brief, enabled: false } };
46  if (args === "on") {
47    const missing = fields.filter((field) => !brief[field]);
48    return missing.length
49      ? { error: `Set every field before enabling: ${missing.join(", ")}.` }
50      : { state: { ...brief, enabled: true } };
51  }
52  const match = /^(\S+)\s+([\s\S]+)$/.exec(args);
53  const field = fields.find((field) => field === match?.[1]);
54  const value = match?.[2]?.trim();
55  if (!field || !value) {
56    return {
57      error:
58        "Usage: /harness brief [objective|guardrails|verification|stop <text>|on|off|clear].",
59    };
60  }
61  if (/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value) || byteLength(value) > 1024) {
62    return {
63      error:
64        "Use 1 line per field, at most 1024 UTF-8 bytes, without control characters.",
65    };
66  }
67  // Changing the brief pauses injection until the person enables the new preview.
68  return { state: { ...brief, [field]: value, enabled: false } };
69}
70
types/index.d.ts 69 lines
1export type ContextReading = {
2  tokens: number;
3  window: number;
4  percent: number;
5} | null;
6
7export type DeskView = "proof" | "skills" | "replay" | "brief";
8
9export type WorkflowBrief = {
10  objective: string;
11  guardrails: string;
12  verification: string;
13  stop: string;
14  enabled: boolean;
15};
16
17export type CheckKind = "types" | "lint" | "tests" | "mods";
18export type ProofReceipt = {
19  toolId: string;
20  outcome: "pending" | "passed" | "failed" | "denied" | "incomplete";
21  fingerprint: string | null;
22  changedDuringCheck: boolean;
23};
24
25export type SkillObservation = {
26  name: string;
27  provenance: string;
28  outcome:
29    | "succeeded"
30    | "failed"
31    | "denied"
32    | "loaded read-only"
33    | "forked (launched; outcome unknown)"
34    | "forked (completed)";
35};
36
37export type SkillsState = { entries: SkillObservation[]; generation: number };
38
39export type ReplayEntry = {
40  path: string;
41  toolId: string;
42  provenance: string;
43  turn: string;
44  diff: string;
45  truncated: boolean;
46};
47export type ReplayState = {
48  entries: ReplayEntry[];
49  index: number;
50  generation: number;
51  excluded: number;
52};
53
54declare module "claude-code" {
55  interface PluginState {
56    "frontend-skills-mods": {
57      context: ContextReading;
58      deskView: DeskView;
59      repository: string | null;
60      proof: StateFamily<ProofReceipt>;
61      lastSkill: string | null;
62      skills: SkillsState;
63      turn: string;
64      replay: ReplayState;
65      brief: WorkflowBrief;
66    };
67  }
68}
69