@fro.bot/systematic 2.33.3 → 3.0.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.
- package/ATTRIBUTIONS.md +1 -1
- package/agents/design/design-iterator.md +1 -0
- package/agents/research/best-practices-researcher.md +6 -8
- package/agents/review/pattern-recognition-specialist.md +1 -1
- package/agents/workflow/systematic-implementer.md +1 -0
- package/dist/cli.js +348 -53
- package/dist/{index-vyzhzvap.js → index-yskzf7vh.js} +587 -386
- package/dist/index.js +100 -155
- package/dist/lib/agent-overlays.d.ts +0 -1
- package/dist/lib/agent-resolver.d.ts +28 -0
- package/dist/lib/bootstrap.d.ts +8 -1
- package/dist/lib/bundled-names.d.ts +3 -3
- package/dist/lib/pi-delegate-session.d.ts +31 -0
- package/dist/lib/pi-delegate-tool.d.ts +42 -0
- package/dist/lib/removed-names.d.ts +3 -3
- package/dist/lib/setup.d.ts +36 -0
- package/dist/lib/skill-loader.d.ts +1 -2
- package/dist/lib/skill-resolver.d.ts +39 -0
- package/dist/lib/skill-tool.d.ts +2 -11
- package/dist/lib/skills.d.ts +1 -9
- package/dist/lib/source-model-defaults.d.ts +1 -1
- package/dist/pi.d.ts +7 -0
- package/dist/pi.js +3641 -0
- package/dist/schemas/systematic-config.schema.json +47 -242
- package/package.json +29 -6
- package/skills/ce-brainstorm/references/handoff.md +5 -26
- package/skills/ce-brainstorm/references/universal-brainstorming.md +0 -1
- package/skills/ce-compound/SKILL.md +8 -12
- package/skills/ce-ideate/references/post-ideation-workflow.md +21 -85
- package/skills/ce-plan/SKILL.md +6 -6
- package/skills/ce-plan/references/deepening-workflow.md +6 -7
- package/skills/ce-plan/references/plan-handoff.md +1 -17
- package/skills/ce-plan/references/universal-planning.md +0 -4
- package/skills/ce-review/SKILL.md +10 -19
- package/skills/ce-review/references/persona-catalog.md +2 -7
- package/skills/ce-review/references/review-output-template.md +0 -1
- package/skills/ce-work/SKILL.md +1 -1
- package/skills/deepen-plan/SKILL.md +6 -7
- package/skills/document-review/SKILL.md +2 -2
- package/skills/git-commit-push-pr/SKILL.md +1 -2
- package/skills/lfg/SKILL.md +1 -1
- package/skills/onboarding/SKILL.md +1 -12
- package/skills/orchestrating-subagents/SKILL.md +2 -2
- package/skills/slfg/SKILL.md +2 -3
- package/skills/test-browser/SKILL.md +2 -2
- package/skills/todos/SKILL.md +239 -0
- package/skills/writing-skills/SKILL.md +102 -1
- package/skills/{writing-systematic-skills → writing-skills}/references/foundation-conventions.md +5 -5
- package/agents/design/design-implementation-reviewer.md +0 -95
- package/agents/design/figma-design-sync.md +0 -173
- package/agents/docs/ankane-readme-writer.md +0 -51
- package/agents/review/cli-agent-readiness-reviewer.md +0 -418
- package/agents/review/data-integrity-guardian.md +0 -72
- package/agents/review/data-migration-expert.md +0 -99
- package/agents/review/dhh-rails-reviewer.md +0 -46
- package/agents/review/julik-frontend-races-reviewer.md +0 -49
- package/agents/review/kieran-python-reviewer.md +0 -47
- package/agents/review/kieran-rails-reviewer.md +0 -47
- package/agents/review/performance-oracle.md +0 -112
- package/agents/review/schema-drift-detector.md +0 -143
- package/agents/review/security-sentinel.md +0 -95
- package/agents/workflow/lint.md +0 -17
- package/dist/lib/converter.d.ts +0 -14
- package/skills/andrew-kane-gem-writer/SKILL.md +0 -184
- package/skills/andrew-kane-gem-writer/references/database-adapters.md +0 -231
- package/skills/andrew-kane-gem-writer/references/module-organization.md +0 -121
- package/skills/andrew-kane-gem-writer/references/rails-integration.md +0 -183
- package/skills/andrew-kane-gem-writer/references/resources.md +0 -119
- package/skills/andrew-kane-gem-writer/references/testing-patterns.md +0 -261
- package/skills/changelog/SKILL.md +0 -139
- package/skills/claude-permissions-optimizer/SKILL.md +0 -166
- package/skills/claude-permissions-optimizer/scripts/extract-commands.mjs +0 -655
- package/skills/claude-permissions-optimizer/scripts/normalize.mjs +0 -151
- package/skills/dhh-rails-style/SKILL.md +0 -186
- package/skills/dhh-rails-style/references/architecture.md +0 -653
- package/skills/dhh-rails-style/references/controllers.md +0 -303
- package/skills/dhh-rails-style/references/frontend.md +0 -510
- package/skills/dhh-rails-style/references/gems.md +0 -266
- package/skills/dhh-rails-style/references/models.md +0 -359
- package/skills/dhh-rails-style/references/testing.md +0 -338
- package/skills/dspy-ruby/SKILL.md +0 -737
- package/skills/dspy-ruby/assets/config-template.rb +0 -187
- package/skills/dspy-ruby/assets/module-template.rb +0 -300
- package/skills/dspy-ruby/assets/signature-template.rb +0 -221
- package/skills/dspy-ruby/references/core-concepts.md +0 -674
- package/skills/dspy-ruby/references/observability.md +0 -366
- package/skills/dspy-ruby/references/optimization.md +0 -603
- package/skills/dspy-ruby/references/providers.md +0 -418
- package/skills/dspy-ruby/references/toolsets.md +0 -502
- package/skills/every-style-editor/SKILL.md +0 -135
- package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +0 -529
- package/skills/feature-video/SKILL.md +0 -383
- package/skills/gemini-imagegen/SKILL.md +0 -237
- package/skills/gemini-imagegen/requirements.txt +0 -2
- package/skills/gemini-imagegen/scripts/compose_images.py +0 -157
- package/skills/gemini-imagegen/scripts/edit_image.py +0 -144
- package/skills/gemini-imagegen/scripts/gemini_images.py +0 -263
- package/skills/gemini-imagegen/scripts/generate_image.py +0 -133
- package/skills/gemini-imagegen/scripts/multi_turn_chat.py +0 -216
- package/skills/generate_command/SKILL.md +0 -164
- package/skills/orchestrating-swarms/SKILL.md +0 -1723
- package/skills/proof/SKILL.md +0 -291
- package/skills/proof/references/hitl-review.md +0 -368
- package/skills/rclone/SKILL.md +0 -150
- package/skills/rclone/scripts/check_setup.sh +0 -60
- package/skills/setup/SKILL.md +0 -22
- package/skills/test-xcode/SKILL.md +0 -214
- package/skills/todo-create/SKILL.md +0 -109
- package/skills/todo-resolve/SKILL.md +0 -68
- package/skills/todo-triage/SKILL.md +0 -70
- package/skills/writing-systematic-skills/SKILL.md +0 -119
- /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-
|
|
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
|
-
-
|
|
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
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
4
|
+
applyEdits,
|
|
5
5
|
findAgentsInDir,
|
|
6
6
|
findCommandsInDir,
|
|
7
7
|
findSkillsInDir,
|
|
8
|
-
getConfigPaths
|
|
9
|
-
|
|
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 =
|
|
17
|
-
if (!
|
|
317
|
+
const packageJsonPath = path2.resolve(import.meta.dirname, "..", "package.json");
|
|
318
|
+
if (!fs2.existsSync(packageJsonPath))
|
|
18
319
|
return "unknown";
|
|
19
|
-
const content =
|
|
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]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
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(
|
|
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 (
|
|
417
|
+
if (fs2.existsSync(paths.projectConfig)) {
|
|
118
418
|
console.log(`
|
|
119
419
|
Project configuration:`);
|
|
120
|
-
console.log(
|
|
420
|
+
console.log(fs2.readFileSync(paths.projectConfig, "utf-8"));
|
|
121
421
|
}
|
|
122
|
-
if (
|
|
422
|
+
if (fs2.existsSync(paths.userConfig)) {
|
|
123
423
|
console.log(`
|
|
124
424
|
User configuration:`);
|
|
125
|
-
console.log(
|
|
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 "
|
|
141
|
-
|
|
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]) {
|