@tangle-network/agent-profile-materialize 0.12.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
  /**
@@ -503,31 +504,105 @@ export interface PrimeProfileSubagent {
503
504
  export interface PrimeProfileControls {
504
505
  systemPrompt?: string;
505
506
  appendSystemPrompt?: string;
507
+ /** Composed caller instructions, lowered to the agent dir's AGENTS.md. */
508
+ instructions?: string;
506
509
  skills: PrimeProfileSkill[];
507
510
  subagents: PrimeProfileSubagent[];
508
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;
509
528
  /**
510
529
  * 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.
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.
513
533
  */
514
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;
515
552
  /** Deterministic harness-state entry id for a profile subagent name. */
516
553
  export declare function primeSubagentEntryId(name: string): string;
517
554
  /**
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.
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.
522
596
  */
523
- 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;
524
598
  /**
525
599
  * Upsert profile subagent specs into the fork's global continual-harness
526
600
  * store and remove profile-sourced entries the current profile no longer
527
- * 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.
528
603
  *
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.
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.
532
607
  */
533
- 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;
package/dist/index.js CHANGED
@@ -22,10 +22,13 @@
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, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
25
26
  import { createHash, randomUUID } from "node:crypto";
26
- import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
27
+ import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
27
28
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32, } from "node:path";
28
29
  import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, agentProfileSecretRefSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
30
+ import { modelIdsMatch, qualifyModelId } from "./model-id.js";
31
+ import { PRIME_MCP_UNSUPPORTED_REASON, PrimeHarnessStateError, rejectPrimeControl, rejectPrimeDeclaration, } from "./prime-fork.js";
29
32
  export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
30
33
  async get() {
31
34
  return undefined;
@@ -3173,12 +3176,29 @@ function materializeCommand(h, name, body, add, unsupported) {
3173
3176
  // and the addition appends after it (system-prompt.ts:61,109-110). Values
3174
3177
  // are materialized as FILES under the agent dir because the flag value is
3175
3178
  // read as a file when the path exists (resource-loader.ts:41-56), which
3176
- // keeps literal-vs-path resolution deterministic.
3179
+ // keeps literal-vs-path resolution deterministic AND removes the argv size
3180
+ // ceiling an inline flag value would run into (Linux MAX_ARG_STRLEN).
3181
+ //
3182
+ // - `prompt.instructions` / `resources.instructions` → `<agentDir>/AGENTS.md`.
3183
+ // The agent dir IS an instruction surface: `loadProjectContextFiles`
3184
+ // (resource-loader.ts:76) reads the agent dir's own AGENTS.md/CLAUDE.md as
3185
+ // the GLOBAL context file (:86 `loadContextFileFromDir(resolvedAgentDir)`,
3186
+ // candidates at :59) BEFORE walking the cwd ancestors (:94-112), and the
3187
+ // result is loaded unless `--no-context-files` (:476). Writing it here
3188
+ // touches no caller workspace content, which is what the agent-dir lowering
3189
+ // exists to guarantee. (`materializeProfile(profile, "prime")` lowers the
3190
+ // same intent to the cwd AGENTS.md for callers that own the workspace
3191
+ // instead of the agent dir.)
3177
3192
  //
3178
3193
  // - `resources.skills` (inline refs) → `--skill <agentDir>/profile/skills`
3179
3194
  // (cli/args.ts:207-209 → main.ts:652 → agent-session-config.ts:67 →
3180
- // additionalSkillPaths, resource-loader.ts:437-442). The loader recurses
3181
- // into `<name>/SKILL.md` subdirs (skills.ts:328-372) and loaded skills are
3195
+ // additionalSkillPaths, resource-loader.ts:437-442). An explicit flag rather
3196
+ // than the agent dir's auto-discovered `skills/` tree (package-manager.ts:884
3197
+ // global base, :2171 skills dir, :2240-2246 discovery), which also loads but
3198
+ // goes through the fork's global-vs-project dedupe (package-manager.ts:880,
3199
+ // "project scope wins over global") — so an unrelated project-scope skill of
3200
+ // the same name silently shadows the profile's. The loader recurses into
3201
+ // `<name>/SKILL.md` subdirs (skills.ts:328-372) and loaded skills are
3182
3202
  // rendered into the system prompt (agent-session.ts:4272-4291,
3183
3203
  // system-prompt.ts:87-89). The fork silently DROPS a SKILL.md without a
3184
3204
  // frontmatter description (skills.ts:416-419), so content is normalized
@@ -3192,25 +3212,17 @@ function materializeCommand(h, name, body, add, unsupported) {
3192
3212
  // reaches the same store through the host-set RLM_GLOBAL_HARNESS_STATE_DIR
3193
3213
  // (agent-session.ts:8805).
3194
3214
  //
3195
- // Kept REJECTED with the fork evidence:
3196
- //
3197
- // - `mcp`: the fork's MCP config surface (`mcpServers` in
3198
- // `<agentDir>/settings.json`, settings-manager.ts:98-120,219) IS read at
3199
- // startup, but a user-declared server never becomes model-visible: the host
3200
- // McpManager only registers OAuth providers and serves mcp.config/refresh
3201
- // host-requests (core/mcp/mcp-manager.ts:62-124,156-181), skips stdio
3202
- // servers entirely (mcp-manager.ts:73), and nothing generates a skill or
3203
- // prompt section for it — model-visible MCP is exactly the built-in
3204
- // linear/notion catalog skills (packages/coding-agent/skills/…). Wiring
3205
- // `profile.mcp` into settings.json would materialize config the model can
3206
- // never discover: a silently dropped control.
3215
+ // Kept REJECTED with the fork evidence — see {@link PRIME_MCP_UNSUPPORTED_REASON}
3216
+ // for `mcp`/`connections`, and the per-control reasons in
3217
+ // {@link readPrimeProfileControls} for permissions, tools, hooks, modes,
3218
+ // confidential, and the resource kinds with no agent-dir read path.
3207
3219
  //
3208
- // - `prompt.instructions`: the fork's caller-instruction surface is the
3209
- // workspace AGENTS.md/CLAUDE.md context file (resource-loader.ts:58-66),
3210
- // i.e. caller workspace content this agent-dir lowering must not write
3211
- // into. (`materializeProfile(profile, "prime")` lowers instructions to the
3212
- // cwd AGENTS.md instead, which the fork walks from cwd:
3213
- // resource-loader.ts:94-109.)
3220
+ // The store is AGENT-OWNED: it is written by the agent mid-session and rides
3221
+ // every later system prompt. A store that exists but no longer parses
3222
+ // therefore fails the turn ({@link PrimeHarnessStateError}) rather than being
3223
+ // reseeded from empty — the fork's own loader degrades to empty in-process
3224
+ // (refinement.ts:290-300), but an executor that OVERWRITES the file makes that
3225
+ // degradation permanent and destroys the evidence of what happened.
3214
3226
  /** Skill name rules enforced by the fork's Agent Skills loader (skills.ts:122-147). */
3215
3227
  const PRIME_SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3216
3228
  const PRIME_MAX_SKILL_NAME_LENGTH = 64;
@@ -3218,6 +3230,8 @@ const PRIME_PROFILE_DIR_NAME = "profile";
3218
3230
  const PRIME_SKILLS_DIR_NAME = "skills";
3219
3231
  const PRIME_SYSTEM_PROMPT_FILE = "system-prompt.md";
3220
3232
  const PRIME_APPEND_SYSTEM_PROMPT_FILE = "append-system-prompt.md";
3233
+ /** Agent-dir global context file the fork reads (resource-loader.ts:59,86). */
3234
+ const PRIME_CONTEXT_FILE = "AGENTS.md";
3221
3235
  /** Fork continual-harness store location under the agent dir (refinement.ts:269-279). */
3222
3236
  const PRIME_HARNESS_DIR_NAME = "harness";
3223
3237
  const PRIME_HARNESS_STATE_FILE = "harness_state.json";
@@ -3228,22 +3242,101 @@ const PRIME_HARNESS_KINDS = ["prompt", "memory", "skill", "subagent"];
3228
3242
  * touched.
3229
3243
  */
3230
3244
  const PRIME_PROFILE_ENTRY_SOURCE = "profile";
3231
- function rejectPrimeControl(control, reason) {
3232
- throw new Error(`prime does not support AgentProfile ${control}: ${reason}`);
3245
+ /**
3246
+ * Provenance markers written by executors that predate this shared lowering.
3247
+ *
3248
+ * Pruning keys on the source tag, so an executor that switches to the shared
3249
+ * implementation would otherwise leave its own earlier entries behind forever:
3250
+ * they match no current profile and no current source, and the model's
3251
+ * spawnable roster silently accumulates duplicates of every subagent. They are
3252
+ * adopted (pruned or replaced) on the first shared run instead.
3253
+ */
3254
+ const PRIME_LEGACY_PROFILE_ENTRY_SOURCES = [
3255
+ "cli-bridge-profile",
3256
+ ];
3257
+ /** True when the controls materialize anything at all. */
3258
+ export function hasPrimeProfileMaterial(controls) {
3259
+ return (controls.systemPrompt !== undefined ||
3260
+ controls.appendSystemPrompt !== undefined ||
3261
+ primeProfileNeedsAgentDir(controls) ||
3262
+ controls.skills.length > 0);
3263
+ }
3264
+ /**
3265
+ * True when the controls must WRITE INTO the agent dir itself.
3266
+ *
3267
+ * Prompt files and skill dirs are named by a flag, so they can live anywhere
3268
+ * the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
3269
+ * path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
3270
+ * arbitrary dir, :437-442). The context file and the continual-harness store
3271
+ * are located BY the agent dir and have no flag, so they cannot move.
3272
+ *
3273
+ * An executor pointed at an agent dir it does not own (an operator's shared
3274
+ * dir) uses this to refuse exactly the controls it would have to write there,
3275
+ * while still binding the ones it can materialize elsewhere.
3276
+ */
3277
+ export function primeProfileNeedsAgentDir(controls) {
3278
+ return controls.instructions !== undefined || controls.subagents.length > 0;
3279
+ }
3280
+ /**
3281
+ * Read one instruction block, which the profile may carry inline or as a
3282
+ * resource ref. Only inline refs are accepted: resolving a GitHub-backed ref
3283
+ * fetches remote content, which is `resolveAgentProfileResources`' job and
3284
+ * must happen before a profile reaches an executor.
3285
+ */
3286
+ function primeInstructionBlock(value, control) {
3287
+ if (typeof value === "string")
3288
+ return value;
3289
+ if (value.kind === "inline")
3290
+ return value.content;
3291
+ rejectPrimeControl(control, "only inline resource refs are accepted — resolve GitHub-backed refs " +
3292
+ "(resolveAgentProfileResources) before handing the profile to this lowering.");
3233
3293
  }
3234
3294
  /**
3235
3295
  * Validate and extract the profile controls the prime agent-dir lowering
3236
- * materializes. Throws on any control (or control facet) the fork has no
3237
- * verified read path for — partial acceptance would silently drop the rest.
3296
+ * materializes. Throws {@link PrimeProfileError} on any control (or control
3297
+ * facet) the fork has no verified read path for — partial acceptance would
3298
+ * silently drop the rest.
3238
3299
  */
3239
3300
  export function readPrimeProfileControls(profile) {
3240
3301
  const controls = { skills: [], subagents: [] };
3241
3302
  if (!profile)
3242
3303
  return controls;
3243
3304
  if (profile.harness !== undefined && profile.harness !== "prime") {
3244
- throw new Error(`prime cannot run a profile pinned to harness "${profile.harness}" — ` +
3305
+ rejectPrimeDeclaration("harness", `prime cannot run a profile pinned to harness "${profile.harness}" — ` +
3245
3306
  'only "prime" (or an unset harness preference) is accepted.');
3246
3307
  }
3308
+ // An entry with `enabled: false` is OFF, not requested. A fully disabled map
3309
+ // asks for nothing, so accepting it drops no control — which is the only
3310
+ // thing this refusal exists to prevent.
3311
+ const requestedMcp = Object.keys(activeMcpServers(profile.mcp));
3312
+ if (requestedMcp.length > 0) {
3313
+ rejectPrimeControl(`mcp: ${requestedMcp.join(", ")}`, PRIME_MCP_UNSUPPORTED_REASON);
3314
+ }
3315
+ if (profile.connections !== undefined) {
3316
+ rejectPrimeControl("connections", `a hub connection grant resolves to MCP tools, and ${PRIME_MCP_UNSUPPORTED_REASON}`);
3317
+ }
3318
+ if (profile.permissions !== undefined) {
3319
+ rejectPrimeControl("permissions", "prime-agent exposes no permission control at all (full flag scan of fork " +
3320
+ "cli/args.ts:93-295; native tools run unconditionally), so a permission map " +
3321
+ "would be accepted and never enforced.");
3322
+ }
3323
+ if (profile.tools !== undefined) {
3324
+ rejectPrimeControl("tools", "the only per-run tool control is `--tools` over the single builtin `ipython` " +
3325
+ "(fork cli/args.ts:63-64,156-167); bash/edit and the rest are extension-provided " +
3326
+ "and not addressable per run, so a harness-neutral tool map cannot bind to it.");
3327
+ }
3328
+ if (profile.hooks !== undefined) {
3329
+ rejectPrimeControl("hooks", "prime-agent has no hook mechanism (fork cli/args.ts flag scan; extensions are " +
3330
+ "host-trusted JS modules loaded into the agent process, not caller command hooks).");
3331
+ }
3332
+ if (profile.modes !== undefined) {
3333
+ rejectPrimeControl("modes", "prime-agent has no mode concept (fork cli/args.ts flag scan).");
3334
+ }
3335
+ if (profile.confidential !== undefined) {
3336
+ rejectPrimeControl("confidential", "confidential execution is a property of the executor's isolation boundary, not " +
3337
+ "of the agent dir; this lowering cannot establish it.");
3338
+ }
3339
+ const instructionBlocks = [];
3247
3340
  if (profile.prompt) {
3248
3341
  const { systemPrompt, appendSystemPrompt, instructions } = profile.prompt;
3249
3342
  const knownPromptFields = new Set([
@@ -3257,15 +3350,9 @@ export function readPrimeProfileControls(profile) {
3257
3350
  if (unknown.length > 0) {
3258
3351
  rejectPrimeControl(`prompt fields: ${unknown.join(", ")}`, "no verified prime-agent read path exists for them.");
3259
3352
  }
3260
- if (instructions !== undefined) {
3261
- rejectPrimeControl("prompt.instructions", "prime-agent's caller-instruction surface is the workspace " +
3262
- "AGENTS.md context file (fork resource-loader.ts:58-66), which is " +
3263
- "caller workspace content this adapter must not write into. Use " +
3264
- "prompt.appendSystemPrompt instead.");
3265
- }
3266
3353
  if (systemPrompt !== undefined) {
3267
3354
  if (typeof systemPrompt !== "string" || systemPrompt.trim() === "") {
3268
- rejectPrimeControl("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
3355
+ rejectPrimeDeclaration("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
3269
3356
  "install nothing.");
3270
3357
  }
3271
3358
  controls.systemPrompt = systemPrompt;
@@ -3273,52 +3360,69 @@ export function readPrimeProfileControls(profile) {
3273
3360
  if (appendSystemPrompt !== undefined) {
3274
3361
  if (typeof appendSystemPrompt !== "string" ||
3275
3362
  appendSystemPrompt.trim() === "") {
3276
- rejectPrimeControl("prompt.appendSystemPrompt", "must be non-empty text.");
3363
+ rejectPrimeDeclaration("prompt.appendSystemPrompt", "must be non-empty text.");
3277
3364
  }
3278
3365
  controls.appendSystemPrompt = appendSystemPrompt;
3279
3366
  }
3367
+ for (const block of instructions ?? []) {
3368
+ if (typeof block === "string" && block.trim() !== "")
3369
+ instructionBlocks.push(block);
3370
+ }
3280
3371
  }
3281
3372
  if (profile.resources) {
3282
- const { skills, failOnError, ...unsupported } = profile.resources;
3373
+ const { skills, instructions, failOnError, ...unsupported } = profile.resources;
3283
3374
  const rejectedKeys = Object.entries(unsupported)
3284
3375
  .filter(([, value]) => value !== undefined)
3285
3376
  .map(([key]) => key);
3286
3377
  if (rejectedKeys.length > 0) {
3287
- rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills has a verified prime-agent read path " +
3288
- "(--skill dirs, fork resource-loader.ts:437-442).");
3378
+ rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills (--skill dirs, fork resource-loader.ts:437-442) and " +
3379
+ "resources.instructions (agent-dir AGENTS.md, resource-loader.ts:86) have a " +
3380
+ "verified prime-agent read path. Generic workspace files, tool files, " +
3381
+ "subagent-definition files and prompt templates either have no headless read " +
3382
+ "path or would run caller-authored code inside the agent process.");
3289
3383
  }
3290
3384
  if (failOnError === false) {
3291
- rejectPrimeControl("resources.failOnError: false", "this adapter always fails loud on a resource it cannot " +
3385
+ rejectPrimeControl("resources.failOnError: false", "this lowering always fails loud on a resource it cannot " +
3292
3386
  "materialize; lenient materialization is not implemented.");
3293
3387
  }
3388
+ if (instructions !== undefined) {
3389
+ const block = primeInstructionBlock(instructions, "resources.instructions");
3390
+ if (block.trim() !== "")
3391
+ instructionBlocks.push(block);
3392
+ }
3294
3393
  const seen = new Set();
3295
- for (const ref of skills ?? []) {
3394
+ for (const [index, ref] of (skills ?? []).entries()) {
3296
3395
  if (ref.kind !== "inline") {
3297
- rejectPrimeControl(`resources.skills ref "${"path" in ref ? ref.path : ""}"`, "only inline skill refs are accepted — resolve GitHub refs " +
3396
+ rejectPrimeControl(`resources.skills[${index}]`, "only inline skill refs are accepted — resolve GitHub-backed refs " +
3298
3397
  "(resolveAgentProfileResources) before handing the profile to " +
3299
- "this adapter.");
3398
+ "this lowering.");
3300
3399
  }
3301
3400
  const name = ref.name;
3302
3401
  if (!PRIME_SKILL_NAME_RE.test(name) ||
3303
3402
  name.length > PRIME_MAX_SKILL_NAME_LENGTH) {
3304
- rejectPrimeControl(`resources.skills name "${name}"`, "prime-agent skill names must be lowercase a-z0-9 with single " +
3305
- `hyphens, at most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
3403
+ rejectPrimeDeclaration(`resources.skills[${index}]`, `name ${JSON.stringify(name)} is not a valid prime skill name — it becomes the ` +
3404
+ "skill directory name, so it must be lowercase a-z0-9 with single hyphens, at " +
3405
+ `most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
3306
3406
  }
3307
3407
  if (seen.has(name)) {
3308
- rejectPrimeControl(`resources.skills name "${name}"`, "duplicate skill names collide in the fork's skill registry.");
3408
+ rejectPrimeDeclaration("resources.skills", `declares ${JSON.stringify(name)} twice; duplicate skill names collide in the ` +
3409
+ "fork's skill registry, which keeps only the first and warns.");
3309
3410
  }
3310
3411
  seen.add(name);
3311
3412
  if (ref.content.trim() === "") {
3312
- rejectPrimeControl(`resources.skills "${name}"`, "skill content is empty.");
3413
+ rejectPrimeDeclaration(`resources.skills[${index}]`, `skill ${JSON.stringify(name)} declares no content.`);
3313
3414
  }
3314
3415
  controls.skills.push({ name, content: ref.content });
3315
3416
  }
3316
3417
  }
3418
+ if (instructionBlocks.length > 0) {
3419
+ controls.instructions = instructionBlocks.join("\n\n");
3420
+ }
3317
3421
  if (profile.subagents) {
3318
3422
  const usedIds = new Map();
3319
3423
  for (const [name, spec] of Object.entries(profile.subagents)) {
3320
3424
  if (name.trim() === "") {
3321
- rejectPrimeControl("subagents", "a subagent name must be non-empty.");
3425
+ rejectPrimeDeclaration("subagents", "a subagent name must be non-empty.");
3322
3426
  }
3323
3427
  const inexpressible = [
3324
3428
  ["model", spec.model],
@@ -3329,19 +3433,20 @@ export function readPrimeProfileControls(profile) {
3329
3433
  .filter(([, value]) => value !== undefined)
3330
3434
  .map(([key]) => key);
3331
3435
  if (inexpressible.length > 0) {
3332
- rejectPrimeControl(`subagents["${name}"] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
3436
+ rejectPrimeControl(`subagents[${JSON.stringify(name)}] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
3333
3437
  "guidance for `await rlm('<task>')` (fork refinement.ts:483-496) " +
3334
3438
  "and pins no model, tool set, permission, or step budget on the " +
3335
3439
  "spawned child.");
3336
3440
  }
3337
3441
  if (!spec.description?.trim() && !spec.prompt?.trim()) {
3338
- rejectPrimeControl(`subagents["${name}"]`, "a spec needs a description and/or prompt — an empty entry gives " +
3339
- "the model an empty roster line.");
3442
+ rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, "declares no behavior — a spec needs a description and/or prompt, since an " +
3443
+ "empty entry gives the model an empty roster line.");
3340
3444
  }
3341
3445
  const id = primeSubagentEntryId(name);
3342
3446
  const clash = usedIds.get(id);
3343
3447
  if (clash !== undefined) {
3344
- rejectPrimeControl(`subagents["${name}"]`, `its id "${id}" collides with subagent "${clash}".`);
3448
+ rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, `collides with subagent ${JSON.stringify(clash)} on harness-state id ` +
3449
+ `${JSON.stringify(id)}.`);
3345
3450
  }
3346
3451
  usedIds.set(id, name);
3347
3452
  controls.subagents.push({
@@ -3354,6 +3459,39 @@ export function readPrimeProfileControls(profile) {
3354
3459
  }
3355
3460
  return controls;
3356
3461
  }
3462
+ /**
3463
+ * Refuse a run whose selected model disagrees with the profile's own.
3464
+ *
3465
+ * One authority per run: a profile names the model it was written and scored
3466
+ * against, so running it on a different one silently scores a different agent.
3467
+ * Both sides go through {@link modelIdsMatch}, so the qualification level a
3468
+ * caller happened to use is not a conflict.
3469
+ *
3470
+ * `provider` is the executor's PROVIDER SELECTION, and is compared only when
3471
+ * the executor's provider is part of model identity. An executor whose
3472
+ * provider key is merely a namespace inside the agent dir passes it as
3473
+ * undefined and gets the model-name comparison alone.
3474
+ */
3475
+ export function assertPrimeModelAgreement(profile, selected) {
3476
+ const declared = profile?.model?.default;
3477
+ const declaredProvider = profile?.model?.provider;
3478
+ if (declared !== undefined) {
3479
+ const selectedId = qualifyModelId(selected.provider, selected.model);
3480
+ const declaredId = qualifyModelId(declaredProvider, declared);
3481
+ if (!modelIdsMatch(selectedId, declaredId, selected.provider)) {
3482
+ rejectPrimeDeclaration("model.default", `the run selects ${JSON.stringify(selectedId)} but the profile names ` +
3483
+ `${JSON.stringify(declaredId)}; a profile must not be scored against a model it ` +
3484
+ "does not name.");
3485
+ }
3486
+ return;
3487
+ }
3488
+ if (selected.provider !== undefined &&
3489
+ declaredProvider !== undefined &&
3490
+ declaredProvider !== selected.provider) {
3491
+ rejectPrimeDeclaration("model.provider", `the run selects provider ${JSON.stringify(selected.provider)} but the profile names ` +
3492
+ `${JSON.stringify(declaredProvider)}.`);
3493
+ }
3494
+ }
3357
3495
  /** Deterministic harness-state entry id for a profile subagent name. */
3358
3496
  export function primeSubagentEntryId(name) {
3359
3497
  const slug = name
@@ -3363,96 +3501,127 @@ export function primeSubagentEntryId(name) {
3363
3501
  return `profile-${slug || "subagent"}`;
3364
3502
  }
3365
3503
  /**
3366
- * Materialize the validated controls into `agentDir` and return the extra
3367
- * spawn args that bind them. Runs before every spawn (same place models.json
3368
- * is materialized), so a continual agent dir is re-pinned to the current
3369
- * profile on each fresh prime process.
3504
+ * Write one agent-dir file atomically (temp + rename).
3505
+ *
3506
+ * Every file this lowering writes lands in a directory a LIVE agent may be
3507
+ * reading: prime reloads models.json at model-resolution time and re-reads the
3508
+ * harness store on every system-prompt build, so a partial write is observable.
3509
+ * `mode` is applied to the temp file before the rename, so the final file is
3510
+ * never briefly world-readable.
3511
+ */
3512
+ export function writePrimeAgentFile(path, data, mode = 0o600) {
3513
+ mkdirSync(dirname(path), { recursive: true });
3514
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
3515
+ writeFileSync(temp, data, { mode });
3516
+ renameSync(temp, path);
3517
+ }
3518
+ /**
3519
+ * Materialize the validated controls and return the extra spawn args that bind
3520
+ * them. Runs before every spawn (same place models.json is materialized), so a
3521
+ * continual agent dir is re-pinned to the current profile on each fresh prime
3522
+ * process.
3370
3523
  */
3371
- export function materializePrimeProfileControls(agentDir, controls, warn) {
3524
+ export function materializePrimeProfileControls(agentDir, controls, options = {}) {
3525
+ const { flagFileRoot = agentDir, warn } = options;
3372
3526
  const args = [];
3373
- const profileDir = join(agentDir, PRIME_PROFILE_DIR_NAME);
3527
+ const profileDir = join(flagFileRoot, PRIME_PROFILE_DIR_NAME);
3374
3528
  if (controls.systemPrompt !== undefined) {
3375
- mkdirSync(profileDir, { recursive: true });
3376
3529
  const promptPath = join(profileDir, PRIME_SYSTEM_PROMPT_FILE);
3377
- writeFileSync(promptPath, controls.systemPrompt, "utf8");
3530
+ writePrimeAgentFile(promptPath, controls.systemPrompt);
3378
3531
  args.push("--system-prompt", promptPath);
3379
3532
  }
3380
3533
  if (controls.appendSystemPrompt !== undefined) {
3381
- mkdirSync(profileDir, { recursive: true });
3382
3534
  const appendPath = join(profileDir, PRIME_APPEND_SYSTEM_PROMPT_FILE);
3383
- writeFileSync(appendPath, controls.appendSystemPrompt, "utf8");
3535
+ writePrimeAgentFile(appendPath, controls.appendSystemPrompt);
3384
3536
  args.push("--append-system-prompt", appendPath);
3385
3537
  }
3538
+ if (controls.instructions !== undefined) {
3539
+ writePrimeAgentFile(join(agentDir, PRIME_CONTEXT_FILE), `${controls.instructions}\n`);
3540
+ }
3386
3541
  if (controls.skills.length > 0) {
3387
3542
  const skillsRoot = join(profileDir, PRIME_SKILLS_DIR_NAME);
3388
3543
  for (const skill of controls.skills) {
3389
- const skillDir = join(skillsRoot, skill.name);
3390
- mkdirSync(skillDir, { recursive: true });
3391
- writeFileSync(join(skillDir, "SKILL.md"), normalizeSkillMd(skill.name, skill.content), "utf8");
3544
+ writePrimeAgentFile(join(skillsRoot, skill.name, "SKILL.md"), normalizeSkillMd(skill.name, skill.content));
3392
3545
  }
3393
3546
  args.push("--skill", skillsRoot);
3394
3547
  }
3395
3548
  seedPrimeProfileSubagents(agentDir, controls.subagents, warn);
3396
3549
  return args;
3397
3550
  }
3398
- function emptyPrimeHarnessState() {
3399
- const entries = {};
3400
- for (const kind of PRIME_HARNESS_KINDS)
3401
- entries[kind] = {};
3402
- return { schema: 1, entries, refinements: [] };
3551
+ /**
3552
+ * Remove the files a PREVIOUS materialization of this lowering wrote, so a
3553
+ * profile that shrank cannot leave a stale instruction file or skill behind for
3554
+ * the next run against the same dirs.
3555
+ *
3556
+ * Only the paths this lowering NAMES are removed. There is deliberately no
3557
+ * manifest: a manifest would live where the agent can write it, and reading a
3558
+ * delete list out of agent-writable state turns pruning into an arbitrary-path
3559
+ * delete.
3560
+ */
3561
+ export function prunePrimeProfileMaterial(agentDir, options = {}) {
3562
+ const { flagFileRoot = agentDir } = options;
3563
+ rmSync(join(flagFileRoot, PRIME_PROFILE_DIR_NAME), {
3564
+ recursive: true,
3565
+ force: true,
3566
+ });
3567
+ rmSync(join(agentDir, PRIME_CONTEXT_FILE), { force: true });
3403
3568
  }
3404
3569
  /**
3405
3570
  * Upsert profile subagent specs into the fork's global continual-harness
3406
3571
  * store and remove profile-sourced entries the current profile no longer
3407
- * declares. Agent-written entries (`source: "agent"`) are never touched.
3572
+ * declares. Agent-written entries (`source: "agent"`) — and every top-level
3573
+ * key this lowering does not manage — are preserved untouched.
3408
3574
  *
3409
- * A pre-existing unparsable store degrades to empty exactly like the fork's
3410
- * own loader (refinement.ts:290-300 — "a corrupt state file must degrade to
3411
- * empty rather than throw"), with a warning so the reset is visible.
3575
+ * A store that exists but no longer parses raises
3576
+ * {@link PrimeHarnessStateError}: the file is the agent's own self-modification
3577
+ * state, and overwriting it destroys what the run was supposed to observe.
3412
3578
  */
3413
3579
  export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
3414
3580
  const statePath = join(agentDir, PRIME_HARNESS_DIR_NAME, PRIME_HARNESS_STATE_FILE);
3415
- let state = emptyPrimeHarnessState();
3581
+ let state = {};
3416
3582
  if (existsSync(statePath)) {
3583
+ let parsed;
3417
3584
  try {
3418
- const raw = JSON.parse(readFileSync(statePath, "utf8"));
3419
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
3420
- const parsed = raw;
3421
- state.schema =
3422
- typeof parsed.schema === "number" ? parsed.schema : state.schema;
3423
- for (const kind of PRIME_HARNESS_KINDS) {
3424
- const records = parsed.entries?.[kind];
3425
- if (records && typeof records === "object") {
3426
- state.entries[kind] = { ...records };
3427
- }
3428
- }
3429
- if (Array.isArray(parsed.refinements)) {
3430
- state.refinements = parsed.refinements;
3431
- }
3432
- }
3433
- else {
3434
- warn("prime: existing harness_state.json is not an object — reseeding", {
3435
- statePath,
3436
- });
3437
- }
3585
+ parsed = JSON.parse(readFileSync(statePath, "utf8"));
3438
3586
  }
3439
3587
  catch (err) {
3440
- warn("prime: existing harness_state.json is unparsable — reseeding", {
3441
- statePath,
3442
- error: err instanceof Error ? err.message : String(err),
3443
- });
3444
- state = emptyPrimeHarnessState();
3588
+ throw new PrimeHarnessStateError(statePath, err instanceof Error ? err.message : String(err));
3589
+ }
3590
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3591
+ throw new PrimeHarnessStateError(statePath, "the store is not a JSON object");
3445
3592
  }
3593
+ state = parsed;
3446
3594
  }
3447
3595
  else if (subagents.length === 0) {
3448
3596
  // Nothing to pin and nothing stale to remove.
3449
3597
  return;
3450
3598
  }
3451
- const existing = state.entries.subagent ?? {};
3599
+ const rawEntries = state.entries;
3600
+ const entries = rawEntries && typeof rawEntries === "object" && !Array.isArray(rawEntries)
3601
+ ? rawEntries
3602
+ : {};
3603
+ for (const kind of PRIME_HARNESS_KINDS) {
3604
+ const records = entries[kind];
3605
+ if (!records || typeof records !== "object" || Array.isArray(records)) {
3606
+ entries[kind] = {};
3607
+ }
3608
+ }
3609
+ const existing = entries.subagent;
3452
3610
  const desiredIds = new Set(subagents.map((s) => primeSubagentEntryId(s.name)));
3453
3611
  let changed = false;
3454
3612
  for (const [id, entry] of Object.entries(existing)) {
3455
- if (entry?.source === PRIME_PROFILE_ENTRY_SOURCE && !desiredIds.has(id)) {
3613
+ const source = entry?.source;
3614
+ const isLegacy = typeof source === "string" &&
3615
+ PRIME_LEGACY_PROFILE_ENTRY_SOURCES.includes(source);
3616
+ if (isLegacy) {
3617
+ warn?.("prime: adopting a subagent entry left by a pre-shared executor", {
3618
+ statePath,
3619
+ id,
3620
+ source,
3621
+ });
3622
+ }
3623
+ if ((source === PRIME_PROFILE_ENTRY_SOURCE || isLegacy) &&
3624
+ !desiredIds.has(id)) {
3456
3625
  delete existing[id];
3457
3626
  changed = true;
3458
3627
  }
@@ -3475,7 +3644,7 @@ export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
3475
3644
  kind: "subagent",
3476
3645
  title: spec.name,
3477
3646
  content,
3478
- path: "profile",
3647
+ path: PRIME_PROFILE_ENTRY_SOURCE,
3479
3648
  scope: "global",
3480
3649
  reference: {},
3481
3650
  arguments: {},
@@ -3489,9 +3658,12 @@ export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
3489
3658
  };
3490
3659
  changed = true;
3491
3660
  }
3492
- state.entries.subagent = existing;
3493
3661
  if (!changed)
3494
3662
  return;
3495
- mkdirSync(join(agentDir, PRIME_HARNESS_DIR_NAME), { recursive: true });
3496
- writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
3663
+ writePrimeAgentFile(statePath, `${JSON.stringify({
3664
+ ...state,
3665
+ schema: typeof state.schema === "number" ? state.schema : 1,
3666
+ entries,
3667
+ refinements: Array.isArray(state.refinements) ? state.refinements : [],
3668
+ }, null, 2)}\n`);
3497
3669
  }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Fork FACTS for prime-agent (PrimeIntellect-ai/prime-agent) — the constants,
3
+ * dialects and refusal reasons every executor that drives the fork needs, with
4
+ * no `node:fs` and no executor assumptions. The agent-dir MATERIALIZATION that
5
+ * consumes them lives in `./index.ts`; both consumers (cli-bridge's host-spawn
6
+ * backend on `--mode rpc`, sdk-provider-prime's in-sandbox adapter on
7
+ * `--mode acp`) read these instead of each keeping a private copy, because a
8
+ * fork bump must move one file, not two repositories.
9
+ *
10
+ * Every value here is verified against the pinned clone named by
11
+ * {@link PRIME_FORK_PIN}; `tests/prime-fork-pin.test.ts` re-asserts them
12
+ * against that clone when it is present, so a fork bump fails loudly in one
13
+ * place.
14
+ */
15
+ /** The prime-agent clone every constant in this module was read from. */
16
+ export declare const PRIME_FORK_PIN: {
17
+ readonly repo: "PrimeIntellect-ai/prime-agent";
18
+ readonly version: "0.7.0";
19
+ readonly commit: "be9e2fa0";
20
+ /** Local clone used by the fork-pin test when it exists. */
21
+ readonly clonePath: "/home/drew/bench-cache/prime-agent";
22
+ /** Package root inside the clone that the file:line citations are relative to. */
23
+ readonly packageRoot: "packages/coding-agent/src";
24
+ };
25
+ /**
26
+ * The fork's package identity, and why it cannot be installed from the npm
27
+ * registry.
28
+ *
29
+ * `@earendil-works/pi-coding-agent` on registry.npmjs.org serves the UPSTREAM
30
+ * pi line (0.84.x); the fork's 0.7.x versions were never published there. The
31
+ * two are wire-incompatible — different daemon schema revision, different
32
+ * config dir, different agent-dir env var (see
33
+ * {@link PRIME_AGENT_DIR_ENV_VARS}) — so `npm install -g` under that name
34
+ * yields an agent none of this lowering was verified against. The fork must be
35
+ * built from source and exposed under a `prime-agent` command name; its own
36
+ * `bin` entry is `pi` (fork package.json `bin`), which is exactly how the wrong
37
+ * binary ends up on `PATH` under the right-looking name.
38
+ */
39
+ export declare const PRIME_FORK_INSTALL: {
40
+ readonly npmName: "@earendil-works/pi-coding-agent";
41
+ /** The fork's own bin name — deliberately NOT the command to install it as. */
42
+ readonly forkBinName: "pi";
43
+ /** Command name an executor's binary path is expected to resolve to. */
44
+ readonly expectedCommandName: "prime-agent";
45
+ readonly sourceRepo: "https://github.com/PrimeIntellect-ai/prime-agent";
46
+ /** Built entrypoint to expose as `prime-agent`. */
47
+ readonly sourceEntrypoint: "packages/coding-agent/dist/bundle/cli.js";
48
+ readonly reason: string;
49
+ };
50
+ /**
51
+ * Env vars the agent resolves its config dir from, most-specific first.
52
+ *
53
+ * The fork derives the name from its own package identity —
54
+ * `ENV_AGENT_DIR = \`${envPrefix}_CODING_AGENT_DIR\`` where `envPrefix` comes
55
+ * from `package.json` `piConfig.name` (config.ts:485-498) — so the fork reads
56
+ * `PRIME_AGENT_CODING_AGENT_DIR` and upstream pi reads `PI_CODING_AGENT_DIR`.
57
+ * An executor that sets only the fork's name has its isolation SILENTLY
58
+ * disabled when the binary on PATH turns out to be upstream pi: `getAgentDir()`
59
+ * falls back to `homedir()/<CONFIG_DIR_NAME>` (config.ts:520-527), one shared
60
+ * dir — and therefore one shared self-modifying `harness_state.json` — for
61
+ * every session on the box. Setting BOTH names costs nothing and keeps the
62
+ * isolation fail-closed under either binary.
63
+ */
64
+ export declare const PRIME_AGENT_DIR_ENV_VARS: readonly ["PRIME_AGENT_CODING_AGENT_DIR", "PI_CODING_AGENT_DIR"];
65
+ /** Canonical agent-dir env var for the fork itself (config.ts:498). */
66
+ export declare const PRIME_AGENT_DIR_ENV_VAR: "PRIME_AGENT_CODING_AGENT_DIR";
67
+ /** Pin `agentDir` under every name the agent could resolve its config dir from. */
68
+ export declare function primeAgentDirEnv(agentDir: string): Record<string, string>;
69
+ /**
70
+ * Operator knobs an executor forwards into the agent when they are set.
71
+ *
72
+ * `PRIME_AGENT_KERNEL_VENV` is load-bearing for any executor that also pins
73
+ * HOME: the kernel venv defaults under `homedir()/.prime/agent/kernel-venv`
74
+ * (kernel/bootstrap.ts:331-342), so a fresh HOME re-bootstraps it through uv on
75
+ * every run unless the operator's venv path is forwarded
76
+ * (bootstrap.ts:340 override, :358 branch).
77
+ */
78
+ export declare const PRIME_KERNEL_ENV_PASSTHROUGH: readonly ["PRIME_AGENT_INSTALL_UV", "PRIME_AGENT_KERNEL_PYTHON", "PRIME_AGENT_KERNEL_VENV"];
79
+ /**
80
+ * Env vars an executor must NEVER forward from its own environment.
81
+ *
82
+ * These re-point the self-modifying continual-harness store OUTSIDE the agent
83
+ * dir (agent-session.ts:8805 hands the kernel its store location), which
84
+ * defeats per-session isolation without changing anything an executor inspects.
85
+ * Isolation is the fail-closed default; sharing state is an explicit opt-in
86
+ * that names a directory, never an inherited env var.
87
+ */
88
+ export declare const PRIME_HARNESS_STATE_ENV_DENYLIST: readonly ["RLM_GLOBAL_HARNESS_STATE_DIR", "RLM_HARNESS_STATE_DIR"];
89
+ /**
90
+ * A prime-agent IPython kernel that dies is never re-provisioned headlessly —
91
+ * every later `ipython` call fails with one of these markers until the process
92
+ * is replaced. An executor that cannot recognize them either reports a generic
93
+ * turn failure or burns its whole timeout against a permanently dead tool.
94
+ *
95
+ * Verified in `core/kernel/index.ts`: :723 ("forked kernel exited
96
+ * unexpectedly"), :750 ("Kernel exited before resolving ports"), :779 ("Kernel
97
+ * exited during startup"), :820/:837/:1172/:1295 ("Kernel has been shut down").
98
+ */
99
+ export declare const PRIME_KERNEL_DEAD_MARKERS: readonly string[];
100
+ /** The first kernel-death marker present in `text`, or undefined. */
101
+ export declare function matchPrimeKernelDeadMarker(text: string): string | undefined;
102
+ /**
103
+ * `--thinking` rungs the fork's argument parser accepts
104
+ * (cli/args.ts:62 `VALID_THINKING_LEVELS`, rejected at :175). Read from the
105
+ * fork itself rather than borrowed from upstream pi's measured table: the two
106
+ * agreeing today is a coincidence this module must not depend on.
107
+ */
108
+ export declare const PRIME_THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
109
+ export type PrimeThinkingLevel = (typeof PRIME_THINKING_LEVELS)[number];
110
+ /**
111
+ * Map the portable reasoning ladder onto the fork's `--thinking` value.
112
+ *
113
+ * Throws on an effort with no rung instead of returning undefined: omitting the
114
+ * flag runs the agent at its default thinking level while the caller believes
115
+ * the requested effort is in force, which is a silently dropped control.
116
+ */
117
+ export declare function primeThinkingLevel(effort: string | null | undefined): PrimeThinkingLevel | undefined;
118
+ /**
119
+ * Why `profile.mcp` (and any hub connection that resolves to MCP) is refused.
120
+ *
121
+ * The fork's `mcpServers` settings surface IS read at startup
122
+ * (settings-manager.ts:98-120,219) but a user-declared server never becomes
123
+ * model-visible: the host McpManager registers OAuth providers and serves
124
+ * mcp.config/refresh host-requests only (core/mcp/mcp-manager.ts:62-124),
125
+ * skips stdio servers entirely (mcp-manager.ts:73), and nothing generates a
126
+ * skill or prompt section for one. Model-visible MCP is exactly the built-in
127
+ * catalog skills, which are hand-written Python `McpIntegration` subclasses
128
+ * (prime-agent-runtime/src/rlm/mcp_base.py:112-125; only linear and notion
129
+ * ship). Materializing a per-run entry would run the agent WITHOUT the
130
+ * requested tools while the config on disk says otherwise.
131
+ */
132
+ export declare const PRIME_MCP_UNSUPPORTED_REASON: string;
133
+ /** Which half of a profile refusal this is: a fork limit, or a bad declaration. */
134
+ export type PrimeProfileErrorKind = "unsupported" | "invalid";
135
+ /**
136
+ * Refusal raised by the shared prime lowering, carrying the control and the
137
+ * reason separately so each executor can render them in its own error contract
138
+ * (an HTTP backend's typed `BackendError`, an SDK adapter's plain `Error`)
139
+ * without the reason text — which is where the fork evidence lives — being
140
+ * rewritten per repository.
141
+ */
142
+ export declare class PrimeProfileError extends Error {
143
+ readonly kind: PrimeProfileErrorKind;
144
+ readonly control: string;
145
+ readonly reason: string;
146
+ constructor(kind: PrimeProfileErrorKind, control: string, reason: string);
147
+ }
148
+ /** Raise the "the fork cannot honor this control" half of the refusal. */
149
+ export declare function rejectPrimeControl(control: string, reason: string): never;
150
+ /** Raise the "this declaration is malformed" half of the refusal. */
151
+ export declare function rejectPrimeDeclaration(control: string, reason: string): never;
152
+ /**
153
+ * The fork's continual-harness store exists but no longer parses.
154
+ *
155
+ * The store is the agent's own self-modification state — refinements, learned
156
+ * prompts, subagents it created mid-session — so an executor that reseeds it
157
+ * from empty destroys evidence and silently mutates the thing under study. The
158
+ * turn is failed instead; the file is the operator's to repair or remove.
159
+ */
160
+ export declare class PrimeHarnessStateError extends Error {
161
+ readonly statePath: string;
162
+ constructor(statePath: string, detail: string);
163
+ }
164
+ /**
165
+ * Strip `//` line comments and trailing commas, leaving string literals
166
+ * untouched — byte-for-byte the fork's own `stripJsonComments`
167
+ * (core/model-registry.ts), so a models.json the agent accepts is never refused
168
+ * by an executor and vice versa.
169
+ */
170
+ export declare function stripPrimeJsonComments(input: string): string;
171
+ /**
172
+ * Parse a models.json with the fork's own laxness and assert the one structural
173
+ * invariant it requires (a `providers` object). Throws naming `sourcePath`.
174
+ *
175
+ * This is the SHAPE and DIALECT only. Producing the file is deliberately not
176
+ * shared: an executor holding real operator credentials copies an
177
+ * operator-authored file and forwards only the env var names its `apiKey`
178
+ * entries reference, while an in-sandbox executor synthesizes a one-provider
179
+ * entry with a literal key that is a proxy dummy. Same schema, opposite threat
180
+ * models.
181
+ */
182
+ export declare function parsePrimeModelsJson(text: string, sourcePath: string): Record<string, unknown>;
183
+ /**
184
+ * Environment variables a models.json names as `apiKey` sources.
185
+ *
186
+ * prime resolves an `apiKey` value env-var first and LITERAL second
187
+ * (core/resolve-config-value.ts), so an executor that forwards exactly the
188
+ * named variables lets an operator keep secrets out of the file — and an
189
+ * executor that forwards NONE of them silently turns the variable name itself
190
+ * into the credential string.
191
+ */
192
+ export declare function primeApiKeyEnvNames(modelsConfig: Record<string, unknown>): string[];
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Fork FACTS for prime-agent (PrimeIntellect-ai/prime-agent) — the constants,
3
+ * dialects and refusal reasons every executor that drives the fork needs, with
4
+ * no `node:fs` and no executor assumptions. The agent-dir MATERIALIZATION that
5
+ * consumes them lives in `./index.ts`; both consumers (cli-bridge's host-spawn
6
+ * backend on `--mode rpc`, sdk-provider-prime's in-sandbox adapter on
7
+ * `--mode acp`) read these instead of each keeping a private copy, because a
8
+ * fork bump must move one file, not two repositories.
9
+ *
10
+ * Every value here is verified against the pinned clone named by
11
+ * {@link PRIME_FORK_PIN}; `tests/prime-fork-pin.test.ts` re-asserts them
12
+ * against that clone when it is present, so a fork bump fails loudly in one
13
+ * place.
14
+ */
15
+ /** The prime-agent clone every constant in this module was read from. */
16
+ export const PRIME_FORK_PIN = {
17
+ repo: "PrimeIntellect-ai/prime-agent",
18
+ version: "0.7.0",
19
+ commit: "be9e2fa0",
20
+ /** Local clone used by the fork-pin test when it exists. */
21
+ clonePath: "/home/drew/bench-cache/prime-agent",
22
+ /** Package root inside the clone that the file:line citations are relative to. */
23
+ packageRoot: "packages/coding-agent/src",
24
+ };
25
+ /**
26
+ * The fork's package identity, and why it cannot be installed from the npm
27
+ * registry.
28
+ *
29
+ * `@earendil-works/pi-coding-agent` on registry.npmjs.org serves the UPSTREAM
30
+ * pi line (0.84.x); the fork's 0.7.x versions were never published there. The
31
+ * two are wire-incompatible — different daemon schema revision, different
32
+ * config dir, different agent-dir env var (see
33
+ * {@link PRIME_AGENT_DIR_ENV_VARS}) — so `npm install -g` under that name
34
+ * yields an agent none of this lowering was verified against. The fork must be
35
+ * built from source and exposed under a `prime-agent` command name; its own
36
+ * `bin` entry is `pi` (fork package.json `bin`), which is exactly how the wrong
37
+ * binary ends up on `PATH` under the right-looking name.
38
+ */
39
+ export const PRIME_FORK_INSTALL = {
40
+ npmName: "@earendil-works/pi-coding-agent",
41
+ /** The fork's own bin name — deliberately NOT the command to install it as. */
42
+ forkBinName: "pi",
43
+ /** Command name an executor's binary path is expected to resolve to. */
44
+ expectedCommandName: "prime-agent",
45
+ sourceRepo: "https://github.com/PrimeIntellect-ai/prime-agent",
46
+ /** Built entrypoint to expose as `prime-agent`. */
47
+ sourceEntrypoint: "packages/coding-agent/dist/bundle/cli.js",
48
+ reason: "registry.npmjs.org serves the upstream pi line under @earendil-works/pi-coding-agent " +
49
+ "(0.84.x); the prime-agent fork's 0.7.x versions do not exist there. Installing from " +
50
+ "the registry yields a wire-incompatible agent with a different daemon schema, a " +
51
+ "different config dir, and a different agent-dir env var. Build from source and expose " +
52
+ "packages/coding-agent/dist/bundle/cli.js as `prime-agent`.",
53
+ };
54
+ /**
55
+ * Env vars the agent resolves its config dir from, most-specific first.
56
+ *
57
+ * The fork derives the name from its own package identity —
58
+ * `ENV_AGENT_DIR = \`${envPrefix}_CODING_AGENT_DIR\`` where `envPrefix` comes
59
+ * from `package.json` `piConfig.name` (config.ts:485-498) — so the fork reads
60
+ * `PRIME_AGENT_CODING_AGENT_DIR` and upstream pi reads `PI_CODING_AGENT_DIR`.
61
+ * An executor that sets only the fork's name has its isolation SILENTLY
62
+ * disabled when the binary on PATH turns out to be upstream pi: `getAgentDir()`
63
+ * falls back to `homedir()/<CONFIG_DIR_NAME>` (config.ts:520-527), one shared
64
+ * dir — and therefore one shared self-modifying `harness_state.json` — for
65
+ * every session on the box. Setting BOTH names costs nothing and keeps the
66
+ * isolation fail-closed under either binary.
67
+ */
68
+ export const PRIME_AGENT_DIR_ENV_VARS = [
69
+ "PRIME_AGENT_CODING_AGENT_DIR",
70
+ "PI_CODING_AGENT_DIR",
71
+ ];
72
+ /** Canonical agent-dir env var for the fork itself (config.ts:498). */
73
+ export const PRIME_AGENT_DIR_ENV_VAR = PRIME_AGENT_DIR_ENV_VARS[0];
74
+ /** Pin `agentDir` under every name the agent could resolve its config dir from. */
75
+ export function primeAgentDirEnv(agentDir) {
76
+ const env = {};
77
+ for (const name of PRIME_AGENT_DIR_ENV_VARS)
78
+ env[name] = agentDir;
79
+ return env;
80
+ }
81
+ /**
82
+ * Operator knobs an executor forwards into the agent when they are set.
83
+ *
84
+ * `PRIME_AGENT_KERNEL_VENV` is load-bearing for any executor that also pins
85
+ * HOME: the kernel venv defaults under `homedir()/.prime/agent/kernel-venv`
86
+ * (kernel/bootstrap.ts:331-342), so a fresh HOME re-bootstraps it through uv on
87
+ * every run unless the operator's venv path is forwarded
88
+ * (bootstrap.ts:340 override, :358 branch).
89
+ */
90
+ export const PRIME_KERNEL_ENV_PASSTHROUGH = [
91
+ /** "1" lets a headless first run install uv itself; "0" forbids it. */
92
+ "PRIME_AGENT_INSTALL_UV",
93
+ /** Offline escape hatch: pin the kernel venv python instead of uv-provisioning. */
94
+ "PRIME_AGENT_KERNEL_PYTHON",
95
+ /** Reuse an operator-provisioned kernel venv instead of re-bootstrapping it. */
96
+ "PRIME_AGENT_KERNEL_VENV",
97
+ ];
98
+ /**
99
+ * Env vars an executor must NEVER forward from its own environment.
100
+ *
101
+ * These re-point the self-modifying continual-harness store OUTSIDE the agent
102
+ * dir (agent-session.ts:8805 hands the kernel its store location), which
103
+ * defeats per-session isolation without changing anything an executor inspects.
104
+ * Isolation is the fail-closed default; sharing state is an explicit opt-in
105
+ * that names a directory, never an inherited env var.
106
+ */
107
+ export const PRIME_HARNESS_STATE_ENV_DENYLIST = [
108
+ "RLM_GLOBAL_HARNESS_STATE_DIR",
109
+ "RLM_HARNESS_STATE_DIR",
110
+ ];
111
+ /**
112
+ * A prime-agent IPython kernel that dies is never re-provisioned headlessly —
113
+ * every later `ipython` call fails with one of these markers until the process
114
+ * is replaced. An executor that cannot recognize them either reports a generic
115
+ * turn failure or burns its whole timeout against a permanently dead tool.
116
+ *
117
+ * Verified in `core/kernel/index.ts`: :723 ("forked kernel exited
118
+ * unexpectedly"), :750 ("Kernel exited before resolving ports"), :779 ("Kernel
119
+ * exited during startup"), :820/:837/:1172/:1295 ("Kernel has been shut down").
120
+ */
121
+ export const PRIME_KERNEL_DEAD_MARKERS = [
122
+ "Kernel has been shut down",
123
+ "Kernel exited before resolving ports",
124
+ "Kernel exited during startup",
125
+ "forked kernel exited unexpectedly",
126
+ ];
127
+ /** The first kernel-death marker present in `text`, or undefined. */
128
+ export function matchPrimeKernelDeadMarker(text) {
129
+ return PRIME_KERNEL_DEAD_MARKERS.find((marker) => text.includes(marker));
130
+ }
131
+ /**
132
+ * `--thinking` rungs the fork's argument parser accepts
133
+ * (cli/args.ts:62 `VALID_THINKING_LEVELS`, rejected at :175). Read from the
134
+ * fork itself rather than borrowed from upstream pi's measured table: the two
135
+ * agreeing today is a coincidence this module must not depend on.
136
+ */
137
+ export const PRIME_THINKING_LEVELS = [
138
+ "off",
139
+ "minimal",
140
+ "low",
141
+ "medium",
142
+ "high",
143
+ "xhigh",
144
+ "max",
145
+ ];
146
+ /** Canonical portable effort → fork `--thinking` rung. */
147
+ const PRIME_THINKING_BY_EFFORT = {
148
+ none: "off",
149
+ minimal: "minimal",
150
+ low: "low",
151
+ medium: "medium",
152
+ high: "high",
153
+ xhigh: "xhigh",
154
+ // The fork carries `max` above `xhigh`, so the ceiling level reaches it.
155
+ ultracode: "max",
156
+ };
157
+ /**
158
+ * Map the portable reasoning ladder onto the fork's `--thinking` value.
159
+ *
160
+ * Throws on an effort with no rung instead of returning undefined: omitting the
161
+ * flag runs the agent at its default thinking level while the caller believes
162
+ * the requested effort is in force, which is a silently dropped control.
163
+ */
164
+ export function primeThinkingLevel(effort) {
165
+ if (effort === undefined || effort === null || effort === "")
166
+ return undefined;
167
+ const level = PRIME_THINKING_BY_EFFORT[effort];
168
+ if (level === undefined) {
169
+ throw new PrimeProfileError("invalid", "model.reasoningEffort", `prime-agent has no --thinking rung for ${JSON.stringify(effort)} ` +
170
+ `(fork cli/args.ts:62 accepts ${PRIME_THINKING_LEVELS.join(", ")}).`);
171
+ }
172
+ return level;
173
+ }
174
+ /**
175
+ * Why `profile.mcp` (and any hub connection that resolves to MCP) is refused.
176
+ *
177
+ * The fork's `mcpServers` settings surface IS read at startup
178
+ * (settings-manager.ts:98-120,219) but a user-declared server never becomes
179
+ * model-visible: the host McpManager registers OAuth providers and serves
180
+ * mcp.config/refresh host-requests only (core/mcp/mcp-manager.ts:62-124),
181
+ * skips stdio servers entirely (mcp-manager.ts:73), and nothing generates a
182
+ * skill or prompt section for one. Model-visible MCP is exactly the built-in
183
+ * catalog skills, which are hand-written Python `McpIntegration` subclasses
184
+ * (prime-agent-runtime/src/rlm/mcp_base.py:112-125; only linear and notion
185
+ * ship). Materializing a per-run entry would run the agent WITHOUT the
186
+ * requested tools while the config on disk says otherwise.
187
+ */
188
+ export const PRIME_MCP_UNSUPPORTED_REASON = "a user-declared mcpServers entry is read from the fork's settings.json at startup but never " +
189
+ "becomes model-visible: the host only registers OAuth/config for it (fork core/mcp/" +
190
+ "mcp-manager.ts:62-124), skips stdio servers entirely (mcp-manager.ts:73), and generates no " +
191
+ "skill or prompt section for it. Model-visible MCP is exactly the built-in Python " +
192
+ "McpIntegration catalog skills (prime-agent-runtime/src/rlm/mcp_base.py:112-125), which no " +
193
+ "per-run configuration creates — so materializing it would be a silently dropped control.";
194
+ /**
195
+ * Refusal raised by the shared prime lowering, carrying the control and the
196
+ * reason separately so each executor can render them in its own error contract
197
+ * (an HTTP backend's typed `BackendError`, an SDK adapter's plain `Error`)
198
+ * without the reason text — which is where the fork evidence lives — being
199
+ * rewritten per repository.
200
+ */
201
+ export class PrimeProfileError extends Error {
202
+ kind;
203
+ control;
204
+ reason;
205
+ constructor(kind, control, reason) {
206
+ super(kind === "unsupported"
207
+ ? `prime does not support AgentProfile ${control}: ${reason}`
208
+ : `prime cannot accept AgentProfile ${control}: ${reason}`);
209
+ this.name = "PrimeProfileError";
210
+ this.kind = kind;
211
+ this.control = control;
212
+ this.reason = reason;
213
+ }
214
+ }
215
+ /** Raise the "the fork cannot honor this control" half of the refusal. */
216
+ export function rejectPrimeControl(control, reason) {
217
+ throw new PrimeProfileError("unsupported", control, reason);
218
+ }
219
+ /** Raise the "this declaration is malformed" half of the refusal. */
220
+ export function rejectPrimeDeclaration(control, reason) {
221
+ throw new PrimeProfileError("invalid", control, reason);
222
+ }
223
+ /**
224
+ * The fork's continual-harness store exists but no longer parses.
225
+ *
226
+ * The store is the agent's own self-modification state — refinements, learned
227
+ * prompts, subagents it created mid-session — so an executor that reseeds it
228
+ * from empty destroys evidence and silently mutates the thing under study. The
229
+ * turn is failed instead; the file is the operator's to repair or remove.
230
+ */
231
+ export class PrimeHarnessStateError extends Error {
232
+ statePath;
233
+ constructor(statePath, detail) {
234
+ super(`prime harness state ${statePath} is not valid JSON and may hold agent self-modification ` +
235
+ `state; refusing to overwrite it — repair or remove the file to continue (${detail})`);
236
+ this.name = "PrimeHarnessStateError";
237
+ this.statePath = statePath;
238
+ }
239
+ }
240
+ /**
241
+ * Strip `//` line comments and trailing commas, leaving string literals
242
+ * untouched — byte-for-byte the fork's own `stripJsonComments`
243
+ * (core/model-registry.ts), so a models.json the agent accepts is never refused
244
+ * by an executor and vice versa.
245
+ */
246
+ export function stripPrimeJsonComments(input) {
247
+ return input
248
+ .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
249
+ .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail !== undefined ? tail : m[0] === '"' ? m : "");
250
+ }
251
+ /**
252
+ * Parse a models.json with the fork's own laxness and assert the one structural
253
+ * invariant it requires (a `providers` object). Throws naming `sourcePath`.
254
+ *
255
+ * This is the SHAPE and DIALECT only. Producing the file is deliberately not
256
+ * shared: an executor holding real operator credentials copies an
257
+ * operator-authored file and forwards only the env var names its `apiKey`
258
+ * entries reference, while an in-sandbox executor synthesizes a one-provider
259
+ * entry with a literal key that is a proxy dummy. Same schema, opposite threat
260
+ * models.
261
+ */
262
+ export function parsePrimeModelsJson(text, sourcePath) {
263
+ let parsed;
264
+ try {
265
+ parsed = JSON.parse(stripPrimeJsonComments(text));
266
+ }
267
+ catch (err) {
268
+ throw new Error(`prime models.json ${sourcePath} is not valid JSON: ${err.message}`);
269
+ }
270
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
271
+ throw new Error(`prime models.json ${sourcePath} must be a JSON object`);
272
+ }
273
+ const providers = parsed.providers;
274
+ if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
275
+ throw new Error(`prime models.json ${sourcePath} must carry a "providers" object (prime's models.json schema)`);
276
+ }
277
+ return parsed;
278
+ }
279
+ /**
280
+ * Environment variables a models.json names as `apiKey` sources.
281
+ *
282
+ * prime resolves an `apiKey` value env-var first and LITERAL second
283
+ * (core/resolve-config-value.ts), so an executor that forwards exactly the
284
+ * named variables lets an operator keep secrets out of the file — and an
285
+ * executor that forwards NONE of them silently turns the variable name itself
286
+ * into the credential string.
287
+ */
288
+ export function primeApiKeyEnvNames(modelsConfig) {
289
+ const names = [];
290
+ const providers = modelsConfig.providers;
291
+ if (!providers || typeof providers !== "object")
292
+ return names;
293
+ for (const provider of Object.values(providers)) {
294
+ if (!provider || typeof provider !== "object")
295
+ continue;
296
+ const key = provider.apiKey;
297
+ if (typeof key !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(key))
298
+ continue;
299
+ if (!names.includes(key))
300
+ names.push(key);
301
+ }
302
+ return names;
303
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",