@fro.bot/systematic 2.7.0 → 2.7.2
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/dist/index.js +55 -7
- package/dist/lib/plugin-singleton.d.ts +78 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ var INTERNAL_AGENT_SIGNATURES = [
|
|
|
24
24
|
"You are a helpful AI assistant tasked with summarizing conversations",
|
|
25
25
|
"Summarize what was done in this conversation"
|
|
26
26
|
];
|
|
27
|
-
function getToolMappingTemplate(
|
|
27
|
+
function getToolMappingTemplate() {
|
|
28
28
|
return `**Tool Mapping for OpenCode:**
|
|
29
29
|
When skills reference tools you don't have, substitute OpenCode equivalents:
|
|
30
30
|
- \`TodoWrite\` \u2192 \`todowrite\`
|
|
@@ -42,7 +42,7 @@ When skills reference tools you don't have, substitute OpenCode equivalents:
|
|
|
42
42
|
- Use the native \`skill\` tool for non-Systematic skills
|
|
43
43
|
|
|
44
44
|
**Skills location:**
|
|
45
|
-
Bundled skills are
|
|
45
|
+
Bundled skills ship with the Systematic plugin and are discoverable via \`systematic_skill\`.`;
|
|
46
46
|
}
|
|
47
47
|
function getBootstrapContent(config, deps) {
|
|
48
48
|
const { bundledSkillsDir } = deps;
|
|
@@ -60,7 +60,7 @@ function getBootstrapContent(config, deps) {
|
|
|
60
60
|
const fullContent = fs.readFileSync(usingSystematicPath, "utf8");
|
|
61
61
|
const { body } = parseFrontmatter(fullContent);
|
|
62
62
|
const content = body.trim();
|
|
63
|
-
const toolMapping = getToolMappingTemplate(
|
|
63
|
+
const toolMapping = getToolMappingTemplate();
|
|
64
64
|
return `<SYSTEMATIC_WORKFLOWS>
|
|
65
65
|
You have access to structured engineering workflows via the systematic plugin.
|
|
66
66
|
|
|
@@ -310,6 +310,37 @@ function registerSkillsPaths(config, skillsDir) {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// src/lib/plugin-singleton.ts
|
|
314
|
+
var SINGLETON_KEY = Symbol.for("systematic.singleton.v1");
|
|
315
|
+
async function plugInOnce({
|
|
316
|
+
doInit,
|
|
317
|
+
onDuplicate,
|
|
318
|
+
pid
|
|
319
|
+
}) {
|
|
320
|
+
const currentPid = pid ?? process.pid;
|
|
321
|
+
const g = globalThis;
|
|
322
|
+
const existing = g[SINGLETON_KEY];
|
|
323
|
+
if (existing && existing.pid === currentPid) {
|
|
324
|
+
if (!existing.warned) {
|
|
325
|
+
existing.warned = true;
|
|
326
|
+
try {
|
|
327
|
+
onDuplicate?.(currentPid);
|
|
328
|
+
} catch {}
|
|
329
|
+
}
|
|
330
|
+
await existing.hooksPromise;
|
|
331
|
+
return { isFirst: false, hooks: {} };
|
|
332
|
+
}
|
|
333
|
+
const hooksPromise = doInit();
|
|
334
|
+
g[SINGLETON_KEY] = {
|
|
335
|
+
pid: currentPid,
|
|
336
|
+
loadedAt: Date.now(),
|
|
337
|
+
hooksPromise,
|
|
338
|
+
warned: false
|
|
339
|
+
};
|
|
340
|
+
const hooks = await hooksPromise;
|
|
341
|
+
return { isFirst: true, hooks };
|
|
342
|
+
}
|
|
343
|
+
|
|
313
344
|
// src/lib/skill-tool.ts
|
|
314
345
|
import fs2 from "fs";
|
|
315
346
|
import path3 from "path";
|
|
@@ -493,8 +524,9 @@ var getPackageVersion = () => {
|
|
|
493
524
|
return "unknown";
|
|
494
525
|
}
|
|
495
526
|
};
|
|
496
|
-
var
|
|
527
|
+
var initializePlugin = async ({ client, directory }) => {
|
|
497
528
|
const config = loadConfig(directory);
|
|
529
|
+
const bootstrapContent = getBootstrapContent(config, { bundledSkillsDir });
|
|
498
530
|
const configHandler = createConfigHandler({
|
|
499
531
|
directory,
|
|
500
532
|
bundledSkillsDir,
|
|
@@ -544,13 +576,29 @@ var SystematicPlugin = async ({ client, directory }) => {
|
|
|
544
576
|
} catch {}
|
|
545
577
|
return;
|
|
546
578
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
applyBootstrapContent(output, content);
|
|
579
|
+
if (bootstrapContent) {
|
|
580
|
+
applyBootstrapContent(output, bootstrapContent);
|
|
550
581
|
}
|
|
551
582
|
}
|
|
552
583
|
};
|
|
553
584
|
};
|
|
585
|
+
var SystematicPlugin = async (input) => {
|
|
586
|
+
const { hooks } = await plugInOnce({
|
|
587
|
+
doInit: () => initializePlugin(input),
|
|
588
|
+
onDuplicate: (pid) => {
|
|
589
|
+
const message = `[systematic] duplicate factory invocation in same process (pid=${pid}); skipping duplicate registration. Multiple opencode.json sources may list this plugin.`;
|
|
590
|
+
console.warn(message);
|
|
591
|
+
input.client.app.log({
|
|
592
|
+
body: {
|
|
593
|
+
service: "systematic",
|
|
594
|
+
level: "warn",
|
|
595
|
+
message
|
|
596
|
+
}
|
|
597
|
+
}).catch(() => {});
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
return hooks;
|
|
601
|
+
};
|
|
554
602
|
var src_default = SystematicPlugin;
|
|
555
603
|
export {
|
|
556
604
|
src_default as default
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-process register-once guard for the Systematic plugin factory.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode invokes the plugin factory more than once per process when the
|
|
5
|
+
* same plugin is referenced by multiple config sources (for example a
|
|
6
|
+
* user-level `~/.config/opencode/opencode.json` AND a project-level
|
|
7
|
+
* `opencode.json`). Each invocation evaluates the plugin module fresh —
|
|
8
|
+
* module-local variables reset between calls — so the guard state must
|
|
9
|
+
* live on `globalThis` to persist across module instances within the
|
|
10
|
+
* same process.
|
|
11
|
+
*
|
|
12
|
+
* On the first invocation `doInit` runs and the resulting hooks promise
|
|
13
|
+
* is cached on `globalThis`; the caller receives `{ isFirst: true, hooks }`.
|
|
14
|
+
* On every subsequent invocation in the same PID `doInit` is skipped, the
|
|
15
|
+
* cached promise is awaited (so sticky rejections still propagate),
|
|
16
|
+
* `onDuplicate` fires exactly once, and the caller receives
|
|
17
|
+
* `{ isFirst: false, hooks: {} as T }`. Across PIDs the guard is treated
|
|
18
|
+
* as absent and init runs fresh — `globalThis` is per-process, but the
|
|
19
|
+
* explicit PID check adds defensive belt-and-suspenders against any
|
|
20
|
+
* state-leakage edge case.
|
|
21
|
+
*
|
|
22
|
+
* **Why empty hooks on duplicate invocations.** A whole-hooks singleton
|
|
23
|
+
* that returns the same hooks reference to both invocations does not
|
|
24
|
+
* deduplicate host-side tool registration: OpenCode iterates each plugin
|
|
25
|
+
* source's returned hook surface and registers every tool entry it finds,
|
|
26
|
+
* even when two sources return the same JS reference. Returning `{}` from
|
|
27
|
+
* the duplicate path is the only shape that prevents host-side double
|
|
28
|
+
* registration of `systematic_skill`, the `config` hook, and the
|
|
29
|
+
* `experimental.chat.system.transform` hook. The Phase 0 follow-up probe
|
|
30
|
+
* (see `docs/plans/2026-05-01-001-fix-idempotent-plugin-registration-plan.md`)
|
|
31
|
+
* verified the duplicate `systematic_skill` entry in OpenCode's
|
|
32
|
+
* `client.tool.list(...)` output, which is the LLM-visible tool catalog.
|
|
33
|
+
*
|
|
34
|
+
* **Known limitation — rejected init is sticky.** If `doInit()` rejects,
|
|
35
|
+
* the rejected promise is stored on `globalThis` and every subsequent
|
|
36
|
+
* invocation in the same PID returns the same rejection without retrying
|
|
37
|
+
* init. This is intentional: re-running heavy init on every call would
|
|
38
|
+
* defeat the guard's purpose. Recovery requires a process restart.
|
|
39
|
+
*/
|
|
40
|
+
export interface PlugInOnceOptions<T> {
|
|
41
|
+
/** Heavy init work that should run at most once per process. */
|
|
42
|
+
doInit: () => Promise<T>;
|
|
43
|
+
/**
|
|
44
|
+
* Called exactly once on the first duplicate invocation in the same
|
|
45
|
+
* process. Subsequent duplicates are silent. Receives the same `pid`
|
|
46
|
+
* value the guard used for its identity check (so test overrides flow
|
|
47
|
+
* through faithfully). Implementations must not throw; fire-and-forget
|
|
48
|
+
* side effects (logging, metrics) are expected. Synchronous exceptions
|
|
49
|
+
* are swallowed defensively.
|
|
50
|
+
*/
|
|
51
|
+
onDuplicate?: (pid: number) => void;
|
|
52
|
+
/** Test override; defaults to `process.pid`. */
|
|
53
|
+
pid?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Result envelope for `plugInOnce(...)`.
|
|
57
|
+
*
|
|
58
|
+
* - `isFirst: true` — caller should return `hooks` to OpenCode.
|
|
59
|
+
* - `isFirst: false` — caller MUST return `hooks` (which is an empty `{}`)
|
|
60
|
+
* so the host loader does not register tools or hooks twice.
|
|
61
|
+
*
|
|
62
|
+
* Callers that just `return result.hooks` do the right thing in both
|
|
63
|
+
* cases without needing to inspect `isFirst`.
|
|
64
|
+
*/
|
|
65
|
+
export interface PlugInOnceResult<T> {
|
|
66
|
+
isFirst: boolean;
|
|
67
|
+
hooks: T;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Run `doInit` at most once per process; on duplicate invocations resolve to
|
|
71
|
+
* empty hooks so the OpenCode host does not double-register tools and hooks.
|
|
72
|
+
*/
|
|
73
|
+
export declare function plugInOnce<T>({ doInit, onDuplicate, pid, }: PlugInOnceOptions<T>): Promise<PlugInOnceResult<T>>;
|
|
74
|
+
/**
|
|
75
|
+
* Test-only: clear the singleton state so the next invocation re-runs
|
|
76
|
+
* init. Must not be called in production code paths.
|
|
77
|
+
*/
|
|
78
|
+
export declare function _resetPluginSingleton(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://fro.bot/systematic",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"docs:preview": "bun run --cwd docs preview",
|
|
38
38
|
"docs:generate": "bun docs/scripts/transform-content.ts",
|
|
39
39
|
"registry:build": "bun scripts/build-registry.ts",
|
|
40
|
+
"registry:drift": "bun scripts/generate-registry.ts --check",
|
|
40
41
|
"registry:validate": "bun scripts/build-registry.ts --validate-only",
|
|
41
42
|
"prepublishOnly": "bun run build"
|
|
42
43
|
},
|