@tangle-network/agent-profile-materialize 0.15.2 → 0.16.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.
Files changed (55) hide show
  1. package/dist/candidate-types.d.ts +32 -0
  2. package/dist/candidate-types.js +1 -0
  3. package/dist/candidate.d.ts +9 -0
  4. package/dist/candidate.js +279 -0
  5. package/dist/commands.d.ts +2 -0
  6. package/dist/commands.js +32 -0
  7. package/dist/control-maps.d.ts +34 -0
  8. package/dist/control-maps.js +185 -0
  9. package/dist/execution-controls.d.ts +5 -0
  10. package/dist/execution-controls.js +186 -0
  11. package/dist/harness.d.ts +81 -0
  12. package/dist/harness.js +167 -0
  13. package/dist/hooks.d.ts +6 -0
  14. package/dist/hooks.js +261 -0
  15. package/dist/index.d.ts +23 -623
  16. package/dist/index.js +14 -3733
  17. package/dist/mcp.d.ts +22 -0
  18. package/dist/mcp.js +244 -0
  19. package/dist/plan-application.d.ts +10 -0
  20. package/dist/plan-application.js +152 -0
  21. package/dist/plan-digest.d.ts +18 -0
  22. package/dist/plan-digest.js +90 -0
  23. package/dist/plan-json.d.ts +18 -0
  24. package/dist/plan-json.js +91 -0
  25. package/dist/plan-normalize.d.ts +13 -0
  26. package/dist/plan-normalize.js +156 -0
  27. package/dist/plan-prompt.d.ts +38 -0
  28. package/dist/plan-prompt.js +49 -0
  29. package/dist/plan-support.d.ts +9 -0
  30. package/dist/plan-support.js +22 -0
  31. package/dist/plan-types.d.ts +294 -0
  32. package/dist/plan-types.js +106 -0
  33. package/dist/prime-agent-dir.d.ts +47 -0
  34. package/dist/prime-agent-dir.js +164 -0
  35. package/dist/prime-profile.d.ts +88 -0
  36. package/dist/prime-profile.js +284 -0
  37. package/dist/profile-materializer.d.ts +12 -0
  38. package/dist/profile-materializer.js +59 -0
  39. package/dist/profile-plan.d.ts +14 -0
  40. package/dist/profile-plan.js +86 -0
  41. package/dist/profile-prompt.d.ts +4 -0
  42. package/dist/profile-prompt.js +105 -0
  43. package/dist/profile-support.d.ts +8 -0
  44. package/dist/profile-support.js +6 -0
  45. package/dist/resource-lowering.d.ts +7 -0
  46. package/dist/resource-lowering.js +103 -0
  47. package/dist/resource-resolution.d.ts +27 -0
  48. package/dist/resource-resolution.js +241 -0
  49. package/dist/subagents.d.ts +6 -0
  50. package/dist/subagents.js +133 -0
  51. package/dist/workspace-apply.d.ts +12 -0
  52. package/dist/workspace-apply.js +334 -0
  53. package/dist/workspace-files.d.ts +8 -0
  54. package/dist/workspace-files.js +46 -0
  55. package/package.json +3 -3
@@ -0,0 +1,156 @@
1
+ export function mergeJsonFiles(files) {
2
+ const byPath = new Map();
3
+ for (const f of files) {
4
+ const prev = byPath.get(f.relPath);
5
+ if (!prev) {
6
+ byPath.set(f.relPath, f);
7
+ continue;
8
+ }
9
+ if (f.relPath.endsWith(".json")) {
10
+ try {
11
+ const next = JSON.parse(f.content);
12
+ const merged = {
13
+ ...JSON.parse(prev.content),
14
+ ...next,
15
+ };
16
+ const replacedRoots = new Set(Object.keys(next));
17
+ const secretSlots = [
18
+ ...(prev.secretSlots ?? []).filter((slot) => typeof slot.path[0] === "string" &&
19
+ !replacedRoots.has(slot.path[0])),
20
+ ...(f.secretSlots ?? []),
21
+ ];
22
+ const { secretSlots: _previousSecretSlots, ...previousFile } = prev;
23
+ byPath.set(f.relPath, {
24
+ ...previousFile,
25
+ content: JSON.stringify(merged, null, 2),
26
+ ...(secretSlots.length > 0 ? { mode: PRIVATE_FILE_MODE } : {}),
27
+ ...(secretSlots.length === 0 ? {} : { secretSlots }),
28
+ });
29
+ continue;
30
+ }
31
+ catch {
32
+ /* fall through to last-wins */
33
+ }
34
+ }
35
+ byPath.set(f.relPath, f); // non-JSON or parse-fail: last wins
36
+ }
37
+ return [...byPath.values()];
38
+ }
39
+ export function valueAtJsonPath(root, path) {
40
+ let current = root;
41
+ for (const segment of path) {
42
+ if (current === null ||
43
+ typeof current !== "object" ||
44
+ !Object.hasOwn(current, segment)) {
45
+ return undefined;
46
+ }
47
+ current = current[segment];
48
+ }
49
+ return current;
50
+ }
51
+ export function normalizePlanFileSecretSlots(file) {
52
+ if (file.secretSlots === undefined)
53
+ return [];
54
+ if (file.source !== "generated" ||
55
+ !file.relPath.endsWith(".json") ||
56
+ file.secretSlots.length === 0) {
57
+ throw new Error(`Workspace secret slots require a generated JSON file: ${file.relPath}`);
58
+ }
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(file.content);
62
+ }
63
+ catch {
64
+ throw new Error(`Workspace secret slots require valid JSON content: ${file.relPath}`);
65
+ }
66
+ const seen = new Set();
67
+ return file.secretSlots.map((slot) => {
68
+ if (!Array.isArray(slot.path) || slot.path.length === 0) {
69
+ throw new Error(`Workspace secret slot path must be non-empty: ${file.relPath}`);
70
+ }
71
+ const path = slot.path.map((segment) => {
72
+ if ((typeof segment !== "string" && typeof segment !== "number") ||
73
+ (typeof segment === "number" &&
74
+ (!Number.isSafeInteger(segment) || segment < 0))) {
75
+ throw new Error(`Workspace secret slot path is invalid: ${file.relPath}`);
76
+ }
77
+ return segment;
78
+ });
79
+ const identity = JSON.stringify(path);
80
+ if (seen.has(identity)) {
81
+ throw new Error(`Duplicate workspace secret slot path: ${file.relPath} ${identity}`);
82
+ }
83
+ seen.add(identity);
84
+ if (valueAtJsonPath(parsed, path) !== null) {
85
+ throw new Error(`Workspace secret slot must point at a null placeholder: ${file.relPath} ${identity}`);
86
+ }
87
+ return {
88
+ path,
89
+ ref: agentProfileSecretRefSchema.parse(slot.ref),
90
+ };
91
+ });
92
+ }
93
+ export function normalizePlanFiles(files) {
94
+ const seen = new Set();
95
+ return files
96
+ .map((file) => {
97
+ const relPath = normalizeWorkspaceRelativePath(file.relPath);
98
+ if (seen.has(relPath)) {
99
+ throw new Error(`Duplicate workspace plan file: ${relPath}`);
100
+ }
101
+ seen.add(relPath);
102
+ if (typeof file.content !== "string") {
103
+ throw new Error(`Workspace file content must be a string: ${relPath}`);
104
+ }
105
+ const mode = validateFileMode(file.mode);
106
+ const secretSlots = normalizePlanFileSecretSlots(file);
107
+ if (secretSlots.length > 0 && (mode & 0o077) !== 0) {
108
+ throw new Error(`Workspace secret-bearing file must not grant group or other access: ${relPath}`);
109
+ }
110
+ return {
111
+ relPath,
112
+ content: file.content,
113
+ mode,
114
+ ownership: file.ownership ?? "shared",
115
+ secretSlots,
116
+ };
117
+ })
118
+ .sort((a, b) => compareStrings(a.relPath, b.relPath));
119
+ }
120
+ export function normalizeWorkspacePlanArgument(value) {
121
+ if (typeof value === "string")
122
+ return value;
123
+ if (value.kind !== "workspace-plan-argument-template" ||
124
+ !Array.isArray(value.segments) ||
125
+ value.segments.length === 0) {
126
+ throw new Error("Workspace plan argument template is malformed");
127
+ }
128
+ const segments = [];
129
+ for (const segment of value.segments) {
130
+ if (typeof segment === "string") {
131
+ const previous = segments.at(-1);
132
+ if (typeof previous === "string") {
133
+ segments[segments.length - 1] = previous + segment;
134
+ }
135
+ else if (segment.length > 0) {
136
+ segments.push(segment);
137
+ }
138
+ continue;
139
+ }
140
+ if (segment.kind !== "secret" ||
141
+ (segment.encoding !== "raw" && segment.encoding !== "json-string")) {
142
+ throw new Error("Workspace plan secret argument segment is malformed");
143
+ }
144
+ segments.push({
145
+ kind: "secret",
146
+ ref: agentProfileSecretRefSchema.parse(segment.ref),
147
+ encoding: segment.encoding,
148
+ });
149
+ }
150
+ if (segments.every((segment) => typeof segment === "string")) {
151
+ return segments.join("");
152
+ }
153
+ return { kind: "workspace-plan-argument-template", segments };
154
+ }
155
+ import { agentProfileSecretRefSchema } from "@tangle-network/agent-interface";
156
+ import { compareStrings, normalizeWorkspaceRelativePath, PRIVATE_FILE_MODE, validateFileMode, } from "./workspace-files.js";
@@ -0,0 +1,38 @@
1
+ export declare function promptIntentFields<In, Out>(source: {
2
+ systemPrompt?: In;
3
+ appendSystemPrompt?: In;
4
+ }, project: (value: In) => Out): {
5
+ systemPrompt?: Out;
6
+ appendSystemPrompt?: Out;
7
+ };
8
+ /** Identity projection for boundaries that pass the prompt intents through. */
9
+ export declare const samePromptIntentValue: <T>(value: T) => T;
10
+ /**
11
+ * The launcher-bound intent, projected onto an outgoing object and omitted when
12
+ * the source does not carry one.
13
+ *
14
+ * Beside {@link promptIntentFields} rather than inside it, because the two
15
+ * disagree on both ends: the candidate boundaries that helper also serves have
16
+ * no field for a binding — an {@link AgentCandidateWorkspacePlan} is built by a
17
+ * caller that declares none, so it can never hold one — and a value projection
18
+ * such as `candidatePublicString` has nothing to do to a structured binding.
19
+ * Every boundary that carries the plan's own prompt intents calls both.
20
+ */
21
+ export declare function systemPromptBindingField(source: {
22
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
23
+ }): {
24
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
25
+ };
26
+ /**
27
+ * The replacement text this launcher must bind to `control`, or `undefined`
28
+ * when the plan carries no launcher-bound intent.
29
+ *
30
+ * Throws for a binding addressed to a DIFFERENT control: the reader cannot
31
+ * execute it, and answering `undefined` would start the agent with the
32
+ * harness's own prompt still in force while nothing reported a problem — the
33
+ * silent downgrade the binding exists to prevent. This is the seam where an
34
+ * executor that cannot bind an intent says so, by name, instead of running a
35
+ * profile whose prompt never took effect.
36
+ */
37
+ export declare function takeSystemPromptBinding(spawn: Pick<WorkspacePlanSpawnInputs, "systemPromptBinding">, control: ExecutorSystemPromptBinding): string | undefined;
38
+ import type { ExecutorSystemPromptBinding, WorkspacePlanSpawnInputs, WorkspacePlanSystemPromptBinding } from "./plan-types.js";
@@ -0,0 +1,49 @@
1
+ export function promptIntentFields(source, project) {
2
+ return {
3
+ ...(source.systemPrompt === undefined
4
+ ? {}
5
+ : { systemPrompt: project(source.systemPrompt) }),
6
+ ...(source.appendSystemPrompt === undefined
7
+ ? {}
8
+ : { appendSystemPrompt: project(source.appendSystemPrompt) }),
9
+ };
10
+ }
11
+ /** Identity projection for boundaries that pass the prompt intents through. */
12
+ export const samePromptIntentValue = (value) => value;
13
+ /**
14
+ * The launcher-bound intent, projected onto an outgoing object and omitted when
15
+ * the source does not carry one.
16
+ *
17
+ * Beside {@link promptIntentFields} rather than inside it, because the two
18
+ * disagree on both ends: the candidate boundaries that helper also serves have
19
+ * no field for a binding — an {@link AgentCandidateWorkspacePlan} is built by a
20
+ * caller that declares none, so it can never hold one — and a value projection
21
+ * such as `candidatePublicString` has nothing to do to a structured binding.
22
+ * Every boundary that carries the plan's own prompt intents calls both.
23
+ */
24
+ export function systemPromptBindingField(source) {
25
+ return source.systemPromptBinding === undefined
26
+ ? {}
27
+ : { systemPromptBinding: { ...source.systemPromptBinding } };
28
+ }
29
+ /**
30
+ * The replacement text this launcher must bind to `control`, or `undefined`
31
+ * when the plan carries no launcher-bound intent.
32
+ *
33
+ * Throws for a binding addressed to a DIFFERENT control: the reader cannot
34
+ * execute it, and answering `undefined` would start the agent with the
35
+ * harness's own prompt still in force while nothing reported a problem — the
36
+ * silent downgrade the binding exists to prevent. This is the seam where an
37
+ * executor that cannot bind an intent says so, by name, instead of running a
38
+ * profile whose prompt never took effect.
39
+ */
40
+ export function takeSystemPromptBinding(spawn, control) {
41
+ const binding = spawn.systemPromptBinding;
42
+ if (!binding)
43
+ return undefined;
44
+ if (binding.control !== control) {
45
+ throw new Error(`Workspace plan carries a system-prompt ${binding.intent} bound to ` +
46
+ `${binding.control}, which this executor cannot apply; it binds ${control}`);
47
+ }
48
+ return binding.systemPrompt;
49
+ }
@@ -0,0 +1,9 @@
1
+ import type { Unsupported, WorkspacePlan } from "./plan-types.js";
2
+ export declare function formatUnsupported(unsupported: Unsupported[]): string;
3
+ /** Fail when a profile asks for controls the chosen harness cannot execute. */
4
+ export declare function assertWorkspacePlanSupported(plan: Pick<WorkspacePlan, "unsupported">): void;
5
+ /**
6
+ * Enforce supportability under the profile's failOnError policy.
7
+ * Strict mode throws; best-effort mode returns visible warnings.
8
+ */
9
+ export declare function enforceWorkspacePlanSupport(plan: Pick<WorkspacePlan, "unsupported">, failOnError: boolean | undefined): string[];
@@ -0,0 +1,22 @@
1
+ export function formatUnsupported(unsupported) {
2
+ return `Unable to materialize ${unsupported.length} profile resource(s):\n${unsupported
3
+ .map(({ dimension, reason }) => `- ${dimension}: ${reason}`)
4
+ .join("\n")}`;
5
+ }
6
+ /** Fail when a profile asks for controls the chosen harness cannot execute. */
7
+ export function assertWorkspacePlanSupported(plan) {
8
+ if (plan.unsupported.length > 0) {
9
+ throw new Error(formatUnsupported(plan.unsupported));
10
+ }
11
+ }
12
+ /**
13
+ * Enforce supportability under the profile's failOnError policy.
14
+ * Strict mode throws; best-effort mode returns visible warnings.
15
+ */
16
+ export function enforceWorkspacePlanSupport(plan, failOnError) {
17
+ if (failOnError !== false) {
18
+ assertWorkspacePlanSupported(plan);
19
+ return [];
20
+ }
21
+ return plan.unsupported.map(({ dimension, reason }) => `${dimension}: ${reason}`);
22
+ }
@@ -0,0 +1,294 @@
1
+ export type { HarnessId } from "./harness.js";
2
+ export interface PlanFile {
3
+ relPath: string;
4
+ content: string;
5
+ /** chmod mode (e.g. 0o755 for hook scripts); default 0o644. */
6
+ mode?: number;
7
+ /**
8
+ * Provenance of this entry. `"generated"` = the materializer synthesized the
9
+ * content itself (native harness config, merged settings, lowered prompt/MCP/
10
+ * hooks/subagents). `"user"` = the exact bytes of a profile-declared resource
11
+ * ref (`resources.files`/`tools`/`agents`/`skills`/`commands`), passed through
12
+ * verbatim at the path the profile author chose.
13
+ *
14
+ * A consumer that needs to reinterpret a SPECIFIC generated file (e.g. parse
15
+ * `opencode.json` as the synthetic native config) MUST gate on
16
+ * `source === "generated"` at that relPath — matching by relPath alone cannot
17
+ * tell a harness-authored config apart from a user resource mount that
18
+ * happens to share the same conventional path, and treating arbitrary user
19
+ * content as trusted generated config is a fail-open bug, not a fallback.
20
+ *
21
+ * Optional: callers that hand-construct a {@link WorkspacePlan} directly for
22
+ * {@link applyWorkspacePlan} (tests, ad hoc writes) are not materializer
23
+ * output and have no provenance to report. `materializeProfile` always sets
24
+ * this on every file it emits.
25
+ */
26
+ source?: "user" | "generated";
27
+ /**
28
+ * Who owns the PATH — which decides whether an existing file there may be
29
+ * replaced.
30
+ *
31
+ * `"materializer"` = a path this package invented. No harness convention
32
+ * names it and no user authors it, so a file already there can only be this
33
+ * materializer's own output from an earlier application into the same
34
+ * workspace, and replacing it is how turn two of a resumed session gets the
35
+ * current profile instead of the previous one.
36
+ *
37
+ * `"shared"` (the default, and every other file) = a path the user or the
38
+ * harness also owns — `CLAUDE.md`, `AGENTS.md`, `opencode.json`,
39
+ * `.gemini/system.md` are all documented, user-authorable names. An existing
40
+ * file there may be the caller's own work, so it is never replaced.
41
+ *
42
+ * Optional because hand-constructed plans have no path provenance to report;
43
+ * absent reads as `"shared"`, the safe answer.
44
+ */
45
+ ownership?: "materializer" | "shared";
46
+ /**
47
+ * Secret leaves in a generated JSON document. `content` contains `null` at
48
+ * each path until the private executor resolves the named reference. The
49
+ * public plan, its digest, and its receipt retain only these references.
50
+ */
51
+ secretSlots?: PlanFileSecretSlot[];
52
+ }
53
+ export interface PlanFileSecretSlot {
54
+ path: Array<string | number>;
55
+ ref: AgentProfileSecretRef;
56
+ }
57
+ export interface WorkspacePlanSecretSegment {
58
+ kind: "secret";
59
+ ref: AgentProfileSecretRef;
60
+ /** Encoding applied after private resolution. */
61
+ encoding: "raw" | "json-string";
62
+ }
63
+ /** A launch argument whose private leaves are resolved immediately before spawn. */
64
+ export interface WorkspacePlanArgumentTemplate {
65
+ kind: "workspace-plan-argument-template";
66
+ segments: Array<string | WorkspacePlanSecretSegment>;
67
+ }
68
+ export type WorkspacePlanArgument = string | WorkspacePlanArgumentTemplate;
69
+ /** Public bytes stay strings; secret references stay opaque until application. */
70
+ export type WorkspacePlanConfigValue = string | AgentProfileSecretRef;
71
+ export interface Unsupported {
72
+ dimension: "files" | "skills" | "mcp" | "hooks" | "subagents" | "commands" | "instructions" | "systemPrompt" | "appendSystemPrompt" | "connections" | "confidential" | "permissions" | "tools" | "modes" | "extensions";
73
+ reason: string;
74
+ }
75
+ /**
76
+ * The subset of {@link BINDABLE_LAUNCH_CONTROLS} the plan HANDS OVER rather than
77
+ * executes: controls a caller reaches only by composing the harness's own
78
+ * configuration, so no file, flag or env var the plan writes can touch them.
79
+ *
80
+ * - `opencode.agent-prompt` — opencode's `agent.<name>.prompt` config field,
81
+ * the one control that replaces its built-in provider prompt. It binds to a
82
+ * SINGLE agent, and the agent is chosen at launch. A caller that builds
83
+ * opencode's config itself and picks the agent there reads neither
84
+ * `plan.files` nor `plan.flags`, so for that one the plan names the control
85
+ * and hands over the text in {@link WorkspacePlan.systemPromptBinding}.
86
+ *
87
+ * Every other bindable control names a capability the plan still lowers itself
88
+ * once the caller has declared the plan's launch inputs really reach the spawn.
89
+ */
90
+ export declare const EXECUTOR_SYSTEM_PROMPT_BINDINGS: readonly ["opencode.agent-prompt"];
91
+ export type ExecutorSystemPromptBinding = (typeof EXECUTOR_SYSTEM_PROMPT_BINDINGS)[number];
92
+ export declare function isExecutorSystemPromptBinding(value: string): value is ExecutorSystemPromptBinding;
93
+ /**
94
+ * A system-prompt intent handed to the launcher that declared it owns the
95
+ * control, instead of being lowered into the workspace.
96
+ *
97
+ * Distinct from {@link WorkspacePlan.systemPrompt} — a launch ARGUMENT the
98
+ * executor hands to a harness flag — and from the file/flag/env lowerings the
99
+ * plan performs on its own. This names the exact control that must receive the
100
+ * text and which of the two intents it is, so the launcher cannot apply it to
101
+ * the wrong channel. Never written into an additive channel and never dropped:
102
+ * a caller that has not declared it owns `control` gets the plan's own lowering
103
+ * (or a refusal where the harness has no control at all) instead of this field.
104
+ */
105
+ export interface WorkspacePlanSystemPromptBinding {
106
+ /** Which of the two prompt intents this is — never inferred from presence. */
107
+ intent: "replace";
108
+ /** The exact launcher control that must receive `systemPrompt`. */
109
+ control: ExecutorSystemPromptBinding;
110
+ /** Exact text the control receives. */
111
+ systemPrompt: string;
112
+ }
113
+ /**
114
+ * The dimensions `materializeProfile`'s `skip` option honors.
115
+ *
116
+ * Every other dimension is materialized unconditionally: generated plan files are pushed through
117
+ * a single site that carries no dimension tag, so there is nothing to gate them on. Naming the
118
+ * honored set keeps `skip: ["hooks"]` a loud rejection instead of a silent no-op.
119
+ */
120
+ export declare const SKIPPABLE_DIMENSIONS: readonly ["mcp", "extensions"];
121
+ export type SkippableDimension = (typeof SKIPPABLE_DIMENSIONS)[number];
122
+ export declare function isSkippableDimension(value: string): value is SkippableDimension;
123
+ /** Reject a dimension `materializeProfile` would not actually skip, rather than ignoring it. */
124
+ export declare function assertSkippableDimensions(skip: readonly string[] | undefined): readonly SkippableDimension[];
125
+ /**
126
+ * Launcher controls an executor can declare it OWNS via `materializeProfile`'s
127
+ * `binds` option.
128
+ *
129
+ * Some harness controls live outside any workspace: opencode's exact
130
+ * system-prompt replacement (`agent.<name>.prompt`) binds to the ONE agent
131
+ * whoever starts the process selects, which a plan-forwarding executor cannot
132
+ * guarantee — so the plan refuses the intent (harnessSystemPromptIntents in
133
+ * @tangle-network/agent-interface answers `replace: false` for opencode).
134
+ *
135
+ * The two declarations describe the two executor shapes that CAN reach it, and
136
+ * each gets the lowering it can actually apply:
137
+ *
138
+ * - `systemPrompt` — the executor applies the plan's launch flags to the
139
+ * spawn it owns, so the plan writes the generated `mode: primary` agent and
140
+ * selects it with `--agent`.
141
+ * - `opencode.agent-prompt` — the executor composes opencode's own config and
142
+ * picks the agent there, reading neither plan files nor plan flags, so the
143
+ * plan hands the text over in {@link WorkspacePlan.systemPromptBinding}
144
+ * instead of writing a workspace agent that would never be selected.
145
+ *
146
+ * They are mutually exclusive claims about the same caller: declaring both is a
147
+ * caller bug, not a broader capability.
148
+ */
149
+ export declare const BINDABLE_LAUNCH_CONTROLS: readonly ["systemPrompt", "opencode.agent-prompt"];
150
+ export type BindableLaunchControl = (typeof BINDABLE_LAUNCH_CONTROLS)[number];
151
+ /** Reject a binding declaration this materializer has no lowering for, rather than ignoring it. */
152
+ export declare function assertBindableLaunchControls(binds: readonly string[] | undefined): ReadonlySet<BindableLaunchControl>;
153
+ /** The plan a caller writes into the run cwd, then applies to the spawn. */
154
+ export interface WorkspacePlan {
155
+ harness: HarnessId;
156
+ /** Files to write relative to the run workspace (cwd). */
157
+ files: PlanFile[];
158
+ /** Env vars to set on the spawned process (for harnesses needing them). */
159
+ env: Record<string, WorkspacePlanConfigValue>;
160
+ /** Extra CLI flags to pass (e.g. kimi --mcp-config-file <path>). */
161
+ flags: WorkspacePlanArgument[];
162
+ /**
163
+ * Exact text that REPLACES the harness's own system prompt, for harnesses
164
+ * whose replacement control takes it as a launch argument the executor
165
+ * supplies (claude-code `--system-prompt`, pi `--system-prompt`).
166
+ *
167
+ * Absent does NOT mean "no replacement was requested": a harness whose
168
+ * control reads a file (codex, gemini) carries the replacement in
169
+ * {@link WorkspacePlan.files} plus a flag or env var instead. Present means
170
+ * the executor MUST bind it to that harness's replacement control — never to
171
+ * an additive one, which would leave the built-in prompt in force.
172
+ */
173
+ systemPrompt?: string;
174
+ /**
175
+ * Exact text ADDED to the harness's own system prompt, which stays intact.
176
+ *
177
+ * Separate from {@link WorkspacePlan.systemPrompt} because the two are
178
+ * different intents against the same channel, and every harness that has one
179
+ * control may lack the other. An executor binds this ONLY to an additive
180
+ * control (claude-code `--append-system-prompt`, pi
181
+ * `--append-system-prompt`); there is no harness for which folding it into
182
+ * the replacement is correct, because that would delete the built-in prompt
183
+ * the caller asked to keep.
184
+ */
185
+ appendSystemPrompt?: string;
186
+ /**
187
+ * A prompt intent no workspace file, flag, or env var can execute, addressed
188
+ * to the launcher control that can — see
189
+ * {@link WorkspacePlanSystemPromptBinding}.
190
+ *
191
+ * Present ONLY when the caller declared it owns the named control, so a plan
192
+ * built by a caller that forwards it to someone else's launcher never carries
193
+ * one; that caller gets the intent in {@link WorkspacePlan.unsupported}
194
+ * instead. Whoever reads it must bind it or throw — {@link
195
+ * takeSystemPromptBinding} does both.
196
+ */
197
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
198
+ /** Dimensions requested but with NO cwd path on this harness — fail-closed record. */
199
+ unsupported: Unsupported[];
200
+ /**
201
+ * Prerequisites the workspace files CANNOT establish on their own.
202
+ *
203
+ * A plan is a set of files, an env map, and flags. Some harness capabilities need more than
204
+ * that to actually function: pi's MCP support is not built in, it arrives from an extension
205
+ * that must be installed into the harness's own package directory and permitted to load. No
206
+ * file written into a run workspace can make either true.
207
+ *
208
+ * Writing the config anyway and hoping is the failure this field exists to prevent: the
209
+ * harness starts, silently has no MCP, and the agent runs without the tools it was configured
210
+ * with while nothing reports a problem. So the plan states the requirement and the executor —
211
+ * which knows where its harness is installed and how it spawns — verifies or refuses it.
212
+ *
213
+ * Optional because this is an additive field on a published type: a plan built before it
214
+ * existed, or by a caller that does not construct one, is still a valid plan with no
215
+ * prerequisites. Every plan this package builds sets it.
216
+ */
217
+ requires?: PlanRequirement[];
218
+ }
219
+ /**
220
+ * One prerequisite a {@link WorkspacePlan} depends on but cannot create.
221
+ *
222
+ * Declarative on purpose: this package builds plans and never probes a filesystem, spawns a
223
+ * process, or reaches the network. The executor holds that context and does the checking.
224
+ */
225
+ export interface PlanRequirement {
226
+ /** The only kind so far: a harness extension/plugin that must be loaded for a dimension to work. */
227
+ kind: "harness-extension";
228
+ /** Dimension that produced the requirement, so a refusal can name what will not work. */
229
+ dimension: Unsupported["dimension"];
230
+ /** Package spec in the harness's own vocabulary, e.g. `npm:pi-mcp-adapter`. */
231
+ id: string;
232
+ /** Why the plan needs it — goes verbatim into the executor's refusal message. */
233
+ reason: string;
234
+ /** The exact command that satisfies it, so a failure is actionable without a doc lookup. */
235
+ remedy: string;
236
+ }
237
+ /**
238
+ * Proof of the profile workspace inputs applied before a harness is spawned.
239
+ * `workspacePlanDigest` covers only the {@link WorkspacePlan}; it is not an
240
+ * execution-plan digest and does not attest unrelated files in `workspaceDir`.
241
+ */
242
+ export interface WorkspacePlanReceipt {
243
+ workspacePlanDigest: string;
244
+ /** Public launch identity only; secret values never enter this receipt. */
245
+ env: Record<string, WorkspacePlanConfigValue>;
246
+ /** Public launch identity only; templates retain opaque secret references. */
247
+ flags: WorkspacePlanArgument[];
248
+ systemPrompt?: string;
249
+ appendSystemPrompt?: string;
250
+ /** The launcher-bound intent, so a receipt says which control owed the work. */
251
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
252
+ unsupported: Unsupported[];
253
+ written: string[];
254
+ }
255
+ /** Private values handed directly to the provider spawn, never to receipts. */
256
+ export interface WorkspacePlanSpawnInputs {
257
+ env: Record<string, string>;
258
+ flags: string[];
259
+ systemPrompt?: string;
260
+ appendSystemPrompt?: string;
261
+ /** Read it with {@link takeSystemPromptBinding}, which refuses a foreign control. */
262
+ systemPromptBinding?: WorkspacePlanSystemPromptBinding;
263
+ }
264
+ /** Result of applying a public plan at the private executor boundary. */
265
+ export interface WorkspacePlanApplication {
266
+ receipt: WorkspacePlanReceipt;
267
+ spawn: WorkspacePlanSpawnInputs;
268
+ /** Resolved generated files intentionally consumed in memory by the runtime. */
269
+ runtimeOnlyFiles: Record<string, string>;
270
+ }
271
+ /** The plan was applied, but one or more private staging files remain. */
272
+ export declare class WorkspacePlanCleanupError extends AggregateError {
273
+ readonly receipt: WorkspacePlanReceipt;
274
+ readonly temporaryPaths: readonly string[];
275
+ constructor(errors: readonly unknown[], receipt: WorkspacePlanReceipt, temporaryPaths: readonly string[]);
276
+ }
277
+ export interface ApplyWorkspacePlanOptions {
278
+ /** Plan files consumed in memory by the runtime and never persisted to cwd. */
279
+ runtimeOnlyFiles?: readonly string[];
280
+ /** Existing workspace paths are either exact idempotent inputs or forbidden. */
281
+ existingFiles?: "allow-identical" | "reject";
282
+ /**
283
+ * `workspacePlanDigest` of a plan a prior call already applied into this
284
+ * workspace. When it equals this plan's digest, application writes no files:
285
+ * the workspace holds the running agent's live state, and a file the agent
286
+ * edited or removed since the prior application stays exactly as the agent
287
+ * left it. Env, flags, prompt intents, and the receipt are still produced.
288
+ * A different digest applies normally, so a plan that changed mid-session
289
+ * still meets the existing-file refusal.
290
+ */
291
+ appliedPlanDigest?: string;
292
+ }
293
+ import type { AgentProfileSecretRef } from "@tangle-network/agent-interface";
294
+ import type { HarnessId } from "./harness.js";