@martinffx/atelier 3.0.1 → 3.1.0

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +19 -1
  3. package/dist/atelier.js +237 -44
  4. package/package.json +1 -1
  5. package/skills/atelier-orchestrator/SKILL.md +16 -3
  6. package/skills/atelier-setup/SKILL.md +43 -20
  7. package/skills/atelier-setup/references/domain.md +22 -0
  8. package/skills/atelier-setup/references/issue-tracker.md +35 -0
  9. package/skills/code-commit/SKILL.md +4 -3
  10. package/skills/code-pull-request/references/common.md +4 -4
  11. package/skills/code-pull-request/references/create-pr.md +15 -15
  12. package/skills/code-pull-request/references/merge-pr.md +3 -3
  13. package/skills/code-pull-request/references/read-comments.md +2 -2
  14. package/skills/code-review/SKILL.md +27 -8
  15. package/skills/code-review/references/output.md +52 -0
  16. package/skills/code-review/references/reviewers.md +113 -13
  17. package/skills/code-review/references/rq.md +195 -53
  18. package/skills/code-review/references/rs.md +41 -5
  19. package/skills/code-subagents/SKILL.md +4 -2
  20. package/skills/code-subagents/references/batch-reviewer-prompt.md +9 -0
  21. package/skills/code-subagents/references/implementor-prompt.md +7 -0
  22. package/skills/oracle-debug/SKILL.md +17 -10
  23. package/skills/oracle-debug/references/techniques.md +41 -137
  24. package/skills/oracle-domain-modelling/SKILL.md +5 -2
  25. package/skills/oracle-grill-me/SKILL.md +4 -1
  26. package/skills/spec-brainstorm/SKILL.md +34 -9
  27. package/skills/spec-finish/SKILL.md +40 -36
  28. package/skills/spec-implement/SKILL.md +38 -47
  29. package/skills/spec-plan/SKILL.md +64 -32
  30. package/skills/spec-plan/references/plan_template.md +15 -2
  31. package/skills/oracle-debug/references/language-tools.md +0 -139
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@
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.1.0](https://github.com/martinffx/atelier/compare/v3.0.2...v3.1.0) (2026-08-09)
8
+
9
+
10
+ ### Features
11
+
12
+ * **code-review:** persist review decisions ([4b1a349](https://github.com/martinffx/atelier/commit/4b1a34962f2b52f248e4581c663ed139eed4e081))
13
+ * **codex:** configure project execution defaults ([74ef9cc](https://github.com/martinffx/atelier/commit/74ef9cc71c25145ba7591bd58306efc7e734a7c6))
14
+ * **skills:** apply writing guidance to prose ([3fce90d](https://github.com/martinffx/atelier/commit/3fce90da2881788a8d8cdcad63a61849b298d33c))
15
+ * **skills:** enforce simplicity throughout workflows ([fcf852b](https://github.com/martinffx/atelier/commit/fcf852bf68139e66d165761beeec8f74827513eb))
16
+
17
+ ## [3.0.2](https://github.com/martinffx/atelier/compare/v3.0.1...v3.0.2) (2026-08-01)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **opencode:** synchronize user-invocable skill commands ([b373120](https://github.com/martinffx/atelier/commit/b37312040ed80cfdab78139f39c8e5150696b763))
23
+
7
24
  ## [3.0.1](https://github.com/martinffx/atelier/compare/v3.0.0...v3.0.1) (2026-07-31)
8
25
 
9
26
 
package/README.md CHANGED
@@ -7,10 +7,28 @@
7
7
  Atelier gives coding agents a disciplined way to move from an idea to reviewed, verified code without taking control away from the developer.
8
8
 
9
9
  ```bash
10
- npx skills add martinffx/atelier
11
10
  npx @martinffx/atelier@latest init --harness <claude|opencode|codex|cursor>
12
11
  ```
13
12
 
13
+ Install the skills separately:
14
+
15
+ ```bash
16
+ npx skills add martinffx/atelier
17
+ ```
18
+
19
+ Update installed skills with:
20
+
21
+ ```bash
22
+ npx skills update martinffx/atelier
23
+ ```
24
+
25
+ For OpenCode, installing skills first also creates slash commands for every installed skill
26
+ marked `user-invocable: true`. If you initialized OpenCode before installing the skills, run:
27
+
28
+ ```bash
29
+ npx @martinffx/atelier@latest update --harness opencode
30
+ ```
31
+
14
32
  ## How Atelier works
15
33
 
16
34
  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
@@ -34090,6 +34277,9 @@ var DEFAULT_MODELS3 = {
34090
34277
  var MANAGED_CODEX_KEYS = [
34091
34278
  { key: "model" },
34092
34279
  { key: "model_reasoning_effort" },
34280
+ { key: "plan_mode_reasoning_effort" },
34281
+ { key: "sandbox_mode" },
34282
+ { key: "approval_policy" },
34093
34283
  {
34094
34284
  key: "features",
34095
34285
  nested: { features: ["multi_agent"] }
@@ -34168,6 +34358,9 @@ function mergeHarnessConfig3(section, basePath) {
34168
34358
  ...existing,
34169
34359
  model: config.default_model,
34170
34360
  model_reasoning_effort: "medium",
34361
+ plan_mode_reasoning_effort: "high",
34362
+ sandbox_mode: "danger-full-access",
34363
+ approval_policy: "on-request",
34171
34364
  features: {
34172
34365
  ...isObject(existing.features) ? existing.features : {},
34173
34366
  multi_agent: true
@@ -34216,7 +34409,7 @@ function removeDirIfEmpty3(dir) {
34216
34409
  return;
34217
34410
  }
34218
34411
  try {
34219
- if (readdirSync3(dir).length === 0) {
34412
+ if (readdirSync4(dir).length === 0) {
34220
34413
  rmdirSync3(dir);
34221
34414
  }
34222
34415
  } catch (err2) {
@@ -34274,7 +34467,7 @@ function removeEmptyCodexDir(codexDir) {
34274
34467
  return;
34275
34468
  }
34276
34469
  try {
34277
- const entries = readdirSync3(codexDir);
34470
+ const entries = readdirSync4(codexDir);
34278
34471
  if (entries.length === 0) {
34279
34472
  rmSync4(codexDir, { recursive: true, force: true });
34280
34473
  }
@@ -34287,7 +34480,7 @@ function isObject(value) {
34287
34480
  }
34288
34481
 
34289
34482
  // 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";
34483
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, rmdirSync as rmdirSync4, writeFileSync as writeFileSync6 } from "fs";
34291
34484
  import { join as join10 } from "path";
34292
34485
  var CURSOR_MODELS = [
34293
34486
  "composer-2.5",
@@ -34378,7 +34571,7 @@ function remove5(section, basePath) {
34378
34571
  rmSync5(join10(agentsDir, `${agent.name}.md`), { force: true });
34379
34572
  }
34380
34573
  rmSync5(join10(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true });
34381
- if (existsSync6(agentsDir) && readdirSync4(agentsDir).length === 0) {
34574
+ if (existsSync6(agentsDir) && readdirSync5(agentsDir).length === 0) {
34382
34575
  rmdirSync4(agentsDir);
34383
34576
  }
34384
34577
  } 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.1.0",
4
4
  "description": "Personal development toolkit - spec-driven development, code quality, deep thinking, and ecosystem patterns",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,18 @@ The plan may be inline in the conversation or backed by persisted spec artifacts
33
33
  the mode before invoking an artifact-producing skill, state the choice with one brief
34
34
  reason, and let the human override it.
35
35
 
36
+ ## Governing Principles
37
+
38
+ Invoking a skill selects relevant guidance. It does not make every pattern in every loaded
39
+ skill part of the solution. Existing source, explicit requirements, and the smallest correct
40
+ change determine which patterns apply.
41
+
42
+ Begin with the current design and the minimum behavior-preserving change. Product,
43
+ infrastructure, and architectural expansion require explicit scope. Keep **ponytail** active
44
+ throughout development when it is available or required by the repository. Keep
45
+ **writing-clearly-and-concisely** active whenever writing or editing prose for humans. Before
46
+ returning or persisting that prose, apply **humanizer** in embedded mode.
47
+
36
48
  ## Planning Modes
37
49
 
38
50
  ### Inline Plan (default)
@@ -125,9 +137,10 @@ The workflow is not purely linear. Expect backflows:
125
137
  - Planning reveals design assumptions are wrong → back to brainstorm
126
138
  - Tasks can't be decomposed without more context → back to brainstorm
127
139
 
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
140
+ ### Implement → Plan (material changes)
141
+ - A minor deviation may be recorded inline and reflected in the plan when it does not change
142
+ approved behavior, scope, architecture, public contracts, or major dependencies.
143
+ - Material missing work or an unplanned dependency → revise the active plan and get approval.
131
144
 
132
145
  ### Implement → Research (fundamental issue)
133
146
  - Inline planning or implementation develops substantial needs → ask the human whether to