@tangle-network/agent-profile-materialize 0.10.3 → 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 CHANGED
@@ -21,6 +21,7 @@
21
21
  * `node:fs` IO consumer. It was previously copied byte-for-byte into both
22
22
  * cli-bridge and agent-dev-container — this is the single source those re-export.
23
23
  */
24
+ export { modelIdsMatch, qualifyModelId } from "./model-id.js";
24
25
  import type { AgentCandidateConfigValue, AgentCandidateProfile, AgentCandidateProfileActivation, AgentCandidateProfileApplication, AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, AgentProfile, AgentProfileConfigValue, AgentProfileMcpServer, AgentProfileSecretProvider, AgentProfileSecretRef, Sha256Digest } from "@tangle-network/agent-interface";
25
26
  export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
26
27
  /**
@@ -28,7 +29,7 @@ export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecr
28
29
  * the canonical harness enum, so a rename upstream is a compile error here rather than an
29
30
  * `isMaterializerHarness` that silently returns false for a harness that still exists.
30
31
  */
31
- export declare const MATERIALIZER_HARNESSES: readonly ["claude-code", "nanoclaw", "codex", "opencode", "kimi-code", "pi", "gemini", "hermes", "openclaw"];
32
+ export declare const MATERIALIZER_HARNESSES: readonly ["claude-code", "nanoclaw", "codex", "opencode", "kimi-code", "pi", "prime", "gemini", "hermes", "openclaw"];
32
33
  export type HarnessId = (typeof MATERIALIZER_HARNESSES)[number];
33
34
  export declare function isMaterializerHarness(harness: string): harness is HarnessId;
34
35
  export interface PlanFile {
@@ -56,6 +57,25 @@ export interface PlanFile {
56
57
  * this on every file it emits.
57
58
  */
58
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";
59
79
  /**
60
80
  * Secret leaves in a generated JSON document. `content` contains `null` at
61
81
  * each path until the private executor resolves the named reference. The
@@ -82,9 +102,47 @@ export type WorkspacePlanArgument = string | WorkspacePlanArgumentTemplate;
82
102
  /** Public bytes stay strings; secret references stay opaque until application. */
83
103
  export type WorkspacePlanConfigValue = string | AgentProfileSecretRef;
84
104
  export interface Unsupported {
85
- 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";
86
106
  reason: string;
87
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
+ }
88
146
  /**
89
147
  * The dimensions `materializeProfile`'s `skip` option honors.
90
148
  *
@@ -95,6 +153,32 @@ export interface Unsupported {
95
153
  export declare const SKIPPABLE_DIMENSIONS: readonly ["mcp", "extensions"];
96
154
  export type SkippableDimension = (typeof SKIPPABLE_DIMENSIONS)[number];
97
155
  export declare function isSkippableDimension(value: string): value is SkippableDimension;
156
+ /**
157
+ * Launcher controls an executor can declare it OWNS via `materializeProfile`'s
158
+ * `binds` option.
159
+ *
160
+ * Some harness controls live outside any workspace: opencode's exact
161
+ * system-prompt replacement (`agent.<name>.prompt`) binds to the ONE agent
162
+ * whoever starts the process selects, which a plan-forwarding executor cannot
163
+ * guarantee — so the plan refuses the intent (harnessSystemPromptIntents in
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.
179
+ */
180
+ export declare const BINDABLE_LAUNCH_CONTROLS: readonly ["systemPrompt", "opencode.agent-prompt"];
181
+ export type BindableLaunchControl = (typeof BINDABLE_LAUNCH_CONTROLS)[number];
98
182
  /** The plan a caller writes into the run cwd, then applies to the spawn. */
99
183
  export interface WorkspacePlan {
100
184
  harness: HarnessId;
@@ -104,8 +188,42 @@ export interface WorkspacePlan {
104
188
  env: Record<string, WorkspacePlanConfigValue>;
105
189
  /** Extra CLI flags to pass (e.g. kimi --mcp-config-file <path>). */
106
190
  flags: WorkspacePlanArgument[];
107
- /** Exact system-prompt replacement supplied to the harness. */
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
+ */
108
202
  systemPrompt?: string;
203
+ /**
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.
213
+ */
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;
109
227
  /** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
110
228
  unsupported: Unsupported[];
111
229
  /**
@@ -157,6 +275,9 @@ export interface WorkspacePlanReceipt {
157
275
  /** Public launch identity only; templates retain opaque secret references. */
158
276
  flags: WorkspacePlanArgument[];
159
277
  systemPrompt?: string;
278
+ appendSystemPrompt?: string;
279
+ /** The launcher-bound intent, so a receipt says which control owed the work. */
280
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
160
281
  unsupported: Unsupported[];
161
282
  written: string[];
162
283
  }
@@ -165,6 +286,9 @@ export interface WorkspacePlanSpawnInputs {
165
286
  env: Record<string, string>;
166
287
  flags: string[];
167
288
  systemPrompt?: string;
289
+ appendSystemPrompt?: string;
290
+ /** Read it with {@link takeSystemPromptBinding}, which refuses a foreign control. */
291
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
168
292
  }
169
293
  /** Result of applying a public plan at the private executor boundary. */
170
294
  export interface WorkspacePlanApplication {
@@ -207,6 +331,7 @@ export interface AgentCandidateWorkspacePlan {
207
331
  env: Record<string, AgentCandidateConfigValue>;
208
332
  flags: AgentCandidateConfigValue[];
209
333
  systemPrompt?: AgentCandidateConfigValue;
334
+ appendSystemPrompt?: AgentCandidateConfigValue;
210
335
  unsupported: Unsupported[];
211
336
  }
212
337
  export interface AgentCandidateWorkspacePlanReceipt {
@@ -215,8 +340,21 @@ export interface AgentCandidateWorkspacePlanReceipt {
215
340
  env: Record<string, AgentCandidateConfigValue>;
216
341
  flags: AgentCandidateConfigValue[];
217
342
  systemPrompt?: AgentCandidateConfigValue;
343
+ appendSystemPrompt?: AgentCandidateConfigValue;
218
344
  unsupported: Unsupported[];
219
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;
220
358
  export interface MaterializeCandidateProfileOptions {
221
359
  /** Exact text fetched from pinned resource refs, keyed by declared SHA-256. */
222
360
  resolvedResources?: ReadonlyMap<Sha256Digest, string>;
@@ -277,6 +415,18 @@ export declare function normalizeSkillMd(name: string, raw: string): string;
277
415
  */
278
416
  export declare function materializeProfile(inputProfile: AgentProfile, harness: HarnessId, opts?: {
279
417
  skip?: SkippableDimension[];
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
+ */
429
+ binds?: readonly BindableLaunchControl[];
280
430
  }): WorkspacePlan;
281
431
  /**
282
432
  * Materialize a sealed candidate profile without weakening its process values.
@@ -339,3 +489,45 @@ export type OpenCodeMcpConfig = Record<string, {
339
489
  }>;
340
490
  /** Lower canonical MCP definitions to OpenCode's native startup configuration. */
341
491
  export declare function openCodeMcpConfig(mcp: Record<string, AgentProfileMcpServer>): OpenCodeMcpConfig;
492
+ export interface PrimeProfileSkill {
493
+ name: string;
494
+ content: string;
495
+ }
496
+ export interface PrimeProfileSubagent {
497
+ name: string;
498
+ description?: string;
499
+ prompt?: string;
500
+ metadata?: Record<string, unknown>;
501
+ }
502
+ /** Validated, materialization-ready view of the profile controls prime applies. */
503
+ export interface PrimeProfileControls {
504
+ systemPrompt?: string;
505
+ appendSystemPrompt?: string;
506
+ skills: PrimeProfileSkill[];
507
+ subagents: PrimeProfileSubagent[];
508
+ }
509
+ /**
510
+ * Validate and extract the profile controls the prime agent-dir lowering
511
+ * materializes. Throws on any control (or control facet) the fork has no
512
+ * verified read path for — partial acceptance would silently drop the rest.
513
+ */
514
+ export declare function readPrimeProfileControls(profile: AgentProfile | undefined): PrimeProfileControls;
515
+ /** Deterministic harness-state entry id for a profile subagent name. */
516
+ export declare function primeSubagentEntryId(name: string): string;
517
+ /**
518
+ * Materialize the validated controls into `agentDir` and return the extra
519
+ * spawn args that bind them. Runs before every spawn (same place models.json
520
+ * is materialized), so a continual agent dir is re-pinned to the current
521
+ * profile on each fresh prime process.
522
+ */
523
+ export declare function materializePrimeProfileControls(agentDir: string, controls: PrimeProfileControls, warn: (msg: string, meta?: Record<string, unknown>) => void): string[];
524
+ /**
525
+ * Upsert profile subagent specs into the fork's global continual-harness
526
+ * store and remove profile-sourced entries the current profile no longer
527
+ * declares. Agent-written entries (`source: "agent"`) are never touched.
528
+ *
529
+ * A pre-existing unparsable store degrades to empty exactly like the fork's
530
+ * own loader (refinement.ts:290-300 — "a corrupt state file must degrade to
531
+ * empty rather than throw"), with a warning so the reset is visible.
532
+ */
533
+ export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn: (msg: string, meta?: Record<string, unknown>) => void): void;