@xenonbyte/xsk 0.1.0 → 0.1.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
@@ -1,32 +1,46 @@
1
1
  # xsk
2
2
 
3
- Agent skill aggregator. `xsk` curates a small set of agent skills and installs them across Claude Code, Codex, opencode, and Gemini with manifest-backed safety.
3
+ > Curate a small set of agent skills and install them across Claude Code, Codex, opencode, and Gemini, with manifest-backed safety.
4
4
 
5
- Package: `@xenonbyte/xsk` · Binary: `xsk` · Runtime: Node >= 20, CommonJS, zero third-party runtime dependencies.
5
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-3c873a)](https://nodejs.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+ [![Runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](package.json)
8
+
9
+ `xsk` (`@xenonbyte/xsk`) is a zero-dependency CLI that installs a curated set of agent skills into every supported AI coding agent, then tracks exactly what it created so uninstall removes only those files.
6
10
 
7
11
  ## Overview
8
12
 
9
- Two recurring frictions when working across AI coding agents: third-party skill packs are all-or-nothing, and self-authored skills are scattered with no shared install/manifest/safety story. `xsk` solves both. It ships six curated skills (two distilled from third parties, four original) and a CLI that installs each skill into every supported platform's skill directory, then tracks exactly what it created so uninstall removes only those files.
13
+ Working across AI coding agents has two recurring frictions: third-party skill packs are all-or-nothing, and self-authored skills are scattered with no shared install/manifest/safety story. `xsk` solves both. It ships eight curated skills (two distilled from third parties, six original) and a CLI that installs each skill into every supported platform's skill directory, then records exactly what it created so uninstall removes only those files.
10
14
 
11
15
  `xsk` is itself an agent-skill project and conforms to the same standard its scaffold skill enforces.
12
16
 
17
+ ## Features
18
+
19
+ - **Eight curated skills**, not an all-or-nothing pack: install everything, or pick per platform.
20
+ - **Four platforms, one shape.** Claude Code, Codex, opencode, and Gemini share a `<name>/SKILL.md` layout; opencode additionally gets directly invocable `/xsk-<name>` commands.
21
+ - **Manifest-backed safety.** Owned-only removal, ownership markers, atomic writes, symlink refusal, and content-hash drift detection.
22
+ - **Uninstall-first installs.** A reinstall resets prior owned files (pruning skills no longer installed) before regenerating, so no manual `uninstall` is needed.
23
+ - **User edits are never clobbered.** A modified owned file is refused and rolled back instead of being overwritten.
24
+ - **Zero runtime dependencies.** Pure Node.js (>= 20), CommonJS.
25
+
13
26
  ## Installation
14
27
 
15
28
  ```sh
16
29
  npm install -g @xenonbyte/xsk
17
30
  ```
18
31
 
19
- Requires Node >= 20 on macOS or Linux.
32
+ > [!IMPORTANT]
33
+ > Requires Node >= 20 on macOS or Linux.
20
34
 
21
35
  ## Usage
22
36
 
23
37
  ```sh
24
- xsk install # install every skill into every platform
38
+ xsk install # install every skill into every platform
25
39
  xsk install --platform claude,codex
26
- xsk status # read-only: what is installed per platform
40
+ xsk status # read-only: what is installed per platform
27
41
  xsk status --json
28
- xsk uninstall # remove only what xsk created
29
- xsk doctor # probe environment + manifest health
42
+ xsk uninstall # remove only what xsk created
43
+ xsk doctor # probe environment + manifest health
30
44
  xsk version
31
45
  xsk help
32
46
  ```
@@ -46,18 +60,21 @@ Unknown options fail loud with a non-zero exit.
46
60
 
47
61
  ## Skills
48
62
 
49
- Six skills, prefixed `xsk-`:
63
+ Eight skills, prefixed `xsk-`:
50
64
 
51
65
  | Skill | Purpose |
52
66
  |---|---|
53
67
  | `xsk-think` | Turn a rough idea into a decision-complete plan before any code is written. Distilled from Waza `/think`. |
54
68
  | `xsk-bypass-claude` | Set the current project to Claude Code bypass-permissions mode by writing `.claude/settings.local.json`. Claude only. |
55
69
  | `xsk-skill-scaffold` | Bring an agent-skill project up to the `xsk` standard, or refuse if it is not one. |
56
- | `xsk-write-req` | Convert plain-language needs into a compliant requirement doc in `requirements/`, grounded in the current project. |
57
- | `xsk-archive-req` | Archive the active requirement doc into `requirements/archive/`. |
70
+ | `xsk-write-req` | Convert plain-language needs into a compliant requirement doc in `.xsk/requirements/`, grounded in the current project. |
71
+ | `xsk-archive-req` | Archive the active requirement doc into `.xsk/requirements/archive/`. |
58
72
  | `xsk-check` | Review a code change before it ships: scope drift, hard stops, evidence-gated findings, then verify and sign off. Distilled from Waza `/check`. |
73
+ | `xsk-point` | Research one aspect of the current project to a decision-complete landed plan and persist it as a point document in `.xsk/points/`. |
74
+ | `xsk-consume-point` | Fold selected `.xsk/points/` documents into one `.xsk/requirements/` doc via `xsk-write-req`, archiving consumed points write-before-remove. |
59
75
 
60
- `xsk-bypass-claude` targets Claude Code only; `xsk install` skips it on the other three platforms.
76
+ > [!NOTE]
77
+ > `xsk-bypass-claude` targets Claude Code only; `xsk install` skips it on the other three platforms.
61
78
 
62
79
  ## Platforms
63
80
 
@@ -67,10 +84,12 @@ All four platforms are full and use the same `<name>/SKILL.md` skill-directory s
67
84
  |---|---|
68
85
  | Claude Code | `~/.claude/skills/<name>/SKILL.md` |
69
86
  | Codex | `~/.agents/skills/<name>/SKILL.md` |
70
- | opencode | `~/.config/opencode/skills/<name>/SKILL.md` |
87
+ | opencode | `~/.config/opencode/skills/<name>/SKILL.md` (skill) and `~/.config/opencode/commands/xsk-<name>.md` (command) |
71
88
  | Gemini | `~/.gemini/skills/<name>/SKILL.md` |
72
89
 
73
- Platform behavior is verified as of 2026-06-25 against the official docs linked from the source repository's `docs/REQUIREMENTS.md`.
90
+ For opencode, `xsk install` writes both a skill directory entry and a flat `commands/xsk-<name>.md` command file for each installed skill, making each skill directly invocable as an opencode `/xsk-<name>` command. Both the skill and the command file are manifest-tracked, and uninstall removes them together.
91
+
92
+ Platform behavior was verified as of 2026-06-25 against the official docs linked from the source repository's requirement spec under `docs/`.
74
93
 
75
94
  ## Discovery aliases and duplicate skills
76
95
 
@@ -82,15 +101,16 @@ Because those aliases are cross-platform visible, a skill copied into one readab
82
101
 
83
102
  ## Safety
84
103
 
85
- Installing into user home config dirs is destructive if careless. `xsk` is manifest-backed:
104
+ > [!WARNING]
105
+ > Installing into user home config dirs is destructive if careless. `xsk` is manifest-backed so every write is owned and reversible.
86
106
 
87
- - Owned-only removal. Uninstall removes only paths the manifest recorded.
88
- - Ownership markers. A `.xsk-owned` marker inside each installed skill dir gates directory removal.
89
- - No symlink traversal or removal. `xsk` refuses on encounter.
90
- - Atomic writes. Each file is written to a temp sibling then renamed into place; a failed write restores the original.
91
- - User edits preserved. If a generated file was user-modified, uninstall keeps it, reports a partial result, and narrows the retained manifest so a later run can finish.
107
+ - **Owned-only removal.** Uninstall removes only paths the manifest recorded.
108
+ - **Ownership markers.** A `.xsk-owned` marker inside each installed skill dir gates directory removal.
109
+ - **No symlink traversal or removal.** `xsk` refuses on encounter.
110
+ - **Atomic writes.** Each file is written to a temp sibling then renamed into place; a failed write restores the original.
111
+ - **User edits preserved.** If a generated file was user-modified, uninstall keeps it, reports a partial result, and narrows the retained manifest so a later run can finish.
92
112
 
93
- `xsk` writes only under `~/.xsk/` and the four platform skill dirs.
113
+ `xsk` writes only under `~/.xsk/`, the four platform skill dirs, and opencode's `~/.config/opencode/commands/` command dir.
94
114
 
95
115
  ## Development
96
116
 
@@ -100,8 +120,8 @@ npm run syntaxcheck # node --check every bin/, lib/, test/ file
100
120
  npm pack --dry-run # verify package contents
101
121
  ```
102
122
 
103
- The full requirement specification lives in the source repository at `docs/REQUIREMENTS.md`.
123
+ The full requirement specification lives in the source repository under `docs/`.
104
124
 
105
- ## License
125
+ ---
106
126
 
107
- MIT
127
+ MIT licensed. See [LICENSE](LICENSE).
package/README.zh-CN.md CHANGED
@@ -1,32 +1,46 @@
1
1
  # xsk
2
2
 
3
- Agent skill aggregator(智能体技能聚合器)。`xsk` 精选了一小组 agent skill,并以 manifest 记录作为安全保障,将它们安装到 Claude Code、Codex、opencode 与 Gemini 四个平台。
3
+ > 精选一小组 agent skill,以 manifest 记录作为安全保障,安装到 Claude Code、Codex、opencode 与 Gemini
4
4
 
5
- Package: `@xenonbyte/xsk` · Binary: `xsk` · Runtime: Node >= 20, CommonJS, 零第三方运行时依赖。
5
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-3c873a)](https://nodejs.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+ [![Runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](package.json)
8
+
9
+ `xsk`(`@xenonbyte/xsk`)是一个零依赖 CLI,把一组精选的 agent skill 安装到每个受支持的 AI coding agent,并精确记录它创建了哪些文件,使 uninstall 只移除这些文件。
6
10
 
7
11
  ## Overview
8
12
 
9
- 跨多个 AI coding agent 工作时有两类反复出现的摩擦:第三方 skill 包要么全装要么不装(all-or-nothing),自写的 skill 又散落各处、缺少统一的安装 / manifest / 安全方案。`xsk` 同时解决这两点。它内置六个精选 skill(两个从第三方蒸馏而来,四个原创),并提供一个 CLI,把每个 skill 安装到所有受支持平台的 skill 目录,再精确记录它创建了哪些文件,使 uninstall 只移除这些文件。
13
+ 跨多个 AI coding agent 工作时有两类反复出现的摩擦:第三方 skill 包要么全装要么不装(all-or-nothing),自写的 skill 又散落各处、缺少统一的安装 / manifest / 安全方案。`xsk` 同时解决这两点。它内置八个精选 skill(两个从第三方蒸馏而来,六个原创),并提供一个 CLI,把每个 skill 安装到所有受支持平台的 skill 目录,再精确记录它创建了哪些文件,使 uninstall 只移除这些文件。
10
14
 
11
15
  `xsk` 本身就是一个 agent-skill 项目,并符合它自己的 scaffold skill 所执行的同一套标准。
12
16
 
17
+ ## Features
18
+
19
+ - **八个精选 skill**,不是全装或不装的整包,可全部安装,也可按平台挑选。
20
+ - **四个平台,同一形态。** Claude Code、Codex、opencode 与 Gemini 共用 `<name>/SKILL.md` 布局;opencode 还额外获得可直接调用的 `/xsk-<name>` 命令。
21
+ - **manifest 为后盾的安全。** owned-only removal、ownership markers、atomic writes、symlink refusal,以及 content-hash 漂移检测。
22
+ - **uninstall-first 安装。** 重装会先重置此前 owned 的文件(清理已不再安装的 skill)再生成,无需手动 `uninstall`。
23
+ - **绝不覆盖用户改动。** 被改过的 owned 文件会被拒绝并回滚,而不会被覆盖。
24
+ - **零运行时依赖。** 纯 Node.js(>= 20),CommonJS。
25
+
13
26
  ## Installation
14
27
 
15
28
  ```sh
16
29
  npm install -g @xenonbyte/xsk
17
30
  ```
18
31
 
19
- 要求 macOS 或 Linux 上的 Node >= 20。
32
+ > [!IMPORTANT]
33
+ > 要求 macOS 或 Linux 上的 Node >= 20。
20
34
 
21
35
  ## Usage
22
36
 
23
37
  ```sh
24
- xsk install # 把所有 skill 安装到所有平台
38
+ xsk install # 把所有 skill 安装到所有平台
25
39
  xsk install --platform claude,codex
26
- xsk status # 只读:查看每个平台已安装的内容
40
+ xsk status # 只读:查看每个平台已安装的内容
27
41
  xsk status --json
28
- xsk uninstall # 只移除 xsk 创建的内容
29
- xsk doctor # 探测环境与 manifest 健康状况
42
+ xsk uninstall # 只移除 xsk 创建的内容
43
+ xsk doctor # 探测环境与 manifest 健康状况
30
44
  xsk version
31
45
  xsk help
32
46
  ```
@@ -46,18 +60,21 @@ xsk help
46
60
 
47
61
  ## Skills
48
62
 
49
- 共六个 skill,统一前缀 `xsk-`:
63
+ 共八个 skill,统一前缀 `xsk-`:
50
64
 
51
65
  | Skill | Purpose |
52
66
  |---|---|
53
67
  | `xsk-think` | 把一个粗略想法在写任何代码之前变成 decision-complete 的 plan。蒸馏自 Waza `/think`。 |
54
68
  | `xsk-bypass-claude` | 通过写 `.claude/settings.local.json` 把当前项目设为 Claude Code bypass-permissions 模式。仅 Claude。 |
55
69
  | `xsk-skill-scaffold` | 把一个 agent-skill 项目带到 `xsk` 标准;若不是这类项目则拒绝。 |
56
- | `xsk-write-req` | 把白话需求转成 `requirements/` 下合规的需求文档,并扎根于当前项目。 |
57
- | `xsk-archive-req` | 把当前 active 需求文档归档到 `requirements/archive/`。 |
70
+ | `xsk-write-req` | 把白话需求转成 `.xsk/requirements/` 下合规的需求文档,并扎根于当前项目。 |
71
+ | `xsk-archive-req` | 把当前 active 需求文档归档到 `.xsk/requirements/archive/`。 |
58
72
  | `xsk-check` | 在改动合入前评审:范围漂移、hard stops、证据门控的发现项,再验证后签收。蒸馏自 Waza `/check`。 |
73
+ | `xsk-point` | 把当前项目某一方面研究到 decision-complete 的落地方案,并作为 point 文档持久化到 `.xsk/points/`。 |
74
+ | `xsk-consume-point` | 通过 `xsk-write-req` 把选定的 `.xsk/points/` 文档折叠进一份 `.xsk/requirements/` 文档,以 write-before-remove 方式归档已消费的 point。 |
59
75
 
60
- `xsk-bypass-claude` 仅面向 Claude Code;`xsk install` 会在其余三个平台跳过它。
76
+ > [!NOTE]
77
+ > `xsk-bypass-claude` 仅面向 Claude Code;`xsk install` 会在其余三个平台跳过它。
61
78
 
62
79
  ## Platforms
63
80
 
@@ -67,10 +84,12 @@ xsk help
67
84
  |---|---|
68
85
  | Claude Code | `~/.claude/skills/<name>/SKILL.md` |
69
86
  | Codex | `~/.agents/skills/<name>/SKILL.md` |
70
- | opencode | `~/.config/opencode/skills/<name>/SKILL.md` |
87
+ | opencode | `~/.config/opencode/skills/<name>/SKILL.md`(skill)和 `~/.config/opencode/commands/xsk-<name>.md`(command) |
71
88
  | Gemini | `~/.gemini/skills/<name>/SKILL.md` |
72
89
 
73
- 平台行为依据源仓库 `docs/REQUIREMENTS.md` 中链接的官方文档,校验日期为 2026-06-25。
90
+ 对于 opencode,`xsk install` 会为每个已安装的 skill 同时写入 skill 目录条目和平坦的 `commands/xsk-<name>.md` 命令文件,使每个 skill 都可作为 opencode `/xsk-<name>` 命令直接调用。skill 与命令文件均由 manifest 跟踪,卸载时一并移除。
91
+
92
+ 平台行为依据源仓库 `docs/` 下需求规格中链接的官方文档,校验日期为 2026-06-25。
74
93
 
75
94
  ## Discovery aliases and duplicate skills
76
95
 
@@ -82,15 +101,16 @@ opencode 除了自己的 `~/.config/opencode/skills/<name>/SKILL.md`,也会读
82
101
 
83
102
  ## Safety
84
103
 
85
- 向用户 home 配置目录写入若不小心具有破坏性。`xsk` 以 manifest 为后盾:
104
+ > [!WARNING]
105
+ > 向用户 home 配置目录写入若不小心具有破坏性。`xsk` 以 manifest 为后盾,使每次写入都是 owned 且可逆的。
86
106
 
87
- - Owned-only removaluninstall 只移除 manifest 记录的路径。
88
- - Ownership markers。每个已安装 skill 目录内有 `.xsk-owned` 标记,目录移除需先校验该标记。
89
- - No symlink traversal or removal。`xsk` 遇到 symlink 会拒绝。
90
- - Atomic writes。每个文件先写入临时同目录文件,再 rename 就位;写入失败时恢复原文件。
91
- - User edits preserved。若生成的文件被用户修改,uninstall 会保留它、报告 partial,并收窄保留的 manifest,使后续可继续完成。
107
+ - **Owned-only removal。** uninstall 只移除 manifest 记录的路径。
108
+ - **Ownership markers。** 每个已安装 skill 目录内有 `.xsk-owned` 标记,目录移除需先校验该标记。
109
+ - **No symlink traversal or removal。** `xsk` 遇到 symlink 会拒绝。
110
+ - **Atomic writes。** 每个文件先写入临时同目录文件,再 rename 就位;写入失败时恢复原文件。
111
+ - **User edits preserved。** 若生成的文件被用户修改,uninstall 会保留它、报告 partial,并收窄保留的 manifest,使后续可继续完成。
92
112
 
93
- `xsk` 只在 `~/.xsk/` 与四个平台 skill 目录下写入。
113
+ `xsk` 只在 `~/.xsk/`、四个平台 skill 目录,以及 opencode 的 `~/.config/opencode/commands/` command 目录下写入。
94
114
 
95
115
  ## Development
96
116
 
@@ -100,8 +120,8 @@ npm run syntaxcheck # 对 bin/、lib/、test/ 下每个文件执行 node --check
100
120
  npm pack --dry-run # 校验 package 内容
101
121
  ```
102
122
 
103
- 完整需求规格见源仓库中的 `docs/REQUIREMENTS.md`。
123
+ 完整需求规格见源仓库 `docs/` 目录。
104
124
 
105
- ## License
125
+ ---
106
126
 
107
- MIT
127
+ 基于 MIT 许可证发布。见 [LICENSE](LICENSE)。
package/bin/xsk.js CHANGED
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const { parse } = require('../lib/input');
5
- const { install } = require('../lib/install');
5
+ const { install, isCommandFilePath } = require('../lib/install');
6
6
  const { uninstall } = require('../lib/uninstall');
7
7
  const { computeStatus, render: renderStatus } = require('../lib/status');
8
8
  const { doctor, render: renderDoctor } = require('../lib/capability');
@@ -41,8 +41,12 @@ function formatInstall(summary) {
41
41
  continue;
42
42
  }
43
43
  const skillCount = (r.installed || []).filter((p) => p.endsWith('SKILL.md')).length;
44
+ const commandCount = (r.installed || []).filter((p) => isCommandFilePath(p)).length;
44
45
  const backCount = (r.backups || []).length;
45
46
  let line = `${platform}: installed ${skillCount} skill${skillCount === 1 ? '' : 's'}`;
47
+ if (commandCount > 0) {
48
+ line += `; installed ${commandCount} command${commandCount === 1 ? '' : 's'}`;
49
+ }
46
50
  if (backCount > 0) {
47
51
  line += `; backed up ${backCount} displaced file${backCount === 1 ? '' : 's'}`;
48
52
  }
@@ -64,9 +68,13 @@ function formatUninstall(summary) {
64
68
  continue;
65
69
  }
66
70
  const removedCount = (r.removed || []).filter((p) => p.endsWith('SKILL.md')).length;
71
+ const removedCommandCount = (r.removed || []).filter((p) => isCommandFilePath(p)).length;
67
72
  const restoredCount = (r.restored || []).length;
68
73
  const retainedCount = (r.retained || []).length;
69
74
  let line = `${platform}: removed ${removedCount} skill${removedCount === 1 ? '' : 's'}`;
75
+ if (removedCommandCount > 0) {
76
+ line += `; removed ${removedCommandCount} command${removedCommandCount === 1 ? '' : 's'}`;
77
+ }
70
78
  if (restoredCount > 0) {
71
79
  line += `, restored ${restoredCount} displaced file${restoredCount === 1 ? '' : 's'}`;
72
80
  }
@@ -102,6 +110,7 @@ function main(argv, options) {
102
110
 
103
111
  const dispatchOptions = {
104
112
  platformRoots: opts.platformRoots,
113
+ platformCommandsRoots: opts.platformCommandsRoots,
105
114
  xskRoot: opts.xskRoot,
106
115
  };
107
116
 
@@ -11,4 +11,10 @@ function skillsRoot(options) {
11
11
  return path.join(home, '.config', 'opencode', 'skills');
12
12
  }
13
13
 
14
- module.exports = { PLATFORM, skillsRoot };
14
+ function commandsRoot(options) {
15
+ const opts = options || {};
16
+ const home = opts.home || os.homedir();
17
+ return path.join(home, '.config', 'opencode', 'commands');
18
+ }
19
+
20
+ module.exports = { PLATFORM, skillsRoot, commandsRoot };
package/lib/capability.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
6
  const { defaultXskRoot, isSafePath } = require('./manifest');
7
- const { rootFor, ALL_PLATFORMS } = require('./install');
7
+ const { rootFor, commandsRootFor, ALL_PLATFORMS } = require('./install');
8
8
  const { computeStatus } = require('./status');
9
9
 
10
10
  const REQUIRED_NODE_MAJOR = 20;
@@ -77,9 +77,36 @@ function doctor(options) {
77
77
  pass: isWritableDir(root),
78
78
  detail: isWritableDir(root) ? 'writable or creatable' : 'not writable',
79
79
  });
80
+
81
+ let commandsRoot;
82
+ try {
83
+ commandsRoot = commandsRootFor(platform, opts.platformCommandsRoots);
84
+ } catch (e) {
85
+ checks.push({
86
+ name: `writable-${platform}-commands`,
87
+ label: `${platform} commands dir writable`,
88
+ pass: false,
89
+ detail: `could not resolve commands dir: ${e.message}`,
90
+ });
91
+ continue;
92
+ }
93
+ if (commandsRoot) {
94
+ const writableCommandsRoot = isWritableDir(commandsRoot);
95
+ checks.push({
96
+ name: `writable-${platform}-commands`,
97
+ label: `${platform} commands dir writable (${commandsRoot})`,
98
+ pass: writableCommandsRoot,
99
+ detail: writableCommandsRoot ? 'writable or creatable' : 'not writable',
100
+ });
101
+ }
80
102
  }
81
103
 
82
- const status = computeStatus({ platforms, xskRoot, platformRoots: opts.platformRoots });
104
+ const status = computeStatus({
105
+ platforms,
106
+ xskRoot,
107
+ platformRoots: opts.platformRoots,
108
+ platformCommandsRoots: opts.platformCommandsRoots,
109
+ });
83
110
  let checkedManifests = 0;
84
111
  let invalidManifests = 0;
85
112
  let driftedManifests = 0;
package/lib/install.js CHANGED
@@ -29,6 +29,42 @@ function rootFor(platform, platformRoots) {
29
29
  return adapter.skillsRoot();
30
30
  }
31
31
 
32
+ function commandsRootFor(platform, platformCommandsRoots) {
33
+ if (platformCommandsRoots && platformCommandsRoots[platform]) {
34
+ return platformCommandsRoots[platform];
35
+ }
36
+ const adapter = require(`./adapters/${platform}.js`);
37
+ return typeof adapter.commandsRoot === 'function' ? adapter.commandsRoot() : null;
38
+ }
39
+
40
+ // A command file is a flat, markerless `.md` recorded in installed_paths whose
41
+ // basename is not the skill file (`SKILL.md`) or the ownership marker. The
42
+ // function set is basename-only in places, so ownership/classification keys on
43
+ // the basename; membership under commandsRoot is the operational-semantics check.
44
+ function isCommandFilePath(p) {
45
+ const base = path.basename(p);
46
+ return base !== 'SKILL.md' && base !== MARKER && base.endsWith('.md');
47
+ }
48
+
49
+ // opencode command body: the generated skill content (already carrying the
50
+ // `description:` frontmatter) plus, when the content lacks `$ARGUMENTS`, a
51
+ // skill-referring invocation section ending in a fenced `$ARGUMENTS` placeholder.
52
+ function buildCommandFileContent(skillContent) {
53
+ if (skillContent.includes('$ARGUMENTS')) {
54
+ return skillContent;
55
+ }
56
+ const prefix = skillContent.endsWith('\n') ? skillContent : `${skillContent}\n`;
57
+ return (
58
+ `${prefix}\n` +
59
+ '## opencode invocation arguments\n\n' +
60
+ 'Use these arguments when running the skill above:\n\n' +
61
+ '```text\n' +
62
+ '$ARGUMENTS\n' +
63
+ '```\n\n' +
64
+ 'If no arguments were supplied, follow the default usage.\n'
65
+ );
66
+ }
67
+
32
68
  function writeGeneratedFile(targetPath, content, label) {
33
69
  atomicWriteFile(targetPath, content, {
34
70
  dirLabel: `${label} dir`,
@@ -79,6 +115,7 @@ function previousInstallState(platform, xskRoot) {
79
115
  backupsByTarget: new Map(),
80
116
  installedDirs: new Set(),
81
117
  installedFiles: new Set(),
118
+ installedCommands: new Set(),
82
119
  installedHashesByTarget: new Map(),
83
120
  };
84
121
  let previous;
@@ -96,10 +133,11 @@ function previousInstallState(platform, xskRoot) {
96
133
  if (!validate(previous, { expectedPlatform: platform })) {
97
134
  throw new Error(`manifest for ${platform} failed shape validation; refusing to install`);
98
135
  }
136
+ const installedCommands = new Set(previous.installed_paths.filter(isCommandFilePath));
99
137
  const installedDirs = new Set(
100
138
  previous.installed_paths.filter((p) => {
101
139
  const base = path.basename(p);
102
- return base !== 'SKILL.md' && base !== MARKER;
140
+ return base !== 'SKILL.md' && base !== MARKER && !isCommandFilePath(p);
103
141
  }),
104
142
  );
105
143
  const installedFiles = new Set(
@@ -110,6 +148,7 @@ function previousInstallState(platform, xskRoot) {
110
148
  backupsByTarget: new Map(previous.backups.map((b) => [b.target, { target: b.target, backup: b.backup }])),
111
149
  installedDirs,
112
150
  installedFiles,
151
+ installedCommands,
113
152
  installedHashesByTarget: new Map(installedHashes.map((h) => [h.target, h.sha256])),
114
153
  };
115
154
  }
@@ -232,7 +271,7 @@ function restorePathState(snapshot) {
232
271
  }
233
272
  }
234
273
 
235
- function capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills }) {
274
+ function capturePlatformSnapshot({ platform, skillsRoot, commandsRoot, xskRoot, skills }) {
236
275
  const seen = new Set();
237
276
  const paths = [];
238
277
  const platformBackupDir = path.join(xskRoot, 'install', 'backups', platform);
@@ -256,6 +295,12 @@ function capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills }) {
256
295
  add(path.join(skillDir, MARKER));
257
296
  add(path.join(xskRoot, 'install', 'backups', platform, `${skill.name}.SKILL.md.bak`));
258
297
  }
298
+ if (commandsRoot) {
299
+ add(commandsRoot);
300
+ for (const skill of skills) {
301
+ add(path.join(commandsRoot, `${skill.name}.md`));
302
+ }
303
+ }
259
304
  // Cover the prior manifest's owned paths too, so an uninstall-first reset of
260
305
  // skills no longer in the install set can still be rolled back on failure.
261
306
  let prior = null;
@@ -267,12 +312,14 @@ function capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills }) {
267
312
  if (prior && typeof prior === 'object') {
268
313
  const priorPaths = Array.isArray(prior.installed_paths) ? prior.installed_paths : [];
269
314
  for (const p of priorPaths) {
270
- if (!isInsideDir(p, skillsRoot)) {
315
+ const insideSkills = isInsideDir(p, skillsRoot);
316
+ const insideCommands = commandsRoot ? isInsideDir(p, commandsRoot) : false;
317
+ if (!insideSkills && !insideCommands) {
271
318
  continue;
272
319
  }
273
320
  add(p);
274
321
  const base = path.basename(p);
275
- if (base === 'SKILL.md' || base === MARKER) {
322
+ if (insideSkills && (base === 'SKILL.md' || base === MARKER)) {
276
323
  add(path.dirname(p));
277
324
  }
278
325
  }
@@ -299,7 +346,7 @@ function restorePlatformSnapshot(snapshot) {
299
346
  }
300
347
  }
301
348
 
302
- function installPlatform({ platform, skillsRoot, xskRoot, version, skills }) {
349
+ function installPlatform({ platform, skillsRoot, commandsRoot, xskRoot, version, skills }) {
303
350
  const manifest = create(platform, version);
304
351
  const installed = [];
305
352
  const backups = [];
@@ -314,6 +361,13 @@ function installPlatform({ platform, skillsRoot, xskRoot, version, skills }) {
314
361
 
315
362
  try {
316
363
  ensurePlatformRoot(skillsRoot);
364
+ if (commandsRoot) {
365
+ const commandsRootExisted = fs.existsSync(commandsRoot);
366
+ ensurePlatformRoot(commandsRoot);
367
+ if (!commandsRootExisted) {
368
+ createdThisRun.push(commandsRoot);
369
+ }
370
+ }
317
371
  for (const skill of skills) {
318
372
  const built = buildSkill(skill);
319
373
  const skillDir = path.join(skillsRoot, skill.name);
@@ -419,6 +473,36 @@ function installPlatform({ platform, skillsRoot, xskRoot, version, skills }) {
419
473
  assertReusableBackupRecord(backup, backupDir);
420
474
  backups.push({ target: backup.target, backup: backup.backup });
421
475
  }
476
+
477
+ if (commandsRoot) {
478
+ const commandFile = path.join(commandsRoot, `${skill.name}.md`);
479
+ const commandContent = buildCommandFileContent(built.content);
480
+ assertNotSymlink(commandFile, 'command file');
481
+ const commandFileExisted = fs.existsSync(commandFile);
482
+ if (commandFileExisted) {
483
+ const stat = fs.statSync(commandFile);
484
+ const existingCommandContent = fs.readFileSync(commandFile);
485
+ // Command files carry no ownership marker, so adoption rests solely on
486
+ // a hash match against the prior manifest: adopt a previously-installed
487
+ // generated command file, refuse a user-edited or unrecorded one.
488
+ const adoptable = isPreviouslyInstalledGeneratedContent({
489
+ existingContent: existingCommandContent,
490
+ builtContent: commandContent,
491
+ previousHash: installedHashesByTarget.get(commandFile),
492
+ wasInstalled: previousState.installedCommands.has(commandFile),
493
+ });
494
+ if (!adoptable) {
495
+ throw new Error(`refusing to overwrite user-edited command file: ${commandFile}`);
496
+ }
497
+ overwrittenThisRun.push({ target: commandFile, content: existingCommandContent, mode: stat.mode });
498
+ }
499
+ writeGeneratedFile(commandFile, commandContent, 'command file');
500
+ if (!commandFileExisted) {
501
+ createdThisRun.push(commandFile);
502
+ }
503
+ installed.push(commandFile);
504
+ installedHashes.push({ target: commandFile, sha256: contentSha256(commandContent) });
505
+ }
422
506
  }
423
507
 
424
508
  manifest.installed_paths = installed;
@@ -444,6 +528,7 @@ function install(options) {
444
528
  try {
445
529
  for (const platform of selectedPlatforms) {
446
530
  const skillsRoot = rootFor(platform, opts.platformRoots);
531
+ const commandsRoot = commandsRootFor(platform, opts.platformCommandsRoots);
447
532
  const applicable = skillsToInstall.filter((s) => s.platforms.includes(platform));
448
533
  if (applicable.length === 0) {
449
534
  summary.platforms[platform] = { platform, installed: [], backups: [], manifest: null, skipped: true };
@@ -452,7 +537,7 @@ function install(options) {
452
537
  // Snapshot the full platform state (including the prior manifest's owned
453
538
  // paths) before any mutation, so the uninstall-first reset and the
454
539
  // install roll back together on any failure.
455
- const snapshot = capturePlatformSnapshot({ platform, skillsRoot, xskRoot, skills: applicable });
540
+ const snapshot = capturePlatformSnapshot({ platform, skillsRoot, commandsRoot, xskRoot, skills: applicable });
456
541
  snapshots.push(snapshot);
457
542
 
458
543
  // Uninstall-first: remove the prior owned state before regenerating, so a
@@ -463,7 +548,7 @@ function install(options) {
463
548
  // retained skill in place -- re-adopting a marker-less generated file, or
464
549
  // refusing to overwrite a user-edited owned skill (which rolls back). An
465
550
  // invalid prior manifest is the one case we refuse outright.
466
- const reset = uninstallPlatform({ platform, xskRoot, skillsRoot });
551
+ const reset = uninstallPlatform({ platform, xskRoot, skillsRoot, commandsRoot });
467
552
  if (reset.invalid) {
468
553
  throw new Error(`refusing to reinstall ${platform}: existing manifest is invalid; uninstall or fix it first`);
469
554
  }
@@ -471,6 +556,7 @@ function install(options) {
471
556
  summary.platforms[platform] = installPlatform({
472
557
  platform,
473
558
  skillsRoot,
559
+ commandsRoot,
474
560
  xskRoot,
475
561
  version,
476
562
  skills: applicable,
@@ -490,6 +576,9 @@ module.exports = {
490
576
  installPlatform,
491
577
  atomicWriteFile,
492
578
  rootFor,
579
+ commandsRootFor,
580
+ isCommandFilePath,
581
+ buildCommandFileContent,
493
582
  PACKAGE_NAME,
494
583
  MARKER,
495
584
  ALL_PLATFORMS,
package/lib/manifest.js CHANGED
@@ -83,15 +83,35 @@ function isInsideDir(child, parent) {
83
83
  function validateOperationalSemantics(options) {
84
84
  const opts = options || {};
85
85
  const skillsRoot = opts.skillsRoot;
86
+ // Optional second owned root for platforms (opencode) that install flat command
87
+ // files outside skillsRoot. Platforms without a commandsRoot stay skillsRoot-only.
88
+ const commandsRoot = opts.commandsRoot;
86
89
  const paths = Array.isArray(opts.manifest && opts.manifest.installed_paths) ? opts.manifest.installed_paths : [];
87
90
  for (const p of paths) {
88
- if (!isInsideDir(p, skillsRoot)) {
91
+ const isCommandPath = isManifestCommandFilePath(p);
92
+ const insideSkills = isInsideDir(p, skillsRoot);
93
+ const insideCommands = commandsRoot ? isInsideDir(p, commandsRoot) : false;
94
+ if (isCommandPath) {
95
+ if (!commandsRoot) {
96
+ return { valid: false, reason: `command path requires commands root: ${p}` };
97
+ }
98
+ if (!insideCommands) {
99
+ return { valid: false, reason: `command path escapes commands root: ${p}` };
100
+ }
101
+ continue;
102
+ }
103
+ if (!insideSkills) {
89
104
  return { valid: false, reason: `installed path escapes platform root: ${p}` };
90
105
  }
91
106
  }
92
107
  return { valid: true };
93
108
  }
94
109
 
110
+ function isManifestCommandFilePath(p) {
111
+ const base = path.basename(p);
112
+ return base !== 'SKILL.md' && base.endsWith('.md');
113
+ }
114
+
95
115
  function copyInstalledHashes(records) {
96
116
  if (!Array.isArray(records)) {
97
117
  return [];
package/lib/skills.js CHANGED
@@ -27,17 +27,31 @@ const skills = [
27
27
  {
28
28
  name: 'xsk-write-req',
29
29
  description:
30
- 'Turn plain-language needs into a grounded requirement document in requirements/, with a self-audit gate before it is finalized.',
30
+ 'Turn plain-language needs into a grounded requirement document in .xsk/requirements/, with a self-audit gate before it is finalized.',
31
31
  platforms: ALL_PLATFORMS.slice(),
32
32
  fragmentBase: 'write-req',
33
33
  },
34
34
  {
35
35
  name: 'xsk-archive-req',
36
36
  description:
37
- 'Archive the active requirement document into requirements/archive/ and leave zero active docs.',
37
+ 'Archive the active requirement document into .xsk/requirements/archive/ and leave zero active docs.',
38
38
  platforms: ALL_PLATFORMS.slice(),
39
39
  fragmentBase: 'archive-req',
40
40
  },
41
+ {
42
+ name: 'xsk-point',
43
+ description:
44
+ 'Research one aspect into .xsk/points/ to a decision-complete landed plan using xsk-think discipline, then persist and manage it as a named point document.',
45
+ platforms: ALL_PLATFORMS.slice(),
46
+ fragmentBase: 'point',
47
+ },
48
+ {
49
+ name: 'xsk-consume-point',
50
+ description:
51
+ 'Fold selected .xsk/points into one .xsk/requirements doc via xsk-write-req, archiving consumed points write-before-remove and leaving unconsumed points active.',
52
+ platforms: ALL_PLATFORMS.slice(),
53
+ fragmentBase: 'consume-point',
54
+ },
41
55
  {
42
56
  name: 'xsk-check',
43
57
  description: