SLOPSHOPPER

agentic-session

Agentic Development companion for Claude Code: a context band above the prompt, shown only past your threshold, with a one-press /ad-handoff, and…

newpanebandcommandprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agentic-session
│ ┃ Briefing ✕ › fix the failing auth test and add an audit log call │ ┃ │ ┃ No briefing: the kit script is not ⏺ Read(src/auth.ts) │ ┃ installed or did not run. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /agentic-briefing │ ⎿ agentic-session: Briefing pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Briefing
No briefing: the kit script is not installed or did not run.
README

Agentic Development

@alexandrealvaro/agentic installs a disciplined engineering skill kit for Claude Code and Codex. It helps an agent research, plan, build, review, ship, and hand work off without losing the project context between sessions.

It is not an agent runtime, a framework, or a product-management tool. It installs local skills and documentation; the host agent still does the work, and you retain every consequential decision.

Start here

From a project root, install the complete skill set for both hosts:

npx --yes @alexandrealvaro/agentic@latest init --yes

Then open the project in Claude Code or Codex and invoke:

/ad-next

/ad-next reads the repository and recommends the next useful action. When a project keeps active work in GitHub instead of local planning files, an optional machine or project configuration lets it include bounded issue and pull-request metadata through /ad-project-state. With no configuration, behavior stays repository-only. See the project-source contract. All skills are installed; you do not need to choose a maturity level or remember a catalog before starting.

Requires Node.js 22.13 or newer. Claude Code skills install under .claude/skills/; Codex skills install under .agents/skills/.

Essential commands

NeedCommand
Install the personal defaultnpx --yes @alexandrealvaro/agentic@latest init --yes
Install personal Claude Code skills onlynpx --yes @alexandrealvaro/agentic@latest init --agent claude-code --yes
Install personal Codex skills onlynpx --yes @alexandrealvaro/agentic@latest init --agent codex --yes
Update the personal kitnpx --yes @alexandrealvaro/agentic@latest update --yes
Preview an updatenpx --yes @alexandrealvaro/agentic@latest update --dry-run --yes
Install into a project deliberatelynpx --yes @alexandrealvaro/agentic@latest init --scope project --yes
Remove a project installationnpx --yes @alexandrealvaro/agentic@latest uninstall --scope project --yes
Install the agentic command globallynpm install --global @alexandrealvaro/agentic
Remove the global commandnpm uninstall --global @alexandrealvaro/agentic

After a global CLI install, agentic init --yes and agentic update --yes maintain the personal, machine-global kit from any directory. Add --scope project only for a deliberate shared project install. A global CLI install and a user-level skill install are different things; the installation guide explains both, including safe project removal.

Everyday workflow

Start every unfamiliar repository or fuzzy request with /ad-next.

SituationUseful path
New product/ad-grill-me → /ad-prd → /ad-bootstrap → /ad-spec → /ad-task
Existing feature/ad-ground → /ad-spec when scope needs a contract → /ad-tdd → /ad-review
Bug or regression/ad-diagnose → regression test → /ad-review
Finish and ship/ad-commit → /ad-pr → /ad-merge; package maintainers use /ad-release

These are routes, not gates. A one-off change should not produce artifacts that do not change the work; a durable or high-risk change should use the documentation and quality checks it needs.

Optional: Claude Code companion plugin

On Claude Code (2.1.287+ in the terminal, 2.1.286+ in the desktop app) you can add agentic-session, an opt-in plugin that draws a context band above the prompt once the session reaches your threshold of the auto-compact point (60% by default, set in /config), with an AD handoff button that runs /ad-handoff:

claude plugin marketplace add alexandremendoncaalvaro/agentic-development
claude plugin install agentic-session@agentic-development

It is separate from the installer above and carries no skills; skills always come from agentic init. Codex has no equivalent surface.

Find the right detail

The installer keeps project files in sync with a state-aware three-way diff: it updates kit changes, preserves user-edited skills by default, and never silently overwrites a conflict. Report issues on GitHub; releases are listed on GitHub Releases.

Source 3 files
hooks/register.mjs 295 lines
1// agentic-session: a context band above the prompt with a one-press
2// /ad-handoff, drawn only at or above the user's threshold (ADR-0088,
3// task-0104), and /agentic-briefing, which opens the work-in-progress briefing
4// in a pane (ADR-0090, task-0111). Shaped on Anthropic's token-weather sample:
5// read on session start, after each main-loop turn and after a compaction,
6// never on every draw. The briefing is the kit script's output, drawn as
7// printed; the plugin establishes no fact. The engine reads on(...) and
8// $.noun.method(...) from source, so they are spelled literally, and helpers
9// that take $ are top-level functions. A threshold changed in /config reloads
10// the module with the new options.
11
12import { HANDOFF_LABEL, bandLabel, fillReading, normalizeThreshold, shouldShow } from './band.mjs';
13import {
14  detailsMarkdown,
15  paneModel,
16  progressSvg,
17  progressText,
18  readBriefing,
19  scriptCandidates,
20} from './briefing-view.mjs';
21
22const PANE = 'agentic-briefing';
23const PANE_TITLE = 'Briefing';
24// A run that outlives this is dropped: the band shows nothing rather than wait.
25const SCRIPT_TIMEOUT_MS = 10_000;
26
27export function register(on, options) {
28  const threshold = normalizeThreshold(options?.threshold);
29  const state = { fill: null, briefing: null, briefingRun: 0 };
30
31  on('session.start', async ($, e, next) => {
32    const result = await next(e);
33    await registerCommand($);
34    await refresh($, state);
35    return result;
36  });
37
38  on('turn.complete', async ($, e, next) => {
39    const result = await next(e);
40    if (!e.agentId) await refresh($, state);
41    return result;
42  });
43
44  on('session.compact', async ($, e, next) => {
45    const result = await next(e);
46    await refresh($, state);
47    return result;
48  });
49
50  // /clear ends the session with no session.start after it: drop the readings.
51  on('session.end', async ($, e, next) => {
52    const result = await next(e);
53    clearReadings($, state);
54    return result;
55  });
56
57  on('command.run', { command: PANE }, async ($) => {
58    await $.ui.open({ id: PANE, title: PANE_TITLE });
59    return { text: 'Briefing pane opened.' };
60  });
61
62  on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => {
63    if (e.hasSurvey || !shouldShow(state.fill, threshold)) return next(e);
64    const { Box, Text, Button } = $.ui.resolve(e);
65    return Box({
66      flexDirection: 'row',
67      justifyContent: 'space-between',
68      paddingX: 1,
69      children: [
70        Text({ dimColor: true, children: bandLabel(state.fill) }),
71        Button({ key: 'handoff', label: HANDOFF_LABEL, onPress: () => submitHandoff($) }),
72      ],
73    });
74  });
75
76  on('ui.render', { component: 'Pane', requestId: PANE }, ($, e) => drawPane($, e, state.briefing));
77}
78
79const LEVEL_COLOR = { ok: 'green', warn: 'yellow', unknown: 'gray', active: '#d97757' };
80const ACCENT = '#d97757';
81const BAR_PX = 160;
82const BAR_CELLS = 16;
83
84function drawPane($, e, briefing) {
85  const el = $.ui.resolve(e);
86  const { Box, Text } = el;
87  if (!briefing) {
88    return Box({
89      padding: 1,
90      children: [
91        Text({
92          dimColor: true,
93          children: 'No briefing: the kit script is not installed or did not run.',
94        }),
95      ],
96    });
97  }
98  const model = paneModel(briefing);
99  const details = detailsMarkdown(briefing);
100  return Box({
101    flexDirection: 'column',
102    gap: 1,
103    paddingX: 1,
104    children: [
105      headerCard(el, model),
106      ...(model.next ? [nextCard(el, model.next)] : []),
107      ...(model.progress.length
108        ? [
109            section(
110              el,
111              'Progress',
112              model.progress.map((p) => progressRow(el, e, p))
113            ),
114          ]
115        : []),
116      section(
117        el,
118        'Health',
119        model.health.map((h) => healthRow(el, h))
120      ),
121      ...(details && el.Markdown ? [el.Markdown({ text: details })] : []),
122    ],
123  });
124}
125
126function headerCard({ Box, Text }, model) {
127  if (!model.header) {
128    return Box({
129      borderStyle: 'round',
130      paddingX: 1,
131      children: [Text({ bold: true, children: 'No single active task' })],
132    });
133  }
134  const { number, title, status, statusLevel, chosenBy } = model.header;
135  return Box({
136    flexDirection: 'column',
137    borderStyle: 'round',
138    paddingX: 1,
139    children: [
140      Box({
141        flexDirection: 'row',
142        gap: 1,
143        children: [
144          Text({ bold: true, color: ACCENT, children: `Task ${number}` }),
145          Text({
146            backgroundColor: LEVEL_COLOR[statusLevel],
147            color: 'black',
148            children: ` ${status} `,
149          }),
150        ],
151      }),
152      Text({ bold: true, children: title }),
153      Text({ dimColor: true, children: `Active because: ${chosenBy}` }),
154    ],
155  });
156}
157
158function nextCard({ Box, Text }, next) {
159  return Box({
160    flexDirection: 'column',
161    borderStyle: 'round',
162    borderColor: ACCENT,
163    paddingX: 1,
164    children: [
165      Text({ dimColor: true, bold: true, children: 'NEXT STEP' }),
166      Text({ bold: true, children: next.step }),
167      ...(next.detail ? [Text({ dimColor: true, children: next.detail })] : []),
168    ],
169  });
170}
171
172function section({ Box, Text }, title, rows) {
173  return Box({
174    flexDirection: 'column',
175    children: [Text({ dimColor: true, bold: true, children: title.toUpperCase() }), ...rows],
176  });
177}
178
179function progressRow({ Box, Text, Svg }, e, item) {
180  const bar =
181    e.surface === 'desktop' && Svg
182      ? [
183          Svg({
184            source: progressSvg(item, BAR_PX),
185            alt: `${item.done} of ${item.total}`,
186            width: BAR_PX,
187            height: 8,
188          }),
189          Text({ children: `${item.done}/${item.total}` }),
190        ]
191      : [Text({ children: progressText(item, BAR_CELLS) })];
192  return Box({
193    flexDirection: 'row',
194    gap: 1,
195    alignItems: 'center',
196    children: [Box({ width: 20, children: [Text({ children: item.label })] }), ...bar],
197  });
198}
199
200function healthRow({ Box, Text }, item) {
201  return Box({
202    flexDirection: 'row',
203    gap: 1,
204    children: [
205      Text({ color: LEVEL_COLOR[item.level], children: '●' }),
206      Box({ width: 18, children: [Text({ bold: true, children: item.label })] }),
207      Text({ children: item.value }),
208    ],
209  });
210}
211
212async function registerCommand($) {
213  try {
214    await $.command.register({
215      name: PANE,
216      description: 'Show the work-in-progress briefing in a pane',
217    });
218  } catch (error) {
219    logFailure($, 'briefing command not registered', error);
220  }
221}
222
223async function refresh($, state) {
224  await takeReading($, state);
225  await takeBriefing($, state);
226  $.ui.invalidate('ui.render');
227}
228
229async function takeReading($, state) {
230  try {
231    const { context } = await $.session.usage({ breakdown: 'summary' });
232    state.fill = fillReading(context);
233  } catch (error) {
234    // Fail closed (ADR-0088): with no fresh reading the context part is not
235    // drawn; the reason goes to the debug log, never on screen.
236    state.fill = null;
237    logFailure($, 'no context reading', error);
238  }
239}
240
241// Fail closed (ADR-0090): no installed script, a failed run or output that is
242// not a briefing leaves the pane without a briefing.
243async function takeBriefing($, state) {
244  // A run that finishes after a newer one started is dropped, never drawn.
245  const run = ++state.briefingRun;
246  const settle = (briefing) => {
247    if (run === state.briefingRun) state.briefing = briefing;
248  };
249  try {
250    const root = await $.session.root();
251    const home = (await $.env.get('HOME')) || (await $.env.get('USERPROFILE'));
252    const script = await firstInstalled($, scriptCandidates(root, home));
253    if (!script) {
254      settle(null);
255      return;
256    }
257    const sessionId = await $.session.id();
258    const result = await $.process.run(['node', script, '--session', sessionId], {
259      cwd: root,
260      timeoutMs: SCRIPT_TIMEOUT_MS,
261    });
262    const briefing = readBriefing(result);
263    settle(briefing);
264    if (!briefing) logFailure($, 'briefing unreadable', `exit ${result.exitCode}`);
265  } catch (error) {
266    settle(null);
267    logFailure($, 'no briefing', error);
268  }
269}
270
271async function firstInstalled($, paths) {
272  for (const path of paths) {
273    const found = await $.fs.stat(path).catch(() => undefined);
274    if (found) return path;
275  }
276  return null;
277}
278
279function clearReadings($, state) {
280  state.fill = null;
281  state.briefing = null;
282  state.briefingRun += 1;
283  $.ui.invalidate('ui.render');
284}
285
286function submitHandoff($) {
287  $.prompt
288    .submit({ text: '/ad-handoff', asUser: true })
289    .catch((error) => logFailure($, 'handoff not submitted', error));
290}
291
292function logFailure($, what, error) {
293  $.ui.log(`agentic-session: ${what} (${error?.message ?? error})`, { to: 'debug' });
294}
295
hooks/band.mjs 39 lines
1// The band's rule, kept free of the engine interface so node:test covers it
2// (ADR-0088, task-0104). register.mjs feeds it what $.session.usage() returns.
3
4export const DEFAULT_THRESHOLD = 60;
5
6// Names the kit's skill, so the press is recognisably Agentic Development's handoff.
7export const HANDOFF_LABEL = 'AD handoff';
8
9// A reading as { percent, basis }, measured toward the auto-compact point when
10// auto-compaction is on (GROUND-0035 E3, RESEARCH-0036 E3), else toward the
11// model's window; null before the first response of the live window.
12export function fillReading(context) {
13  if (!context || typeof context.tokens !== 'number') return null;
14  const breakdown = context.breakdown;
15  if (breakdown?.isAutoCompactEnabled && breakdown.autoCompactThreshold > 0) {
16    return reading(context.tokens, breakdown.autoCompactThreshold, 'auto-compact');
17  }
18  if (context.window > 0) return reading(context.tokens, context.window, 'window');
19  return null;
20}
21
22function reading(tokens, limit, basis) {
23  return { percent: Math.min(100, Math.round((tokens / limit) * 100)), basis };
24}
25
26export function normalizeThreshold(value) {
27  const number = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
28  return Number.isInteger(number) && number >= 1 && number <= 99 ? number : DEFAULT_THRESHOLD;
29}
30
31export function shouldShow(fill, threshold) {
32  return fill !== null && fill.percent >= threshold;
33}
34
35export function bandLabel(fill) {
36  const against = fill.basis === 'auto-compact' ? 'the auto-compact point' : 'the window';
37  return `Context ${fill.percent}% of ${against}`;
38}
39
hooks/briefing-view.mjs 194 lines
1// The briefing pane's model, kept free of the engine interface so node:test
2// covers it (ADR-0090, task-0111). register.mjs feeds it the JSON that the
3// kit's ad-next/scripts/briefing.mjs prints and draws what it returns; nothing
4// here establishes a fact the script did not report.
5
6// The task's number from its slug, `0111-show-...` -> `0111`.
7function taskNumber(slug) {
8  return slug.split('-')[0];
9}
10
11// The script's briefing from a $.process.run result, or null when the run
12// failed, was cut, or printed anything but a JSON object: the pane then shows
13// no briefing rather than a guess.
14export function readBriefing(result) {
15  if (!result || result.exitCode !== 0 || result.isStdoutTruncated) return null;
16  try {
17    const value = JSON.parse(result.stdout);
18    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
19  } catch {
20    return null;
21  }
22}
23
24const SCRIPT = '.claude/skills/ad-next/scripts/briefing.mjs';
25
26// Where the kit installs the script: a project install pins the repository's
27// own kit version, so it wins over the user install.
28export function scriptCandidates(root, home) {
29  return [`${root}/${SCRIPT}`, ...(home ? [`${home}/${SCRIPT}`] : [])];
30}
31
32const RULES = {
33  'single-in-progress': 'the only in-progress task',
34  'newest-commit-ahead': 'newest commit ahead of main',
35};
36
37const STATUS_LEVEL = { 'in-progress': 'active', done: 'ok', blocked: 'warn' };
38
39// A task title from its slug: `0111-show-the-work-in-progress-...` ->
40// `Show the work in progress ...`.
41function taskTitle(slug) {
42  const words = slug.split('-').slice(1).join(' ');
43  return words.charAt(0).toUpperCase() + words.slice(1);
44}
45
46function nextStep(plan) {
47  if (!plan.open.length) return null;
48  const item = plan.open[0];
49  const cut = item.indexOf(':');
50  return cut === -1
51    ? { step: item.trim(), detail: '' }
52    : { step: item.slice(0, cut).trim(), detail: item.slice(cut + 1).trim() };
53}
54
55function approvalHealth(approval, cannotTell = []) {
56  if (cannotTell.includes('approval')) {
57    const value = cannotTell.includes('git')
58      ? 'cannot tell: commits ahead of main not listed'
59      : 'cannot tell: approval not committed yet';
60    return { value, level: 'unknown' };
61  }
62  const order = approval.precedesFirstImplementingCommit;
63  if (order === true) return { value: 'before the first code', level: 'ok' };
64  if (order === false) {
65    const value = approval.entry ? 'after code was committed' : 'missing, and code is committed';
66    return { value, level: 'warn' };
67  }
68  return approval.entry
69    ? { value: 'order unknown', level: 'unknown' }
70    : { value: 'not approved yet', level: 'ok' };
71}
72
73function gateHealth(gate) {
74  if (!gate) return { value: 'no evidence for this session', level: 'unknown' };
75  if (!gate.wouldBlock) return { value: `none of ${gate.lines} checks would block`, level: 'ok' };
76  const latest = gate.lastWouldBlock
77    ? `; latest: ${gate.lastWouldBlock.check} before ${gate.lastWouldBlock.action}`
78    : '';
79  return {
80    value: `${gate.wouldBlock} of ${gate.lines} checks would block${latest}`,
81    level: 'warn',
82  };
83}
84
85// The pane's model: what the pane draws, derived only from the briefing.
86export function paneModel(b) {
87  const progress = [];
88  if (b.task) {
89    progress.push(
90      { label: 'Plan', done: b.plan.done.length, total: b.plan.done.length + b.plan.open.length },
91      {
92        label: 'Criteria',
93        done: b.acceptance.done,
94        total: b.acceptance.done + b.acceptance.open.length,
95      },
96      {
97        label: 'Definition of Done',
98        done: b.definitionOfDone.done,
99        total: b.definitionOfDone.done + b.definitionOfDone.open.length,
100      }
101    );
102  }
103  if (b.roadmap) {
104    progress.push({
105      label: 'Roadmap tasks',
106      done: b.roadmap.tasksDone,
107      total: b.roadmap.tasksTotal,
108    });
109  }
110  const health = [];
111  if (b.task) {
112    health.push({ label: 'Plan approval', ...approvalHealth(b.approval, b.cannotTell) });
113    health.push(
114      b.deviations.length
115        ? { label: 'Deviations', value: `${b.deviations.length} recorded`, level: 'warn' }
116        : { label: 'Deviations', value: 'none recorded', level: 'ok' }
117    );
118  }
119  if (!b.roadmap) {
120    health.push({
121      label: 'Roadmap',
122      value: 'cannot tell: no doc/product/PRD.md',
123      level: 'unknown',
124    });
125  }
126  health.push({ label: 'Gate (shadow)', ...gateHealth(b.gate) });
127  return {
128    header: b.task
129      ? {
130          number: taskNumber(b.task.slug),
131          title: taskTitle(b.task.slug),
132          status: b.task.status,
133          statusLevel: STATUS_LEVEL[b.task.status] ?? 'unknown',
134          chosenBy: RULES[b.task.rule] ?? b.task.rule,
135        }
136      : null,
137    next: b.task ? nextStep(b.plan) : null,
138    progress,
139    health,
140  };
141}
142
143const checklist = (items, done) => items.map((item) => `- [${done ? 'x' : ' '}] ${item}`);
144
145// The pane's detail block, as Markdown: checklists the surface draws as such.
146const CUT_NOTE = '\n\n_Cut to fit the pane; the task file has the rest._';
147
148// The Markdown element takes at most 10000 characters.
149export function detailsMarkdown(b, limit = 10_000) {
150  const full = fullDetails(b);
151  if (full.length <= limit) return full;
152  const room = full.slice(0, Math.max(0, limit - CUT_NOTE.length));
153  const lineEnd = room.lastIndexOf('\n');
154  return `${lineEnd === -1 ? room : room.slice(0, lineEnd)}${CUT_NOTE}`;
155}
156
157function fullDetails(b) {
158  const blocks = [];
159  if (b.task) {
160    blocks.push(['#### Plan', ...checklist(b.plan.done, true), ...checklist(b.plan.open, false)]);
161    if (b.acceptance.open.length) {
162      blocks.push(['#### Open criteria', ...checklist(b.acceptance.open, false)]);
163    }
164    if (b.definitionOfDone.open.length) {
165      blocks.push(['#### Definition of Done', ...checklist(b.definitionOfDone.open, false)]);
166    }
167    if (b.deviations.length) {
168      blocks.push(['#### Deviations', ...b.deviations.map((d) => `- **${d.heading}**: ${d.text}`)]);
169    }
170  }
171  if (b.unreadable.length) {
172    blocks.push(['#### Unreadable', ...b.unreadable.map((u) => `- \`${u.path}\` (${u.code})`)]);
173  }
174  return blocks.map((lines) => `${lines.join('\n')}\n`).join('\n');
175}
176
177const share = ({ done, total }) => (total > 0 ? Math.min(1, done / total) : 0);
178
179export function progressText(item, cells) {
180  const filled = Math.round(share(item) * cells);
181  return `${'█'.repeat(filled)}${'░'.repeat(cells - filled)} ${item.done}/${item.total}`;
182}
183
184// A rounded bar: the track in a muted tone, the fill in the accent.
185export function progressSvg(item, width) {
186  const filled = Math.round(share(item) * width);
187  return (
188    `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="8" viewBox="0 0 ${width} 8">` +
189    `<rect class="track" x="0" y="0" width="${width}" height="8" rx="4" fill="#8884"/>` +
190    `<rect class="fill" x="0" y="0" width="${filled}" height="8" rx="4" fill="#d97757"/>` +
191    '</svg>'
192  );
193}
194