@ericsanchezok/synergy-plugin-blueprint 0.0.0-stage → 3.1.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/AGENTS.md +9 -0
- package/dist/adapter.js +285 -0
- package/dist/component.js +18 -0
- package/dist/features.js +14 -0
- package/dist/register.js +8 -0
- package/dist/worker.js +6 -0
- package/package.json +81 -4
- package/src/adapter.ts +345 -0
- package/src/component.ts +19 -0
- package/src/features.ts +14 -0
- package/src/register.ts +8 -0
- package/src/worker.ts +6 -0
- package/README.md +0 -3
package/AGENTS.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# plugin-blueprint Package
|
|
2
|
+
|
|
3
|
+
Plugin Host BlueprintLoop services and observer delivery. Required components: Plugin Host and Blueprint. See [owning architecture](../../docs/architecture/workflows.md).
|
|
4
|
+
|
|
5
|
+
Keep plugin protocol translation in this bridge. Preserve generation checks and terminal delivery evidence.
|
|
6
|
+
|
|
7
|
+
Expose selection through `./component`; imports are inert. Keep factory requirements, published `synergy` metadata, worker roles and production dependencies aligned. Register owner schemas, events, tools and HTTP features before Runtime composition closes.
|
|
8
|
+
|
|
9
|
+
Tests under `test/` select this owner and its real requirements. Run `bun run typecheck` and focused `bun test test/<file>.test.ts` with the package preload; then run the root dependency and installed-composition checks.
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { hash as sha256 } from "@ericsanchezok/synergy-util/encode";
|
|
2
|
+
import { Agent } from "@ericsanchezok/synergy-harness/agent/agent";
|
|
3
|
+
import { Cortex } from "@ericsanchezok/synergy-harness/cortex";
|
|
4
|
+
import { Provider } from "@ericsanchezok/synergy-harness/provider/provider";
|
|
5
|
+
import { NoteMarkdown, NoteStore } from "@ericsanchezok/synergy-note";
|
|
6
|
+
import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context";
|
|
7
|
+
import { Session } from "@ericsanchezok/synergy-harness/session";
|
|
8
|
+
import { readPluginManifest } from "@ericsanchezok/synergy-plugin-host/plugin/spec-resolver";
|
|
9
|
+
import { BlueprintLoopStore } from "@ericsanchezok/synergy-blueprint/blueprint/loop-store";
|
|
10
|
+
import { BlueprintLoopService } from "@ericsanchezok/synergy-blueprint/blueprint/service";
|
|
11
|
+
import { BlueprintLoopRuntime } from "@ericsanchezok/synergy-blueprint/blueprint/loop-runtime";
|
|
12
|
+
import { Log } from "@ericsanchezok/synergy-harness/util/log";
|
|
13
|
+
const log = Log.create({ service: "blueprint.plugin-adapter" });
|
|
14
|
+
export const BlueprintPluginErrorCode = {
|
|
15
|
+
DIGEST_MISMATCH: "BLUEPRINT_DIGEST_MISMATCH",
|
|
16
|
+
NOT_FOUND: "BLUEPRINT_NOT_FOUND",
|
|
17
|
+
NOT_PLUGIN_OWNED: "BLUEPRINT_NOT_PLUGIN_OWNED",
|
|
18
|
+
};
|
|
19
|
+
function pluginAdapterError(code, message) {
|
|
20
|
+
return Object.assign(new Error(message), { name: "BlueprintPluginError", code });
|
|
21
|
+
}
|
|
22
|
+
function publicLoop(loop) {
|
|
23
|
+
return loop;
|
|
24
|
+
}
|
|
25
|
+
async function assertBlueprintDelegation(input, manifestCapability) {
|
|
26
|
+
const execAgent = await Agent.get(input.executionAgent);
|
|
27
|
+
if (!execAgent)
|
|
28
|
+
throw new Error(`Unknown execution agent: ${input.executionAgent}`);
|
|
29
|
+
if (execAgent.mode === "primary")
|
|
30
|
+
throw new Error(`Execution agent "${input.executionAgent}" is a primary agent`);
|
|
31
|
+
if (!execAgent.hidden)
|
|
32
|
+
throw new Error(`Execution agent "${input.executionAgent}" is not hidden`);
|
|
33
|
+
const auditAgentInfo = await Agent.get(input.auditAgent);
|
|
34
|
+
if (!auditAgentInfo)
|
|
35
|
+
throw new Error(`Unknown audit agent: ${input.auditAgent}`);
|
|
36
|
+
if (auditAgentInfo.mode === "primary")
|
|
37
|
+
throw new Error(`Audit agent "${input.auditAgent}" is a primary agent`);
|
|
38
|
+
if (!auditAgentInfo.hidden)
|
|
39
|
+
throw new Error(`Audit agent "${input.auditAgent}" is not hidden`);
|
|
40
|
+
if (input.executionAgent === input.auditAgent)
|
|
41
|
+
throw new Error("Execution and audit agents must differ");
|
|
42
|
+
if (input.pluginDir) {
|
|
43
|
+
const manifest = await readPluginManifest(input.pluginDir);
|
|
44
|
+
const cap = manifest.capabilities.find((c) => c.id === manifestCapability);
|
|
45
|
+
if (!cap)
|
|
46
|
+
throw new Error(`Plugin manifest does not declare capability ${manifestCapability}`);
|
|
47
|
+
const agents = Array.isArray(cap.constraints?.agents) ? cap.constraints.agents : undefined;
|
|
48
|
+
if (agents) {
|
|
49
|
+
if (!agents.includes(input.executionAgent))
|
|
50
|
+
throw new Error(`Plugin manifest does not allow Blueprint execution agent "${input.executionAgent}"`);
|
|
51
|
+
if (!agents.includes(input.auditAgent))
|
|
52
|
+
throw new Error(`Plugin manifest does not allow Blueprint audit agent "${input.auditAgent}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const execOwner = Agent.pluginOwner(execAgent);
|
|
56
|
+
if (!execOwner || execOwner.pluginId !== input.pluginId || execOwner.pluginGeneration !== input.pluginGeneration) {
|
|
57
|
+
throw new Error(`Execution agent "${input.executionAgent}" is not owned by this plugin generation`);
|
|
58
|
+
}
|
|
59
|
+
const auditOwner = Agent.pluginOwner(auditAgentInfo);
|
|
60
|
+
if (!auditOwner || auditOwner.pluginId !== input.pluginId || auditOwner.pluginGeneration !== input.pluginGeneration) {
|
|
61
|
+
throw new Error(`Audit agent "${input.auditAgent}" is not owned by this plugin generation`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function firstPromptText(title, noteID, loopID) {
|
|
65
|
+
return `Execute the Blueprint "${title}" (note ID: ${noteID}, loop ID: ${loopID}).
|
|
66
|
+
First call note_read with noteIds=["${noteID}"] and read the full Blueprint content.
|
|
67
|
+
Treat the Blueprint as the authoritative engineering contract for this run: requirements, non-goals, deliverables, constraints, quality criteria, and acceptance criteria.
|
|
68
|
+
Continue until every Blueprint requirement is implemented, verified, and integrated.
|
|
69
|
+
When the Blueprint is complete and verified, call blueprint_loop_stop with a concise summary, completed requirements, concrete evidence, and any known limitations to request independent review.`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Protocol 5 atomic Blueprint start.
|
|
73
|
+
*
|
|
74
|
+
* Validates all inputs, hashes markdown, creates a Blueprint Note, a dedicated
|
|
75
|
+
* hidden execution Session via Session.create (no execution yet), a Store record,
|
|
76
|
+
* binds, marks running, and THEN launches Cortex with the pre-created sessionID
|
|
77
|
+
* delivering exactly ONE formatted execution prompt. The source content stays in
|
|
78
|
+
* the frozen Note only.
|
|
79
|
+
*
|
|
80
|
+
* On failure at any stage, all previously created resources are rolled back
|
|
81
|
+
* (archived/cancelled).
|
|
82
|
+
*/
|
|
83
|
+
export async function startBlueprint(input) {
|
|
84
|
+
const ctx = input.context;
|
|
85
|
+
const r = input.request;
|
|
86
|
+
// --- validation ---
|
|
87
|
+
if (!r.title?.trim())
|
|
88
|
+
throw new Error("blueprint.start requires title");
|
|
89
|
+
if (!r.markdown?.trim())
|
|
90
|
+
throw new Error("blueprint.start requires markdown");
|
|
91
|
+
if (!r.correlationId?.trim())
|
|
92
|
+
throw new Error("blueprint.start requires correlationId");
|
|
93
|
+
if (!r.executionAgent?.trim())
|
|
94
|
+
throw new Error("blueprint.start requires executionAgent");
|
|
95
|
+
if (!r.auditAgent?.trim())
|
|
96
|
+
throw new Error("blueprint.start requires auditAgent");
|
|
97
|
+
if (!r.budget)
|
|
98
|
+
throw new Error("blueprint.start requires budget");
|
|
99
|
+
if (!Number.isFinite(r.budget.maxRuntimeMs) || r.budget.maxRuntimeMs <= 0)
|
|
100
|
+
throw new Error("budget.maxRuntimeMs must be a positive integer");
|
|
101
|
+
if (!Number.isFinite(r.budget.maxIterations) || r.budget.maxIterations <= 0)
|
|
102
|
+
throw new Error("budget.maxIterations must be a positive integer");
|
|
103
|
+
const title = r.title.trim();
|
|
104
|
+
const markdown = r.markdown.trim();
|
|
105
|
+
const correlationId = r.correlationId.trim();
|
|
106
|
+
const executionAgent = r.executionAgent.trim();
|
|
107
|
+
const auditAgent = r.auditAgent.trim();
|
|
108
|
+
const description = r.description?.trim();
|
|
109
|
+
// Digest validation
|
|
110
|
+
const computedDigest = await sha256(markdown);
|
|
111
|
+
if (computedDigest !== r.sourceDigest) {
|
|
112
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.DIGEST_MISMATCH, "sourceDigest does not match markdown content");
|
|
113
|
+
}
|
|
114
|
+
await assertBlueprintDelegation({
|
|
115
|
+
pluginId: ctx.pluginId,
|
|
116
|
+
pluginGeneration: ctx.pluginGeneration,
|
|
117
|
+
pluginDir: ctx.pluginDir,
|
|
118
|
+
executionAgent,
|
|
119
|
+
auditAgent,
|
|
120
|
+
}, "blueprint.delegate");
|
|
121
|
+
const scopeID = ctx.scopeId;
|
|
122
|
+
const execAgentInfo = (await Agent.get(executionAgent));
|
|
123
|
+
const model = r.executionModel ?? (await Agent.getAvailableModel(execAgentInfo)) ?? (await Provider.defaultModel());
|
|
124
|
+
const pluginOwner = {
|
|
125
|
+
pluginId: ctx.pluginId,
|
|
126
|
+
pluginGeneration: ctx.pluginGeneration,
|
|
127
|
+
scopeId: scopeID,
|
|
128
|
+
correlationId,
|
|
129
|
+
};
|
|
130
|
+
// --- state for rollback ---
|
|
131
|
+
let noteID;
|
|
132
|
+
let sessionID;
|
|
133
|
+
let taskID;
|
|
134
|
+
let loopID;
|
|
135
|
+
const rollback = async () => {
|
|
136
|
+
if (taskID) {
|
|
137
|
+
await Cortex.cancel(taskID).catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
if (loopID) {
|
|
140
|
+
await BlueprintLoopStore.updateStatus(scopeID, loopID, { status: "cancelled" }).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
if (sessionID) {
|
|
143
|
+
await Session.update(sessionID, (draft) => {
|
|
144
|
+
draft.time.archived = Date.now();
|
|
145
|
+
}).catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
if (noteID) {
|
|
148
|
+
await NoteStore.update(scopeID, noteID, { archived: true }).catch(() => { });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
// Step 1: Create a project-scoped Blueprint Note from the verified source Markdown
|
|
153
|
+
const note = await NoteStore.create({
|
|
154
|
+
title,
|
|
155
|
+
content: NoteMarkdown.fromMarkdown(markdown),
|
|
156
|
+
kind: "blueprint",
|
|
157
|
+
blueprint: { description },
|
|
158
|
+
});
|
|
159
|
+
noteID = note.id;
|
|
160
|
+
// Step 2: Create a dedicated hidden execution Session WITHOUT starting execution
|
|
161
|
+
const scope = ScopeContext.current.scope;
|
|
162
|
+
const parent = await Session.get(ctx.parentSessionID).catch(() => undefined);
|
|
163
|
+
const workspace = parent ? parent.workspace : ScopeContext.defaultWorkspace(scope);
|
|
164
|
+
const execSession = await Session.create({
|
|
165
|
+
scope,
|
|
166
|
+
parentID: ctx.parentSessionID,
|
|
167
|
+
title: `[Blueprint] ${title.slice(0, 80)}`,
|
|
168
|
+
cortex: {
|
|
169
|
+
taskID: "",
|
|
170
|
+
parentSessionID: ctx.parentSessionID,
|
|
171
|
+
parentMessageID: ctx.parentMessageID,
|
|
172
|
+
description: `[Blueprint] ${title.slice(0, 80)}`,
|
|
173
|
+
agent: executionAgent,
|
|
174
|
+
model,
|
|
175
|
+
executionRole: "delegated_subagent",
|
|
176
|
+
status: "queued",
|
|
177
|
+
startedAt: Date.now(),
|
|
178
|
+
visibility: "hidden",
|
|
179
|
+
owner: pluginOwner,
|
|
180
|
+
timeoutMs: r.budget.maxRuntimeMs,
|
|
181
|
+
},
|
|
182
|
+
workspace,
|
|
183
|
+
completionNotice: { silent: true },
|
|
184
|
+
permission: [{ permission: "question", pattern: "*", action: "deny" }],
|
|
185
|
+
});
|
|
186
|
+
sessionID = execSession.id;
|
|
187
|
+
// Step 3: Create BlueprintLoopStore record (armed)
|
|
188
|
+
const loop = await BlueprintLoopStore.create({
|
|
189
|
+
noteID,
|
|
190
|
+
noteVersion: note.version,
|
|
191
|
+
title,
|
|
192
|
+
description,
|
|
193
|
+
sessionID,
|
|
194
|
+
executionAgent,
|
|
195
|
+
auditAgent,
|
|
196
|
+
runMode: "current",
|
|
197
|
+
source: "plugin",
|
|
198
|
+
sourceDigest: r.sourceDigest,
|
|
199
|
+
budget: r.budget,
|
|
200
|
+
pluginOwner,
|
|
201
|
+
model: r.executionModel,
|
|
202
|
+
executionTools: r.executionTools,
|
|
203
|
+
auditTools: r.auditTools,
|
|
204
|
+
});
|
|
205
|
+
loopID = loop.id;
|
|
206
|
+
// Step 4: Bind session to loop and mark running
|
|
207
|
+
await BlueprintLoopService.bindSessionToLoop(sessionID, loop.id, "execution");
|
|
208
|
+
const runningLoop = await BlueprintLoopStore.updateStatus(scopeID, loop.id, {
|
|
209
|
+
status: "running",
|
|
210
|
+
});
|
|
211
|
+
// Step 5: Launch Cortex with the pre-created session, delivering exactly ONE formatted prompt.
|
|
212
|
+
// Blueprint content stays in the Note; the Cortex prompt is the execution instruction.
|
|
213
|
+
const ft = firstPromptText(title, noteID, loop.id);
|
|
214
|
+
const task = await Cortex.launch({
|
|
215
|
+
description: `[Blueprint] ${title.slice(0, 80)}`,
|
|
216
|
+
prompt: ft,
|
|
217
|
+
agent: executionAgent,
|
|
218
|
+
executionRole: "delegated_subagent",
|
|
219
|
+
category: "general",
|
|
220
|
+
parentSessionID: ctx.parentSessionID,
|
|
221
|
+
parentMessageID: ctx.parentMessageID,
|
|
222
|
+
sessionID, // reuse pre-created session
|
|
223
|
+
model,
|
|
224
|
+
visibility: "hidden",
|
|
225
|
+
notifyParentOnComplete: false,
|
|
226
|
+
timeoutMs: r.budget.maxRuntimeMs,
|
|
227
|
+
tools: r.executionTools,
|
|
228
|
+
owner: pluginOwner,
|
|
229
|
+
});
|
|
230
|
+
taskID = task.id;
|
|
231
|
+
// Schedule timer for maxRuntimeMs
|
|
232
|
+
BlueprintLoopRuntime.scheduleDeadline(scopeID, loop.id, r.budget.maxRuntimeMs);
|
|
233
|
+
return publicLoop(runningLoop);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
await rollback();
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export async function getBlueprint(input) {
|
|
241
|
+
const loop = await BlueprintLoopStore.get(input.scopeId, input.loopID).catch(() => {
|
|
242
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`);
|
|
243
|
+
});
|
|
244
|
+
if (loop.source !== "plugin" || !loop.pluginOwner) {
|
|
245
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_PLUGIN_OWNED, "BlueprintLoop is not plugin-owned");
|
|
246
|
+
}
|
|
247
|
+
if (loop.pluginOwner.pluginId !== input.pluginId ||
|
|
248
|
+
loop.pluginOwner.pluginGeneration !== input.pluginGeneration ||
|
|
249
|
+
loop.pluginOwner.scopeId !== input.scopeId) {
|
|
250
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`);
|
|
251
|
+
}
|
|
252
|
+
return publicLoop(loop);
|
|
253
|
+
}
|
|
254
|
+
export async function cancelBlueprint(input) {
|
|
255
|
+
const loop = await BlueprintLoopStore.get(input.scopeId, input.loopID).catch(() => {
|
|
256
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`);
|
|
257
|
+
});
|
|
258
|
+
if (loop.source !== "plugin" || !loop.pluginOwner) {
|
|
259
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_PLUGIN_OWNED, "BlueprintLoop is not plugin-owned");
|
|
260
|
+
}
|
|
261
|
+
if (loop.pluginOwner.pluginId !== input.pluginId ||
|
|
262
|
+
loop.pluginOwner.pluginGeneration !== input.pluginGeneration ||
|
|
263
|
+
loop.pluginOwner.scopeId !== input.scopeId) {
|
|
264
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`);
|
|
265
|
+
}
|
|
266
|
+
// Cancel execution and audit tasks/sessions
|
|
267
|
+
if (loop.sessionID) {
|
|
268
|
+
const execSession = await Session.get(loop.sessionID).catch(() => undefined);
|
|
269
|
+
if (execSession?.cortex) {
|
|
270
|
+
await Cortex.cancel(execSession.cortex.taskID).catch(() => { });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (loop.auditSessionID) {
|
|
274
|
+
const auditSession = await Session.get(loop.auditSessionID).catch(() => undefined);
|
|
275
|
+
if (auditSession?.cortex) {
|
|
276
|
+
await Cortex.cancel(auditSession.cortex.taskID).catch(() => { });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Transition to cancelled (fires blueprint.after hook, archives resources)
|
|
280
|
+
const updated = await BlueprintLoopStore.updateStatus(input.scopeId, input.loopID, {
|
|
281
|
+
status: "cancelled",
|
|
282
|
+
});
|
|
283
|
+
BlueprintLoopRuntime.cancelDeadline(input.scopeId, input.loopID);
|
|
284
|
+
return publicLoop(updated);
|
|
285
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { componentFeatures } from "./features.js";
|
|
2
|
+
import { version } from "../package.json" with { type: "json" };
|
|
3
|
+
import { registerBridge } from "./register.js";
|
|
4
|
+
import { BlueprintLoopSources } from "@ericsanchezok/synergy-blueprint/blueprint/source";
|
|
5
|
+
export function pluginBlueprint() {
|
|
6
|
+
return {
|
|
7
|
+
id: "plugin-blueprint",
|
|
8
|
+
features: componentFeatures,
|
|
9
|
+
apiVersion: 1,
|
|
10
|
+
version,
|
|
11
|
+
requires: { "plugin-host": version, blueprint: version },
|
|
12
|
+
workers: { agent: new URL("./worker.js", import.meta.url) },
|
|
13
|
+
register() {
|
|
14
|
+
BlueprintLoopSources.register("plugin");
|
|
15
|
+
registerBridge();
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const componentFeatures = {
|
|
2
|
+
kind: "bridge",
|
|
3
|
+
definitions: [
|
|
4
|
+
{
|
|
5
|
+
id: "plugin-blueprint",
|
|
6
|
+
owner: "plugin-blueprint",
|
|
7
|
+
maturity: "stable",
|
|
8
|
+
activation: "restart",
|
|
9
|
+
name: "Plugin Blueprint",
|
|
10
|
+
description: "Provide plugin blueprint capabilities.",
|
|
11
|
+
component: "plugin-blueprint",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Plugin } from "@ericsanchezok/synergy-plugin-host/plugin";
|
|
2
|
+
import { registerPluginBlueprintAdapter } from "@ericsanchezok/synergy-plugin-host/plugin/host-services";
|
|
3
|
+
import { setTerminalHookDeliverer } from "@ericsanchezok/synergy-blueprint/blueprint/terminal-hook";
|
|
4
|
+
import { startBlueprint, getBlueprint, cancelBlueprint } from "./adapter.js";
|
|
5
|
+
export function registerBridge() {
|
|
6
|
+
setTerminalHookDeliverer((...args) => Plugin.deliverHookForPlugin(...args));
|
|
7
|
+
registerPluginBlueprintAdapter({ start: startBlueprint, get: getBlueprint, cancel: cancelBlueprint });
|
|
8
|
+
}
|
package/dist/worker.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ericsanchezok/synergy-plugin-blueprint",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "3.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./adapter": {
|
|
8
|
+
"types": "./src/adapter.ts",
|
|
9
|
+
"bun": "./dist/adapter.js",
|
|
10
|
+
"import": "./dist/adapter.js"
|
|
11
|
+
},
|
|
12
|
+
"./component": {
|
|
13
|
+
"types": "./src/component.ts",
|
|
14
|
+
"bun": "./dist/component.js",
|
|
15
|
+
"import": "./dist/component.js"
|
|
16
|
+
},
|
|
17
|
+
"./register": {
|
|
18
|
+
"types": "./src/register.ts",
|
|
19
|
+
"bun": "./dist/register.js",
|
|
20
|
+
"import": "./dist/register.js"
|
|
21
|
+
},
|
|
22
|
+
"./worker": {
|
|
23
|
+
"types": "./src/worker.ts",
|
|
24
|
+
"bun": "./dist/worker.js",
|
|
25
|
+
"import": "./dist/worker.js"
|
|
26
|
+
},
|
|
27
|
+
"./features": {
|
|
28
|
+
"types": "./src/features.ts",
|
|
29
|
+
"bun": "./dist/features.js",
|
|
30
|
+
"import": "./dist/features.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@ericsanchezok/synergy-blueprint": "3.1.1",
|
|
35
|
+
"@ericsanchezok/synergy-harness": "3.1.1",
|
|
36
|
+
"@ericsanchezok/synergy-note": "3.1.1",
|
|
37
|
+
"@ericsanchezok/synergy-plugin": "3.1.1",
|
|
38
|
+
"@ericsanchezok/synergy-plugin-host": "3.1.1",
|
|
39
|
+
"@ericsanchezok/synergy-util": "3.1.1"
|
|
40
|
+
},
|
|
41
|
+
"synergy": {
|
|
42
|
+
"formatVersion": 1,
|
|
43
|
+
"kind": "component",
|
|
44
|
+
"id": "plugin-blueprint",
|
|
45
|
+
"version": "3.1.1",
|
|
46
|
+
"compatibility": {
|
|
47
|
+
"synergy": "3.1.1"
|
|
48
|
+
},
|
|
49
|
+
"apiVersion": 1,
|
|
50
|
+
"entry": "./dist/component.js",
|
|
51
|
+
"export": "pluginBlueprint",
|
|
52
|
+
"requires": {
|
|
53
|
+
"plugin-host": "3.1.1",
|
|
54
|
+
"blueprint": "3.1.1"
|
|
55
|
+
},
|
|
56
|
+
"packages": {
|
|
57
|
+
"@ericsanchezok/synergy-blueprint": "3.1.1"
|
|
58
|
+
},
|
|
59
|
+
"features": {
|
|
60
|
+
"kind": "bridge",
|
|
61
|
+
"definitions": [
|
|
62
|
+
{
|
|
63
|
+
"id": "plugin-blueprint",
|
|
64
|
+
"owner": "plugin-blueprint",
|
|
65
|
+
"maturity": "stable",
|
|
66
|
+
"activation": "restart",
|
|
67
|
+
"name": "Plugin Blueprint",
|
|
68
|
+
"description": "Provide plugin blueprint capabilities.",
|
|
69
|
+
"component": "plugin-blueprint"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"files": [
|
|
75
|
+
"src",
|
|
76
|
+
"dist",
|
|
77
|
+
"README.md",
|
|
78
|
+
"AGENTS.md"
|
|
79
|
+
],
|
|
80
|
+
"engines": {
|
|
81
|
+
"bun": ">=1.4.3"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { BlueprintLoopInfo, BlueprintStartInput } from "@ericsanchezok/synergy-plugin"
|
|
2
|
+
import { hash as sha256 } from "@ericsanchezok/synergy-util/encode"
|
|
3
|
+
import { Agent } from "@ericsanchezok/synergy-harness/agent/agent"
|
|
4
|
+
import { Cortex } from "@ericsanchezok/synergy-harness/cortex"
|
|
5
|
+
import { Provider } from "@ericsanchezok/synergy-harness/provider/provider"
|
|
6
|
+
import { NoteMarkdown, NoteStore } from "@ericsanchezok/synergy-note"
|
|
7
|
+
import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context"
|
|
8
|
+
import { Session } from "@ericsanchezok/synergy-harness/session"
|
|
9
|
+
import { SessionManager } from "@ericsanchezok/synergy-harness/session/manager"
|
|
10
|
+
import { Identifier } from "@ericsanchezok/synergy-harness/id/id"
|
|
11
|
+
import { readPluginManifest } from "@ericsanchezok/synergy-plugin-host/plugin/spec-resolver"
|
|
12
|
+
import { BlueprintLoopStore } from "@ericsanchezok/synergy-blueprint/blueprint/loop-store"
|
|
13
|
+
import { BlueprintLoopService } from "@ericsanchezok/synergy-blueprint/blueprint/service"
|
|
14
|
+
import { BlueprintLoopRuntime } from "@ericsanchezok/synergy-blueprint/blueprint/loop-runtime"
|
|
15
|
+
import type { Info } from "@ericsanchezok/synergy-blueprint/blueprint/types"
|
|
16
|
+
import { Log } from "@ericsanchezok/synergy-harness/util/log"
|
|
17
|
+
|
|
18
|
+
const log = Log.create({ service: "blueprint.plugin-adapter" })
|
|
19
|
+
|
|
20
|
+
export const BlueprintPluginErrorCode = {
|
|
21
|
+
DIGEST_MISMATCH: "BLUEPRINT_DIGEST_MISMATCH",
|
|
22
|
+
NOT_FOUND: "BLUEPRINT_NOT_FOUND",
|
|
23
|
+
NOT_PLUGIN_OWNED: "BLUEPRINT_NOT_PLUGIN_OWNED",
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
function pluginAdapterError(code: string, message: string) {
|
|
27
|
+
return Object.assign(new Error(message), { name: "BlueprintPluginError", code })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function publicLoop(loop: Info): BlueprintLoopInfo {
|
|
31
|
+
return loop
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function assertBlueprintDelegation(
|
|
35
|
+
input: {
|
|
36
|
+
pluginId: string
|
|
37
|
+
pluginGeneration: string
|
|
38
|
+
pluginDir?: string
|
|
39
|
+
executionAgent: string
|
|
40
|
+
auditAgent: string
|
|
41
|
+
},
|
|
42
|
+
manifestCapability: string,
|
|
43
|
+
) {
|
|
44
|
+
const execAgent = await Agent.get(input.executionAgent)
|
|
45
|
+
if (!execAgent) throw new Error(`Unknown execution agent: ${input.executionAgent}`)
|
|
46
|
+
if (execAgent.mode === "primary") throw new Error(`Execution agent "${input.executionAgent}" is a primary agent`)
|
|
47
|
+
if (!execAgent.hidden) throw new Error(`Execution agent "${input.executionAgent}" is not hidden`)
|
|
48
|
+
|
|
49
|
+
const auditAgentInfo = await Agent.get(input.auditAgent)
|
|
50
|
+
if (!auditAgentInfo) throw new Error(`Unknown audit agent: ${input.auditAgent}`)
|
|
51
|
+
if (auditAgentInfo.mode === "primary") throw new Error(`Audit agent "${input.auditAgent}" is a primary agent`)
|
|
52
|
+
if (!auditAgentInfo.hidden) throw new Error(`Audit agent "${input.auditAgent}" is not hidden`)
|
|
53
|
+
|
|
54
|
+
if (input.executionAgent === input.auditAgent) throw new Error("Execution and audit agents must differ")
|
|
55
|
+
|
|
56
|
+
if (input.pluginDir) {
|
|
57
|
+
const manifest = await readPluginManifest(input.pluginDir)
|
|
58
|
+
const cap = manifest.capabilities.find((c) => c.id === manifestCapability)
|
|
59
|
+
if (!cap) throw new Error(`Plugin manifest does not declare capability ${manifestCapability}`)
|
|
60
|
+
|
|
61
|
+
const agents = Array.isArray(cap.constraints?.agents) ? cap.constraints.agents : undefined
|
|
62
|
+
if (agents) {
|
|
63
|
+
if (!agents.includes(input.executionAgent))
|
|
64
|
+
throw new Error(`Plugin manifest does not allow Blueprint execution agent "${input.executionAgent}"`)
|
|
65
|
+
if (!agents.includes(input.auditAgent))
|
|
66
|
+
throw new Error(`Plugin manifest does not allow Blueprint audit agent "${input.auditAgent}"`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const execOwner = Agent.pluginOwner(execAgent)
|
|
71
|
+
if (!execOwner || execOwner.pluginId !== input.pluginId || execOwner.pluginGeneration !== input.pluginGeneration) {
|
|
72
|
+
throw new Error(`Execution agent "${input.executionAgent}" is not owned by this plugin generation`)
|
|
73
|
+
}
|
|
74
|
+
const auditOwner = Agent.pluginOwner(auditAgentInfo)
|
|
75
|
+
if (!auditOwner || auditOwner.pluginId !== input.pluginId || auditOwner.pluginGeneration !== input.pluginGeneration) {
|
|
76
|
+
throw new Error(`Audit agent "${input.auditAgent}" is not owned by this plugin generation`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BlueprintStartContext {
|
|
81
|
+
pluginId: string
|
|
82
|
+
pluginGeneration: string
|
|
83
|
+
scopeId: string
|
|
84
|
+
pluginDir?: string
|
|
85
|
+
/** Parent session + message for child Cortex task delegation */
|
|
86
|
+
parentSessionID: string
|
|
87
|
+
parentMessageID: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function firstPromptText(title: string, noteID: string, loopID: string): string {
|
|
91
|
+
return `Execute the Blueprint "${title}" (note ID: ${noteID}, loop ID: ${loopID}).
|
|
92
|
+
First call note_read with noteIds=["${noteID}"] and read the full Blueprint content.
|
|
93
|
+
Treat the Blueprint as the authoritative engineering contract for this run: requirements, non-goals, deliverables, constraints, quality criteria, and acceptance criteria.
|
|
94
|
+
Continue until every Blueprint requirement is implemented, verified, and integrated.
|
|
95
|
+
When the Blueprint is complete and verified, call blueprint_loop_stop with a concise summary, completed requirements, concrete evidence, and any known limitations to request independent review.`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Protocol 5 atomic Blueprint start.
|
|
100
|
+
*
|
|
101
|
+
* Validates all inputs, hashes markdown, creates a Blueprint Note, a dedicated
|
|
102
|
+
* hidden execution Session via Session.create (no execution yet), a Store record,
|
|
103
|
+
* binds, marks running, and THEN launches Cortex with the pre-created sessionID
|
|
104
|
+
* delivering exactly ONE formatted execution prompt. The source content stays in
|
|
105
|
+
* the frozen Note only.
|
|
106
|
+
*
|
|
107
|
+
* On failure at any stage, all previously created resources are rolled back
|
|
108
|
+
* (archived/cancelled).
|
|
109
|
+
*/
|
|
110
|
+
export async function startBlueprint(input: {
|
|
111
|
+
context: BlueprintStartContext
|
|
112
|
+
request: BlueprintStartInput
|
|
113
|
+
}): Promise<BlueprintLoopInfo> {
|
|
114
|
+
const ctx = input.context
|
|
115
|
+
const r = input.request
|
|
116
|
+
|
|
117
|
+
// --- validation ---
|
|
118
|
+
if (!r.title?.trim()) throw new Error("blueprint.start requires title")
|
|
119
|
+
if (!r.markdown?.trim()) throw new Error("blueprint.start requires markdown")
|
|
120
|
+
if (!r.correlationId?.trim()) throw new Error("blueprint.start requires correlationId")
|
|
121
|
+
if (!r.executionAgent?.trim()) throw new Error("blueprint.start requires executionAgent")
|
|
122
|
+
if (!r.auditAgent?.trim()) throw new Error("blueprint.start requires auditAgent")
|
|
123
|
+
if (!r.budget) throw new Error("blueprint.start requires budget")
|
|
124
|
+
if (!Number.isFinite(r.budget.maxRuntimeMs) || r.budget.maxRuntimeMs <= 0)
|
|
125
|
+
throw new Error("budget.maxRuntimeMs must be a positive integer")
|
|
126
|
+
if (!Number.isFinite(r.budget.maxIterations) || r.budget.maxIterations <= 0)
|
|
127
|
+
throw new Error("budget.maxIterations must be a positive integer")
|
|
128
|
+
|
|
129
|
+
const title = r.title.trim()
|
|
130
|
+
const markdown = r.markdown.trim()
|
|
131
|
+
const correlationId = r.correlationId.trim()
|
|
132
|
+
const executionAgent = r.executionAgent.trim()
|
|
133
|
+
const auditAgent = r.auditAgent.trim()
|
|
134
|
+
const description = r.description?.trim()
|
|
135
|
+
|
|
136
|
+
// Digest validation
|
|
137
|
+
const computedDigest = await sha256(markdown)
|
|
138
|
+
if (computedDigest !== r.sourceDigest) {
|
|
139
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.DIGEST_MISMATCH, "sourceDigest does not match markdown content")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await assertBlueprintDelegation(
|
|
143
|
+
{
|
|
144
|
+
pluginId: ctx.pluginId,
|
|
145
|
+
pluginGeneration: ctx.pluginGeneration,
|
|
146
|
+
pluginDir: ctx.pluginDir,
|
|
147
|
+
executionAgent,
|
|
148
|
+
auditAgent,
|
|
149
|
+
},
|
|
150
|
+
"blueprint.delegate",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const scopeID = ctx.scopeId
|
|
154
|
+
const execAgentInfo = (await Agent.get(executionAgent))!
|
|
155
|
+
const model = r.executionModel ?? (await Agent.getAvailableModel(execAgentInfo)) ?? (await Provider.defaultModel())
|
|
156
|
+
const pluginOwner = {
|
|
157
|
+
pluginId: ctx.pluginId,
|
|
158
|
+
pluginGeneration: ctx.pluginGeneration,
|
|
159
|
+
scopeId: scopeID,
|
|
160
|
+
correlationId,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- state for rollback ---
|
|
164
|
+
let noteID: string | undefined
|
|
165
|
+
let sessionID: string | undefined
|
|
166
|
+
let taskID: string | undefined
|
|
167
|
+
let loopID: string | undefined
|
|
168
|
+
|
|
169
|
+
const rollback = async () => {
|
|
170
|
+
if (taskID) {
|
|
171
|
+
await Cortex.cancel(taskID).catch(() => {})
|
|
172
|
+
}
|
|
173
|
+
if (loopID) {
|
|
174
|
+
await BlueprintLoopStore.updateStatus(scopeID, loopID, { status: "cancelled" }).catch(() => {})
|
|
175
|
+
}
|
|
176
|
+
if (sessionID) {
|
|
177
|
+
await Session.update(sessionID, (draft) => {
|
|
178
|
+
draft.time.archived = Date.now()
|
|
179
|
+
}).catch(() => {})
|
|
180
|
+
}
|
|
181
|
+
if (noteID) {
|
|
182
|
+
await NoteStore.update(scopeID, noteID, { archived: true }).catch(() => {})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Step 1: Create a project-scoped Blueprint Note from the verified source Markdown
|
|
188
|
+
const note = await NoteStore.create({
|
|
189
|
+
title,
|
|
190
|
+
content: NoteMarkdown.fromMarkdown(markdown),
|
|
191
|
+
kind: "blueprint",
|
|
192
|
+
blueprint: { description },
|
|
193
|
+
})
|
|
194
|
+
noteID = note.id
|
|
195
|
+
|
|
196
|
+
// Step 2: Create a dedicated hidden execution Session WITHOUT starting execution
|
|
197
|
+
const scope = ScopeContext.current.scope
|
|
198
|
+
const parent = await Session.get(ctx.parentSessionID).catch(() => undefined)
|
|
199
|
+
const workspace = parent ? parent.workspace : ScopeContext.defaultWorkspace(scope)
|
|
200
|
+
const execSession = await Session.create({
|
|
201
|
+
scope,
|
|
202
|
+
parentID: ctx.parentSessionID,
|
|
203
|
+
title: `[Blueprint] ${title.slice(0, 80)}`,
|
|
204
|
+
cortex: {
|
|
205
|
+
taskID: "",
|
|
206
|
+
parentSessionID: ctx.parentSessionID,
|
|
207
|
+
parentMessageID: ctx.parentMessageID,
|
|
208
|
+
description: `[Blueprint] ${title.slice(0, 80)}`,
|
|
209
|
+
agent: executionAgent,
|
|
210
|
+
model,
|
|
211
|
+
executionRole: "delegated_subagent",
|
|
212
|
+
status: "queued",
|
|
213
|
+
startedAt: Date.now(),
|
|
214
|
+
visibility: "hidden",
|
|
215
|
+
owner: pluginOwner,
|
|
216
|
+
timeoutMs: r.budget.maxRuntimeMs,
|
|
217
|
+
},
|
|
218
|
+
workspace,
|
|
219
|
+
completionNotice: { silent: true },
|
|
220
|
+
permission: [{ permission: "question", pattern: "*", action: "deny" } as any],
|
|
221
|
+
})
|
|
222
|
+
sessionID = execSession.id
|
|
223
|
+
|
|
224
|
+
// Step 3: Create BlueprintLoopStore record (armed)
|
|
225
|
+
const loop = await BlueprintLoopStore.create({
|
|
226
|
+
noteID,
|
|
227
|
+
noteVersion: note.version,
|
|
228
|
+
title,
|
|
229
|
+
description,
|
|
230
|
+
sessionID,
|
|
231
|
+
executionAgent,
|
|
232
|
+
auditAgent,
|
|
233
|
+
runMode: "current",
|
|
234
|
+
source: "plugin",
|
|
235
|
+
sourceDigest: r.sourceDigest,
|
|
236
|
+
budget: r.budget,
|
|
237
|
+
pluginOwner,
|
|
238
|
+
model: r.executionModel,
|
|
239
|
+
executionTools: r.executionTools,
|
|
240
|
+
auditTools: r.auditTools,
|
|
241
|
+
})
|
|
242
|
+
loopID = loop.id
|
|
243
|
+
|
|
244
|
+
// Step 4: Bind session to loop and mark running
|
|
245
|
+
await BlueprintLoopService.bindSessionToLoop(sessionID, loop.id, "execution")
|
|
246
|
+
const runningLoop = await BlueprintLoopStore.updateStatus(scopeID, loop.id, {
|
|
247
|
+
status: "running",
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Step 5: Launch Cortex with the pre-created session, delivering exactly ONE formatted prompt.
|
|
251
|
+
// Blueprint content stays in the Note; the Cortex prompt is the execution instruction.
|
|
252
|
+
const ft = firstPromptText(title, noteID, loop.id)
|
|
253
|
+
const task = await Cortex.launch({
|
|
254
|
+
description: `[Blueprint] ${title.slice(0, 80)}`,
|
|
255
|
+
prompt: ft,
|
|
256
|
+
agent: executionAgent,
|
|
257
|
+
executionRole: "delegated_subagent",
|
|
258
|
+
category: "general",
|
|
259
|
+
parentSessionID: ctx.parentSessionID,
|
|
260
|
+
parentMessageID: ctx.parentMessageID,
|
|
261
|
+
sessionID, // reuse pre-created session
|
|
262
|
+
model,
|
|
263
|
+
visibility: "hidden",
|
|
264
|
+
notifyParentOnComplete: false,
|
|
265
|
+
timeoutMs: r.budget.maxRuntimeMs,
|
|
266
|
+
tools: r.executionTools,
|
|
267
|
+
owner: pluginOwner,
|
|
268
|
+
})
|
|
269
|
+
taskID = task.id
|
|
270
|
+
|
|
271
|
+
// Schedule timer for maxRuntimeMs
|
|
272
|
+
BlueprintLoopRuntime.scheduleDeadline(scopeID, loop.id, r.budget.maxRuntimeMs)
|
|
273
|
+
|
|
274
|
+
return publicLoop(runningLoop)
|
|
275
|
+
} catch (err) {
|
|
276
|
+
await rollback()
|
|
277
|
+
throw err
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function getBlueprint(input: {
|
|
282
|
+
scopeId: string
|
|
283
|
+
loopID: string
|
|
284
|
+
pluginId: string
|
|
285
|
+
pluginGeneration: string
|
|
286
|
+
}): Promise<BlueprintLoopInfo> {
|
|
287
|
+
const loop = await BlueprintLoopStore.get(input.scopeId, input.loopID).catch(() => {
|
|
288
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`)
|
|
289
|
+
})
|
|
290
|
+
if (loop.source !== "plugin" || !loop.pluginOwner) {
|
|
291
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_PLUGIN_OWNED, "BlueprintLoop is not plugin-owned")
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
loop.pluginOwner.pluginId !== input.pluginId ||
|
|
295
|
+
loop.pluginOwner.pluginGeneration !== input.pluginGeneration ||
|
|
296
|
+
loop.pluginOwner.scopeId !== input.scopeId
|
|
297
|
+
) {
|
|
298
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`)
|
|
299
|
+
}
|
|
300
|
+
return publicLoop(loop)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function cancelBlueprint(input: {
|
|
304
|
+
scopeId: string
|
|
305
|
+
loopID: string
|
|
306
|
+
pluginId: string
|
|
307
|
+
pluginGeneration: string
|
|
308
|
+
}): Promise<BlueprintLoopInfo> {
|
|
309
|
+
const loop = await BlueprintLoopStore.get(input.scopeId, input.loopID).catch(() => {
|
|
310
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`)
|
|
311
|
+
})
|
|
312
|
+
if (loop.source !== "plugin" || !loop.pluginOwner) {
|
|
313
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_PLUGIN_OWNED, "BlueprintLoop is not plugin-owned")
|
|
314
|
+
}
|
|
315
|
+
if (
|
|
316
|
+
loop.pluginOwner.pluginId !== input.pluginId ||
|
|
317
|
+
loop.pluginOwner.pluginGeneration !== input.pluginGeneration ||
|
|
318
|
+
loop.pluginOwner.scopeId !== input.scopeId
|
|
319
|
+
) {
|
|
320
|
+
throw pluginAdapterError(BlueprintPluginErrorCode.NOT_FOUND, `BlueprintLoop not found: ${input.loopID}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Cancel execution and audit tasks/sessions
|
|
324
|
+
if (loop.sessionID) {
|
|
325
|
+
const execSession = await Session.get(loop.sessionID).catch(() => undefined)
|
|
326
|
+
if (execSession?.cortex) {
|
|
327
|
+
await Cortex.cancel(execSession.cortex.taskID).catch(() => {})
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (loop.auditSessionID) {
|
|
331
|
+
const auditSession = await Session.get(loop.auditSessionID).catch(() => undefined)
|
|
332
|
+
if (auditSession?.cortex) {
|
|
333
|
+
await Cortex.cancel(auditSession.cortex.taskID).catch(() => {})
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Transition to cancelled (fires blueprint.after hook, archives resources)
|
|
338
|
+
const updated = await BlueprintLoopStore.updateStatus(input.scopeId, input.loopID, {
|
|
339
|
+
status: "cancelled",
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
BlueprintLoopRuntime.cancelDeadline(input.scopeId, input.loopID)
|
|
343
|
+
|
|
344
|
+
return publicLoop(updated)
|
|
345
|
+
}
|
package/src/component.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { componentFeatures } from "./features"
|
|
2
|
+
import type { RuntimeComponent } from "@ericsanchezok/synergy-harness/lifecycle"
|
|
3
|
+
import { version } from "../package.json" with { type: "json" }
|
|
4
|
+
import { registerBridge } from "./register"
|
|
5
|
+
import { BlueprintLoopSources } from "@ericsanchezok/synergy-blueprint/blueprint/source"
|
|
6
|
+
export function pluginBlueprint(): RuntimeComponent {
|
|
7
|
+
return {
|
|
8
|
+
id: "plugin-blueprint",
|
|
9
|
+
features: componentFeatures,
|
|
10
|
+
apiVersion: 1,
|
|
11
|
+
version,
|
|
12
|
+
requires: { "plugin-host": version, blueprint: version },
|
|
13
|
+
workers: { agent: new URL("./worker.ts", import.meta.url) },
|
|
14
|
+
register() {
|
|
15
|
+
BlueprintLoopSources.register("plugin")
|
|
16
|
+
registerBridge()
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/features.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const componentFeatures = {
|
|
2
|
+
kind: "bridge",
|
|
3
|
+
definitions: [
|
|
4
|
+
{
|
|
5
|
+
id: "plugin-blueprint",
|
|
6
|
+
owner: "plugin-blueprint",
|
|
7
|
+
maturity: "stable",
|
|
8
|
+
activation: "restart",
|
|
9
|
+
name: "Plugin Blueprint",
|
|
10
|
+
description: "Provide plugin blueprint capabilities.",
|
|
11
|
+
component: "plugin-blueprint",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
} as const
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Plugin } from "@ericsanchezok/synergy-plugin-host/plugin"
|
|
2
|
+
import { registerPluginBlueprintAdapter } from "@ericsanchezok/synergy-plugin-host/plugin/host-services"
|
|
3
|
+
import { setTerminalHookDeliverer } from "@ericsanchezok/synergy-blueprint/blueprint/terminal-hook"
|
|
4
|
+
import { startBlueprint, getBlueprint, cancelBlueprint } from "./adapter"
|
|
5
|
+
export function registerBridge() {
|
|
6
|
+
setTerminalHookDeliverer((...args) => Plugin.deliverHookForPlugin(...args))
|
|
7
|
+
registerPluginBlueprintAdapter({ start: startBlueprint, get: getBlueprint, cancel: cancelBlueprint })
|
|
8
|
+
}
|
package/src/worker.ts
ADDED
package/README.md
DELETED