@martinffx/atelier 3.0.1 → 3.0.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@
4
4
 
5
5
  - **breaking:** rename the `recon` agent to `sentinel`. Existing Atelier configuration is migrated automatically and legacy generated files are removed during the next command.
6
6
 
7
+ ## [3.0.2](https://github.com/martinffx/atelier/compare/v3.0.1...v3.0.2) (2026-08-01)
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * **opencode:** synchronize user-invocable skill commands ([b373120](https://github.com/martinffx/atelier/commit/b37312040ed80cfdab78139f39c8e5150696b763))
13
+
7
14
  ## [3.0.1](https://github.com/martinffx/atelier/compare/v3.0.0...v3.0.1) (2026-07-31)
8
15
 
9
16
 
package/README.md CHANGED
@@ -11,6 +11,10 @@ npx skills add martinffx/atelier
11
11
  npx @martinffx/atelier@latest init --harness <claude|opencode|codex|cursor>
12
12
  ```
13
13
 
14
+ For OpenCode, installing skills first also creates slash commands for every installed skill
15
+ marked `user-invocable: true`. If you initialize first, run `npx @martinffx/atelier@latest update --harness opencode`
16
+ after installing skills.
17
+
14
18
  ## How Atelier works
15
19
 
16
20
  Atelier uses as much process as each request needs. Bounded work gets a concise plan in the conversation. Substantial work gets a durable spec, an implementation plan, and tracked execution. The developer approves the plan before implementation begins.
package/dist/atelier.js CHANGED
@@ -25433,7 +25433,7 @@ var {
25433
25433
  } = import__.default;
25434
25434
 
25435
25435
  // src/commands/init.ts
25436
- import { join as join3 } from "path";
25436
+ import { join as join4 } from "path";
25437
25437
  import { homedir as homedir3 } from "os";
25438
25438
 
25439
25439
  // node_modules/@inquirer/core/dist/esm/lib/key.js
@@ -32042,6 +32042,12 @@ class HarnessConfigError extends AtelierError {
32042
32042
  super(`Failed to parse ${file}: ${cause}`, "HARNESS_CONFIG_ERROR", 1);
32043
32043
  }
32044
32044
  }
32045
+ class SkillDiscoveryError extends AtelierError {
32046
+ constructor(path2, cause) {
32047
+ super(`Failed to read skills from ${path2}: ${cause}`, "SKILL_DISCOVERY_ERROR", 1);
32048
+ }
32049
+ }
32050
+
32045
32051
  class InvalidConfigError extends AtelierError {
32046
32052
  constructor(cause, { suggestReinit = false } = {}) {
32047
32053
  const suffix = suggestReinit ? ` Run \`atelier init --harness <${Harness.join("|")}>\` to reconfigure.` : "";
@@ -32183,6 +32189,13 @@ function writeConfig(config, path2 = CONFIG_PATH) {
32183
32189
  writeFileSync2(path2, JSON.stringify(config, null, 2) + `
32184
32190
  `);
32185
32191
  }
32192
+ function toSharedConfig(config) {
32193
+ return {
32194
+ version: config.version,
32195
+ skills_source: config.skills_source,
32196
+ skills_path: config.skills_path
32197
+ };
32198
+ }
32186
32199
  function getConfiguredHarnesses(config) {
32187
32200
  return Harness.filter((harness) => config[harness] !== undefined);
32188
32201
  }
@@ -32205,7 +32218,7 @@ function getDefaultConfig(harness) {
32205
32218
 
32206
32219
  // src/services/paths.ts
32207
32220
  import { homedir as homedir2 } from "os";
32208
- import { join as join2 } from "path";
32221
+ import { isAbsolute, join as join2, resolve } from "path";
32209
32222
  function shortPath(p) {
32210
32223
  const home = homedir2();
32211
32224
  if (p === home || p.startsWith(home + "/")) {
@@ -32216,6 +32229,15 @@ function shortPath(p) {
32216
32229
  function getGlobalOpencodeDir() {
32217
32230
  return join2(homedir2(), ".config", "opencode");
32218
32231
  }
32232
+ function resolveSkillsPath(skillsPath) {
32233
+ if (skillsPath === "~") {
32234
+ return homedir2();
32235
+ }
32236
+ if (skillsPath.startsWith("~/")) {
32237
+ return join2(homedir2(), skillsPath.slice(2));
32238
+ }
32239
+ return isAbsolute(skillsPath) ? skillsPath : resolve(skillsPath);
32240
+ }
32219
32241
  function resolveBasePath(harness) {
32220
32242
  return harness === "opencode" ? getGlobalOpencodeDir() : homedir2();
32221
32243
  }
@@ -32305,7 +32327,7 @@ async function promptForOpenCodeModels(prompt2, section, models, provider) {
32305
32327
  };
32306
32328
  }
32307
32329
  function formatFileList(files) {
32308
- return files.map((f) => ` ${f.exists ? "~" : "+"} ${f.path}`).join(`
32330
+ return files.map((f) => ` ${f.action === "delete" ? "-" : f.exists ? "~" : "+"} ${f.path}`).join(`
32309
32331
  `);
32310
32332
  }
32311
32333
  function guardProvider(provider) {
@@ -32315,6 +32337,67 @@ function guardProvider(provider) {
32315
32337
  return provider;
32316
32338
  }
32317
32339
 
32340
+ // src/utils/templates.ts
32341
+ var import_gray_matter = __toESM(require_gray_matter(), 1);
32342
+ import { readFileSync as readFileSync3, existsSync as existsSync2, readdirSync, lstatSync, statSync } from "fs";
32343
+ import { join as join3, dirname as dirname2 } from "path";
32344
+ import { fileURLToPath } from "url";
32345
+ var __dirname2 = dirname2(fileURLToPath(import.meta.url));
32346
+ var AGENTS_DIR = existsSync2(join3(__dirname2, "..", "agents")) ? join3(__dirname2, "..", "agents") : join3(__dirname2, "..", "..", "agents");
32347
+ var MAX_SKILL_FILE_SIZE = 1024 * 1024;
32348
+ function readTemplate(name) {
32349
+ const filePath = join3(AGENTS_DIR, `${name}.md`);
32350
+ const raw = readFileSync3(filePath, "utf-8");
32351
+ const { data, content } = import_gray_matter.default(raw);
32352
+ return {
32353
+ name: data.name,
32354
+ description: data.description,
32355
+ body: content.trim()
32356
+ };
32357
+ }
32358
+ function readUserInvocableSkills(skillsPath) {
32359
+ let entries;
32360
+ try {
32361
+ entries = readdirSync(skillsPath, { withFileTypes: true });
32362
+ } catch (err2) {
32363
+ if (err2.code === "ENOENT") {
32364
+ return { skills: [], warnings: [] };
32365
+ }
32366
+ throw new SkillDiscoveryError(skillsPath, err2 instanceof Error ? err2.message : String(err2));
32367
+ }
32368
+ const warnings = [];
32369
+ const skills = entries.flatMap((entry) => {
32370
+ const skillDir = join3(skillsPath, entry.name);
32371
+ try {
32372
+ if (!entry.isDirectory() && !(entry.isSymbolicLink() && statSync(skillDir).isDirectory())) {
32373
+ return [];
32374
+ }
32375
+ const skillPath = join3(skillDir, "SKILL.md");
32376
+ if (!existsSync2(skillPath)) {
32377
+ return [];
32378
+ }
32379
+ const stat = lstatSync(skillPath);
32380
+ if (!stat.isFile() || stat.size > MAX_SKILL_FILE_SIZE) {
32381
+ warnings.push(`Skipped ${skillPath}: expected a regular SKILL.md no larger than ${MAX_SKILL_FILE_SIZE} bytes.`);
32382
+ return [];
32383
+ }
32384
+ const { data } = import_gray_matter.default(readFileSync3(skillPath, "utf-8"));
32385
+ if (data["user-invocable"] !== true) {
32386
+ return [];
32387
+ }
32388
+ if (typeof data.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(data.name) || typeof data.description !== "string") {
32389
+ warnings.push(`Skipped ${skillPath}: invalid user-invocable skill metadata.`);
32390
+ return [];
32391
+ }
32392
+ return [{ name: data.name, description: data.description }];
32393
+ } catch (err2) {
32394
+ warnings.push(`Skipped ${skillDir}: ${err2 instanceof Error ? err2.message : String(err2)}`);
32395
+ return [];
32396
+ }
32397
+ }).sort((a, b) => a.name.localeCompare(b.name));
32398
+ return { skills: [...new Map(skills.map((skill) => [skill.name, skill])).values()], warnings };
32399
+ }
32400
+
32318
32401
  // src/commands/init.ts
32319
32402
  async function init(options2) {
32320
32403
  const harnessOption = options2.harness;
@@ -32338,7 +32421,7 @@ async function init(options2) {
32338
32421
  ]);
32339
32422
  harness = answer.harness;
32340
32423
  }
32341
- const configPath = join3(homedir3(), CONFIG_FILE);
32424
+ const configPath = join4(homedir3(), CONFIG_FILE);
32342
32425
  const configResult = readConfig(configPath);
32343
32426
  if (!configResult.ok) {
32344
32427
  if (configResult.error.type !== "not-found") {
@@ -32357,7 +32440,7 @@ async function init(options2) {
32357
32440
  validateConfig(config);
32358
32441
  const harnessBasePath = resolveBasePath(harness);
32359
32442
  if (!options2.yes) {
32360
- const files = adapter.fileList(harnessBasePath);
32443
+ const files = adapter.fileList(harnessBasePath, toSharedConfig(config));
32361
32444
  console.log(`
32362
32445
  Files to write:`);
32363
32446
  console.log(formatFileList(files));
@@ -32373,7 +32456,7 @@ Files to write:`);
32373
32456
  }
32374
32457
  }
32375
32458
  adapter.mergeHarnessConfig(section, harnessBasePath);
32376
- adapter.installAgents(section, harnessBasePath);
32459
+ adapter.installAgents(section, harnessBasePath, toSharedConfig(config));
32377
32460
  writeConfig(config, configPath);
32378
32461
  console.log(`
32379
32462
  Atelier initialized for ${harness}.`);
@@ -32383,10 +32466,13 @@ Next step:`);
32383
32466
  console.log(" opencode auth login --provider openai # connect your OpenAI account");
32384
32467
  }
32385
32468
  console.log(" npx skills add martinffx/atelier # install skills");
32469
+ if (harness === "opencode" && readUserInvocableSkills(resolveSkillsPath(config.skills_path)).skills.length === 0) {
32470
+ console.log(" npx @martinffx/atelier@latest update --harness opencode # create commands after installing skills");
32471
+ }
32386
32472
  }
32387
32473
 
32388
32474
  // src/commands/update.ts
32389
- import { join as join4 } from "path";
32475
+ import { join as join5 } from "path";
32390
32476
  import { homedir as homedir4 } from "os";
32391
32477
 
32392
32478
  // src/commands/utils.ts
@@ -32418,7 +32504,7 @@ async function resolveHarness(config, harnessOption, command) {
32418
32504
 
32419
32505
  // src/commands/update.ts
32420
32506
  async function update(options2) {
32421
- const configPath = join4(homedir4(), CONFIG_FILE);
32507
+ const configPath = join5(homedir4(), CONFIG_FILE);
32422
32508
  const configResult = readConfig(configPath);
32423
32509
  if (!configResult.ok) {
32424
32510
  if (configResult.error.type === "not-found") {
@@ -32435,7 +32521,7 @@ async function update(options2) {
32435
32521
  const adapter = getAdapter(harness);
32436
32522
  const section = await promptForSection(adapter, existingSection);
32437
32523
  const harnessBasePath = resolveBasePath(harness);
32438
- const files = adapter.fileList(harnessBasePath);
32524
+ const files = adapter.fileList(harnessBasePath, toSharedConfig(config));
32439
32525
  console.log(`
32440
32526
  Files to write:`);
32441
32527
  console.log(formatFileList(files));
@@ -32452,17 +32538,17 @@ Files to write:`);
32452
32538
  config[harness] = section;
32453
32539
  validateConfig(config);
32454
32540
  adapter.mergeHarnessConfig(section, harnessBasePath);
32455
- adapter.installAgents(section, harnessBasePath);
32541
+ adapter.installAgents(section, harnessBasePath, toSharedConfig(config));
32456
32542
  writeConfig(config, configPath);
32457
32543
  console.log(`Atelier updated for ${harness}.`);
32458
32544
  console.log("Skills are managed separately. Run `npx skills update martinffx/atelier` to update skills.");
32459
32545
  }
32460
32546
 
32461
32547
  // src/commands/remove.ts
32462
- import { join as join5 } from "path";
32548
+ import { join as join6 } from "path";
32463
32549
  import { homedir as homedir5 } from "os";
32464
32550
  async function remove(options2) {
32465
- const configPath = join5(homedir5(), CONFIG_FILE);
32551
+ const configPath = join6(homedir5(), CONFIG_FILE);
32466
32552
  const configResult = readConfig(configPath);
32467
32553
  if (!configResult.ok) {
32468
32554
  if (configResult.error.type === "not-found") {
@@ -32492,27 +32578,9 @@ async function remove(options2) {
32492
32578
  }
32493
32579
 
32494
32580
  // src/adapters/claude.ts
32495
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, rmdirSync, existsSync as existsSync3, readdirSync } from "fs";
32581
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, rmdirSync, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
32496
32582
  import { join as join7 } from "path";
32497
32583
 
32498
- // src/utils/templates.ts
32499
- var import_gray_matter = __toESM(require_gray_matter(), 1);
32500
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
32501
- import { join as join6, dirname as dirname2 } from "path";
32502
- import { fileURLToPath } from "url";
32503
- var __dirname2 = dirname2(fileURLToPath(import.meta.url));
32504
- var AGENTS_DIR = existsSync2(join6(__dirname2, "..", "agents")) ? join6(__dirname2, "..", "agents") : join6(__dirname2, "..", "..", "agents");
32505
- function readTemplate(name) {
32506
- const filePath = join6(AGENTS_DIR, `${name}.md`);
32507
- const raw = readFileSync3(filePath, "utf-8");
32508
- const { data, content } = import_gray_matter.default(raw);
32509
- return {
32510
- name: data.name,
32511
- description: data.description,
32512
- body: content.trim()
32513
- };
32514
- }
32515
-
32516
32584
  // src/utils/schemas.ts
32517
32585
  var AgentSchema = exports_external.object({
32518
32586
  template: exports_external.enum(AGENT_NAMES),
@@ -32643,7 +32711,7 @@ function removeDirIfEmpty(dir) {
32643
32711
  return;
32644
32712
  }
32645
32713
  try {
32646
- if (readdirSync(dir).length === 0) {
32714
+ if (readdirSync2(dir).length === 0) {
32647
32715
  rmdirSync(dir);
32648
32716
  }
32649
32717
  } catch (err2) {
@@ -32692,8 +32760,8 @@ function readExistingSettings(settingsPath) {
32692
32760
  }
32693
32761
 
32694
32762
  // src/adapters/opencode.ts
32695
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, existsSync as existsSync4, rmSync as rmSync3, rmdirSync as rmdirSync2, readdirSync as readdirSync2 } from "fs";
32696
- import { join as join8 } from "path";
32763
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, existsSync as existsSync4, rmSync as rmSync3, rmdirSync as rmdirSync2, readdirSync as readdirSync3, lstatSync as lstatSync2, realpathSync, statSync as statSync2 } from "fs";
32764
+ import { basename, join as join8 } from "path";
32697
32765
  var OPENCODE_PROVIDERS2 = [
32698
32766
  { name: "OpenCode Zen", value: "opencode-zen" },
32699
32767
  { name: "OpenCode Go", value: "opencode-go" },
@@ -32781,6 +32849,7 @@ var DEFAULT_MODELS2 = {
32781
32849
  plan: "openai/gpt-5.6-sol"
32782
32850
  }
32783
32851
  };
32852
+ var COMMAND_MARKER = "<!-- Generated by Atelier -->";
32784
32853
  var opencodeAdapter = {
32785
32854
  name: "opencode",
32786
32855
  providerChoices: OPENCODE_PROVIDERS2,
@@ -32817,7 +32886,7 @@ async function promptSection2(prompt2, section) {
32817
32886
  const models = modelsForProvider2(provider);
32818
32887
  return promptForOpenCodeModels(prompt2, openCodeSection, models, provider);
32819
32888
  }
32820
- function installAgents2(section, basePath) {
32889
+ function installAgents2(section, basePath, sharedConfig) {
32821
32890
  const config = section;
32822
32891
  const opencodeRoot = getOpencodeRoot(basePath);
32823
32892
  const agentsDir = join8(opencodeRoot, "agent");
@@ -32842,6 +32911,41 @@ temperature: 0.2
32842
32911
  writeFileSync4(agentPath, content);
32843
32912
  console.log(`Created ${shortPath(agentPath)}`);
32844
32913
  }
32914
+ if (sharedConfig) {
32915
+ installCommands(opencodeRoot, sharedConfig);
32916
+ }
32917
+ }
32918
+ function installCommands(opencodeRoot, sharedConfig) {
32919
+ const discovery = readUserInvocableSkills(resolveSkillsPath(sharedConfig.skills_path));
32920
+ reportSkillWarnings(discovery.warnings);
32921
+ const commandsDir = getCommandsDir(opencodeRoot, discovery.skills.length > 0);
32922
+ if (!commandsDir) {
32923
+ return;
32924
+ }
32925
+ const desiredCommands = new Map(discovery.skills.map((skill) => [skill.name, skill]));
32926
+ for (const path2 of managedCommandPaths(commandsDir)) {
32927
+ if (!desiredCommands.has(basename(path2, ".md"))) {
32928
+ rmSync3(path2, { force: true });
32929
+ console.log(`Removed ${shortPath(path2)}`);
32930
+ }
32931
+ }
32932
+ for (const skill of discovery.skills) {
32933
+ const commandPath = join8(commandsDir, `${skill.name}.md`);
32934
+ if (commandStatus(commandPath) === "custom") {
32935
+ console.log(`Skipped ${shortPath(commandPath)} because it is not managed by Atelier.`);
32936
+ continue;
32937
+ }
32938
+ const content = `---
32939
+ description: ${JSON.stringify(skill.description)}
32940
+ ---
32941
+ ${COMMAND_MARKER}
32942
+ Activate the ${skill.name} skill and follow its instructions precisely.
32943
+
32944
+ User request: $ARGUMENTS
32945
+ `;
32946
+ writeFileSync4(commandPath, content);
32947
+ console.log(`Created ${shortPath(commandPath)}`);
32948
+ }
32845
32949
  }
32846
32950
  function mergeHarnessConfig2(section, basePath) {
32847
32951
  const config = section;
@@ -32885,7 +32989,7 @@ function mergeHarnessConfig2(section, basePath) {
32885
32989
  }
32886
32990
  console.log(`${isNew ? "Created" : "Updated"} ${shortPath(opencodeJsonPath)}`);
32887
32991
  }
32888
- function fileList2(basePath) {
32992
+ function fileList2(basePath, sharedConfig) {
32889
32993
  const files = [
32890
32994
  { path: join8(basePath, "opencode.json"), exists: existsSync4(join8(basePath, "opencode.json")) }
32891
32995
  ];
@@ -32894,6 +32998,27 @@ function fileList2(basePath) {
32894
32998
  const path2 = join8(opencodeRoot, "agent", `${name}.md`);
32895
32999
  files.push({ path: path2, exists: existsSync4(path2) });
32896
33000
  }
33001
+ if (sharedConfig) {
33002
+ const discovery = readUserInvocableSkills(resolveSkillsPath(sharedConfig.skills_path));
33003
+ reportSkillWarnings(discovery.warnings);
33004
+ const commandsDir = getCommandsDir(opencodeRoot, discovery.skills.length > 0);
33005
+ if (!commandsDir) {
33006
+ return files;
33007
+ }
33008
+ const desiredNames = new Set(discovery.skills.map((skill) => skill.name));
33009
+ for (const path2 of managedCommandPaths(commandsDir)) {
33010
+ if (!desiredNames.has(basename(path2, ".md"))) {
33011
+ files.push({ path: path2, exists: true, action: "delete" });
33012
+ }
33013
+ }
33014
+ for (const skill of discovery.skills) {
33015
+ const path2 = join8(opencodeRoot, "command", `${skill.name}.md`);
33016
+ const commandPath = join8(commandsDir, `${skill.name}.md`);
33017
+ if (commandStatus(commandPath) !== "custom") {
33018
+ files.push({ path: path2, exists: existsSync4(commandPath) });
33019
+ }
33020
+ }
33021
+ }
32897
33022
  return files;
32898
33023
  }
32899
33024
  function remove3(section, basePath) {
@@ -32919,7 +33044,7 @@ function removeDirIfEmpty2(dir) {
32919
33044
  return;
32920
33045
  }
32921
33046
  try {
32922
- if (readdirSync2(dir).length === 0) {
33047
+ if (readdirSync3(dir).length === 0) {
32923
33048
  rmdirSync2(dir);
32924
33049
  }
32925
33050
  } catch (err2) {
@@ -32931,9 +33056,71 @@ function removeLegacyFiles(opencodeRoot) {
32931
33056
  if (existsSync4(pluginPath)) {
32932
33057
  rmSync3(pluginPath, { force: true });
32933
33058
  }
33059
+ const commandsDir = getCommandsDir(opencodeRoot, false);
33060
+ if (!commandsDir) {
33061
+ return;
33062
+ }
33063
+ for (const path2 of managedCommandPaths(commandsDir)) {
33064
+ rmSync3(path2, { force: true });
33065
+ }
33066
+ if (!lstatSync2(join8(opencodeRoot, "command")).isSymbolicLink()) {
33067
+ removeDirIfEmpty2(commandsDir);
33068
+ }
33069
+ }
33070
+ function reportSkillWarnings(warnings) {
33071
+ for (const warning of warnings) {
33072
+ console.warn(warning);
33073
+ }
33074
+ }
33075
+ function getCommandsDir(opencodeRoot, create) {
32934
33076
  const commandsDir = join8(opencodeRoot, "command");
32935
- if (existsSync4(commandsDir)) {
32936
- rmSync3(commandsDir, { recursive: true, force: true });
33077
+ if (!existsSync4(commandsDir)) {
33078
+ if (!create) {
33079
+ return;
33080
+ }
33081
+ try {
33082
+ mkdirSync3(commandsDir, { recursive: true });
33083
+ return commandsDir;
33084
+ } catch (err2) {
33085
+ throw new FileWriteError(commandsDir, err2 instanceof Error ? err2.message : String(err2));
33086
+ }
33087
+ }
33088
+ try {
33089
+ const stat = lstatSync2(commandsDir);
33090
+ if (stat.isSymbolicLink()) {
33091
+ const target = realpathSync(commandsDir);
33092
+ if (!statSync2(target).isDirectory()) {
33093
+ throw new Error("symbolic link target is not a directory");
33094
+ }
33095
+ return target;
33096
+ }
33097
+ if (!stat.isDirectory()) {
33098
+ throw new Error("path is not a directory");
33099
+ }
33100
+ return commandsDir;
33101
+ } catch (err2) {
33102
+ throw new FileWriteError(commandsDir, err2 instanceof Error ? err2.message : String(err2));
33103
+ }
33104
+ }
33105
+ function managedCommandPaths(commandsDir) {
33106
+ try {
33107
+ return readdirSync3(commandsDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => join8(commandsDir, entry.name)).filter((path2) => readFileSync5(path2, "utf-8").includes(COMMAND_MARKER));
33108
+ } catch (err2) {
33109
+ throw new FileWriteError(commandsDir, err2 instanceof Error ? err2.message : String(err2));
33110
+ }
33111
+ }
33112
+ function commandStatus(path2) {
33113
+ try {
33114
+ const stat = lstatSync2(path2);
33115
+ if (stat.isSymbolicLink() || !stat.isFile()) {
33116
+ throw new Error("command path must be a regular file");
33117
+ }
33118
+ return readFileSync5(path2, "utf-8").includes(COMMAND_MARKER) ? "managed" : "custom";
33119
+ } catch (err2) {
33120
+ if (err2.code === "ENOENT") {
33121
+ return "missing";
33122
+ }
33123
+ throw new FileWriteError(path2, err2 instanceof Error ? err2.message : String(err2));
32937
33124
  }
32938
33125
  }
32939
33126
  function writeOrDeleteOpenCodeJson(basePath, config) {
@@ -32998,7 +33185,7 @@ function deepMerge2(target, source) {
32998
33185
  }
32999
33186
 
33000
33187
  // src/adapters/codex.ts
33001
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, existsSync as existsSync5, rmSync as rmSync4, rmdirSync as rmdirSync3, readdirSync as readdirSync3 } from "fs";
33188
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, existsSync as existsSync5, rmSync as rmSync4, rmdirSync as rmdirSync3, readdirSync as readdirSync4 } from "fs";
33002
33189
  import { join as join9 } from "path";
33003
33190
 
33004
33191
  // node_modules/smol-toml/dist/date.js
@@ -34216,7 +34403,7 @@ function removeDirIfEmpty3(dir) {
34216
34403
  return;
34217
34404
  }
34218
34405
  try {
34219
- if (readdirSync3(dir).length === 0) {
34406
+ if (readdirSync4(dir).length === 0) {
34220
34407
  rmdirSync3(dir);
34221
34408
  }
34222
34409
  } catch (err2) {
@@ -34274,7 +34461,7 @@ function removeEmptyCodexDir(codexDir) {
34274
34461
  return;
34275
34462
  }
34276
34463
  try {
34277
- const entries = readdirSync3(codexDir);
34464
+ const entries = readdirSync4(codexDir);
34278
34465
  if (entries.length === 0) {
34279
34466
  rmSync4(codexDir, { recursive: true, force: true });
34280
34467
  }
@@ -34287,7 +34474,7 @@ function isObject(value) {
34287
34474
  }
34288
34475
 
34289
34476
  // src/adapters/cursor.ts
34290
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync4, rmSync as rmSync5, rmdirSync as rmdirSync4, writeFileSync as writeFileSync6 } from "fs";
34477
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, rmdirSync as rmdirSync4, writeFileSync as writeFileSync6 } from "fs";
34291
34478
  import { join as join10 } from "path";
34292
34479
  var CURSOR_MODELS = [
34293
34480
  "composer-2.5",
@@ -34378,7 +34565,7 @@ function remove5(section, basePath) {
34378
34565
  rmSync5(join10(agentsDir, `${agent.name}.md`), { force: true });
34379
34566
  }
34380
34567
  rmSync5(join10(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true });
34381
- if (existsSync6(agentsDir) && readdirSync4(agentsDir).length === 0) {
34568
+ if (existsSync6(agentsDir) && readdirSync5(agentsDir).length === 0) {
34382
34569
  rmdirSync4(agentsDir);
34383
34570
  }
34384
34571
  } catch (err2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinffx/atelier",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Personal development toolkit - spec-driven development, code quality, deep thinking, and ecosystem patterns",
5
5
  "type": "module",
6
6
  "bin": {
@@ -125,9 +125,10 @@ The workflow is not purely linear. Expect backflows:
125
125
  - Planning reveals design assumptions are wrong → back to brainstorm
126
126
  - Tasks can't be decomposed without more context → back to brainstorm
127
127
 
128
- ### Implement → Plan (missing tasks)
129
- - Implementation reveals missing work → revise the active plan and get approval
130
- - Spec-backed work is blocked on an unplanned dependency → update plan.json
128
+ ### Implement → Plan (material changes)
129
+ - A minor deviation may be recorded inline and reflected in the plan when it does not change
130
+ approved behavior, scope, architecture, public contracts, or major dependencies.
131
+ - Material missing work or an unplanned dependency → revise the active plan and get approval.
131
132
 
132
133
  ### Implement → Research (fundamental issue)
133
134
  - Inline planning or implementation develops substantial needs → ask the human whether to
@@ -1,14 +1,15 @@
1
1
  ---
2
2
  name: atelier-setup
3
- description: Configure a repository's generic agent guidance for Atelier's development workflow. Use only when the user explicitly invokes atelier-setup; it inspects existing repository instructions and domain-document conventions, previews an idempotent project-guidance update, and writes only after approval. It does not install Atelier, configure a harness, or scaffold empty workflow artifacts.
3
+ description: Configure a repository for Atelier's development workflow. Use only when explicitly invoked; inspect existing guidance, issue-tracker and domain-document conventions, preview the proposed configuration, and write only after approval. It does not install Atelier or initialize external tooling.
4
4
  user-invocable: true
5
+ disable-model-invocation: true
5
6
  ---
6
7
 
7
8
  # Atelier Setup
8
9
 
9
- Set up generic repository guidance that Atelier skills need without taking ownership of the
10
- developer's installation, harness configuration, or project structure. Explore first, present
11
- the proposed edit exactly, and write only after the developer approves it.
10
+ Set up repository guidance that Atelier skills need without taking ownership of the developer's
11
+ installation, harness configuration, or project structure. Explore first, present every proposed
12
+ edit exactly, and write only after the developer approves it.
12
13
 
13
14
  ## 1. Explore
14
15
 
@@ -18,6 +19,9 @@ Read the repository before proposing any change:
18
19
  heading.
19
20
  - Root `CONTEXT.md` and `CONTEXT-MAP.md`.
20
21
  - `docs/adr/` and any context-local `docs/adr/` directories.
22
+ - `docs/agents/`, including existing `issue-tracker.md` and `domain.md`.
23
+ - Existing issue-tracker conventions: remotes, issue links in documentation, local issue
24
+ directories, and project guidance.
21
25
  - Genuine monorepo signals: a workspace manifest, or multiple independently deployable packages
22
26
  with their own source directories.
23
27
 
@@ -33,9 +37,19 @@ Summarize what exists and recommend a domain-document layout:
33
37
  contexts. `CONTEXT-MAP.md` belongs at the root and points to each context's `CONTEXT.md` and
34
38
  `docs/adr/` directory.
35
39
 
36
- Explain that these documents remain lazy: do not create `CONTEXT.md`, `CONTEXT-MAP.md`, or an ADR
40
+ Explain that domain documents remain lazy: do not create `CONTEXT.md`, `CONTEXT-MAP.md`, or an ADR
37
41
  directory until another Atelier skill has resolved terminology or made a decision worth recording.
38
42
 
43
+ Select or confirm the issue tracker:
44
+
45
+ 1. If `docs/agents/issue-tracker.md` already defines one, preserve it unless the developer asks to
46
+ change it.
47
+ 2. Otherwise recommend the tracker indicated by existing repository practice, then ask the
48
+ developer to confirm or describe their tracker.
49
+ 3. Accept GitHub, GitLab, local Markdown, another tracker described by the developer, or `none`.
50
+ 4. Record the chosen workflow in `docs/agents/issue-tracker.md`; do not install, initialize, or
51
+ create external tracker resources.
52
+
39
53
  Select the instruction file to edit:
40
54
 
41
55
  1. If both files exist, ask which file should own the agent guidance; do not duplicate it.
@@ -43,8 +57,13 @@ Select the instruction file to edit:
43
57
  3. Otherwise use root `CLAUDE.md` when it exists.
44
58
  4. If neither exists, ask which instruction file to create; do not choose on the developer's behalf.
45
59
 
46
- Show the developer the selected file, domain layout, and the exact `## Agent skills` block that will be
47
- written. Stop and wait for explicit approval.
60
+ Show the developer the selected file, domain layout, and exact contents for:
61
+
62
+ - the `## Agent skills` block;
63
+ - `docs/agents/issue-tracker.md` using `references/issue-tracker.md`;
64
+ - `docs/agents/domain.md` using `references/domain.md`.
65
+
66
+ Stop and wait for explicit approval.
48
67
 
49
68
  ## 3. Write After Approval
50
69
 
@@ -58,23 +77,27 @@ Add the following block to the selected instruction file:
58
77
  Use `atelier-orchestrator` at the start of development work. It selects an Inline Plan for bounded
59
78
  changes or a Spec-backed Plan when durable design and coordination artifacts are warranted.
60
79
 
61
- ### Domain documentation
80
+ ### Issue tracking
81
+
82
+ Read `docs/agents/issue-tracker.md` when issue tracking is relevant. It defines this repository's
83
+ tracker workflow; `plan.json` remains authoritative for Spec-backed task details and dependencies.
62
84
 
63
- Before working in a domain area, read root `CONTEXT-MAP.md` when present and follow it to the
64
- relevant context documentation. Otherwise, read root `CONTEXT.md` when present. Read relevant
65
- architectural decisions under `docs/adr/` and any context-specific ADR directories.
85
+ ### Domain documentation
66
86
 
67
- If these documents do not exist, proceed silently. Create or update them only when domain
68
- terminology or an architectural decision has been resolved.
87
+ Read `docs/agents/domain.md` before working in a domain area. It defines how to locate and use this
88
+ repository's context documents and ADRs.
69
89
  ```
70
90
 
71
- If an `## Agent skills` section already exists, update the `### Planning and implementation` and
72
- `### Domain documentation` subsections in place rather than appending duplicates. Preserve other
73
- subsections and all content outside the section, including user edits. Do not create any other
74
- files or directories.
91
+ If an `## Agent skills` section already exists, update the `### Planning and implementation`,
92
+ `### Issue tracking`, and `### Domain documentation` subsections in place rather than appending
93
+ duplicates. Preserve other subsections and all content outside the section, including user edits.
94
+
95
+ Create `docs/agents/` when needed, then write the approved tracker and domain documents. Do not
96
+ create any other files or directories.
75
97
 
76
98
  ## 4. Report
77
99
 
78
- State which instruction file changed and the chosen domain-document layout. Remind the developer
79
- that the layout is a convention only: the first domain-modelling or architectural-decision task
80
- creates the relevant documents when it has substantive content.
100
+ State which instruction file and agent documents changed, the selected tracker, and the chosen
101
+ domain-document layout. Remind the developer that the layout is a convention only: the first
102
+ domain-modelling or architectural-decision task creates the relevant documents when it has
103
+ substantive content.
@@ -0,0 +1,22 @@
1
+ # Domain Documentation
2
+
3
+ ## Layout
4
+
5
+ - **Mode:** [single-context or multi-context]
6
+ - **Context map:** [path or not used]
7
+ - **System ADRs:** [path]
8
+ - **Context ADRs:** [path pattern or not used]
9
+
10
+ ## Before Domain Work
11
+
12
+ 1. Read the context map when configured, then select the relevant context.
13
+ 2. Read that context's `CONTEXT.md` when it exists.
14
+ 3. Read ADRs relevant to the work.
15
+ 4. Proceed silently when these documents do not yet exist.
16
+
17
+ ## Ownership
18
+
19
+ - `CONTEXT.md` records domain language, distinctions, and business invariants.
20
+ - `CONTEXT-MAP.md` maps bounded contexts to their context documents and ADRs.
21
+ - ADRs record architectural decisions and trade-offs.
22
+ - This file only tells agents how to locate and consume those documents.