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

@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.
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/.
| Need | Command |
|---|---|
| Install the personal default | npx --yes @alexandrealvaro/agentic@latest init --yes |
| Install personal Claude Code skills only | npx --yes @alexandrealvaro/agentic@latest init --agent claude-code --yes |
| Install personal Codex skills only | npx --yes @alexandrealvaro/agentic@latest init --agent codex --yes |
| Update the personal kit | npx --yes @alexandrealvaro/agentic@latest update --yes |
| Preview an update | npx --yes @alexandrealvaro/agentic@latest update --dry-run --yes |
| Install into a project deliberately | npx --yes @alexandrealvaro/agentic@latest init --scope project --yes |
| Remove a project installation | npx --yes @alexandrealvaro/agentic@latest uninstall --scope project --yes |
Install the agentic command globally | npm install --global @alexandrealvaro/agentic |
| Remove the global command | npm 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.
Start every unfamiliar repository or fuzzy request with /ad-next.
| Situation | Useful 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.
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.
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.
hooks/register.mjs 295 lines1// 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}
295hooks/band.mjs 39 lines1// 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}
39hooks/briefing-view.mjs 194 lines1// 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