@tangle-network/agent-profile-materialize 0.11.0 → 0.12.0
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.d.ts +133 -16
- package/dist/index.js +368 -135
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,25 @@ export interface PlanFile {
|
|
|
57
57
|
* this on every file it emits.
|
|
58
58
|
*/
|
|
59
59
|
source?: "user" | "generated";
|
|
60
|
+
/**
|
|
61
|
+
* Who owns the PATH — which decides whether an existing file there may be
|
|
62
|
+
* replaced.
|
|
63
|
+
*
|
|
64
|
+
* `"materializer"` = a path this package invented. No harness convention
|
|
65
|
+
* names it and no user authors it, so a file already there can only be this
|
|
66
|
+
* materializer's own output from an earlier application into the same
|
|
67
|
+
* workspace, and replacing it is how turn two of a resumed session gets the
|
|
68
|
+
* current profile instead of the previous one.
|
|
69
|
+
*
|
|
70
|
+
* `"shared"` (the default, and every other file) = a path the user or the
|
|
71
|
+
* harness also owns — `CLAUDE.md`, `AGENTS.md`, `opencode.json`,
|
|
72
|
+
* `.gemini/system.md` are all documented, user-authorable names. An existing
|
|
73
|
+
* file there may be the caller's own work, so it is never replaced.
|
|
74
|
+
*
|
|
75
|
+
* Optional because hand-constructed plans have no path provenance to report;
|
|
76
|
+
* absent reads as `"shared"`, the safe answer.
|
|
77
|
+
*/
|
|
78
|
+
ownership?: "materializer" | "shared";
|
|
60
79
|
/**
|
|
61
80
|
* Secret leaves in a generated JSON document. `content` contains `null` at
|
|
62
81
|
* each path until the private executor resolves the named reference. The
|
|
@@ -83,9 +102,47 @@ export type WorkspacePlanArgument = string | WorkspacePlanArgumentTemplate;
|
|
|
83
102
|
/** Public bytes stay strings; secret references stay opaque until application. */
|
|
84
103
|
export type WorkspacePlanConfigValue = string | AgentProfileSecretRef;
|
|
85
104
|
export interface Unsupported {
|
|
86
|
-
dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
|
|
105
|
+
dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "appendSystemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
|
|
87
106
|
reason: string;
|
|
88
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The subset of {@link BINDABLE_LAUNCH_CONTROLS} the plan HANDS OVER rather than
|
|
110
|
+
* executes: controls a caller reaches only by composing the harness's own
|
|
111
|
+
* configuration, so no file, flag or env var the plan writes can touch them.
|
|
112
|
+
*
|
|
113
|
+
* - `opencode.agent-prompt` — opencode's `agent.<name>.prompt` config field,
|
|
114
|
+
* the one control that replaces its built-in provider prompt. It binds to a
|
|
115
|
+
* SINGLE agent, and the agent is chosen at launch. A caller that builds
|
|
116
|
+
* opencode's config itself and picks the agent there reads neither
|
|
117
|
+
* `plan.files` nor `plan.flags`, so for that one the plan names the control
|
|
118
|
+
* and hands over the text in {@link WorkspacePlan.systemPromptBinding}.
|
|
119
|
+
*
|
|
120
|
+
* Every other bindable control names a capability the plan still lowers itself
|
|
121
|
+
* once the caller has declared the plan's launch inputs really reach the spawn.
|
|
122
|
+
*/
|
|
123
|
+
export declare const EXECUTOR_SYSTEM_PROMPT_BINDINGS: readonly ["opencode.agent-prompt"];
|
|
124
|
+
export type ExecutorSystemPromptBinding = (typeof EXECUTOR_SYSTEM_PROMPT_BINDINGS)[number];
|
|
125
|
+
export declare function isExecutorSystemPromptBinding(value: string): value is ExecutorSystemPromptBinding;
|
|
126
|
+
/**
|
|
127
|
+
* A system-prompt intent handed to the launcher that declared it owns the
|
|
128
|
+
* control, instead of being lowered into the workspace.
|
|
129
|
+
*
|
|
130
|
+
* Distinct from {@link WorkspacePlan.systemPrompt} — a launch ARGUMENT the
|
|
131
|
+
* executor hands to a harness flag — and from the file/flag/env lowerings the
|
|
132
|
+
* plan performs on its own. This names the exact control that must receive the
|
|
133
|
+
* text and which of the two intents it is, so the launcher cannot apply it to
|
|
134
|
+
* the wrong channel. Never written into an additive channel and never dropped:
|
|
135
|
+
* a caller that has not declared it owns `control` gets the plan's own lowering
|
|
136
|
+
* (or a refusal where the harness has no control at all) instead of this field.
|
|
137
|
+
*/
|
|
138
|
+
export interface WorkspacePlanSystemPromptBinding {
|
|
139
|
+
/** Which of the two prompt intents this is — never inferred from presence. */
|
|
140
|
+
intent: "replace";
|
|
141
|
+
/** The exact launcher control that must receive `systemPrompt`. */
|
|
142
|
+
control: ExecutorSystemPromptBinding;
|
|
143
|
+
/** Exact text the control receives. */
|
|
144
|
+
systemPrompt: string;
|
|
145
|
+
}
|
|
89
146
|
/**
|
|
90
147
|
* The dimensions `materializeProfile`'s `skip` option honors.
|
|
91
148
|
*
|
|
@@ -104,13 +161,23 @@ export declare function isSkippableDimension(value: string): value is SkippableD
|
|
|
104
161
|
* system-prompt replacement (`agent.<name>.prompt`) binds to the ONE agent
|
|
105
162
|
* whoever starts the process selects, which a plan-forwarding executor cannot
|
|
106
163
|
* guarantee — so the plan refuses the intent (harnessSystemPromptIntents in
|
|
107
|
-
* @tangle-network/agent-interface answers `replace: false` for opencode).
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
164
|
+
* @tangle-network/agent-interface answers `replace: false` for opencode).
|
|
165
|
+
*
|
|
166
|
+
* The two declarations describe the two executor shapes that CAN reach it, and
|
|
167
|
+
* each gets the lowering it can actually apply:
|
|
168
|
+
*
|
|
169
|
+
* - `systemPrompt` — the executor applies the plan's launch flags to the
|
|
170
|
+
* spawn it owns, so the plan writes the generated `mode: primary` agent and
|
|
171
|
+
* selects it with `--agent`.
|
|
172
|
+
* - `opencode.agent-prompt` — the executor composes opencode's own config and
|
|
173
|
+
* picks the agent there, reading neither plan files nor plan flags, so the
|
|
174
|
+
* plan hands the text over in {@link WorkspacePlan.systemPromptBinding}
|
|
175
|
+
* instead of writing a workspace agent that would never be selected.
|
|
176
|
+
*
|
|
177
|
+
* They are mutually exclusive claims about the same caller: declaring both is a
|
|
178
|
+
* caller bug, not a broader capability.
|
|
112
179
|
*/
|
|
113
|
-
export declare const BINDABLE_LAUNCH_CONTROLS: readonly ["systemPrompt"];
|
|
180
|
+
export declare const BINDABLE_LAUNCH_CONTROLS: readonly ["systemPrompt", "opencode.agent-prompt"];
|
|
114
181
|
export type BindableLaunchControl = (typeof BINDABLE_LAUNCH_CONTROLS)[number];
|
|
115
182
|
/** The plan a caller writes into the run cwd, then applies to the spawn. */
|
|
116
183
|
export interface WorkspacePlan {
|
|
@@ -121,18 +188,42 @@ export interface WorkspacePlan {
|
|
|
121
188
|
env: Record<string, WorkspacePlanConfigValue>;
|
|
122
189
|
/** Extra CLI flags to pass (e.g. kimi --mcp-config-file <path>). */
|
|
123
190
|
flags: WorkspacePlanArgument[];
|
|
124
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Exact text that REPLACES the harness's own system prompt, for harnesses
|
|
193
|
+
* whose replacement control takes it as a launch argument the executor
|
|
194
|
+
* supplies (claude-code `--system-prompt`, pi `--system-prompt`).
|
|
195
|
+
*
|
|
196
|
+
* Absent does NOT mean "no replacement was requested": a harness whose
|
|
197
|
+
* control reads a file (codex, gemini) carries the replacement in
|
|
198
|
+
* {@link WorkspacePlan.files} plus a flag or env var instead. Present means
|
|
199
|
+
* the executor MUST bind it to that harness's replacement control — never to
|
|
200
|
+
* an additive one, which would leave the built-in prompt in force.
|
|
201
|
+
*/
|
|
125
202
|
systemPrompt?: string;
|
|
126
203
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
204
|
+
* Exact text ADDED to the harness's own system prompt, which stays intact.
|
|
205
|
+
*
|
|
206
|
+
* Separate from {@link WorkspacePlan.systemPrompt} because the two are
|
|
207
|
+
* different intents against the same channel, and every harness that has one
|
|
208
|
+
* control may lack the other. An executor binds this ONLY to an additive
|
|
209
|
+
* control (claude-code `--append-system-prompt`, pi
|
|
210
|
+
* `--append-system-prompt`); there is no harness for which folding it into
|
|
211
|
+
* the replacement is correct, because that would delete the built-in prompt
|
|
212
|
+
* the caller asked to keep.
|
|
134
213
|
*/
|
|
135
214
|
appendSystemPrompt?: string;
|
|
215
|
+
/**
|
|
216
|
+
* A prompt intent no workspace file, flag, or env var can execute, addressed
|
|
217
|
+
* to the launcher control that can — see
|
|
218
|
+
* {@link WorkspacePlanSystemPromptBinding}.
|
|
219
|
+
*
|
|
220
|
+
* Present ONLY when the caller declared it owns the named control, so a plan
|
|
221
|
+
* built by a caller that forwards it to someone else's launcher never carries
|
|
222
|
+
* one; that caller gets the intent in {@link WorkspacePlan.unsupported}
|
|
223
|
+
* instead. Whoever reads it must bind it or throw — {@link
|
|
224
|
+
* takeSystemPromptBinding} does both.
|
|
225
|
+
*/
|
|
226
|
+
systemPromptBinding?: WorkspacePlanSystemPromptBinding;
|
|
136
227
|
/** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
|
|
137
228
|
unsupported: Unsupported[];
|
|
138
229
|
/**
|
|
@@ -185,6 +276,8 @@ export interface WorkspacePlanReceipt {
|
|
|
185
276
|
flags: WorkspacePlanArgument[];
|
|
186
277
|
systemPrompt?: string;
|
|
187
278
|
appendSystemPrompt?: string;
|
|
279
|
+
/** The launcher-bound intent, so a receipt says which control owed the work. */
|
|
280
|
+
systemPromptBinding?: WorkspacePlanSystemPromptBinding;
|
|
188
281
|
unsupported: Unsupported[];
|
|
189
282
|
written: string[];
|
|
190
283
|
}
|
|
@@ -194,6 +287,8 @@ export interface WorkspacePlanSpawnInputs {
|
|
|
194
287
|
flags: string[];
|
|
195
288
|
systemPrompt?: string;
|
|
196
289
|
appendSystemPrompt?: string;
|
|
290
|
+
/** Read it with {@link takeSystemPromptBinding}, which refuses a foreign control. */
|
|
291
|
+
systemPromptBinding?: WorkspacePlanSystemPromptBinding;
|
|
197
292
|
}
|
|
198
293
|
/** Result of applying a public plan at the private executor boundary. */
|
|
199
294
|
export interface WorkspacePlanApplication {
|
|
@@ -248,6 +343,18 @@ export interface AgentCandidateWorkspacePlanReceipt {
|
|
|
248
343
|
appendSystemPrompt?: AgentCandidateConfigValue;
|
|
249
344
|
unsupported: Unsupported[];
|
|
250
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* The replacement text this launcher must bind to `control`, or `undefined`
|
|
348
|
+
* when the plan carries no launcher-bound intent.
|
|
349
|
+
*
|
|
350
|
+
* Throws for a binding addressed to a DIFFERENT control: the reader cannot
|
|
351
|
+
* execute it, and answering `undefined` would start the agent with the
|
|
352
|
+
* harness's own prompt still in force while nothing reported a problem — the
|
|
353
|
+
* silent downgrade the binding exists to prevent. This is the seam where an
|
|
354
|
+
* executor that cannot bind an intent says so, by name, instead of running a
|
|
355
|
+
* profile whose prompt never took effect.
|
|
356
|
+
*/
|
|
357
|
+
export declare function takeSystemPromptBinding(spawn: Pick<WorkspacePlanSpawnInputs, "systemPromptBinding">, control: ExecutorSystemPromptBinding): string | undefined;
|
|
251
358
|
export interface MaterializeCandidateProfileOptions {
|
|
252
359
|
/** Exact text fetched from pinned resource refs, keyed by declared SHA-256. */
|
|
253
360
|
resolvedResources?: ReadonlyMap<Sha256Digest, string>;
|
|
@@ -308,7 +415,17 @@ export declare function normalizeSkillMd(name: string, raw: string): string;
|
|
|
308
415
|
*/
|
|
309
416
|
export declare function materializeProfile(inputProfile: AgentProfile, harness: HarnessId, opts?: {
|
|
310
417
|
skip?: SkippableDimension[];
|
|
311
|
-
/**
|
|
418
|
+
/**
|
|
419
|
+
* Launcher controls THIS caller owns and will bind itself — see
|
|
420
|
+
* {@link BINDABLE_LAUNCH_CONTROLS}. Each names a control no workspace file,
|
|
421
|
+
* flag or env var reaches on its own, so declaring one turns a plan-time
|
|
422
|
+
* refusal into the lowering (or the hand-over) that control can execute.
|
|
423
|
+
*
|
|
424
|
+
* A promise, not a hint: declare a control only from the code that starts
|
|
425
|
+
* the harness and writes that control. A caller that hands the plan to
|
|
426
|
+
* someone else's launcher declares nothing and keeps the refusal, which is
|
|
427
|
+
* what it can actually deliver.
|
|
428
|
+
*/
|
|
312
429
|
binds?: readonly BindableLaunchControl[];
|
|
313
430
|
}): WorkspacePlan;
|
|
314
431
|
/**
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
export { modelIdsMatch, qualifyModelId } from "./model-id.js";
|
|
25
25
|
import { createHash, randomUUID } from "node:crypto";
|
|
26
|
-
import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
26
|
+
import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
27
27
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32, } from "node:path";
|
|
28
28
|
import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, agentProfileSecretRefSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
|
|
29
29
|
export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
|
|
@@ -86,6 +86,28 @@ function assertMaterializerHarness(harness) {
|
|
|
86
86
|
throw new Error(`Unsupported profile materialization harness "${harness}"`);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The subset of {@link BINDABLE_LAUNCH_CONTROLS} the plan HANDS OVER rather than
|
|
91
|
+
* executes: controls a caller reaches only by composing the harness's own
|
|
92
|
+
* configuration, so no file, flag or env var the plan writes can touch them.
|
|
93
|
+
*
|
|
94
|
+
* - `opencode.agent-prompt` — opencode's `agent.<name>.prompt` config field,
|
|
95
|
+
* the one control that replaces its built-in provider prompt. It binds to a
|
|
96
|
+
* SINGLE agent, and the agent is chosen at launch. A caller that builds
|
|
97
|
+
* opencode's config itself and picks the agent there reads neither
|
|
98
|
+
* `plan.files` nor `plan.flags`, so for that one the plan names the control
|
|
99
|
+
* and hands over the text in {@link WorkspacePlan.systemPromptBinding}.
|
|
100
|
+
*
|
|
101
|
+
* Every other bindable control names a capability the plan still lowers itself
|
|
102
|
+
* once the caller has declared the plan's launch inputs really reach the spawn.
|
|
103
|
+
*/
|
|
104
|
+
export const EXECUTOR_SYSTEM_PROMPT_BINDINGS = [
|
|
105
|
+
"opencode.agent-prompt",
|
|
106
|
+
];
|
|
107
|
+
const EXECUTOR_SYSTEM_PROMPT_BINDING_SET = new Set(EXECUTOR_SYSTEM_PROMPT_BINDINGS);
|
|
108
|
+
export function isExecutorSystemPromptBinding(value) {
|
|
109
|
+
return EXECUTOR_SYSTEM_PROMPT_BINDING_SET.has(value);
|
|
110
|
+
}
|
|
89
111
|
/**
|
|
90
112
|
* The dimensions `materializeProfile`'s `skip` option honors.
|
|
91
113
|
*
|
|
@@ -120,13 +142,26 @@ function assertSkippableDimensions(skip) {
|
|
|
120
142
|
* system-prompt replacement (`agent.<name>.prompt`) binds to the ONE agent
|
|
121
143
|
* whoever starts the process selects, which a plan-forwarding executor cannot
|
|
122
144
|
* guarantee — so the plan refuses the intent (harnessSystemPromptIntents in
|
|
123
|
-
* @tangle-network/agent-interface answers `replace: false` for opencode).
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
145
|
+
* @tangle-network/agent-interface answers `replace: false` for opencode).
|
|
146
|
+
*
|
|
147
|
+
* The two declarations describe the two executor shapes that CAN reach it, and
|
|
148
|
+
* each gets the lowering it can actually apply:
|
|
149
|
+
*
|
|
150
|
+
* - `systemPrompt` — the executor applies the plan's launch flags to the
|
|
151
|
+
* spawn it owns, so the plan writes the generated `mode: primary` agent and
|
|
152
|
+
* selects it with `--agent`.
|
|
153
|
+
* - `opencode.agent-prompt` — the executor composes opencode's own config and
|
|
154
|
+
* picks the agent there, reading neither plan files nor plan flags, so the
|
|
155
|
+
* plan hands the text over in {@link WorkspacePlan.systemPromptBinding}
|
|
156
|
+
* instead of writing a workspace agent that would never be selected.
|
|
157
|
+
*
|
|
158
|
+
* They are mutually exclusive claims about the same caller: declaring both is a
|
|
159
|
+
* caller bug, not a broader capability.
|
|
128
160
|
*/
|
|
129
|
-
export const BINDABLE_LAUNCH_CONTROLS = [
|
|
161
|
+
export const BINDABLE_LAUNCH_CONTROLS = [
|
|
162
|
+
"systemPrompt",
|
|
163
|
+
...EXECUTOR_SYSTEM_PROMPT_BINDINGS,
|
|
164
|
+
];
|
|
130
165
|
const BINDABLE_LAUNCH_CONTROL_SET = new Set(BINDABLE_LAUNCH_CONTROLS);
|
|
131
166
|
/** Reject a binding declaration this materializer has no lowering for, rather than ignoring it. */
|
|
132
167
|
function assertBindableLaunchControls(binds) {
|
|
@@ -137,7 +172,14 @@ function assertBindableLaunchControls(binds) {
|
|
|
137
172
|
throw new Error(`materializeProfile: cannot bind ${unknown.join(", ")}. ` +
|
|
138
173
|
`Bindable launch controls are: ${BINDABLE_LAUNCH_CONTROLS.join(", ")}.`);
|
|
139
174
|
}
|
|
140
|
-
|
|
175
|
+
const declared = new Set(binds);
|
|
176
|
+
const handedOver = [...EXECUTOR_SYSTEM_PROMPT_BINDINGS].filter((control) => declared.has(control));
|
|
177
|
+
if (declared.has("systemPrompt") && handedOver.length > 0) {
|
|
178
|
+
throw new Error(`materializeProfile: cannot bind systemPrompt together with ${handedOver.join(", ")}. ` +
|
|
179
|
+
"The first says the plan's launch flags reach the spawn; the second says " +
|
|
180
|
+
"the caller composes the harness config itself and reads neither.");
|
|
181
|
+
}
|
|
182
|
+
return declared;
|
|
141
183
|
}
|
|
142
184
|
/** The plan was applied, but one or more private staging files remain. */
|
|
143
185
|
export class WorkspacePlanCleanupError extends AggregateError {
|
|
@@ -171,6 +213,66 @@ export function enforceWorkspacePlanSupport(plan, failOnError) {
|
|
|
171
213
|
assertWorkspacePlanSupported(plan);
|
|
172
214
|
return [];
|
|
173
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* The two prompt intents a plan carries, projected together onto an outgoing
|
|
218
|
+
* object with each field omitted when the source does not carry it.
|
|
219
|
+
*
|
|
220
|
+
* One helper instead of a pair of inline spreads at each of the digest,
|
|
221
|
+
* receipt, candidate, and spawn boundaries: the intents must travel as a pair,
|
|
222
|
+
* and a projection that copies the replacement and forgets the addition drops
|
|
223
|
+
* it silently — downstream cannot tell "the caller asked for nothing" from
|
|
224
|
+
* "this boundary lost it". Adding a third intent later is one edit here rather
|
|
225
|
+
* than nine.
|
|
226
|
+
*/
|
|
227
|
+
function promptIntentFields(source, project) {
|
|
228
|
+
return {
|
|
229
|
+
...(source.systemPrompt === undefined
|
|
230
|
+
? {}
|
|
231
|
+
: { systemPrompt: project(source.systemPrompt) }),
|
|
232
|
+
...(source.appendSystemPrompt === undefined
|
|
233
|
+
? {}
|
|
234
|
+
: { appendSystemPrompt: project(source.appendSystemPrompt) }),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** Identity projection for boundaries that pass the prompt intents through. */
|
|
238
|
+
const samePromptIntentValue = (value) => value;
|
|
239
|
+
/**
|
|
240
|
+
* The launcher-bound intent, projected onto an outgoing object and omitted when
|
|
241
|
+
* the source does not carry one.
|
|
242
|
+
*
|
|
243
|
+
* Beside {@link promptIntentFields} rather than inside it, because the two
|
|
244
|
+
* disagree on both ends: the candidate boundaries that helper also serves have
|
|
245
|
+
* no field for a binding — an {@link AgentCandidateWorkspacePlan} is built by a
|
|
246
|
+
* caller that declares none, so it can never hold one — and a value projection
|
|
247
|
+
* such as `candidatePublicString` has nothing to do to a structured binding.
|
|
248
|
+
* Every boundary that carries the plan's own prompt intents calls both.
|
|
249
|
+
*/
|
|
250
|
+
function systemPromptBindingField(source) {
|
|
251
|
+
return source.systemPromptBinding === undefined
|
|
252
|
+
? {}
|
|
253
|
+
: { systemPromptBinding: { ...source.systemPromptBinding } };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The replacement text this launcher must bind to `control`, or `undefined`
|
|
257
|
+
* when the plan carries no launcher-bound intent.
|
|
258
|
+
*
|
|
259
|
+
* Throws for a binding addressed to a DIFFERENT control: the reader cannot
|
|
260
|
+
* execute it, and answering `undefined` would start the agent with the
|
|
261
|
+
* harness's own prompt still in force while nothing reported a problem — the
|
|
262
|
+
* silent downgrade the binding exists to prevent. This is the seam where an
|
|
263
|
+
* executor that cannot bind an intent says so, by name, instead of running a
|
|
264
|
+
* profile whose prompt never took effect.
|
|
265
|
+
*/
|
|
266
|
+
export function takeSystemPromptBinding(spawn, control) {
|
|
267
|
+
const binding = spawn.systemPromptBinding;
|
|
268
|
+
if (!binding)
|
|
269
|
+
return undefined;
|
|
270
|
+
if (binding.control !== control) {
|
|
271
|
+
throw new Error(`Workspace plan carries a system-prompt ${binding.intent} bound to ` +
|
|
272
|
+
`${binding.control}, which this executor cannot apply; it binds ${control}`);
|
|
273
|
+
}
|
|
274
|
+
return binding.systemPrompt;
|
|
275
|
+
}
|
|
174
276
|
// ─── Verified per-harness conventions (capability matrix) ───────────────────
|
|
175
277
|
/**
|
|
176
278
|
* Whether the harness LOADS its {@link CONTEXT_FILE} from cwd on its own.
|
|
@@ -204,6 +306,61 @@ export function harnessContextFile(harness) {
|
|
|
204
306
|
}
|
|
205
307
|
return path;
|
|
206
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* File codex's `model_instructions_file` config key reads.
|
|
311
|
+
*
|
|
312
|
+
* cwd-relative on purpose: codex resolves the key against the process working
|
|
313
|
+
* directory, which is the same workspace root the plan is applied into, so the
|
|
314
|
+
* flag stays correct under an executor that binds the workspace at a different
|
|
315
|
+
* absolute path (docker, a jail, a remote box).
|
|
316
|
+
*/
|
|
317
|
+
const CODEX_SYSTEM_PROMPT_FILE = ".codex/system-prompt.md";
|
|
318
|
+
/**
|
|
319
|
+
* File carrying opencode's ADDITION to its system prompt.
|
|
320
|
+
*
|
|
321
|
+
* Separate from opencode's instruction file rather than merged into it: the two
|
|
322
|
+
* are different privilege levels of the same request, and one file for both
|
|
323
|
+
* would leave the plan unable to say which intent produced which bytes.
|
|
324
|
+
*/
|
|
325
|
+
const OPENCODE_APPEND_SYSTEM_PROMPT_FILE = ".opencode/agent-system-prompt.md";
|
|
326
|
+
/**
|
|
327
|
+
* Why a harness refuses a system-prompt REPLACEMENT.
|
|
328
|
+
*
|
|
329
|
+
* The reason names the control that does exist, so the refusal is actionable
|
|
330
|
+
* without a capability-matrix lookup — a bare "unsupported" sends the caller
|
|
331
|
+
* looking for a flag that was never there.
|
|
332
|
+
*/
|
|
333
|
+
function systemPromptRefusal(harness) {
|
|
334
|
+
switch (harness) {
|
|
335
|
+
case "opencode":
|
|
336
|
+
// The control exists, so the reason names the two declarations that reach
|
|
337
|
+
// it rather than claiming opencode has none.
|
|
338
|
+
return 'opencode: the only system-prompt replacement is per-agent (`agent.<name>.prompt`), which binds to the agent selected at launch and a workspace plan cannot guarantee; an executor that applies the plan\'s "--agent" flag to its own spawn declares binds: ["systemPrompt"], and one that composes opencode\'s config itself declares binds: ["opencode.agent-prompt"]';
|
|
339
|
+
default:
|
|
340
|
+
return `${harness}: no verified exact system-prompt replacement control`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Why a harness refuses an ADDITION to the system prompt.
|
|
345
|
+
*
|
|
346
|
+
* gemini puts its context file in the system-prompt position, so the temptation
|
|
347
|
+
* is to lower an addition there. That is refused because GEMINI.md is the ONE
|
|
348
|
+
* file it auto-loads: an addition written there is byte-identical to the same
|
|
349
|
+
* text under `prompt.instructions`, and no consumer could tell which intent the
|
|
350
|
+
* caller expressed. opencode is not refused for the same situation, because it
|
|
351
|
+
* loads exactly the files its config names and can therefore carry the two
|
|
352
|
+
* intents in separate, separately-ordered files.
|
|
353
|
+
*/
|
|
354
|
+
function appendSystemPromptRefusal(harness) {
|
|
355
|
+
switch (harness) {
|
|
356
|
+
case "gemini":
|
|
357
|
+
return "gemini: the only additive system-prompt channel is GEMINI.md memory, the single context file it auto-loads, which `prompt.instructions` already materializes — use it instead";
|
|
358
|
+
case "codex":
|
|
359
|
+
return "codex: no verified additive system-prompt control — its AGENTS.md lands in a developer/user message, not the system channel";
|
|
360
|
+
default:
|
|
361
|
+
return `${harness}: no verified additive system-prompt control`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
207
364
|
/** Project context file each harness actually reads from cwd. NEVER a constant. */
|
|
208
365
|
const CONTEXT_FILE = {
|
|
209
366
|
"claude-code": "CLAUDE.md",
|
|
@@ -220,6 +377,35 @@ const CONTEXT_FILE = {
|
|
|
220
377
|
hermes: "HERMES.md", // highest-priority native name; only ONE context loads (first match)
|
|
221
378
|
openclaw: "AGENTS.md",
|
|
222
379
|
};
|
|
380
|
+
/**
|
|
381
|
+
* The paths whose only possible author is this package, per
|
|
382
|
+
* {@link PlanFile.ownership}.
|
|
383
|
+
*
|
|
384
|
+
* Codex names no system-prompt file of its own — `model_instructions_file`
|
|
385
|
+
* takes whatever path it is given — and opencode names none either, since it
|
|
386
|
+
* reads exactly the files its config lists. All three names below were chosen
|
|
387
|
+
* here, so a file already at one of them is this materializer's own output from
|
|
388
|
+
* an earlier application into the same workspace.
|
|
389
|
+
*
|
|
390
|
+
* That is an inference from the path name, not verified provenance: nothing
|
|
391
|
+
* checks the existing bytes are prior materializer output, so a workspace that
|
|
392
|
+
* carried a foreign file at one of these invented names is overwritten on the
|
|
393
|
+
* first application. Accepted cost of stateless multi-turn re-application,
|
|
394
|
+
* bounded by the names being ones no harness or user convention uses.
|
|
395
|
+
*
|
|
396
|
+
* Deliberately absent: `.gemini/system.md` (gemini's own documented name),
|
|
397
|
+
* `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`, `opencode.json`. A user's repository
|
|
398
|
+
* can carry any of those, and clobbering one is worse than refusing the turn.
|
|
399
|
+
* For `opencode.json` this means a profile change that alters the
|
|
400
|
+
* `instructions[]` membership between turns of one workspace refuses loudly
|
|
401
|
+
* (the consolidated config no longer matches byte-for-byte) rather than
|
|
402
|
+
* replacing a possibly user-authored config.
|
|
403
|
+
*/
|
|
404
|
+
const MATERIALIZER_OWNED_PATHS = new Set([
|
|
405
|
+
CODEX_SYSTEM_PROMPT_FILE,
|
|
406
|
+
OPENCODE_APPEND_SYSTEM_PROMPT_FILE,
|
|
407
|
+
CONTEXT_FILE.opencode,
|
|
408
|
+
]);
|
|
223
409
|
/**
|
|
224
410
|
* cwd-relative native skill dir per harness, or null if the harness has no
|
|
225
411
|
* cwd skill primitive (skills live in a user/global dir the workspace plan
|
|
@@ -271,8 +457,6 @@ const MAX_PROFILE_RESOURCE_BYTES = 32 * 1024 * 1024;
|
|
|
271
457
|
const MAX_PROFILE_RESOURCE_REFS = 128;
|
|
272
458
|
const MAX_CONCURRENT_RESOURCE_FETCHES = 8;
|
|
273
459
|
const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
|
|
274
|
-
const CODEX_SYSTEM_PROMPT_PATH = ".codex/system-prompt.md";
|
|
275
|
-
const OPENCODE_APPEND_SYSTEM_PROMPT_PATH = ".opencode/agent-system-prompt.md";
|
|
276
460
|
const SAFE_WRITE_FLAGS = fsConstants.O_WRONLY |
|
|
277
461
|
fsConstants.O_CREAT |
|
|
278
462
|
fsConstants.O_EXCL |
|
|
@@ -645,6 +829,12 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
645
829
|
relPath: normalizedPath,
|
|
646
830
|
content,
|
|
647
831
|
source,
|
|
832
|
+
// Only a path this package invented AND generated here is its own to
|
|
833
|
+
// replace; the same path carrying a caller's `resources` mount is that
|
|
834
|
+
// caller's file, whatever its name.
|
|
835
|
+
...(source === "generated" && MATERIALIZER_OWNED_PATHS.has(normalizedPath)
|
|
836
|
+
? { ownership: "materializer" }
|
|
837
|
+
: {}),
|
|
648
838
|
...(fileMode === undefined ? {} : { mode: validateFileMode(fileMode) }),
|
|
649
839
|
...(secretSlots === undefined
|
|
650
840
|
? {}
|
|
@@ -665,6 +855,14 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
665
855
|
if (previousDimension) {
|
|
666
856
|
throw new Error(`Duplicate profile resource path: ${normalizedPath} (${previousDimension}, ${dimension})`);
|
|
667
857
|
}
|
|
858
|
+
// Refuse at plan time so {@link MATERIALIZER_OWNED_PATHS} stays true: those
|
|
859
|
+
// paths are replaced on every re-application, so a mount there would be
|
|
860
|
+
// silently overwritten by the next turn rather than kept.
|
|
861
|
+
if (MATERIALIZER_OWNED_PATHS.has(normalizedPath)) {
|
|
862
|
+
throw new Error(`Profile resource path is reserved by the materializer: ${normalizedPath} (${dimension}). ` +
|
|
863
|
+
"This package writes that path itself and replaces it on re-application, so a mount " +
|
|
864
|
+
"there cannot survive; choose another path.");
|
|
865
|
+
}
|
|
668
866
|
explicitResourcePaths.set(normalizedPath, dimension);
|
|
669
867
|
pushPlanFile(normalizedPath, content, "user", mode);
|
|
670
868
|
};
|
|
@@ -725,36 +923,84 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
725
923
|
}
|
|
726
924
|
}
|
|
727
925
|
}
|
|
728
|
-
// 4) PROMPT.
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
926
|
+
// 4) PROMPT. Replacing the harness's own system prompt and adding to it are
|
|
927
|
+
// two intents against one channel, and a harness almost never has both. Each
|
|
928
|
+
// lowers ONLY to a control whose measured job is exactly that intent; the
|
|
929
|
+
// other is refused. Substituting one for the other is the silent downgrade
|
|
930
|
+
// this section exists to prevent — an addition standing in for a replacement
|
|
931
|
+
// leaves the instructions the caller asked to delete in force, and a
|
|
932
|
+
// replacement standing in for an addition deletes the harness's own prompt.
|
|
933
|
+
// Neither is visible in the result, which is why both fail closed instead.
|
|
734
934
|
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
// the
|
|
739
|
-
|
|
935
|
+
// `instructions` is a third, lower-privilege surface (the harness's
|
|
936
|
+
// AGENTS.md/CLAUDE.md-style context file). On gemini and opencode that surface
|
|
937
|
+
// does reach the model in the system-prompt position, so an addition can be
|
|
938
|
+
// lowered there ONLY where the plan can keep the two apart. opencode can: it
|
|
939
|
+
// loads exactly the files `opencode.json` `instructions[]` names, so the
|
|
940
|
+
// addition gets its own file registered ahead of the instruction file. gemini
|
|
941
|
+
// cannot: GEMINI.md is the one context file it auto-loads, so an addition
|
|
942
|
+
// lowered there would be byte-indistinguishable from `instructions`, which is
|
|
943
|
+
// one-channel-two-meanings recreated a level down.
|
|
944
|
+
//
|
|
945
|
+
// Which harness owns which intent is the measured harnessSystemPromptIntents
|
|
946
|
+
// table in @tangle-network/agent-interface; the mechanics per owned cell live
|
|
947
|
+
// here.
|
|
740
948
|
if (profile.prompt?.systemPrompt !== undefined) {
|
|
741
|
-
if (h === "
|
|
949
|
+
if (h === "claude-code" || h === "pi" || h === "prime") {
|
|
950
|
+
// claude-code `--system-prompt`, pi `--system-prompt`: both take the
|
|
951
|
+
// replacement as a launch argument the executor supplies, and both
|
|
952
|
+
// measurably drop the harness's built-in prompt from the wire request.
|
|
953
|
+
// prime inherits the same flag pair from its pi line (fork
|
|
954
|
+
// cli/args.ts:139-143 → resourceLoaderOptions main.ts:745-746, the
|
|
955
|
+
// replacement deletes the built-in prompt: resource-loader.ts:482-485,
|
|
956
|
+
// system-prompt.ts:72-73).
|
|
957
|
+
plan.systemPrompt = profile.prompt.systemPrompt;
|
|
958
|
+
}
|
|
959
|
+
else if (h === "codex") {
|
|
960
|
+
// codex has no replacement FLAG; its control is the config key
|
|
961
|
+
// `model_instructions_file`, which reads a cwd-relative file and becomes
|
|
962
|
+
// the request's entire `instructions` field. File + flag keeps the plan
|
|
963
|
+
// self-applying for every executor that already forwards plan.flags.
|
|
964
|
+
add(CODEX_SYSTEM_PROMPT_FILE, profile.prompt.systemPrompt);
|
|
965
|
+
plan.flags.push("-c", `model_instructions_file=${CODEX_SYSTEM_PROMPT_FILE}`);
|
|
966
|
+
}
|
|
967
|
+
else if (h === "gemini") {
|
|
968
|
+
add(".gemini/system.md", profile.prompt.systemPrompt);
|
|
969
|
+
plan.env.GEMINI_SYSTEM_MD = "1";
|
|
970
|
+
}
|
|
971
|
+
else if (h === "opencode") {
|
|
972
|
+
// opencode's replacement control is `agent.<name>.prompt`: it binds to
|
|
973
|
+
// ONE agent, and the agent is chosen at launch, so executing it means
|
|
974
|
+
// writing that agent AND selecting it. There are two shapes of executor
|
|
975
|
+
// that can promise one of those, and each gets the lowering it can apply;
|
|
976
|
+
// an executor that promises neither gets the refusal, because a plan
|
|
977
|
+
// written for an unknown launcher cannot deliver either half.
|
|
978
|
+
//
|
|
979
|
+
// No shape can express an EMPTY replacement — an agent `prompt` with no
|
|
980
|
+
// text falls back to the built-in prompt instead of deleting it — so the
|
|
981
|
+
// guard sits above the split and no path bypasses it.
|
|
742
982
|
const systemPrompt = profile.prompt.systemPrompt;
|
|
743
983
|
if (systemPrompt.trim().length === 0) {
|
|
744
984
|
throw new Error("opencode cannot express an empty exact system-prompt replacement");
|
|
745
985
|
}
|
|
746
|
-
if (
|
|
747
|
-
// opencode's
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
"
|
|
754
|
-
|
|
755
|
-
|
|
986
|
+
if (binds.has("opencode.agent-prompt")) {
|
|
987
|
+
// A caller that composes opencode's own config and picks the agent
|
|
988
|
+
// itself owns the control outright; it does not read plan files or
|
|
989
|
+
// forward plan flags, so a workspace agent would never be selected.
|
|
990
|
+
// Hand it the intent named against the exact control it claimed.
|
|
991
|
+
plan.systemPromptBinding = {
|
|
992
|
+
intent: "replace",
|
|
993
|
+
control: "opencode.agent-prompt",
|
|
994
|
+
systemPrompt,
|
|
995
|
+
};
|
|
756
996
|
}
|
|
757
|
-
else {
|
|
997
|
+
else if (binds.has("systemPrompt")) {
|
|
998
|
+
// A caller that applies the plan's launch flags to the spawn it owns
|
|
999
|
+
// lets the plan execute the control itself: a `mode: primary` agent
|
|
1000
|
+
// file holding the prompt, plus the `--agent` flag that selects it.
|
|
1001
|
+
// Without the flag the file is inert and the built-in prompt the
|
|
1002
|
+
// caller asked to delete stays in force, so the two are pushed
|
|
1003
|
+
// together or not at all.
|
|
758
1004
|
const primaryAgentName = `${OPENCODE_PRIMARY_AGENT_PREFIX}${sha256(systemPrompt).slice(0, 16)}`;
|
|
759
1005
|
const primaryAgentPath = `.opencode/agents/${primaryAgentName}.md`;
|
|
760
1006
|
if (profile.subagents?.[primaryAgentName] !== undefined) {
|
|
@@ -764,61 +1010,57 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
764
1010
|
add(primaryAgentPath, ["---", "mode: primary", "---", systemPrompt, ""].join("\n"));
|
|
765
1011
|
plan.flags.push("--agent", primaryAgentName);
|
|
766
1012
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
// inherits the same flag pair from its pi line (fork cli/args.ts:139-143
|
|
771
|
-
// → resourceLoaderOptions main.ts:745-746, replacement deletes the
|
|
772
|
-
// built-in prompt: resource-loader.ts:482-485, system-prompt.ts:72-73).
|
|
773
|
-
plan.systemPrompt = profile.prompt.systemPrompt;
|
|
774
|
-
}
|
|
775
|
-
else if (h === "codex") {
|
|
776
|
-
// codex's replacement is a config file, never a launch-argument string:
|
|
777
|
-
// `model_instructions_file` becomes the request's entire instructions
|
|
778
|
-
// text (measured against codex 0.146.0).
|
|
779
|
-
add(CODEX_SYSTEM_PROMPT_PATH, profile.prompt.systemPrompt);
|
|
780
|
-
plan.flags.push("-c", `model_instructions_file=${CODEX_SYSTEM_PROMPT_PATH}`);
|
|
781
|
-
}
|
|
782
|
-
else if (h === "gemini") {
|
|
783
|
-
add(".gemini/system.md", profile.prompt.systemPrompt);
|
|
784
|
-
plan.env.GEMINI_SYSTEM_MD = "1";
|
|
1013
|
+
else {
|
|
1014
|
+
unsupported("systemPrompt", systemPromptRefusal(h));
|
|
1015
|
+
}
|
|
785
1016
|
}
|
|
786
1017
|
else {
|
|
787
|
-
unsupported("systemPrompt",
|
|
1018
|
+
unsupported("systemPrompt", systemPromptRefusal(h));
|
|
788
1019
|
}
|
|
789
1020
|
}
|
|
1021
|
+
// Ordered registration list for opencode, whose TUI and headless runs read
|
|
1022
|
+
// ONLY the files named here. Addition first so it frames the lower-privilege
|
|
1023
|
+
// instruction file that follows, matching the order the model receives them.
|
|
1024
|
+
const openCodeInstructionFiles = [];
|
|
1025
|
+
// A launcher that declared `opencode.agent-prompt` composes opencode's config
|
|
1026
|
+
// itself and reads neither plan files nor plan flags, so the workspace
|
|
1027
|
+
// `opencode.json` that registers these additive files would never load.
|
|
1028
|
+
// Lowering them anyway would be the silent drop this module refuses to
|
|
1029
|
+
// perform, so each additive intent is refused by name instead: the launcher
|
|
1030
|
+
// owns the `instructions[]` channel too and must compose it in the config it
|
|
1031
|
+
// builds.
|
|
1032
|
+
const openCodeConfigUnreachable = h === "opencode" && binds.has("opencode.agent-prompt");
|
|
790
1033
|
if (profile.prompt?.appendSystemPrompt !== undefined) {
|
|
791
1034
|
const appendSystemPrompt = profile.prompt.appendSystemPrompt;
|
|
792
1035
|
if (appendSystemPrompt.trim().length === 0) {
|
|
793
1036
|
throw new Error("prompt.appendSystemPrompt must be non-empty text");
|
|
794
1037
|
}
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
// adds this text after it (claude-code/pi measured; prime fork
|
|
798
|
-
// cli/args.ts:139-143, addition appends after the built-in prompt:
|
|
799
|
-
// system-prompt.ts:61,109-110).
|
|
800
|
-
plan.appendSystemPrompt = appendSystemPrompt;
|
|
1038
|
+
if (openCodeConfigUnreachable) {
|
|
1039
|
+
unsupported("appendSystemPrompt", "opencode: the launcher that binds opencode.agent-prompt composes opencode's config itself and loads no workspace opencode.json, so an addition registered there would never reach the model; compose it into the launcher's own instructions[]");
|
|
801
1040
|
}
|
|
802
1041
|
else if (h === "opencode") {
|
|
803
|
-
// opencode composes config-declared `instructions[]`
|
|
804
|
-
// single role:"system" message as its built-in prompt, which
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
|
|
1042
|
+
// opencode composes config-declared `instructions[]` file contents into
|
|
1043
|
+
// the same single role:"system" message as its built-in prompt, which
|
|
1044
|
+
// stays in place (its replacement control is the separate per-agent
|
|
1045
|
+
// `prompt` field). A dedicated file keeps this intent addressable on its
|
|
1046
|
+
// own rather than merged into the instruction file.
|
|
1047
|
+
add(OPENCODE_APPEND_SYSTEM_PROMPT_FILE, appendSystemPrompt.endsWith("\n")
|
|
808
1048
|
? appendSystemPrompt
|
|
809
1049
|
: `${appendSystemPrompt}\n`);
|
|
810
|
-
openCodeInstructionFiles.push(
|
|
811
|
-
}
|
|
812
|
-
else if (h === "codex") {
|
|
813
|
-
unsupported("systemPrompt", "codex: no verified additive system-prompt control — its AGENTS.md lands in a " +
|
|
814
|
-
"developer/user message, not the system channel");
|
|
1050
|
+
openCodeInstructionFiles.push(OPENCODE_APPEND_SYSTEM_PROMPT_FILE);
|
|
815
1051
|
}
|
|
816
|
-
else if (h === "
|
|
817
|
-
|
|
818
|
-
|
|
1052
|
+
else if (h === "claude-code" || h === "pi" || h === "prime") {
|
|
1053
|
+
// claude-code `--append-system-prompt`, pi `--append-system-prompt`: both
|
|
1054
|
+
// add the caller's text to the harness's own system prompt, which stays
|
|
1055
|
+
// on the wire intact. prime inherits the same flag from its pi line (fork
|
|
1056
|
+
// cli/args.ts:139-143, the addition appends after the built-in prompt:
|
|
1057
|
+
// system-prompt.ts:61,109-110). Carried beside the replacement rather
|
|
1058
|
+
// than merged into it, so an executor applies each through its own
|
|
1059
|
+
// control and the plan's identity distinguishes the two intents.
|
|
1060
|
+
plan.appendSystemPrompt = appendSystemPrompt;
|
|
819
1061
|
}
|
|
820
1062
|
else {
|
|
821
|
-
unsupported("
|
|
1063
|
+
unsupported("appendSystemPrompt", appendSystemPromptRefusal(h));
|
|
822
1064
|
}
|
|
823
1065
|
}
|
|
824
1066
|
const instr = [];
|
|
@@ -833,12 +1075,20 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
833
1075
|
unsupported("instructions", "instruction ref is not inline (github refs need async pre-resolution)");
|
|
834
1076
|
}
|
|
835
1077
|
if (instr.length) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1078
|
+
if (openCodeConfigUnreachable) {
|
|
1079
|
+
unsupported("instructions", "opencode: the launcher that binds opencode.agent-prompt composes opencode's config itself and loads no workspace opencode.json, so an instruction file registered there would never reach the model; compose it into the launcher's own instructions[]");
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
const instructionPath = CONTEXT_FILE[h];
|
|
1083
|
+
add(instructionPath, `${instr.join("\n\n")}\n`);
|
|
1084
|
+
if (h === "opencode")
|
|
1085
|
+
openCodeInstructionFiles.push(instructionPath);
|
|
1086
|
+
// gemini reads AGENTS.md only via settings; we write GEMINI.md so no extra step.
|
|
1087
|
+
}
|
|
841
1088
|
}
|
|
1089
|
+
// ONE opencode.json write for the whole prompt section: top-level JSON merge
|
|
1090
|
+
// is last-wins per key, and two `add` calls at the same relPath are a
|
|
1091
|
+
// duplicate-file error, so a second write would drop whichever lost.
|
|
842
1092
|
if (openCodeInstructionFiles.length > 0) {
|
|
843
1093
|
add("opencode.json", serializeWorkspacePlanJson({
|
|
844
1094
|
$schema: "https://opencode.ai/config.json",
|
|
@@ -957,14 +1207,7 @@ export function materializeCandidateProfile(profile, harness, opts = {}) {
|
|
|
957
1207
|
files: classicPlan.files,
|
|
958
1208
|
env: candidatePublicRecord(classicPlan.env),
|
|
959
1209
|
flags: classicPlan.flags.map(candidatePublicArgument),
|
|
960
|
-
...(classicPlan
|
|
961
|
-
? {}
|
|
962
|
-
: { systemPrompt: candidatePublicValue(classicPlan.systemPrompt) }),
|
|
963
|
-
...(classicPlan.appendSystemPrompt === undefined
|
|
964
|
-
? {}
|
|
965
|
-
: {
|
|
966
|
-
appendSystemPrompt: candidatePublicValue(classicPlan.appendSystemPrompt),
|
|
967
|
-
}),
|
|
1210
|
+
...promptIntentFields(classicPlan, candidatePublicValue),
|
|
968
1211
|
unsupported: classicPlan.unsupported,
|
|
969
1212
|
};
|
|
970
1213
|
}
|
|
@@ -1281,6 +1524,7 @@ function normalizePlanFiles(files) {
|
|
|
1281
1524
|
relPath,
|
|
1282
1525
|
content: file.content,
|
|
1283
1526
|
mode,
|
|
1527
|
+
ownership: file.ownership ?? "shared",
|
|
1284
1528
|
secretSlots,
|
|
1285
1529
|
};
|
|
1286
1530
|
})
|
|
@@ -1332,6 +1576,10 @@ export function hashWorkspacePlan(plan) {
|
|
|
1332
1576
|
relPath: file.relPath,
|
|
1333
1577
|
mode: file.mode,
|
|
1334
1578
|
contentSha256: sha256(file.content),
|
|
1579
|
+
// Ownership decides whether re-application may replace the file, so two
|
|
1580
|
+
// plans identical in bytes but different here behave differently in the
|
|
1581
|
+
// same workspace — the digest a receipt proves must say which one ran.
|
|
1582
|
+
ownership: file.ownership,
|
|
1335
1583
|
...(file.secretSlots.length === 0
|
|
1336
1584
|
? {}
|
|
1337
1585
|
: {
|
|
@@ -1355,12 +1603,10 @@ export function hashWorkspacePlan(plan) {
|
|
|
1355
1603
|
files,
|
|
1356
1604
|
env,
|
|
1357
1605
|
flags: plan.flags.map(normalizeWorkspacePlanArgument),
|
|
1358
|
-
...(plan
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
...(plan
|
|
1362
|
-
? {}
|
|
1363
|
-
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1606
|
+
...promptIntentFields(plan, samePromptIntentValue),
|
|
1607
|
+
// A plan that owes a launcher a replacement is not the same plan as one
|
|
1608
|
+
// that owes nothing, even when every byte it writes is identical.
|
|
1609
|
+
...systemPromptBindingField(plan),
|
|
1364
1610
|
unsupported,
|
|
1365
1611
|
...(requires.length === 0 ? {} : { requires }),
|
|
1366
1612
|
});
|
|
@@ -1378,12 +1624,7 @@ export function agentCandidateProfilePlanMaterial(plan) {
|
|
|
1378
1624
|
})),
|
|
1379
1625
|
env: Object.fromEntries(Object.entries(plan.env).sort(([left], [right]) => compareStrings(left, right))),
|
|
1380
1626
|
flags: [...plan.flags],
|
|
1381
|
-
...(plan
|
|
1382
|
-
? {}
|
|
1383
|
-
: { systemPrompt: plan.systemPrompt }),
|
|
1384
|
-
...(plan.appendSystemPrompt === undefined
|
|
1385
|
-
? {}
|
|
1386
|
-
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1627
|
+
...promptIntentFields(plan, samePromptIntentValue),
|
|
1387
1628
|
unsupported: [...plan.unsupported].sort((left, right) => compareStrings(`${left.dimension}\0${left.reason}`, `${right.dimension}\0${right.reason}`)),
|
|
1388
1629
|
};
|
|
1389
1630
|
return agentCandidateProfilePlanMaterialSchema.parse(material);
|
|
@@ -1443,14 +1684,7 @@ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorks
|
|
|
1443
1684
|
candidatePublicString(value),
|
|
1444
1685
|
])),
|
|
1445
1686
|
flags: plan.flags.map(candidatePublicString),
|
|
1446
|
-
...(plan
|
|
1447
|
-
? {}
|
|
1448
|
-
: { systemPrompt: candidatePublicString(plan.systemPrompt) }),
|
|
1449
|
-
...(plan.appendSystemPrompt === undefined
|
|
1450
|
-
? {}
|
|
1451
|
-
: {
|
|
1452
|
-
appendSystemPrompt: candidatePublicString(plan.appendSystemPrompt),
|
|
1453
|
-
}),
|
|
1687
|
+
...promptIntentFields(plan, candidatePublicString),
|
|
1454
1688
|
unsupported: plan.unsupported,
|
|
1455
1689
|
}, workspaceDir);
|
|
1456
1690
|
return {
|
|
@@ -1462,12 +1696,7 @@ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorks
|
|
|
1462
1696
|
},
|
|
1463
1697
|
env: { ...plan.env },
|
|
1464
1698
|
flags: [...plan.flags],
|
|
1465
|
-
...(plan
|
|
1466
|
-
? {}
|
|
1467
|
-
: { systemPrompt: plan.systemPrompt }),
|
|
1468
|
-
...(plan.appendSystemPrompt === undefined
|
|
1469
|
-
? {}
|
|
1470
|
-
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1699
|
+
...promptIntentFields(plan, samePromptIntentValue),
|
|
1471
1700
|
unsupported: [...plan.unsupported],
|
|
1472
1701
|
};
|
|
1473
1702
|
}
|
|
@@ -1552,12 +1781,8 @@ function resolvePublicWorkspacePlan(plan) {
|
|
|
1552
1781
|
files,
|
|
1553
1782
|
env,
|
|
1554
1783
|
flags,
|
|
1555
|
-
...(plan
|
|
1556
|
-
|
|
1557
|
-
: { systemPrompt: plan.systemPrompt }),
|
|
1558
|
-
...(plan.appendSystemPrompt === undefined
|
|
1559
|
-
? {}
|
|
1560
|
-
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1784
|
+
...promptIntentFields(plan, samePromptIntentValue),
|
|
1785
|
+
...systemPromptBindingField(plan),
|
|
1561
1786
|
};
|
|
1562
1787
|
}
|
|
1563
1788
|
async function resolvePrivateWorkspacePlan(plan, secretProvider) {
|
|
@@ -1623,12 +1848,8 @@ async function resolvePrivateWorkspacePlan(plan, secretProvider) {
|
|
|
1623
1848
|
files,
|
|
1624
1849
|
env,
|
|
1625
1850
|
flags,
|
|
1626
|
-
...(plan
|
|
1627
|
-
|
|
1628
|
-
: { systemPrompt: plan.systemPrompt }),
|
|
1629
|
-
...(plan.appendSystemPrompt === undefined
|
|
1630
|
-
? {}
|
|
1631
|
-
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1851
|
+
...promptIntentFields(plan, samePromptIntentValue),
|
|
1852
|
+
...systemPromptBindingField(plan),
|
|
1632
1853
|
};
|
|
1633
1854
|
}
|
|
1634
1855
|
function safeWorkspacePlanReceipt(plan, written, workspacePlanDigest = hashWorkspacePlan(plan)) {
|
|
@@ -1639,12 +1860,8 @@ function safeWorkspacePlanReceipt(plan, written, workspacePlanDigest = hashWorks
|
|
|
1639
1860
|
typeof value === "string" ? value : { ...value },
|
|
1640
1861
|
])),
|
|
1641
1862
|
flags: plan.flags.map(normalizeWorkspacePlanArgument),
|
|
1642
|
-
...(plan
|
|
1643
|
-
|
|
1644
|
-
: { systemPrompt: plan.systemPrompt }),
|
|
1645
|
-
...(plan.appendSystemPrompt === undefined
|
|
1646
|
-
? {}
|
|
1647
|
-
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1863
|
+
...promptIntentFields(plan, samePromptIntentValue),
|
|
1864
|
+
...systemPromptBindingField(plan),
|
|
1648
1865
|
unsupported: plan.unsupported.map((entry) => ({ ...entry })),
|
|
1649
1866
|
written: [...written],
|
|
1650
1867
|
};
|
|
@@ -1666,11 +1883,17 @@ function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, opti
|
|
|
1666
1883
|
...file,
|
|
1667
1884
|
absPath: resolveWorkspaceFilePath(workspaceRoot, file.relPath),
|
|
1668
1885
|
}));
|
|
1886
|
+
// A path this package invented can only hold this package's own earlier
|
|
1887
|
+
// output, so re-applying into the same workspace replaces it rather than
|
|
1888
|
+
// failing the turn. `reject` still refuses: that policy is "nothing may
|
|
1889
|
+
// pre-exist here", which is a statement about the workspace, not authorship.
|
|
1890
|
+
const mayReplace = (file) => existingFiles === "allow-identical" && file.ownership === "materializer";
|
|
1669
1891
|
// Validate the complete plan before the first write so a late traversal or
|
|
1670
1892
|
// existing symlink cannot leave a partially materialized workspace.
|
|
1671
1893
|
for (const file of files) {
|
|
1672
1894
|
assertNoSymlinkPathComponents(workspaceRoot, file.absPath);
|
|
1673
1895
|
if (workspaceFileExists(file.absPath) &&
|
|
1896
|
+
!mayReplace(file) &&
|
|
1674
1897
|
(existingFiles === "reject" || !workspaceFileMatches(file))) {
|
|
1675
1898
|
throw new Error(`Refusing to replace existing workspace file: ${file.relPath}`);
|
|
1676
1899
|
}
|
|
@@ -1707,6 +1930,20 @@ function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, opti
|
|
|
1707
1930
|
workspaceFileMatches(entry.file)) {
|
|
1708
1931
|
continue;
|
|
1709
1932
|
}
|
|
1933
|
+
if (isFsError(error, "EEXIST") && mayReplace(entry.file)) {
|
|
1934
|
+
// rename() replaces in one step, so no window exists in which the
|
|
1935
|
+
// path is missing and a concurrent spawn reads nothing. The staged
|
|
1936
|
+
// inode moves onto the path, which is what rollback identifies the
|
|
1937
|
+
// file by, so an abort still removes exactly what this call
|
|
1938
|
+
// published — leaving the path ABSENT rather than restored to the
|
|
1939
|
+
// superseded bytes. That is the fail-closed direction: the harness
|
|
1940
|
+
// whose flag names a missing file refuses to start, whereas silently
|
|
1941
|
+
// restored previous-turn bytes would run the wrong prompt.
|
|
1942
|
+
assertNoSymlinkPathComponents(workspaceRoot, entry.file.absPath);
|
|
1943
|
+
renameSync(entry.temporaryPath, entry.file.absPath);
|
|
1944
|
+
entry.published = true;
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1710
1947
|
if (isFsError(error, "EEXIST")) {
|
|
1711
1948
|
throw new Error(`Refusing to replace existing workspace file: ${entry.file.relPath}`, { cause: error });
|
|
1712
1949
|
}
|
|
@@ -1762,12 +1999,8 @@ function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, opti
|
|
|
1762
1999
|
spawn: {
|
|
1763
2000
|
env: { ...resolvedPlan.env },
|
|
1764
2001
|
flags: [...resolvedPlan.flags],
|
|
1765
|
-
...(resolvedPlan
|
|
1766
|
-
|
|
1767
|
-
: { systemPrompt: resolvedPlan.systemPrompt }),
|
|
1768
|
-
...(resolvedPlan.appendSystemPrompt === undefined
|
|
1769
|
-
? {}
|
|
1770
|
-
: { appendSystemPrompt: resolvedPlan.appendSystemPrompt }),
|
|
2002
|
+
...promptIntentFields(resolvedPlan, samePromptIntentValue),
|
|
2003
|
+
...systemPromptBindingField(resolvedPlan),
|
|
1771
2004
|
},
|
|
1772
2005
|
runtimeOnlyFiles: privateRuntimeFiles,
|
|
1773
2006
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|