@alexandrealvaro/agentic 0.7.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
@@ -46,6 +46,35 @@ A short TUI shows the detected mode, agent, and feature signals (frontend / `.cl
46
46
 
47
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.
48
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
+
49
78
  ## Updating an existing project
50
79
 
51
80
  To pull upstream kit changes into a project that already has agentic skills installed:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexandrealvaro/agentic",
3
- "version": "0.7.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
  {
@@ -92,6 +95,10 @@ export const CONDITIONAL_SKILLS = [
92
95
  },
93
96
  ];
94
97
 
98
+ const CONDITIONAL_BY_NAME = Object.fromEntries(
99
+ CONDITIONAL_SKILLS.map((s) => [s.name, s])
100
+ );
101
+
95
102
  function resolveAgents(flagValue, detectedAgents) {
96
103
  if (flagValue === 'both') return ['claude-code', 'codex'];
97
104
  if (flagValue) return [flagValue];
@@ -99,18 +106,39 @@ function resolveAgents(flagValue, detectedAgents) {
99
106
  return ['claude-code'];
100
107
  }
101
108
 
102
- function pickConditionalAuto(features, targetAgents) {
103
- return CONDITIONAL_SKILLS.filter(
104
- (s) => s.autoIf(features) && s.agents.some((a) => targetAgents.includes(a))
105
- ).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;
106
133
  }
107
134
 
108
- function skillsForAgent(agent, optedSkills) {
135
+ function skillsForAgent(agent, profileName, optedSkills) {
136
+ const universal = requiredSkillsForProfile(profileName);
109
137
  const conditional = optedSkills.filter((skillName) => {
110
- const def = CONDITIONAL_SKILLS.find((s) => s.name === skillName);
138
+ const def = CONDITIONAL_BY_NAME[skillName];
111
139
  return def && def.agents.includes(agent);
112
140
  });
113
- return [...REQUIRED_SKILLS, ...conditional];
141
+ return [...universal, ...conditional];
114
142
  }
115
143
 
116
144
  export async function initCommand(opts) {
@@ -120,6 +148,12 @@ export async function initCommand(opts) {
120
148
  );
121
149
  }
122
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
+
123
157
  const cwd = process.cwd();
124
158
  const interactive = process.stdout.isTTY && !opts.yes && !opts.agent;
125
159
 
@@ -127,18 +161,20 @@ export async function initCommand(opts) {
127
161
  const detectedAgents = detectAgents(cwd);
128
162
  const features = detectFeatures(cwd);
129
163
 
164
+ let profileName = profileOrDefault(opts.profile);
130
165
  let agents;
131
166
  let optedSkills;
132
167
 
133
168
  if (interactive) {
134
169
  p.intro('agentic init');
135
- const featureLine = [
136
- features.frontend ? 'frontend' : null,
137
- features.hasClaudeCode ? '.claude/ present' : null,
138
- features.hasCodex ? '.agents/.openai/ present' : null,
139
- ]
140
- .filter(Boolean)
141
- .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';
142
178
 
143
179
  p.note(
144
180
  `Mode: ${MODE_LABEL[detectedMode]}\n` +
@@ -151,6 +187,21 @@ export async function initCommand(opts) {
151
187
  'Detected context'
152
188
  );
153
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
+
154
205
  const choice = await p.select({
155
206
  message: 'Install skills for which agent(s)?',
156
207
  options: [
@@ -169,14 +220,20 @@ export async function initCommand(opts) {
169
220
  }
170
221
  agents = choice;
171
222
 
172
- const conditionalOptions = CONDITIONAL_SKILLS.filter((s) =>
173
- s.agents.some((a) => agents.includes(a))
174
- ).map((s) => ({
175
- value: s.name,
176
- label: s.name,
177
- hint: s.autoIf(features) ? s.hintWhenAuto : s.hintWhenManual,
178
- }));
179
- 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);
180
237
 
181
238
  if (conditionalOptions.length > 0) {
182
239
  const picked = await p.multiselect({
@@ -194,14 +251,17 @@ export async function initCommand(opts) {
194
251
  optedSkills = [];
195
252
  }
196
253
 
197
- const totalCount = REQUIRED_SKILLS.length + optedSkills.length;
254
+ const universalForProfile = requiredSkillsForProfile(profileName);
255
+ const totalCount = universalForProfile.length + optedSkills.length;
198
256
  const optedSummary = optedSkills.length
199
257
  ? `, plus ${optedSkills.join(', ')}`
200
258
  : '';
201
259
  const confirm = await p.confirm({
202
- message: `Install ${totalCount} skill${totalCount === 1 ? '' : 's'} (${REQUIRED_SKILLS.join(
203
- ', '
204
- )}${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(' + ')}?`,
205
265
  initialValue: true,
206
266
  });
207
267
  if (p.isCancel(confirm) || !confirm) {
@@ -210,7 +270,7 @@ export async function initCommand(opts) {
210
270
  }
211
271
  } else {
212
272
  agents = resolveAgents(opts.agent, detectedAgents);
213
- optedSkills = pickConditionalAuto(features, agents);
273
+ optedSkills = pickConditionalAuto(features, agents, profileName);
214
274
  }
215
275
 
216
276
  const confirmReplace = interactive
@@ -224,7 +284,7 @@ export async function initCommand(opts) {
224
284
  const allActions = [];
225
285
  const installedSkillSet = new Set();
226
286
  for (const agent of agents) {
227
- const agentSkills = skillsForAgent(agent, optedSkills);
287
+ const agentSkills = skillsForAgent(agent, profileName, optedSkills);
228
288
  for (const s of agentSkills) installedSkillSet.add(s);
229
289
  const previousStates = { [agent]: loadState(cwd, agent) };
230
290
  const { actions, nextStates } = await installSkills({
@@ -236,7 +296,9 @@ export async function initCommand(opts) {
236
296
  kitVersion: pkg.version,
237
297
  });
238
298
  allActions.push(...actions);
239
- saveState(cwd, agent, nextStates[agent]);
299
+ const next = nextStates[agent];
300
+ next.profile = profileName;
301
+ saveState(cwd, agent, next);
240
302
  }
241
303
 
242
304
  const skillDisplayOrder = [
@@ -283,9 +345,30 @@ export async function initCommand(opts) {
283
345
  : []),
284
346
  ...(optedSkills.includes('agentic-skill') ? ['/agentic-skill'] : []),
285
347
  ...(optedSkills.includes('agentic-hooks') ? ['/agentic-hooks (WORKFLOW §11)'] : []),
286
- ].join(', ');
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(', ');
287
370
  p.outro(
288
- `Done. In ${agents
371
+ `Done (profile: ${profileName}). In ${agents
289
372
  .map((a) => AGENT_LABEL[a])
290
373
  .join(' or ')}: ${slashLine}. agentic-philosophy auto-loads on non-trivial work.`
291
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
+ }
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
  }