@tangle-network/agent-profile-materialize 0.11.0 → 0.13.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
@@ -22,6 +22,7 @@
22
22
  * cli-bridge and agent-dev-container — this is the single source those re-export.
23
23
  */
24
24
  export { modelIdsMatch, qualifyModelId } from "./model-id.js";
25
+ export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_FORK_INSTALL, PRIME_FORK_PIN, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PRIME_THINKING_LEVELS, PrimeHarnessStateError, PrimeProfileError, type PrimeProfileErrorKind, type PrimeThinkingLevel, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
25
26
  import type { AgentCandidateConfigValue, AgentCandidateProfile, AgentCandidateProfileActivation, AgentCandidateProfileApplication, AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, AgentProfile, AgentProfileConfigValue, AgentProfileMcpServer, AgentProfileSecretProvider, AgentProfileSecretRef, Sha256Digest } from "@tangle-network/agent-interface";
26
27
  export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
27
28
  /**
@@ -57,6 +58,25 @@ export interface PlanFile {
57
58
  * this on every file it emits.
58
59
  */
59
60
  source?: "user" | "generated";
61
+ /**
62
+ * Who owns the PATH — which decides whether an existing file there may be
63
+ * replaced.
64
+ *
65
+ * `"materializer"` = a path this package invented. No harness convention
66
+ * names it and no user authors it, so a file already there can only be this
67
+ * materializer's own output from an earlier application into the same
68
+ * workspace, and replacing it is how turn two of a resumed session gets the
69
+ * current profile instead of the previous one.
70
+ *
71
+ * `"shared"` (the default, and every other file) = a path the user or the
72
+ * harness also owns — `CLAUDE.md`, `AGENTS.md`, `opencode.json`,
73
+ * `.gemini/system.md` are all documented, user-authorable names. An existing
74
+ * file there may be the caller's own work, so it is never replaced.
75
+ *
76
+ * Optional because hand-constructed plans have no path provenance to report;
77
+ * absent reads as `"shared"`, the safe answer.
78
+ */
79
+ ownership?: "materializer" | "shared";
60
80
  /**
61
81
  * Secret leaves in a generated JSON document. `content` contains `null` at
62
82
  * each path until the private executor resolves the named reference. The
@@ -83,9 +103,47 @@ export type WorkspacePlanArgument = string | WorkspacePlanArgumentTemplate;
83
103
  /** Public bytes stay strings; secret references stay opaque until application. */
84
104
  export type WorkspacePlanConfigValue = string | AgentProfileSecretRef;
85
105
  export interface Unsupported {
86
- dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
106
+ dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "appendSystemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
87
107
  reason: string;
88
108
  }
109
+ /**
110
+ * The subset of {@link BINDABLE_LAUNCH_CONTROLS} the plan HANDS OVER rather than
111
+ * executes: controls a caller reaches only by composing the harness's own
112
+ * configuration, so no file, flag or env var the plan writes can touch them.
113
+ *
114
+ * - `opencode.agent-prompt` — opencode's `agent.<name>.prompt` config field,
115
+ * the one control that replaces its built-in provider prompt. It binds to a
116
+ * SINGLE agent, and the agent is chosen at launch. A caller that builds
117
+ * opencode's config itself and picks the agent there reads neither
118
+ * `plan.files` nor `plan.flags`, so for that one the plan names the control
119
+ * and hands over the text in {@link WorkspacePlan.systemPromptBinding}.
120
+ *
121
+ * Every other bindable control names a capability the plan still lowers itself
122
+ * once the caller has declared the plan's launch inputs really reach the spawn.
123
+ */
124
+ export declare const EXECUTOR_SYSTEM_PROMPT_BINDINGS: readonly ["opencode.agent-prompt"];
125
+ export type ExecutorSystemPromptBinding = (typeof EXECUTOR_SYSTEM_PROMPT_BINDINGS)[number];
126
+ export declare function isExecutorSystemPromptBinding(value: string): value is ExecutorSystemPromptBinding;
127
+ /**
128
+ * A system-prompt intent handed to the launcher that declared it owns the
129
+ * control, instead of being lowered into the workspace.
130
+ *
131
+ * Distinct from {@link WorkspacePlan.systemPrompt} — a launch ARGUMENT the
132
+ * executor hands to a harness flag — and from the file/flag/env lowerings the
133
+ * plan performs on its own. This names the exact control that must receive the
134
+ * text and which of the two intents it is, so the launcher cannot apply it to
135
+ * the wrong channel. Never written into an additive channel and never dropped:
136
+ * a caller that has not declared it owns `control` gets the plan's own lowering
137
+ * (or a refusal where the harness has no control at all) instead of this field.
138
+ */
139
+ export interface WorkspacePlanSystemPromptBinding {
140
+ /** Which of the two prompt intents this is — never inferred from presence. */
141
+ intent: "replace";
142
+ /** The exact launcher control that must receive `systemPrompt`. */
143
+ control: ExecutorSystemPromptBinding;
144
+ /** Exact text the control receives. */
145
+ systemPrompt: string;
146
+ }
89
147
  /**
90
148
  * The dimensions `materializeProfile`'s `skip` option honors.
91
149
  *
@@ -104,13 +162,23 @@ export declare function isSkippableDimension(value: string): value is SkippableD
104
162
  * system-prompt replacement (`agent.<name>.prompt`) binds to the ONE agent
105
163
  * whoever starts the process selects, which a plan-forwarding executor cannot
106
164
  * guarantee — so the plan refuses the intent (harnessSystemPromptIntents in
107
- * @tangle-network/agent-interface answers `replace: false` for opencode). An
108
- * executor that really applies the plan's launch flags to the spawn it owns
109
- * (selecting the generated primary agent via the plan's `--agent` flag) states
110
- * so with `binds: ["systemPrompt"]`, which turns that refusal into the
111
- * primary-agent lowering the executor must then apply.
165
+ * @tangle-network/agent-interface answers `replace: false` for opencode).
166
+ *
167
+ * The two declarations describe the two executor shapes that CAN reach it, and
168
+ * each gets the lowering it can actually apply:
169
+ *
170
+ * - `systemPrompt` — the executor applies the plan's launch flags to the
171
+ * spawn it owns, so the plan writes the generated `mode: primary` agent and
172
+ * selects it with `--agent`.
173
+ * - `opencode.agent-prompt` — the executor composes opencode's own config and
174
+ * picks the agent there, reading neither plan files nor plan flags, so the
175
+ * plan hands the text over in {@link WorkspacePlan.systemPromptBinding}
176
+ * instead of writing a workspace agent that would never be selected.
177
+ *
178
+ * They are mutually exclusive claims about the same caller: declaring both is a
179
+ * caller bug, not a broader capability.
112
180
  */
113
- export declare const BINDABLE_LAUNCH_CONTROLS: readonly ["systemPrompt"];
181
+ export declare const BINDABLE_LAUNCH_CONTROLS: readonly ["systemPrompt", "opencode.agent-prompt"];
114
182
  export type BindableLaunchControl = (typeof BINDABLE_LAUNCH_CONTROLS)[number];
115
183
  /** The plan a caller writes into the run cwd, then applies to the spawn. */
116
184
  export interface WorkspacePlan {
@@ -121,18 +189,42 @@ export interface WorkspacePlan {
121
189
  env: Record<string, WorkspacePlanConfigValue>;
122
190
  /** Extra CLI flags to pass (e.g. kimi --mcp-config-file <path>). */
123
191
  flags: WorkspacePlanArgument[];
124
- /** Exact system-prompt replacement supplied to the harness. */
192
+ /**
193
+ * Exact text that REPLACES the harness's own system prompt, for harnesses
194
+ * whose replacement control takes it as a launch argument the executor
195
+ * supplies (claude-code `--system-prompt`, pi `--system-prompt`).
196
+ *
197
+ * Absent does NOT mean "no replacement was requested": a harness whose
198
+ * control reads a file (codex, gemini) carries the replacement in
199
+ * {@link WorkspacePlan.files} plus a flag or env var instead. Present means
200
+ * the executor MUST bind it to that harness's replacement control — never to
201
+ * an additive one, which would leave the built-in prompt in force.
202
+ */
125
203
  systemPrompt?: string;
126
204
  /**
127
- * Additive system-prompt text: the harness's own prompt stays in force and
128
- * this text is added after it. The OPPOSITE intent of {@link systemPrompt} on
129
- * the same channel — an executor must never bind one to the other's control
130
- * (an addition on the replacement flag deletes the prompt the caller asked to
131
- * keep; a replacement on the additive flag leaves the harness's prompt
132
- * running). Set only for harnesses whose own control executes the addition
133
- * (harnessSystemPromptIntents in @tangle-network/agent-interface).
205
+ * Exact text ADDED to the harness's own system prompt, which stays intact.
206
+ *
207
+ * Separate from {@link WorkspacePlan.systemPrompt} because the two are
208
+ * different intents against the same channel, and every harness that has one
209
+ * control may lack the other. An executor binds this ONLY to an additive
210
+ * control (claude-code `--append-system-prompt`, pi
211
+ * `--append-system-prompt`); there is no harness for which folding it into
212
+ * the replacement is correct, because that would delete the built-in prompt
213
+ * the caller asked to keep.
134
214
  */
135
215
  appendSystemPrompt?: string;
216
+ /**
217
+ * A prompt intent no workspace file, flag, or env var can execute, addressed
218
+ * to the launcher control that can — see
219
+ * {@link WorkspacePlanSystemPromptBinding}.
220
+ *
221
+ * Present ONLY when the caller declared it owns the named control, so a plan
222
+ * built by a caller that forwards it to someone else's launcher never carries
223
+ * one; that caller gets the intent in {@link WorkspacePlan.unsupported}
224
+ * instead. Whoever reads it must bind it or throw — {@link
225
+ * takeSystemPromptBinding} does both.
226
+ */
227
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
136
228
  /** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
137
229
  unsupported: Unsupported[];
138
230
  /**
@@ -185,6 +277,8 @@ export interface WorkspacePlanReceipt {
185
277
  flags: WorkspacePlanArgument[];
186
278
  systemPrompt?: string;
187
279
  appendSystemPrompt?: string;
280
+ /** The launcher-bound intent, so a receipt says which control owed the work. */
281
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
188
282
  unsupported: Unsupported[];
189
283
  written: string[];
190
284
  }
@@ -194,6 +288,8 @@ export interface WorkspacePlanSpawnInputs {
194
288
  flags: string[];
195
289
  systemPrompt?: string;
196
290
  appendSystemPrompt?: string;
291
+ /** Read it with {@link takeSystemPromptBinding}, which refuses a foreign control. */
292
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
197
293
  }
198
294
  /** Result of applying a public plan at the private executor boundary. */
199
295
  export interface WorkspacePlanApplication {
@@ -248,6 +344,18 @@ export interface AgentCandidateWorkspacePlanReceipt {
248
344
  appendSystemPrompt?: AgentCandidateConfigValue;
249
345
  unsupported: Unsupported[];
250
346
  }
347
+ /**
348
+ * The replacement text this launcher must bind to `control`, or `undefined`
349
+ * when the plan carries no launcher-bound intent.
350
+ *
351
+ * Throws for a binding addressed to a DIFFERENT control: the reader cannot
352
+ * execute it, and answering `undefined` would start the agent with the
353
+ * harness's own prompt still in force while nothing reported a problem — the
354
+ * silent downgrade the binding exists to prevent. This is the seam where an
355
+ * executor that cannot bind an intent says so, by name, instead of running a
356
+ * profile whose prompt never took effect.
357
+ */
358
+ export declare function takeSystemPromptBinding(spawn: Pick<WorkspacePlanSpawnInputs, "systemPromptBinding">, control: ExecutorSystemPromptBinding): string | undefined;
251
359
  export interface MaterializeCandidateProfileOptions {
252
360
  /** Exact text fetched from pinned resource refs, keyed by declared SHA-256. */
253
361
  resolvedResources?: ReadonlyMap<Sha256Digest, string>;
@@ -308,7 +416,17 @@ export declare function normalizeSkillMd(name: string, raw: string): string;
308
416
  */
309
417
  export declare function materializeProfile(inputProfile: AgentProfile, harness: HarnessId, opts?: {
310
418
  skip?: SkippableDimension[];
311
- /** Launcher controls the calling executor owns — see {@link BINDABLE_LAUNCH_CONTROLS}. */
419
+ /**
420
+ * Launcher controls THIS caller owns and will bind itself — see
421
+ * {@link BINDABLE_LAUNCH_CONTROLS}. Each names a control no workspace file,
422
+ * flag or env var reaches on its own, so declaring one turns a plan-time
423
+ * refusal into the lowering (or the hand-over) that control can execute.
424
+ *
425
+ * A promise, not a hint: declare a control only from the code that starts
426
+ * the harness and writes that control. A caller that hands the plan to
427
+ * someone else's launcher declares nothing and keeps the refusal, which is
428
+ * what it can actually deliver.
429
+ */
312
430
  binds?: readonly BindableLaunchControl[];
313
431
  }): WorkspacePlan;
314
432
  /**
@@ -386,31 +504,105 @@ export interface PrimeProfileSubagent {
386
504
  export interface PrimeProfileControls {
387
505
  systemPrompt?: string;
388
506
  appendSystemPrompt?: string;
507
+ /** Composed caller instructions, lowered to the agent dir's AGENTS.md. */
508
+ instructions?: string;
389
509
  skills: PrimeProfileSkill[];
390
510
  subagents: PrimeProfileSubagent[];
391
511
  }
512
+ /** True when the controls materialize anything at all. */
513
+ export declare function hasPrimeProfileMaterial(controls: PrimeProfileControls): boolean;
514
+ /**
515
+ * True when the controls must WRITE INTO the agent dir itself.
516
+ *
517
+ * Prompt files and skill dirs are named by a flag, so they can live anywhere
518
+ * the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
519
+ * path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
520
+ * arbitrary dir, :437-442). The context file and the continual-harness store
521
+ * are located BY the agent dir and have no flag, so they cannot move.
522
+ *
523
+ * An executor pointed at an agent dir it does not own (an operator's shared
524
+ * dir) uses this to refuse exactly the controls it would have to write there,
525
+ * while still binding the ones it can materialize elsewhere.
526
+ */
527
+ export declare function primeProfileNeedsAgentDir(controls: PrimeProfileControls): boolean;
392
528
  /**
393
529
  * Validate and extract the profile controls the prime agent-dir lowering
394
- * materializes. Throws on any control (or control facet) the fork has no
395
- * verified read path for — partial acceptance would silently drop the rest.
530
+ * materializes. Throws {@link PrimeProfileError} on any control (or control
531
+ * facet) the fork has no verified read path for — partial acceptance would
532
+ * silently drop the rest.
396
533
  */
397
534
  export declare function readPrimeProfileControls(profile: AgentProfile | undefined): PrimeProfileControls;
535
+ /**
536
+ * Refuse a run whose selected model disagrees with the profile's own.
537
+ *
538
+ * One authority per run: a profile names the model it was written and scored
539
+ * against, so running it on a different one silently scores a different agent.
540
+ * Both sides go through {@link modelIdsMatch}, so the qualification level a
541
+ * caller happened to use is not a conflict.
542
+ *
543
+ * `provider` is the executor's PROVIDER SELECTION, and is compared only when
544
+ * the executor's provider is part of model identity. An executor whose
545
+ * provider key is merely a namespace inside the agent dir passes it as
546
+ * undefined and gets the model-name comparison alone.
547
+ */
548
+ export declare function assertPrimeModelAgreement(profile: AgentProfile | undefined, selected: {
549
+ provider?: string;
550
+ model: string;
551
+ }): void;
398
552
  /** Deterministic harness-state entry id for a profile subagent name. */
399
553
  export declare function primeSubagentEntryId(name: string): string;
400
554
  /**
401
- * Materialize the validated controls into `agentDir` and return the extra
402
- * spawn args that bind them. Runs before every spawn (same place models.json
403
- * is materialized), so a continual agent dir is re-pinned to the current
404
- * profile on each fresh prime process.
555
+ * Write one agent-dir file atomically (temp + rename).
556
+ *
557
+ * Every file this lowering writes lands in a directory a LIVE agent may be
558
+ * reading: prime reloads models.json at model-resolution time and re-reads the
559
+ * harness store on every system-prompt build, so a partial write is observable.
560
+ * `mode` is applied to the temp file before the rename, so the final file is
561
+ * never briefly world-readable.
562
+ */
563
+ export declare function writePrimeAgentFile(path: string, data: string, mode?: number): void;
564
+ export interface PrimeMaterializeOptions {
565
+ /**
566
+ * Where the FLAG-NAMED files go — the prompt files and the skills root.
567
+ * Defaults to `agentDir`. An executor that does not own the agent dir points
568
+ * this at a per-run directory of its own, so binding a profile's prompts and
569
+ * skills writes nothing into a dir it must not modify. The context file and
570
+ * the harness store have no flag and always land in `agentDir`; refuse those
571
+ * with {@link primeProfileNeedsAgentDir} instead.
572
+ */
573
+ flagFileRoot?: string;
574
+ /**
575
+ * Reports state the executor should see but that does not fail the run
576
+ * (currently: adopting entries a pre-shared executor left behind).
577
+ */
578
+ warn?: (msg: string, meta?: Record<string, unknown>) => void;
579
+ }
580
+ /**
581
+ * Materialize the validated controls and return the extra spawn args that bind
582
+ * them. Runs before every spawn (same place models.json is materialized), so a
583
+ * continual agent dir is re-pinned to the current profile on each fresh prime
584
+ * process.
585
+ */
586
+ export declare function materializePrimeProfileControls(agentDir: string, controls: PrimeProfileControls, options?: PrimeMaterializeOptions): string[];
587
+ /**
588
+ * Remove the files a PREVIOUS materialization of this lowering wrote, so a
589
+ * profile that shrank cannot leave a stale instruction file or skill behind for
590
+ * the next run against the same dirs.
591
+ *
592
+ * Only the paths this lowering NAMES are removed. There is deliberately no
593
+ * manifest: a manifest would live where the agent can write it, and reading a
594
+ * delete list out of agent-writable state turns pruning into an arbitrary-path
595
+ * delete.
405
596
  */
406
- export declare function materializePrimeProfileControls(agentDir: string, controls: PrimeProfileControls, warn: (msg: string, meta?: Record<string, unknown>) => void): string[];
597
+ export declare function prunePrimeProfileMaterial(agentDir: string, options?: Pick<PrimeMaterializeOptions, "flagFileRoot">): void;
407
598
  /**
408
599
  * Upsert profile subagent specs into the fork's global continual-harness
409
600
  * store and remove profile-sourced entries the current profile no longer
410
- * declares. Agent-written entries (`source: "agent"`) are never touched.
601
+ * declares. Agent-written entries (`source: "agent"`) — and every top-level
602
+ * key this lowering does not manage — are preserved untouched.
411
603
  *
412
- * A pre-existing unparsable store degrades to empty exactly like the fork's
413
- * own loader (refinement.ts:290-300 — "a corrupt state file must degrade to
414
- * empty rather than throw"), with a warning so the reset is visible.
604
+ * A store that exists but no longer parses raises
605
+ * {@link PrimeHarnessStateError}: the file is the agent's own self-modification
606
+ * state, and overwriting it destroys what the run was supposed to observe.
415
607
  */
416
- export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn: (msg: string, meta?: Record<string, unknown>) => void): void;
608
+ export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn?: (msg: string, meta?: Record<string, unknown>) => void): void;