SLOPSHOPPER

ask-check

When an answer ends in a question, shows the user's own earlier sentence that already answers it and sends it back on one key

newbandprompt
A shopper browsing a rack in a slop shop
README

Masterclass: Agentic Software Engineering with Claude Code

Great software no longer has to cost a fortune or consume every hour of your day. With Claude Code, you get an engineering partner that plans, builds, tests, and ships alongside you at a fraction of the time and cost. This isn't about prompting an AI. It's about designing systems that multiply what you can deliver. Welcome to the new standard.

Agentic software engineering is a discipline shift, and once you experience it, there's no going back. Instead of spending days on tasks that take agents minutes, you orchestrate workflows that reduce cost, eliminate repetition, and scale your output without scaling your team. The developers who master this don't just move faster. They do more with less, and they do it consistently.

Across 11 hands-on modules, you'll go from Claude Code fundamentals to production-grade multi-agent pipelines. You'll design composable skills, build MCP tools that connect agents to real systems, architect parallel sub-agent workflows, and harden everything for real-world conditions. Every module is built around genuine engineering decisions, because the goal was never to write more code. It was always to build software that matters.

ModuleDescription
01 FundamentalsClaude product family, installation and authentication, model selection and prompt caching
02 HarnessSettings hierarchy, CLAUDE.md constitution, custom slash commands, and event-driven hooks
03 SkillsSkill anatomy, invocation model, authoring by hand and from a conversation
04 MCPMCP architecture, connecting remote endpoints, wrapping REST APIs as servers
05 CLIHeadless -p runs, tool scoping, parallel isolation, and long-running goal patterns
06 Sub-agents and Agent TeamsSub-agent frontmatter, tool restriction, and orchestrator decomposition patterns; lead, teammates, shared task list and mailbox, and where a team beats a sub-agent
07 Optimize and ObservabilityPrompt audit, caveman compression, the autoresearch promote/revert loop, prompt injection, hook guardrails, and tracing
08 DistributionPlugin manifests, converting a .claude/ config, and marketplace catalogs
09 Agentic DevOpsContainerized deploy to a plain Ubuntu host over SSH, with the agent proving the result
10 Agent SDK & Managed AgentsThe query() async iterator, session resume, custom tools and typed output, then cloud containers, mid-execution steering via SSE, and multiagent with outcomes and memory
11 PlanningOptional. Plan Mode mechanics, decomposition, and independently verifiable sub-agent packages
Source 2 files
hooks/ask-check.mjs 173 lines
1const PROMPT_WINDOW = 40;
2const USER_ORIGINS = new Set(["composer", "bridge", "sdk"]);
3
4const promptsRef = { plugin: "ask-check", key: "prompts" };
5const questionsRef = { plugin: "ask-check", key: "questions" };
6const countRef = { plugin: "ask-check", key: "count" };
7
8const ASKED_WINDOW = 40;
9const NO_QUESTIONS = /\b(no (other |more |further )?(questions?|q)\b|(do ?n.?t|stop|never) (ask|bother|bloat)\w*( me)?( with)? (any |more )?(questions?|findings)|without questions|bloat\w* me with questions|(invent|make up) (findings and )?questions)/i;
10const askedRef = { plugin: "ask-check", key: "asked" };
11const answersRef = { plugin: "ask-check", key: "answers" };
12const ANSWER_WINDOW = 10;
13const LEAD = { rule: "you said", said: "you said", replied: "asked before, you replied" };
14
15const STOP = new Set(`
16a about above after again all also am an and any are as at be because been before being below between both but by can could did do does doing done down during each either else even ever every few for from further get got had has have having he her here hers him his how i if in into is it its just keep let like make may me might mine more most much must my no nor not now of off on once one only or other our out over own please put rather really same see shall she should since so some still such sure than that the their them then there these they this those through to too under until up upon us use used very via want was way we were what when where whether which while who whom why will with would yes yet you your yours
17able already anything currently either exactly instead maybe need needs next okay ok prefer right something thing things think whatever wish wanted proceed option options go ahead fine
18aber alle als also am an auch auf aus bei bin bis bitte da dann das dass dem den der des die dies diese doch du ein eine einem einen einer es für hat hier ich ihr im in ist ja jetzt kann mit nach nicht noch nur oder schon sein sich sie sind so und uns von vor was wenn wie wir wird zu zum zur
19`.split(/\s+/).filter(Boolean));
20
21export function register(on) {
22  on("prompt.submit", async ($, e, next) => {
23    const result = await next(e);
24    if (!USER_ORIGINS.has(e.origin?.kind)) return result;
25    const text = String(e.text ?? "").trim();
26    if (text === "") return result;
27    const { value: count = 0 } = await $.state.get(countRef);
28    const { value: prompts = [] } = await $.state.get(promptsRef);
29    await $.state.set(countRef, count + 1);
30    await $.state.set(promptsRef, [...prompts, { n: count + 1, text: text.slice(0, 4000) }].slice(-PROMPT_WINDOW));
31    await $.state.set(questionsRef, []);
32    return result;
33  });
34
35  on("turn.complete", async ($, e, next) => {
36    const result = await next(e);
37    if (e.agentId || e.reason !== "answer" || typeof e.answer !== "string") return result;
38    const { value: prompts = [] } = await $.state.get(promptsRef);
39    const { value: asked = [] } = await $.state.get(askedRef);
40    const { value: count = 0 } = await $.state.get(countRef);
41    const { value: answers = [] } = await $.state.get(answersRef);
42    const questions = checkAnswer(e.answer, prompts, asked, answers);
43    await $.state.set(answersRef, [...answers, normalize(e.answer).slice(0, 8000)].slice(-ANSWER_WINDOW));
44    await $.state.set(questionsRef, questions);
45    if (questions.length) await $.state.set(askedRef, [...asked, ...questions.map((q) => ({ question: q.question, n: count }))].slice(-ASKED_WINDOW));
46    return result;
47  });
48
49  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
50    const below = await next(e);
51    const { value: questions = [] } = await $.state.get(questionsRef);
52    if (e.props.hasSurvey || e.props.isWorking || questions.length === 0) return below;
53    const answered = questions.filter((q) => q.answer && q.answer.via !== "replied");
54    const before = questions.filter((q) => q.answer?.via === "replied");
55    const matched = [...answered, ...before];
56    const { Box, Text, Button } = $.ui.resolve(e);
57    const width = Math.max(20, (e.props.bodyColumns ?? 80) - 4);
58    const summary = [`asked ${questions.length}`, `already answered ${answered.length}`, ...(before.length ? [`asked before ${before.length}`] : [])].join(" · ");
59    const rows = [
60      Box({
61        flexDirection: "row",
62        children: [
63          Text({ color: matched.length ? "yellow" : undefined, dimColor: matched.length === 0, children: summary }),
64          ...(matched.length
65            ? [Text({ children: "   " }), Button({ key: "send", label: "send already answered", hotkey: "1", plain: true, onPress: () => sendAnswered($) })]
66            : []),
67        ],
68      }),
69      ...matched.map((q) =>
70        Text({ wrap: "truncate-end", children: `  ↳ ${LEAD[q.answer.via]} "${clip(q.answer.text, width - 34)}" (prompt ${q.answer.n})` }),
71      ),
72    ];
73    const mine = Box({ flexDirection: "column", paddingX: 1, children: rows });
74    return below ? Box({ flexDirection: "column", children: [mine, below] }) : mine;
75  });
76}
77
78async function sendAnswered($) {
79  const { value: questions = [] } = await $.state.get(questionsRef);
80  const matched = questions.filter((q) => q.answer);
81  if (matched.length === 0) return;
82  const unique = [...new Map(matched.map((q) => [q.answer.text, q.answer])).values()];
83  const said = unique.filter((a) => a.via !== "replied").map((a) => `"${a.text}" (my prompt ${a.n})`);
84  const replied = unique.filter((a) => a.via === "replied").map((a) => `"${a.text}" (my prompt ${a.n})`);
85  const parts = [...(said.length ? [`Already answered: ${said.join("; ")}.`] : []), ...(replied.length ? [`You asked this before and I replied: ${replied.join("; ")}.`] : [])];
86  await $.state.set(questionsRef, []);
87  await $.prompt.submit({ text: `${parts.join(" ")} Proceed.`, asUser: true });
88}
89
90export function checkAnswer(answer, prompts, asked = [], answers = []) {
91  const standing = standingRule(prompts);
92  return questionsIn(answer).map((question) => ({
93    question,
94    answer: standing ?? repliedBefore(question, prompts, asked) ?? findAnswer(question, prompts, answers),
95  }));
96}
97
98function standingRule(prompts) {
99  for (let i = prompts.length - 1; i >= 0; i--) {
100    const hit = clauses(prompts[i].text).find((c) => NO_QUESTIONS.test(c));
101    if (hit) return { text: hit, n: prompts[i].n, via: "rule" };
102  }
103  return null;
104}
105
106function repliedBefore(question, prompts, asked) {
107  const q = new Set(tokens(question));
108  if (q.size === 0) return null;
109  for (let i = asked.length - 1; i >= 0; i--) {
110    const before = new Set(tokens(asked[i].question));
111    let shared = 0;
112    for (const t of before) if (q.has(t)) shared++;
113    if (shared < 2 || shared / Math.min(q.size, before.size) < 0.5) continue;
114    const reply = prompts.find((p) => p.n === asked[i].n + 1);
115    if (reply) return { text: clip(reply.text.replace(/\s+/g, " "), 300), n: reply.n, via: "replied" };
116  }
117  return null;
118}
119
120export function questionsIn(answer) {
121  const prose = answer.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]*`/g, (m) => m.slice(1, -1));
122  return prose
123    .split(/\n+|(?<=[.!?])\s+(?=[A-Z0-9"*(])/)
124    .map((s) => s.replace(/^[\s>*#\-\d.)]+/, "").replace(/\*\*/g, "").trim())
125    .filter((s) => s.endsWith("?") && s.length >= 12 && s.length <= 500);
126}
127
128export function findAnswer(question, prompts, answers = []) {
129  const q = new Set(tokens(question));
130  if (q.size === 0) return null;
131  let best = null;
132  for (const p of prompts) {
133    for (const clause of clauses(p.text)) {
134      if (clause.length >= 25 && answers.some((a) => a.includes(normalize(clause)))) continue;
135      const c = new Set(tokens(clause));
136      let shared = 0;
137      for (const t of c) if (q.has(t)) shared++;
138      const ratio = shared / q.size;
139      if (shared < 2 || ratio < 0.45) continue;
140      const score = shared + ratio;
141      if (!best || score >= best.score) best = { score, text: clause, n: p.n };
142    }
143  }
144  return best ? { text: best.text, n: best.n, via: "said" } : null;
145}
146
147function clauses(text) {
148  return text
149    .replace(/<pasted_content[\s\S]*?<\/pasted_content[^>]*>/g, " ")
150    .split(/\n+|\s*(?:\.\.\.|…|->|[.!?;])\s+|\s+\d+[.):]\s+/)
151    .map((s) => s.trim())
152    .filter((s) => s.length >= 3 && s.length <= 300);
153}
154
155function tokens(text) {
156  return (text.toLowerCase().match(/[a-zäöüß0-9][a-zäöüß0-9._-]*[a-zäöüß0-9]/g) ?? [])
157    .filter((w) => w.length >= 3 && !STOP.has(w) && !/^\d+$/.test(w))
158    .map(stem);
159}
160
161function stem(w) {
162  if (/[._-]/.test(w)) return w;
163  return w.replace(/(ing|ed|es|s)$/, "").replace(/(.)\1$/, "$1");
164}
165
166function normalize(text) {
167  return text.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ");
168}
169
170function clip(text, max) {
171  return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
172}
173
types/index.d.ts 10 lines
1export type AskCheckPrompt = { n: number; text: string };
2export type AskCheckQuestion = { question: string; answer: { text: string; n: number } | null };
3export type AskCheckAsked = { question: string; n: number };
4
5declare module "claude-code" {
6  interface PluginState {
7    "ask-check": { prompts: AskCheckPrompt[]; questions: AskCheckQuestion[]; asked: AskCheckAsked[]; answers: string[]; count: number };
8  }
9}
10