@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 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.
@@ -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
+ }
@@ -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
+ };
@@ -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
@@ -0,0 +1,6 @@
1
+ import { version } from "../package.json" with { type: "json" };
2
+ import { pluginBlueprint } from "./component.js";
3
+ export const metadata = { id: "plugin-blueprint", version, apiVersion: 1 };
4
+ export function registerWorker() {
5
+ pluginBlueprint().register();
6
+ }
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@ericsanchezok/synergy-plugin-blueprint",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
+ }
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ import { version } from "../package.json" with { type: "json" }
2
+ import { pluginBlueprint } from "./component"
3
+ export const metadata = { id: "plugin-blueprint", version, apiVersion: 1 }
4
+ export function registerWorker() {
5
+ pluginBlueprint().register()
6
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.