@tangle-network/agent-profile-materialize 0.10.3 → 0.11.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 +76 -1
- package/dist/index.js +544 -13
- package/dist/model-id.d.ts +42 -0
- package/dist/model-id.js +59 -0
- package/package.json +3 -3
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 {
|
|
@@ -95,6 +96,22 @@ export interface Unsupported {
|
|
|
95
96
|
export declare const SKIPPABLE_DIMENSIONS: readonly ["mcp", "extensions"];
|
|
96
97
|
export type SkippableDimension = (typeof SKIPPABLE_DIMENSIONS)[number];
|
|
97
98
|
export declare function isSkippableDimension(value: string): value is SkippableDimension;
|
|
99
|
+
/**
|
|
100
|
+
* Launcher controls an executor can declare it OWNS via `materializeProfile`'s
|
|
101
|
+
* `binds` option.
|
|
102
|
+
*
|
|
103
|
+
* Some harness controls live outside any workspace: opencode's exact
|
|
104
|
+
* system-prompt replacement (`agent.<name>.prompt`) binds to the ONE agent
|
|
105
|
+
* whoever starts the process selects, which a plan-forwarding executor cannot
|
|
106
|
+
* 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.
|
|
112
|
+
*/
|
|
113
|
+
export declare const BINDABLE_LAUNCH_CONTROLS: readonly ["systemPrompt"];
|
|
114
|
+
export type BindableLaunchControl = (typeof BINDABLE_LAUNCH_CONTROLS)[number];
|
|
98
115
|
/** The plan a caller writes into the run cwd, then applies to the spawn. */
|
|
99
116
|
export interface WorkspacePlan {
|
|
100
117
|
harness: HarnessId;
|
|
@@ -106,6 +123,16 @@ export interface WorkspacePlan {
|
|
|
106
123
|
flags: WorkspacePlanArgument[];
|
|
107
124
|
/** Exact system-prompt replacement supplied to the harness. */
|
|
108
125
|
systemPrompt?: string;
|
|
126
|
+
/**
|
|
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).
|
|
134
|
+
*/
|
|
135
|
+
appendSystemPrompt?: string;
|
|
109
136
|
/** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
|
|
110
137
|
unsupported: Unsupported[];
|
|
111
138
|
/**
|
|
@@ -157,6 +184,7 @@ export interface WorkspacePlanReceipt {
|
|
|
157
184
|
/** Public launch identity only; templates retain opaque secret references. */
|
|
158
185
|
flags: WorkspacePlanArgument[];
|
|
159
186
|
systemPrompt?: string;
|
|
187
|
+
appendSystemPrompt?: string;
|
|
160
188
|
unsupported: Unsupported[];
|
|
161
189
|
written: string[];
|
|
162
190
|
}
|
|
@@ -165,6 +193,7 @@ export interface WorkspacePlanSpawnInputs {
|
|
|
165
193
|
env: Record<string, string>;
|
|
166
194
|
flags: string[];
|
|
167
195
|
systemPrompt?: string;
|
|
196
|
+
appendSystemPrompt?: string;
|
|
168
197
|
}
|
|
169
198
|
/** Result of applying a public plan at the private executor boundary. */
|
|
170
199
|
export interface WorkspacePlanApplication {
|
|
@@ -207,6 +236,7 @@ export interface AgentCandidateWorkspacePlan {
|
|
|
207
236
|
env: Record<string, AgentCandidateConfigValue>;
|
|
208
237
|
flags: AgentCandidateConfigValue[];
|
|
209
238
|
systemPrompt?: AgentCandidateConfigValue;
|
|
239
|
+
appendSystemPrompt?: AgentCandidateConfigValue;
|
|
210
240
|
unsupported: Unsupported[];
|
|
211
241
|
}
|
|
212
242
|
export interface AgentCandidateWorkspacePlanReceipt {
|
|
@@ -215,6 +245,7 @@ export interface AgentCandidateWorkspacePlanReceipt {
|
|
|
215
245
|
env: Record<string, AgentCandidateConfigValue>;
|
|
216
246
|
flags: AgentCandidateConfigValue[];
|
|
217
247
|
systemPrompt?: AgentCandidateConfigValue;
|
|
248
|
+
appendSystemPrompt?: AgentCandidateConfigValue;
|
|
218
249
|
unsupported: Unsupported[];
|
|
219
250
|
}
|
|
220
251
|
export interface MaterializeCandidateProfileOptions {
|
|
@@ -277,6 +308,8 @@ export declare function normalizeSkillMd(name: string, raw: string): string;
|
|
|
277
308
|
*/
|
|
278
309
|
export declare function materializeProfile(inputProfile: AgentProfile, harness: HarnessId, opts?: {
|
|
279
310
|
skip?: SkippableDimension[];
|
|
311
|
+
/** Launcher controls the calling executor owns — see {@link BINDABLE_LAUNCH_CONTROLS}. */
|
|
312
|
+
binds?: readonly BindableLaunchControl[];
|
|
280
313
|
}): WorkspacePlan;
|
|
281
314
|
/**
|
|
282
315
|
* Materialize a sealed candidate profile without weakening its process values.
|
|
@@ -339,3 +372,45 @@ export type OpenCodeMcpConfig = Record<string, {
|
|
|
339
372
|
}>;
|
|
340
373
|
/** Lower canonical MCP definitions to OpenCode's native startup configuration. */
|
|
341
374
|
export declare function openCodeMcpConfig(mcp: Record<string, AgentProfileMcpServer>): OpenCodeMcpConfig;
|
|
375
|
+
export interface PrimeProfileSkill {
|
|
376
|
+
name: string;
|
|
377
|
+
content: string;
|
|
378
|
+
}
|
|
379
|
+
export interface PrimeProfileSubagent {
|
|
380
|
+
name: string;
|
|
381
|
+
description?: string;
|
|
382
|
+
prompt?: string;
|
|
383
|
+
metadata?: Record<string, unknown>;
|
|
384
|
+
}
|
|
385
|
+
/** Validated, materialization-ready view of the profile controls prime applies. */
|
|
386
|
+
export interface PrimeProfileControls {
|
|
387
|
+
systemPrompt?: string;
|
|
388
|
+
appendSystemPrompt?: string;
|
|
389
|
+
skills: PrimeProfileSkill[];
|
|
390
|
+
subagents: PrimeProfileSubagent[];
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 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.
|
|
396
|
+
*/
|
|
397
|
+
export declare function readPrimeProfileControls(profile: AgentProfile | undefined): PrimeProfileControls;
|
|
398
|
+
/** Deterministic harness-state entry id for a profile subagent name. */
|
|
399
|
+
export declare function primeSubagentEntryId(name: string): string;
|
|
400
|
+
/**
|
|
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.
|
|
405
|
+
*/
|
|
406
|
+
export declare function materializePrimeProfileControls(agentDir: string, controls: PrimeProfileControls, warn: (msg: string, meta?: Record<string, unknown>) => void): string[];
|
|
407
|
+
/**
|
|
408
|
+
* Upsert profile subagent specs into the fork's global continual-harness
|
|
409
|
+
* store and remove profile-sourced entries the current profile no longer
|
|
410
|
+
* declares. Agent-written entries (`source: "agent"`) are never touched.
|
|
411
|
+
*
|
|
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.
|
|
415
|
+
*/
|
|
416
|
+
export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn: (msg: string, meta?: Record<string, unknown>) => void): void;
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
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 { createHash, randomUUID } from "node:crypto";
|
|
25
|
-
import { closeSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
26
|
+
import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
26
27
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32, } from "node:path";
|
|
27
28
|
import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, agentProfileSecretRefSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
|
|
28
29
|
export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
|
|
@@ -71,6 +72,7 @@ export const MATERIALIZER_HARNESSES = [
|
|
|
71
72
|
"opencode",
|
|
72
73
|
"kimi-code",
|
|
73
74
|
"pi",
|
|
75
|
+
"prime",
|
|
74
76
|
"gemini",
|
|
75
77
|
"hermes",
|
|
76
78
|
"openclaw",
|
|
@@ -110,6 +112,33 @@ function assertSkippableDimensions(skip) {
|
|
|
110
112
|
}
|
|
111
113
|
return skip;
|
|
112
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Launcher controls an executor can declare it OWNS via `materializeProfile`'s
|
|
117
|
+
* `binds` option.
|
|
118
|
+
*
|
|
119
|
+
* Some harness controls live outside any workspace: opencode's exact
|
|
120
|
+
* system-prompt replacement (`agent.<name>.prompt`) binds to the ONE agent
|
|
121
|
+
* whoever starts the process selects, which a plan-forwarding executor cannot
|
|
122
|
+
* guarantee — so the plan refuses the intent (harnessSystemPromptIntents in
|
|
123
|
+
* @tangle-network/agent-interface answers `replace: false` for opencode). An
|
|
124
|
+
* executor that really applies the plan's launch flags to the spawn it owns
|
|
125
|
+
* (selecting the generated primary agent via the plan's `--agent` flag) states
|
|
126
|
+
* so with `binds: ["systemPrompt"]`, which turns that refusal into the
|
|
127
|
+
* primary-agent lowering the executor must then apply.
|
|
128
|
+
*/
|
|
129
|
+
export const BINDABLE_LAUNCH_CONTROLS = ["systemPrompt"];
|
|
130
|
+
const BINDABLE_LAUNCH_CONTROL_SET = new Set(BINDABLE_LAUNCH_CONTROLS);
|
|
131
|
+
/** Reject a binding declaration this materializer has no lowering for, rather than ignoring it. */
|
|
132
|
+
function assertBindableLaunchControls(binds) {
|
|
133
|
+
if (!binds)
|
|
134
|
+
return new Set();
|
|
135
|
+
const unknown = binds.filter((control) => !BINDABLE_LAUNCH_CONTROL_SET.has(control));
|
|
136
|
+
if (unknown.length > 0) {
|
|
137
|
+
throw new Error(`materializeProfile: cannot bind ${unknown.join(", ")}. ` +
|
|
138
|
+
`Bindable launch controls are: ${BINDABLE_LAUNCH_CONTROLS.join(", ")}.`);
|
|
139
|
+
}
|
|
140
|
+
return new Set(binds);
|
|
141
|
+
}
|
|
113
142
|
/** The plan was applied, but one or more private staging files remain. */
|
|
114
143
|
export class WorkspacePlanCleanupError extends AggregateError {
|
|
115
144
|
receipt;
|
|
@@ -183,6 +212,10 @@ const CONTEXT_FILE = {
|
|
|
183
212
|
opencode: ".opencode/profile-instructions.md",
|
|
184
213
|
"kimi-code": "AGENTS.md",
|
|
185
214
|
pi: "AGENTS.md",
|
|
215
|
+
// prime (PrimeIntellect-ai/prime-agent v0.7.0, be9e2fa0) walks cwd and its
|
|
216
|
+
// ancestors for AGENTS.md/CLAUDE.md, cwd first (fork resource-loader.ts:59
|
|
217
|
+
// candidates, :94-109 walk from cwd inside loadProjectContextFiles).
|
|
218
|
+
prime: "AGENTS.md",
|
|
186
219
|
gemini: "GEMINI.md", // current default; AGENTS.md only via .gemini/settings.json context.fileName
|
|
187
220
|
hermes: "HERMES.md", // highest-priority native name; only ONE context loads (first match)
|
|
188
221
|
openclaw: "AGENTS.md",
|
|
@@ -202,6 +235,12 @@ export const HARNESS_SKILL_DIRS = {
|
|
|
202
235
|
opencode: ".opencode/skills",
|
|
203
236
|
"kimi-code": ".kimi/skills",
|
|
204
237
|
pi: ".pi/skills",
|
|
238
|
+
// prime auto-discovers project skills from <cwd>/<configDir>/skills where the
|
|
239
|
+
// fork ships configDir ".prime/agent" (fork package.json piConfig, config.ts:494;
|
|
240
|
+
// package-manager.ts:885 projectBaseDir, :2175-2180 projectDirs.skills,
|
|
241
|
+
// :2209-2213 collectAutoSkillEntries). The loader recurses into
|
|
242
|
+
// <name>/SKILL.md subdirs (skills.ts:328-372).
|
|
243
|
+
prime: ".prime/agent/skills",
|
|
205
244
|
gemini: ".gemini/skills",
|
|
206
245
|
hermes: null, // skills are ~/.hermes/skills (user) only — no cwd discovery
|
|
207
246
|
openclaw: "skills", // <workspace>/skills/<name>/SKILL.md
|
|
@@ -226,11 +265,14 @@ const DEFAULT_FILE_MODE = 0o644;
|
|
|
226
265
|
const PRIVATE_FILE_MODE = 0o600;
|
|
227
266
|
const EXECUTABLE_FILE_MODE = 0o755;
|
|
228
267
|
const DEFAULT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
268
|
+
const OPENCODE_PRIMARY_AGENT_PREFIX = "tangle-profile-";
|
|
229
269
|
const MAX_RESOURCE_BYTES = 10 * 1024 * 1024;
|
|
230
270
|
const MAX_PROFILE_RESOURCE_BYTES = 32 * 1024 * 1024;
|
|
231
271
|
const MAX_PROFILE_RESOURCE_REFS = 128;
|
|
232
272
|
const MAX_CONCURRENT_RESOURCE_FETCHES = 8;
|
|
233
273
|
const CLAUDE_SETTINGS_PATH = ".tangle/claude-settings.json";
|
|
274
|
+
const CODEX_SYSTEM_PROMPT_PATH = ".codex/system-prompt.md";
|
|
275
|
+
const OPENCODE_APPEND_SYSTEM_PROMPT_PATH = ".opencode/agent-system-prompt.md";
|
|
234
276
|
const SAFE_WRITE_FLAGS = fsConstants.O_WRONLY |
|
|
235
277
|
fsConstants.O_CREAT |
|
|
236
278
|
fsConstants.O_EXCL |
|
|
@@ -575,6 +617,7 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
575
617
|
const profile = agentProfileSchema.parse(inputProfile);
|
|
576
618
|
const h = harness;
|
|
577
619
|
const skip = new Set(assertSkippableDimensions(opts.skip));
|
|
620
|
+
const binds = assertBindableLaunchControls(opts.binds);
|
|
578
621
|
const plan = {
|
|
579
622
|
harness: h,
|
|
580
623
|
files: [],
|
|
@@ -682,15 +725,60 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
682
725
|
}
|
|
683
726
|
}
|
|
684
727
|
}
|
|
685
|
-
// 4) PROMPT. A system prompt is a replacement,
|
|
686
|
-
//
|
|
728
|
+
// 4) PROMPT. A system prompt is a replacement, an appended system prompt is
|
|
729
|
+
// an addition on the SAME channel, and instructions are additive workspace
|
|
730
|
+
// context. Never lower one into another: substituting controls is a silent
|
|
731
|
+
// semantic change nothing in the run would reveal. Which harness owns which
|
|
732
|
+
// intent is the measured harnessSystemPromptIntents table in
|
|
733
|
+
// @tangle-network/agent-interface; the mechanics per owned cell live here.
|
|
734
|
+
//
|
|
735
|
+
// opencode registers every additive prompt file in ONE generated
|
|
736
|
+
// opencode.json `instructions` array (top-level JSON merge is last-wins per
|
|
737
|
+
// key, so two writers would clobber each other). Order is privilege order:
|
|
738
|
+
// the profile's explicit addition leads, project instructions follow.
|
|
739
|
+
const openCodeInstructionFiles = [];
|
|
687
740
|
if (profile.prompt?.systemPrompt !== undefined) {
|
|
688
|
-
if (h === "
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
741
|
+
if (h === "opencode") {
|
|
742
|
+
const systemPrompt = profile.prompt.systemPrompt;
|
|
743
|
+
if (systemPrompt.trim().length === 0) {
|
|
744
|
+
throw new Error("opencode cannot express an empty exact system-prompt replacement");
|
|
745
|
+
}
|
|
746
|
+
if (!binds.has("systemPrompt")) {
|
|
747
|
+
// opencode's exact replacement (`agent.<name>.prompt`) binds to the one
|
|
748
|
+
// agent selected at launch, which a plan-forwarding executor cannot
|
|
749
|
+
// guarantee — its only system-prompt replacement is per-agent. An
|
|
750
|
+
// executor that selects the plan's generated primary agent declares
|
|
751
|
+
// binds: ["systemPrompt"].
|
|
752
|
+
unsupported("systemPrompt", "opencode: only system-prompt replacement is per-agent (`agent.<name>.prompt` " +
|
|
753
|
+
"binds to the agent selected at launch, which a workspace plan cannot " +
|
|
754
|
+
'guarantee); an executor that applies the plan\'s "--agent" flag to its own ' +
|
|
755
|
+
'spawn declares binds: ["systemPrompt"]');
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
const primaryAgentName = `${OPENCODE_PRIMARY_AGENT_PREFIX}${sha256(systemPrompt).slice(0, 16)}`;
|
|
759
|
+
const primaryAgentPath = `.opencode/agents/${primaryAgentName}.md`;
|
|
760
|
+
if (profile.subagents?.[primaryAgentName] !== undefined) {
|
|
761
|
+
throw new Error(`opencode: subagent name "${primaryAgentName}" is reserved for the profile primary agent`);
|
|
762
|
+
}
|
|
763
|
+
plan.systemPrompt = systemPrompt;
|
|
764
|
+
add(primaryAgentPath, ["---", "mode: primary", "---", systemPrompt, ""].join("\n"));
|
|
765
|
+
plan.flags.push("--agent", primaryAgentName);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
else if (h === "claude-code" || h === "pi" || h === "prime") {
|
|
769
|
+
// Launch-argument channel: claude-code/pi `--system-prompt`; prime
|
|
770
|
+
// inherits the same flag pair from its pi line (fork cli/args.ts:139-143
|
|
771
|
+
// → resourceLoaderOptions main.ts:745-746, replacement deletes the
|
|
772
|
+
// built-in prompt: resource-loader.ts:482-485, system-prompt.ts:72-73).
|
|
692
773
|
plan.systemPrompt = profile.prompt.systemPrompt;
|
|
693
774
|
}
|
|
775
|
+
else if (h === "codex") {
|
|
776
|
+
// codex's replacement is a config file, never a launch-argument string:
|
|
777
|
+
// `model_instructions_file` becomes the request's entire instructions
|
|
778
|
+
// text (measured against codex 0.146.0).
|
|
779
|
+
add(CODEX_SYSTEM_PROMPT_PATH, profile.prompt.systemPrompt);
|
|
780
|
+
plan.flags.push("-c", `model_instructions_file=${CODEX_SYSTEM_PROMPT_PATH}`);
|
|
781
|
+
}
|
|
694
782
|
else if (h === "gemini") {
|
|
695
783
|
add(".gemini/system.md", profile.prompt.systemPrompt);
|
|
696
784
|
plan.env.GEMINI_SYSTEM_MD = "1";
|
|
@@ -699,6 +787,40 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
699
787
|
unsupported("systemPrompt", `${h}: no verified exact system-prompt replacement control`);
|
|
700
788
|
}
|
|
701
789
|
}
|
|
790
|
+
if (profile.prompt?.appendSystemPrompt !== undefined) {
|
|
791
|
+
const appendSystemPrompt = profile.prompt.appendSystemPrompt;
|
|
792
|
+
if (appendSystemPrompt.trim().length === 0) {
|
|
793
|
+
throw new Error("prompt.appendSystemPrompt must be non-empty text");
|
|
794
|
+
}
|
|
795
|
+
if (h === "claude-code" || h === "pi" || h === "prime") {
|
|
796
|
+
// `--append-system-prompt` leaves the harness's own prompt in place and
|
|
797
|
+
// adds this text after it (claude-code/pi measured; prime fork
|
|
798
|
+
// cli/args.ts:139-143, addition appends after the built-in prompt:
|
|
799
|
+
// system-prompt.ts:61,109-110).
|
|
800
|
+
plan.appendSystemPrompt = appendSystemPrompt;
|
|
801
|
+
}
|
|
802
|
+
else if (h === "opencode") {
|
|
803
|
+
// opencode composes config-declared `instructions[]` files into the same
|
|
804
|
+
// single role:"system" message as its built-in prompt, which stays in
|
|
805
|
+
// place — a real additive channel, distinct from the instructions
|
|
806
|
+
// surface only by position (registered first, below).
|
|
807
|
+
add(OPENCODE_APPEND_SYSTEM_PROMPT_PATH, appendSystemPrompt.endsWith("\n")
|
|
808
|
+
? appendSystemPrompt
|
|
809
|
+
: `${appendSystemPrompt}\n`);
|
|
810
|
+
openCodeInstructionFiles.push(OPENCODE_APPEND_SYSTEM_PROMPT_PATH);
|
|
811
|
+
}
|
|
812
|
+
else if (h === "codex") {
|
|
813
|
+
unsupported("systemPrompt", "codex: no verified additive system-prompt control — its AGENTS.md lands in a " +
|
|
814
|
+
"developer/user message, not the system channel");
|
|
815
|
+
}
|
|
816
|
+
else if (h === "gemini") {
|
|
817
|
+
unsupported("systemPrompt", "gemini: the only additive system-prompt channel is GEMINI.md memory, which IS " +
|
|
818
|
+
"the instructions surface — use prompt.instructions");
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
unsupported("systemPrompt", `${h}: no verified additive system-prompt control`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
702
824
|
const instr = [];
|
|
703
825
|
if (profile.prompt?.instructions?.length)
|
|
704
826
|
instr.push(...profile.prompt.instructions);
|
|
@@ -713,14 +835,16 @@ export function materializeProfile(inputProfile, harness, opts = {}) {
|
|
|
713
835
|
if (instr.length) {
|
|
714
836
|
const instructionPath = CONTEXT_FILE[h];
|
|
715
837
|
add(instructionPath, `${instr.join("\n\n")}\n`);
|
|
716
|
-
if (h === "opencode")
|
|
717
|
-
|
|
718
|
-
$schema: "https://opencode.ai/config.json",
|
|
719
|
-
instructions: [instructionPath],
|
|
720
|
-
}));
|
|
721
|
-
}
|
|
838
|
+
if (h === "opencode")
|
|
839
|
+
openCodeInstructionFiles.push(instructionPath);
|
|
722
840
|
// gemini reads AGENTS.md only via settings; we write GEMINI.md so no extra step.
|
|
723
841
|
}
|
|
842
|
+
if (openCodeInstructionFiles.length > 0) {
|
|
843
|
+
add("opencode.json", serializeWorkspacePlanJson({
|
|
844
|
+
$schema: "https://opencode.ai/config.json",
|
|
845
|
+
instructions: openCodeInstructionFiles,
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
724
848
|
// 5) ACTIVE EXECUTION CONTROLS. These alter the tools visible to the primary
|
|
725
849
|
// process and its permission policy, so they must be lowered into native
|
|
726
850
|
// launch/config inputs rather than merely retained in profile metadata.
|
|
@@ -836,6 +960,11 @@ export function materializeCandidateProfile(profile, harness, opts = {}) {
|
|
|
836
960
|
...(classicPlan.systemPrompt === undefined
|
|
837
961
|
? {}
|
|
838
962
|
: { systemPrompt: candidatePublicValue(classicPlan.systemPrompt) }),
|
|
963
|
+
...(classicPlan.appendSystemPrompt === undefined
|
|
964
|
+
? {}
|
|
965
|
+
: {
|
|
966
|
+
appendSystemPrompt: candidatePublicValue(classicPlan.appendSystemPrompt),
|
|
967
|
+
}),
|
|
839
968
|
unsupported: classicPlan.unsupported,
|
|
840
969
|
};
|
|
841
970
|
}
|
|
@@ -1229,6 +1358,9 @@ export function hashWorkspacePlan(plan) {
|
|
|
1229
1358
|
...(plan.systemPrompt === undefined
|
|
1230
1359
|
? {}
|
|
1231
1360
|
: { systemPrompt: plan.systemPrompt }),
|
|
1361
|
+
...(plan.appendSystemPrompt === undefined
|
|
1362
|
+
? {}
|
|
1363
|
+
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1232
1364
|
unsupported,
|
|
1233
1365
|
...(requires.length === 0 ? {} : { requires }),
|
|
1234
1366
|
});
|
|
@@ -1249,6 +1381,9 @@ export function agentCandidateProfilePlanMaterial(plan) {
|
|
|
1249
1381
|
...(plan.systemPrompt === undefined
|
|
1250
1382
|
? {}
|
|
1251
1383
|
: { systemPrompt: plan.systemPrompt }),
|
|
1384
|
+
...(plan.appendSystemPrompt === undefined
|
|
1385
|
+
? {}
|
|
1386
|
+
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1252
1387
|
unsupported: [...plan.unsupported].sort((left, right) => compareStrings(`${left.dimension}\0${left.reason}`, `${right.dimension}\0${right.reason}`)),
|
|
1253
1388
|
};
|
|
1254
1389
|
return agentCandidateProfilePlanMaterialSchema.parse(material);
|
|
@@ -1311,6 +1446,11 @@ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorks
|
|
|
1311
1446
|
...(plan.systemPrompt === undefined
|
|
1312
1447
|
? {}
|
|
1313
1448
|
: { systemPrompt: candidatePublicString(plan.systemPrompt) }),
|
|
1449
|
+
...(plan.appendSystemPrompt === undefined
|
|
1450
|
+
? {}
|
|
1451
|
+
: {
|
|
1452
|
+
appendSystemPrompt: candidatePublicString(plan.appendSystemPrompt),
|
|
1453
|
+
}),
|
|
1314
1454
|
unsupported: plan.unsupported,
|
|
1315
1455
|
}, workspaceDir);
|
|
1316
1456
|
return {
|
|
@@ -1325,6 +1465,9 @@ export function applyAgentCandidateWorkspacePlan(plan, workspaceDir, targetWorks
|
|
|
1325
1465
|
...(plan.systemPrompt === undefined
|
|
1326
1466
|
? {}
|
|
1327
1467
|
: { systemPrompt: plan.systemPrompt }),
|
|
1468
|
+
...(plan.appendSystemPrompt === undefined
|
|
1469
|
+
? {}
|
|
1470
|
+
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1328
1471
|
unsupported: [...plan.unsupported],
|
|
1329
1472
|
};
|
|
1330
1473
|
}
|
|
@@ -1412,6 +1555,9 @@ function resolvePublicWorkspacePlan(plan) {
|
|
|
1412
1555
|
...(plan.systemPrompt === undefined
|
|
1413
1556
|
? {}
|
|
1414
1557
|
: { systemPrompt: plan.systemPrompt }),
|
|
1558
|
+
...(plan.appendSystemPrompt === undefined
|
|
1559
|
+
? {}
|
|
1560
|
+
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1415
1561
|
};
|
|
1416
1562
|
}
|
|
1417
1563
|
async function resolvePrivateWorkspacePlan(plan, secretProvider) {
|
|
@@ -1480,6 +1626,9 @@ async function resolvePrivateWorkspacePlan(plan, secretProvider) {
|
|
|
1480
1626
|
...(plan.systemPrompt === undefined
|
|
1481
1627
|
? {}
|
|
1482
1628
|
: { systemPrompt: plan.systemPrompt }),
|
|
1629
|
+
...(plan.appendSystemPrompt === undefined
|
|
1630
|
+
? {}
|
|
1631
|
+
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1483
1632
|
};
|
|
1484
1633
|
}
|
|
1485
1634
|
function safeWorkspacePlanReceipt(plan, written, workspacePlanDigest = hashWorkspacePlan(plan)) {
|
|
@@ -1493,6 +1642,9 @@ function safeWorkspacePlanReceipt(plan, written, workspacePlanDigest = hashWorks
|
|
|
1493
1642
|
...(plan.systemPrompt === undefined
|
|
1494
1643
|
? {}
|
|
1495
1644
|
: { systemPrompt: plan.systemPrompt }),
|
|
1645
|
+
...(plan.appendSystemPrompt === undefined
|
|
1646
|
+
? {}
|
|
1647
|
+
: { appendSystemPrompt: plan.appendSystemPrompt }),
|
|
1496
1648
|
unsupported: plan.unsupported.map((entry) => ({ ...entry })),
|
|
1497
1649
|
written: [...written],
|
|
1498
1650
|
};
|
|
@@ -1613,6 +1765,9 @@ function applyResolvedWorkspacePlan(publicPlan, resolvedPlan, workspaceDir, opti
|
|
|
1613
1765
|
...(resolvedPlan.systemPrompt === undefined
|
|
1614
1766
|
? {}
|
|
1615
1767
|
: { systemPrompt: resolvedPlan.systemPrompt }),
|
|
1768
|
+
...(resolvedPlan.appendSystemPrompt === undefined
|
|
1769
|
+
? {}
|
|
1770
|
+
: { appendSystemPrompt: resolvedPlan.appendSystemPrompt }),
|
|
1616
1771
|
},
|
|
1617
1772
|
runtimeOnlyFiles: privateRuntimeFiles,
|
|
1618
1773
|
};
|
|
@@ -2214,6 +2369,19 @@ function materializeMcp(h, mcp, plan, add, unsupported) {
|
|
|
2214
2369
|
});
|
|
2215
2370
|
break;
|
|
2216
2371
|
}
|
|
2372
|
+
case "prime":
|
|
2373
|
+
// The fork's MCP config surface (`mcpServers` in <agentDir>/settings.json,
|
|
2374
|
+
// fork settings-manager.ts:98-120,219) IS read at startup, but a
|
|
2375
|
+
// user-declared server never becomes model-visible: the host McpManager
|
|
2376
|
+
// only registers OAuth providers and serves mcp.config/refresh
|
|
2377
|
+
// host-requests (fork core/mcp/mcp-manager.ts:62-124,156-181), skips
|
|
2378
|
+
// stdio servers entirely (mcp-manager.ts:73), and nothing generates a
|
|
2379
|
+
// skill or prompt section for it — materializing profile.mcp would be a
|
|
2380
|
+
// silently dropped control.
|
|
2381
|
+
unsupported("mcp", "prime: settings.json mcpServers never becomes model-visible (fork mcp-manager.ts:62-124 " +
|
|
2382
|
+
"registers OAuth providers only and skips stdio servers) — model-visible MCP is exactly " +
|
|
2383
|
+
"the fork's built-in catalog skills");
|
|
2384
|
+
break;
|
|
2217
2385
|
case "hermes":
|
|
2218
2386
|
unsupported("mcp", "hermes MCP is global config.yaml (mcp_servers), no cwd discovery — write to the hermes global config or pass a config flag");
|
|
2219
2387
|
break;
|
|
@@ -2418,6 +2586,9 @@ function materializeHooks(h, hooks, add, unsupported, plan) {
|
|
|
2418
2586
|
case "pi":
|
|
2419
2587
|
unsupported("hooks", "pi hooks are TS extensions (.pi/extensions/*.ts) — build an extension");
|
|
2420
2588
|
break;
|
|
2589
|
+
case "prime":
|
|
2590
|
+
unsupported("hooks", "prime hooks are TS extensions (fork core/extensions), not command hooks — build an extension");
|
|
2591
|
+
break;
|
|
2421
2592
|
case "hermes":
|
|
2422
2593
|
unsupported("hooks", "hermes project plugins do not expose an exact command-hook mapping");
|
|
2423
2594
|
break;
|
|
@@ -2678,6 +2849,16 @@ function materializeSubagent(h, name, sa, plan, add, unsupported) {
|
|
|
2678
2849
|
registerCodexAgent(plan, name, relPath, sa.description ?? name);
|
|
2679
2850
|
break;
|
|
2680
2851
|
}
|
|
2852
|
+
case "prime":
|
|
2853
|
+
// Prime subagents ARE expressible — as entries in the continual-harness
|
|
2854
|
+
// store at <agentDir>/harness/harness_state.json (fork refinement.ts:269-279,
|
|
2855
|
+
// re-read on every system-prompt build: agent-session.ts:7548-7554) — but
|
|
2856
|
+
// that store lives under the executor-owned agent dir, which no
|
|
2857
|
+
// workspace-relative plan file can reach. Executors that own the agent
|
|
2858
|
+
// dir seed it with seedPrimeProfileSubagents; the plan stays fail-closed
|
|
2859
|
+
// so a plan-forwarding caller can never believe the roster is in force.
|
|
2860
|
+
unsupported("subagents", `prime: subagents live in the agent-dir continual-harness store (<agentDir>/harness/harness_state.json), outside any workspace plan — seed "${name}" with seedPrimeProfileSubagents from the executor that owns the agent dir`);
|
|
2861
|
+
break;
|
|
2681
2862
|
default:
|
|
2682
2863
|
unsupported("subagents", `${h}: subagents have no cwd file (config/flag/global only); "${name}" not mounted`);
|
|
2683
2864
|
}
|
|
@@ -2721,6 +2902,12 @@ function materializeCommand(h, name, body, add, unsupported) {
|
|
|
2721
2902
|
case "pi":
|
|
2722
2903
|
add(`.pi/prompts/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
|
|
2723
2904
|
break;
|
|
2905
|
+
case "prime":
|
|
2906
|
+
// Auto-discovered project prompt templates: <cwd>/.prime/agent/prompts
|
|
2907
|
+
// (fork prompt-templates.ts:218 resolves CONFIG_DIR_NAME "prompts";
|
|
2908
|
+
// package-manager.ts:2176-2180 auto-adds the same projectDirs.prompts).
|
|
2909
|
+
add(`.prime/agent/prompts/${name}.md`, body.endsWith("\n") ? body : `${body}\n`);
|
|
2910
|
+
break;
|
|
2724
2911
|
case "kimi-code":
|
|
2725
2912
|
unsupported("commands", "kimi commands ARE skills (/skill:<name>) — provide as a skill instead");
|
|
2726
2913
|
break;
|
|
@@ -2731,3 +2918,347 @@ function materializeCommand(h, name, body, add, unsupported) {
|
|
|
2731
2918
|
unsupported("commands", `${h}: no cwd command path`);
|
|
2732
2919
|
}
|
|
2733
2920
|
}
|
|
2921
|
+
// ─── Prime agent-dir lowering ────────────────────────────────────────────────
|
|
2922
|
+
//
|
|
2923
|
+
// Prime (PrimeIntellect-ai/prime-agent v0.7.0, be9e2fa0) reads several profile
|
|
2924
|
+
// dimensions from its AGENT DIR — a per-process config root the EXECUTOR owns
|
|
2925
|
+
// (env PRIME_AGENT_CODING_AGENT_DIR, fork config.ts:498,521-527), which no
|
|
2926
|
+
// workspace-relative plan file can reach. The helpers below are that lowering,
|
|
2927
|
+
// shared so cli-bridge's prime backend and sdk-provider-prime consume one
|
|
2928
|
+
// implementation instead of hand-rolling byte-divergent copies. Every control
|
|
2929
|
+
// accepted here is backed by a verified read path in the pinned fork; every
|
|
2930
|
+
// other control is rejected loudly so a caller can never believe a
|
|
2931
|
+
// behavior-relevant setting is in force when it is not. The verified paths:
|
|
2932
|
+
//
|
|
2933
|
+
// - `prompt.systemPrompt` / `prompt.appendSystemPrompt` → `--system-prompt` /
|
|
2934
|
+
// `--append-system-prompt` (cli/args.ts:139-143). Both ride
|
|
2935
|
+
// `AgentSessionRuntimeConfig` (main.ts:643-644) through BOTH acp spawn
|
|
2936
|
+
// paths — the daemon-client create request (main.ts:1512-1520 →
|
|
2937
|
+
// daemon-mode.ts:1310/1478) and the in-process fallback — into
|
|
2938
|
+
// `resourceLoaderOptions` (main.ts:745-746), where the replacement DELETES
|
|
2939
|
+
// the built-in prompt (resource-loader.ts:482-485, system-prompt.ts:72-73)
|
|
2940
|
+
// and the addition appends after it (system-prompt.ts:61,109-110). Values
|
|
2941
|
+
// are materialized as FILES under the agent dir because the flag value is
|
|
2942
|
+
// read as a file when the path exists (resource-loader.ts:41-56), which
|
|
2943
|
+
// keeps literal-vs-path resolution deterministic.
|
|
2944
|
+
//
|
|
2945
|
+
// - `resources.skills` (inline refs) → `--skill <agentDir>/profile/skills`
|
|
2946
|
+
// (cli/args.ts:207-209 → main.ts:652 → agent-session-config.ts:67 →
|
|
2947
|
+
// additionalSkillPaths, resource-loader.ts:437-442). The loader recurses
|
|
2948
|
+
// into `<name>/SKILL.md` subdirs (skills.ts:328-372) and loaded skills are
|
|
2949
|
+
// rendered into the system prompt (agent-session.ts:4272-4291,
|
|
2950
|
+
// system-prompt.ts:87-89). The fork silently DROPS a SKILL.md without a
|
|
2951
|
+
// frontmatter description (skills.ts:416-419), so content is normalized
|
|
2952
|
+
// through {@link normalizeSkillMd}, which guarantees one.
|
|
2953
|
+
//
|
|
2954
|
+
// - `subagents` → `subagent` entries in the fork's continual-harness store at
|
|
2955
|
+
// `<agentDir>/harness/harness_state.json` (refinement.ts:269-279). The
|
|
2956
|
+
// store is re-read on every system-prompt build (agent-session.ts:4288,
|
|
2957
|
+
// 7548-7554) and subagent entries render as a task-shaped roster the model
|
|
2958
|
+
// spawns via `await rlm('<task>')` (refinement.ts:483-496); the kernel
|
|
2959
|
+
// reaches the same store through the host-set RLM_GLOBAL_HARNESS_STATE_DIR
|
|
2960
|
+
// (agent-session.ts:8805).
|
|
2961
|
+
//
|
|
2962
|
+
// Kept REJECTED with the fork evidence:
|
|
2963
|
+
//
|
|
2964
|
+
// - `mcp`: the fork's MCP config surface (`mcpServers` in
|
|
2965
|
+
// `<agentDir>/settings.json`, settings-manager.ts:98-120,219) IS read at
|
|
2966
|
+
// startup, but a user-declared server never becomes model-visible: the host
|
|
2967
|
+
// McpManager only registers OAuth providers and serves mcp.config/refresh
|
|
2968
|
+
// host-requests (core/mcp/mcp-manager.ts:62-124,156-181), skips stdio
|
|
2969
|
+
// servers entirely (mcp-manager.ts:73), and nothing generates a skill or
|
|
2970
|
+
// prompt section for it — model-visible MCP is exactly the built-in
|
|
2971
|
+
// linear/notion catalog skills (packages/coding-agent/skills/…). Wiring
|
|
2972
|
+
// `profile.mcp` into settings.json would materialize config the model can
|
|
2973
|
+
// never discover: a silently dropped control.
|
|
2974
|
+
//
|
|
2975
|
+
// - `prompt.instructions`: the fork's caller-instruction surface is the
|
|
2976
|
+
// workspace AGENTS.md/CLAUDE.md context file (resource-loader.ts:58-66),
|
|
2977
|
+
// i.e. caller workspace content this agent-dir lowering must not write
|
|
2978
|
+
// into. (`materializeProfile(profile, "prime")` lowers instructions to the
|
|
2979
|
+
// cwd AGENTS.md instead, which the fork walks from cwd:
|
|
2980
|
+
// resource-loader.ts:94-109.)
|
|
2981
|
+
/** Skill name rules enforced by the fork's Agent Skills loader (skills.ts:122-147). */
|
|
2982
|
+
const PRIME_SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2983
|
+
const PRIME_MAX_SKILL_NAME_LENGTH = 64;
|
|
2984
|
+
const PRIME_PROFILE_DIR_NAME = "profile";
|
|
2985
|
+
const PRIME_SKILLS_DIR_NAME = "skills";
|
|
2986
|
+
const PRIME_SYSTEM_PROMPT_FILE = "system-prompt.md";
|
|
2987
|
+
const PRIME_APPEND_SYSTEM_PROMPT_FILE = "append-system-prompt.md";
|
|
2988
|
+
/** Fork continual-harness store location under the agent dir (refinement.ts:269-279). */
|
|
2989
|
+
const PRIME_HARNESS_DIR_NAME = "harness";
|
|
2990
|
+
const PRIME_HARNESS_STATE_FILE = "harness_state.json";
|
|
2991
|
+
const PRIME_HARNESS_KINDS = ["prompt", "memory", "skill", "subagent"];
|
|
2992
|
+
/**
|
|
2993
|
+
* Provenance marker for entries pinned from the profile. Stale removal keys on
|
|
2994
|
+
* it, so agent-written (`source: "agent"`) and user-written entries are never
|
|
2995
|
+
* touched.
|
|
2996
|
+
*/
|
|
2997
|
+
const PRIME_PROFILE_ENTRY_SOURCE = "profile";
|
|
2998
|
+
function rejectPrimeControl(control, reason) {
|
|
2999
|
+
throw new Error(`prime does not support AgentProfile ${control}: ${reason}`);
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Validate and extract the profile controls the prime agent-dir lowering
|
|
3003
|
+
* materializes. Throws on any control (or control facet) the fork has no
|
|
3004
|
+
* verified read path for — partial acceptance would silently drop the rest.
|
|
3005
|
+
*/
|
|
3006
|
+
export function readPrimeProfileControls(profile) {
|
|
3007
|
+
const controls = { skills: [], subagents: [] };
|
|
3008
|
+
if (!profile)
|
|
3009
|
+
return controls;
|
|
3010
|
+
if (profile.harness !== undefined && profile.harness !== "prime") {
|
|
3011
|
+
throw new Error(`prime cannot run a profile pinned to harness "${profile.harness}" — ` +
|
|
3012
|
+
'only "prime" (or an unset harness preference) is accepted.');
|
|
3013
|
+
}
|
|
3014
|
+
if (profile.prompt) {
|
|
3015
|
+
const { systemPrompt, appendSystemPrompt, instructions } = profile.prompt;
|
|
3016
|
+
const knownPromptFields = new Set([
|
|
3017
|
+
"systemPrompt",
|
|
3018
|
+
"appendSystemPrompt",
|
|
3019
|
+
"instructions",
|
|
3020
|
+
]);
|
|
3021
|
+
const unknown = Object.entries(profile.prompt)
|
|
3022
|
+
.filter(([key, value]) => !knownPromptFields.has(key) && value !== undefined)
|
|
3023
|
+
.map(([key]) => key);
|
|
3024
|
+
if (unknown.length > 0) {
|
|
3025
|
+
rejectPrimeControl(`prompt fields: ${unknown.join(", ")}`, "no verified prime-agent read path exists for them.");
|
|
3026
|
+
}
|
|
3027
|
+
if (instructions !== undefined) {
|
|
3028
|
+
rejectPrimeControl("prompt.instructions", "prime-agent's caller-instruction surface is the workspace " +
|
|
3029
|
+
"AGENTS.md context file (fork resource-loader.ts:58-66), which is " +
|
|
3030
|
+
"caller workspace content this adapter must not write into. Use " +
|
|
3031
|
+
"prompt.appendSystemPrompt instead.");
|
|
3032
|
+
}
|
|
3033
|
+
if (systemPrompt !== undefined) {
|
|
3034
|
+
if (typeof systemPrompt !== "string" || systemPrompt.trim() === "") {
|
|
3035
|
+
rejectPrimeControl("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
|
|
3036
|
+
"install nothing.");
|
|
3037
|
+
}
|
|
3038
|
+
controls.systemPrompt = systemPrompt;
|
|
3039
|
+
}
|
|
3040
|
+
if (appendSystemPrompt !== undefined) {
|
|
3041
|
+
if (typeof appendSystemPrompt !== "string" ||
|
|
3042
|
+
appendSystemPrompt.trim() === "") {
|
|
3043
|
+
rejectPrimeControl("prompt.appendSystemPrompt", "must be non-empty text.");
|
|
3044
|
+
}
|
|
3045
|
+
controls.appendSystemPrompt = appendSystemPrompt;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (profile.resources) {
|
|
3049
|
+
const { skills, failOnError, ...unsupported } = profile.resources;
|
|
3050
|
+
const rejectedKeys = Object.entries(unsupported)
|
|
3051
|
+
.filter(([, value]) => value !== undefined)
|
|
3052
|
+
.map(([key]) => key);
|
|
3053
|
+
if (rejectedKeys.length > 0) {
|
|
3054
|
+
rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills has a verified prime-agent read path " +
|
|
3055
|
+
"(--skill dirs, fork resource-loader.ts:437-442).");
|
|
3056
|
+
}
|
|
3057
|
+
if (failOnError === false) {
|
|
3058
|
+
rejectPrimeControl("resources.failOnError: false", "this adapter always fails loud on a resource it cannot " +
|
|
3059
|
+
"materialize; lenient materialization is not implemented.");
|
|
3060
|
+
}
|
|
3061
|
+
const seen = new Set();
|
|
3062
|
+
for (const ref of skills ?? []) {
|
|
3063
|
+
if (ref.kind !== "inline") {
|
|
3064
|
+
rejectPrimeControl(`resources.skills ref "${"path" in ref ? ref.path : ""}"`, "only inline skill refs are accepted — resolve GitHub refs " +
|
|
3065
|
+
"(resolveAgentProfileResources) before handing the profile to " +
|
|
3066
|
+
"this adapter.");
|
|
3067
|
+
}
|
|
3068
|
+
const name = ref.name;
|
|
3069
|
+
if (!PRIME_SKILL_NAME_RE.test(name) ||
|
|
3070
|
+
name.length > PRIME_MAX_SKILL_NAME_LENGTH) {
|
|
3071
|
+
rejectPrimeControl(`resources.skills name "${name}"`, "prime-agent skill names must be lowercase a-z0-9 with single " +
|
|
3072
|
+
`hyphens, at most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
|
|
3073
|
+
}
|
|
3074
|
+
if (seen.has(name)) {
|
|
3075
|
+
rejectPrimeControl(`resources.skills name "${name}"`, "duplicate skill names collide in the fork's skill registry.");
|
|
3076
|
+
}
|
|
3077
|
+
seen.add(name);
|
|
3078
|
+
if (ref.content.trim() === "") {
|
|
3079
|
+
rejectPrimeControl(`resources.skills "${name}"`, "skill content is empty.");
|
|
3080
|
+
}
|
|
3081
|
+
controls.skills.push({ name, content: ref.content });
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
if (profile.subagents) {
|
|
3085
|
+
const usedIds = new Map();
|
|
3086
|
+
for (const [name, spec] of Object.entries(profile.subagents)) {
|
|
3087
|
+
if (name.trim() === "") {
|
|
3088
|
+
rejectPrimeControl("subagents", "a subagent name must be non-empty.");
|
|
3089
|
+
}
|
|
3090
|
+
const inexpressible = [
|
|
3091
|
+
["model", spec.model],
|
|
3092
|
+
["tools", spec.tools],
|
|
3093
|
+
["permissions", spec.permissions],
|
|
3094
|
+
["maxSteps", spec.maxSteps],
|
|
3095
|
+
]
|
|
3096
|
+
.filter(([, value]) => value !== undefined)
|
|
3097
|
+
.map(([key]) => key);
|
|
3098
|
+
if (inexpressible.length > 0) {
|
|
3099
|
+
rejectPrimeControl(`subagents["${name}"] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
|
|
3100
|
+
"guidance for `await rlm('<task>')` (fork refinement.ts:483-496) " +
|
|
3101
|
+
"and pins no model, tool set, permission, or step budget on the " +
|
|
3102
|
+
"spawned child.");
|
|
3103
|
+
}
|
|
3104
|
+
if (!spec.description?.trim() && !spec.prompt?.trim()) {
|
|
3105
|
+
rejectPrimeControl(`subagents["${name}"]`, "a spec needs a description and/or prompt — an empty entry gives " +
|
|
3106
|
+
"the model an empty roster line.");
|
|
3107
|
+
}
|
|
3108
|
+
const id = primeSubagentEntryId(name);
|
|
3109
|
+
const clash = usedIds.get(id);
|
|
3110
|
+
if (clash !== undefined) {
|
|
3111
|
+
rejectPrimeControl(`subagents["${name}"]`, `its id "${id}" collides with subagent "${clash}".`);
|
|
3112
|
+
}
|
|
3113
|
+
usedIds.set(id, name);
|
|
3114
|
+
controls.subagents.push({
|
|
3115
|
+
name,
|
|
3116
|
+
description: spec.description,
|
|
3117
|
+
prompt: spec.prompt,
|
|
3118
|
+
metadata: spec.metadata,
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
return controls;
|
|
3123
|
+
}
|
|
3124
|
+
/** Deterministic harness-state entry id for a profile subagent name. */
|
|
3125
|
+
export function primeSubagentEntryId(name) {
|
|
3126
|
+
const slug = name
|
|
3127
|
+
.toLowerCase()
|
|
3128
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
3129
|
+
.replace(/^-+|-+$/g, "");
|
|
3130
|
+
return `profile-${slug || "subagent"}`;
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Materialize the validated controls into `agentDir` and return the extra
|
|
3134
|
+
* spawn args that bind them. Runs before every spawn (same place models.json
|
|
3135
|
+
* is materialized), so a continual agent dir is re-pinned to the current
|
|
3136
|
+
* profile on each fresh prime process.
|
|
3137
|
+
*/
|
|
3138
|
+
export function materializePrimeProfileControls(agentDir, controls, warn) {
|
|
3139
|
+
const args = [];
|
|
3140
|
+
const profileDir = join(agentDir, PRIME_PROFILE_DIR_NAME);
|
|
3141
|
+
if (controls.systemPrompt !== undefined) {
|
|
3142
|
+
mkdirSync(profileDir, { recursive: true });
|
|
3143
|
+
const promptPath = join(profileDir, PRIME_SYSTEM_PROMPT_FILE);
|
|
3144
|
+
writeFileSync(promptPath, controls.systemPrompt, "utf8");
|
|
3145
|
+
args.push("--system-prompt", promptPath);
|
|
3146
|
+
}
|
|
3147
|
+
if (controls.appendSystemPrompt !== undefined) {
|
|
3148
|
+
mkdirSync(profileDir, { recursive: true });
|
|
3149
|
+
const appendPath = join(profileDir, PRIME_APPEND_SYSTEM_PROMPT_FILE);
|
|
3150
|
+
writeFileSync(appendPath, controls.appendSystemPrompt, "utf8");
|
|
3151
|
+
args.push("--append-system-prompt", appendPath);
|
|
3152
|
+
}
|
|
3153
|
+
if (controls.skills.length > 0) {
|
|
3154
|
+
const skillsRoot = join(profileDir, PRIME_SKILLS_DIR_NAME);
|
|
3155
|
+
for (const skill of controls.skills) {
|
|
3156
|
+
const skillDir = join(skillsRoot, skill.name);
|
|
3157
|
+
mkdirSync(skillDir, { recursive: true });
|
|
3158
|
+
writeFileSync(join(skillDir, "SKILL.md"), normalizeSkillMd(skill.name, skill.content), "utf8");
|
|
3159
|
+
}
|
|
3160
|
+
args.push("--skill", skillsRoot);
|
|
3161
|
+
}
|
|
3162
|
+
seedPrimeProfileSubagents(agentDir, controls.subagents, warn);
|
|
3163
|
+
return args;
|
|
3164
|
+
}
|
|
3165
|
+
function emptyPrimeHarnessState() {
|
|
3166
|
+
const entries = {};
|
|
3167
|
+
for (const kind of PRIME_HARNESS_KINDS)
|
|
3168
|
+
entries[kind] = {};
|
|
3169
|
+
return { schema: 1, entries, refinements: [] };
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Upsert profile subagent specs into the fork's global continual-harness
|
|
3173
|
+
* store and remove profile-sourced entries the current profile no longer
|
|
3174
|
+
* declares. Agent-written entries (`source: "agent"`) are never touched.
|
|
3175
|
+
*
|
|
3176
|
+
* A pre-existing unparsable store degrades to empty exactly like the fork's
|
|
3177
|
+
* own loader (refinement.ts:290-300 — "a corrupt state file must degrade to
|
|
3178
|
+
* empty rather than throw"), with a warning so the reset is visible.
|
|
3179
|
+
*/
|
|
3180
|
+
export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
|
|
3181
|
+
const statePath = join(agentDir, PRIME_HARNESS_DIR_NAME, PRIME_HARNESS_STATE_FILE);
|
|
3182
|
+
let state = emptyPrimeHarnessState();
|
|
3183
|
+
if (existsSync(statePath)) {
|
|
3184
|
+
try {
|
|
3185
|
+
const raw = JSON.parse(readFileSync(statePath, "utf8"));
|
|
3186
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
3187
|
+
const parsed = raw;
|
|
3188
|
+
state.schema =
|
|
3189
|
+
typeof parsed.schema === "number" ? parsed.schema : state.schema;
|
|
3190
|
+
for (const kind of PRIME_HARNESS_KINDS) {
|
|
3191
|
+
const records = parsed.entries?.[kind];
|
|
3192
|
+
if (records && typeof records === "object") {
|
|
3193
|
+
state.entries[kind] = { ...records };
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
if (Array.isArray(parsed.refinements)) {
|
|
3197
|
+
state.refinements = parsed.refinements;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
else {
|
|
3201
|
+
warn("prime: existing harness_state.json is not an object — reseeding", {
|
|
3202
|
+
statePath,
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
catch (err) {
|
|
3207
|
+
warn("prime: existing harness_state.json is unparsable — reseeding", {
|
|
3208
|
+
statePath,
|
|
3209
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3210
|
+
});
|
|
3211
|
+
state = emptyPrimeHarnessState();
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
else if (subagents.length === 0) {
|
|
3215
|
+
// Nothing to pin and nothing stale to remove.
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
const existing = state.entries.subagent ?? {};
|
|
3219
|
+
const desiredIds = new Set(subagents.map((s) => primeSubagentEntryId(s.name)));
|
|
3220
|
+
let changed = false;
|
|
3221
|
+
for (const [id, entry] of Object.entries(existing)) {
|
|
3222
|
+
if (entry?.source === PRIME_PROFILE_ENTRY_SOURCE && !desiredIds.has(id)) {
|
|
3223
|
+
delete existing[id];
|
|
3224
|
+
changed = true;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
const now = new Date().toISOString();
|
|
3228
|
+
for (const spec of subagents) {
|
|
3229
|
+
const id = primeSubagentEntryId(spec.name);
|
|
3230
|
+
const content = [spec.description?.trim(), spec.prompt?.trim()]
|
|
3231
|
+
.filter((part) => !!part)
|
|
3232
|
+
.join("\n\n");
|
|
3233
|
+
const previous = existing[id];
|
|
3234
|
+
if (previous &&
|
|
3235
|
+
previous.source === PRIME_PROFILE_ENTRY_SOURCE &&
|
|
3236
|
+
previous.title === spec.name &&
|
|
3237
|
+
previous.content === content) {
|
|
3238
|
+
continue;
|
|
3239
|
+
}
|
|
3240
|
+
existing[id] = {
|
|
3241
|
+
id,
|
|
3242
|
+
kind: "subagent",
|
|
3243
|
+
title: spec.name,
|
|
3244
|
+
content,
|
|
3245
|
+
path: "profile",
|
|
3246
|
+
scope: "global",
|
|
3247
|
+
reference: {},
|
|
3248
|
+
arguments: {},
|
|
3249
|
+
metadata: spec.metadata ?? {},
|
|
3250
|
+
source: PRIME_PROFILE_ENTRY_SOURCE,
|
|
3251
|
+
created_at: previous?.created_at ?? now,
|
|
3252
|
+
updated_at: now,
|
|
3253
|
+
version: previous && typeof previous.version === "number"
|
|
3254
|
+
? previous.version + 1
|
|
3255
|
+
: 1,
|
|
3256
|
+
};
|
|
3257
|
+
changed = true;
|
|
3258
|
+
}
|
|
3259
|
+
state.entries.subagent = existing;
|
|
3260
|
+
if (!changed)
|
|
3261
|
+
return;
|
|
3262
|
+
mkdirSync(join(agentDir, PRIME_HARNESS_DIR_NAME), { recursive: true });
|
|
3263
|
+
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
3264
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one canonicalization of a model id, shared by everything that writes a
|
|
3
|
+
* model id into an {@link AgentProfile} and everything that later compares one
|
|
4
|
+
* against it.
|
|
5
|
+
*
|
|
6
|
+
* A model id is qualified by prepending provider segments, and the same model
|
|
7
|
+
* is legitimately spelled at more than one qualification level as it travels:
|
|
8
|
+
* the caller sends `{ provider: "openai-compat", model: "deepseek/deepseek-v4-flash" }`,
|
|
9
|
+
* the profile records the qualified `openai-compat/deepseek/deepseek-v4-flash`,
|
|
10
|
+
* and the CLI is spawned with that qualified form. Two spellings, one model.
|
|
11
|
+
*
|
|
12
|
+
* Every writer and every reader must run both sides through here, so a
|
|
13
|
+
* comparison cannot mistake two spellings of one model for a conflict. Two
|
|
14
|
+
* independent copies of this rule are exactly how that bug arises.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Qualify `modelId` with `provider` — the canonical form of a model id.
|
|
18
|
+
*
|
|
19
|
+
* The already-qualified check is prefix-SPECIFIC rather than "already contains
|
|
20
|
+
* a slash": a router slug legitimately carries an upstream segment of its own
|
|
21
|
+
* (`deepseek/deepseek-v4-flash`, `zai/glm-4.7`), so a generic slash test would
|
|
22
|
+
* leave those unqualified, while an unconditional prefix would double-qualify
|
|
23
|
+
* an id that has already been through here.
|
|
24
|
+
*
|
|
25
|
+
* Idempotent by construction:
|
|
26
|
+
* `qualifyModelId(p, qualifyModelId(p, m)) === qualifyModelId(p, m)`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function qualifyModelId<T extends string | undefined>(provider: string | undefined, modelId: T): T;
|
|
29
|
+
/**
|
|
30
|
+
* Do two model ids name the same model?
|
|
31
|
+
*
|
|
32
|
+
* Both operands are qualified by `provider` before comparison, so the
|
|
33
|
+
* qualification level a caller happened to use is not a difference. Two
|
|
34
|
+
* different model names are still different — this narrows a comparison to the
|
|
35
|
+
* model's identity, it does not make the comparison permissive.
|
|
36
|
+
*
|
|
37
|
+
* `provider` is optional because a profile may carry a `model.default` that was
|
|
38
|
+
* qualified by a writer which did not also record `model.provider`. With no
|
|
39
|
+
* provider to qualify by, a bare id and its single-segment-qualified spelling
|
|
40
|
+
* are still the same model, which is the only tolerance the undeclared case gets.
|
|
41
|
+
*/
|
|
42
|
+
export declare function modelIdsMatch(a: string | undefined, b: string | undefined, provider?: string): boolean;
|
package/dist/model-id.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one canonicalization of a model id, shared by everything that writes a
|
|
3
|
+
* model id into an {@link AgentProfile} and everything that later compares one
|
|
4
|
+
* against it.
|
|
5
|
+
*
|
|
6
|
+
* A model id is qualified by prepending provider segments, and the same model
|
|
7
|
+
* is legitimately spelled at more than one qualification level as it travels:
|
|
8
|
+
* the caller sends `{ provider: "openai-compat", model: "deepseek/deepseek-v4-flash" }`,
|
|
9
|
+
* the profile records the qualified `openai-compat/deepseek/deepseek-v4-flash`,
|
|
10
|
+
* and the CLI is spawned with that qualified form. Two spellings, one model.
|
|
11
|
+
*
|
|
12
|
+
* Every writer and every reader must run both sides through here, so a
|
|
13
|
+
* comparison cannot mistake two spellings of one model for a conflict. Two
|
|
14
|
+
* independent copies of this rule are exactly how that bug arises.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Qualify `modelId` with `provider` — the canonical form of a model id.
|
|
18
|
+
*
|
|
19
|
+
* The already-qualified check is prefix-SPECIFIC rather than "already contains
|
|
20
|
+
* a slash": a router slug legitimately carries an upstream segment of its own
|
|
21
|
+
* (`deepseek/deepseek-v4-flash`, `zai/glm-4.7`), so a generic slash test would
|
|
22
|
+
* leave those unqualified, while an unconditional prefix would double-qualify
|
|
23
|
+
* an id that has already been through here.
|
|
24
|
+
*
|
|
25
|
+
* Idempotent by construction:
|
|
26
|
+
* `qualifyModelId(p, qualifyModelId(p, m)) === qualifyModelId(p, m)`.
|
|
27
|
+
*/
|
|
28
|
+
export function qualifyModelId(provider, modelId) {
|
|
29
|
+
if (!provider || !modelId)
|
|
30
|
+
return modelId;
|
|
31
|
+
if (modelId === provider || modelId.startsWith(`${provider}/`))
|
|
32
|
+
return modelId;
|
|
33
|
+
return `${provider}/${modelId}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Do two model ids name the same model?
|
|
37
|
+
*
|
|
38
|
+
* Both operands are qualified by `provider` before comparison, so the
|
|
39
|
+
* qualification level a caller happened to use is not a difference. Two
|
|
40
|
+
* different model names are still different — this narrows a comparison to the
|
|
41
|
+
* model's identity, it does not make the comparison permissive.
|
|
42
|
+
*
|
|
43
|
+
* `provider` is optional because a profile may carry a `model.default` that was
|
|
44
|
+
* qualified by a writer which did not also record `model.provider`. With no
|
|
45
|
+
* provider to qualify by, a bare id and its single-segment-qualified spelling
|
|
46
|
+
* are still the same model, which is the only tolerance the undeclared case gets.
|
|
47
|
+
*/
|
|
48
|
+
export function modelIdsMatch(a, b, provider) {
|
|
49
|
+
if (a === b)
|
|
50
|
+
return true;
|
|
51
|
+
if (a === undefined || b === undefined)
|
|
52
|
+
return false;
|
|
53
|
+
if (qualifyModelId(provider, a) === qualifyModelId(provider, b))
|
|
54
|
+
return true;
|
|
55
|
+
if (provider)
|
|
56
|
+
return false;
|
|
57
|
+
return ((!a.includes("/") && b.endsWith(`/${a}`)) ||
|
|
58
|
+
(!b.includes("/") && a.endsWith(`/${b}`)));
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@tangle-network/agent-interface": "0.
|
|
23
|
+
"@tangle-network/agent-interface": "0.45.0",
|
|
24
24
|
"@types/node": "25.6.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"vitest": "^4.1.10"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@tangle-network/agent-interface": ">=0.
|
|
29
|
+
"@tangle-network/agent-interface": ">=0.45.0 <0.46.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -p tsconfig.json",
|