portable-agent-layer 0.115.0 → 0.115.1-canary.1

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.
@@ -136,11 +136,13 @@ The repo's `CLAUDE.md` is read from your checkout when the task starts, so a rol
136
136
  - `"repoInstructions": false` in `.dark-factory.json` leaves out the repo's `CLAUDE.md`.
137
137
  - `"userInstructions": false` in `$DARK_FACTORY_HOME/config.json` (`~/.dark-factory/config.json`, optional; a missing file means both parts are on) leaves out your `~/.claude/CLAUDE.md`.
138
138
 
139
- Hooks never run in a role, and neither do your settings or MCP servers.
139
+ Your own hooks never run in a role, and neither do MCP servers. The repo's hooks do run, so a repo `Stop` hook that fails its checks keeps the role working until they pass; the gate stays as the check for repos without hooks.
140
+
141
+ A role loads the repo's `.claude/settings.json` and `.claude/settings.local.json` from its worktree as Claude Code does, permissions included. Your `~/.claude/settings.json` (or `CLAUDE_CONFIG_DIR`) is passed in `--settings` without `hooks`, `permissions`, `enabledPlugins` and `extraKnownMarketplaces`, so commit attribution and your other preferences apply. Each role call records which of your keys it passed and dropped as a `role_settings` event, without their values.
140
142
 
141
143
  ## Permissions
142
144
 
143
- - Roles run isolated: `--setting-sources ""`, the plugin from `--plugin-dir`, and their own tool policy from `roles.mjs`, so user hooks and settings don't reach them; only the CLAUDE.md instructions above do.
145
+ - Roles run with `--setting-sources project,local`, the plugin from `--plugin-dir`, and their own tool policy from `roles.mjs`. Your user settings come only through the filtered `--settings` file, so your hooks don't reach them. A repo's `permissions.allow` can widen a role's tools; the role's denies still win.
144
146
  - The PO session only needs `"Bash(node *dark-factory*/scripts/*)"`, for example in `.claude/settings.json`:
145
147
 
146
148
  ```json
@@ -13,6 +13,7 @@ import {
13
13
  userHome,
14
14
  } from "./lib.mjs";
15
15
  import { policyFor, schemaFor } from "./roles.mjs";
16
+ import { writeRoleSettings } from "./settings.mjs";
16
17
 
17
18
  export const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
18
19
  export const ONE_COMMAND_PER_CALL =
@@ -298,6 +299,7 @@ export function claudeInvocation(
298
299
  resumeSessionId,
299
300
  pluginDir = PLUGIN_ROOT,
300
301
  instructionsFile,
302
+ settingsFile,
301
303
  } = {}
302
304
  ) {
303
305
  const policy = policyFor(role);
@@ -312,7 +314,8 @@ export function claudeInvocation(
312
314
  "--output-format",
313
315
  "json",
314
316
  "--setting-sources",
315
- "",
317
+ "project,local",
318
+ ...optionalFlag("--settings", settingsFile),
316
319
  "--plugin-dir",
317
320
  pluginDir,
318
321
  ...optionalFlag("--append-system-prompt-file", instructionsFile),
@@ -353,6 +356,7 @@ function attempt(
353
356
  try {
354
357
  const task = readTask(taskId, env);
355
358
  const instructionsFile = writeRoleInstructions(task, env);
359
+ const settingsFile = writeRoleSettings(task, env);
356
360
  const invocation = claudeInvocation(task, role, {
357
361
  budgetUsd,
358
362
  extraArgs,
@@ -361,6 +365,7 @@ function attempt(
361
365
  instructions,
362
366
  resumeSessionId,
363
367
  instructionsFile,
368
+ settingsFile,
364
369
  });
365
370
  const outcome = outcomeFrom(
366
371
  taskId,
@@ -0,0 +1,62 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { appendEvent, taskDir, userHome } from "./lib.mjs";
4
+
5
+ const SETTINGS_FILE = "role-settings.json";
6
+ const KEYS_THAT_BREAK_ISOLATION = [
7
+ "hooks",
8
+ "permissions",
9
+ "enabledPlugins",
10
+ "extraKnownMarketplaces",
11
+ ];
12
+
13
+ function isPlainObject(value) {
14
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+
17
+ function readJsonObject(file) {
18
+ try {
19
+ const value = JSON.parse(readFileSync(file, "utf8"));
20
+ return isPlainObject(value) ? value : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function userSettings(env) {
27
+ const configDir = env.CLAUDE_CONFIG_DIR || join(userHome(env), ".claude");
28
+ return readJsonObject(join(configDir, "settings.json")) ?? {};
29
+ }
30
+
31
+ export function roleSettingsFrom(userSettings) {
32
+ const kept = Object.fromEntries(
33
+ Object.entries(userSettings).filter(
34
+ ([key]) => !KEYS_THAT_BREAK_ISOLATION.includes(key)
35
+ )
36
+ );
37
+ return Object.keys(kept).length > 0 ? kept : null;
38
+ }
39
+
40
+ function recordRoleSettings(taskId, user, kept, env) {
41
+ appendEvent(
42
+ {
43
+ event: "role_settings",
44
+ task: taskId,
45
+ passed: Object.keys(kept ?? {}),
46
+ dropped: Object.keys(user).filter((key) => KEYS_THAT_BREAK_ISOLATION.includes(key)),
47
+ },
48
+ env
49
+ );
50
+ }
51
+
52
+ export function writeRoleSettings(task, env = process.env) {
53
+ const user = userSettings(env);
54
+ const kept = roleSettingsFrom(user);
55
+ recordRoleSettings(task.id, user, kept, env);
56
+ if (kept === null) return null;
57
+ const dir = taskDir(task.id, env);
58
+ mkdirSync(dir, { recursive: true });
59
+ const file = join(dir, SETTINGS_FILE);
60
+ writeFileSync(file, `${JSON.stringify(kept, null, 2)}\n`, { mode: 0o600 });
61
+ return file;
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.115.0",
3
+ "version": "0.115.1-canary.1",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {