@alexandrealvaro/agentic 0.11.2-beta.1 → 0.11.3-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexandrealvaro/agentic",
3
- "version": "0.11.2-beta.1",
3
+ "version": "0.11.3-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": {
@@ -94,6 +94,28 @@ export const CONDITIONAL_SKILLS = [
94
94
  hintWhenAuto: 'opt-in',
95
95
  hintWhenManual: 'WORKFLOW §11 hooks scaffolder (pre-commit, pre-push)',
96
96
  },
97
+ // The next two skills are universal in `team` / `mature` profiles
98
+ // (declared in PROFILES['team' / 'mature'].universal in src/lib/profiles.js)
99
+ // and conditional/opt-in in `solo`. They must appear in this catalog so
100
+ // `availableConditionalsForProfile('solo')` lookups in `pickConditionalAuto`
101
+ // succeed — without these entries, `if (!def) continue` silently skipped
102
+ // them and a `solo` user could not opt-in to either (review B1, v0.11.3).
103
+ // The autoIf rule here is the universal-default; per-profile overrides
104
+ // come from `availableConditionalsForProfile`'s rule field.
105
+ {
106
+ name: 'agentic-architecture',
107
+ autoIf: () => true,
108
+ agents: ['claude-code', 'codex'],
109
+ hintWhenAuto: 'system patterns + boundaries',
110
+ hintWhenManual: 'opt-in (recommended once load-bearing patterns emerge)',
111
+ },
112
+ {
113
+ name: 'agentic-adr',
114
+ autoIf: () => true,
115
+ agents: ['claude-code', 'codex'],
116
+ hintWhenAuto: 'binding architectural decisions (Nygard pattern)',
117
+ hintWhenManual: 'opt-in (recommended for binding decisions worth recording)',
118
+ },
97
119
  ];
98
120
 
99
121
  const CONDITIONAL_BY_NAME = Object.fromEntries(
@@ -295,16 +317,23 @@ export async function initCommand(opts) {
295
317
  confirmReplace,
296
318
  previousStates,
297
319
  kitVersion: pkg.version,
320
+ profile: profileName,
298
321
  });
299
322
  allActions.push(...actions);
300
- const next = nextStates[agent];
301
- next.profile = profileName;
302
- saveState(cwd, agent, next);
323
+ // installSkills now stamps `profile` into nextStates per review C3.
324
+ // No post-hoc injection.
325
+ saveState(cwd, agent, nextStates[agent]);
303
326
  }
304
327
 
328
+ // Dedup: agentic-architecture and agentic-adr are universal at team /
329
+ // mature (in REQUIRED_SKILLS) AND conditional at solo (in
330
+ // CONDITIONAL_SKILLS) per review B1 (v0.11.3). Without the Set, the
331
+ // managed-skills section would list those rows twice.
305
332
  const skillDisplayOrder = [
306
- ...REQUIRED_SKILLS,
307
- ...CONDITIONAL_SKILLS.map((s) => s.name),
333
+ ...new Set([
334
+ ...REQUIRED_SKILLS,
335
+ ...CONDITIONAL_SKILLS.map((s) => s.name),
336
+ ]),
308
337
  ].filter((s) => installedSkillSet.has(s));
309
338
 
310
339
  const confirmAppend = interactive
@@ -17,6 +17,10 @@ const AGENT_LABEL = {
17
17
  };
18
18
 
19
19
  function readProjectProfile(cwd) {
20
+ // Returns { agent: profileName }. Use `loadProjectStates(cwd)` instead
21
+ // when you need both the profile and the full state objects in a single
22
+ // pass (avoids the TOCTOU window where state files could be deleted
23
+ // between two reads — review C2, v0.11.3).
20
24
  const perAgent = {};
21
25
  for (const agent of VALID_AGENTS) {
22
26
  const state = loadState(cwd, agent);
@@ -25,6 +29,23 @@ function readProjectProfile(cwd) {
25
29
  return perAgent;
26
30
  }
27
31
 
32
+ function loadProjectStates(cwd) {
33
+ // Single-pass load of every per-agent state file. Returns
34
+ // { statesByAgent, profilesByAgent }. Both objects share the same agent
35
+ // keys so callers can iterate one and look up the other without a
36
+ // second filesystem read.
37
+ const statesByAgent = {};
38
+ const profilesByAgent = {};
39
+ for (const agent of VALID_AGENTS) {
40
+ const state = loadState(cwd, agent);
41
+ if (state) {
42
+ statesByAgent[agent] = state;
43
+ profilesByAgent[agent] = state.profile ?? DEFAULT_PROFILE;
44
+ }
45
+ }
46
+ return { statesByAgent, profilesByAgent };
47
+ }
48
+
28
49
  function showProfile(cwd) {
29
50
  const perAgent = readProjectProfile(cwd);
30
51
  if (Object.keys(perAgent).length === 0) {
@@ -64,8 +85,12 @@ function formatRule(rule) {
64
85
  async function setProfile(cwd, name, opts) {
65
86
  validateProfile(name);
66
87
 
67
- const perAgent = readProjectProfile(cwd);
68
- if (Object.keys(perAgent).length === 0) {
88
+ // Single load — reuse below to write. Avoids the TOCTOU window where
89
+ // state files could be deleted between read and re-read (review C2,
90
+ // v0.11.3). Previous implementation called readProjectProfile then
91
+ // loadState again per agent in the write loop.
92
+ const { statesByAgent, profilesByAgent } = loadProjectStates(cwd);
93
+ if (Object.keys(statesByAgent).length === 0) {
69
94
  throw new Error(
70
95
  'no agentic install detected. Run `agentic init --profile <name>` first.'
71
96
  );
@@ -73,7 +98,7 @@ async function setProfile(cwd, name, opts) {
73
98
 
74
99
  const interactive = process.stdout.isTTY && !opts.yes;
75
100
 
76
- const currentProfiles = [...new Set(Object.values(perAgent))];
101
+ const currentProfiles = [...new Set(Object.values(profilesByAgent))];
77
102
  if (currentProfiles.length === 1 && currentProfiles[0] === name) {
78
103
  process.stdout.write(`Profile already \`${name}\` for all installed agents. No change.\n`);
79
104
  return;
@@ -82,7 +107,7 @@ async function setProfile(cwd, name, opts) {
82
107
  if (interactive) {
83
108
  p.intro(`agentic profile set ${name}`);
84
109
  p.note(
85
- Object.entries(perAgent)
110
+ Object.entries(profilesByAgent)
86
111
  .map(([agent, profile]) => `${AGENT_LABEL[agent]}: ${profile} → ${name}`)
87
112
  .join('\n'),
88
113
  'Profile change'
@@ -101,9 +126,9 @@ async function setProfile(cwd, name, opts) {
101
126
  }
102
127
 
103
128
  // 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);
129
+ // running update, so update reads the new profile. Reuses the in-memory
130
+ // states loaded above — no second filesystem read.
131
+ for (const [agent, state] of Object.entries(statesByAgent)) {
107
132
  state.profile = name;
108
133
  saveState(cwd, agent, state);
109
134
  }
@@ -128,18 +128,27 @@ function previouslyOptedConditional(previousStates, currentAgents, profileName)
128
128
  return [...opted];
129
129
  }
130
130
 
131
- function profileFromStates(previousStates, currentAgents) {
132
- // If multiple agents disagree on profile, surface and bail. Profile is
133
- // expected to match across agents in the same project.
131
+ function profileFromStates(statesByAgent, currentAgents) {
132
+ // Profile must match across every installed agent in the project — not
133
+ // only across the agents the current invocation targets. Without this,
134
+ // `--agent claude-code` on a project where codex was installed with a
135
+ // different profile masks the disagreement and produces inconsistent
136
+ // installs. Per review B2 (v0.11.3): always inspect the FULL set of
137
+ // loaded states, not the narrowed slice.
134
138
  const seen = new Set();
135
- for (const agent of currentAgents) {
136
- const prev = previousStates[agent];
137
- if (prev?.profile) seen.add(prev.profile);
139
+ for (const [agent, state] of Object.entries(statesByAgent)) {
140
+ if (state?.profile) seen.add(state.profile);
141
+ }
142
+ if (seen.size === 0) {
143
+ // No state on disk for any agent. Fall back to the default; current
144
+ // invocation is a fresh / legacy install handled by the legacy path.
145
+ return DEFAULT_PROFILE;
138
146
  }
139
- if (seen.size === 0) return DEFAULT_PROFILE;
140
147
  if (seen.size > 1) {
141
148
  throw new Error(
142
- `state files disagree on profile (${[...seen].join(', ')}). Run \`agentic profile set <name>\` to reconcile.`
149
+ `state files disagree on profile (${[...seen].join(
150
+ ', '
151
+ )}). Run \`agentic profile set <name>\` to reconcile across all installed agents before re-running update.`
143
152
  );
144
153
  }
145
154
  return [...seen][0];
@@ -172,7 +181,10 @@ export async function updateCommand(opts) {
172
181
  previousStates[agent] = statesByAgent[agent] ?? null;
173
182
  }
174
183
 
175
- const profileName = profileFromStates(previousStates, agents);
184
+ // Pass the FULL loaded set, not the narrowed slice. profileFromStates
185
+ // surfaces cross-agent disagreement even when the current invocation
186
+ // targets only one agent (review B2, v0.11.3).
187
+ const profileName = profileFromStates(statesByAgent, agents);
176
188
  const previousOpted = previouslyOptedConditional(
177
189
  previousStates,
178
190
  agents,
@@ -269,13 +281,14 @@ export async function updateCommand(opts) {
269
281
  confirmReplace,
270
282
  previousStates: { [agent]: previousStates[agent] ?? null },
271
283
  kitVersion: pkg.version,
284
+ profile: profileName,
272
285
  dryRun,
273
286
  force,
274
287
  });
275
288
  allActions.push(...result.actions);
276
- const next = result.nextStates[agent];
277
- next.profile = profileName;
278
- nextStates[agent] = next;
289
+ // installSkills now stamps `profile` into nextStates per review C3.
290
+ // No post-hoc injection.
291
+ nextStates[agent] = result.nextStates[agent];
279
292
  }
280
293
 
281
294
  if (!dryRun) {
@@ -284,9 +297,15 @@ export async function updateCommand(opts) {
284
297
  }
285
298
  }
286
299
 
300
+ // Dedup: agentic-architecture and agentic-adr are universal at team /
301
+ // mature (in REQUIRED_SKILLS) AND conditional at solo (in
302
+ // CONDITIONAL_SKILLS) per review B1 (v0.11.3). Without the Set, the
303
+ // managed-skills section would list those rows twice.
287
304
  const skillDisplayOrder = [
288
- ...REQUIRED_SKILLS,
289
- ...CONDITIONAL_SKILLS.map((s) => s.name),
305
+ ...new Set([
306
+ ...REQUIRED_SKILLS,
307
+ ...CONDITIONAL_SKILLS.map((s) => s.name),
308
+ ]),
290
309
  ].filter((s) => installedSkillSet.has(s));
291
310
 
292
311
  const confirmAppend = interactive
package/src/index.js CHANGED
@@ -36,12 +36,25 @@ export async function run(argv) {
36
36
  .option('--force', 'overwrite user-edited files on conflict (non-interactive default: no)')
37
37
  .action(updateCommand);
38
38
 
39
+ // Profile command accepts two positionals so `agentic profile set <name>`
40
+ // captures the name natively. Per review C1 (v0.11.3): the prior single-
41
+ // positional form had Commander swallow the second arg, leaving the
42
+ // documented `Usage: agentic profile set <name>` error message misleading.
43
+ // All forms work now:
44
+ // agentic profile → show
45
+ // agentic profile show → show
46
+ // agentic profile list → list
47
+ // agentic profile set <name> → set
48
+ // agentic profile <name> → shorthand for `set <name>`
49
+ // agentic profile set --name <name> → flag form (back-compat)
39
50
  program
40
- .command('profile [subcommand]')
51
+ .command('profile [subcommand] [name]')
41
52
  .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')
53
+ .option('-n, --name <name>', 'profile name (alternative to positional, for `set` subcommand)')
43
54
  .option('-y, --yes', 'skip confirmation prompts (non-interactive)')
44
- .action(profileCommand);
55
+ .action((subcommand, name, opts) =>
56
+ profileCommand(subcommand, { ...opts, name: opts.name ?? name })
57
+ );
45
58
 
46
59
  await program.parseAsync(argv);
47
60
  }
@@ -12,6 +12,7 @@ import {
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { basename, dirname, join, relative, sep as PATH_SEP } from 'node:path';
14
14
  import { SCHEMA_VERSION } from './state.js';
15
+ import { DEFAULT_PROFILE, validateProfile } from './profiles.js';
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
18
  const KIT_ROOT = join(__dirname, '..', '..');
@@ -190,9 +191,11 @@ export async function installSkills({
190
191
  confirmReplace = async () => false,
191
192
  previousStates = {},
192
193
  kitVersion = null,
194
+ profile = null,
193
195
  dryRun = false,
194
196
  force = false,
195
197
  }) {
198
+ if (profile !== null) validateProfile(profile);
196
199
  const actions = [];
197
200
  const nextStates = {};
198
201
 
@@ -302,10 +305,15 @@ export async function installSkills({
302
305
  };
303
306
  }
304
307
 
308
+ // Profile resolution order: explicit `profile` arg > prior state's
309
+ // profile > DEFAULT_PROFILE. installSkills is the single owner of the
310
+ // returned nextStates' shape; callers no longer inject `profile`
311
+ // post-hoc per review C3 (v0.11.3).
305
312
  nextStates[agent] = {
306
313
  schemaVersion: SCHEMA_VERSION,
307
314
  kitVersion: kitVersion ?? prev?.kitVersion ?? null,
308
315
  agent,
316
+ profile: profile ?? prev?.profile ?? DEFAULT_PROFILE,
309
317
  skills: nextSkills,
310
318
  };
311
319
  }