@fro.bot/systematic 2.33.3 → 3.0.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.
Files changed (112) hide show
  1. package/ATTRIBUTIONS.md +1 -1
  2. package/agents/design/design-iterator.md +1 -0
  3. package/agents/research/best-practices-researcher.md +6 -8
  4. package/agents/review/pattern-recognition-specialist.md +1 -1
  5. package/agents/workflow/systematic-implementer.md +1 -0
  6. package/dist/cli.js +348 -53
  7. package/dist/{index-vyzhzvap.js → index-yskzf7vh.js} +587 -386
  8. package/dist/index.js +100 -155
  9. package/dist/lib/agent-overlays.d.ts +0 -1
  10. package/dist/lib/agent-resolver.d.ts +28 -0
  11. package/dist/lib/bootstrap.d.ts +8 -1
  12. package/dist/lib/bundled-names.d.ts +3 -3
  13. package/dist/lib/pi-delegate-session.d.ts +31 -0
  14. package/dist/lib/pi-delegate-tool.d.ts +42 -0
  15. package/dist/lib/removed-names.d.ts +3 -3
  16. package/dist/lib/setup.d.ts +36 -0
  17. package/dist/lib/skill-loader.d.ts +1 -2
  18. package/dist/lib/skill-resolver.d.ts +39 -0
  19. package/dist/lib/skill-tool.d.ts +2 -11
  20. package/dist/lib/skills.d.ts +1 -9
  21. package/dist/lib/source-model-defaults.d.ts +1 -1
  22. package/dist/pi.d.ts +7 -0
  23. package/dist/pi.js +3641 -0
  24. package/dist/schemas/systematic-config.schema.json +47 -242
  25. package/package.json +29 -6
  26. package/skills/ce-brainstorm/references/handoff.md +5 -26
  27. package/skills/ce-brainstorm/references/universal-brainstorming.md +0 -1
  28. package/skills/ce-compound/SKILL.md +8 -12
  29. package/skills/ce-ideate/references/post-ideation-workflow.md +21 -85
  30. package/skills/ce-plan/SKILL.md +6 -6
  31. package/skills/ce-plan/references/deepening-workflow.md +6 -7
  32. package/skills/ce-plan/references/plan-handoff.md +1 -17
  33. package/skills/ce-plan/references/universal-planning.md +0 -4
  34. package/skills/ce-review/SKILL.md +10 -19
  35. package/skills/ce-review/references/persona-catalog.md +2 -7
  36. package/skills/ce-review/references/review-output-template.md +0 -1
  37. package/skills/ce-work/SKILL.md +1 -1
  38. package/skills/deepen-plan/SKILL.md +6 -7
  39. package/skills/document-review/SKILL.md +2 -2
  40. package/skills/git-commit-push-pr/SKILL.md +1 -2
  41. package/skills/lfg/SKILL.md +1 -1
  42. package/skills/onboarding/SKILL.md +1 -12
  43. package/skills/orchestrating-subagents/SKILL.md +2 -2
  44. package/skills/slfg/SKILL.md +2 -3
  45. package/skills/test-browser/SKILL.md +2 -2
  46. package/skills/todos/SKILL.md +239 -0
  47. package/skills/writing-skills/SKILL.md +102 -1
  48. package/skills/{writing-systematic-skills → writing-skills}/references/foundation-conventions.md +5 -5
  49. package/agents/design/design-implementation-reviewer.md +0 -95
  50. package/agents/design/figma-design-sync.md +0 -173
  51. package/agents/docs/ankane-readme-writer.md +0 -51
  52. package/agents/review/cli-agent-readiness-reviewer.md +0 -418
  53. package/agents/review/data-integrity-guardian.md +0 -72
  54. package/agents/review/data-migration-expert.md +0 -99
  55. package/agents/review/dhh-rails-reviewer.md +0 -46
  56. package/agents/review/julik-frontend-races-reviewer.md +0 -49
  57. package/agents/review/kieran-python-reviewer.md +0 -47
  58. package/agents/review/kieran-rails-reviewer.md +0 -47
  59. package/agents/review/performance-oracle.md +0 -112
  60. package/agents/review/schema-drift-detector.md +0 -143
  61. package/agents/review/security-sentinel.md +0 -95
  62. package/agents/workflow/lint.md +0 -17
  63. package/dist/lib/converter.d.ts +0 -14
  64. package/skills/andrew-kane-gem-writer/SKILL.md +0 -184
  65. package/skills/andrew-kane-gem-writer/references/database-adapters.md +0 -231
  66. package/skills/andrew-kane-gem-writer/references/module-organization.md +0 -121
  67. package/skills/andrew-kane-gem-writer/references/rails-integration.md +0 -183
  68. package/skills/andrew-kane-gem-writer/references/resources.md +0 -119
  69. package/skills/andrew-kane-gem-writer/references/testing-patterns.md +0 -261
  70. package/skills/changelog/SKILL.md +0 -139
  71. package/skills/claude-permissions-optimizer/SKILL.md +0 -166
  72. package/skills/claude-permissions-optimizer/scripts/extract-commands.mjs +0 -655
  73. package/skills/claude-permissions-optimizer/scripts/normalize.mjs +0 -151
  74. package/skills/dhh-rails-style/SKILL.md +0 -186
  75. package/skills/dhh-rails-style/references/architecture.md +0 -653
  76. package/skills/dhh-rails-style/references/controllers.md +0 -303
  77. package/skills/dhh-rails-style/references/frontend.md +0 -510
  78. package/skills/dhh-rails-style/references/gems.md +0 -266
  79. package/skills/dhh-rails-style/references/models.md +0 -359
  80. package/skills/dhh-rails-style/references/testing.md +0 -338
  81. package/skills/dspy-ruby/SKILL.md +0 -737
  82. package/skills/dspy-ruby/assets/config-template.rb +0 -187
  83. package/skills/dspy-ruby/assets/module-template.rb +0 -300
  84. package/skills/dspy-ruby/assets/signature-template.rb +0 -221
  85. package/skills/dspy-ruby/references/core-concepts.md +0 -674
  86. package/skills/dspy-ruby/references/observability.md +0 -366
  87. package/skills/dspy-ruby/references/optimization.md +0 -603
  88. package/skills/dspy-ruby/references/providers.md +0 -418
  89. package/skills/dspy-ruby/references/toolsets.md +0 -502
  90. package/skills/every-style-editor/SKILL.md +0 -135
  91. package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +0 -529
  92. package/skills/feature-video/SKILL.md +0 -383
  93. package/skills/gemini-imagegen/SKILL.md +0 -237
  94. package/skills/gemini-imagegen/requirements.txt +0 -2
  95. package/skills/gemini-imagegen/scripts/compose_images.py +0 -157
  96. package/skills/gemini-imagegen/scripts/edit_image.py +0 -144
  97. package/skills/gemini-imagegen/scripts/gemini_images.py +0 -263
  98. package/skills/gemini-imagegen/scripts/generate_image.py +0 -133
  99. package/skills/gemini-imagegen/scripts/multi_turn_chat.py +0 -216
  100. package/skills/generate_command/SKILL.md +0 -164
  101. package/skills/orchestrating-swarms/SKILL.md +0 -1723
  102. package/skills/proof/SKILL.md +0 -291
  103. package/skills/proof/references/hitl-review.md +0 -368
  104. package/skills/rclone/SKILL.md +0 -150
  105. package/skills/rclone/scripts/check_setup.sh +0 -60
  106. package/skills/setup/SKILL.md +0 -22
  107. package/skills/test-xcode/SKILL.md +0 -214
  108. package/skills/todo-create/SKILL.md +0 -109
  109. package/skills/todo-resolve/SKILL.md +0 -68
  110. package/skills/todo-triage/SKILL.md +0 -70
  111. package/skills/writing-systematic-skills/SKILL.md +0 -119
  112. /package/skills/{todo-create → todos}/assets/todo-template.md +0 -0
package/ATTRIBUTIONS.md CHANGED
@@ -35,7 +35,7 @@ The two `SKILL.md` files (`skills/test-driven-development/SKILL.md` and `skills/
35
35
 
36
36
  - Adaptation was light: rewrote upstream's `@filename` force-load syntax to repo-local `references/` paths; rewrote `superpowers:<skill-name>` namespace cross-references to bare names (matching Systematic's runtime convention where the `systematic:` prefix is applied at load time); swapped `~/.claude/skills` path mentions to the canonical OpenCode path `~/.agents/skills/`; renamed the `CLAUDE_MD_TESTING.md` worked example to `skill-testing-walkthrough.md` to fit Systematic's descriptive-filename convention.
37
37
  - Per-file copyright comments are deliberately omitted. Frontmatter `license: MIT` plus this attribution file constitute the full attribution surface.
38
- - These files are load-bearing for `ce:work`, `ce:plan`, and `writing-systematic-skills` cross-references. Future contributors editing them should preserve the discipline-enforcing prose (e.g., the Iron Law, the rationalization tables, the RED-GREEN-REFACTOR cycle).
38
+ - These files are load-bearing for `ce:work`, `ce:plan`, and the "Systematic Bundled Skills" section of `writing-skills` itself. Future contributors editing them should preserve the discipline-enforcing prose (e.g., the Iron Law, the rationalization tables, the RED-GREEN-REFACTOR cycle).
39
39
 
40
40
  ### Future refresh discipline
41
41
 
@@ -4,6 +4,7 @@ description: "Iteratively refines UI design through N screenshot-analyze-improve
4
4
  color: accent
5
5
  mode: subagent
6
6
  temperature: 0.6
7
+ tools: Read, Grep, Glob, Edit, Write, Bash
7
8
  ---
8
9
 
9
10
  You are an expert UI/UX design iterator specializing in systematic, progressive refinement of web components. Your methodology combines visual analysis, competitor research, and incremental improvements to transform ordinary interfaces into polished, professional designs.
@@ -25,13 +25,11 @@ Before going online, check if curated knowledge already exists in skills:
25
25
 
26
26
  2. **Identify Relevant Skills**:
27
27
  Match the research topic to available skills. Common mappings:
28
- - Rails/Ruby → `dhh-rails-style`, `andrew-kane-gem-writer`, `dspy-ruby`
29
- - Frontend/Design → `frontend-design`, `swiss-design`
30
- - TypeScript/React → `react-best-practices`
28
+ - Frontend/Design → `frontend-design`
31
29
  - AI/Agents → `agent-native-architecture`
32
- - Documentation → `ce:compound`, `every-style-editor`
33
- - File operations → `rclone`, `git-worktree`
34
- - Image generation → `gemini-imagegen`
30
+ - Documentation → `ce:compound`, `compound-docs`
31
+ - Git workflows → `git-worktree`, `git-commit-push-pr`
32
+ - Testing → `test-driven-development`, `test-browser`
35
33
 
36
34
  3. **Extract Patterns from Skills**:
37
35
  - Read the full content of relevant SKILL.md files
@@ -83,7 +81,7 @@ Only after checking skills AND verifying API availability, gather additional inf
83
81
 
84
82
  2. **Organize Discoveries**:
85
83
  - Organize into clear categories (e.g., "Must Have", "Recommended", "Optional")
86
- - Clearly indicate source: "From skill: dhh-rails-style" vs "From official docs" vs "Community consensus"
84
+ - Clearly indicate source: "From skill: frontend-design" vs "From official docs" vs "Community consensus"
87
85
  - Provide specific examples from real projects when possible
88
86
  - Explain the reasoning behind each best practice
89
87
  - Highlight any technology-specific or domain-specific considerations
@@ -106,7 +104,7 @@ For GitHub issue best practices specifically, you will research:
106
104
  ## Source Attribution
107
105
 
108
106
  Always cite your sources and indicate the authority level:
109
- - **Skill-based**: "The dhh-rails-style skill recommends..." (highest authority - curated)
107
+ - **Skill-based**: "The frontend-design skill recommends..." (highest authority - curated)
110
108
  - **Official docs**: "Official GitHub documentation recommends..."
111
109
  - **Community**: "Many successful projects tend to..."
112
110
 
@@ -36,7 +36,7 @@ Your primary responsibilities:
36
36
 
37
37
  Your workflow:
38
38
 
39
- 1. Start with a broad pattern search using the built-in Grep tool (or `ast-grep` for structural AST matching when needed)
39
+ 1. Start with a broad pattern search using the built-in grep tool (or `ast-grep` for structural AST matching when needed)
40
40
  2. Compile a comprehensive list of identified patterns and their locations
41
41
  3. Search for common anti-pattern indicators (TODO, FIXME, HACK, XXX)
42
42
  4. Analyze naming conventions by sampling representative files
@@ -3,6 +3,7 @@ name: systematic-implementer
3
3
  description: Implements one plan unit in a fresh subagent context and reports bounded changes back to the orchestrator.
4
4
  mode: subagent
5
5
  temperature: 0.2
6
+ tools: Read, Grep, Glob, Edit, Write, Bash
6
7
  ---
7
8
 
8
9
  You are a focused implementer dispatched by a parent OpenCode session orchestrating a multi-unit plan. You implement one unit's worth of changes and report back to the orchestrator.
package/dist/cli.js CHANGED
@@ -1,22 +1,323 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
3
  import {
4
- convertContent,
4
+ applyEdits,
5
5
  findAgentsInDir,
6
6
  findCommandsInDir,
7
7
  findSkillsInDir,
8
- getConfigPaths
9
- } from "./index-vyzhzvap.js";
8
+ getConfigPaths,
9
+ modify,
10
+ parse,
11
+ parseTree
12
+ } from "./index-yskzf7vh.js";
10
13
 
11
14
  // src/cli.ts
15
+ import fs2 from "fs";
16
+ import path2 from "path";
17
+
18
+ // src/lib/setup.ts
12
19
  import fs from "fs";
13
20
  import path from "path";
21
+ var SYSTEMATIC_PACKAGE_NAME = "@fro.bot/systematic";
22
+ var PI_PACKAGE_IDENTIFIER = `npm:${SYSTEMATIC_PACKAGE_NAME}`;
23
+ var SETUP_ERROR_NAME = "SetupError";
24
+ function createSetupError(message) {
25
+ const error = new Error(message);
26
+ error.name = SETUP_ERROR_NAME;
27
+ return error;
28
+ }
29
+ var DEFAULT_OPS = {
30
+ writeFileSync: fs.writeFileSync,
31
+ renameSync: fs.renameSync,
32
+ unlinkSync: fs.unlinkSync,
33
+ chmodSync: fs.chmodSync
34
+ };
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function errorMessage(error) {
39
+ return error instanceof Error ? error.message : String(error);
40
+ }
41
+ function matchesIdentity(identifier, base) {
42
+ if (identifier === base)
43
+ return true;
44
+ const prefix = `${base}@`;
45
+ return identifier.startsWith(prefix) && identifier.length > prefix.length;
46
+ }
47
+ function isSystematicIdentifier(identifier) {
48
+ return matchesIdentity(identifier, SYSTEMATIC_PACKAGE_NAME);
49
+ }
50
+ function isPiSystematicIdentifier(identifier) {
51
+ return matchesIdentity(identifier, PI_PACKAGE_IDENTIFIER);
52
+ }
53
+ function lstatOrNull(targetPath) {
54
+ try {
55
+ return fs.lstatSync(targetPath);
56
+ } catch (error) {
57
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
58
+ return null;
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+ function assertRealpathUnderCwd(dir, cwd) {
64
+ const realDir = fs.realpathSync(dir);
65
+ const realCwd = fs.realpathSync(cwd);
66
+ const relative = path.relative(realCwd, realDir);
67
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
68
+ throw createSetupError(`Refusing to write under ${dir}: resolved directory escapes the project root`);
69
+ }
70
+ }
71
+ function assertParentTrusted(parentDir, cwd) {
72
+ const stat = lstatOrNull(parentDir);
73
+ if (stat) {
74
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
75
+ throw createSetupError(`Refusing to write under ${parentDir}: not a real directory`);
76
+ }
77
+ assertRealpathUnderCwd(parentDir, cwd);
78
+ return;
79
+ }
80
+ fs.mkdirSync(parentDir, { recursive: true });
81
+ assertRealpathUnderCwd(parentDir, cwd);
82
+ }
83
+ var OPENCODE_TARGET_CANDIDATES = [
84
+ ".opencode/opencode.jsonc",
85
+ ".opencode/opencode.json",
86
+ "opencode.jsonc",
87
+ "opencode.json"
88
+ ];
89
+ function resolveOpenCodeTargetPath(cwd) {
90
+ const opencodeDirStat = lstatOrNull(path.join(cwd, ".opencode"));
91
+ if (opencodeDirStat?.isSymbolicLink()) {
92
+ return path.join(cwd, OPENCODE_TARGET_CANDIDATES[0]);
93
+ }
94
+ for (const candidate of OPENCODE_TARGET_CANDIDATES) {
95
+ const candidatePath = path.join(cwd, candidate);
96
+ if (lstatOrNull(candidatePath) !== null)
97
+ return candidatePath;
98
+ }
99
+ return path.join(cwd, "opencode.jsonc");
100
+ }
101
+ var IS_WINDOWS = process.platform === "win32";
102
+ var OPEN_FLAGS = IS_WINDOWS ? fs.constants.O_RDONLY : fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK;
103
+ function assertWindowsPreOpenTrust(targetPath) {
104
+ const preStat = lstatOrNull(targetPath);
105
+ if (preStat && (preStat.isSymbolicLink() || !preStat.isFile())) {
106
+ throw createSetupError(`Refusing to read ${targetPath}: not a regular file`);
107
+ }
108
+ }
109
+ function openTrustedExisting(targetPath) {
110
+ if (IS_WINDOWS)
111
+ assertWindowsPreOpenTrust(targetPath);
112
+ let fd;
113
+ try {
114
+ fd = fs.openSync(targetPath, OPEN_FLAGS);
115
+ } catch (error) {
116
+ if (error instanceof Error && "code" in error) {
117
+ if (error.code === "ENOENT")
118
+ return null;
119
+ if (error.code === "ELOOP") {
120
+ throw createSetupError(`Refusing to read ${targetPath}: target is a symlink`);
121
+ }
122
+ }
123
+ throw error;
124
+ }
125
+ try {
126
+ const stat = fs.fstatSync(fd);
127
+ if (!stat.isFile()) {
128
+ throw createSetupError(`Refusing to read ${targetPath}: not a regular file`);
129
+ }
130
+ return { bytes: fs.readFileSync(fd), mode: stat.mode & 511 };
131
+ } finally {
132
+ fs.closeSync(fd);
133
+ }
134
+ }
135
+ function atomicWrite(targetPath, content, originalBytes, mode, ops) {
136
+ const preserveMode = originalBytes !== null;
137
+ if (originalBytes !== null) {
138
+ const backupPath = `${targetPath}.bak`;
139
+ if (lstatOrNull(backupPath) !== null) {
140
+ throw createSetupError(`Refusing to write backup ${backupPath}: a file already exists at that path`);
141
+ }
142
+ writeTempAndRename(backupPath, originalBytes, mode, preserveMode, ops);
143
+ }
144
+ writeTempAndRename(targetPath, content, mode, preserveMode, ops);
145
+ }
146
+ function writeTempAndRename(destPath, content, mode, preserveExactMode, ops) {
147
+ const parentDir = path.dirname(destPath);
148
+ const tempPath = makeTempPath(parentDir, path.basename(destPath));
149
+ try {
150
+ ops.writeFileSync(tempPath, content, { flag: "wx", mode });
151
+ if (preserveExactMode) {
152
+ ops.chmodSync(tempPath, mode);
153
+ }
154
+ ops.renameSync(tempPath, destPath);
155
+ } catch (error) {
156
+ cleanupTemp(tempPath, ops);
157
+ throw error;
158
+ }
159
+ }
160
+ function makeTempPath(parentDir, basename) {
161
+ return path.join(parentDir, `.${basename}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
162
+ }
163
+ function cleanupTemp(tempPath, ops) {
164
+ if (!fs.existsSync(tempPath))
165
+ return;
166
+ try {
167
+ ops.unlinkSync(tempPath);
168
+ } catch {}
169
+ }
170
+ function assertNoDuplicateTopLevelKeys(rawText, targetPath, names, diagnosticContext) {
171
+ const root = parseTree(rawText);
172
+ if (!root?.children)
173
+ return;
174
+ const seen = new Set;
175
+ for (const child of root.children) {
176
+ const keyNode = child.children?.[0];
177
+ const key = keyNode?.value;
178
+ if (typeof key !== "string" || !names.includes(key))
179
+ continue;
180
+ if (seen.has(key)) {
181
+ throw createSetupError(`Invalid ${diagnosticContext} in ${targetPath}: duplicate top-level \`${key}\` key`);
182
+ }
183
+ seen.add(key);
184
+ }
185
+ }
186
+ function parseOpenCodeJsonc(rawText, targetPath) {
187
+ const errors = [];
188
+ const parsed = parse(rawText, errors, {
189
+ allowTrailingComma: true
190
+ });
191
+ if (errors.length > 0) {
192
+ throw createSetupError(`Invalid OpenCode config in ${targetPath}: JSONC parse error (code ${errors[0]?.error})`);
193
+ }
194
+ return parsed;
195
+ }
196
+ function setupOpenCode(cwd, ops) {
197
+ const targetPath = resolveOpenCodeTargetPath(cwd);
198
+ assertParentTrusted(path.dirname(targetPath), cwd);
199
+ const existing = openTrustedExisting(targetPath);
200
+ if (existing === null) {
201
+ const content = `${JSON.stringify({ plugin: [SYSTEMATIC_PACKAGE_NAME] }, null, 2)}
202
+ `;
203
+ atomicWrite(targetPath, content, null, 420, ops);
204
+ return { status: "configured", targetPath };
205
+ }
206
+ const rawText = existing.bytes.toString("utf8");
207
+ assertNoDuplicateTopLevelKeys(rawText, targetPath, ["plugin", "plugins"], "OpenCode config");
208
+ const parsed = parseOpenCodeJsonc(rawText, targetPath);
209
+ if (!isRecord(parsed)) {
210
+ throw createSetupError(`Invalid OpenCode config in ${targetPath}: root must be an object`);
211
+ }
212
+ if (Object.hasOwn(parsed, "plugins")) {
213
+ throw createSetupError(`Invalid OpenCode config in ${targetPath}: \`plugins\` (plural) is not supported by OpenCode's schema; use singular \`plugin\``);
214
+ }
215
+ if (!Object.hasOwn(parsed, "plugin")) {
216
+ const edits2 = modify(rawText, ["plugin"], [SYSTEMATIC_PACKAGE_NAME], {});
217
+ atomicWrite(targetPath, applyEdits(rawText, edits2), existing.bytes, existing.mode, ops);
218
+ return { status: "configured", targetPath };
219
+ }
220
+ const existingValue = parsed.plugin;
221
+ if (!Array.isArray(existingValue)) {
222
+ throw createSetupError(`Invalid OpenCode config in ${targetPath}: \`plugin\` must be an array`);
223
+ }
224
+ const identifiers = [];
225
+ for (const entry of existingValue) {
226
+ if (typeof entry !== "string") {
227
+ throw createSetupError(`Invalid OpenCode config in ${targetPath}: \`plugin\` contains a non-string entry`);
228
+ }
229
+ identifiers.push(entry);
230
+ }
231
+ if (identifiers.some((identifier) => isSystematicIdentifier(identifier))) {
232
+ return { status: "already-configured", targetPath };
233
+ }
234
+ const edits = modify(rawText, ["plugin", existingValue.length], SYSTEMATIC_PACKAGE_NAME, {});
235
+ atomicWrite(targetPath, applyEdits(rawText, edits), existing.bytes, existing.mode, ops);
236
+ return { status: "configured", targetPath };
237
+ }
238
+ function getPiEntryIdentifier(entry) {
239
+ if (typeof entry === "string")
240
+ return entry;
241
+ if (isRecord(entry) && typeof entry.source === "string")
242
+ return entry.source;
243
+ return null;
244
+ }
245
+ function assertPiTaggedEntryUsable(entry, targetPath) {
246
+ if (!isRecord(entry))
247
+ return;
248
+ if (entry.autoload === false) {
249
+ throw createSetupError(`Invalid Pi settings in ${targetPath}: the existing \`${SYSTEMATIC_PACKAGE_NAME}\` package entry has \`autoload: false\`, which may prevent Systematic from loading; remove that flag or the entry before re-running setup`);
250
+ }
251
+ if (Object.hasOwn(entry, "extensions") || Object.hasOwn(entry, "skills")) {
252
+ throw createSetupError(`Invalid Pi settings in ${targetPath}: the existing \`${SYSTEMATIC_PACKAGE_NAME}\` package entry declares an \`extensions\`/\`skills\` filter, which cannot be proven safe for Systematic's extension/skills; adjust the filter before re-running setup`);
253
+ }
254
+ }
255
+ function setupPi(cwd, ops) {
256
+ const targetPath = path.join(cwd, ".pi", "settings.json");
257
+ assertParentTrusted(path.dirname(targetPath), cwd);
258
+ const existing = openTrustedExisting(targetPath);
259
+ if (existing === null) {
260
+ const content = `${JSON.stringify({ packages: [PI_PACKAGE_IDENTIFIER] }, null, 2)}
261
+ `;
262
+ atomicWrite(targetPath, content, null, 420, ops);
263
+ return { status: "configured", targetPath };
264
+ }
265
+ const rawText = existing.bytes.toString("utf8");
266
+ assertNoDuplicateTopLevelKeys(rawText, targetPath, ["packages"], "Pi settings");
267
+ let parsed;
268
+ try {
269
+ parsed = JSON.parse(rawText);
270
+ } catch (error) {
271
+ throw createSetupError(`Invalid Pi settings in ${targetPath}: unable to parse JSON (${errorMessage(error)})`);
272
+ }
273
+ if (!isRecord(parsed)) {
274
+ throw createSetupError(`Invalid Pi settings in ${targetPath}: root must be an object`);
275
+ }
276
+ const existingPackages = parsed.packages;
277
+ if (existingPackages === undefined) {
278
+ const edits2 = modify(rawText, ["packages"], [PI_PACKAGE_IDENTIFIER], {});
279
+ atomicWrite(targetPath, applyEdits(rawText, edits2), existing.bytes, existing.mode, ops);
280
+ return { status: "configured", targetPath };
281
+ }
282
+ if (!Array.isArray(existingPackages)) {
283
+ throw createSetupError(`Invalid Pi settings in ${targetPath}: \`packages\` must be an array`);
284
+ }
285
+ const identifiers = [];
286
+ for (const entry of existingPackages) {
287
+ const identifier = getPiEntryIdentifier(entry);
288
+ if (identifier === null) {
289
+ throw createSetupError(`Invalid Pi settings in ${targetPath}: \`packages\` contains an entry of unsupported shape`);
290
+ }
291
+ identifiers.push(identifier);
292
+ }
293
+ const matchIndex = identifiers.findIndex((identifier) => isPiSystematicIdentifier(identifier));
294
+ if (matchIndex !== -1) {
295
+ assertPiTaggedEntryUsable(existingPackages[matchIndex], targetPath);
296
+ return { status: "already-configured", targetPath };
297
+ }
298
+ const edits = modify(rawText, ["packages", existingPackages.length], PI_PACKAGE_IDENTIFIER, {});
299
+ atomicWrite(targetPath, applyEdits(rawText, edits), existing.bytes, existing.mode, ops);
300
+ return { status: "configured", targetPath };
301
+ }
302
+ function setupHarness(harness, cwd, opsOverride) {
303
+ const ops = { ...DEFAULT_OPS, ...opsOverride };
304
+ switch (harness) {
305
+ case "opencode":
306
+ return setupOpenCode(cwd, ops);
307
+ case "pi":
308
+ return setupPi(cwd, ops);
309
+ default:
310
+ throw createSetupError(`Unknown harness: ${String(harness)}. Use: opencode, pi`);
311
+ }
312
+ }
313
+
314
+ // src/cli.ts
14
315
  var getPackageVersion = () => {
15
316
  try {
16
- const packageJsonPath = path.resolve(import.meta.dirname, "..", "package.json");
17
- if (!fs.existsSync(packageJsonPath))
317
+ const packageJsonPath = path2.resolve(import.meta.dirname, "..", "package.json");
318
+ if (!fs2.existsSync(packageJsonPath))
18
319
  return "unknown";
19
- const content = fs.readFileSync(packageJsonPath, "utf8");
320
+ const content = fs2.readFileSync(packageJsonPath, "utf8");
20
321
  const parsed = JSON.parse(content);
21
322
  return parsed.version ?? "unknown";
22
323
  } catch {
@@ -31,12 +332,11 @@ Usage:
31
332
  systematic <command> [options]
32
333
 
33
334
  Commands:
34
- list [type] List available skills, agents, or commands
35
- convert <type> <file> [--mode=primary|subagent]
36
- Convert and inspect a file (outputs to stdout)
37
- config [subcommand] Configuration management
38
- show Show configuration
39
- path Print config file locations
335
+ list [type] List available skills, agents, or commands
336
+ config [subcommand] Configuration management
337
+ show Show configuration
338
+ path Print config file locations
339
+ setup --harness opencode|pi Configure a harness to load Systematic (project-local only)
40
340
 
41
341
  Options:
42
342
  -h, --help Show this help message
@@ -45,13 +345,38 @@ Options:
45
345
  Examples:
46
346
  systematic list skills
47
347
  systematic list agents
48
- systematic convert agent ./agents/my-agent.md
49
- systematic convert agent ./agents/my-agent.md --mode=primary
50
- systematic convert skill ./skills/my-skill/SKILL.md
51
348
  systematic config show
349
+ systematic setup --harness opencode
350
+ systematic setup --harness pi
52
351
  `;
352
+ function isHarness(value) {
353
+ return value === "opencode" || value === "pi";
354
+ }
355
+ function setupCommand(rest) {
356
+ if (rest[0] !== "--harness" || rest.length !== 2) {
357
+ console.error("Usage: systematic setup --harness opencode|pi (project-local only, no --global)");
358
+ process.exit(1);
359
+ }
360
+ const harnessArg = rest[1];
361
+ if (!harnessArg || !isHarness(harnessArg)) {
362
+ console.error(`Unknown or missing --harness value: ${harnessArg ?? ""}`);
363
+ console.error("Available: opencode, pi");
364
+ process.exit(1);
365
+ }
366
+ try {
367
+ const result = setupHarness(harnessArg, process.cwd());
368
+ if (result.status === "already-configured") {
369
+ console.log(`Already configured: ${result.targetPath}`);
370
+ } else {
371
+ console.log(`Configured: ${result.targetPath}`);
372
+ }
373
+ } catch (error) {
374
+ console.error(`Setup failed: ${error instanceof Error ? error.message : String(error)}`);
375
+ process.exit(1);
376
+ }
377
+ }
53
378
  function listItems(type) {
54
- const packageRoot = path.resolve(import.meta.dirname, "..");
379
+ const packageRoot = path2.resolve(import.meta.dirname, "..");
55
380
  const bundledDir = packageRoot;
56
381
  let finder;
57
382
  let subdir;
@@ -72,7 +397,7 @@ function listItems(type) {
72
397
  console.error(`Unknown type: ${type}. Use: skills, agents, commands`);
73
398
  process.exit(1);
74
399
  }
75
- const items = finder(path.join(bundledDir, subdir));
400
+ const items = finder(path2.join(bundledDir, subdir));
76
401
  if (items.length === 0) {
77
402
  console.log(`No ${type} found.`);
78
403
  return;
@@ -83,46 +408,21 @@ function listItems(type) {
83
408
  console.log(` ${item.name}`);
84
409
  }
85
410
  }
86
- function runConvert(type, filePath, modeArg) {
87
- const validTypes = ["skill", "agent", "command"];
88
- if (!validTypes.includes(type)) {
89
- console.error(`Invalid type: ${type}. Must be one of: ${validTypes.join(", ")}`);
90
- process.exit(1);
91
- }
92
- const resolvedPath = path.resolve(filePath);
93
- if (!fs.existsSync(resolvedPath)) {
94
- console.error(`File not found: ${resolvedPath}`);
95
- process.exit(1);
96
- }
97
- let agentMode = "subagent";
98
- if (modeArg) {
99
- const modeMatch = modeArg.match(/^--mode=(primary|subagent)$/);
100
- if (modeMatch) {
101
- agentMode = modeMatch[1];
102
- } else {
103
- console.error("Invalid --mode flag. Use: --mode=primary or --mode=subagent");
104
- process.exit(1);
105
- }
106
- }
107
- const content = fs.readFileSync(resolvedPath, "utf8");
108
- const converted = convertContent(content, type, { agentMode });
109
- console.log(converted);
110
- }
111
411
  function configShow() {
112
412
  const paths = getConfigPaths(process.cwd());
113
413
  console.log(`Configuration locations:
114
414
  `);
115
415
  console.log(` User config: ${paths.userConfig}`);
116
416
  console.log(` Project config: ${paths.projectConfig}`);
117
- if (fs.existsSync(paths.projectConfig)) {
417
+ if (fs2.existsSync(paths.projectConfig)) {
118
418
  console.log(`
119
419
  Project configuration:`);
120
- console.log(fs.readFileSync(paths.projectConfig, "utf-8"));
420
+ console.log(fs2.readFileSync(paths.projectConfig, "utf-8"));
121
421
  }
122
- if (fs.existsSync(paths.userConfig)) {
422
+ if (fs2.existsSync(paths.userConfig)) {
123
423
  console.log(`
124
424
  User configuration:`);
125
- console.log(fs.readFileSync(paths.userConfig, "utf-8"));
425
+ console.log(fs2.readFileSync(paths.userConfig, "utf-8"));
126
426
  }
127
427
  }
128
428
  function configPath() {
@@ -137,13 +437,8 @@ switch (command) {
137
437
  case "list":
138
438
  listItems(args[1] || "skills");
139
439
  break;
140
- case "convert":
141
- if (!args[1] || !args[2]) {
142
- console.error("Usage: systematic convert <type> <file> [--mode=primary|subagent]");
143
- console.error(" type: skill, agent, or command");
144
- process.exit(1);
145
- }
146
- runConvert(args[1], args[2], args[3]);
440
+ case "setup":
441
+ setupCommand(args.slice(1));
147
442
  break;
148
443
  case "config":
149
444
  switch (args[1]) {