thoth-agents 0.3.5 → 0.3.6
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +19 -13
- package/dist/{chunk-W5GCMYA3.js → chunk-HIXSRHEG.js} +295 -66
- package/dist/cli/index.js +37 -4
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/operations/opencode.d.ts +2 -2
- package/dist/cli/owned-skills.d.ts +20 -0
- package/dist/cli/tui/index.js +1 -1
- package/dist/harness/core/owned-skills.d.ts +2 -0
- package/dist/index.js +5 -4
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/skills/thoth-init/SKILL.md +21 -18
- package/plugin/skills/thoth-init/scripts/init.mjs +152 -68
- package/skills/README.md +5 -3
- package/skills/thoth-init/SKILL.md +21 -18
- package/skills/thoth-init/scripts/init.mjs +152 -68
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import {
|
|
4
4
|
cpSync,
|
|
5
5
|
existsSync,
|
|
6
|
+
lstatSync,
|
|
6
7
|
mkdirSync,
|
|
7
8
|
readdirSync,
|
|
8
9
|
readFileSync,
|
|
9
|
-
statSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from 'node:fs';
|
|
12
12
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -15,12 +15,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
16
16
|
const SKILL_ROOT = dirname(dirname(SCRIPT_PATH));
|
|
17
17
|
const BUNDLE_ROOT = dirname(SKILL_ROOT);
|
|
18
|
-
const
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
18
|
+
const OPEN_SPEC_DIRECTORIES = [
|
|
19
|
+
'openspec',
|
|
20
|
+
join('openspec', 'changes'),
|
|
21
|
+
join('openspec', 'changes', 'archive'),
|
|
22
|
+
join('openspec', 'specs'),
|
|
23
|
+
join('openspec', 'memory'),
|
|
24
|
+
join('openspec', 'templates'),
|
|
24
25
|
];
|
|
25
26
|
|
|
26
27
|
function parseArgs(argv) {
|
|
@@ -29,110 +30,193 @@ function parseArgs(argv) {
|
|
|
29
30
|
const argument = argv[index];
|
|
30
31
|
if (argument === '--json') options.json = true;
|
|
31
32
|
else if (argument === '--project') options.project = argv[++index];
|
|
32
|
-
else if (argument === '--harness') options.harness = argv[++index];
|
|
33
33
|
else throw new Error(`Unknown argument: ${argument}`);
|
|
34
34
|
}
|
|
35
35
|
if (!options.project) throw new Error('--project is required');
|
|
36
|
-
if (!['opencode', 'codex', 'claude'].includes(options.harness)) {
|
|
37
|
-
throw new Error('--harness must be opencode, codex, or claude');
|
|
38
|
-
}
|
|
39
36
|
return options;
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
function
|
|
43
|
-
if (!existsSync(
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
function assertDirectory(path, label) {
|
|
40
|
+
if (!existsSync(path)) return;
|
|
41
|
+
const stat = lstatSync(path);
|
|
42
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
43
|
+
throw new Error(`${label} must be a directory: ${path}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertRegularFile(path, label) {
|
|
48
|
+
if (!existsSync(path)) return;
|
|
49
|
+
const stat = lstatSync(path);
|
|
50
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
51
|
+
throw new Error(`${label} must be a regular file: ${path}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function collectFiles(sourceRoot, targetRoot) {
|
|
56
|
+
if (!existsSync(sourceRoot)) {
|
|
57
|
+
throw new Error(`Bundled SDD template directory is missing: ${sourceRoot}`);
|
|
58
|
+
}
|
|
59
|
+
assertDirectory(sourceRoot, 'Bundled template directory');
|
|
60
|
+
const entries = [];
|
|
61
|
+
|
|
62
|
+
function visit(source, target) {
|
|
63
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
64
|
+
const sourcePath = join(source, entry.name);
|
|
65
|
+
const targetPath = join(target, entry.name);
|
|
66
|
+
if (entry.isSymbolicLink()) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Bundled templates cannot contain symlinks: ${sourcePath}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
entries.push({ kind: 'directory', target: targetPath });
|
|
73
|
+
visit(sourcePath, targetPath);
|
|
74
|
+
} else if (entry.isFile()) {
|
|
75
|
+
entries.push({ kind: 'file', source: sourcePath, target: targetPath });
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(`Unsupported bundled template entry: ${sourcePath}`);
|
|
78
|
+
}
|
|
49
79
|
}
|
|
50
|
-
return;
|
|
51
80
|
}
|
|
81
|
+
|
|
82
|
+
visit(sourceRoot, targetRoot);
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function preflight(project) {
|
|
87
|
+
assertDirectory(project, 'Project root');
|
|
88
|
+
if (!existsSync(project)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`--project must reference an existing project directory: ${project}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const constitutionSource = join(
|
|
95
|
+
BUNDLE_ROOT,
|
|
96
|
+
'thoth-constitution',
|
|
97
|
+
'templates',
|
|
98
|
+
'constitution.md',
|
|
99
|
+
);
|
|
100
|
+
assertRegularFile(constitutionSource, 'Bundled constitution template');
|
|
101
|
+
if (!existsSync(constitutionSource)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Bundled constitution template is missing: ${constitutionSource}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const templates = collectFiles(
|
|
108
|
+
join(BUNDLE_ROOT, 'thoth-sdd', 'templates'),
|
|
109
|
+
join(project, 'openspec', 'templates'),
|
|
110
|
+
);
|
|
111
|
+
for (const directory of OPEN_SPEC_DIRECTORIES) {
|
|
112
|
+
assertDirectory(join(project, directory), 'OpenSpec path');
|
|
113
|
+
}
|
|
114
|
+
for (const entry of templates) {
|
|
115
|
+
if (entry.kind === 'directory') {
|
|
116
|
+
assertDirectory(entry.target, 'OpenSpec template path');
|
|
117
|
+
} else {
|
|
118
|
+
assertRegularFile(entry.target, 'OpenSpec template path');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const constitutionTarget = join(
|
|
123
|
+
project,
|
|
124
|
+
'openspec',
|
|
125
|
+
'memory',
|
|
126
|
+
'constitution.md',
|
|
127
|
+
);
|
|
128
|
+
const manifestTarget = join(project, 'openspec', '.thoth-agents.json');
|
|
129
|
+
assertRegularFile(constitutionTarget, 'OpenSpec constitution path');
|
|
130
|
+
assertRegularFile(manifestTarget, 'OpenSpec manifest path');
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
constitutionSource,
|
|
134
|
+
constitutionTarget,
|
|
135
|
+
manifestTarget,
|
|
136
|
+
templates,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createDirectory(target, report) {
|
|
141
|
+
if (existsSync(target)) return;
|
|
142
|
+
mkdirSync(target);
|
|
143
|
+
report.created.push(target);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function copyPreservingExisting(source, target, report) {
|
|
52
147
|
if (existsSync(target)) {
|
|
53
|
-
report.
|
|
148
|
+
report.preserved.push(target);
|
|
54
149
|
return;
|
|
55
150
|
}
|
|
56
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
57
151
|
cpSync(source, target, { errorOnExist: true });
|
|
58
152
|
report.created.push(target);
|
|
59
153
|
}
|
|
60
154
|
|
|
61
|
-
function
|
|
155
|
+
function writePreservingExisting(target, content, report) {
|
|
62
156
|
if (existsSync(target)) {
|
|
63
|
-
report.
|
|
157
|
+
report.preserved.push(target);
|
|
64
158
|
return;
|
|
65
159
|
}
|
|
66
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
67
160
|
writeFileSync(target, content);
|
|
68
161
|
report.created.push(target);
|
|
69
162
|
}
|
|
70
163
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
readFileSync(template, 'utf8').replaceAll('YYYY-MM-DD', today),
|
|
84
|
-
report,
|
|
85
|
-
);
|
|
164
|
+
function synchronizeManagedFile(target, content, report) {
|
|
165
|
+
if (!existsSync(target)) {
|
|
166
|
+
writeFileSync(target, content);
|
|
167
|
+
report.created.push(target);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (readFileSync(target, 'utf8') === content) {
|
|
171
|
+
report.preserved.push(target);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
writeFileSync(target, content);
|
|
175
|
+
report.managed.push(target);
|
|
86
176
|
}
|
|
87
177
|
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (existsSync(templatesRoot)) {
|
|
92
|
-
for (const entry of readdirSync(templatesRoot)) {
|
|
93
|
-
copyMissing(
|
|
94
|
-
join(templatesRoot, entry),
|
|
95
|
-
join(project, 'openspec', 'templates', entry),
|
|
96
|
-
report,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
178
|
+
function synchronizeOpenSpec(project, assets, report) {
|
|
179
|
+
for (const directory of OPEN_SPEC_DIRECTORIES) {
|
|
180
|
+
createDirectory(join(project, directory), report);
|
|
99
181
|
}
|
|
100
|
-
|
|
101
|
-
|
|
182
|
+
for (const entry of assets.templates) {
|
|
183
|
+
if (entry.kind === 'directory') createDirectory(entry.target, report);
|
|
184
|
+
else copyPreservingExisting(entry.source, entry.target, report);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
188
|
+
writePreservingExisting(
|
|
189
|
+
assets.constitutionTarget,
|
|
190
|
+
readFileSync(assets.constitutionSource, 'utf8').replaceAll(
|
|
191
|
+
'YYYY-MM-DD',
|
|
192
|
+
today,
|
|
193
|
+
),
|
|
194
|
+
report,
|
|
195
|
+
);
|
|
196
|
+
synchronizeManagedFile(
|
|
197
|
+
assets.manifestTarget,
|
|
102
198
|
`${JSON.stringify({ version: 1, initializedBy: 'thoth-agents' }, null, 2)}\n`,
|
|
103
199
|
report,
|
|
104
200
|
);
|
|
105
201
|
}
|
|
106
202
|
|
|
107
|
-
function installOpenCode(project, report) {
|
|
108
|
-
for (const skillName of OWNED_SKILL_NAMES) {
|
|
109
|
-
copyMissing(
|
|
110
|
-
join(BUNDLE_ROOT, skillName),
|
|
111
|
-
join(project, '.agents', 'skills', skillName),
|
|
112
|
-
report,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
203
|
try {
|
|
118
204
|
const options = parseArgs(process.argv.slice(2));
|
|
119
205
|
const project = resolve(options.project);
|
|
120
|
-
|
|
206
|
+
const assets = preflight(project);
|
|
121
207
|
const report = {
|
|
122
208
|
status: 'ready',
|
|
123
|
-
harness: options.harness,
|
|
124
209
|
project,
|
|
125
210
|
created: [],
|
|
126
211
|
managed: [],
|
|
127
|
-
|
|
212
|
+
preserved: [],
|
|
128
213
|
};
|
|
129
214
|
|
|
130
|
-
|
|
131
|
-
if (options.harness === 'opencode') installOpenCode(project, report);
|
|
215
|
+
synchronizeOpenSpec(project, assets, report);
|
|
132
216
|
|
|
133
217
|
const output = options.json
|
|
134
218
|
? JSON.stringify(report)
|
|
135
|
-
: `thoth-agents
|
|
219
|
+
: `thoth-agents synchronized OpenSpec governance in ${project}`;
|
|
136
220
|
process.stdout.write(`${output}\n`);
|
|
137
221
|
} catch (error) {
|
|
138
222
|
process.stderr.write(
|