@alexandrealvaro/agentic 0.6.0-beta.1 → 0.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,11 +40,41 @@ Two categories ([ADR-0007](doc/adr/0007-workflow-operational-skills.md)) and two
40
40
  | `agentic-design` | spec-driven | auto if frontend detected | Bootstrap `DESIGN.md` from existing tokens (Figma, tailwind.config, tokens.json, CSS custom props) | `/agentic-design` |
41
41
  | `agentic-subagent` | spec-driven | auto if installing for Claude Code | Drafts `.claude/agents/<name>.md` (Claude Code only — Codex has no subagent primitive) | `/agentic-subagent` |
42
42
  | `agentic-skill` | spec-driven | opt-in only | Drafts a new Claude Code or Codex skill at the appropriate path | `/agentic-skill` |
43
+ | `agentic-hooks` | workflow-operational | opt-in only | Scaffolds deterministic quality gates per WORKFLOW §11 (pre-commit + pre-push); detects stack and recommends a runner (Husky / lefthook / pre-commit / native) | `/agentic-hooks` |
43
44
 
44
45
  A short TUI shows the detected mode, agent, and feature signals (frontend / `.claude/` / `.agents/` presence) and lets you toggle the conditional skills. Non-interactive flags: `--agent claude-code | codex | both`, `--yes` to skip confirmations — auto-checked conditionals (e.g., `agentic-design` if the project has React) install; `agentic-skill` stays opt-in. Re-running on an installed project is idempotent — unchanged files report `·`, divergent ones prompt to replace.
45
46
 
46
47
  If your project already has an `AGENTS.md` (or `CLAUDE.md`), the installer appends a managed `Skills installed by agentic` section bracketed by `<!-- agentic-managed-skills:start -->` / `:end -->` markers. User content outside those markers is byte-preserved; re-runs update only the managed block.
47
48
 
49
+ ## Project maturity profiles
50
+
51
+ The kit ships four profiles that select which skills auto-install. Same WORKFLOW principles bind every profile; only the artifact set scales.
52
+
53
+ | Profile | Universal install set | Conditional posture | Recommended for |
54
+ | --- | --- | --- | --- |
55
+ | `poc` | philosophy, ground, audit | all blocked | spike, hackathon, exploration |
56
+ | `solo` | + bootstrap, spec, task, review | architecture / adr / hooks opt-in; design auto if frontend; subagent auto for Claude Code | solo developer shipping a real product |
57
+ | `team` (default) | + architecture, adr | hooks opt-in; design / subagent / skill follow autoIf | team product, shared discipline |
58
+ | `mature` | same as team | hooks **recommended**; future evals + spike skills land here | regulated / public-facing production |
59
+
60
+ Select at init time:
61
+
62
+ ```bash
63
+ npx @alexandrealvaro/agentic@beta init --profile poc
64
+ ```
65
+
66
+ Or change later:
67
+
68
+ ```bash
69
+ npx @alexandrealvaro/agentic@beta profile set solo # add solo's universal skills
70
+ npx @alexandrealvaro/agentic@beta profile show # show current per-agent profile
71
+ npx @alexandrealvaro/agentic@beta profile list # list all profiles
72
+ ```
73
+
74
+ `profile set` runs the equivalent of `update` after writing the new profile, so the install set matches. The state-aware three-way diff prompts before overwriting user-edited files.
75
+
76
+ Existing v0.7 installs with no profile field migrate to `team` automatically — same install set as before, no user action needed.
77
+
48
78
  ## Updating an existing project
49
79
 
50
80
  To pull upstream kit changes into a project that already has agentic skills installed:
@@ -118,7 +148,7 @@ The kit ships nine universal skills plus three conditional ones — twelve discr
118
148
  2. Decide whether the answer becomes a spec (`/agentic-spec`) or a one-off task (`/agentic-task`).
119
149
  3. Continue from step 6 of the greenfield flow.
120
150
 
121
- The kit's discipline scales with the project's maturity. A solo PoC may legitimately skip `/agentic-spec` and `/agentic-adr` (the WORKFLOW §1 prune principle applies — don't add an artifact that wouldn't change agent behavior). A team product running on this kit is expected to use the full sequence; CI + hooks (deferred see Task 0013) will eventually enforce the gates that today are advisory.
151
+ The kit's discipline scales with the project's maturity. A solo PoC may legitimately skip `/agentic-spec` and `/agentic-adr` (the WORKFLOW §1 prune principle applies — don't add an artifact that wouldn't change agent behavior). A team product running on this kit is expected to use the full sequence and additionally invoke `/agentic-hooks` once to scaffold the deterministic gates per WORKFLOW §11 (pre-commit lint / format / secret-scan; pre-push build / unit / integration). Project maturity profiles that automate the recommendation by stack are deferred — see the next planned release.
122
152
 
123
153
  ## Manual prompts
124
154
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexandrealvaro/agentic",
3
- "version": "0.6.0-beta.1",
3
+ "version": "0.8.0-beta.1",
4
4
  "description": "Bootstrap and audit AGENTS.md, ARCHITECTURE.md, ADRs, skills, and subagents for engineering production code with LLMs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "scripts": {
22
22
  "start": "node bin/agentic.js",
23
- "test": "node bin/agentic.js --help && node bin/agentic.js init --help && node bin/agentic.js update --help && node --test test/*.test.js",
23
+ "test": "node bin/agentic.js --help && node bin/agentic.js init --help && node bin/agentic.js update --help && node bin/agentic.js profile --help && node --test test/*.test.js",
24
24
  "prepublishOnly": "npm test"
25
25
  },
26
26
  "keywords": [
@@ -5,6 +5,14 @@ import { dirname, join } from 'node:path';
5
5
  import { detectAgents, detectFeatures, detectMode } from '../lib/detect.js';
6
6
  import { installSkills } from '../lib/install.js';
7
7
  import { saveState, loadState } from '../lib/state.js';
8
+ import {
9
+ DEFAULT_PROFILE,
10
+ PROFILES,
11
+ PROFILE_NAMES,
12
+ availableConditionalsForProfile,
13
+ profileOrDefault,
14
+ requiredSkillsForProfile,
15
+ } from '../lib/profiles.js';
8
16
  import { updateRootDoc } from '../lib/rootdoc.js';
9
17
 
10
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -43,23 +51,18 @@ const ROOT_DOC_LABEL = {
43
51
  absent: '',
44
52
  };
45
53
 
46
- export const REQUIRED_SKILLS = [
47
- 'agentic-bootstrap',
48
- 'agentic-philosophy',
49
- 'agentic-architecture',
50
- 'agentic-adr',
51
- 'agentic-spec',
52
- 'agentic-task',
53
- 'agentic-audit',
54
- 'agentic-review',
55
- 'agentic-ground',
56
- ];
54
+ /**
55
+ * Backward-compatible export: the `team` profile's universal skill list.
56
+ * Tests and downstream code that imported REQUIRED_SKILLS pre-v0.8 get the
57
+ * same list. New code should call `requiredSkillsForProfile(profileName)`.
58
+ */
59
+ export const REQUIRED_SKILLS = requiredSkillsForProfile('team');
57
60
 
58
61
  /**
59
- * Conditional skills.
60
- * - autoIf(features): true pre-checked / installed by default in non-interactive.
61
- * - agents: which agents have a source tree for this skill (claude-code, codex, or both).
62
- * - hint: shown next to the option in the TUI.
62
+ * Backward-compatible export: the v0.7-shape conditional skill catalog.
63
+ * The four entries match the four conditional skills available in v0.7
64
+ * with their autoIf / agents / hint configuration. The profile-aware
65
+ * install path overrides `autoIf` per profile via `availableConditionalsForProfile`.
63
66
  */
64
67
  export const CONDITIONAL_SKILLS = [
65
68
  {
@@ -83,8 +86,19 @@ export const CONDITIONAL_SKILLS = [
83
86
  hintWhenAuto: 'opt-in',
84
87
  hintWhenManual: 'opt-in (rarely needed)',
85
88
  },
89
+ {
90
+ name: 'agentic-hooks',
91
+ autoIf: () => false,
92
+ agents: ['claude-code', 'codex'],
93
+ hintWhenAuto: 'opt-in',
94
+ hintWhenManual: 'WORKFLOW §11 hooks scaffolder (pre-commit, pre-push)',
95
+ },
86
96
  ];
87
97
 
98
+ const CONDITIONAL_BY_NAME = Object.fromEntries(
99
+ CONDITIONAL_SKILLS.map((s) => [s.name, s])
100
+ );
101
+
88
102
  function resolveAgents(flagValue, detectedAgents) {
89
103
  if (flagValue === 'both') return ['claude-code', 'codex'];
90
104
  if (flagValue) return [flagValue];
@@ -92,18 +106,39 @@ function resolveAgents(flagValue, detectedAgents) {
92
106
  return ['claude-code'];
93
107
  }
94
108
 
95
- function pickConditionalAuto(features, targetAgents) {
96
- return CONDITIONAL_SKILLS.filter(
97
- (s) => s.autoIf(features) && s.agents.some((a) => targetAgents.includes(a))
98
- ).map((s) => s.name);
109
+ /**
110
+ * Translate a profile rule (`'frontend'`, `'claude-code'`, `true`, `false`)
111
+ * into a boolean: should this conditional auto-install for the current
112
+ * features and target agents?
113
+ */
114
+ function evaluateRule(rule, features, targetAgents) {
115
+ if (rule === 'frontend') return features.frontend === true;
116
+ if (rule === 'claude-code') return targetAgents.includes('claude-code');
117
+ if (rule === true) return true;
118
+ if (rule === false) return false;
119
+ return false;
120
+ }
121
+
122
+ function pickConditionalAuto(features, targetAgents, profileName) {
123
+ const out = [];
124
+ for (const { name, rule } of availableConditionalsForProfile(profileName)) {
125
+ const def = CONDITIONAL_BY_NAME[name];
126
+ if (!def) continue;
127
+ if (!def.agents.some((a) => targetAgents.includes(a))) continue;
128
+ if (evaluateRule(rule, features, targetAgents)) {
129
+ out.push(name);
130
+ }
131
+ }
132
+ return out;
99
133
  }
100
134
 
101
- function skillsForAgent(agent, optedSkills) {
135
+ function skillsForAgent(agent, profileName, optedSkills) {
136
+ const universal = requiredSkillsForProfile(profileName);
102
137
  const conditional = optedSkills.filter((skillName) => {
103
- const def = CONDITIONAL_SKILLS.find((s) => s.name === skillName);
138
+ const def = CONDITIONAL_BY_NAME[skillName];
104
139
  return def && def.agents.includes(agent);
105
140
  });
106
- return [...REQUIRED_SKILLS, ...conditional];
141
+ return [...universal, ...conditional];
107
142
  }
108
143
 
109
144
  export async function initCommand(opts) {
@@ -113,6 +148,12 @@ export async function initCommand(opts) {
113
148
  );
114
149
  }
115
150
 
151
+ if (opts.profile && !PROFILE_NAMES.includes(opts.profile)) {
152
+ throw new Error(
153
+ `invalid profile "${opts.profile}". Use one of: ${PROFILE_NAMES.join(', ')}`
154
+ );
155
+ }
156
+
116
157
  const cwd = process.cwd();
117
158
  const interactive = process.stdout.isTTY && !opts.yes && !opts.agent;
118
159
 
@@ -120,18 +161,20 @@ export async function initCommand(opts) {
120
161
  const detectedAgents = detectAgents(cwd);
121
162
  const features = detectFeatures(cwd);
122
163
 
164
+ let profileName = profileOrDefault(opts.profile);
123
165
  let agents;
124
166
  let optedSkills;
125
167
 
126
168
  if (interactive) {
127
169
  p.intro('agentic init');
128
- const featureLine = [
129
- features.frontend ? 'frontend' : null,
130
- features.hasClaudeCode ? '.claude/ present' : null,
131
- features.hasCodex ? '.agents/.openai/ present' : null,
132
- ]
133
- .filter(Boolean)
134
- .join(', ') || 'none';
170
+ const featureLine =
171
+ [
172
+ features.frontend ? 'frontend' : null,
173
+ features.hasClaudeCode ? '.claude/ present' : null,
174
+ features.hasCodex ? '.agents/.openai/ present' : null,
175
+ ]
176
+ .filter(Boolean)
177
+ .join(', ') || 'none';
135
178
 
136
179
  p.note(
137
180
  `Mode: ${MODE_LABEL[detectedMode]}\n` +
@@ -144,6 +187,21 @@ export async function initCommand(opts) {
144
187
  'Detected context'
145
188
  );
146
189
 
190
+ const profileChoice = await p.select({
191
+ message: 'Project maturity profile?',
192
+ options: PROFILE_NAMES.map((name) => ({
193
+ value: name,
194
+ label: name,
195
+ hint: PROFILES[name].note,
196
+ })),
197
+ initialValue: profileName,
198
+ });
199
+ if (p.isCancel(profileChoice)) {
200
+ p.cancel('Cancelled.');
201
+ return;
202
+ }
203
+ profileName = profileChoice;
204
+
147
205
  const choice = await p.select({
148
206
  message: 'Install skills for which agent(s)?',
149
207
  options: [
@@ -162,14 +220,20 @@ export async function initCommand(opts) {
162
220
  }
163
221
  agents = choice;
164
222
 
165
- const conditionalOptions = CONDITIONAL_SKILLS.filter((s) =>
166
- s.agents.some((a) => agents.includes(a))
167
- ).map((s) => ({
168
- value: s.name,
169
- label: s.name,
170
- hint: s.autoIf(features) ? s.hintWhenAuto : s.hintWhenManual,
171
- }));
172
- const initialValues = pickConditionalAuto(features, agents);
223
+ const conditionalOptions = availableConditionalsForProfile(profileName)
224
+ .map(({ name, rule }) => {
225
+ const def = CONDITIONAL_BY_NAME[name];
226
+ if (!def) return null;
227
+ if (!def.agents.some((a) => agents.includes(a))) return null;
228
+ const auto = evaluateRule(rule, features, agents);
229
+ return {
230
+ value: name,
231
+ label: name,
232
+ hint: auto ? def.hintWhenAuto : def.hintWhenManual,
233
+ };
234
+ })
235
+ .filter(Boolean);
236
+ const initialValues = pickConditionalAuto(features, agents, profileName);
173
237
 
174
238
  if (conditionalOptions.length > 0) {
175
239
  const picked = await p.multiselect({
@@ -187,14 +251,17 @@ export async function initCommand(opts) {
187
251
  optedSkills = [];
188
252
  }
189
253
 
190
- const totalCount = REQUIRED_SKILLS.length + optedSkills.length;
254
+ const universalForProfile = requiredSkillsForProfile(profileName);
255
+ const totalCount = universalForProfile.length + optedSkills.length;
191
256
  const optedSummary = optedSkills.length
192
257
  ? `, plus ${optedSkills.join(', ')}`
193
258
  : '';
194
259
  const confirm = await p.confirm({
195
- message: `Install ${totalCount} skill${totalCount === 1 ? '' : 's'} (${REQUIRED_SKILLS.join(
196
- ', '
197
- )}${optedSummary}) for ${agents.map((a) => AGENT_LABEL[a]).join(' + ')}?`,
260
+ message: `Install ${totalCount} skill${
261
+ totalCount === 1 ? '' : 's'
262
+ } (${universalForProfile.join(', ')}${optedSummary}) for ${agents
263
+ .map((a) => AGENT_LABEL[a])
264
+ .join(' + ')}?`,
198
265
  initialValue: true,
199
266
  });
200
267
  if (p.isCancel(confirm) || !confirm) {
@@ -203,7 +270,7 @@ export async function initCommand(opts) {
203
270
  }
204
271
  } else {
205
272
  agents = resolveAgents(opts.agent, detectedAgents);
206
- optedSkills = pickConditionalAuto(features, agents);
273
+ optedSkills = pickConditionalAuto(features, agents, profileName);
207
274
  }
208
275
 
209
276
  const confirmReplace = interactive
@@ -217,7 +284,7 @@ export async function initCommand(opts) {
217
284
  const allActions = [];
218
285
  const installedSkillSet = new Set();
219
286
  for (const agent of agents) {
220
- const agentSkills = skillsForAgent(agent, optedSkills);
287
+ const agentSkills = skillsForAgent(agent, profileName, optedSkills);
221
288
  for (const s of agentSkills) installedSkillSet.add(s);
222
289
  const previousStates = { [agent]: loadState(cwd, agent) };
223
290
  const { actions, nextStates } = await installSkills({
@@ -229,7 +296,9 @@ export async function initCommand(opts) {
229
296
  kitVersion: pkg.version,
230
297
  });
231
298
  allActions.push(...actions);
232
- saveState(cwd, agent, nextStates[agent]);
299
+ const next = nextStates[agent];
300
+ next.profile = profileName;
301
+ saveState(cwd, agent, next);
233
302
  }
234
303
 
235
304
  const skillDisplayOrder = [
@@ -275,9 +344,31 @@ export async function initCommand(opts) {
275
344
  ? ['/agentic-subagent']
276
345
  : []),
277
346
  ...(optedSkills.includes('agentic-skill') ? ['/agentic-skill'] : []),
278
- ].join(', ');
347
+ ...(optedSkills.includes('agentic-hooks') ? ['/agentic-hooks (WORKFLOW §11)'] : []),
348
+ ]
349
+ .filter((line) => {
350
+ // Filter the universal-set entries to only those actually installed
351
+ // for this profile.
352
+ const universalNames = requiredSkillsForProfile(profileName);
353
+ const universalLabels = {
354
+ 'agentic-bootstrap': '/agentic-bootstrap (AGENTS.md)',
355
+ 'agentic-architecture': '/agentic-architecture (ARCHITECTURE.md)',
356
+ 'agentic-adr': '/agentic-adr',
357
+ 'agentic-spec': '/agentic-spec (doc/specs/)',
358
+ 'agentic-task': '/agentic-task',
359
+ 'agentic-audit': '/agentic-audit',
360
+ 'agentic-review': '/agentic-review (WORKFLOW §10)',
361
+ 'agentic-ground': '/agentic-ground (WORKFLOW §4 + §5)',
362
+ // 'agentic-philosophy' is implicit and not listed.
363
+ };
364
+ for (const [skill, label] of Object.entries(universalLabels)) {
365
+ if (line === label) return universalNames.includes(skill);
366
+ }
367
+ return true;
368
+ })
369
+ .join(', ');
279
370
  p.outro(
280
- `Done. In ${agents
371
+ `Done (profile: ${profileName}). In ${agents
281
372
  .map((a) => AGENT_LABEL[a])
282
373
  .join(' or ')}: ${slashLine}. agentic-philosophy auto-loads on non-trivial work.`
283
374
  );
@@ -0,0 +1,165 @@
1
+ import * as p from '@clack/prompts';
2
+ import { detectAgents } from '../lib/detect.js';
3
+ import {
4
+ DEFAULT_PROFILE,
5
+ PROFILES,
6
+ PROFILE_NAMES,
7
+ validateProfile,
8
+ } from '../lib/profiles.js';
9
+ import { loadState, saveState } from '../lib/state.js';
10
+ import { updateCommand } from './update.js';
11
+
12
+ const VALID_AGENTS = ['claude-code', 'codex'];
13
+
14
+ const AGENT_LABEL = {
15
+ 'claude-code': 'Claude Code',
16
+ codex: 'Codex',
17
+ };
18
+
19
+ function readProjectProfile(cwd) {
20
+ const perAgent = {};
21
+ for (const agent of VALID_AGENTS) {
22
+ const state = loadState(cwd, agent);
23
+ if (state) perAgent[agent] = state.profile ?? DEFAULT_PROFILE;
24
+ }
25
+ return perAgent;
26
+ }
27
+
28
+ function showProfile(cwd) {
29
+ const perAgent = readProjectProfile(cwd);
30
+ if (Object.keys(perAgent).length === 0) {
31
+ process.stdout.write(
32
+ 'No agentic install detected (no .claude/agentic-state.json or .agents/agentic-state.json).\n'
33
+ );
34
+ process.stdout.write(
35
+ `Run \`agentic init\` to scaffold; default profile is \`${DEFAULT_PROFILE}\`.\n`
36
+ );
37
+ return;
38
+ }
39
+ for (const [agent, profile] of Object.entries(perAgent)) {
40
+ process.stdout.write(`${AGENT_LABEL[agent]}: ${profile}\n`);
41
+ }
42
+ }
43
+
44
+ function listProfiles() {
45
+ for (const name of PROFILE_NAMES) {
46
+ const def = PROFILES[name];
47
+ process.stdout.write(`${name}\n ${def.note}\n`);
48
+ process.stdout.write(` Universal: ${def.universal.join(', ')}\n`);
49
+ const conditional = Object.entries(def.conditional)
50
+ .filter(([, rule]) => rule !== 'blocked')
51
+ .map(([skill, rule]) => `${skill}=${formatRule(rule)}`)
52
+ .join(', ');
53
+ process.stdout.write(` Conditional: ${conditional || 'none'}\n\n`);
54
+ }
55
+ }
56
+
57
+ function formatRule(rule) {
58
+ if (rule === true) return 'recommended';
59
+ if (rule === false) return 'opt-in';
60
+ if (typeof rule === 'string') return `auto-if-${rule}`;
61
+ return String(rule);
62
+ }
63
+
64
+ async function setProfile(cwd, name, opts) {
65
+ validateProfile(name);
66
+
67
+ const perAgent = readProjectProfile(cwd);
68
+ if (Object.keys(perAgent).length === 0) {
69
+ throw new Error(
70
+ 'no agentic install detected. Run `agentic init --profile <name>` first.'
71
+ );
72
+ }
73
+
74
+ const interactive = process.stdout.isTTY && !opts.yes;
75
+
76
+ const currentProfiles = [...new Set(Object.values(perAgent))];
77
+ if (currentProfiles.length === 1 && currentProfiles[0] === name) {
78
+ process.stdout.write(`Profile already \`${name}\` for all installed agents. No change.\n`);
79
+ return;
80
+ }
81
+
82
+ if (interactive) {
83
+ p.intro(`agentic profile set ${name}`);
84
+ p.note(
85
+ Object.entries(perAgent)
86
+ .map(([agent, profile]) => `${AGENT_LABEL[agent]}: ${profile} → ${name}`)
87
+ .join('\n'),
88
+ 'Profile change'
89
+ );
90
+ p.note(PROFILES[name].note, `${name} profile`);
91
+
92
+ const confirm = await p.confirm({
93
+ message:
94
+ 'This may add or remove skills under the new profile. Continue? (`agentic update` runs after.)',
95
+ initialValue: true,
96
+ });
97
+ if (p.isCancel(confirm) || !confirm) {
98
+ p.cancel('Cancelled.');
99
+ return;
100
+ }
101
+ }
102
+
103
+ // Write the new profile to each installed agent's state file before
104
+ // running update, so update reads the new profile.
105
+ for (const agent of Object.keys(perAgent)) {
106
+ const state = loadState(cwd, agent);
107
+ state.profile = name;
108
+ saveState(cwd, agent, state);
109
+ }
110
+
111
+ // Run the equivalent of `agentic update` so the install set matches the
112
+ // new profile. The update command picks profile up from the state files
113
+ // we just wrote.
114
+ await updateCommand({
115
+ agent: undefined,
116
+ yes: opts.yes,
117
+ dryRun: false,
118
+ force: false,
119
+ });
120
+
121
+ if (interactive) {
122
+ p.outro(`Profile set to \`${name}\`. Install state refreshed.`);
123
+ } else {
124
+ process.stdout.write(`Profile set to \`${name}\`. Install state refreshed.\n`);
125
+ }
126
+ }
127
+
128
+ export async function profileCommand(arg, opts) {
129
+ const cwd = process.cwd();
130
+
131
+ if (!arg) {
132
+ showProfile(cwd);
133
+ return;
134
+ }
135
+
136
+ if (arg === 'list') {
137
+ listProfiles();
138
+ return;
139
+ }
140
+
141
+ if (arg === 'show') {
142
+ showProfile(cwd);
143
+ return;
144
+ }
145
+
146
+ if (arg === 'set') {
147
+ if (!opts.name) {
148
+ throw new Error(
149
+ 'profile set requires a profile name. Usage: `agentic profile set <name>`'
150
+ );
151
+ }
152
+ await setProfile(cwd, opts.name, opts);
153
+ return;
154
+ }
155
+
156
+ // If `arg` is not a subcommand, treat it as `set <name>` shorthand.
157
+ if (PROFILE_NAMES.includes(arg)) {
158
+ await setProfile(cwd, arg, opts);
159
+ return;
160
+ }
161
+
162
+ throw new Error(
163
+ `unknown subcommand "${arg}". Use \`show\`, \`list\`, or \`set <name>\` (or pass a profile name directly).`
164
+ );
165
+ }
@@ -5,9 +5,27 @@ import { dirname, join } from 'node:path';
5
5
  import { detectAgents, detectFeatures } from '../lib/detect.js';
6
6
  import { installSkills, removeOrphanSkills } from '../lib/install.js';
7
7
  import { loadState, saveState } from '../lib/state.js';
8
+ import {
9
+ DEFAULT_PROFILE,
10
+ availableConditionalsForProfile,
11
+ profileOrDefault,
12
+ requiredSkillsForProfile,
13
+ } from '../lib/profiles.js';
8
14
  import { updateRootDoc } from '../lib/rootdoc.js';
9
15
  import { CONDITIONAL_SKILLS, REQUIRED_SKILLS } from './init.js';
10
16
 
17
+ const CONDITIONAL_BY_NAME = Object.fromEntries(
18
+ CONDITIONAL_SKILLS.map((s) => [s.name, s])
19
+ );
20
+
21
+ function evaluateRule(rule, features, targetAgents) {
22
+ if (rule === 'frontend') return features.frontend === true;
23
+ if (rule === 'claude-code') return targetAgents.includes('claude-code');
24
+ if (rule === true) return true;
25
+ if (rule === false) return false;
26
+ return false;
27
+ }
28
+
11
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
30
  const pkg = JSON.parse(
13
31
  readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')
@@ -48,18 +66,24 @@ function resolveAgents(flagValue, detectedAgents, previousAgents) {
48
66
  return ['claude-code'];
49
67
  }
50
68
 
51
- function pickConditionalAuto(features, targetAgents) {
52
- return CONDITIONAL_SKILLS.filter(
53
- (s) => s.autoIf(features) && s.agents.some((a) => targetAgents.includes(a))
54
- ).map((s) => s.name);
69
+ function pickConditionalAuto(features, targetAgents, profileName) {
70
+ const out = [];
71
+ for (const { name, rule } of availableConditionalsForProfile(profileName)) {
72
+ const def = CONDITIONAL_BY_NAME[name];
73
+ if (!def) continue;
74
+ if (!def.agents.some((a) => targetAgents.includes(a))) continue;
75
+ if (evaluateRule(rule, features, targetAgents)) out.push(name);
76
+ }
77
+ return out;
55
78
  }
56
79
 
57
- function skillsForAgent(agent, optedSkills) {
80
+ function skillsForAgent(agent, profileName, optedSkills) {
81
+ const universal = requiredSkillsForProfile(profileName);
58
82
  const conditional = optedSkills.filter((skillName) => {
59
- const def = CONDITIONAL_SKILLS.find((s) => s.name === skillName);
83
+ const def = CONDITIONAL_BY_NAME[skillName];
60
84
  return def && def.agents.includes(agent);
61
85
  });
62
- return [...REQUIRED_SKILLS, ...conditional];
86
+ return [...universal, ...conditional];
63
87
  }
64
88
 
65
89
  function previousAgentsFromStates(cwd) {
@@ -71,13 +95,19 @@ function previousAgentsFromStates(cwd) {
71
95
  return out;
72
96
  }
73
97
 
74
- function previouslyOptedConditional(previousStates, currentAgents) {
98
+ function previouslyOptedConditional(previousStates, currentAgents, profileName) {
99
+ const available = new Set(
100
+ availableConditionalsForProfile(profileName).map((c) => c.name)
101
+ );
75
102
  const opted = new Set();
76
103
  for (const agent of currentAgents) {
77
104
  const prev = previousStates[agent];
78
105
  if (!prev) continue;
79
106
  for (const skill of Object.keys(prev.skills ?? {})) {
80
- if (CONDITIONAL_SKILLS.some((s) => s.name === skill)) {
107
+ if (
108
+ CONDITIONAL_BY_NAME[skill] &&
109
+ available.has(skill)
110
+ ) {
81
111
  opted.add(skill);
82
112
  }
83
113
  }
@@ -85,6 +115,23 @@ function previouslyOptedConditional(previousStates, currentAgents) {
85
115
  return [...opted];
86
116
  }
87
117
 
118
+ function profileFromStates(previousStates, currentAgents) {
119
+ // If multiple agents disagree on profile, surface and bail. Profile is
120
+ // expected to match across agents in the same project.
121
+ const seen = new Set();
122
+ for (const agent of currentAgents) {
123
+ const prev = previousStates[agent];
124
+ if (prev?.profile) seen.add(prev.profile);
125
+ }
126
+ if (seen.size === 0) return DEFAULT_PROFILE;
127
+ if (seen.size > 1) {
128
+ throw new Error(
129
+ `state files disagree on profile (${[...seen].join(', ')}). Run \`agentic profile set <name>\` to reconcile.`
130
+ );
131
+ }
132
+ return [...seen][0];
133
+ }
134
+
88
135
  export async function updateCommand(opts) {
89
136
  if (opts.agent && !AGENT_FLAG_VALUES.includes(opts.agent)) {
90
137
  throw new Error(
@@ -107,8 +154,13 @@ export async function updateCommand(opts) {
107
154
  previousStates[agent] = loadState(cwd, agent);
108
155
  }
109
156
 
110
- const previousOpted = previouslyOptedConditional(previousStates, agents);
111
- const autoOpted = pickConditionalAuto(features, agents);
157
+ const profileName = profileFromStates(previousStates, agents);
158
+ const previousOpted = previouslyOptedConditional(
159
+ previousStates,
160
+ agents,
161
+ profileName
162
+ );
163
+ const autoOpted = pickConditionalAuto(features, agents, profileName);
112
164
  const defaultOpted = previousOpted.length ? previousOpted : autoOpted;
113
165
 
114
166
  let optedSkills;
@@ -120,17 +172,24 @@ export async function updateCommand(opts) {
120
172
  p.note(
121
173
  `Previous install: ${previousLine}\n` +
122
174
  `Updating for: ${agents.map((a) => AGENT_LABEL[a]).join(' + ')}\n` +
175
+ `Profile: ${profileName}\n` +
123
176
  `Kit version: ${pkg.version}`,
124
177
  'Update plan'
125
178
  );
126
179
 
127
- const conditionalOptions = CONDITIONAL_SKILLS.filter((s) =>
128
- s.agents.some((a) => agents.includes(a))
129
- ).map((s) => ({
130
- value: s.name,
131
- label: s.name,
132
- hint: s.autoIf(features) ? s.hintWhenAuto : s.hintWhenManual,
133
- }));
180
+ const conditionalOptions = availableConditionalsForProfile(profileName)
181
+ .map(({ name, rule }) => {
182
+ const def = CONDITIONAL_BY_NAME[name];
183
+ if (!def) return null;
184
+ if (!def.agents.some((a) => agents.includes(a))) return null;
185
+ const auto = evaluateRule(rule, features, agents);
186
+ return {
187
+ value: name,
188
+ label: name,
189
+ hint: auto ? def.hintWhenAuto : def.hintWhenManual,
190
+ };
191
+ })
192
+ .filter(Boolean);
134
193
 
135
194
  if (conditionalOptions.length > 0) {
136
195
  const picked = await p.multiselect({
@@ -172,7 +231,7 @@ export async function updateCommand(opts) {
172
231
  const nextStates = {};
173
232
 
174
233
  for (const agent of agents) {
175
- const agentSkills = skillsForAgent(agent, optedSkills);
234
+ const agentSkills = skillsForAgent(agent, profileName, optedSkills);
176
235
  for (const s of agentSkills) installedSkillSet.add(s);
177
236
 
178
237
  const orphanResult = await removeOrphanSkills({
@@ -196,7 +255,9 @@ export async function updateCommand(opts) {
196
255
  force,
197
256
  });
198
257
  allActions.push(...result.actions);
199
- nextStates[agent] = result.nextStates[agent];
258
+ const next = result.nextStates[agent];
259
+ next.profile = profileName;
260
+ nextStates[agent] = next;
200
261
  }
201
262
 
202
263
  if (!dryRun) {
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { initCommand } from './commands/init.js';
6
6
  import { updateCommand } from './commands/update.js';
7
+ import { profileCommand } from './commands/profile.js';
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
  const pkg = JSON.parse(
@@ -22,6 +23,7 @@ export async function run(argv) {
22
23
  .command('init')
23
24
  .description('Install agentic skills into this project for Claude Code and/or Codex')
24
25
  .option('-a, --agent <agent>', 'install for a specific agent: claude-code | codex | both')
26
+ .option('-p, --profile <profile>', 'project maturity profile: poc | solo | team | mature (default: team)')
25
27
  .option('-y, --yes', 'skip confirmation prompts (non-interactive)')
26
28
  .action(initCommand);
27
29
 
@@ -34,5 +36,12 @@ export async function run(argv) {
34
36
  .option('--force', 'overwrite user-edited files on conflict (non-interactive default: no)')
35
37
  .action(updateCommand);
36
38
 
39
+ program
40
+ .command('profile [subcommand]')
41
+ .description('Show, list, or set the project maturity profile (poc | solo | team | mature)')
42
+ .option('-n, --name <name>', 'profile name when used with `set` subcommand')
43
+ .option('-y, --yes', 'skip confirmation prompts (non-interactive)')
44
+ .action(profileCommand);
45
+
37
46
  await program.parseAsync(argv);
38
47
  }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Project maturity profiles per ADR-0013.
3
+ *
4
+ * Each profile declares:
5
+ * - `universal`: skills that always install for this profile.
6
+ * - `conditional`: per-conditional-skill rules. `autoIf` is one of:
7
+ * - a feature predicate name from detectFeatures (`'frontend'`, `'claude-code'`)
8
+ * - `true` — recommended (pre-checked in TUI; auto-installed in non-interactive)
9
+ * - `false` — allowed but not recommended (un-checked in TUI; not auto-installed)
10
+ * - `'blocked'` — not available at this profile (omitted from TUI options)
11
+ * - `note`: human-readable summary shown in the profile-selection TUI and
12
+ * in `agentic profile list`.
13
+ *
14
+ * Profiles are monotone supersets: poc ⊆ solo ⊆ team ⊆ mature.
15
+ */
16
+
17
+ export const DEFAULT_PROFILE = 'team';
18
+
19
+ export const PROFILE_NAMES = ['poc', 'solo', 'team', 'mature'];
20
+
21
+ export const PROFILES = {
22
+ poc: {
23
+ universal: ['agentic-philosophy', 'agentic-ground', 'agentic-audit'],
24
+ conditional: {
25
+ 'agentic-design': 'blocked',
26
+ 'agentic-subagent': 'blocked',
27
+ 'agentic-skill': 'blocked',
28
+ 'agentic-hooks': 'blocked',
29
+ },
30
+ note: 'PoC / spike / experiment. Posture (philosophy) + research (ground) + drift (audit). No mandatory artifact-producing skills. Adds discipline you can grow into; never pre-imposes ceremony.',
31
+ },
32
+ solo: {
33
+ universal: [
34
+ 'agentic-philosophy',
35
+ 'agentic-ground',
36
+ 'agentic-audit',
37
+ 'agentic-bootstrap',
38
+ 'agentic-spec',
39
+ 'agentic-task',
40
+ 'agentic-review',
41
+ ],
42
+ conditional: {
43
+ 'agentic-architecture': false,
44
+ 'agentic-adr': false,
45
+ 'agentic-design': 'frontend',
46
+ 'agentic-subagent': 'claude-code',
47
+ 'agentic-skill': false,
48
+ 'agentic-hooks': false,
49
+ },
50
+ note: 'Solo developer shipping a real product. Specs and tasks are universal; ADRs and architecture are opt-in for binding decisions only.',
51
+ },
52
+ team: {
53
+ universal: [
54
+ 'agentic-bootstrap',
55
+ 'agentic-philosophy',
56
+ 'agentic-architecture',
57
+ 'agentic-adr',
58
+ 'agentic-spec',
59
+ 'agentic-task',
60
+ 'agentic-audit',
61
+ 'agentic-review',
62
+ 'agentic-ground',
63
+ ],
64
+ conditional: {
65
+ 'agentic-design': 'frontend',
66
+ 'agentic-subagent': 'claude-code',
67
+ 'agentic-skill': false,
68
+ 'agentic-hooks': false,
69
+ },
70
+ note: 'Team product. Full universal stack; conditional skills auto-detect by signal. This was the v0.7 default and is the migration target for existing installs.',
71
+ },
72
+ mature: {
73
+ universal: [
74
+ 'agentic-bootstrap',
75
+ 'agentic-philosophy',
76
+ 'agentic-architecture',
77
+ 'agentic-adr',
78
+ 'agentic-spec',
79
+ 'agentic-task',
80
+ 'agentic-audit',
81
+ 'agentic-review',
82
+ 'agentic-ground',
83
+ ],
84
+ conditional: {
85
+ 'agentic-design': 'frontend',
86
+ 'agentic-subagent': 'claude-code',
87
+ 'agentic-skill': false,
88
+ 'agentic-hooks': true,
89
+ },
90
+ note: 'Mature / regulated product. Recommends agentic-hooks alongside the team stack for deterministic gates per WORKFLOW §11. Future evals + spike skills land here when shipped.',
91
+ },
92
+ };
93
+
94
+ export function validateProfile(name) {
95
+ if (!PROFILE_NAMES.includes(name)) {
96
+ throw new Error(
97
+ `unknown profile "${name}". Valid: ${PROFILE_NAMES.join(', ')}`
98
+ );
99
+ }
100
+ return name;
101
+ }
102
+
103
+ export function profileOrDefault(name) {
104
+ if (!name) return DEFAULT_PROFILE;
105
+ return validateProfile(name);
106
+ }
107
+
108
+ export function requiredSkillsForProfile(name) {
109
+ return [...PROFILES[validateProfile(name)].universal];
110
+ }
111
+
112
+ export function conditionalRulesForProfile(name) {
113
+ return { ...PROFILES[validateProfile(name)].conditional };
114
+ }
115
+
116
+ /**
117
+ * Returns the conditional skills available at this profile, with their
118
+ * effective `autoIf` posture. Skipped (blocked) skills are omitted.
119
+ */
120
+ export function availableConditionalsForProfile(profileName) {
121
+ const rules = conditionalRulesForProfile(profileName);
122
+ const out = [];
123
+ for (const [skill, rule] of Object.entries(rules)) {
124
+ if (rule === 'blocked') continue;
125
+ out.push({ name: skill, rule });
126
+ }
127
+ return out;
128
+ }
@@ -29,6 +29,8 @@ export const SKILL_DESCRIPTIONS = {
29
29
  'agentic-design': 'Bootstrap `DESIGN.md` from existing tokens (frontend projects).',
30
30
  'agentic-subagent': 'Draft a new Claude Code subagent at `.claude/agents/<name>.md`.',
31
31
  'agentic-skill': 'Draft a new Claude Code or Codex skill at the appropriate path.',
32
+ 'agentic-hooks':
33
+ 'Scaffold deterministic quality gates per WORKFLOW §11 — pre-commit + pre-push, runner detected from stack signals.',
32
34
  };
33
35
 
34
36
  /**
package/src/lib/state.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
+ import { DEFAULT_PROFILE, validateProfile } from './profiles.js';
3
4
 
4
5
  export const SCHEMA_VERSION = 1;
5
6
  export const STATE_FILE = 'agentic-state.json';
@@ -15,11 +16,12 @@ export function statePath(cwd, agent) {
15
16
  return join(cwd, dir, STATE_FILE);
16
17
  }
17
18
 
18
- export function emptyState(agent, kitVersion) {
19
+ export function emptyState(agent, kitVersion, profile = DEFAULT_PROFILE) {
19
20
  return {
20
21
  schemaVersion: SCHEMA_VERSION,
21
22
  kitVersion,
22
23
  agent,
24
+ profile: validateProfile(profile),
23
25
  skills: {},
24
26
  };
25
27
  }
@@ -46,10 +48,23 @@ export function loadState(cwd, agent) {
46
48
  `state at ${path} declares agent "${raw.agent}" but expected "${agent}"`
47
49
  );
48
50
  }
51
+ // `profile` is optional and forward-compatible. Pre-v0.8 state files have
52
+ // no profile field; default to `team` per ADR-0013 (the v0.7 install set is
53
+ // byte-identical to the team profile).
54
+ let profile = DEFAULT_PROFILE;
55
+ if (raw.profile) {
56
+ try {
57
+ profile = validateProfile(raw.profile);
58
+ } catch (err) {
59
+ throw new Error(`state at ${path}: ${err.message}`);
60
+ }
61
+ }
62
+
49
63
  return {
50
64
  schemaVersion: raw.schemaVersion,
51
65
  kitVersion: raw.kitVersion ?? null,
52
66
  agent,
67
+ profile,
53
68
  skills: raw.skills ?? {},
54
69
  };
55
70
  }
@@ -75,6 +90,7 @@ function orderState(state) {
75
90
  schemaVersion: state.schemaVersion,
76
91
  kitVersion: state.kitVersion,
77
92
  agent: state.agent,
93
+ profile: state.profile ?? DEFAULT_PROFILE,
78
94
  skills,
79
95
  };
80
96
  }
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: agentic-hooks
3
+ description: Scaffold deterministic quality gates per WORKFLOW.md §11 — pre-commit (lint, format, secret-scan), pre-push (build, unit, integration). Detects the project's stack and recommends a hook runner (Husky / lefthook / pre-commit / native), scaffolds the runner config, and updates AGENTS.md Quality Gates. Use when the user wants to wire hooks, configure pre-commit / pre-push, set up quality gates, prevent --no-verify bypass, or close the WORKFLOW §11 advisory-vs-deterministic gap. Opt-in skill; not auto-installed in the universal set.
4
+ allowed-tools: Read, Write, Glob, Bash
5
+ ---
6
+
7
+ # /agentic-hooks
8
+
9
+ Scaffolds the deterministic gates `WORKFLOW.md` §11 names. The skill writes config files for a hook runner and updates `AGENTS.md` Quality Gates; it does not execute install scripts. The user is responsible for the runner's one-time bootstrap (e.g., `npx husky init`, `lefthook install`, `pre-commit install`) — the skill says exactly which command to run.
10
+
11
+ ## Step 0 — Confirm the gates the user wants
12
+
13
+ `WORKFLOW.md` §11 names two tiers:
14
+
15
+ * **Pre-commit (fast):** lint, format, secret-scan. Runs on every commit. Slow pre-commits push devs to `--no-verify`; keep it under ~5s.
16
+ * **Pre-push (thorough):** build, unit tests, integration tests. Runs on every push. Acceptable to be slow; the cost is paid less often than commit.
17
+
18
+ Confirm both tiers are in scope for this project. If the user wants only one tier, scaffold only that tier — do not add gates the user did not ask for.
19
+
20
+ Visual / E2E for UI projects (Cypress, Playwright, Claude in Chrome) are mentioned by §11 but live in CI, not pre-push. Out of scope for this skill.
21
+
22
+ ## Step 1 — Detect the runner
23
+
24
+ Read the repo signals in this order:
25
+
26
+ 1. **Existing runner.** `.husky/` → Husky present. `lefthook.yml` or `.lefthook.yml` → lefthook present. `.pre-commit-config.yaml` → pre-commit present. `.git/hooks/` with non-sample scripts → native hooks present.
27
+ 2. **Stack signals (if no runner present).** `package.json` → Node-rooted; recommend Husky (most common in Node ecosystem) or lefthook (cross-language fit). `pyproject.toml` → Python-rooted; recommend pre-commit. `go.mod` → Go-rooted; recommend lefthook. `Cargo.toml` → Rust-rooted; recommend lefthook. Multiple stacks → recommend lefthook (cross-language by default).
28
+ 3. **No signals.** Recommend native `.git/hooks/` only as fallback. Warn the user that native hooks are not portable across clones (every contributor has to run a setup script).
29
+
30
+ If multiple runners are present, surface the conflict and ask the user before scaffolding. Never silently pick.
31
+
32
+ ## Step 2 — Recommend the per-stack commands
33
+
34
+ For the chosen runner, propose the per-tier command set:
35
+
36
+ * **Node (`package.json` present):** lint = `npm run lint` (fall back to `npx eslint .` if no `lint` script); format check = `npm run format:check` (fall back to `npx prettier --check .`); secret-scan = `gitleaks detect --no-banner` (cite the install instruction); build = `npm run build` (skip if no script); test = `npm test`.
37
+ * **Python (`pyproject.toml` present):** lint = `ruff check .`; format check = `ruff format --check .` or `black --check .`; secret-scan = `gitleaks detect --no-banner`; test = `pytest -q`.
38
+ * **Go (`go.mod` present):** lint = `golangci-lint run`; format check = `gofmt -d .`; secret-scan = `gitleaks detect --no-banner`; build = `go build ./...`; test = `go test ./...`.
39
+ * **Rust (`Cargo.toml` present):** lint = `cargo clippy -- -D warnings`; format check = `cargo fmt --check`; secret-scan = `gitleaks detect --no-banner`; build = `cargo build`; test = `cargo test`.
40
+ * **Mixed / other:** ask the user for the per-tier command list. Do not invent.
41
+
42
+ Offer to swap any default. Confirm before writing.
43
+
44
+ ## Step 3 — Scaffold the runner config
45
+
46
+ Write the runner-specific config file. Below are the canonical shapes; adapt to the user's tier choices.
47
+
48
+ **Husky** — `.husky/pre-commit` and `.husky/pre-push` (shell scripts). Plus a `prepare` script in `package.json` (`"prepare": "husky"`). User runs `npm install` once to bootstrap.
49
+
50
+ **lefthook** — `lefthook.yml` at the repo root with `pre-commit` and `pre-push` commands keyed by stage. User runs `lefthook install` once.
51
+
52
+ **pre-commit (pre-commit.com)** — `.pre-commit-config.yaml` referencing the canonical hooks for the stack (e.g., `pre-commit/mirrors-eslint`, `psf/black`, `astral-sh/ruff-pre-commit`). User runs `pre-commit install` and `pre-commit install --hook-type pre-push`.
53
+
54
+ **Native `.git/hooks/`** — `.git/hooks/pre-commit` and `.git/hooks/pre-push` plus a `setup-hooks.sh` script the user runs after every clone (since `.git/` is not committed). Document the setup-script invocation in `AGENTS.md`.
55
+
56
+ ## Step 4 — Update `AGENTS.md` Quality Gates section
57
+
58
+ Append (or refresh, if a Quality Gates section already exists) the following content:
59
+
60
+ ```
61
+ ## Quality Gates
62
+
63
+ Deterministic enforcement — agent cannot skip.
64
+
65
+ - Pre-commit hook (fast): <stack-specific lint, format, secret-scan commands>
66
+ - Pre-push hook (thorough): <stack-specific build, unit, integration commands>
67
+ - Hook runner: <Husky | lefthook | pre-commit | native>; config at <path>
68
+ - Bootstrap: <one-line setup command>
69
+ - CI blocks on: <list — skip if CI not yet wired>
70
+ - Never bypass: no `--no-verify`, no skipped hooks, no deleted failing tests. WORKFLOW §11 is binding.
71
+ ```
72
+
73
+ Honor the existing managed-skills / managed-quality-gates markers if `agentic-bootstrap` already wrote a Quality Gates section. The skill refreshes the section in place; user content outside the markers is preserved.
74
+
75
+ ## Step 5 — Tell the user what to run
76
+
77
+ After writing the config, output exactly the bootstrap command the user must run (e.g., `npm install` for Husky, `lefthook install` for lefthook, `pre-commit install` for pre-commit). The skill does not execute the bootstrap — that is the user's call.
78
+
79
+ If the user is wiring CI alongside hooks (GitHub Actions / GitLab CI / Circle), point them at the existing `.github/workflows/`, `.gitlab-ci.yml`, or `.circleci/` directory. CI scaffolding is a separate skill's responsibility (deferred — not this one).
80
+
81
+ ## Output contract
82
+
83
+ Filesystem changes:
84
+
85
+ - The runner's config file (e.g., `.husky/pre-commit`, `lefthook.yml`, `.pre-commit-config.yaml`).
86
+ - An updated `AGENTS.md` Quality Gates section (or appended if absent), naming the runner, the gates wired, the bootstrap command, and the no-bypass policy.
87
+ - For the native-hooks fallback only: a `setup-hooks.sh` script the user runs after every clone.
88
+
89
+ The skill does not execute the runner's install command. The skill does not write CI config. The skill does not configure agent-side hooks (`.claude/settings.json` `Stop` / `PreToolUse` / `PostToolUse`) — that is a different surface; future ADR may cover it.
90
+
91
+ A narrative document, so the documentation discipline rules apply at write time:
92
+
93
+ - No emoji anywhere in the scaffolded config or in the AGENTS.md update.
94
+ - No version stamps or DRAFT markers.
95
+ - The Quality Gates section opens with the operational rule (gates are deterministic) before listing the gates themselves.
96
+ - One scope: Quality Gates. Do not duplicate ARCHITECTURE.md or ADR rationale here.
97
+ - No commented-out scripts. No orphan TODO / FIXME — every deferred command references a tracked task or GitHub Issue.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: agentic-hooks
3
+ description: Scaffold deterministic quality gates per WORKFLOW.md §11 — pre-commit (lint, format, secret-scan), pre-push (build, unit, integration). Detects the project's stack and recommends a hook runner (Husky / lefthook / pre-commit / native), scaffolds the runner config, and updates AGENTS.md Quality Gates. Use when the user wants to wire hooks, configure pre-commit / pre-push, set up quality gates, prevent --no-verify bypass, or close the WORKFLOW §11 advisory-vs-deterministic gap. Opt-in skill; not auto-installed.
4
+ ---
5
+
6
+ <background_information>
7
+ Scaffolds the deterministic gates `WORKFLOW.md` §11 names. The skill writes config files for a hook runner and updates `AGENTS.md` Quality Gates; it does not execute install scripts. The user runs the runner's one-time bootstrap (`npx husky init`, `lefthook install`, `pre-commit install`) — the skill names the exact command.
8
+
9
+ Codex auto-trigger on description keywords is less mature than Claude Code's. If auto-invocation does not fire when the user asks about hooks, pre-commit, or quality gates, invoke the skill manually.
10
+ </background_information>
11
+
12
+ <instructions>
13
+ Step 0 — confirm the gates the user wants. WORKFLOW §11 names two tiers:
14
+ - Pre-commit (fast): lint, format, secret-scan. Runs on every commit. Keep under ~5s; slow pre-commits push devs to `--no-verify`.
15
+ - Pre-push (thorough): build, unit tests, integration tests. Runs on every push. Acceptable to be slow.
16
+
17
+ Confirm both tiers are in scope. If the user wants only one, scaffold only that tier.
18
+
19
+ Visual / E2E for UI projects (Cypress, Playwright) live in CI, not pre-push. Out of scope.
20
+
21
+ Step 1 — detect the runner. Read repo signals in this order:
22
+ 1. Existing runner. `.husky/` → Husky. `lefthook.yml` or `.lefthook.yml` → lefthook. `.pre-commit-config.yaml` → pre-commit. `.git/hooks/` with non-sample scripts → native hooks.
23
+ 2. Stack signals (if no runner present). `package.json` → recommend Husky or lefthook. `pyproject.toml` → recommend pre-commit. `go.mod` → recommend lefthook. `Cargo.toml` → recommend lefthook. Multiple stacks → recommend lefthook (cross-language by default).
24
+ 3. No signals. Recommend native `.git/hooks/` only as fallback. Warn the user that native hooks are not portable across clones.
25
+
26
+ If multiple runners are present, surface the conflict and ask the user before scaffolding. Never silently pick.
27
+
28
+ Step 2 — recommend the per-stack commands:
29
+ - Node: lint = `npm run lint` or `npx eslint .`; format check = `npm run format:check` or `npx prettier --check .`; secret-scan = `gitleaks detect --no-banner`; build = `npm run build` (skip if no script); test = `npm test`.
30
+ - Python: lint = `ruff check .`; format check = `ruff format --check .` or `black --check .`; secret-scan = `gitleaks detect --no-banner`; test = `pytest -q`.
31
+ - Go: lint = `golangci-lint run`; format check = `gofmt -d .`; secret-scan = `gitleaks detect --no-banner`; build = `go build ./...`; test = `go test ./...`.
32
+ - Rust: lint = `cargo clippy -- -D warnings`; format check = `cargo fmt --check`; secret-scan = `gitleaks detect --no-banner`; build = `cargo build`; test = `cargo test`.
33
+ - Mixed / other: ask for the per-tier command list. Do not invent.
34
+
35
+ Offer to swap any default. Confirm before writing.
36
+
37
+ Step 3 — scaffold the runner config. Write the runner-specific config file:
38
+ - Husky: `.husky/pre-commit` and `.husky/pre-push` (shell scripts) + `"prepare": "husky"` in `package.json`. Bootstrap: `npm install`.
39
+ - lefthook: `lefthook.yml` at the repo root with `pre-commit` and `pre-push` commands keyed by stage. Bootstrap: `lefthook install`.
40
+ - pre-commit: `.pre-commit-config.yaml` with stack-specific hook references. Bootstrap: `pre-commit install` and `pre-commit install --hook-type pre-push`.
41
+ - Native: `.git/hooks/pre-commit` and `.git/hooks/pre-push` + a `setup-hooks.sh` script the user runs after every clone (since `.git/` is not committed).
42
+
43
+ Step 4 — update `AGENTS.md` Quality Gates. Append or refresh the section with: pre-commit gate list, pre-push gate list, runner name + config path, bootstrap command, CI status if known, no-bypass policy. Honor existing managed markers if `agentic-bootstrap` already wrote Quality Gates.
44
+
45
+ Step 5 — tell the user the bootstrap command. Output the exact one-line command the user runs. The skill does not execute it.
46
+ </instructions>
47
+
48
+ <output_contract>
49
+ Filesystem changes:
50
+ - The runner's config file (`.husky/pre-commit`, `lefthook.yml`, `.pre-commit-config.yaml`, or `.git/hooks/`).
51
+ - An updated `AGENTS.md` Quality Gates section.
52
+ - For native-hooks fallback: a `setup-hooks.sh` script.
53
+
54
+ The skill does not execute the runner's install command. The skill does not write CI config. The skill does not configure agent-side hooks (`.claude/settings.json`) — different surface, deferred.
55
+
56
+ Documentation discipline rules apply at write time:
57
+ - No emoji anywhere in scaffolded config or AGENTS.md update.
58
+ - No version stamps or DRAFT markers.
59
+ - Quality Gates section opens with the operational rule (gates are deterministic) before listing the gates.
60
+ - One scope: Quality Gates. No duplication of ARCHITECTURE.md or ADR rationale.
61
+ - No commented-out scripts. No orphan TODO / FIXME.
62
+ </output_contract>
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: agentic-hooks
3
+ short_description: Scaffold deterministic quality gates per WORKFLOW §11 (pre-commit / pre-push). Detect-then-recommend Husky / lefthook / pre-commit / native; never silently pick.
4
+ policy:
5
+ allow_implicit_invocation: false