@kisev/skills-opencode 2.1.0 → 2.2.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 +6 -5
- package/README.ru.md +5 -5
- package/dist/assets/agents/mapper.md +10 -1
- package/dist/assets/agents/worker.md +12 -1
- package/dist/assets/commands/agents-md.md +1 -1
- package/dist/assets/commands/askme.md +1 -1
- package/dist/assets/commands/ast-grep.md +1 -1
- package/dist/assets/commands/code-explain.md +1 -1
- package/dist/assets/commands/code-review.md +1 -1
- package/dist/assets/commands/commit-msg.md +1 -1
- package/dist/assets/commands/docs-prepare.md +1 -1
- package/dist/assets/commands/docs-review.md +1 -1
- package/dist/assets/commands/doit.md +1 -1
- package/dist/assets/commands/goal.md +1 -1
- package/dist/assets/commands/humanize.md +1 -1
- package/dist/assets/commands/lsp-report.md +1 -1
- package/dist/assets/commands/mattermost.md +1 -1
- package/dist/assets/commands/mr-prepare.md +1 -1
- package/dist/assets/commands/release-prepare.md +1 -1
- package/dist/assets/commands/release-review.md +1 -1
- package/dist/assets/commands/rtk.md +1 -1
- package/dist/assets/commands/skill-improve.md +1 -1
- package/dist/assets/commands/slides-prompts-prepare.md +1 -1
- package/dist/assets/commands/spec-manage.md +1 -1
- package/dist/assets/commands/stopit.md +1 -1
- package/dist/assets/commands/summary.md +1 -1
- package/dist/assets/commands/task-prepare.md +1 -1
- package/dist/assets/commands/task-review.md +1 -1
- package/dist/assets/commands/task-triage.md +1 -1
- package/dist/assets/commands/team-retro.md +1 -1
- package/dist/assets/commands/team-roadmap.md +1 -1
- package/dist/assets/commands/team-sprint-close.md +1 -1
- package/dist/assets/commands/team-sprint-start.md +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/cli.js +101 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/installer.js +246 -45
- package/dist/plugins/rtk.js +35 -13
- package/dist/plugins/rules-injector.d.ts +1 -1
- package/dist/plugins/rules-injector.js +61 -26
- package/dist/plugins/zed-bell.js +4 -2
- package/dist/registry.js +68 -14
- package/dist/routing.js +34 -2
- package/dist/runtime/state.js +6 -2
- package/package.json +1 -1
package/dist/installer.js
CHANGED
|
@@ -44,7 +44,8 @@ export function normalizeSelection(value = {}) {
|
|
|
44
44
|
commands,
|
|
45
45
|
agents: agents.sort(),
|
|
46
46
|
plugins: plugins.sort(),
|
|
47
|
-
core_activation: Boolean(value.core_activation ??
|
|
47
|
+
core_activation: Boolean(value.core_activation ??
|
|
48
|
+
(commands.some((name) => PACKAGE_COMMANDS.includes(name)) || agents.length)),
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
export class InstallerError extends LifecycleError {
|
|
@@ -112,27 +113,57 @@ function parseManifest(raw, path) {
|
|
|
112
113
|
package_version: manifest.version,
|
|
113
114
|
version: manifest.version,
|
|
114
115
|
scope: "global",
|
|
115
|
-
commands: Object.keys(files)
|
|
116
|
+
commands: Object.keys(files)
|
|
117
|
+
.filter((path) => path.startsWith("commands/"))
|
|
118
|
+
.map((path) => path.slice(9, -3))
|
|
119
|
+
.sort(),
|
|
116
120
|
agents: [...FIXED_AGENT_ROLES].filter((role) => `agents/${role}.md` in files),
|
|
117
|
-
plugins: Object.keys(files)
|
|
121
|
+
plugins: Object.keys(files)
|
|
122
|
+
.filter((path) => path.startsWith("plugins/"))
|
|
123
|
+
.map((path) => path.slice(8, -3))
|
|
124
|
+
.filter((name) => SELECTABLE_PLUGINS.includes(name)),
|
|
118
125
|
core_activation: true,
|
|
119
126
|
files,
|
|
120
127
|
};
|
|
121
128
|
}
|
|
122
|
-
if (manifest.schema_version !== 2 ||
|
|
129
|
+
if (manifest.schema_version !== 2 ||
|
|
130
|
+
manifest.package !== PACKAGE_NAME ||
|
|
131
|
+
typeof manifest.package_version !== "string" ||
|
|
132
|
+
(manifest.scope !== "global" && manifest.scope !== "project") ||
|
|
133
|
+
!Array.isArray(manifest.commands) ||
|
|
134
|
+
!Array.isArray(manifest.agents) ||
|
|
135
|
+
!Array.isArray(manifest.plugins) ||
|
|
136
|
+
typeof manifest.core_activation !== "boolean" ||
|
|
137
|
+
!manifest.files ||
|
|
138
|
+
typeof manifest.files !== "object" ||
|
|
139
|
+
Array.isArray(manifest.files)) {
|
|
123
140
|
throw new InstallerError("invalid_manifest", `Ownership manifest has an unexpected format: ${path}`);
|
|
124
141
|
}
|
|
125
142
|
const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => {
|
|
126
143
|
destination("/", relativePath);
|
|
127
|
-
if (!record ||
|
|
144
|
+
if (!record ||
|
|
145
|
+
typeof record !== "object" ||
|
|
146
|
+
typeof record.sha256 !== "string" ||
|
|
147
|
+
!/^[a-f0-9]{64}$/.test(record.sha256)) {
|
|
128
148
|
throw new InstallerError("invalid_manifest", `Ownership manifest has an invalid record: ${relativePath}`);
|
|
129
149
|
}
|
|
130
150
|
const existing = record;
|
|
131
|
-
return [
|
|
151
|
+
return [
|
|
152
|
+
relativePath,
|
|
153
|
+
{
|
|
132
154
|
sha256: existing.sha256,
|
|
133
155
|
mode: Number.isInteger(existing.mode) ? existing.mode : 0o644,
|
|
134
|
-
kind: ["command", "agent", "plugin", "state"].includes(String(existing.kind))
|
|
135
|
-
|
|
156
|
+
kind: ["command", "agent", "plugin", "state"].includes(String(existing.kind))
|
|
157
|
+
? existing.kind
|
|
158
|
+
: relativePath.startsWith("commands/")
|
|
159
|
+
? "command"
|
|
160
|
+
: relativePath.startsWith("plugins/")
|
|
161
|
+
? "plugin"
|
|
162
|
+
: relativePath.startsWith("agents/")
|
|
163
|
+
? "agent"
|
|
164
|
+
: "state",
|
|
165
|
+
},
|
|
166
|
+
];
|
|
136
167
|
}));
|
|
137
168
|
return { ...manifest, files };
|
|
138
169
|
}
|
|
@@ -150,14 +181,16 @@ export async function archiveMutations(candidates, scope, cwd, home, sourceVersi
|
|
|
150
181
|
return undefined;
|
|
151
182
|
throw new InstallerError("unsafe_path", "Archive root is inaccessible");
|
|
152
183
|
});
|
|
153
|
-
if (rootInfo &&
|
|
184
|
+
if (rootInfo &&
|
|
185
|
+
(!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o077) !== 0))
|
|
154
186
|
throw new InstallerError("unsafe_path", "Archive root must be a private directory");
|
|
155
187
|
const objectsInfo = await lstat(join(root, "objects")).catch((error) => {
|
|
156
188
|
if (error.code === "ENOENT")
|
|
157
189
|
return undefined;
|
|
158
190
|
throw new InstallerError("unsafe_path", "Archive object store is inaccessible");
|
|
159
191
|
});
|
|
160
|
-
if (objectsInfo &&
|
|
192
|
+
if (objectsInfo &&
|
|
193
|
+
(!objectsInfo.isDirectory() || objectsInfo.isSymbolicLink() || (objectsInfo.mode & 0o077) !== 0))
|
|
161
194
|
throw new InstallerError("unsafe_path", "Archive object store must be private");
|
|
162
195
|
const indexPath = destination(root, "index.json");
|
|
163
196
|
const existingRaw = await readRegular(indexPath);
|
|
@@ -218,7 +251,14 @@ export async function archiveMutations(candidates, scope, cwd, home, sourceVersi
|
|
|
218
251
|
}
|
|
219
252
|
const next = Buffer.from(`${stable({ schema_version: 1, version: packageVersion(), entries: entries.sort((a, b) => String(a.original_path).localeCompare(String(b.original_path)) || String(a.digest).localeCompare(String(b.digest))) })}\n`);
|
|
220
253
|
if (!existingRaw || !existingRaw.equals(next)) {
|
|
221
|
-
mutations.push({
|
|
254
|
+
mutations.push({
|
|
255
|
+
root,
|
|
256
|
+
path: "index.json",
|
|
257
|
+
operation: "write",
|
|
258
|
+
content: next,
|
|
259
|
+
mode: 0o600,
|
|
260
|
+
expected: existingRaw ? { sha256: sha256(existingRaw) } : { absent: true },
|
|
261
|
+
});
|
|
222
262
|
}
|
|
223
263
|
return mutations;
|
|
224
264
|
}
|
|
@@ -263,7 +303,9 @@ async function validateGenericDeployment(root, action, expectedAssets, plannedOp
|
|
|
263
303
|
}
|
|
264
304
|
}
|
|
265
305
|
function asLegacy(manifest) {
|
|
266
|
-
return manifest?.version === "1.0.0"
|
|
306
|
+
return manifest?.version === "1.0.0"
|
|
307
|
+
? manifest
|
|
308
|
+
: undefined;
|
|
267
309
|
}
|
|
268
310
|
async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection) {
|
|
269
311
|
const root = deploymentRoot(scope, cwd, home);
|
|
@@ -280,10 +322,18 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
280
322
|
const bundled = await assets(selection);
|
|
281
323
|
const retiredPaths = retiredAssetPaths();
|
|
282
324
|
const legacyRecord = owned.manifest && owned.raw && asLegacy(owned.manifest)
|
|
283
|
-
? {
|
|
325
|
+
? {
|
|
326
|
+
manifest: asLegacy(owned.manifest),
|
|
327
|
+
manifestPath: destination(root, MANIFEST_NAME),
|
|
328
|
+
manifestSha256: sha256(owned.raw),
|
|
329
|
+
}
|
|
284
330
|
: undefined;
|
|
285
331
|
const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
|
|
286
|
-
const operations = profiles.plan.operations.map((item) => ({
|
|
332
|
+
const operations = profiles.plan.operations.map((item) => ({
|
|
333
|
+
path: item.path,
|
|
334
|
+
operation: item.operation,
|
|
335
|
+
reason: item.reason,
|
|
336
|
+
}));
|
|
287
337
|
const mutations = [...profiles.mutations];
|
|
288
338
|
const archiveCandidates = [];
|
|
289
339
|
const desiredFiles = {};
|
|
@@ -298,13 +348,29 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
298
348
|
const record = owned.manifest?.files[asset.relativePath];
|
|
299
349
|
if (!current) {
|
|
300
350
|
operations.push({ path: asset.relativePath, operation: "create", sha256: asset.sha256 });
|
|
301
|
-
mutations.push({
|
|
351
|
+
mutations.push({
|
|
352
|
+
path: asset.relativePath,
|
|
353
|
+
operation: "write",
|
|
354
|
+
content: asset.content,
|
|
355
|
+
mode: asset.mode,
|
|
356
|
+
expected: { absent: true },
|
|
357
|
+
});
|
|
302
358
|
}
|
|
303
359
|
else if (!record) {
|
|
304
|
-
operations.push({
|
|
360
|
+
operations.push({
|
|
361
|
+
path: asset.relativePath,
|
|
362
|
+
operation: "conflict",
|
|
363
|
+
reason: "unmanaged_file",
|
|
364
|
+
sha256: sha256(current),
|
|
365
|
+
});
|
|
305
366
|
}
|
|
306
367
|
else if (sha256(current) !== record.sha256) {
|
|
307
|
-
operations.push({
|
|
368
|
+
operations.push({
|
|
369
|
+
path: asset.relativePath,
|
|
370
|
+
operation: "conflict",
|
|
371
|
+
reason: "managed_file_changed",
|
|
372
|
+
sha256: sha256(current),
|
|
373
|
+
});
|
|
308
374
|
}
|
|
309
375
|
else if (current.equals(asset.content) &&
|
|
310
376
|
((await lstat(destination(root, asset.relativePath))).mode & 0o777) === asset.mode) {
|
|
@@ -312,7 +378,13 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
312
378
|
}
|
|
313
379
|
else {
|
|
314
380
|
operations.push({ path: asset.relativePath, operation: "update", sha256: asset.sha256 });
|
|
315
|
-
mutations.push({
|
|
381
|
+
mutations.push({
|
|
382
|
+
path: asset.relativePath,
|
|
383
|
+
operation: "write",
|
|
384
|
+
content: asset.content,
|
|
385
|
+
mode: asset.mode,
|
|
386
|
+
expected: { sha256: record.sha256 },
|
|
387
|
+
});
|
|
316
388
|
}
|
|
317
389
|
}
|
|
318
390
|
const active = new Set(bundled.map((asset) => asset.relativePath));
|
|
@@ -325,28 +397,72 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
325
397
|
operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
|
|
326
398
|
}
|
|
327
399
|
else if (sha256(current) !== record.sha256) {
|
|
328
|
-
operations.push({
|
|
400
|
+
operations.push({
|
|
401
|
+
path: relativePath,
|
|
402
|
+
operation: "conflict",
|
|
403
|
+
reason: "managed_file_changed",
|
|
404
|
+
sha256: sha256(current),
|
|
405
|
+
});
|
|
329
406
|
}
|
|
330
407
|
else {
|
|
331
|
-
operations.push({
|
|
332
|
-
|
|
333
|
-
|
|
408
|
+
operations.push({
|
|
409
|
+
path: relativePath,
|
|
410
|
+
operation: "archive-pending",
|
|
411
|
+
reason: "archive lifecycle is pending",
|
|
412
|
+
sha256: record.sha256,
|
|
413
|
+
});
|
|
414
|
+
archiveCandidates.push({
|
|
415
|
+
path: relativePath,
|
|
416
|
+
record,
|
|
417
|
+
content: current,
|
|
418
|
+
reason: "retired managed asset",
|
|
419
|
+
kind: record.kind,
|
|
420
|
+
});
|
|
421
|
+
mutations.push({
|
|
422
|
+
path: relativePath,
|
|
423
|
+
operation: "remove",
|
|
424
|
+
expected: { sha256: record.sha256 },
|
|
425
|
+
});
|
|
334
426
|
}
|
|
335
427
|
continue;
|
|
336
428
|
}
|
|
337
429
|
if (relativePath.startsWith("agents/")) {
|
|
338
|
-
operations.push({
|
|
430
|
+
operations.push({
|
|
431
|
+
path: relativePath,
|
|
432
|
+
operation: "conflict",
|
|
433
|
+
reason: "v1.0.0_agent_ownership_mismatch",
|
|
434
|
+
});
|
|
339
435
|
continue;
|
|
340
436
|
}
|
|
341
437
|
if (!current)
|
|
342
438
|
operations.push({ path: relativePath, operation: "missing" });
|
|
343
439
|
else if (sha256(current) === record.sha256) {
|
|
344
|
-
operations.push({
|
|
345
|
-
|
|
346
|
-
|
|
440
|
+
operations.push({
|
|
441
|
+
path: relativePath,
|
|
442
|
+
operation: "archive-pending",
|
|
443
|
+
reason: "stale managed asset is archived",
|
|
444
|
+
sha256: record.sha256,
|
|
445
|
+
});
|
|
446
|
+
archiveCandidates.push({
|
|
447
|
+
path: relativePath,
|
|
448
|
+
record,
|
|
449
|
+
content: current,
|
|
450
|
+
reason: "stale managed asset",
|
|
451
|
+
kind: record.kind,
|
|
452
|
+
});
|
|
453
|
+
mutations.push({
|
|
454
|
+
path: relativePath,
|
|
455
|
+
operation: "remove",
|
|
456
|
+
expected: { sha256: record.sha256 },
|
|
457
|
+
});
|
|
347
458
|
}
|
|
348
459
|
else
|
|
349
|
-
operations.push({
|
|
460
|
+
operations.push({
|
|
461
|
+
path: relativePath,
|
|
462
|
+
operation: "conflict",
|
|
463
|
+
reason: "managed_file_changed",
|
|
464
|
+
sha256: sha256(current),
|
|
465
|
+
});
|
|
350
466
|
}
|
|
351
467
|
}
|
|
352
468
|
else {
|
|
@@ -357,28 +473,68 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
357
473
|
operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
|
|
358
474
|
}
|
|
359
475
|
else if (sha256(current) !== record.sha256) {
|
|
360
|
-
operations.push({
|
|
476
|
+
operations.push({
|
|
477
|
+
path: relativePath,
|
|
478
|
+
operation: "conflict",
|
|
479
|
+
reason: "managed_file_changed",
|
|
480
|
+
sha256: sha256(current),
|
|
481
|
+
});
|
|
361
482
|
}
|
|
362
483
|
else {
|
|
363
|
-
operations.push({
|
|
364
|
-
|
|
365
|
-
|
|
484
|
+
operations.push({
|
|
485
|
+
path: relativePath,
|
|
486
|
+
operation: "archive-pending",
|
|
487
|
+
reason: "archive lifecycle is pending",
|
|
488
|
+
sha256: record.sha256,
|
|
489
|
+
});
|
|
490
|
+
archiveCandidates.push({
|
|
491
|
+
path: relativePath,
|
|
492
|
+
record,
|
|
493
|
+
content: current,
|
|
494
|
+
reason: "retired managed asset",
|
|
495
|
+
kind: record.kind,
|
|
496
|
+
});
|
|
497
|
+
mutations.push({
|
|
498
|
+
path: relativePath,
|
|
499
|
+
operation: "remove",
|
|
500
|
+
expected: { sha256: record.sha256 },
|
|
501
|
+
});
|
|
366
502
|
}
|
|
367
503
|
}
|
|
368
504
|
else if (!current)
|
|
369
505
|
operations.push({ path: relativePath, operation: "missing" });
|
|
370
506
|
else if (sha256(current) === record.sha256) {
|
|
371
|
-
operations.push({
|
|
372
|
-
|
|
373
|
-
|
|
507
|
+
operations.push({
|
|
508
|
+
path: relativePath,
|
|
509
|
+
operation: "archive-pending",
|
|
510
|
+
reason: "stale managed asset is archived",
|
|
511
|
+
sha256: record.sha256,
|
|
512
|
+
});
|
|
513
|
+
archiveCandidates.push({
|
|
514
|
+
path: relativePath,
|
|
515
|
+
record,
|
|
516
|
+
content: current,
|
|
517
|
+
reason: "stale managed asset",
|
|
518
|
+
kind: record.kind,
|
|
519
|
+
});
|
|
520
|
+
mutations.push({
|
|
521
|
+
path: relativePath,
|
|
522
|
+
operation: "remove",
|
|
523
|
+
expected: { sha256: record.sha256 },
|
|
524
|
+
});
|
|
374
525
|
}
|
|
375
526
|
else {
|
|
376
|
-
operations.push({
|
|
527
|
+
operations.push({
|
|
528
|
+
path: relativePath,
|
|
529
|
+
operation: "conflict",
|
|
530
|
+
reason: "managed_file_changed",
|
|
531
|
+
sha256: sha256(current),
|
|
532
|
+
});
|
|
377
533
|
desiredFiles[relativePath] = record;
|
|
378
534
|
}
|
|
379
535
|
}
|
|
380
536
|
}
|
|
381
|
-
mutations.push(...await archiveMutations(archiveCandidates, scope, cwd, home, owned.manifest?.package_version ?? "1.0.0"));
|
|
537
|
+
mutations.push(...(await archiveMutations(archiveCandidates, scope, cwd, home, owned.manifest?.package_version ?? "1.0.0")));
|
|
382
538
|
const nextManifest = action === "install" || Object.keys(desiredFiles).length
|
|
383
539
|
? {
|
|
384
540
|
schema_version: 2,
|
|
@@ -395,15 +551,46 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
395
551
|
: undefined;
|
|
396
552
|
const manifestContent = nextManifest ? Buffer.from(`${stable(nextManifest)}\n`) : undefined;
|
|
397
553
|
if (manifestContent && (!owned.raw || !owned.raw.equals(manifestContent))) {
|
|
398
|
-
operations.push({
|
|
399
|
-
|
|
554
|
+
operations.push({
|
|
555
|
+
path: MANIFEST_NAME,
|
|
556
|
+
operation: owned.raw ? "update" : "create",
|
|
557
|
+
reason: "generic installer ownership",
|
|
558
|
+
});
|
|
559
|
+
mutations.push({
|
|
560
|
+
path: MANIFEST_NAME,
|
|
561
|
+
operation: "write",
|
|
562
|
+
content: manifestContent,
|
|
563
|
+
mode: 0o600,
|
|
564
|
+
expected: owned.raw ? { sha256: sha256(owned.raw) } : { absent: true },
|
|
565
|
+
});
|
|
400
566
|
}
|
|
401
567
|
else if (!manifestContent && owned.raw) {
|
|
402
|
-
operations.push({
|
|
403
|
-
|
|
568
|
+
operations.push({
|
|
569
|
+
path: MANIFEST_NAME,
|
|
570
|
+
operation: "remove",
|
|
571
|
+
reason: "generic assets uninstalled",
|
|
572
|
+
});
|
|
573
|
+
mutations.push({
|
|
574
|
+
path: MANIFEST_NAME,
|
|
575
|
+
operation: "remove",
|
|
576
|
+
expected: { sha256: sha256(owned.raw) },
|
|
577
|
+
});
|
|
404
578
|
}
|
|
405
579
|
const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
|
|
406
|
-
const base = {
|
|
580
|
+
const base = {
|
|
581
|
+
schema_version: 2,
|
|
582
|
+
action,
|
|
583
|
+
scope,
|
|
584
|
+
root,
|
|
585
|
+
package_version: packageVersion(),
|
|
586
|
+
selection,
|
|
587
|
+
operations: sorted,
|
|
588
|
+
requires_restart: (action === "install" && owned.manifest?.package_version !== packageVersion()) ||
|
|
589
|
+
profiles.plan.requires_restart ||
|
|
590
|
+
mutations.some((item) => item.path.startsWith("agents/") ||
|
|
591
|
+
item.path.startsWith("commands/") ||
|
|
592
|
+
item.path.startsWith("plugins/")),
|
|
593
|
+
};
|
|
407
594
|
const planDigest = digest(base);
|
|
408
595
|
return {
|
|
409
596
|
plan: { ...base, plan_digest: planDigest, digest: planDigest },
|
|
@@ -446,16 +633,25 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
|
|
|
446
633
|
return await withLifecycleLock(stateRoot, async () => {
|
|
447
634
|
if (await recoverTransaction(root, stateRoot))
|
|
448
635
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
449
|
-
const receipt = (await consumeReceipt(stateRoot, {
|
|
636
|
+
const receipt = (await consumeReceipt(stateRoot, {
|
|
637
|
+
digest: confirmationDigest,
|
|
638
|
+
kind: `installer:${action}`,
|
|
639
|
+
scope,
|
|
640
|
+
root,
|
|
641
|
+
}));
|
|
450
642
|
const built = await build(action, scope, cwd, home, selection);
|
|
451
643
|
const savedPlanDigest = receipt.plan_digest ??
|
|
452
644
|
receipt.digest;
|
|
453
645
|
if (built.plan.digest !== savedPlanDigest)
|
|
454
646
|
throw new InstallerError("stale_plan", "Installer plan changed after preview");
|
|
455
|
-
if (built.plan.operations.some((item) => item.operation === "conflict" &&
|
|
647
|
+
if (built.plan.operations.some((item) => item.operation === "conflict" &&
|
|
648
|
+
(item.reason === "unmanaged_file" ||
|
|
649
|
+
item.reason === "v1.0.0_agent_ownership_mismatch" ||
|
|
650
|
+
item.reason?.includes("collision")))) {
|
|
456
651
|
throw new InstallerError("conflict", "Installer plan contains an exact-name ownership conflict");
|
|
457
652
|
}
|
|
458
|
-
if (action === "install" &&
|
|
653
|
+
if (action === "install" &&
|
|
654
|
+
built.plan.operations.some((item) => item.operation === "conflict"))
|
|
459
655
|
throw new InstallerError("conflict", "Installer plan contains managed drift");
|
|
460
656
|
await applyTransaction(root, stateRoot, built.mutations, {
|
|
461
657
|
...options,
|
|
@@ -466,7 +662,12 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
|
|
|
466
662
|
if (action !== "install")
|
|
467
663
|
return;
|
|
468
664
|
const inventory = await listAgentProfiles(scope, cwd, home);
|
|
469
|
-
if (built.plan.selection.agents.length > 0 &&
|
|
665
|
+
if (built.plan.selection.agents.length > 0 &&
|
|
666
|
+
(inventory.collisions.length ||
|
|
667
|
+
inventory.drift.length ||
|
|
668
|
+
inventory.profiles
|
|
669
|
+
.filter((item) => item.ownership !== "user-owned")
|
|
670
|
+
.some((item) => item.state !== "current"))) {
|
|
470
671
|
throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
|
|
471
672
|
}
|
|
472
673
|
},
|
package/dist/plugins/rtk.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
const THRESHOLD = 8_000;
|
|
3
3
|
const FILTERS = [
|
|
4
|
-
{ prefix: ["git", "log"], filter: "git-log" },
|
|
5
|
-
{ prefix: ["git", "
|
|
6
|
-
{ prefix: ["
|
|
4
|
+
{ prefix: ["git", "log"], filter: "git-log" },
|
|
5
|
+
{ prefix: ["git", "diff"], filter: "git-diff" },
|
|
6
|
+
{ prefix: ["git", "status"], filter: "git-status" },
|
|
7
|
+
{ prefix: ["rg"], filter: "grep" },
|
|
8
|
+
{ prefix: ["grep"], filter: "grep" },
|
|
9
|
+
{ prefix: ["pytest"], filter: "pytest" },
|
|
10
|
+
{ prefix: ["tsc"], filter: "tsc" },
|
|
7
11
|
];
|
|
8
12
|
function filter(command) {
|
|
9
13
|
if (/[|;$&\n()`]/.test(command))
|
|
@@ -16,14 +20,21 @@ function filter(command) {
|
|
|
16
20
|
function truncate(value) {
|
|
17
21
|
const points = Array.from(value);
|
|
18
22
|
const size = 3_200;
|
|
19
|
-
return points.length <= size * 2
|
|
23
|
+
return points.length <= size * 2
|
|
24
|
+
? value
|
|
25
|
+
: `${points.slice(0, size).join("")}\n…\n${points.slice(-size).join("")}`;
|
|
20
26
|
}
|
|
21
27
|
function external(binary, selected, input) {
|
|
22
28
|
return new Promise((done) => {
|
|
23
|
-
const child = spawn(binary, ["pipe", "--filter", selected], {
|
|
29
|
+
const child = spawn(binary, ["pipe", "--filter", selected], {
|
|
30
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
31
|
+
timeout: 5_000,
|
|
32
|
+
});
|
|
24
33
|
let output = "";
|
|
25
34
|
child.stdout.setEncoding("utf8");
|
|
26
|
-
child.stdout.on("data", (chunk) => {
|
|
35
|
+
child.stdout.on("data", (chunk) => {
|
|
36
|
+
output += chunk;
|
|
37
|
+
});
|
|
27
38
|
child.once("error", () => done(undefined));
|
|
28
39
|
child.once("close", (code) => done(code === 0 ? output : undefined));
|
|
29
40
|
child.stdin.end(input, "utf8");
|
|
@@ -32,20 +43,31 @@ function external(binary, selected, input) {
|
|
|
32
43
|
export async function rtk(options = {}) {
|
|
33
44
|
if (options.enabled === false)
|
|
34
45
|
return {};
|
|
35
|
-
const run = options.run ??
|
|
36
|
-
|
|
46
|
+
const run = options.run ??
|
|
47
|
+
((selected, input) => external(options.bin ?? "rtk", selected, input));
|
|
48
|
+
return {
|
|
49
|
+
"tool.execute.after": async (input, output) => {
|
|
37
50
|
try {
|
|
38
|
-
if (input.tool === "edit" &&
|
|
51
|
+
if (input.tool === "edit" &&
|
|
52
|
+
typeof output.output === "string" &&
|
|
53
|
+
/oldString (not found|found multiple times|and newString must be different)/i.test(output.output))
|
|
39
54
|
output.output = `${output.output}\nSTOP. Read the file before retrying Edit.`;
|
|
40
|
-
if (input.tool !== "bash" ||
|
|
55
|
+
if (input.tool !== "bash" ||
|
|
56
|
+
typeof output.output !== "string" ||
|
|
57
|
+
output.output.length < (options.threshold ?? THRESHOLD))
|
|
41
58
|
return;
|
|
42
59
|
const selected = typeof input.args?.command === "string" ? filter(input.args.command) : undefined;
|
|
43
60
|
const compressed = selected ? await run(selected, output.output) : undefined;
|
|
44
|
-
const result = compressed && compressed.length < output.output.length
|
|
61
|
+
const result = compressed && compressed.length < output.output.length
|
|
62
|
+
? compressed
|
|
63
|
+
: truncate(output.output);
|
|
45
64
|
const originalSize = output.output.length;
|
|
46
65
|
output.output = `${result}\n[rtk: compressed method=${compressed ? `rtk/${selected}` : "head+tail"}; sizes=${originalSize}->${result.length}; evidence_complete=false; loss=possible]`;
|
|
47
66
|
}
|
|
48
|
-
catch {
|
|
49
|
-
|
|
67
|
+
catch {
|
|
68
|
+
/* RTK is fail-open: preserve original tool output on plugin failure. */
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
50
72
|
}
|
|
51
73
|
export default rtk;
|
|
@@ -9,7 +9,7 @@ export declare function rulesInjector(options?: RulesInjectorOptions): Promise<{
|
|
|
9
9
|
"experimental.chat.system.transform"?: undefined;
|
|
10
10
|
} | {
|
|
11
11
|
"tool.execute.after": (input: unknown, result: unknown) => Promise<void>;
|
|
12
|
-
event: ({ event }: {
|
|
12
|
+
event: ({ event, }: {
|
|
13
13
|
event?: {
|
|
14
14
|
type?: string;
|
|
15
15
|
properties?: Record<string, unknown>;
|
|
@@ -2,19 +2,33 @@ import { lstat, readFile, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
const NAME = "AGENTS.md";
|
|
5
|
-
function sessionID(value) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
function sessionID(value) {
|
|
6
|
+
const item = value;
|
|
7
|
+
for (const key of ["sessionID", "sessionId", "session_id"])
|
|
8
|
+
if (typeof item?.[key] === "string" && item[key])
|
|
9
|
+
return item[key];
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
function pathOf(input, output) {
|
|
13
|
+
for (const value of [input, output]) {
|
|
14
|
+
const args = value?.args;
|
|
15
|
+
for (const key of ["filePath", "file_path", "path"])
|
|
16
|
+
if (typeof args?.[key] === "string")
|
|
17
|
+
return args[key];
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function output(value, text) {
|
|
22
|
+
const item = value;
|
|
23
|
+
if (typeof item?.output === "string")
|
|
24
|
+
item.output = `${item.output}\n${text}`;
|
|
25
|
+
}
|
|
26
|
+
function marker(rule) {
|
|
27
|
+
return `[rules-injector source=${rule.path} revision=${rule.revision}]`;
|
|
28
|
+
}
|
|
29
|
+
function block(rule) {
|
|
30
|
+
return `${marker(rule)}\n${rule.content}\n[/rules-injector source=${rule.path}]`;
|
|
31
|
+
}
|
|
18
32
|
export async function rulesInjector(options = {}) {
|
|
19
33
|
if (options.enabled === false)
|
|
20
34
|
return {};
|
|
@@ -22,10 +36,14 @@ export async function rulesInjector(options = {}) {
|
|
|
22
36
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
23
37
|
const global = resolve(process.env.XDG_CONFIG_HOME ?? resolve(homedir(), ".config"), "opencode");
|
|
24
38
|
const sessions = new Map();
|
|
25
|
-
const state = (identifier) => {
|
|
26
|
-
item =
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
const state = (identifier) => {
|
|
40
|
+
let item = sessions.get(identifier);
|
|
41
|
+
if (!item) {
|
|
42
|
+
item = { loaded: new Map(), injected: new Set(), used: 0, replay: false };
|
|
43
|
+
sessions.set(identifier, item);
|
|
44
|
+
}
|
|
45
|
+
return item;
|
|
46
|
+
};
|
|
29
47
|
const inject = async (input, result) => {
|
|
30
48
|
const call = input;
|
|
31
49
|
if (call?.tool !== "read" && call?.tool !== "edit")
|
|
@@ -42,7 +60,9 @@ export async function rulesInjector(options = {}) {
|
|
|
42
60
|
const candidate = resolve(current, NAME);
|
|
43
61
|
try {
|
|
44
62
|
const info = await lstat(candidate);
|
|
45
|
-
if (info.isFile() &&
|
|
63
|
+
if (info.isFile() &&
|
|
64
|
+
candidate !== resolve(native, NAME) &&
|
|
65
|
+
candidate !== resolve(global, NAME))
|
|
46
66
|
paths.push(await realpath(candidate));
|
|
47
67
|
}
|
|
48
68
|
catch (error) {
|
|
@@ -63,7 +83,11 @@ export async function rulesInjector(options = {}) {
|
|
|
63
83
|
const info = await lstat(path);
|
|
64
84
|
if (!info.isFile())
|
|
65
85
|
continue;
|
|
66
|
-
const rule = {
|
|
86
|
+
const rule = {
|
|
87
|
+
path,
|
|
88
|
+
revision: `${Math.trunc(info.mtimeMs)}:${info.size}`,
|
|
89
|
+
content: await readFile(path, "utf8"),
|
|
90
|
+
};
|
|
67
91
|
const text = block(rule);
|
|
68
92
|
if (currentState.used + text.length > budget) {
|
|
69
93
|
additions.push(`[rules-injector warning] context budget exhausted; skipped ${path}`);
|
|
@@ -85,12 +109,23 @@ export async function rulesInjector(options = {}) {
|
|
|
85
109
|
output(result, `[rules-injector warning] cannot read AGENTS.md: ${String(error)}`);
|
|
86
110
|
}
|
|
87
111
|
};
|
|
88
|
-
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
return {
|
|
113
|
+
"tool.execute.after": inject,
|
|
114
|
+
event: async ({ event, }) => {
|
|
115
|
+
if (event?.type?.includes("compaction")) {
|
|
116
|
+
const identifier = sessionID(event.properties);
|
|
117
|
+
if (identifier)
|
|
118
|
+
state(identifier).replay = true;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"experimental.chat.system.transform": async (input, result) => {
|
|
122
|
+
const identifier = sessionID(input);
|
|
123
|
+
if (!identifier || !state(identifier).replay || !Array.isArray(result.system))
|
|
124
|
+
return;
|
|
125
|
+
for (const rule of state(identifier).loaded.values())
|
|
126
|
+
result.system.push(block(rule));
|
|
127
|
+
state(identifier).replay = false;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
95
130
|
}
|
|
96
131
|
export default rulesInjector;
|