@agentworkforce/cli 0.9.0 → 0.10.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/CHANGELOG.md +6 -0
- package/README.md +51 -8
- package/dist/cli.d.ts +45 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +177 -11
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +142 -4
- package/dist/cli.test.js.map +1 -1
- package/dist/local-personas.d.ts +30 -1
- package/dist/local-personas.d.ts.map +1 -1
- package/dist/local-personas.js +247 -15
- package/dist/local-personas.js.map +1 -1
- package/dist/local-personas.test.js +228 -1
- package/dist/local-personas.test.js.map +1 -1
- package/dist/persona-install.d.ts.map +1 -1
- package/dist/persona-install.js +105 -7
- package/dist/persona-install.js.map +1 -1
- package/dist/persona-install.test.js +43 -0
- package/dist/persona-install.test.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
14
|
- Persona source config supports `defaultCreateTarget` for the implicit create target.
|
|
15
15
|
- `--version`/`-v` now prints the CLI package version.
|
|
16
16
|
|
|
17
|
+
## [0.10.0] - 2026-05-08
|
|
18
|
+
|
|
19
|
+
### Released
|
|
20
|
+
|
|
21
|
+
- v0.10.0
|
|
22
|
+
|
|
17
23
|
## [0.8.0] - 2026-05-07
|
|
18
24
|
|
|
19
25
|
### Added
|
package/README.md
CHANGED
|
@@ -515,6 +515,10 @@ library has no `extends`.
|
|
|
515
515
|
},
|
|
516
516
|
"env": { … }, // union, local wins per key
|
|
517
517
|
"mcpServers": { … }, // union by server name, local wins per key
|
|
518
|
+
"mount": { // Relayfile file scope; pattern arrays append
|
|
519
|
+
"ignoredPatterns": ["…"],
|
|
520
|
+
"readonlyPatterns": ["…"]
|
|
521
|
+
},
|
|
518
522
|
"permissions": { // allow/deny union (dedup), mode replaces
|
|
519
523
|
"allow": ["…"], "deny": ["…"], "mode": "default"
|
|
520
524
|
},
|
|
@@ -617,6 +621,9 @@ Use this when you are not extending an existing persona:
|
|
|
617
621
|
"deny": ["Bash(npm publish *)"],
|
|
618
622
|
"mode": "default"
|
|
619
623
|
},
|
|
624
|
+
"mount": {
|
|
625
|
+
"readonlyPatterns": ["*"]
|
|
626
|
+
},
|
|
620
627
|
"tiers": {
|
|
621
628
|
"best": {
|
|
622
629
|
"harness": "codex",
|
|
@@ -729,12 +736,39 @@ either a literal string or an env reference. Two forms:
|
|
|
729
736
|
Secrets therefore stay in your shell/keychain, not in files on disk — local
|
|
730
737
|
persona JSON remains commit-safe as long as you only use references.
|
|
731
738
|
|
|
739
|
+
## Relayfile mount rules
|
|
740
|
+
|
|
741
|
+
Interactive `claude` and `opencode` sessions run inside a Relayfile mount by
|
|
742
|
+
default. File visibility and writability are controlled by the persona's
|
|
743
|
+
`mount` block plus project-level dotfiles:
|
|
744
|
+
|
|
745
|
+
```jsonc
|
|
746
|
+
{
|
|
747
|
+
"mount": {
|
|
748
|
+
"ignoredPatterns": ["secrets/**", ".env*"],
|
|
749
|
+
"readonlyPatterns": [
|
|
750
|
+
"*",
|
|
751
|
+
"!docs/",
|
|
752
|
+
"!docs/**"
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- `ignoredPatterns` are omitted from the mount entirely.
|
|
759
|
+
- `readonlyPatterns` are copied into the mount but edits do not sync back.
|
|
760
|
+
- Patterns use gitignore semantics, so later `!` negations can reopen paths.
|
|
761
|
+
- Persona patterns append to inherited persona patterns. At launch, the CLI
|
|
762
|
+
also merges project-root `.agentignore`, `.agentreadonly`,
|
|
763
|
+
`.<personaId>.agentignore`, and `.<personaId>.agentreadonly`.
|
|
764
|
+
|
|
732
765
|
## Permissions
|
|
733
766
|
|
|
734
767
|
A persona can declare which tool calls the harness should auto-approve, block,
|
|
735
768
|
or gate via a permission mode. Skip the approval prompts for trusted tools
|
|
736
769
|
(e.g. a persona's own MCP server); keep them on for anything you want to
|
|
737
|
-
eyeball.
|
|
770
|
+
eyeball. File visibility and writability are not defined here; use Relayfile
|
|
771
|
+
mount rules (`.agentignore` / `.agentreadonly`) for that.
|
|
738
772
|
|
|
739
773
|
```jsonc
|
|
740
774
|
{
|
|
@@ -747,9 +781,8 @@ eyeball.
|
|
|
747
781
|
```
|
|
748
782
|
|
|
749
783
|
- **Tool patterns** are passed through verbatim; use the harness's native
|
|
750
|
-
grammar. For Claude Code: `Bash(<pattern>)`, `
|
|
751
|
-
|
|
752
|
-
(specific tool).
|
|
784
|
+
grammar. For Claude Code: `Bash(<pattern>)`, `mcp__<server>` (all tools
|
|
785
|
+
from that server), `mcp__<server>__<tool>` (specific tool).
|
|
753
786
|
- **Harness support today:** only `claude` is wired (flags: `--allowedTools`,
|
|
754
787
|
`--disallowedTools`, `--permission-mode`). codex and opencode emit a
|
|
755
788
|
warning and fall back to their defaults when `permissions` is set.
|
|
@@ -922,13 +955,23 @@ stage dir conflicts with something else (network filesystem, read-only
|
|
|
922
955
|
|
|
923
956
|
By default, claude and opencode interactive sessions run inside a
|
|
924
957
|
[`@relayfile/local-mount`](https://www.npmjs.com/package/@relayfile/local-mount)
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
958
|
+
mount that hides repo-level harness configuration from the session, applies
|
|
959
|
+
the persona `mount` block plus Relayfile `.agentignore` / `.agentreadonly`
|
|
960
|
+
rules, and routes skill-install writes into the sandbox — so the model sees
|
|
961
|
+
persona context + user-level context, and only the project files the mount
|
|
962
|
+
exposes. Codex sessions never mount (no harness-side support).
|
|
929
963
|
|
|
930
964
|
`--install-in-repo` opts out and runs against the real cwd.
|
|
931
965
|
|
|
966
|
+
The CLI reads these files from the project root before creating the mount:
|
|
967
|
+
|
|
968
|
+
| File | Effect |
|
|
969
|
+
| --- | --- |
|
|
970
|
+
| `.agentignore` | Hide matching files for every persona. |
|
|
971
|
+
| `.agentreadonly` | Copy matching files into the mount as read-only and skip syncing their edits back. |
|
|
972
|
+
| `.<personaId>.agentignore` | Hide matching files only for that persona. |
|
|
973
|
+
| `.<personaId>.agentreadonly` | Make matching files read-only only for that persona. |
|
|
974
|
+
|
|
932
975
|
**What's hidden (gitignore semantics, at any depth):**
|
|
933
976
|
|
|
934
977
|
For claude:
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type Harness } from '@agentworkforce/workload-router';
|
|
2
|
+
import { type Harness, type PersonaMount, type PersonaSelection, type SidecarMdMode } from '@agentworkforce/workload-router';
|
|
3
3
|
export declare const CLI_VERSION: string;
|
|
4
4
|
export declare const CREATE_SELECTOR = "persona-maker@best";
|
|
5
5
|
/**
|
|
@@ -40,7 +40,7 @@ export declare function assertSafeRelativePath(relPath: string): void;
|
|
|
40
40
|
* Applied by `@relayfile/local-mount` with gitignore semantics, so bare names
|
|
41
41
|
* match at any depth in the project tree (e.g. `.claude` hides both
|
|
42
42
|
* `./.claude/` and `./packages/foo/.claude/`). */
|
|
43
|
-
export declare const CLEAN_IGNORED_PATTERNS: readonly ["CLAUDE.md", "CLAUDE.local.md", ".claude", ".mcp.json"];
|
|
43
|
+
export declare const CLEAN_IGNORED_PATTERNS: readonly ["CLAUDE.md", "CLAUDE.local.md", ".claude", ".mcp.json", "AGENTS.md"];
|
|
44
44
|
/**
|
|
45
45
|
* Skill-install artifacts that should never be copied into the mount nor
|
|
46
46
|
* synced back to the real repo. Applied to non-claude interactive sessions
|
|
@@ -51,7 +51,18 @@ export declare const CLEAN_IGNORED_PATTERNS: readonly ["CLAUDE.md", "CLAUDE.loca
|
|
|
51
51
|
* Claude sessions use `installRoot` for out-of-repo staging instead, so
|
|
52
52
|
* these patterns don't apply there.
|
|
53
53
|
*/
|
|
54
|
-
export declare const SKILL_INSTALL_IGNORED_PATTERNS: readonly [".agents", ".claude/skills", ".factory/skills", ".kiro/skills", "skills", ".opencode", ".skills", "prpm.lock", "skills-lock.json"];
|
|
54
|
+
export declare const SKILL_INSTALL_IGNORED_PATTERNS: readonly [".agents", ".claude/skills", ".factory/skills", ".kiro/skills", "skills", ".opencode", ".skills", "prpm.lock", "skills-lock.json", "AGENTS.md"];
|
|
55
|
+
export interface RelayfileMountPatterns {
|
|
56
|
+
ignoredPatterns: string[];
|
|
57
|
+
readonlyPatterns: string[];
|
|
58
|
+
}
|
|
59
|
+
export declare function buildRelayfileMountPatterns(input: {
|
|
60
|
+
projectDir: string;
|
|
61
|
+
personaId: string;
|
|
62
|
+
harness: Harness;
|
|
63
|
+
mount?: PersonaMount;
|
|
64
|
+
configFilePaths?: readonly string[];
|
|
65
|
+
}): RelayfileMountPatterns;
|
|
55
66
|
/**
|
|
56
67
|
* Build the block appended to `<mount>/.git/info/exclude` so untracked-and-
|
|
57
68
|
* hidden files (e.g. `.claude/skills/` materialized by skill installs, or
|
|
@@ -78,6 +89,37 @@ export declare function buildMountGitExcludeBlock(patterns: readonly string[]):
|
|
|
78
89
|
* writes here are sandboxed and never leak to the user's main checkout.
|
|
79
90
|
*/
|
|
80
91
|
export declare function configureGitForMount(mountDir: string, patterns: readonly string[]): void;
|
|
92
|
+
/**
|
|
93
|
+
* Persona-supplied sidecar markdown materialized into a sandbox mount.
|
|
94
|
+
* Pure data carrier — `runInteractive` translates it into the on-disk
|
|
95
|
+
* write inside `onBeforeLaunch` (mount path) and warns/skips when the
|
|
96
|
+
* harness has no mount (codex / `--install-in-repo`).
|
|
97
|
+
*/
|
|
98
|
+
export interface ResolvedSidecar {
|
|
99
|
+
/** Filename inside the mount: `CLAUDE.md` (claude) or `AGENTS.md` (opencode). */
|
|
100
|
+
mountFile: 'CLAUDE.md' | 'AGENTS.md';
|
|
101
|
+
/** Persona-author content. Already inlined for built-ins; read from disk for local. */
|
|
102
|
+
personaContent: string;
|
|
103
|
+
mode: SidecarMdMode;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the sidecar for a given selection + harness, returning the
|
|
107
|
+
* persona-author content the runtime should materialize into the mount.
|
|
108
|
+
* Returns `{}` when no sidecar applies (no path/content set, or harness
|
|
109
|
+
* doesn't support sidecar files at all). Read errors surface as a warning
|
|
110
|
+
* string so the caller can drop the sidecar gracefully rather than
|
|
111
|
+
* failing the whole session.
|
|
112
|
+
*/
|
|
113
|
+
export declare function loadSidecarForSelection(selection: PersonaSelection): {
|
|
114
|
+
sidecar?: ResolvedSidecar;
|
|
115
|
+
warning?: string;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Compute the bytes to write into the mount for a sidecar. In `extend`
|
|
119
|
+
* mode, prepends the user's real-cwd file (if any) joined to the persona
|
|
120
|
+
* content with `\n\n---\n\n`. Pure — exposed for unit tests.
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildSidecarBody(sidecar: ResolvedSidecar, realCwdDir: string): string;
|
|
81
123
|
/**
|
|
82
124
|
* Decide whether to run the interactive session inside a
|
|
83
125
|
* `@relayfile/local-mount` sandbox.
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAgBA,OAAO,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -5,9 +5,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync,
|
|
|
5
5
|
import { constants, homedir } from 'node:os';
|
|
6
6
|
import { dirname, isAbsolute, join, resolve as resolvePath } from 'node:path';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import { HARNESS_VALUES, PERSONA_TAGS, PERSONA_TIERS, personaCatalog, routingProfiles, useSelection } from '@agentworkforce/workload-router';
|
|
8
|
+
import { HARNESS_VALUES, PERSONA_TAGS, PERSONA_TIERS, personaCatalog, resolveSidecar, routingProfiles, useSelection } from '@agentworkforce/workload-router';
|
|
9
9
|
import { buildInteractiveSpec, detectHarnesses, formatDropWarnings, MissingPersonaInputError, renderPersonaInputs, resolvePersonaInputs, resolveMcpServersLenient, resolveStringMapLenient } from '@agentworkforce/harness-kit';
|
|
10
|
-
import { launchOnMount } from '@relayfile/local-mount';
|
|
10
|
+
import { launchOnMount, readAgentDotfiles } from '@relayfile/local-mount';
|
|
11
11
|
import ora from 'ora';
|
|
12
12
|
import { buildPersonaSourceDirectories, defaultCwdPersonaDir, loadLocalPersonas, loadPersonaSourceConfig, normalizePersonaDir, savePersonaSourceConfig } from './local-personas.js';
|
|
13
13
|
import { installPersonas } from './persona-install.js';
|
|
@@ -187,6 +187,7 @@ function buildSelection(spec, tier, kind) {
|
|
|
187
187
|
...rawRuntime,
|
|
188
188
|
systemPrompt: resolveSystemPromptPlaceholders(rawRuntime.systemPrompt, rawRuntime.harness)
|
|
189
189
|
};
|
|
190
|
+
const sidecar = resolveSidecar(spec, tier);
|
|
190
191
|
return {
|
|
191
192
|
personaId: spec.id,
|
|
192
193
|
tier,
|
|
@@ -196,7 +197,18 @@ function buildSelection(spec, tier, kind) {
|
|
|
196
197
|
...(spec.inputs ? { inputs: spec.inputs } : {}),
|
|
197
198
|
...(spec.env ? { env: spec.env } : {}),
|
|
198
199
|
...(spec.mcpServers ? { mcpServers: spec.mcpServers } : {}),
|
|
199
|
-
...(spec.permissions ? { permissions: spec.permissions } : {})
|
|
200
|
+
...(spec.permissions ? { permissions: spec.permissions } : {}),
|
|
201
|
+
...(spec.mount ? { mount: spec.mount } : {}),
|
|
202
|
+
...(sidecar.claudeMd ? { claudeMd: sidecar.claudeMd } : {}),
|
|
203
|
+
...(sidecar.claudeMdContent ? { claudeMdContent: sidecar.claudeMdContent } : {}),
|
|
204
|
+
...(sidecar.claudeMd || sidecar.claudeMdContent
|
|
205
|
+
? { claudeMdMode: sidecar.claudeMdMode }
|
|
206
|
+
: {}),
|
|
207
|
+
...(sidecar.agentsMd ? { agentsMd: sidecar.agentsMd } : {}),
|
|
208
|
+
...(sidecar.agentsMdContent ? { agentsMdContent: sidecar.agentsMdContent } : {}),
|
|
209
|
+
...(sidecar.agentsMd || sidecar.agentsMdContent
|
|
210
|
+
? { agentsMdMode: sidecar.agentsMdMode }
|
|
211
|
+
: {})
|
|
200
212
|
};
|
|
201
213
|
}
|
|
202
214
|
function emitDropWarnings(lines) {
|
|
@@ -368,7 +380,12 @@ export const CLEAN_IGNORED_PATTERNS = [
|
|
|
368
380
|
'CLAUDE.md',
|
|
369
381
|
'CLAUDE.local.md',
|
|
370
382
|
'.claude',
|
|
371
|
-
'.mcp.json'
|
|
383
|
+
'.mcp.json',
|
|
384
|
+
// Per-persona AGENTS.md sidecars get materialized into the mount when
|
|
385
|
+
// running under opencode; without this the user's real-cwd AGENTS.md
|
|
386
|
+
// would copy in (masking the persona content) and writes from
|
|
387
|
+
// onBeforeLaunch would sync back out.
|
|
388
|
+
'AGENTS.md'
|
|
372
389
|
];
|
|
373
390
|
/**
|
|
374
391
|
* Skill-install artifacts that should never be copied into the mount nor
|
|
@@ -392,8 +409,32 @@ export const SKILL_INSTALL_IGNORED_PATTERNS = [
|
|
|
392
409
|
'.skills',
|
|
393
410
|
// provider lockfiles written at the repo root
|
|
394
411
|
'prpm.lock',
|
|
395
|
-
'skills-lock.json'
|
|
412
|
+
'skills-lock.json',
|
|
413
|
+
// Per-persona AGENTS.md sidecars (opencode harness) get materialized
|
|
414
|
+
// into the mount; hide so the real-cwd AGENTS.md isn't copied in and
|
|
415
|
+
// the persona-written copy doesn't sync back out.
|
|
416
|
+
'AGENTS.md'
|
|
396
417
|
];
|
|
418
|
+
export function buildRelayfileMountPatterns(input) {
|
|
419
|
+
const dotfiles = readAgentDotfiles(input.projectDir, {
|
|
420
|
+
agentName: input.personaId
|
|
421
|
+
});
|
|
422
|
+
const builtInIgnored = input.harness === 'claude'
|
|
423
|
+
? CLEAN_IGNORED_PATTERNS
|
|
424
|
+
: SKILL_INSTALL_IGNORED_PATTERNS;
|
|
425
|
+
return {
|
|
426
|
+
ignoredPatterns: [
|
|
427
|
+
...dotfiles.ignoredPatterns,
|
|
428
|
+
...(input.mount?.ignoredPatterns ?? []),
|
|
429
|
+
...builtInIgnored,
|
|
430
|
+
...(input.configFilePaths ?? [])
|
|
431
|
+
],
|
|
432
|
+
readonlyPatterns: [
|
|
433
|
+
...dotfiles.readonlyPatterns,
|
|
434
|
+
...(input.mount?.readonlyPatterns ?? [])
|
|
435
|
+
]
|
|
436
|
+
};
|
|
437
|
+
}
|
|
397
438
|
/**
|
|
398
439
|
* Build the block appended to `<mount>/.git/info/exclude` so untracked-and-
|
|
399
440
|
* hidden files (e.g. `.claude/skills/` materialized by skill installs, or
|
|
@@ -465,6 +506,96 @@ export function configureGitForMount(mountDir, patterns) {
|
|
|
465
506
|
}
|
|
466
507
|
}
|
|
467
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Resolve the sidecar for a given selection + harness, returning the
|
|
511
|
+
* persona-author content the runtime should materialize into the mount.
|
|
512
|
+
* Returns `{}` when no sidecar applies (no path/content set, or harness
|
|
513
|
+
* doesn't support sidecar files at all). Read errors surface as a warning
|
|
514
|
+
* string so the caller can drop the sidecar gracefully rather than
|
|
515
|
+
* failing the whole session.
|
|
516
|
+
*/
|
|
517
|
+
export function loadSidecarForSelection(selection) {
|
|
518
|
+
const harness = selection.runtime.harness;
|
|
519
|
+
if (harness !== 'claude' && harness !== 'opencode')
|
|
520
|
+
return {};
|
|
521
|
+
if (harness === 'claude') {
|
|
522
|
+
if (selection.claudeMdContent) {
|
|
523
|
+
return {
|
|
524
|
+
sidecar: {
|
|
525
|
+
mountFile: 'CLAUDE.md',
|
|
526
|
+
personaContent: selection.claudeMdContent,
|
|
527
|
+
mode: selection.claudeMdMode ?? 'overwrite'
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (selection.claudeMd) {
|
|
532
|
+
try {
|
|
533
|
+
const content = readFileSync(selection.claudeMd, 'utf8');
|
|
534
|
+
return {
|
|
535
|
+
sidecar: {
|
|
536
|
+
mountFile: 'CLAUDE.md',
|
|
537
|
+
personaContent: content,
|
|
538
|
+
mode: selection.claudeMdMode ?? 'overwrite'
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
return { warning: `claudeMd: could not read ${selection.claudeMd}: ${err.message}` };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return {};
|
|
547
|
+
}
|
|
548
|
+
if (selection.agentsMdContent) {
|
|
549
|
+
return {
|
|
550
|
+
sidecar: {
|
|
551
|
+
mountFile: 'AGENTS.md',
|
|
552
|
+
personaContent: selection.agentsMdContent,
|
|
553
|
+
mode: selection.agentsMdMode ?? 'overwrite'
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (selection.agentsMd) {
|
|
558
|
+
try {
|
|
559
|
+
const content = readFileSync(selection.agentsMd, 'utf8');
|
|
560
|
+
return {
|
|
561
|
+
sidecar: {
|
|
562
|
+
mountFile: 'AGENTS.md',
|
|
563
|
+
personaContent: content,
|
|
564
|
+
mode: selection.agentsMdMode ?? 'overwrite'
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
return { warning: `agentsMd: could not read ${selection.agentsMd}: ${err.message}` };
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return {};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Compute the bytes to write into the mount for a sidecar. In `extend`
|
|
576
|
+
* mode, prepends the user's real-cwd file (if any) joined to the persona
|
|
577
|
+
* content with `\n\n---\n\n`. Pure — exposed for unit tests.
|
|
578
|
+
*/
|
|
579
|
+
export function buildSidecarBody(sidecar, realCwdDir) {
|
|
580
|
+
if (sidecar.mode === 'extend') {
|
|
581
|
+
const realPath = join(realCwdDir, sidecar.mountFile);
|
|
582
|
+
try {
|
|
583
|
+
const realContent = readFileSync(realPath, 'utf8');
|
|
584
|
+
return `${realContent}\n\n---\n\n${sidecar.personaContent}`;
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
// Only "missing path" errors degrade to overwrite. Real I/O
|
|
588
|
+
// problems (EACCES, EISDIR, …) propagate so callers see them
|
|
589
|
+
// instead of silently dropping the user's CLAUDE.md/AGENTS.md.
|
|
590
|
+
const code = err.code;
|
|
591
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
592
|
+
return sidecar.personaContent;
|
|
593
|
+
}
|
|
594
|
+
throw err;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return sidecar.personaContent;
|
|
598
|
+
}
|
|
468
599
|
/**
|
|
469
600
|
* Decide whether to run the interactive session inside a
|
|
470
601
|
* `@relayfile/local-mount` sandbox.
|
|
@@ -500,6 +631,21 @@ async function runInteractive(selection, options = {}) {
|
|
|
500
631
|
// below). The --install-in-repo flag forces legacy in-repo installs
|
|
501
632
|
// across the board.
|
|
502
633
|
const useClean = decideCleanMode(runtime.harness, options.installInRepo === true).useClean;
|
|
634
|
+
// Per-persona CLAUDE.md / AGENTS.md: load the author content if any. The
|
|
635
|
+
// file is materialized into the mount inside onBeforeLaunch (claude/
|
|
636
|
+
// opencode default). Without a mount (codex, --install-in-repo) we
|
|
637
|
+
// skip-and-warn — writing into the real cwd would pollute the user's
|
|
638
|
+
// repo and is explicitly out of scope.
|
|
639
|
+
const sidecarLookup = loadSidecarForSelection(effectiveSelection);
|
|
640
|
+
if (sidecarLookup.warning) {
|
|
641
|
+
process.stderr.write(`warning: ${sidecarLookup.warning}\n`);
|
|
642
|
+
}
|
|
643
|
+
const resolvedSidecar = useClean ? sidecarLookup.sidecar : undefined;
|
|
644
|
+
if (sidecarLookup.sidecar && !useClean) {
|
|
645
|
+
process.stderr.write(`warning: persona declares ${sidecarLookup.sidecar.mountFile} but no sandbox mount is available (` +
|
|
646
|
+
`${runtime.harness === 'codex' ? 'codex harness has no mount' : '--install-in-repo disengages the mount'})` +
|
|
647
|
+
`; skipping sidecar materialization to avoid writing into your repo.\n`);
|
|
648
|
+
}
|
|
503
649
|
// A session dir is needed whenever we either (a) stage skills out-of-repo
|
|
504
650
|
// via claude's installRoot, or (b) open a mount. Both engage for claude/
|
|
505
651
|
// opencode by default; --install-in-repo disengages both.
|
|
@@ -608,9 +754,6 @@ async function runInteractive(selection, options = {}) {
|
|
|
608
754
|
// copied in from the real repo nor synced back on exit.
|
|
609
755
|
if (useClean && sessionRoot) {
|
|
610
756
|
const mountDir = sessionMountDir(sessionRoot);
|
|
611
|
-
const ignoredPatterns = runtime.harness === 'claude'
|
|
612
|
-
? [...CLEAN_IGNORED_PATTERNS]
|
|
613
|
-
: [...SKILL_INSTALL_IGNORED_PATTERNS];
|
|
614
757
|
// Anything we materialize into the mount via onBeforeLaunch must be
|
|
615
758
|
// hidden from the mount-mirror in both directions: without this, any
|
|
616
759
|
// opencode.json already present in the real repo would be copied into
|
|
@@ -618,9 +761,13 @@ async function runInteractive(selection, options = {}) {
|
|
|
618
761
|
// fresh write from onBeforeLaunch would sync back out on exit and
|
|
619
762
|
// pollute the user's working tree. Added dynamically so this stays
|
|
620
763
|
// generic for any future configFile producer.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
764
|
+
const { ignoredPatterns, readonlyPatterns } = buildRelayfileMountPatterns({
|
|
765
|
+
projectDir: process.cwd(),
|
|
766
|
+
personaId,
|
|
767
|
+
harness: runtime.harness,
|
|
768
|
+
mount: effectiveSelection.mount,
|
|
769
|
+
configFilePaths: spec.configFiles.map((file) => file.path)
|
|
770
|
+
});
|
|
624
771
|
process.stderr.write(`• sandbox mount → ${mountDir}\n`);
|
|
625
772
|
// Three-stage SIGINT handler layered on top of launchOnMount's own signal
|
|
626
773
|
// forwarding. launchOnMount catches the first SIGINT to kill the child
|
|
@@ -696,6 +843,7 @@ async function runInteractive(selection, options = {}) {
|
|
|
696
843
|
// `git push` to persist work — local-only commits evaporate with
|
|
697
844
|
// the session.
|
|
698
845
|
includeGit: true,
|
|
846
|
+
readonlyPatterns,
|
|
699
847
|
// Second Ctrl-C aborts this signal → local-mount skips autosync's
|
|
700
848
|
// draining reconcile and returns the partial syncBack count. Cleanup
|
|
701
849
|
// still runs, so there's no leaked mount dir.
|
|
@@ -745,6 +893,10 @@ async function runInteractive(selection, options = {}) {
|
|
|
745
893
|
mkdirSync(dirname(target), { recursive: true });
|
|
746
894
|
writeFileSync(target, file.contents, 'utf8');
|
|
747
895
|
}
|
|
896
|
+
if (resolvedSidecar) {
|
|
897
|
+
const body = buildSidecarBody(resolvedSidecar, process.cwd());
|
|
898
|
+
writeFileSync(join(dir, resolvedSidecar.mountFile), body, 'utf8');
|
|
899
|
+
}
|
|
748
900
|
}
|
|
749
901
|
});
|
|
750
902
|
return result.exitCode;
|
|
@@ -1454,6 +1606,20 @@ function formatPersonaShow(spec, source, tiers, tierNote) {
|
|
|
1454
1606
|
lines.push(` deny: ${perms.deny.join(', ')}`);
|
|
1455
1607
|
}
|
|
1456
1608
|
lines.push('');
|
|
1609
|
+
lines.push('MOUNT');
|
|
1610
|
+
const mount = spec.mount;
|
|
1611
|
+
if (!mount || (!mount.ignoredPatterns?.length && !mount.readonlyPatterns?.length)) {
|
|
1612
|
+
lines.push(' (none)');
|
|
1613
|
+
}
|
|
1614
|
+
else {
|
|
1615
|
+
if (mount.ignoredPatterns?.length) {
|
|
1616
|
+
lines.push(` ignored: ${mount.ignoredPatterns.join(', ')}`);
|
|
1617
|
+
}
|
|
1618
|
+
if (mount.readonlyPatterns?.length) {
|
|
1619
|
+
lines.push(` readonly: ${mount.readonlyPatterns.join(', ')}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
lines.push('');
|
|
1457
1623
|
lines.push('ENV');
|
|
1458
1624
|
const envKeys = Object.keys(spec.env ?? {});
|
|
1459
1625
|
if (envKeys.length === 0) {
|